mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
1017 lines
41 KiB
Python
1017 lines
41 KiB
Python
"""
|
|
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
|
|
from typing import 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 ws_re, logging, docname_join
|
|
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:
|
|
targetname = '%s-%s' % (self.objtype, name)
|
|
signode['ids'].append(targetname)
|
|
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,
|
|
targetname, '', None))
|
|
|
|
std = cast(StandardDomain, self.env.get_domain('std'))
|
|
std.note_object(self.objtype, name, targetname,
|
|
location=(self.env.docname, self.lineno))
|
|
|
|
|
|
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())
|
|
targetname = '%s-%s' % (self.name, fullname)
|
|
node = nodes.target('', '', ids=[targetname])
|
|
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,
|
|
targetname, '', 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, targetname, location=(self.env.docname, self.lineno))
|
|
|
|
return ret
|
|
|
|
|
|
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=(self.env.docname, self.lineno))
|
|
continue
|
|
optname, args = m.groups()
|
|
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', []):
|
|
targetname = optname.replace('/', '-')
|
|
if not targetname.startswith('-'):
|
|
targetname = '-arg-' + targetname
|
|
if currprogram:
|
|
targetname = '-' + currprogram + targetname
|
|
targetname = 'cmdoption' + targetname
|
|
signode['names'].append(targetname)
|
|
|
|
domain = cast(StandardDomain, self.env.get_domain('std'))
|
|
self.state.document.note_explicit_target(signode)
|
|
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))
|
|
|
|
|
|
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)
|
|
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.lower(), node_id, location=(env.docname, lineno))
|
|
|
|
# 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
|
|
# 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)
|
|
subnode['tokenname'] = name.strip()
|
|
if subnode['tokenname']:
|
|
# nodes.make_id converts '_' to '-',
|
|
# so we can use '_' to delimit group from name,
|
|
# and make sure we don't clash with other IDs.
|
|
idname = 'grammar-token-%s_%s' \
|
|
% (nodes.make_id(productionGroup), nodes.make_id(name))
|
|
if idname not in self.state.document.ids:
|
|
subnode['ids'].append(idname)
|
|
|
|
idnameOld = nodes.make_id('grammar-token-' + name)
|
|
if idnameOld not in self.state.document.ids:
|
|
subnode['ids'].append(idnameOld)
|
|
self.state.document.note_implicit_target(subnode, subnode)
|
|
if len(productionGroup) != 0:
|
|
objName = "%s:%s" % (productionGroup, name)
|
|
else:
|
|
objName = name
|
|
domain.note_object(objtype='token', name=objName, labelid=idname,
|
|
location=(self.env.docname, self.lineno))
|
|
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 = {
|
|
'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(lowercase=True, 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',
|
|
'ref': 'undefined label: %(target)s (if the link has no caption '
|
|
'the label must precede a section header)',
|
|
'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
|
|
|
|
@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)
|
|
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)
|
|
domain = env.get_domain('citation')
|
|
return domain.resolve_xref(env, fromdocname, builder, typ, target, node, contnode)
|
|
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(__("no number is assigned for %s: %s"), figtype, 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_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):
|
|
if node.get('literal_block') and has_child(node, nodes.literal_block):
|
|
return 'code-block'
|
|
else:
|
|
return None
|
|
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):
|
|
# target_node is found, but fignumber is not assigned.
|
|
# Maybe it is defined in orphaned document.
|
|
raise ValueError
|
|
|
|
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)
|
|
|
|
def note_citation_refs(self, env: "BuildEnvironment", docname: str, document: nodes.document) -> None: # NOQA
|
|
warnings.warn('StandardDomain.note_citation_refs() is deprecated.',
|
|
RemovedInSphinx40Warning)
|
|
|
|
def note_labels(self, env: "BuildEnvironment", docname: str, document: nodes.document) -> None: # NOQA
|
|
warnings.warn('StandardDomain.note_labels() is deprecated.',
|
|
RemovedInSphinx40Warning)
|
|
|
|
|
|
def setup(app: "Sphinx") -> Dict[str, Any]:
|
|
app.add_domain(StandardDomain)
|
|
|
|
return {
|
|
'version': 'builtin',
|
|
'env_version': 1,
|
|
'parallel_read_safe': True,
|
|
'parallel_write_safe': True,
|
|
}
|