summaryrefslogtreecommitdiffstats
path: root/sphinx/roles.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/roles.py')
-rw-r--r--sphinx/roles.py433
1 files changed, 433 insertions, 0 deletions
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 <target>`.
+ * 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'(?<!&)&(?![&\s])')
+
+
+class GUILabel(SphinxRole):
+ amp_re = re.compile(r'(?<!&)&(?![&\s])')
+
+ def run(self) -> 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,
+ }