Skip to content
Open
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
88 changes: 74 additions & 14 deletions sphinx/builders/html/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"""

import re
from typing import Any, Dict
from typing import Any, Dict, List, Optional, Set, Tuple

from docutils import nodes

Expand Down Expand Up @@ -37,26 +37,86 @@ class KeyboardTransform(SphinxPostTransform):
"""
default_priority = 400
builders = ('html',)
pattern = re.compile(r'(-|\+|\^|\s+)')
pattern = re.compile(r'(?<=\S)(?:-|\+|\^|\s+)(?=\S)')

def run(self, **kwargs: Any) -> None:
matcher = NodeMatcher(nodes.literal, classes=["kbd"])
for node in self.document.traverse(matcher): # type: nodes.literal
parts = self.pattern.split(node[-1].astext())
masked_text, placeholders = self._mask_escaped_separators(
node.astext(), getattr(node, 'rawsource', None))
parts = self.pattern.split(masked_text)
if len(parts) == 1:
continue

node.pop()
while parts:
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
separators = self.pattern.findall(masked_text)

node[:] = []
for index, key in enumerate(parts):
restored_key = self._restore_placeholders(key, placeholders)
node += nodes.literal('', restored_key, classes=["kbd"])

if index < len(separators):
separator = self._restore_placeholders(separators[index], placeholders)
node += nodes.Text(separator)

def _mask_escaped_separators(self, text: str, rawsource: Optional[str]) -> Tuple[str, Dict[str, str]]:
placeholders: Dict[str, str] = {}
escaped_indices = self._find_escaped_indices(text, rawsource)
if not escaped_indices:
return text, placeholders

masked_parts: List[str] = []
for index, char in enumerate(text):
if index in escaped_indices and char in '-+^':
placeholder = f'@KBD_ESC_{len(placeholders)}@'
placeholders[placeholder] = char
masked_parts.append(placeholder)
else:
masked_parts.append(char)

return ''.join(masked_parts), placeholders

def _find_escaped_indices(self, text: str, rawsource: Optional[str]) -> Set[int]:
if not rawsource:
return set()

start = rawsource.find('`')
end = rawsource.rfind('`')
if start == -1 or end <= start:
content = rawsource
else:
content = rawsource[start + 1:end]

indices: Set[int] = set()
text_index = 0
pos = 0
text_len = len(text)

while pos < len(content) and text_index < text_len:
char = content[pos]
if char == '\\':
if pos + 1 >= len(content):
pos += 1
continue

escaped = content[pos + 1]
if escaped in '-+^':
indices.add(text_index)

pos += 2
text_index += 1
continue

pos += 1
text_index += 1

return indices

@staticmethod
def _restore_placeholders(text: str, placeholders: Dict[str, str]) -> str:
for placeholder, char in placeholders.items():
text = text.replace(placeholder, char)
return text


def setup(app: Sphinx) -> Dict[str, Any]:
Expand Down
74 changes: 74 additions & 0 deletions tests/test_markup.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,27 @@ def get(name):
'<p><kbd class="kbd docutils literal notranslate">space</kbd></p>',
'\\sphinxkeyboard{\\sphinxupquote{space}}',
),
(
# kbd role literal hyphen
'verify',
':kbd:`-`',
'<p><kbd class="kbd docutils literal notranslate">-</kbd></p>',
None,
),
(
# kbd role literal plus
'verify',
':kbd:`+`',
'<p><kbd class="kbd docutils literal notranslate">+</kbd></p>',
None,
),
(
# kbd role literal caret
'verify',
':kbd:`^`',
'<p><kbd class="kbd docutils literal notranslate">^</kbd></p>',
None,
),
(
# kbd role
'verify',
Expand All @@ -266,6 +287,59 @@ def get(name):
'</kbd></p>'),
'\\sphinxkeyboard{\\sphinxupquote{M\\sphinxhyphen{}x M\\sphinxhyphen{}s}}',
),
(
# kbd role compound with literal separator key
'verify',
':kbd:`Shift-+`',
('<p><kbd class="kbd docutils literal notranslate">'
'<kbd class="kbd docutils literal notranslate">Shift</kbd>'
'-'
'<kbd class="kbd docutils literal notranslate">+</kbd>'
'</kbd></p>'),
None,
),
(
# kbd role boundary hyphen prefix
'verify',
':kbd:`-X`',
'<p><kbd class="kbd docutils literal notranslate">-X</kbd></p>',
None,
),
(
# kbd role boundary hyphen suffix
'verify',
':kbd:`X-`',
'<p><kbd class="kbd docutils literal notranslate">X-</kbd></p>',
None,
),
(
# kbd role escaped hyphen separator
'verify',
':kbd:`C\\-x`',
'<p><kbd class="kbd docutils literal notranslate">C-x</kbd></p>',
None,
),
(
# kbd role escaped plus separator
'verify',
':kbd:`A\\+B`',
'<p><kbd class="kbd docutils literal notranslate">A+B</kbd></p>',
None,
),
(
# kbd role escaped caret separator
'verify',
':kbd:`^\\^`',
'<p><kbd class="kbd docutils literal notranslate">^^</kbd></p>',
None,
),
(
# kbd role escaped separator after other escapes
'verify',
':kbd:`\\|\\-\\|`',
'<p><kbd class="kbd docutils literal notranslate">|-|</kbd></p>',
None,
),
(
# non-interpolation of dashes in option role
'verify_re',
Expand Down