refactor: Add LaTeXFootnoteTransform to restructure footnotes for LaTeX

This commit is contained in:
Takeshi KOMIYA 2018-04-20 21:21:45 +09:00
parent ff0f008574
commit 89d68d9ac3
4 changed files with 360 additions and 71 deletions

View File

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

View 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

View File

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

View File

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