diff --git a/CHANGES b/CHANGES index abd7c1dd1..a10de4a17 100644 --- a/CHANGES +++ b/CHANGES @@ -49,6 +49,7 @@ Features added to generate stub files recursively * #4030: autosummary: Add :confval:`autosummary_context` to add template variables for custom templates +* #7530: html: Support nested elements * #7481: html theme: Add right margin to footnote/citation labels * #7482: html theme: CSS spacing for code blocks with captions and line numbers * #7443: html theme: Add new options :confval:`globaltoc_collapse` and diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 320c7feb6..7ad87ffa9 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -1243,6 +1243,9 @@ def setup(app: Sphinx) -> Dict[str, Any]: # 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, diff --git a/sphinx/builders/html/transforms.py b/sphinx/builders/html/transforms.py new file mode 100644 index 000000000..c91da57e9 --- /dev/null +++ b/sphinx/builders/html/transforms.py @@ -0,0 +1,69 @@ +""" + sphinx.builders.html.transforms + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Transforms for HTML builder. + + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import re +from typing import Any, Dict + +from docutils import nodes + +from sphinx.application import Sphinx +from sphinx.transforms.post_transforms import SphinxPostTransform +from sphinx.util.nodes import NodeMatcher + + +class KeyboardTransform(SphinxPostTransform): + """Transform :kbd: role to more detailed form. + + Before:: + + + Control-x + + After:: + + + + Control + - + + x + """ + default_priority = 400 + builders = ('html',) + pattern = re.compile(r'(-|\+|\^|\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()) + 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 + + +def setup(app: Sphinx) -> Dict[str, Any]: + app.add_post_transform(KeyboardTransform) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/tests/test_markup.py b/tests/test_markup.py index b6d99db90..1d5c81bfa 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -16,6 +16,7 @@ from docutils.parsers.rst import Parser as RstParser from docutils.transforms.universal import SmartQuotes from sphinx import addnodes +from sphinx.builders.html.transforms import KeyboardTransform from sphinx.builders.latex import LaTeXBuilder from sphinx.roles import XRefRole from sphinx.testing.util import Struct, assert_node @@ -94,6 +95,7 @@ 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() @@ -237,6 +239,32 @@ def get_verifier(verify, verify_re): '

space

', '\\sphinxkeyboard{\\sphinxupquote{space}}', ), + ( + # kbd role + 'verify', + ':kbd:`Control+X`', + ('

' + 'Control' + '+' + 'X' + '

'), + '\\sphinxkeyboard{\\sphinxupquote{Control+X}}', + ), + ( + # kbd role + 'verify', + ':kbd:`M-x M-s`', + ('

' + 'M' + '-' + 'x' + ' ' + 'M' + '-' + 's' + '

'), + '\\sphinxkeyboard{\\sphinxupquote{M\\sphinxhyphen{}x M\\sphinxhyphen{}s}}', + ), ( # non-interpolation of dashes in option role 'verify_re',