Repository: executablebooks/MyST-Parser (python)

Issue:
support mermaid
**Describe the solution you'd like**

support mermaid

**Describe alternatives you've considered**

The conf.py I used to make work `mermaid` with `recommonmark`:

```
$ tail conf.py
extensions = [ 'recommonmark', 'sphinxcontrib.mermaid']
from recommonmark.transform import AutoStructify
def setup(app):
    app.add_transform(AutoStructify)
$
```

This mermaid is rendered

```
```mermaid::

  graph LR
    a --> b
\```
```

Note that the above need to have a new line after `mermaid::` and `graph` need to start with a space.

    

Pull request:
✨ NEW: Add `myst_fence_as_directive` config
closes #366

Setting the following config, for example:

```python
extensions = ["myst_parser", "sphinxcontrib.mermaid"]
myst_fence_as_directive = ["mermaid"]
# optional to use https://sphinxcontrib-mermaid-demo.readthedocs.io/en/latest/#directive-options
myst_enable_extensions = ["attrs_block"]
```

allows for one to write

````markdown
{caption="My caption"}
{alt="HTML alt" align=center}
```mermaid
graph LR
a --> b
```
````

and have interoperable rendering with places like GitHub:

{caption="My caption"}
{alt="HTML alt" align=center}
```mermaid
graph LR
a --> b
```

Patch:
diff --git a/myst_parser/config/main.py b/myst_parser/config/main.py
index 4f3889d..f22791a 100644
--- a/myst_parser/config/main.py
+++ b/myst_parser/config/main.py
@@ -166,6 +166,14 @@ def _test_slug_func(text: str) -> str:
     return text[::-1]
 
 
+def check_fence_as_directive(
+    inst: "MdParserConfig", field: dc.Field, value: Any
+) -> None:
+    """Check that the extensions are a sequence of known strings"""
+    deep_iterable(instance_of(str), instance_of((list, tuple, set)))(inst, field, value)
+    setattr(inst, field.name, set(value))
+
+
 @dc.dataclass()
 class MdParserConfig:
     """Configuration options for the Markdown Parser.
@@ -250,6 +258,16 @@ class MdParserConfig:
         },
     )
 
+    fence_as_directive: Set[str] = dc.field(
+        default_factory=set,
+        metadata={
+            "validator": check_fence_as_directive,
+            "help": "Interpret a code fence as a directive, for certain language names. "
+            "This can be useful for fences like dot and mermaid, "
+            "and interoperability with other Markdown renderers.",
+        },
+    )
+
     number_code_blocks: Sequence[str] = dc.field(
         default_factory=list,
         metadata={
diff --git a/myst_parser/mdit_to_docutils/base.py b/myst_parser/mdit_to_docutils/base.py
index 653ce6a..c85f454 100644
--- a/myst_parser/mdit_to_docutils/base.py
+++ b/myst_parser/mdit_to_docutils/base.py
@@ -743,6 +743,13 @@ class DocutilsRenderer(RendererProtocol):
                 return self.render_restructuredtext(token)
             if name.startswith("{") and name.endswith("}"):
                 return self.render_directive(token, name[1:-1], arguments)
+            if name in self.md_config.fence_as_directive:
+                options = {k: str(v) for k, v in token.attrs.items()}
+                if "id" in options:
+                    options["name"] = options.pop("id")
+                return self.render_directive(
+                    token, name, arguments, additional_options=options
+                )
 
         if not name and self.sphinx_env is not None:
             # use the current highlight setting, via the ``highlight`` directive,
@@ -1664,7 +1671,12 @@ class DocutilsRenderer(RendererProtocol):
         self.current_node.extend(newdoc.children)
 
     def render_directive(
-        self, token: SyntaxTreeNode, name: str, arguments: str
+        self,
+        token: SyntaxTreeNode,
+        name: str,
+        arguments: str,
+        *,
+        additional_options: dict[str, str] | None = None,
     ) -> None:
         """Render special fenced code blocks as directives.
 
@@ -1673,7 +1685,13 @@ class DocutilsRenderer(RendererProtocol):
         :param arguments: The remaining text on the same line as the directive name.
         """
         position = token_line(token)
-        nodes_list = self.run_directive(name, arguments, token.content, position)
+        nodes_list = self.run_directive(
+            name,
+            arguments,
+            token.content,
+            position,
+            additional_options=additional_options,
+        )
         self.current_node += nodes_list
 
     def run_directive(


Test patch:
diff --git a/tests/test_renderers/fixtures/myst-config.txt b/tests/test_renderers/fixtures/myst-config.txt
index 62226c9..86e36d7 100644
--- a/tests/test_renderers/fixtures/myst-config.txt
+++ b/tests/test_renderers/fixtures/myst-config.txt
@@ -468,3 +468,31 @@ My paragraph
                 <reference id_link="True" refid="title">
                     reversed
 .
+
+[fence_as_directive] --myst-fence-as-directive=unknown,admonition --myst-enable-extensions=attrs_block
+.
+```unknown
+```
+
+{#myname .class1}
+{a=b}
+```admonition title
+content
+```
+.
+<document source="<string>">
+    <system_message level="2" line="1" source="<string>" type="WARNING">
+        <paragraph>
+            Unknown directive type: 'unknown' [myst.directive_unknown]
+    <system_message level="2" line="6" source="<string>" type="WARNING">
+        <paragraph>
+            'admonition': Unknown option keys: ['a'] (allowed: ['class', 'name']) [myst.directive_parse]
+    <admonition classes="class1" ids="myname" names="myname">
+        <title>
+            title
+        <paragraph>
+            content
+
+<string>:1: (WARNING/2) Unknown directive type: 'unknown' [myst.directive_unknown]
+<string>:6: (WARNING/2) 'admonition': Unknown option keys: ['a'] (allowed: ['class', 'name']) [myst.directive_parse]
+.