Procházet zdrojové kódy

Add :target option for the TOC keyword

* doc/org-manual.org, etc/ORG_NEWS: Document :target option
  for the TOC keyword.

* lisp/ox.el (org-export-resolve-link): New function.

* lisp/ox-ascii.el (org-ascii-keyword): Added :target to the TOC
  keyword.
  (org-ascii--build-toc): Changed LOCAL argument to SCOPE.

* lisp/ox-html.el (org-html-keyword): Added :target to the TOC keyword.

* lisp/ox-md.el (org-md-keyword): Added :target to the TOC keyword.
  (org-md--build-toc): Changed LOCAL argument to SCOPE.

* lisp/ox-odt.el (org-odt-keyword): Added :target to the TOC keyword.

* testing/lisp/test-ox.el (test-org-export/collect-headlines): Added
  tests for specifying scope by CUSTOM_ID or by fuzzy matching.
  (test-org-export/resolve-link): New test.
Sacha Chua před 5 roky
rodič
revize
a41e9950ae
8 změnil soubory, kde provedl 182 přidání a 18 odebrání
  1. 16 0
      doc/org-manual.org
  2. 16 0
      etc/ORG-NEWS
  3. 12 7
      lisp/ox-ascii.el
  4. 7 2
      lisp/ox-html.el
  5. 12 7
      lisp/ox-md.el
  6. 7 2
      lisp/ox-odt.el
  7. 28 0
      lisp/ox.el
  8. 84 0
      testing/lisp/test-ox.el

+ 16 - 0
doc/org-manual.org

@@ -11551,6 +11551,22 @@ file requires the inclusion of the titletoc package.  Because of
 compatibility issues, titletoc has to be loaded /before/ hyperref.
 compatibility issues, titletoc has to be loaded /before/ hyperref.
 Customize the ~org-latex-default-packages-alist~ variable.
 Customize the ~org-latex-default-packages-alist~ variable.
 
 
+The following example inserts a table of contents that links to the
+children of the specified target.
+
+#+begin_example
+,* Target
+  :PROPERTIES:
+  :CUSTOM_ID: TargetSection
+  :END:
+,** Heading A
+,** Heading B
+,* Another section
+,#+TOC: headlines 1 :target #TargetSection
+#+end_example
+
+The =:target= attribute is supported in HTML, Markdown, ODT, and ASCII export.
+
 Use the =TOC= keyword to generate list of tables---respectively, all
 Use the =TOC= keyword to generate list of tables---respectively, all
 listings---with captions.
 listings---with captions.
 
 

+ 16 - 0
etc/ORG-NEWS

@@ -212,6 +212,22 @@ This attribute overrides the =:width= and =:height= attributes.
 [[https://orgmode.org/img/org-mode-unicorn-logo.png]]
 [[https://orgmode.org/img/org-mode-unicorn-logo.png]]
 #+end_example
 #+end_example
 
 
+*** Allow specifying the target for a table of contents
+
+The =+TOC= keyword now accepts a =:target:= attribute that specifies
+the headline to use for making the table of contents.
+
+#+begin_example
+,* Target
+  :PROPERTIES:
+  :CUSTOM_ID: TargetSection
+  :END:
+,** Heading A
+,** Heading B
+,* Another section
+,#+TOC: headlines 1 :target "#TargetSection"
+#+end_example
+
 ** New functions
 ** New functions
 *** ~org-dynamic-block-insert-dblock~
 *** ~org-dynamic-block-insert-dblock~
 
 

+ 12 - 7
lisp/ox-ascii.el

@@ -731,7 +731,7 @@ caption keyword."
 		 (org-export-data caption info))
 		 (org-export-data caption info))
 	 (org-ascii--current-text-width element info) info)))))
 	 (org-ascii--current-text-width element info) info)))))
 
 
-(defun org-ascii--build-toc (info &optional n keyword local)
+(defun org-ascii--build-toc (info &optional n keyword scope)
   "Return a table of contents.
   "Return a table of contents.
 
 
 INFO is a plist used as a communication channel.
 INFO is a plist used as a communication channel.
@@ -742,10 +742,10 @@ depth of the table.
 Optional argument KEYWORD specifies the TOC keyword, if any, from
 Optional argument KEYWORD specifies the TOC keyword, if any, from
 which the table of contents generation has been initiated.
 which the table of contents generation has been initiated.
 
 
-When optional argument LOCAL is non-nil, build a table of
-contents according to the current headline."
+When optional argument SCOPE is non-nil, build a table of
+contents according to the specified scope."
   (concat
   (concat
-   (unless local
+   (unless scope
      (let ((title (org-ascii--translate "Table of Contents" info)))
      (let ((title (org-ascii--translate "Table of Contents" info)))
        (concat title "\n"
        (concat title "\n"
 	       (make-string
 	       (make-string
@@ -767,7 +767,7 @@ contents according to the current headline."
 	    (or (not (plist-get info :with-tags))
 	    (or (not (plist-get info :with-tags))
 		(eq (plist-get info :with-tags) 'not-in-toc))
 		(eq (plist-get info :with-tags) 'not-in-toc))
 	    'toc))))
 	    'toc))))
-      (org-export-collect-headlines info n (and local keyword)) "\n"))))
+      (org-export-collect-headlines info n scope) "\n"))))
 
 
 (defun org-ascii--list-listings (keyword info)
 (defun org-ascii--list-listings (keyword info)
   "Return a list of listings.
   "Return a list of listings.
@@ -1516,8 +1516,13 @@ information."
 	  ((string-match-p "\\<headlines\\>" value)
 	  ((string-match-p "\\<headlines\\>" value)
 	   (let ((depth (and (string-match "\\<[0-9]+\\>" value)
 	   (let ((depth (and (string-match "\\<[0-9]+\\>" value)
 			     (string-to-number (match-string 0 value))))
 			     (string-to-number (match-string 0 value))))
-		 (localp (string-match-p "\\<local\\>" value)))
-	     (org-ascii--build-toc info depth keyword localp)))
+		 (scope
+		  (cond
+		   ((string-match ":target +\\(\".+?\"\\|\\S-+\\)" value) ;link
+		    (org-export-resolve-link
+		     (org-strip-quotes (match-string 1 value)) info))
+		   ((string-match-p "\\<local\\>" value) keyword)))) ;local
+	     (org-ascii--build-toc info depth keyword scope)))
 	  ((string-match-p "\\<tables\\>" value)
 	  ((string-match-p "\\<tables\\>" value)
 	   (org-ascii--list-tables keyword info))
 	   (org-ascii--list-tables keyword info))
 	  ((string-match-p "\\<listings\\>" value)
 	  ((string-match-p "\\<listings\\>" value)

+ 7 - 2
lisp/ox-html.el

@@ -2813,8 +2813,13 @@ CONTENTS is nil.  INFO is a plist holding contextual information."
 	 ((string-match "\\<headlines\\>" value)
 	 ((string-match "\\<headlines\\>" value)
 	  (let ((depth (and (string-match "\\<[0-9]+\\>" value)
 	  (let ((depth (and (string-match "\\<[0-9]+\\>" value)
 			    (string-to-number (match-string 0 value))))
 			    (string-to-number (match-string 0 value))))
-		(localp (string-match-p "\\<local\\>" value)))
-	    (org-html-toc depth info (and localp keyword))))
+		(scope
+		 (cond
+		  ((string-match ":target +\\(\".+?\"\\|\\S-+\\)" value) ;link
+		   (org-export-resolve-link
+		    (org-strip-quotes (match-string 1 value)) info))
+		  ((string-match-p "\\<local\\>" value) keyword)))) ;local
+	    (org-html-toc depth info scope)))
 	 ((string= "listings" value) (org-html-list-of-listings info))
 	 ((string= "listings" value) (org-html-list-of-listings info))
 	 ((string= "tables" value) (org-html-list-of-tables info))))))))
 	 ((string= "tables" value) (org-html-list-of-tables info))))))))
 
 

+ 12 - 7
lisp/ox-md.el

@@ -363,9 +363,14 @@ channel."
 	((string-match-p "\\<headlines\\>" value)
 	((string-match-p "\\<headlines\\>" value)
 	 (let ((depth (and (string-match "\\<[0-9]+\\>" value)
 	 (let ((depth (and (string-match "\\<[0-9]+\\>" value)
 			   (string-to-number (match-string 0 value))))
 			   (string-to-number (match-string 0 value))))
-	       (local? (string-match-p "\\<local\\>" value)))
+	       (scope
+		(cond
+		 ((string-match ":target +\\(\".+?\"\\|\\S-+\\)" value) ;link
+		  (org-export-resolve-link
+		   (org-strip-quotes (match-string 1 value)) info))
+		 ((string-match-p "\\<local\\>" value) keyword)))) ;local
 	   (org-remove-indentation
 	   (org-remove-indentation
-	    (org-md--build-toc info depth keyword local?)))))))
+	    (org-md--build-toc info depth keyword scope)))))))
     (_ (org-export-with-backend 'html keyword contents info))))
     (_ (org-export-with-backend 'html keyword contents info))))
 
 
 
 
@@ -550,7 +555,7 @@ a communication channel."
 
 
 ;;;; Template
 ;;;; Template
 
 
-(defun org-md--build-toc (info &optional n keyword local)
+(defun org-md--build-toc (info &optional n keyword scope)
   "Return a table of contents.
   "Return a table of contents.
 
 
 INFO is a plist used as a communication channel.
 INFO is a plist used as a communication channel.
@@ -561,10 +566,10 @@ depth of the table.
 Optional argument KEYWORD specifies the TOC keyword, if any, from
 Optional argument KEYWORD specifies the TOC keyword, if any, from
 which the table of contents generation has been initiated.
 which the table of contents generation has been initiated.
 
 
-When optional argument LOCAL is non-nil, build a table of
-contents according to the current headline."
+When optional argument SCOPE is non-nil, build a table of
+contents according to the specified element."
   (concat
   (concat
-   (unless local
+   (unless scope
      (let ((style (plist-get info :md-headline-style))
      (let ((style (plist-get info :md-headline-style))
 	   (title (org-html--translate "Table of Contents" info)))
 	   (title (org-html--translate "Table of Contents" info)))
        (org-md--headline-title style 1 title nil)))
        (org-md--headline-title style 1 title nil)))
@@ -594,7 +599,7 @@ contents according to the current headline."
 			(org-make-tag-string
 			(org-make-tag-string
 			 (org-export-get-tags headline info)))))
 			 (org-export-get-tags headline info)))))
 	(concat indentation bullet title tags)))
 	(concat indentation bullet title tags)))
-    (org-export-collect-headlines info n (and local keyword)) "\n")
+    (org-export-collect-headlines info n scope) "\n")
    "\n"))
    "\n"))
 
 
 (defun org-md--footnote-formatted (footnote info)
 (defun org-md--footnote-formatted (footnote info)

+ 7 - 2
lisp/ox-odt.el

@@ -1991,8 +1991,13 @@ information."
 	  (let ((depth (or (and (string-match "\\<[0-9]+\\>" value)
 	  (let ((depth (or (and (string-match "\\<[0-9]+\\>" value)
 				(string-to-number (match-string 0 value)))
 				(string-to-number (match-string 0 value)))
 			   (plist-get info :headline-levels)))
 			   (plist-get info :headline-levels)))
-		(localp (string-match-p "\\<local\\>" value)))
-	    (org-odt-toc depth info (and localp keyword))))
+		(scope
+		 (cond
+		  ((string-match ":target +\\(\".+?\"\\|\\S-+\\)" value) ;link
+		   (org-export-resolve-link
+		    (org-strip-quotes (match-string 1 value)) info))
+		  ((string-match-p "\\<local\\>" value) keyword)))) ;local
+	    (org-odt-toc depth info scope)))
 	 ((string-match-p "tables\\|figures\\|listings" value)
 	 ((string-match-p "tables\\|figures\\|listings" value)
 	  ;; FIXME
 	  ;; FIXME
 	  (ignore))))))))
 	  (ignore))))))))

+ 28 - 0
lisp/ox.el

@@ -4171,6 +4171,9 @@ meant to be translated with `org-export-data' or alike."
 ;; specified id or custom-id in parse tree, the path to the external
 ;; specified id or custom-id in parse tree, the path to the external
 ;; file with the id.
 ;; file with the id.
 ;;
 ;;
+;; `org-export-resolve-link' searches for the destination of a link
+;; within the parsed tree and returns the element.
+;;
 ;; `org-export-resolve-coderef' associates a reference to a line
 ;; `org-export-resolve-coderef' associates a reference to a line
 ;; number in the element it belongs, or returns the reference itself
 ;; number in the element it belongs, or returns the reference itself
 ;; when the element isn't numbered.
 ;; when the element isn't numbered.
@@ -4457,6 +4460,31 @@ has type \"radio\"."
 	     radio))
 	     radio))
       info 'first-match)))
       info 'first-match)))
 
 
+(defun org-export-resolve-link (link info)
+  "Return LINK destination.
+
+LINK is a string or a link object.
+
+INFO is a plist holding contextual information.
+
+Return value can be an object or an element:
+
+- If LINK path matches an ID or a custom ID, return the headline.
+
+- If LINK path matches a fuzzy link, return its destination.
+
+- Otherwise, throw an error."
+  ;; Convert string links to link objects.
+  (when (stringp link)
+    (setq link (with-temp-buffer
+		 (save-excursion
+		   (insert (org-make-link-string link)))
+		 (org-element-link-parser))))
+  (pcase (org-element-property :type link)
+    ((or "custom-id" "id") (org-export-resolve-id-link link info))
+    ("fuzzy" (org-export-resolve-fuzzy-link link info))
+    (_ (signal 'org-link-broken (list (org-element-property :path link))))))
+
 (defun org-export-file-uri (filename)
 (defun org-export-file-uri (filename)
   "Return file URI associated to FILENAME."
   "Return file URI associated to FILENAME."
   (cond ((string-prefix-p "//" filename) (concat "file:" filename))
   (cond ((string-prefix-p "//" filename) (concat "file:" filename))

+ 84 - 0
testing/lisp/test-ox.el

@@ -3197,6 +3197,40 @@ Paragraph[fn:1][fn:2][fn:lbl3:C<<target>>][[test]][[target]]
        (lambda (link) (org-export-resolve-fuzzy-link link info))
        (lambda (link) (org-export-resolve-fuzzy-link link info))
        info t))))
        info t))))
 
 
+(ert-deftest test-org-export/resolve-link ()
+  "Test `org-export-resolve-link' specifications."
+  (should
+   ;; Match ID links
+   (equal
+    "Headline1"
+    (org-test-with-parsed-data "* Headline1
+:PROPERTIES:
+:ID: aaaa
+:END:
+* Headline2"
+      (org-element-property
+       :raw-value (org-export-resolve-link "#aaaa" info)))))
+   ;; Match Custom ID links
+  (should
+   (equal
+    "Headline1"
+    (org-test-with-parsed-data
+	"* Headline1
+:PROPERTIES:
+:CUSTOM_ID: test
+:END:
+* Headline2"
+      (org-element-property
+       :raw-value (org-export-resolve-link "#test" info)))))
+  ;; Match fuzzy links
+  (should
+   (equal
+    "B"
+    (org-test-with-parsed-data
+	"* A\n* B\n* C"
+      (org-element-property
+       :raw-value (org-export-resolve-link "B" info))))))
+
 (defun test-org-gen-loc-list(text type)
 (defun test-org-gen-loc-list(text type)
   (org-test-with-parsed-data text
   (org-test-with-parsed-data text
     (org-element-map tree type
     (org-element-map tree type
@@ -4610,6 +4644,56 @@ Another text. (ref:text)
 	    (let ((scope (org-element-map tree 'headline #'identity info t)))
 	    (let ((scope (org-element-map tree 'headline #'identity info t)))
 	      (mapcar (lambda (h) (org-element-property :raw-value h))
 	      (mapcar (lambda (h) (org-element-property :raw-value h))
 		      (org-export-collect-headlines info nil scope))))))
 		      (org-export-collect-headlines info nil scope))))))
+  ;; Collect headlines from a scope specified by a fuzzy match
+  (should
+   (equal '("H3" "H4")
+	  (org-test-with-parsed-data "* HA
+** H1
+** H2
+* Target
+  :PROPERTIES:
+  :CUSTOM_ID: TargetSection
+  :END:
+** H3
+** H4
+* HB
+** H5
+"
+	    (mapcar
+	     (lambda (h) (org-element-property :raw-value h))
+	     (org-export-collect-headlines
+	      info
+	      nil
+	      (org-export-resolve-fuzzy-link
+	       (with-temp-buffer
+		 (save-excursion (insert "[[Target]]"))
+		 (org-element-link-parser))
+	       info))))))
+  ;; Collect headlines from a scope specified by CUSTOM_ID
+  (should
+   (equal '("H3" "H4")
+	  (org-test-with-parsed-data "* Not this section
+** H1
+** H2
+* Target
+  :PROPERTIES:
+  :CUSTOM_ID: TargetSection
+  :END:
+** H3
+** H4
+* Another
+** H5
+"
+	    (mapcar
+	     (lambda (h) (org-element-property :raw-value h))
+	     (org-export-collect-headlines
+	      info
+	      nil
+	      (org-export-resolve-id-link
+	       (with-temp-buffer
+		 (save-excursion (insert "[[#TargetSection]]"))
+		 (org-element-link-parser))
+	       info))))))
   ;; When collecting locally, optional level is relative.
   ;; When collecting locally, optional level is relative.
   (should
   (should
    (equal '("H2")
    (equal '("H2")