|
9 | 9 | // tags). It does not implement the WHATWG error-recovery tree-construction |
10 | 10 | // algorithm (that is not a context-free grammar); conformance is measured against |
11 | 11 | // `parse5` on well-formed input. See memory: html-vue-markup. |
12 | | -import { token, rule, defineGrammar, many, opt, alt } from './src/api.ts'; |
| 12 | +import { token, rule, defineGrammar, many, opt, alt, seq, oneOf, noneOf, range, anyChar, star, plus, notFollowedBy } from './src/api.ts'; |
13 | 13 | import type { MarkupConfig } from './src/types.ts'; |
14 | 14 |
|
15 | 15 | // ── Tokens ── |
| 16 | +const word = oneOf(range('A', 'Z'), range('a', 'z'), range('0', '9'), '_'); |
| 17 | +const whitespace = oneOf('\t', '\n', '\f', '\r', ' '); |
| 18 | + |
16 | 19 | // Tag and attribute names: a letter, then name chars (incl. `-` for custom |
17 | 20 | // elements / data-*, `:` for namespaced names like `xlink:href`). |
18 | | -const Name = token(/[a-zA-Z][\w:.-]*/, { identifier: true }); |
| 21 | +const Name = token(seq(oneOf(range('a', 'z'), range('A', 'Z')), star(oneOf(word, ':', '.', '-'))), { identifier: true }); |
19 | 22 | // An OPEN void-element name (`br`, `img`, `meta`, …). The lexer retags these from |
20 | 23 | // Name (driven by `markup.voidTags`); the pattern is a placeholder, never matched |
21 | 24 | // fresh. A distinct token lets the parser's void branch match void elements without |
22 | 25 | // the generic engine knowing any tag names. |
23 | | -const VoidName = token(/[a-zA-Z][\w:.-]*/, { scope: 'entity.name.tag' }); |
| 26 | +const VoidName = token(seq(oneOf(range('a', 'z'), range('A', 'Z')), star(oneOf(word, ':', '.', '-'))), { scope: 'entity.name.tag' }); |
24 | 27 | // Quoted attribute value (double or single). |
25 | | -const AttrValue = token(/"[^"]*"|'[^']*'/, { string: true }); |
| 28 | +const AttrValue = token(alt(seq('"', star(noneOf('"')), '"'), seq("'", star(noneOf("'")), "'")), { string: true }); |
26 | 29 | // Unquoted attribute value (`colspan=2`, `value=5px`, `href=https://x/`, `href=/a/b.css`): |
27 | 30 | // per WHATWG, an unquoted value ends ONLY at whitespace or `>`, so `/` is a legal value char |
28 | 31 | // (URLs / paths). The lexer scans the whole value as ONE token the moment it follows `=` (see |
29 | 32 | // markup.unquotedValueToken below) — so the leading `/` of a path and the trailing `/` of a URL |
30 | 33 | // stay in the value, while a `/>` self-close (where no value is being read) stays punctuation. |
31 | 34 | // `\`` excluded to mirror the highlighter's value pattern. The leading-char-class scan in the |
32 | 35 | // lexer makes this token's own pattern a backstop (it is no longer subject to the Name-first race). |
33 | | -const UnquotedValue = token(/[^\s"'<>=`]+/, { scope: 'string.unquoted.html' }); |
| 36 | +const UnquotedValue = token(plus(noneOf(whitespace, '"', "'", '<', '>', '=', '`')), { scope: 'string.unquoted.html' }); |
34 | 37 | // Markup-mode content tokens — emitted by the lexer state machine, not matched by |
35 | 38 | // these patterns (the patterns are placeholders; see gen-lexer markupTokenNames). |
36 | | -const Text = token(/[^<]+/, { scope: 'text.html' }); |
37 | | -const RawText = token(/[^<]+/, { scope: 'source.embedded' }); |
38 | | -const Comment = token(/<!--[\s\S]*?-->/, { scope: 'comment.block.html' }); |
| 39 | +const Text = token(plus(noneOf('<')), { scope: 'text.html' }); |
| 40 | +const RawText = token(plus(noneOf('<')), { scope: 'source.embedded' }); |
| 41 | +const Comment = token(seq('<!--', star(seq(notFollowedBy('-->'), anyChar()), { greedy: false }), '-->'), { scope: 'comment.block.html' }); |
39 | 42 |
|
40 | 43 | // ── Rules ── |
41 | 44 | // An attribute: a name, optionally `= value` (quoted, or an unquoted name/number). |
|
0 commit comments