Browse Source

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 1 month ago
parent
commit
a41e9950ae
8 changed files with 182 additions and 18 deletions
  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.
 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
 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]]
 #+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
 *** ~org-dynamic-block-insert-dblock~
 

+ 12 - 7
lisp/ox-ascii.el

@@ -731,7 +731,7 @@ caption keyword."
 		 (org-export-data caption 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.
 
 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
 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
-   (unless local
+   (unless scope
      (let ((title (org-ascii--translate "Table of Contents" info)))
        (concat title "\n"
 	       (make-string
@@ -767,7 +767,7 @@ contents according to the current headline."
 	    (or (not (plist-get info :with-tags))
 		(eq (plist-get info :with-tags) 'not-in-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)
   "Return a list of listings.
@@ -1516,8 +1516,13 @@ information."
 	  ((string-match-p "\\<headlines\\>" value)
 	   (let ((depth (and (string-match "\\<[0-9]+\\>" 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)
 	   (org-ascii--list-tables keyword info))
 	  ((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)
 	  (let ((depth (and (string-match "\\<[0-9]+\\>" 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= "tables" value) (org-html-list-of-tables info))))))))
 

+ 12 - 7
lisp/ox-md.el

@@ -363,9 +363,14 @@ channel."
 	((string-match-p "\\<headlines\\>" value)
 	 (let ((depth (and (string-match "\\<[0-9]+\\>" 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-md--build-toc info depth keyword local?)))))))
+	    (org-md--build-toc info depth keyword scope)))))))
     (_ (org-export-with-backend 'html keyword contents info))))
 
 
@@ -550,7 +555,7 @@ a communication channel."
 
 ;;;; 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.
 
 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
 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
-   (unless local
+   (unless scope
      (let ((style (plist-get info :md-headline-style))
 	   (title (org-html--translate "Table of Contents" info)))
        (org-md--headline-title style 1 title nil)))
@@ -594,7 +599,7 @@ contents according to the current headline."
 			(org-make-tag-string
 			 (org-export-get-tags headline info)))))
 	(concat indentation bullet title tags)))
-    (org-export-collect-headlines info n (and local keyword)) "\n")
+    (org-export-collect-headlines info n scope) "\n")
    "\n"))
 
 (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)
 				(string-to-number (match-string 0 value)))
 			   (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)
 	  ;; FIXME
 	  (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
 ;; 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
 ;; number in the element it belongs, or returns the reference itself
 ;; when the element isn't numbered.
@@ -4457,6 +4460,31 @@ has type \"radio\"."
 	     radio))
       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)
   "Return file URI associated to 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))
        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)
   (org-test-with-parsed-data text
     (org-element-map tree type
@@ -4610,6 +4644,56 @@ Another text. (ref:text)
 	    (let ((scope (org-element-map tree 'headline #'identity info t)))
 	      (mapcar (lambda (h) (org-element-property :raw-value h))
 		      (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.
   (should
    (equal '("H2")