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}}' - '}' + '}}' ), ), (