mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Implement key splitting in the `:kbd: role and remove KeyboardTransform` (#13227)
``KeyboardTransform.run`` takes 1.2% of the runtime of a build, but is not be needed if ``:kbd:`` is converted to a proper role. We implement key splitting in the ``:kbd:`` role, and return a sequence of parsed nodes rather than a nested collection of roles with ``class="kbd compound"``.
This commit is contained in:
@@ -15,6 +15,12 @@ Incompatible changes
|
||||
now unconditionally returns ``True``.
|
||||
These are replaced by the ``has_maths_elements`` key of the page context dict.
|
||||
Patch by Adam Turner.
|
||||
* #13227: HTML output for sequences of keys in the :rst:role:`kbd` role
|
||||
no longer uses a ``<kbd class="kbd compound">`` element to wrap
|
||||
the keys and separators, but places them directly in the relevant parent node.
|
||||
This means that CSS rulesets targeting ``kbd.compound`` or ``.kbd.compound``
|
||||
will no longer have any effect.
|
||||
Patch by Adam Turner.
|
||||
|
||||
Deprecated
|
||||
----------
|
||||
@@ -36,6 +42,8 @@ Features added
|
||||
* #13146: Napoleon: Unify the type preprocessing logic to allow
|
||||
Google-style docstrings to use the optional and default keywords.
|
||||
Patch by Chris Barrick.
|
||||
* #13227: Implement the :rst:role:`kbd` role as a ``SphinxRole``.
|
||||
Patch by Adam Turner.
|
||||
|
||||
Bugs fixed
|
||||
----------
|
||||
|
||||
@@ -1510,9 +1510,6 @@ def setup(app: Sphinx) -> ExtensionMetadata:
|
||||
# load default math renderer
|
||||
app.setup_extension('sphinx.ext.mathjax')
|
||||
|
||||
# load transforms for HTML builder
|
||||
app.setup_extension('sphinx.builders.html.transforms')
|
||||
|
||||
return {
|
||||
'version': 'builtin',
|
||||
'parallel_read_safe': True,
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
"""Transforms for HTML builder."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from docutils import nodes
|
||||
|
||||
from sphinx.transforms.post_transforms import SphinxPostTransform
|
||||
from sphinx.util.nodes import NodeMatcher
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.util.typing import ExtensionMetadata
|
||||
|
||||
|
||||
class KeyboardTransform(SphinxPostTransform):
|
||||
"""Transform :kbd: role to more detailed form.
|
||||
|
||||
Before::
|
||||
|
||||
<literal class="kbd">
|
||||
Control-x
|
||||
|
||||
After::
|
||||
|
||||
<literal class="kbd compound">
|
||||
<literal class="kbd">
|
||||
Control
|
||||
-
|
||||
<literal class="kbd">
|
||||
x
|
||||
"""
|
||||
|
||||
default_priority = 400
|
||||
formats = ('html',)
|
||||
pattern = re.compile(r'(?<=.)(-|\+|\^|\s+)(?=.)')
|
||||
multiwords_keys = (
|
||||
('caps', 'lock'),
|
||||
('page', 'down'),
|
||||
('page', 'up'),
|
||||
('scroll', 'lock'),
|
||||
('num', 'lock'),
|
||||
('sys', 'rq'),
|
||||
('back', 'space'),
|
||||
)
|
||||
|
||||
def run(self, **kwargs: Any) -> None:
|
||||
matcher = NodeMatcher(nodes.literal, classes=['kbd'])
|
||||
# this list must be pre-created as during iteration new nodes
|
||||
# are added which match the condition in the NodeMatcher.
|
||||
for node in list(matcher.findall(self.document)):
|
||||
parts = self.pattern.split(node[-1].astext())
|
||||
if len(parts) == 1 or self.is_multiwords_key(parts):
|
||||
continue
|
||||
|
||||
node['classes'].append('compound')
|
||||
node.pop()
|
||||
while parts:
|
||||
if self.is_multiwords_key(parts):
|
||||
key = ''.join(parts[:3])
|
||||
parts[:3] = []
|
||||
else:
|
||||
key = parts.pop(0)
|
||||
node += nodes.literal('', key, classes=['kbd'])
|
||||
|
||||
try:
|
||||
# key separator (ex. -, +, ^)
|
||||
sep = parts.pop(0)
|
||||
node += nodes.Text(sep)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def is_multiwords_key(self, parts: list[str]) -> bool:
|
||||
if len(parts) >= 3 and not parts[1].strip():
|
||||
name = parts[0].lower(), parts[2].lower()
|
||||
return name in self.multiwords_keys
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> ExtensionMetadata:
|
||||
app.add_post_transform(KeyboardTransform)
|
||||
|
||||
return {
|
||||
'version': 'builtin',
|
||||
'parallel_read_safe': True,
|
||||
'parallel_write_safe': True,
|
||||
}
|
||||
@@ -29,7 +29,6 @@ if TYPE_CHECKING:
|
||||
generic_docroles = {
|
||||
'command': addnodes.literal_strong,
|
||||
'dfn': nodes.emphasis,
|
||||
'kbd': nodes.literal,
|
||||
'mailheader': addnodes.literal_emphasis,
|
||||
'makevar': addnodes.literal_strong,
|
||||
'mimetype': addnodes.literal_emphasis,
|
||||
@@ -479,6 +478,59 @@ class Abbreviation(SphinxRole):
|
||||
return [nodes.abbreviation(self.rawtext, text, **options)], []
|
||||
|
||||
|
||||
class Keyboard(SphinxRole):
|
||||
"""Implement the :kbd: role.
|
||||
|
||||
Split words in the text by separator or whitespace,
|
||||
but keep multi-word keys together.
|
||||
"""
|
||||
|
||||
# capture ('-', '+', '^', or whitespace) in between any two characters
|
||||
_pattern: Final = re.compile(r'(?<=.)([\-+^]| +)(?=.)')
|
||||
|
||||
def run(self) -> tuple[list[Node], list[system_message]]:
|
||||
classes = ['kbd']
|
||||
if 'classes' in self.options:
|
||||
classes.extend(self.options['classes'])
|
||||
|
||||
parts = self._pattern.split(self.text)
|
||||
if len(parts) == 1 or self._is_multi_word_key(parts):
|
||||
return [nodes.literal(self.rawtext, self.text, classes=classes)], []
|
||||
|
||||
compound: list[Node] = []
|
||||
while parts:
|
||||
if self._is_multi_word_key(parts):
|
||||
key = ''.join(parts[:3])
|
||||
parts[:3] = []
|
||||
else:
|
||||
key = parts.pop(0)
|
||||
compound.append(nodes.literal(key, key, classes=classes))
|
||||
|
||||
try:
|
||||
sep = parts.pop(0) # key separator ('-', '+', '^', etc)
|
||||
except IndexError:
|
||||
break
|
||||
else:
|
||||
compound.append(nodes.Text(sep))
|
||||
|
||||
return compound, []
|
||||
|
||||
@staticmethod
|
||||
def _is_multi_word_key(parts: list[str]) -> bool:
|
||||
if len(parts) <= 2 or not parts[1].isspace():
|
||||
return False
|
||||
name = parts[0].lower(), parts[2].lower()
|
||||
return name in frozenset({
|
||||
('back', 'space'),
|
||||
('caps', 'lock'),
|
||||
('num', 'lock'),
|
||||
('page', 'down'),
|
||||
('page', 'up'),
|
||||
('scroll', 'lock'),
|
||||
('sys', 'rq'),
|
||||
})
|
||||
|
||||
|
||||
class Manpage(ReferenceRole):
|
||||
_manpage_re = re.compile(r'^(?P<path>(?P<page>.+)[(.](?P<section>[1-9]\w*)?\)?)$')
|
||||
|
||||
@@ -576,6 +628,7 @@ specific_docroles: dict[str, RoleFunction] = {
|
||||
'samp': EmphasizedLiteral(),
|
||||
# other
|
||||
'abbr': Abbreviation(),
|
||||
'kbd': Keyboard(),
|
||||
'manpage': Manpage(),
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ from docutils import frontend, nodes, utils
|
||||
from docutils.parsers.rst import Parser as RstParser
|
||||
|
||||
from sphinx import addnodes
|
||||
from sphinx.builders.html.transforms import KeyboardTransform
|
||||
from sphinx.builders.latex import LaTeXBuilder
|
||||
from sphinx.environment import default_settings
|
||||
from sphinx.roles import XRefRole
|
||||
@@ -100,7 +99,6 @@ class ForgivingLaTeXTranslator(LaTeXTranslator, ForgivingTranslator):
|
||||
def verify_re_html(app, parse):
|
||||
def verify(rst, html_expected):
|
||||
document = parse(rst)
|
||||
KeyboardTransform(document).apply()
|
||||
html_translator = ForgivingHTMLTranslator(document, app.builder)
|
||||
document.walkabout(html_translator)
|
||||
html_translated = ''.join(html_translator.fragment).strip()
|
||||
@@ -357,28 +355,35 @@ def get_verifier(verify, verify_re):
|
||||
'verify',
|
||||
':kbd:`Control+X`',
|
||||
(
|
||||
'<p><kbd class="kbd compound docutils literal notranslate">'
|
||||
'<p>'
|
||||
'<kbd class="kbd docutils literal notranslate">Control</kbd>'
|
||||
'+'
|
||||
'<kbd class="kbd docutils literal notranslate">X</kbd>'
|
||||
'</kbd></p>'
|
||||
'</p>'
|
||||
),
|
||||
(
|
||||
'\\sphinxAtStartPar\n'
|
||||
'\\sphinxkeyboard{\\sphinxupquote{Control}}'
|
||||
'+'
|
||||
'\\sphinxkeyboard{\\sphinxupquote{X}}'
|
||||
),
|
||||
'\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{Control+X}}',
|
||||
),
|
||||
(
|
||||
# kbd role
|
||||
'verify',
|
||||
':kbd:`Alt+^`',
|
||||
(
|
||||
'<p><kbd class="kbd compound docutils literal notranslate">'
|
||||
'<p>'
|
||||
'<kbd class="kbd docutils literal notranslate">Alt</kbd>'
|
||||
'+'
|
||||
'<kbd class="kbd docutils literal notranslate">^</kbd>'
|
||||
'</kbd></p>'
|
||||
'</p>'
|
||||
),
|
||||
(
|
||||
'\\sphinxAtStartPar\n'
|
||||
'\\sphinxkeyboard{\\sphinxupquote{Alt+\\textasciicircum{}}}'
|
||||
'\\sphinxkeyboard{\\sphinxupquote{Alt}}'
|
||||
'+'
|
||||
'\\sphinxkeyboard{\\sphinxupquote{\\textasciicircum{}}}'
|
||||
),
|
||||
),
|
||||
(
|
||||
@@ -386,7 +391,7 @@ def get_verifier(verify, verify_re):
|
||||
'verify',
|
||||
':kbd:`M-x M-s`',
|
||||
(
|
||||
'<p><kbd class="kbd compound docutils literal notranslate">'
|
||||
'<p>'
|
||||
'<kbd class="kbd docutils literal notranslate">M</kbd>'
|
||||
'-'
|
||||
'<kbd class="kbd docutils literal notranslate">x</kbd>'
|
||||
@@ -394,11 +399,17 @@ def get_verifier(verify, verify_re):
|
||||
'<kbd class="kbd docutils literal notranslate">M</kbd>'
|
||||
'-'
|
||||
'<kbd class="kbd docutils literal notranslate">s</kbd>'
|
||||
'</kbd></p>'
|
||||
'</p>'
|
||||
),
|
||||
(
|
||||
'\\sphinxAtStartPar\n'
|
||||
'\\sphinxkeyboard{\\sphinxupquote{M\\sphinxhyphen{}x M\\sphinxhyphen{}s}}'
|
||||
'\\sphinxkeyboard{\\sphinxupquote{M}}'
|
||||
'\\sphinxhyphen{}'
|
||||
'\\sphinxkeyboard{\\sphinxupquote{x}}'
|
||||
' '
|
||||
'\\sphinxkeyboard{\\sphinxupquote{M}}'
|
||||
'\\sphinxhyphen{}'
|
||||
'\\sphinxkeyboard{\\sphinxupquote{s}}'
|
||||
),
|
||||
),
|
||||
(
|
||||
@@ -422,6 +433,28 @@ def get_verifier(verify, verify_re):
|
||||
'<p><kbd class="kbd docutils literal notranslate">sys rq</kbd></p>',
|
||||
'\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{sys rq}}',
|
||||
),
|
||||
(
|
||||
# kbd role
|
||||
'verify',
|
||||
':kbd:`⌘+⇧+M`',
|
||||
(
|
||||
'<p>'
|
||||
'<kbd class="kbd docutils literal notranslate">⌘</kbd>'
|
||||
'+'
|
||||
'<kbd class="kbd docutils literal notranslate">⇧</kbd>'
|
||||
'+'
|
||||
'<kbd class="kbd docutils literal notranslate">M</kbd>'
|
||||
'</p>'
|
||||
),
|
||||
(
|
||||
'\\sphinxAtStartPar\n'
|
||||
'\\sphinxkeyboard{\\sphinxupquote{⌘}}'
|
||||
'+'
|
||||
'\\sphinxkeyboard{\\sphinxupquote{⇧}}'
|
||||
'+'
|
||||
'\\sphinxkeyboard{\\sphinxupquote{M}}'
|
||||
),
|
||||
),
|
||||
(
|
||||
# non-interpolation of dashes in option role
|
||||
'verify_re',
|
||||
|
||||
Reference in New Issue
Block a user