diff --git a/CHANGES b/CHANGES index 0c280a67a5b..1ae36d21be7 100644 --- a/CHANGES +++ b/CHANGES @@ -22,6 +22,7 @@ Bugs fixed * #5508: ``linenothreshold`` option for ``highlight`` directive was ignored * texinfo: ``make install-info`` causes syntax error * texinfo: ``make install-info`` fails on macOS +* #2155: Support ``code`` directive Testing -------- diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 8dbb1cec6e8..99d579863a7 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -19,18 +19,6 @@ from sphinx.util.docfields import DocFieldTransformer from sphinx.util.docutils import SphinxDirective -# import all directives sphinx provides -from sphinx.directives.code import ( # noqa - Highlight, CodeBlock, LiteralInclude -) -from sphinx.directives.other import ( # noqa - TocTree, Author, Index, VersionChange, SeeAlso, - TabularColumns, Centered, Acks, HList, Only, Include, Class -) -from sphinx.directives.patches import ( # noqa - Figure, Meta -) - if False: # For type annotation from typing import Any, Dict # NOQA @@ -46,6 +34,19 @@ strip_backslash_re = re.compile(r'\\(.)') +def optional_int(argument): + """ + Check for an integer argument or None value; raise ``ValueError`` if not. + """ + if argument is None: + return None + else: + value = int(argument) + if value < 0: + raise ValueError('negative value; must be positive or zero') + return value + + class ObjectDescription(SphinxDirective): """ Directive to describe a class, function or similar object. Not used @@ -243,6 +244,18 @@ def run(self): self.env.temp_data['default_domain'] = self.env.domains.get(domain_name) return [] +# import all directives sphinx provides (for compatibility) +from sphinx.directives.code import ( # noqa + Highlight, CodeBlock, LiteralInclude +) +from sphinx.directives.other import ( # noqa + TocTree, Author, Index, VersionChange, SeeAlso, + TabularColumns, Centered, Acks, HList, Only, Include, Class +) +from sphinx.directives.patches import ( # noqa + Figure, Meta +) + def setup(app): # type: (Sphinx) -> Dict[str, Any] diff --git a/sphinx/directives/code.py b/sphinx/directives/code.py index 73afac4ba94..ef27e3369b7 100644 --- a/sphinx/directives/code.py +++ b/sphinx/directives/code.py @@ -16,6 +16,7 @@ from sphinx import addnodes from sphinx.deprecation import RemovedInSphinx40Warning +from sphinx.directives import optional_int from sphinx.locale import __ from sphinx.util import logging from sphinx.util import parselinenos @@ -392,6 +393,50 @@ def dedent_filter(self, lines, location=None): return lines +class Code(SphinxDirective): + """Parse and mark up content of a code block. + """ + optional_arguments = 1 + option_spec = { + 'class': directives.class_option, + 'name': directives.unchanged, + 'number-lines': optional_int, + } + has_content = True + + def run(self): + # type: () -> List[nodes.Node] + self.assert_has_content() + + code = '\n'.join(self.content) + node = nodes.literal_block(code, code, + classes=self.options.get('classes', []), + highlight_args={}) + self.add_name(node) + set_source_info(self, node) + + if self.arguments: + # highlight language specified + node['language'] = self.arguments[0] + node['force_highlighting'] = True + else: + # no highlight language specified. Then this directive refers the current + # highlight setting via ``highlight`` directive or ``highlight_language`` + # configuration. + node['language'] = self.env.temp_data.get('highlight_language', + self.config.highlight_language) + node['force_highlighting'] = False + + if 'number-lines' in self.options: + node['linenos'] = True + + # if number given, treat as lineno-start. + if self.options['number-lines']: + node['highlight_args']['linenostart'] = self.options['number-lines'] + + return [node] + + class LiteralInclude(SphinxDirective): """ Like ``.. include:: :literal:``, but only warns if the include file is @@ -482,6 +527,7 @@ def setup(app): # type: (Sphinx) -> Dict[str, Any] directives.register_directive('highlight', Highlight) directives.register_directive('highlightlang', HighlightLang) + directives.register_directive('code', Code) directives.register_directive('code-block', CodeBlock) directives.register_directive('sourcecode', CodeBlock) directives.register_directive('literalinclude', LiteralInclude) diff --git a/tests/test_directive_code.py b/tests/test_directive_code.py index 2e1a1fde2df..0146ba51202 100644 --- a/tests/test_directive_code.py +++ b/tests/test_directive_code.py @@ -15,7 +15,8 @@ from sphinx.config import Config from sphinx.directives.code import LiteralIncludeReader -from sphinx.testing.util import etree_parse +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node, etree_parse DUMMY_CONFIG = Config({}, {}) @@ -614,3 +615,43 @@ def test_linenothreshold(app, status, warning): '2\n' '3' + lineos_tail) assert not matched + + +def test_code_directive(app): + # normal case + text = ('.. code::\n' + '\n' + ' print("hello world")\n') + + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, nodes.literal_block, 'print("hello world")']) + assert_node(doctree[0], language="default", highlight_args={}) + + # with language + text = ('.. code:: python\n' + '\n' + ' print("hello world")\n') + + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, nodes.literal_block, 'print("hello world")']) + assert_node(doctree[0], language="python", highlight_args={}) + + # :number-lines: option + text = ('.. code:: python\n' + ' :number-lines:\n' + '\n' + ' print("hello world")\n') + + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, nodes.literal_block, 'print("hello world")']) + assert_node(doctree[0], language="python", linenos=True, highlight_args={}) + + # :number-lines: option + text = ('.. code:: python\n' + ' :number-lines: 5\n' + '\n' + ' print("hello world")\n') + + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, nodes.literal_block, 'print("hello world")']) + assert_node(doctree[0], language="python", linenos=True, highlight_args={'linenostart': 5})