diff --git a/lisp/ox.el b/lisp/ox.el index ac4fde971..6a169974b 100644 --- a/lisp/ox.el +++ b/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 diff --git a/testing/lisp/test-ox.el b/testing/lisp/test-ox.el index 4a74ab3bb..8a39f174e 100644 --- a/testing/lisp/test-ox.el +++ b/testing/lisp/test-ox.el @@ -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"