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:
Nicolas Goaziou 2015-02-13 14:10:51 +01:00
parent b95f7aa823
commit ccb663c742
2 changed files with 215 additions and 95 deletions

View File

@ -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

View File

@ -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"