From: Matěj C. <mc...@ce...> - 2025-07-22 00:16:19
|
From: John Thorvald Wodder II <gi...@va...> Attached is a patch that implements the --latex-footnotes option for 99% of use cases. I don't know whether you'd find it satisfactory enough to accept, but I thought I'd at least try. 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 (à 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. Originally: https://sourceforge.net/p/docutils/patches/182/ --- docutils/docutils/writers/latex2e/__init__.py | 91 +++++++++----- docutils/test/test_writers/test_latex2e.py | 116 ++++++++++++++++++ 2 files changed, 179 insertions(+), 28 deletions(-) diff --git a/docutils/docutils/writers/latex2e/__init__.py b/docutils/docutils/writers/latex2e/__init__.py index b8264ee7d..1dcf13466 100644 --- a/docutils/docutils/writers/latex2e/__init__.py +++ b/docutils/docutils/writers/latex2e/__init__.py @@ -227,14 +227,17 @@ class Writer(writers.Writer): {'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}), ) relative_path_settings = ('template',) @@ -1253,7 +1256,6 @@ def __init__(self, document, babel_class=Babel) -> None: 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 @@ -1319,6 +1321,15 @@ def __init__(self, document, babel_class=Babel) -> None: 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: @@ -2324,11 +2335,11 @@ def depart_footer(self, node) -> None: 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': @@ -2341,10 +2352,12 @@ def visit_footnote(self, node) -> None: # 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 = '' @@ -2352,25 +2365,47 @@ def visit_footnote_reference(self, node) -> None: 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()) diff --git a/docutils/test/test_writers/test_latex2e.py b/docutils/test/test_writers/test_latex2e.py index 84bb94e02..434786690 100755 --- a/docutils/test/test_writers/test_latex2e.py +++ b/docutils/test/test_writers/test_latex2e.py @@ -500,5 +500,121 @@ def test_body(self): ]) +totest_latex_footnotes['simple'] = [ +# input +["""\ +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. +""", +## # expected output +head_template.substitute(dict(parts)) + 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. +}). + +\end{document} +"""], +] + +totest_latex_footnotes['nested'] = [ +# input +["""\ +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. +""", +## # expected output +head_template.substitute(dict(parts)) + 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. +} + +\end{document} +"""], +] + +totest_latex_footnotes['chained'] = [ +# input +["""\ +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. +""", +## # expected output +head_template.substitute(dict(parts)) + 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. +} + +\end{document} +"""], +] + +totest_latex_footnotes['multinested'] = [ +# input +["""\ +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. +""", +## # expected output +head_template.substitute(dict(parts)) + 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. +} + +\end{document} +"""], +] + if __name__ == '__main__': unittest.main() -- 2.50.1 |