Browse Source

org-export: Fix footnotes export outside parsing scope

* contrib/lisp/org-export.el (org-export-get-buffer-attributes):
  Function renamed from `org-export-initial-options'.
(org-export-store-footnote-definitions): New function.
(org-export-as): Store footnote definitions at the appropriate place.
It happens twice if buffer gets expanded.
* testing/lisp/test-org-export.el: Add test.
Nicolas Goaziou 8 years ago
parent
commit
36711b05ba
2 changed files with 138 additions and 128 deletions
  1. 110 92
      contrib/lisp/org-export.el
  2. 28 36
      testing/lisp/test-org-export.el

+ 110 - 92
contrib/lisp/org-export.el

@@ -652,7 +652,6 @@ standard mode."
 
 ;; 1. Environment options are collected once at the very beginning of
 ;;    the process, out of the original buffer and configuration.
-;;    Associated to the parse tree, they make an Org closure.
 ;;    Collecting them is handled by `org-export-get-environment'
 ;;    function.
 ;;
@@ -891,6 +890,7 @@ standard mode."
 ;; increasing precedence order:
 ;;
 ;; - Global variables,
+;; - Buffer's attributes,
 ;; - Options keyword symbols,
 ;; - Buffer keywords,
 ;; - Subtree properties.
@@ -901,14 +901,11 @@ standard mode."
 ;; the different sources.
 
 ;;  The internal functions doing the retrieval are:
-;;  `org-export-parse-option-keyword' ,
-;;  `org-export-get-subtree-options' ,
-;;  `org-export-get-inbuffer-options' and
-;;  `org-export-get-global-options'.
-;;
-;;  Some properties, which do not rely on the previous sources but
-;;  still depend on the original buffer, are taken care of with
-;;  `org-export-initial-options'.
+;;  `org-export-get-global-options',
+;;  `org-export-get-buffer-attributes',
+;;  `org-export-parse-option-keyword',
+;;  `org-export-get-subtree-options' and
+;;  `org-export-get-inbuffer-options'
 
 ;; Also, `org-export-confirm-letbind' and `org-export-install-letbind'
 ;; take care of the part relative to "#+BIND:" keywords.
@@ -931,13 +928,8 @@ inferior to file-local settings."
   (let ((options (org-combine-plists
 		  ;; ... from global variables...
 		  (org-export-get-global-options backend)
-		  ;; ... from buffer's name (default title)...
-		  `(:title
-		    ,(or (let ((file (buffer-file-name (buffer-base-buffer))))
-			   (and file
-				(file-name-sans-extension
-				 (file-name-nondirectory file))))
-			 (buffer-name (buffer-base-buffer))))
+		  ;; ... from buffer's attributes...
+		  (org-export-get-buffer-attributes)
 		  ;; ... from an external property list...
 		  ext-plist
 		  ;; ... from in-buffer settings...
@@ -947,8 +939,6 @@ inferior to file-local settings."
 			(org-remove-double-quotes buffer-file-name)))
 		  ;; ... and from subtree, when appropriate.
 		  (and subtreep (org-export-get-subtree-options)))))
-    ;; Add initial options.
-    (setq options (append (org-export-initial-options) options))
     ;; Return plist.
     options))
 
@@ -1140,6 +1130,32 @@ Assume buffer is in Org mode.  Narrowing, if any, is ignored."
      ;; 3. Return final value.
      plist)))
 
+(defun org-export-get-buffer-attributes ()
+  "Return properties related to buffer attributes, as a plist."
+  (let ((visited-file (buffer-file-name (buffer-base-buffer))))
+    (list
+     ;; Store full path of input file name, or nil.  For internal use.
+     :input-file visited-file
+     :title (or (and visited-file
+		     (file-name-sans-extension
+		      (file-name-nondirectory visited-file)))
+		(buffer-name (buffer-base-buffer)))
+     :macro-modification-time
+     (and visited-file
+	  (file-exists-p visited-file)
+	  (concat "(eval (format-time-string \"$1\" '"
+		  (prin1-to-string (nth 5 (file-attributes visited-file)))
+		  "))"))
+     ;; Store input file name as a macro.
+     :macro-input-file (and visited-file (file-name-nondirectory visited-file))
+     ;; `:macro-date', `:macro-time' and `:macro-property' could as
+     ;; well be initialized as tree properties, since they don't
+     ;; depend on buffer properties.  Though, it may be more logical
+     ;; to keep them close to other ":macro-" properties.
+     :macro-date "(eval (format-time-string \"$1\"))"
+     :macro-time "(eval (format-time-string \"$1\"))"
+     :macro-property "(eval (org-entry-get nil \"$1\" 'selective))")))
+
 (defun org-export-get-global-options (&optional backend)
   "Return global export options as a plist.
 
@@ -1159,50 +1175,46 @@ process."
     ;; Return value.
     plist))
 
-(defun org-export-initial-options ()
-  "Return a plist with properties related to input buffer."
-  (let ((visited-file (buffer-file-name (buffer-base-buffer))))
-    (list
-     ;; Store full path of input file name, or nil.  For internal use.
-     :input-file visited-file
-     ;; `:macro-date', `:macro-time' and `:macro-property' could as well
-     ;; be initialized as tree properties, since they don't depend on
-     ;; initial environment.  Though, it may be more logical to keep
-     ;; them close to other ":macro-" properties.
-     :macro-date "(eval (format-time-string \"$1\"))"
-     :macro-time "(eval (format-time-string \"$1\"))"
-     :macro-property "(eval (org-entry-get nil \"$1\" 'selective))"
-     :macro-modification-time
-     (and visited-file
-	  (file-exists-p visited-file)
-	  (concat "(eval (format-time-string \"$1\" '"
-		  (prin1-to-string (nth 5 (file-attributes visited-file)))
-		  "))"))
-     ;; Store input file name as a macro.
-     :macro-input-file (and visited-file (file-name-nondirectory visited-file))
-     ;; Footnotes definitions must be collected in the original buffer,
-     ;; as there's no insurance that they will still be in the parse
-     ;; tree, due to some narrowing.
-     :footnote-definition-alist
-     (let (alist)
-       (org-with-wide-buffer
-	(goto-char (point-min))
-	(while (re-search-forward org-footnote-definition-re nil t)
-	  (let ((def (org-footnote-at-definition-p)))
-	    (when def
-	      (org-skip-whitespace)
-	      (push (cons (car def)
-			  (save-restriction
-			    (narrow-to-region (point) (nth 2 def))
-			    ;; Like `org-element-parse-buffer', but
-			    ;; makes sure the definition doesn't start
-			    ;; with a section element.
-			    (nconc
-			     (list 'org-data nil)
-			     (org-element-parse-elements
-			      (point-min) (point-max) nil nil nil nil nil))))
-		    alist))))
-	alist)))))
+(defun org-export-store-footnote-definitions (info)
+  "Collect and store footnote definitions from current buffer in INFO.
+
+INFO is a plist containing export options.
+
+Footnotes definitions are stored as a alist whose CAR is
+footnote's label, as a string, and CDR the contents, as a parse
+tree.  This alist will be consed to the value of
+`:footnote-definition-alist' in INFO, if any.
+
+The new plist is returned; use
+
+  \(setq info (org-export-store-footnote-definitions info))
+
+to be sure to use the new value.  INFO is modified by side
+effects."
+  ;; Footnotes definitions must be collected in the original buffer,
+  ;; as there's no insurance that they will still be in the parse
+  ;; tree, due to some narrowing.
+  (plist-put
+   info :footnote-definition-alist
+   (let ((alist (plist-get info :footnote-definition-alist)))
+     (org-with-wide-buffer
+      (goto-char (point-min))
+      (while (re-search-forward org-footnote-definition-re nil t)
+	(let ((def (org-footnote-at-definition-p)))
+	  (when def
+	    (org-skip-whitespace)
+	    (push (cons (car def)
+			(save-restriction
+			  (narrow-to-region (point) (nth 2 def))
+			  ;; Like `org-element-parse-buffer', but makes
+			  ;; sure the definition doesn't start with
+			  ;; a section element.
+			  (nconc
+			   (list 'org-data nil)
+			   (org-element-parse-elements
+			    (point-min) (point-max) nil nil nil nil nil))))
+		  alist))))
+      alist))))
 
 (defvar org-export-allow-BIND-local nil)
 (defun org-export-confirm-letbind ()
@@ -2078,54 +2090,60 @@ to be expanded and Babel code to be executed.
 Return code as a string."
   (save-excursion
     (save-restriction
-      (let (info tree)
-	;; Narrow buffer to an appropriate region or subtree for
-	;; parsing.  If parsing subtree, be sure to remove main
-	;; headline too.
-	(cond ((org-region-active-p)
-	       (narrow-to-region (region-beginning) (region-end)))
-	      (subtreep
-	       (org-narrow-to-subtree)
-	       (goto-char (point-min))
-	       (forward-line)
-	       (narrow-to-region (point) (point-max))))
-	;; 1. Get export environment and tree.  Environment is
-	;;    relative to the buffer being parsed, which isn't always
-	;;    the original one, depending on the NOEXPAND value.
+      ;; Narrow buffer to an appropriate region or subtree for
+      ;; parsing.  If parsing subtree, be sure to remove main headline
+      ;; too.
+      (cond ((org-region-active-p)
+	     (narrow-to-region (region-beginning) (region-end)))
+	    (subtreep
+	     (org-narrow-to-subtree)
+	     (goto-char (point-min))
+	     (forward-line)
+	     (narrow-to-region (point) (point-max))))
+      ;; 1. Get export environment from original buffer.  Store
+      ;;    original footnotes definitions in communication channel as
+      ;;    they might not be accessible anymore in a narrowed parse
+      ;;    tree.  Also install user's and developer's filters.
+      (let ((info (org-export-install-filters
+		   backend
+		   (org-export-store-footnote-definitions
+		    (org-export-get-environment backend subtreep ext-plist))))
+	    tree)
+	;; 2. Get parse tree.
 	(if noexpand
 	    ;; If NOEXPAND is non-nil, simply parse current visible
-	    ;; part of buffer and retrieve environment from original
-	    ;; buffer.
-	    (setq info (org-export-get-environment backend subtreep ext-plist)
-		  tree (org-element-parse-buffer nil visible-only))
+	    ;; part of buffer.
+	    (setq tree (org-element-parse-buffer nil visible-only))
 	  ;; Otherwise, buffer isn't parsed directly.  Instead,
 	  ;; a temporary copy is created, where include keywords are
-	  ;; expanded and code blocks are evaluated.  Environment is
-	  ;; retrieved from that buffer.  Moreover, save original file
-	  ;; name or buffer in order to properly resolve babel block
-	  ;; expansion when body is outside scope.
+	  ;; expanded and code blocks are evaluated.
 	  (let ((buf (or (buffer-file-name (buffer-base-buffer))
 			 (current-buffer))))
 	    (org-export-with-current-buffer-copy
 	     (org-export-expand-include-keyword)
+	     ;; Setting `org-current-export-file' is required by Org
+	     ;; Babel to properly resolve noweb references.
 	     (let ((org-current-export-file buf))
 	       (org-export-blocks-preprocess))
-	     (setq info (org-export-get-environment backend subtreep ext-plist)
-		   tree (org-element-parse-buffer nil visible-only)))))
-	;; 2. Install user's and developer's filters in communication
-	;;    channel.  Then call parse-tree filters to get the final
-	;;    tree.
-	(setq info (org-export-install-filters backend info))
+	     (setq tree (org-element-parse-buffer nil visible-only)
+		   ;; Footnote definitions must be stored again, since
+		   ;; buffer's expansion might have modified
+		   ;; boundaries of footnote definitions contained in
+		   ;; the parse tree.  This way, definitions in
+		   ;; `footnote-definition-alist' are bound to
+		   ;; coincide with those in the parse tree.
+		   info (org-export-store-footnote-definitions info)))))
+	;; 3. Call parse-tree filters to get the final tree.
 	(setq tree
 	      (org-export-filter-apply-functions
 	       (plist-get info :filter-parse-tree) tree backend info))
-	;; 3. Now tree is complete, compute its properties and add
+	;; 4. Now tree is complete, compute its properties and add
 	;;    them to communication channel.
 	(setq info
 	      (org-combine-plists
 	       info
 	       (org-export-collect-tree-properties tree info backend)))
-	;; 4. Eventually transcode TREE.  Wrap the resulting string
+	;; 5. Eventually transcode TREE.  Wrap the resulting string
 	;;    into a template, if required.  Eventually call
 	;;    final-output filter.
 	(let* ((body (org-element-normalize-string

+ 28 - 36
testing/lisp/test-org-export.el

@@ -338,10 +338,8 @@ body\n")))
     (org-test-with-temp-text
 	"Text[fn:1] [1] [fn:label:C] [fn::D]\n\n[fn:1] A\n\n[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)))
+	     (info (org-export-store-footnote-definitions
+		    `(:parse-tree ,tree :with-footnotes t))))
 	(should
 	 (equal
 	  '((1 . "A") (2 . "B") (3 . "C") (4 . "D"))
@@ -358,10 +356,8 @@ body\n")))
     (org-test-with-temp-text
 	"Text[fn:1:A[fn:2]] [fn:3].\n\n[fn:2] B [fn:3] [fn::D].\n\n[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)))
+	     (info (org-export-store-footnote-definitions
+		    `(:parse-tree ,tree :with-footnotes t))))
 	(should
 	 (equal
 	  '((1 . "fn:1") (2 . "fn:2") (3 . "fn:3") (4))
@@ -377,10 +373,8 @@ body\n")))
       ;; Hide definitions.
       (narrow-to-region (point) (point-at-eol))
       (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)))
+	     (info (org-export-store-footnote-definitions
+		    `(:parse-tree ,tree :with-footnotes t))))
 	;; Both footnotes should be seen.
 	(should
 	 (= (length (org-export-collect-footnote-definitions tree info)) 2))))
@@ -390,13 +384,23 @@ body\n")))
 \[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)))
+      (let* ((tree (org-element-parse-buffer))
+	     (info (org-export-store-footnote-definitions
+		    `(:parse-tree ,tree :with-footnotes t))))
 	(should (= (length (org-export-collect-footnote-definitions tree info))
-		   4))))))
+		   4))))
+    ;; 5. Test export of footnotes defined outside parsing scope.
+    (org-test-with-temp-text "[fn:1] Out of scope
+* Title
+Paragraph[fn:1]"
+      (org-test-with-backend "test"
+	(flet ((org-test-footnote-reference
+		(fn-ref contents info)
+		(org-element-interpret-data
+		 (org-export-get-footnote-definition fn-ref info))))
+	  (forward-line)
+	  (should (equal "ParagraphOut of scope\n"
+			 (org-export-as 'test 'subtree))))))))
 
 
 
@@ -408,9 +412,7 @@ body\n")))
   (org-test-with-temp-text
       "Paragraph.\n#+TARGET: Test\n[[Test]]"
     (let* ((tree (org-element-parse-buffer))
-	   (info (org-combine-plists (org-export-initial-options))))
-      (setq info (org-combine-plists
-		  info (org-export-collect-tree-properties tree info 'test)))
+	   (info (org-export-collect-tree-properties tree nil 'test)))
       (should-not
        (org-element-map
 	tree 'link
@@ -421,9 +423,7 @@ body\n")))
   (org-test-with-temp-text
       "Paragraph.\n* Head1\n* Head2\n* Head3\n[[Head2]]"
     (let* ((tree (org-element-parse-buffer))
-	   (info (org-combine-plists (org-export-initial-options))))
-      (setq info (org-combine-plists
-		  info (org-export-collect-tree-properties tree info 'test)))
+	   (info (org-export-collect-tree-properties tree nil 'test)))
       (should
        ;; Note: Headline's number is in fact a list of numbers.
        (equal '(2)
@@ -436,9 +436,7 @@ body\n")))
   (org-test-with-temp-text
       "- Item1\n  - Item11\n  - <<test>>Item12\n- Item2\n\n\n[[test]]"
     (let* ((tree (org-element-parse-buffer))
-	   (info (org-combine-plists (org-export-initial-options))))
-      (setq info (org-combine-plists
-		  info (org-export-collect-tree-properties tree info 'test)))
+	   (info (org-export-collect-tree-properties tree nil 'test)))
       (should
        ;; Note: Item's number is in fact a list of numbers.
        (equal '(1 2)
@@ -452,9 +450,7 @@ body\n")))
   (org-test-with-temp-text
       "Paragraph[1][2][fn:lbl3:C<<target>>][[test]][[target]]\n[1] A\n\n[2] <<test>>B"
     (let* ((tree (org-element-parse-buffer))
-	   (info (org-combine-plists (org-export-initial-options))))
-      (setq info (org-combine-plists
-		  info (org-export-collect-tree-properties tree info 'test)))
+	   (info (org-export-collect-tree-properties tree nil 'test)))
       (should
        (equal '(2 3)
 	      (org-element-map
@@ -467,9 +463,7 @@ body\n")))
   (org-test-with-temp-text
       "#+NAME: tbl1\n|1|2|\n#+NAME: tbl2\n|3|4|\n#+NAME: tbl3\n|5|6|\n[[tbl2]]"
     (let* ((tree (org-element-parse-buffer))
-	   (info (org-combine-plists (org-export-initial-options))))
-      (setq info (org-combine-plists
-		  info (org-export-collect-tree-properties tree info 'test)))
+	   (info (org-export-collect-tree-properties tree nil 'test)))
       (should
        (= 2
 	  (org-element-map
@@ -482,9 +476,7 @@ body\n")))
   (org-test-with-temp-text
       "* Head1\n* Head2\nParagraph<<target>>\n* Head3\n[[target]]"
     (let* ((tree (org-element-parse-buffer))
-	   (info (org-combine-plists (org-export-initial-options))))
-      (setq info (org-combine-plists
-		  info (org-export-collect-tree-properties tree info 'test)))
+	   (info (org-export-collect-tree-properties tree nil 'test)))
       (should
        (equal '(2)
 	      (org-element-map