summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSacha Chua <sacha@sachachua.com>2019-05-15 14:22:05 -0400
committerNicolas Goaziou <mail@nicolasgoaziou.fr>2019-05-30 15:25:46 +0200
commita41e9950ae91c0711d93d5bf3e8ac07159876d89 (patch)
tree43b2843827b63a56568dada515b6087b887e7d15
parent99aa99426d042a5c2e9d3b6bcdc18a1b57e4acbe (diff)
downloadorg-mode-a41e9950ae91c0711d93d5bf3e8ac07159876d89.tar.gz
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.
-rw-r--r--doc/org-manual.org16
-rw-r--r--etc/ORG-NEWS16
-rw-r--r--lisp/ox-ascii.el19
-rw-r--r--lisp/ox-html.el9
-rw-r--r--lisp/ox-md.el19
-rw-r--r--lisp/ox-odt.el9
-rw-r--r--lisp/ox.el28
-rw-r--r--testing/lisp/test-ox.el84
8 files changed, 182 insertions, 18 deletions
diff --git a/doc/org-manual.org b/doc/org-manual.org
index c519d31..469e164 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 541559e..95358ca 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 7917e3d..6e6c17c 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,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)
diff --git a/lisp/ox-html.el b/lisp/ox-html.el
index 9d9de62..4e7ff5c 100644
--- a/lisp/ox-html.el
+++ b/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))))))))
diff --git a/lisp/ox-md.el b/lisp/ox-md.el
index d574e69..089e77b 100644
--- a/lisp/ox-md.el
+++ b/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)
diff --git a/lisp/ox-odt.el b/lisp/ox-odt.el
index 497488e..3251736 100644
--- a/lisp/ox-odt.el
+++ b/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))))))))
diff --git a/lisp/ox.el b/lisp/ox.el
index 3490774..87daaff 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 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))
diff --git a/testing/lisp/test-ox.el b/testing/lisp/test-ox.el
index 9d2cf80..2c2778f 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<<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")