浏览代码

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.
Nicolas Goaziou 10 年之前
父节点
当前提交
ccb663c742
共有 2 个文件被更改,包括 215 次插入95 次删除
  1. 91 75
      lisp/ox.el
  2. 124 20
      testing/lisp/test-ox.el

+ 91 - 75
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

+ 124 - 20
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"