summaryrefslogtreecommitdiffstats
path: root/sphinx/domains/rst.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/domains/rst.py')
-rw-r--r--sphinx/domains/rst.py299
1 files changed, 299 insertions, 0 deletions
diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py
new file mode 100644
index 0000000..480aba5
--- /dev/null
+++ b/sphinx/domains/rst.py
@@ -0,0 +1,299 @@
+"""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,
+ }