Skip to content
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

Make code role highlighting consistent with code-block directive #10251

Merged
merged 1 commit into from May 5, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions doc/usage/restructuredtext/directives.rst
Expand Up @@ -499,8 +499,10 @@ __ https://pygments.org/docs/lexers
The directive's alias name :rst:dir:`sourcecode` works as well. This
directive takes a language name as an argument. It can be `any lexer alias
supported by Pygments <https://pygments.org/docs/lexers/>`_. If it is not
given, the setting of :rst:dir:`highlight` directive will be used.
If not set, :confval:`highlight_language` will be used.
given, the setting of :rst:dir:`highlight` directive will be used. If not
set, :confval:`highlight_language` will be used. To display a code example
*inline* within other text, rather than as a separate block, you can use the
:rst:role:`code` role instead.

.. versionchanged:: 2.0
The ``language`` argument becomes optional.
Expand Down
28 changes: 28 additions & 0 deletions doc/usage/restructuredtext/roles.rst
Expand Up @@ -276,6 +276,34 @@ The following role creates a cross-reference to a term in a
If you use a term that's not explained in a glossary, you'll get a warning
during build.

Inline code highlighting
------------------------

.. rst:role:: code

An *inline* code example. When used directly, this role just displays the
text *without* syntax highlighting, as a literal.

.. code-block:: rst

By default, inline code such as :code:`1 + 2` just displays without
highlighting.

Unlike the :rst:dir:`code-block` directive, this role does not respect the
default language set by the :rst:dir:`highlight` directive.

To enable syntax highlighting, you must first use the ``role`` directive to
define a custom ``code`` role for a particular language:

.. code-block:: rst

.. role:: python(code)
:language: python

In Python, :python:`1 + 2` is equal to :python:`3`.

To display a multi-line code example, use the :rst:dir:`code-block` directive
instead.

Math
----
Expand Down
58 changes: 58 additions & 0 deletions sphinx/roles.py
Expand Up @@ -11,6 +11,9 @@
import re
from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Type

import docutils.parsers.rst.directives
import docutils.parsers.rst.roles
import docutils.parsers.rst.states
from docutils import nodes, utils
from docutils.nodes import Element, Node, TextElement, system_message

Expand Down Expand Up @@ -341,6 +344,57 @@ def run(self) -> Tuple[List[Node], List[system_message]]:
return [nodes.abbreviation(self.rawtext, text, **options)], []


# Sphinx provides the `code-block` directive for highlighting code blocks.
# Docutils provides the `code` role which in theory can be used similarly by
# defining a custom role for a given programming language:
#
# .. .. role:: python(code)
# :language: python
# :class: highlight
#
# In practice this does not produce correct highlighting because it uses a
# separate highlighting mechanism that results in the "long" pygments class
# names rather than "short" pygments class names produced by the Sphinx
# `code-block` directive and for which this extension contains CSS rules.
#
# In addition, even if that issue is fixed, because the highlighting
# implementation in docutils, despite being based on pygments, differs from that
# used by Sphinx, the output does not exactly match that produced by the Sphinx
# `code-block` directive.
#
# This issue is noted here: //github.com/sphinx-doc/sphinx/issues/5157
#
# This overrides the docutils `code` role to perform highlighting in the same
# way as the Sphinx `code-block` directive.
#
# TODO: Change to use `SphinxRole` once SphinxRole is fixed to support options.
def code_role(name: str, rawtext: str, text: str, lineno: int,
jbms marked this conversation as resolved.
Show resolved Hide resolved
inliner: docutils.parsers.rst.states.Inliner,
options: Dict = {}, content: List[str] = []
) -> Tuple[List[Node], List[system_message]]:
options = options.copy()
docutils.parsers.rst.roles.set_classes(options)
language = options.get('language', '')
classes = ['code']
if language:
classes.append('highlight')
if 'classes' in options:
classes.extend(options['classes'])

if language and language not in classes:
classes.append(language)

node = nodes.literal(rawtext, text, classes=classes, language=language)

return [node], []


code_role.options = { # type: ignore
'class': docutils.parsers.rst.directives.class_option,
'language': docutils.parsers.rst.directives.unchanged,
}


specific_docroles: Dict[str, RoleFunction] = {
# links to download references
'download': XRefRole(nodeclass=addnodes.download_reference),
Expand Down Expand Up @@ -368,6 +422,10 @@ def setup(app: "Sphinx") -> Dict[str, Any]:
for rolename, func in specific_docroles.items():
roles.register_local_role(rolename, func)

# Since docutils registers it as a canonical role, override it as a
# canonical role as well.
roles.register_canonical_role('code', code_role)

return {
'version': 'builtin',
'parallel_read_safe': True,
Expand Down
17 changes: 16 additions & 1 deletion sphinx/writers/html.py
Expand Up @@ -511,10 +511,25 @@ def visit_literal(self, node: Element) -> None:
if 'kbd' in node['classes']:
self.body.append(self.starttag(node, 'kbd', '',
CLASS='docutils literal notranslate'))
else:
return
lang = node.get("language", None)
if 'code' not in node['classes'] or not lang:
tk0miya marked this conversation as resolved.
Show resolved Hide resolved
self.body.append(self.starttag(node, 'code', '',
CLASS='docutils literal notranslate'))
self.protect_literal_text += 1
return

opts = self.config.highlight_options.get(lang, {})
highlighted = self.highlighter.highlight_block(
node.astext(), lang, opts=opts, location=node, nowrap=True)
starttag = self.starttag(
node,
"code",
suffix="",
CLASS="docutils literal highlight highlight-%s" % lang,
)
self.body.append(starttag + highlighted.strip() + "</code>")
raise nodes.SkipNode

def depart_literal(self, node: Element) -> None:
if 'kbd' in node['classes']:
Expand Down
17 changes: 16 additions & 1 deletion sphinx/writers/html5.py
Expand Up @@ -470,10 +470,25 @@ def visit_literal(self, node: Element) -> None:
if 'kbd' in node['classes']:
self.body.append(self.starttag(node, 'kbd', '',
CLASS='docutils literal notranslate'))
else:
return
lang = node.get("language", None)
if 'code' not in node['classes'] or not lang:
self.body.append(self.starttag(node, 'code', '',
CLASS='docutils literal notranslate'))
self.protect_literal_text += 1
return

opts = self.config.highlight_options.get(lang, {})
highlighted = self.highlighter.highlight_block(
node.astext(), lang, opts=opts, location=node, nowrap=True)
starttag = self.starttag(
node,
"code",
suffix="",
CLASS="docutils literal highlight highlight-%s" % lang,
)
self.body.append(starttag + highlighted.strip() + "</code>")
raise nodes.SkipNode

def depart_literal(self, node: Element) -> None:
if 'kbd' in node['classes']:
Expand Down
19 changes: 18 additions & 1 deletion sphinx/writers/latex.py
Expand Up @@ -1747,10 +1747,27 @@ def depart_citation_reference(self, node: Element) -> None:
def visit_literal(self, node: Element) -> None:
if self.in_title:
self.body.append(r'\sphinxstyleliteralintitle{\sphinxupquote{')
return
elif 'kbd' in node['classes']:
self.body.append(r'\sphinxkeyboard{\sphinxupquote{')
else:
return
lang = node.get("language", None)
if 'code' not in node['classes'] or not lang:
self.body.append(r'\sphinxcode{\sphinxupquote{')
return

opts = self.config.highlight_options.get(lang, {})
hlcode = self.highlighter.highlight_block(
node.astext(), lang, opts=opts, location=node)
# TODO: Use nowrap option once LaTeX formatter supports it
# https://github.com/pygments/pygments/pull/1343
hlcode = hlcode.replace(r'\begin{Verbatim}[commandchars=\\\{\}]',
jbms marked this conversation as resolved.
Show resolved Hide resolved
r'\sphinxcode{\sphinxupquote{')
# get consistent trailer
hlcode = hlcode.rstrip()[:-14] # strip \end{Verbatim}
self.body.append(hlcode)
self.body.append('}}')
raise nodes.SkipNode

def depart_literal(self, node: Element) -> None:
self.body.append('}}')
Expand Down
Empty file.
9 changes: 9 additions & 0 deletions tests/roots/test-reST-code-role/index.rst
@@ -0,0 +1,9 @@
.. role:: python(code)
:language: python
:class: highlight

Inline :python:`def foo(1 + 2 + None + "abc"): pass` code block

.. code-block:: python

def foo(1 + 2 + None + "abc"): pass
24 changes: 24 additions & 0 deletions tests/test_build_html.py
Expand Up @@ -1718,3 +1718,27 @@ def test_html_signaturereturn_icon(app):
content = (app.outdir / 'index.html').read_text()

assert ('<span class="sig-return-icon">&#x2192;</span>' in content)


@pytest.mark.sphinx('html', testroot='reST-code-role')
def test_html_code_role(app):
app.build()
content = (app.outdir / 'index.html').read_text()

common_content = (
'<span class="k">def</span> <span class="nf">foo</span>'
'<span class="p">(</span>'
'<span class="mi">1</span> '
'<span class="o">+</span> '
'<span class="mi">2</span> '
'<span class="o">+</span> '
'<span class="kc">None</span> '
'<span class="o">+</span> '
'<span class="s2">&quot;abc&quot;</span>'
'<span class="p">):</span> '
'<span class="k">pass</span>')
assert ('<p>Inline <code class="code highlight python docutils literal highlight-python">' +
common_content + '</code> code block</p>') in content
assert ('<div class="highlight-python notranslate">' +
'<div class="highlight"><pre><span></span>' +
common_content) in content
27 changes: 27 additions & 0 deletions tests/test_build_latex.py
Expand Up @@ -1628,3 +1628,30 @@ def test_latex_container(app, status, warning):
result = (app.outdir / 'python.tex').read_text()
assert r'\begin{sphinxuseclass}{classname}' in result
assert r'\end{sphinxuseclass}' in result


@pytest.mark.sphinx('latex', testroot='reST-code-role')
def test_latex_code_role(app):
app.build()
content = (app.outdir / 'python.tex').read_text()

common_content = (
r'\PYG{k}{def} '
r'\PYG{n+nf}{foo}'
r'\PYG{p}{(}'
r'\PYG{l+m+mi}{1} '
r'\PYG{o}{+} '
r'\PYG{l+m+mi}{2} '
r'\PYG{o}{+} '
r'\PYG{k+kc}{None} '
r'\PYG{o}{+} '
r'\PYG{l+s+s2}{\PYGZdq{}}'
r'\PYG{l+s+s2}{abc}'
r'\PYG{l+s+s2}{\PYGZdq{}}'
r'\PYG{p}{)}'
r'\PYG{p}{:} '
r'\PYG{k}{pass}')
assert (r'Inline \sphinxcode{\sphinxupquote{' + '\n' +
common_content + '\n}} code block') in content
assert (r'\begin{sphinxVerbatim}[commandchars=\\\{\}]' +
'\n' + common_content + '\n' + r'\end{sphinxVerbatim}') in content