Repository: executablebooks/MyST-Parser
Pull request #703 (2023-02-18 11:59:37)

✨ NEW: Add `attrs_block` extension
By adding `"attrs_block"` to `myst_enable_extensions` (in the sphinx `conf.py`), you can enable parsing of inline attributes after certain inline syntaxes. This is adapted from [djot block attributes](https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html#block-attributes)

codecov[bot]: # [Codecov](https://codecov.io/gh/executablebooks/MyST-Parser/pull/703?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) Report
Base: **89.67**% // Head: **89.14**% // Decreases project coverage by **`-0.54%`** :warning:
> Coverage data is based on head [(`83a24fb`)](https://codecov.io/gh/executablebooks/MyST-Parser/pull/703?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) compared to base [(`6e32143`)](https://codecov.io/gh/executablebooks/MyST-Parser/commit/6e3214318ea97356668628818bfc00a77a35cb66?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks).
> Patch coverage: 62.74% of modified lines in pull request are covered.

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


```diff
@@            Coverage Diff             @@
##           master     #703      +/-   ##
==========================================
- Coverage   89.67%   89.14%   -0.54%     
==========================================
  Files          22       22              
  Lines        2674     2717      +43     
==========================================
+ Hits         2398     2422      +24     
- Misses        276      295      +19     
```

| Flag | Coverage Δ | |
|---|---|---|
| pytests | `89.14% <62.74%> (-0.54%)` | :arrow_down: |

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/703?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) | Coverage Δ | |
|---|---|---|
| [myst\_parser/config/main.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/703?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvY29uZmlnL21haW4ucHk=) | `86.03% <ø> (ø)` | |
| [myst\_parser/mdit\_to\_docutils/base.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/703?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvbWRpdF90b19kb2N1dGlscy9iYXNlLnB5) | `91.23% <60.41%> (-1.82%)` | :arrow_down: |
| [myst\_parser/parsers/mdit.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/703?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvcGFyc2Vycy9tZGl0LnB5) | `97.05% <100.00%> (+0.08%)` | :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/703?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).

chrisjsewell: cc @mgielda  https://myst-parser--703.org.readthedocs.build/en/703/syntax/optional.html#block-attributes

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4c4f87a..9d5ca3e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -49,7 +49,7 @@ repos:
       additional_dependencies:
       - sphinx~=5.0
       - markdown-it-py>=1.0.0,<3.0.0
-      - mdit-py-plugins~=0.3.1
+      - mdit-py-plugins~=0.3.4
       files: >
         (?x)^(
             myst_parser/.*py|
diff --git a/docs/conf.py b/docs/conf.py
index 2a931d4..2e26f4c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -100,6 +100,7 @@ myst_enable_extensions = [
     "substitution",
     "tasklist",
     "attrs_inline",
+    "attrs_block",
     "inv_link",
 ]
 myst_url_schemes = {
diff --git a/docs/syntax/optional.md b/docs/syntax/optional.md
index 0e332eb..1323492 100644
--- a/docs/syntax/optional.md
+++ b/docs/syntax/optional.md
@@ -653,13 +653,6 @@ based on the [reStructureText syntax](https://docutils.sourceforge.io/docs/ref/r
 :Paragraphs: Since the field marker may be quite long, the second
    and subsequent lines of a paragraph do not have to line up
    with the first line.
-:Alignment 1: If the field body starts on the first line...
-
-              Then the entire field body must be indented the same.
-:Alignment 2:
-  If the field body starts on a subsequent line...
-
-  Then the indentation is always two spaces.
 :Blocks:
 
   As well as paragraphs, any block syntaxes may be used in a field body:
@@ -679,13 +672,6 @@ based on the [reStructureText syntax](https://docutils.sourceforge.io/docs/ref/r
 :Paragraphs: Since the field marker may be quite long, the second
    and subsequent lines of a paragraph do not have to line up
    with the first line.
-:Alignment 1: If the field body starts on the first line...
-
-              Then the entire field body must be indented the same.
-:Alignment 2:
-  If the field body starts on a subsequent line...
-
-  Then the indentation is always two spaces.
 :Blocks:
 
   As well as paragraphs, any block syntaxes may be used in a field body:
@@ -731,19 +717,20 @@ Currently `sphinx.ext.autodoc` does not support MyST, see [](howto/autodoc).
 :::
 
 (syntax/attributes)=
-## Inline attributes
+## Attributes
 
 :::{versionadded} 0.19
-This feature is in *beta*, and may change in future versions.
-It replace the previous `attrs_image` extension, which is now deprecated.
+This feature is in *beta*, and feedback is welcome.
+
+`attrs_inline` also replace the previous `attrs_image` extension, which is now deprecated.
 :::
 
-By adding `"attrs_inline"` to `myst_enable_extensions` (in the {{ confpy }}),
-you can enable parsing of inline attributes after certain inline syntaxes.
-This is adapted from [djot inline attributes](https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html#inline-attributes),
-and also related to [pandoc bracketed spans](https://pandoc.org/MANUAL.html#extension-bracketed_spans).
+Attributes are a way of enriching standard CommonMark syntax, by adding additional information to elements.
+
+Attributes are specified inside curly braces `{}`,
+for example `{#my-id .my-class key="value"}`,
+and come before a block element or after an inline element.
 
-Attributes are specified in curly braces after the inline syntax.
 Inside the curly braces, the following syntax is recognised:
 
 - `.foo` specifies `foo` as a class.
@@ -758,6 +745,126 @@ Inside the curly braces, the following syntax is recognised:
     **Note** only certain keys are supported, see below.
 - `%` begins a comment, which ends with the next `%` or the end of the attribute (`}`).
 
+Attributes are cumulative, so that if multiple attributes follow each other, the inner attributes override the outer ones. One exception is that if multiple classes are given, they are combined.
+For example:
+
+```markdown
+{#id1 .class1 key1="value1"}
+{#id2 .class2 key2="value2"}
+block
+
+[inline]{#id2 .class2 key2="value2"}{#id1 .class1 key1="value1"}
+```
+
+is equivalent to:
+
+```markdown
+{#id2 .class1 .class2 key1="value1" key2="value2"}
+block
+
+[inline]{#id2 .class1 .class2 key1="value1" key2="value2"}
+```
+
+:::{seealso}
+This is adapted from [djot inline/block attributes](https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html#inline-attributes),
+and also related to [pandoc bracketed spans](https://pandoc.org/MANUAL.html#extension-bracketed_spans).
+:::
+
+(syntax/attributes/block)=
+### Block attributes
+
+By adding `"attrs_block"` to `myst_enable_extensions` (in the {{ confpy }}),
+you can enable parsing of block attributes before certain block syntaxes.
+
+For example, the following Markdown:
+
+```markdown
+{#mypara .bg-warning}
+Here is a paragraph with attributes.
+
+{ref}`A reference to my paragraph <mypara>`
+```
+
+will be rendered as:
+
+{#mypara .bg-warning}
+Here is a paragraph with attributes.
+
+{ref}`A reference to my paragraph <mypara>`
+
+`id` and `class` are supported for most block syntaxes,
+but only certain key-value attributes are supported for each syntax.
+
+For **ordered lists**, the `style` key is supported, and can be one of `decimal`, `lower-alpha`, `upper-alpha`, `lower-roman`, `upper-roman`:
+
+```markdown
+{style=lower-alpha}
+1. a
+2. b
+
+{style=upper-alpha}
+1. a
+2. b
+
+{style=lower-roman}
+1. a
+2. b
+
+{style=upper-roman}
+1. a
+2. b
+```
+
+{style=lower-alpha}
+1. a
+2. b
+
+{style=upper-alpha}
+1. a
+2. b
+
+{style=lower-roman}
+1. a
+2. b
+
+{style=upper-roman}
+1. a
+2. b
+
+For **code fences**, the `lineno-start` and `emphasize-lines` keys are supported:
+
+````md
+{lineno-start=1 emphasize-lines="2,3"}
+```python
+a = 1
+b = 2
+c = 3
+```
+````
+
+{lineno-start=1 emphasize-lines="2,3"}
+```python
+a = 1
+b = 2
+c = 3
+```
+
+For **block quotes**, the `attribution` key is supported:
+
+```markdown
+{attribution="Chris Sewell"}
+> Hallo
+```
+
+{attribution="Chris Sewell"}
+> Hallo
+
+(syntax/attributes/inline)=
+### Inline attributes
+
+By adding `"attrs_inline"` to `myst_enable_extensions` (in the {{ confpy }}),
+you can enable parsing of inline attributes after certain inline syntaxes.
+
 For example, the following Markdown:
 
 ```md
@@ -794,9 +901,7 @@ will be parsed as:
 - ![An image with attribute](img/fun-fish.png){#imgid .bg-warning w="100px" align=center}
   {ref}`a reference to the image <imgid>`
 
-### key-value attributes
-
-`id` and `class` are supported for all inline syntaxes,
+`id` and `class` are supported for most inline syntaxes,
 but only certain key-value attributes are supported for each syntax.
 
 For **literals**, the following attributes are supported:
@@ -812,7 +917,6 @@ For **images**, the following attributes are supported (equivalent to the `image
 - `align`/`a` defines the scale of the image (`left`, `center`, or `right`)
 
 (syntax/images)=
-
 ## HTML Images
 
 MyST provides a few different syntaxes for including images in your documentation, as explained below.
diff --git a/docs/syntax/syntax.md b/docs/syntax/syntax.md
index 571b920..b0d9213 100644
--- a/docs/syntax/syntax.md
+++ b/docs/syntax/syntax.md
@@ -38,6 +38,10 @@ Ordered List    | `1. item`
 Unordered List  | `- item`
 Code Fence      | opening ```` ```lang ```` to closing ```` ``` ````
 
+:::{tip}
+The {ref}`attribute extensions <syntax/attributes>` allow you to enrich these syntaxes with additional information.
+:::
+
 (syntax/frontmatter)=
 
 ## Front Matter
@@ -314,7 +318,7 @@ You can customise this behaviour in a number of ways using [configuration option
 Most simply, by setting the `myst_all_links_external` configuration option to `True`,
 all links will be treated as external [URL] links.
 
-To apply selectively to specific links, you can enable the [attrs_inline](optional.md#inline-attributes) extension,
+To apply selectively to specific links, you can enable the [attrs_inline](syntax/attributes/inline) extension,
 then add an `external` class to the link.\
 For example, `[my-external-link](my-external-link){.external}` becomes [my-external-link](my-external-link){.external}.
 
diff --git a/myst_parser/config/main.py b/myst_parser/config/main.py
index b4cd423..71b037c 100644
--- a/myst_parser/config/main.py
+++ b/myst_parser/config/main.py
@@ -37,6 +37,7 @@ def check_extensions(inst: "MdParserConfig", field: dc.Field, value: Any) -> Non
             "amsmath",
             "attrs_image",
             "attrs_inline",
+            "attrs_block",
             "colon_fence",
             "deflist",
             "dollarmath",
diff --git a/myst_parser/mdit_to_docutils/base.py b/myst_parser/mdit_to_docutils/base.py
index a45ebb1..9c83f6a 100644
--- a/myst_parser/mdit_to_docutils/base.py
+++ b/myst_parser/mdit_to_docutils/base.py
@@ -516,6 +516,7 @@ class DocutilsRenderer(RendererProtocol):
 
     def render_paragraph(self, token: SyntaxTreeNode) -> None:
         para = nodes.paragraph(token.children[0].content if token.children else "")
+        self.copy_attributes(token, para, keys=("class", "id"))
         self.add_line_and_source_path(para, token)
         with self.current_node_context(para, append=True):
             self.render_children(token)
@@ -530,27 +531,32 @@ class DocutilsRenderer(RendererProtocol):
         list_node = nodes.bullet_list()
         if token.markup:
             list_node["bullet"] = token.markup
-        if token.attrs.get("class"):
-            # this is used e.g. by tasklist
-            list_node["classes"] = str(token.attrs["class"]).split()
+        self.copy_attributes(token, list_node, keys=("class", "id"))
         self.add_line_and_source_path(list_node, token)
         with self.current_node_context(list_node, append=True):
             self.render_children(token)
 
     def render_ordered_list(self, token: SyntaxTreeNode) -> None:
-        list_node = nodes.enumerated_list(enumtype="arabic", prefix="")
+        style = "arabic"
+        if "style" in token.attrs:
+            style = {
+                "decimal": "arabic",
+                "lower-alpha": "loweralpha",
+                "upper-alpha": "upperalpha",
+                "lower-roman": "lowerroman",
+                "upper-roman": "upperroman",
+            }.get(str(token.attrs["style"]), style)
+        list_node = nodes.enumerated_list(enumtype=style, prefix="")
         list_node["suffix"] = token.markup  # for CommonMark, this should be "." or ")"
-        if "start" in token.attrs:  # starting number
-            list_node["start"] = token.attrs["start"]
+        # start is starting number
+        self.copy_attributes(token, list_node, keys=("class", "id", "start"))
         self.add_line_and_source_path(list_node, token)
         with self.current_node_context(list_node, append=True):
             self.render_children(token)
 
     def render_list_item(self, token: SyntaxTreeNode) -> None:
         item_node = nodes.list_item()
-        if token.attrs.get("class"):
-            # this is used e.g. by tasklist
-            item_node["classes"] = str(token.attrs["class"]).split()
+        self.copy_attributes(token, item_node, keys=("class", "id"))
         self.add_line_and_source_path(item_node, token)
         with self.current_node_context(item_node, append=True):
             self.render_children(token)
@@ -576,9 +582,16 @@ class DocutilsRenderer(RendererProtocol):
 
     def render_blockquote(self, token: SyntaxTreeNode) -> None:
         quote = nodes.block_quote()
+        self.copy_attributes(token, quote, keys=("class", "id"))
         self.add_line_and_source_path(quote, token)
         with self.current_node_context(quote, append=True):
             self.render_children(token)
+        if "attribution" in token.attrs:
+            attribution = nodes.attribution(
+                token.attrs["attribution"], "", nodes.Text(token.attrs["attribution"])
+            )
+            self.add_line_and_source_path(attribution, token)
+            quote.append(attribution)
 
     def render_hr(self, token: SyntaxTreeNode) -> None:
         node = nodes.transition()
@@ -598,6 +611,20 @@ class DocutilsRenderer(RendererProtocol):
             node["classes"].append("code")
         self.current_node.append(node)
 
+    @staticmethod
+    def _parse_linenos(emphasize_lines: str, num_lines: int) -> list[int]:
+        """Parse the `emphasize_lines` argument.
+
+        Raises ValueError if the argument is invalid.
+        """
+        from sphinx.util import parselinenos
+
+        hl_lines = parselinenos(emphasize_lines, num_lines)
+        if any(i >= num_lines for i in hl_lines):
+            raise ValueError(f"out of range(1-{num_lines}")
+
+        return [x + 1 for x in hl_lines if x < num_lines]
+
     def create_highlighted_code_block(
         self,
         text: str,
@@ -607,6 +634,7 @@ class DocutilsRenderer(RendererProtocol):
         source: str | None = None,
         line: int | None = None,
         node_cls: type[nodes.Element] = nodes.literal_block,
+        emphasize_lines: list[int] | str | None = None,
     ) -> nodes.Element:
         """Create a literal block with syntax highlighting.
 
@@ -625,6 +653,22 @@ class DocutilsRenderer(RendererProtocol):
                 node["linenos"] = True
                 if lineno_start != 1:
                     node["highlight_args"] = {"linenostart": lineno_start}
+            if isinstance(emphasize_lines, str):
+                try:
+                    emphasize_lines = self._parse_linenos(
+                        emphasize_lines, len(text.splitlines())
+                    )
+                except ValueError as err:
+                    self.create_warning(
+                        f"emphasize_lines: {err}",
+                        MystWarnings.INVALID_ATTRIBUTE,
+                        line=line,
+                    )
+            if isinstance(emphasize_lines, (list, tuple)):
+                # TODO emphasize_lines in docutils?
+                if "highlight_args" not in node:
+                    node["highlight_args"] = {}
+                node["highlight_args"]["hl_lines"] = emphasize_lines
         else:
             node = node_cls(
                 text, classes=["code"] + ([lexer_name] if lexer_name else [])
@@ -666,12 +710,27 @@ class DocutilsRenderer(RendererProtocol):
 
     def render_code_block(self, token: SyntaxTreeNode) -> None:
         lexer = token.info.split()[0] if token.info else None
+        lineno_start = 1
+        number_lines = False
+        emphasize_lines = (
+            str(token.attrs.get("emphasize-lines"))
+            if "emphasize-lines" in token.attrs
+            else None
+        )
+        if "lineno-start" in token.attrs:
+            with suppress(ValueError):
+                lineno_start = int(token.attrs["lineno-start"])
+                number_lines = True
         node = self.create_highlighted_code_block(
             token.content,
             lexer,
+            lineno_start=lineno_start,
+            number_lines=number_lines,
             source=self.document["source"],
             line=token_line(token, 0) or None,
+            emphasize_lines=emphasize_lines,
         )
+        self.copy_attributes(token, node, ("class", "id"))
         self.current_node.append(node)
 
     def render_fence(self, token: SyntaxTreeNode) -> None:
@@ -693,13 +752,28 @@ class DocutilsRenderer(RendererProtocol):
                 "highlight_language", self.sphinx_env.config.highlight_language
             )
 
+        lineno_start = 1
+        number_lines = language in self.md_config.number_code_blocks
+        emphasize_lines = (
+            str(token.attrs.get("emphasize-lines"))
+            if "emphasize-lines" in token.attrs
+            else None
+        )
+        if "lineno-start" in token.attrs:
+            with suppress(ValueError):
+                lineno_start = int(token.attrs["lineno-start"])
+                number_lines = True
+
         node = self.create_highlighted_code_block(
             text,
             language,
-            number_lines=language in self.md_config.number_code_blocks,
+            number_lines=number_lines,
+            lineno_start=lineno_start,
             source=self.document["source"],
             line=token_line(token, 0) or None,
+            emphasize_lines=emphasize_lines,
         )
+        self.copy_attributes(token, node, ("class", "id"))
         self.current_node.append(node)
 
     @property
@@ -763,6 +837,9 @@ class DocutilsRenderer(RendererProtocol):
 
         # add possible reference slug, this may be different to the standard name above,
         # and does not have to be normalised, so we treat it separately
+        # TODO this id can now come from attributes, which we actually want to be explicit
+        # I think rather than using the mdit anchors_plugin,
+        # we should just compute them here (with the same logic)
         if "id" in token.attrs:
             slug = str(token.attrs["id"])
             new_section["slug"] = slug
@@ -1210,6 +1287,7 @@ class DocutilsRenderer(RendererProtocol):
         # top-level element
         table = nodes.table()
         table["classes"] += ["colwidths-auto"]
+        self.copy_attributes(token, table, ("class", "id"))
         self.add_line_and_source_path(table, token)
         self.current_node.append(table)
 
@@ -1405,6 +1483,7 @@ class DocutilsRenderer(RendererProtocol):
     def render_dl(self, token: SyntaxTreeNode) -> None:
         """Render a definition list."""
         node = nodes.definition_list(classes=["simple", "myst"])
+        self.copy_attributes(token, node, ("class", "id"))
         self.add_line_and_source_path(node, token)
         with self.current_node_context(node, append=True):
             item = None
@@ -1449,6 +1528,7 @@ class DocutilsRenderer(RendererProtocol):
     def render_field_list(self, token: SyntaxTreeNode) -> None:
         """Render a field list."""
         field_list = nodes.field_list(classes=["myst"])
+        self.copy_attributes(token, field_list, ("class", "id"))
         self.add_line_and_source_path(field_list, token)
         with self.current_node_context(field_list, append=True):
             # raise ValueError(token.pretty(show_text=True))
diff --git a/myst_parser/parsers/mdit.py b/myst_parser/parsers/mdit.py
index 73cd0b4..1910531 100644
--- a/myst_parser/parsers/mdit.py
+++ b/myst_parser/parsers/mdit.py
@@ -9,7 +9,7 @@ from markdown_it import MarkdownIt
 from markdown_it.renderer import RendererProtocol
 from mdit_py_plugins.amsmath import amsmath_plugin
 from mdit_py_plugins.anchors import anchors_plugin
-from mdit_py_plugins.attrs import attrs_plugin
+from mdit_py_plugins.attrs import attrs_block_plugin, attrs_plugin
 from mdit_py_plugins.colon_fence import colon_fence_plugin
 from mdit_py_plugins.deflist import deflist_plugin
 from mdit_py_plugins.dollarmath import dollarmath_plugin
@@ -111,6 +111,8 @@ def create_md_parser(
     elif "attrs_image" in config.enable_extensions:
         # TODO deprecate
         md.use(attrs_plugin, after=("image",))
+    if "attrs_block" in config.enable_extensions:
+        md.use(attrs_block_plugin)
     if config.heading_anchors is not None:
         md.use(
             anchors_plugin,
diff --git a/pyproject.toml b/pyproject.toml
index 2b2b825..baf7770 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,7 +37,7 @@ dependencies = [
     "docutils>=0.15,<0.20",
     "jinja2", # required for substitutions, but let sphinx choose version
     "markdown-it-py>=1.0.0,<3.0.0",
-    "mdit-py-plugins~=0.3.3",
+    "mdit-py-plugins~=0.3.4",
     "pyyaml",
     "sphinx>=5,<7",
     'typing-extensions; python_version < "3.8"',
diff --git a/tox.ini b/tox.ini
index 74208cc..3dd9a06 100644
--- a/tox.ini
+++ b/tox.ini
@@ -32,6 +32,8 @@ extras =
 whitelist_externals =
     rm
     echo
+passenv =
+    TERM
 commands =
     clean: rm -rf docs/_build/{posargs:html}
     sphinx-build -nW --keep-going -b {posargs:html} docs/ docs/_build/{posargs:html}


Test patch:
diff --git a/tests/test_renderers/fixtures/attributes.md b/tests/test_renderers/fixtures/attributes.md
new file mode 100644
index 0000000..e14175a
--- /dev/null
+++ b/tests/test_renderers/fixtures/attributes.md
@@ -0,0 +1,77 @@
+code fence
+.
+{lineno-start=1 emphasize-lines="2,3"}
+```python
+a = 1
+b = 2
+c = 3
+```
+.
+<document source="<src>/index.md">
+    <literal_block highlight_args="{'hl_lines': [2, 3]}" language="python" linenos="True" xml:space="preserve">
+        a = 1
+        b = 2
+        c = 3
+.
+
+blockquote
+.
+{attribution="Chris Sewell"}
+> Hallo
+.
+<document source="<src>/index.md">
+    <block_quote>
+        <paragraph>
+            Hallo
+        <attribution>
+            Chris Sewell
+.
+
+list-style
+.
+{style=lower-alpha}
+1. a
+2. b
+
+{style=upper-alpha}
+1. a
+2. b
+
+{style=lower-roman}
+1. a
+2. b
+
+{style=upper-roman}
+1. a
+2. b
+.
+<document source="<src>/index.md">
+    <enumerated_list enumtype="loweralpha" prefix="" suffix=".">
+        <list_item>
+            <paragraph>
+                a
+        <list_item>
+            <paragraph>
+                b
+    <enumerated_list enumtype="upperalpha" prefix="" suffix=".">
+        <list_item>
+            <paragraph>
+                a
+        <list_item>
+            <paragraph>
+                b
+    <enumerated_list enumtype="lowerroman" prefix="" suffix=".">
+        <list_item>
+            <paragraph>
+                a
+        <list_item>
+            <paragraph>
+                b
+    <enumerated_list enumtype="upperroman" prefix="" suffix=".">
+        <list_item>
+            <paragraph>
+                a
+        <list_item>
+            <paragraph>
+                b
+.
diff --git a/tests/test_renderers/fixtures/myst-config.txt b/tests/test_renderers/fixtures/myst-config.txt
index ea5c1ae..5c2de78 100644
--- a/tests/test_renderers/fixtures/myst-config.txt
+++ b/tests/test_renderers/fixtures/myst-config.txt
@@ -341,6 +341,25 @@ a
 <string>:1: (WARNING/2) Invalid 'align' attribute value: 'other' [myst.attribute]
 .
 
+[attr_block] --myst-enable-extensions=attrs_block
+.
+{#myid1 .class1 .class2}
+{#myid2 .class3}
+My paragraph
+
+{attribution="Chris Sewell"}
+> Hallo
+.
+<document source="<string>">
+    <paragraph classes="class1 class2 class3" ids="myid2" names="myid2">
+        My paragraph
+    <block_quote>
+        <paragraph>
+            Hallo
+        <attribution>
+            Chris Sewell
+.
+
 [inv_link] --myst-enable-extensions=inv_link
 .
 <inv:#index>
diff --git a/tests/test_renderers/test_fixtures_docutils.py b/tests/test_renderers/test_fixtures_docutils.py
index a1e16e5..2ff45ed 100644
--- a/tests/test_renderers/test_fixtures_docutils.py
+++ b/tests/test_renderers/test_fixtures_docutils.py
@@ -32,6 +32,7 @@ def test_syntax_elements(file_params, monkeypatch):
 
     # in docutils 0.18 footnote ids have changed
     outcome = doctree.pformat().replace('"footnote-reference-1"', '"id1"')
+    outcome = outcome.replace(' language=""', "")
     file_params.assert_expected(outcome, rstrip_lines=True)
 
 
diff --git a/tests/test_renderers/test_fixtures_sphinx.py b/tests/test_renderers/test_fixtures_sphinx.py
index 735ba70..65534db 100644
--- a/tests/test_renderers/test_fixtures_sphinx.py
+++ b/tests/test_renderers/test_fixtures_sphinx.py
@@ -124,3 +124,15 @@ def test_definition_lists(file_params, sphinx_doctree_no_tr: CreateDoctree):
     )
     result = sphinx_doctree_no_tr(file_params.content, "index.md")
     file_params.assert_expected(result.pformat("index"), rstrip_lines=True)
+
+
+@pytest.mark.param_file(FIXTURE_PATH / "attributes.md")
+def test_attributes(file_params, sphinx_doctree_no_tr: CreateDoctree):
+    sphinx_doctree_no_tr.set_conf(
+        {
+            "extensions": ["myst_parser"],
+            "myst_enable_extensions": ["attrs_inline", "attrs_block"],
+        }
+    )
+    result = sphinx_doctree_no_tr(file_params.content, "index.md")
+    file_params.assert_expected(result.pformat("index"), rstrip_lines=True)
diff --git a/tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.xml b/tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.xml
index 1fb20d4..bb5d11e 100644
--- a/tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.xml
+++ b/tests/test_sphinx/test_sphinx_builds/test_fieldlist_extension.xml
@@ -2,7 +2,7 @@
     <section ids="test" names="test">
         <title>
             Test
-        <field_list classes="myst">
+        <field_list classes="myst field-list">
             <field>
                 <field_name>
                     field
@@ -29,7 +29,7 @@
             <desc_content>
                 <paragraph>
                     Send a message to a recipient
-                <field_list classes="myst">
+                <field_list classes="myst field-list">
                     <field>
                         <field_name>
                             Parameters