Skip to content

Implement key splitting in the :kbd: role and remove KeyboardTransform #13227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand All @@ -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
----------
Expand Down
3 changes: 0 additions & 3 deletions sphinx/builders/html/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
90 changes: 0 additions & 90 deletions sphinx/builders/html/transforms.py

This file was deleted.

55 changes: 54 additions & 1 deletion sphinx/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
generic_docroles = {
'command': addnodes.literal_strong,
'dfn': nodes.emphasis,
'kbd': nodes.literal,
'mailheader': addnodes.literal_emphasis,
'makevar': addnodes.literal_strong,
'mimetype': addnodes.literal_emphasis,
Expand Down Expand Up @@ -479,6 +478,59 @@ def run(self) -> tuple[list[Node], list[system_message]]:
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*)?\)?)$')

Expand Down Expand Up @@ -576,6 +628,7 @@ def code_role(
'samp': EmphasizedLiteral(),
# other
'abbr': Abbreviation(),
'kbd': Keyboard(),
'manpage': Manpage(),
}

Expand Down
55 changes: 44 additions & 11 deletions tests/test_markup/test_markup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -357,48 +355,61 @@ def get(name):
'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{}}}'
),
),
(
# kbd role
'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>'
' '
'<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}}'
),
),
(
Expand All @@ -422,6 +433,28 @@ def get(name):
'<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',
Expand Down
Loading