Repository: onnx/onnx

Issue:
Cast-19 table in onnx.ai docs is broken
# Bug Report

### Is the issue related to model conversion?
<!-- If the ONNX checker reports issues with this model then this is most probably related to the converter used to convert the original framework model to ONNX. Please create this bug in the appropriate converter's GitHub repo (pytorch, tensorflow-onnx, sklearn-onnx, keras-onnx, onnxmltools) to get the best help. -->

### Describe the bug
<!-- Please describe the bug clearly and concisely -->
Tables in https://onnx.ai/onnx/operators/onnx__Cast.html#cast-19 is broken but markdown version in https://github.com/onnx/onnx/blob/main/docs/Operators.md#Cast is fine.

### System information
<!--
- OS Platform and Distribution (*e.g. Linux Ubuntu 20.04*):
- ONNX version (*e.g. 1.13*):
- Python version:
- GCC/Compiler version (if compiling from source):
- CMake version:
- Protobuf version:
- Visual Studio version (if applicable):-->


### Reproduction instructions
<!--
- Describe the code to reproduce the behavior.
```
import onnx
model = onnx.load('model.onnx')
...
```
- Attach the ONNX model to the issue (where applicable)-->

See https://onnx.ai/onnx/operators/onnx__Cast.html#cast-19

### Expected behavior
<!-- A clear and concise description of what you expected to happen. -->

### Notes
<!-- Any additional information -->
I don't know why but markdown syntax and sphinx syntax seems to have some diff?


Discussion:
Hu @take-cheeze,
Thanks for catching this. Yes, markdown syntax and sphinx syntax are slightly different. For instance, https://github.com/onnx/onnx/issues/4725. cc @xadupre 
The syntax is different. I wrote some code to improve the rendering but parsing tables was beyond scope. [MyST](https://myst-parser.readthedocs.io/en/v0.13.5/index.html) would be one option. I did not try it.
I think we can use myst. We can update https://github.com/onnx/onnx/blob/main/docs/docsgen/source/onnx_sphinx.py to create markdown files.

Resolving patch:
diff --git a/docs/docsgen/source/conf.py b/docs/docsgen/source/conf.py
--- a/docs/docsgen/source/conf.py
+++ b/docs/docsgen/source/conf.py
@@ -36,12 +36,12 @@
 # -- General configuration ---------------------------------------------------
 
 extensions = [
+    "myst_parser",
     "onnx_sphinx",
     "sphinx_copybutton",
     "sphinx_exec_code",
     "sphinx_tabs.tabs",
     "sphinx.ext.autodoc",
-    "sphinx.ext.autodoc",
     "sphinx.ext.autosummary",
     "sphinx.ext.coverage",
     "sphinx.ext.doctest",
@@ -54,6 +54,20 @@
     "sphinx.ext.viewcode",
 ]
 
+myst_enable_extensions = [
+    "amsmath",
+    "colon_fence",
+    "dollarmath",
+    "fieldlist",
+    "html_admonition",
+    "html_image",
+    "linkify",
+    "smartquotes",
+    "strikethrough",
+    "substitution",
+    "tasklist",
+]
+
 coverage_show_missing_items = True
 exclude_patterns = []
 graphviz_output_format = "svg"
diff --git a/docs/docsgen/source/onnx_sphinx.py b/docs/docsgen/source/onnx_sphinx.py
--- a/docs/docsgen/source/onnx_sphinx.py
+++ b/docs/docsgen/source/onnx_sphinx.py
@@ -1,119 +1,86 @@
 # Copyright (c) ONNX Project Contributors
 #
 # SPDX-License-Identifier: Apache-2.0
-
-# pylint: disable=C0415,R0912,R0913,R0914,R0915
-"""
-Automates the generation of ONNX operators.
-"""
+"""Automates the generation of ONNX operators."""
+import difflib
 import importlib
 import inspect
 import keyword
 import os
+import pathlib
 import re
 import sys
 import textwrap
-from difflib import Differ
+from typing import Any
 
+import jinja2
 import numpy as np
 from sphinx.util import logging
 
 import onnx
 from onnx.backend.test.case.base import _Exporter
-from onnx.defs import OpSchema, get_all_schemas_with_history, get_schema
-from onnx.numpy_helper import to_array
-from onnx.onnx_cpp2py_export.defs import (  # pylint: disable=E1101,E0611,E0401
-    SchemaError,
-)
-
-
-def get_template():  # type: ignore
-    try:
-        from jinja2 import Template
-    except ImportError:
-
-        class Template:  # type: ignore
-            "Docstring template"
-
-            def __init__(self, *args):
-                pass
-
-            def render(self, **context):
-                "render"
-                schemas = context["schemas"]
-                rows = []
-                for sch in schemas:
-                    doc = sch.doc or ""
-                    name = sch.name
-                    if name is None:
-                        raise RuntimeError("An operator must have a name.")
-                    rows.extend([name, "=" * len(name), "", doc, ""])
-                return "\n".join(rows)
-
-    return Template
+from onnx.defs import OpSchema
 
 
-def _get_diff_template():  # type: ignore
-    Template = get_template()
-    return Template(
+def _get_diff_template():
+    return jinja2.Template(
         textwrap.dedent(
             """
-        <div id="{{ div_name }}"></div>
-        <link rel="stylesheet" type="text/css" href="../_static/diff2html.min.css" />
-        <script type="text/javascript" src="../_static/diff2html-ui-slim.min.js"></script>
-        <script>
-        const diffString = `
-        --- a/{{ op_name }}{{ version1 }}
-        +++ b/{{ op_name }}{{ version2 }}
-        @@ -1 +1 @@
-        {{ diff_content }}
-        `;
-
-        document.addEventListener('DOMContentLoaded', function () {
-        var targetElement = document.getElementById('{{ div_name }}');
-        var configuration = {
-            drawFileList: true,
-            fileListToggle: false,
-            fileListStartVisible: false,
-            fileContentToggle: false,
-            matching: 'lines',
-            outputFormat: 'line-by-line',
-            synchronisedScroll: true,
-            highlight: true,
-            renderNothingWhenEmpty: false,
-        };
-        var diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);
-        diff2htmlUi.draw();
-        diff2htmlUi.highlightCode();
-        });
-        </script>
-        """
+            <div id="{{ div_name }}"></div>
+            <link rel="stylesheet" type="text/css" href="../_static/diff2html.min.css" />
+            <script type="text/javascript" src="../_static/diff2html-ui-slim.min.js"></script>
+            <script>
+            const diffString = `
+            --- a/{{ op_name }}{{ version1 }}
+            +++ b/{{ op_name }}{{ version2 }}
+            @@ -1 +1 @@
+            {{ diff_content }}
+            `;
+
+            document.addEventListener('DOMContentLoaded', function () {
+            var targetElement = document.getElementById('{{ div_name }}');
+            var configuration = {
+                drawFileList: true,
+                fileListToggle: false,
+                fileListStartVisible: false,
+                fileContentToggle: false,
+                matching: 'lines',
+                outputFormat: 'line-by-line',
+                synchronisedScroll: true,
+                highlight: true,
+                renderNothingWhenEmpty: false,
+            };
+            var diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);
+            diff2htmlUi.draw();
+            diff2htmlUi.highlightCode();
+            });
+            </script>
+            """
         ),
         autoescape=True,
     )
 
 
-def _get_ops_template():  # type: ignore
-    Template = get_template()
-    return Template(
+def _get_ops_template():
+    return jinja2.Template(
         textwrap.dedent(
-            """
+        """
         {% for sch in schemas %}
 
         .. tag-diff-insert.
-        .. _l-onnx-op{{sch.domain.lower().replace(".", "-")}}-{{sch.name.lower()}}-{{str(sch.since_version)}}:
 
-        {{format_name_with_domain(sch)}}
-        {{'=' * len(format_name_with_domain(sch))}}
+        (l-onnx-op{{sch.domain.lower().replace(".", "-")}}-{{sch.name.lower()}}-{{str(sch.since_version)}})=
 
-        **Version**
+        ## {{format_name_with_domain(sch)}}
 
-        * **name**: `{{sch.name}} (GitHub) <{{build_doc_url(sch)}}{{sch.name}}>`_
-        * **domain**: **{% if sch.domain == '' %}main{% else %}{{sch.domain}}{% endif %}**
-        * **since_version**: **{{sch.since_version}}**
-        * **function**: {{sch.has_function or sch.has_context_dependent_function}}
-        * **support_level**: {{sch.support_level}}
-        * **shape inference**: {{sch.has_type_and_shape_inference_function}}
+        ### Version
+
+        - **name**: [{{sch.name}} (GitHub)]({{build_doc_url(sch)}}{{sch.name}})
+        - **domain**: `{% if sch.domain == '' %}main{% else %}{{sch.domain}}{% endif %}`
+        - **since_version**: `{{sch.since_version}}`
+        - **function**: `{{sch.has_function or sch.has_context_dependent_function}}`
+        - **support_level**: `{{sch.support_level}}`
+        - **shape inference**: `{{sch.has_type_and_shape_inference_function}}`
 
         {% if sch.support_level == OpSchema.SupportType.EXPERIMENTAL %}
         No versioning maintained for experimental ops.
@@ -128,12 +95,12 @@ def _get_ops_template():  # type: ignore
         {% endif %}
         {% endif %}
 
-        **Summary**
+        ### Summary
 
         {{process_documentation(sch.doc)}}
         {% if sch.attributes %}
 
-        **Attributes**
+        ### Attributes
 
         {% for _, attr in sorted(sch.attributes.items())
         %}* **{{attr.name}} - {{str(attr.type).split('.')[-1]}}**{%
@@ -144,29 +111,29 @@ def _get_ops_template():  # type: ignore
         {% endif %}
         {% if sch.inputs %}
 
-        **Inputs**
+        ### Inputs
 
         {% if sch.min_input != sch.max_input %}Between {{sch.min_input
         }} and {{sch.max_input}} inputs.
         {% endif %}
         {% for ii, inp in enumerate(sch.inputs) %}
-        * **{{getname(inp, ii)}}**{{format_option(inp)}} - **{{inp.type_str}}**:
+        - **{{getname(inp, ii)}}**{{format_option(inp)}} - **{{inp.type_str}}**:
         {{text_wrap(inp.description, 2)}}{% endfor %}
         {% endif %}
         {% if sch.outputs %}
 
-        **Outputs**
+        ### Outputs
 
         {% if sch.min_output != sch.max_output %}Between {{sch.min_output
         }} and {{sch.max_output}} outputs.
         {% endif %}
         {% for ii, out in enumerate(sch.outputs) %}
-        * **{{getname(out, ii)}}**{{format_option(out)}} - **{{out.type_str}}**:
+        - **{{getname(out, ii)}}**{{format_option(out)}} - **{{out.type_str}}**:
         {{text_wrap(out.description, 2)}}{% endfor %}
         {% endif %}
         {% if sch.type_constraints %}
 
-        **Type Constraints**
+        ### Type Constraints
 
         {% for ii, type_constraint in enumerate(sch.type_constraints)
         %}* {{get_constraint(type_constraint, ii)}}:
@@ -175,58 +142,57 @@ def _get_ops_template():  # type: ignore
         {% endif %}
         {% if get_onnx_example and is_last_schema(sch): %}
 
-        **Examples**
+        ### Examples
 
         {% for example, code in get_onnx_example(sch.name).items(): %}
 
-        **{{ example }}**
-
-        ::
+        #### {{ example }}
 
+        ```python
         {{ format_example(code) }}
+        ```
         {% endfor %}
         {% endif %}
         {% endfor %}
-    """
+        """
         ),
-        autoescape=True,
+        autoescape=False,
     )
 
 
-def _get_main_template():  # type: ignore
-    Template = get_template()
-    return Template(
+def _get_main_template():
+    return jinja2.Template(
         textwrap.dedent(
             """
-        .. _l-onnx-operators:
+            .. _l-onnx-operators:
 
-        {{ title }}
-        {{ "=" * len(title) }}
+            {{ title }}
+            {{ "=" * len(title) }}
 
-        Lists out all the ONNX operators. For each operator, lists out the usage guide,
-        parameters, examples, and line-by-line version history.
-        This section also includes tables detailing each operator
-        with its versions, as done in `Operators.md
-        <https://github.com/onnx/onnx/blob/main/docs/Operators.md>`_.
+            Lists out all the ONNX operators. For each operator, lists out the usage guide,
+            parameters, examples, and line-by-line version history.
+            This section also includes tables detailing each operator
+            with its versions, as done in `Operators.md
+            <https://github.com/onnx/onnx/blob/main/docs/Operators.md>`_.
 
-        All examples end by calling function `expect`.
-        which checks a runtime produces the expected output for this example.
-        One implementation based on `onnxruntime <https://onnxruntime.ai/>`_
-        can be found at :ref:`l-function-expect`.
+            All examples end by calling function `expect`.
+            which checks a runtime produces the expected output for this example.
+            One implementation based on `onnxruntime <https://onnxruntime.ai/>`_
+            can be found at :ref:`l-function-expect`.
 
-        .. toctree::
-            :hidden:
+            .. toctree::
+                :hidden:
 
-            ../expect_onnxruntime
-            {% for p in pages %}{{ os.path.split(p)[-1] }}
-            {% endfor %}
+                ../expect_onnxruntime
+                {% for p in pages %}{{ os.path.split(p)[-1] }}
+                {% endfor %}
 
-        .. tabs::
+            .. tabs::
 
-            {% for t in tabs %}.. tab:: {{ t.domain_name }}
-                {{ t.render(indent="        ") }}
-            {% endfor %}
-    """
+                {% for t in tabs %}.. tab:: {{ t.domain_name }}
+                    {{ t.render(indent="        ") }}
+                {% endfor %}
+            """
         ),
         autoescape=True,
     )
@@ -245,7 +211,7 @@ def _clean_unicode(text):
 _template_diff = _get_diff_template()
 _template_operator = _get_ops_template()
 _template_main = _get_main_template()
-__get_all_schemas_with_history = None
+_all_schemas_with_history = None
 
 
 _attribute_conversion_functions = {
@@ -259,16 +225,16 @@ def _clean_unicode(text):
     # AttributeProto.SPARSE_TENSORS(12)
     onnx.AttributeProto.STRING: lambda att: att.s.decode("utf-8"),
     onnx.AttributeProto.STRINGS: lambda att: [s.decode("utf-8") for s in att.strings],
-    onnx.AttributeProto.TENSOR: lambda att: to_array(att.t),
+    onnx.AttributeProto.TENSOR: lambda att: onnx.numpy_helper.to_array(att.t),
     # AttributeProto.TENSORS(9)
     # onnx.AttributeProto.TYPE_PROTO: lambda att: OnnxType(att.tp),
     # AttributeProto.TYPE_PROTOS(14)
 }
 
 
-def _populate__get_all_schemas_with_history():  # type: ignore
-    res = {}  # type: ignore
-    for schema in get_all_schemas_with_history():
+def _populate_all_schemas_with_history():
+    res: dict[str, Any] = {}
+    for schema in onnx.defs.get_all_schemas_with_history():
         domain = schema.domain
         version = schema.since_version
         name = schema.name
@@ -281,21 +247,14 @@ def _populate__get_all_schemas_with_history():  # type: ignore
     return res
 
 
-def _get_all_schemas_with_history():  # type: ignore
-    global __get_all_schemas_with_history  # pylint: disable=W0603
-    if __get_all_schemas_with_history is None:
-        __get_all_schemas_with_history = _populate__get_all_schemas_with_history()
-    return __get_all_schemas_with_history
-
+def _get_all_schemas_with_history():
+    global _all_schemas_with_history  # pylint: disable=global-statement
+    if _all_schemas_with_history is None:
+        _all_schemas_with_history = _populate_all_schemas_with_history()
+    return _all_schemas_with_history
 
-def get_domain_list():  # type: ignore
-    """
-    Returns the list of available domains.
-    """
-    return list(sorted(set(map(lambda s: s.domain, get_all_schemas_with_history()))))
 
-
-def get_operator_schemas(op_name, version=None, domain=None):  # type: ignore
+def get_operator_schemas(op_name, version=None, domain=None):
     """
     Returns all schemas mapped to an operator name.
     :param op_name: name of the operator
@@ -305,7 +264,7 @@ def get_operator_schemas(op_name, version=None, domain=None):  # type: ignore
     """
     if version == "last" and op_name is not None:
         if domain is not None:
-            return [get_schema(op_name, domain=domain)]
+            return [onnx.defs.get_schema(op_name, domain=domain)]
     all_schemas = _get_all_schemas_with_history()
     if domain is None:
         domains = []
@@ -325,8 +284,8 @@ def get_operator_schemas(op_name, version=None, domain=None):  # type: ignore
                     sch.extend(v.values())
                 elif version == "last" and (dom == "" or "onnx" in dom):
                     try:
-                        sch.append(get_schema(op, domain=dom))
-                    except SchemaError:  # pragma: no cover
+                        sch.append(onnx.defs.get_schema(op, domain=dom))
+                    except onnx.defs.SchemaError:
                         sch.append(v[max(v)])
                 elif version == "last":
                     sch.append(v[max(v)])
@@ -344,7 +303,7 @@ def get_operator_schemas(op_name, version=None, domain=None):  # type: ignore
     return [v[-1] for v in vals]
 
 
-def get_rst_doc(  # type: ignore
+def get_markdown_doc(
     folder,
     op_name=None,
     domain=None,
@@ -354,7 +313,7 @@ def get_rst_doc(  # type: ignore
     example=False,
 ):
     """
-    Returns a documentation in RST format
+    Returns a documentation in Markdown format
     for all :class:`OnnxOperator`.
 
     :param op_name: operator name of None for all
@@ -364,15 +323,9 @@ def get_rst_doc(  # type: ignore
     :param diff: highlights differences between two versions
     :param example: add example to the documentation
     :return: string
-    The function relies on module `jinja2` or replaces it
-    with a simple rendering if not present.
     """
     schemas = get_operator_schemas(op_name, domain=domain, version=version)
 
-    # from onnx.backend.sample.ops import collect_sample_implementations
-    # from onnx.backend.test.case import collect_snippets
-    # SNIPPETS = collect_snippets()
-    # SAMPLE_IMPLEMENTATIONS = collect_sample_implementations()
     def format_name_with_domain(sch):
         if version == "last":
             if sch.domain:
@@ -395,7 +348,6 @@ def format_option(obj):
         return ""
 
     def format_example(code):
-        code = textwrap.indent(code, "    ")
         return code
 
     def get_constraint(const, ii):
@@ -405,7 +357,7 @@ def get_constraint(const, ii):
             name = str(ii)
         name = f"**{name}** in ("
         if const.allowed_type_strs:
-            types = [f"``{type_str}``" for type_str in sorted(const.allowed_type_strs)]
+            types = [f"`{type_str}`" for type_str in sorted(const.allowed_type_strs)]
             text = ", ".join(types)
             name += " " + text + " )"
         return name
@@ -420,41 +372,17 @@ def process_documentation(doc):
         if doc is None:
             doc = ""
         if not isinstance(doc, str):
-            raise TypeError(  # pragma: no cover
+            raise TypeError(
                 f"doc must be a string not {type(doc)!r} - {doc + 42!r}."
             )
-        doc = textwrap.dedent(doc)
         main_docs_url = "https://github.com/onnx/onnx/blob/main/"
         rep = {
-            "[the doc](IR.md)": "`ONNX <{0}docs/IR.md>`_",
-            "[the doc](Broadcasting.md)": "`Broadcasting in ONNX <{0}docs/Broadcasting.md>`_",
-            "<dl>": "",
-            "</dl>": "",
-            "<dt>": "* ",
-            "<dd>": "  ",
-            "</dt>": "",
-            "</dd>": "",
-            "<tt>": "``",
-            "</tt>": "``",
-            "<br>": "\n",
+            "[the doc](IR.md)": f"[ONNX IR]({main_docs_url}docs/IR.md)",
+            "[the doc](Broadcasting.md)": f"[Broadcasting in ONNX]({main_docs_url}docs/Broadcasting.md)",
         }
-        for k, v in rep.items():
-            doc = doc.replace(k, v.format(main_docs_url))
-        move = 0
-        lines = []
-        for line in doc.split("\n"):
-            if line.startswith("```"):
-                if move > 0:
-                    move -= 4
-                    lines.append("\n")
-                else:
-                    lines.append("::\n")
-                    move += 4
-            elif move > 0:
-                lines.append(" " * move + line)
-            else:
-                lines.append(line)
-        return "\n".join(lines)
+        for key, value in rep.items():
+            doc = doc.replace(key, value)
+        return textwrap.dedent(doc)
 
     def build_doc_url(sch):
         doc_url = "https://github.com/onnx/onnx/blob/main/docs/Operators"
@@ -486,22 +414,20 @@ def clean_default_value(attr):
         ):
             if attr.type in _attribute_conversion_functions:
                 sval = _attribute_conversion_functions[attr.type](default_value)
-                return f"(default is ``{sval!r}``)"
+                return f"(default is `{sval!r}`)"
 
         if isinstance(default_value, list):
             sval = [format_default_value(val) for val in default_value]
         else:
             sval = format_default_value(default_value)
-        return f"(default is ``{sval!r}``)"
+        return f"(default is `{sval!r}`)"
 
     def text_wrap(text, indent):
         s = " " * indent
         lines = textwrap.wrap(text, initial_indent=s, subsequent_indent=s)
         return "\n".join(lines)
 
-    fnwd = format_name_with_domain
-    tmpl = _template_operator
-    docs = tmpl.render(
+    docs = _template_operator.render(
         schemas=schemas,
         OpSchema=OpSchema,
         len=len,
@@ -511,7 +437,7 @@ def text_wrap(text, indent):
         get_constraint=get_constraint,
         getname=getname,
         enumerate=enumerate,
-        format_name_with_domain=fnwd,
+        format_name_with_domain=format_name_with_domain,
         process_documentation=process_documentation,
         build_doc_url=build_doc_url,
         text_wrap=text_wrap,
@@ -521,7 +447,6 @@ def text_wrap(text, indent):
         format_example=format_example,
         is_last_schema=is_last_schema,
     )
-    docs = _clean_unicode(docs)
 
     d_links = {}
     for schema in schemas:
@@ -562,41 +487,41 @@ def text_wrap(text, indent):
     return docs, d_links
 
 
-def _insert_diff(folder, docs, split=".. tag-diff-insert.", op_name=None, version=None, domain=None):  # type: ignore
+def _insert_diff(folder, docs, split=".. tag-diff-insert.", op_name=None, version=None, domain=None):
     """
     Splits a using `split`, insert HTML differences between pieces.
     The function relies on package `pyquickhelper`.
     """
-    spl = docs.split(split)
-    if len(spl) <= 1:
+    doc_parts = docs.split(split)
+    if len(doc_parts) <= 1:
         return docs
 
     reg = re.compile("([A-Z][A-Za-z0-9_]*) - ([0-9]+)")
 
-    d_links = {}  # type: ignore
-    pieces = [spl[0]]  # type: ignore
-    mds = []  # type: ignore
-    for i in range(1, len(spl)):
-        spl1 = spl[i - 1].strip("\n ")
-        spl2 = spl[i].strip("\n ")
+    d_links = {}
+    pieces = [doc_parts[0]]
+    mds = []
+    for i in range(1, len(doc_parts)):
+        spl1 = doc_parts[i - 1].strip("\n ")
+        spl2 = doc_parts[i].strip("\n ")
         vers1 = reg.findall(spl1)
         vers2 = reg.findall(spl2)
 
-        spl1 = spl1.split("**Examples**")[0].replace("`", "")
-        spl2 = spl2.split("**Examples**")[0].replace("`", "")
-        spl1 = spl1.split("**Summary**")[-1].strip("\n ")
-        spl2 = spl2.split("**Summary**")[-1].strip("\n ")
+        spl1 = spl1.split("### Examples")[0].replace("`", "")
+        spl2 = spl2.split("### Examples")[0].replace("`", "")
+        spl1 = spl1.split("### Summary")[-1].strip("\n ")
+        spl2 = spl2.split("### Summary")[-1].strip("\n ")
         if len(spl1) < 5 or len(spl2) < 5:
-            pieces.append(spl[i])
+            pieces.append(doc_parts[i])
             continue
-        if len(vers1) == 0:
+        if not vers1:
             raise ValueError(f"Unable to find version {version!r} in\n{spl1}")
-        if len(vers2) == 0:
+        if not vers2:
             raise ValueError(f"Unable to find version {version!r} in\n{spl2}")
         v2 = vers2[0][1]
         v1 = vers1[0][1]
 
-        if len(mds) == 0:
+        if not mds:
             mds.append(
                 (v1, textwrap.dedent(spl1.strip(" \n\r\t")).splitlines(keepends=True))
             )
@@ -605,19 +530,23 @@ def _insert_diff(folder, docs, split=".. tag-diff-insert.", op_name=None, versio
         )
 
         if len(mds) > 1:
-            pieces.extend([".. toctree::", ""])
+            show_diff_toc = True
+        else:
+            show_diff_toc = False
+
+        if show_diff_toc:
+            pieces.append("```{toctree}")
 
         for di in range(len(mds) - 1):
             dj = len(mds) - 1
 
             v1, s1 = mds[di]
             v2, s2 = mds[dj]
-            d = Differ()
-            result = list(d.compare(s2, s1))
+            differ = difflib.Differ()
+            result = list(differ.compare(s2, s1))
             raw = "".join(result)
 
-            tmpl = _template_diff
-            diff = tmpl.render(
+            diff = _template_diff.render(
                 op_name=op_name,
                 version1=v2,
                 version2=v1,
@@ -629,8 +558,8 @@ def _insert_diff(folder, docs, split=".. tag-diff-insert.", op_name=None, versio
             title = f"{op_name} - {v2} vs {v1}"
 
             name = f"text_diff_{op_name}_{v2}_{v1}"
-            sdom = domain.replace(".", "-")
-            link = f"l-onnx-op{sdom}-{op_name.lower()}-d{v2}-{v1}"
+            domain_str = domain.replace(".", "-")
+            link = f"l-onnx-op{domain_str}-{op_name.lower()}-d{v2}-{v1}"
             d_links[int(v2), int(v1)] = link
             content = "\n".join(
                 [
@@ -651,23 +580,20 @@ def _insert_diff(folder, docs, split=".. tag-diff-insert.", op_name=None, versio
                 ]
             )
             filename = os.path.join(folder, name + ".rst")
-            if os.path.exists(filename):
-                with open(filename, "r", encoding="utf-8") as f:
-                    old_content = f.read()
-                    write = old_content != content
-            else:
-                write = True
-            if write:
-                with open(filename, "w", encoding="utf-8") as f:
-                    f.write(content)
-            pieces.append(f"    {name}")
+            pathlib.Path(filename).write_text(content, encoding="utf-8")
+            # Add diff page to the toctree using myst syntax
+            pieces.append(name)
 
-        pieces.extend(["", spl[i]])
+        if show_diff_toc:
+            # End the toctree
+            pieces.append("```")
+
+        pieces.extend(["", doc_parts[i]])
 
     return "\n".join(pieces), d_links
 
 
-def change_style(name: str) -> str:
+def pascal_to_snake_case(name: str) -> str:
     """
     Switches from *AaBb* into *aa_bb*.
     :param name: name to convert
@@ -682,13 +608,13 @@ def _process_example(code: str) -> str:
     """
     Add necessary imports to make the example work.
     """
-    code = code.replace("  # type: ignore", "")
+    code = code.replace("", "")
     missing_imports = ["import numpy as np", "import onnx"]
-    elements = missing_imports + ["", "", code.strip("\n"), ""]
+    elements = [*missing_imports, "", "", code.strip("\n")]
     return "\n".join(elements)
 
 
-def get_onnx_example(op_name):  # type: ignore
+def get_onnx_example(op_name):
     """
     Retrieves examples associated to one operator
     stored in onnx packages.
@@ -698,7 +624,7 @@ def get_onnx_example(op_name):  # type: ignore
     """
     modules = [
         f"onnx.backend.test.case.node.{op_name.lower()}",
-        f"onnx.backend.test.case.node.{change_style(op_name).lower()}",
+        f"onnx.backend.test.case.node.{pascal_to_snake_case(op_name)}",
     ]
     module = None
     for m in modules:
@@ -710,7 +636,7 @@ def get_onnx_example(op_name):  # type: ignore
     if module is None:
         # Unable to find an example for 'op_name'.
         return {}
-    results = {}  # type: ignore
+    results: dict[str, Any] = {}
     for v in mod.__dict__.values():
         if not isinstance(v, _Exporter):
             continue
@@ -725,7 +651,7 @@ def get_onnx_example(op_name):  # type: ignore
                 if sub in code:
                     found = code
             if found is None:
-                raise RuntimeError(  # pragma: no cover
+                raise RuntimeError(
                     f"Unable to find {sub!r} in\n{code_cls}"
                 )
             found = textwrap.dedent(found)
@@ -751,16 +677,13 @@ def is_last_schema(sch: OpSchema) -> bool:
     :return: True
     """
     try:
-        last = get_schema(sch.name, domain=sch.domain)
-    except SchemaError:  # pragma: no cover
-        # raise RuntimeError(
-        #     "Unable to find schema for operator %r and domain %r."
-        #     "" % (sch.name, sch.domain))
+        last = onnx.defs.get_schema(sch.name, domain=sch.domain)
+    except onnx.defs.SchemaError:
         return True
     return last.since_version == sch.since_version
 
 
-def onnx_documentation_folder(folder, title="ONNX Operators", flog=None, max_opsets=None):  # type: ignore
+def onnx_documentation_folder(folder, title="ONNX Operators", flog=None, max_opsets=None):
     """
     Creates documentation in a folder for all known
     ONNX operators or a subset.
@@ -801,7 +724,7 @@ def render(self, indent=""):
             for op in self.ops:
                 name = op["name"]
                 dom = self.domain.replace(".", "-")
-                table_dom.append(f"    * - :ref:`l-onnx-doc{dom}-{name}`")
+                table_dom.append(f"    * - :ref:`{name} <l-onnx-doc{dom}-{name}>`")
                 versions = list(
                     reversed(
                         sorted(
@@ -872,9 +795,9 @@ def render(self, indent=""):
         # loop on operators
         for op in sorted(do):
             if flog is not None:
-                flog(f"generate page for onnx {dom!r} - {op!r}")  # pragma: no cover
+                flog(f"generate page for onnx {dom!r} - {op!r}")
             page_name = f"onnx_{dom.replace('.', '')}_{op}"
-            doc, d_links = get_rst_doc(
+            doc, d_links = get_markdown_doc(
                 folder, op, domain=dom, version=None, example=True, diff=True
             )
             if dom == "":
@@ -882,29 +805,20 @@ def render(self, indent=""):
             else:
                 main = f"{dom} - {op}"
             sdom = dom.replace(".", "-")
-            ref_link = f".. _l-onnx-doc{sdom}-{op}:"
+            # Target in MyST https://myst-parser.readthedocs.io/en/v0.15.1/syntax/syntax.html?highlight=role#extra-markdown-syntax
+            ref_link = f"(l-onnx-doc{sdom}-{op})="
             rows = [
                 "",
                 ref_link,
                 "",
-                "=" * len(main),
-                main,
-                "=" * len(main),
+                f"# {main}",
                 "",
                 doc,
             ]
 
-            full = os.path.join(folder, page_name + ".rst")
+            full = os.path.join(folder, page_name + ".md")
             content = "\n".join(rows)
-            if os.path.exists(full):
-                with open(full, "r", encoding="utf-8") as f:
-                    old_content = f.read()
-                write = old_content != content
-            else:
-                write = True
-            if write:
-                with open(full, "w", encoding="utf-8") as f:
-                    f.write(content)
+            pathlib.Path(full).write_text(content, encoding="utf-8")
             pages.append(full)
             dom_pages.append({"name": op, "links": d_links})
 
@@ -913,12 +827,10 @@ def render(self, indent=""):
     # final
     if len(tables) < 3:
         raise RuntimeError(f"At least three domain are expected not {len(tables)}.")
-    tmpl = _template_main
-    index = tmpl.render(pages=pages, tabs=tables, os=os, len=len, title=title)
+    index = _template_main.render(pages=pages, tabs=tables, os=os, len=len, title=title)
     index = _clean_unicode(index)
     page_name = os.path.join(folder, "index.rst")
-    with open(page_name, "w", encoding="utf-8") as f:
-        f.write(index)
+    pathlib.Path(page_name).write_text(index, encoding="utf-8")
     pages.append(page_name)
     return pages
 


Test patch: