"""The reStructuredText domain.""" from __future__ import annotations import re from typing import TYPE_CHECKING, Any, cast from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType from sphinx.locale import _, __ from sphinx.roles import XRefRole from sphinx.util import logging from sphinx.util.nodes import make_id, make_refnode if TYPE_CHECKING: from collections.abc import Iterator from docutils.nodes import Element from sphinx.addnodes import desc_signature, pending_xref from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.environment import BuildEnvironment from sphinx.util.typing import OptionSpec logger = logging.getLogger(__name__) dir_sig_re = re.compile(r'\.\. (.+?)::(.*)$') class ReSTMarkup(ObjectDescription[str]): """ Description of generic reST markup. """ 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, } def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None: node_id = make_id(self.env, self.state.document, self.objtype, name) signode['ids'].append(node_id) self.state.document.note_explicit_target(signode) domain = cast(ReSTDomain, self.env.get_domain('rst')) domain.note_object(self.objtype, name, node_id, location=signode) if 'no-index-entry' not in self.options: indextext = self.get_index_text(self.objtype, name) if indextext: self.indexnode['entries'].append(('single', indextext, node_id, '', None)) def get_index_text(self, objectname: str, name: str) -> str: return '' def _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]: if 'fullname' not in sig_node: return () directive_names = [] for parent in self.env.ref_context.get('rst:directives', ()): directive_names += parent.split(':') name = sig_node['fullname'] return tuple(directive_names + name.split(':')) def _toc_entry_name(self, sig_node: desc_signature) -> str: if not sig_node.get('_toc_parts'): return '' config = self.env.app.config objtype = sig_node.parent.get('objtype') *parents, name = sig_node['_toc_parts'] if objtype == 'directive:option': return f':{name}:' if config.toc_object_entries_show_parents in {'domain', 'all'}: name = ':'.join(sig_node['_toc_parts']) if objtype == 'role': return f':{name}:' if objtype == 'directive': return f'.. {name}::' return '' def parse_directive(d: str) -> tuple[str, str]: """Parse a directive signature. Returns (directive, arguments) string tuple. If no arguments are given, returns (directive, ''). """ dir = d.strip() if not dir.startswith('.'): # Assume it is a directive without syntax return (dir, '') m = dir_sig_re.match(dir) if not m: return (dir, '') parsed_dir, parsed_args = m.groups() if parsed_args.strip(): return (parsed_dir.strip(), ' ' + parsed_args.strip()) else: return (parsed_dir.strip(), '') class ReSTDirective(ReSTMarkup): """ Description of a reST directive. """ def handle_signature(self, sig: str, signode: desc_signature) -> str: name, args = parse_directive(sig) desc_name = f'.. {name}::' signode['fullname'] = name.strip() signode += addnodes.desc_name(desc_name, desc_name) if len(args) > 0: signode += addnodes.desc_addname(args, args) return name def get_index_text(self, objectname: str, name: str) -> str: return _('%s (directive)') % name def before_content(self) -> None: if self.names: directives = self.env.ref_context.setdefault('rst:directives', []) directives.append(self.names[0]) def after_content(self) -> None: directives = self.env.ref_context.setdefault('rst:directives', []) if directives: directives.pop() class ReSTDirectiveOption(ReSTMarkup): """ Description of an option for reST directive. """ option_spec: OptionSpec = ReSTMarkup.option_spec.copy() option_spec.update({ 'type': directives.unchanged, }) def handle_signature(self, sig: str, signode: desc_signature) -> str: try: name, argument = re.split(r'\s*:\s+', sig.strip(), maxsplit=1) except ValueError: name, argument = sig, None desc_name = f':{name}:' signode['fullname'] = name.strip() signode += addnodes.desc_name(desc_name, desc_name) if argument: signode += addnodes.desc_annotation(' ' + argument, ' ' + argument) if self.options.get('type'): text = ' (%s)' % self.options['type'] signode += addnodes.desc_annotation(text, text) return name def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None: domain = cast(ReSTDomain, self.env.get_domain('rst')) directive_name = self.current_directive if directive_name: prefix = '-'.join([self.objtype, directive_name]) objname = ':'.join([directive_name, name]) else: prefix = self.objtype objname = name node_id = make_id(self.env, self.state.document, prefix, name) signode['ids'].append(node_id) self.state.document.note_explicit_target(signode) domain.note_object(self.objtype, objname, node_id, location=signode) if directive_name: key = name[0].upper() pair = [_('%s (directive)') % directive_name, _(':%s: (directive option)') % name] self.indexnode['entries'].append(('pair', '; '.join(pair), node_id, '', key)) else: key = name[0].upper() text = _(':%s: (directive option)') % name self.indexnode['entries'].append(('single', text, node_id, '', key)) @property def current_directive(self) -> str: directives = self.env.ref_context.get('rst:directives') if directives: return directives[-1] else: return '' class ReSTRole(ReSTMarkup): """ Description of a reST role. """ def handle_signature(self, sig: str, signode: desc_signature) -> str: desc_name = f':{sig}:' signode['fullname'] = sig.strip() signode += addnodes.desc_name(desc_name, desc_name) return sig def get_index_text(self, objectname: str, name: str) -> str: return _('%s (role)') % name class ReSTDomain(Domain): """ReStructuredText domain.""" name = 'rst' label = 'reStructuredText' object_types = { 'directive': ObjType(_('directive'), 'dir'), 'directive:option': ObjType(_('directive-option'), 'dir'), 'role': ObjType(_('role'), 'role'), } directives = { 'directive': ReSTDirective, 'directive:option': ReSTDirectiveOption, 'role': ReSTRole, } roles = { 'dir': XRefRole(), 'role': XRefRole(), } initial_data: dict[str, dict[tuple[str, str], str]] = { 'objects': {}, # fullname -> docname, objtype } @property def objects(self) -> dict[tuple[str, str], tuple[str, str]]: return self.data.setdefault('objects', {}) # (objtype, fullname) -> (docname, node_id) def note_object(self, objtype: str, name: str, node_id: str, location: Any = None) -> None: if (objtype, name) in self.objects: docname, node_id = self.objects[objtype, name] logger.warning(__('duplicate description of %s %s, other instance in %s') % (objtype, name, docname), location=location) self.objects[objtype, name] = (self.env.docname, node_id) def clear_doc(self, docname: str) -> None: for (typ, name), (doc, _node_id) in list(self.objects.items()): if doc == docname: del self.objects[typ, name] def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: # XXX check duplicates for (typ, name), (doc, node_id) in otherdata['objects'].items(): if doc in docnames: self.objects[typ, name] = (doc, node_id) def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element, ) -> Element | None: objtypes = self.objtypes_for_role(typ) if not objtypes: return None for objtype in objtypes: result = self.objects.get((objtype, target)) if result: todocname, node_id = result return make_refnode(builder, fromdocname, todocname, node_id, contnode, target + ' ' + objtype) return None def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element, ) -> list[tuple[str, Element]]: results: list[tuple[str, Element]] = [] for objtype in self.object_types: result = self.objects.get((objtype, target)) if result: todocname, node_id = result results.append( ('rst:' + self.role_for_objtype(objtype), # type: ignore[operator] make_refnode(builder, fromdocname, todocname, node_id, contnode, target + ' ' + objtype))) return results def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: for (typ, name), (docname, node_id) in self.data['objects'].items(): yield name, name, typ, docname, node_id, 1 def setup(app: Sphinx) -> dict[str, Any]: app.add_domain(ReSTDomain) return { 'version': 'builtin', 'env_version': 2, 'parallel_read_safe': True, 'parallel_write_safe': True, }