From cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 19:25:40 +0200 Subject: Adding upstream version 7.2.6. Signed-off-by: Daniel Baumann --- sphinx/roles.py | 433 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 sphinx/roles.py (limited to 'sphinx/roles.py') diff --git a/sphinx/roles.py b/sphinx/roles.py new file mode 100644 index 0000000..d734429 --- /dev/null +++ b/sphinx/roles.py @@ -0,0 +1,433 @@ +"""Handlers for additional ReST roles.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING, Any + +import docutils.parsers.rst.directives +import docutils.parsers.rst.roles +import docutils.parsers.rst.states +from docutils import nodes, utils + +from sphinx import addnodes +from sphinx.locale import _, __ +from sphinx.util import ws_re +from sphinx.util.docutils import ReferenceRole, SphinxRole + +if TYPE_CHECKING: + from collections.abc import Sequence + + from docutils.nodes import Element, Node, TextElement, system_message + + from sphinx.application import Sphinx + from sphinx.environment import BuildEnvironment + from sphinx.util.typing import RoleFunction + + +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: type[Element] = addnodes.pending_xref + innernodeclass: type[TextElement] = nodes.literal + + def __init__(self, fix_parens: bool = False, lowercase: bool = False, + nodeclass: type[Element] | None = None, + innernodeclass: type[TextElement] | None = 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 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, f'{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 + + +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]]: + 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'(\\*{|\\*})') + + +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: 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])) + 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)) + + return result + + +_abbr_re = re.compile(r'\((.*)\)$', re.S) + + +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)], [] + + +# Sphinx provides the `code-block` directive for highlighting code blocks. +# Docutils provides the `code` role which in theory can be used similarly by +# defining a custom role for a given programming language: +# +# .. .. role:: python(code) +# :language: python +# :class: highlight +# +# In practice this does not produce correct highlighting because it uses a +# separate highlighting mechanism that results in the "long" pygments class +# names rather than "short" pygments class names produced by the Sphinx +# `code-block` directive and for which this extension contains CSS rules. +# +# In addition, even if that issue is fixed, because the highlighting +# implementation in docutils, despite being based on pygments, differs from that +# used by Sphinx, the output does not exactly match that produced by the Sphinx +# `code-block` directive. +# +# This issue is noted here: //github.com/sphinx-doc/sphinx/issues/5157 +# +# This overrides the docutils `code` role to perform highlighting in the same +# way as the Sphinx `code-block` directive. +# +# TODO: Change to use `SphinxRole` once SphinxRole is fixed to support options. +def code_role(name: str, rawtext: str, text: str, lineno: int, + inliner: docutils.parsers.rst.states.Inliner, + options: dict | None = None, content: Sequence[str] = (), + ) -> tuple[list[Node], list[system_message]]: + if options is None: + options = {} + options = options.copy() + docutils.parsers.rst.roles.set_classes(options) + language = options.get('language', '') + classes = ['code'] + if language: + classes.append('highlight') + if 'classes' in options: + classes.extend(options['classes']) + + if language and language not in classes: + classes.append(language) + + node = nodes.literal(rawtext, text, classes=classes, language=language) + + return [node], [] + + +code_role.options = { # type: ignore[attr-defined] + 'class': docutils.parsers.rst.directives.class_option, + 'language': docutils.parsers.rst.directives.unchanged, +} + + +specific_docroles: dict[str, RoleFunction] = { + # 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(), +} + + +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) + + # Since docutils registers it as a canonical role, override it as a + # canonical role as well. + roles.register_canonical_role('code', code_role) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } -- cgit v1.2.3