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.