From 81035b85a10ec62d7a8ddc594349189e97346960 Mon Sep 17 00:00:00 2001 From: Sacha Chua Date: Wed, 15 May 2019 14:22:05 -0400 Subject: [PATCH 1/1] Add :target option for the TOC keyword * doc/org-manual.org, etc/ORG_NEWS: Document :target option for the TOC keyword. * 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/examples/toc-with-fuzzy-target.org: New example file for testing target headlines by fuzzy matching. * testing/examples/toc-with-target.org: New example file for testing target headlines by CUSTOM_ID. * testing/lisp/test-ox-html.el: New file. * testing/lisp/test-ox-md.el: New file. * testing/lisp/test-ox-odt.el: New file. * testing/lisp/test-ox.el (test-org-export/collect-headlines): Added tests for specifying scope by CUSTOM_ID or by fuzzy matching. * testing/org-test.el (org-test-toc-with-target-file): New. (org-test-toc-with-fuzzy-target-file): New. --- doc/org-manual.org | 16 +++++++ etc/ORG-NEWS | 16 +++++++ lisp/ox-ascii.el | 27 +++++++++--- lisp/ox-html.el | 17 +++++++- lisp/ox-md.el | 27 +++++++++--- lisp/ox-odt.el | 17 +++++++- testing/examples/toc-with-fuzzy-target.org | 12 +++++ testing/examples/toc-with-target.org | 12 +++++ testing/lisp/test-ox-html.el | 51 ++++++++++++++++++++++ testing/lisp/test-ox-md.el | 51 ++++++++++++++++++++++ testing/lisp/test-ox-odt.el | 51 ++++++++++++++++++++++ testing/lisp/test-ox.el | 50 +++++++++++++++++++++ testing/org-test.el | 6 +++ 13 files changed, 335 insertions(+), 18 deletions(-) create mode 100644 testing/examples/toc-with-fuzzy-target.org create mode 100644 testing/examples/toc-with-target.org create mode 100644 testing/lisp/test-ox-html.el create mode 100644 testing/lisp/test-ox-md.el create mode 100644 testing/lisp/test-ox-odt.el 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..969f632b0 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,21 @@ 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) + (let ((link (with-temp-buffer + (save-excursion + (insert (org-make-link-string (match-string 1 value)))) + (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)) + (_ nil)))) + ;; 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..e7059fd35 100644 --- a/lisp/ox-html.el +++ b/lisp/ox-html.el @@ -2811,8 +2811,21 @@ 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) + (let ((link (with-temp-buffer + (save-excursion + (insert (org-make-link-string (match-string 1 value)))) + (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)) + (_ nil)))) + ;; 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..b7addfd54 100644 --- a/lisp/ox-md.el +++ b/lisp/ox-md.el @@ -363,9 +363,22 @@ 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) + (let ((link (with-temp-buffer + (save-excursion + (insert (org-make-link-string (match-string 1 value)))) + (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)) + (_ nil)))) + ;; 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 +563,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 +574,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 +607,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..5711bbd49 100644 --- a/lisp/ox-odt.el +++ b/lisp/ox-odt.el @@ -1991,8 +1991,21 @@ 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) + (let ((link (with-temp-buffer + (save-excursion + (insert (org-make-link-string (match-string 1 value)))) + (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)) + (_ nil)))) + ;; local + ((string-match-p "\\" value) keyword)))) + (org-odt-toc depth info scope))) ((string-match-p "tables\\|figures\\|listings" value) ;; FIXME (ignore)))))))) diff --git a/testing/examples/toc-with-fuzzy-target.org b/testing/examples/toc-with-fuzzy-target.org new file mode 100644 index 000000000..1e0731c2a --- /dev/null +++ b/testing/examples/toc-with-fuzzy-target.org @@ -0,0 +1,12 @@ +#+OPTIONS: toc:nil +* Not this section +** Heading X +** Heading Y +* Target + :PROPERTIES: + :CUSTOM_ID: TargetSection + :END: +** Heading A +** Heading B +* Another section +#+TOC: headlines 1 :target "Target" diff --git a/testing/examples/toc-with-target.org b/testing/examples/toc-with-target.org new file mode 100644 index 000000000..5f3fde4c6 --- /dev/null +++ b/testing/examples/toc-with-target.org @@ -0,0 +1,12 @@ +#+OPTIONS: toc:nil +* Not this section +** Heading X +** Heading Y +* Target + :PROPERTIES: + :CUSTOM_ID: TargetSection + :END: +** Heading A +** Heading B +* Another section +#+TOC: headlines 1 :target "#TargetSection" diff --git a/testing/lisp/test-ox-html.el b/testing/lisp/test-ox-html.el new file mode 100644 index 000000000..61c3ea870 --- /dev/null +++ b/testing/lisp/test-ox-html.el @@ -0,0 +1,51 @@ +;;; test-ox-html.el --- Tests for ox-html.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2019 Sacha Chua + +;; Author: Sacha Chua + +;; This file is not part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Code: +(unless (featurep 'ox) + (signal 'missing-test-dependency "org-export")) + +(unless (featurep 'ox-html) + (signal 'missing-test-dependency "ox-html")) + +;;; Table of contents + +(ert-deftest test-org-html/keyword () + "Test `org-html-keyword' specifications." + (org-test-in-example-file org-test-toc-with-target-file + (let* ((body (org-export-as (org-export-get-backend 'html) nil nil t)) + (toc + (and (string-match "text-table-of-contents.*\\(\n.*\\)*" body) + (match-string 0 body)))) + (should (string-match "Heading A" toc)) + (should (string-match "Heading B" toc)) + (should-not (string-match "Heading X" toc)))) + (org-test-in-example-file org-test-toc-with-fuzzy-target-file + (let* ((body (org-export-as (org-export-get-backend 'html) nil nil t)) + (toc + (and (string-match "text-table-of-contents.*\\(\n.*\\)*" body) + (match-string 0 body)))) + (should (string-match "Heading A" toc)) + (should (string-match "Heading B" toc)) + (should-not (string-match "Heading X" toc))))) + +(provide 'test-ox-html) +;;; test-ox-html.el ends here diff --git a/testing/lisp/test-ox-md.el b/testing/lisp/test-ox-md.el new file mode 100644 index 000000000..7ed65b21b --- /dev/null +++ b/testing/lisp/test-ox-md.el @@ -0,0 +1,51 @@ +;;; test-ox-md.el --- Tests for ox-md.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2019 Sacha Chua + +;; Author: Sacha Chua + +;; This file is not part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +(unless (featurep 'ox) + (signal 'missing-test-dependency "org-export")) +(unless (featurep 'ox-md) + (signal 'missing-test-dependency "ox-md")) + +;;; Code: +;;; Table of contents +(ert-deftest test-org-md/keyword () + "Test `org-md-keyword' specifications." + (org-test-in-example-file org-test-toc-with-target-file + (let* ((body (org-export-as (org-export-get-backend 'md) nil nil t)) + (toc + (and (string-match "# Another section.*\\(\n.*\\)*" body) + (match-string 0 body)))) + (should (string-match "\\[Heading A" toc)) + (should (string-match "\\[Heading B" toc)) + (should-not (string-match "\\[Heading X" toc)))) + (org-test-in-example-file org-test-toc-with-fuzzy-target-file + (let* ((body (org-export-as (org-export-get-backend 'md) nil nil t)) + (toc + (and (string-match "# Another section.*\\(\n.*\\)*" body) + (match-string 0 body)))) + (should (string-match "\\[Heading A" toc)) + (should (string-match "\\[Heading B" toc)) + (should-not (string-match "\\[Heading X" toc))))) + +(provide 'test-ox-md) + +;;; test-ox-md.el ends here + diff --git a/testing/lisp/test-ox-odt.el b/testing/lisp/test-ox-odt.el new file mode 100644 index 000000000..15788b0ae --- /dev/null +++ b/testing/lisp/test-ox-odt.el @@ -0,0 +1,51 @@ +;;; test-ox-odt.el --- Tests for ox-md.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2019 Sacha Chua + +;; Author: Sacha Chua + +;; This file is not part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +(unless (featurep 'ox) + (signal 'missing-test-dependency "org-export")) +(unless (featurep 'ox-odt) + (signal 'missing-test-dependency "ox-odt")) + +;;; Code: +;;; Table of contents +(ert-deftest test-org-odt/keyword () + "Test `org-odt-keyword' specifications." + (org-test-in-example-file org-test-toc-with-target-file + (let* ((body (org-export-as (org-export-get-backend 'odt) nil nil t)) + (toc + (and (string-match ".*\\(\n.*\\)*" body) + (match-string 0 body)))) + (should (string-match "Heading A" toc)) + (should (string-match "Heading B" toc)) + (should-not (string-match "Heading X" toc)))) + (org-test-in-example-file org-test-toc-with-fuzzy-target-file + (let* ((body (org-export-as (org-export-get-backend 'odt) nil nil t)) + (toc + (and (string-match ".*\\(\n.*\\)*" body) + (match-string 0 body)))) + (should (string-match "Heading A" toc)) + (should (string-match "Heading B" toc)) + (should-not (string-match "Heading X" toc))))) + +(provide 'test-ox-odt) + +;;; test-ox-odt.el ends here + diff --git a/testing/lisp/test-ox.el b/testing/lisp/test-ox.el index 43637b926..36740364f 100644 --- a/testing/lisp/test-ox.el +++ b/testing/lisp/test-ox.el @@ -4615,6 +4615,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") diff --git a/testing/org-test.el b/testing/org-test.el index 39c346410..4e8794348 100644 --- a/testing/org-test.el +++ b/testing/org-test.el @@ -90,6 +90,12 @@ org-test searches this directory up the directory tree.") (defconst org-test-link-in-heading-file (expand-file-name "link-in-heading.org" org-test-dir)) +(defconst org-test-toc-with-target-file + (expand-file-name "toc-with-target.org" org-test-example-dir)) + +(defconst org-test-toc-with-fuzzy-target-file + (expand-file-name "toc-with-fuzzy-target.org" org-test-example-dir)) + (defconst org-id-locations-file (expand-file-name ".test-org-id-locations" org-test-dir)) -- 2.17.1