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.
This commit is contained in:
Nicolas Goaziou 2012-04-13 15:30:02 +02:00
parent 0c302d412c
commit 36711b05ba
2 changed files with 138 additions and 128 deletions

View File

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

View File

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