usse/scrape/venv/lib/python3.10/site-packages/sphinx/domains/std.py
2023-12-22 15:26:01 +01:00

1124 lines
45 KiB
Python

"""The standard domain."""
from __future__ import annotations
import re
from copy import copy
from typing import TYPE_CHECKING, Any, Callable, Final, 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.directives import ObjectDescription
from sphinx.domains import Domain, ObjType, TitleGetter
from sphinx.locale import _, __
from sphinx.roles import EmphasizedLiteral, 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
if TYPE_CHECKING:
from collections.abc import Iterable, Iterator
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.environment import BuildEnvironment
from sphinx.util.typing import OptionSpec, RoleFunction
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*:)?\w+)`', re.U)
samp_role = EmphasizedLiteral()
class GenericObject(ObjectDescription[str]):
"""
A generic x-ref directive registered with Sphinx.add_object_type().
"""
indextemplate: str = ''
parse_node: Callable[[BuildEnvironment, str, desc_signature], str] | None = None
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)
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)
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: OptionSpec = {}
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)
self.state.document.note_explicit_target(node)
ret: list[Node] = [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
class Cmdoption(ObjectDescription[str]):
"""
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[-1] == '[' and args[-1] == ']':
# optional value surrounded by brackets (ex. foo[=bar])
optname = optname[:-1]
args = '[' + args
if count:
if self.env.config.option_emphasise_placeholders:
signode += addnodes.desc_sig_punctuation(',', ',')
signode += addnodes.desc_sig_space()
else:
signode += addnodes.desc_addname(', ', ', ')
signode += addnodes.desc_name(optname, optname)
if self.env.config.option_emphasise_placeholders:
add_end_bracket = False
if args:
if args[0] == '[' and args[-1] == ']':
add_end_bracket = True
signode += addnodes.desc_sig_punctuation('[', '[')
args = args[1:-1]
elif args[0] == ' ':
signode += addnodes.desc_sig_space()
args = args.strip()
elif args[0] == '=':
signode += addnodes.desc_sig_punctuation('=', '=')
args = args[1:]
for part in samp_role.parse(args):
if isinstance(part, nodes.Text):
signode += nodes.Text(part.astext())
else:
signode += part
if add_end_bracket:
signode += addnodes.desc_sig_punctuation(']', ']')
else:
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)
self.state.document.note_explicit_target(signode)
domain = self.env.domains['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 signode.get('allnames', []):
entry = '; '.join([descr, option])
self.indexnode['entries'].append(('pair', entry, signode['ids'][0], '', None))
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: OptionSpec = {}
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[str | None]:
# split line into a term and classifiers. if no classifier, None is used..
parts: list[str | None] = 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,
) -> 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)
else:
node_id = make_id(env, document, 'term', termtext)
term['ids'].append(node_id)
document.note_explicit_target(term)
std = cast(StandardDomain, env.get_domain('std'))
std._note_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: OptionSpec = {
'sorted': directives.flag,
}
def run(self) -> list[Node]:
node = addnodes.glossary()
node.document = self.state.document
node['sorted'] = ('sorted' in self.options)
# 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: list[tuple[list[tuple[str, str, int]], StringList]] = []
in_definition = True
in_comment = False
was_empty = True
messages: 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
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: list[nodes.definition_list_item] = []
for terms, definition in entries:
termnodes: list[Node] = []
system_messages: 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], # type: ignore[arg-type]
lineno)
# use first classifier as a index key
term = make_glossary_term(self.env, textnodes,
parts[1], source, lineno, # type: ignore[arg-type]
node_id=None, document=self.state.document)
term.rawsource = line
system_messages.extend(sysmsg)
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(nodes.definition_list_item('', *termnodes))
dlist = nodes.definition_list('', *items)
dlist['classes'].append('glossary')
node += dlist
return messages + [node]
def token_xrefs(text: str, productionGroup: str = '') -> list[Node]:
if len(productionGroup) != 0:
productionGroup += ':'
retnodes: 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))
token = m.group(1)
if ':' in token:
if token[0] == '~':
_, title = token.split(':')
target = token[1:]
elif token[0] == ':':
title = token[1:]
target = title
else:
title = token
target = token
else:
title = token
target = productionGroup + token
refnode = pending_xref(title, reftype='token', refdomain='std',
reftarget=target)
refnode += nodes.literal(token, title, classes=['xref'])
retnodes.append(refnode)
pos = m.end()
if pos < len(text):
retnodes.append(nodes.Text(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: OptionSpec = {}
def run(self) -> list[Node]:
domain = cast(StandardDomain, self.env.get_domain('std'))
node: Element = addnodes.productionlist()
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 = ""
first_rule_seen = False
for rule in lines:
if not first_rule_seen and ':' not in rule:
productionGroup = rule.strip()
continue
first_rule_seen = True
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)
self.state.document.note_implicit_target(subnode, subnode)
if len(productionGroup) != 0:
objName = f"{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]
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: dict[str, ObjType] = {
'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),
}
directives: dict[str, type[Directive]] = {
'program': Program,
'cmdoption': Cmdoption, # old name for backwards compatibility
'option': Cmdoption,
'envvar': EnvVar,
'glossary': Glossary,
'productionlist': ProductionList,
}
roles: dict[str, RoleFunction | XRefRole] = {
'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),
}
initial_data: Final = { # type: ignore[misc]
'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', ''),
},
}
_virtual_doc_names: dict[str, tuple[str, str]] = { # labelname -> docname, sectionname
'genindex': ('genindex', _('Index')),
'modindex': ('py-modindex', _('Module Index')),
'search': ('search', _('Search Page')),
}
dangling_warnings = {
'term': 'term not in glossary: %(target)r',
'numref': 'undefined label: %(target)r',
'keyword': 'unknown keyword: %(target)r',
'doc': 'unknown document: %(target)r',
'option': 'unknown option: %(target)r',
}
# node_class -> (figtype, title_getter)
enumerable_nodes: dict[type[Node], tuple[str, TitleGetter | None]] = {
nodes.figure: ('figure', None),
nodes.table: ('table', None),
nodes.container: ('code-block', None),
}
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)
@property
def _terms(self) -> dict[str, tuple[str, str]]:
""".. note:: Will be removed soon. internal use only."""
return self.data.setdefault('terms', {}) # (name) -> docname, labelid
def _note_term(self, term: str, labelid: str, location: Any = None) -> None:
"""Note a term for cross reference.
.. note:: Will be removed soon. internal use only.
"""
self.note_object('term', term, labelid, location)
self._terms[term.lower()] = (self.env.docname, labelid)
@property
def progoptions(self) -> dict[tuple[str | None, 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: Any = None
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) in list(self._terms.items()):
if fn == docname:
del self._terms[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[str, Any]) -> 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['terms'].items():
if data[0] in docnames:
self._terms[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:
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']) # type: ignore[assignment]
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 == 'section':
title = cast(nodes.title, node[0])
sectname = clean_astext(title)
elif node.tagname == 'rubric':
sectname = clean_astext(node)
elif self.is_enumerable_node(node):
sectname = self.get_numfig_title(node) or ''
if not sectname:
continue
else:
if (isinstance(node, (nodes.definition_list,
nodes.field_list)) and
node.children):
node = cast(nodes.Element, node.children[0])
if isinstance(node, (nodes.field, nodes.definition_list_item)):
node = cast(nodes.Element, node.children[0])
if isinstance(node, (nodes.term, nodes.field_name)):
sectname = clean_astext(node)
else:
toctree = next(node.findall(addnodes.toctree), None)
if toctree and toctree.get('caption'):
sectname = toctree['caption']
else:
# anonymous-only labels
continue
self.labels[name] = docname, labelid, sectname
def add_program_option(self, program: str | None, name: str,
docname: str, labelid: str) -> None:
# prefer first command option entry
if (program, name) not in self.progoptions:
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 | None:
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 == '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 | None:
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 | None:
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)
assert target_node is not None
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 | None:
# 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 | None:
# 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 | None:
progname = node.get('std:program')
target = target.strip()
docname, labelid = self.progoptions.get((progname, target), ('', ''))
if not docname:
# Support also reference that contain an option value:
# * :option:`-foo=bar`
# * :option:`-foo[=bar]`
# * :option:`-foo bar`
for needle in {'=', '[=', ' '}:
if needle in target:
stem, _, _ = target.partition(needle)
docname, labelid = self.progoptions.get((progname, stem), ('', ''))
if docname:
break
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 | None:
result = self._resolve_obj_xref(env, fromdocname, builder, typ,
target, node, contnode)
if result:
return result
else:
# fallback to case insensitive match
if target.lower() in self._terms:
docname, labelid = self._terms[target.lower()]
return make_refnode(builder, fromdocname, docname, labelid, contnode)
else:
return None
def _resolve_obj_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) 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: 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]
role = 'std:' + self.role_for_objtype(objtype) # type: ignore[operator]
results.append((role, 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 | None:
"""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 | None:
"""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, ...] | None:
if figtype == 'section':
if builder.name == 'latex':
return ()
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 | None:
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 warn_missing_reference(app: Sphinx, domain: Domain, node: pending_xref,
) -> bool | None:
if (domain and domain.name != 'std') or node['reftype'] != 'ref':
return None
else:
target = node['reftarget']
if target not in domain.anonlabels: # type: ignore[attr-defined]
msg = __('undefined label: %r')
else:
msg = __('Failed to create a cross reference. A title or caption not found: %r')
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': 2,
'parallel_read_safe': True,
'parallel_write_safe': True,
}