From 3d7fde1d0787bec1e49a48f3999fa2bfba4003c2 Mon Sep 17 00:00:00 2001
From: Chris Sewell
Date: Sun, 4 Dec 2022 20:30:41 +0100
Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20NEW:=20Add=20span=20plugin?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Also update attrs plugin to support non self-closing syntaxes (like links)
---
docs/index.md | 4 ++
mdit_py_plugins/attrs/__init__.py | 2 +-
mdit_py_plugins/attrs/index.py | 76 ++++++++++++++++++++++++++++---
tests/fixtures/attrs.md | 16 +++++++
tests/fixtures/span.md | 62 +++++++++++++++++++++++++
tests/test_attrs.py | 21 +++++++--
6 files changed, 169 insertions(+), 12 deletions(-)
create mode 100644 tests/fixtures/span.md
diff --git a/docs/index.md b/docs/index.md
index 8b38d31..893377d 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -87,6 +87,10 @@ html_string = md.render("some *Markdown*")
## Inline Attributes
+```{eval-rst}
+.. autofunction:: mdit_py_plugins.attrs.span_plugin
+```
+
```{eval-rst}
.. autofunction:: mdit_py_plugins.attrs.attrs_plugin
```
diff --git a/mdit_py_plugins/attrs/__init__.py b/mdit_py_plugins/attrs/__init__.py
index 9359cf8..32be50f 100644
--- a/mdit_py_plugins/attrs/__init__.py
+++ b/mdit_py_plugins/attrs/__init__.py
@@ -1 +1 @@
-from .index import attrs_plugin # noqa: F401
+from .index import attrs_plugin, span_plugin # noqa: F401
diff --git a/mdit_py_plugins/attrs/index.py b/mdit_py_plugins/attrs/index.py
index bc3feda..df48deb 100644
--- a/mdit_py_plugins/attrs/index.py
+++ b/mdit_py_plugins/attrs/index.py
@@ -1,10 +1,13 @@
+from typing import List, Optional
+
from markdown_it import MarkdownIt
from markdown_it.rules_inline import StateInline
+from markdown_it.token import Token
from .parse import ParseError, parse
-def attrs_plugin(md: MarkdownIt, *, after=("image", "code_inline")):
+def attrs_plugin(md: MarkdownIt, *, after=("image", "code_inline", "link_close")):
"""Parse inline attributes that immediately follow certain inline elements::
{#id .a b=c}
@@ -22,11 +25,10 @@ def attrs_plugin(md: MarkdownIt, *, after=("image", "code_inline")):
Backslash escapes may be used inside quoted values.
- `%` begins a comment, which ends with the next `%` or the end of the attribute (`}`).
- **Note:** This plugin is currently limited to "self-closing" elements,
- such as images and code spans. It does not work with links or emphasis.
-
:param md: The MarkdownIt instance to modify.
:param after: The names of inline elements after which attributes may be specified.
+ This plugin does not support attributes after emphasis, strikethrough or text elements,
+ which all require post-parse processing.
"""
def attr_rule(state: StateInline, silent: bool):
@@ -39,12 +41,72 @@ def attr_rule(state: StateInline, silent: bool):
new_pos, attrs = parse(state.src[state.pos :])
except ParseError:
return False
+ token_index = _find_opening(state.tokens, len(state.tokens) - 1)
+ if token_index is None:
+ return False
state.pos += new_pos + 1
if not silent:
+ attr_token = state.tokens[token_index]
if "class" in attrs and "class" in token.attrs:
- attrs["class"] = f"{token.attrs['class']} {attrs['class']}"
- token.attrs.update(attrs)
-
+ attrs["class"] = f"{attr_token.attrs['class']} {attrs['class']}"
+ attr_token.attrs.update(attrs)
return True
md.inline.ruler.push("attr", attr_rule)
+
+
+def _find_opening(tokens: List[Token], index: int) -> Optional[int]:
+ """Find the opening token index, if the token is closing."""
+ if tokens[index].nesting != -1:
+ return index
+ level = 0
+ while index >= 0:
+ level += tokens[index].nesting
+ if level == 0:
+ return index
+ index -= 1
+ return None
+
+
+def span_plugin(md: MarkdownIt):
+ """Parse inline attributes that immediately follow a span of text, encapsulated by `[]`.
+
+ .. code-block:: none
+
+ [This is a span]{#id .a b=c}
+ """
+
+ def span_rule(state: StateInline, silent: bool):
+ if state.srcCharCode[state.pos] != 0x5B: # /* [ */
+ return False
+
+ maximum = state.posMax
+ labelStart = state.pos + 1
+ labelEnd = state.md.helpers.parseLinkLabel(state, state.pos, False)
+
+ # parser failed to find ']', so it's not a valid span
+ if labelEnd < 0:
+ return False
+
+ pos = labelEnd + 1
+
+ try:
+ new_pos, attrs = parse(state.src[pos:])
+ except ParseError:
+ return False
+
+ pos += new_pos + 1
+
+ if not silent:
+ state.pos = labelStart
+ state.posMax = labelEnd
+ token = state.push("span_open", "span", 1)
+ token.attrs = attrs
+ state.md.inline.tokenize(state)
+ token = state.push("span_close", "span", -1)
+
+ state.pos = pos
+ state.posMax = maximum
+ return True
+
+ md.inline.ruler.before("link", "span", span_rule)
diff --git a/tests/fixtures/attrs.md b/tests/fixtures/attrs.md
index 5910f00..119db82 100644
--- a/tests/fixtures/attrs.md
+++ b/tests/fixtures/attrs.md
@@ -1,3 +1,19 @@
+simple reference link
+.
+[text *emphasis*](a){#id .a}
+.
+text emphasis
+.
+
+simple definition link
+.
+[a][]{#id .b}
+
+[a]: /url
+.
+a
+.
+
simple image
.
{#id .a b=c}
diff --git a/tests/fixtures/span.md b/tests/fixtures/span.md
new file mode 100644
index 0000000..08b0698
--- /dev/null
+++ b/tests/fixtures/span.md
@@ -0,0 +1,62 @@
+simple
+.
+[a]{#id .b}c
+.
+ac
+.
+
+space between brace and attrs
+.
+[a] {.b}
+.
+[a] {.b}
+.
+
+nested text syntax
+.
+[*a*]{.b}c
+.
+ac
+.
+
+nested span
+.
+*[a]{.b}c*
+.
+ac
+.
+
+multi-line
+.
+x [a
+b]{#id
+b=c} y
+.
+x a
+b y
+.
+
+nested spans
+.
+[[a]{.b}]{.c}
+.
+a
+.
+
+span trumps short link
+.
+[a] [a]{#id .b}
+
+[a]: /url
+.
+a a
+.
+
+long link trumps span
+.
+[a][a]{#id .b}
+
+[a]: /url
+.
+a{#id .b}
+.
diff --git a/tests/test_attrs.py b/tests/test_attrs.py
index 729162c..24883d6 100644
--- a/tests/test_attrs.py
+++ b/tests/test_attrs.py
@@ -4,15 +4,28 @@
from markdown_it.utils import read_fixture_file
import pytest
-from mdit_py_plugins.attrs import attrs_plugin
+from mdit_py_plugins.attrs import attrs_plugin, span_plugin
-FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures", "attrs.md")
+FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures")
-@pytest.mark.parametrize("line,title,input,expected", read_fixture_file(FIXTURE_PATH))
-def test_fixture(line, title, input, expected):
+@pytest.mark.parametrize(
+ "line,title,input,expected", read_fixture_file(FIXTURE_PATH / "attrs.md")
+)
+def test_attrs(line, title, input, expected):
md = MarkdownIt("commonmark").use(attrs_plugin)
md.options["xhtmlOut"] = False
text = md.render(input)
print(text)
assert text.rstrip() == expected.rstrip()
+
+
+@pytest.mark.parametrize(
+ "line,title,input,expected", read_fixture_file(FIXTURE_PATH / "span.md")
+)
+def test_span(line, title, input, expected):
+ md = MarkdownIt("commonmark").use(span_plugin)
+ md.options["xhtmlOut"] = False
+ text = md.render(input)
+ print(text)
+ assert text.rstrip() == expected.rstrip()
From f7800f60e81133dd1f88fcc606c4fbee1e46d768 Mon Sep 17 00:00:00 2001
From: Chris Sewell
Date: Sun, 4 Dec 2022 22:28:53 +0100
Subject: [PATCH 2/4] Make links take precedence
---
mdit_py_plugins/attrs/index.py | 8 ++++++-
tests/fixtures/span.md | 38 ++++++++++++++++++++++++++++++----
2 files changed, 41 insertions(+), 5 deletions(-)
diff --git a/mdit_py_plugins/attrs/index.py b/mdit_py_plugins/attrs/index.py
index df48deb..568164c 100644
--- a/mdit_py_plugins/attrs/index.py
+++ b/mdit_py_plugins/attrs/index.py
@@ -74,6 +74,12 @@ def span_plugin(md: MarkdownIt):
.. code-block:: none
[This is a span]{#id .a b=c}
+
+ This syntax is inspired by
+ `Djot spans `_. # noqa: E501
+
+ Note Markdown link references take precedence over this syntax.
+
"""
def span_rule(state: StateInline, silent: bool):
@@ -109,4 +115,4 @@ def span_rule(state: StateInline, silent: bool):
state.posMax = maximum
return True
- md.inline.ruler.before("link", "span", span_rule)
+ md.inline.ruler.after("link", "span", span_rule)
diff --git a/tests/fixtures/span.md b/tests/fixtures/span.md
index 08b0698..eb65ad2 100644
--- a/tests/fixtures/span.md
+++ b/tests/fixtures/span.md
@@ -12,6 +12,27 @@ space between brace and attrs
[a] {.b}
.
+escaped span start
+.
+\[a]{.b}
+.
+[a]{.b}
+.
+
+escaped span end
+.
+[a\]{.b}
+.
+[a]{.b}
+.
+
+escaped span attribute
+.
+[a]\{.b}
+.
+[a]{.b}
+.
+
nested text syntax
.
[*a*]{.b}c
@@ -43,16 +64,16 @@ nested spans
a
.
-span trumps short link
+short link takes precedence over span
.
-[a] [a]{#id .b}
+[a]{#id .b}
[a]: /url
.
-a a
+a{#id .b}
.
-long link trumps span
+long link takes precedence over span
.
[a][a]{#id .b}
@@ -60,3 +81,12 @@ long link trumps span
.
a{#id .b}
.
+
+link inside span
+.
+[[a]]{#id .b}
+
+[a]: /url
+.
+a
+.
From 7eed62eb67b7a80cb0aab2449ccad2ea1ff0af5c Mon Sep 17 00:00:00 2001
From: Chris Sewell
Date: Sun, 4 Dec 2022 22:33:28 +0100
Subject: [PATCH 3/4] update
---
codecov.yml | 2 +-
mdit_py_plugins/attrs/index.py | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/codecov.yml b/codecov.yml
index f4796f9..80dcc51 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -2,7 +2,7 @@ coverage:
status:
project:
default:
- target: 93%
+ target: 92%
threshold: 0.2%
patch:
default:
diff --git a/mdit_py_plugins/attrs/index.py b/mdit_py_plugins/attrs/index.py
index 568164c..044c01a 100644
--- a/mdit_py_plugins/attrs/index.py
+++ b/mdit_py_plugins/attrs/index.py
@@ -7,7 +7,9 @@
from .parse import ParseError, parse
-def attrs_plugin(md: MarkdownIt, *, after=("image", "code_inline", "link_close")):
+def attrs_plugin(
+ md: MarkdownIt, *, after=("image", "code_inline", "link_close", "span_close")
+):
"""Parse inline attributes that immediately follow certain inline elements::
{#id .a b=c}
From 28f8f5a1ced2a43b870b301cbe0ae4d070e0d137 Mon Sep 17 00:00:00 2001
From: Chris Sewell
Date: Sun, 4 Dec 2022 23:03:09 +0100
Subject: [PATCH 4/4] merge span into attrs
---
docs/index.md | 4 --
mdit_py_plugins/attrs/__init__.py | 2 +-
mdit_py_plugins/attrs/index.py | 86 ++++++++++++-------------
tests/fixtures/attrs.md | 102 +++++++++++++++++++++++++++++-
tests/fixtures/span.md | 92 ---------------------------
tests/test_attrs.py | 13 +---
6 files changed, 145 insertions(+), 154 deletions(-)
diff --git a/docs/index.md b/docs/index.md
index 893377d..8b38d31 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -87,10 +87,6 @@ html_string = md.render("some *Markdown*")
## Inline Attributes
-```{eval-rst}
-.. autofunction:: mdit_py_plugins.attrs.span_plugin
-```
-
```{eval-rst}
.. autofunction:: mdit_py_plugins.attrs.attrs_plugin
```
diff --git a/mdit_py_plugins/attrs/__init__.py b/mdit_py_plugins/attrs/__init__.py
index 32be50f..9359cf8 100644
--- a/mdit_py_plugins/attrs/__init__.py
+++ b/mdit_py_plugins/attrs/__init__.py
@@ -1 +1 @@
-from .index import attrs_plugin, span_plugin # noqa: F401
+from .index import attrs_plugin # noqa: F401
diff --git a/mdit_py_plugins/attrs/index.py b/mdit_py_plugins/attrs/index.py
index 044c01a..7150e5d 100644
--- a/mdit_py_plugins/attrs/index.py
+++ b/mdit_py_plugins/attrs/index.py
@@ -8,12 +8,19 @@
def attrs_plugin(
- md: MarkdownIt, *, after=("image", "code_inline", "link_close", "span_close")
+ md: MarkdownIt,
+ *,
+ after=("image", "code_inline", "link_close", "span_close"),
+ spans=True,
):
"""Parse inline attributes that immediately follow certain inline elements::
{#id .a b=c}
+ This syntax is inspired by
+ `Djot spans
+ `_.
+
Inside the curly braces, the following syntax is possible:
- `.foo` specifies foo as a class.
@@ -27,13 +34,18 @@ def attrs_plugin(
Backslash escapes may be used inside quoted values.
- `%` begins a comment, which ends with the next `%` or the end of the attribute (`}`).
+ Multiple attribute blocks are merged.
+
:param md: The MarkdownIt instance to modify.
:param after: The names of inline elements after which attributes may be specified.
This plugin does not support attributes after emphasis, strikethrough or text elements,
which all require post-parse processing.
+ :param spans: If True, also parse attributes after spans of text, encapsulated by `[]`.
+ Note Markdown link references take precedence over this syntax.
+
"""
- def attr_rule(state: StateInline, silent: bool):
+ def _attr_rule(state: StateInline, silent: bool):
if state.pending or not state.tokens:
return False
token = state.tokens[-1]
@@ -54,7 +66,9 @@ def attr_rule(state: StateInline, silent: bool):
attr_token.attrs.update(attrs)
return True
- md.inline.ruler.push("attr", attr_rule)
+ if spans:
+ md.inline.ruler.after("link", "span", _span_rule)
+ md.inline.ruler.push("attr", _attr_rule)
def _find_opening(tokens: List[Token], index: int) -> Optional[int]:
@@ -70,51 +84,35 @@ def _find_opening(tokens: List[Token], index: int) -> Optional[int]:
return None
-def span_plugin(md: MarkdownIt):
- """Parse inline attributes that immediately follow a span of text, encapsulated by `[]`.
-
- .. code-block:: none
+def _span_rule(state: StateInline, silent: bool):
+ if state.srcCharCode[state.pos] != 0x5B: # /* [ */
+ return False
- [This is a span]{#id .a b=c}
+ maximum = state.posMax
+ labelStart = state.pos + 1
+ labelEnd = state.md.helpers.parseLinkLabel(state, state.pos, False)
- This syntax is inspired by
- `Djot spans `_. # noqa: E501
+ # parser failed to find ']', so it's not a valid span
+ if labelEnd < 0:
+ return False
- Note Markdown link references take precedence over this syntax.
+ pos = labelEnd + 1
- """
+ try:
+ new_pos, attrs = parse(state.src[pos:])
+ except ParseError:
+ return False
- def span_rule(state: StateInline, silent: bool):
- if state.srcCharCode[state.pos] != 0x5B: # /* [ */
- return False
+ pos += new_pos + 1
- maximum = state.posMax
- labelStart = state.pos + 1
- labelEnd = state.md.helpers.parseLinkLabel(state, state.pos, False)
-
- # parser failed to find ']', so it's not a valid span
- if labelEnd < 0:
- return False
-
- pos = labelEnd + 1
-
- try:
- new_pos, attrs = parse(state.src[pos:])
- except ParseError:
- return False
-
- pos += new_pos + 1
-
- if not silent:
- state.pos = labelStart
- state.posMax = labelEnd
- token = state.push("span_open", "span", 1)
- token.attrs = attrs
- state.md.inline.tokenize(state)
- token = state.push("span_close", "span", -1)
-
- state.pos = pos
- state.posMax = maximum
- return True
+ if not silent:
+ state.pos = labelStart
+ state.posMax = labelEnd
+ token = state.push("span_open", "span", 1)
+ token.attrs = attrs
+ state.md.inline.tokenize(state)
+ token = state.push("span_close", "span", -1)
- md.inline.ruler.after("link", "span", span_rule)
+ state.pos = pos
+ state.posMax = maximum
+ return True
diff --git a/tests/fixtures/attrs.md b/tests/fixtures/attrs.md
index 119db82..bd21ba8 100644
--- a/tests/fixtures/attrs.md
+++ b/tests/fixtures/attrs.md
@@ -54,9 +54,109 @@ more
more
.
-combined
+merging attributes
.
{#a .a}{.b class=x other=h}{#x class="x g" other=a}
.

.
+
+spans: simple
+.
+[a]{#id .b}c
+.
+ac
+.
+
+spans: space between brace and attrs
+.
+[a] {.b}
+.
+[a] {.b}
+.
+
+spans: escaped span start
+.
+\[a]{.b}
+.
+[a]{.b}
+.
+
+spans: escaped span end
+.
+[a\]{.b}
+.
+[a]{.b}
+.
+
+spans: escaped span attribute
+.
+[a]\{.b}
+.
+[a]{.b}
+.
+
+spans: nested text syntax
+.
+[*a*]{.b}c
+.
+ac
+.
+
+spans: nested span
+.
+*[a]{.b}c*
+.
+ac
+.
+
+spans: multi-line
+.
+x [a
+b]{#id
+b=c} y
+.
+x a
+b y
+.
+
+spans: nested spans
+.
+[[a]{.b}]{.c}
+.
+a
+.
+
+spans: short link takes precedence over span
+.
+[a]{#id .b}
+
+[a]: /url
+.
+a
+.
+
+spans: long link takes precedence over span
+.
+[a][a]{#id .b}
+
+[a]: /url
+.
+a
+.
+
+spans: link inside span
+.
+[[a]]{#id .b}
+
+[a]: /url
+.
+a
+.
+
+spans: merge attributes
+.
+[a]{#a .a}{#b .a .b other=c}{other=d}
+.
+a
+.
diff --git a/tests/fixtures/span.md b/tests/fixtures/span.md
index eb65ad2..e69de29 100644
--- a/tests/fixtures/span.md
+++ b/tests/fixtures/span.md
@@ -1,92 +0,0 @@
-simple
-.
-[a]{#id .b}c
-.
-ac
-.
-
-space between brace and attrs
-.
-[a] {.b}
-.
-[a] {.b}
-.
-
-escaped span start
-.
-\[a]{.b}
-.
-[a]{.b}
-.
-
-escaped span end
-.
-[a\]{.b}
-.
-[a]{.b}
-.
-
-escaped span attribute
-.
-[a]\{.b}
-.
-[a]{.b}
-.
-
-nested text syntax
-.
-[*a*]{.b}c
-.
-ac
-.
-
-nested span
-.
-*[a]{.b}c*
-.
-ac
-.
-
-multi-line
-.
-x [a
-b]{#id
-b=c} y
-.
-x a
-b y
-.
-
-nested spans
-.
-[[a]{.b}]{.c}
-.
-a
-.
-
-short link takes precedence over span
-.
-[a]{#id .b}
-
-[a]: /url
-.
-a{#id .b}
-.
-
-long link takes precedence over span
-.
-[a][a]{#id .b}
-
-[a]: /url
-.
-a{#id .b}
-.
-
-link inside span
-.
-[[a]]{#id .b}
-
-[a]: /url
-.
-a
-.
diff --git a/tests/test_attrs.py b/tests/test_attrs.py
index 24883d6..735374b 100644
--- a/tests/test_attrs.py
+++ b/tests/test_attrs.py
@@ -4,7 +4,7 @@
from markdown_it.utils import read_fixture_file
import pytest
-from mdit_py_plugins.attrs import attrs_plugin, span_plugin
+from mdit_py_plugins.attrs import attrs_plugin
FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures")
@@ -18,14 +18,3 @@ def test_attrs(line, title, input, expected):
text = md.render(input)
print(text)
assert text.rstrip() == expected.rstrip()
-
-
-@pytest.mark.parametrize(
- "line,title,input,expected", read_fixture_file(FIXTURE_PATH / "span.md")
-)
-def test_span(line, title, input, expected):
- md = MarkdownIt("commonmark").use(span_plugin)
- md.options["xhtmlOut"] = False
- text = md.render(input)
- print(text)
- assert text.rstrip() == expected.rstrip()