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)
(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.
Optional argument INCLUDED is a list of included file names along
with their line restriction, when appropriate. It is used to
avoid infinite recursion. Optional argument DIR is the current
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)
(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))
(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))))
(when (eq (org-element-type element) 'keyword)
(beginning-of-line)
@ -3108,8 +3121,7 @@ paths."
(if (string-match ":minlevel +\\([0-9]+\\)" value)
(prog1 (string-to-number (match-string 1 value))
(setq value (replace-match "" nil nil value)))
(let ((cur (org-current-level)))
(if cur (1+ (org-reduced-level cur)) 1)))))
(get-text-property (point) :org-include-induced-level))))
(src-args (and (eq env 'literal)
(match-string 1 value)))
(block (and (string-match "\\<\\(\\S-+\\)\\>" value)
@ -3131,8 +3143,8 @@ paths."
(insert
(let ((ind-str (make-string ind ? ))
(arg-str (if (stringp src-args)
(format " %s" src-args)
""))
(format " %s" src-args)
""))
(contents
(org-escape-code-in-string
(org-export--prepare-file-contents file lines))))
@ -3142,7 +3154,7 @@ paths."
(insert
(let ((ind-str (make-string ind ? ))
(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"
ind-str block contents ind-str block))))
(t
@ -3155,15 +3167,23 @@ paths."
file location only-contents lines)
lines)))
(org-mode)
(insert
(org-export--prepare-file-contents
file lines ind minlevel
(or (gethash file file-prefix)
(puthash file (incf current-prefix) file-prefix)))))
(insert (org-export--prepare-file-contents
file lines ind minlevel
(or (gethash file file-prefix)
(puthash file (incf current-prefix) file-prefix))
footnotes)))
(org-export-expand-include-keyword
(cons (list file lines) included)
(file-name-directory file))
(buffer-string)))))))))))))
(file-name-directory file)
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)
"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))
counter))))))))
(defun org-export--prepare-file-contents (file &optional lines ind minlevel id)
"Prepare the contents of FILE for inclusion and return them as a string.
(defun org-export--update-footnote-label (ref-begin digit-label id)
"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
lines, include only those lines.
@ -3242,11 +3280,14 @@ headline encountered.
Optional argument MINLEVEL, when non-nil, is an integer
specifying the level that any top-level headline in the included
file should have.
Optional argument ID is an integer that will be inserted before
each footnote definition and reference if FILE is an 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
(insert-file-contents file)
(when lines
@ -3307,20 +3348,40 @@ with footnotes is included in a document."
(insert (make-string offset ?*)))))))))))
;; Append ID to all footnote references and definitions, so they
;; become file specific and cannot collide with footnotes in other
;; included files.
;; included files. Further, collect relevant footnotes outside of
;; LINES.
(when id
(goto-char (point-min))
(while (re-search-forward org-footnote-re nil t)
(let ((reference (org-element-context)))
(when (memq (org-element-type reference)
'(footnote-reference footnote-definition))
(goto-char (org-element-property :begin reference))
(forward-char)
(let ((label (org-element-property :label reference)))
(cond ((not label))
((org-string-match-p "\\`[0-9]+\\'" label)
(insert (format "fn:%d-" id)))
(t (forward-char 3) (insert (format "%d-" id)))))))))
(let ((marker-min (point-min-marker))
(marker-max (point-max-marker)))
(goto-char (point-min))
(while (re-search-forward org-footnote-re nil t)
(let ((reference (org-element-context)))
(when (eq (org-element-type reference) 'footnote-reference)
(let* ((label (org-element-property :label reference))
(digit-label (and label (org-string-match-p "\\`[0-9]+\\'" label))))
;; Update the footnote-reference at point and collect
;; the new label, which is only used for footnotes
;; outsides LINES.
(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))))
(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)))))))))))))
;; Footnotes labels are not local to each include keyword.
(should
(= 3
(= 4
(length
(delete-dups
(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
\[2] Footnote 2
\[fn:test] Footnote \"test\""))
(org-test-with-temp-text-in-file contents
(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)
'footnote-reference
(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.
(should
(equal
@ -975,7 +1003,42 @@ Footnotes[fn:1], [fn:test] and [fn:inline:anonymous footnote].
(org-test-with-temp-text
(format "#+INCLUDE: \"%s/examples/include.org::#dh\" :only-contents t" org-test-dir)
(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 ()
"Test macro expansion in an Org buffer."