LaTeX: correct footnote marks, extended with page of link target

Fix #10188

Footnotes in some LaTeX environments (tables, fulllineitems for object
descriptions) are gathered and appear after the environment, causing the
footnote to possibly appear on a page later than some of the footnote
marks referring it.

With this commit, the footnote mark compares page numbers and
incorporates the destination page number if it turns out to be distinct
from the page where it stands.
This commit is contained in:
jfbu 2022-02-13 12:28:52 +01:00
parent 301c7bdf57
commit 184f98ae95
4 changed files with 120 additions and 121 deletions

View File

@ -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

View File

@ -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
%%
%% 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
%% 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.
%%
%% 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,59 +363,50 @@
}%
%
% 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{<scope>.<num>}
% 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 <scope> 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
\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
}%
\H@@footnotemark
\let\@makefnmark\spx@@makefnmark
}%
\def\sphinx@footref@get #1#2#3#4#5\relax{%
\def\spx@footreflabel{#1}%
\def\spx@footrefpage {#2}%
\def\spx@footrefHref {#4}%
}%
\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
\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@saved@makefnmark
}%
\AtBeginDocument{%
% let hyperref less complain

View File

@ -814,16 +814,14 @@ class LaTeXTranslator(SphinxTranslator):
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 @@ class LaTeXTranslator(SphinxTranslator):
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:

View File

@ -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}}'