"""Texinfo builder."""
from __future__ import annotations
import os
import warnings
from os import path
from typing import TYPE_CHECKING, Any
from docutils import nodes
from docutils.frontend import OptionParser
from docutils.io import FileOutput
from sphinx import addnodes, package_dir
from sphinx.builders import Builder
from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.errors import NoUri
from sphinx.locale import _, __
from sphinx.util import logging
from sphinx.util.console import darkgreen
from sphinx.util.display import progress_message, status_iterator
from sphinx.util.docutils import new_document
from sphinx.util.nodes import inline_all_toctrees
from sphinx.util.osutil import SEP, copyfile, ensuredir, make_filename_from_project
from sphinx.writers.texinfo import TexinfoTranslator, TexinfoWriter
if TYPE_CHECKING:
from collections.abc import Iterable
from docutils.nodes import Node
from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
template_dir = os.path.join(package_dir, 'templates', 'texinfo')
[docs]
class TexinfoBuilder(Builder):
"""
Builds Texinfo output to create Info documentation.
"""
name = 'texinfo'
format = 'texinfo'
epilog = __('The Texinfo files are in %(outdir)s.')
if os.name == 'posix':
epilog += __(
"\nRun 'make' in that directory to run these through "
'makeinfo\n'
"(use 'make info' here to do that automatically)."
)
supported_image_types = ['image/png', 'image/jpeg', 'image/gif']
default_translator_class = TexinfoTranslator
def init(self) -> None:
self.docnames: Iterable[str] = []
self.document_data: list[tuple[str, str, str, str, str, str, str, bool]] = []
def get_outdated_docs(self) -> str | list[str]:
return 'all documents' # for now
def get_target_uri(self, docname: str, typ: str | None = None) -> str:
if docname not in self.docnames:
raise NoUri(docname, typ)
return '%' + docname
def get_relative_uri(self, from_: str, to: str, typ: str | None = None) -> str:
# ignore source path
return self.get_target_uri(to, typ)
def init_document_data(self) -> None:
preliminary_document_data = [list(x) for x in self.config.texinfo_documents]
if not preliminary_document_data:
logger.warning(
__(
'no "texinfo_documents" config value found; no documents '
'will be written'
)
)
return
# assign subdirs to titles
self.titles: list[tuple[str, str]] = []
for entry in preliminary_document_data:
docname = entry[0]
if docname not in self.env.all_docs:
logger.warning(
__(
'"texinfo_documents" config value references unknown '
'document %s'
),
docname,
)
continue
self.document_data.append(entry) # type: ignore[arg-type]
docname = docname.removesuffix(SEP + 'index')
self.titles.append((docname, entry[2]))
def write(self, *ignored: Any) -> None:
self.init_document_data()
self.copy_assets()
for entry in self.document_data:
docname, targetname, title, author = entry[:4]
targetname += '.texi'
direntry = description = category = ''
if len(entry) > 6:
direntry, description, category = entry[4:7]
toctree_only = False
if len(entry) > 7:
toctree_only = entry[7]
destination = FileOutput(
destination_path=path.join(self.outdir, targetname), encoding='utf-8'
)
with progress_message(__('processing %s') % targetname, nonl=False):
appendices = self.config.texinfo_appendices or []
doctree = self.assemble_doctree(
docname, toctree_only, appendices=appendices
)
with progress_message(__('writing')):
self.post_process_images(doctree)
docwriter = TexinfoWriter(self)
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=DeprecationWarning)
# DeprecationWarning: The frontend.OptionParser class will be replaced
# by a subclass of argparse.ArgumentParser in Docutils 0.21 or later.
settings: Any = OptionParser(
defaults=self.env.settings,
components=(docwriter,),
read_config_files=True,
).get_default_values()
settings.author = author
settings.title = title
settings.texinfo_filename = targetname[:-5] + '.info'
settings.texinfo_elements = self.config.texinfo_elements
settings.texinfo_dir_entry = direntry or ''
settings.texinfo_dir_category = category or ''
settings.texinfo_dir_description = description or ''
settings.docname = docname
doctree.settings = settings
docwriter.write(doctree, destination)
self.copy_image_files(targetname[:-5])
def assemble_doctree(
self,
indexfile: str,
toctree_only: bool,
appendices: list[str],
) -> nodes.document:
self.docnames = {indexfile, *appendices}
logger.info(darkgreen(indexfile))
tree = self.env.get_doctree(indexfile)
tree['docname'] = indexfile
if toctree_only:
# extract toctree nodes from the tree and put them in a
# fresh document
new_tree = new_document('<texinfo output>')
new_sect = nodes.section()
new_sect += nodes.title('<Set title in conf.py>', '<Set title in conf.py>')
new_tree += new_sect
for node in tree.findall(addnodes.toctree):
new_sect += node
tree = new_tree
largetree = inline_all_toctrees(
self, self.docnames, indexfile, tree, darkgreen, [indexfile]
)
largetree['docname'] = indexfile
for docname in appendices:
appendix = self.env.get_doctree(docname)
appendix['docname'] = docname
largetree.append(appendix)
logger.info('')
logger.info(__('resolving references...'))
self.env.resolve_references(largetree, indexfile, self)
# TODO: add support for external :ref:s
for pendingnode in largetree.findall(addnodes.pending_xref):
docname = pendingnode['refdocname']
sectname = pendingnode['refsectname']
newnodes: list[Node] = [nodes.emphasis(sectname, sectname)]
for subdir, title in self.titles:
if docname.startswith(subdir):
newnodes.extend((
nodes.Text(_(' (in ')),
nodes.emphasis(title, title),
nodes.Text(')'),
))
break
else:
pass
pendingnode.replace_self(newnodes)
return largetree
def copy_assets(self) -> None:
self.copy_support_files()
def copy_image_files(self, targetname: str) -> None:
if self.images:
stringify_func = ImageAdapter(self.app.env).get_original_image_uri
for src in status_iterator(
self.images,
__('copying images... '),
'brown',
len(self.images),
self.app.verbosity,
stringify_func=stringify_func,
):
dest = self.images[src]
try:
imagedir = self.outdir / f'{targetname}-figures'
ensuredir(imagedir)
copyfile(
self.srcdir / src,
imagedir / dest,
force=True,
)
except Exception as err:
logger.warning(
__('cannot copy image file %r: %s'),
path.join(self.srcdir, src),
err,
)
def copy_support_files(self) -> None:
try:
with progress_message(__('copying Texinfo support files')):
logger.info('Makefile ', nonl=True)
copyfile(
os.path.join(template_dir, 'Makefile'),
self.outdir / 'Makefile',
force=True,
)
except OSError as err:
logger.warning(__('error writing file Makefile: %s'), err)
def default_texinfo_documents(
config: Config,
) -> list[tuple[str, str, str, str, str, str, str]]:
"""Better default texinfo_documents settings."""
filename = make_filename_from_project(config.project)
return [
(
config.root_doc,
filename,
config.project,
config.author,
filename,
'One line description of project',
'Miscellaneous',
)
]
def setup(app: Sphinx) -> ExtensionMetadata:
app.add_builder(TexinfoBuilder)
app.add_config_value('texinfo_documents', default_texinfo_documents, '')
app.add_config_value('texinfo_appendices', [], '')
app.add_config_value('texinfo_elements', {}, '')
app.add_config_value('texinfo_domain_indices', True, '', types={set, list})
app.add_config_value('texinfo_show_urls', 'footnote', '')
app.add_config_value('texinfo_no_detailmenu', False, '')
app.add_config_value('texinfo_cross_references', True, '')
return {
'version': 'builtin',
'parallel_read_safe': True,
'parallel_write_safe': True,
}