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/directives/other.py | |
parent | Initial commit. (diff) | |
download | sphinx-upstream/7.2.6.tar.xz sphinx-upstream/7.2.6.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/directives/other.py')
-rw-r--r-- | sphinx/directives/other.py | 443 |
1 files changed, 443 insertions, 0 deletions
diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py new file mode 100644 index 0000000..65cd90b --- /dev/null +++ b/sphinx/directives/other.py @@ -0,0 +1,443 @@ +from __future__ import annotations + +import re +from os.path import abspath, relpath +from pathlib import Path +from typing import TYPE_CHECKING, Any, cast + +from docutils import nodes +from docutils.parsers.rst import directives +from docutils.parsers.rst.directives.admonitions import BaseAdmonition +from docutils.parsers.rst.directives.misc import Class +from docutils.parsers.rst.directives.misc import Include as BaseInclude +from docutils.statemachine import StateMachine + +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 +from sphinx.util.matching import Matcher, patfilter +from sphinx.util.nodes import explicit_title_re + +if TYPE_CHECKING: + from docutils.nodes import Element, Node + + from sphinx.application import Sphinx + from sphinx.util.typing import OptionSpec + + +glob_re = re.compile(r'.*[*?\[].*') +logger = logging.getLogger(__name__) + + +def int_or_nothing(argument: str) -> int: + if not argument: + return 999 + return int(argument) + + +class TocTree(SphinxDirective): + """ + Directive to notify Sphinx about the hierarchical structure of the docs, + and to include a table-of-contents like tree in the current document. + """ + has_content = True + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = { + 'maxdepth': int, + 'name': directives.unchanged, + 'caption': directives.unchanged_required, + 'glob': directives.flag, + 'hidden': directives.flag, + 'includehidden': directives.flag, + 'numbered': int_or_nothing, + 'titlesonly': directives.flag, + 'reversed': directives.flag, + } + + def run(self) -> list[Node]: + subnode = addnodes.toctree() + subnode['parent'] = self.env.docname + + # (title, ref) pairs, where ref may be a document, or an external link, + # and title may be None if the document's title is to be used + subnode['entries'] = [] + subnode['includefiles'] = [] + subnode['maxdepth'] = self.options.get('maxdepth', -1) + subnode['caption'] = self.options.get('caption') + subnode['glob'] = 'glob' in self.options + subnode['hidden'] = 'hidden' in self.options + subnode['includehidden'] = 'includehidden' in self.options + subnode['numbered'] = self.options.get('numbered', 0) + subnode['titlesonly'] = 'titlesonly' in self.options + self.set_source_info(subnode) + wrappernode = nodes.compound(classes=['toctree-wrapper']) + wrappernode.append(subnode) + self.add_name(wrappernode) + + ret = self.parse_content(subnode) + ret.append(wrappernode) + return ret + + def parse_content(self, toctree: addnodes.toctree) -> list[Node]: + 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(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) + 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 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: + ref = docname = entry + title = None + + # remove suffixes (backwards compatibility) + for suffix in suffixes: + if docname.endswith(suffix): + docname = docname.removesuffix(suffix) + break + + # 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: + toctree['entries'] = list(reversed(toctree['entries'])) + toctree['includefiles'] = list(reversed(toctree['includefiles'])) + + return ret + + +class Author(SphinxDirective): + """ + Directive to give the name of the author of the current document + or section. Shown in the output only if the show_authors option is on. + """ + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + if not self.config.show_authors: + return [] + para: Element = nodes.paragraph(translatable=False) + emph = nodes.emphasis() + para += emph + if self.name == 'sectionauthor': + text = _('Section author: ') + elif self.name == 'moduleauthor': + text = _('Module author: ') + elif self.name == 'codeauthor': + text = _('Code author: ') + else: + text = _('Author: ') + emph += nodes.Text(text) + inodes, messages = self.state.inline_text(self.arguments[0], self.lineno) + emph.extend(inodes) + + ret: list[Node] = [para] + ret += messages + return ret + + +class SeeAlso(BaseAdmonition): + """ + An admonition mentioning things to look at as reference. + """ + node_class = addnodes.seealso + + +class TabularColumns(SphinxDirective): + """ + Directive to give an explicit tabulary column definition to LaTeX. + """ + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + node = addnodes.tabular_col_spec() + node['spec'] = self.arguments[0] + self.set_source_info(node) + return [node] + + +class Centered(SphinxDirective): + """ + Directive to create a centered line of bold text. + """ + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + if not self.arguments: + return [] + subnode: Element = addnodes.centered() + inodes, messages = self.state.inline_text(self.arguments[0], self.lineno) + subnode.extend(inodes) + + ret: list[Node] = [subnode] + ret += messages + return ret + + +class Acks(SphinxDirective): + """ + Directive for a list of names. + """ + has_content = True + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + node = addnodes.acks() + node.document = self.state.document + self.state.nested_parse(self.content, self.content_offset, node) + if len(node.children) != 1 or not isinstance(node.children[0], + nodes.bullet_list): + logger.warning(__('.. acks content is not a list'), + location=(self.env.docname, self.lineno)) + return [] + return [node] + + +class HList(SphinxDirective): + """ + Directive for a list that gets compacted horizontally. + """ + has_content = True + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + option_spec: OptionSpec = { + 'columns': int, + } + + def run(self) -> list[Node]: + ncolumns = self.options.get('columns', 2) + node = nodes.paragraph() + node.document = self.state.document + self.state.nested_parse(self.content, self.content_offset, node) + if len(node.children) != 1 or not isinstance(node.children[0], + nodes.bullet_list): + logger.warning(__('.. hlist content is not a list'), + location=(self.env.docname, self.lineno)) + return [] + fulllist = node.children[0] + # create a hlist node where the items are distributed + npercol, nmore = divmod(len(fulllist), ncolumns) + index = 0 + newnode = addnodes.hlist() + newnode['ncolumns'] = str(ncolumns) + for column in range(ncolumns): + endindex = index + ((npercol + 1) if column < nmore else npercol) + bullet_list = nodes.bullet_list() + bullet_list += fulllist.children[index:endindex] + newnode += addnodes.hlistcol('', bullet_list) + index = endindex + return [newnode] + + +class Only(SphinxDirective): + """ + Directive to only include text if the given tag(s) are enabled. + """ + has_content = True + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + node = addnodes.only() + node.document = self.state.document + self.set_source_info(node) + node['expr'] = self.arguments[0] + + # Same as util.nested_parse_with_titles but try to handle nested + # sections which should be raised higher up the doctree. + memo: Any = self.state.memo + surrounding_title_styles = memo.title_styles + surrounding_section_level = memo.section_level + memo.title_styles = [] + memo.section_level = 0 + try: + self.state.nested_parse(self.content, self.content_offset, + node, match_titles=True) + title_styles = memo.title_styles + if (not surrounding_title_styles or + not title_styles or + title_styles[0] not in surrounding_title_styles or + not self.state.parent): + # No nested sections so no special handling needed. + return [node] + # Calculate the depths of the current and nested sections. + current_depth = 0 + parent = self.state.parent + while parent: + current_depth += 1 + parent = parent.parent + current_depth -= 2 + title_style = title_styles[0] + nested_depth = len(surrounding_title_styles) + if title_style in surrounding_title_styles: + nested_depth = surrounding_title_styles.index(title_style) + # Use these depths to determine where the nested sections should + # be placed in the doctree. + n_sects_to_raise = current_depth - nested_depth + 1 + parent = cast(nodes.Element, self.state.parent) + for _i in range(n_sects_to_raise): + if parent.parent: + parent = parent.parent + parent.append(node) + return [] + finally: + memo.title_styles = surrounding_title_styles + memo.section_level = surrounding_section_level + + +class Include(BaseInclude, SphinxDirective): + """ + Like the standard "Include" directive, but interprets absolute paths + "correctly", i.e. relative to source directory. + """ + + def run(self) -> list[Node]: + + # To properly emit "include-read" events from included RST text, + # we must patch the ``StateMachine.insert_input()`` method. + # In the future, docutils will hopefully offer a way for Sphinx + # to provide the RST parser to use + # when parsing RST text that comes in via Include directive. + def _insert_input(include_lines, source): + # First, we need to combine the lines back into text so that + # we can send it with the include-read event. + # In docutils 0.18 and later, there are two lines at the end + # that act as markers. + # We must preserve them and leave them out of the include-read event: + text = "\n".join(include_lines[:-2]) + + path = Path(relpath(abspath(source), start=self.env.srcdir)) + docname = self.env.docname + + # Emit the "include-read" event + arg = [text] + self.env.app.events.emit('include-read', path, docname, arg) + text = arg[0] + + # Split back into lines and reattach the two marker lines + include_lines = text.splitlines() + include_lines[-2:] + + # Call the parent implementation. + # Note that this snake does not eat its tail because we patch + # the *Instance* method and this call is to the *Class* method. + return StateMachine.insert_input(self.state_machine, include_lines, source) + + # Only enable this patch if there are listeners for 'include-read'. + if self.env.app.events.listeners.get('include-read'): + # See https://github.com/python/mypy/issues/2427 for details on the mypy issue + self.state_machine.insert_input = _insert_input # type: ignore[method-assign] + + if self.arguments[0].startswith('<') and \ + self.arguments[0].endswith('>'): + # docutils "standard" includes, do not do path processing + return super().run() + rel_filename, filename = self.env.relfn2path(self.arguments[0]) + self.arguments[0] = filename + self.env.note_included(filename) + return super().run() + + +def setup(app: Sphinx) -> dict[str, Any]: + directives.register_directive('toctree', TocTree) + directives.register_directive('sectionauthor', Author) + directives.register_directive('moduleauthor', Author) + directives.register_directive('codeauthor', Author) + directives.register_directive('seealso', SeeAlso) + directives.register_directive('tabularcolumns', TabularColumns) + directives.register_directive('centered', Centered) + directives.register_directive('acks', Acks) + directives.register_directive('hlist', HList) + directives.register_directive('only', Only) + directives.register_directive('include', Include) + + # register the standard rst class directive under a different name + # only for backwards compatibility now + directives.register_directive('cssclass', Class) + # new standard name when default-domain with "class" is in effect + directives.register_directive('rst-class', Class) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } |