forked from mirrors/org-mode
ox: Add optional order argument to some footnotes related functions
* lisp/ox.el (org-export--footnote-reference-map): New function. (org-export-footnote-first-reference-p, org-export-get-footnote-number): Allow to change order when footnotes references are contained within footnote definitions. * testing/lisp/test-ox.el (test-org-export/footnote-first-reference-p): (test-org-export/get-footnote-number): New tests. (test-org-export/footnotes): Update test.
This commit is contained in:
parent
b95f7aa823
commit
ccb663c742
166
lisp/ox.el
166
lisp/ox.el
|
@ -3571,38 +3571,6 @@ footnotes. Unreferenced definitions are ignored."
|
|||
(funcall collect-fn (plist-get info :parse-tree))
|
||||
(reverse num-alist)))
|
||||
|
||||
(defun org-export-footnote-first-reference-p (footnote-reference info)
|
||||
"Non-nil when a footnote reference is the first one for its label.
|
||||
|
||||
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)))
|
||||
;; 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 ; for byte-compiler.
|
||||
(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)))))
|
||||
;; Don't enter footnote definitions since it will
|
||||
;; happen when their first reference is found.
|
||||
info 'first-match 'footnote-definition)))))
|
||||
(eq (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.
|
||||
INFO is the plist used as a communication channel. If no such
|
||||
|
@ -3613,52 +3581,100 @@ definition can be found, raise an error."
|
|||
(org-element-contents footnote-reference))
|
||||
(error "Definition not found for footnote %s" label))))
|
||||
|
||||
(defun org-export-get-footnote-number (footnote info)
|
||||
(defun org-export--footnote-reference-map (function info &optional body-first)
|
||||
"Apply FUNCTION on every footnote reference in parse tree.
|
||||
INFO is a plist containing export state. By default, as soon as
|
||||
a new footnote reference is encountered, FUNCTION is called onto
|
||||
its definition. However, if BODY-FIRST is non-nil, this step is
|
||||
delayed until the end of the process."
|
||||
(let* ((definitions)
|
||||
(seen-refs)
|
||||
(search-ref) ; For byte-compiler.
|
||||
(search-ref
|
||||
(lambda (data delayp)
|
||||
;; Search footnote references through DATA, filling
|
||||
;; SEEN-REFS along the way. When DELAYP is non-nil, store
|
||||
;; footnote definitions so they can be entered later.
|
||||
(org-element-map data 'footnote-reference
|
||||
(lambda (f)
|
||||
(funcall function f)
|
||||
(let ((--label (org-element-property :label f)))
|
||||
(unless (and --label (member --label seen-refs))
|
||||
(when --label (push --label seen-refs))
|
||||
;; Search for subsequent references in footnote
|
||||
;; definition so numbering follows reading logic,
|
||||
;; unless DELAYP in non-nil.
|
||||
(cond
|
||||
(delayp
|
||||
(push (org-export-get-footnote-definition f info)
|
||||
definitions))
|
||||
;; Do not force entering inline definitions,
|
||||
;; since `org-element-map' already traverses them
|
||||
;; at the right time.
|
||||
((eq (org-element-property :type f) 'inline))
|
||||
(t (funcall search-ref
|
||||
(org-export-get-footnote-definition f info)
|
||||
nil))))))
|
||||
info nil
|
||||
;; Don't enter footnote definitions since it will happen
|
||||
;; when their first reference is found. Moreover, if
|
||||
;; DELAYP is non-nil, make sure we postpone entering
|
||||
;; definitions of inline references.
|
||||
(if delayp '(footnote-definition footnote-reference)
|
||||
'footnote-definition)))))
|
||||
(funcall search-ref (plist-get info :parse-tree) body-first)
|
||||
(funcall search-ref (nreverse definitions) nil)))
|
||||
|
||||
(defun org-export-footnote-first-reference-p
|
||||
(footnote-reference info &optional body-first)
|
||||
"Non-nil when a footnote reference is the first one for its label.
|
||||
|
||||
FOOTNOTE-REFERENCE is the footnote reference being considered.
|
||||
INFO is a plist containing current export state.
|
||||
|
||||
By default, as soon as a new footnote reference is encountered,
|
||||
other references are searched within its definition. However, if
|
||||
BODY-FIRST is non-nil, this step is delayed after the whole tree
|
||||
is checked. This alters results when references are found in
|
||||
footnote definitions."
|
||||
(let ((label (org-element-property :label footnote-reference)))
|
||||
;; Anonymous footnotes are always a first reference.
|
||||
(or (not label)
|
||||
(catch 'exit
|
||||
(org-export--footnote-reference-map
|
||||
(lambda (f)
|
||||
(let ((l (org-element-property :label f)))
|
||||
(when (and l label (string= label l))
|
||||
(throw 'exit (eq footnote-reference f)))))
|
||||
info body-first)))))
|
||||
|
||||
(defun org-export-get-footnote-number (footnote info &optional body-first)
|
||||
"Return number associated to a footnote.
|
||||
|
||||
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
|
||||
search-ref ; For byte-compiler.
|
||||
(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) (eq 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 follows
|
||||
;; reading logic. Note that we don't have to care
|
||||
;; about inline definitions, since
|
||||
;; `org-element-map' already traverses 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)
|
||||
(funcall search-ref
|
||||
(org-export-get-footnote-definition fn info))
|
||||
nil))))
|
||||
;; Don't enter footnote definitions since it will
|
||||
;; happen when their first reference is found.
|
||||
info 'first-match 'footnote-definition)))))
|
||||
(catch 'exit (funcall search-ref (plist-get info :parse-tree)))))
|
||||
INFO is the plist containing export state.
|
||||
|
||||
By default, as soon as a new footnote reference is encountered,
|
||||
counting process moves into its definition. However, if
|
||||
BODY-FIRST is non-nil, this step is delayed until the end of the
|
||||
process, leading to a different order when footnotes are nested."
|
||||
(let ((count 0)
|
||||
(seen)
|
||||
(label (org-element-property :label footnote)))
|
||||
(catch 'exit
|
||||
(org-export--footnote-reference-map
|
||||
(lambda (f)
|
||||
(let ((l (org-element-property :label f)))
|
||||
(cond
|
||||
;; Anonymous footnote match: return number.
|
||||
((and (not l) (not label) (eq footnote f)) (throw 'exit (1+ count)))
|
||||
;; Labels match: return number.
|
||||
((and label l (string= label l)) (throw 'exit (1+ count)))
|
||||
;; Otherwise store label and increase counter if label
|
||||
;; wasn't encountered yet.
|
||||
((not l) (incf count))
|
||||
((not (member l seen)) (push l seen) (incf count)))))
|
||||
info body-first))))
|
||||
|
||||
|
||||
;;;; For Headlines
|
||||
|
|
|
@ -1538,11 +1538,127 @@ Footnotes[fn:2], foot[fn:test], digit only[3], and [fn:inline:anonymous footnote
|
|||
|
||||
;;; Footnotes
|
||||
|
||||
(ert-deftest test-org-export/footnote-first-reference-p ()
|
||||
"Test `org-export-footnote-first-reference-p' specifications."
|
||||
(should
|
||||
(equal
|
||||
'(t nil)
|
||||
(org-test-with-temp-text "Text[fn:1][fn:1]\n\n[fn:1] Definition"
|
||||
(let (result)
|
||||
(org-export-as
|
||||
(org-export-create-backend
|
||||
:transcoders
|
||||
`(,(cons 'footnote-reference
|
||||
(lambda (f c i)
|
||||
(push (org-export-footnote-first-reference-p f info)
|
||||
result)
|
||||
""))
|
||||
(section . (lambda (s c i) c))
|
||||
(paragraph . (lambda (p c i) c))))
|
||||
nil nil nil '(:with-footnotes t))
|
||||
(nreverse result)))))
|
||||
;; If optional argument BODY-FIRST is non-nil, first find footnote
|
||||
;; in the main body of the document. Otherwise, enter footnote
|
||||
;; definitions when they are encountered.
|
||||
(should
|
||||
(equal
|
||||
'(t nil)
|
||||
(org-test-with-temp-text
|
||||
":BODY:\nText[fn:1][fn:2]\n:END:\n\n[fn:1] Definition[fn:2]\n\n[fn:2] Inner"
|
||||
(let (result)
|
||||
(org-export-as
|
||||
(org-export-create-backend
|
||||
:transcoders
|
||||
`(,(cons 'footnote-reference
|
||||
(lambda (f c i)
|
||||
(when (org-element-lineage f '(drawer))
|
||||
(push (org-export-footnote-first-reference-p f info nil)
|
||||
result))
|
||||
""))
|
||||
(drawer . (lambda (d c i) c))
|
||||
(footnote-definition . (lambda (d c i) c))
|
||||
(section . (lambda (s c i) c))
|
||||
(paragraph . (lambda (p c i) c))))
|
||||
nil nil nil '(:with-footnotes t))
|
||||
(nreverse result)))))
|
||||
(should
|
||||
(equal
|
||||
'(t t)
|
||||
(org-test-with-temp-text
|
||||
":BODY:\nText[fn:1][fn:2]\n:END:\n\n[fn:1] Definition[fn:2]\n\n[fn:2] Inner"
|
||||
(let (result)
|
||||
(org-export-as
|
||||
(org-export-create-backend
|
||||
:transcoders
|
||||
`(,(cons 'footnote-reference
|
||||
(lambda (f c i)
|
||||
(when (org-element-lineage f '(drawer))
|
||||
(push (org-export-footnote-first-reference-p f info t)
|
||||
result))
|
||||
""))
|
||||
(drawer . (lambda (d c i) c))
|
||||
(footnote-definition . (lambda (d c i) c))
|
||||
(section . (lambda (s c i) c))
|
||||
(paragraph . (lambda (p c i) c))))
|
||||
nil nil nil '(:with-footnotes t))
|
||||
(nreverse result))))))
|
||||
|
||||
(ert-deftest test-org-export/get-footnote-number ()
|
||||
"Test `org-export-get-footnote-number' specifications."
|
||||
(should
|
||||
(equal '(1 2 1)
|
||||
(org-test-with-parsed-data
|
||||
"Text[fn:1][fn:2][fn:1]\n\n[fn:1] Def\n[fn:2] Def"
|
||||
(org-element-map tree 'footnote-reference
|
||||
(lambda (ref) (org-export-get-footnote-number ref info))
|
||||
info))))
|
||||
;; Anonymous footnotes all get a new number.
|
||||
(should
|
||||
(equal '(1 2)
|
||||
(org-test-with-parsed-data
|
||||
"Text[fn::anon1][fn::anon2]"
|
||||
(org-element-map tree 'footnote-reference
|
||||
(lambda (ref) (org-export-get-footnote-number ref info))
|
||||
info))))
|
||||
;; Test nested footnotes order.
|
||||
(should
|
||||
(equal
|
||||
'((1 . "fn:1") (2 . "fn:2") (3 . "fn:3") (3 . "fn:3") (4))
|
||||
(org-test-with-parsed-data
|
||||
"Text[fn:1:A[fn:2]] [fn:3].\n\n[fn:2] B [fn:3] [fn::D].\n\n[fn:3] C."
|
||||
(org-element-map tree 'footnote-reference
|
||||
(lambda (ref)
|
||||
(cons (org-export-get-footnote-number ref info)
|
||||
(org-element-property :label ref)))
|
||||
info))))
|
||||
;; With a non-nil optional argument, first check body, then footnote
|
||||
;; definitions.
|
||||
(should
|
||||
(equal
|
||||
'(("fn:1" . 1) ("fn:2" . 2) ("fn:3" . 3) ("fn:3" . 3))
|
||||
(org-test-with-parsed-data
|
||||
"Text[fn:1][fn:2][fn:3]\n\n[fn:1] Def[fn:3]\n[fn:2] Def\n[fn:3] Def"
|
||||
(org-element-map tree 'footnote-reference
|
||||
(lambda (ref)
|
||||
(cons (org-element-property :label ref)
|
||||
(org-export-get-footnote-number ref info t)))
|
||||
info))))
|
||||
(should
|
||||
(equal
|
||||
'(("fn:1" . 1) ("fn:2" . 3) ("fn:3" . 2) ("fn:3" . 2))
|
||||
(org-test-with-parsed-data
|
||||
"Text[fn:1][fn:2][fn:3]\n\n[fn:1] Def[fn:3]\n[fn:2] Def\n[fn:3] Def"
|
||||
(org-element-map tree 'footnote-reference
|
||||
(lambda (ref)
|
||||
(cons (org-element-property :label ref)
|
||||
(org-export-get-footnote-number ref info nil)))
|
||||
info)))))
|
||||
|
||||
(ert-deftest test-org-export/footnotes ()
|
||||
"Test footnotes specifications."
|
||||
"Miscellaneous tests on footnotes."
|
||||
(let ((org-footnote-section nil)
|
||||
(org-export-with-footnotes t))
|
||||
;; 1. Read every type of footnote.
|
||||
;; Read every type of footnote.
|
||||
(should
|
||||
(equal
|
||||
'((1 . "A\n") (2 . "B") (3 . "C") (4 . "D"))
|
||||
|
@ -1556,19 +1672,7 @@ Footnotes[fn:2], foot[fn:test], digit only[3], and [fn:inline:anonymous footnote
|
|||
(car (org-element-contents
|
||||
(car (org-element-contents def))))))))
|
||||
info))))
|
||||
;; 2. Test nested footnotes order.
|
||||
(should
|
||||
(equal
|
||||
'((1 . "fn:1") (2 . "fn:2") (3 . "fn:3") (4))
|
||||
(org-test-with-parsed-data
|
||||
"Text[fn:1:A[fn:2]] [fn:3].\n\n[fn:2] B [fn:3] [fn::D].\n\n[fn:3] C."
|
||||
(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))))
|
||||
;; 3. Test nested footnote in invisible definitions.
|
||||
;; Test nested footnote in invisible definitions.
|
||||
(org-test-with-temp-text "Text[1]\n\n[1] B [2]\n\n[2] C."
|
||||
;; Hide definitions.
|
||||
(narrow-to-region (point) (point-at-eol))
|
||||
|
@ -1580,7 +1684,7 @@ Footnotes[fn:2], foot[fn:test], digit only[3], and [fn:inline:anonymous footnote
|
|||
;; Both footnotes should be seen.
|
||||
(should
|
||||
(= (length (org-export-collect-footnote-definitions tree info)) 2))))
|
||||
;; 4. Test footnotes definitions collection.
|
||||
;; Test footnotes definitions collection.
|
||||
(should
|
||||
(= 4
|
||||
(org-test-with-parsed-data "Text[fn:1:A[fn:2]] [fn:3].
|
||||
|
@ -1589,7 +1693,7 @@ Footnotes[fn:2], foot[fn:test], digit only[3], and [fn:inline:anonymous footnote
|
|||
|
||||
\[fn:3] C."
|
||||
(length (org-export-collect-footnote-definitions tree info)))))
|
||||
;; 5. Test export of footnotes defined outside parsing scope.
|
||||
;; Test export of footnotes defined outside parsing scope.
|
||||
(should
|
||||
(equal
|
||||
"ParagraphOut of scope\n"
|
||||
|
@ -1605,13 +1709,13 @@ Paragraph[fn:1]"
|
|||
(org-export-backend-transcoders backend)))
|
||||
(forward-line)
|
||||
(org-export-as backend 'subtree)))))
|
||||
;; 6. Footnotes without a definition should throw an error.
|
||||
;; Footnotes without a definition should throw an error.
|
||||
(should-error
|
||||
(org-test-with-parsed-data "Text[fn:1]"
|
||||
(org-export-get-footnote-definition
|
||||
(org-element-map tree 'footnote-reference 'identity info t) info)))
|
||||
;; 7. Footnote section should be ignored in TOC and in headlines
|
||||
;; numbering.
|
||||
;; Footnote section should be ignored in TOC and in headlines
|
||||
;; numbering.
|
||||
(should
|
||||
(= 1 (let ((org-footnote-section "Footnotes"))
|
||||
(length (org-test-with-parsed-data "* H1\n* Footnotes\n"
|
||||
|
|
Loading…
Reference in New Issue