ox.el: Fix footnote-bug in #+INCLUDE-keyword

* ox.el (org-export--prepare-file-contents): Preserve footnotes
when using the LINES argument.  New optional argument FOOTNOTES.
 (org-export-expand-include-keyword): New optional argument
 FOOTNOTES.
* test-ox.el (test-org-export/expand-include): Add test for INCLUDE
  with :lines and footnotes.
This commit is contained in:
rasmus 2014-12-09 12:40:52 +01:00 committed by Rasmus
parent 6dcaafff3e
commit 7509d6d721
2 changed files with 109 additions and 29 deletions

View File

@ -3052,17 +3052,20 @@ locally for the subtree through node properties."
(car key) (car key)
(if (org-string-nw-p val) (format " %s" val) "")))))))) (if (org-string-nw-p val) (format " %s" val) ""))))))))
(defun org-export-expand-include-keyword (&optional included dir) (defun org-export-expand-include-keyword (&optional included dir footnotes)
"Expand every include keyword in buffer. "Expand every include keyword in buffer.
Optional argument INCLUDED is a list of included file names along Optional argument INCLUDED is a list of included file names along
with their line restriction, when appropriate. It is used to with their line restriction, when appropriate. It is used to
avoid infinite recursion. Optional argument DIR is the current avoid infinite recursion. Optional argument DIR is the current
working directory. It is used to properly resolve relative working directory. It is used to properly resolve relative
paths." paths. Optional argument FOOTNOTES is a hash-table used for
storing and resolving footnotes. It is created automatically."
(let ((case-fold-search t) (let ((case-fold-search t)
(file-prefix (make-hash-table :test #'equal)) (file-prefix (make-hash-table :test #'equal))
(current-prefix 0)) (current-prefix 0)
(footnotes (or footnotes (make-hash-table :test #'equal))))
(goto-char (point-min)) (goto-char (point-min))
;; Expand INCLUDE keywords.
(while (re-search-forward "^[ \t]*#\\+INCLUDE:" nil t) (while (re-search-forward "^[ \t]*#\\+INCLUDE:" nil t)
(let ((element (save-match-data (org-element-at-point)))) (let ((element (save-match-data (org-element-at-point))))
(when (eq (org-element-type element) 'keyword) (when (eq (org-element-type element) 'keyword)
@ -3155,15 +3158,23 @@ paths."
file location only-contents lines) file location only-contents lines)
lines))) lines)))
(org-mode) (org-mode)
(insert (insert (org-export--prepare-file-contents
(org-export--prepare-file-contents
file lines ind minlevel file lines ind minlevel
(or (gethash file file-prefix) (or (gethash file file-prefix)
(puthash file (incf current-prefix) file-prefix))))) (puthash file (incf current-prefix) file-prefix))
footnotes)))
(org-export-expand-include-keyword (org-export-expand-include-keyword
(cons (list file lines) included) (cons (list file lines) included)
(file-name-directory file)) (file-name-directory file)
(buffer-string))))))))))))) footnotes)
(buffer-string)))))
;; Expand footnotes after all files have been
;; included. Footnotes are stored at end of buffer.
(unless included
(org-with-wide-buffer
(goto-char (point-max))
(maphash (lambda (ref def) (insert (format "\n[%s] %s\n" ref def)))
footnotes)))))))))))
(defun org-export--inclusion-absolute-lines (file location only-contents lines) (defun org-export--inclusion-absolute-lines (file location only-contents lines)
"Resolve absolute lines for an included file with file-link. "Resolve absolute lines for an included file with file-link.
@ -3227,8 +3238,26 @@ Return a string of lines to be included in the format expected by
(while (< (point) end) (incf counter) (forward-line)) (while (< (point) end) (incf counter) (forward-line))
counter)))))))) counter))))))))
(defun org-export--prepare-file-contents (file &optional lines ind minlevel id) (defun org-export--update-footnote-label (ref-begin digit-label id)
"Prepare the contents of FILE for inclusion and return them as a string. "Prefix footnote-label at point REF-BEGIN in buffer with ID.
REF-BEGIN corresponds to the property `:begin' of objects of type
footnote-definition and footnote-reference.
If DIGIT-LABEL is non-nil the label is assumed to be of the form
\[N] where N is one or more numbers.
Return the new label."
(goto-char (1+ ref-begin))
(buffer-substring (point)
(progn
(if digit-label (insert (format "fn:%d-" id))
(forward-char 3)
(insert (format "%d-" id)))
(1- (search-forward "]")))))
(defun org-export--prepare-file-contents (file &optional lines ind minlevel id footnotes)
"Prepare contents of FILE for inclusion and return it as a string.
When optional argument LINES is a string specifying a range of When optional argument LINES is a string specifying a range of
lines, include only those lines. lines, include only those lines.
@ -3242,11 +3271,14 @@ headline encountered.
Optional argument MINLEVEL, when non-nil, is an integer Optional argument MINLEVEL, when non-nil, is an integer
specifying the level that any top-level headline in the included specifying the level that any top-level headline in the included
file should have. file should have.
Optional argument ID is an integer that will be inserted before Optional argument ID is an integer that will be inserted before
each footnote definition and reference if FILE is an Org file. each footnote definition and reference if FILE is an Org file.
This is useful to avoid conflicts when more than one Org file This is useful to avoid conflicts when more than one Org file
with footnotes is included in a document." with footnotes is included in a document.
Optional argument FOOTNOTES is a hash-table to store footnotes in
the included document.
"
(with-temp-buffer (with-temp-buffer
(insert-file-contents file) (insert-file-contents file)
(when lines (when lines
@ -3307,20 +3339,40 @@ with footnotes is included in a document."
(insert (make-string offset ?*))))))))))) (insert (make-string offset ?*)))))))))))
;; Append ID to all footnote references and definitions, so they ;; Append ID to all footnote references and definitions, so they
;; become file specific and cannot collide with footnotes in other ;; become file specific and cannot collide with footnotes in other
;; included files. ;; included files. Further, collect relevant footnotes outside of
;; LINES.
(when id (when id
(let ((marker-min (point-min-marker))
(marker-max (point-max-marker)))
(goto-char (point-min)) (goto-char (point-min))
(while (re-search-forward org-footnote-re nil t) (while (re-search-forward org-footnote-re nil t)
(let ((reference (org-element-context))) (let ((reference (org-element-context)))
(when (memq (org-element-type reference) (when (eq (org-element-type reference) 'footnote-reference)
'(footnote-reference footnote-definition)) (let* ((label (org-element-property :label reference))
(goto-char (org-element-property :begin reference)) (digit-label (and label (org-string-match-p "\\`[0-9]+\\'" label))))
(forward-char) ;; Update the footnote-reference at point and collect
(let ((label (org-element-property :label reference))) ;; the new label, which is only used for footnotes
(cond ((not label)) ;; outsides LINES.
((org-string-match-p "\\`[0-9]+\\'" label) (when label
(insert (format "fn:%d-" id))) ;; If label is akin to [1] convert it to [fn:ID-1].
(t (forward-char 3) (insert (format "%d-" id))))))))) ;; Otherwise add "ID-" after "fn:".
(let ((new-label (org-export--update-footnote-label
(org-element-property :begin reference) digit-label id)))
(unless (eq (org-element-property :type reference) 'inline)
(org-with-wide-buffer
(let* ((definition (org-footnote-get-definition label))
(beginning (nth 1 definition)))
(unless definition
(error "Definition not found for footnote %s in file %s" label file))
(if (or (< beginning marker-min) (> beginning marker-max))
;; Store since footnote-definition is outside of LINES.
(puthash new-label
(org-element-normalize-string (nth 3 definition))
footnotes)
;; Update label of definition since it is included directly.
(org-export--update-footnote-label beginning digit-label id)))))))))))
(set-marker marker-min nil)
(set-marker marker-max nil)))
(org-element-normalize-string (buffer-string)))) (org-element-normalize-string (buffer-string))))
(defun org-export-execute-babel-code () (defun org-export-execute-babel-code ()

View File

@ -904,12 +904,13 @@ Footnotes[fn:1], [fn:test] and [fn:inline:anonymous footnote].
(org-element-property :label ref))))))))))))) (org-element-property :label ref)))))))))))))
;; Footnotes labels are not local to each include keyword. ;; Footnotes labels are not local to each include keyword.
(should (should
(= 3 (= 4
(length (length
(delete-dups (delete-dups
(let ((contents " (let ((contents "
Footnotes[fn:1], [fn:test] and [fn:inline:anonymous footnote]. Footnotes[fn:1], [fn:test], [2] and [fn:inline:anonymous footnote].
\[fn:1] Footnote 1 \[fn:1] Footnote 1
\[2] Footnote 2
\[fn:test] Footnote \"test\"")) \[fn:test] Footnote \"test\""))
(org-test-with-temp-text-in-file contents (org-test-with-temp-text-in-file contents
(let ((file (buffer-file-name))) (let ((file (buffer-file-name)))
@ -919,6 +920,33 @@ Footnotes[fn:1], [fn:test] and [fn:inline:anonymous footnote].
(org-element-map (org-element-parse-buffer) (org-element-map (org-element-parse-buffer)
'footnote-reference 'footnote-reference
(lambda (ref) (org-element-property :label ref))))))))))) (lambda (ref) (org-element-property :label ref)))))))))))
;; Footnotes are supported by :lines-like elements and unnecessary
;; footnotes are dropped.
(should
(= 4
(length
(delete-dups
(let ((contents "
* foo
Footnotes[fn:1]
* bar
Footnotes[fn:2], foot[fn:test], digit only[3], and [fn:inline:anonymous footnote]
\[fn:1] Footnote 1
\[fn:2] Footnote 1
* Footnotes
\[fn:test] Footnote \"test\"
\[3] Footnote 3
"))
(org-test-with-temp-text-in-file contents
(let ((file (buffer-file-name)))
(org-test-with-temp-text
(format "#+INCLUDE: \"%s::*bar\"
" file)
(org-export-expand-include-keyword)
(org-element-map (org-element-parse-buffer)
'footnote-definition
(lambda (ref) (org-element-property :label ref)))))))))))
;; If only-contents is non-nil only include contents of element. ;; If only-contents is non-nil only include contents of element.
(should (should
(equal (equal