From 9a8e36568d402e151a85e1cec939e7e7b90d5042 Mon Sep 17 00:00:00 2001 From: jfbu Date: Tue, 14 Feb 2017 23:30:50 +0100 Subject: [PATCH] (latex) hyperlinked footnotes from table bodies Memo: footnotehyper-sphinx LaTeX package incorporates here some fixes from upstream footnotehyper 0.99. At Sphinx 1.6 il will for simplicity copy all of upstream footnotehyper 0.99. Then old footnote.sty LaTeX package is not a dependency of Sphinx anymore. --- CHANGES | 5 ++ sphinx/templates/latex/longtable.tex_t | 4 +- sphinx/templates/latex/tabular.tex_t | 4 +- sphinx/templates/latex/tabulary.tex_t | 4 +- sphinx/texinputs/footnotehyper-sphinx.sty | 69 +++++++++++++++++------ sphinx/texinputs/sphinx.sty | 5 +- sphinx/writers/latex.py | 9 ++- tests/test_build_latex.py | 61 +++++++++++--------- 8 files changed, 105 insertions(+), 56 deletions(-) diff --git a/CHANGES b/CHANGES index 697ab7e59..cc3c1e4c2 100644 --- a/CHANGES +++ b/CHANGES @@ -56,6 +56,7 @@ Features added :confval:`suppress_warnings`. * #3377: latex: Add support for Docutils 0.13 ``:align:`` option for tables (but does not implement text flow around table). +* (latex) footnotes from inside tables are hyperlinked (except inside headers). Bugs fixed ---------- @@ -82,6 +83,10 @@ Deprecated - ``BuildEnvironment.create_index()`` Please use ``sphinx.environment.adapters`` modules instead. +* The LaTeX package ``footnote.sty`` will not be loaded anymore at Sphinx 1.7, + as Sphinx's ``footnotehyper-sphinx.sty`` will define all macros rather than + patch them. In particular the ``\makesavenoteenv`` macro is not in use anymore + and its definition will be removed at Sphinx 1.7. Release 1.5.3 (in development) ============================== diff --git a/sphinx/templates/latex/longtable.tex_t b/sphinx/templates/latex/longtable.tex_t index 703a61418..537c61bf0 100644 --- a/sphinx/templates/latex/longtable.tex_t +++ b/sphinx/templates/latex/longtable.tex_t @@ -1,4 +1,4 @@ -\begin{longtable} +\begin{savenotes}\begin{longtable} <%- if table.align == 'center' -%> [c] <%- elif table.align == 'left' -%> @@ -28,4 +28,4 @@ \endlastfoot <%= ''.join(table.body) %> -\end{longtable} +\end{longtable}\end{savenotes} diff --git a/sphinx/templates/latex/tabular.tex_t b/sphinx/templates/latex/tabular.tex_t index ea8649ccf..af3ea1ea9 100644 --- a/sphinx/templates/latex/tabular.tex_t +++ b/sphinx/templates/latex/tabular.tex_t @@ -1,4 +1,4 @@ -\begingroup +\begin{savenotes} <% if table.align -%> <%- if table.align == 'center' -%> \centering @@ -23,4 +23,4 @@ \end{threeparttable} <%- endif %> \par -\endgroup +\end{savenotes} diff --git a/sphinx/templates/latex/tabulary.tex_t b/sphinx/templates/latex/tabulary.tex_t index fed923f8b..7240456d7 100644 --- a/sphinx/templates/latex/tabulary.tex_t +++ b/sphinx/templates/latex/tabulary.tex_t @@ -1,4 +1,4 @@ -\begingroup +\begin{savenotes} <% if table.align -%> <%- if table.align == 'center' -%> \centering @@ -23,4 +23,4 @@ \end{threeparttable} <%- endif %> \par -\endgroup +\end{savenotes} diff --git a/sphinx/texinputs/footnotehyper-sphinx.sty b/sphinx/texinputs/footnotehyper-sphinx.sty index 763ce7323..2ce91c1aa 100644 --- a/sphinx/texinputs/footnotehyper-sphinx.sty +++ b/sphinx/texinputs/footnotehyper-sphinx.sty @@ -1,6 +1,6 @@ \NeedsTeXFormat{LaTeX2e} \ProvidesPackage{footnotehyper-sphinx}% - [2017/01/16 v1.5.2 hyperref aware footnote.sty for sphinx (JFB)] + [2017/02/15 v1.6 hyperref aware footnote.sty for sphinx (JFB)] %% %% Package: footnotehyper-sphinx %% Version: based on footnotehyper.sty v0.9f (2016/10/03) @@ -10,13 +10,14 @@ %% Differences from footnotehyper v0.9f (2016/10/03): %% 1. hyperref is assumed in use (with default hyperfootnotes=true), %% 2. no need to check if footnote.sty was loaded, -%% 3. a special tabulary compatibility layer added, (partial but enough for -%% Sphinx), -%% 4. \sphinxfootnotemark, and use of \spx@opt@BeforeFootnote from sphinx.sty. -%% Note: with \footnotemark[N]/\footnotetext[N] syntax, hyperref -%% does not insert an hyperlink. This is _not_ improved here. +%% 3. a tabulary compatibility layer added, (partial but enough for Sphinx), +%% 4. \sphinxfootnotemark, and use of \spx@opt@BeforeFootnote from sphinx.sty, %% 5. use of \sphinxunactivateextrasandspace for parsed literals -%% +%% 6. macro \sphinxlongtablepatch +%% 7. some import from future footnotehyper v0.99 (\FNH@fn@fntext) +%% 8. deprecation of \makesavenoteenv. +%% Future version of footnotehyper will not load footnote.sty and it will define +%% all macros rather than adding hyperref awareness and fixing bugs. \DeclareOption*{\PackageWarning{footnotehyper}{Option `\CurrentOption' is unknown}}% \ProcessOptions\relax \let\FNH@@makefntext\@makefntext\let\@makefntext\@firstofone @@ -31,6 +32,7 @@ \let\fn@latex@@footnote \footnote % meaning of \footnote at end of preamble \let\fn@latex@@footnotetext\footnotetext \let\fn@fntext \FNH@hyper@fntext + \let\savenotes \FNH@savenotes \let\spewnotes \FNH@hyper@spewnotes \let\endsavenotes\spewnotes \let\fn@endfntext\FNH@fixed@endfntext @@ -39,13 +41,33 @@ \let\endfootnote\fn@endfntext \let\endfootnotetext\endfootnote }% -\def\FNH@hyper@fntext {% +%% patch \savenotes for good functioning of \footnotetext[N]{..} syntax even +%% though Sphinx now uses only environment form. In future, will simply copy +%% over full footnotehyper v0.99. +\toks@\expandafter\expandafter\expandafter{\expandafter\@gobble\savenotes}% +\edef\FNH@savenotes{\begingroup + \unexpanded{\if@savingnotes\else\let\H@@mpfootnotetext\FNH@fn@fntext\fi}% + \the\toks@ }% +\def\FNH@fn@fntext {\FNH@fntext\FNH@fn@fntext@i}% +\def\FNH@hyper@fntext{\FNH@fntext\FNH@hyper@fntext@i}% +\def\FNH@fntext #1{% %% amsmath compatibility \ifx\ifmeasuring@\undefined\expandafter\@secondoftwo \else\expandafter\@firstofone\fi {\ifmeasuring@\expandafter\@gobbletwo\else\expandafter\@firstofone\fi}% %% partial tabulary compatibility, [N] must be used, but Sphinx does it - {\ifx\equation$\expandafter\@gobbletwo\fi\FNH@hyper@fntext@i }%$ + {\ifx\equation$\expandafter\@gobbletwo\fi #1}%$ +}% +%% footnote.sty's replacement for \@footnotetext +\long\def\FNH@fn@fntext@i #1{\global\setbox\fn@notes\vbox + {\unvbox\fn@notes + \fn@startnote + \@makefntext + {\rule\z@\footnotesep\ignorespaces + #1% + \@finalstrut\strutbox + }% + \fn@endnote }% }% \long\def\FNH@hyper@fntext@i #1{\global\setbox\fn@notes\vbox {\unvbox\fn@notes @@ -62,7 +84,8 @@ \let\@currentHref\Hy@footnote@currentHref \let\@currentlabelname\@empty #1}% - \@finalstrut\strutbox }% + \@finalstrut\strutbox + }% \fn@endnote }% }% \def\FNH@hyper@spewnotes {\endgroup @@ -87,7 +110,7 @@ \def\FNH@endfntext@nolink {\begingroup \let\@makefntext\@empty\let\@finalstrut\@gobble \let\rule\@gobbletwo - \if@savingnotes\expandafter\fn@fntext\else\expandafter\H@@footnotetext\fi + \if@savingnotes\expandafter\FNH@fn@fntext\else\expandafter\H@@footnotetext\fi {\unvbox\z@}\endgroup }% %% \spx@opt@BeforeFootnote is defined in sphinx.sty @@ -139,12 +162,13 @@ \def\FNH@check@c #11.2!3?4,#2#3\relax {\ifx\FNH@check@c#2\expandafter\@gobble\fi\FNH@bad@footnote@env}% \def\FNH@bad@footnote@env -{\PackageWarningNoLine{footnotehyper}% - {The footnote environment from package footnote^^J - will be dysfunctional, sorry (not my fault...). You may try to make a bug^^J - report at https://github.com/sphinx-doc/sphinx including the next lines:}% +{\PackageWarningNoLine{footnotehyper-sphinx}% + {Footnotes will be sub-optimal, sorry. This is due to the class or^^J + some package modifying macro \string\@makefntext.^^J + You can try to report this incompatibility at^^J + https://github.com/sphinx-doc/sphinx with this info:}% \typeout{\meaning\@makefntext}% - \let\fn@prefntext\@empty\let\fn@postfntext\@empty + \let\fn@prefntext\@empty\let\fn@postfntext\@empty }% %% \sphinxfootnotemark: usable in section titles and silently removed from %% TOCs. @@ -153,6 +177,19 @@ \protect\footnotemark[#1]\fi}% \AtBeginDocument % let hyperref less complain {\pdfstringdefDisableCommands{\def\sphinxfootnotemark [#1]{}}}% +% to obtain hyperlinked footnotes in longtable environment we must replace +% hyperref's patch of longtable's patch of \@footnotetext by our own +\def\sphinxlongtablepatch {% only for longtable wrapped in "savenotes" + \let\LT@p@ftntext\FNH@hyper@fntext +}% +%% deprecate \makesavenoteenv +\def\makesavenoteenv{% + \AtEndDocument{\PackageWarning{footnotehyper-sphinx} + {^^J^^J^^J!\@spaces**** SPHINX DEPRECATION WARNING ****^^J!^^J% + !\@spaces\string\makesavenoteenv\space from footnote.sty is deprecated^^J% + !\@spaces and will be removed at Sphinx 1.7 !^^J^^J^^J}}% + \@ifnextchar[\fn@msne@ii\fn@msne@i%] +}% \endinput %% %% End of file `footnotehyper-sphinx.sty'. diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index 8f17cf5aa..8e0301f30 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -59,10 +59,7 @@ % For hyperlinked footnotes in tables; also for gathering footnotes from % topic and warning blocks. Also to allow code-blocks in footnotes. \RequirePackage{footnotehyper-sphinx} -\makesavenoteenv{tabulary} -\makesavenoteenv{tabular} -\makesavenoteenv{threeparttable} -% (longtable is hyperref compatible and needs no special treatment here.) +\AtBeginDocument{\@ifpackageloaded{longtable}{\sphinxlongtablepatch}{}}% % For the H specifier. Do not \restylefloat{figure}, it breaks Sphinx code % for allowing figures in tables. \RequirePackage{float} diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 9d919713d..8faeee459 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -1262,7 +1262,8 @@ class LaTeXTranslator(nodes.NodeVisitor): def depart_collected_footnote(self, node): # type: (nodes.Node) -> None if 'footnotetext' in node: - self.body.append('%\n\\end{footnotetext}') + # the \ignorespaces in particular for after table header use + self.body.append('%\n\\end{footnotetext}\\ignorespaces ') else: if self.in_parsed_literal: self.body.append('\\end{footnote}') @@ -1310,7 +1311,6 @@ class LaTeXTranslator(nodes.NodeVisitor): self.body.append(table) self.body.append("\n") - self.unrestrict_footnote(node) self.table = None def visit_colspec(self, node): @@ -1342,6 +1342,9 @@ class LaTeXTranslator(nodes.NodeVisitor): def visit_tbody(self, node): # type: (nodes.Node) -> None self.pushbody(self.table.body) # Redirect body output until table is finished. + if self.footnote_restricted: + # releases footnotetexts from header in first non header cell + self.unrestrict_footnote(node.parent.parent) def depart_tbody(self, node): # type: (nodes.Node) -> None @@ -2244,7 +2247,7 @@ class LaTeXTranslator(nodes.NodeVisitor): def visit_line(self, node): # type: (nodes.Node) -> None - self.body.append(r'\item[] ') + self.body.append('\\item[] ') def depart_line(self, node): # type: (nodes.Node) -> None diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index cd040eb1b..6c237f631 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -480,12 +480,11 @@ def test_footnote(app, status, warning): '{\\phantomsection\\label{\\detokenize{footnote:bar}} ' '\ncite\n}') in result assert '\\caption{Table caption \\sphinxfootnotemark[4]' in result - assert 'name \\sphinxfootnotemark[5]' in result - assert ('\\end{threeparttable}\n\\par\n\\endgroup\n%\n' - '\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n' - 'footnotes in table caption\n%\n\\end{footnotetext}%\n' + assert ('\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n' + 'footnotes in table caption\n%\n\\end{footnotetext}\\ignorespaces %\n' '\\begin{footnotetext}[5]\\sphinxAtStartFootnote\n' - 'footnotes in table\n%\n\\end{footnotetext}') in result + 'footnotes in table\n%\n\\end{footnotetext}\\ignorespaces ') in result + assert '\\end{threeparttable}\n\\par\n\\end{savenotes}\n' in result @pytest.mark.sphinx('latex', testroot='footnotes') @@ -514,14 +513,18 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning): 'in caption of normal table}\\label{\\detokenize{index:id28}}') in result assert ('\\caption{footnote \\sphinxfootnotemark[8] ' 'in caption \\sphinxfootnotemark[9] of longtable}') in result - assert ('\\end{longtable}\n%\n\\begin{footnotetext}[8]' - '\\sphinxAtStartFootnote\n' - 'Foot note in longtable\n%\n\\end{footnotetext}' in result) + assert ('\\begin{footnotetext}[8]\\sphinxAtStartFootnote\n' + 'Foot note in longtable\n%\n\\end{footnotetext}\\ignorespaces %\n' + '\\begin{footnotetext}[9]\\sphinxAtStartFootnote\n' + 'Second footnote in caption of longtable\n') in result assert ('This is a reference to the code-block in the footnote:\n' '{\\hyperref[\\detokenize{index:codeblockinfootnote}]' '{\\sphinxcrossref{\\DUrole{std,std-ref}{I am in a footnote}}}}') in result - assert ('&\nThis is one more footnote with some code in it ' - '\\sphinxfootnotemark[10].\n\\\\') in result + assert ('&\nThis is one more footnote with some code in it %\n' + '\\begin{footnote}[10]\\sphinxAtStartFootnote\n' + 'Third footnote in longtable\n') in result + assert ('\\end{sphinxVerbatim}\n\\let\\sphinxVerbatimTitle\\empty\n' + '\\let\\sphinxLiteralBlockLabel\\empty\n%\n\\end{footnote}.\n') in result assert '\\begin{sphinxVerbatim}[commandchars=\\\\\\{\\}]' in result @@ -561,7 +564,8 @@ def test_latex_show_urls_is_inline(app, status, warning): '(http://sphinx-doc.org/)}] \\leavevmode\nDescription' in result) assert ('\\item[{Footnote in term \\sphinxfootnotemark[5]}] ' '\\leavevmode%\n\\begin{footnotetext}[5]\\sphinxAtStartFootnote\n' - 'Footnote in term\n%\n\\end{footnotetext}\nDescription') in result + 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces \n' + 'Description') in result assert ('\\item[{\\href{http://sphinx-doc.org/}{Term in deflist} ' '(http://sphinx-doc.org/)}] \\leavevmode\nDescription') in result assert '\\url{https://github.com/sphinx-doc/sphinx}\n' in result @@ -607,15 +611,16 @@ def test_latex_show_urls_is_footnote(app, status, warning): '{URL in term}\\sphinxfootnotemark[8]}] ' '\\leavevmode%\n\\begin{footnotetext}[8]\\sphinxAtStartFootnote\n' '\\nolinkurl{http://sphinx-doc.org/}\n%\n' - '\\end{footnotetext}\nDescription') in result + '\\end{footnotetext}\\ignorespaces \nDescription') in result assert ('\\item[{Footnote in term \\sphinxfootnotemark[10]}] ' '\\leavevmode%\n\\begin{footnotetext}[10]\\sphinxAtStartFootnote\n' - 'Footnote in term\n%\n\\end{footnotetext}\nDescription') in result + 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces \n' + 'Description') in result assert ('\\item[{\\href{http://sphinx-doc.org/}{Term in deflist}' '\\sphinxfootnotemark[9]}] ' '\\leavevmode%\n\\begin{footnotetext}[9]\\sphinxAtStartFootnote\n' '\\nolinkurl{http://sphinx-doc.org/}\n%\n' - '\\end{footnotetext}\nDescription') in result + '\\end{footnotetext}\\ignorespaces \nDescription') in result assert ('\\url{https://github.com/sphinx-doc/sphinx}\n' in result) assert ('\\href{mailto:sphinx-dev@googlegroups.com}' '{sphinx-dev@googlegroups.com}\n') in result @@ -655,7 +660,8 @@ def test_latex_show_urls_is_no(app, status, warning): '\\leavevmode\nDescription') in result assert ('\\item[{Footnote in term \\sphinxfootnotemark[5]}] ' '\\leavevmode%\n\\begin{footnotetext}[5]\\sphinxAtStartFootnote\n' - 'Footnote in term\n%\n\\end{footnotetext}\nDescription') in result + 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces \n' + 'Description') in result assert ('\\item[{\\href{http://sphinx-doc.org/}{Term in deflist}}] ' '\\leavevmode\nDescription') in result assert ('\\url{https://github.com/sphinx-doc/sphinx}\n' in result) @@ -831,32 +837,33 @@ def test_latex_table_tabulars(app, status, warning): # simple_table table = tables['simple table'] - assert ('\\begingroup\n\\centering\n\\begin{tabulary}{\\linewidth}{|L|L|}' in table) + assert ('\\begin{savenotes}\n\\centering\n' + '\\begin{tabulary}{\\linewidth}{|L|L|}' in table) assert ('\\hline\n' '\\sphinxstylethead{\\relax \nheader1\n\\unskip}\\relax &' '\\sphinxstylethead{\\relax \nheader2\n\\unskip}\\relax' in table) assert ('\\hline\ncell1-1\n&\ncell1-2\n\\\\' in table) assert ('\\hline\ncell2-1\n&\ncell2-2\n\\\\' in table) assert ('\\hline\ncell3-1\n&\ncell3-2\n\\\\' in table) - assert ('\\hline\n\\end{tabulary}\n\\par\n\\endgroup' in table) + assert ('\\hline\n\\end{tabulary}\n\\par\n\\end{savenotes}' in table) # table having :widths: option table = tables['table having :widths: option'] - assert ('\\begingroup\n\\centering\n' + assert ('\\begin{savenotes}\n\\centering\n' '\\begin{tabular}{|\\X{30}{100}|\\X{70}{100}|}' in table) - assert ('\\hline\n\\end{tabular}\n\\par\n\\endgroup' in table) + assert ('\\hline\n\\end{tabular}\n\\par\n\\end{savenotes}' in table) # table having :align: option (tabulary) table = tables['table having :align: option (tabulary)'] - assert ('\\begingroup\n\\raggedleft\n' + assert ('\\begin{savenotes}\n\\raggedleft\n' '\\begin{tabulary}{\\linewidth}{|L|L|}\n' in table) - assert ('\\hline\n\\end{tabulary}\n\\par\n\\endgroup' in table) + assert ('\\hline\n\\end{tabulary}\n\\par\n\\end{savenotes}' in table) # table having :align: option (tabular) table = tables['table having :align: option (tabular)'] - assert ('\\begingroup\n\\raggedright\n' + assert ('\\begin{savenotes}\n\\raggedright\n' '\\begin{tabular}{|\\X{30}{100}|\\X{70}{100}|}\n' in table) - assert ('\\hline\n\\end{tabular}\n\\par\n\\endgroup' in table) + assert ('\\hline\n\\end{tabular}\n\\par\n\\end{savenotes}' in table) # table with tabularcolumn table = tables['table with tabularcolumn'] @@ -864,12 +871,12 @@ def test_latex_table_tabulars(app, status, warning): # table having caption table = tables['table having caption'] - assert ('\\begingroup\n\\centering\n' + assert ('\\begin{savenotes}\n\\centering\n' '\\begin{threeparttable}\n\\capstart\\caption{caption for table}' '\\label{\\detokenize{tabular:id1}}' in table) assert ('\\begin{tabulary}{\\linewidth}{|L|L|}' in table) assert ('\\hline\n\\end{tabulary}\n\\end{threeparttable}' - '\n\\par\n\\endgroup' in table) + '\n\\par\n\\end{savenotes}' in table) # table having verbatim table = tables['table having verbatim'] @@ -898,7 +905,7 @@ def test_latex_table_longtable(app, status, warning): # longtable table = tables['longtable'] - assert ('\\begin{longtable}{|l|l|}\n\\hline' in table) + assert ('\\begin{savenotes}\\begin{longtable}{|l|l|}\n\\hline' in table) assert ('\\hline\n' '\\sphinxstylethead{\\relax \nheader1\n\\unskip}\\relax &' '\\sphinxstylethead{\\relax \nheader2\n\\unskip}\\relax \\\\\n' @@ -915,7 +922,7 @@ def test_latex_table_longtable(app, status, warning): assert ('\ncell1-1\n&\ncell1-2\n\\\\' in table) assert ('\\hline\ncell2-1\n&\ncell2-2\n\\\\' in table) assert ('\\hline\ncell3-1\n&\ncell3-2\n\\\\' in table) - assert ('\\hline\n\\end{longtable}' in table) + assert ('\\hline\n\\end{longtable}\\end{savenotes}' in table) # longtable having :widths: option table = tables['longtable having :widths: option']