diff --git a/CHANGES b/CHANGES index faee75610..0da36ae19 100644 --- a/CHANGES +++ b/CHANGES @@ -118,6 +118,9 @@ Bugs fixed * #8683: :confval:`html_last_updated_fmt` generates wrong time zone for %Z * #1112: ``download`` role creates duplicated copies when relative path is specified +* #2616 (fifth item): LaTeX: footnotes from captions are not clickable, + and for manually numbered footnotes only first one with same number is + an hyperlink * #7576: LaTeX with French babel and memoir crash: "Illegal parameter number in definition of ``\FNH@prefntext``" * #8055: LaTeX (docs): A potential display bug with the LaTeX generation step diff --git a/sphinx/texinputs/footnotehyper-sphinx.sty b/sphinx/texinputs/footnotehyper-sphinx.sty index 5ed0e8e6e..3bba385a8 100644 --- a/sphinx/texinputs/footnotehyper-sphinx.sty +++ b/sphinx/texinputs/footnotehyper-sphinx.sty @@ -325,8 +325,58 @@ % some extras for Sphinx : % \sphinxfootnotemark: usable in section titles and silently removed from TOCs. \def\sphinxfootnotemark [#1]% - {\ifx\thepage\relax\else\protect\spx@opt@BeforeFootnote - \protect\footnotemark[#1]\fi}% + {\ifx\thepage\relax\else\sphinxfootref{#1}\fi}% +% \sphinxfootref: +% - \spx@opt@BeforeFootnote is from BeforeFootnote sphinxsetup option +% - \ref: +% the latex.py writer inserts a \phantomsection\label{.} +% whenever +% - the footnote was explicitly numbered in sources, +% - or it was in restrained context and is rendered using footnotetext +% +% These are the two types of footnotes that \sphinxfootnotemark must +% handle. But for explicitly numbered footnotes the same number +% can be found in document. So a secondary part in is updated +% at each novel such footnote to know what is the target from then on +% for \sphinxfootnotemark and already encountered [1], or [2],... +% +% LaTeX package varioref is not supported by hyperref (from its doc: "There +% are too many problems with varioref. Nobody has time to sort them out. +% Therefore this package is now unsupported.") So we will simply use our own +% macros to access the page number of footnote text and decide whether to print +% it. \pagename is internationalized by latex-babel. +\def\spx@thefnmark#1#2{% + % #1=label for reference, #2=page where footnote was printed + \ifx\spx@tempa\spx@tempb + % same page + #1% + \else + \sphinxthefootnotemark{#1}{#2}% + \fi +}% +\def\sphinxfootref@get #1#2#3#4#5\relax{% + \def\sphinxfootref@label{#1}% + \def\sphinxfootref@page {#2}% + \def\sphinxfootref@Href {#4}% +}% +\protected\def\sphinxfootref#1{% #1 always explicit number in Sphinx usage + \spx@opt@BeforeFootnote + \ltx@ifundefined{r@\thesphinxscope.#1}% + {\gdef\@thefnmark{?}\H@@footnotemark}% + {\expandafter\expandafter\expandafter\sphinxfootref@get + \csname r@\thesphinxscope.#1\endcsname\relax + \edef\spx@tempa{\thepage}\edef\spx@tempb{\sphinxfootref@page}% + \protected@xdef\@thefnmark{\spx@thefnmark{\sphinxfootref@label}{\sphinxfootref@page}}% + \let\spx@@makefnmark\@makefnmark + \def\@makefnmark{% + \hyper@linkstart{link}{\sphinxfootref@Href}% + \spx@@makefnmark + \hyper@linkend + }% + \H@@footnotemark + \let\@makefnmark\spx@@makefnmark + }% +}% \AtBeginDocument{% % let hyperref less complain \pdfstringdefDisableCommands{\def\sphinxfootnotemark [#1]{}}% diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index adc9a23a7..556302930 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -847,6 +847,28 @@ %% FOOTNOTES % +% Support scopes for footnote numbering +\newcounter{sphinxscope} +\newcommand{\sphinxstepscope}{\stepcounter{sphinxscope}} +% Explictly 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 +\newcounter{sphinxexplicit} +\newcommand{\sphinxstepexplicit}{\stepcounter{sphinxexplicit}} +% Some babel/polyglossia languages fiddle with \@arabic, so let's be extra +% cautious and redefine \thesphinxscope with \number not \@arabic. +% Memo: we expect some subtle redefinition of \thesphinxscope to be a part of page +% scoping for footnotes, when we shall implement it. +\renewcommand{\thesphinxscope}{\number\value{sphinxscope}.\number\value{sphinxexplicit}} +\newcommand\sphinxthefootnotemark[2]{% + % this is used to make reference to an explicitly numbered footnote not on same page + % #1=label of footnote text, #2=page number where footnote text was printed + \ifdefined\pagename + \pagename\space#2, % <- space + \else + p. #2, % <- space + \fi #1% no space +} % Support large numbered footnotes in minipage % But now obsolete due to systematic use of \savenotes/\spewnotes % when minipages are in use in the various macro definitions next. diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 8319cbbfe..cf166fb21 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -846,11 +846,16 @@ class LaTeXTranslator(SphinxTranslator): def visit_footnote(self, node: Element) -> None: self.in_footnote += 1 label = cast(nodes.label, node[0]) + if 'auto' not in node.attributes: + self.body.append('\\sphinxstepexplicit ') if self.in_parsed_literal: self.body.append('\\begin{footnote}[%s]' % label.astext()) else: self.body.append('%\n') self.body.append('\\begin{footnote}[%s]' % label.astext()) + if 'auto' not in node: + self.body.append('\\phantomsection' + '\\label{\\thesphinxscope.%s}%%\n' % label.astext()) self.body.append('\\sphinxAtStartFootnote\n') def depart_footnote(self, node: Element) -> None: @@ -1749,7 +1754,9 @@ class LaTeXTranslator(SphinxTranslator): label = cast(nodes.label, node[0]) self.body.append('%\n') self.body.append('\\begin{footnotetext}[%s]' - '\\sphinxAtStartFootnote\n' % label.astext()) + '\\phantomsection\\label{\\thesphinxscope.%s}%%\n' + % (label.astext(), label.astext())) + self.body.append('\\sphinxAtStartFootnote\n') def depart_footnotetext(self, node: Element) -> None: # the \ignorespaces in particular for after table header use diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index f268064c6..cc79580f6 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -724,7 +724,8 @@ def test_footnote(app, status, warning): print(result) print(status.getvalue()) print(warning.getvalue()) - assert ('\\begin{footnote}[1]\\sphinxAtStartFootnote\nnumbered\n%\n' + assert ('\\sphinxstepexplicit %\n\\begin{footnote}[1]\\phantomsection' + '\\label{\\thesphinxscope.1}%\n\\sphinxAtStartFootnote\nnumbered\n%\n' '\\end{footnote}') in result assert ('\\begin{footnote}[2]\\sphinxAtStartFootnote\nauto numbered\n%\n' '\\end{footnote}') in result @@ -732,9 +733,13 @@ def test_footnote(app, status, warning): assert '\\sphinxcite{footnote:bar}' in result assert ('\\bibitem[bar]{footnote:bar}\n\\sphinxAtStartPar\ncite\n') in result assert '\\sphinxcaption{Table caption \\sphinxfootnotemark[4]' in result - assert ('\\hline%\n\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n' + assert ('\\hline%\n\\begin{footnotetext}[4]' + '\\phantomsection\\label{\\thesphinxscope.4}%\n' + '\\sphinxAtStartFootnote\n' 'footnote in table caption\n%\n\\end{footnotetext}\\ignorespaces %\n' - '\\begin{footnotetext}[5]\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[5]' + '\\phantomsection\\label{\\thesphinxscope.5}%\n' + '\\sphinxAtStartFootnote\n' 'footnote in table header\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\n' 'VIDIOC\\_CROPCAP\n&\n\\sphinxAtStartPar\n') in result @@ -760,19 +765,27 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning): assert '\\subsubsection*{The rubric title with a reference to {[}AuthorYear{]}}' in result assert ('\\chapter{The section with a reference to \\sphinxfootnotemark[5]}\n' '\\label{\\detokenize{index:the-section-with-a-reference-to}}' - '%\n\\begin{footnotetext}[5]\\sphinxAtStartFootnote\n' + '%\n\\begin{footnotetext}[5]' + '\\phantomsection\\label{\\thesphinxscope.5}%\n' + '\\sphinxAtStartFootnote\n' 'Footnote in section\n%\n\\end{footnotetext}') in result assert ('\\caption{This is the figure caption with a footnote to ' '\\sphinxfootnotemark[7].}\\label{\\detokenize{index:id29}}\\end{figure}\n' - '%\n\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n' + '%\n\\begin{footnotetext}[7]' + '\\phantomsection\\label{\\thesphinxscope.7}%\n' + '\\sphinxAtStartFootnote\n' 'Footnote in caption\n%\n\\end{footnotetext}') in result assert ('\\sphinxcaption{footnote \\sphinxfootnotemark[8] in ' 'caption of normal table}\\label{\\detokenize{index:id30}}') in result assert ('\\caption{footnote \\sphinxfootnotemark[9] ' 'in caption \\sphinxfootnotemark[10] of longtable\\strut}') in result - assert ('\\endlastfoot\n%\n\\begin{footnotetext}[9]\\sphinxAtStartFootnote\n' + assert ('\\endlastfoot\n%\n\\begin{footnotetext}[9]' + '\\phantomsection\\label{\\thesphinxscope.9}%\n' + '\\sphinxAtStartFootnote\n' 'Foot note in longtable\n%\n\\end{footnotetext}\\ignorespaces %\n' - '\\begin{footnotetext}[10]\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[10]' + '\\phantomsection\\label{\\thesphinxscope.10}%\n' + '\\sphinxAtStartFootnote\n' 'Second footnote in caption of longtable\n') in result assert ('This is a reference to the code\\sphinxhyphen{}block in the footnote:\n' '{\\hyperref[\\detokenize{index:codeblockinfootnote}]' @@ -793,7 +806,9 @@ def test_latex_show_urls_is_inline(app, status, warning): print(result) print(status.getvalue()) print(warning.getvalue()) - assert ('Same footnote number %\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n' + assert ('Same footnote number \\sphinxstepexplicit %\n' + '\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n' + '\\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 @@ -807,12 +822,16 @@ def test_latex_show_urls_is_inline(app, status, warning): '{\\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: %\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n' + assert ('Second footnote: \\sphinxstepexplicit %\n' + '\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n' + '\\sphinxAtStartFootnote\n' 'Second\n%\n\\end{footnote}') 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' 'Third \\sphinxfootnotemark[4]\n%\n\\end{footnote}%\n' - '\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[4]' + '\\phantomsection\\label{\\thesphinxscope.4}%\n' + '\\sphinxAtStartFootnote\n' 'Footnote inside footnote\n%\n\\end{footnotetext}\\ignorespaces') in result assert ('\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde} ' '(http://sphinx\\sphinxhyphen{}doc.org/\\textasciitilde{}test/)') in result @@ -820,7 +839,9 @@ def test_latex_show_urls_is_inline(app, status, warning): '(http://sphinx\\sphinxhyphen{}doc.org/)}] ' '\\leavevmode\n\\sphinxAtStartPar\nDescription' in result) assert ('\\item[{Footnote in term \\sphinxfootnotemark[6]}] ' - '\\leavevmode%\n\\begin{footnotetext}[6]\\sphinxAtStartFootnote\n' + '\\leavevmode%\n\\begin{footnotetext}[6]' + '\\phantomsection\\label{\\thesphinxscope.6}%\n' + '\\sphinxAtStartFootnote\n' 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\nDescription') in result assert ('\\item[{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist} ' @@ -841,7 +862,9 @@ def test_latex_show_urls_is_footnote(app, status, warning): print(result) print(status.getvalue()) print(warning.getvalue()) - assert ('Same footnote number %\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n' + assert ('Same footnote number \\sphinxstepexplicit %\n' + '\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n' + '\\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 @@ -854,14 +877,18 @@ def test_latex_show_urls_is_footnote(app, status, warning): '{\\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: %\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n' + assert ('Second footnote: \\sphinxstepexplicit %\n' + '\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n' + '\\sphinxAtStartFootnote\n' 'Second\n%\n\\end{footnote}') in result assert ('\\sphinxhref{http://sphinx-doc.org/}{Sphinx}' '%\n\\begin{footnote}[4]\\sphinxAtStartFootnote\n' '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n\\end{footnote}') in result assert ('Third footnote: %\n\\begin{footnote}[6]\\sphinxAtStartFootnote\n' 'Third \\sphinxfootnotemark[7]\n%\n\\end{footnote}%\n' - '\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[7]' + '\\phantomsection\\label{\\thesphinxscope.7}%\n' + '\\sphinxAtStartFootnote\n' 'Footnote inside footnote\n%\n' '\\end{footnotetext}\\ignorespaces') in result assert ('\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde}' @@ -869,16 +896,22 @@ def test_latex_show_urls_is_footnote(app, status, warning): '\\sphinxnolinkurl{http://sphinx-doc.org/~test/}\n%\n\\end{footnote}') in result assert ('\\item[{\\sphinxhref{http://sphinx-doc.org/}' '{URL in term}\\sphinxfootnotemark[9]}] ' - '\\leavevmode%\n\\begin{footnotetext}[9]\\sphinxAtStartFootnote\n' + '\\leavevmode%\n\\begin{footnotetext}[9]' + '\\phantomsection\\label{\\thesphinxscope.9}%\n' + '\\sphinxAtStartFootnote\n' '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n' '\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result assert ('\\item[{Footnote in term \\sphinxfootnotemark[11]}] ' - '\\leavevmode%\n\\begin{footnotetext}[11]\\sphinxAtStartFootnote\n' + '\\leavevmode%\n\\begin{footnotetext}[11]' + '\\phantomsection\\label{\\thesphinxscope.11}%\n' + '\\sphinxAtStartFootnote\n' 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\nDescription') in result assert ('\\item[{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist}' '\\sphinxfootnotemark[10]}] ' - '\\leavevmode%\n\\begin{footnotetext}[10]\\sphinxAtStartFootnote\n' + '\\leavevmode%\n\\begin{footnotetext}[10]' + '\\phantomsection\\label{\\thesphinxscope.10}%\n' + '\\sphinxAtStartFootnote\n' '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n' '\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result assert ('\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result) @@ -896,7 +929,9 @@ def test_latex_show_urls_is_no(app, status, warning): print(result) print(status.getvalue()) print(warning.getvalue()) - assert ('Same footnote number %\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n' + assert ('Same footnote number \\sphinxstepexplicit %\n' + '\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n' + '\\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 @@ -909,18 +944,24 @@ def test_latex_show_urls_is_no(app, status, warning): '{\\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: %\n\\begin{footnote}[1]\\sphinxAtStartFootnote\n' + assert ('Second footnote: \\sphinxstepexplicit %\n' + '\\begin{footnote}[1]\\phantomsection\\label{\\thesphinxscope.1}%\n' + '\\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' 'Third \\sphinxfootnotemark[4]\n%\n\\end{footnote}%\n' - '\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[4]' + '\\phantomsection\\label{\\thesphinxscope.4}%\n' + '\\sphinxAtStartFootnote\n' 'Footnote inside footnote\n%\n\\end{footnotetext}\\ignorespaces') in result assert '\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde}' in result assert ('\\item[{\\sphinxhref{http://sphinx-doc.org/}{URL in term}}] ' '\\leavevmode\n\\sphinxAtStartPar\nDescription') in result assert ('\\item[{Footnote in term \\sphinxfootnotemark[6]}] ' - '\\leavevmode%\n\\begin{footnotetext}[6]\\sphinxAtStartFootnote\n' + '\\leavevmode%\n\\begin{footnotetext}[6]' + '\\phantomsection\\label{\\thesphinxscope.6}%\n' + '\\sphinxAtStartFootnote\n' 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\nDescription') in result assert ('\\item[{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist}}] '