emacs-orgmode
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[O] Add :target option for the TOC keyword


From: Sacha Chua
Subject: [O] Add :target option for the TOC keyword
Date: Wed, 15 May 2019 15:00:00 -0400
User-agent: Gnus/5.13 (Gnus v5.13) Emacs/27.0.50 (gnu/linux)

Hi! Here's a patch to allow table of contents to target a specified
headline. I've added tests for HTML, ASCII, Markdown, and ODT export.
I should have copyright assignment papers already on file.

Does it work for anyone other than me? =)

>From 81035b85a10ec62d7a8ddc594349189e97346960 Mon Sep 17 00:00:00 2001
From: Sacha Chua <address@hidden>
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 "\\<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
+                 ;; 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 "\\<local\\>" value) keyword))))
+            (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 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 "\\<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
+                 ;; 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 "\\<local\\>" 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 "\\<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
+                 ;; 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 "\\<local\\>" 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 "\\<local\\>" 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 "\\<local\\>" 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 <sacha at sachachua dot com>
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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.*\\)*</div>" 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.*\\)*</div>" 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 <sacha at sachachua dot com>
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+(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 <sacha at sachachua dot com>
+
+;; 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 <http://www.gnu.org/licenses/>.
+
+(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 "<text:index-body>.*\\(\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 "<text:index-body>.*\\(\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


reply via email to

[Prev in Thread] Current Thread [Next in Thread]