Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #9529: LaTeX: named footnotes are converted to "?" #10169

Merged
merged 3 commits into from Feb 11, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES
Expand Up @@ -24,6 +24,8 @@ Bugs fixed
----------

* #10133: autodoc: Crashed when mocked module is used for type annotation
* #9529: LaTeX: named auto numbered footnote (ex. ``[#named]``) that is referred
multiple times was rendered to a question mark
* #10122: sphinx-build: make.bat does not check the installation of sphinx-build
command before showing help

Expand Down
31 changes: 17 additions & 14 deletions sphinx/builders/latex/transforms.py
Expand Up @@ -237,7 +237,8 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
blah blah blah ...

* Replace second and subsequent footnote references which refers same footnote definition
by footnotemark node.
by footnotemark node. Additionally, the footnote definition node is marked as
"referred".

Before::

Expand All @@ -258,7 +259,7 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
After::

blah blah blah
<footnote ids="id1">
<footnote ids="id1" referred=True>
<label>
1
<paragraph>
Expand Down Expand Up @@ -358,7 +359,7 @@ def run(self, **kwargs: Any) -> None:

class LaTeXFootnoteVisitor(nodes.NodeVisitor):
def __init__(self, document: nodes.document, footnotes: List[nodes.footnote]) -> None:
self.appeared: Set[Tuple[str, str]] = set()
self.appeared: Dict[Tuple[str, str], nodes.footnote] = {}
self.footnotes: List[nodes.footnote] = footnotes
self.pendings: List[nodes.footnote] = []
self.table_footnotes: List[nodes.footnote] = []
Expand Down Expand Up @@ -439,22 +440,24 @@ def depart_footnote(self, node: nodes.footnote) -> None:
def visit_footnote_reference(self, node: nodes.footnote_reference) -> None:
number = node.astext().strip()
docname = node['docname']
if self.restricted:
mark = footnotemark('', number, refid=node['refid'])
node.replace_self(mark)
if (docname, number) not in self.appeared:
footnote = self.get_footnote_by_reference(node)
self.pendings.append(footnote)
elif (docname, number) in self.appeared:
if (docname, number) in self.appeared:
footnote = self.appeared.get((docname, number))
footnote["referred"] = True

mark = footnotemark('', number, refid=node['refid'])
node.replace_self(mark)
else:
footnote = self.get_footnote_by_reference(node)
self.footnotes.remove(footnote)
node.replace_self(footnote)
footnote.walkabout(self)
if self.restricted:
mark = footnotemark('', number, refid=node['refid'])
node.replace_self(mark)
self.pendings.append(footnote)
else:
self.footnotes.remove(footnote)
node.replace_self(footnote)
footnote.walkabout(self)

self.appeared.add((docname, number))
self.appeared[(docname, number)] = footnote
raise nodes.SkipNode

def get_footnote_by_reference(self, node: nodes.footnote_reference) -> nodes.footnote:
Expand Down
4 changes: 2 additions & 2 deletions sphinx/writers/latex.py
Expand Up @@ -856,14 +856,14 @@ def depart_rubric(self, node: Element) -> None:
def visit_footnote(self, node: Element) -> None:
self.in_footnote += 1
label = cast(nodes.label, node[0])
if 'auto' not in node:
if 'referred' in node:
self.body.append(r'\sphinxstepexplicit ')
if self.in_parsed_literal:
self.body.append(r'\begin{footnote}[%s]' % label.astext())
else:
self.body.append('%' + CR)
self.body.append(r'\begin{footnote}[%s]' % label.astext())
if 'auto' not in node:
if 'referred' in node:
self.body.append(r'\phantomsection'
r'\label{\thesphinxscope.%s}%%' % label.astext() + CR)
self.body.append(r'\sphinxAtStartFootnote' + CR)
Expand Down
9 changes: 9 additions & 0 deletions tests/roots/test-footnotes/index.rst
Expand Up @@ -177,3 +177,12 @@ The section with an object description

.. py:function:: dummy(N)
:noindex:

Footnotes referred twice
========================

* Explicitly numbered footnote: [100]_ [100]_
* Named footnote: [#twice]_ [#twice]_

.. [100] Numbered footnote
.. [#twice] Named footnote
71 changes: 42 additions & 29 deletions tests/test_build_latex.py
Expand Up @@ -723,9 +723,8 @@ def test_footnote(app, status, warning):
print(result)
print(status.getvalue())
print(warning.getvalue())
assert ('\\sphinxstepexplicit %\n\\begin{footnote}[1]\\phantomsection'
'\\label{\\thesphinxscope.1}%\n\\sphinxAtStartFootnote\nnumbered\n%\n'
'\\end{footnote}') in result
assert ('\\sphinxAtStartPar\n%\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'numbered\n%\n\\end{footnote}') in result
assert ('\\begin{footnote}[2]\\sphinxAtStartFootnote\nauto numbered\n%\n'
'\\end{footnote}') in result
assert '\\begin{footnote}[3]\\sphinxAtStartFootnote\nnamed\n%\n\\end{footnote}' in result
Expand Down Expand Up @@ -769,13 +768,13 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning):
'\\sphinxAtStartFootnote\n'
'Footnote in section\n%\n\\end{footnotetext}') in result
assert ('\\caption{This is the figure caption with a footnote to '
'\\sphinxfootnotemark[8].}\\label{\\detokenize{index:id30}}\\end{figure}\n'
'\\sphinxfootnotemark[8].}\\label{\\detokenize{index:id35}}\\end{figure}\n'
'%\n\\begin{footnotetext}[8]'
'\\phantomsection\\label{\\thesphinxscope.8}%\n'
'\\sphinxAtStartFootnote\n'
'Footnote in caption\n%\n\\end{footnotetext}') in result
assert ('\\sphinxcaption{footnote \\sphinxfootnotemark[9] in '
'caption of normal table}\\label{\\detokenize{index:id31}}') in result
'caption of normal table}\\label{\\detokenize{index:id36}}') in result
assert ('\\caption{footnote \\sphinxfootnotemark[10] '
'in caption \\sphinxfootnotemark[11] of longtable\\strut}') in result
assert ('\\endlastfoot\n%\n\\begin{footnotetext}[10]'
Expand All @@ -796,6 +795,26 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning):
assert '\\begin{sphinxVerbatim}[commandchars=\\\\\\{\\}]' in result


@pytest.mark.sphinx('latex', testroot='footnotes')
def test_footnote_referred_multiple_times(app, status, warning):
app.builder.build_all()
result = (app.outdir / 'python.tex').read_text()
print(result)
print(status.getvalue())
print(warning.getvalue())

assert ('Explicitly numbered footnote: \\sphinxstepexplicit %\n'
'\\begin{footnote}[100]\\phantomsection\\label{\\thesphinxscope.100}%\n'
'\\sphinxAtStartFootnote\nNumbered footnote\n%\n'
'\\end{footnote} \\sphinxfootnotemark[100]\n'
in result)
assert ('Named footnote: \\sphinxstepexplicit %\n'
'\\begin{footnote}[13]\\phantomsection\\label{\\thesphinxscope.13}%\n'
'\\sphinxAtStartFootnote\nNamed footnote\n%\n'
'\\end{footnote} \\sphinxfootnotemark[13]\n'
in result)


@pytest.mark.sphinx(
'latex', testroot='footnotes',
confoverrides={'latex_show_urls': 'inline'})
Expand All @@ -805,25 +824,23 @@ def test_latex_show_urls_is_inline(app, status, warning):
print(result)
print(status.getvalue())
print(warning.getvalue())
assert ('Same footnote number \\sphinxstepexplicit %\n'
'\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n'
'\\sphinxAtStartFootnote\n'
assert ('Same footnote number %\n'
'\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'footnote in bar\n%\n\\end{footnote} in bar.rst') in result
assert ('Auto footnote number %\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'footnote in baz\n%\n\\end{footnote} in baz.rst') in result
assert ('\\phantomsection\\label{\\detokenize{index:id33}}'
assert ('\\phantomsection\\label{\\detokenize{index:id38}}'
'{\\hyperref[\\detokenize{index:the-section'
'-with-a-reference-to-authoryear}]'
'{\\sphinxcrossref{The section with a reference to '
'\\sphinxcite{index:authoryear}}}}') in result
assert ('\\phantomsection\\label{\\detokenize{index:id34}}'
assert ('\\phantomsection\\label{\\detokenize{index:id39}}'
'{\\hyperref[\\detokenize{index:the-section-with-a-reference-to}]'
'{\\sphinxcrossref{The section with a reference to }}}' in result)
assert ('First footnote: %\n\\begin{footnote}[2]\\sphinxAtStartFootnote\n'
'First\n%\n\\end{footnote}') in result
assert ('Second footnote: \\sphinxstepexplicit %\n'
'\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n'
'\\sphinxAtStartFootnote\n'
assert ('Second footnote: %\n'
'\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'Second\n%\n\\end{footnote}\n') in result
assert '\\sphinxhref{http://sphinx-doc.org/}{Sphinx} (http://sphinx\\sphinxhyphen{}doc.org/)' in result
assert ('Third footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n'
Expand Down Expand Up @@ -863,24 +880,22 @@ def test_latex_show_urls_is_footnote(app, status, warning):
print(result)
print(status.getvalue())
print(warning.getvalue())
assert ('Same footnote number \\sphinxstepexplicit %\n'
'\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n'
'\\sphinxAtStartFootnote\n'
assert ('Same footnote number %\n'
'\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'footnote in bar\n%\n\\end{footnote} in bar.rst') in result
assert ('Auto footnote number %\n\\begin{footnote}[2]\\sphinxAtStartFootnote\n'
'footnote in baz\n%\n\\end{footnote} in baz.rst') in result
assert ('\\phantomsection\\label{\\detokenize{index:id33}}'
assert ('\\phantomsection\\label{\\detokenize{index:id38}}'
'{\\hyperref[\\detokenize{index:the-section-with-a-reference-to-authoryear}]'
'{\\sphinxcrossref{The section with a reference '
'to \\sphinxcite{index:authoryear}}}}') in result
assert ('\\phantomsection\\label{\\detokenize{index:id34}}'
assert ('\\phantomsection\\label{\\detokenize{index:id39}}'
'{\\hyperref[\\detokenize{index:the-section-with-a-reference-to}]'
'{\\sphinxcrossref{The section with a reference to }}}') in result
assert ('First footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n'
'First\n%\n\\end{footnote}') in result
assert ('Second footnote: \\sphinxstepexplicit %\n'
'\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n'
'\\sphinxAtStartFootnote\n'
assert ('Second footnote: %\n'
'\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'Second\n%\n\\end{footnote}') in result
assert ('\\sphinxhref{http://sphinx-doc.org/}{Sphinx}'
'%\n\\begin{footnote}[4]\\sphinxAtStartFootnote\n'
Expand Down Expand Up @@ -932,24 +947,22 @@ def test_latex_show_urls_is_no(app, status, warning):
print(result)
print(status.getvalue())
print(warning.getvalue())
assert ('Same footnote number \\sphinxstepexplicit %\n'
'\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n'
'\\sphinxAtStartFootnote\n'
assert ('Same footnote number %\n'
'\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'footnote in bar\n%\n\\end{footnote} in bar.rst') in result
assert ('Auto footnote number %\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'footnote in baz\n%\n\\end{footnote} in baz.rst') in result
assert ('\\phantomsection\\label{\\detokenize{index:id33}}'
assert ('\\phantomsection\\label{\\detokenize{index:id38}}'
'{\\hyperref[\\detokenize{index:the-section-with-a-reference-to-authoryear}]'
'{\\sphinxcrossref{The section with a reference '
'to \\sphinxcite{index:authoryear}}}}') in result
assert ('\\phantomsection\\label{\\detokenize{index:id34}}'
assert ('\\phantomsection\\label{\\detokenize{index:id39}}'
'{\\hyperref[\\detokenize{index:the-section-with-a-reference-to}]'
'{\\sphinxcrossref{The section with a reference to }}}' in result)
assert ('First footnote: %\n\\begin{footnote}[2]\\sphinxAtStartFootnote\n'
'First\n%\n\\end{footnote}') in result
assert ('Second footnote: \\sphinxstepexplicit %\n'
'\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n'
'\\sphinxAtStartFootnote\n'
assert ('Second footnote: %\n'
'\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'Second\n%\n\\end{footnote}') in result
assert '\\sphinxhref{http://sphinx-doc.org/}{Sphinx}' in result
assert ('Third footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n'
Expand Down