From 12c94310a24c0313f61ba9794406248687149f1d Mon Sep 17 00:00:00 2001 From: Nicolas Goaziou Date: Sun, 26 Feb 2012 01:37:27 +0100 Subject: [PATCH] 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. --- EXPERIMENTAL/org-e-latex.el | 35 +++++--- contrib/lisp/org-element.el | 20 ++--- contrib/lisp/org-export.el | 106 ++++++++++++++++++------ testing/contrib/lisp/test-org-export.el | 54 ++++++++---- 4 files changed, 150 insertions(+), 65 deletions(-) diff --git a/EXPERIMENTAL/org-e-latex.el b/EXPERIMENTAL/org-e-latex.el index 459954969..43bbde731 100644 --- a/EXPERIMENTAL/org-e-latex.el +++ b/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 diff --git a/contrib/lisp/org-element.el b/contrib/lisp/org-element.el index 75d50f570..78b40629e 100644 --- a/contrib/lisp/org-element.el +++ b/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 diff --git a/contrib/lisp/org-export.el b/contrib/lisp/org-export.el index 8d5204c4c..e8cc1193f 100644 --- a/contrib/lisp/org-export.el +++ b/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))) diff --git a/testing/contrib/lisp/test-org-export.el b/testing/contrib/lisp/test-org-export.el index 401975b0b..adf4f2a9c 100644 --- a/testing/contrib/lisp/test-org-export.el +++ b/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)))))))