From 714fa361ef6d6d60b84e7453493cc047a278f1e0 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 27 Sep 2022 10:27:39 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20NEW:=20Add=20`attrs=5Fimage`=20(exp?= =?UTF-8?q?erimental)=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use inline attributes to configure MD images: `![alt](img/fun-fish.png){#id .class width="100px" align=center}` --- .pre-commit-config.yaml | 6 +-- docs/conf.py | 1 + docs/syntax/optional.md | 36 +++++++++++++++ myst_parser/config/main.py | 1 + myst_parser/mdit_to_docutils/base.py | 45 +++++++++++++++++++ myst_parser/parsers/mdit.py | 3 ++ pyproject.toml | 3 +- tests/test_renderers/fixtures/myst-config.txt | 33 ++++++++++++++ tests/test_renderers/test_myst_config.py | 6 ++- 9 files changed, 129 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6fb2bd17..aee3233a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v2.38.2 hooks: - id: pyupgrade args: [--py37-plus] @@ -31,7 +31,7 @@ repos: - id: isort - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.8.0 hooks: - id: black @@ -52,7 +52,7 @@ repos: additional_dependencies: - sphinx~=5.0 - markdown-it-py>=1.0.0,<3.0.0 - - mdit-py-plugins~=0.3.0 + - mdit-py-plugins~=0.3.1 files: > (?x)^( myst_parser/.*py| diff --git a/docs/conf.py b/docs/conf.py index e2d0c5dc..199a32a4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -90,6 +90,7 @@ "strikethrough", "substitution", "tasklist", + "attrs_image", ] myst_number_code_blocks = ["typescript"] myst_heading_anchors = 2 diff --git a/docs/syntax/optional.md b/docs/syntax/optional.md index 4045eb2e..c5ee44ed 100644 --- a/docs/syntax/optional.md +++ b/docs/syntax/optional.md @@ -786,6 +786,42 @@ HTML image can also be used inline! I'm an inline image: +### 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 ` +``` + +will be parsed as: + +![image attrs](img/fun-fish.png){#imgattr .bg-primary width="100px" align=center} + +{ref}`a reference to the image ` + +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 diff --git a/myst_parser/config/main.py b/myst_parser/config/main.py index c1f1c7f1..a134ea7d 100644 --- a/myst_parser/config/main.py +++ b/myst_parser/config/main.py @@ -31,6 +31,7 @@ def check_extensions(_, __, value): diff = set(value).difference( [ "amsmath", + "attrs_image", "colon_fence", "deflist", "dollarmath", diff --git a/myst_parser/mdit_to_docutils/base.py b/myst_parser/mdit_to_docutils/base.py index 1709f1bc..cedd6c35 100644 --- a/myst_parser/mdit_to_docutils/base.py +++ b/myst_parser/mdit_to_docutils/base.py @@ -785,6 +785,51 @@ def render_image(self, token: SyntaxTreeNode) -> None: 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}", + line=token_line(token, default=0), + subtype="image", + 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}", + line=token_line(token, default=0), + subtype="image", + 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}", + line=token_line(token, default=0), + subtype="image", + 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.current_node.append(img_node) # ### render methods for plugin tokens diff --git a/myst_parser/parsers/mdit.py b/myst_parser/parsers/mdit.py index 249acd68..84764957 100644 --- a/myst_parser/parsers/mdit.py +++ b/myst_parser/parsers/mdit.py @@ -9,6 +9,7 @@ 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.colon_fence import colon_fence_plugin from mdit_py_plugins.deflist import deflist_plugin from mdit_py_plugins.dollarmath import dollarmath_plugin @@ -100,6 +101,8 @@ def create_md_parser( md.use(tasklists_plugin) if "substitution" in config.enable_extensions: md.use(substitution_plugin, *config.sub_delimiters) + if "attrs_image" in config.enable_extensions: + md.use(attrs_plugin, after=("image",)) if config.heading_anchors is not None: md.use( anchors_plugin, diff --git a/pyproject.toml b/pyproject.toml index d375a604..880a4fd4 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.0", + "mdit-py-plugins~=0.3.1", "pyyaml", "sphinx>=4,<6", "typing-extensions", @@ -68,6 +68,7 @@ testing = [ "pytest-regressions", "pytest-param-files~=0.3.4", "sphinx-pytest", + "sphinx<5.2", # TODO 5.2 changes the attributes of desc/desc_signature nodes ] [project.scripts] diff --git a/tests/test_renderers/fixtures/myst-config.txt b/tests/test_renderers/fixtures/myst-config.txt index 020623c3..668895a2 100644 --- a/tests/test_renderers/fixtures/myst-config.txt +++ b/tests/test_renderers/fixtures/myst-config.txt @@ -49,6 +49,8 @@ www.example.com strike + +:1: (WARNING/2) Strikethrough is currently only supported in HTML output [myst.strikethrough] . [gfm-disallowed-html] --myst-gfm-only="yes" @@ -141,3 +143,34 @@ www.commonmark.org/he + + a +. + +[attrs_image_warnings] --myst-enable-extensions=attrs_image +. +![a](b){width=1x height=2x align=other } +. + + + + + Invalid width value for image: '1x' [myst.image] + + + Invalid height value for image: '2x' [myst.image] + + + Invalid align value for image: 'other' [myst.image] + a + +:1: (WARNING/2) Invalid width value for image: '1x' [myst.image] +:1: (WARNING/2) Invalid height value for image: '2x' [myst.image] +:1: (WARNING/2) Invalid align value for image: 'other' [myst.image] +. diff --git a/tests/test_renderers/test_myst_config.py b/tests/test_renderers/test_myst_config.py index 0f58cd76..31e2444e 100644 --- a/tests/test_renderers/test_myst_config.py +++ b/tests/test_renderers/test_myst_config.py @@ -31,4 +31,8 @@ def test_cmdline(file_params): parser=Parser(), settings_overrides=settings, ) - file_params.assert_expected(doctree.pformat(), rstrip_lines=True) + output = doctree.pformat() + warnings = report_stream.getvalue() + if warnings: + output += "\n" + warnings + file_params.assert_expected(output, rstrip_lines=True)