Repository: executablebooks/MyST-Parser
Pull request #713 (2023-02-21 21:15:48)

👌 Improve: Non-directive colon-fences
At present, colon fences without a directive specified (e.g. `:::{note}`) are treated as code bocks 

```
:::name
Some text
:::
```

```xml
<code language=name>
    Some text
```

In keeping with the purpose of these block, as containers for nested MyST content, they are now converted to:

```xml
<div class=name>
   <paragraph>
      Some text
```

This is also in keeping with the original pandoc/djot inspiration <https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html#div>

chrisjsewell: cc @rowanc1 @choldgraf 
rowanc1: Makes sense to me, and like that there is a difference between colons and backticks.
codecov[bot]: # [Codecov](https://codecov.io/gh/executablebooks/MyST-Parser/pull/713?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) Report
Base: **89.31**% // Head: **89.36**% // Increases project coverage by **`+0.05%`** :tada:
> Coverage data is based on head [(`599202e`)](https://codecov.io/gh/executablebooks/MyST-Parser/pull/713?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) compared to base [(`ac111ea`)](https://codecov.io/gh/executablebooks/MyST-Parser/commit/ac111ea6f58508b0987091595edfb33db1ee033f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks).
> Patch coverage: 83.33% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #713      +/-   ##
==========================================
+ Coverage   89.31%   89.36%   +0.05%     
==========================================
  Files          23       23              
  Lines        2844     2867      +23     
==========================================
+ Hits         2540     2562      +22     
- Misses        304      305       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| pytests | `89.36% <83.33%> (+0.05%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/executablebooks/MyST-Parser/pull/713?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) | Coverage Δ | |
|---|---|---|
| [myst\_parser/mdit\_to\_docutils/base.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/713?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvbWRpdF90b19kb2N1dGlscy9iYXNlLnB5) | `92.88% <75.00%> (+0.07%)` | :arrow_up: |
| [myst\_parser/parsers/docutils\_.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/713?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvcGFyc2Vycy9kb2N1dGlsc18ucHk=) | `80.81% <91.66%> (+0.81%)` | :arrow_up: |
| [myst\_parser/sphinx\_ext/main.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/713?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvc3BoaW54X2V4dC9tYWluLnB5) | `89.74% <100.00%> (+0.26%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/executablebooks/MyST-Parser/pull/713?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks).


Patch:
diff --git a/docs/syntax/optional.md b/docs/syntax/optional.md
index 1323492..7d6bc01 100644
--- a/docs/syntax/optional.md
+++ b/docs/syntax/optional.md
@@ -403,7 +403,7 @@ However, since Jinja2 substitutions allow for Python methods to be used, you can
 ## Code fences using colons
 
 By adding `"colon_fence"` to `myst_enable_extensions` (in the {{ confpy }}),
-you can also use `:::` delimiters to denote code fences, instead of ```` ``` ````.
+you can also use `:::` delimiters to denote directives, instead of ```` ``` ````.
 
 Using colons instead of back-ticks has the benefit of allowing the content to be rendered correctly, when you are working in any standard Markdown editor.
 It is ideal for admonition type directives (as documented in [Directives](syntax/directives)) or tables with titles, for example:
@@ -477,6 +477,7 @@ This text is **standard** _Markdown_
 This text is **standard** _Markdown_
 :::
 
+
 (syntax/admonitions)=
 
 ## Admonition directives
diff --git a/myst_parser/mdit_to_docutils/base.py b/myst_parser/mdit_to_docutils/base.py
index 22bb799..9613706 100644
--- a/myst_parser/mdit_to_docutils/base.py
+++ b/myst_parser/mdit_to_docutils/base.py
@@ -1479,15 +1479,27 @@ class DocutilsRenderer(RendererProtocol):
     def render_colon_fence(self, token: SyntaxTreeNode) -> None:
         """Render a code fence with ``:`` colon delimiters."""
 
-        if token.content.startswith(":::"):
-            # the content starts with a nested fence block,
-            # but must distinguish between ``:options:``, so we add a new line
-            assert token.token is not None, '"colon_fence" must have a `token`'
-            linear_token = token.token.copy()
-            linear_token.content = "\n" + linear_token.content
-            token.token = linear_token
-
-        return self.render_fence(token)
+        info = token.info.strip() if token.info else token.info
+        name = info.split()[0] if info else ""
+
+        if name.startswith("{") and name.endswith("}"):
+            if token.content.startswith(":::"):
+                # the content starts with a nested fence block,
+                # but must distinguish between ``:options:``, so we add a new line
+                assert token.token is not None, '"colon_fence" must have a `token`'
+                linear_token = token.token.copy()
+                linear_token.content = "\n" + linear_token.content
+                token.token = linear_token
+            return self.render_directive(token)
+
+        container = nodes.container(is_div=True)
+        self.add_line_and_source_path(container, token)
+        self.copy_attributes(token, container, ("class", "id"))
+        if name:
+            # note, as per djot, the name is added to the end of the classes
+            container["classes"].append(name)
+        self.current_node.append(container)
+        self.nested_render_text(token.content, token_line(token, 0))
 
     def render_dl(self, token: SyntaxTreeNode) -> None:
         """Render a definition list."""
diff --git a/myst_parser/parsers/docutils_.py b/myst_parser/parsers/docutils_.py
index 61c7bb4..472f947 100644
--- a/myst_parser/parsers/docutils_.py
+++ b/myst_parser/parsers/docutils_.py
@@ -261,6 +261,8 @@ class Parser(RstParser):
 
         HTMLTranslator.visit_rubric = visit_rubric_html
         HTMLTranslator.depart_rubric = depart_rubric_html
+        HTMLTranslator.visit_container = visit_container_html
+        HTMLTranslator.depart_container = depart_container_html
 
         self.setup_parse(inputstring, document)
 
@@ -426,3 +428,27 @@ def depart_rubric_html(self, node):
         self.body.append(f'</h{node["level"]}>\n')
     else:
         self.body.append("</p>\n")
+
+
+def visit_container_html(self, node: nodes.Node):
+    """Override the default HTML visit method for container nodes.
+
+    to remove the "container" class for divs
+    this avoids CSS clashes with the bootstrap theme
+    """
+    classes = "docutils container"
+    attrs = {}
+    if node.get("is_div", False):
+        # we don't want the CSS for container for these nodes
+        classes = "docutils"
+    if "style" in node:
+        attrs["style"] = node["style"]
+    self.body.append(self.starttag(node, "div", CLASS=classes, **attrs))
+
+
+def depart_container_html(self, node: nodes.Node):
+    """Override the default HTML depart method for container nodes.
+
+    See explanation in `visit_container_html`
+    """
+    self.body.append("</div>\n")
diff --git a/myst_parser/sphinx_ext/main.py b/myst_parser/sphinx_ext/main.py
index 2dd0c04..01c30d9 100644
--- a/myst_parser/sphinx_ext/main.py
+++ b/myst_parser/sphinx_ext/main.py
@@ -4,7 +4,12 @@ from typing import Any
 from docutils import nodes
 from sphinx.application import Sphinx
 
-from myst_parser.parsers.docutils_ import depart_rubric_html, visit_rubric_html
+from myst_parser.parsers.docutils_ import (
+    depart_container_html,
+    depart_rubric_html,
+    visit_container_html,
+    visit_rubric_html,
+)
 from myst_parser.warnings_ import MystWarnings
 
 
@@ -31,9 +36,18 @@ def setup_sphinx(app: Sphinx, load_parser=False):
     app.add_post_transform(MystReferenceResolver)
 
     # override only the html writer visit methods for rubric, to use the "level" attribute
+    # this allows for nested headers to be correctly rendered
     app.add_node(
         nodes.rubric, override=True, html=(visit_rubric_html, depart_rubric_html)
     )
+    # override only the html writer visit methods for container,
+    # to remove the "container" class for divs
+    # this avoids CSS clashes with the bootstrap theme
+    app.add_node(
+        nodes.container,
+        override=True,
+        html=(visit_container_html, depart_container_html),
+    )
 
     for name, default, field in MdParserConfig().as_triple():
         if "sphinx" not in field.metadata.get("omit", []):


Test patch:
diff --git a/tests/test_renderers/fixtures/containers.md b/tests/test_renderers/fixtures/containers.md
index f67bea8..a8bf0e3 100644
--- a/tests/test_renderers/fixtures/containers.md
+++ b/tests/test_renderers/fixtures/containers.md
@@ -29,3 +29,31 @@ Admonition with options:
             <emphasis>
                 hallo
 .
+
+empty name:
+.
+:::
+This is **content**
+:::
+.
+<document source="<src>/index.md">
+    <container is_div="True">
+    <paragraph>
+        This is
+        <strong>
+            content
+.
+
+has name:
+.
+:::name
+This is **content**
+:::
+.
+<document source="<src>/index.md">
+    <container classes="name" is_div="True">
+    <paragraph>
+        This is
+        <strong>
+            content
+.
diff --git a/tests/test_sphinx/sourcedirs/extended_syntaxes/index.md b/tests/test_sphinx/sourcedirs/extended_syntaxes/index.md
index e9fcea9..08ae970 100644
--- a/tests/test_sphinx/sourcedirs/extended_syntaxes/index.md
+++ b/tests/test_sphinx/sourcedirs/extended_syntaxes/index.md
@@ -55,6 +55,10 @@ This is a caption in **Markdown**
 This is a caption in **Markdown**
 :::
 
+:::other
+Hallo *there*
+:::
+
 linkify URL: www.example.com
 
 - [ ] hallo
diff --git a/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.html b/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.html
index cc6e85b..7ef7e37 100644
--- a/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.html
+++ b/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.html
@@ -131,6 +131,11 @@ b=2
       </p>
      </figcaption>
     </figure>
+    <div class="other docutils">
+    </div>
+    <p>
+     Hallo *there*
+    </p>
     <p>
      linkify URL:
      <a class="reference external" href="http://www.example.com">
diff --git a/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.xml b/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.xml
index 8cefbe3..860ce8f 100644
--- a/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.xml
+++ b/tests/test_sphinx/test_sphinx_builds/test_extended_syntaxes.xml
@@ -69,6 +69,9 @@
             <image alt="fishy" candidates="{'*': 'fun-fish.png'}" classes="bg-primary mb-1" uri="fun-fish.png" width="200px">
             <caption>
                 This is a caption in **Markdown**
+        <container classes="other" is_div="True">
+        <paragraph>
+            Hallo *there*
         <paragraph>
             linkify URL: 
             <reference refuri="http://www.example.com">