Sphinx

# Source code for sphinx.builders.singlehtml

    """Single HTML builders."""

    from __future__ import annotations

    import warnings
    from typing import TYPE_CHECKING

    from docutils import nodes

    from sphinx._cli.util.colour import darkgreen
    from sphinx.builders.html import StandaloneHTMLBuilder
    from sphinx.deprecation import RemovedInSphinx10Warning
    from sphinx.environment.adapters.toctree import global_toctree_for_doc
    from sphinx.locale import __
    from sphinx.util import logging
    from sphinx.util.display import progress_message
    from sphinx.util.nodes import inline_all_toctrees

    if TYPE_CHECKING:
        from collections.abc import Set
        from typing import Any

        from docutils.nodes import Node

        from sphinx.application import Sphinx
        from sphinx.util.typing import ExtensionMetadata

    logger = logging.getLogger(__name__)

    class SingleFileHTMLBuilder(StandaloneHTMLBuilder):
        """Builds the whole document tree as a single HTML page."""

        name = 'singlehtml'
        epilog = __('The HTML page is in %(outdir)s.')

        copysource = False

        def get_outdated_docs(self) -> str | list[str]:  # type: ignore[override]
            return 'all documents'

        def get_target_uri(self, docname: str, typ: str | None = None) -> str:
            if docname in self.env.all_docs:
                # all references are on the same page...
                return '#document-' + docname
            else:
                # chances are this is a html_additional_page
                return docname + self.out_suffix

        def get_relative_uri(self, from_: str, to: str, typ: str | None = None) -> str:
            # ignore source
            return self.get_target_uri(to, typ)

        def fix_refuris(self, tree: Node) -> None:
            deprecation_msg = (
                "The 'SingleFileHTMLBuilder.fix_refuris' method is no longer used "
                'within the builder and is planned for removal in Sphinx 10. '
                'Please report malformed URIs generated by the Sphinx singlehtml '
                'builder as bugreports.'
            )
            warnings.warn(deprecation_msg, RemovedInSphinx10Warning, stacklevel=2)

            # fix refuris with double anchor
            for refnode in tree.findall(nodes.reference):
                if 'refuri' not in refnode:
                    continue
                refuri = refnode['refuri']
                hashindex = refuri.find('#')
                if hashindex < 0:
                    continue
                hashindex = refuri.find('#', hashindex + 1)
                if hashindex >= 0:
                    # all references are on the same page...
                    refnode['refuri'] = refuri[hashindex:]

        def _get_local_toctree(
            self, docname: str, collapse: bool = True, **kwargs: Any
        ) -> str:
            if isinstance(includehidden := kwargs.get('includehidden'), str):
                if includehidden.lower() == 'false':
                    kwargs['includehidden'] = False
                elif includehidden.lower() == 'true':
                    kwargs['includehidden'] = True
            if kwargs.get('maxdepth') == '':  # NoQA: PLC1901
                kwargs.pop('maxdepth')
            toctree = global_toctree_for_doc(
                self.env, docname, self, collapse=collapse, **kwargs
            )
            return self.render_partial(toctree)['fragment']

        def assemble_doctree(self) -> nodes.document:
            master = self.config.root_doc
            tree = self.env.get_doctree(master)
            logger.info(darkgreen(master))
            tree = inline_all_toctrees(self, set(), master, tree, darkgreen, [master])
            tree['docname'] = master
            self.env.resolve_references(tree, master, self)
            return tree

        def assemble_toc_secnumbers(self) -> dict[str, dict[str, tuple[int, ...]]]:
            # Assemble toc_secnumbers to resolve section numbers on SingleHTML.
            # Merge all secnumbers to single secnumber.
            #
            # Note: current Sphinx has refid confliction in singlehtml mode.
            #       To avoid the problem, it replaces key of secnumbers to
            #       tuple of docname and refid.
            #
            #       There are related codes in inline_all_toctres() and
            #       HTMLTranslter#add_secnumber().
            new_secnumbers: dict[str, tuple[int, ...]] = {}
            for docname, secnums in self.env.toc_secnumbers.items():
                for id, secnum in secnums.items():
                    alias = f'{docname}/{id}'
                    new_secnumbers[alias] = secnum

            return {self.config.root_doc: new_secnumbers}

        def assemble_toc_fignumbers(
            self,
        ) -> dict[str, dict[str, dict[str, tuple[int, ...]]]]:
            # Assemble toc_fignumbers to resolve figure numbers on SingleHTML.
            # Merge all fignumbers to single fignumber.
            #
            # Note: current Sphinx has refid confliction in singlehtml mode.
            #       To avoid the problem, it replaces key of secnumbers to
            #       tuple of docname and refid.
            #
            #       There are related codes in inline_all_toctres() and
            #       HTMLTranslter#add_fignumber().
            new_fignumbers: dict[str, dict[str, tuple[int, ...]]] = {}
            # {'foo': {'figure': {'id2': (2,), 'id1': (1,)}}, 'bar': {'figure': {'id1': (3,)}}}
            for docname, fignumlist in self.env.toc_fignumbers.items():
                for figtype, fignums in fignumlist.items():
                    alias = f'{docname}/{figtype}'
                    new_fignumbers.setdefault(alias, {})
                    for id, fignum in fignums.items():
                        new_fignumbers[alias][id] = fignum

            return {self.config.root_doc: new_fignumbers}

        def get_doc_context(self, docname: str, body: str, metatags: str) -> dict[str, Any]:
            # no relation links...
            toctree = global_toctree_for_doc(
                self.env, self.config.root_doc, self, collapse=False
            )
            # if there is no toctree, toc is None
            if toctree:
                toc = self.render_partial(toctree)['fragment']
                display_toc = True
            else:
                toc = ''
                display_toc = False
            return {
                'parents': [],
                'prev': None,
                'next': None,
                'docstitle': None,
                'title': self.config.html_title,
                'meta': None,
                'body': body,
                'metatags': metatags,
                'rellinks': [],
                'sourcename': '',
                'toc': toc,
                'display_toc': display_toc,
            }

        def write_documents(self, _docnames: Set[str]) -> None:
            self.prepare_writing(self.env.all_docs.keys())

            with progress_message(__('assembling single document'), nonl=False):
                doctree = self.assemble_doctree()
                self.env.toc_secnumbers = self.assemble_toc_secnumbers()
                self.env.toc_fignumbers = self.assemble_toc_fignumbers()

            with progress_message(__('writing')):
                self.write_doc_serialized(self.config.root_doc, doctree)
                self.write_doc(self.config.root_doc, doctree)

        def finish(self) -> None:
            self.write_additional_files()
            self.copy_image_files()
            self.copy_download_files()
            self.copy_static_files()
            self.copy_extra_files()
            self.write_buildinfo()
            self.dump_inventory()

        @progress_message(__('writing additional files'))
        def write_additional_files(self) -> None:
            # no indices or search pages are supported

            # additional pages from conf.py
            for pagename, template in self.config.html_additional_pages.items():
                logger.info(' %s', pagename, nonl=True)
                self.handle_page(pagename, {}, template)

            if self.config.html_use_opensearch:
                logger.info(' opensearch', nonl=True)
                self.handle_page(
                    'opensearch',
                    {},
                    'opensearch.xml',
                    outfilename=self._static_dir / 'opensearch.xml',
                )

    def setup(app: Sphinx) -> ExtensionMetadata:
        app.setup_extension('sphinx.builders.html')

        app.add_builder(SingleFileHTMLBuilder)
        app.add_config_value(
            'singlehtml_sidebars',
            lambda self: self.html_sidebars,
            'html',
            types=frozenset({dict}),
        )

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