diff --git a/mdit_py_plugins/attrs/index.py b/mdit_py_plugins/attrs/index.py index 0f608c8..124b6d0 100644 --- a/mdit_py_plugins/attrs/index.py +++ b/mdit_py_plugins/attrs/index.py @@ -41,8 +41,14 @@ def attrs_plugin( - `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 (`}`). + Backslash escapes may be used inside quoted values, + to allow a ``"`` character within the value. + Note that the backslash is retained in the value: + ``{k="a\\"b"}`` gives ``k`` the value ``a\\"b``. + - `%` begins a comment, which ends with the next `%`. + If no further `%` occurs in the rest of the line (for a block attribute) + or of the paragraph (for an inline attribute), + the comment instead ends at the next `}`, which also ends the attribute. Multiple attribute blocks are merged. @@ -175,8 +181,8 @@ def _attr_inline_rule( 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']}" + if "class" in attrs and "class" in attr_token.attrs: + attrs["class"] = f"{attr_token.attrs['class']} {attrs['class']}" _add_attrs(attr_token, attrs, allowed) return True @@ -238,7 +244,8 @@ def _attr_resolve_block_rule(state: StateCore, *, allowed: set[str] | None) -> N i += 1 continue - if i + 1 < len_tokens: + # skip closing tokens; attributes would be rendered into the closing tag + if i + 1 < len_tokens and state.tokens[i + 1].nesting >= 0: next_token = state.tokens[i + 1] # classes are appended diff --git a/mdit_py_plugins/attrs/parse.py b/mdit_py_plugins/attrs/parse.py index 061574f..605bbfb 100644 --- a/mdit_py_plugins/attrs/parse.py +++ b/mdit_py_plugins/attrs/parse.py @@ -100,17 +100,28 @@ def parse(string: str) -> tuple[int, dict[str, str]]: """Parse attributes from start of string. :returns: (length of parsed string, dict of attributes) + :raises ParseError: if the attributes are malformed, + or the string ends before the closing `}` """ pos = 0 state: State = State.START tokens = TokenState() while pos < len(string): - state = HANDLERS[state](string[pos], pos, tokens) + if ( + state == State.SCANNING_COMMENT + and string[pos] == "}" + and "%" not in string[pos + 1 :] + ): + # a comment ends at the next `%`, but there is none left to close it, + # so this `}` ends both the comment and the attributes + state = State.DONE + else: + state = HANDLERS[state](string[pos], pos, tokens) if state == State.DONE: return pos, tokens.compile(string) pos = pos + 1 - return pos, tokens.compile(string) + raise ParseError("Attributes not terminated", pos) def handle_start(char: str, pos: int, tokens: TokenState) -> State: diff --git a/tests/fixtures/attrs.md b/tests/fixtures/attrs.md index b6d3890..6523b94 100644 --- a/tests/fixtures/attrs.md +++ b/tests/fixtures/attrs.md @@ -61,6 +61,61 @@ a = 1 . +block: attrs last in a blockquote +. +> {.a} + +para +. +
+para
+. + +block: attrs last in a list item +. +- {.a} +. +++. + block after paragraph . a @@ -70,6 +125,64 @@ a {#a .a c=1} . +unterminated: block group is not an attribute block +. +{.a +para +. +para
+
{.a +para
+. + +comment: block +. +{%x} +para +. +para
+. + +comment: block after a class +. +{.a %c} +para +. +para
+. + +comment: block closed by a percent +. +{% just a comment %} +para +. +para
+. + +comment: block with a brace inside the comment +. +{% x } y %} +para +. +para
+. + +comment: block, attributes after a brace inside the comment +. +{% c } % .b} +para +. +para
+. + +comment: block, a brace inside the comment does not terminate the group +. +{% see } below %{x} +para +. +{% see } below %{x} +para
+. + simple reference link . @@ -101,6 +214,13 @@ simple inline codea
a
a{
a{.a
a{
+. + +unterminated: image +. +{.x +. +{.x
a{.a
+more
a{
a{ .a b
a{k="a}
a
a
ab
a
a
a
a and {more}
a{.a %c} and 100% sure
[a]{.b}
. +spans: unterminated attributes are not a span +. +[a]{ +. +[a]{
+. + +spans: unterminated attributes with a class are not a span +. +[a]{.x +. +[a]{.x
+. + +spans: comment +. +[a]{%c} +. +a
+. + +spans: a brace inside an unterminated comment is not a span +. +[a]{% a } %{b} +. +[a]{% a } %{b}
+. + spans: nested text syntax . [*a*]{.b}c @@ -238,7 +514,49 @@ spans: merge attributes . [a]{#a .a}{#b .a .b other=c}{other=d} . -a
+a
+. + +spans: merge classes from two groups +. +[a]{.x}{.y} +. +a
+. + +spans: merge classes from three groups +. +[a]{.x}{.y}{.z} +. +a
+. + +spans: merge classes with an id in each group +. +[a]{.x #p}{.y #q} +. +a
+. + +spans: merge classes on the outer of nested spans +. +[[a]{.i}]{.x}{.y} +. +a
+. + +links: merge classes from two groups +. +[a](u){.x}{.y} +. + +. + +links: merge classes from three groups +. +[a](u){.x}{.y}{.z} +. + . Indented by 4 spaces