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

milde--- via Docutils-checkins <[email protected]> Thu, 30 Apr 2026 19:16:40 +0000
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 10314
          http://sourceforge.net/p/docutils/code/10314
Author:   milde
Date:     2026-04-30 19:16:39 +0000 (Thu, 30 Apr 2026)
Log Message:
-----------
Implement the --latex-footnotes option for 99% of use cases.

Contributed by John Thorvald Wodder II <[email protected]>
on https://sourceforge.net/p/docutils/patches/182/

Shortcomings of this implementation:

 * Footnotes aren't hyperlinked back to their references.
   I am not aware of a way to solve this without basically
   reimplemeting docutils-footnotes.

 * Recursive footnotes are not supported and will cause
   a recursion error. Support would require tracking and
   referencing (a la https://tex.stackexchange.com/a/23158/) the
   number that LaTeX assigns to each footnote, which normally
   resets on chapters and would be broken by packages like
   footmisc and perpage.

 * If the same footnote is referenced multiple times, it will be
   treated as a new footnote each time. I believe this has the
   same solution as the above.

 * If a footnote contains two or more nested
   footnotes, the numbering will be messed up; see
   https://tex.stackexchange.com/q/38643/ for a way to address
   this.

Modified Paths:
--------------
    trunk/docutils/docutils/writers/latex2e/__init__.py
    trunk/docutils/test/data/help/rst2latex.rst
    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-29 15:15:49 UTC (rev 10313)
+++ trunk/docutils/docutils/writers/latex2e/__init__.py	2026-04-30 19:16:39 UTC (rev 10314)
@@ -226,13 +226,16 @@
           {'dest': 'legacy_column_widths',
            'action': 'store_false',
            'validator': frontend.validate_boolean}),
-         # TODO: implement "latex footnotes" alternative
-         ('Footnotes with numbers/symbols by Docutils. (default) '
-          '(The alternative, --latex-footnotes, is not implemented yet.)',
+         ('Footnotes with numbers/symbols by Docutils. (default)',
           ['--docutils-footnotes'],
           {'default': True,
            'action': 'store_true',
            'validator': frontend.validate_boolean}),
+         ('Footnotes with numbers by LaTeX.',
+          ['--latex-footnotes'],
+          {'dest': 'docutils_footnotes',
+           'action': 'store_false',
+           'validator': frontend.validate_boolean}),
          ),
         )
 
@@ -1260,7 +1263,6 @@
         else:
             self.graphicx_package = (r'\usepackage[%s]{graphicx}' %
                                      settings.graphicx_option)
-        # footnotes: TODO: implement LaTeX footnotes
         self.docutils_footnotes = settings.docutils_footnotes
 
         # Output collection stacks
@@ -1325,6 +1327,15 @@
         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 = []
+
         # Process settings
         # ~~~~~~~~~~~~~~~~
         # Encodings:
@@ -2353,11 +2364,11 @@
         self.pop_output_collector()
 
     def visit_footnote(self, node) -> None:
-        try:
-            backref = node['backrefs'][0]
-        except IndexError:
-            backref = node['ids'][0]  # no backref, use self-ref instead
         if self.docutils_footnotes:
+            try:
+                backref = node['backrefs'][0]
+            except IndexError:
+                backref = node['ids'][0]  # no backref, use self-ref instead
             self.provide_fallback('footnotes')
             num = node[0].astext()
             if self.settings.footnote_references == 'brackets':
@@ -2367,10 +2378,12 @@
             # prevent spurious whitespace if footnote starts with paragraph:
             if len(node) > 1 and isinstance(node[1], nodes.paragraph):
                 self.out.append('%')
-        # TODO: "real" LaTeX \footnote{}s (see visit_footnotes_reference())
+        elif not self.footnote_queues:
+            raise nodes.SkipNode
 
     def depart_footnote(self, node) -> None:
-        self.out.append('}\n')
+        if self.docutils_footnotes:
+            self.out.append('}\n')
 
     def visit_footnote_reference(self, node) -> None:
         href = ''
@@ -2378,25 +2391,48 @@
             href = node['refid']
         elif 'refname' in node:
             href = self.document.nameids[node['refname']]
-        # if not self.docutils_footnotes:
-        #     # TODO: insert footnote content at (or near) this place
-        #     #       see also docs/dev/todo.rst
-        #     try:
-        #         referenced_node = self.document.ids[node['refid']]
-        #     except (AttributeError, KeyError):
-        #         self.document.reporter.error(
-        #             'unresolved footnote-reference %s' % node)
-        #     print('footnote-ref to %s' % referenced_node)
-        format = self.settings.footnote_references
-        if format == 'brackets':
-            self.append_hypertargets(node)
-            self.out.append('\\hyperlink{%s}{[' % href)
-            self.context.append(']}')
+        if self.docutils_footnotes:
+            format = self.settings.footnote_references
+            if format == 'brackets':
+                self.append_hypertargets(node)
+                self.out.append('\\hyperlink{%s}{[' % href)
+                self.context.append(']}')
+            else:
+                if not self.fallback_stylesheet:
+                    self.fallbacks['footnotes'] = PreambleCmds.footnotes
+                self.out.append(r'\DUfootnotemark{%s}{%s}{' %
+                                (node['ids'][0], href))
+                self.context.append('}')
         else:
-            self.provide_fallback('footnotes')
-            self.out.append(r'\DUfootnotemark{%s}{%s}{' %
-                            (node['ids'][0], href))
-            self.context.append('}')
+            footnotes = (self.document.footnotes
+                         + self.document.autofootnotes
+                         + self.document.symbol_footnotes)
+            for footnote in footnotes:
+                if href in footnote['ids']:
+                    self.footnote_queues.append([])
+                    self.push_output_collector([])
+                    footnote.walkabout(self)
+                    text = ''.join(self.out)
+                    self.pop_output_collector()
+                    break
+            else:
+                self.document.reporter.error(
+                    "Footnote %s referenced but not found" % href)
+                raise nodes.SkipNode
+            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
 
     def depart_footnote_reference(self, node) -> None:
         self.out.append(self.context.pop())

Modified: trunk/docutils/test/data/help/rst2latex.rst
===================================================================
--- trunk/docutils/test/data/help/rst2latex.rst	2026-04-29 15:15:49 UTC (rev 10313)
+++ trunk/docutils/test/data/help/rst2latex.rst	2026-04-30 19:16:39 UTC (rev 10314)
@@ -245,5 +245,4 @@
 --new-column-widths     Use new algorithm to determine table column widths.
                         (future default)
 --docutils-footnotes    Footnotes with numbers/symbols by Docutils. (default)
-                        (The alternative, --latex-footnotes, is not
-                        implemented yet.)
+--latex-footnotes       Footnotes with numbers by LaTeX.

Modified: trunk/docutils/test/test_writers/test_latex2e.py
===================================================================
--- trunk/docutils/test/test_writers/test_latex2e.py	2026-04-29 15:15:49 UTC (rev 10313)
+++ trunk/docutils/test/test_writers/test_latex2e.py	2026-04-30 19:16:39 UTC (rev 10314)
@@ -984,5 +984,100 @@
 ])
 
 
+samples['latex-footnotes'] = ({'docutils_footnotes': False}, [
+# simple
+["""\
+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.
+
+   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.
+
+.. [#] This footnote is numbered automatically and anonymously using a
+   label of "#" only.
+
+.. [*] Footnotes may also use symbols, specified with a "*" label.
+""",
+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{%
+This footnote is numbered automatically and anonymously using a
+label of \textquotedbl{}\#\textquotedbl{} only.
+}, labeled auto-numbered\footnote{%
+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.
+}).
+"""],
+# nested
+["""\
+It's possible to produce nested footnotes in LaTeX. [#]_
+
+.. [#] It takes some work, though. [#]_
+.. [#] And don't even get me started on how tricky recursive footnotes would be.
+""",
+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.
+}
+"""],
+# 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.
+}
+"""],
+# 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.
+}
+"""],
+])
+
 if __name__ == '__main__':
     unittest.main()

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