""" sphinx.roles ~~~~~~~~~~~~ Handlers for additional ReST roles. :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re import warnings from typing import Any, Dict, List, Tuple from docutils import nodes, utils from docutils.nodes import Element, Node, TextElement, system_message from docutils.parsers.rst.states import Inliner from sphinx import addnodes from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.locale import _ from sphinx.util import ws_re from sphinx.util.docutils import ReferenceRole, SphinxRole from sphinx.util.nodes import process_index_entry, set_role_source_info, split_explicit_title 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.environment import BuildEnvironment generic_docroles = { 'command': addnodes.literal_strong, 'dfn': nodes.emphasis, 'kbd': nodes.literal, 'mailheader': addnodes.literal_emphasis, 'makevar': addnodes.literal_strong, 'manpage': addnodes.manpage, 'mimetype': addnodes.literal_emphasis, 'newsgroup': addnodes.literal_emphasis, 'program': addnodes.literal_strong, # XXX should be an x-ref 'regexp': nodes.literal, } # -- generic cross-reference role ---------------------------------------------- class XRefRole(ReferenceRole): """ A generic cross-referencing role. To create a callable that can be used as a role function, create an instance of this class. The general features of this role are: * Automatic creation of a reference and a content node. * Optional separation of title and target with `title `. * The implementation is a class rather than a function to make customization easier. Customization can be done in two ways: * Supplying constructor parameters: * `fix_parens` to normalize parentheses (strip from target, and add to title if configured) * `lowercase` to lowercase the target * `nodeclass` and `innernodeclass` select the node classes for the reference and the content node * Subclassing and overwriting `process_link()` and/or `result_nodes()`. """ nodeclass = addnodes.pending_xref # type: Type[Element] innernodeclass = nodes.literal # type: Type[TextElement] def __init__(self, fix_parens: bool = False, lowercase: bool = False, nodeclass: "Type[Element]" = None, innernodeclass: "Type[TextElement]" = None, warn_dangling: bool = False) -> None: self.fix_parens = fix_parens self.lowercase = lowercase self.warn_dangling = warn_dangling if nodeclass is not None: self.nodeclass = nodeclass if innernodeclass is not None: self.innernodeclass = innernodeclass super().__init__() def _fix_parens(self, env: "BuildEnvironment", has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]: warnings.warn('XRefRole._fix_parens() is deprecated.', RemovedInSphinx40Warning, stacklevel=2) if not has_explicit_title: if title.endswith('()'): # remove parentheses title = title[:-2] if env.config.add_function_parentheses: # add them back to all occurrences if configured title += '()' # remove parentheses from the target too if target.endswith('()'): target = target[:-2] return title, target def update_title_and_target(self, title: str, target: str) -> Tuple[str, str]: if not self.has_explicit_title: if title.endswith('()'): # remove parentheses title = title[:-2] if self.config.add_function_parentheses: # add them back to all occurrences if configured title += '()' # remove parentheses from the target too if target.endswith('()'): target = target[:-2] return title, target def run(self) -> Tuple[List[Node], List[system_message]]: if ':' not in self.name: self.refdomain, self.reftype = '', self.name self.classes = ['xref', self.reftype] else: self.refdomain, self.reftype = self.name.split(':', 1) self.classes = ['xref', self.refdomain, '%s-%s' % (self.refdomain, self.reftype)] if self.disabled: return self.create_non_xref_node() else: return self.create_xref_node() def create_non_xref_node(self) -> Tuple[List[Node], List[system_message]]: text = utils.unescape(self.text[1:]) if self.fix_parens: self.has_explicit_title = False # treat as implicit text, target = self.update_title_and_target(text, "") node = self.innernodeclass(self.rawtext, text, classes=self.classes) return self.result_nodes(self.inliner.document, self.env, node, is_ref=False) def create_xref_node(self) -> Tuple[List[Node], List[system_message]]: target = self.target title = self.title if self.lowercase: target = target.lower() if self.fix_parens: title, target = self.update_title_and_target(title, target) # create the reference node options = {'refdoc': self.env.docname, 'refdomain': self.refdomain, 'reftype': self.reftype, 'refexplicit': self.has_explicit_title, 'refwarn': self.warn_dangling} refnode = self.nodeclass(self.rawtext, **options) self.set_source_info(refnode) # determine the target and title for the class title, target = self.process_link(self.env, refnode, self.has_explicit_title, title, target) refnode['reftarget'] = target refnode += self.innernodeclass(self.rawtext, title, classes=self.classes) return self.result_nodes(self.inliner.document, self.env, refnode, is_ref=True) # methods that can be overwritten def process_link(self, env: "BuildEnvironment", refnode: Element, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]: """Called after parsing title and target text, and creating the reference node (given in *refnode*). This method can alter the reference node and must return a new (or the same) ``(title, target)`` tuple. """ return title, ws_re.sub(' ', target) def result_nodes(self, document: nodes.document, env: "BuildEnvironment", node: Element, is_ref: bool) -> Tuple[List[Node], List[system_message]]: """Called before returning the finished nodes. *node* is the reference node if one was created (*is_ref* is then true), else the content node. This method can add other nodes and must return a ``(nodes, messages)`` tuple (the usual return value of a role function). """ return [node], [] class AnyXRefRole(XRefRole): def process_link(self, env: "BuildEnvironment", refnode: Element, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]: result = super().process_link(env, refnode, has_explicit_title, title, target) # add all possible context info (i.e. std:program, py:module etc.) refnode.attributes.update(env.ref_context) return result def indexmarkup_role(typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: Dict = {}, content: List[str] = [] ) -> Tuple[List[Node], List[system_message]]: """Role for PEP/RFC references that generate an index entry.""" warnings.warn('indexmarkup_role() is deprecated. Please use PEP or RFC class instead.', RemovedInSphinx40Warning, stacklevel=2) env = inliner.document.settings.env if not typ: assert env.temp_data['default_role'] typ = env.temp_data['default_role'].lower() else: typ = typ.lower() has_explicit_title, title, target = split_explicit_title(text) title = utils.unescape(title) target = utils.unescape(target) targetid = 'index-%s' % env.new_serialno('index') indexnode = addnodes.index() targetnode = nodes.target('', '', ids=[targetid]) inliner.document.note_explicit_target(targetnode) if typ == 'pep': indexnode['entries'] = [ ('single', _('Python Enhancement Proposals; PEP %s') % target, targetid, '', None)] anchor = '' anchorindex = target.find('#') if anchorindex > 0: target, anchor = target[:anchorindex], target[anchorindex:] if not has_explicit_title: title = "PEP " + utils.unescape(title) try: pepnum = int(target) except ValueError: msg = inliner.reporter.error('invalid PEP number %s' % target, line=lineno) prb = inliner.problematic(rawtext, rawtext, msg) return [prb], [msg] ref = inliner.document.settings.pep_base_url + 'pep-%04d' % pepnum sn = nodes.strong(title, title) rn = nodes.reference('', '', internal=False, refuri=ref + anchor, classes=[typ]) rn += sn return [indexnode, targetnode, rn], [] elif typ == 'rfc': indexnode['entries'] = [ ('single', 'RFC; RFC %s' % target, targetid, '', None)] anchor = '' anchorindex = target.find('#') if anchorindex > 0: target, anchor = target[:anchorindex], target[anchorindex:] if not has_explicit_title: title = "RFC " + utils.unescape(title) try: rfcnum = int(target) except ValueError: msg = inliner.reporter.error('invalid RFC number %s' % target, line=lineno) prb = inliner.problematic(rawtext, rawtext, msg) return [prb], [msg] ref = inliner.document.settings.rfc_base_url + inliner.rfc_url % rfcnum sn = nodes.strong(title, title) rn = nodes.reference('', '', internal=False, refuri=ref + anchor, classes=[typ]) rn += sn return [indexnode, targetnode, rn], [] else: raise ValueError('unknown role type: %s' % typ) class PEP(ReferenceRole): def run(self) -> Tuple[List[Node], List[system_message]]: target_id = 'index-%s' % self.env.new_serialno('index') entries = [('single', _('Python Enhancement Proposals; PEP %s') % self.target, target_id, '', None)] index = addnodes.index(entries=entries) target = nodes.target('', '', ids=[target_id]) self.inliner.document.note_explicit_target(target) try: refuri = self.build_uri() reference = nodes.reference('', '', internal=False, refuri=refuri, classes=['pep']) if self.has_explicit_title: reference += nodes.strong(self.title, self.title) else: title = "PEP " + self.title reference += nodes.strong(title, title) except ValueError: msg = self.inliner.reporter.error('invalid PEP number %s' % self.target, line=self.lineno) prb = self.inliner.problematic(self.rawtext, self.rawtext, msg) return [prb], [msg] return [index, target, reference], [] def build_uri(self) -> str: base_url = self.inliner.document.settings.pep_base_url ret = self.target.split('#', 1) if len(ret) == 2: return base_url + 'pep-%04d#%s' % (int(ret[0]), ret[1]) else: return base_url + 'pep-%04d' % int(ret[0]) class RFC(ReferenceRole): def run(self) -> Tuple[List[Node], List[system_message]]: target_id = 'index-%s' % self.env.new_serialno('index') entries = [('single', 'RFC; RFC %s' % self.target, target_id, '', None)] index = addnodes.index(entries=entries) target = nodes.target('', '', ids=[target_id]) self.inliner.document.note_explicit_target(target) try: refuri = self.build_uri() reference = nodes.reference('', '', internal=False, refuri=refuri, classes=['rfc']) if self.has_explicit_title: reference += nodes.strong(self.title, self.title) else: title = "RFC " + self.title reference += nodes.strong(title, title) except ValueError: msg = self.inliner.reporter.error('invalid RFC number %s' % self.target, line=self.lineno) prb = self.inliner.problematic(self.rawtext, self.rawtext, msg) return [prb], [msg] return [index, target, reference], [] def build_uri(self) -> str: base_url = self.inliner.document.settings.rfc_base_url ret = self.target.split('#', 1) if len(ret) == 2: return base_url + self.inliner.rfc_url % int(ret[0]) + '#' + ret[1] else: return base_url + self.inliner.rfc_url % int(ret[0]) _amp_re = re.compile(r'(? Tuple[List[Node], List[system_message]]: warnings.warn('menusel_role() is deprecated. ' 'Please use MenuSelection or GUILabel class instead.', RemovedInSphinx40Warning, stacklevel=2) env = inliner.document.settings.env if not typ: assert env.temp_data['default_role'] typ = env.temp_data['default_role'].lower() else: typ = typ.lower() text = utils.unescape(text) if typ == 'menuselection': text = text.replace('-->', '\N{TRIANGULAR BULLET}') spans = _amp_re.split(text) node = nodes.inline(rawtext=rawtext) for i, span in enumerate(spans): span = span.replace('&&', '&') if i == 0: if len(span) > 0: textnode = nodes.Text(span) node += textnode continue accel_node = nodes.inline() letter_node = nodes.Text(span[0]) accel_node += letter_node accel_node['classes'].append('accelerator') node += accel_node textnode = nodes.Text(span[1:]) node += textnode node['classes'].append(typ) return [node], [] class GUILabel(SphinxRole): amp_re = re.compile(r'(? Tuple[List[Node], List[system_message]]: node = nodes.inline(rawtext=self.rawtext, classes=[self.name]) spans = self.amp_re.split(self.text) node += nodes.Text(spans.pop(0)) for span in spans: span = span.replace('&&', '&') letter = nodes.Text(span[0]) accelerator = nodes.inline('', '', letter, classes=['accelerator']) node += accelerator node += nodes.Text(span[1:]) return [node], [] class MenuSelection(GUILabel): BULLET_CHARACTER = '\N{TRIANGULAR BULLET}' def run(self) -> Tuple[List[Node], List[system_message]]: self.text = self.text.replace('-->', self.BULLET_CHARACTER) return super().run() _litvar_re = re.compile('{([^}]+)}') parens_re = re.compile(r'(\\*{|\\*})') def emph_literal_role(typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: Dict = {}, content: List[str] = [] ) -> Tuple[List[Node], List[system_message]]: warnings.warn('emph_literal_role() is deprecated. ' 'Please use EmphasizedLiteral class instead.', RemovedInSphinx40Warning, stacklevel=2) env = inliner.document.settings.env if not typ: assert env.temp_data['default_role'] typ = env.temp_data['default_role'].lower() else: typ = typ.lower() retnode = nodes.literal(role=typ.lower(), classes=[typ]) parts = list(parens_re.split(utils.unescape(text))) stack = [''] for part in parts: matched = parens_re.match(part) if matched: backslashes = len(part) - 1 if backslashes % 2 == 1: # escaped stack[-1] += "\\" * int((backslashes - 1) / 2) + part[-1] elif part[-1] == '{': # rparen stack[-1] += "\\" * int(backslashes / 2) if len(stack) >= 2 and stack[-2] == "{": # nested stack[-1] += "{" else: # start emphasis stack.append('{') stack.append('') else: # lparen stack[-1] += "\\" * int(backslashes / 2) if len(stack) == 3 and stack[1] == "{" and len(stack[2]) > 0: # emphasized word found if stack[0]: retnode += nodes.Text(stack[0], stack[0]) retnode += nodes.emphasis(stack[2], stack[2]) stack = [''] else: # emphasized word not found; the rparen is not a special symbol stack.append('}') stack = [''.join(stack)] else: stack[-1] += part if ''.join(stack): # remaining is treated as Text text = ''.join(stack) retnode += nodes.Text(text, text) return [retnode], [] class EmphasizedLiteral(SphinxRole): parens_re = re.compile(r'(\\\\|\\{|\\}|{|})') def run(self) -> Tuple[List[Node], List[system_message]]: children = self.parse(self.text) node = nodes.literal(self.rawtext, '', *children, role=self.name.lower(), classes=[self.name]) return [node], [] def parse(self, text: str) -> List[Node]: result = [] # type: List[Node] stack = [''] for part in self.parens_re.split(text): if part == '\\\\': # escaped backslash stack[-1] += '\\' elif part == '{': if len(stack) >= 2 and stack[-2] == "{": # nested stack[-1] += "{" else: # start emphasis stack.append('{') stack.append('') elif part == '}': if len(stack) == 3 and stack[1] == "{" and len(stack[2]) > 0: # emphasized word found if stack[0]: result.append(nodes.Text(stack[0], stack[0])) result.append(nodes.emphasis(stack[2], stack[2])) stack = [''] else: # emphasized word not found; the rparen is not a special symbol stack.append('}') stack = [''.join(stack)] elif part == '\\{': # escaped left-brace stack[-1] += '{' elif part == '\\}': # escaped right-brace stack[-1] += '}' else: # others (containing escaped braces) stack[-1] += part if ''.join(stack): # remaining is treated as Text text = ''.join(stack) result.append(nodes.Text(text, text)) return result _abbr_re = re.compile(r'\((.*)\)$', re.S) def abbr_role(typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: Dict = {}, content: List[str] = [] ) -> Tuple[List[Node], List[system_message]]: warnings.warn('abbr_role() is deprecated. Please use Abbrevation class instead.', RemovedInSphinx40Warning, stacklevel=2) text = utils.unescape(text) m = _abbr_re.search(text) if m is None: return [nodes.abbreviation(text, text, **options)], [] abbr = text[:m.start()].strip() expl = m.group(1) options = options.copy() options['explanation'] = expl return [nodes.abbreviation(abbr, abbr, **options)], [] class Abbreviation(SphinxRole): abbr_re = re.compile(r'\((.*)\)$', re.S) def run(self) -> Tuple[List[Node], List[system_message]]: options = self.options.copy() matched = self.abbr_re.search(self.text) if matched: text = self.text[:matched.start()].strip() options['explanation'] = matched.group(1) else: text = self.text return [nodes.abbreviation(self.rawtext, text, **options)], [] def index_role(typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: Dict = {}, content: List[str] = [] ) -> Tuple[List[Node], List[system_message]]: warnings.warn('index_role() is deprecated. Please use Index class instead.', RemovedInSphinx40Warning, stacklevel=2) # create new reference target env = inliner.document.settings.env targetid = 'index-%s' % env.new_serialno('index') targetnode = nodes.target('', '', ids=[targetid]) # split text and target in role content has_explicit_title, title, target = split_explicit_title(text) title = utils.unescape(title) target = utils.unescape(target) # if an explicit target is given, we can process it as a full entry if has_explicit_title: entries = process_index_entry(target, targetid) # otherwise we just create a "single" entry else: # but allow giving main entry main = '' if target.startswith('!'): target = target[1:] title = title[1:] main = 'main' entries = [('single', target, targetid, main, None)] indexnode = addnodes.index() indexnode['entries'] = entries set_role_source_info(inliner, lineno, indexnode) textnode = nodes.Text(title, title) return [indexnode, targetnode, textnode], [] class Index(ReferenceRole): def run(self) -> Tuple[List[Node], List[system_message]]: warnings.warn('Index role is deprecated.', RemovedInSphinx40Warning, stacklevel=2) target_id = 'index-%s' % self.env.new_serialno('index') if self.has_explicit_title: # if an explicit target is given, process it as a full entry title = self.title entries = process_index_entry(self.target, target_id) else: # otherwise we just create a single entry if self.target.startswith('!'): title = self.title[1:] entries = [('single', self.target[1:], target_id, 'main', None)] else: title = self.title entries = [('single', self.target, target_id, '', None)] index = addnodes.index(entries=entries) target = nodes.target('', '', ids=[target_id]) text = nodes.Text(title, title) self.set_source_info(index) return [index, target, text], [] specific_docroles = { # links to download references 'download': XRefRole(nodeclass=addnodes.download_reference), # links to anything 'any': AnyXRefRole(warn_dangling=True), 'pep': PEP(), 'rfc': RFC(), 'guilabel': GUILabel(), 'menuselection': MenuSelection(), 'file': EmphasizedLiteral(), 'samp': EmphasizedLiteral(), 'abbr': Abbreviation(), } # type: Dict[str, RoleFunction] def setup(app: "Sphinx") -> Dict[str, Any]: from docutils.parsers.rst import roles for rolename, nodeclass in generic_docroles.items(): generic = roles.GenericRole(rolename, nodeclass) role = roles.CustomRole(rolename, generic, {'classes': [rolename]}) roles.register_local_role(rolename, role) for rolename, func in specific_docroles.items(): roles.register_local_role(rolename, func) return { 'version': 'builtin', 'parallel_read_safe': True, 'parallel_write_safe': True, }