""" sphinx.domains.rst ~~~~~~~~~~~~~~~~~~ The reStructuredText domain. :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re from typing import Any, Dict, Iterator, List, Tuple from typing import cast from docutils.nodes import Element from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.addnodes import desc_signature, pending_xref from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType from sphinx.environment import BuildEnvironment from sphinx.locale import _, __ from sphinx.roles import XRefRole from sphinx.util import logging from sphinx.util.nodes import make_refnode logger = logging.getLogger(__name__) dir_sig_re = re.compile(r'\.\. (.+?)::(.*)$') class ReSTMarkup(ObjectDescription): """ Description of generic reST markup. """ def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None: targetname = self.objtype + '-' + name if targetname not in self.state.document.ids: signode['names'].append(targetname) signode['ids'].append(targetname) signode['first'] = (not self.names) self.state.document.note_explicit_target(signode) domain = cast(ReSTDomain, self.env.get_domain('rst')) domain.note_object(self.objtype, name, location=(self.env.docname, self.lineno)) indextext = self.get_index_text(self.objtype, name) if indextext: self.indexnode['entries'].append(('single', indextext, targetname, '', None)) def get_index_text(self, objectname: str, name: str) -> str: 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 = '.. %s::' % name 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 = 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(), 1) except ValueError: name, argument = sig, None signode += addnodes.desc_name(':%s:' % name, ':%s:' % 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: directive_name = self.current_directive targetname = '-'.join([self.objtype, self.current_directive, name]) if targetname not in self.state.document.ids: signode['names'].append(targetname) signode['ids'].append(targetname) signode['first'] = (not self.names) self.state.document.note_explicit_target(signode) objname = ':'.join(filter(None, [directive_name, name])) domain = cast(ReSTDomain, self.env.get_domain('rst')) domain.note_object(self.objtype, objname, location=(self.env.docname, self.lineno)) if directive_name: key = name[0].upper() pair = [_('%s (directive)') % directive_name, _(':%s: (directive option)') % name] self.indexnode['entries'].append(('pair', '; '.join(pair), targetname, '', key)) else: key = name[0].upper() text = _(':%s: (directive option)') % name self.indexnode['entries'].append(('single', text, targetname, '', 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: signode += addnodes.desc_name(':%s:' % sig, ':%s:' % sig) 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 = { 'objects': {}, # fullname -> docname, objtype } # type: Dict[str, Dict[Tuple[str, str], str]] @property def objects(self) -> Dict[Tuple[str, str], str]: return self.data.setdefault('objects', {}) # (objtype, fullname) -> docname def note_object(self, objtype: str, name: str, location: Any = None) -> None: if (objtype, name) in self.objects: docname = 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 def clear_doc(self, docname: str) -> None: for (typ, name), doc in list(self.objects.items()): if doc == docname: del self.objects[typ, name] def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None: # XXX check duplicates for (typ, name), doc in otherdata['objects'].items(): if doc in docnames: self.objects[typ, name] = doc def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element ) -> Element: objtypes = self.objtypes_for_role(typ) for objtype in objtypes: todocname = self.objects.get((objtype, target)) if todocname: return make_refnode(builder, fromdocname, todocname, objtype + '-' + target, 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 = [] # type: List[Tuple[str, Element]] for objtype in self.object_types: todocname = self.objects.get((objtype, target)) if todocname: results.append(('rst:' + self.role_for_objtype(objtype), make_refnode(builder, fromdocname, todocname, objtype + '-' + target, contnode, target + ' ' + objtype))) return results def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]: for (typ, name), docname in self.data['objects'].items(): yield name, name, typ, docname, typ + '-' + name, 1 def setup(app: Sphinx) -> Dict[str, Any]: app.add_domain(ReSTDomain) return { 'version': 'builtin', 'env_version': 1, 'parallel_read_safe': True, 'parallel_write_safe': True, }