diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index 6c9f1606eb..1f7eb1bd0c 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -298,18 +298,11 @@ %% FOOTNOTES % % Support scopes for footnote numbering +% This is currently stepped at each input file \newcounter{sphinxscope} \newcommand{\sphinxstepscope}{\stepcounter{sphinxscope}} -% 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 -% 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}} +% We ensure \thesphinxscope expands to digits tokens, independently of language +\renewcommand{\thesphinxscope}{\number\value{sphinxscope}} \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 diff --git a/sphinx/texinputs/sphinxpackagefootnote.sty b/sphinx/texinputs/sphinxpackagefootnote.sty index a6071cf103..a43034e805 100644 --- a/sphinx/texinputs/sphinxpackagefootnote.sty +++ b/sphinx/texinputs/sphinxpackagefootnote.sty @@ -1,27 +1,37 @@ \NeedsTeXFormat{LaTeX2e} \ProvidesPackage{sphinxpackagefootnote}% - [2021/02/04 v1.1d footnotehyper adapted to sphinx (Sphinx team)] -% Provides support for this output mark-up from Sphinx latex writer: -% - footnote environment -% - savenotes environment (table templates) -% - \sphinxfootnotemark -% + [2022/02/12 v4.5.0 Sphinx custom footnotehyper package (Sphinx team)] %% %% Package: sphinxpackagefootnote %% Version: based on footnotehyper.sty 2021/02/04 v1.1d -%% as available at https://www.ctan.org/pkg/footnotehyper +%% https://www.ctan.org/pkg/footnotehyper %% License: the one applying to Sphinx %% -%% Refer to the PDF documentation at https://www.ctan.org/pkg/footnotehyper for -%% the code comments. +% Provides support for footnote mark-up from Sphinx latex writer: +% - "footnote" and "footnotetext" environments allowing verbatim material +% - "savenotes" environment for wrapping environments, such as for tables +% which have problems with LaTeX footnotes +% - hyperlinks +% +% Sphinx uses exclusively this mark-up for footnotes: +% - \begin{footnote}[N] +% - \begin{footnotetext}[N] +% - \sphinxfootnotemark[N] +% where N is a number. +% +%% Some small differences from upstream footnotehyper.sty: +%% - a tabulary compatibility layer (partial but enough for Sphinx), +%% - usage of \spx@opt@BeforeFootnote +%% - usage of \sphinxunactivateextrasandspace from sphinx.sty, +%% - \sphinxlongtablepatch +%% +%% Starting with Sphinx v4.5.0, inherited footnotehyper macros for +%% footnote/footnotetext receive some Sphinx specific extras to +%% implement "intelligent" footnote marks checking page numbers. %% -%% Differences: -%% 1. a partial tabulary compatibility layer added (enough for Sphinx mark-up), -%% 2. use of \spx@opt@BeforeFootnote from sphinx.sty, -%% 3. use of \sphinxunactivateextrasandspace from sphinx.sty, -%% 4. macro definition \sphinxfootnotemark, -%% 5. macro definition \sphinxlongtablepatch -%% 6. replaced some \undefined by \@undefined +%% All footnotes output from Sphinx are hyperlinked. With "savenotes" +%% footnotes may appear on page distinct from footnote mark, the latter +%% will indicate page number of the footnote. \newif\iffootnotehyperparse\footnotehyperparsetrue \DeclareOption*{\PackageWarning{sphinxpackagefootnote}{Option `\CurrentOption' is unknown}}% \ProcessOptions\relax @@ -42,6 +52,7 @@ \let\footnotetext \FNH@footnotetext \let\endfootnote \FNH@endfntext \let\endfootnotetext\FNH@endfntext + % always True branch taken with Sphinx \@ifpackageloaded{hyperref} {\ifHy@hyperfootnotes \let\FNH@H@@footnotetext\H@@footnotetext @@ -175,12 +186,33 @@ }% \def\FNH@footnoteenv@i[#1]{% \begingroup + % This legacy code from LaTeX core restricts #1 to be digits only + % This limitation could be lifted but legacy Sphinx anyhow obeys it \csname c@\@mpfn\endcsname #1\relax \unrestored@protected@xdef\@thefnmark{\thempfn}% \endgroup +% -- Sphinx specific: + \global\let\spx@saved@thefnmark\@thefnmark + % this is done to access robustly the page number where footnote mark is + \refstepcounter{sphinxfootnotemark}\label{footnotemark.\thesphinxfootnotemark}% + % if possible, compare page numbers of mark and footnote to define \@thefnmark + \ltx@ifundefined{r@\thesphinxscope.footnote.#1}% + {}% one more latex run is needed + {\sphinx@xdef@thefnmark{#1}}% check of page numbers possible +% -- \@footnotemark \def\FNH@endfntext@fntext{\@footnotetext}% +% -- Sphinx specific: + % we need to reset \@thefnmark as it is used by \FNH@startfntext via + % \FNH@startnote to set \@currentlabel which will be used by \label + \global\let\@thefnmark\spx@saved@thefnmark +% -- \FNH@startfntext +% -- again Sphinx specific + % \@currentlabel as needed by \label got set by \FNH@startnote + % insert this at start of footnote text then the label will allow + % to robustly know on which page the footnote text ends up + \phantomsection\label{\thesphinxscope.footnote.#1}% }% \def\FNH@footnotetext{% \ifx\@currenvir\FNH@footnotetext@envname @@ -207,6 +239,8 @@ \def\FNH@endfntext@fntext{\FNH@H@@footnotetext}% \fi \FNH@startfntext +% -- Sphinx specific addition + \phantomsection\label{\thesphinxscope.footnote.#1}% }% \def\FNH@startfntext{% \setbox\z@\vbox\bgroup @@ -329,60 +363,51 @@ }% % % some extras for Sphinx : -% \sphinxfootnotemark: usable in section titles and silently removed from TOCs. +% \sphinxfootnotemark: +% - if in section titles will auto-remove itself from TOC \def\sphinxfootnotemark [#1]% {\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 +\newcounter{sphinxfootnotemark} +\renewcommand\thesphinxfootnotemark{\number\value{sphinxfootnotemark}} +% - compares page number of footnote mark versus the one of footnote text +\def\sphinx@xdef@thefnmark#1{% + \expandafter\expandafter\expandafter\sphinx@footref@get + \csname r@\thesphinxscope.footnote.#1\endcsname\relax + \expandafter\expandafter\expandafter\sphinx@footmark@getpage + \csname r@footnotemark.\thesphinxfootnotemark\endcsname\thepage\relax + \protected@xdef\@thefnmark{% + \ifx\spx@footmarkpage\spx@footrefpage + \spx@footreflabel + \else + % the macro \sphinxthefootnotemark is in sphinx.sty + \sphinxthefootnotemark{\spx@footreflabel}{\spx@footrefpage}% + \fi + }% }% -\def\sphinxfootref@get #1#2#3#4#5\relax{% - \def\sphinxfootref@label{#1}% - \def\sphinxfootref@page {#2}% - \def\sphinxfootref@Href {#4}% +\def\sphinx@footref@get #1#2#3#4#5\relax{% + \def\spx@footreflabel{#1}% + \def\spx@footrefpage {#2}% + \def\spx@footrefHref {#4}% }% -\protected\def\sphinxfootref#1{% #1 always explicit number in Sphinx usage +\def\sphinx@footmark@getpage #1#2#3\relax{% + \def\spx@footmarkpage{#2}% +}% +\protected\def\sphinxfootref#1{% #1 always is explicit number in Sphinx \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 + \refstepcounter{sphinxfootnotemark}\label{footnotemark.\thesphinxfootnotemark}% + \let\spx@saved@makefnmark\@makefnmark + \ltx@ifundefined{r@\thesphinxscope.footnote.#1}% + {\gdef\@thefnmark{?}}% on first LaTeX run + {\sphinx@xdef@thefnmark{#1}% also defines \spx@footrefHref + \def\@makefnmark{% will be used by \H@@footnotemark + \hyper@linkstart{link}{\spx@footrefHref}% + \spx@saved@makefnmark \hyper@linkend - }% - \H@@footnotemark - \let\@makefnmark\spx@@makefnmark + }% }% -}% + \H@@footnotemark + \let\@makefnmark\spx@saved@makefnmark +}% \AtBeginDocument{% % let hyperref less complain \pdfstringdefDisableCommands{\def\sphinxfootnotemark [#1]{}}% diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 680f06bf5c..246f523626 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -814,16 +814,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 '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 'referred' in node: - self.body.append(r'\phantomsection' - r'\label{\thesphinxscope.%s}%%' % label.astext() + CR) + # TODO: in future maybe output a latex macro with backrefs here + pass self.body.append(r'\sphinxAtStartFootnote' + CR) def depart_footnote(self, node: Element) -> None: @@ -1717,9 +1715,7 @@ def depart_footnotemark(self, node: Element) -> None: def visit_footnotetext(self, node: Element) -> None: label = cast(nodes.label, node[0]) self.body.append('%' + CR) - self.body.append(r'\begin{footnotetext}[%s]' - r'\phantomsection\label{\thesphinxscope.%s}%%' - % (label.astext(), label.astext()) + CR) + self.body.append(r'\begin{footnotetext}[%s]' % label.astext()) self.body.append(r'\sphinxAtStartFootnote' + CR) def depart_footnotetext(self, node: Element) -> None: diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 50b2b693a6..da853d0b5f 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -723,13 +723,9 @@ 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{\\thesphinxscope.4}%\n' - '\\sphinxAtStartFootnote\n' + assert ('\\hline%\n\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n' 'footnote in table caption\n%\n\\end{footnotetext}\\ignorespaces %\n' - '\\begin{footnotetext}[5]' - '\\phantomsection\\label{\\thesphinxscope.5}%\n' - '\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[5]\\sphinxAtStartFootnote\n' 'footnote in table header\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\n' 'VIDIOC\\_CROPCAP\n&\n\\sphinxAtStartPar\n') in result @@ -755,27 +751,19 @@ 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[6]}\n' '\\label{\\detokenize{index:the-section-with-a-reference-to}}' - '%\n\\begin{footnotetext}[6]' - '\\phantomsection\\label{\\thesphinxscope.6}%\n' - '\\sphinxAtStartFootnote\n' + '%\n\\begin{footnotetext}[6]\\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:id35}}\\end{figure}\n' - '%\n\\begin{footnotetext}[8]' - '\\phantomsection\\label{\\thesphinxscope.8}%\n' - '\\sphinxAtStartFootnote\n' + '%\n\\begin{footnotetext}[8]\\sphinxAtStartFootnote\n' 'Footnote in caption\n%\n\\end{footnotetext}') in result assert ('\\sphinxcaption{footnote \\sphinxfootnotemark[9] in ' '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]' - '\\phantomsection\\label{\\thesphinxscope.10}%\n' - '\\sphinxAtStartFootnote\n' + assert ('\\endlastfoot\n%\n\\begin{footnotetext}[10]\\sphinxAtStartFootnote\n' 'Foot note in longtable\n%\n\\end{footnotetext}\\ignorespaces %\n' - '\\begin{footnotetext}[11]' - '\\phantomsection\\label{\\thesphinxscope.11}%\n' - '\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[11]\\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}]' @@ -795,13 +783,13 @@ def test_footnote_referred_multiple_times(app, status, warning): print(status.getvalue()) print(warning.getvalue()) - assert ('Explicitly numbered footnote: \\sphinxstepexplicit %\n' - '\\begin{footnote}[100]\\phantomsection\\label{\\thesphinxscope.100}%\n' + assert ('Explicitly numbered footnote: %\n' + '\\begin{footnote}[100]' '\\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' + assert ('Named footnote: %\n' + '\\begin{footnote}[13]' '\\sphinxAtStartFootnote\nNamed footnote\n%\n' '\\end{footnote} \\sphinxfootnotemark[13]\n' in result) @@ -837,9 +825,7 @@ 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{\\thesphinxscope.4}%\n' - '\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n' 'Footnote inside footnote\n%\n\\end{footnotetext}\\ignorespaces') in result assert ('Fourth footnote: %\n\\begin{footnote}[5]\\sphinxAtStartFootnote\n' 'Fourth\n%\n\\end{footnote}\n') in result @@ -849,8 +835,12 @@ def test_latex_show_urls_is_inline(app, status, warning): '(http://sphinx\\sphinxhyphen{}doc.org/)}\n' '\\sphinxAtStartPar\nDescription' in result) assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[7]}%\n' - '\\begin{footnotetext}[7]\\phantomsection\\label{\\thesphinxscope.7}%\n' - '\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n') + assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{URL in term} ' + '(http://sphinx\\sphinxhyphen{}doc.org/)}\n' + '\\sphinxAtStartPar\nDescription' in result) + assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[7]}%\n' + '\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n' 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\nDescription') in result assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist} ' @@ -893,9 +883,7 @@ 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{\\thesphinxscope.7}%\n' - '\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n' 'Footnote inside footnote\n%\n' '\\end{footnotetext}\\ignorespaces') in result assert ('Fourth footnote: %\n\\begin{footnote}[8]\\sphinxAtStartFootnote\n' @@ -905,18 +893,18 @@ def test_latex_show_urls_is_footnote(app, status, warning): '\\sphinxnolinkurl{http://sphinx-doc.org/~test/}\n%\n\\end{footnote}') in result assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}' '{URL in term}\\sphinxfootnotemark[10]}%\n' - '\\begin{footnotetext}[10]\\phantomsection\\label{\\thesphinxscope.10}%\n' + '\\begin{footnotetext}[10]' '\\sphinxAtStartFootnote\n' '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n' '\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[12]}%\n' - '\\begin{footnotetext}[12]\\phantomsection\\label{\\thesphinxscope.12}%\n' + '\\begin{footnotetext}[12]' '\\sphinxAtStartFootnote\n' 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\nDescription') in result assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist}' '\\sphinxfootnotemark[11]}%\n' - '\\begin{footnotetext}[11]\\phantomsection\\label{\\thesphinxscope.11}%\n' + '\\begin{footnotetext}[11]' '\\sphinxAtStartFootnote\n' '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n' '\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result @@ -955,9 +943,7 @@ 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{\\thesphinxscope.4}%\n' - '\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n' 'Footnote inside footnote\n%\n\\end{footnotetext}\\ignorespaces') in result assert ('Fourth footnote: %\n\\begin{footnote}[5]\\sphinxAtStartFootnote\n' 'Fourth\n%\n\\end{footnote}\n') in result @@ -965,8 +951,7 @@ def test_latex_show_urls_is_no(app, status, warning): assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{URL in term}}\n' '\\sphinxAtStartPar\nDescription') in result assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[7]}%\n' - '\\begin{footnotetext}[7]\\phantomsection\\label{\\thesphinxscope.7}%\n' - '\\sphinxAtStartFootnote\n' + '\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n' 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\nDescription') in result assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist}}'