mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
refactor: Add LaTeXFootnoteTransform to restructure footnotes for LaTeX
This commit is contained in:
parent
ff0f008574
commit
89d68d9ac3
@ -19,7 +19,9 @@ from six import text_type
|
||||
|
||||
from sphinx import package_dir, addnodes, highlighting
|
||||
from sphinx.builders import Builder
|
||||
from sphinx.builders.latex.transforms import FootnoteDocnameUpdater, ShowUrlsTransform
|
||||
from sphinx.builders.latex.transforms import (
|
||||
FootnoteDocnameUpdater, LaTeXFootnoteTransform, ShowUrlsTransform
|
||||
)
|
||||
from sphinx.config import string_classes, ENUM
|
||||
from sphinx.environment import NoUri
|
||||
from sphinx.environment.adapters.asset import ImageAdapter
|
||||
@ -219,7 +221,8 @@ class LaTeXBuilder(Builder):
|
||||
transformer = SphinxTransformer(doctree)
|
||||
transformer.set_environment(self.env)
|
||||
transformer.add_transforms([SubstitutionDefinitionsRemover,
|
||||
ShowUrlsTransform])
|
||||
ShowUrlsTransform,
|
||||
LaTeXFootnoteTransform])
|
||||
transformer.apply_transforms()
|
||||
|
||||
def finish(self):
|
||||
|
23
sphinx/builders/latex/nodes.py
Normal file
23
sphinx/builders/latex/nodes.py
Normal file
@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.builders.latex.nodes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
docutils nodes for LaTeX builder.
|
||||
|
||||
:copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
from docutils import nodes
|
||||
|
||||
|
||||
class footnotemark(nodes.Inline, nodes.Referential, nodes.TextElement):
|
||||
"""A node represents ``\footnotemark``."""
|
||||
pass
|
||||
|
||||
|
||||
class footnotetext(nodes.General, nodes.BackLinkable, nodes.Element,
|
||||
nodes.Labeled, nodes.Targetable):
|
||||
"""A node represents ``\footnotetext``."""
|
||||
pass
|
@ -12,6 +12,7 @@
|
||||
from docutils import nodes
|
||||
|
||||
from sphinx import addnodes
|
||||
from sphinx.builders.latex.nodes import footnotemark, footnotetext
|
||||
from sphinx.transforms import SphinxTransform
|
||||
|
||||
if False:
|
||||
@ -35,6 +36,8 @@ class ShowUrlsTransform(SphinxTransform):
|
||||
"""Expand references to inline text or footnotes.
|
||||
|
||||
For more information, see :confval:`latex_show_urls`.
|
||||
|
||||
.. note:: This transform is used for integrated doctree
|
||||
"""
|
||||
default_priority = 400
|
||||
|
||||
@ -165,3 +168,302 @@ class FootnoteCollector(nodes.NodeVisitor):
|
||||
def visit_footnote_reference(self, node):
|
||||
# type: (nodes.footnote_reference) -> None
|
||||
self.footnote_refs.append(node)
|
||||
|
||||
|
||||
class LaTeXFootnoteTransform(SphinxTransform):
|
||||
"""Convert footnote definitions and references to appropriate form to LaTeX.
|
||||
|
||||
* Replace footnotes on restricted zone (e.g. headings) by footnotemark node.
|
||||
In addition, append a footnotetext node after the zone.
|
||||
|
||||
Before::
|
||||
|
||||
<section>
|
||||
<title>
|
||||
headings having footnotes
|
||||
<footnote_reference>
|
||||
1
|
||||
<footnote ids="1">
|
||||
<label>
|
||||
1
|
||||
<paragraph>
|
||||
footnote body
|
||||
|
||||
After::
|
||||
|
||||
<section>
|
||||
<title>
|
||||
headings having footnotes
|
||||
<footnotemark>
|
||||
1
|
||||
<footnotetext>
|
||||
footnote body
|
||||
<footnotetext>
|
||||
<label>
|
||||
1
|
||||
<paragraph>
|
||||
footnote body
|
||||
|
||||
* Integrate footnote definitions and footnote references to single footnote node
|
||||
|
||||
Before::
|
||||
|
||||
blah blah blah
|
||||
<footnote_reference refid="id1">
|
||||
1
|
||||
blah blah blah ...
|
||||
|
||||
<footnote ids="1">
|
||||
<label>
|
||||
1
|
||||
<paragraph>
|
||||
footnote body
|
||||
|
||||
After::
|
||||
|
||||
blah blah blah
|
||||
<footnote ids="1">
|
||||
<label>
|
||||
1
|
||||
<paragraph>
|
||||
footnote body
|
||||
blah blah blah ...
|
||||
|
||||
* Replace second and subsequent footnote references which refers same footnote definition
|
||||
by footnotemark node.
|
||||
|
||||
Before::
|
||||
|
||||
blah blah blah
|
||||
<footnote_reference refid="id1">
|
||||
1
|
||||
blah blah blah
|
||||
<footnote_reference refid="id1">
|
||||
1
|
||||
blah blah blah ...
|
||||
|
||||
<footnote ids="1">
|
||||
<label>
|
||||
1
|
||||
<paragraph>
|
||||
footnote body
|
||||
|
||||
After::
|
||||
|
||||
blah blah blah
|
||||
<footnote ids="1">
|
||||
<label>
|
||||
1
|
||||
<paragraph>
|
||||
footnote body
|
||||
blah blah blah
|
||||
<footnotemark>
|
||||
1
|
||||
blah blah blah ...
|
||||
|
||||
* Remove unreferenced footnotes
|
||||
|
||||
Before::
|
||||
|
||||
<footnote ids="1">
|
||||
<label>
|
||||
1
|
||||
<paragraph>
|
||||
Unreferenced footnote!
|
||||
|
||||
After::
|
||||
|
||||
<!-- nothing! -->
|
||||
|
||||
* Move footnotes in a title of table or thead to head of tbody
|
||||
|
||||
Before::
|
||||
|
||||
<table>
|
||||
<title>
|
||||
title having footnote_reference
|
||||
<footnote_reference refid="1">
|
||||
1
|
||||
<tgroup>
|
||||
<thead>
|
||||
<row>
|
||||
<entry>
|
||||
header having footnote_reference
|
||||
<footnote_reference refid="2">
|
||||
2
|
||||
<tbody>
|
||||
<row>
|
||||
...
|
||||
|
||||
<footnote ids="1">
|
||||
<label>
|
||||
1
|
||||
<paragraph>
|
||||
footnote body
|
||||
|
||||
<footnote ids="2">
|
||||
<label>
|
||||
2
|
||||
<paragraph>
|
||||
footnote body
|
||||
|
||||
After::
|
||||
|
||||
<table>
|
||||
<title>
|
||||
title having footnote_reference
|
||||
<footnotemark>
|
||||
1
|
||||
<tgroup>
|
||||
<thead>
|
||||
<row>
|
||||
<entry>
|
||||
header having footnote_reference
|
||||
<footnotemark>
|
||||
2
|
||||
<tbody>
|
||||
<footnotetext>
|
||||
<label>
|
||||
1
|
||||
<paragraph>
|
||||
footnote body
|
||||
|
||||
<footnotetext>
|
||||
<label>
|
||||
2
|
||||
<paragraph>
|
||||
footnote body
|
||||
<row>
|
||||
...
|
||||
"""
|
||||
|
||||
default_priority = 600
|
||||
|
||||
def apply(self):
|
||||
footnotes = list(self.document.traverse(nodes.footnote))
|
||||
for node in footnotes:
|
||||
node.parent.remove(node)
|
||||
|
||||
visitor = LaTeXFootnoteVisitor(self.document, footnotes)
|
||||
self.document.walkabout(visitor)
|
||||
|
||||
|
||||
class LaTeXFootnoteVisitor(nodes.NodeVisitor):
|
||||
def __init__(self, document, footnotes):
|
||||
# type: (nodes.document, List[nodes.footnote]) -> None
|
||||
self.appeared = set() # type: Set[Tuple[unicode, nodes.footnote]]
|
||||
self.footnotes = footnotes # type: List[nodes.footnote]
|
||||
self.pendings = [] # type: List[nodes.Node]
|
||||
self.table_footnotes = [] # type: List[nodes.Node]
|
||||
self.restricted = None # type: nodes.Node
|
||||
nodes.NodeVisitor.__init__(self, document)
|
||||
|
||||
def unknown_visit(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def unknown_departure(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
def restrict(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if self.restricted is None:
|
||||
self.restricted = node
|
||||
|
||||
def unrestrict(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if self.restricted == node:
|
||||
self.restricted = None
|
||||
pos = node.parent.index(node)
|
||||
for i, footnote, in enumerate(self.pendings):
|
||||
fntext = footnotetext('', *footnote.children)
|
||||
node.parent.insert(pos + i + 1, fntext)
|
||||
self.pendings = []
|
||||
|
||||
def visit_figure(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.restrict(node)
|
||||
|
||||
def depart_figure(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.unrestrict(node)
|
||||
|
||||
def visit_term(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.restrict(node)
|
||||
|
||||
def depart_term(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.unrestrict(node)
|
||||
|
||||
def visit_caption(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.restrict(node)
|
||||
|
||||
def depart_caption(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.unrestrict(node)
|
||||
|
||||
def visit_title(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if isinstance(node.parent, (nodes.section, nodes.table)):
|
||||
self.restrict(node)
|
||||
|
||||
def depart_title(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if isinstance(node.parent, nodes.section):
|
||||
self.unrestrict(node)
|
||||
elif isinstance(node.parent, nodes.table):
|
||||
self.table_footnotes += self.pendings
|
||||
self.pendings = []
|
||||
self.unrestrict(node)
|
||||
|
||||
def visit_thead(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.restrict(node)
|
||||
|
||||
def depart_thead(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.table_footnotes += self.pendings
|
||||
self.pendings = []
|
||||
self.unrestrict(node)
|
||||
|
||||
def depart_table(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
tbody = list(node.traverse(nodes.tbody))[0]
|
||||
for footnote in reversed(self.table_footnotes):
|
||||
fntext = footnotetext('', *footnote.children)
|
||||
tbody.insert(0, fntext)
|
||||
|
||||
self.table_footnotes = []
|
||||
|
||||
def visit_footnote_reference(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
number = node.astext().strip()
|
||||
docname = node['docname']
|
||||
if self.restricted:
|
||||
mark = footnotemark('', number)
|
||||
node.replace_self(mark)
|
||||
if (docname, number) not in self.appeared:
|
||||
footnote = self.get_footnote_by_reference(node)
|
||||
self.pendings.append(footnote)
|
||||
elif (docname, number) in self.appeared:
|
||||
mark = footnotemark('', number)
|
||||
node.replace_self(mark)
|
||||
else:
|
||||
footnote = self.get_footnote_by_reference(node)
|
||||
self.footnotes.remove(footnote)
|
||||
node.replace_self(footnote)
|
||||
|
||||
self.appeared.add((docname, number))
|
||||
raise nodes.SkipNode
|
||||
|
||||
def get_footnote_by_reference(self, node):
|
||||
# type: (nodes.Node) -> nodes.Node
|
||||
docname = node['docname']
|
||||
for footnote in self.footnotes:
|
||||
if docname == footnote['docname'] and footnote['ids'][0] == node['refid']:
|
||||
return footnote
|
||||
|
||||
return None
|
||||
|
@ -23,6 +23,7 @@ from six import itervalues, text_type
|
||||
|
||||
from sphinx import addnodes
|
||||
from sphinx import highlighting
|
||||
from sphinx.builders.latex.nodes import footnotetext
|
||||
from sphinx.builders.latex.transforms import URI_SCHEMES, ShowUrlsTransform # NOQA # for compatibility
|
||||
from sphinx.errors import SphinxError
|
||||
from sphinx.locale import admonitionlabels, _, __
|
||||
@ -834,7 +835,6 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
|
||||
def visit_document(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.footnotestack.append(self.collect_footnotes(node))
|
||||
self.curfilestack.append(node.get('docname', ''))
|
||||
if self.first_document == 1:
|
||||
# the first document is all the regular content ...
|
||||
@ -868,8 +868,6 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
|
||||
def visit_start_of_file(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
# collect new footnotes
|
||||
self.footnotestack.append(self.collect_footnotes(node))
|
||||
# also add a document target
|
||||
self.next_section_ids.add(':doc')
|
||||
self.curfilestack.append(node['docname'])
|
||||
@ -898,7 +896,6 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
|
||||
def depart_start_of_file(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.footnotestack.pop()
|
||||
self.curfilestack.pop()
|
||||
self.hlsettingstack.pop()
|
||||
|
||||
@ -1010,7 +1007,6 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
self.body.append(r'\%s%s{' % (self.sectionnames[-1], short))
|
||||
self.context.append('}\n')
|
||||
|
||||
self.restrict_footnote(node)
|
||||
if self.next_section_ids:
|
||||
for id in self.next_section_ids:
|
||||
self.context[-1] += self.hypertarget(id, anchor=False)
|
||||
@ -1027,7 +1023,6 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
elif isinstance(parent, nodes.table):
|
||||
# Redirect body output until title is finished.
|
||||
self.pushbody([])
|
||||
self.restrict_footnote(node)
|
||||
else:
|
||||
logger.warning(__('encountered title node not in section, topic, table, '
|
||||
'admonition or sidebar'),
|
||||
@ -1041,14 +1036,8 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
self.in_title = 0
|
||||
if isinstance(node.parent, nodes.table):
|
||||
self.table.caption = self.popbody()
|
||||
# temporary buffer for footnotes from caption
|
||||
self.pushbody([])
|
||||
self.unrestrict_footnote(node)
|
||||
# the footnote texts from caption
|
||||
self.table.caption_footnotetexts = self.popbody()
|
||||
else:
|
||||
self.body.append(self.context.pop())
|
||||
self.unrestrict_footnote(node)
|
||||
|
||||
def visit_subtitle(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
@ -1224,32 +1213,20 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
self.body.append(self.context.pop())
|
||||
|
||||
def visit_footnote(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
raise nodes.SkipNode
|
||||
|
||||
def visit_collected_footnote(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.in_footnote += 1
|
||||
if 'footnotetext' in node:
|
||||
self.body.append('%%\n\\begin{footnotetext}[%s]'
|
||||
'\\sphinxAtStartFootnote\n' % node['number'])
|
||||
if self.in_parsed_literal:
|
||||
self.body.append('\\begin{footnote}[%s]' % node[0].astext())
|
||||
else:
|
||||
if self.in_parsed_literal:
|
||||
self.body.append('\\begin{footnote}[%s]' % node['number'])
|
||||
else:
|
||||
self.body.append('%%\n\\begin{footnote}[%s]' % node['number'])
|
||||
self.body.append('\\sphinxAtStartFootnote\n')
|
||||
self.body.append('%%\n\\begin{footnote}[%s]' % node[0].astext())
|
||||
self.body.append('\\sphinxAtStartFootnote\n')
|
||||
|
||||
def depart_collected_footnote(self, node):
|
||||
def depart_footnote(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
if 'footnotetext' in node:
|
||||
# the \ignorespaces in particular for after table header use
|
||||
self.body.append('%\n\\end{footnotetext}\\ignorespaces ')
|
||||
if self.in_parsed_literal:
|
||||
self.body.append('\\end{footnote}')
|
||||
else:
|
||||
if self.in_parsed_literal:
|
||||
self.body.append('\\end{footnote}')
|
||||
else:
|
||||
self.body.append('%\n\\end{footnote}')
|
||||
self.body.append('%\n\\end{footnote}')
|
||||
self.in_footnote -= 1
|
||||
|
||||
def visit_label(self, node):
|
||||
@ -1320,25 +1297,15 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
# type: (nodes.Node) -> None
|
||||
# Redirect head output until header is finished.
|
||||
self.pushbody(self.table.header)
|
||||
# footnotes in longtable header must be restricted
|
||||
self.restrict_footnote(node)
|
||||
|
||||
def depart_thead(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.popbody()
|
||||
# temporary buffer for footnotes from table header
|
||||
self.pushbody([])
|
||||
self.unrestrict_footnote(node)
|
||||
# the footnote texts from header
|
||||
self.table.header_footnotetexts = self.popbody()
|
||||
|
||||
def visit_tbody(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
# Redirect body output until table is finished.
|
||||
self.pushbody(self.table.body)
|
||||
# insert footnotetexts from header at start of body (due to longtable)
|
||||
# those from caption are handled by templates (to allow caption at foot)
|
||||
self.body.extend(self.table.header_footnotetexts)
|
||||
|
||||
def depart_tbody(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
@ -1534,13 +1501,11 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
if node.get('ids'):
|
||||
ctx += self.hypertarget(node['ids'][0])
|
||||
self.body.append('\\item[{')
|
||||
self.restrict_footnote(node)
|
||||
self.context.append(ctx)
|
||||
|
||||
def depart_term(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(self.context.pop())
|
||||
self.unrestrict_footnote(node)
|
||||
self.in_term -= 1
|
||||
|
||||
def visit_classifier(self, node):
|
||||
@ -1591,8 +1556,9 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
not isinstance(node.parent[index - 1], nodes.compound)):
|
||||
# insert blank line, if the paragraph follows a non-paragraph node in a compound
|
||||
self.body.append('\\noindent\n')
|
||||
elif index == 0 and isinstance(node.parent, nodes.footnote):
|
||||
# don't insert blank line, if the paragraph is first child of a footnote
|
||||
elif index == 1 and isinstance(node.parent, (nodes.footnote, footnotetext)):
|
||||
# don't insert blank line, if the paragraph is second child of a footnote
|
||||
# (first one is label node)
|
||||
pass
|
||||
else:
|
||||
self.body.append('\n')
|
||||
@ -1735,7 +1701,6 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
ids += self.hypertarget(id, anchor=False)
|
||||
if node['ids']:
|
||||
ids += self.hypertarget(node['ids'][0], anchor=False)
|
||||
self.restrict_footnote(node)
|
||||
if (len(node.children) and
|
||||
isinstance(node.children[0], nodes.image) and
|
||||
node.children[0]['ids']):
|
||||
@ -1774,12 +1739,10 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
def depart_figure(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(self.context.pop())
|
||||
self.unrestrict_footnote(node)
|
||||
|
||||
def visit_caption(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.in_caption += 1
|
||||
self.restrict_footnote(node)
|
||||
if self.in_container_literal_block:
|
||||
self.body.append('\\sphinxSetupCaptionForVerbatim{')
|
||||
elif self.in_minipage and isinstance(node.parent, nodes.figure):
|
||||
@ -1793,7 +1756,6 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append('}')
|
||||
self.in_caption -= 1
|
||||
self.unrestrict_footnote(node)
|
||||
|
||||
def visit_legend(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
@ -2172,27 +2134,26 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
|
||||
def visit_footnote_reference(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
num = node.astext().strip()
|
||||
try:
|
||||
footnode, used = self.footnotestack[-1][num]
|
||||
except (KeyError, IndexError):
|
||||
raise nodes.SkipNode
|
||||
# if a footnote has been inserted once, it shouldn't be repeated
|
||||
# by the next reference
|
||||
if used:
|
||||
self.body.append('\\sphinxfootnotemark[%s]' % num)
|
||||
elif self.footnote_restricted:
|
||||
self.footnotestack[-1][num][1] = True
|
||||
self.body.append('\\sphinxfootnotemark[%s]' % num)
|
||||
self.pending_footnotes.append(footnode)
|
||||
else:
|
||||
self.footnotestack[-1][num][1] = True
|
||||
footnode.walkabout(self) # type: ignore
|
||||
raise nodes.SkipChildren
|
||||
raise nodes.SkipNode
|
||||
|
||||
def depart_footnote_reference(self, node):
|
||||
def visit_footnotemark(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
self.body.append('\\sphinxfootnotemark[')
|
||||
|
||||
def depart_footnotemark(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
self.body.append(']')
|
||||
|
||||
def visit_footnotetext(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
number = node[0].astext()
|
||||
self.body.append('%%\n\\begin{footnotetext}[%s]'
|
||||
'\\sphinxAtStartFootnote\n' % number)
|
||||
|
||||
def depart_footnotetext(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
# the \ignorespaces in particular for after table header use
|
||||
self.body.append('%\n\\end{footnotetext}\\ignorespaces ')
|
||||
|
||||
def visit_literal_block(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
|
Loading…
Reference in New Issue
Block a user