>From be360242b00a079afb110b019c6dd9ff6ebc36c3 Mon Sep 17 00:00:00 2001 From: Sacha Chua Date: Wed, 15 May 2019 14:22:05 -0400 Subject: [PATCH] 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. --- doc/org-manual.org | 16 ++++++++ etc/ORG-NEWS | 16 ++++++++ lisp/ox-ascii.el | 20 ++++++---- lisp/ox-html.el | 10 ++++- lisp/ox-md.el | 20 ++++++---- lisp/ox-odt.el | 10 ++++- lisp/ox.el | 28 ++++++++++++++ testing/lisp/test-ox.el | 84 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 186 insertions(+), 18 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index 54b89e5bf..9f3fae308 100644 --- a/doc/org-manual.org +++ b/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. diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 541559e64..95358ca7b 100644 --- a/etc/ORG-NEWS +++ b/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~ diff --git a/lisp/ox-ascii.el b/lisp/ox-ascii.el index 7917e3dad..20327169b 100644 --- a/lisp/ox-ascii.el +++ b/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,14 @@ information." ((string-match-p "\\" value) (let ((depth (and (string-match "\\<[0-9]+\\>" value) (string-to-number (match-string 0 value)))) - (localp (string-match-p "\\" value))) - (org-ascii--build-toc info depth keyword localp))) + (scope + (cond + ;; link + ((string-match ":target +\"\\([^\"]+\\)\"" value) + (org-export-resolve-link (match-string 1 value) info)) + ;; local + ((string-match-p "\\" value) keyword)))) + (org-ascii--build-toc info depth keyword scope))) ((string-match-p "\\" value) (org-ascii--list-tables keyword info)) ((string-match-p "\\" value) diff --git a/lisp/ox-html.el b/lisp/ox-html.el index e4bd28050..86ee07b8d 100644 --- a/lisp/ox-html.el +++ b/lisp/ox-html.el @@ -2811,8 +2811,14 @@ CONTENTS is nil. INFO is a plist holding contextual information." ((string-match "\\" value) (let ((depth (and (string-match "\\<[0-9]+\\>" value) (string-to-number (match-string 0 value)))) - (localp (string-match-p "\\" value))) - (org-html-toc depth info (and localp keyword)))) + (scope + (cond + ;; link + ((string-match ":target +\"\\([^\"]+\\)\"" value) + (org-export-resolve-link (match-string 1 value) info)) + ;; local + ((string-match-p "\\" value) keyword)))) + (org-html-toc depth info scope))) ((string= "listings" value) (org-html-list-of-listings info)) ((string= "tables" value) (org-html-list-of-tables info)))))))) diff --git a/lisp/ox-md.el b/lisp/ox-md.el index d574e696e..566705ca0 100644 --- a/lisp/ox-md.el +++ b/lisp/ox-md.el @@ -363,9 +363,15 @@ channel." ((string-match-p "\\" value) (let ((depth (and (string-match "\\<[0-9]+\\>" value) (string-to-number (match-string 0 value)))) - (local? (string-match-p "\\" value))) + (scope + (cond + ;; link + ((string-match ":target +\"\\([^\"]+\\)\"" value) + (org-export-resolve-link (match-string 1 value) info)) + ;; local + ((string-match-p "\\" value) keyword)))) (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 +556,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 +567,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 +600,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) diff --git a/lisp/ox-odt.el b/lisp/ox-odt.el index 497488ef4..e630fe3a8 100644 --- a/lisp/ox-odt.el +++ b/lisp/ox-odt.el @@ -1991,8 +1991,14 @@ 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 "\\" value))) - (org-odt-toc depth info (and localp keyword)))) + (scope + (cond + ;; link + ((string-match ":target +\"\\([^\"]+\\)\"" value) + (org-export-resolve-link (match-string 1 value) info)) + ;; local + ((string-match-p "\\" value) keyword)))) + (org-odt-toc depth info scope))) ((string-match-p "tables\\|figures\\|listings" value) ;; FIXME (ignore)))))))) diff --git a/lisp/ox.el b/lisp/ox.el index f0ecd8978..e989b37c6 100644 --- a/lisp/ox.el +++ b/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 that element. + +- If LINK path matches a fuzzy link, return that element. + +- 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)) diff --git a/testing/lisp/test-ox.el b/testing/lisp/test-ox.el index 43637b926..fc5f857b2 100644 --- a/testing/lisp/test-ox.el +++ b/testing/lisp/test-ox.el @@ -3197,6 +3197,40 @@ Paragraph[fn:1][fn:2][fn:lbl3:C<>][[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 @@ -4615,6 +4649,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") -- 2.17.1