diff --git a/CHANGES b/CHANGES index 4c3c5ba0949..21e4dd820e4 100644 --- a/CHANGES +++ b/CHANGES @@ -30,10 +30,13 @@ Bugs fixed * #6147: classes attribute of ``citation_reference`` node is lost * AssertionError is raised when custom ``citation_reference`` node having classes attribute refers missing citation (refs: #6147) +* #2155: Support ``code`` directive Testing -------- +* Add a helper function: ``sphinx.testing.restructuredtext.parse()`` + Release 2.0.0 beta1 (in development) ==================================== diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index c639885f63f..1f4520541af 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 @@ -44,6 +32,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 @@ -241,6 +242,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/patches.py b/sphinx/directives/patches.py index 2d8ca636af7..e89355cf761 100644 --- a/sphinx/directives/patches.py +++ b/sphinx/directives/patches.py @@ -14,6 +14,7 @@ from docutils.parsers.rst.directives import images, html, tables from sphinx import addnodes +from sphinx.directives import optional_int from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import set_source_info @@ -110,6 +111,52 @@ def make_title(self): return title, message +class Code(SphinxDirective): + """Parse and mark up content of a code block. + + This is compatible with docutils' :rst:dir:`code` directive. + """ + 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 MathDirective(SphinxDirective): has_content = True required_arguments = 0 @@ -174,6 +221,7 @@ def setup(app): directives.register_directive('table', RSTTable) directives.register_directive('csv-table', CSVTable) directives.register_directive('list-table', ListTable) + directives.register_directive('code', Code) directives.register_directive('math', MathDirective) return { diff --git a/sphinx/testing/restructuredtext.py b/sphinx/testing/restructuredtext.py new file mode 100644 index 00000000000..8bf1c041e0b --- /dev/null +++ b/sphinx/testing/restructuredtext.py @@ -0,0 +1,38 @@ +""" + sphinx.testing.restructuredtext + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from os import path + +from docutils.core import publish_doctree + +from sphinx.io import SphinxStandaloneReader +from sphinx.parsers import RSTParser +from sphinx.util.docutils import sphinx_domains + + +if False: + # For type annotation + from docutils import nodes # NOQA + from sphinx.application import Sphinx # NOQA + + +def parse(app, text, docname='index'): + # type: (Sphinx, str, str) -> nodes.document + """Parse a string as reStructuredText with Sphinx application.""" + try: + app.env.temp_data['docname'] = docname + parser = RSTParser() + parser.set_application(app) + with sphinx_domains(app.env): + return publish_doctree(text, path.join(app.srcdir, docname + '.rst'), + reader=SphinxStandaloneReader(app), + parser=parser, + settings_overrides={'env': app.env, + 'gettext_compact': True}) + finally: + app.env.temp_data.pop('docname', None) diff --git a/tests/test_directive_other.py b/tests/test_directive_other.py index cbbebee5cfd..376b9cd6984 100644 --- a/tests/test_directive_other.py +++ b/tests/test_directive_other.py @@ -10,25 +10,12 @@ import pytest from docutils import nodes -from docutils.core import publish_doctree from sphinx import addnodes -from sphinx.io import SphinxStandaloneReader -from sphinx.parsers import RSTParser +from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node -def parse(app, docname, text): - app.env.temp_data['docname'] = docname - parser = RSTParser() - parser.set_application(app) - return publish_doctree(text, app.srcdir / docname + '.rst', - reader=SphinxStandaloneReader(app), - parser=parser, - settings_overrides={'env': app.env, - 'gettext_compact': True}) - - @pytest.mark.sphinx(testroot='toctree-glob') def test_toctree(app): text = (".. toctree::\n" @@ -38,7 +25,7 @@ def test_toctree(app): " baz\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'foo'), (None, 'bar/index'), (None, 'baz')], @@ -55,7 +42,7 @@ def test_relative_toctree(app): " ../quux\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'bar/index', text) + doctree = restructuredtext.parse(app, text, 'bar/index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'bar/bar_1'), (None, 'bar/bar_2'), (None, 'bar/bar_3'), @@ -72,7 +59,7 @@ def test_toctree_urls_and_titles(app): " The BAR \n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[('Sphinx', 'https://www.sphinx-doc.org/'), @@ -89,7 +76,7 @@ def test_toctree_glob(app): " *\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'baz'), (None, 'foo'), (None, 'quux')], @@ -103,7 +90,7 @@ def test_toctree_glob(app): " *\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'foo'), (None, 'baz'), (None, 'quux')], @@ -117,7 +104,7 @@ def test_toctree_glob(app): " foo\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'baz'), (None, 'foo'), (None, 'quux'), (None, 'foo')], @@ -132,7 +119,7 @@ def test_toctree_glob_and_url(app): " https://example.com/?q=sphinx\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'https://example.com/?q=sphinx')], @@ -147,7 +134,7 @@ def test_toctree_twice(app): " foo\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'foo'), (None, 'foo')], diff --git a/tests/test_directive_patch.py b/tests/test_directive_patch.py new file mode 100644 index 00000000000..4f61f2d0be7 --- /dev/null +++ b/tests/test_directive_patch.py @@ -0,0 +1,54 @@ +""" + test_directive_patch + ~~~~~~~~~~~~~~~~~~~ + + Test the patched directives. + + :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from docutils import nodes + +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node + + +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})