From b07e2f6ff1feddde83506b7fdb370bfe8e0a5337 Mon Sep 17 00:00:00 2001 From: Nicolas Goaziou Date: Sat, 25 Oct 2014 17:14:34 +0200 Subject: [PATCH] ox: Implement local table of contents * lisp/ox.el (org-export-collect-headlines): Allow to collect headlines locally. * testing/lisp/test-ox.el (test-org-export/collect-headlines): Add tests. * lisp/ox-ascii.el (org-ascii--build-toc): (org-ascii-keyword): * lisp/ox-html.el (org-html-toc): (org-html-keyword): * lisp/ox-odt.el (org-odt-toc): Allow local table of contents. (org-odt--format-toc): New function. (org-odt-begin-toc, org-odt-end-toc): Remove functions. * lisp/ox-latex.el (org-latex-logfiles-extensions): Optionally remove "ptc" files. (org-latex-headline, org-latex-keyword): Implement partial table of contents assuming "titletoc" package is loaded. * etc/ORG-NEWS: * doc/org.texi (Table of contents): Document new parameter. --- doc/org.texi | 15 +++++-- etc/ORG-NEWS | 3 ++ lisp/ox-ascii.el | 77 ++++++++++++++++++---------------- lisp/ox-html.el | 51 +++++++++++----------- lisp/ox-latex.el | 55 +++++++++++++++++------- lisp/ox-odt.el | 93 ++++++++++++++++++++++------------------- lisp/ox.el | 31 ++++++++++---- testing/lisp/test-ox.el | 14 ++++++- 8 files changed, 207 insertions(+), 132 deletions(-) diff --git a/doc/org.texi b/doc/org.texi index 4c11d0394..f353caba3 100644 --- a/doc/org.texi +++ b/doc/org.texi @@ -9700,9 +9700,18 @@ location(s). #+TOC: headlines 2 (insert TOC here, with two headline levels) @end example -Multiple @code{#+TOC: headline} lines are allowed. The same @code{TOC} -keyword can also generate a list of all tables (resp.@: all listings) with a -caption in the buffer. +Moreover, if you append @samp{local} parameter, the table contains only +entries for current section's children@footnote{For @LaTeX{} export, this +feature requires ``titletoc'' package.}. In this case, any depth parameter +becomes relative to the current level. + +@example +* Section +#+TOC: headlines 1 local (insert local TOC, with direct children only) +@end example + +The same @code{TOC} keyword can also generate a list of all tables (resp.@: +all listings) with a caption in the document. @example #+TOC: listings (build a list of listings) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 13f9dc713..339f0073b 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -180,6 +180,9 @@ property is inherited by children. It is now possible to specify a function, both programatically, through a new optional argument, and interactively with ~f~ or ~F~ keys, to sort a table. +*** Table of contents can be local to a section +The ~TOC~ keywords now accepts an optional ~local~ parameter. See +manual for details. *** Countdown timers can now be paused. ~org-timer-pause-time~ wil now pause and restart both relative and countdown timers. diff --git a/lisp/ox-ascii.el b/lisp/ox-ascii.el index 5da956ad0..e294fab6a 100644 --- a/lisp/ox-ascii.el +++ b/lisp/ox-ascii.el @@ -744,7 +744,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) +(defun org-ascii--build-toc (info &optional n keyword local) "Return a table of contents. INFO is a plist used as a communication channel. @@ -753,29 +753,34 @@ Optional argument N, when non-nil, is an integer specifying the depth of the table. Optional argument KEYWORD specifies the TOC keyword, if any, from -which the table of contents generation has been initiated." - (let ((title (org-ascii--translate "Table of Contents" info))) - (concat - title "\n" - (make-string (string-width title) - (if (eq (plist-get info :ascii-charset) 'utf-8) ?─ ?_)) - "\n\n" - (let ((text-width - (if keyword (org-ascii--current-text-width keyword info) - (- (plist-get info :ascii-text-width) - (plist-get info :ascii-global-margin))))) - (mapconcat - (lambda (headline) - (let* ((level (org-export-get-relative-level headline info)) - (indent (* (1- level) 3))) - (concat - (unless (zerop indent) (concat (make-string (1- indent) ?.) " ")) - (org-ascii--build-title - headline info (- text-width indent) nil - (or (not (plist-get info :with-tags)) - (eq (plist-get info :with-tags) 'not-in-toc)) - 'toc)))) - (org-export-collect-headlines info n) "\n"))))) +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." + (concat + (unless local + (let ((title (org-ascii--translate "Table of Contents" info))) + (concat title "\n" + (make-string + (string-width title) + (if (eq (plist-get info :ascii-charset) 'utf-8) ?─ ?_)) + "\n\n"))) + (let ((text-width + (if keyword (org-ascii--current-text-width keyword info) + (- (plist-get info :ascii-text-width) + (plist-get info :ascii-global-margin))))) + (mapconcat + (lambda (headline) + (let* ((level (org-export-get-relative-level headline info)) + (indent (* (1- level) 3))) + (concat + (unless (zerop indent) (concat (make-string (1- indent) ?.) " ")) + (org-ascii--build-title + headline info (- text-width indent) nil + (or (not (plist-get info :with-tags)) + (eq (plist-get info :with-tags) 'not-in-toc)) + 'toc)))) + (org-export-collect-headlines info n keyword) "\n")))) (defun org-ascii--list-listings (keyword info) "Return a list of listings. @@ -1459,24 +1464,22 @@ contextual information." "Transcode a KEYWORD element from Org to ASCII. CONTENTS is nil. INFO is a plist holding contextual information." - (let ((key (org-element-property :key keyword))) + (let ((key (org-element-property :key keyword)) + (value (org-element-property :value keyword))) (cond - ((string= key "ASCII") - (org-ascii--justify-element - (org-element-property :value keyword) keyword info)) + ((string= key "ASCII") (org-ascii--justify-element value keyword info)) ((string= key "TOC") (org-ascii--justify-element - (let ((value (downcase (org-element-property :value keyword)))) + (let ((case-fold-search t)) (cond - ((string-match "\\" value) - (let ((depth (or (and (string-match "[0-9]+" value) - (string-to-number (match-string 0 value))) - (plist-get info :with-toc)))) - (org-ascii--build-toc - info (and (wholenump depth) depth) keyword))) - ((string= "tables" value) + ((org-string-match-p "\\" value) + (let ((depth (and (string-match "\\<[0-9]+\\>" value) + (string-to-number (match-string 0 value)))) + (localp (org-string-match-p "\\" value))) + (org-ascii--build-toc info depth keyword localp))) + ((org-string-match-p "\\" value) (org-ascii--list-tables keyword info)) - ((string= "listings" value) + ((org-string-match-p "\\" value) (org-ascii--list-listings keyword info)))) keyword info))))) diff --git a/lisp/ox-html.el b/lisp/ox-html.el index d4ccbbe30..db8ee8be1 100644 --- a/lisp/ox-html.el +++ b/lisp/ox-html.el @@ -2026,31 +2026,34 @@ a plist used as a communication channel." ;;; Tables of Contents -(defun org-html-toc (depth info) +(defun org-html-toc (depth info &optional scope) "Build a table of contents. -DEPTH is an integer specifying the depth of the table. INFO is a -plist used as a communication channel. Return the table of -contents as a string, or nil if it is empty." +DEPTH is an integer specifying the depth of the table. INFO is +a plist used as a communication channel. Optional argument SCOPE +is an element defining the scope of the table. Return the table +of contents as a string, or nil if it is empty." (let ((toc-entries (mapcar (lambda (headline) (cons (org-html--format-toc-headline headline info) (org-export-get-relative-level headline info))) - (org-export-collect-headlines info depth))) - (outer-tag (if (and (org-html-html5-p info) - (plist-get info :html-html5-fancy)) - "nav" - "div"))) + (org-export-collect-headlines info depth scope)))) (when toc-entries - (concat (format "<%s id=\"table-of-contents\">\n" outer-tag) - (let ((top-level (plist-get info :html-toplevel-hlevel))) - (format "%s\n" - top-level - (org-html--translate "Table of Contents" info) - top-level)) - "
" - (org-html--toc-text toc-entries) - "
\n" - (format "\n" outer-tag))))) + (let ((toc (concat "
" + (org-html--toc-text toc-entries) + "
\n"))) + (if scope toc + (let ((outer-tag (if (and (org-html-html5-p info) + (plist-get info :html-html5-fancy)) + "nav" + "div"))) + (concat (format "<%s id=\"table-of-contents\">\n" outer-tag) + (let ((top-level (plist-get info :html-toplevel-hlevel))) + (format "%s\n" + top-level + (org-html--translate "Table of Contents" info) + top-level)) + toc + (format "\n" outer-tag)))))))) (defun org-html--toc-text (toc-entries) "Return innards of a table of contents, as a string. @@ -2550,13 +2553,13 @@ CONTENTS is nil. INFO is a plist holding contextual information." (cond ((string= key "HTML") value) ((string= key "TOC") - (let ((value (downcase value))) + (let ((case-fold-search t)) (cond ((string-match "\\" value) - (let ((depth (or (and (string-match "[0-9]+" value) - (string-to-number (match-string 0 value))) - (plist-get info :with-toc)))) - (org-html-toc depth info))) + (let ((depth (and (string-match "\\<[0-9]+\\>" value) + (string-to-number (match-string 0 value)))) + (localp (org-string-match-p "\\" value))) + (org-html-toc depth info (and localp keyword)))) ((string= "listings" value) (org-html-list-of-listings info)) ((string= "tables" value) (org-html-list-of-tables info)))))))) diff --git a/lisp/ox-latex.el b/lisp/ox-latex.el index 66cc6c207..d64af87d7 100644 --- a/lisp/ox-latex.el +++ b/lisp/ox-latex.el @@ -953,11 +953,13 @@ file name as its single argument." (defcustom org-latex-logfiles-extensions '("aux" "bcf" "blg" "fdb_latexmk" "fls" "figlist" "idx" "log" "nav" "out" - "run.xml" "snm" "toc" "vrb" "xdv") + "ptc" "run.xml" "snm" "toc" "vrb" "xdv") "The list of file extensions to consider as LaTeX logfiles. -The logfiles will be remove if `org-latex-remove-logfiles' is +The logfiles will be removed if `org-latex-remove-logfiles' is non-nil." :group 'org-export-latex + :version "25.1" + :package-version '(Org . "8.3") :type '(repeat (string :tag "Extension"))) (defcustom org-latex-remove-logfiles t @@ -1536,7 +1538,23 @@ holding contextual information." (org-export-get-alt-title headline info) section-back-end info) (and (eq (plist-get info :with-tags) t) tags) - info))) + info)) + ;; Maybe end local TOC (see `org-latex-keyword'). + (contents + (concat + contents + (let ((case-fold-search t) + (section + (let ((first (car (org-element-contents headline)))) + (and (eq (org-element-type first) 'section) first)))) + (org-element-map section 'keyword + (lambda (k) + (and (equal (org-element-property :key k) "TOC") + (let ((v (org-element-property :value k))) + (and (org-string-match-p "\\" v) + (org-string-match-p "\\" v) + (format "\\stopcontents[level-%d]" level))))) + info t))))) (if (and numberedp opt-title (not (equal opt-title full-text)) (string-match "\\`\\\\\\(.*?[^*]\\){" section-fmt)) @@ -1754,18 +1772,27 @@ CONTENTS is nil. INFO is a plist holding contextual information." ((string= key "LATEX") value) ((string= key "INDEX") (format "\\index{%s}" value)) ((string= key "TOC") - (let ((value (downcase value))) + (let ((case-fold-search t)) (cond - ((string-match "\\" value) - (let ((depth (or (and (string-match "[0-9]+" value) - (string-to-number (match-string 0 value))) - (plist-get info :with-toc)))) - (concat - (when (wholenump depth) - (format "\\setcounter{tocdepth}{%s}\n" depth)) - "\\tableofcontents"))) - ((string= "tables" value) "\\listoftables") - ((string= "listings" value) + ((org-string-match-p "\\" value) + (let* ((localp (org-string-match-p "\\" value)) + (parent (org-element-lineage keyword '(headline))) + (level (if (not (and localp parent)) 0 + (org-export-get-relative-level parent info))) + (depth + (and (string-match "\\<[0-9]+\\>" value) + (format + "\\setcounter{tocdepth}{%d}" + (+ (string-to-number (match-string 0 value)) level))))) + (if (and localp parent) + ;; Start local TOC, assuming package "titletoc" is + ;; required. + (format "\\startcontents[level-%d] +\\printcontents[level-%d]{}{0}{%s}" + level level (or depth "")) + (concat depth (and depth "\n") "\\tableofcontents")))) + ((org-string-match-p "\\" value) "\\listoftables") + ((org-string-match-p "\\" value) (case (plist-get info :latex-listings) ((nil) "\\listoffigures") (minted "\\listoflistings") diff --git a/lisp/ox-odt.el b/lisp/ox-odt.el index e96bc1d64..aff9edd88 100644 --- a/lisp/ox-odt.el +++ b/lisp/ox-odt.el @@ -1080,13 +1080,20 @@ See `org-odt--build-date-styles' for implementation details." ;;;; Table of Contents -(defun org-odt-begin-toc (index-title depth) +(defun org-odt--format-toc (title entries depth) + "Return a table of contents. +TITLE is the title of the table, as a string, or nil. ENTRIES is +the contents of the table, as a string. DEPTH is an integer +specifying the depth of the table." (concat - (format " - - - %s -" depth index-title) + " +\n" + (format " " depth) + (and title + (format " + %s +" + title)) (let ((levels (number-sequence 1 10))) (mapconcat @@ -1098,23 +1105,21 @@ See `org-odt--build-date-styles' for implementation details." - -" level level)) levels "")) - - (format " - - - - - %s - - " index-title))) - -(defun org-odt-end-toc () - (format " - - -")) + \n" + level level)) levels "")) + " + + " + (and title + (format " + + %s + \n" + title)) + entries + " + +")) (defun* org-odt-format-toc-headline (todo todo-type priority text tags @@ -1149,7 +1154,12 @@ See `org-odt--build-date-styles' for implementation details." (format "%s" headline-label text)) -(defun org-odt-toc (depth info) +(defun org-odt-toc (depth info &optional scope) + "Build a table of contents. +DEPTH is an integer specifying the depth of the table. INFO is +a plist containing current export properties. Optional argument +SCOPE, when non-nil, defines the scope of the table. Return the +table of contents as a string, or nil." (assert (wholenump depth)) ;; When a headline is marked as a radio target, as in the example below: ;; @@ -1161,24 +1171,17 @@ See `org-odt--build-date-styles' for implementation details." ;; /TOC/, as otherwise there will be duplicated anchors one in TOC ;; and one in the document body. ;; - ;; FIXME-1: Currently exported headings are memoized. `org-export.el' - ;; doesn't provide a way to disable memoization. So this doesn't - ;; work. - ;; - ;; FIXME-2: Are there any other objects that need to be suppressed + ;; FIXME: Are there any other objects that need to be suppressed ;; within TOC? - (let* ((title (org-export-translate "Table of Contents" :utf-8 info)) - (headlines (org-export-collect-headlines - info (and (wholenump depth) depth))) + (let* ((headlines (org-export-collect-headlines info depth scope)) (backend (org-export-create-backend - :parent (org-export-backend-name - (plist-get info :back-end)) + :parent (org-export-backend-name (plist-get info :back-end)) :transcoders (mapcar (lambda (type) (cons type (lambda (d c i) c))) (list 'radio-target))))) (when headlines - (concat - (org-odt-begin-toc title depth) + (org-odt--format-toc + (and (not scope) (org-export-translate "Table of Contents" :utf-8 info)) (mapconcat (lambda (headline) (let* ((entry (org-odt-format-headline--wrap @@ -1188,7 +1191,7 @@ See `org-odt--build-date-styles' for implementation details." (format "\n%s" style entry))) headlines "\n") - (org-odt-end-toc))))) + depth)))) ;;;; Document styles @@ -2013,7 +2016,8 @@ contextual information." (defun org-odt-keyword (keyword contents info) "Transcode a KEYWORD element from Org to ODT. -CONTENTS is nil. INFO is a plist holding contextual information." +CONTENTS is nil. INFO is a plist holding contextual +information." (let ((key (org-element-property :key keyword)) (value (org-element-property :value keyword))) (cond @@ -2022,14 +2026,15 @@ CONTENTS is nil. INFO is a plist holding contextual information." ;; FIXME (ignore)) ((string= key "TOC") - (let ((value (downcase value))) + (let ((case-fold-search t)) (cond - ((string-match "\\" value) - (let ((depth (or (and (string-match "[0-9]+" value) + ((org-string-match-p "\\" value) + (let ((depth (or (and (string-match "\\<[0-9]+\\>" value) (string-to-number (match-string 0 value))) - (plist-get info :with-toc)))) - (when (wholenump depth) (org-odt-toc depth info)))) - ((member value '("tables" "figures" "listings")) + (plist-get info :headline-levels))) + (localp (org-string-match-p "\\" value))) + (org-odt-toc depth info (and localp keyword)))) + ((org-string-match-p "tables\\|figures\\|listings" value) ;; FIXME (ignore)))))))) diff --git a/lisp/ox.el b/lisp/ox.el index 85a2ff00b..8880e10ef 100644 --- a/lisp/ox.el +++ b/lisp/ox.el @@ -4828,7 +4828,7 @@ return nil." ;; `org-export-collect-tables', `org-export-collect-figures' and ;; `org-export-collect-listings' can be derived from it. -(defun org-export-collect-headlines (info &optional n) +(defun org-export-collect-headlines (info &optional n scope) "Collect headlines in order to build a table of contents. INFO is a plist used as a communication channel. @@ -4838,15 +4838,28 @@ the table of contents. Otherwise, it is set to the value of the last headline level. See `org-export-headline-levels' for more information. +Optional argument SCOPE, when non-nil, is an element. If it is +a headline, only children of SCOPE are collected. Otherwise, +collect children of the headline containing provided element. If +there is no such headline, collect all headlines. In any case, +argument N becomes relative to the level of that headline. + Return a list of all exportable headlines as parsed elements. -Footnote sections, if any, will be ignored." - (let ((limit (plist-get info :headline-levels))) - (setq n (if (wholenump n) (min n limit) limit)) - (org-element-map (plist-get info :parse-tree) 'headline - #'(lambda (headline) - (unless (org-element-property :footnote-section-p headline) - (let ((level (org-export-get-relative-level headline info))) - (and (<= level n) headline)))) +Footnote sections are ignored." + (let* ((scope (cond ((not scope) (plist-get info :parse-tree)) + ((eq (org-element-type scope) 'headline) scope) + ((org-export-get-parent-headline scope)) + (t (plist-get info :parse-tree)))) + (limit (plist-get info :headline-levels)) + (n (if (not (wholenump n)) limit + (min (if (eq (org-element-type scope) 'org-data) n + (+ (org-export-get-relative-level scope info) n)) + limit)))) + (org-element-map (org-element-contents scope) 'headline + (lambda (headline) + (unless (org-element-property :footnote-section-p headline) + (let ((level (org-export-get-relative-level headline info))) + (and (<= level n) headline)))) info))) (defun org-export-collect-elements (type info &optional predicate) diff --git a/testing/lisp/test-ox.el b/testing/lisp/test-ox.el index 290b02980..7e4eb50b9 100644 --- a/testing/lisp/test-ox.el +++ b/testing/lisp/test-ox.el @@ -3185,7 +3185,19 @@ Another text. (ref:text) (= 1 (length (org-test-with-parsed-data "#+OPTIONS: H:1\n* H1\n** H2" - (org-export-collect-headlines info 2)))))) + (org-export-collect-headlines info 2))))) + ;; Collect headlines locally. + (should + (= 2 + (org-test-with-parsed-data "* H1\n** H2\n** H3" + (let ((scope (org-element-map tree 'headline #'identity info t))) + (length (org-export-collect-headlines info nil scope)))))) + ;; When collecting locally, optional level is relative. + (should + (= 1 + (org-test-with-parsed-data "* H1\n** H2\n*** H3" + (let ((scope (org-element-map tree 'headline #'identity info t))) + (length (org-export-collect-headlines info 1 scope)))))))