Skip to content

Parser rejects attribute names HTML allows (@click, #slot, [prop]), and blames the tag #45

Description

@fcapolini
<button @click="x()">hi</button>
/t.html:2:9: Unterminated tag BUTTON

:on-click=${…} and data-x="y" on the same markup are fine.

It is not just @

Every one of these fails identically, and all of them are legal HTML:

written result
@click="x" Unterminated tag BUTTON
#slot="x" Unterminated tag BUTTON
[prop]="x" Unterminated tag BUTTON
(evt)="x" Unterminated tag BUTTON
%x="x" Unterminated tag BUTTON
^y="x" Unterminated tag BUTTON
~z="x" Unterminated tag BUTTON

The HTML attribute-name production forbids only controls, space, ", ', >, /, = and noncharacters. Everything above is permitted, so this is Markout being stricter than HTML — against the pitch that HTML is the syntax.

It shows up in practice when porting markup from Vue, Alpine or Angular, which is where it was found (building the benchmark ports, 2026-08-25). The attribute is inert to Markout either way; the page just has to parse.

Where

skipName() in packages/core/src/html/parser.ts is an allowlist: letters, digits, -, _, :, plus ., $, *, +, ! when it is an attribute. Anything else ends the name, so the = that follows is read as stray text and the tag never closes.

Its own doc comment states the policy the fix should follow:

The lexer stays permissive about where they appear — which names are meaningful is the compiler's judgement, not the parser's.

That is exactly right, and @ is where it is not being followed. The natural fix is to invert the test — accept anything the spec does not forbid — rather than extend the allowlist one character at a time.

The diagnostic is wrong regardless

Unterminated tag BUTTON names the tag and points at column 9, which is the @. Even if some character stays rejected, the message should say the attribute name is what it cannot read. As it stands the error describes a consequence several tokens downstream of the cause, and the author is left looking for a missing >.

Care needed

+, ! and = interact: class+= and class!= are composite-attribute operators, so whatever replaces the allowlist has to keep = terminating a name while +/! immediately before it do not. Worth a test per operator alongside the new characters.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions