Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ NEW: Add span parsing to inline attributes plugin #55

Merged
merged 4 commits into from
Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ coverage:
status:
project:
default:
target: 93%
target: 92%
threshold: 0.2%
patch:
default:
Expand Down
84 changes: 76 additions & 8 deletions mdit_py_plugins/attrs/index.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
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", "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
<https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html#inline-attributes>`_.

Inside the curly braces, the following syntax is possible:

- `.foo` specifies foo as a class.
Expand All @@ -22,14 +34,18 @@ 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.
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]
Expand All @@ -39,12 +55,64 @@ 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)
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]:
"""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_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
118 changes: 117 additions & 1 deletion tests/fixtures/attrs.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
simple reference link
.
[text *emphasis*](a){#id .a}
.
<p><a href="a" id="id" class="a">text <em>emphasis</em></a></p>
.

simple definition link
.
[a][]{#id .b}

[a]: /url
.
<p><a href="/url" id="id" class="b">a</a></p>
.

simple image
.
![a](b){#id .a b=c}
Expand Down Expand Up @@ -38,9 +54,109 @@ more
more</p>
.

combined
merging attributes
.
![a](b){#a .a}{.b class=x other=h}{#x class="x g" other=a}
.
<p><img src="b" alt="a" id="x" class="a b x x g" other="a"></p>
.

spans: simple
.
[a]{#id .b}c
.
<p><span id="id" class="b">a</span>c</p>
.

spans: space between brace and attrs
.
[a] {.b}
.
<p>[a] {.b}</p>
.

spans: escaped span start
.
\[a]{.b}
.
<p>[a]{.b}</p>
.

spans: escaped span end
.
[a\]{.b}
.
<p>[a]{.b}</p>
.

spans: escaped span attribute
.
[a]\{.b}
.
<p>[a]{.b}</p>
.

spans: nested text syntax
.
[*a*]{.b}c
.
<p><span class="b"><em>a</em></span>c</p>
.

spans: nested span
.
*[a]{.b}c*
.
<p><em><span class="b">a</span>c</em></p>
.

spans: multi-line
.
x [a
b]{#id
b=c} y
.
<p>x <span id="id" b="c">a
b</span> y</p>
.

spans: nested spans
.
[[a]{.b}]{.c}
.
<p><span class="c"><span class="b">a</span></span></p>
.

spans: short link takes precedence over span
.
[a]{#id .b}

[a]: /url
.
<p><a href="/url" id="id" class="b">a</a></p>
.

spans: long link takes precedence over span
.
[a][a]{#id .b}

[a]: /url
.
<p><a href="/url" id="id" class="b">a</a></p>
.

spans: link inside span
.
[[a]]{#id .b}

[a]: /url
.
<p><span id="id" class="b"><a href="/url">a</a></span></p>
.

spans: merge attributes
.
[a]{#a .a}{#b .a .b other=c}{other=d}
.
<p><span id="b" class="a b" other="d">a</span></p>
.
Empty file added tests/fixtures/span.md
Empty file.
8 changes: 5 additions & 3 deletions tests/test_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@

from mdit_py_plugins.attrs import attrs_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)
Expand Down