ox: Allow file-links with #+INCLUDE-keyword

* org.el (org-edit-special): Handle file-links for INCLUDE.
* ox.el (org-export--prepare-file-contents): Handle links and
add option no-heading.
* ox.el (org-export-expand-include-keyword): Resolve headline
links and add option :only-contents.
* orgguide.texi (Include files)
org.texi (Include files): Updated.
* testing/examples/include.org: New examples.
* test-ox.el (test-org-export/expand-include): New tests.
This commit is contained in:
Rasmus 2014-09-28 21:05:17 +02:00 committed by Nicolas Goaziou
parent 58d95c3a4f
commit 986037a538
6 changed files with 220 additions and 16 deletions

View File

@ -10008,6 +10008,23 @@ to use the obvious defaults.
#+INCLUDE: "~/.emacs" :lines "10-" @r{Include lines from 10 to EOF} #+INCLUDE: "~/.emacs" :lines "10-" @r{Include lines from 10 to EOF}
@end example @end example
Finally, you may use a file-link to extract an object as matched by
@code{org-link-search}@footnote{Note that
@code{org-link-search-must-match-exact-headline} is locally bound to non-nil.
Therefore, @code{org-link-search} only matches headlines and named elements.}
(@pxref{Search options}). If the @code{:only-contents} property is non-nil,
only the contents of the requested element will be included, omitting
properties drawer and planning-line if present. The @code{:lines} keyword
operates locally with respect to the requested element. Some examples:
@example
#+INCLUDE: "./paper.org::#theory" :only-contents t
@r{Include the body of the heading with the custom id @code{theory}}
#+INCLUDE: "./paper.org::mytable" @r{Include named element.}
#+INCLUDE: "./paper.org::*conclusion" :lines 1-20
@r{Include the first 20 lines of the headline named conclusion.}
@end example
@table @kbd @table @kbd
@kindex C-c ' @kindex C-c '
@item C-c ' @item C-c '

View File

@ -2264,8 +2264,13 @@ include your @file{.emacs} file, you could use:
The optional second and third parameter are the markup (i.e., @samp{example} The optional second and third parameter are the markup (i.e., @samp{example}
or @samp{src}), and, if the markup is @samp{src}, the language for formatting or @samp{src}), and, if the markup is @samp{src}, the language for formatting
the contents. The markup is optional, if it is not given, the text will be the contents. The markup is optional, if it is not given, the text will be
assumed to be in Org mode format and will be processed normally. @kbd{C-c '} assumed to be in Org mode format and will be processed normally. File-links
will visit the included file. will be interpreted as well:
@smallexample
#+INCLUDE: "./otherfile.org::#my_custom_id" :only-contents t
@end smallexample
@noindent
@kbd{C-c '} will visit the included file.
@node Embedded @LaTeX{}, , Include files, Markup @node Embedded @LaTeX{}, , Include files, Markup
@section Embedded @LaTeX{} @section Embedded @LaTeX{}

View File

@ -20525,9 +20525,12 @@ Otherwise, return a user error."
session params)))))) session params))))))
(keyword (keyword
(if (member (org-element-property :key element) '("INCLUDE" "SETUPFILE")) (if (member (org-element-property :key element) '("INCLUDE" "SETUPFILE"))
(find-file-other-window (org-open-link-from-string
(org-remove-double-quotes (format "[[%s]]"
(car (org-split-string (org-element-property :value element))))) (expand-file-name
(org-remove-double-quotes
(car (org-split-string
(org-element-property :value element)))))))
(user-error "No special environment to edit here"))) (user-error "No special environment to edit here")))
(table (table
(if (eq (org-element-property :type element) 'table.el) (if (eq (org-element-property :type element) 'table.el)

View File

@ -3325,13 +3325,25 @@ paths."
;; Extract arguments from keyword's value. ;; Extract arguments from keyword's value.
(let* ((value (org-element-property :value element)) (let* ((value (org-element-property :value element))
(ind (org-get-indentation)) (ind (org-get-indentation))
location
(file (and (string-match (file (and (string-match
"^\\(\".+?\"\\|\\S-+\\)\\(?:\\s-+\\|$\\)" value) "^\\(\".+?\"\\|\\S-+\\)\\(?:\\s-+\\|$\\)" value)
(prog1 (expand-file-name (prog1
(org-remove-double-quotes (save-match-data
(match-string 1 value)) (let ((matched (match-string 1 value)))
dir) (when (string-match "\\(::\\(.*?\\)\\)\"?\\'" matched)
(setq location (match-string 2 matched))
(setq matched
(replace-match "" nil nil matched 1)))
(expand-file-name
(org-remove-double-quotes
matched)
dir)))
(setq value (replace-match "" nil nil value))))) (setq value (replace-match "" nil nil value)))))
(only-contents
(and (string-match ":only-contents *\\([^: \r\t\n]\\S-*\\)?" value)
(prog1 (org-not-nil (match-string 1 value))
(setq value (replace-match "" nil nil value)))))
(lines (lines
(and (string-match (and (string-match
":lines +\"\\(\\(?:[0-9]+\\)?-\\(?:[0-9]+\\)?\\)\"" ":lines +\"\\(\\(?:[0-9]+\\)?-\\(?:[0-9]+\\)?\\)\""
@ -3391,17 +3403,88 @@ paths."
(t (t
(insert (insert
(with-temp-buffer (with-temp-buffer
(let ((org-inhibit-startup t)) (org-mode)) (let ((org-inhibit-startup t)
(insert (lines
(org-export--prepare-file-contents (if location
file lines ind minlevel (org-export--inclusion-absolute-lines
(or (gethash file file-prefix) file location only-contents lines)
(puthash file (incf current-prefix) file-prefix)))) 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)))))
(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))))))))))))) (buffer-string)))))))))))))
(defun org-export--inclusion-absolute-lines (file location only-contents lines)
"Resolve absolute lines for an included file with file-link.
FILE is string file-name of the file to include. LOCATION is a
string name within FILE to be included (located via
`org-link-search'). If ONLY-CONTENTS is non-nil only the
contents of the named element will be included, as determined
Org-Element. If LINES is non-nil only those lines are included.
Return a string of lines to be included in the format expected by
`org-export--prepare-file-contents'."
(with-temp-buffer
(insert-file-contents file)
(unless (eq major-mode 'org-mode)
(let ((org-inhibit-startup t)) (org-mode)))
(condition-case err
;; Enforce consistent search.
(let ((org-link-search-must-match-exact-headline t))
(org-link-search location))
(error
(error (format "%s for %s::%s" (error-message-string err) file location))))
(let* ((element (org-element-at-point))
(contents-begin
(and only-contents (org-element-property :contents-begin element))))
(narrow-to-region
(or contents-begin (org-element-property :begin element))
(org-element-property (if contents-begin :contents-end :end) element))
(when (and only-contents
(memq (org-element-type element) '(headline inlinetask)))
;; Skip planning line and property-drawer. If a normal drawer
;; precedes a property-drawer both will be included.
;; Remaining property-drawers are removed as needed in
;; `org-export--prepare-file-contents'.
(goto-char (point-min))
(when (org-looking-at-p org-planning-line-re) (forward-line))
(when (looking-at org-property-drawer-re) (goto-char (match-end 0)))
(unless (bolp) (forward-line))
(narrow-to-region (point) (point-max))))
(when lines
(org-skip-whitespace)
(beginning-of-line)
(let* ((lines (split-string lines "-"))
(lbeg (string-to-number (car lines)))
(lend (string-to-number (cadr lines)))
(beg (if (zerop lbeg) (point-min)
(goto-char (point-min))
(forward-line (1- lbeg))
(point)))
(end (if (zerop lend) (point-max)
(goto-char beg)
(forward-line (1- lend))
(point))))
(narrow-to-region beg end)))
(let ((end (point-max)))
(goto-char (point-min))
(widen)
(let ((start-line (line-number-at-pos)))
(format "%d-%d"
start-line
(save-excursion
(+ start-line
(let ((counter 0))
(while (< (point) end) (incf counter) (forward-line))
counter))))))))
(defun org-export--prepare-file-contents (file &optional lines ind minlevel id) (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. "Prepare the contents of FILE for inclusion and return them as a string.
@ -3448,6 +3531,20 @@ with footnotes is included in a document."
(skip-chars-backward " \r\t\n") (skip-chars-backward " \r\t\n")
(forward-line) (forward-line)
(delete-region (point) (point-max)) (delete-region (point) (point-max))
;; Remove property-drawers after drawers.
(when (or ind minlevel)
(unless (eq major-mode 'org-mode)
(let ((org-inhibit-startup t)) (org-mode)))
(goto-char (point-min))
(when (looking-at org-drawer-regexp)
(goto-char (match-end 0))
(search-forward-regexp org-drawer-regexp)
(forward-line 1)
(beginning-of-line))
(when (looking-at org-property-drawer-re)
(delete-region (match-beginning 0) (match-end 0))
(beginning-of-line))
(delete-region (point) (save-excursion (and (org-skip-whitespace) (point)))))
;; If IND is set, preserve indentation of include keyword until ;; If IND is set, preserve indentation of include keyword until
;; the first headline encountered. ;; the first headline encountered.
(when ind (when ind

View File

@ -8,3 +8,28 @@ Small Org file with an include keyword.
* Heading * Heading
body body
* Another heading
:PROPERTIES:
:CUSTOM_ID: ah
:END:
1
2
3
* A headline with a table
:PROPERTIES:
:CUSTOM_ID: ht
:END:
#+CAPTION: a table
#+NAME: tbl
| 1 |
* drawer-headline
:LOGBOOK:
drawer
:END:
:PROPERTIES:
:CUSTOM_ID: dh
:END:
content

View File

@ -918,7 +918,64 @@ Footnotes[fn:1], [fn:test] and [fn:inline:anonymous footnote].
(org-export-expand-include-keyword) (org-export-expand-include-keyword)
(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)))))))))))
;; If only-contents is non-nil only include contents of element.
(should
(equal
"body\n"
(org-test-with-temp-text
(concat
(format "#+INCLUDE: \"%s/examples/include.org::*Heading\" " org-test-dir)
":only-contents t")
(org-export-expand-include-keyword)
(buffer-string))))
;; Headings can be included via CUSTOM_ID.
(should
(org-test-with-temp-text
(format "#+INCLUDE: \"%s/examples/include.org::#ah\"" org-test-dir)
(org-export-expand-include-keyword)
(goto-char (point-min))
(looking-at "* Another heading")))
;; Named objects can be included.
(should
(equal
"| 1 |\n"
(org-test-with-temp-text
(format "#+INCLUDE: \"%s/examples/include.org::tbl\" :only-contents t" org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))))
;; Including non-existing elements should result in an error.
(should-error
(org-test-with-temp-text
(format "#+INCLUDE: \"%s/examples/include.org::*non-existing heading\"" org-test-dir)
(org-export-expand-include-keyword)))
;; Lines work relatively to an included element.
(should
(equal
"2\n3\n"
(org-test-with-temp-text
(format "#+INCLUDE: \"%s/examples/include.org::#ah\" :only-contents t :lines \"2-3\"" org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))))
;; Properties should be dropped from headlines.
(should
(equal
(org-test-with-temp-text
(format "#+INCLUDE: \"%s/examples/include.org::#ht\" :only-contents t" org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))
(org-test-with-temp-text
(format "#+INCLUDE: \"%s/examples/include.org::tbl\"" org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))))
;; Properties should be dropped, drawers should not be.
(should
(equal
":LOGBOOK:\ndrawer\n:END:\ncontent\n"
(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)))))
(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."