"""Transforms for LaTeX builder."""
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from docutils import nodes
from docutils.transforms.references import Substitutions
from sphinx import addnodes
from sphinx.builders.latex.nodes import (
captioned_literal_block,
footnotemark,
footnotetext,
math_reference,
thebibliography,
)
from sphinx.locale import __
from sphinx.transforms import SphinxTransform
from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util.nodes import NodeMatcher
if TYPE_CHECKING:
from typing import Any
from docutils.nodes import Element, Node
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata
URI_SCHEMES = ('mailto:', 'http:', 'https:', 'ftp:')
class FootnoteDocnameUpdater(SphinxTransform):
"""Add docname to footnote and footnote_reference nodes."""
default_priority = 700
TARGET_NODES = (nodes.footnote, nodes.footnote_reference)
def apply(self, **kwargs: Any) -> None:
matcher = NodeMatcher(*self.TARGET_NODES)
for node in matcher.findall(self.document):
node['docname'] = self.env.current_document.docname
class SubstitutionDefinitionsRemover(SphinxPostTransform):
"""Remove ``substitution_definition`` nodes from doctrees."""
# should be invoked after Substitutions process
default_priority = Substitutions.default_priority + 1
formats = ('latex',)
def run(self, **kwargs: Any) -> None:
for node in list(self.document.findall(nodes.substitution_definition)):
node.parent.remove(node)
class ShowUrlsTransform(SphinxPostTransform):
"""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
formats = ('latex',)
# references are expanded to footnotes (or not)
expanded = False
def run(self, **kwargs: Any) -> None:
try:
# replace id_prefix temporarily
settings: Any = self.document.settings
id_prefix = settings.id_prefix
settings.id_prefix = 'show_urls'
self.expand_show_urls()
if self.expanded:
self.renumber_footnotes()
finally:
# restore id_prefix
settings.id_prefix = id_prefix
def expand_show_urls(self) -> None:
show_urls = self.config.latex_show_urls
if show_urls is False or show_urls == 'no':
return
for node in list(self.document.findall(nodes.reference)):
uri = node.get('refuri', '')
if uri.startswith(URI_SCHEMES):
uri = uri.removeprefix('mailto:')
if node.astext() != uri:
index = node.parent.index(node)
docname = self.get_docname_for_node(node)
if show_urls == 'footnote':
fn, fnref = self.create_footnote(uri, docname)
node.parent.insert(index + 1, fn)
node.parent.insert(index + 2, fnref)
self.expanded = True
else: # all other true values (b/w compat)
textnode = nodes.Text(' (%s)' % uri)
node.parent.insert(index + 1, textnode)
def get_docname_for_node(self, node: Node) -> str:
while node:
if isinstance(node, nodes.document):
return self.env.path2doc(node['source']) or ''
elif isinstance(node, addnodes.start_of_file):
return node['docname']
else:
node = node.parent
try:
source = node['source']
except TypeError:
raise ValueError(__('Failed to get a docname!')) from None
msg = __('Failed to get a docname for source %r!') % source
raise ValueError(msg)
def create_footnote(
self, uri: str, docname: str
) -> tuple[nodes.footnote, nodes.footnote_reference]:
reference = nodes.reference('', nodes.Text(uri), refuri=uri, nolinkurl=True)
footnote = nodes.footnote(uri, auto=1, docname=docname)
footnote['names'].append('#')
footnote += nodes.label('', '#')
footnote += nodes.paragraph('', '', reference)
self.document.note_autofootnote(footnote)
footnote_ref = nodes.footnote_reference(
'[#]_', auto=1, refid=footnote['ids'][0], docname=docname
)
footnote_ref += nodes.Text('#')
self.document.note_autofootnote_ref(footnote_ref)
footnote.add_backref(footnote_ref['ids'][0])
return footnote, footnote_ref
def renumber_footnotes(self) -> None:
collector = FootnoteCollector(self.document)
self.document.walkabout(collector)
num = 0
for footnote in collector.auto_footnotes:
# search unused footnote number
while True:
num += 1
if str(num) not in collector.used_footnote_numbers:
break
# assign new footnote number
old_label = cast('nodes.label', footnote[0])
old_label.replace_self(nodes.label('', str(num)))
if old_label in footnote['names']:
footnote['names'].remove(old_label.astext())
footnote['names'].append(str(num))
# update footnote_references by new footnote number
docname = footnote['docname']
for ref in collector.footnote_refs:
if docname == ref['docname'] and footnote['ids'][0] == ref['refid']:
ref.remove(ref[0])
ref += nodes.Text(str(num))
class FootnoteCollector(nodes.NodeVisitor):
"""Collect footnotes and footnote references on the document"""
def __init__(self, document: nodes.document) -> None:
self.auto_footnotes: list[nodes.footnote] = []
self.used_footnote_numbers: set[str] = set()
self.footnote_refs: list[nodes.footnote_reference] = []
super().__init__(document)
def unknown_visit(self, node: Node) -> None:
pass
def unknown_departure(self, node: Node) -> None:
pass
def visit_footnote(self, node: nodes.footnote) -> None:
if node.get('auto'):
self.auto_footnotes.append(node)
else:
for name in node['names']:
self.used_footnote_numbers.add(name)
def visit_footnote_reference(self, node: nodes.footnote_reference) -> None:
self.footnote_refs.append(node)
class LaTeXFootnoteTransform(SphinxPostTransform):
"""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::
headings having footnotes
1