瀏覽代碼

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.
Nicolas Goaziou 13 年之前
父節點
當前提交
ad235400a6
共有 3 個文件被更改,包括 194 次插入187 次删除
  1. 4 0
      contrib/lisp/org-e-latex.el
  2. 52 54
      contrib/lisp/org-export.el
  3. 138 133
      testing/lisp/test-org-export.el

+ 4 - 0
contrib/lisp/org-e-latex.el

@@ -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

+ 52 - 54
contrib/lisp/org-export.el

@@ -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.

+ 138 - 133
testing/lisp/test-org-export.el

@@ -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."