Repository: sphinx-doc/sphinx

Issue:
Use reStructuredText's `code` directive
Since [docutils 0.9](http://docutils.sourceforge.net/RELEASE-NOTES.html#release-0-9-2012-05-02), reStructuredText supports [`code` directive](http://docutils.sourceforge.net/docs/ref/rst/directives.html#code), but Sphinx, unfortunately, can't handle this. By design, `code` directive supports syntax highlighting, but Sphinx ignores this due to `visit_literal_block` reimplementation in [custom `HTMLTranslator`](https://github.com/sphinx-doc/sphinx/blob/0dd6ce3063ddb2f8b7f084e923ecc7f4ab4e9423/sphinx/writers/html.py#L307-L335).

So basically:
- Sphinx handles `code` directive as literal block.
- There's no syntax highlighting in produced block (it's supposed to be generated by docutils, and it does, but as children nodes) due to HTMLTranslator limitations.

Well, I see two possible ways to solve this issue:
1. Use Sphinx's `CodeBlock` implementation for `code` directive:
   
   ``` python
   directives.register_directive('code', CodeBlock)
   ```
   
   Pros:
   - easy to implement
   - features/options similar `code-block` / `sourcecode`
   
   Cons:
   - doesn't compatible with docutils implementation
2. Use docutils' (native) implementation for `code` directive.
   
   Pros:
   - backward compatible, since standard (native) implementation is used
   
   Cons:
   - requires changes in many places, since we need:
     - distinguish Sphinx's code blocks from docutils' one on HTMLTranslator level (could be solved by injecting some marker from Sphinx's implementation)
     - use default `visit_literal_block` implementation for non-Sphinx code blocks
     - pass `short` Pygments CSS notation to docutils (`long` is default)
   - user will lack of Sphinx-specific `code-block` options

Folks, could you please share your opinion on this?

@birkenfeld It'd be nice to see your input. 


Discussion:
tk0miya: I did not update our document. I suspect two "code block" directives will confuse users. This change is only needed for compatibility with standard reStructuredText, I believe.

Patch:
diff --git a/CHANGES b/CHANGES
index 4c3c5ba09..21e4dd820 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 c639885f6..1f4520541 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 2d8ca636a..e89355cf7 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 {


Test patch:
diff --git a/sphinx/testing/restructuredtext.py b/sphinx/testing/restructuredtext.py
new file mode 100644
index 000000000..8bf1c041e
--- /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 cbbebee5c..376b9cd69 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 <bar/index>\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 000000000..4f61f2d0b
--- /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})