mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Refactor `sphinx.environment.adapters.TocTree
` (#11565)
This commit is contained in:
@@ -32,7 +32,7 @@ from sphinx.domains import Domain, Index, IndexEntry
|
||||
from sphinx.environment import BuildEnvironment
|
||||
from sphinx.environment.adapters.asset import ImageAdapter
|
||||
from sphinx.environment.adapters.indexentries import IndexEntries
|
||||
from sphinx.environment.adapters.toctree import TocTree
|
||||
from sphinx.environment.adapters.toctree import document_toc, global_toctree_for_doc
|
||||
from sphinx.errors import ConfigError, ThemeError
|
||||
from sphinx.highlighting import PygmentsBridge
|
||||
from sphinx.locale import _, __
|
||||
@@ -638,7 +638,7 @@ class StandaloneHTMLBuilder(Builder):
|
||||
meta = self.env.metadata.get(docname)
|
||||
|
||||
# local TOC and global TOC tree
|
||||
self_toc = TocTree(self.env).get_toc_for(docname, self)
|
||||
self_toc = document_toc(self.env, docname, self.tags)
|
||||
toc = self.render_partial(self_toc)['fragment']
|
||||
|
||||
return {
|
||||
@@ -969,8 +969,8 @@ class StandaloneHTMLBuilder(Builder):
|
||||
kwargs['includehidden'] = False
|
||||
if kwargs.get('maxdepth') == '':
|
||||
kwargs.pop('maxdepth')
|
||||
return self.render_partial(TocTree(self.env).get_toctree_for(
|
||||
docname, self, collapse, **kwargs))['fragment']
|
||||
toctree = global_toctree_for_doc(self.env, docname, self, collapse=collapse, **kwargs)
|
||||
return self.render_partial(toctree)['fragment']
|
||||
|
||||
def get_outfilename(self, pagename: str) -> str:
|
||||
return path.join(self.outdir, os_path(pagename) + self.out_suffix)
|
||||
|
@@ -10,7 +10,7 @@ from docutils.nodes import Node
|
||||
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.builders.html import StandaloneHTMLBuilder
|
||||
from sphinx.environment.adapters.toctree import TocTree
|
||||
from sphinx.environment.adapters.toctree import global_toctree_for_doc
|
||||
from sphinx.locale import __
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.console import darkgreen # type: ignore
|
||||
@@ -61,9 +61,13 @@ class SingleFileHTMLBuilder(StandaloneHTMLBuilder):
|
||||
refnode['refuri'] = fname + refuri[hashindex:]
|
||||
|
||||
def _get_local_toctree(self, docname: str, collapse: bool = True, **kwargs: Any) -> str:
|
||||
if 'includehidden' not in kwargs:
|
||||
if kwargs.get('includehidden', 'false').lower() == 'false':
|
||||
kwargs['includehidden'] = False
|
||||
toctree = TocTree(self.env).get_toctree_for(docname, self, collapse, **kwargs)
|
||||
elif kwargs['includehidden'].lower() == 'true':
|
||||
kwargs['includehidden'] = True
|
||||
if kwargs.get('maxdepth') == '':
|
||||
kwargs.pop('maxdepth')
|
||||
toctree = global_toctree_for_doc(self.env, docname, self, collapse=collapse, **kwargs)
|
||||
if toctree is not None:
|
||||
self.fix_refuris(toctree)
|
||||
return self.render_partial(toctree)['fragment']
|
||||
@@ -118,7 +122,7 @@ class SingleFileHTMLBuilder(StandaloneHTMLBuilder):
|
||||
|
||||
def get_doc_context(self, docname: str, body: str, metatags: str) -> dict[str, Any]:
|
||||
# no relation links...
|
||||
toctree = TocTree(self.env).get_toctree_for(self.config.root_doc, self, False)
|
||||
toctree = global_toctree_for_doc(self.env, self.config.root_doc, self, collapse=False)
|
||||
# if there is no toctree, toc is None
|
||||
if toctree:
|
||||
self.fix_refuris(toctree)
|
||||
|
@@ -12,6 +12,7 @@ from docutils.parsers.rst.directives.misc import Include as BaseInclude
|
||||
|
||||
from sphinx import addnodes
|
||||
from sphinx.domains.changeset import VersionChange # noqa: F401 # for compatibility
|
||||
from sphinx.domains.std import StandardDomain
|
||||
from sphinx.locale import _, __
|
||||
from sphinx.util import docname_join, logging, url_re
|
||||
from sphinx.util.docutils import SphinxDirective
|
||||
@@ -79,71 +80,81 @@ class TocTree(SphinxDirective):
|
||||
return ret
|
||||
|
||||
def parse_content(self, toctree: addnodes.toctree) -> list[Node]:
|
||||
generated_docnames = frozenset(self.env.domains['std']._virtual_doc_names)
|
||||
generated_docnames = frozenset(StandardDomain._virtual_doc_names)
|
||||
suffixes = self.config.source_suffix
|
||||
current_docname = self.env.docname
|
||||
glob = toctree['glob']
|
||||
|
||||
# glob target documents
|
||||
all_docnames = self.env.found_docs.copy() | generated_docnames
|
||||
all_docnames.remove(self.env.docname) # remove current document
|
||||
all_docnames.remove(current_docname) # remove current document
|
||||
frozen_all_docnames = frozenset(all_docnames)
|
||||
|
||||
ret: list[Node] = []
|
||||
excluded = Matcher(self.config.exclude_patterns)
|
||||
for entry in self.content:
|
||||
if not entry:
|
||||
continue
|
||||
|
||||
# look for explicit titles ("Some Title <document>")
|
||||
explicit = explicit_title_re.match(entry)
|
||||
if (toctree['glob'] and glob_re.match(entry) and
|
||||
not explicit and not url_re.match(entry)):
|
||||
patname = docname_join(self.env.docname, entry)
|
||||
docnames = sorted(patfilter(all_docnames, patname))
|
||||
for docname in docnames:
|
||||
url_match = url_re.match(entry) is not None
|
||||
if glob and glob_re.match(entry) and not explicit and not url_match:
|
||||
pat_name = docname_join(current_docname, entry)
|
||||
doc_names = sorted(patfilter(all_docnames, pat_name))
|
||||
for docname in doc_names:
|
||||
if docname in generated_docnames:
|
||||
# don't include generated documents in globs
|
||||
continue
|
||||
all_docnames.remove(docname) # don't include it again
|
||||
toctree['entries'].append((None, docname))
|
||||
toctree['includefiles'].append(docname)
|
||||
if not docnames:
|
||||
if not doc_names:
|
||||
logger.warning(__("toctree glob pattern %r didn't match any documents"),
|
||||
entry, location=toctree)
|
||||
continue
|
||||
|
||||
if explicit:
|
||||
ref = explicit.group(2)
|
||||
title = explicit.group(1)
|
||||
docname = ref
|
||||
else:
|
||||
if explicit:
|
||||
ref = explicit.group(2)
|
||||
title = explicit.group(1)
|
||||
docname = ref
|
||||
else:
|
||||
ref = docname = entry
|
||||
title = None
|
||||
# remove suffixes (backwards compatibility)
|
||||
for suffix in suffixes:
|
||||
if docname.endswith(suffix):
|
||||
docname = docname[:-len(suffix)]
|
||||
break
|
||||
# absolutize filenames
|
||||
docname = docname_join(self.env.docname, docname)
|
||||
if url_re.match(ref) or ref == 'self':
|
||||
toctree['entries'].append((title, ref))
|
||||
elif docname not in self.env.found_docs | generated_docnames:
|
||||
if excluded(self.env.doc2path(docname, False)):
|
||||
message = __('toctree contains reference to excluded document %r')
|
||||
subtype = 'excluded'
|
||||
else:
|
||||
message = __('toctree contains reference to nonexisting document %r')
|
||||
subtype = 'not_readable'
|
||||
ref = docname = entry
|
||||
title = None
|
||||
|
||||
logger.warning(message, docname, type='toc', subtype=subtype,
|
||||
location=toctree)
|
||||
self.env.note_reread()
|
||||
else:
|
||||
if docname in all_docnames:
|
||||
all_docnames.remove(docname)
|
||||
else:
|
||||
logger.warning(__('duplicated entry found in toctree: %s'), docname,
|
||||
location=toctree)
|
||||
# remove suffixes (backwards compatibility)
|
||||
for suffix in suffixes:
|
||||
if docname.endswith(suffix):
|
||||
docname = docname.removesuffix(suffix)
|
||||
break
|
||||
|
||||
toctree['entries'].append((title, docname))
|
||||
toctree['includefiles'].append(docname)
|
||||
# absolutise filenames
|
||||
docname = docname_join(current_docname, docname)
|
||||
if url_match or ref == 'self':
|
||||
toctree['entries'].append((title, ref))
|
||||
continue
|
||||
|
||||
if docname not in frozen_all_docnames:
|
||||
if excluded(self.env.doc2path(docname, False)):
|
||||
message = __('toctree contains reference to excluded document %r')
|
||||
subtype = 'excluded'
|
||||
else:
|
||||
message = __('toctree contains reference to nonexisting document %r')
|
||||
subtype = 'not_readable'
|
||||
|
||||
logger.warning(message, docname, type='toc', subtype=subtype,
|
||||
location=toctree)
|
||||
self.env.note_reread()
|
||||
continue
|
||||
|
||||
if docname in all_docnames:
|
||||
all_docnames.remove(docname)
|
||||
else:
|
||||
logger.warning(__('duplicated entry found in toctree: %s'), docname,
|
||||
location=toctree)
|
||||
|
||||
toctree['entries'].append((title, docname))
|
||||
toctree['includefiles'].append(docname)
|
||||
|
||||
# entries contains all entries (self references, external links etc.)
|
||||
if 'reversed' in self.options:
|
||||
|
@@ -17,7 +17,7 @@ from docutils.nodes import Node
|
||||
from sphinx import addnodes
|
||||
from sphinx.config import Config
|
||||
from sphinx.domains import Domain
|
||||
from sphinx.environment.adapters.toctree import TocTree
|
||||
from sphinx.environment.adapters.toctree import _resolve_toctree
|
||||
from sphinx.errors import BuildEnvironmentError, DocumentError, ExtensionError, SphinxError
|
||||
from sphinx.events import EventManager
|
||||
from sphinx.locale import __
|
||||
@@ -58,7 +58,7 @@ default_settings: dict[str, Any] = {
|
||||
|
||||
# This is increased every time an environment attribute is added
|
||||
# or changed to properly invalidate pickle files.
|
||||
ENV_VERSION = 58
|
||||
ENV_VERSION = 59
|
||||
|
||||
# config status
|
||||
CONFIG_UNSET = -1
|
||||
@@ -630,9 +630,9 @@ class BuildEnvironment:
|
||||
|
||||
# now, resolve all toctree nodes
|
||||
for toctreenode in doctree.findall(addnodes.toctree):
|
||||
result = TocTree(self).resolve(docname, builder, toctreenode,
|
||||
prune=prune_toctrees,
|
||||
includehidden=includehidden)
|
||||
result = _resolve_toctree(self, docname, builder, toctreenode,
|
||||
prune=prune_toctrees,
|
||||
includehidden=includehidden)
|
||||
if result is None:
|
||||
toctreenode.parent.replace(toctreenode, [])
|
||||
else:
|
||||
@@ -654,9 +654,8 @@ class BuildEnvironment:
|
||||
If *collapse* is True, all branches not containing docname will
|
||||
be collapsed.
|
||||
"""
|
||||
return TocTree(self).resolve(docname, builder, toctree, prune,
|
||||
maxdepth, titles_only, collapse,
|
||||
includehidden)
|
||||
return _resolve_toctree(self, docname, builder, toctree, prune,
|
||||
maxdepth, titles_only, collapse, includehidden)
|
||||
|
||||
def resolve_references(self, doctree: nodes.document, fromdocname: str,
|
||||
builder: Builder) -> None:
|
||||
@@ -680,38 +679,21 @@ class BuildEnvironment:
|
||||
self.events.emit('doctree-resolved', doctree, docname)
|
||||
|
||||
def collect_relations(self) -> dict[str, list[str | None]]:
|
||||
traversed = set()
|
||||
|
||||
def traverse_toctree(
|
||||
parent: str | None, docname: str,
|
||||
) -> Iterator[tuple[str | None, str]]:
|
||||
if parent == docname:
|
||||
logger.warning(__('self referenced toctree found. Ignored.'),
|
||||
location=docname, type='toc',
|
||||
subtype='circular')
|
||||
return
|
||||
|
||||
# traverse toctree by pre-order
|
||||
yield parent, docname
|
||||
traversed.add(docname)
|
||||
|
||||
for child in (self.toctree_includes.get(docname) or []):
|
||||
for subparent, subdocname in traverse_toctree(docname, child):
|
||||
if subdocname not in traversed:
|
||||
yield subparent, subdocname
|
||||
traversed.add(subdocname)
|
||||
traversed: set[str] = set()
|
||||
|
||||
relations = {}
|
||||
docnames = traverse_toctree(None, self.config.root_doc)
|
||||
prevdoc = None
|
||||
docnames = _traverse_toctree(
|
||||
traversed, None, self.config.root_doc, self.toctree_includes,
|
||||
)
|
||||
prev_doc = None
|
||||
parent, docname = next(docnames)
|
||||
for nextparent, nextdoc in docnames:
|
||||
relations[docname] = [parent, prevdoc, nextdoc]
|
||||
prevdoc = docname
|
||||
docname = nextdoc
|
||||
parent = nextparent
|
||||
for next_parent, next_doc in docnames:
|
||||
relations[docname] = [parent, prev_doc, next_doc]
|
||||
prev_doc = docname
|
||||
docname = next_doc
|
||||
parent = next_parent
|
||||
|
||||
relations[docname] = [parent, prevdoc, None]
|
||||
relations[docname] = [parent, prev_doc, None]
|
||||
|
||||
return relations
|
||||
|
||||
@@ -750,3 +732,28 @@ def _last_modified_time(filename: str | os.PathLike[str]) -> int:
|
||||
|
||||
# upside-down floor division to get the ceiling
|
||||
return -(os.stat(filename).st_mtime_ns // -1_000)
|
||||
|
||||
|
||||
def _traverse_toctree(
|
||||
traversed: set[str],
|
||||
parent: str | None,
|
||||
docname: str,
|
||||
toctree_includes: dict[str, list[str]],
|
||||
) -> Iterator[tuple[str | None, str]]:
|
||||
if parent == docname:
|
||||
logger.warning(__('self referenced toctree found. Ignored.'),
|
||||
location=docname, type='toc',
|
||||
subtype='circular')
|
||||
return
|
||||
|
||||
# traverse toctree by pre-order
|
||||
yield parent, docname
|
||||
traversed.add(docname)
|
||||
|
||||
for child in toctree_includes.get(docname, ()):
|
||||
for sub_parent, sub_docname in _traverse_toctree(
|
||||
traversed, docname, child, toctree_includes,
|
||||
):
|
||||
if sub_docname not in traversed:
|
||||
yield sub_parent, sub_docname
|
||||
traversed.add(sub_docname)
|
||||
|
@@ -2,8 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
||||
from typing import TYPE_CHECKING, Any, TypeVar
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.nodes import Element, Node
|
||||
@@ -12,330 +11,501 @@ from sphinx import addnodes
|
||||
from sphinx.locale import __
|
||||
from sphinx.util import logging, url_re
|
||||
from sphinx.util.matching import Matcher
|
||||
from sphinx.util.nodes import clean_astext, process_only_nodes
|
||||
from sphinx.util.nodes import _only_node_keep_children, clean_astext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Set
|
||||
|
||||
from sphinx.builders import Builder
|
||||
from sphinx.environment import BuildEnvironment
|
||||
from sphinx.util.tags import Tags
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def note_toctree(env: BuildEnvironment, docname: str, toctreenode: addnodes.toctree) -> None:
|
||||
"""Note a TOC tree directive in a document and gather information about
|
||||
file relations from it.
|
||||
"""
|
||||
if toctreenode['glob']:
|
||||
env.glob_toctrees.add(docname)
|
||||
if toctreenode.get('numbered'):
|
||||
env.numbered_toctrees.add(docname)
|
||||
include_files = toctreenode['includefiles']
|
||||
for include_file in include_files:
|
||||
# note that if the included file is rebuilt, this one must be
|
||||
# too (since the TOC of the included file could have changed)
|
||||
env.files_to_rebuild.setdefault(include_file, set()).add(docname)
|
||||
env.toctree_includes.setdefault(docname, []).extend(include_files)
|
||||
|
||||
|
||||
def document_toc(env: BuildEnvironment, docname: str, tags: Tags) -> Node:
|
||||
"""Get the (local) table of contents for a document.
|
||||
|
||||
Note that this is only the sections within the document.
|
||||
For a ToC tree that shows the document's place in the
|
||||
ToC structure, use `get_toctree_for`.
|
||||
"""
|
||||
|
||||
tocdepth = env.metadata[docname].get('tocdepth', 0)
|
||||
try:
|
||||
toc = _toctree_copy(env.tocs[docname], 2, tocdepth, False, tags)
|
||||
except KeyError:
|
||||
# the document does not exist any more:
|
||||
# return a dummy node that renders to nothing
|
||||
return nodes.paragraph()
|
||||
|
||||
for node in toc.findall(nodes.reference):
|
||||
node['refuri'] = node['anchorname'] or '#'
|
||||
return toc
|
||||
|
||||
|
||||
def global_toctree_for_doc(
|
||||
env: BuildEnvironment,
|
||||
docname: str,
|
||||
builder: Builder,
|
||||
maxdepth: int = 0,
|
||||
titles_only: bool = False,
|
||||
collapse: bool = False,
|
||||
includehidden: bool = True,
|
||||
) -> Element | None:
|
||||
"""Get the global ToC tree at a given document.
|
||||
|
||||
This gives the global ToC, with all ancestors and their siblings.
|
||||
"""
|
||||
|
||||
toctrees: list[Element] = []
|
||||
for toctree_node in env.master_doctree.findall(addnodes.toctree):
|
||||
if toctree := _resolve_toctree(
|
||||
env,
|
||||
docname,
|
||||
builder,
|
||||
toctree_node,
|
||||
prune=True,
|
||||
maxdepth=int(maxdepth),
|
||||
titles_only=titles_only,
|
||||
collapse=collapse,
|
||||
includehidden=includehidden,
|
||||
):
|
||||
toctrees.append(toctree)
|
||||
if not toctrees:
|
||||
return None
|
||||
result = toctrees[0]
|
||||
for toctree in toctrees[1:]:
|
||||
result.extend(toctree.children)
|
||||
return result
|
||||
|
||||
|
||||
def _resolve_toctree(
|
||||
env: BuildEnvironment, docname: str, builder: Builder, toctree: addnodes.toctree,
|
||||
prune: bool = True, maxdepth: int = 0, titles_only: bool = False,
|
||||
collapse: bool = False, includehidden: bool = False,
|
||||
) -> Element | None:
|
||||
"""Resolve a *toctree* node into individual bullet lists with titles
|
||||
as items, returning None (if no containing titles are found) or
|
||||
a new node.
|
||||
|
||||
If *prune* is True, the tree is pruned to *maxdepth*, or if that is 0,
|
||||
to the value of the *maxdepth* option on the *toctree* node.
|
||||
If *titles_only* is True, only toplevel document titles will be in the
|
||||
resulting tree.
|
||||
If *collapse* is True, all branches not containing docname will
|
||||
be collapsed.
|
||||
"""
|
||||
|
||||
if toctree.get('hidden', False) and not includehidden:
|
||||
return None
|
||||
|
||||
# For reading the following two helper function, it is useful to keep
|
||||
# in mind the node structure of a toctree (using HTML-like node names
|
||||
# for brevity):
|
||||
#
|
||||
# <ul>
|
||||
# <li>
|
||||
# <p><a></p>
|
||||
# <p><a></p>
|
||||
# ...
|
||||
# <ul>
|
||||
# ...
|
||||
# </ul>
|
||||
# </li>
|
||||
# </ul>
|
||||
#
|
||||
# The transformation is made in two passes in order to avoid
|
||||
# interactions between marking and pruning the tree (see bug #1046).
|
||||
|
||||
toctree_ancestors = _get_toctree_ancestors(env.toctree_includes, docname)
|
||||
included = Matcher(env.config.include_patterns)
|
||||
excluded = Matcher(env.config.exclude_patterns)
|
||||
|
||||
maxdepth = maxdepth or toctree.get('maxdepth', -1)
|
||||
if not titles_only and toctree.get('titlesonly', False):
|
||||
titles_only = True
|
||||
if not includehidden and toctree.get('includehidden', False):
|
||||
includehidden = True
|
||||
|
||||
tocentries = _entries_from_toctree(
|
||||
env,
|
||||
prune,
|
||||
titles_only,
|
||||
collapse,
|
||||
includehidden,
|
||||
builder.tags,
|
||||
toctree_ancestors,
|
||||
included,
|
||||
excluded,
|
||||
toctree,
|
||||
[],
|
||||
)
|
||||
if not tocentries:
|
||||
return None
|
||||
|
||||
newnode = addnodes.compact_paragraph('', '')
|
||||
if caption := toctree.attributes.get('caption'):
|
||||
caption_node = nodes.title(caption, '', *[nodes.Text(caption)])
|
||||
caption_node.line = toctree.line
|
||||
caption_node.source = toctree.source
|
||||
caption_node.rawsource = toctree['rawcaption']
|
||||
if hasattr(toctree, 'uid'):
|
||||
# move uid to caption_node to translate it
|
||||
caption_node.uid = toctree.uid # type: ignore[attr-defined]
|
||||
del toctree.uid
|
||||
newnode.append(caption_node)
|
||||
newnode.extend(tocentries)
|
||||
newnode['toctree'] = True
|
||||
|
||||
# prune the tree to maxdepth, also set toc depth and current classes
|
||||
_toctree_add_classes(newnode, 1, docname)
|
||||
newnode = _toctree_copy(newnode, 1, maxdepth if prune else 0, collapse, builder.tags)
|
||||
|
||||
if isinstance(newnode[-1], nodes.Element) and len(newnode[-1]) == 0: # No titles found
|
||||
return None
|
||||
|
||||
# set the target paths in the toctrees (they are not known at TOC
|
||||
# generation time)
|
||||
for refnode in newnode.findall(nodes.reference):
|
||||
if url_re.match(refnode['refuri']) is None:
|
||||
rel_uri = builder.get_relative_uri(docname, refnode['refuri'])
|
||||
refnode['refuri'] = rel_uri + refnode['anchorname']
|
||||
return newnode
|
||||
|
||||
|
||||
def _entries_from_toctree(
|
||||
env: BuildEnvironment,
|
||||
prune: bool,
|
||||
titles_only: bool,
|
||||
collapse: bool,
|
||||
includehidden: bool,
|
||||
tags: Tags,
|
||||
toctree_ancestors: Set[str],
|
||||
included: Matcher,
|
||||
excluded: Matcher,
|
||||
toctreenode: addnodes.toctree,
|
||||
parents: list[str],
|
||||
subtree: bool = False,
|
||||
) -> list[Element]:
|
||||
"""Return TOC entries for a toctree node."""
|
||||
entries: list[Element] = []
|
||||
for (title, ref) in toctreenode['entries']:
|
||||
try:
|
||||
toc, refdoc = _toctree_entry(
|
||||
title, ref, env, prune, collapse, tags, toctree_ancestors,
|
||||
included, excluded, toctreenode, parents,
|
||||
)
|
||||
except LookupError:
|
||||
continue
|
||||
|
||||
# children of toc are:
|
||||
# - list_item + compact_paragraph + (reference and subtoc)
|
||||
# - only + subtoc
|
||||
# - toctree
|
||||
children: Iterable[nodes.Element] = toc.children # type: ignore[assignment]
|
||||
|
||||
# if titles_only is given, only keep the main title and
|
||||
# sub-toctrees
|
||||
if titles_only:
|
||||
# delete everything but the toplevel title(s)
|
||||
# and toctrees
|
||||
for top_level in children:
|
||||
# nodes with length 1 don't have any children anyway
|
||||
if len(top_level) > 1:
|
||||
if subtrees := list(top_level.findall(addnodes.toctree)):
|
||||
top_level[1][:] = subtrees # type: ignore
|
||||
else:
|
||||
top_level.pop(1)
|
||||
# resolve all sub-toctrees
|
||||
for sub_toc_node in list(toc.findall(addnodes.toctree)):
|
||||
if sub_toc_node.get('hidden', False) and not includehidden:
|
||||
continue
|
||||
for i, entry in enumerate(
|
||||
_entries_from_toctree(
|
||||
env,
|
||||
prune,
|
||||
titles_only,
|
||||
collapse,
|
||||
includehidden,
|
||||
tags,
|
||||
toctree_ancestors,
|
||||
included,
|
||||
excluded,
|
||||
sub_toc_node,
|
||||
[refdoc] + parents,
|
||||
subtree=True,
|
||||
),
|
||||
start=sub_toc_node.parent.index(sub_toc_node) + 1,
|
||||
):
|
||||
sub_toc_node.parent.insert(i, entry)
|
||||
sub_toc_node.parent.remove(sub_toc_node)
|
||||
|
||||
entries.extend(children)
|
||||
|
||||
if not subtree:
|
||||
ret = nodes.bullet_list()
|
||||
ret += entries
|
||||
return [ret]
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def _toctree_entry(
|
||||
title: str,
|
||||
ref: str,
|
||||
env: BuildEnvironment,
|
||||
prune: bool,
|
||||
collapse: bool,
|
||||
tags: Tags,
|
||||
toctree_ancestors: Set[str],
|
||||
included: Matcher,
|
||||
excluded: Matcher,
|
||||
toctreenode: addnodes.toctree,
|
||||
parents: list[str],
|
||||
) -> tuple[Element, str]:
|
||||
from sphinx.domains.std import StandardDomain
|
||||
|
||||
try:
|
||||
refdoc = ''
|
||||
if url_re.match(ref):
|
||||
toc = _toctree_url_entry(title, ref)
|
||||
elif ref == 'self':
|
||||
toc = _toctree_self_entry(title, toctreenode['parent'], env.titles)
|
||||
elif ref in StandardDomain._virtual_doc_names:
|
||||
toc = _toctree_generated_entry(title, ref)
|
||||
else:
|
||||
if ref in parents:
|
||||
logger.warning(__('circular toctree references '
|
||||
'detected, ignoring: %s <- %s'),
|
||||
ref, ' <- '.join(parents),
|
||||
location=ref, type='toc', subtype='circular')
|
||||
raise LookupError('circular reference')
|
||||
|
||||
toc, refdoc = _toctree_standard_entry(
|
||||
title,
|
||||
ref,
|
||||
env.metadata[ref].get('tocdepth', 0),
|
||||
env.tocs[ref],
|
||||
toctree_ancestors,
|
||||
prune,
|
||||
collapse,
|
||||
tags,
|
||||
)
|
||||
|
||||
if not toc.children:
|
||||
# empty toc means: no titles will show up in the toctree
|
||||
logger.warning(__('toctree contains reference to document %r that '
|
||||
"doesn't have a title: no link will be generated"),
|
||||
ref, location=toctreenode)
|
||||
except KeyError:
|
||||
# this is raised if the included file does not exist
|
||||
ref_path = env.doc2path(ref, False)
|
||||
if excluded(ref_path):
|
||||
message = __('toctree contains reference to excluded document %r')
|
||||
elif not included(ref_path):
|
||||
message = __('toctree contains reference to non-included document %r')
|
||||
else:
|
||||
message = __('toctree contains reference to nonexisting document %r')
|
||||
|
||||
logger.warning(message, ref, location=toctreenode)
|
||||
raise
|
||||
return toc, refdoc
|
||||
|
||||
|
||||
def _toctree_url_entry(title: str, ref: str) -> nodes.bullet_list:
|
||||
if title is None:
|
||||
title = ref
|
||||
reference = nodes.reference('', '', internal=False,
|
||||
refuri=ref, anchorname='',
|
||||
*[nodes.Text(title)])
|
||||
para = addnodes.compact_paragraph('', '', reference)
|
||||
item = nodes.list_item('', para)
|
||||
toc = nodes.bullet_list('', item)
|
||||
return toc
|
||||
|
||||
|
||||
def _toctree_self_entry(
|
||||
title: str, ref: str, titles: dict[str, nodes.title],
|
||||
) -> nodes.bullet_list:
|
||||
# 'self' refers to the document from which this
|
||||
# toctree originates
|
||||
if not title:
|
||||
title = clean_astext(titles[ref])
|
||||
reference = nodes.reference('', '', internal=True,
|
||||
refuri=ref,
|
||||
anchorname='',
|
||||
*[nodes.Text(title)])
|
||||
para = addnodes.compact_paragraph('', '', reference)
|
||||
item = nodes.list_item('', para)
|
||||
# don't show subitems
|
||||
toc = nodes.bullet_list('', item)
|
||||
return toc
|
||||
|
||||
|
||||
def _toctree_generated_entry(title: str, ref: str, ) -> nodes.bullet_list:
|
||||
from sphinx.domains.std import StandardDomain
|
||||
|
||||
docname, sectionname = StandardDomain._virtual_doc_names[ref]
|
||||
if not title:
|
||||
title = sectionname
|
||||
reference = nodes.reference('', title, internal=True,
|
||||
refuri=docname, anchorname='')
|
||||
para = addnodes.compact_paragraph('', '', reference)
|
||||
item = nodes.list_item('', para)
|
||||
# don't show subitems
|
||||
toc = nodes.bullet_list('', item)
|
||||
return toc
|
||||
|
||||
|
||||
def _toctree_standard_entry(
|
||||
title: str,
|
||||
ref: str,
|
||||
maxdepth: int,
|
||||
toc: nodes.bullet_list,
|
||||
toctree_ancestors: Set[str],
|
||||
prune: bool,
|
||||
collapse: bool,
|
||||
tags: Tags,
|
||||
) -> tuple[nodes.bullet_list, str]:
|
||||
refdoc = ref
|
||||
if ref in toctree_ancestors and (not prune or maxdepth <= 0):
|
||||
toc = toc.deepcopy()
|
||||
else:
|
||||
toc = _toctree_copy(toc, 2, maxdepth, collapse, tags)
|
||||
|
||||
if title and toc.children and len(toc.children) == 1:
|
||||
child = toc.children[0]
|
||||
for refnode in child.findall(nodes.reference):
|
||||
if refnode['refuri'] == ref and not refnode['anchorname']:
|
||||
refnode.children[:] = [nodes.Text(title)]
|
||||
return toc, refdoc
|
||||
|
||||
|
||||
def _toctree_add_classes(node: Element, depth: int, docname: str) -> None:
|
||||
"""Add 'toctree-l%d' and 'current' classes to the toctree."""
|
||||
for subnode in node.children:
|
||||
if isinstance(subnode, (addnodes.compact_paragraph, nodes.list_item)):
|
||||
# for <p> and <li>, indicate the depth level and recurse
|
||||
subnode['classes'].append(f'toctree-l{depth - 1}')
|
||||
_toctree_add_classes(subnode, depth, docname)
|
||||
elif isinstance(subnode, nodes.bullet_list):
|
||||
# for <ul>, just recurse
|
||||
_toctree_add_classes(subnode, depth + 1, docname)
|
||||
elif isinstance(subnode, nodes.reference):
|
||||
# for <a>, identify which entries point to the current
|
||||
# document and therefore may not be collapsed
|
||||
if subnode['refuri'] == docname:
|
||||
if not subnode['anchorname']:
|
||||
# give the whole branch a 'current' class
|
||||
# (useful for styling it differently)
|
||||
branchnode: Element = subnode
|
||||
while branchnode:
|
||||
branchnode['classes'].append('current')
|
||||
branchnode = branchnode.parent
|
||||
# mark the list_item as "on current page"
|
||||
if subnode.parent.parent.get('iscurrent'):
|
||||
# but only if it's not already done
|
||||
return
|
||||
while subnode:
|
||||
subnode['iscurrent'] = True
|
||||
subnode = subnode.parent
|
||||
|
||||
|
||||
ET = TypeVar('ET', bound=Element)
|
||||
|
||||
|
||||
def _toctree_copy(node: ET, depth: int, maxdepth: int, collapse: bool, tags: Tags) -> ET:
|
||||
"""Utility: Cut and deep-copy a TOC at a specified depth."""
|
||||
keep_bullet_list_sub_nodes = (depth <= 1
|
||||
or ((depth <= maxdepth or maxdepth <= 0)
|
||||
and (not collapse or 'iscurrent' in node)))
|
||||
|
||||
copy = node.copy()
|
||||
for subnode in node.children:
|
||||
if isinstance(subnode, (addnodes.compact_paragraph, nodes.list_item)):
|
||||
# for <p> and <li>, just recurse
|
||||
copy.append(_toctree_copy(subnode, depth, maxdepth, collapse, tags))
|
||||
elif isinstance(subnode, nodes.bullet_list):
|
||||
# for <ul>, copy if the entry is top-level
|
||||
# or, copy if the depth is within bounds and;
|
||||
# collapsing is disabled or the sub-entry's parent is 'current'.
|
||||
# The boolean is constant so is calculated outwith the loop.
|
||||
if keep_bullet_list_sub_nodes:
|
||||
copy.append(_toctree_copy(subnode, depth + 1, maxdepth, collapse, tags))
|
||||
elif isinstance(subnode, addnodes.toctree):
|
||||
# copy sub toctree nodes for later processing
|
||||
copy.append(subnode.copy())
|
||||
elif isinstance(subnode, addnodes.only):
|
||||
# only keep children if the only node matches the tags
|
||||
if _only_node_keep_children(subnode, tags):
|
||||
for child in subnode.children:
|
||||
copy.append(_toctree_copy(
|
||||
child, depth, maxdepth, collapse, tags, # type: ignore[type-var]
|
||||
))
|
||||
elif isinstance(subnode, (nodes.reference, nodes.title)):
|
||||
# deep copy references and captions
|
||||
sub_node_copy = subnode.copy()
|
||||
sub_node_copy.children = [child.deepcopy() for child in subnode.children]
|
||||
for child in sub_node_copy.children:
|
||||
child.parent = sub_node_copy
|
||||
copy.append(sub_node_copy)
|
||||
else:
|
||||
raise ValueError(f"Unexpected node type {subnode.__class__.__name__!r}!")
|
||||
return copy
|
||||
|
||||
|
||||
def _get_toctree_ancestors(
|
||||
toctree_includes: dict[str, list[str]], docname: str,
|
||||
) -> Set[str]:
|
||||
parent: dict[str, str] = {}
|
||||
for p, children in toctree_includes.items():
|
||||
parent |= dict.fromkeys(children, p)
|
||||
ancestors: list[str] = []
|
||||
d = docname
|
||||
while d in parent and d not in ancestors:
|
||||
ancestors.append(d)
|
||||
d = parent[d]
|
||||
# use dict keys for ordered set operations
|
||||
return dict.fromkeys(ancestors).keys()
|
||||
|
||||
|
||||
class TocTree:
|
||||
def __init__(self, env: BuildEnvironment) -> None:
|
||||
self.env = env
|
||||
|
||||
def note(self, docname: str, toctreenode: addnodes.toctree) -> None:
|
||||
"""Note a TOC tree directive in a document and gather information about
|
||||
file relations from it.
|
||||
"""
|
||||
if toctreenode['glob']:
|
||||
self.env.glob_toctrees.add(docname)
|
||||
if toctreenode.get('numbered'):
|
||||
self.env.numbered_toctrees.add(docname)
|
||||
includefiles = toctreenode['includefiles']
|
||||
for includefile in includefiles:
|
||||
# note that if the included file is rebuilt, this one must be
|
||||
# too (since the TOC of the included file could have changed)
|
||||
self.env.files_to_rebuild.setdefault(includefile, set()).add(docname)
|
||||
self.env.toctree_includes.setdefault(docname, []).extend(includefiles)
|
||||
note_toctree(self.env, docname, toctreenode)
|
||||
|
||||
def resolve(self, docname: str, builder: Builder, toctree: addnodes.toctree,
|
||||
prune: bool = True, maxdepth: int = 0, titles_only: bool = False,
|
||||
collapse: bool = False, includehidden: bool = False) -> Element | None:
|
||||
"""Resolve a *toctree* node into individual bullet lists with titles
|
||||
as items, returning None (if no containing titles are found) or
|
||||
a new node.
|
||||
|
||||
If *prune* is True, the tree is pruned to *maxdepth*, or if that is 0,
|
||||
to the value of the *maxdepth* option on the *toctree* node.
|
||||
If *titles_only* is True, only toplevel document titles will be in the
|
||||
resulting tree.
|
||||
If *collapse* is True, all branches not containing docname will
|
||||
be collapsed.
|
||||
"""
|
||||
if toctree.get('hidden', False) and not includehidden:
|
||||
return None
|
||||
generated_docnames: dict[str, tuple[str, str]] = self.env.domains['std']._virtual_doc_names.copy() # NoQA: E501
|
||||
|
||||
# For reading the following two helper function, it is useful to keep
|
||||
# in mind the node structure of a toctree (using HTML-like node names
|
||||
# for brevity):
|
||||
#
|
||||
# <ul>
|
||||
# <li>
|
||||
# <p><a></p>
|
||||
# <p><a></p>
|
||||
# ...
|
||||
# <ul>
|
||||
# ...
|
||||
# </ul>
|
||||
# </li>
|
||||
# </ul>
|
||||
#
|
||||
# The transformation is made in two passes in order to avoid
|
||||
# interactions between marking and pruning the tree (see bug #1046).
|
||||
|
||||
toctree_ancestors = self.get_toctree_ancestors(docname)
|
||||
included = Matcher(self.env.config.include_patterns)
|
||||
excluded = Matcher(self.env.config.exclude_patterns)
|
||||
|
||||
def _toctree_add_classes(node: Element, depth: int) -> None:
|
||||
"""Add 'toctree-l%d' and 'current' classes to the toctree."""
|
||||
for subnode in node.children:
|
||||
if isinstance(subnode, (addnodes.compact_paragraph,
|
||||
nodes.list_item)):
|
||||
# for <p> and <li>, indicate the depth level and recurse
|
||||
subnode['classes'].append(f'toctree-l{depth - 1}')
|
||||
_toctree_add_classes(subnode, depth)
|
||||
elif isinstance(subnode, nodes.bullet_list):
|
||||
# for <ul>, just recurse
|
||||
_toctree_add_classes(subnode, depth + 1)
|
||||
elif isinstance(subnode, nodes.reference):
|
||||
# for <a>, identify which entries point to the current
|
||||
# document and therefore may not be collapsed
|
||||
if subnode['refuri'] == docname:
|
||||
if not subnode['anchorname']:
|
||||
# give the whole branch a 'current' class
|
||||
# (useful for styling it differently)
|
||||
branchnode: Element = subnode
|
||||
while branchnode:
|
||||
branchnode['classes'].append('current')
|
||||
branchnode = branchnode.parent
|
||||
# mark the list_item as "on current page"
|
||||
if subnode.parent.parent.get('iscurrent'):
|
||||
# but only if it's not already done
|
||||
return
|
||||
while subnode:
|
||||
subnode['iscurrent'] = True
|
||||
subnode = subnode.parent
|
||||
|
||||
def _entries_from_toctree(toctreenode: addnodes.toctree, parents: list[str],
|
||||
subtree: bool = False) -> list[Element]:
|
||||
"""Return TOC entries for a toctree node."""
|
||||
refs = [(e[0], e[1]) for e in toctreenode['entries']]
|
||||
entries: list[Element] = []
|
||||
for (title, ref) in refs:
|
||||
try:
|
||||
refdoc = None
|
||||
if url_re.match(ref):
|
||||
if title is None:
|
||||
title = ref
|
||||
reference = nodes.reference('', '', internal=False,
|
||||
refuri=ref, anchorname='',
|
||||
*[nodes.Text(title)])
|
||||
para = addnodes.compact_paragraph('', '', reference)
|
||||
item = nodes.list_item('', para)
|
||||
toc = nodes.bullet_list('', item)
|
||||
elif ref == 'self':
|
||||
# 'self' refers to the document from which this
|
||||
# toctree originates
|
||||
ref = toctreenode['parent']
|
||||
if not title:
|
||||
title = clean_astext(self.env.titles[ref])
|
||||
reference = nodes.reference('', '', internal=True,
|
||||
refuri=ref,
|
||||
anchorname='',
|
||||
*[nodes.Text(title)])
|
||||
para = addnodes.compact_paragraph('', '', reference)
|
||||
item = nodes.list_item('', para)
|
||||
# don't show subitems
|
||||
toc = nodes.bullet_list('', item)
|
||||
elif ref in generated_docnames:
|
||||
docname, sectionname = generated_docnames[ref]
|
||||
if not title:
|
||||
title = sectionname
|
||||
reference = nodes.reference('', title, internal=True,
|
||||
refuri=docname, anchorname='')
|
||||
para = addnodes.compact_paragraph('', '', reference)
|
||||
item = nodes.list_item('', para)
|
||||
# don't show subitems
|
||||
toc = nodes.bullet_list('', item)
|
||||
else:
|
||||
if ref in parents:
|
||||
logger.warning(__('circular toctree references '
|
||||
'detected, ignoring: %s <- %s'),
|
||||
ref, ' <- '.join(parents),
|
||||
location=ref, type='toc', subtype='circular')
|
||||
continue
|
||||
refdoc = ref
|
||||
maxdepth = self.env.metadata[ref].get('tocdepth', 0)
|
||||
toc = self.env.tocs[ref]
|
||||
if ref not in toctree_ancestors or (prune and maxdepth > 0):
|
||||
toc = self._toctree_copy(toc, 2, maxdepth, collapse)
|
||||
else:
|
||||
toc = toc.deepcopy()
|
||||
process_only_nodes(toc, builder.tags)
|
||||
if title and toc.children and len(toc.children) == 1:
|
||||
child = toc.children[0]
|
||||
for refnode in child.findall(nodes.reference):
|
||||
if refnode['refuri'] == ref and \
|
||||
not refnode['anchorname']:
|
||||
refnode.children = [nodes.Text(title)]
|
||||
if not toc.children:
|
||||
# empty toc means: no titles will show up in the toctree
|
||||
logger.warning(__('toctree contains reference to document %r that '
|
||||
"doesn't have a title: no link will be generated"),
|
||||
ref, location=toctreenode)
|
||||
except KeyError:
|
||||
# this is raised if the included file does not exist
|
||||
if excluded(self.env.doc2path(ref, False)):
|
||||
message = __('toctree contains reference to excluded document %r')
|
||||
elif not included(self.env.doc2path(ref, False)):
|
||||
message = __('toctree contains reference to non-included document %r')
|
||||
else:
|
||||
message = __('toctree contains reference to nonexisting document %r')
|
||||
|
||||
logger.warning(message, ref, location=toctreenode)
|
||||
else:
|
||||
# children of toc are:
|
||||
# - list_item + compact_paragraph + (reference and subtoc)
|
||||
# - only + subtoc
|
||||
# - toctree
|
||||
children = cast(Iterable[nodes.Element], toc)
|
||||
|
||||
# if titles_only is given, only keep the main title and
|
||||
# sub-toctrees
|
||||
if titles_only:
|
||||
# delete everything but the toplevel title(s)
|
||||
# and toctrees
|
||||
for toplevel in children:
|
||||
# nodes with length 1 don't have any children anyway
|
||||
if len(toplevel) > 1:
|
||||
subtrees = list(toplevel.findall(addnodes.toctree))
|
||||
if subtrees:
|
||||
toplevel[1][:] = subtrees # type: ignore
|
||||
else:
|
||||
toplevel.pop(1)
|
||||
# resolve all sub-toctrees
|
||||
for sub_toc_node in list(toc.findall(addnodes.toctree)):
|
||||
if sub_toc_node.get('hidden', False) and not includehidden:
|
||||
continue
|
||||
for i, entry in enumerate(
|
||||
_entries_from_toctree(sub_toc_node, [refdoc or ''] + parents,
|
||||
subtree=True),
|
||||
start=sub_toc_node.parent.index(sub_toc_node) + 1,
|
||||
):
|
||||
sub_toc_node.parent.insert(i, entry)
|
||||
sub_toc_node.parent.remove(sub_toc_node)
|
||||
|
||||
entries.extend(children)
|
||||
if not subtree:
|
||||
ret = nodes.bullet_list()
|
||||
ret += entries
|
||||
return [ret]
|
||||
return entries
|
||||
|
||||
maxdepth = maxdepth or toctree.get('maxdepth', -1)
|
||||
if not titles_only and toctree.get('titlesonly', False):
|
||||
titles_only = True
|
||||
if not includehidden and toctree.get('includehidden', False):
|
||||
includehidden = True
|
||||
|
||||
tocentries = _entries_from_toctree(toctree, [])
|
||||
if not tocentries:
|
||||
return None
|
||||
|
||||
newnode = addnodes.compact_paragraph('', '')
|
||||
caption = toctree.attributes.get('caption')
|
||||
if caption:
|
||||
caption_node = nodes.title(caption, '', *[nodes.Text(caption)])
|
||||
caption_node.line = toctree.line
|
||||
caption_node.source = toctree.source
|
||||
caption_node.rawsource = toctree['rawcaption']
|
||||
if hasattr(toctree, 'uid'):
|
||||
# move uid to caption_node to translate it
|
||||
caption_node.uid = toctree.uid # type: ignore
|
||||
del toctree.uid
|
||||
newnode += caption_node
|
||||
newnode.extend(tocentries)
|
||||
newnode['toctree'] = True
|
||||
|
||||
# prune the tree to maxdepth, also set toc depth and current classes
|
||||
_toctree_add_classes(newnode, 1)
|
||||
newnode = self._toctree_copy(newnode, 1, maxdepth if prune else 0, collapse)
|
||||
|
||||
if isinstance(newnode[-1], nodes.Element) and len(newnode[-1]) == 0: # No titles found
|
||||
return None
|
||||
|
||||
# set the target paths in the toctrees (they are not known at TOC
|
||||
# generation time)
|
||||
for refnode in newnode.findall(nodes.reference):
|
||||
if not url_re.match(refnode['refuri']):
|
||||
refnode['refuri'] = builder.get_relative_uri(
|
||||
docname, refnode['refuri']) + refnode['anchorname']
|
||||
return newnode
|
||||
|
||||
def get_toctree_ancestors(self, docname: str) -> list[str]:
|
||||
parent = {}
|
||||
for p, children in self.env.toctree_includes.items():
|
||||
for child in children:
|
||||
parent[child] = p
|
||||
ancestors: list[str] = []
|
||||
d = docname
|
||||
while d in parent and d not in ancestors:
|
||||
ancestors.append(d)
|
||||
d = parent[d]
|
||||
return ancestors
|
||||
|
||||
ET = TypeVar('ET', bound=Element)
|
||||
|
||||
def _toctree_copy(self, node: ET, depth: int, maxdepth: int, collapse: bool) -> ET:
|
||||
"""Utility: Cut and deep-copy a TOC at a specified depth."""
|
||||
keep_bullet_list_sub_nodes = (depth <= 1
|
||||
or ((depth <= maxdepth or maxdepth <= 0)
|
||||
and (not collapse or 'iscurrent' in node)))
|
||||
|
||||
copy = node.copy()
|
||||
for subnode in node.children:
|
||||
if isinstance(subnode, (addnodes.compact_paragraph, nodes.list_item)):
|
||||
# for <p> and <li>, just recurse
|
||||
copy.append(self._toctree_copy(subnode, depth, maxdepth, collapse))
|
||||
elif isinstance(subnode, nodes.bullet_list):
|
||||
# for <ul>, copy if the entry is top-level
|
||||
# or, copy if the depth is within bounds and;
|
||||
# collapsing is disabled or the sub-entry's parent is 'current'.
|
||||
# The boolean is constant so is calculated outwith the loop.
|
||||
if keep_bullet_list_sub_nodes:
|
||||
copy.append(self._toctree_copy(subnode, depth + 1, maxdepth, collapse))
|
||||
else:
|
||||
copy.append(subnode.deepcopy())
|
||||
return copy
|
||||
return _resolve_toctree(
|
||||
self.env, docname, builder, toctree, prune,
|
||||
maxdepth, titles_only, collapse, includehidden,
|
||||
)
|
||||
|
||||
def get_toc_for(self, docname: str, builder: Builder) -> Node:
|
||||
"""Return a TOC nodetree -- for use on the same page only!"""
|
||||
tocdepth = self.env.metadata[docname].get('tocdepth', 0)
|
||||
try:
|
||||
toc = self._toctree_copy(self.env.tocs[docname], 2, tocdepth, False)
|
||||
except KeyError:
|
||||
# the document does not exist anymore: return a dummy node that
|
||||
# renders to nothing
|
||||
return nodes.paragraph()
|
||||
process_only_nodes(toc, builder.tags)
|
||||
for node in toc.findall(nodes.reference):
|
||||
node['refuri'] = node['anchorname'] or '#'
|
||||
return toc
|
||||
return document_toc(self.env, docname, self.env.app.builder.tags)
|
||||
|
||||
def get_toctree_for(self, docname: str, builder: Builder, collapse: bool,
|
||||
**kwargs: Any) -> Element | None:
|
||||
"""Return the global TOC nodetree."""
|
||||
doctree = self.env.master_doctree
|
||||
toctrees: list[Element] = []
|
||||
if 'includehidden' not in kwargs:
|
||||
kwargs['includehidden'] = True
|
||||
if 'maxdepth' not in kwargs or not kwargs['maxdepth']:
|
||||
kwargs['maxdepth'] = 0
|
||||
else:
|
||||
kwargs['maxdepth'] = int(kwargs['maxdepth'])
|
||||
kwargs['collapse'] = collapse
|
||||
for toctreenode in doctree.findall(addnodes.toctree):
|
||||
toctree = self.resolve(docname, builder, toctreenode, prune=True, **kwargs)
|
||||
if toctree:
|
||||
toctrees.append(toctree)
|
||||
if not toctrees:
|
||||
return None
|
||||
result = toctrees[0]
|
||||
for toctree in toctrees[1:]:
|
||||
result.extend(toctree.children)
|
||||
return result
|
||||
def get_toctree_for(
|
||||
self, docname: str, builder: Builder, collapse: bool, **kwargs: Any,
|
||||
) -> Element | None:
|
||||
return global_toctree_for_doc(self.env, docname, builder, collapse=collapse, **kwargs)
|
||||
|
@@ -10,7 +10,7 @@ from docutils.nodes import Element, Node
|
||||
from sphinx import addnodes
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.environment import BuildEnvironment
|
||||
from sphinx.environment.adapters.toctree import TocTree
|
||||
from sphinx.environment.adapters.toctree import note_toctree
|
||||
from sphinx.environment.collectors import EnvironmentCollector
|
||||
from sphinx.locale import __
|
||||
from sphinx.transforms import SphinxContentsFilter
|
||||
@@ -108,7 +108,7 @@ class TocTreeCollector(EnvironmentCollector):
|
||||
item = toctreenode.copy()
|
||||
entries.append(item)
|
||||
# important: do the inventory stuff
|
||||
TocTree(app.env).note(docname, toctreenode)
|
||||
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:
|
||||
|
@@ -51,8 +51,7 @@ url_re: re.Pattern[str] = re.compile(r'(?P<schema>.+)://.*')
|
||||
# High-level utility functions.
|
||||
|
||||
def docname_join(basedocname: str, docname: str) -> str:
|
||||
return posixpath.normpath(
|
||||
posixpath.join('/' + basedocname, '..', docname))[1:]
|
||||
return posixpath.normpath(posixpath.join('/' + basedocname, '..', docname))[1:]
|
||||
|
||||
|
||||
def get_filetype(source_suffix: dict[str, str], filename: str) -> str:
|
||||
|
@@ -603,33 +603,65 @@ def is_smartquotable(node: Node) -> bool:
|
||||
def process_only_nodes(document: Node, tags: Tags) -> None:
|
||||
"""Filter ``only`` nodes which do not match *tags*."""
|
||||
for node in document.findall(addnodes.only):
|
||||
try:
|
||||
ret = tags.eval_condition(node['expr'])
|
||||
except Exception as err:
|
||||
logger.warning(__('exception while evaluating only directive expression: %s'), err,
|
||||
location=node)
|
||||
if _only_node_keep_children(node, tags):
|
||||
node.replace_self(node.children or nodes.comment())
|
||||
else:
|
||||
if ret:
|
||||
node.replace_self(node.children or nodes.comment())
|
||||
else:
|
||||
# A comment on the comment() nodes being inserted: replacing by [] would
|
||||
# result in a "Losing ids" exception if there is a target node before
|
||||
# the only node, so we make sure docutils can transfer the id to
|
||||
# something, even if it's just a comment and will lose the id anyway...
|
||||
node.replace_self(nodes.comment())
|
||||
# A comment on the comment() nodes being inserted: replacing by [] would
|
||||
# result in a "Losing ids" exception if there is a target node before
|
||||
# the only node, so we make sure docutils can transfer the id to
|
||||
# something, even if it's just a comment and will lose the id anyway...
|
||||
node.replace_self(nodes.comment())
|
||||
|
||||
|
||||
def _copy_except__document(self: Element) -> Element:
|
||||
def _only_node_keep_children(node: addnodes.only, tags: Tags) -> bool:
|
||||
"""Keep children if tags match or error."""
|
||||
try:
|
||||
return tags.eval_condition(node['expr'])
|
||||
except Exception as err:
|
||||
logger.warning(
|
||||
__('exception while evaluating only directive expression: %s'),
|
||||
err,
|
||||
location=node)
|
||||
return True
|
||||
|
||||
|
||||
def _copy_except__document(el: Element) -> Element:
|
||||
"""Monkey-patch ```nodes.Element.copy``` to not copy the ``_document``
|
||||
attribute.
|
||||
|
||||
xref: https://github.com/sphinx-doc/sphinx/issues/11116#issuecomment-1376767086
|
||||
"""
|
||||
newnode = self.__class__(rawsource=self.rawsource, **self.attributes)
|
||||
newnode.source = self.source
|
||||
newnode.line = self.line
|
||||
newnode = object.__new__(el.__class__)
|
||||
# set in Element.__init__()
|
||||
newnode.children = []
|
||||
newnode.rawsource = el.rawsource
|
||||
newnode.tagname = el.tagname
|
||||
# copied in Element.copy()
|
||||
newnode.attributes = {k: (v
|
||||
if k not in {'ids', 'classes', 'names', 'dupnames', 'backrefs'}
|
||||
else v[:])
|
||||
for k, v in el.attributes.items()}
|
||||
newnode.line = el.line
|
||||
newnode.source = el.source
|
||||
return newnode
|
||||
|
||||
|
||||
nodes.Element.copy = _copy_except__document # type: ignore
|
||||
|
||||
|
||||
def _deepcopy(el: Element) -> Element:
|
||||
"""Monkey-patch ```nodes.Element.deepcopy``` for speed."""
|
||||
newnode = el.copy()
|
||||
newnode.children = [child.deepcopy() for child in el.children]
|
||||
for child in newnode.children:
|
||||
child.parent = newnode
|
||||
if el.document:
|
||||
child.document = el.document
|
||||
if child.source is None:
|
||||
child.source = el.document.current_source
|
||||
if child.line is None:
|
||||
child.line = el.document.current_line
|
||||
return newnode
|
||||
|
||||
|
||||
nodes.Element.deepcopy = _deepcopy # type: ignore
|
||||
|
@@ -2,12 +2,12 @@
|
||||
|
||||
import pytest
|
||||
from docutils import nodes
|
||||
from docutils.nodes import bullet_list, comment, list_item, literal, reference, title
|
||||
from docutils.nodes import bullet_list, list_item, literal, reference, title
|
||||
|
||||
from sphinx import addnodes
|
||||
from sphinx.addnodes import compact_paragraph, only
|
||||
from sphinx.builders.html import StandaloneHTMLBuilder
|
||||
from sphinx.environment.adapters.toctree import TocTree
|
||||
from sphinx.environment.adapters.toctree import document_toc, global_toctree_for_doc
|
||||
from sphinx.testing.util import assert_node
|
||||
|
||||
|
||||
@@ -128,15 +128,12 @@ def test_glob(app):
|
||||
|
||||
@pytest.mark.sphinx('dummy', testroot='toctree-domain-objects')
|
||||
def test_domain_objects(app):
|
||||
includefiles = ['domains']
|
||||
|
||||
app.build()
|
||||
|
||||
assert app.env.toc_num_entries['index'] == 0
|
||||
assert app.env.toc_num_entries['domains'] == 9
|
||||
assert app.env.toctree_includes['index'] == includefiles
|
||||
for file in includefiles:
|
||||
assert 'index' in app.env.files_to_rebuild[file]
|
||||
assert app.env.toctree_includes['index'] == ['domains']
|
||||
assert 'index' in app.env.files_to_rebuild['domains']
|
||||
assert app.env.glob_toctrees == set()
|
||||
assert app.env.numbered_toctrees == {'index'}
|
||||
|
||||
@@ -166,22 +163,21 @@ def test_domain_objects(app):
|
||||
|
||||
@pytest.mark.sphinx('xml', testroot='toctree')
|
||||
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
|
||||
def test_get_toc_for(app):
|
||||
def test_document_toc(app):
|
||||
app.build()
|
||||
toctree = TocTree(app.env).get_toc_for('index', app.builder)
|
||||
toctree = document_toc(app.env, 'index', app.builder.tags)
|
||||
|
||||
assert_node(toctree,
|
||||
[bullet_list, ([list_item, (compact_paragraph, # [0][0]
|
||||
[bullet_list, (addnodes.toctree, # [0][1][0]
|
||||
comment, # [0][1][1]
|
||||
list_item)])], # [0][1][2]
|
||||
list_item)])], # [0][1][1]
|
||||
[list_item, (compact_paragraph, # [1][0]
|
||||
[bullet_list, (addnodes.toctree,
|
||||
addnodes.toctree)])],
|
||||
[list_item, compact_paragraph])]) # [2][0]
|
||||
assert_node(toctree[0][0],
|
||||
[compact_paragraph, reference, "Welcome to Sphinx Tests’s documentation!"])
|
||||
assert_node(toctree[0][1][2],
|
||||
assert_node(toctree[0][1][1],
|
||||
([compact_paragraph, reference, "subsection"],
|
||||
[bullet_list, list_item, compact_paragraph, reference, "subsubsection"]))
|
||||
assert_node(toctree[1][0],
|
||||
@@ -192,10 +188,10 @@ def test_get_toc_for(app):
|
||||
|
||||
@pytest.mark.sphinx('xml', testroot='toctree')
|
||||
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
|
||||
def test_get_toc_for_only(app):
|
||||
def test_document_toc_only(app):
|
||||
app.build()
|
||||
builder = StandaloneHTMLBuilder(app, app.env)
|
||||
toctree = TocTree(app.env).get_toc_for('index', builder)
|
||||
toctree = document_toc(app.env, 'index', builder.tags)
|
||||
|
||||
assert_node(toctree,
|
||||
[bullet_list, ([list_item, (compact_paragraph, # [0][0]
|
||||
@@ -222,9 +218,9 @@ def test_get_toc_for_only(app):
|
||||
|
||||
@pytest.mark.sphinx('xml', testroot='toctree')
|
||||
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
|
||||
def test_get_toc_for_tocdepth(app):
|
||||
def test_document_toc_tocdepth(app):
|
||||
app.build()
|
||||
toctree = TocTree(app.env).get_toc_for('tocdepth', app.builder)
|
||||
toctree = document_toc(app.env, 'tocdepth', app.builder.tags)
|
||||
|
||||
assert_node(toctree,
|
||||
[bullet_list, list_item, (compact_paragraph, # [0][0]
|
||||
@@ -237,9 +233,9 @@ def test_get_toc_for_tocdepth(app):
|
||||
|
||||
@pytest.mark.sphinx('xml', testroot='toctree')
|
||||
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
|
||||
def test_get_toctree_for(app):
|
||||
def test_global_toctree_for_doc(app):
|
||||
app.build()
|
||||
toctree = TocTree(app.env).get_toctree_for('index', app.builder, collapse=False)
|
||||
toctree = global_toctree_for_doc(app.env, 'index', app.builder, collapse=False)
|
||||
assert_node(toctree,
|
||||
[compact_paragraph, ([title, "Table of Contents"],
|
||||
bullet_list,
|
||||
@@ -277,9 +273,9 @@ def test_get_toctree_for(app):
|
||||
|
||||
@pytest.mark.sphinx('xml', testroot='toctree')
|
||||
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
|
||||
def test_get_toctree_for_collapse(app):
|
||||
def test_global_toctree_for_doc_collapse(app):
|
||||
app.build()
|
||||
toctree = TocTree(app.env).get_toctree_for('index', app.builder, collapse=True)
|
||||
toctree = global_toctree_for_doc(app.env, 'index', app.builder, collapse=True)
|
||||
assert_node(toctree,
|
||||
[compact_paragraph, ([title, "Table of Contents"],
|
||||
bullet_list,
|
||||
@@ -308,10 +304,10 @@ def test_get_toctree_for_collapse(app):
|
||||
|
||||
@pytest.mark.sphinx('xml', testroot='toctree')
|
||||
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
|
||||
def test_get_toctree_for_maxdepth(app):
|
||||
def test_global_toctree_for_doc_maxdepth(app):
|
||||
app.build()
|
||||
toctree = TocTree(app.env).get_toctree_for('index', app.builder,
|
||||
collapse=False, maxdepth=3)
|
||||
toctree = global_toctree_for_doc(app.env, 'index', app.builder,
|
||||
collapse=False, maxdepth=3)
|
||||
assert_node(toctree,
|
||||
[compact_paragraph, ([title, "Table of Contents"],
|
||||
bullet_list,
|
||||
@@ -354,10 +350,10 @@ def test_get_toctree_for_maxdepth(app):
|
||||
|
||||
@pytest.mark.sphinx('xml', testroot='toctree')
|
||||
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
|
||||
def test_get_toctree_for_includehidden(app):
|
||||
def test_global_toctree_for_doc_includehidden(app):
|
||||
app.build()
|
||||
toctree = TocTree(app.env).get_toctree_for('index', app.builder, collapse=False,
|
||||
includehidden=False)
|
||||
toctree = global_toctree_for_doc(app.env, 'index', app.builder,
|
||||
collapse=False, includehidden=False)
|
||||
assert_node(toctree,
|
||||
[compact_paragraph, ([title, "Table of Contents"],
|
||||
bullet_list,
|
||||
|
Reference in New Issue
Block a user