Skip to content

Commit

Permalink
Merge pull request #10169 from tk0miya/9529_named_footnotes_in_latex
Browse files Browse the repository at this point in the history
Fix #9529: LaTeX: named footnotes are converted to "?"
  • Loading branch information
tk0miya committed Feb 11, 2022
2 parents b6931ac + 777b1fa commit a32d609
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 48 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Expand Up @@ -26,6 +26,8 @@ Bugs fixed
* #10133: autodoc: Crashed when mocked module is used for type annotation
* #10146: autodoc: :confval:`autodoc_default_options` does not support
``no-value`` option
* #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
6 changes: 3 additions & 3 deletions sphinx/texinputs/sphinx.sty
Expand Up @@ -300,9 +300,9 @@
% Support scopes for footnote numbering
\newcounter{sphinxscope}
\newcommand{\sphinxstepscope}{\stepcounter{sphinxscope}}
% Explicitly numbered footnotes may be referred to, and for this to be
% clickable we need to have only one target. So we will step this at each
% explicit footnote and let \thesphinxscope take it into account
% Some footnotes are multiply referred-to. For unique hypertarget in pdf,
% we need an additional counter. It is called "sphinxexplicit" for legacy
% reasons as "explicitly" numbered footnotes may be multiply referred-to.
\newcounter{sphinxexplicit}
\newcommand{\sphinxstepexplicit}{\stepcounter{sphinxexplicit}}
% Some babel/polyglossia languages fiddle with \@arabic, so let's be extra
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

0 comments on commit a32d609

Please sign in to comment.