From 8781d3613d78c3d3e45c55a42af6b07a3e426c2d Mon Sep 17 00:00:00 2001
From: Brecht Machiels
Date: Fri, 5 Sep 2025 17:36:01 +0200
Subject: [PATCH 1/2] Group nodes generated by :kbd: role and add class to
separators (#13876)
---
CHANGES.rst | 2 +
sphinx/roles.py | 11 +-
.../test_builders/test_build_html_5_output.py | 2 +-
tests/test_markup/test_markup.py | 104 ++++++++++++++----
4 files changed, 89 insertions(+), 30 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 64f94e14ec3..e1cc14bbe55 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -66,6 +66,8 @@ Features added
Patch by Adam Turner.
* #13805: LaTeX: add support for ``fontawesome7`` package.
Patch by Jean-François B.
+* #13876: Group nodes generated by :kbd: role and add class to separators
+ Patch by Brecht Machiels.
Bugs fixed
----------
diff --git a/sphinx/roles.py b/sphinx/roles.py
index cadfb5a027b..dc5c3b599b9 100644
--- a/sphinx/roles.py
+++ b/sphinx/roles.py
@@ -491,27 +491,24 @@ def run(self) -> tuple[list[Node], list[system_message]]:
if 'classes' in self.options:
classes.extend(self.options['classes'])
+ compound = nodes.inline(self.rawtext, classes=['kbdcompound'])
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))
+ compound += nodes.literal(key, key, classes=['kbd'])
try:
sep = parts.pop(0) # key separator ('-', '+', '^', etc)
except IndexError:
break
else:
- compound.append(nodes.Text(sep))
+ compound += nodes.inline(sep, sep, classes=['kbdsep'])
- return compound, []
+ return [compound], []
@staticmethod
def _is_multi_word_key(parts: list[str]) -> bool:
diff --git a/tests/test_builders/test_build_html_5_output.py b/tests/test_builders/test_build_html_5_output.py
index db9dd8a749c..50d3ff297d9 100644
--- a/tests/test_builders/test_build_html_5_output.py
+++ b/tests/test_builders/test_build_html_5_output.py
@@ -127,7 +127,7 @@ def checker(nodes: Iterable[Element]) -> Literal[True]:
('markup.html', './/li/p/strong', r'^command\\n$'),
('markup.html', './/li/p/strong', r'^program\\n$'),
('markup.html', './/li/p/em', r'^dfn\\n$'),
- ('markup.html', './/li/p/kbd', r'^kbd\\n$'),
+ ('markup.html', './/li/p/span/kbd', r'^kbd\\n$'),
('markup.html', './/li/p/span', 'File \N{TRIANGULAR BULLET} Close'),
('markup.html', ".//li/p/code/span[@class='pre']", '^a/$'),
('markup.html', ".//li/p/code/em/span[@class='pre']", '^varpart$'),
diff --git a/tests/test_markup/test_markup.py b/tests/test_markup/test_markup.py
index fb4df4c400b..03b0e12b5dd 100644
--- a/tests/test_markup/test_markup.py
+++ b/tests/test_markup/test_markup.py
@@ -332,8 +332,19 @@ def get_verifier(verify, verify_re):
# kbd role
'verify',
':kbd:`space`',
- 'space
',
- '\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{space}}',
+ (
+ ''
+ ''
+ 'space'
+ ''
+ '
'
+ ),
+ (
+ '\\sphinxAtStartPar\n'
+ '\\DUrole{kbdcompound}{'
+ '\\sphinxkeyboard{\\sphinxupquote{space}}'
+ '}'
+ ),
),
(
# kbd role
@@ -341,16 +352,20 @@ def get_verifier(verify, verify_re):
':kbd:`Control+X`',
(
''
+ ''
'Control'
- '+'
+ '+'
'X'
+ ''
'
'
),
(
'\\sphinxAtStartPar\n'
+ '\\DUrole{kbdcompound}{'
'\\sphinxkeyboard{\\sphinxupquote{Control}}'
- '+'
+ '\\DUrole{kbdsep}{+}'
'\\sphinxkeyboard{\\sphinxupquote{X}}'
+ '}'
),
),
(
@@ -359,16 +374,20 @@ def get_verifier(verify, verify_re):
':kbd:`Alt+^`',
(
''
+ ''
'Alt'
- '+'
+ '+'
'^'
+ ''
'
'
),
(
'\\sphinxAtStartPar\n'
+ '\\DUrole{kbdcompound}{'
'\\sphinxkeyboard{\\sphinxupquote{Alt}}'
- '+'
+ '\\DUrole{kbdsep}{+}'
'\\sphinxkeyboard{\\sphinxupquote{\\textasciicircum{}}}'
+ '}'
),
),
(
@@ -377,46 +396,83 @@ def get_verifier(verify, verify_re):
':kbd:`M-x M-s`',
(
''
+ ''
'M'
- '-'
+ '-'
'x'
- ' '
+ ' '
'M'
- '-'
+ '-'
's'
+ ''
'
'
),
(
'\\sphinxAtStartPar\n'
+ '\\DUrole{kbdcompound}{'
'\\sphinxkeyboard{\\sphinxupquote{M}}'
- '\\sphinxhyphen{}'
+ '\\DUrole{kbdsep}{\\sphinxhyphen{}}'
'\\sphinxkeyboard{\\sphinxupquote{x}}'
- ' '
+ '\\DUrole{kbdsep}{ }'
'\\sphinxkeyboard{\\sphinxupquote{M}}'
- '\\sphinxhyphen{}'
+ '\\DUrole{kbdsep}{\\sphinxhyphen{}}'
'\\sphinxkeyboard{\\sphinxupquote{s}}'
+ '}'
),
),
(
# kbd role
'verify',
':kbd:`-`',
- '-
',
- '\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{\\sphinxhyphen{}}}',
+ (
+ ''
+ ''
+ '-'
+ ''
+ '
'
+ ),
+ (
+ '\\sphinxAtStartPar\n'
+ '\\DUrole{kbdcompound}{'
+ '\\sphinxkeyboard{\\sphinxupquote{\\sphinxhyphen{}}}'
+ '}'
+ ),
),
(
# kbd role
'verify',
':kbd:`Caps Lock`',
- 'Caps Lock
',
- '\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{Caps Lock}}',
+ (
+ ''
+ ''
+ 'Caps Lock'
+ ''
+ '
'
+ ),
+ (
+ '\\sphinxAtStartPar\n'
+ '\\DUrole{kbdcompound}{'
+ '\\sphinxkeyboard{\\sphinxupquote{Caps Lock}}'
+ '}'
+ ),
),
(
# kbd role
'verify',
':kbd:`sys rq`',
- 'sys rq
',
- '\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{sys rq}}',
+ (
+ ''
+ ''
+ 'sys rq'
+ ''
+ '
'
+ ),
+ (
+ '\\sphinxAtStartPar\n'
+ '\\DUrole{kbdcompound}{'
+ '\\sphinxkeyboard{\\sphinxupquote{sys rq}}'
+ '}'
+ ),
),
(
# kbd role
@@ -424,20 +480,24 @@ def get_verifier(verify, verify_re):
':kbd:`⌘+⇧+M`',
(
''
+ ''
'⌘'
- '+'
+ '+'
'⇧'
- '+'
+ '+'
'M'
+ ''
'
'
),
(
'\\sphinxAtStartPar\n'
+ '\\DUrole{kbdcompound}{'
'\\sphinxkeyboard{\\sphinxupquote{⌘}}'
- '+'
+ '\\DUrole{kbdsep}{+}'
'\\sphinxkeyboard{\\sphinxupquote{⇧}}'
- '+'
+ '\\DUrole{kbdsep}{+}'
'\\sphinxkeyboard{\\sphinxupquote{M}}'
+ '}'
),
),
(
From bf315910d721ef803dd53e1eb8b65483e5fa4a8c Mon Sep 17 00:00:00 2001
From: Adam Turner <9087854+aa-turner@users.noreply.github.com>
Date: Sun, 5 Oct 2025 22:43:28 +0100
Subject: [PATCH 2/2] Only wrap `` nodes when multiple elements are
present
---
CHANGES.rst | 6 +-
sphinx/roles.py | 10 +-
.../test_builders/test_build_html_5_output.py | 2 +-
tests/test_markup/test_markup.py | 120 ++++++------------
4 files changed, 50 insertions(+), 88 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 8d5f37fe983..e2f125998cc 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -66,8 +66,6 @@ Features added
Patch by Adam Turner.
* #13805: LaTeX: add support for ``fontawesome7`` package.
Patch by Jean-François B.
-* #13876: Group nodes generated by :kbd: role and add class to separators
- Patch by Brecht Machiels.
Bugs fixed
----------
@@ -128,6 +126,10 @@ Bugs fixed
* #13929: Duplicate equation label warnings now have a new warning
sub-type, ``ref.equation``.
Patch by Jared Dillard.
+* #13876: Restore the ``compound`` class for groups of nodes generated
+ by the :rst:role:`kbd` role, and add the ``kbd-sep`` class to separators
+ within the group.
+ Patch by Brecht Machiels.
Testing
diff --git a/sphinx/roles.py b/sphinx/roles.py
index dc5c3b599b9..b4288fb488d 100644
--- a/sphinx/roles.py
+++ b/sphinx/roles.py
@@ -491,22 +491,26 @@ def run(self) -> tuple[list[Node], list[system_message]]:
if 'classes' in self.options:
classes.extend(self.options['classes'])
- compound = nodes.inline(self.rawtext, classes=['kbdcompound'])
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 = nodes.literal(self.rawtext, classes=classes)
+ compound['classes'].append('compound')
while parts:
if self._is_multi_word_key(parts):
key = ''.join(parts[:3])
parts[:3] = []
else:
key = parts.pop(0)
- compound += nodes.literal(key, key, classes=['kbd'])
+ compound.append(nodes.literal(key, key, classes=classes))
try:
sep = parts.pop(0) # key separator ('-', '+', '^', etc)
except IndexError:
break
else:
- compound += nodes.inline(sep, sep, classes=['kbdsep'])
+ compound.append(nodes.inline(sep, sep, classes=['kbd-sep']))
return [compound], []
diff --git a/tests/test_builders/test_build_html_5_output.py b/tests/test_builders/test_build_html_5_output.py
index 50d3ff297d9..db9dd8a749c 100644
--- a/tests/test_builders/test_build_html_5_output.py
+++ b/tests/test_builders/test_build_html_5_output.py
@@ -127,7 +127,7 @@ def checker(nodes: Iterable[Element]) -> Literal[True]:
('markup.html', './/li/p/strong', r'^command\\n$'),
('markup.html', './/li/p/strong', r'^program\\n$'),
('markup.html', './/li/p/em', r'^dfn\\n$'),
- ('markup.html', './/li/p/span/kbd', r'^kbd\\n$'),
+ ('markup.html', './/li/p/kbd', r'^kbd\\n$'),
('markup.html', './/li/p/span', 'File \N{TRIANGULAR BULLET} Close'),
('markup.html', ".//li/p/code/span[@class='pre']", '^a/$'),
('markup.html', ".//li/p/code/em/span[@class='pre']", '^varpart$'),
diff --git a/tests/test_markup/test_markup.py b/tests/test_markup/test_markup.py
index 03b0e12b5dd..75c992e2305 100644
--- a/tests/test_markup/test_markup.py
+++ b/tests/test_markup/test_markup.py
@@ -332,19 +332,8 @@ def get_verifier(verify, verify_re):
# kbd role
'verify',
':kbd:`space`',
- (
- ''
- ''
- 'space'
- ''
- '
'
- ),
- (
- '\\sphinxAtStartPar\n'
- '\\DUrole{kbdcompound}{'
- '\\sphinxkeyboard{\\sphinxupquote{space}}'
- '}'
- ),
+ 'space
',
+ '\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{space}}',
),
(
# kbd role
@@ -352,20 +341,20 @@ def get_verifier(verify, verify_re):
':kbd:`Control+X`',
(
''
- ''
+ ''
'Control'
- '+'
+ '+'
'X'
- ''
+ '
'
'
'
),
(
'\\sphinxAtStartPar\n'
- '\\DUrole{kbdcompound}{'
+ '\\sphinxkeyboard{\\sphinxupquote{'
'\\sphinxkeyboard{\\sphinxupquote{Control}}'
- '\\DUrole{kbdsep}{+}'
+ '\\DUrole{kbd-sep}{+}'
'\\sphinxkeyboard{\\sphinxupquote{X}}'
- '}'
+ '}}'
),
),
(
@@ -374,20 +363,20 @@ def get_verifier(verify, verify_re):
':kbd:`Alt+^`',
(
''
- ''
+ ''
'Alt'
- '+'
+ '+'
'^'
- ''
+ ''
'
'
),
(
'\\sphinxAtStartPar\n'
- '\\DUrole{kbdcompound}{'
+ '\\sphinxkeyboard{\\sphinxupquote{'
'\\sphinxkeyboard{\\sphinxupquote{Alt}}'
- '\\DUrole{kbdsep}{+}'
+ '\\DUrole{kbd-sep}{+}'
'\\sphinxkeyboard{\\sphinxupquote{\\textasciicircum{}}}'
- '}'
+ '}}'
),
),
(
@@ -396,83 +385,50 @@ def get_verifier(verify, verify_re):
':kbd:`M-x M-s`',
(
''
- ''
+ ''
'M'
- '-'
+ '-'
'x'
- ' '
+ ' '
'M'
- '-'
+ '-'
's'
- ''
+ ''
'
'
),
(
'\\sphinxAtStartPar\n'
- '\\DUrole{kbdcompound}{'
+ '\\sphinxkeyboard{\\sphinxupquote{'
'\\sphinxkeyboard{\\sphinxupquote{M}}'
- '\\DUrole{kbdsep}{\\sphinxhyphen{}}'
+ '\\DUrole{kbd-sep}{\\sphinxhyphen{}}'
'\\sphinxkeyboard{\\sphinxupquote{x}}'
- '\\DUrole{kbdsep}{ }'
+ '\\DUrole{kbd-sep}{ }'
'\\sphinxkeyboard{\\sphinxupquote{M}}'
- '\\DUrole{kbdsep}{\\sphinxhyphen{}}'
+ '\\DUrole{kbd-sep}{\\sphinxhyphen{}}'
'\\sphinxkeyboard{\\sphinxupquote{s}}'
- '}'
+ '}}'
),
),
(
# kbd role
'verify',
':kbd:`-`',
- (
- ''
- ''
- '-'
- ''
- '
'
- ),
- (
- '\\sphinxAtStartPar\n'
- '\\DUrole{kbdcompound}{'
- '\\sphinxkeyboard{\\sphinxupquote{\\sphinxhyphen{}}}'
- '}'
- ),
+ '-
',
+ '\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{\\sphinxhyphen{}}}',
),
(
# kbd role
'verify',
':kbd:`Caps Lock`',
- (
- ''
- ''
- 'Caps Lock'
- ''
- '
'
- ),
- (
- '\\sphinxAtStartPar\n'
- '\\DUrole{kbdcompound}{'
- '\\sphinxkeyboard{\\sphinxupquote{Caps Lock}}'
- '}'
- ),
+ 'Caps Lock
',
+ '\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{Caps Lock}}',
),
(
# kbd role
'verify',
':kbd:`sys rq`',
- (
- ''
- ''
- 'sys rq'
- ''
- '
'
- ),
- (
- '\\sphinxAtStartPar\n'
- '\\DUrole{kbdcompound}{'
- '\\sphinxkeyboard{\\sphinxupquote{sys rq}}'
- '}'
- ),
+ 'sys rq
',
+ '\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{sys rq}}',
),
(
# kbd role
@@ -480,24 +436,24 @@ def get_verifier(verify, verify_re):
':kbd:`⌘+⇧+M`',
(
''
- ''
+ ''
'⌘'
- '+'
+ '+'
'⇧'
- '+'
+ '+'
'M'
- ''
+ ''
'
'
),
(
'\\sphinxAtStartPar\n'
- '\\DUrole{kbdcompound}{'
+ '\\sphinxkeyboard{\\sphinxupquote{'
'\\sphinxkeyboard{\\sphinxupquote{⌘}}'
- '\\DUrole{kbdsep}{+}'
+ '\\DUrole{kbd-sep}{+}'
'\\sphinxkeyboard{\\sphinxupquote{⇧}}'
- '\\DUrole{kbdsep}{+}'
+ '\\DUrole{kbd-sep}{+}'
'\\sphinxkeyboard{\\sphinxupquote{M}}'
- '}'
+ '}}'
),
),
(