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:: ![alt](https://image.com){#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 . ![a](b){#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:: ![alt](https://image.com){#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:: ![alt](https://image.com){#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](b){#a .a}{.b class=x other=h}{#x class="x g" other=a} .

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()