diff --git a/sphinx/texinputs/footnotehyper-sphinx.sty b/sphinx/texinputs/footnotehyper-sphinx.sty index f8a153c3e9..3bba385a82 100644 --- a/sphinx/texinputs/footnotehyper-sphinx.sty +++ b/sphinx/texinputs/footnotehyper-sphinx.sty @@ -174,9 +174,6 @@ \unrestored@protected@xdef\@thefnmark{\thempfn}% \endgroup \@footnotemark -% Sphinx addition: store internal hyperref anchor reference to allow -% multiple clickable reference to same numbered footnote - \expandafter\xdef\csname FNH!\thesphinxscope!#1\endcsname{\Hy@footnote@currentHref}% \def\FNH@endfntext@fntext{\@footnotetext}% \FNH@startfntext }% @@ -331,34 +328,54 @@ {\ifx\thepage\relax\else\sphinxfootref{#1}\fi}% % \sphinxfootref: % - \spx@opt@BeforeFootnote is from BeforeFootnote sphinxsetup option -% - \ref branch: -% the latex.py writer inserts at start of footnotetext contents -% \phantomsection\label{.}, so we can use \ref which -% hyperref converts into an hyperlink. is the number of -% the scope and the number of the footnote, local to scope. -% - FNH! branch: -% explicitly numbered footnotes in latex.py output always use first -% the footnote environment, then \sphinxfootnotemark. We patched -% \FNH@footnoteenv@i to define the \FNH!! macro as storage -% for the internal hyperref target id for the hyperref anchor. So -% we can then create an hyperlink from any \sphinxfootnotemark with -% this as argument and taking into account current . +% - \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 - \ifcsname FNH!\thesphinxscope!#1\endcsname - \gdef\@thefnmark{#1}% \@thefnmark always redefined globally by latex - \let\spx@saved@makefnmark\@makefnmark - \edef\@makefnmark{% we pre-expand the sphinxscope value, to be safe - \noexpand\hyper@linkstart{link}{\@nameuse{FNH!\thesphinxscope!#1}}% - \noexpand\spx@saved@makefnmark - \noexpand\hyper@linkend + \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@saved@makefnmark - \else - \xdef\@thefnmark{\noexpand\ref{\thesphinxscope.#1}}% #1 = number - \H@@footnotemark - \fi + \let\@makefnmark\spx@@makefnmark + }% }% \AtBeginDocument{% % let hyperref less complain diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index d74e6360b4..57ecdd4414 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -847,11 +847,25 @@ % 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}} +\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 ea2f540980..ac1f1123a1 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -841,10 +841,15 @@ 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\\begin{footnote}[%s]' % label.astext()) + if 'auto' not in node.attributes: + self.body.append('\\phantomsection' + '\\label{\\thesphinxscope.%s}%%\n' % label.astext()) self.body.append('\\sphinxAtStartFootnote\n') def depart_footnote(self, node: Element) -> None: diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index a64de25955..cc79580f65 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 @@ -805,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 @@ -819,7 +822,9 @@ 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' @@ -857,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 @@ -870,7 +877,9 @@ 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' @@ -920,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 @@ -933,7 +944,9 @@ 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'