from __future__ import annotations import os from os import path from typing import TYPE_CHECKING, ClassVar, 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 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 ExtensionMetadata, 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: ClassVar[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: ClassVar[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) -> ExtensionMetadata: 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, }