瀏覽代碼

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.
Nicolas Goaziou 13 年之前
父節點
當前提交
12c94310a2
共有 4 個文件被更改,包括 150 次插入65 次删除
  1. 22 13
      EXPERIMENTAL/org-e-latex.el
  2. 10 10
      contrib/lisp/org-element.el
  3. 79 27
      contrib/lisp/org-export.el
  4. 39 15
      testing/contrib/lisp/test-org-export.el

+ 22 - 13
EXPERIMENTAL/org-e-latex.el

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

+ 10 - 10
contrib/lisp/org-element.el

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

+ 79 - 27
contrib/lisp/org-export.el

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

+ 39 - 15
testing/contrib/lisp/test-org-export.el

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