Pedro Andres Aranda Gutierrez <[email protected]> writes:
>> May you please explain this change?
>
> At one point, while debugging, it helped me assuring need-alternative being a
> boolean. Ignore if you don’t like it.
>>> - ;; legacy behavior (á la LaTeX)
>>> + ;; Treat the ToC as it has been originally
>>
>> Maybe "Ignore unnumbered headings in ToC - as in LaTeX"
>
> OK, could live with that.
I have updated your patch, addressing my comments, adding some
refactoring, and fixing a bug I found.
See the attached updated patch.
Main changes:
1.
(when (and (not (string= full-text-no-footnote full-text)) ;; when we have
footnotess
(string= full-text opt-title)) ;; And we do not
impose an alternative title
(setq opt-title full-text-no-footnote))
You previously had (string= full-text-no-footenote opt-title), which
makes no sense. I also added a test for the case addressed by this part
of the code.
2. I renamed need-alternative to need-toc. IMHO, it is a bit more
clear. I also updated the commentary to explain what is going on.
3. I added a FIXME
;; FIXME: In theory, user may customize section-fmt
;; to use, e.g. \section{...} for unnumbered headings
;; We do not handle such scenario.
Not sure if it is something we need to worry about.
In addition, I have a question about
(if (string-suffix-p "*" section-kw)
;; Subsection that needs alternative title:
;; Keep section format, use \\addcontentsline
(setq new-extra
(format "\\addcontentsline{toc}{%s}{%s}\n"
(string-remove-suffix "*" section-kw)
opt-title))
;; section... we need the brackets
(let*
;; Replace square brackets with parenthesis
;; since square brackets are not supported in
;; optional arguments.
((un-bracketed-alt (replace-regexp-in-string
"\\[" "(" (replace-regexp-in-string
"\\]" ")" opt-title)))
(replacement-re (concat "\\1[" un-bracketed-alt "]")))
(setq new-format (replace-match replacement-re nil nil
section-fmt 1))))
I am wondering if we can sompletely drop the part with using
\sectionkwd[...]{...}
and instead always use \addcontentsline. This way, we won't need to
replace square brackets in the title. WDYT?
>From 278f4dd91919c505e108f3d0b4714c363c6be55c Mon Sep 17 00:00:00 2001
Message-ID: <278f4dd91919c505e108f3d0b4714c363c6be55c.1740302355.git.yanta...@posteo.net>
From: "Pedro A. Aranda" <[email protected]>
Date: Wed, 12 Feb 2025 07:27:27 +0100
Subject: [PATCH v2] New combined patch for ToC handling
Ihor's comments and fix alternative title handling
---
doc/org-manual.org | 41 +++++++++++-
etc/ORG-NEWS | 15 +++++
lisp/ox-latex.el | 114 ++++++++++++++++++++++++++------
testing/lisp/test-ox-latex.el | 118 ++++++++++++++++++++++++++++++++++
4 files changed, 266 insertions(+), 22 deletions(-)
diff --git a/doc/org-manual.org b/doc/org-manual.org
index 65a1185d17..dcc9a6dec0 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -14852,6 +14852,41 @@ *** Quote blocks in LaTeX export
\end{foreigndisplayquote}
#+end_example
+*** Controlling the way the [[*Table of Contents]] is generated
+#+cindex: LaTeX ToC export
+
+When exporting your document to LaTeX, only numbered sections will be
+included. This is closer to LaTeX and different to how other exporters
+work. If you need an unnumbered section to appear in the table of
+contents, use the property =UNNUMBERED= and set it to =toc=:
+
+#+begin_example
+:PROPERTIES:
+:UNNUMBERED: toc
+:END:
+#+end_example
+
+#+cindex: LaTeX ToC export a la ~org~
+#+cindex: @samp{org-latex-toc-include-unnumbered}
+If you want the LaTeX exporter to behave like other exporters,
+customise the variable ~org-latex-toc-include-unnumbered~ and
+set it to ~t~:
+
+#+begin_example
+(setq org-latex-toc-include-unnumbered t)
+#+end_example
+
+or add this setting in the local variables.
+
+In this case, unnumbered sections will be included in the table of
+contents, unless you set the =UNNUMBERED= property to ~notoc~:
+
+#+begin_example
+:PROPERTIES:
+:UNNUMBERED: notoc
+:END:
+#+end_example
+
** Markdown Export
:PROPERTIES:
:DESCRIPTION: Exporting to Markdown.
@@ -23509,8 +23544,10 @@ * Footnotes
configuration. See [[LaTeX specific export settings]].
[fn:43] At the moment, some export backends do not obey this
-specification. For example, LaTeX export excludes every unnumbered
-headline from the table of contents.
+specification. For example, LaTeX export excludes by default
+every unnumbered headline from the table of contents, unless you set
+the custom variable =org-latex-toc-include-unnumbered= to =t= or add
+=:UNNUMBERED: toc= to the section's properties.
[fn:44] Note that ~org-link-search-must-match-exact-headline~ is
locally bound to non-~nil~. Therefore, ~org-link-search~ only matches
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 9eb4f711c1..7f067481e2 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -305,6 +305,21 @@ slide to specific animation steps.
This text will be displayed on animation step 2 and later.
#+END_SRC
+*** ox-latex: Table of contents generation has been fixed and augmented
+
+The LaTeX exporter differs from other exporters in that it *does not*
+export unnumbered sections by default.
+
+The LaTeX exporter will use the new property value =:UNNUMBERED: toc= to
+include unnumbered sections in the table of contents.
+
+Additionally, a new custom variable
+~org-latex-toc-include-unnumbered~ has been introduced. It
+enables including unnumbered sections in the ToC aligned with the
+behaviour of other exporters. In this case, to exclude a section from
+the table of contents, mark it as =:UNNUMBERED: notoc= in its
+properties.
+
** New functions and changes in function arguments
# This also includes changes in function behavior from Elisp perspective.
diff --git a/lisp/ox-latex.el b/lisp/ox-latex.el
index bc31df6f52..dbfcdacf9c 100644
--- a/lisp/ox-latex.el
+++ b/lisp/ox-latex.el
@@ -643,7 +643,8 @@ (defcustom org-latex-hyperref-template
:version "26.1"
:package-version '(Org . "8.3")
:type '(choice (const :tag "No template" nil)
- (string :tag "Format string")))
+ (string :tag "Format string"))
+ :safe #'string-or-null-p)
;;;; Headline
@@ -675,6 +676,7 @@ (defcustom org-latex-default-footnote-command "\\footnote{%s%s}"
The value will be passed as an argument to `format' as the following
(format org-latex-default-footnote-command
footnote-description footnote-label)"
+
:group 'org-export-latex
:package-version '(Org . "9.7")
:type 'string)
@@ -1537,6 +1539,18 @@ (defcustom org-latex-known-warnings
(string :tag "Message"))))
+(defcustom org-latex-toc-include-unnumbered nil
+ "Set this variable to true to include unnumbered headings in the
+table of contents as other exporters do.
+
+The default behaviour is to include numbered headings only.
+To include an unnumbered heading, set the `:UNNUMBERED:'
+property to `toc'"
+ :group 'org-export-latex
+ :package-version '(Org . "9.8")
+ :type 'boolean
+ :safe #'booleanp)
+
;;; Internal Functions
@@ -2275,6 +2289,12 @@ (defun org-latex-headline (headline contents info)
(unless (org-element-property :footnote-section-p headline)
(let* ((class (plist-get info :latex-class))
(level (org-export-get-relative-level headline info))
+ ;; "LaTeX TOC handling"
+ ;; :unnumbered: toc will add the heading to the ToC
+ ;; "Org TOC handling"
+ ;; :unnumbered: notoc to suppress heading from the ToC
+ ;; else include all headings (including unnumbered) like other modes
+ (unnumbered-type (org-export-get-node-property :UNNUMBERED headline t))
(numberedp (org-export-numbered-headline-p headline info))
(class-sectioning (assoc class (plist-get info :latex-classes)))
;; Section formatting will set two placeholders: one for
@@ -2381,6 +2401,8 @@ (defun org-latex-headline (headline contents info)
(funcall (plist-get info :latex-format-headline-function)
todo todo-type priority
(org-export-data-with-backend
+ ;; Returns alternative title when provided or
+ ;; title itself.
(org-export-get-alt-title headline info)
section-backend info)
(and (eq (plist-get info :with-tags) t) tags)
@@ -2401,25 +2423,77 @@ (defun org-latex-headline (headline contents info)
(string-match-p "\\<local\\>" v)
(format "\\stopcontents[level-%d]" level)))))
info t)))))
- (if (and (or (and opt-title (not (equal opt-title full-text)))
- ;; Heading contains footnotes. Add optional title
- ;; version without footnotes to avoid footnotes in
- ;; TOC/footers.
- (and (not (equal full-text-no-footnote full-text))
- (setq opt-title full-text-no-footnote)))
- (string-match "\\`\\\\\\(.+?\\){" section-fmt))
- (format (replace-match "\\1[%s]" nil nil section-fmt 1)
- ;; Replace square brackets with parenthesis
- ;; since square brackets are not supported in
- ;; optional arguments.
- (replace-regexp-in-string
- "\\[" "(" (replace-regexp-in-string "\\]" ")" opt-title))
- full-text
- (concat headline-label pre-blanks contents))
- ;; Impossible to add an alternative heading. Fallback to
- ;; regular sectioning format string.
- (format section-fmt full-text
- (concat headline-label pre-blanks contents))))))))
+ ;; When do we need to explicitly specify a heading for TOC?
+ ;; 1. On numbered section with footnotes in title or alt_title
+ ;; 2. On an unnumbered section if :UNNUMBERED: allows it regardless of footnotes
+ ;; This applies to anything that may go into the ToC.
+ ;; Specifically for paragraphs, see first answer of
+ ;; https://tex.stackexchange.com/questions/288072/footnotes-within-paragraph
+ (let ((section-kw
+ (and (string-match "\\`\\\\\\(.+?\\){" section-fmt)
+ (match-string 1 section-fmt)))
+ need-toc)
+ (if (not section-kw)
+ ;; We only know how to add \SECTION-KW{...} to TOC.
+ (setq need-toc nil)
+ (if (string-suffix-p "*" section-kw)
+ ;; FIXME: In theory, user may customize section-fmt
+ ;; to use, e.g. \section{...} for unnumbered headings
+ ;; We do not handle such scenario.
+ (progn ;; unnumbered sections (ending with *)
+ ;; Then we need to obey what the :UNNUMBERED: property says
+ (if org-latex-toc-include-unnumbered
+ ;; Treat the ToC closer to what other exporters do
+ ;; Include unnumbered section into TOC unless
+ ;; explicitly requested not to.
+ (if (string= unnumbered-type "notoc")
+ (setq need-toc nil)
+ (setq need-toc t))
+ ;; Ignore unnumbered headings in ToC - as in LaTeX
+ ;; unless explicitly requested to include.
+ (if (string= unnumbered-type "toc")
+ (setq need-toc t)
+ (setq need-toc nil))))
+ ;; Numbered sections
+ ;; Specify special TOC title only when there is
+ ;; opt-title or when title contains footnotes.
+ (if (and (string= full-text full-text-no-footnote) ;; no footnotes
+ ;; opt-title is either ALT_TITLE or title itself
+ ;; as returned by `org-export-get-alt-title'
+ (string= full-text opt-title)) ;; same alternative title
+ (setq need-toc nil)
+ (setq need-toc t))))
+ ;; In all cases
+ ;; Get rid of the footnotes in opt-title
+ (when (and (not (string= full-text-no-footnote full-text)) ;; when we have footnotess
+ (string= full-text opt-title)) ;; And we do not impose an alternative title
+ (setq opt-title full-text-no-footnote))
+ (if need-toc
+ (let ((new-format section-fmt)
+ (new-extra "")) ;; put the addcontentsline here
+ (if (string-suffix-p "*" section-kw)
+ ;; Subsection that needs alternative title:
+ ;; Keep section format, use \\addcontentsline
+ (setq new-extra
+ (format "\\addcontentsline{toc}{%s}{%s}\n"
+ (string-remove-suffix "*" section-kw)
+ opt-title))
+ ;; section... we need the brackets
+ (let*
+ ;; Replace square brackets with parenthesis
+ ;; since square brackets are not supported in
+ ;; optional arguments.
+ ((un-bracketed-alt (replace-regexp-in-string
+ "\\[" "(" (replace-regexp-in-string "\\]" ")" opt-title)))
+ (replacement-re (concat "\\1[" un-bracketed-alt "]")))
+ (setq new-format (replace-match replacement-re nil nil section-fmt 1))))
+ (format new-format
+ full-text
+ (concat headline-label new-extra pre-blanks contents)))
+ ;; Don't need or cannot have alternative heading.
+ ;; Use regular sectioning format string.
+ (format section-fmt full-text
+ (concat headline-label pre-blanks contents)))))))))
(defun org-latex-format-headline-default-function
(todo _todo-type priority text tags _info)
diff --git a/testing/lisp/test-ox-latex.el b/testing/lisp/test-ox-latex.el
index b75921ae78..04164767c0 100644
--- a/testing/lisp/test-ox-latex.el
+++ b/testing/lisp/test-ox-latex.el
@@ -154,5 +154,123 @@ (ert-deftest test-ox-latex/inline-image ()
(search-forward
"\\href{https://orgmode.org/worg/images/orgmode/org-mode-unicorn.svg}{\\includegraphics[width=.9\\linewidth]{/wallpaper.png}}"))))
+(ert-deftest test-ox-latex/num-t ()
+ "Test toc treatment for fixed num:t"
+ (org-test-with-exported-text
+ 'latex
+ "#+TITLE: num: fix
+#+OPTIONS: toc:t H:3 num:t
+
+* Section
+
+** Subsection 1
+:PROPERTIES:
+:UNNUMBERED: t
+:END:
+is suppressed
+** Subsection 2
+:PROPERTIES:
+:UNNUMBERED: toc
+:END:
+
+** Subsection 3
+:PROPERTIES:
+:UNNUMBERED: toc
+:ALT_TITLE: Alternative
+:END:
+
+* Section 2[fn::Test]
+:PROPERTIES:
+:ALT_TITLE: SECTION 2
+:END:
+"
+ (goto-char (point-min))
+ (should
+ (search-forward "\\begin{document}
+
+\\maketitle
+\\tableofcontents
+
+\\section{Section}
+\\label{"))
+ (should (search-forward "}
+
+\\subsection*{Subsection 1}
+\\label{"))
+ (should (search-forward "}
+is suppressed
+\\subsection*{Subsection 2}
+\\label{"))
+ (should (search-forward "}
+\\addcontentsline{toc}{subsection}{Subsection 2}
+\\subsection*{Subsection 3}
+\\label{"))
+ (should (search-forward "}
+\\addcontentsline{toc}{subsection}{Alternative}
+\\section[SECTION 2]{Section 2\\footnote{Test}}
+\\label{"))
+ (should (search-forward "}
+\\end{document}"))))
+
+(ert-deftest test-ox-latex/new-toc-as-org ()
+ "test toc treatment with `org-latex-toc-include-unnumbered' set to `t'"
+ (let ((org-latex-toc-include-unnumbered t))
+ (org-test-with-exported-text 'latex
+ "#+TITLE: num: fix
+#+OPTIONS: toc:t H:3 num:nil
+
+* Section
+
+** Subsection 1
+
+** Subsection 2
+:PROPERTIES:
+:UNNUMBERED: notoc
+:END:
+is suppressed
+
+** Subsection 3
+:PROPERTIES:
+:ALT_TITLE: Alternative
+:END:
+
+* Section 2[fn::Test]
+:PROPERTIES:
+:ALT_TITLE: SECTION 2
+:END:
+
+* Section 3[fn::Test]
+"
+ (goto-char (point-min))
+ (should (search-forward "\\begin{document}
+
+\\maketitle
+\\tableofcontents
+
+\\section*{Section}
+\\label{"))
+ (should (search-forward "}
+\\addcontentsline{toc}{section}{Section}
+
+\\subsection*{Subsection 1}
+\\label{"))
+ (should (search-forward "}
+\\addcontentsline{toc}{subsection}{Subsection 1}
+
+\\subsection*{Subsection 2}
+\\label{"))
+ (should (search-forward "}
+is suppressed
+\\subsection*{Subsection 3}
+\\label{"))
+ (should (search-forward "}
+\\addcontentsline{toc}{subsection}{Alternative}
+\\section*{Section 2\\footnote{Test}}
+\\label{"))
+ (should (search-forward "}
+\\addcontentsline{toc}{section}{SECTION 2}"))
+ (should (search-forward "}
+\\addcontentsline{toc}{section}{Section 3}")))))
+
(provide 'test-ox-latex)
;;; test-ox-latex.el ends here
--
2.47.1
--
Ihor Radchenko // yantar92,
Org mode maintainer,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>