Sphinx

# Source code for sphinx.builders.texinfo

    """Texinfo builder."""

    from __future__ import annotations

    import os.path
    from typing import TYPE_CHECKING

    from docutils import nodes

    from sphinx import addnodes, package_dir
    from sphinx._cli.util.colour import darkgreen
    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.display import progress_message, status_iterator
    from sphinx.util.docutils import _get_settings, 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, Set

        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 = package_dir.joinpath('templates', 'texinfo')

    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 prepare_writing(self, _docnames: Set[str]) -> 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_documents(self, _docnames: Set[str]) -> None:
            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]
                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)
                    settings = _get_settings(
                        TexinfoWriter, defaults=self.env.settings, read_config_files=True
                    )
                    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
                    visitor: TexinfoTranslator = self.create_translator(doctree, self)  # type: ignore[assignment]
                    doctree.walkabout(visitor)
                    visitor.finish()
                    (self.outdir / targetname).write_text(visitor.output, encoding='utf-8')
                    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
                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.env).get_original_image_uri
                for src in status_iterator(
                    self.images,
                    __('copying images... '),
                    'brown',
                    len(self.images),
                    self.config.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'),
                            self.srcdir / src,
                            err,
                        )

        def copy_support_files(self) -> None:
            try:
                with progress_message(__('copying Texinfo support files')):
                    logger.info('Makefile ', nonl=True)
                    copyfile(
                        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,
            '',
            types=frozenset({list, tuple}),
        )
        app.add_config_value('texinfo_appendices', [], '', types=frozenset({list, tuple}))
        app.add_config_value('texinfo_elements', {}, '', types=frozenset({dict}))
        app.add_config_value(
            'texinfo_domain_indices',
            True,
            '',
            types=frozenset({frozenset, list, set, tuple}),
        )
        app.add_config_value('texinfo_show_urls', 'footnote', '', types=frozenset({str}))
        app.add_config_value('texinfo_no_detailmenu', False, '', types=frozenset({bool}))
        app.add_config_value('texinfo_cross_references', True, '', types=frozenset({bool}))

        return {
            'version': 'builtin',
            'parallel_read_safe': True,
            'parallel_write_safe': True,
        }