Skip to content

Commit

Permalink
✨ NEW: Add attrs_inline extension (#654)
Browse files Browse the repository at this point in the history
By adding `"attrs_inline"` 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 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).

This extension replaces `"attrs_image"`, which is still present, but deprecated.
  • Loading branch information
chrisjsewell committed Jan 5, 2023
1 parent 1b84a5b commit bf56662
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 116 deletions.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"strikethrough",
"substitution",
"tasklist",
"attrs_image",
"attrs_inline",
]
myst_number_code_blocks = ["typescript"]
myst_heading_anchors = 2
Expand Down
128 changes: 87 additions & 41 deletions docs/syntax/optional.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ To enable all the syntaxes explained below:
```python
myst_enable_extensions = [
"amsmath",
"attrs_inline",
"colon_fence",
"deflist",
"dollarmath",
Expand All @@ -43,7 +44,7 @@ myst_enable_extensions = [
]
```

:::{important}
:::{versionchanged} 0.13.0
`myst_enable_extensions` replaces previous configuration options:
`admonition_enable`, `figure_enable`, `dmath_enable`, `amsmath_enable`, `deflist_enable`, `html_img_enable`
:::
Expand Down Expand Up @@ -101,7 +102,7 @@ Math is parsed by adding to the `myst_enable_extensions` list option, in the sph

These options enable their respective Markdown parser plugins, as detailed in the [markdown-it plugin guide](markdown_it:md/plugins).

:::{important}
:::{versionchanged} 0.13.0
`myst_dmath_enable=True` and `myst_amsmath_enable=True` are deprecated, and replaced by `myst_enable_extensions = ["dollarmath", "amsmath"]`
:::

Expand Down Expand Up @@ -484,7 +485,7 @@ This text is **standard** _Markdown_

## Admonition directives

:::{important}
:::{versionchanged} 0.13.0
`myst_admonition_enable` is deprecated and replaced by `myst_enable_extensions = ["colon_fence"]` (see above).
Also, classes should now be set with the `:class: myclass` option.

Expand Down Expand Up @@ -727,9 +728,90 @@ Send a message to a recipient
Currently `sphinx.ext.autodoc` does not support MyST, see [](howto/autodoc).
:::

(syntax/attributes)=
## Inline attributes

By adding `"attrs_inline"` to `myst_enable_extensions` (in the sphinx `conf.py` [configuration file](https://www.sphinx-doc.org/en/master/usage/configuration.html)),
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).

:::{important}
This feature is in *beta*, and may change in future versions.
It replace the previous `attrs_image` extension, which is now deprecated.
:::

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.
Multiple classes may be given in this way; they will be combined.
- `#foo` specifies `foo` as an identifier.
An element may have only one identifier;
if multiple identifiers are given, the last one is used.
- `key="value"` or `key=value` specifies a key-value attribute.
Quotes are not needed when the value consists entirely of
ASCII alphanumeric characters or `_` or `:` or `-`.
Backslash escapes may be used inside quoted values.
**Note** only certain keys are supported, see below.
- `%` begins a comment, which ends with the next `%` or the end of the attribute (`}`).

For example, the following Markdown:

```md

- [A span of text with attributes]{#spanid .bg-warning},
{ref}`a reference to the span <spanid>`

- `A literal with attributes`{#literalid .bg-warning},
{ref}`a reference to the literal <literalid>

- An autolink with attributes: <https://example.com>{.bg-warning}

- [A link with attributes](syntax/attributes){#linkid .bg-warning},
{ref}`a reference to the link <linkid>`

- ![An image with attribute](img/fun-fish.png){#imgid .bg-warning w=100px align=center}
{ref}`a reference to the image <imgid>`

```

will be parsed as:

- [A span of text with attributes]{#spanid .bg-warning},
{ref}`a reference to the span <spanid>`

- `A literal with attributes`{#literalid .bg-warning},
{ref}`a reference to the literal <literalid>`

- An autolink with attributes: <https://example.com>{.bg-warning}

- [A link with attributes](syntax/attributes){#linkid .bg-warning},
{ref}`a reference to the link <linkid>`

- ![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,
but only certain key-value attributes are supported for each syntax.

For **literals**, the following attributes are supported:

- `language`/`lexer`/`l` defines the syntax lexer,
e.g. `` `a = "b"`{l=python} `` is displayed as `a = "b"`{l=python}.
Note, this is only supported in `sphinx >= 5`.

For **images**, the following attributes are supported (equivalent to the `image` directive):

- `width`/`w` defines the width of the image (in `%`, `px`, `em`, `cm`, etc)
- `height`/`h` defines the height of the image (in `px`, `em`, `cm`, etc)
- `align`/`a` defines the scale of the image (`left`, `center`, or `right`)

(syntax/images)=

## Images
## HTML Images

MyST provides a few different syntaxes for including images in your documentation, as explained below.

Expand Down Expand Up @@ -786,42 +868,6 @@ HTML image can also be used inline!

I'm an inline image: <img src="img/fun-fish.png" height="20px">

### Inline attributes

:::{warning}
This extension is currently experimental, and may change in future versions.
:::

By adding `"attrs_image"` to `myst_enable_extensions` (in the sphinx `conf.py` [configuration file](https://www.sphinx-doc.org/en/master/usage/configuration.html)),
you can enable parsing of inline attributes for images.

For example, the following Markdown:

```md
![image attrs](img/fun-fish.png){#imgattr .bg-primary width="100px" align=center}

{ref}`a reference to the image <imgattr>`
```

will be parsed as:

![image attrs](img/fun-fish.png){#imgattr .bg-primary width="100px" align=center}

{ref}`a reference to the image <imgattr>`

Inside the curly braces, the following syntax is possible:

- `.foo` specifies `foo` as a class.
Multiple classes may be given in this way; they will be combined.
- `#foo` specifies `foo` as an identifier.
An element may have only one identifier;
if multiple identifiers are given, the last one is used.
- `key="value"` or `key=value` specifies a key-value attribute.
Quotes are not needed when the value consists entirely of
ASCII alphanumeric characters or `_` or `:` or `-`.
Backslash escapes may be used inside quoted values.
- `%` begins a comment, which ends with the next `%` or the end of the attribute (`}`).

(syntax/figures)=

## Markdown Figures
Expand All @@ -830,7 +876,7 @@ By adding `"colon_fence"` to `myst_enable_extensions` (in the sphinx `conf.py` [
we can combine the above two extended syntaxes,
to create a fully Markdown compliant version of the `figure` directive named `figure-md`.

:::{important}
:::{versionchanged} 0.13.0
`myst_figure_enable` with the `figure` directive is deprecated and replaced by `myst_enable_extensions = ["colon_fence"]` and `figure-md`.
:::

Expand Down
1 change: 1 addition & 0 deletions myst_parser/config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def check_extensions(_, __, value):
[
"amsmath",
"attrs_image",
"attrs_inline",
"colon_fence",
"deflist",
"dollarmath",
Expand Down
132 changes: 78 additions & 54 deletions myst_parser/mdit_to_docutils/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@
from contextlib import contextmanager
from datetime import date, datetime
from types import ModuleType
from typing import TYPE_CHECKING, Any, Iterator, MutableMapping, Sequence, cast
from typing import (
TYPE_CHECKING,
Any,
Callable,
Iterator,
MutableMapping,
Sequence,
cast,
)
from urllib.parse import urlparse

import jinja2
Expand Down Expand Up @@ -362,6 +370,44 @@ def add_line_and_source_path_r(
for child in findall(node)():
self.add_line_and_source_path(child, token)

def copy_attributes(
self,
token: SyntaxTreeNode,
node: nodes.Element,
keys: Sequence[str] = ("class",),
*,
converters: dict[str, Callable[[str], Any]] | None = None,
aliases: dict[str, str] | None = None,
) -> None:
"""Copy attributes on the token to the docutils node."""
if converters is None:
converters = {}
if aliases is None:
aliases = {}
for key, value in token.attrs.items():
key = aliases.get(key, key)
if key not in keys:
continue
if key == "class":
node["classes"].extend(str(value).split())
elif key == "id":
name = nodes.fully_normalize_name(str(value))
node["names"].append(name)
self.document.note_explicit_target(node, node)
else:
if key in converters:
try:
value = converters[key](str(value))
except ValueError:
self.create_warning(
f"Invalid {key!r} attribute value: {token.attrs[key]!r}",
MystWarnings.INVALID_ATTRIBUTE,
line=token_line(token, default=0),
append_to=node,
)
continue
node[key] = value

def update_section_level_state(self, section: nodes.section, level: int) -> None:
"""Update the section level state, with the new current section and level."""
# find the closest parent section
Expand Down Expand Up @@ -490,6 +536,14 @@ def render_hr(self, token: SyntaxTreeNode) -> None:
def render_code_inline(self, token: SyntaxTreeNode) -> None:
node = nodes.literal(token.content, token.content)
self.add_line_and_source_path(node, token)
self.copy_attributes(
token,
node,
("class", "id", "language"),
aliases={"lexer": "language", "l": "language"},
)
if "language" in node and "code" not in node["classes"]:
node["classes"].append("code")
self.current_node.append(node)

def create_highlighted_code_block(
Expand Down Expand Up @@ -697,10 +751,8 @@ def render_external_url(self, token: SyntaxTreeNode) -> None:
"""
ref_node = nodes.reference()
self.add_line_and_source_path(ref_node, token)
self.copy_attributes(token, ref_node, ("class", "id", "title"))
ref_node["refuri"] = cast(str, token.attrGet("href") or "")
title = token.attrGet("title")
if title:
ref_node["title"] = title
with self.current_node_context(ref_node, append=True):
self.render_children(token)

Expand All @@ -717,17 +769,16 @@ def render_internal_link(self, token: SyntaxTreeNode) -> None:
"""
ref_node = nodes.reference()
self.add_line_and_source_path(ref_node, token)
self.copy_attributes(token, ref_node, ("class", "id", "title"))
ref_node["refname"] = cast(str, token.attrGet("href") or "")
self.document.note_refname(ref_node)
title = token.attrGet("title")
if title:
ref_node["title"] = title
with self.current_node_context(ref_node, append=True):
self.render_children(token)

def render_autolink(self, token: SyntaxTreeNode) -> None:
refuri = escapeHtml(token.attrGet("href") or "") # type: ignore[arg-type]
ref_node = nodes.reference()
self.copy_attributes(token, ref_node, ("class", "id"))
ref_node["refuri"] = refuri
self.add_line_and_source_path(ref_node, token)
with self.current_node_context(ref_node, append=True):
Expand Down Expand Up @@ -760,58 +811,31 @@ def render_image(self, token: SyntaxTreeNode) -> None:
img_node["uri"] = destination

img_node["alt"] = self.renderInlineAsText(token.children or [])
title = token.attrGet("title")
if title:
img_node["title"] = token.attrGet("title")

# apply other attributes that can be set on the image
if "class" in token.attrs:
img_node["classes"].extend(str(token.attrs["class"]).split())
if "width" in token.attrs:
try:
width = directives.length_or_percentage_or_unitless(
str(token.attrs["width"])
)
except ValueError:
self.create_warning(
f"Invalid width value for image: {token.attrs['width']!r}",
MystWarnings.INVALID_ATTRIBUTE,
line=token_line(token, default=0),
append_to=self.current_node,
)
else:
img_node["width"] = width
if "height" in token.attrs:
try:
height = directives.length_or_unitless(str(token.attrs["height"]))
except ValueError:
self.create_warning(
f"Invalid height value for image: {token.attrs['height']!r}",
MystWarnings.INVALID_ATTRIBUTE,
line=token_line(token, default=0),
append_to=self.current_node,
)
else:
img_node["height"] = height
if "align" in token.attrs:
if token.attrs["align"] not in ("left", "center", "right"):
self.create_warning(
f"Invalid align value for image: {token.attrs['align']!r}",
MystWarnings.INVALID_ATTRIBUTE,
line=token_line(token, default=0),
append_to=self.current_node,
)
else:
img_node["align"] = token.attrs["align"]
if "id" in token.attrs:
name = nodes.fully_normalize_name(str(token.attrs["id"]))
img_node["names"].append(name)
self.document.note_explicit_target(img_node, img_node)

self.copy_attributes(
token,
img_node,
("class", "id", "title", "width", "height", "align"),
converters={
"width": directives.length_or_percentage_or_unitless,
"height": directives.length_or_unitless,
"align": lambda x: directives.choice(x, ("left", "center", "right")),
},
aliases={"w": "width", "h": "height", "a": "align"},
)

self.current_node.append(img_node)

# ### render methods for plugin tokens

def render_span(self, token: SyntaxTreeNode) -> None:
"""Render an inline span token."""
node = nodes.inline()
self.add_line_and_source_path(node, token)
self.copy_attributes(token, node, ("class", "id"))
with self.current_node_context(node, append=True):
self.render_children(token)

def render_front_matter(self, token: SyntaxTreeNode) -> None:
"""Pass document front matter data."""
position = token_line(token, default=0)
Expand Down
4 changes: 1 addition & 3 deletions myst_parser/mdit_to_docutils/sphinx_.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,7 @@ def render_internal_link(self, token: SyntaxTreeNode) -> None:
text = ""

self.add_line_and_source_path(wrap_node, token)
title = token.attrGet("title")
if title:
wrap_node["title"] = title
self.copy_attributes(token, wrap_node, ("class", "id", "title"))
self.current_node.append(wrap_node)

inner_node = nodes.inline("", text, classes=classes)
Expand Down

0 comments on commit bf56662

Please sign in to comment.