summaryrefslogtreecommitdiffstats
path: root/sphinx/directives
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 17:25:40 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 17:25:40 +0000
commitcf7da1843c45a4c2df7a749f7886a2d2ba0ee92a (patch)
tree18dcde1a8d1f5570a77cd0c361de3b490d02c789 /sphinx/directives
parentInitial commit. (diff)
downloadsphinx-cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a.tar.xz
sphinx-cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a.zip
Adding upstream version 7.2.6.upstream/7.2.6upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sphinx/directives')
-rw-r--r--sphinx/directives/__init__.py373
-rw-r--r--sphinx/directives/code.py482
-rw-r--r--sphinx/directives/other.py443
-rw-r--r--sphinx/directives/patches.py189
4 files changed, 1487 insertions, 0 deletions
diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py
new file mode 100644
index 0000000..d4cf28e
--- /dev/null
+++ b/sphinx/directives/__init__.py
@@ -0,0 +1,373 @@
+"""Handlers for additional ReST directives."""
+
+from __future__ import annotations
+
+import re
+from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
+
+from docutils import nodes
+from docutils.parsers.rst import directives, roles
+
+from sphinx import addnodes
+from sphinx.addnodes import desc_signature # NoQA: TCH001
+from sphinx.util import docutils
+from sphinx.util.docfields import DocFieldTransformer, Field, TypedField
+from sphinx.util.docutils import SphinxDirective
+from sphinx.util.nodes import nested_parse_with_titles
+from sphinx.util.typing import OptionSpec # NoQA: TCH001
+
+if TYPE_CHECKING:
+ from docutils.nodes import Node
+
+ from sphinx.application import Sphinx
+
+
+# RE to strip backslash escapes
+nl_escape_re = re.compile(r'\\\n')
+strip_backslash_re = re.compile(r'\\(.)')
+
+
+def optional_int(argument: str) -> int | None:
+ """
+ Check for an integer argument or None value; raise ``ValueError`` if not.
+ """
+ if argument is None:
+ return None
+ else:
+ value = int(argument)
+ if value < 0:
+ msg = 'negative value; must be positive or zero'
+ raise ValueError(msg)
+ return value
+
+
+ObjDescT = TypeVar('ObjDescT')
+
+
+class ObjectDescription(SphinxDirective, Generic[ObjDescT]):
+ """
+ Directive to describe a class, function or similar object. Not used
+ directly, but subclassed (in domain-specific directives) to add custom
+ behavior.
+ """
+
+ has_content = True
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ option_spec: OptionSpec = {
+ 'no-index': directives.flag,
+ 'no-index-entry': directives.flag,
+ 'no-contents-entry': directives.flag,
+ 'no-typesetting': directives.flag,
+ 'noindex': directives.flag,
+ 'noindexentry': directives.flag,
+ 'nocontentsentry': directives.flag,
+ }
+
+ # types of doc fields that this directive handles, see sphinx.util.docfields
+ doc_field_types: list[Field] = []
+ domain: str | None = None
+ objtype: str # set when `run` method is called
+ indexnode: addnodes.index
+
+ # Warning: this might be removed in future version. Don't touch this from extensions.
+ _doc_field_type_map: dict[str, tuple[Field, bool]] = {}
+
+ def get_field_type_map(self) -> dict[str, tuple[Field, bool]]:
+ if self._doc_field_type_map == {}:
+ self._doc_field_type_map = {}
+ for field in self.doc_field_types:
+ for name in field.names:
+ self._doc_field_type_map[name] = (field, False)
+
+ if field.is_typed:
+ typed_field = cast(TypedField, field)
+ for name in typed_field.typenames:
+ self._doc_field_type_map[name] = (field, True)
+
+ return self._doc_field_type_map
+
+ def get_signatures(self) -> list[str]:
+ """
+ Retrieve the signatures to document from the directive arguments. By
+ default, signatures are given as arguments, one per line.
+ """
+ lines = nl_escape_re.sub('', self.arguments[0]).split('\n')
+ if self.config.strip_signature_backslash:
+ # remove backslashes to support (dummy) escapes; helps Vim highlighting
+ return [strip_backslash_re.sub(r'\1', line.strip()) for line in lines]
+ else:
+ return [line.strip() for line in lines]
+
+ def handle_signature(self, sig: str, signode: desc_signature) -> ObjDescT:
+ """
+ Parse the signature *sig* into individual nodes and append them to
+ *signode*. If ValueError is raised, parsing is aborted and the whole
+ *sig* is put into a single desc_name node.
+
+ The return value should be a value that identifies the object. It is
+ passed to :meth:`add_target_and_index()` unchanged, and otherwise only
+ used to skip duplicates.
+ """
+ raise ValueError
+
+ def add_target_and_index(self, name: ObjDescT, sig: str, signode: desc_signature) -> None:
+ """
+ Add cross-reference IDs and entries to self.indexnode, if applicable.
+
+ *name* is whatever :meth:`handle_signature()` returned.
+ """
+ return # do nothing by default
+
+ def before_content(self) -> None:
+ """
+ Called before parsing content. Used to set information about the current
+ directive context on the build environment.
+ """
+ pass
+
+ def transform_content(self, contentnode: addnodes.desc_content) -> None:
+ """
+ Called after creating the content through nested parsing,
+ but before the ``object-description-transform`` event is emitted,
+ and before the info-fields are transformed.
+ Can be used to manipulate the content.
+ """
+ pass
+
+ def after_content(self) -> None:
+ """
+ Called after parsing content. Used to reset information about the
+ current directive context on the build environment.
+ """
+ pass
+
+ def _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]:
+ """
+ Returns a tuple of strings, one entry for each part of the object's
+ hierarchy (e.g. ``('module', 'submodule', 'Class', 'method')``). The
+ returned tuple is used to properly nest children within parents in the
+ table of contents, and can also be used within the
+ :py:meth:`_toc_entry_name` method.
+
+ This method must not be used outwith table of contents generation.
+ """
+ return ()
+
+ def _toc_entry_name(self, sig_node: desc_signature) -> str:
+ """
+ Returns the text of the table of contents entry for the object.
+
+ This function is called once, in :py:meth:`run`, to set the name for the
+ table of contents entry (a special attribute ``_toc_name`` is set on the
+ object node, later used in
+ ``environment.collectors.toctree.TocTreeCollector.process_doc().build_toc()``
+ when the table of contents entries are collected).
+
+ To support table of contents entries for their objects, domains must
+ override this method, also respecting the configuration setting
+ ``toc_object_entries_show_parents``. Domains must also override
+ :py:meth:`_object_hierarchy_parts`, with one (string) entry for each part of the
+ object's hierarchy. The result of this method is set on the signature
+ node, and can be accessed as ``sig_node['_toc_parts']`` for use within
+ this method. The resulting tuple is also used to properly nest children
+ within parents in the table of contents.
+
+ An example implementations of this method is within the python domain
+ (:meth:`!PyObject._toc_entry_name`). The python domain sets the
+ ``_toc_parts`` attribute within the :py:meth:`handle_signature()`
+ method.
+ """
+ return ''
+
+ def run(self) -> list[Node]:
+ """
+ Main directive entry function, called by docutils upon encountering the
+ directive.
+
+ This directive is meant to be quite easily subclassable, so it delegates
+ to several additional methods. What it does:
+
+ * find out if called as a domain-specific directive, set self.domain
+ * create a `desc` node to fit all description inside
+ * parse standard options, currently `no-index`
+ * create an index node if needed as self.indexnode
+ * parse all given signatures (as returned by self.get_signatures())
+ using self.handle_signature(), which should either return a name
+ or raise ValueError
+ * add index entries using self.add_target_and_index()
+ * parse the content and handle doc fields in it
+ """
+ if ':' in self.name:
+ self.domain, self.objtype = self.name.split(':', 1)
+ else:
+ self.domain, self.objtype = '', self.name
+ self.indexnode = addnodes.index(entries=[])
+
+ node = addnodes.desc()
+ node.document = self.state.document
+ source, line = self.get_source_info()
+ # If any options were specified to the directive,
+ # self.state.document.current_line will at this point be set to
+ # None. To ensure nodes created as part of the signature have a line
+ # number set, set the document's line number correctly.
+ #
+ # Note that we need to subtract one from the line number since
+ # note_source uses 0-based line numbers.
+ if line is not None:
+ line -= 1
+ self.state.document.note_source(source, line)
+ node['domain'] = self.domain
+ # 'desctype' is a backwards compatible attribute
+ node['objtype'] = node['desctype'] = self.objtype
+ node['no-index'] = node['noindex'] = no_index = (
+ 'no-index' in self.options
+ # xref RemovedInSphinx90Warning
+ # deprecate noindex in Sphinx 9.0
+ or 'noindex' in self.options)
+ node['no-index-entry'] = node['noindexentry'] = (
+ 'no-index-entry' in self.options
+ # xref RemovedInSphinx90Warning
+ # deprecate noindexentry in Sphinx 9.0
+ or 'noindexentry' in self.options)
+ node['no-contents-entry'] = node['nocontentsentry'] = (
+ 'no-contents-entry' in self.options
+ # xref RemovedInSphinx90Warning
+ # deprecate nocontentsentry in Sphinx 9.0
+ or 'nocontentsentry' in self.options)
+ node['no-typesetting'] = ('no-typesetting' in self.options)
+ if self.domain:
+ node['classes'].append(self.domain)
+ node['classes'].append(node['objtype'])
+
+ self.names: list[ObjDescT] = []
+ signatures = self.get_signatures()
+ for sig in signatures:
+ # add a signature node for each signature in the current unit
+ # and add a reference target for it
+ signode = addnodes.desc_signature(sig, '')
+ self.set_source_info(signode)
+ node.append(signode)
+ try:
+ # name can also be a tuple, e.g. (classname, objname);
+ # this is strictly domain-specific (i.e. no assumptions may
+ # be made in this base class)
+ name = self.handle_signature(sig, signode)
+ except ValueError:
+ # signature parsing failed
+ signode.clear()
+ signode += addnodes.desc_name(sig, sig)
+ continue # we don't want an index entry here
+ finally:
+ # Private attributes for ToC generation. Will be modified or removed
+ # without notice.
+ if self.env.app.config.toc_object_entries:
+ signode['_toc_parts'] = self._object_hierarchy_parts(signode)
+ signode['_toc_name'] = self._toc_entry_name(signode)
+ else:
+ signode['_toc_parts'] = ()
+ signode['_toc_name'] = ''
+ if name not in self.names:
+ self.names.append(name)
+ if not no_index:
+ # only add target and index entry if this is the first
+ # description of the object with this name in this desc block
+ self.add_target_and_index(name, sig, signode)
+
+ contentnode = addnodes.desc_content()
+ node.append(contentnode)
+
+ if self.names:
+ # needed for association of version{added,changed} directives
+ self.env.temp_data['object'] = self.names[0]
+ self.before_content()
+ nested_parse_with_titles(self.state, self.content, contentnode, self.content_offset)
+ self.transform_content(contentnode)
+ self.env.app.emit('object-description-transform',
+ self.domain, self.objtype, contentnode)
+ DocFieldTransformer(self).transform_all(contentnode)
+ self.env.temp_data['object'] = None
+ self.after_content()
+
+ if node['no-typesetting']:
+ # Attempt to return the index node, and a new target node
+ # containing all the ids of this node and its children.
+ # If ``:no-index:`` is set, or there are no ids on the node
+ # or any of its children, then just return the index node,
+ # as Docutils expects a target node to have at least one id.
+ if node_ids := [node_id for el in node.findall(nodes.Element)
+ for node_id in el.get('ids', ())]:
+ target_node = nodes.target(ids=node_ids)
+ self.set_source_info(target_node)
+ return [self.indexnode, target_node]
+ return [self.indexnode]
+ return [self.indexnode, node]
+
+
+class DefaultRole(SphinxDirective):
+ """
+ Set the default interpreted text role. Overridden from docutils.
+ """
+
+ optional_arguments = 1
+ final_argument_whitespace = False
+
+ def run(self) -> list[Node]:
+ if not self.arguments:
+ docutils.unregister_role('')
+ return []
+ role_name = self.arguments[0]
+ role, messages = roles.role(role_name, self.state_machine.language,
+ self.lineno, self.state.reporter)
+ if role: # type: ignore[truthy-function]
+ docutils.register_role('', role) # type: ignore[arg-type]
+ self.env.temp_data['default_role'] = role_name
+ else:
+ literal_block = nodes.literal_block(self.block_text, self.block_text)
+ reporter = self.state.reporter
+ error = reporter.error('Unknown interpreted text role "%s".' % role_name,
+ literal_block, line=self.lineno)
+ messages += [error]
+
+ return cast(list[nodes.Node], messages)
+
+
+class DefaultDomain(SphinxDirective):
+ """
+ Directive to (re-)set the default domain for this source file.
+ """
+
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = False
+ option_spec: OptionSpec = {}
+
+ def run(self) -> list[Node]:
+ domain_name = self.arguments[0].lower()
+ # if domain_name not in env.domains:
+ # # try searching by label
+ # for domain in env.domains.values():
+ # if domain.label.lower() == domain_name:
+ # domain_name = domain.name
+ # break
+ self.env.temp_data['default_domain'] = self.env.domains.get(domain_name)
+ return []
+
+
+def setup(app: Sphinx) -> dict[str, Any]:
+ app.add_config_value("strip_signature_backslash", False, 'env')
+ directives.register_directive('default-role', DefaultRole)
+ directives.register_directive('default-domain', DefaultDomain)
+ directives.register_directive('describe', ObjectDescription)
+ # new, more consistent, name
+ directives.register_directive('object', ObjectDescription)
+
+ app.add_event('object-description-transform')
+
+ return {
+ 'version': 'builtin',
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
diff --git a/sphinx/directives/code.py b/sphinx/directives/code.py
new file mode 100644
index 0000000..8de1661
--- /dev/null
+++ b/sphinx/directives/code.py
@@ -0,0 +1,482 @@
+from __future__ import annotations
+
+import sys
+import textwrap
+from difflib import unified_diff
+from typing import TYPE_CHECKING, Any
+
+from docutils import nodes
+from docutils.parsers.rst import directives
+from docutils.statemachine import StringList
+
+from sphinx import addnodes
+from sphinx.directives import optional_int
+from sphinx.locale import __
+from sphinx.util import logging, parselinenos
+from sphinx.util.docutils import SphinxDirective
+
+if TYPE_CHECKING:
+ from docutils.nodes import Element, Node
+
+ from sphinx.application import Sphinx
+ from sphinx.config import Config
+ from sphinx.util.typing import OptionSpec
+
+logger = logging.getLogger(__name__)
+
+
+class Highlight(SphinxDirective):
+ """
+ Directive to set the highlighting language for code blocks, as well
+ as the threshold for line numbers.
+ """
+
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = False
+ option_spec: OptionSpec = {
+ 'force': directives.flag,
+ 'linenothreshold': directives.positive_int,
+ }
+
+ def run(self) -> list[Node]:
+ language = self.arguments[0].strip()
+ linenothreshold = self.options.get('linenothreshold', sys.maxsize)
+ force = 'force' in self.options
+
+ self.env.temp_data['highlight_language'] = language
+ return [addnodes.highlightlang(lang=language,
+ force=force,
+ linenothreshold=linenothreshold)]
+
+
+def dedent_lines(
+ lines: list[str], dedent: int | None, location: tuple[str, int] | None = None,
+) -> list[str]:
+ if dedent is None:
+ return textwrap.dedent(''.join(lines)).splitlines(True)
+
+ if any(s[:dedent].strip() for s in lines):
+ logger.warning(__('non-whitespace stripped by dedent'), location=location)
+
+ new_lines = []
+ for line in lines:
+ new_line = line[dedent:]
+ if line.endswith('\n') and not new_line:
+ new_line = '\n' # keep CRLF
+ new_lines.append(new_line)
+
+ return new_lines
+
+
+def container_wrapper(
+ directive: SphinxDirective, literal_node: Node, caption: str,
+) -> nodes.container:
+ container_node = nodes.container('', literal_block=True,
+ classes=['literal-block-wrapper'])
+ parsed = nodes.Element()
+ directive.state.nested_parse(StringList([caption], source=''),
+ directive.content_offset, parsed)
+ if isinstance(parsed[0], nodes.system_message):
+ msg = __('Invalid caption: %s' % parsed[0].astext())
+ raise ValueError(msg)
+ if isinstance(parsed[0], nodes.Element):
+ caption_node = nodes.caption(parsed[0].rawsource, '',
+ *parsed[0].children)
+ caption_node.source = literal_node.source
+ caption_node.line = literal_node.line
+ container_node += caption_node
+ container_node += literal_node
+ return container_node
+ raise RuntimeError # never reached
+
+
+class CodeBlock(SphinxDirective):
+ """
+ Directive for a code block with special highlighting or line numbering
+ settings.
+ """
+
+ has_content = True
+ required_arguments = 0
+ optional_arguments = 1
+ final_argument_whitespace = False
+ option_spec: OptionSpec = {
+ 'force': directives.flag,
+ 'linenos': directives.flag,
+ 'dedent': optional_int,
+ 'lineno-start': int,
+ 'emphasize-lines': directives.unchanged_required,
+ 'caption': directives.unchanged_required,
+ 'class': directives.class_option,
+ 'name': directives.unchanged,
+ }
+
+ def run(self) -> list[Node]:
+ document = self.state.document
+ code = '\n'.join(self.content)
+ location = self.state_machine.get_source_and_line(self.lineno)
+
+ linespec = self.options.get('emphasize-lines')
+ if linespec:
+ try:
+ nlines = len(self.content)
+ hl_lines = parselinenos(linespec, nlines)
+ if any(i >= nlines for i in hl_lines):
+ logger.warning(__('line number spec is out of range(1-%d): %r') %
+ (nlines, self.options['emphasize-lines']),
+ location=location)
+
+ hl_lines = [x + 1 for x in hl_lines if x < nlines]
+ except ValueError as err:
+ return [document.reporter.warning(err, line=self.lineno)]
+ else:
+ hl_lines = None
+
+ if 'dedent' in self.options:
+ location = self.state_machine.get_source_and_line(self.lineno)
+ lines = code.splitlines(True)
+ lines = dedent_lines(lines, self.options['dedent'], location=location)
+ code = ''.join(lines)
+
+ literal: Element = nodes.literal_block(code, code)
+ if 'linenos' in self.options or 'lineno-start' in self.options:
+ literal['linenos'] = True
+ literal['classes'] += self.options.get('class', [])
+ literal['force'] = 'force' in self.options
+ if self.arguments:
+ # highlight language specified
+ literal['language'] = self.arguments[0]
+ else:
+ # no highlight language specified. Then this directive refers the current
+ # highlight setting via ``highlight`` directive or ``highlight_language``
+ # configuration.
+ literal['language'] = self.env.temp_data.get('highlight_language',
+ self.config.highlight_language)
+ extra_args = literal['highlight_args'] = {}
+ if hl_lines is not None:
+ extra_args['hl_lines'] = hl_lines
+ if 'lineno-start' in self.options:
+ extra_args['linenostart'] = self.options['lineno-start']
+ self.set_source_info(literal)
+
+ caption = self.options.get('caption')
+ if caption:
+ try:
+ literal = container_wrapper(self, literal, caption)
+ except ValueError as exc:
+ return [document.reporter.warning(exc, line=self.lineno)]
+
+ # literal will be note_implicit_target that is linked from caption and numref.
+ # when options['name'] is provided, it should be primary ID.
+ self.add_name(literal)
+
+ return [literal]
+
+
+class LiteralIncludeReader:
+ INVALID_OPTIONS_PAIR = [
+ ('lineno-match', 'lineno-start'),
+ ('lineno-match', 'append'),
+ ('lineno-match', 'prepend'),
+ ('start-after', 'start-at'),
+ ('end-before', 'end-at'),
+ ('diff', 'pyobject'),
+ ('diff', 'lineno-start'),
+ ('diff', 'lineno-match'),
+ ('diff', 'lines'),
+ ('diff', 'start-after'),
+ ('diff', 'end-before'),
+ ('diff', 'start-at'),
+ ('diff', 'end-at'),
+ ]
+
+ def __init__(self, filename: str, options: dict[str, Any], config: Config) -> None:
+ self.filename = filename
+ self.options = options
+ self.encoding = options.get('encoding', config.source_encoding)
+ self.lineno_start = self.options.get('lineno-start', 1)
+
+ self.parse_options()
+
+ def parse_options(self) -> None:
+ for option1, option2 in self.INVALID_OPTIONS_PAIR:
+ if option1 in self.options and option2 in self.options:
+ raise ValueError(__('Cannot use both "%s" and "%s" options') %
+ (option1, option2))
+
+ def read_file(
+ self, filename: str, location: tuple[str, int] | None = None,
+ ) -> list[str]:
+ try:
+ with open(filename, encoding=self.encoding, errors='strict') as f:
+ text = f.read()
+ if 'tab-width' in self.options:
+ text = text.expandtabs(self.options['tab-width'])
+
+ return text.splitlines(True)
+ except OSError as exc:
+ raise OSError(__('Include file %r not found or reading it failed') %
+ filename) from exc
+ except UnicodeError as exc:
+ raise UnicodeError(__('Encoding %r used for reading included file %r seems to '
+ 'be wrong, try giving an :encoding: option') %
+ (self.encoding, filename)) from exc
+
+ def read(self, location: tuple[str, int] | None = None) -> tuple[str, int]:
+ if 'diff' in self.options:
+ lines = self.show_diff()
+ else:
+ filters = [self.pyobject_filter,
+ self.start_filter,
+ self.end_filter,
+ self.lines_filter,
+ self.dedent_filter,
+ self.prepend_filter,
+ self.append_filter]
+ lines = self.read_file(self.filename, location=location)
+ for func in filters:
+ lines = func(lines, location=location)
+
+ return ''.join(lines), len(lines)
+
+ def show_diff(self, location: tuple[str, int] | None = None) -> list[str]:
+ new_lines = self.read_file(self.filename)
+ old_filename = self.options['diff']
+ old_lines = self.read_file(old_filename)
+ diff = unified_diff(old_lines, new_lines, str(old_filename), str(self.filename))
+ return list(diff)
+
+ def pyobject_filter(
+ self, lines: list[str], location: tuple[str, int] | None = None,
+ ) -> list[str]:
+ pyobject = self.options.get('pyobject')
+ if pyobject:
+ from sphinx.pycode import ModuleAnalyzer
+ analyzer = ModuleAnalyzer.for_file(self.filename, '')
+ tags = analyzer.find_tags()
+ if pyobject not in tags:
+ raise ValueError(__('Object named %r not found in include file %r') %
+ (pyobject, self.filename))
+ start = tags[pyobject][1]
+ end = tags[pyobject][2]
+ lines = lines[start - 1:end]
+ if 'lineno-match' in self.options:
+ self.lineno_start = start
+
+ return lines
+
+ def lines_filter(
+ self, lines: list[str], location: tuple[str, int] | None = None,
+ ) -> list[str]:
+ linespec = self.options.get('lines')
+ if linespec:
+ linelist = parselinenos(linespec, len(lines))
+ if any(i >= len(lines) for i in linelist):
+ logger.warning(__('line number spec is out of range(1-%d): %r') %
+ (len(lines), linespec), location=location)
+
+ if 'lineno-match' in self.options:
+ # make sure the line list is not "disjoint".
+ first = linelist[0]
+ if all(first + i == n for i, n in enumerate(linelist)):
+ self.lineno_start += linelist[0]
+ else:
+ raise ValueError(__('Cannot use "lineno-match" with a disjoint '
+ 'set of "lines"'))
+
+ lines = [lines[n] for n in linelist if n < len(lines)]
+ if lines == []:
+ raise ValueError(__('Line spec %r: no lines pulled from include file %r') %
+ (linespec, self.filename))
+
+ return lines
+
+ def start_filter(
+ self, lines: list[str], location: tuple[str, int] | None = None,
+ ) -> list[str]:
+ if 'start-at' in self.options:
+ start = self.options.get('start-at')
+ inclusive = False
+ elif 'start-after' in self.options:
+ start = self.options.get('start-after')
+ inclusive = True
+ else:
+ start = None
+
+ if start:
+ for lineno, line in enumerate(lines):
+ if start in line:
+ if inclusive:
+ if 'lineno-match' in self.options:
+ self.lineno_start += lineno + 1
+
+ return lines[lineno + 1:]
+ else:
+ if 'lineno-match' in self.options:
+ self.lineno_start += lineno
+
+ return lines[lineno:]
+
+ if inclusive is True:
+ raise ValueError('start-after pattern not found: %s' % start)
+ else:
+ raise ValueError('start-at pattern not found: %s' % start)
+
+ return lines
+
+ def end_filter(
+ self, lines: list[str], location: tuple[str, int] | None = None,
+ ) -> list[str]:
+ if 'end-at' in self.options:
+ end = self.options.get('end-at')
+ inclusive = True
+ elif 'end-before' in self.options:
+ end = self.options.get('end-before')
+ inclusive = False
+ else:
+ end = None
+
+ if end:
+ for lineno, line in enumerate(lines):
+ if end in line:
+ if inclusive:
+ return lines[:lineno + 1]
+ else:
+ if lineno == 0:
+ pass # end-before ignores first line
+ else:
+ return lines[:lineno]
+ if inclusive is True:
+ raise ValueError('end-at pattern not found: %s' % end)
+ else:
+ raise ValueError('end-before pattern not found: %s' % end)
+
+ return lines
+
+ def prepend_filter(
+ self, lines: list[str], location: tuple[str, int] | None = None,
+ ) -> list[str]:
+ prepend = self.options.get('prepend')
+ if prepend:
+ lines.insert(0, prepend + '\n')
+
+ return lines
+
+ def append_filter(
+ self, lines: list[str], location: tuple[str, int] | None = None,
+ ) -> list[str]:
+ append = self.options.get('append')
+ if append:
+ lines.append(append + '\n')
+
+ return lines
+
+ def dedent_filter(
+ self, lines: list[str], location: tuple[str, int] | None = None,
+ ) -> list[str]:
+ if 'dedent' in self.options:
+ return dedent_lines(lines, self.options.get('dedent'), location=location)
+ else:
+ return lines
+
+
+class LiteralInclude(SphinxDirective):
+ """
+ Like ``.. include:: :literal:``, but only warns if the include file is
+ not found, and does not raise errors. Also has several options for
+ selecting what to include.
+ """
+
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ option_spec: OptionSpec = {
+ 'dedent': optional_int,
+ 'linenos': directives.flag,
+ 'lineno-start': int,
+ 'lineno-match': directives.flag,
+ 'tab-width': int,
+ 'language': directives.unchanged_required,
+ 'force': directives.flag,
+ 'encoding': directives.encoding,
+ 'pyobject': directives.unchanged_required,
+ 'lines': directives.unchanged_required,
+ 'start-after': directives.unchanged_required,
+ 'end-before': directives.unchanged_required,
+ 'start-at': directives.unchanged_required,
+ 'end-at': directives.unchanged_required,
+ 'prepend': directives.unchanged_required,
+ 'append': directives.unchanged_required,
+ 'emphasize-lines': directives.unchanged_required,
+ 'caption': directives.unchanged,
+ 'class': directives.class_option,
+ 'name': directives.unchanged,
+ 'diff': directives.unchanged_required,
+ }
+
+ def run(self) -> list[Node]:
+ document = self.state.document
+ if not document.settings.file_insertion_enabled:
+ return [document.reporter.warning('File insertion disabled',
+ line=self.lineno)]
+ # convert options['diff'] to absolute path
+ if 'diff' in self.options:
+ _, path = self.env.relfn2path(self.options['diff'])
+ self.options['diff'] = path
+
+ try:
+ location = self.state_machine.get_source_and_line(self.lineno)
+ rel_filename, filename = self.env.relfn2path(self.arguments[0])
+ self.env.note_dependency(rel_filename)
+
+ reader = LiteralIncludeReader(filename, self.options, self.config)
+ text, lines = reader.read(location=location)
+
+ retnode: Element = nodes.literal_block(text, text, source=filename)
+ retnode['force'] = 'force' in self.options
+ self.set_source_info(retnode)
+ if self.options.get('diff'): # if diff is set, set udiff
+ retnode['language'] = 'udiff'
+ elif 'language' in self.options:
+ retnode['language'] = self.options['language']
+ if ('linenos' in self.options or 'lineno-start' in self.options or
+ 'lineno-match' in self.options):
+ retnode['linenos'] = True
+ retnode['classes'] += self.options.get('class', [])
+ extra_args = retnode['highlight_args'] = {}
+ if 'emphasize-lines' in self.options:
+ hl_lines = parselinenos(self.options['emphasize-lines'], lines)
+ if any(i >= lines for i in hl_lines):
+ logger.warning(__('line number spec is out of range(1-%d): %r') %
+ (lines, self.options['emphasize-lines']),
+ location=location)
+ extra_args['hl_lines'] = [x + 1 for x in hl_lines if x < lines]
+ extra_args['linenostart'] = reader.lineno_start
+
+ if 'caption' in self.options:
+ caption = self.options['caption'] or self.arguments[0]
+ retnode = container_wrapper(self, retnode, caption)
+
+ # retnode will be note_implicit_target that is linked from caption and numref.
+ # when options['name'] is provided, it should be primary ID.
+ self.add_name(retnode)
+
+ return [retnode]
+ except Exception as exc:
+ return [document.reporter.warning(exc, line=self.lineno)]
+
+
+def setup(app: Sphinx) -> dict[str, Any]:
+ directives.register_directive('highlight', Highlight)
+ directives.register_directive('code-block', CodeBlock)
+ directives.register_directive('sourcecode', CodeBlock)
+ directives.register_directive('literalinclude', LiteralInclude)
+
+ return {
+ 'version': 'builtin',
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
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,
+ }
diff --git a/sphinx/directives/patches.py b/sphinx/directives/patches.py
new file mode 100644
index 0000000..965b385
--- /dev/null
+++ b/sphinx/directives/patches.py
@@ -0,0 +1,189 @@
+from __future__ import annotations
+
+import os
+from os import path
+from typing import TYPE_CHECKING, Any, cast
+
+from docutils import nodes
+from docutils.nodes import Node, make_id
+from docutils.parsers.rst import directives
+from docutils.parsers.rst.directives import images, tables
+from docutils.parsers.rst.directives.misc import Meta # type: ignore[attr-defined]
+from docutils.parsers.rst.roles import set_classes
+
+from sphinx.directives import optional_int
+from sphinx.domains.math import MathDomain
+from sphinx.locale import __
+from sphinx.util import logging
+from sphinx.util.docutils import SphinxDirective
+from sphinx.util.nodes import set_source_info
+from sphinx.util.osutil import SEP, os_path, relpath
+
+if TYPE_CHECKING:
+ from sphinx.application import Sphinx
+ from sphinx.util.typing import OptionSpec
+
+
+logger = logging.getLogger(__name__)
+
+
+class Figure(images.Figure):
+ """The figure directive which applies `:name:` option to the figure node
+ instead of the image node.
+ """
+
+ def run(self) -> list[Node]:
+ name = self.options.pop('name', None)
+ result = super().run()
+ if len(result) == 2 or isinstance(result[0], nodes.system_message):
+ return result
+
+ assert len(result) == 1
+ figure_node = cast(nodes.figure, result[0])
+ if name:
+ # set ``name`` to figure_node if given
+ self.options['name'] = name
+ self.add_name(figure_node)
+
+ # copy lineno from image node
+ if figure_node.line is None and len(figure_node) == 2:
+ caption = cast(nodes.caption, figure_node[1])
+ figure_node.line = caption.line
+
+ return [figure_node]
+
+
+class CSVTable(tables.CSVTable):
+ """The csv-table directive which searches a CSV file from Sphinx project's source
+ directory when an absolute path is given via :file: option.
+ """
+
+ def run(self) -> list[Node]:
+ if 'file' in self.options and self.options['file'].startswith((SEP, os.sep)):
+ env = self.state.document.settings.env
+ filename = self.options['file']
+ if path.exists(filename):
+ logger.warning(__('":file:" option for csv-table directive now recognizes '
+ 'an absolute path as a relative path from source directory. '
+ 'Please update your document.'),
+ location=(env.docname, self.lineno))
+ else:
+ abspath = path.join(env.srcdir, os_path(self.options['file'][1:]))
+ docdir = path.dirname(env.doc2path(env.docname))
+ self.options['file'] = relpath(abspath, docdir)
+
+ return super().run()
+
+
+class Code(SphinxDirective):
+ """Parse and mark up content of a code block.
+
+ This is compatible with docutils' :rst:dir:`code` directive.
+ """
+ optional_arguments = 1
+ option_spec: OptionSpec = {
+ 'class': directives.class_option,
+ 'force': directives.flag,
+ 'name': directives.unchanged,
+ 'number-lines': optional_int,
+ }
+ has_content = True
+
+ def run(self) -> list[Node]:
+ self.assert_has_content()
+
+ set_classes(self.options)
+ code = '\n'.join(self.content)
+ node = nodes.literal_block(code, code,
+ classes=self.options.get('classes', []),
+ force='force' in self.options,
+ highlight_args={})
+ self.add_name(node)
+ set_source_info(self, node)
+
+ if self.arguments:
+ # highlight language specified
+ node['language'] = self.arguments[0]
+ else:
+ # no highlight language specified. Then this directive refers the current
+ # highlight setting via ``highlight`` directive or ``highlight_language``
+ # configuration.
+ node['language'] = self.env.temp_data.get('highlight_language',
+ self.config.highlight_language)
+
+ if 'number-lines' in self.options:
+ node['linenos'] = True
+
+ # if number given, treat as lineno-start.
+ if self.options['number-lines']:
+ node['highlight_args']['linenostart'] = self.options['number-lines']
+
+ return [node]
+
+
+class MathDirective(SphinxDirective):
+ has_content = True
+ required_arguments = 0
+ optional_arguments = 1
+ final_argument_whitespace = True
+ option_spec: OptionSpec = {
+ 'label': directives.unchanged,
+ 'name': directives.unchanged,
+ 'class': directives.class_option,
+ 'nowrap': directives.flag,
+ }
+
+ def run(self) -> list[Node]:
+ latex = '\n'.join(self.content)
+ if self.arguments and self.arguments[0]:
+ latex = self.arguments[0] + '\n\n' + latex
+ label = self.options.get('label', self.options.get('name'))
+ node = nodes.math_block(latex, latex,
+ classes=self.options.get('class', []),
+ docname=self.env.docname,
+ number=None,
+ label=label,
+ nowrap='nowrap' in self.options)
+ self.add_name(node)
+ self.set_source_info(node)
+
+ ret: list[Node] = [node]
+ self.add_target(ret)
+ return ret
+
+ def add_target(self, ret: list[Node]) -> None:
+ node = cast(nodes.math_block, ret[0])
+
+ # assign label automatically if math_number_all enabled
+ if node['label'] == '' or (self.config.math_number_all and not node['label']):
+ seq = self.env.new_serialno('sphinx.ext.math#equations')
+ node['label'] = "%s:%d" % (self.env.docname, seq)
+
+ # no targets and numbers are needed
+ if not node['label']:
+ return
+
+ # register label to domain
+ domain = cast(MathDomain, self.env.get_domain('math'))
+ domain.note_equation(self.env.docname, node['label'], location=node)
+ node['number'] = domain.get_equation_number_for(node['label'])
+
+ # add target node
+ node_id = make_id('equation-%s' % node['label'])
+ target = nodes.target('', '', ids=[node_id])
+ self.state.document.note_explicit_target(target)
+ ret.insert(0, target)
+
+
+def setup(app: Sphinx) -> dict[str, Any]:
+ directives.register_directive('figure', Figure)
+ directives.register_directive('meta', Meta)
+ directives.register_directive('csv-table', CSVTable)
+ directives.register_directive('code', Code)
+ directives.register_directive('math', MathDirective)
+
+ return {
+ 'version': 'builtin',
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }