Merge branch 'include'

This commit is contained in:
Rasmus 2014-12-24 18:43:34 +01:00
commit 9528bef2a8
2 changed files with 160 additions and 36 deletions

View file

@ -3052,18 +3052,31 @@ 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)))
(include-re "^[ \t]*#\\+INCLUDE:"))
;; If :minlevel is not set the text-property
;; `:org-include-induced-level' will be used to determine the
;; relative level when expanding INCLUDE.
;; Only affects included Org documents.
(goto-char (point-min)) (goto-char (point-min))
(while (re-search-forward "^[ \t]*#\\+INCLUDE:" nil t) (while (re-search-forward include-re nil t)
(put-text-property (line-beginning-position) (line-end-position)
:org-include-induced-level
(1+ (org-reduced-level (or (org-current-level) 0)))))
;; Expand INCLUDE keywords.
(goto-char (point-min))
(while (re-search-forward include-re 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)
(beginning-of-line) (beginning-of-line)
@ -3108,8 +3121,7 @@ paths."
(if (string-match ":minlevel +\\([0-9]+\\)" value) (if (string-match ":minlevel +\\([0-9]+\\)" value)
(prog1 (string-to-number (match-string 1 value)) (prog1 (string-to-number (match-string 1 value))
(setq value (replace-match "" nil nil value))) (setq value (replace-match "" nil nil value)))
(let ((cur (org-current-level))) (get-text-property (point) :org-include-induced-level))))
(if cur (1+ (org-reduced-level cur)) 1)))))
(src-args (and (eq env 'literal) (src-args (and (eq env 'literal)
(match-string 1 value))) (match-string 1 value)))
(block (and (string-match "\\<\\(\\S-+\\)\\>" value) (block (and (string-match "\\<\\(\\S-+\\)\\>" value)
@ -3131,8 +3143,8 @@ paths."
(insert (insert
(let ((ind-str (make-string ind ? )) (let ((ind-str (make-string ind ? ))
(arg-str (if (stringp src-args) (arg-str (if (stringp src-args)
(format " %s" src-args) (format " %s" src-args)
"")) ""))
(contents (contents
(org-escape-code-in-string (org-escape-code-in-string
(org-export--prepare-file-contents file lines)))) (org-export--prepare-file-contents file lines))))
@ -3142,7 +3154,7 @@ paths."
(insert (insert
(let ((ind-str (make-string ind ? )) (let ((ind-str (make-string ind ? ))
(contents (contents
(org-export--prepare-file-contents file lines))) (org-export--prepare-file-contents file lines)))
(format "%s#+BEGIN_%s\n%s%s#+END_%s\n" (format "%s#+BEGIN_%s\n%s%s#+END_%s\n"
ind-str block contents ind-str block)))) ind-str block contents ind-str block))))
(t (t
@ -3155,15 +3167,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 +3247,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 +3280,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 +3348,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
(goto-char (point-min)) (let ((marker-min (point-min-marker))
(while (re-search-forward org-footnote-re nil t) (marker-max (point-max-marker)))
(let ((reference (org-element-context))) (goto-char (point-min))
(when (memq (org-element-type reference) (while (re-search-forward org-footnote-re nil t)
'(footnote-reference footnote-definition)) (let ((reference (org-element-context)))
(goto-char (org-element-property :begin reference)) (when (eq (org-element-type reference) 'footnote-reference)
(forward-char) (let* ((label (org-element-property :label reference))
(let ((label (org-element-property :label reference))) (digit-label (and label (org-string-match-p "\\`[0-9]+\\'" label))))
(cond ((not label)) ;; Update the footnote-reference at point and collect
((org-string-match-p "\\`[0-9]+\\'" label) ;; the new label, which is only used for footnotes
(insert (format "fn:%d-" id))) ;; outsides LINES.
(t (forward-char 3) (insert (format "%d-" id))))))))) (when label
;; If label is akin to [1] convert it to [fn:ID-1].
;; 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
@ -975,7 +1003,42 @@ Footnotes[fn:1], [fn:test] and [fn:inline:anonymous footnote].
(org-test-with-temp-text (org-test-with-temp-text
(format "#+INCLUDE: \"%s/examples/include.org::#dh\" :only-contents t" org-test-dir) (format "#+INCLUDE: \"%s/examples/include.org::#dh\" :only-contents t" org-test-dir)
(org-export-expand-include-keyword) (org-export-expand-include-keyword)
(buffer-string))))) (buffer-string))))
;; Adjacent INCLUDE-keywords should have the same :minlevel if unspecified.
(should
(org-every (lambda (level) (zerop (1- level)))
(org-test-with-temp-text
(concat
(format "#+INCLUDE: \"%s/examples/include.org::#ah\"\n" org-test-dir)
(format "#+INCLUDE: \"%s/examples/include.org::*Heading\"" org-test-dir))
(org-export-expand-include-keyword)
(org-element-map (org-element-parse-buffer) 'headline
(lambda (head) (org-element-property :level head))))))
;; INCLUDE does not insert induced :minlevel for src-blocks.
(should-not
(equal
(org-test-with-temp-text
(format "#+INCLUDE: \"%s/examples/include2.org\" src emacs-lisp" org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))
(org-test-with-temp-text
(format "#+INCLUDE: \"%s/examples/include2.org\" src emacs-lisp :minlevel 1" org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))))
;; INCLUDE assigns the relative :minlevel conditional on narrowing.
(should
(org-test-with-temp-text-in-file
(format "* h1\n<point>#+INCLUDE: \"%s/examples/include.org::#ah\"" org-test-dir)
(narrow-to-region (point) (point-max))
(org-export-expand-include-keyword)
(eq 1 (org-current-level))))
;; If :minlevel is present do not alter it.
(should
(org-test-with-temp-text
(format "* h1\n<point>#+INCLUDE: \"%s/examples/include.org::#ah\" :minlevel 3" org-test-dir)
(narrow-to-region (point) (point-max))
(org-export-expand-include-keyword)
(eq 3 (org-current-level)))))
(ert-deftest test-org-export/expand-macro () (ert-deftest test-org-export/expand-macro ()
"Test macro expansion in an Org buffer." "Test macro expansion in an Org buffer."