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

Named auto numbered footnote (ex. ``[#named]``) that is referred
multiple times was rendered to a question mark.

This calls `\sphinxstepexplicit` for every footnote node that is
referred multiple times.
This commit is contained in:
Takeshi KOMIYA 2022-02-06 14:54:21 +09:00
parent 9b142f15e6
commit 1546b21f89
5 changed files with 72 additions and 45 deletions

View File

@ -24,6 +24,8 @@ Bugs fixed
---------- ----------
* #10133: autodoc: Crashed when mocked module is used for type annotation * #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 * #10122: sphinx-build: make.bat does not check the installation of sphinx-build
command before showing help command before showing help

View File

@ -237,7 +237,8 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
blah blah blah ... blah blah blah ...
* Replace second and subsequent footnote references which refers same footnote definition * 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:: Before::
@ -258,7 +259,7 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
After:: After::
blah blah blah blah blah blah
<footnote ids="id1"> <footnote ids="id1" referred=True>
<label> <label>
1 1
<paragraph> <paragraph>
@ -358,7 +359,7 @@ class LaTeXFootnoteTransform(SphinxPostTransform):
class LaTeXFootnoteVisitor(nodes.NodeVisitor): class LaTeXFootnoteVisitor(nodes.NodeVisitor):
def __init__(self, document: nodes.document, footnotes: List[nodes.footnote]) -> None: 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.footnotes: List[nodes.footnote] = footnotes
self.pendings: List[nodes.footnote] = [] self.pendings: List[nodes.footnote] = []
self.table_footnotes: List[nodes.footnote] = [] self.table_footnotes: List[nodes.footnote] = []
@ -439,22 +440,24 @@ class LaTeXFootnoteVisitor(nodes.NodeVisitor):
def visit_footnote_reference(self, node: nodes.footnote_reference) -> None: def visit_footnote_reference(self, node: nodes.footnote_reference) -> None:
number = node.astext().strip() number = node.astext().strip()
docname = node['docname'] docname = node['docname']
if self.restricted: if (docname, number) in self.appeared:
mark = footnotemark('', number, refid=node['refid']) footnote = self.appeared.get((docname, number))
node.replace_self(mark) footnote["referred"] = True
if (docname, number) not in self.appeared:
footnote = self.get_footnote_by_reference(node)
self.pendings.append(footnote)
elif (docname, number) in self.appeared:
mark = footnotemark('', number, refid=node['refid']) mark = footnotemark('', number, refid=node['refid'])
node.replace_self(mark) node.replace_self(mark)
else: else:
footnote = self.get_footnote_by_reference(node) footnote = self.get_footnote_by_reference(node)
self.footnotes.remove(footnote) if self.restricted:
node.replace_self(footnote) mark = footnotemark('', number, refid=node['refid'])
footnote.walkabout(self) 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 raise nodes.SkipNode
def get_footnote_by_reference(self, node: nodes.footnote_reference) -> nodes.footnote: def get_footnote_by_reference(self, node: nodes.footnote_reference) -> nodes.footnote:

View File

@ -856,14 +856,14 @@ class LaTeXTranslator(SphinxTranslator):
def visit_footnote(self, node: Element) -> None: def visit_footnote(self, node: Element) -> None:
self.in_footnote += 1 self.in_footnote += 1
label = cast(nodes.label, node[0]) label = cast(nodes.label, node[0])
if 'auto' not in node: if 'referred' in node:
self.body.append(r'\sphinxstepexplicit ') self.body.append(r'\sphinxstepexplicit ')
if self.in_parsed_literal: if self.in_parsed_literal:
self.body.append(r'\begin{footnote}[%s]' % label.astext()) self.body.append(r'\begin{footnote}[%s]' % label.astext())
else: else:
self.body.append('%' + CR) self.body.append('%' + CR)
self.body.append(r'\begin{footnote}[%s]' % label.astext()) self.body.append(r'\begin{footnote}[%s]' % label.astext())
if 'auto' not in node: if 'referred' in node:
self.body.append(r'\phantomsection' self.body.append(r'\phantomsection'
r'\label{\thesphinxscope.%s}%%' % label.astext() + CR) r'\label{\thesphinxscope.%s}%%' % label.astext() + CR)
self.body.append(r'\sphinxAtStartFootnote' + CR) self.body.append(r'\sphinxAtStartFootnote' + CR)

View File

@ -177,3 +177,12 @@ The section with an object description
.. py:function:: dummy(N) .. py:function:: dummy(N)
:noindex: :noindex:
Footnotes referred twice
========================
* Explicitly numbered footnote: [100]_ [100]_
* Named footnote: [#twice]_ [#twice]_
.. [100] Numbered footnote
.. [#twice] Named footnote

View File

@ -723,9 +723,8 @@ def test_footnote(app, status, warning):
print(result) print(result)
print(status.getvalue()) print(status.getvalue())
print(warning.getvalue()) print(warning.getvalue())
assert ('\\sphinxstepexplicit %\n\\begin{footnote}[1]\\phantomsection' assert ('\\sphinxAtStartPar\n%\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'\\label{\\thesphinxscope.1}%\n\\sphinxAtStartFootnote\nnumbered\n%\n' 'numbered\n%\n\\end{footnote}') in result
'\\end{footnote}') in result
assert ('\\begin{footnote}[2]\\sphinxAtStartFootnote\nauto numbered\n%\n' assert ('\\begin{footnote}[2]\\sphinxAtStartFootnote\nauto numbered\n%\n'
'\\end{footnote}') in result '\\end{footnote}') in result
assert '\\begin{footnote}[3]\\sphinxAtStartFootnote\nnamed\n%\n\\end{footnote}' in result assert '\\begin{footnote}[3]\\sphinxAtStartFootnote\nnamed\n%\n\\end{footnote}' in result
@ -769,13 +768,13 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning):
'\\sphinxAtStartFootnote\n' '\\sphinxAtStartFootnote\n'
'Footnote in section\n%\n\\end{footnotetext}') in result 'Footnote in section\n%\n\\end{footnotetext}') in result
assert ('\\caption{This is the figure caption with a footnote to ' 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]' '%\n\\begin{footnotetext}[8]'
'\\phantomsection\\label{\\thesphinxscope.8}%\n' '\\phantomsection\\label{\\thesphinxscope.8}%\n'
'\\sphinxAtStartFootnote\n' '\\sphinxAtStartFootnote\n'
'Footnote in caption\n%\n\\end{footnotetext}') in result 'Footnote in caption\n%\n\\end{footnotetext}') in result
assert ('\\sphinxcaption{footnote \\sphinxfootnotemark[9] in ' 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] ' assert ('\\caption{footnote \\sphinxfootnotemark[10] '
'in caption \\sphinxfootnotemark[11] of longtable\\strut}') in result 'in caption \\sphinxfootnotemark[11] of longtable\\strut}') in result
assert ('\\endlastfoot\n%\n\\begin{footnotetext}[10]' assert ('\\endlastfoot\n%\n\\begin{footnotetext}[10]'
@ -796,6 +795,26 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning):
assert '\\begin{sphinxVerbatim}[commandchars=\\\\\\{\\}]' in result 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( @pytest.mark.sphinx(
'latex', testroot='footnotes', 'latex', testroot='footnotes',
confoverrides={'latex_show_urls': 'inline'}) confoverrides={'latex_show_urls': 'inline'})
@ -805,25 +824,23 @@ def test_latex_show_urls_is_inline(app, status, warning):
print(result) print(result)
print(status.getvalue()) print(status.getvalue())
print(warning.getvalue()) print(warning.getvalue())
assert ('Same footnote number \\sphinxstepexplicit %\n' assert ('Same footnote number %\n'
'\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n' '\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'\\sphinxAtStartFootnote\n'
'footnote in bar\n%\n\\end{footnote} in bar.rst') in result 'footnote in bar\n%\n\\end{footnote} in bar.rst') in result
assert ('Auto footnote number %\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n' assert ('Auto footnote number %\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'footnote in baz\n%\n\\end{footnote} in baz.rst') in result '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' '{\\hyperref[\\detokenize{index:the-section'
'-with-a-reference-to-authoryear}]' '-with-a-reference-to-authoryear}]'
'{\\sphinxcrossref{The section with a reference to ' '{\\sphinxcrossref{The section with a reference to '
'\\sphinxcite{index:authoryear}}}}') in result '\\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}]' '{\\hyperref[\\detokenize{index:the-section-with-a-reference-to}]'
'{\\sphinxcrossref{The section with a reference to }}}' in result) '{\\sphinxcrossref{The section with a reference to }}}' in result)
assert ('First footnote: %\n\\begin{footnote}[2]\\sphinxAtStartFootnote\n' assert ('First footnote: %\n\\begin{footnote}[2]\\sphinxAtStartFootnote\n'
'First\n%\n\\end{footnote}') in result 'First\n%\n\\end{footnote}') in result
assert ('Second footnote: \\sphinxstepexplicit %\n' assert ('Second footnote: %\n'
'\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n' '\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'\\sphinxAtStartFootnote\n'
'Second\n%\n\\end{footnote}\n') in result 'Second\n%\n\\end{footnote}\n') in result
assert '\\sphinxhref{http://sphinx-doc.org/}{Sphinx} (http://sphinx\\sphinxhyphen{}doc.org/)' 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' assert ('Third footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n'
@ -863,24 +880,22 @@ def test_latex_show_urls_is_footnote(app, status, warning):
print(result) print(result)
print(status.getvalue()) print(status.getvalue())
print(warning.getvalue()) print(warning.getvalue())
assert ('Same footnote number \\sphinxstepexplicit %\n' assert ('Same footnote number %\n'
'\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n' '\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'\\sphinxAtStartFootnote\n'
'footnote in bar\n%\n\\end{footnote} in bar.rst') in result 'footnote in bar\n%\n\\end{footnote} in bar.rst') in result
assert ('Auto footnote number %\n\\begin{footnote}[2]\\sphinxAtStartFootnote\n' assert ('Auto footnote number %\n\\begin{footnote}[2]\\sphinxAtStartFootnote\n'
'footnote in baz\n%\n\\end{footnote} in baz.rst') in result '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}]' '{\\hyperref[\\detokenize{index:the-section-with-a-reference-to-authoryear}]'
'{\\sphinxcrossref{The section with a reference ' '{\\sphinxcrossref{The section with a reference '
'to \\sphinxcite{index:authoryear}}}}') in result '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}]' '{\\hyperref[\\detokenize{index:the-section-with-a-reference-to}]'
'{\\sphinxcrossref{The section with a reference to }}}') in result '{\\sphinxcrossref{The section with a reference to }}}') in result
assert ('First footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n' assert ('First footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n'
'First\n%\n\\end{footnote}') in result 'First\n%\n\\end{footnote}') in result
assert ('Second footnote: \\sphinxstepexplicit %\n' assert ('Second footnote: %\n'
'\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n' '\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'\\sphinxAtStartFootnote\n'
'Second\n%\n\\end{footnote}') in result 'Second\n%\n\\end{footnote}') in result
assert ('\\sphinxhref{http://sphinx-doc.org/}{Sphinx}' assert ('\\sphinxhref{http://sphinx-doc.org/}{Sphinx}'
'%\n\\begin{footnote}[4]\\sphinxAtStartFootnote\n' '%\n\\begin{footnote}[4]\\sphinxAtStartFootnote\n'
@ -932,24 +947,22 @@ def test_latex_show_urls_is_no(app, status, warning):
print(result) print(result)
print(status.getvalue()) print(status.getvalue())
print(warning.getvalue()) print(warning.getvalue())
assert ('Same footnote number \\sphinxstepexplicit %\n' assert ('Same footnote number %\n'
'\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n' '\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'\\sphinxAtStartFootnote\n'
'footnote in bar\n%\n\\end{footnote} in bar.rst') in result 'footnote in bar\n%\n\\end{footnote} in bar.rst') in result
assert ('Auto footnote number %\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n' assert ('Auto footnote number %\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'footnote in baz\n%\n\\end{footnote} in baz.rst') in result '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}]' '{\\hyperref[\\detokenize{index:the-section-with-a-reference-to-authoryear}]'
'{\\sphinxcrossref{The section with a reference ' '{\\sphinxcrossref{The section with a reference '
'to \\sphinxcite{index:authoryear}}}}') in result '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}]' '{\\hyperref[\\detokenize{index:the-section-with-a-reference-to}]'
'{\\sphinxcrossref{The section with a reference to }}}' in result) '{\\sphinxcrossref{The section with a reference to }}}' in result)
assert ('First footnote: %\n\\begin{footnote}[2]\\sphinxAtStartFootnote\n' assert ('First footnote: %\n\\begin{footnote}[2]\\sphinxAtStartFootnote\n'
'First\n%\n\\end{footnote}') in result 'First\n%\n\\end{footnote}') in result
assert ('Second footnote: \\sphinxstepexplicit %\n' assert ('Second footnote: %\n'
'\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n' '\\begin{footnote}[1]\\sphinxAtStartFootnote\n'
'\\sphinxAtStartFootnote\n'
'Second\n%\n\\end{footnote}') in result 'Second\n%\n\\end{footnote}') in result
assert '\\sphinxhref{http://sphinx-doc.org/}{Sphinx}' in result assert '\\sphinxhref{http://sphinx-doc.org/}{Sphinx}' in result
assert ('Third footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n' assert ('Third footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n'