diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:25:40 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:25:40 +0000 |
commit | cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a (patch) | |
tree | 18dcde1a8d1f5570a77cd0c361de3b490d02c789 /sphinx/environment/collectors/toctree.py | |
parent | Initial commit. (diff) | |
download | sphinx-cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a.tar.xz sphinx-cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a.zip |
Adding upstream version 7.2.6.upstream/7.2.6
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sphinx/environment/collectors/toctree.py')
-rw-r--r-- | sphinx/environment/collectors/toctree.py | 355 |
1 files changed, 355 insertions, 0 deletions
diff --git a/sphinx/environment/collectors/toctree.py b/sphinx/environment/collectors/toctree.py new file mode 100644 index 0000000..772591e --- /dev/null +++ b/sphinx/environment/collectors/toctree.py @@ -0,0 +1,355 @@ +"""Toctree collector for sphinx.environment.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from docutils import nodes + +from sphinx import addnodes +from sphinx.environment.adapters.toctree import note_toctree +from sphinx.environment.collectors import EnvironmentCollector +from sphinx.locale import __ +from sphinx.transforms import SphinxContentsFilter +from sphinx.util import logging, url_re + +if TYPE_CHECKING: + from collections.abc import Sequence + + from docutils.nodes import Element, Node + + from sphinx.application import Sphinx + from sphinx.environment import BuildEnvironment + +N = TypeVar('N') + +logger = logging.getLogger(__name__) + + +class TocTreeCollector(EnvironmentCollector): + def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None: + env.tocs.pop(docname, None) + env.toc_secnumbers.pop(docname, None) + env.toc_fignumbers.pop(docname, None) + env.toc_num_entries.pop(docname, None) + env.toctree_includes.pop(docname, None) + env.glob_toctrees.discard(docname) + env.numbered_toctrees.discard(docname) + + for subfn, fnset in list(env.files_to_rebuild.items()): + fnset.discard(docname) + if not fnset: + del env.files_to_rebuild[subfn] + + def merge_other(self, app: Sphinx, env: BuildEnvironment, docnames: set[str], + other: BuildEnvironment) -> None: + for docname in docnames: + env.tocs[docname] = other.tocs[docname] + env.toc_num_entries[docname] = other.toc_num_entries[docname] + if docname in other.toctree_includes: + env.toctree_includes[docname] = other.toctree_includes[docname] + if docname in other.glob_toctrees: + env.glob_toctrees.add(docname) + if docname in other.numbered_toctrees: + env.numbered_toctrees.add(docname) + + for subfn, fnset in other.files_to_rebuild.items(): + env.files_to_rebuild.setdefault(subfn, set()).update(fnset & set(docnames)) + + def process_doc(self, app: Sphinx, doctree: nodes.document) -> None: + """Build a TOC from the doctree and store it in the inventory.""" + docname = app.env.docname + numentries = [0] # nonlocal again... + + def build_toc( + node: Element | Sequence[Element], + depth: int = 1, + ) -> nodes.bullet_list | None: + # list of table of contents entries + entries: list[Element] = [] + # cache of parents -> list item + memo_parents: dict[tuple[str, ...], nodes.list_item] = {} + for sectionnode in node: + # find all toctree nodes in this section and add them + # to the toc (just copying the toctree node which is then + # resolved in self.get_and_resolve_doctree) + if isinstance(sectionnode, nodes.section): + title = sectionnode[0] + # copy the contents of the section title, but without references + # and unnecessary stuff + visitor = SphinxContentsFilter(doctree) + title.walkabout(visitor) + nodetext = visitor.get_entry_text() + anchorname = _make_anchor_name(sectionnode['ids'], numentries) + # make these nodes: + # list_item -> compact_paragraph -> reference + reference = nodes.reference( + '', '', internal=True, refuri=docname, + anchorname=anchorname, *nodetext) + para = addnodes.compact_paragraph('', '', reference) + item: Element = nodes.list_item('', para) + sub_item = build_toc(sectionnode, depth + 1) + if sub_item: + item += sub_item + entries.append(item) + # Wrap items under an ``.. only::`` directive in a node for + # post-processing + elif isinstance(sectionnode, addnodes.only): + onlynode = addnodes.only(expr=sectionnode['expr']) + blist = build_toc(sectionnode, depth) + if blist: + onlynode += blist.children + entries.append(onlynode) + # check within the section for other node types + elif isinstance(sectionnode, nodes.Element): + toctreenode: nodes.Node + for toctreenode in sectionnode.findall(): + if isinstance(toctreenode, nodes.section): + continue + if isinstance(toctreenode, addnodes.toctree): + item = toctreenode.copy() + entries.append(item) + # important: do the inventory stuff + note_toctree(app.env, docname, toctreenode) + # add object signatures within a section to the ToC + elif isinstance(toctreenode, addnodes.desc): + for sig_node in toctreenode: + if not isinstance(sig_node, addnodes.desc_signature): + continue + # Skip if no name set + if not sig_node.get('_toc_name', ''): + continue + # Skip if explicitly disabled + if sig_node.parent.get('no-contents-entry'): + continue + # Skip entries with no ID (e.g. with :no-index: set) + ids = sig_node['ids'] + if not ids: + continue + + anchorname = _make_anchor_name(ids, numentries) + + reference = nodes.reference( + '', '', nodes.literal('', sig_node['_toc_name']), + internal=True, refuri=docname, anchorname=anchorname) + para = addnodes.compact_paragraph('', '', reference, + skip_section_number=True) + entry = nodes.list_item('', para) + *parents, _ = sig_node['_toc_parts'] + parents = tuple(parents) + + # Cache parents tuple + memo_parents[sig_node['_toc_parts']] = entry + + # Nest children within parents + if parents and parents in memo_parents: + root_entry = memo_parents[parents] + if isinstance(root_entry[-1], nodes.bullet_list): + root_entry[-1].append(entry) + else: + root_entry.append(nodes.bullet_list('', entry)) + continue + + entries.append(entry) + + if entries: + return nodes.bullet_list('', *entries) + return None + + toc = build_toc(doctree) + if toc: + app.env.tocs[docname] = toc + else: + app.env.tocs[docname] = nodes.bullet_list('') + app.env.toc_num_entries[docname] = numentries[0] + + def get_updated_docs(self, app: Sphinx, env: BuildEnvironment) -> list[str]: + return self.assign_section_numbers(env) + self.assign_figure_numbers(env) + + def assign_section_numbers(self, env: BuildEnvironment) -> list[str]: + """Assign a section number to each heading under a numbered toctree.""" + # a list of all docnames whose section numbers changed + rewrite_needed = [] + + assigned: set[str] = set() + old_secnumbers = env.toc_secnumbers + env.toc_secnumbers = {} + + def _walk_toc( + node: Element, secnums: dict, depth: int, titlenode: nodes.title | None = None, + ) -> None: + # titlenode is the title of the document, it will get assigned a + # secnumber too, so that it shows up in next/prev/parent rellinks + for subnode in node.children: + if isinstance(subnode, nodes.bullet_list): + numstack.append(0) + _walk_toc(subnode, secnums, depth - 1, titlenode) + numstack.pop() + titlenode = None + elif isinstance(subnode, nodes.list_item): # NoQA: SIM114 + _walk_toc(subnode, secnums, depth, titlenode) + titlenode = None + elif isinstance(subnode, addnodes.only): + # at this stage we don't know yet which sections are going + # to be included; just include all of them, even if it leads + # to gaps in the numbering + _walk_toc(subnode, secnums, depth, titlenode) + titlenode = None + elif isinstance(subnode, addnodes.compact_paragraph): + if 'skip_section_number' in subnode: + continue + numstack[-1] += 1 + reference = cast(nodes.reference, subnode[0]) + if depth > 0: + number = list(numstack) + secnums[reference['anchorname']] = tuple(numstack) + else: + number = None + secnums[reference['anchorname']] = None + reference['secnumber'] = number + if titlenode: + titlenode['secnumber'] = number + titlenode = None + elif isinstance(subnode, addnodes.toctree): + _walk_toctree(subnode, depth) + + def _walk_toctree(toctreenode: addnodes.toctree, depth: int) -> None: + if depth == 0: + return + for (_title, ref) in toctreenode['entries']: + if url_re.match(ref) or ref == 'self': + # don't mess with those + continue + if ref in assigned: + logger.warning(__('%s is already assigned section numbers ' + '(nested numbered toctree?)'), ref, + location=toctreenode, type='toc', subtype='secnum') + elif ref in env.tocs: + secnums: dict[str, tuple[int, ...]] = {} + env.toc_secnumbers[ref] = secnums + assigned.add(ref) + _walk_toc(env.tocs[ref], secnums, depth, env.titles.get(ref)) + if secnums != old_secnumbers.get(ref): + rewrite_needed.append(ref) + + for docname in env.numbered_toctrees: + assigned.add(docname) + doctree = env.get_doctree(docname) + for toctreenode in doctree.findall(addnodes.toctree): + depth = toctreenode.get('numbered', 0) + if depth: + # every numbered toctree gets new numbering + numstack = [0] + _walk_toctree(toctreenode, depth) + + return rewrite_needed + + def assign_figure_numbers(self, env: BuildEnvironment) -> list[str]: + """Assign a figure number to each figure under a numbered toctree.""" + generated_docnames = frozenset(env.domains['std']._virtual_doc_names) + + rewrite_needed = [] + + assigned: set[str] = set() + old_fignumbers = env.toc_fignumbers + env.toc_fignumbers = {} + fignum_counter: dict[str, dict[tuple[int, ...], int]] = {} + + def get_figtype(node: Node) -> str | None: + for domain in env.domains.values(): + figtype = domain.get_enumerable_node_type(node) + if (domain.name == 'std' + and not domain.get_numfig_title(node)): # type: ignore[attr-defined] # NoQA: E501 + # Skip if uncaptioned node + continue + + if figtype: + return figtype + + return None + + def get_section_number(docname: str, section: nodes.section) -> tuple[int, ...]: + anchorname = '#' + section['ids'][0] + secnumbers = env.toc_secnumbers.get(docname, {}) + if anchorname in secnumbers: + secnum = secnumbers.get(anchorname) + else: + secnum = secnumbers.get('') + + return secnum or () + + def get_next_fignumber(figtype: str, secnum: tuple[int, ...]) -> tuple[int, ...]: + counter = fignum_counter.setdefault(figtype, {}) + + secnum = secnum[:env.config.numfig_secnum_depth] + counter[secnum] = counter.get(secnum, 0) + 1 + return secnum + (counter[secnum],) + + def register_fignumber(docname: str, secnum: tuple[int, ...], + figtype: str, fignode: Element) -> None: + env.toc_fignumbers.setdefault(docname, {}) + fignumbers = env.toc_fignumbers[docname].setdefault(figtype, {}) + figure_id = fignode['ids'][0] + + fignumbers[figure_id] = get_next_fignumber(figtype, secnum) + + def _walk_doctree(docname: str, doctree: Element, secnum: tuple[int, ...]) -> None: + nonlocal generated_docnames + for subnode in doctree.children: + if isinstance(subnode, nodes.section): + next_secnum = get_section_number(docname, subnode) + if next_secnum: + _walk_doctree(docname, subnode, next_secnum) + else: + _walk_doctree(docname, subnode, secnum) + elif isinstance(subnode, addnodes.toctree): + for _title, subdocname in subnode['entries']: + if url_re.match(subdocname) or subdocname == 'self': + # don't mess with those + continue + if subdocname in generated_docnames: + # or these + continue + + _walk_doc(subdocname, secnum) + elif isinstance(subnode, nodes.Element): + figtype = get_figtype(subnode) + if figtype and subnode['ids']: + register_fignumber(docname, secnum, figtype, subnode) + + _walk_doctree(docname, subnode, secnum) + + def _walk_doc(docname: str, secnum: tuple[int, ...]) -> None: + if docname not in assigned: + assigned.add(docname) + doctree = env.get_doctree(docname) + _walk_doctree(docname, doctree, secnum) + + if env.config.numfig: + _walk_doc(env.config.root_doc, ()) + for docname, fignums in env.toc_fignumbers.items(): + if fignums != old_fignumbers.get(docname): + rewrite_needed.append(docname) + + return rewrite_needed + + +def _make_anchor_name(ids: list[str], num_entries: list[int]) -> str: + if not num_entries[0]: + # for the very first toc entry, don't add an anchor + # as it is the file's title anyway + anchorname = '' + else: + anchorname = '#' + ids[0] + num_entries[0] += 1 + return anchorname + + +def setup(app: Sphinx) -> dict[str, Any]: + app.add_env_collector(TocTreeCollector) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } |