From 4a239bb886ffdf9e3ce9c78e0dfbc559ff043ad5 Mon Sep 17 00:00:00 2001 From: jfbu Date: Sat, 6 Feb 2021 19:34:44 +0100 Subject: [PATCH] Extra references to explicitly numbered footnote get page indication To achieve this the simplest was to use also the \label/\ref mechanism as for footnotes typeset using footnotetext. Removing the hack into footnotehyper internal macro, we use an enriched scope, which will enable references with same number to each generate correct links. --- sphinx/texinputs/footnotehyper-sphinx.sty | 71 ++++++++++++++--------- sphinx/texinputs/sphinx.sty | 16 ++++- sphinx/writers/latex.py | 5 ++ tests/test_build_latex.py | 27 ++++++--- 4 files changed, 84 insertions(+), 35 deletions(-) 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'