From a0b0a08e38d59c9751f837e6a3f41a980f6aa18d Mon Sep 17 00:00:00 2001 From: jfbu Date: Fri, 5 Feb 2021 14:56:23 +0100 Subject: [PATCH 01/10] LaTeX footnotes from caption titles now hyperlinked in PDF This is fifth item of #2616 --- sphinx/texinputs/footnotehyper-sphinx.sty | 13 +++++-- sphinx/writers/latex.py | 3 +- tests/test_build_latex.py | 42 +++++++++++++++-------- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/sphinx/texinputs/footnotehyper-sphinx.sty b/sphinx/texinputs/footnotehyper-sphinx.sty index 5ed0e8e6e..8fce532b1 100644 --- a/sphinx/texinputs/footnotehyper-sphinx.sty +++ b/sphinx/texinputs/footnotehyper-sphinx.sty @@ -325,8 +325,17 @@ % 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: almost same as \footref from footmisc package as +% re-defined by hyperref: +% - \spx@opt@BeforeFootnote is from BeforeFootnote sphinxsetup option +% - 'fn:' prefix is output by LaTeX writer in \label of footnotetext +\protected\def\sphinxfootref#1{\spx@opt@BeforeFootnote + \begingroup + \unrestored@protected@xdef\@thefnmark{\ref{fn:#1}}% #1 = number + \endgroup + \H@@footnotemark +}% \AtBeginDocument{% % let hyperref less complain \pdfstringdefDisableCommands{\def\sphinxfootnotemark [#1]{}}% diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index b3ec5de56..79619c6ad 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -1738,7 +1738,8 @@ class LaTeXTranslator(SphinxTranslator): def visit_footnotetext(self, node: Element) -> None: label = cast(nodes.label, node[0]) self.body.append('%%\n\\begin{footnotetext}[%s]' - '\\sphinxAtStartFootnote\n' % label.astext()) + '\\phantomsection\\label{fn:%s}%%\n' + '\\sphinxAtStartFootnote\n' % (label.astext(), label.astext())) 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..ad245e5f6 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -732,9 +732,11 @@ 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{fn:4}%\n' + '\\sphinxAtStartFootnote\n' 'footnote in table caption\n%\n\\end{footnotetext}\\ignorespaces %\n' - '\\begin{footnotetext}[5]\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[5]\\phantomsection\\label{fn: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 +762,23 @@ 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{fn: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{fn: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{fn:9}%\n' + '\\sphinxAtStartFootnote\n' 'Foot note in longtable\n%\n\\end{footnotetext}\\ignorespaces %\n' - '\\begin{footnotetext}[10]\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[10]\\phantomsection\\label{fn: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}]' @@ -812,7 +818,8 @@ def test_latex_show_urls_is_inline(app, status, warning): 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{fn: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 +827,8 @@ 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{fn: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} ' @@ -861,7 +869,8 @@ def test_latex_show_urls_is_footnote(app, status, warning): '\\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{fn: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 +878,19 @@ 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{fn: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{fn: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{fn: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) @@ -914,13 +926,15 @@ def test_latex_show_urls_is_no(app, status, warning): 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{fn: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{fn: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}}] ' From eb91fd292f4cebc139d1139f2a34bdd3abb881ba Mon Sep 17 00:00:00 2001 From: jfbu Date: Fri, 5 Feb 2021 16:49:41 +0100 Subject: [PATCH 02/10] Update CHANGES --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 9e3e29fe0..42fe77104 100644 --- a/CHANGES +++ b/CHANGES @@ -114,6 +114,7 @@ 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 * #7576: LaTeX with French babel and memoir crash: "Illegal parameter number in definition of ``\FNH@prefntext``" * #8072: LaTeX: Directive :rst:dir:`hlist` not implemented in LaTeX From 98f4732a98956879796cfba476392fffc8913d55 Mon Sep 17 00:00:00 2001 From: jfbu Date: Fri, 5 Feb 2021 20:26:17 +0100 Subject: [PATCH 03/10] LaTeX: make all references to explicitly numbered footnote clickable --- sphinx/texinputs/footnotehyper-sphinx.sty | 41 ++++++++++++++++++----- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/sphinx/texinputs/footnotehyper-sphinx.sty b/sphinx/texinputs/footnotehyper-sphinx.sty index 8fce532b1..156152ea4 100644 --- a/sphinx/texinputs/footnotehyper-sphinx.sty +++ b/sphinx/texinputs/footnotehyper-sphinx.sty @@ -174,6 +174,9 @@ \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@!#1\endcsname{\Hy@footnote@currentHref}% \def\FNH@endfntext@fntext{\@footnotetext}% \FNH@startfntext }% @@ -326,15 +329,37 @@ % \sphinxfootnotemark: usable in section titles and silently removed from TOCs. \def\sphinxfootnotemark [#1]% {\ifx\thepage\relax\else\sphinxfootref{#1}\fi}% -% \sphinxfootref: almost same as \footref from footmisc package as -% re-defined by hyperref: +% \sphinxfootref: inspired by \footref from footmisc package as +% re-defined by hyperref, but with an extra FNH@! branch: % - \spx@opt@BeforeFootnote is from BeforeFootnote sphinxsetup option -% - 'fn:' prefix is output by LaTeX writer in \label of footnotetext -\protected\def\sphinxfootref#1{\spx@opt@BeforeFootnote - \begingroup - \unrestored@protected@xdef\@thefnmark{\ref{fn:#1}}% #1 = number - \endgroup - \H@@footnotemark +% - \ref{fn:#1} branch: +% the latex.py writer inserts at start of footnotetext contents +% \phantomsection\label{fn:}, so we can use \ref which +% hyperref converts into an hyperlink. This is basically the +% idea of the \footref macro from package footmisc. +% - 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. +\protected\def\sphinxfootref#1{% + \spx@opt@BeforeFootnote + \ifcsname FNH@!#1\endcsname + \def\@thefnmark{#1}% + \let\spx@saved@makefnmark\@makefnmark + \def\@makefnmark{% + \hyper@linkstart{link}{\@nameuse{FNH@!#1}}% + \spx@saved@makefnmark + \hyper@linkend + }% + \H@@footnotemark + \let\@makefnmark\spx@saved@makefnmark + \else + \def\@thefnmark{\ref{fn:#1}}% #1 = number + \H@@footnotemark + \fi }% \AtBeginDocument{% % let hyperref less complain From 9c823478e01645aff448f91d83a9d5eb89dbaf6e Mon Sep 17 00:00:00 2001 From: jfbu Date: Fri, 5 Feb 2021 20:34:15 +0100 Subject: [PATCH 04/10] Update CHANGES --- CHANGES | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 42fe77104..3c0645cb9 100644 --- a/CHANGES +++ b/CHANGES @@ -114,7 +114,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 +* #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``" * #8072: LaTeX: Directive :rst:dir:`hlist` not implemented in LaTeX From 628c58abef12b25d491f234fef4da0b347cee0dc Mon Sep 17 00:00:00 2001 From: jfbu Date: Sat, 6 Feb 2021 10:45:50 +0100 Subject: [PATCH 05/10] Prepare for scope-limited footnote numbering --- sphinx/texinputs/footnotehyper-sphinx.sty | 35 +++++++++---------- sphinx/texinputs/sphinx.sty | 2 ++ sphinx/writers/latex.py | 2 +- tests/test_build_latex.py | 42 +++++++++++++++-------- 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/sphinx/texinputs/footnotehyper-sphinx.sty b/sphinx/texinputs/footnotehyper-sphinx.sty index 156152ea4..b5babc279 100644 --- a/sphinx/texinputs/footnotehyper-sphinx.sty +++ b/sphinx/texinputs/footnotehyper-sphinx.sty @@ -176,7 +176,7 @@ \@footnotemark % Sphinx addition: store internal hyperref anchor reference to allow % multiple clickable reference to same numbered footnote - \expandafter\xdef\csname FNH@!#1\endcsname{\Hy@footnote@currentHref}% + \expandafter\xdef\csname FNH!\number\value{sphinxscope}!#1\endcsname{\Hy@footnote@currentHref}% \def\FNH@endfntext@fntext{\@footnotetext}% \FNH@startfntext }% @@ -329,35 +329,34 @@ % \sphinxfootnotemark: usable in section titles and silently removed from TOCs. \def\sphinxfootnotemark [#1]% {\ifx\thepage\relax\else\sphinxfootref{#1}\fi}% -% \sphinxfootref: inspired by \footref from footmisc package as -% re-defined by hyperref, but with an extra FNH@! branch: +% \sphinxfootref: % - \spx@opt@BeforeFootnote is from BeforeFootnote sphinxsetup option -% - \ref{fn:#1} branch: +% - \ref branch: % the latex.py writer inserts at start of footnotetext contents -% \phantomsection\label{fn:}, so we can use \ref which -% hyperref converts into an hyperlink. This is basically the -% idea of the \footref macro from package footmisc. -% - FNH@! branch: +% \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 +% \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. -\protected\def\sphinxfootref#1{% +% this as argument and taking into account current . +\protected\def\sphinxfootref#1{% #1 always explicit number in Sphinx usage \spx@opt@BeforeFootnote - \ifcsname FNH@!#1\endcsname - \def\@thefnmark{#1}% + \ifcsname FNH!\number\value{sphinxscope}!#1\endcsname + \gdef\@thefnmark{#1}% \@thefnmark always redefined globally by latex \let\spx@saved@makefnmark\@makefnmark - \def\@makefnmark{% - \hyper@linkstart{link}{\@nameuse{FNH@!#1}}% - \spx@saved@makefnmark - \hyper@linkend + \edef\@makefnmark{% we pre-expand the sphinxscope value, to be safe + \noexpand\hyper@linkstart{link}{\@nameuse{FNH!\number\value{sphinxscope}!#1}}% + \noexpand\spx@saved@makefnmark + \noexpand\hyper@linkend }% \H@@footnotemark \let\@makefnmark\spx@saved@makefnmark \else - \def\@thefnmark{\ref{fn:#1}}% #1 = number + \xdef\@thefnmark{\noexpand\ref{\number\value{sphinxscope}.#1}}% #1 = number \H@@footnotemark \fi }% diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index eb5617dda..831f736f1 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -844,6 +844,8 @@ %% FOOTNOTES % +% Support scopes for footnote numbering +\newcounter{sphinxscope} % 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 79619c6ad..77dde08d8 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -1738,7 +1738,7 @@ class LaTeXTranslator(SphinxTranslator): def visit_footnotetext(self, node: Element) -> None: label = cast(nodes.label, node[0]) self.body.append('%%\n\\begin{footnotetext}[%s]' - '\\phantomsection\\label{fn:%s}%%\n' + '\\phantomsection\\label{\\number\\value{sphinxscope}.%s}%%\n' '\\sphinxAtStartFootnote\n' % (label.astext(), label.astext())) def depart_footnotetext(self, node: Element) -> None: diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index ad245e5f6..76c254f9c 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -732,10 +732,12 @@ 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]\\phantomsection\\label{fn:4}%\n' + assert ('\\hline%\n\\begin{footnotetext}[4]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.4}%\n' '\\sphinxAtStartFootnote\n' 'footnote in table caption\n%\n\\end{footnotetext}\\ignorespaces %\n' - '\\begin{footnotetext}[5]\\phantomsection\\label{fn:5}%\n' + '\\begin{footnotetext}[5]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.5}%\n' '\\sphinxAtStartFootnote\n' 'footnote in table header\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\n' @@ -762,22 +764,26 @@ 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]\\phantomsection\\label{fn:5}%\n' + '%\n\\begin{footnotetext}[5]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.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]\\phantomsection\\label{fn:7}%\n' + '%\n\\begin{footnotetext}[7]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.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]\\phantomsection\\label{fn:9}%\n' + assert ('\\endlastfoot\n%\n\\begin{footnotetext}[9]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.9}%\n' '\\sphinxAtStartFootnote\n' 'Foot note in longtable\n%\n\\end{footnotetext}\\ignorespaces %\n' - '\\begin{footnotetext}[10]\\phantomsection\\label{fn:10}%\n' + '\\begin{footnotetext}[10]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.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' @@ -818,7 +824,8 @@ def test_latex_show_urls_is_inline(app, status, warning): 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]\\phantomsection\\label{fn:4}%\n' + '\\begin{footnotetext}[4]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.4}%\n' '\\sphinxAtStartFootnote\n' 'Footnote inside footnote\n%\n\\end{footnotetext}\\ignorespaces') in result assert ('\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde} ' @@ -827,7 +834,8 @@ 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]\\phantomsection\\label{fn:6}%\n' + '\\leavevmode%\n\\begin{footnotetext}[6]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.6}%\n' '\\sphinxAtStartFootnote\n' 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\nDescription') in result @@ -869,7 +877,8 @@ def test_latex_show_urls_is_footnote(app, status, warning): '\\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]\\phantomsection\\label{fn:7}%\n' + '\\begin{footnotetext}[7]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.7}%\n' '\\sphinxAtStartFootnote\n' 'Footnote inside footnote\n%\n' '\\end{footnotetext}\\ignorespaces') in result @@ -878,18 +887,21 @@ 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]\\phantomsection\\label{fn:9}%\n' + '\\leavevmode%\n\\begin{footnotetext}[9]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.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]\\phantomsection\\label{fn:11}%\n' + '\\leavevmode%\n\\begin{footnotetext}[11]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.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]\\phantomsection\\label{fn:10}%\n' + '\\leavevmode%\n\\begin{footnotetext}[10]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.10}%\n' '\\sphinxAtStartFootnote\n' '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n' '\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result @@ -926,14 +938,16 @@ def test_latex_show_urls_is_no(app, status, warning): 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]\\phantomsection\\label{fn:4}%\n' + '\\begin{footnotetext}[4]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.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]\\phantomsection\\label{fn:6}%\n' + '\\leavevmode%\n\\begin{footnotetext}[6]' + '\\phantomsection\\label{\\number\\value{sphinxscope}.6}%\n' '\\sphinxAtStartFootnote\n' 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\nDescription') in result From c55216858e59f805a5655d026db6d647bff3412d Mon Sep 17 00:00:00 2001 From: jfbu Date: Sat, 6 Feb 2021 11:38:24 +0100 Subject: [PATCH 06/10] Abstract \stepcounter{sphinxscope} into macro for flexibility --- sphinx/texinputs/sphinx.sty | 1 + 1 file changed, 1 insertion(+) diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index 831f736f1..cfe2164c1 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -846,6 +846,7 @@ % % Support scopes for footnote numbering \newcounter{sphinxscope} +\newcommand{\sphinxstepscope}{\stepcounter{sphinxscope}} % 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. From 6275a7756f5a19ecf6968d4a7f18485f733cc94d Mon Sep 17 00:00:00 2001 From: jfbu Date: Sat, 6 Feb 2021 13:43:12 +0100 Subject: [PATCH 07/10] Use \thesphinxscope mark-up (shorter, and may help trick for page scope) --- sphinx/texinputs/footnotehyper-sphinx.sty | 8 +++---- sphinx/writers/latex.py | 2 +- tests/test_build_latex.py | 28 +++++++++++------------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/sphinx/texinputs/footnotehyper-sphinx.sty b/sphinx/texinputs/footnotehyper-sphinx.sty index b5babc279..f8a153c3e 100644 --- a/sphinx/texinputs/footnotehyper-sphinx.sty +++ b/sphinx/texinputs/footnotehyper-sphinx.sty @@ -176,7 +176,7 @@ \@footnotemark % Sphinx addition: store internal hyperref anchor reference to allow % multiple clickable reference to same numbered footnote - \expandafter\xdef\csname FNH!\number\value{sphinxscope}!#1\endcsname{\Hy@footnote@currentHref}% + \expandafter\xdef\csname FNH!\thesphinxscope!#1\endcsname{\Hy@footnote@currentHref}% \def\FNH@endfntext@fntext{\@footnotetext}% \FNH@startfntext }% @@ -345,18 +345,18 @@ % this as argument and taking into account current . \protected\def\sphinxfootref#1{% #1 always explicit number in Sphinx usage \spx@opt@BeforeFootnote - \ifcsname FNH!\number\value{sphinxscope}!#1\endcsname + \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!\number\value{sphinxscope}!#1}}% + \noexpand\hyper@linkstart{link}{\@nameuse{FNH!\thesphinxscope!#1}}% \noexpand\spx@saved@makefnmark \noexpand\hyper@linkend }% \H@@footnotemark \let\@makefnmark\spx@saved@makefnmark \else - \xdef\@thefnmark{\noexpand\ref{\number\value{sphinxscope}.#1}}% #1 = number + \xdef\@thefnmark{\noexpand\ref{\thesphinxscope.#1}}% #1 = number \H@@footnotemark \fi }% diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 77dde08d8..ea2f54098 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -1738,7 +1738,7 @@ class LaTeXTranslator(SphinxTranslator): def visit_footnotetext(self, node: Element) -> None: label = cast(nodes.label, node[0]) self.body.append('%%\n\\begin{footnotetext}[%s]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.%s}%%\n' + '\\phantomsection\\label{\\thesphinxscope.%s}%%\n' '\\sphinxAtStartFootnote\n' % (label.astext(), label.astext())) def depart_footnotetext(self, node: Element) -> None: diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 76c254f9c..a64de2595 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -733,11 +733,11 @@ def test_footnote(app, status, warning): 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]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.4}%\n' + '\\phantomsection\\label{\\thesphinxscope.4}%\n' '\\sphinxAtStartFootnote\n' 'footnote in table caption\n%\n\\end{footnotetext}\\ignorespaces %\n' '\\begin{footnotetext}[5]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.5}%\n' + '\\phantomsection\\label{\\thesphinxscope.5}%\n' '\\sphinxAtStartFootnote\n' 'footnote in table header\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\n' @@ -765,13 +765,13 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning): assert ('\\chapter{The section with a reference to \\sphinxfootnotemark[5]}\n' '\\label{\\detokenize{index:the-section-with-a-reference-to}}' '%\n\\begin{footnotetext}[5]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.5}%\n' + '\\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]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.7}%\n' + '\\phantomsection\\label{\\thesphinxscope.7}%\n' '\\sphinxAtStartFootnote\n' 'Footnote in caption\n%\n\\end{footnotetext}') in result assert ('\\sphinxcaption{footnote \\sphinxfootnotemark[8] in ' @@ -779,11 +779,11 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning): assert ('\\caption{footnote \\sphinxfootnotemark[9] ' 'in caption \\sphinxfootnotemark[10] of longtable\\strut}') in result assert ('\\endlastfoot\n%\n\\begin{footnotetext}[9]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.9}%\n' + '\\phantomsection\\label{\\thesphinxscope.9}%\n' '\\sphinxAtStartFootnote\n' 'Foot note in longtable\n%\n\\end{footnotetext}\\ignorespaces %\n' '\\begin{footnotetext}[10]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.10}%\n' + '\\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' @@ -825,7 +825,7 @@ def test_latex_show_urls_is_inline(app, status, warning): assert ('Third footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n' 'Third \\sphinxfootnotemark[4]\n%\n\\end{footnote}%\n' '\\begin{footnotetext}[4]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.4}%\n' + '\\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} ' @@ -835,7 +835,7 @@ def test_latex_show_urls_is_inline(app, status, warning): '\\leavevmode\n\\sphinxAtStartPar\nDescription' in result) assert ('\\item[{Footnote in term \\sphinxfootnotemark[6]}] ' '\\leavevmode%\n\\begin{footnotetext}[6]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.6}%\n' + '\\phantomsection\\label{\\thesphinxscope.6}%\n' '\\sphinxAtStartFootnote\n' 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\nDescription') in result @@ -878,7 +878,7 @@ def test_latex_show_urls_is_footnote(app, status, warning): assert ('Third footnote: %\n\\begin{footnote}[6]\\sphinxAtStartFootnote\n' 'Third \\sphinxfootnotemark[7]\n%\n\\end{footnote}%\n' '\\begin{footnotetext}[7]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.7}%\n' + '\\phantomsection\\label{\\thesphinxscope.7}%\n' '\\sphinxAtStartFootnote\n' 'Footnote inside footnote\n%\n' '\\end{footnotetext}\\ignorespaces') in result @@ -888,20 +888,20 @@ def test_latex_show_urls_is_footnote(app, status, warning): assert ('\\item[{\\sphinxhref{http://sphinx-doc.org/}' '{URL in term}\\sphinxfootnotemark[9]}] ' '\\leavevmode%\n\\begin{footnotetext}[9]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.9}%\n' + '\\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]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.11}%\n' + '\\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]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.10}%\n' + '\\phantomsection\\label{\\thesphinxscope.10}%\n' '\\sphinxAtStartFootnote\n' '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n' '\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result @@ -939,7 +939,7 @@ def test_latex_show_urls_is_no(app, status, warning): assert ('Third footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n' 'Third \\sphinxfootnotemark[4]\n%\n\\end{footnote}%\n' '\\begin{footnotetext}[4]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.4}%\n' + '\\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 @@ -947,7 +947,7 @@ def test_latex_show_urls_is_no(app, status, warning): '\\leavevmode\n\\sphinxAtStartPar\nDescription') in result assert ('\\item[{Footnote in term \\sphinxfootnotemark[6]}] ' '\\leavevmode%\n\\begin{footnotetext}[6]' - '\\phantomsection\\label{\\number\\value{sphinxscope}.6}%\n' + '\\phantomsection\\label{\\thesphinxscope.6}%\n' '\\sphinxAtStartFootnote\n' 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\nDescription') in result From ff305ec25857bab34fa4aac797ea32466655b15f Mon Sep 17 00:00:00 2001 From: jfbu Date: Sat, 6 Feb 2021 13:57:07 +0100 Subject: [PATCH 08/10] Be extra cautious to not use \@arabic in \thesphinxscope --- sphinx/texinputs/sphinx.sty | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index cfe2164c1..d74e6360b 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -847,6 +847,11 @@ % Support scopes for footnote numbering \newcounter{sphinxscope} \newcommand{\sphinxstepscope}{\stepcounter{sphinxscope}} +% 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}} % 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. From 4a239bb886ffdf9e3ce9c78e0dfbc559ff043ad5 Mon Sep 17 00:00:00 2001 From: jfbu Date: Sat, 6 Feb 2021 19:34:44 +0100 Subject: [PATCH 09/10] 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 f8a153c3e..3bba385a8 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 d74e6360b..57ecdd441 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 ea2f54098..ac1f1123a 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 a64de2595..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 @@ -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' From e759bd14c87097cb6ea5633e2cd5c62039c9953a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20B?= Date: Tue, 9 Feb 2021 16:45:49 +0100 Subject: [PATCH 10/10] Update sphinx/writers/latex.py Co-authored-by: Takeshi KOMIYA --- sphinx/writers/latex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index fbe77ba4b..cf166fb21 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -853,7 +853,7 @@ class LaTeXTranslator(SphinxTranslator): else: self.body.append('%\n') self.body.append('\\begin{footnote}[%s]' % label.astext()) - if 'auto' not in node.attributes: + if 'auto' not in node: self.body.append('\\phantomsection' '\\label{\\thesphinxscope.%s}%%\n' % label.astext()) self.body.append('\\sphinxAtStartFootnote\n')