""" sphinx.domains.std ~~~~~~~~~~~~~~~~~~ The standard domain. :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re import unicodedata import warnings from copy import copy from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union, cast from docutils import nodes from docutils.nodes import Element, Node, system_message from docutils.parsers.rst import Directive, directives from docutils.statemachine import StringList from sphinx import addnodes from sphinx.addnodes import desc_signature, pending_xref from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType from sphinx.locale import _, __ from sphinx.roles import XRefRole from sphinx.util import docname_join, logging, ws_re from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import clean_astext, make_id, make_refnode from sphinx.util.typing import RoleFunction if False: # For type annotation from typing import Type # for python3.5.1 from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.environment import BuildEnvironment logger = logging.getLogger(__name__) # RE for option descriptions option_desc_re = re.compile(r'((?:/|--|-|\+)?[^\s=]+)(=?\s*.*)') # RE for grammar tokens token_re = re.compile(r'`(\w+)`', re.U) class GenericObject(ObjectDescription): """ A generic x-ref directive registered with Sphinx.add_object_type(). """ indextemplate = '' parse_node = None # type: Callable[[GenericObject, BuildEnvironment, str, desc_signature], str] # NOQA def handle_signature(self, sig: str, signode: desc_signature) -> str: if self.parse_node: name = self.parse_node(self.env, sig, signode) else: signode.clear() signode += addnodes.desc_name(sig, sig) # normalize whitespace like XRefRole does name = ws_re.sub(' ', sig) return name 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) # Assign old styled node_id not to break old hyperlinks (if possible) # Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning) old_node_id = self.make_old_id(name) if old_node_id not in self.state.document.ids and old_node_id not in signode['ids']: signode['ids'].append(old_node_id) self.state.document.note_explicit_target(signode) if self.indextemplate: colon = self.indextemplate.find(':') if colon != -1: indextype = self.indextemplate[:colon].strip() indexentry = self.indextemplate[colon + 1:].strip() % (name,) else: indextype = 'single' indexentry = self.indextemplate % (name,) self.indexnode['entries'].append((indextype, indexentry, node_id, '', None)) std = cast(StandardDomain, self.env.get_domain('std')) std.note_object(self.objtype, name, node_id, location=signode) def make_old_id(self, name: str) -> str: """Generate old styled node_id for generic objects. .. note:: Old Styled node_id was used until Sphinx-3.0. This will be removed in Sphinx-5.0. """ return self.objtype + '-' + name class EnvVar(GenericObject): indextemplate = _('environment variable; %s') class EnvVarXRefRole(XRefRole): """ Cross-referencing role for environment variables (adds an index entry). """ def result_nodes(self, document: nodes.document, env: "BuildEnvironment", node: Element, is_ref: bool) -> Tuple[List[Node], List[system_message]]: if not is_ref: return [node], [] varname = node['reftarget'] tgtid = 'index-%s' % env.new_serialno('index') indexnode = addnodes.index() indexnode['entries'] = [ ('single', varname, tgtid, '', None), ('single', _('environment variable; %s') % varname, tgtid, '', None) ] targetnode = nodes.target('', '', ids=[tgtid]) document.note_explicit_target(targetnode) return [indexnode, targetnode, node], [] class Target(SphinxDirective): """ Generic target for user-defined cross-reference types. """ indextemplate = '' has_content = False required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True option_spec = {} # type: Dict def run(self) -> List[Node]: # normalize whitespace in fullname like XRefRole does fullname = ws_re.sub(' ', self.arguments[0].strip()) node_id = make_id(self.env, self.state.document, self.name, fullname) node = nodes.target('', '', ids=[node_id]) self.set_source_info(node) # Assign old styled node_id not to break old hyperlinks (if possible) # Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning) old_node_id = self.make_old_id(fullname) if old_node_id not in self.state.document.ids and old_node_id not in node['ids']: node['ids'].append(old_node_id) self.state.document.note_explicit_target(node) ret = [node] # type: List[Node] if self.indextemplate: indexentry = self.indextemplate % (fullname,) indextype = 'single' colon = indexentry.find(':') if colon != -1: indextype = indexentry[:colon].strip() indexentry = indexentry[colon + 1:].strip() inode = addnodes.index(entries=[(indextype, indexentry, node_id, '', None)]) ret.insert(0, inode) name = self.name if ':' in self.name: _, name = self.name.split(':', 1) std = cast(StandardDomain, self.env.get_domain('std')) std.note_object(name, fullname, node_id, location=node) return ret def make_old_id(self, name: str) -> str: """Generate old styled node_id for targets. .. note:: Old Styled node_id was used until Sphinx-3.0. This will be removed in Sphinx-5.0. """ return self.name + '-' + name class Cmdoption(ObjectDescription): """ Description of a command-line option (.. option). """ def handle_signature(self, sig: str, signode: desc_signature) -> str: """Transform an option description into RST nodes.""" count = 0 firstname = '' for potential_option in sig.split(', '): potential_option = potential_option.strip() m = option_desc_re.match(potential_option) if not m: logger.warning(__('Malformed option description %r, should ' 'look like "opt", "-opt args", "--opt args", ' '"/opt args" or "+opt args"'), potential_option, location=signode) continue optname, args = m.groups() if optname.endswith('[') and args.endswith(']'): # optional value surrounded by brackets (ex. foo[=bar]) optname = optname[:-1] args = '[' + args if count: signode += addnodes.desc_addname(', ', ', ') signode += addnodes.desc_name(optname, optname) signode += addnodes.desc_addname(args, args) if not count: firstname = optname signode['allnames'] = [optname] else: signode['allnames'].append(optname) count += 1 if not firstname: raise ValueError return firstname def add_target_and_index(self, firstname: str, sig: str, signode: desc_signature) -> None: currprogram = self.env.ref_context.get('std:program') for optname in signode.get('allnames', []): prefixes = ['cmdoption'] if currprogram: prefixes.append(currprogram) if not optname.startswith(('-', '/')): prefixes.append('arg') prefix = '-'.join(prefixes) node_id = make_id(self.env, self.state.document, prefix, optname) signode['ids'].append(node_id) old_node_id = self.make_old_id(prefix, optname) if old_node_id not in self.state.document.ids and \ old_node_id not in signode['ids']: signode['ids'].append(old_node_id) self.state.document.note_explicit_target(signode) domain = cast(StandardDomain, self.env.get_domain('std')) for optname in signode.get('allnames', []): domain.add_program_option(currprogram, optname, self.env.docname, signode['ids'][0]) # create an index entry if currprogram: descr = _('%s command line option') % currprogram else: descr = _('command line option') for option in sig.split(', '): entry = '; '.join([descr, option]) self.indexnode['entries'].append(('pair', entry, signode['ids'][0], '', None)) def make_old_id(self, prefix: str, optname: str) -> str: """Generate old styled node_id for cmdoption. .. note:: Old Styled node_id was used until Sphinx-3.0. This will be removed in Sphinx-5.0. """ return nodes.make_id(prefix + '-' + optname) class Program(SphinxDirective): """ Directive to name the program for which options are documented. """ has_content = False required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True option_spec = {} # type: Dict def run(self) -> List[Node]: program = ws_re.sub('-', self.arguments[0].strip()) if program == 'None': self.env.ref_context.pop('std:program', None) else: self.env.ref_context['std:program'] = program return [] class OptionXRefRole(XRefRole): def process_link(self, env: "BuildEnvironment", refnode: Element, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]: refnode['std:program'] = env.ref_context.get('std:program') return title, target def split_term_classifiers(line: str) -> List[Optional[str]]: # split line into a term and classifiers. if no classifier, None is used.. parts = re.split(' +: +', line) + [None] return parts def make_glossary_term(env: "BuildEnvironment", textnodes: Iterable[Node], index_key: str, source: str, lineno: int, node_id: str = None, document: nodes.document = None) -> nodes.term: # get a text-only representation of the term and register it # as a cross-reference target term = nodes.term('', '', *textnodes) term.source = source term.line = lineno termtext = term.astext() if node_id: # node_id is given from outside (mainly i18n module), use it forcedly term['ids'].append(node_id) elif document: node_id = make_id(env, document, 'term', termtext) term['ids'].append(node_id) document.note_explicit_target(term) else: warnings.warn('make_glossary_term() expects document is passed as an argument.', RemovedInSphinx40Warning, stacklevel=2) gloss_entries = env.temp_data.setdefault('gloss_entries', set()) node_id = nodes.make_id('term-' + termtext) if node_id == 'term': # "term" is not good for node_id. Generate it by sequence number instead. node_id = 'term-%d' % env.new_serialno('glossary') while node_id in gloss_entries: node_id = 'term-%d' % env.new_serialno('glossary') gloss_entries.add(node_id) term['ids'].append(node_id) std = cast(StandardDomain, env.get_domain('std')) std.note_object('term', termtext, node_id, location=term) # add an index entry too indexnode = addnodes.index() indexnode['entries'] = [('single', termtext, node_id, 'main', index_key)] indexnode.source, indexnode.line = term.source, term.line term.append(indexnode) return term class Glossary(SphinxDirective): """ Directive to create a glossary with cross-reference targets for :term: roles. """ has_content = True required_arguments = 0 optional_arguments = 0 final_argument_whitespace = False option_spec = { 'sorted': directives.flag, } def run(self) -> List[Node]: node = addnodes.glossary() node.document = self.state.document # This directive implements a custom format of the reST definition list # that allows multiple lines of terms before the definition. This is # easy to parse since we know that the contents of the glossary *must # be* a definition list. # first, collect single entries entries = [] # type: List[Tuple[List[Tuple[str, str, int]], StringList]] in_definition = True in_comment = False was_empty = True messages = [] # type: List[Node] for line, (source, lineno) in zip(self.content, self.content.items): # empty line -> add to last definition if not line: if in_definition and entries: entries[-1][1].append('', source, lineno) was_empty = True continue # unindented line -> a term if line and not line[0].isspace(): # enable comments if line.startswith('.. '): in_comment = True continue else: in_comment = False # first term of definition if in_definition: if not was_empty: messages.append(self.state.reporter.warning( _('glossary term must be preceded by empty line'), source=source, line=lineno)) entries.append(([(line, source, lineno)], StringList())) in_definition = False # second term and following else: if was_empty: messages.append(self.state.reporter.warning( _('glossary terms must not be separated by empty lines'), source=source, line=lineno)) if entries: entries[-1][0].append((line, source, lineno)) else: messages.append(self.state.reporter.warning( _('glossary seems to be misformatted, check indentation'), source=source, line=lineno)) elif in_comment: pass else: if not in_definition: # first line of definition, determines indentation in_definition = True indent_len = len(line) - len(line.lstrip()) if entries: entries[-1][1].append(line[indent_len:], source, lineno) else: messages.append(self.state.reporter.warning( _('glossary seems to be misformatted, check indentation'), source=source, line=lineno)) was_empty = False # now, parse all the entries into a big definition list items = [] for terms, definition in entries: termtexts = [] # type: List[str] termnodes = [] # type: List[Node] system_messages = [] # type: List[Node] for line, source, lineno in terms: parts = split_term_classifiers(line) # parse the term with inline markup # classifiers (parts[1:]) will not be shown on doctree textnodes, sysmsg = self.state.inline_text(parts[0], lineno) # use first classifier as a index key term = make_glossary_term(self.env, textnodes, parts[1], source, lineno, document=self.state.document) term.rawsource = line system_messages.extend(sysmsg) termtexts.append(term.astext()) termnodes.append(term) termnodes.extend(system_messages) defnode = nodes.definition() if definition: self.state.nested_parse(definition, definition.items[0][1], defnode) termnodes.append(defnode) items.append((termtexts, nodes.definition_list_item('', *termnodes))) if 'sorted' in self.options: items.sort(key=lambda x: unicodedata.normalize('NFD', x[0][0].lower())) dlist = nodes.definition_list() dlist['classes'].append('glossary') dlist.extend(item[1] for item in items) node += dlist return messages + [node] def token_xrefs(text: str, productionGroup: str = '') -> List[Node]: if len(productionGroup) != 0: productionGroup += ':' retnodes = [] # type: List[Node] pos = 0 for m in token_re.finditer(text): if m.start() > pos: txt = text[pos:m.start()] retnodes.append(nodes.Text(txt, txt)) refnode = pending_xref(m.group(1), reftype='token', refdomain='std', reftarget=productionGroup + m.group(1)) refnode += nodes.literal(m.group(1), m.group(1), classes=['xref']) retnodes.append(refnode) pos = m.end() if pos < len(text): retnodes.append(nodes.Text(text[pos:], text[pos:])) return retnodes class ProductionList(SphinxDirective): """ Directive to list grammar productions. """ has_content = False required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True option_spec = {} # type: Dict def run(self) -> List[Node]: domain = cast(StandardDomain, self.env.get_domain('std')) node = addnodes.productionlist() # type: Element self.set_source_info(node) # The backslash handling is from ObjectDescription.get_signatures nl_escape_re = re.compile(r'\\\n') lines = nl_escape_re.sub('', self.arguments[0]).split('\n') productionGroup = "" i = 0 for rule in lines: if i == 0 and ':' not in rule: productionGroup = rule.strip() continue i += 1 try: name, tokens = rule.split(':', 1) except ValueError: break subnode = addnodes.production(rule) name = name.strip() subnode['tokenname'] = name if subnode['tokenname']: prefix = 'grammar-token-%s' % productionGroup node_id = make_id(self.env, self.state.document, prefix, name) subnode['ids'].append(node_id) # Assign old styled node_id not to break old hyperlinks (if possible) # Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning) old_node_id = self.make_old_id(name) if (old_node_id not in self.state.document.ids and old_node_id not in subnode['ids']): subnode['ids'].append(old_node_id) self.state.document.note_implicit_target(subnode, subnode) if len(productionGroup) != 0: objName = "%s:%s" % (productionGroup, name) else: objName = name domain.note_object('token', objName, node_id, location=node) subnode.extend(token_xrefs(tokens, productionGroup)) node.append(subnode) return [node] def make_old_id(self, token: str) -> str: """Generate old styled node_id for tokens. .. note:: Old Styled node_id was used until Sphinx-3.0. This will be removed in Sphinx-5.0. """ return nodes.make_id('grammar-token-' + token) class TokenXRefRole(XRefRole): def process_link(self, env: "BuildEnvironment", refnode: Element, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]: target = target.lstrip('~') # a title-specific thing if not self.has_explicit_title and title[0] == '~': if ':' in title: _, title = title.split(':') else: title = title[1:] return title, target class StandardDomain(Domain): """ Domain for all objects that don't fit into another domain or are added via the application interface. """ name = 'std' label = 'Default' object_types = { 'term': ObjType(_('glossary term'), 'term', searchprio=-1), 'token': ObjType(_('grammar token'), 'token', searchprio=-1), 'label': ObjType(_('reference label'), 'ref', 'keyword', searchprio=-1), 'envvar': ObjType(_('environment variable'), 'envvar'), 'cmdoption': ObjType(_('program option'), 'option'), 'doc': ObjType(_('document'), 'doc', searchprio=-1) } # type: Dict[str, ObjType] directives = { 'program': Program, 'cmdoption': Cmdoption, # old name for backwards compatibility 'option': Cmdoption, 'envvar': EnvVar, 'glossary': Glossary, 'productionlist': ProductionList, } # type: Dict[str, Type[Directive]] roles = { 'option': OptionXRefRole(warn_dangling=True), 'envvar': EnvVarXRefRole(), # links to tokens in grammar productions 'token': TokenXRefRole(), # links to terms in glossary 'term': XRefRole(innernodeclass=nodes.inline, warn_dangling=True), # links to headings or arbitrary labels 'ref': XRefRole(lowercase=True, innernodeclass=nodes.inline, warn_dangling=True), # links to labels of numbered figures, tables and code-blocks 'numref': XRefRole(lowercase=True, warn_dangling=True), # links to labels, without a different title 'keyword': XRefRole(warn_dangling=True), # links to documents 'doc': XRefRole(warn_dangling=True, innernodeclass=nodes.inline), } # type: Dict[str, Union[RoleFunction, XRefRole]] initial_data = { 'progoptions': {}, # (program, name) -> docname, labelid 'objects': {}, # (type, name) -> docname, labelid 'labels': { # labelname -> docname, labelid, sectionname 'genindex': ('genindex', '', _('Index')), 'modindex': ('py-modindex', '', _('Module Index')), 'search': ('search', '', _('Search Page')), }, 'anonlabels': { # labelname -> docname, labelid 'genindex': ('genindex', ''), 'modindex': ('py-modindex', ''), 'search': ('search', ''), }, } dangling_warnings = { 'term': 'term not in glossary: %(target)s', 'numref': 'undefined label: %(target)s', 'keyword': 'unknown keyword: %(target)s', 'doc': 'unknown document: %(target)s', 'option': 'unknown option: %(target)s', } enumerable_nodes = { # node_class -> (figtype, title_getter) nodes.figure: ('figure', None), nodes.table: ('table', None), nodes.container: ('code-block', None), } # type: Dict[Type[Node], Tuple[str, Callable]] def __init__(self, env: "BuildEnvironment") -> None: super().__init__(env) # set up enumerable nodes self.enumerable_nodes = copy(self.enumerable_nodes) # create a copy for this instance for node, settings in env.app.registry.enumerable_nodes.items(): self.enumerable_nodes[node] = settings def note_hyperlink_target(self, name: str, docname: str, node_id: str, title: str = '') -> None: """Add a hyperlink target for cross reference. .. warning:: This is only for internal use. Please don't use this from your extension. ``document.note_explicit_target()`` or ``note_implicit_target()`` are recommended to add a hyperlink target to the document. This only adds a hyperlink target to the StandardDomain. And this does not add a node_id to node. Therefore, it is very fragile to calling this without understanding hyperlink target framework in both docutils and Sphinx. .. versionadded:: 3.0 """ if name in self.anonlabels and self.anonlabels[name] != (docname, node_id): logger.warning(__('duplicate label %s, other instance in %s'), name, self.env.doc2path(self.anonlabels[name][0])) self.anonlabels[name] = (docname, node_id) if title: self.labels[name] = (docname, node_id, title) @property def objects(self) -> Dict[Tuple[str, str], Tuple[str, str]]: return self.data.setdefault('objects', {}) # (objtype, name) -> docname, labelid def note_object(self, objtype: str, name: str, labelid: str, location: Any = None ) -> None: """Note a generic object for cross reference. .. versionadded:: 3.0 """ if (objtype, name) in self.objects: docname = self.objects[objtype, name][0] logger.warning(__('duplicate %s description of %s, other instance in %s'), objtype, name, docname, location=location) self.objects[objtype, name] = (self.env.docname, labelid) def add_object(self, objtype: str, name: str, docname: str, labelid: str) -> None: warnings.warn('StandardDomain.add_object() is deprecated.', RemovedInSphinx50Warning, stacklevel=2) self.objects[objtype, name] = (docname, labelid) @property def progoptions(self) -> Dict[Tuple[str, str], Tuple[str, str]]: return self.data.setdefault('progoptions', {}) # (program, name) -> docname, labelid @property def labels(self) -> Dict[str, Tuple[str, str, str]]: return self.data.setdefault('labels', {}) # labelname -> docname, labelid, sectionname @property def anonlabels(self) -> Dict[str, Tuple[str, str]]: return self.data.setdefault('anonlabels', {}) # labelname -> docname, labelid def clear_doc(self, docname: str) -> None: key = None # type: Any for key, (fn, _l) in list(self.progoptions.items()): if fn == docname: del self.progoptions[key] for key, (fn, _l) in list(self.objects.items()): if fn == docname: del self.objects[key] for key, (fn, _l, _l) in list(self.labels.items()): if fn == docname: del self.labels[key] for key, (fn, _l) in list(self.anonlabels.items()): if fn == docname: del self.anonlabels[key] def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None: # XXX duplicates? for key, data in otherdata['progoptions'].items(): if data[0] in docnames: self.progoptions[key] = data for key, data in otherdata['objects'].items(): if data[0] in docnames: self.objects[key] = data for key, data in otherdata['labels'].items(): if data[0] in docnames: self.labels[key] = data for key, data in otherdata['anonlabels'].items(): if data[0] in docnames: self.anonlabels[key] = data def process_doc(self, env: "BuildEnvironment", docname: str, document: nodes.document) -> None: # NOQA for name, explicit in document.nametypes.items(): if not explicit: continue labelid = document.nameids[name] if labelid is None: continue node = document.ids[labelid] if isinstance(node, nodes.target) and 'refid' in node: # indirect hyperlink targets node = document.ids.get(node['refid']) labelid = node['names'][0] if (node.tagname == 'footnote' or 'refuri' in node or node.tagname.startswith('desc_')): # ignore footnote labels, labels automatically generated from a # link and object descriptions continue if name in self.labels: logger.warning(__('duplicate label %s, other instance in %s'), name, env.doc2path(self.labels[name][0]), location=node) self.anonlabels[name] = docname, labelid if node.tagname in ('section', 'rubric'): title = cast(nodes.title, node[0]) sectname = clean_astext(title) elif self.is_enumerable_node(node): sectname = self.get_numfig_title(node) if not sectname: continue else: toctree = next(iter(node.traverse(addnodes.toctree)), None) if toctree and toctree.get('caption'): sectname = toctree.get('caption') else: # anonymous-only labels continue self.labels[name] = docname, labelid, sectname def add_program_option(self, program: str, name: str, docname: str, labelid: str) -> None: self.progoptions[program, name] = (docname, labelid) def build_reference_node(self, fromdocname: str, builder: "Builder", docname: str, labelid: str, sectname: str, rolename: str, **options: Any ) -> Element: nodeclass = options.pop('nodeclass', nodes.reference) newnode = nodeclass('', '', internal=True, **options) innernode = nodes.inline(sectname, sectname) if innernode.get('classes') is not None: innernode['classes'].append('std') innernode['classes'].append('std-' + rolename) if docname == fromdocname: newnode['refid'] = labelid else: # set more info in contnode; in case the # get_relative_uri call raises NoUri, # the builder will then have to resolve these contnode = pending_xref('') contnode['refdocname'] = docname contnode['refsectname'] = sectname newnode['refuri'] = builder.get_relative_uri( fromdocname, docname) if labelid: newnode['refuri'] += '#' + labelid newnode.append(innernode) return newnode def resolve_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, contnode: Element) -> Element: if typ == 'ref': resolver = self._resolve_ref_xref elif typ == 'numref': resolver = self._resolve_numref_xref elif typ == 'keyword': resolver = self._resolve_keyword_xref elif typ == 'doc': resolver = self._resolve_doc_xref elif typ == 'option': resolver = self._resolve_option_xref elif typ == 'citation': warnings.warn('pending_xref(domain=std, type=citation) is deprecated: %r' % node, RemovedInSphinx40Warning, stacklevel=2) domain = env.get_domain('citation') return domain.resolve_xref(env, fromdocname, builder, typ, target, node, contnode) elif typ == 'term': resolver = self._resolve_term_xref else: resolver = self._resolve_obj_xref return resolver(env, fromdocname, builder, typ, target, node, contnode) def _resolve_ref_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, contnode: Element) -> Element: if node['refexplicit']: # reference to anonymous label; the reference uses # the supplied link caption docname, labelid = self.anonlabels.get(target, ('', '')) sectname = node.astext() else: # reference to named label; the final node will # contain the section name after the label docname, labelid, sectname = self.labels.get(target, ('', '', '')) if not docname: return None return self.build_reference_node(fromdocname, builder, docname, labelid, sectname, 'ref') def _resolve_numref_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, contnode: Element) -> Element: if target in self.labels: docname, labelid, figname = self.labels.get(target, ('', '', '')) else: docname, labelid = self.anonlabels.get(target, ('', '')) figname = None if not docname: return None target_node = env.get_doctree(docname).ids.get(labelid) figtype = self.get_enumerable_node_type(target_node) if figtype is None: return None if figtype != 'section' and env.config.numfig is False: logger.warning(__('numfig is disabled. :numref: is ignored.'), location=node) return contnode try: fignumber = self.get_fignumber(env, builder, figtype, docname, target_node) if fignumber is None: return contnode except ValueError: logger.warning(__("Failed to create a cross reference. Any number is not " "assigned: %s"), labelid, location=node) return contnode try: if node['refexplicit']: title = contnode.astext() else: title = env.config.numfig_format.get(figtype, '') if figname is None and '{name}' in title: logger.warning(__('the link has no caption: %s'), title, location=node) return contnode else: fignum = '.'.join(map(str, fignumber)) if '{name}' in title or 'number' in title: # new style format (cf. "Fig.{number}") if figname: newtitle = title.format(name=figname, number=fignum) else: newtitle = title.format(number=fignum) else: # old style format (cf. "Fig.%s") newtitle = title % fignum except KeyError as exc: logger.warning(__('invalid numfig_format: %s (%r)'), title, exc, location=node) return contnode except TypeError: logger.warning(__('invalid numfig_format: %s'), title, location=node) return contnode return self.build_reference_node(fromdocname, builder, docname, labelid, newtitle, 'numref', nodeclass=addnodes.number_reference, title=title) def _resolve_keyword_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, contnode: Element) -> Element: # keywords are oddballs: they are referenced by named labels docname, labelid, _ = self.labels.get(target, ('', '', '')) if not docname: return None return make_refnode(builder, fromdocname, docname, labelid, contnode) def _resolve_doc_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, contnode: Element) -> Element: # directly reference to document by source name; can be absolute or relative refdoc = node.get('refdoc', fromdocname) docname = docname_join(refdoc, node['reftarget']) if docname not in env.all_docs: return None else: if node['refexplicit']: # reference with explicit title caption = node.astext() else: caption = clean_astext(env.titles[docname]) innernode = nodes.inline(caption, caption, classes=['doc']) return make_refnode(builder, fromdocname, docname, None, innernode) def _resolve_option_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, contnode: Element) -> Element: progname = node.get('std:program') target = target.strip() docname, labelid = self.progoptions.get((progname, target), ('', '')) if not docname: commands = [] while ws_re.search(target): subcommand, target = ws_re.split(target, 1) commands.append(subcommand) progname = "-".join(commands) docname, labelid = self.progoptions.get((progname, target), ('', '')) if docname: break else: return None return make_refnode(builder, fromdocname, docname, labelid, contnode) def _resolve_term_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, contnode: Element) -> Element: result = self._resolve_obj_xref(env, fromdocname, builder, typ, target, node, contnode) if result: return result else: for objtype, term in self.objects: if objtype == 'term' and term.lower() == target.lower(): docname, labelid = self.objects[objtype, term] logger.warning(__('term %s not found in case sensitive match.' 'made a reference to %s instead.'), target, term, location=node, type='ref', subtype='term') break else: docname, labelid = '', '' if not docname: return None return make_refnode(builder, fromdocname, docname, labelid, contnode) def _resolve_obj_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, contnode: Element) -> Element: objtypes = self.objtypes_for_role(typ) or [] for objtype in objtypes: if (objtype, target) in self.objects: docname, labelid = self.objects[objtype, target] break else: docname, labelid = '', '' if not docname: return None return make_refnode(builder, fromdocname, docname, labelid, contnode) 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]] ltarget = target.lower() # :ref: lowercases its target automatically for role in ('ref', 'option'): # do not try "keyword" res = self.resolve_xref(env, fromdocname, builder, role, ltarget if role == 'ref' else target, node, contnode) if res: results.append(('std:' + role, res)) # all others for objtype in self.object_types: key = (objtype, target) if objtype == 'term': key = (objtype, ltarget) if key in self.objects: docname, labelid = self.objects[key] results.append(('std:' + self.role_for_objtype(objtype), make_refnode(builder, fromdocname, docname, labelid, contnode))) return results def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]: # handle the special 'doc' reference here for doc in self.env.all_docs: yield (doc, clean_astext(self.env.titles[doc]), 'doc', doc, '', -1) for (prog, option), info in self.progoptions.items(): if prog: fullname = ".".join([prog, option]) yield (fullname, fullname, 'cmdoption', info[0], info[1], 1) else: yield (option, option, 'cmdoption', info[0], info[1], 1) for (type, name), info in self.objects.items(): yield (name, name, type, info[0], info[1], self.object_types[type].attrs['searchprio']) for name, (docname, labelid, sectionname) in self.labels.items(): yield (name, sectionname, 'label', docname, labelid, -1) # add anonymous-only labels as well non_anon_labels = set(self.labels) for name, (docname, labelid) in self.anonlabels.items(): if name not in non_anon_labels: yield (name, name, 'label', docname, labelid, -1) def get_type_name(self, type: ObjType, primary: bool = False) -> str: # never prepend "Default" return type.lname def is_enumerable_node(self, node: Node) -> bool: return node.__class__ in self.enumerable_nodes def get_numfig_title(self, node: Node) -> str: """Get the title of enumerable nodes to refer them using its title""" if self.is_enumerable_node(node): elem = cast(Element, node) _, title_getter = self.enumerable_nodes.get(elem.__class__, (None, None)) if title_getter: return title_getter(elem) else: for subnode in elem: if isinstance(subnode, (nodes.caption, nodes.title)): return clean_astext(subnode) return None def get_enumerable_node_type(self, node: Node) -> str: """Get type of enumerable nodes.""" def has_child(node: Element, cls: "Type") -> bool: return any(isinstance(child, cls) for child in node) if isinstance(node, nodes.section): return 'section' elif (isinstance(node, nodes.container) and 'literal_block' in node and has_child(node, nodes.literal_block)): # given node is a code-block having caption return 'code-block' else: figtype, _ = self.enumerable_nodes.get(node.__class__, (None, None)) return figtype def get_fignumber(self, env: "BuildEnvironment", builder: "Builder", figtype: str, docname: str, target_node: Element) -> Tuple[int, ...]: if figtype == 'section': if builder.name == 'latex': return tuple() elif docname not in env.toc_secnumbers: raise ValueError # no number assigned else: anchorname = '#' + target_node['ids'][0] if anchorname not in env.toc_secnumbers[docname]: # try first heading which has no anchor return env.toc_secnumbers[docname].get('') else: return env.toc_secnumbers[docname].get(anchorname) else: try: figure_id = target_node['ids'][0] return env.toc_fignumbers[docname][figtype][figure_id] except (KeyError, IndexError) as exc: # target_node is found, but fignumber is not assigned. # Maybe it is defined in orphaned document. raise ValueError from exc def get_full_qualified_name(self, node: Element) -> str: if node.get('reftype') == 'option': progname = node.get('std:program') command = ws_re.split(node.get('reftarget')) if progname: command.insert(0, progname) option = command.pop() if command: return '.'.join(['-'.join(command), option]) else: return None else: return None def note_citations(self, env: "BuildEnvironment", docname: str, document: nodes.document) -> None: # NOQA warnings.warn('StandardDomain.note_citations() is deprecated.', RemovedInSphinx40Warning, stacklevel=2) def note_citation_refs(self, env: "BuildEnvironment", docname: str, document: nodes.document) -> None: # NOQA warnings.warn('StandardDomain.note_citation_refs() is deprecated.', RemovedInSphinx40Warning, stacklevel=2) def note_labels(self, env: "BuildEnvironment", docname: str, document: nodes.document) -> None: # NOQA warnings.warn('StandardDomain.note_labels() is deprecated.', RemovedInSphinx40Warning, stacklevel=2) def warn_missing_reference(app: "Sphinx", domain: Domain, node: pending_xref) -> bool: if (domain and domain.name != 'std') or node['reftype'] != 'ref': return None else: target = node['reftarget'] if target not in domain.anonlabels: # type: ignore msg = __('undefined label: %s') else: msg = __('Failed to create a cross reference. A title or caption not found: %s') logger.warning(msg % target, location=node, type='ref', subtype=node['reftype']) return True def setup(app: "Sphinx") -> Dict[str, Any]: app.add_domain(StandardDomain) app.connect('warn-missing-reference', warn_missing_reference) return { 'version': 'builtin', 'env_version': 1, 'parallel_read_safe': True, 'parallel_write_safe': True, }