org-export: Support for external id links

* contrib/lisp/org-export.el (org-export-get-buffer-attributes):
  Retrieve footnote definitions and id in buffer.
(org-export-store-footnote-definitions): Removed function.
(org-export-collect-tree-properties): Update docstring.
(org-export-as): Do not call `org-export-store-footnote-definitions'.
(org-export-resolve-id-link): Return external file name when there's
no match for id in current parse tree.
* contrib/lisp/org-e-latex.el (org-e-latex-link): Handle external id
  links.
* testing/lisp/test-org-export.el: Add tests.
This commit is contained in:
Nicolas Goaziou 2012-06-06 23:39:04 +02:00
parent 343a705ba5
commit ad235400a6
3 changed files with 194 additions and 187 deletions

View File

@ -1598,6 +1598,10 @@ INFO is a plist holding contextual information. See
(org-export-resolve-fuzzy-link link info)
(org-export-resolve-id-link link info))))
(case (org-element-type destination)
;; Id link points to an external file.
(plain-text
(if desc (format "\\href{file://%s}{%s}" destination desc)
(format "\\url{file://%s}" destination)))
;; Fuzzy link points nowhere.
('nil
(format org-e-latex-link-with-unknown-path-format

View File

@ -1065,7 +1065,7 @@ inferior to file-local settings."
(and buffer-file-name (org-remove-double-quotes buffer-file-name)))
;; ... and from subtree, when appropriate.
(and subtreep (org-export-get-subtree-options))
;; Also install back-end symbol and its translation table.
;; Eventually install back-end symbol and its translation table.
`(:back-end
,backend
:translate-alist
@ -1266,6 +1266,40 @@ Assume buffer is in Org mode. Narrowing, if any, is ignored."
(file-name-sans-extension
(file-name-nondirectory visited-file)))
(buffer-name (buffer-base-buffer)))
:footnote-definition-alist
;; 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 possible narrowing.
(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))
:id-alist
;; Collect id references.
(let (alist)
(org-with-wide-buffer
(goto-char (point-min))
(while (re-search-forward
"\\[\\[id:\\(\\S-+?\\)\\]\\(?:\\[.*?\\]\\)?\\]" nil t)
(let* ((id (org-match-string-no-properties 1))
(file (org-id-find-id-file id)))
(when file (push (cons id (file-relative-name file)) alist)))))
alist)
:macro-modification-time
(and visited-file
(file-exists-p visited-file)
@ -1301,47 +1335,6 @@ process."
;; Return value.
plist))
(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 ()
"Can we use #+BIND values during export?
@ -1405,7 +1398,9 @@ Following tree properties are set or updated:
`:ignore-list' List of elements that should be ignored during
export.
`:target-list' List of all targets in the parse tree."
`:target-list' List of all targets in the parse tree.
Return updated plist."
;; Install the parse tree in the communication channel, in order to
;; use `org-export-get-genealogy' and al.
(setq info (plist-put info :parse-tree data))
@ -2267,8 +2262,7 @@ Return code as a string."
;; 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
(org-export-store-footnote-definitions
(org-export-get-environment backend subtreep ext-plist))))
(org-export-get-environment backend subtreep ext-plist)))
;; 2. Get parse tree. Buffer isn't parsed directly.
;; Instead, a temporary copy is created, where include
;; keywords are expanded and code blocks are evaluated.
@ -3007,16 +3001,20 @@ Assume LINK type is \"fuzzy\"."
INFO is a plist used as a communication channel.
Return value can be an headline element or nil. Assume LINK type
is either \"id\" or \"custom-id\"."
Return value can be the headline element matched in current parse
tree, a file name or nil. Assume LINK type is either \"id\" or
\"custom-id\"."
(let ((id (org-element-property :path link)))
(org-element-map
(plist-get info :parse-tree) 'headline
(lambda (headline)
(when (or (string= (org-element-property :id headline) id)
(string= (org-element-property :custom-id headline) id))
headline))
info 'first-match)))
;; First check if id is within the current parse tree.
(or (org-element-map
(plist-get info :parse-tree) 'headline
(lambda (headline)
(when (or (string= (org-element-property :id headline) id)
(string= (org-element-property :custom-id headline) id))
headline))
info 'first-match)
;; Otherwise, look for external files.
(cdr (assoc id (plist-get info :id-alist))))))
(defun org-export-resolve-radio-link (link info)
"Return radio-target object referenced as LINK destination.

View File

@ -375,62 +375,56 @@ body\n")))
(ert-deftest test-org-export/footnotes ()
"Test footnotes specifications."
(let ((org-footnote-section nil))
(let ((org-footnote-section nil)
(org-export-with-footnotes t))
;; 1. Read every type of footnote.
(org-test-with-temp-text
(org-test-with-parsed-data
"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-export-store-footnote-definitions
`(:parse-tree ,tree :with-footnotes t))))
(should
(equal
'((1 . "A") (2 . "B") (3 . "C") (4 . "D"))
(org-element-map
tree 'footnote-reference
(lambda (ref)
(let ((def (org-export-get-footnote-definition ref info)))
(cons (org-export-get-footnote-number ref info)
(if (eq (org-element-property :type ref) 'inline) (car def)
(car (org-element-contents
(car (org-element-contents def))))))))
info)))))
(should
(equal
'((1 . "A") (2 . "B") (3 . "C") (4 . "D"))
(org-element-map
tree 'footnote-reference
(lambda (ref)
(let ((def (org-export-get-footnote-definition ref info)))
(cons (org-export-get-footnote-number ref info)
(if (eq (org-element-property :type ref) 'inline) (car def)
(car (org-element-contents
(car (org-element-contents def))))))))
info))))
;; 2. Test nested footnotes order.
(org-test-with-temp-text
(org-test-with-parsed-data
"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-export-store-footnote-definitions
`(:parse-tree ,tree :with-footnotes t))))
(should
(equal
'((1 . "fn:1") (2 . "fn:2") (3 . "fn:3") (4))
(org-element-map
tree 'footnote-reference
(lambda (ref)
(when (org-export-footnote-first-reference-p ref info)
(cons (org-export-get-footnote-number ref info)
(org-element-property :label ref))))
info)))))
(should
(equal
'((1 . "fn:1") (2 . "fn:2") (3 . "fn:3") (4))
(org-element-map
tree 'footnote-reference
(lambda (ref)
(when (org-export-footnote-first-reference-p ref info)
(cons (org-export-get-footnote-number ref info)
(org-element-property :label ref))))
info))))
;; 3. Test nested footnote in invisible definitions.
(org-test-with-temp-text "Text[1]\n\n[1] B [2]\n\n[2] C."
;; Hide definitions.
(narrow-to-region (point) (point-at-eol))
(let* ((tree (org-element-parse-buffer))
(info (org-export-store-footnote-definitions
`(:parse-tree ,tree :with-footnotes t))))
(info (org-combine-plists
`(:parse-tree ,tree)
(org-export-collect-tree-properties
tree (org-export-get-environment)))))
;; Both footnotes should be seen.
(should
(= (length (org-export-collect-footnote-definitions tree info)) 2))))
;; 4. Test footnotes definitions collection.
(org-test-with-temp-text "Text[fn:1:A[fn:2]] [fn:3].
(org-test-with-parsed-data "Text[fn:1:A[fn:2]] [fn:3].
\[fn:2] B [fn:3] [fn::D].
\[fn:3] C."
(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))))
(should (= (length (org-export-collect-footnote-definitions tree info))
4)))
;; 5. Test export of footnotes defined outside parsing scope.
(org-test-with-temp-text "[fn:1] Out of scope
* Title
@ -484,8 +478,73 @@ Paragraph[fn:1]"
;;; Links
(ert-deftest test-org-export/fuzzy-links ()
"Test fuzzy link export specifications."
(ert-deftest test-org-export/resolve-coderef ()
"Test `org-export-resolve-coderef' specifications."
(let ((org-coderef-label-format "(ref:%s)"))
;; 1. A link to a "-n -k -r" block returns line number.
(org-test-with-parsed-data
"#+BEGIN_EXAMPLE -n -k -r\nText (ref:coderef)\n#+END_EXAMPLE"
(should (= (org-export-resolve-coderef "coderef" info) 1)))
(org-test-with-parsed-data
"#+BEGIN_SRC emacs-lisp -n -k -r\n(+ 1 1) (ref:coderef)\n#+END_SRC"
(should (= (org-export-resolve-coderef "coderef" info) 1)))
;; 2. A link to a "-n -r" block returns line number.
(org-test-with-parsed-data
"#+BEGIN_EXAMPLE -n -r\nText (ref:coderef)\n#+END_EXAMPLE"
(should (= (org-export-resolve-coderef "coderef" info) 1)))
(org-test-with-parsed-data
"#+BEGIN_SRC emacs-lisp -n -r\n(+ 1 1) (ref:coderef)\n#+END_SRC"
(should (= (org-export-resolve-coderef "coderef" info) 1)))
;; 3. A link to a "-n" block returns coderef.
(org-test-with-parsed-data
"#+BEGIN_SRC emacs-lisp -n\n(+ 1 1) (ref:coderef)\n#+END_SRC"
(should (equal (org-export-resolve-coderef "coderef" info) "coderef")))
(org-test-with-parsed-data
"#+BEGIN_EXAMPLE -n\nText (ref:coderef)\n#+END_EXAMPLE"
(should (equal (org-export-resolve-coderef "coderef" info) "coderef")))
;; 4. A link to a "-r" block returns line number.
(org-test-with-parsed-data
"#+BEGIN_SRC emacs-lisp -r\n(+ 1 1) (ref:coderef)\n#+END_SRC"
(should (= (org-export-resolve-coderef "coderef" info) 1)))
(org-test-with-parsed-data
"#+BEGIN_EXAMPLE -r\nText (ref:coderef)\n#+END_EXAMPLE"
(should (= (org-export-resolve-coderef "coderef" info) 1)))
;; 5. A link to a block without a switch returns coderef.
(org-test-with-parsed-data
"#+BEGIN_SRC emacs-lisp\n(+ 1 1) (ref:coderef)\n#+END_SRC"
(should (equal (org-export-resolve-coderef "coderef" info) "coderef")))
(org-test-with-parsed-data
"#+BEGIN_EXAMPLE\nText (ref:coderef)\n#+END_EXAMPLE"
(should (equal (org-export-resolve-coderef "coderef" info) "coderef")))
;; 6. Correctly handle continued line numbers. A "+n" switch
;; should resume numbering from previous block with numbered
;; lines, ignoring blocks not numbering lines in the process.
;; A "-n" switch resets count.
(org-test-with-parsed-data "
#+BEGIN_EXAMPLE -n
Text.
#+END_EXAMPLE
#+BEGIN_SRC emacs-lisp
\(- 1 1)
#+END_SRC
#+BEGIN_SRC emacs-lisp +n -r
\(+ 1 1) (ref:addition)
#+END_SRC
#+BEGIN_EXAMPLE -n -r
Another text. (ref:text)
#+END_EXAMPLE"
(should (= (org-export-resolve-coderef "addition" info) 2))
(should (= (org-export-resolve-coderef "text" info) 1)))
;; 7. Recognize coderef with user-specified syntax.
(org-test-with-parsed-data
"#+BEGIN_EXAMPLE -l \"[ref:%s]\"\nText. [ref:text]\n#+END_EXAMPLE"
(should (equal (org-export-resolve-coderef "text" info) "text")))))
(ert-deftest test-org-export/resolve-fuzzy-link ()
"Test `org-export-resolve-fuzzy-link' specifications."
;; 1. Links to invisible (keyword) targets should be ignored.
(org-test-with-parsed-data
"Paragraph.\n#+TARGET: Test\n[[Test]]"
@ -551,98 +610,44 @@ Paragraph[1][2][fn:lbl3:C<<target>>][[test]][[target]]\n[1] A\n\n[2] <<test>>B"
(org-export-get-ordinal
(org-export-resolve-fuzzy-link link info) info)) info t)))))
(ert-deftest test-org-export/resolve-coderef ()
"Test `org-export-resolve-coderef' specifications."
(let ((org-coderef-label-format "(ref:%s)"))
;; 1. A link to a "-n -k -r" block returns line number.
(org-test-with-temp-text
"#+BEGIN_EXAMPLE -n -k -r\nText (ref:coderef)\n#+END_EXAMPLE"
(let ((tree (org-element-parse-buffer)))
(should
(= (org-export-resolve-coderef "coderef" `(:parse-tree ,tree)) 1))))
(org-test-with-temp-text
"#+BEGIN_SRC emacs-lisp -n -k -r\n(+ 1 1) (ref:coderef)\n#+END_SRC"
(let ((tree (org-element-parse-buffer)))
(should
(= (org-export-resolve-coderef "coderef" `(:parse-tree ,tree)) 1))))
;; 2. A link to a "-n -r" block returns line number.
(org-test-with-temp-text
"#+BEGIN_EXAMPLE -n -r\nText (ref:coderef)\n#+END_EXAMPLE"
(let ((tree (org-element-parse-buffer)))
(should
(= (org-export-resolve-coderef "coderef" `(:parse-tree ,tree)) 1))))
(org-test-with-temp-text
"#+BEGIN_SRC emacs-lisp -n -r\n(+ 1 1) (ref:coderef)\n#+END_SRC"
(let ((tree (org-element-parse-buffer)))
(should
(= (org-export-resolve-coderef "coderef" `(:parse-tree ,tree)) 1))))
;; 3. A link to a "-n" block returns coderef.
(org-test-with-temp-text
"#+BEGIN_SRC emacs-lisp -n\n(+ 1 1) (ref:coderef)\n#+END_SRC"
(let ((tree (org-element-parse-buffer)))
(should
(equal (org-export-resolve-coderef "coderef" `(:parse-tree ,tree))
"coderef"))))
(org-test-with-temp-text
"#+BEGIN_EXAMPLE -n\nText (ref:coderef)\n#+END_EXAMPLE"
(let ((tree (org-element-parse-buffer)))
(should
(equal (org-export-resolve-coderef "coderef" `(:parse-tree ,tree))
"coderef"))))
;; 4. A link to a "-r" block returns line number.
(org-test-with-temp-text
"#+BEGIN_SRC emacs-lisp -r\n(+ 1 1) (ref:coderef)\n#+END_SRC"
(let ((tree (org-element-parse-buffer)))
(should
(= (org-export-resolve-coderef "coderef" `(:parse-tree ,tree)) 1))))
(org-test-with-temp-text
"#+BEGIN_EXAMPLE -r\nText (ref:coderef)\n#+END_EXAMPLE"
(let ((tree (org-element-parse-buffer)))
(should
(= (org-export-resolve-coderef "coderef" `(:parse-tree ,tree)) 1))))
;; 5. A link to a block without a switch returns coderef.
(org-test-with-temp-text
"#+BEGIN_SRC emacs-lisp\n(+ 1 1) (ref:coderef)\n#+END_SRC"
(let ((tree (org-element-parse-buffer)))
(should
(equal (org-export-resolve-coderef "coderef" `(:parse-tree ,tree))
"coderef"))))
(org-test-with-temp-text
"#+BEGIN_EXAMPLE\nText (ref:coderef)\n#+END_EXAMPLE"
(let ((tree (org-element-parse-buffer)))
(should
(equal (org-export-resolve-coderef "coderef" `(:parse-tree ,tree))
"coderef"))))
;; 6. Correctly handle continued line numbers. A "+n" switch
;; should resume numbering from previous block with numbered
;; lines, ignoring blocks not numbering lines in the process.
;; A "-n" switch resets count.
(org-test-with-temp-text "
#+BEGIN_EXAMPLE -n
Text.
#+END_EXAMPLE
#+BEGIN_SRC emacs-lisp
\(- 1 1)
#+END_SRC
#+BEGIN_SRC emacs-lisp +n -r
\(+ 1 1) (ref:addition)
#+END_SRC
#+BEGIN_EXAMPLE -n -r
Another text. (ref:text)
#+END_EXAMPLE"
(let* ((tree (org-element-parse-buffer))
(info `(:parse-tree ,tree)))
(should (= (org-export-resolve-coderef "addition" info) 2))
(should (= (org-export-resolve-coderef "text" info) 1))))
;; 7. Recognize coderef with user-specified syntax.
(org-test-with-temp-text
"#+BEGIN_EXAMPLE -l \"[ref:%s]\"\nText. [ref:text]\n#+END_EXAMPLE"
(let ((tree (org-element-parse-buffer)))
(should (equal (org-export-resolve-coderef "text" `(:parse-tree ,tree))
"text"))))))
(ert-deftest test-org-export/resolve-id-link ()
"Test `org-export-resolve-id-link' specifications."
;; 1. Regular test for custom-id link.
(org-test-with-parsed-data "* Headline1
:PROPERTIES:
:CUSTOM-ID: test
:END:
* Headline 2
\[[#test]]"
(should
(org-export-resolve-id-link
(org-element-map tree 'link 'identity info t) info)))
;; 2. Failing test for custom-id link.
(org-test-with-parsed-data "* Headline1
:PROPERTIES:
:CUSTOM-ID: test
:END:
* Headline 2
\[[#no-match]]"
(should-not
(org-export-resolve-id-link
(org-element-map tree 'link 'identity info t) info)))
;; 3. Test for internal id target.
(org-test-with-parsed-data "* Headline1
:PROPERTIES:
:ID: aaaa
:END:
* Headline 2
\[[id:aaaa]]"
(should
(org-export-resolve-id-link
(org-element-map tree 'link 'identity info t) info)))
;; 4. Test for external id target.
(org-test-with-parsed-data "[[id:aaaa]]"
(should
(org-export-resolve-id-link
(org-element-map tree 'link 'identity info t)
(org-combine-plists info '(:id-alist (("aaaa" . "external-file"))))))))
(ert-deftest test-org-export/resolve-radio-link ()
"Test `org-export-resolve-radio-link' specifications."