Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/close-tag-less-than.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"htmljs-parser": patch
---

In HTML mode, a "</" after a whitespace-terminated unenclosed attribute value is now treated as a close tag instead of being consumed as a less-than operator, and a close tag found before the open tag is closed reports a targeted error suggesting parentheses.
5 changes: 5 additions & 0 deletions .changeset/quiet-pears-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"htmljs-parser": minor
---

In HTML mode, a whitespace-preceded `>=` in an unenclosed attribute value is now parsed as a comparison operator (eg `<if=count >= 10>`), and a whitespace-preceded `>` that looks like a split comparison (eg `<if=count > 10>`) now reports an error suggesting parentheses instead of silently ending the tag.
6 changes: 0 additions & 6 deletions agent-feedback/bugs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@

Out-of-scope defects noticed while working on something else. Format and rules: [README.md](README.md).

## Emit an error when a `>=` truncates an unquoted attribute-value expression

`src/states/ATTRIBUTE.ts` › `shouldTerminateHtmlAttrValue` | 2026-07-30 | impact:med | effort:low

`shouldTerminateHtmlAttrValue` terminates on the first unenclosed `CODE.CLOSE_ANGLE_BRACKET` with a single look-behind exception for `=>`, and nothing covers the sibling `>=`, so `<const/positive=input.delta >= 0/>` tokenizes as `attrValue "input.delta"`, `openTagEnd`, then text `"= 0/>"` and raises no `onError` at all — marko turns that into `const positive = input.delta` plus a literal `= 0/>` text node, a silent miscompile of an ordinary comparison. Unlike a lone `>`, which is genuinely ambiguous with idiomatic HTML (`<div class=x > text</div>`), a tag close immediately followed by `=` is almost never intended, so this form is decidable here rather than in a consumer's heuristic. Add the look-ahead to the `CLOSE_ANGLE_BRACKET` case: keep terminating, but when `pos !== this.start && data.charCodeAt(pos + 1) === CODE.EQUAL` record it on the `ExpressionMeta` so `ATTRIBUTE.return`'s `ATTR_STAGE.VALUE` case — which already calls `this.emitError` for `INVALID_ATTRIBUTE_VALUE` — can emit `ErrorCode.INVALID_EXPRESSION`; the emit has to land there because `shouldTerminate` is handed no `Parser`. The one false positive to weigh is `<div class=x>=1</div>`, whose text legitimately begins with `=`. Re-verify with `node --input-type=module -e 'import{createParser,TagType}from"./src/index.ts";const p=createParser({onError:e=>console.log("ERR",e.message),onText:r=>console.log("text",JSON.stringify(p.read(r))),onAttrValue:r=>console.log("value",JSON.stringify(p.read(r.value))),onOpenTagName:()=>TagType.html});p.parse("<if=input.delta >= 0>yes</if>")'` — it prints `value "input.delta"` and `text "= 0>yes"` with no `ERR` line today, and `pnpm test` must stay green after the change.

## Treat a backslash-escaped quote as text in parsed-text bodies and parsed strings

`src/states/PARSED_TEXT_CONTENT.ts` › `PARSED_TEXT_CONTENT` | 2026-07-30 | impact:med | effort:low
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
1╭─ <if=x > y.map(z => z)>Hi</if>
│ ││ │╰─ error(AMBIGUOUS_ATTRIBUTE_VALUE:Ambiguous ">" in attribute. A ">" preceded by whitespace ends the tag. If "x > y.map(z => z)" was intended as a single expression, wrap it in parentheses, eg "=(x > y.map(z => z))". If the tag was instead meant to end at the first ">", leaving "y.map(z => z)>" as body content, remove the whitespace before that ">".) "x > y.map(z => z)"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=x > y.map(z => z)>Hi</if>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
1╭─ <if=count > = 10>Hi</if>
│ ││ ││ ││ │ │ ╰─ closeTagEnd(if)
│ ││ ││ ││ │ ╰─ closeTagName "if"
│ ││ ││ ││ ╰─ closeTagStart "</"
│ ││ ││ │╰─ text " = 10>Hi"
│ ││ ││ ╰─ openTagEnd
│ ││ │╰─ attrValue.value "count"
│ ││ ├─ attrValue "=count"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=count > = 10>Hi</if>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
1╭─ <if=a > b + c
│ ││ ││ │╰─ text " b + c"
│ ││ ││ ╰─ openTagEnd
│ ││ │╰─ attrValue.value
│ ││ ├─ attrValue "=a"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
│ ├─ error(MISSING_END_TAG:Missing ending "if" tag) "<if=a >"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=a > b + c
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
1╭─ <if=a > (b >= c)>Hi</if>
│ ││ │╰─ error(AMBIGUOUS_ATTRIBUTE_VALUE:Ambiguous ">" in attribute. A ">" preceded by whitespace ends the tag. If "a > (b >= c)" was intended as a single expression, wrap it in parentheses, eg "=(a > (b >= c))". If the tag was instead meant to end at the first ">", leaving "(b >= c)>" as body content, remove the whitespace before that ">".) "a > (b >= c)"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=a > (b >= c)>Hi</if>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
1╭─ <if=a > b[0] / c>Hi</if>
│ ││ │╰─ error(AMBIGUOUS_ATTRIBUTE_VALUE:Ambiguous ">" in attribute. A ">" preceded by whitespace ends the tag. If "a > b[0] / c" was intended as a single expression, wrap it in parentheses, eg "=(a > b[0] / c)". If the tag was instead meant to end at the first ">", leaving "b[0] / c>" as body content, remove the whitespace before that ">".) "a > b[0] / c"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=a > b[0] / c>Hi</if>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
1╭─ <if=count > 10 && flag>Hi</if>
│ ││ │╰─ error(AMBIGUOUS_ATTRIBUTE_VALUE:Ambiguous ">" in attribute. A ">" preceded by whitespace ends the tag. If "count > 10 && flag" was intended as a single expression, wrap it in parentheses, eg "=(count > 10 && flag)". If the tag was instead meant to end at the first ">", leaving "10 && flag>" as body content, remove the whitespace before that ">".) "count > 10 && flag"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=count > 10 && flag>Hi</if>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
1╭─ <if count > 10>Hi</if>
│ ││ ├─ error(AMBIGUOUS_ATTRIBUTE_VALUE:Ambiguous ">" in attribute. A ">" preceded by whitespace ends the tag. If "count > 10" was intended as a single expression, wrap it in parentheses, eg "=(count > 10)". If the tag was instead meant to end at the first ">", leaving "10>" as body content, remove the whitespace before that ">".) "count > 10"
│ ││ ╰─ attrName "count"
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if count > 10>Hi</if>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
1╭─ <if=a > b >= c>Hi</if>
│ ││ │╰─ error(AMBIGUOUS_ATTRIBUTE_VALUE:Ambiguous ">" in attribute. A ">" preceded by whitespace ends the tag. If "a > b >= c" was intended as a single expression, wrap it in parentheses, eg "=(a > b >= c)". If the tag was instead meant to end at the first ">", leaving "b >= c>" as body content, remove the whitespace before that ">".) "a > b >= c"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=a > b >= c>Hi</if>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
1╭─ <if=a > /b/>Hi</if>
│ ││ ││ ││ │ │ ╰─ closeTagEnd(if)
│ ││ ││ ││ │ ╰─ closeTagName "if"
│ ││ ││ ││ ╰─ closeTagStart "</"
│ ││ ││ │╰─ text " /b/>Hi"
│ ││ ││ ╰─ openTagEnd
│ ││ │╰─ attrValue.value
│ ││ ├─ attrValue "=a"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=a > /b/>Hi</if>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
1╭─ <input value=count > 10/>
│ ││ │ ╰─ error(AMBIGUOUS_ATTRIBUTE_VALUE:Ambiguous ">" in attribute. A ">" preceded by whitespace ends the tag. If "count > 10" was intended as a single expression, wrap it in parentheses, eg "=(count > 10)". If the tag was instead meant to end at the first ">", leaving "10/>" as body content, remove the whitespace before that ">".) "count > 10"
│ ││ ╰─ attrName "value"
│ │╰─ tagName "input"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<input value=count > 10/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
1╭─ <if=a > "b>" >Hi</if>
│ ││ ││ ││ │ │ ╰─ closeTagEnd(if)
│ ││ ││ ││ │ ╰─ closeTagName "if"
│ ││ ││ ││ ╰─ closeTagStart "</"
│ ││ ││ │╰─ text " \"b>\" >Hi"
│ ││ ││ ╰─ openTagEnd
│ ││ │╰─ attrValue.value
│ ││ ├─ attrValue "=a"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=a > "b>" >Hi</if>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
1╭─ <if=count > 10 && flag />
│ ││ │╰─ error(AMBIGUOUS_ATTRIBUTE_VALUE:Ambiguous ">" in attribute. A ">" preceded by whitespace ends the tag. If "count > 10 && flag" was intended as a single expression, wrap it in parentheses, eg "=(count > 10 && flag)". If the tag was instead meant to end at the first ">", leaving "10 && flag />" as body content, remove the whitespace before that ">".) "count > 10 && flag"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=count > 10 && flag />
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
1╭─ <div class=x > foo(bar></div>
│ ││ │ ││ ││ │ │ ╰─ closeTagEnd(div)
│ ││ │ ││ ││ │ ╰─ closeTagName "div"
│ ││ │ ││ ││ ╰─ closeTagStart "</"
│ ││ │ ││ │╰─ text " foo(bar>"
│ ││ │ ││ ╰─ openTagEnd
│ ││ │ │╰─ attrValue.value
│ ││ │ ╰─ attrValue "=x"
│ ││ ╰─ attrName "class"
│ │╰─ tagName "div"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class=x > foo(bar></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
1╭─ <div class=x > oops)></div>
│ ││ │ ││ ││ │ │ ╰─ closeTagEnd(div)
│ ││ │ ││ ││ │ ╰─ closeTagName "div"
│ ││ │ ││ ││ ╰─ closeTagStart "</"
│ ││ │ ││ │╰─ text " oops)>"
│ ││ │ ││ ╰─ openTagEnd
│ ││ │ │╰─ attrValue.value
│ ││ │ ╰─ attrValue "=x"
│ ││ ╰─ attrName "class"
│ │╰─ tagName "div"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class=x > oops)></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
1╭─ <if=count > 10>Hi</if>
│ ││ │╰─ error(AMBIGUOUS_ATTRIBUTE_VALUE:Ambiguous ">" in attribute. A ">" preceded by whitespace ends the tag. If "count > 10" was intended as a single expression, wrap it in parentheses, eg "=(count > 10)". If the tag was instead meant to end at the first ">", leaving "10>" as body content, remove the whitespace before that ">".) "count > 10"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=count > 10>Hi</if>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
1╭─ <div class=x + y </div>
│ ││ │ ││ ╰─ error(MALFORMED_OPEN_TAG:A close tag was found before the "div" open tag was closed. If the "</" was intended as part of an attribute expression (eg a less-than comparison), wrap the value in parentheses.)
│ ││ │ │╰─ attrValue.value "x + y"
│ ││ │ ╰─ attrValue "=x + y"
│ ││ ╰─ attrName "class"
│ │╰─ tagName "div"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class=x + y </div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
1╭─ <if=a < /b/.test(c)>Hi</if>
│ ││ ││ ││ │ │ ╰─ closeTagEnd(if)
│ ││ ││ ││ │ ╰─ closeTagName "if"
│ ││ ││ ││ ╰─ closeTagStart "</"
│ ││ ││ │╰─ text "Hi"
│ ││ ││ ╰─ openTagEnd
│ ││ │╰─ attrValue.value "a < /b/.test(c)"
│ ││ ├─ attrValue "=a < /b/.test(c)"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
1 change: 1 addition & 0 deletions src/__tests__/fixtures/attr-less-than-regex/input.marko
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=a < /b/.test(c)>Hi</if>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
1╭─ <if=a < b>Hi</if>
│ ││ ││ ││ │ │ ╰─ closeTagEnd(if)
│ ││ ││ ││ │ ╰─ closeTagName "if"
│ ││ ││ ││ ╰─ closeTagStart "</"
│ ││ ││ │╰─ text "Hi"
│ ││ ││ ╰─ openTagEnd
│ ││ │╰─ attrValue.value "a < b"
│ ││ ├─ attrValue "=a < b"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
1 change: 1 addition & 0 deletions src/__tests__/fixtures/attr-less-than/input.marko
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=a < b>Hi</if>
Original file line number Diff line number Diff line change
Expand Up @@ -683,17 +683,14 @@
│ │╰─ openTagEnd:selfClosed "/>"
╰─ ╰─ attrName
159╭─ <a=x >=
│ ││││ │╰─ text "=\ny "
│ ││││ ╰─ openTagEnd
│ │││╰─ attrValue.value
│ ││├─ attrValue "=x"
│ │││╰─ attrValue.value "x >=\ny"
│ ││├─ attrValue "=x >=\ny"
│ ││╰─ attrName
│ │╰─ tagName
╰─ ╰─ openTagStart
160╭─ y </a>
│ │ │╰─ closeTagEnd(a)
│ │ ╰─ closeTagName
╰─ ╰─ closeTagStart "</"
160╭─ y a/>
│ │╰─ openTagEnd:selfClosed "/>"
╰─ ╰─ attrName
161╭─ <a=x &=
│ │││╰─ attrValue.value "x &=\ny"
│ ││├─ attrValue "=x &=\ny"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ y a/>
<a=x <=
y a/>
<a=x >=
y </a>
y a/>
<a=x &=
y a/>
<a=x &&=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,17 +191,14 @@
│ │╰─ openTagEnd:selfClosed "/>"
╰─ ╰─ attrName
45╭─ <a=x
│ │││╰─ attrValue.value
│ ││├─ attrValue "=x"
│ │││╰─ attrValue.value "x\n>= y"
│ ││├─ attrValue "=x\n>= y"
│ ││╰─ attrName
│ │╰─ tagName
╰─ ╰─ openTagStart
46╭─ >= y </a>
│ ││ │ │╰─ closeTagEnd(a)
│ ││ │ ╰─ closeTagName
│ ││ ╰─ closeTagStart "</"
│ │╰─ text "= y "
╰─ ╰─ openTagEnd
46╭─ >= y a/>
│ │╰─ openTagEnd:selfClosed "/>"
╰─ ╰─ attrName
47╭─ <a=x
│ │││╰─ attrValue.value "x\n&= y"
│ ││├─ attrValue "=x\n&= y"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ a=x
<a=x
<= y a/>
<a=x
>= y </a>
>= y a/>
<a=x
&= y a/>
<a=x
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -511,14 +511,11 @@
│ ││╰─ attrName
│ │╰─ tagName
╰─ ╰─ openTagStart
66╭─ <a=x >=y </a>
│ ││││ ││ │ │╰─ closeTagEnd(a)
│ ││││ ││ │ ╰─ closeTagName
│ ││││ ││ ╰─ closeTagStart "</"
│ ││││ │╰─ text "=y "
│ ││││ ╰─ openTagEnd
│ │││╰─ attrValue.value
│ ││├─ attrValue "=x"
66╭─ <a=x >=y a/>
│ ││││ │╰─ openTagEnd:selfClosed "/>"
│ ││││ ╰─ attrName
│ │││╰─ attrValue.value "x >=y"
│ ││├─ attrValue "=x >=y"
│ ││╰─ attrName
│ │╰─ tagName
╰─ ╰─ openTagStart
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ a=( x ) {y } a
<a=x !=y a/>
<a=x !==y a/>
<a=x <=y a/>
<a=x >=y </a>
<a=x >=y a/>
<a=x &=y a/>
<a=x &&=y a/>
<a=x |=y a/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -707,14 +707,11 @@
│ ││╰─ attrName
│ │╰─ tagName
╰─ ╰─ openTagStart
90╭─ <a=x >= y </a>
│ ││││ ││ │ │╰─ closeTagEnd(a)
│ ││││ ││ │ ╰─ closeTagName
│ ││││ ││ ╰─ closeTagStart "</"
│ ││││ │╰─ text "= y "
│ ││││ ╰─ openTagEnd
│ │││╰─ attrValue.value
│ ││├─ attrValue "=x"
90╭─ <a=x >= y a/>
│ ││││ │╰─ openTagEnd:selfClosed "/>"
│ ││││ ╰─ attrName
│ │││╰─ attrValue.value "x >= y"
│ ││├─ attrValue "=x >= y"
│ ││╰─ attrName
│ │╰─ tagName
╰─ ╰─ openTagStart
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ a = async function (x) { console.log("y") } a
<a=x != y a/>
<a=x !== y a/>
<a=x <= y a/>
<a=x >= y </a>
<a=x >= y a/>
<a=x &= y a/>
<a=x &&= y a/>
<a=x |= y a/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
1╭─ <if=count >= 10>Hi</if>
│ ││ ││ ││ │ │ ╰─ closeTagEnd(if)
│ ││ ││ ││ │ ╰─ closeTagName "if"
│ ││ ││ ││ ╰─ closeTagStart "</"
│ ││ ││ │╰─ text "Hi"
│ ││ ││ ╰─ openTagEnd
│ ││ │╰─ attrValue.value "count >= 10"
│ ││ ├─ attrValue "=count >= 10"
│ ││ ╰─ attrName
│ │╰─ tagName "if"
╰─ ╰─ openTagStart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<if=count >= 10>Hi</if>
Loading