org-export: Allow nested footnotes (part 2)
* EXPERIMENTAL/org-e-latex.el (org-e-latex-footnote-reference): Correctly handle numbering with nested footnotes. * contrib/lisp/org-element.el (org-element-map): Apply function to element or object before applying it to its secondary string, if any. Otherwise, linearity is broken. * contrib/lisp/org-export.el (org-export-footnote-first-reference-p, org-export-get-footnote-number): Take care of recursive footnotes. (org-export-get-genealogy): Correctly get genealogy of an item within a secondary string. * testing/contrib/lisp/test-org-export.el: Add tests.
This commit is contained in:
parent
73f2ff2acc
commit
12c94310a2
|
@ -1023,8 +1023,8 @@ CONTENTS is nil. INFO is a plist holding contextual information."
|
|||
((loop for parent in (org-export-get-genealogy footnote-reference info)
|
||||
thereis (memq (org-element-type parent)
|
||||
'(footnote-reference footnote-definition)))
|
||||
(format "\\footnotemark[%s]{}"
|
||||
(org-export-get-footnote-number footnote-reference info)))
|
||||
(let ((num (org-export-get-footnote-number footnote-reference info)))
|
||||
(format "\\footnotemark[%s]{}\\setcounter{footnote}{%s}" num num)))
|
||||
;; Otherwise, define it with \footnote command.
|
||||
(t
|
||||
(let ((def (org-export-get-footnote-definition footnote-reference info)))
|
||||
|
@ -1032,15 +1032,24 @@ CONTENTS is nil. INFO is a plist holding contextual information."
|
|||
(setq def (cons 'org-data (cons nil def))))
|
||||
(concat
|
||||
(format "\\footnote{%s}" (org-trim (org-export-data def 'e-latex info)))
|
||||
;; Retrieve all footnote references within the footnote to add
|
||||
;; their definition after it, since LaTeX doesn't support them
|
||||
;; inside.
|
||||
(let ((all-refs
|
||||
(org-element-map
|
||||
def 'footnote-reference
|
||||
(lambda (ref)
|
||||
(when (org-export-footnote-first-reference-p ref info) ref))
|
||||
info)))
|
||||
;; Retrieve all footnote references within the footnote and
|
||||
;; add their definition after it, since LaTeX doesn't support
|
||||
;; them inside.
|
||||
(let (all-refs
|
||||
(search-refs
|
||||
(function
|
||||
(lambda (data)
|
||||
;; Return a list of all footnote references in DATA.
|
||||
(org-element-map
|
||||
data 'footnote-reference
|
||||
(lambda (ref)
|
||||
(when (org-export-footnote-first-reference-p ref info)
|
||||
(push ref all-refs)
|
||||
(when (eq (org-element-property :type ref) 'standard)
|
||||
(funcall
|
||||
search-refs
|
||||
(org-export-get-footnote-definition ref info)))))
|
||||
info) (reverse all-refs)))))
|
||||
(mapconcat
|
||||
(lambda (ref)
|
||||
(format
|
||||
|
@ -1048,11 +1057,11 @@ CONTENTS is nil. INFO is a plist holding contextual information."
|
|||
(org-export-get-footnote-number ref info)
|
||||
(org-trim
|
||||
(funcall
|
||||
(if (org-element-property :inline-definition ref)
|
||||
(if (eq (org-element-property :type ref) 'inline)
|
||||
'org-export-secondary-string
|
||||
'org-export-data)
|
||||
(org-export-get-footnote-definition ref info) 'e-latex info))))
|
||||
all-refs ""))))))))
|
||||
(funcall search-refs def) ""))))))))
|
||||
|
||||
|
||||
;;;; Headline
|
||||
|
|
|
@ -2989,12 +2989,17 @@ Nil values returned from FUN are ignored in the result."
|
|||
--acc
|
||||
(--check-blob
|
||||
(function
|
||||
(lambda (--type types fun --blob info)
|
||||
(lambda (--type types fun --blob)
|
||||
;; Check if TYPE is matching among TYPES. If so, apply
|
||||
;; FUN to --BLOB and accumulate return value into --ACC.
|
||||
;; INFO is the communication channel. If --BLOB has
|
||||
;; a secondary string that can contain objects with their
|
||||
;; type amond TYPES, look into that string first.
|
||||
(when (memq --type types)
|
||||
(let ((result (funcall fun --blob)))
|
||||
(cond ((not result))
|
||||
(first-match (throw 'first-match result))
|
||||
(t (push result --acc)))))
|
||||
(when (memq --type --restricts)
|
||||
(funcall
|
||||
--walk-tree
|
||||
|
@ -3002,12 +3007,7 @@ Nil values returned from FUN are ignored in the result."
|
|||
nil
|
||||
,@(org-element-property
|
||||
(cdr (assq --type org-element-secondary-value-alist))
|
||||
--blob))))
|
||||
(when (memq --type types)
|
||||
(let ((result (funcall fun --blob)))
|
||||
(cond ((not result))
|
||||
(first-match (throw 'first-match result))
|
||||
(t (push result --acc))))))))
|
||||
--blob)))))))
|
||||
(--walk-tree
|
||||
(function
|
||||
(lambda (--data)
|
||||
|
@ -3025,7 +3025,7 @@ Nil values returned from FUN are ignored in the result."
|
|||
;; isn't one.
|
||||
((and (eq --category 'greater-elements)
|
||||
(not (memq --type org-element-greater-elements)))
|
||||
(funcall --check-blob --type types fun --blob info))
|
||||
(funcall --check-blob --type types fun --blob))
|
||||
;; Limiting recursion to elements, and --BLOB only
|
||||
;; contains objects.
|
||||
((and (eq --category 'elements) (eq --type 'paragraph)))
|
||||
|
@ -3035,10 +3035,10 @@ Nil values returned from FUN are ignored in the result."
|
|||
(not (or (eq --type 'paragraph)
|
||||
(memq --type org-element-greater-elements)
|
||||
(memq --type org-element-recursive-objects))))
|
||||
(funcall --check-blob --type types fun --blob info))
|
||||
(funcall --check-blob --type types fun --blob))
|
||||
;; Recursion is possible and allowed: Maybe apply
|
||||
;; FUN to --BLOB, then move into it.
|
||||
(t (funcall --check-blob --type types fun --blob info)
|
||||
(t (funcall --check-blob --type types fun --blob)
|
||||
(funcall --walk-tree --blob)))))
|
||||
(org-element-contents --data))))))
|
||||
(catch 'first-match
|
||||
|
|
|
@ -2474,15 +2474,29 @@ ignored."
|
|||
FOOTNOTE-REFERENCE is the footnote reference being considered.
|
||||
INFO is the plist used as a communication channel."
|
||||
(let ((label (org-element-property :label footnote-reference)))
|
||||
(or (not label)
|
||||
(equal
|
||||
footnote-reference
|
||||
(org-element-map
|
||||
(plist-get info :parse-tree) 'footnote-reference
|
||||
(lambda (footnote)
|
||||
(when (string= (org-element-property :label footnote) label)
|
||||
footnote))
|
||||
info 'first-match)))))
|
||||
;; Anonymous footnotes are always a first reference.
|
||||
(if (not label) t
|
||||
;; Otherwise, return the first footnote with the same LABEL and
|
||||
;; test if it is equal to FOOTNOTE-REFERENCE.
|
||||
(let ((search-refs
|
||||
(function
|
||||
(lambda (data)
|
||||
(org-element-map
|
||||
data 'footnote-reference
|
||||
(lambda (fn)
|
||||
(cond
|
||||
((string= (org-element-property :label fn) label)
|
||||
(throw 'exit fn))
|
||||
;; If FN isn't inlined, be sure to traverse its
|
||||
;; definition before resuming search. See
|
||||
;; comments in `org-export-get-footnote-number'
|
||||
;; for more information.
|
||||
((eq (org-element-property :type fn) 'standard)
|
||||
(funcall search-refs
|
||||
(org-export-get-footnote-definition fn info)))))
|
||||
info 'first-match)))))
|
||||
(equal (catch 'exit (funcall search-refs (plist-get info :parse-tree)))
|
||||
footnote-reference)))))
|
||||
|
||||
(defun org-export-get-footnote-definition (footnote-reference info)
|
||||
"Return definition of FOOTNOTE-REFERENCE as parsed data.
|
||||
|
@ -2496,22 +2510,45 @@ INFO is the plist used as a communication channel."
|
|||
|
||||
FOOTNOTE is either a footnote reference or a footnote definition.
|
||||
INFO is the plist used as a communication channel."
|
||||
(let ((label (org-element-property :label footnote)) seen-refs)
|
||||
(org-element-map
|
||||
(plist-get info :parse-tree) 'footnote-reference
|
||||
(lambda (fn)
|
||||
(let ((fn-lbl (org-element-property :label fn)))
|
||||
(cond
|
||||
((and (not fn-lbl) (equal fn footnote)) (1+ (length seen-refs)))
|
||||
((and label (string= label fn-lbl)) (1+ (length seen-refs)))
|
||||
;; Anonymous footnote: it's always a new one. Also, be sure
|
||||
;; to return nil from the `cond' so `first-match' doesn't
|
||||
;; get us out of the loop.
|
||||
((not fn-lbl) (push 'inline seen-refs) nil)
|
||||
;; Label not seen so far: add it so SEEN-REFS. Again,
|
||||
;; return nil to stay in the loop.
|
||||
((not (member fn-lbl seen-refs)) (push fn-lbl seen-refs) nil))))
|
||||
info 'first-match)))
|
||||
(let ((label (org-element-property :label footnote))
|
||||
seen-refs
|
||||
(search-ref
|
||||
(function
|
||||
(lambda (data)
|
||||
;; Search footnote references through DATA, filling
|
||||
;; SEEN-REFS along the way.
|
||||
(org-element-map
|
||||
data 'footnote-reference
|
||||
(lambda (fn)
|
||||
(let ((fn-lbl (org-element-property :label fn)))
|
||||
(cond
|
||||
;; Anonymous footnote match: return number.
|
||||
((and (not fn-lbl) (equal fn footnote))
|
||||
(throw 'exit (1+ (length seen-refs))))
|
||||
;; Labels match: return number.
|
||||
((and label (string= label fn-lbl))
|
||||
(throw 'exit (1+ (length seen-refs))))
|
||||
;; Anonymous footnote: it's always a new one. Also,
|
||||
;; be sure to return nil from the `cond' so
|
||||
;; `first-match' doesn't get us out of the loop.
|
||||
((not fn-lbl) (push 'inline seen-refs) nil)
|
||||
;; Label not seen so far: add it so SEEN-REFS.
|
||||
;;
|
||||
;; Also search for subsequent references in footnote
|
||||
;; definition so numbering following reading logic.
|
||||
;; Note that we don't have to care about inline
|
||||
;; definitions, since `org-element-map' already
|
||||
;; traverse them at the right time.
|
||||
;;
|
||||
;; Once again, return nil to stay in the loop.
|
||||
((not (member fn-lbl seen-refs))
|
||||
(push fn-lbl seen-refs)
|
||||
(when (eq (org-element-type fn) 'standard)
|
||||
(funcall search-ref
|
||||
(org-export-get-footnote-definition fn info)))
|
||||
nil))))
|
||||
info 'first-match)))))
|
||||
(catch 'exit (funcall search-ref (plist-get info :parse-tree)))))
|
||||
|
||||
|
||||
;;;; For Headlines
|
||||
|
@ -3178,15 +3215,30 @@ affiliated keyword."
|
|||
"Return genealogy relative to a given element or object.
|
||||
BLOB is the element or object being considered. INFO is a plist
|
||||
used as a communication channel."
|
||||
(let* ((end (org-element-property :end blob))
|
||||
(let* ((type (org-element-type blob))
|
||||
(end (org-element-property :end blob))
|
||||
(walk-data
|
||||
(lambda (data genealogy)
|
||||
;; Walk DATA, looking for BLOB. GENEALOGY is the list of
|
||||
;; parents of all elements in DATA.
|
||||
(mapc
|
||||
(lambda (el)
|
||||
(cond
|
||||
((stringp el))
|
||||
((stringp el) nil)
|
||||
((equal el blob) (throw 'exit genealogy))
|
||||
((>= (org-element-property :end el) end)
|
||||
;; If BLOB is an object and EL contains a secondary
|
||||
;; string, be sure to check it.
|
||||
(when (memq type org-element-all-objects)
|
||||
(let ((sec-prop
|
||||
(cdr (assq (org-element-type el)
|
||||
org-element-secondary-value-alist))))
|
||||
(when sec-prop
|
||||
(funcall
|
||||
walk-data
|
||||
(cons 'org-data
|
||||
(cons nil (org-element-property sec-prop el)))
|
||||
(cons el genealogy)))))
|
||||
(funcall walk-data el (cons el genealogy)))))
|
||||
(org-element-contents data)))))
|
||||
(catch 'exit (funcall walk-data (plist-get info :parse-tree) nil) nil)))
|
||||
|
|
|
@ -318,27 +318,51 @@ body\n")))
|
|||
|
||||
(ert-deftest test-org-export/footnotes ()
|
||||
"Test footnotes specifications."
|
||||
;; 1. Test nested footnotes.
|
||||
(let ((org-footnote-section nil))
|
||||
;; 1. Read every type of footnote.
|
||||
(org-test-with-temp-text "
|
||||
Some text[fn:1] and some other text[fn:new:and an inline
|
||||
footnote with another one[fn:label:reference to[fn:1] and a new
|
||||
one[fn:label2:label2]].
|
||||
Text[fn:1] [1] [fn:label:C] [fn::D]
|
||||
|
||||
[fn:1] with a footnote inside[fn:inside] and a new footnote [fn:label3:label3].
|
||||
[fn:1] A
|
||||
|
||||
[fn:inside] like that."
|
||||
[1] B"
|
||||
(let* ((tree (org-element-parse-buffer))
|
||||
(info (org-combine-plists
|
||||
(org-export-initial-options) '(:with-footnotes t))))
|
||||
(setq info (org-combine-plists
|
||||
info (org-export-collect-tree-properties tree info 'test)))
|
||||
(let* ((fn-numbers
|
||||
(org-element-map
|
||||
tree 'footnote-reference
|
||||
(lambda (ref)
|
||||
(or (org-export-get-footnote-number ref info) 'unknown)) info)))
|
||||
;; 1.1. Every nested footnote has a number.
|
||||
(should (every 'numberp fn-numbers))
|
||||
;; 1.2. Can tell which are new and which aren't.
|
||||
(should (= (apply 'max fn-numbers) 5)))))))
|
||||
(should
|
||||
(equal
|
||||
'((1 . "A") (2 . "B") (3 . "C") (4 . "D"))
|
||||
(org-element-map
|
||||
tree 'footnote-reference
|
||||
(lambda (ref)
|
||||
(cons (org-export-get-footnote-number ref info)
|
||||
(if (eq (org-element-property :type ref) 'inline)
|
||||
(car (org-export-get-footnote-definition ref info))
|
||||
(car (org-element-contents
|
||||
(car (org-element-contents
|
||||
(org-export-get-footnote-definition ref info))))))))
|
||||
info)))))
|
||||
;; 2. Test nested footnotes.
|
||||
(org-test-with-temp-text "
|
||||
Text[fn:1:A[fn:2]] [fn:3].
|
||||
|
||||
[fn:2] B [fn:3] [fn::D].
|
||||
|
||||
[fn:3] C."
|
||||
(let* ((tree (org-element-parse-buffer))
|
||||
(info (org-combine-plists
|
||||
(org-export-initial-options) '(:with-footnotes t))))
|
||||
(setq info (org-combine-plists
|
||||
info (org-export-collect-tree-properties tree info 'test)))
|
||||
(should
|
||||
(equal
|
||||
'((1 . "fn:1") (2 . "fn:2") (3 . "fn:3") (4))
|
||||
(org-element-map
|
||||
tree 'footnote-reference
|
||||
(lambda (ref)
|
||||
(when (org-export-footnote-first-reference-p ref info)
|
||||
(cons (org-export-get-footnote-number ref info)
|
||||
(org-element-property :label ref))))
|
||||
info)))))))
|
||||
|
|
Loading…
Reference in New Issue