summaryrefslogtreecommitdiffstats
path: root/sphinx/directives/other.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/directives/other.py')
-rw-r--r--sphinx/directives/other.py443
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,
+ }