SF.net SVN: docutils:[10317 ] trunk/docutils

milde--- via Docutils-checkins <[email protected]> Thu, 30 Apr 2026 19:17:18 +0000
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 10317
          http://sourceforge.net/p/docutils/code/10317
Author:   milde
Date:     2026-04-30 19:17:18 +0000 (Thu, 30 Apr 2026)
Log Message:
-----------
latex-footnotes: Fix nested footnotes and multiple references.

Store footnote ID (instead of footnote content) in the "footnote_queue".
Store IDs of already set footnotes in "processed_footnotes".

Add a `\label` to all footnotes.

Use `\footref` and `\footnotetext` for nested footnotes.

If a footnote is referenced again, just insert a `\footref` reference.

Call `visit_reference()` if the target of a footnote-reference
is not a footnote.

Modified Paths:
--------------
    trunk/docutils/docutils/writers/latex2e/__init__.py
    trunk/docutils/test/functional/expected/latex_footnotes.tex
    trunk/docutils/test/test_writers/test_latex2e.py

Modified: trunk/docutils/docutils/writers/latex2e/__init__.py
===================================================================
--- trunk/docutils/docutils/writers/latex2e/__init__.py	2026-04-30 19:17:00 UTC (rev 10316)
+++ trunk/docutils/docutils/writers/latex2e/__init__.py	2026-04-30 19:17:18 UTC (rev 10317)
@@ -1327,14 +1327,9 @@
         self.out = self.body
         self.out_stack = []  # stack of output collectors
 
-        # Texts of nested footnotes to emit once we finish the topmost
-        # footnote.  footnote_queues[i] contains the text of footnotes
-        # encountered while processing the current footnote (which is nested
-        # within `i` higher footnotes).  If i == 0, they will be emitted
-        # immediately after the current footnote ends; if i > 0; they will be
-        # added to footnote_queues[i-1] after ending the current footnote,
-        # which is added to the same queue before them.
-        self.footnote_queues = []
+        # LaTeX footnotes (see `visit_footnote_reference()`)
+        self.footnote_queue = []  # FIFO queue of footnote IDs
+        self.processed_footnotes = set()  # IDs of already set footnotes
 
         # Process settings
         # ~~~~~~~~~~~~~~~~
@@ -2380,7 +2375,7 @@
             # prevent spurious whitespace if footnote starts with paragraph:
             if len(node) > 1 and isinstance(node[1], nodes.paragraph):
                 self.out.append('%')
-        elif not self.footnote_queues:
+        elif not self.footnote_queue:
             # latex-footnotes: set only scheduled footnotes
             # (see `visit_footnote_reference()`)
             raise nodes.SkipNode
@@ -2404,33 +2399,40 @@
                                 (node['ids'][0], href))
                 self.context.append('}')
         else:  # latex-footnotes
-            footnote = self.document.ids[href]
-            # write footnote content into string `text`
-            self.footnote_queues.append([])
-            self.push_output_collector([])
-            footnote.walkabout(self)
-            text = ''.join(self.pop_output_collector())
-            queued = self.footnote_queues.pop()
-            if not self.footnote_queues:
-                self.out.append("\\footnote{%")
-                self.out.append(text)
-                self.out.append("}")
-                for fn in queued:
-                    self.out.append("\\footnotetext{%")
-                    self.out.append(fn)
-                    self.out.append("}")
-            else:
-                self.out.append("\\footnotemark{}")
-                self.footnote_queues[-1].append(text)
-                self.footnote_queues[-1].extend(queued)
-            raise nodes.SkipNode
-            # TODO:
-            # * Prepend a label if the footnote is an explicit target.
-            # * use \footref (part of LaTeX since 2021-05-01)
-            #   for multiple refs to the same footnote.
+            target = self.document.ids[href]
+            if not isinstance(target, nodes.footnote):
+                self.out.append(r'\textsuperscript{')
+                self.visit_reference(node)
+                return
+            if href in self.processed_footnotes:
+                # footnote is already set, just insert a reference:
+                self.out.append(r'\footref{%s}' % href)
+                raise nodes.SkipNode
+            self.footnote_queue.append(href)
+            if len(self.footnote_queue) > 1:
+                # nested footnote: insert reference now and text later
+                self.out.append(r'\footref{%s}' % href)
+                raise nodes.SkipNode
+            # if we reach this point, the first queue item is a top level note
+            footnote_cmd = r'\footnote'
+            while self.footnote_queue:
+                ID = self.footnote_queue[0]
+                footnote = self.document.ids[ID]
+                self.out += [footnote_cmd, r'{\label{', ID, '}%']
+                footnote.walkabout(self)  # visit/depart footnote & content
+                self.out.append('}')
+                # update queue, keep ID in case the note is referred to again
+                self.processed_footnotes.add(self.footnote_queue.pop(0))
+                # remaining items have the footnote-reference already set:
+                footnote_cmd = '%\n\\refstepcounter{footnote}\\footnotetext'
+            raise nodes.SkipNode  # ignore content (footnote number/symbol)
 
     def depart_footnote_reference(self, node) -> None:
-        self.out.append(self.context.pop())
+        if self.docutils_footnotes:
+            self.out.append(self.context.pop())
+        elif not isinstance(self.document.ids[node['refid']], nodes.footnote):
+            self.depart_reference(node)
+            self.out.append('}')
 
     # footnote/citation label
     def label_delim(self, node, bracket, superscript):

Modified: trunk/docutils/test/functional/expected/latex_footnotes.tex
===================================================================
--- trunk/docutils/test/functional/expected/latex_footnotes.tex	2026-04-30 19:17:00 UTC (rev 10316)
+++ trunk/docutils/test/functional/expected/latex_footnotes.tex	2026-04-30 19:17:18 UTC (rev 10317)
@@ -28,44 +28,40 @@
 
 \section{Test features of rST footnotes}
 
-\emph{Numbered} footnotes\footnote{%
+\emph{Numbered} footnotes\footnote{\label{exi}%
 autonumber-label
-}\footnote{%
+}\footnote{\label{footnote-2}%
 auto-numbered (skipping 2)
-}\footnote{%
+}\footnote{\label{footnote-1}%
 manually numbered
-}\footnote{%
+}\footnote{\label{footnote-3}%
 second auto-numbered (skipping \hyperref[footnote-1]{4} and \hyperref[section-2]{5})
 } may be referenced more than
 once and also via hyperlink references:
 
 \begin{description}
-\item[{autonumber label\footnote{%
-autonumber-label
-}}] 
+\item[{autonumber label\footref{exi}}] 
 Reference name is the \emph{label} (without \#),
 \hyperref[exi]{exi}, not the number (both \texttt{1\_} and \texttt{{[}1{]}\_} fail).
 
-\item[{manually numbered\footnote{%
-manually numbered
-}}] 
+\item[{manually numbered\footref{footnote-1}}] 
 Reference name is the specified number, \hyperref[footnote-1]{4}.
 
-\item[{auto-numbered\footnote{%
-auto-numbered (skipping 2)
-}}] 
+\item[{auto-numbered\footref{footnote-2}}] 
 Reference name is the auto-assigned number, \hyperref[footnote-2]{3}
 (caution: the assigned number may change if another
 auto-numbered footnote is added or removed).
 \end{description}
 
-\emph{Symbol} footnotes\footnote{%
-auto-symbol with nested footnotes\footnotemark{}\footnotemark{}
-}\footnotetext{%
+\emph{Symbol} footnotes\footnote{\label{footnote-4}%
+auto-symbol with nested footnotes\footref{footnote-6}\footref{footnote-7}
+}%
+\refstepcounter{footnote}\footnotetext{\label{footnote-6}%
 referenced from the first auto-symbol footnote
-}\footnotetext{%
+}%
+\refstepcounter{footnote}\footnotetext{\label{footnote-7}%
 referenced from the first auto-symbol footnote, too
-}\footnote{%
+}\footnote{\label{footnote-5}%
 second auto-symbol
 } do not have a reference name.
 They can only be referenced once and only with a footnote-reference.
@@ -82,12 +78,8 @@
 \section{4}
 
 An \emph{implicit} target with conflicting refname (like this section) is
-overwritten by manually numbered footnotes\footnote{%
-manually numbered
-} and footnotes with
-autonumber-label\footnote{%
-autonumber-label
-}. An INFO is generated.
+overwritten by manually numbered footnotes\footref{footnote-1} and footnotes with
+autonumber-label\footref{exi}. An INFO is generated.
 
 Hyperlink references to \hyperref[footnote-1]{4} and \hyperref[exi]{exi} point to the footnotes, not the
 sections.
@@ -108,7 +100,7 @@
 (e.g. the inline target \textquotedbl{}%
 \phantomsection\label{target-4}2\textquotedbl{} and the section title \textquotedbl{}5\textquotedbl{} above)
 cause a gap in footnote auto-numbering. The number can be used in a
-footnote-reference although it does not refer to a footnote!
+footnote-reference although it does not refer to a footnote!\textsuperscript{\hyperref[section-2]{5}}\textsuperscript{\hyperref[target-4]{2}}
 
 \begin{description}
 \item[{TODO:}] 

Modified: trunk/docutils/test/test_writers/test_latex2e.py
===================================================================
--- trunk/docutils/test/test_writers/test_latex2e.py	2026-04-30 19:17:00 UTC (rev 10316)
+++ trunk/docutils/test/test_writers/test_latex2e.py	2026-04-30 19:17:18 UTC (rev 10317)
@@ -985,97 +985,97 @@
 
 
 samples['latex-footnotes'] = ({'docutils_footnotes': False}, [
-# simple
-["""\
+# different markup variants
+[r"""
 Paragraphs contain text and may contain footnote references (manually
 numbered [1]_, anonymous auto-numbered [#]_, labeled auto-numbered
 [#label]_, or symbolic [*]_).
 
-.. [1] A footnote contains body elements, consistently indented by at
-   least 3 spaces.
+.. [1] A footnote.
 
-   This is the footnote's second paragraph.
-
 .. [#label] Footnotes may be numbered, either manually or
    automatically using a "#"-prefixed label.  This footnote has a
    label so it can be referred to from multiple places, both as a
-   footnote reference and as a hyperlink reference.
+   footnote reference ([#label]_) and as a `hyperlink reference`__.
 
+   __ label_
+
 .. [#] This footnote is numbered automatically and anonymously using a
    label of "#" only.
 
-.. [*] Footnotes may also use symbols, specified with a "*" label.
+.. [*] With "latex-footnotes", symbolic footnotes and numbered footnotes
+   are merged, the choice of symbol vs. number is done by LaTeX styling.
 """,
 r"""
 Paragraphs contain text and may contain footnote references (manually
-numbered\footnote{%
-A footnote contains body elements, consistently indented by at
-least 3 spaces.
-
-This is the footnote's second paragraph.
-}, anonymous auto-numbered\footnote{%
+numbered\footnote{\label{footnote-1}%
+A footnote.
+}, anonymous auto-numbered\footnote{\label{footnote-2}%
 This footnote is numbered automatically and anonymously using a
 label of \textquotedbl{}\#\textquotedbl{} only.
-}, labeled auto-numbered\footnote{%
+}, labeled auto-numbered\footnote{\label{label}%
 Footnotes may be numbered, either manually or
 automatically using a \textquotedbl{}\#\textquotedbl{}-prefixed label.  This footnote has a
 label so it can be referred to from multiple places, both as a
-footnote reference and as a hyperlink reference.
-}, or symbolic\footnote{%
-Footnotes may also use symbols, specified with a \textquotedbl{}*\textquotedbl{} label.
+footnote reference (\footref{label}) and as a \hyperref[label]{hyperlink reference}.
+}%
+\refstepcounter{footnote}\footnotetext{\label{label}%
+Footnotes may be numbered, either manually or
+automatically using a \textquotedbl{}\#\textquotedbl{}-prefixed label.  This footnote has a
+label so it can be referred to from multiple places, both as a
+footnote reference (\footref{label}) and as a \hyperref[label]{hyperlink reference}.
+}, or symbolic\footnote{\label{footnote-3}%
+With \textquotedbl{}latex-footnotes\textquotedbl{}, symbolic footnotes and numbered footnotes
+are merged, the choice of symbol vs. number is done by LaTeX styling.
 }).
 """],
-# nested
+# multi-nested footnotes
 ["""\
-It's possible to produce nested footnotes in LaTeX. [#]_
+A footnote [#multi]_
 
-.. [#] It takes some work, though. [#]_
-.. [#] And don't even get me started on how tricky recursive footnotes would be.
+.. [#multi] This is a footnote with nested [#]_ footnotes. [#]_ [#]_
+.. [#] First nested [#]_ footnote. [#]_
+.. [#] Second nested footnote. [#]_
+.. [#] Third nested footnote.
+.. [#] First double-nested footnote.
+.. [#] Second double-nested footnote.
+.. [#] First triple-nested footnote.
+.. [#] Not nested, referenced after footnote text.
+
+Ref to a new footnote [#]_
+
+A second reference to the first one [#multi]_.
+We can also write a hyperlink to multi_.
 """,
 r"""
-It's possible to produce nested footnotes in LaTeX.\footnote{%
-It takes some work, though.\footnotemark{}
-}\footnotetext{%
-And don't even get me started on how tricky recursive footnotes would be.
+A footnote\footnote{\label{multi}%
+This is a footnote with nested\footref{footnote-1} footnotes.\footref{footnote-2}\footref{footnote-3}
+}%
+\refstepcounter{footnote}\footnotetext{\label{footnote-1}%
+First nested\footref{footnote-4} footnote.\footref{footnote-5}
+}%
+\refstepcounter{footnote}\footnotetext{\label{footnote-2}%
+Second nested footnote.\footref{footnote-6}
+}%
+\refstepcounter{footnote}\footnotetext{\label{footnote-3}%
+Third nested footnote.
+}%
+\refstepcounter{footnote}\footnotetext{\label{footnote-4}%
+First double-nested footnote.
+}%
+\refstepcounter{footnote}\footnotetext{\label{footnote-5}%
+Second double-nested footnote.
+}%
+\refstepcounter{footnote}\footnotetext{\label{footnote-6}%
+First triple-nested footnote.
 }
-"""],
-# chained
-["""\
-It's possible to produce chained footnotes in LaTeX. [#]_
 
-.. [#] They're just a special case of nested footnotes. [#]_
-.. [#] A nested footnote is a footnote on a footnote. [#]_
-.. [#] This is a footnote on a footnote on a footnote.
-""",
-r"""
-It's possible to produce chained footnotes in LaTeX.\footnote{%
-They're just a special case of nested footnotes.\footnotemark{}
-}\footnotetext{%
-A nested footnote is a footnote on a footnote.\footnotemark{}
-}\footnotetext{%
-This is a footnote on a footnote on a footnote.
+Ref to a new footnote\footnote{\label{footnote-7}%
+Not nested, referenced after footnote text.
 }
-"""],
-# multinested
-["""\
-LaTeX isn't the best at nested footnote support. [#]_
 
-.. [#] Specifically, it gets the numbers wrong [#]_ for "multinested"
-   footnotes. [#]_
-.. [#] For example, this should be footnote 2, but both it and the next one
-   show up as footnote 3.
-.. [#] That's a footnote that contains more than one footnote of its own.
-""",
-r"""
-LaTeX isn't the best at nested footnote support.\footnote{%
-Specifically, it gets the numbers wrong\footnotemark{} for \textquotedbl{}multinested\textquotedbl{}
-footnotes.\footnotemark{}
-}\footnotetext{%
-For example, this should be footnote 2, but both it and the next one
-show up as footnote 3.
-}\footnotetext{%
-That's a footnote that contains more than one footnote of its own.
-}
+A second reference to the first one\footref{multi}.
+We can also write a hyperlink to \hyperref[multi]{multi}.
 """],
 ])
 

This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.