Skip to content

Commit 880fdfd

Browse files
Implement token pattern IR (#11)
1 parent cca4c6a commit 880fdfd

47 files changed

Lines changed: 1715 additions & 1332 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,17 @@ And — from the same grammar — generators for the rest of the ecosystem, at v
222222
A grammar is a TypeScript module: tokens, operator precedence, and rules built from small combinators. A self-contained mini-example:
223223

224224
```ts
225-
import { token, rule, defineGrammar, left, op, sep } from './src/api.ts';
226-
227-
const Ident = token(/[a-zA-Z_$][a-zA-Z0-9_$]*/, { identifier: true });
228-
const Number = token(/[0-9]+(\.[0-9]+)?/);
225+
import {
226+
token, rule, defineGrammar, left, op, sep,
227+
seq, oneOf, range, named, plus, star, opt,
228+
} from './src/api.ts';
229+
230+
const digit = named('digit');
231+
const Ident = token(seq(
232+
oneOf(range('a', 'z'), range('A', 'Z'), '_', '$'),
233+
star(named('idCont')),
234+
), { identifier: true });
235+
const Number = token(seq(plus(digit), opt(seq('.', plus(digit)))));
229236

230237
const Expr = rule($ => [
231238
Ident,
@@ -258,8 +265,27 @@ Flat, irreducible facts — which keywords are control flow, which punctuation i
258265
Nothing in the engine knows about TypeScript. Everything language-specific lives in the grammar — keywords, which token is the identifier, template-literal delimiters, the regex-vs-division lexer ambiguity — all *declared per token*:
259266

260267
```ts
261-
const Template = token(/`…`/, { template: { open: '`', interpOpen: '${', interpClose: '}' } });
262-
const Regex = token(/\/\//, {
268+
import { token, seq, alt, noneOf, named, oneOf, plus, star, notFollowedBy } from './src/api.ts';
269+
270+
const escaped = seq('\\', named('any'));
271+
272+
const Template = token(seq(
273+
'`',
274+
star(alt(noneOf('`', '\\', '$'), escaped, seq('$', notFollowedBy('{')))),
275+
'`',
276+
), {
277+
template: { open: '`', interpOpen: '${', interpClose: '}' },
278+
});
279+
const Regex = token(seq(
280+
'/',
281+
plus(alt(
282+
noneOf('/', '\\', '[', '\n'),
283+
escaped,
284+
seq('[', star(alt(noneOf(']', '\\', '\n'), escaped)), ']'),
285+
)),
286+
'/',
287+
star(oneOf('g', 'i', 'm', 's', 'u', 'y', 'd', 'v')),
288+
), {
263289
regex: true,
264290
regexContext: {
265291
divisionAfterTypes: ['Ident', 'Number', 'String', 'Template'],

html.language-configuration.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,5 @@
3838
]
3939
],
4040
"autoCloseBefore": "> \n\t",
41-
"wordPattern": "[a-zA-Z][\\w:.-]*"
41+
"wordPattern": "[a-zA-Z][A-Za-z0-9_:.\\-]*"
4242
}

html.monarch.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
}
6262
],
6363
[
64-
"[a-zA-Z][\\w:.-]*",
64+
"[a-zA-Z][A-Za-z0-9_:.\\-]*",
6565
{
6666
"token": "tag",
6767
"switchTo": "@tag"
@@ -75,7 +75,7 @@
7575
],
7676
"closetag": [
7777
[
78-
"[a-zA-Z][\\w:.-]*",
78+
"[a-zA-Z][A-Za-z0-9_:.\\-]*",
7979
"tag"
8080
],
8181
[
@@ -92,7 +92,7 @@
9292
],
9393
"tag": [
9494
[
95-
"[a-zA-Z][\\w:.-]*",
95+
"[a-zA-Z][A-Za-z0-9_:.\\-]*",
9696
"attribute.name"
9797
],
9898
[
@@ -170,7 +170,7 @@
170170
],
171171
"rawtag_script": [
172172
[
173-
"[a-zA-Z][\\w:.-]*",
173+
"[a-zA-Z][A-Za-z0-9_:.\\-]*",
174174
"attribute.name"
175175
],
176176
[
@@ -222,7 +222,7 @@
222222
],
223223
"rawtag_style": [
224224
[
225-
"[a-zA-Z][\\w:.-]*",
225+
"[a-zA-Z][A-Za-z0-9_:.\\-]*",
226226
"attribute.name"
227227
],
228228
[
@@ -274,7 +274,7 @@
274274
],
275275
"rawtag_textarea": [
276276
[
277-
"[a-zA-Z][\\w:.-]*",
277+
"[a-zA-Z][A-Za-z0-9_:.\\-]*",
278278
"attribute.name"
279279
],
280280
[
@@ -326,7 +326,7 @@
326326
],
327327
"rawtag_title": [
328328
[
329-
"[a-zA-Z][\\w:.-]*",
329+
"[a-zA-Z][A-Za-z0-9_:.\\-]*",
330330
"attribute.name"
331331
],
332332
[

html.tmLanguage.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,7 @@
10101010
},
10111011
"tag": {
10121012
"name": "meta.tag.html",
1013-
"begin": "(<)(/?)([a-zA-Z][\\w:.-]*)",
1013+
"begin": "(<)(/?)([a-zA-Z][A-Za-z0-9_:.\\-\\p{L}\\p{Nl}\\p{Nd}\\p{Mn}\\p{Mc}\\p{Pc}]*)",
10141014
"beginCaptures": {
10151015
"1": {
10161016
"name": "punctuation.definition.tag.begin.html"
@@ -1196,7 +1196,7 @@
11961196
]
11971197
},
11981198
{
1199-
"match": "([a-zA-Z][\\w:.-]*)(?=\\s*=)",
1199+
"match": "([a-zA-Z][A-Za-z0-9_:.\\-\\p{L}\\p{Nl}\\p{Nd}\\p{Mn}\\p{Mc}\\p{Pc}]*)(?=\\s*=)",
12001200
"name": "entity.other.attribute-name.html"
12011201
},
12021202
{
@@ -1245,7 +1245,7 @@
12451245
]
12461246
},
12471247
{
1248-
"match": "([a-zA-Z][\\w:.-]*)",
1248+
"match": "([a-zA-Z][A-Za-z0-9_:.\\-\\p{L}\\p{Nl}\\p{Nd}\\p{Mn}\\p{Mc}\\p{Pc}]*)",
12491249
"name": "entity.other.attribute-name.html"
12501250
}
12511251
]

html.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,36 @@
99
// tags). It does not implement the WHATWG error-recovery tree-construction
1010
// algorithm (that is not a context-free grammar); conformance is measured against
1111
// `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';
1313
import type { MarkupConfig } from './src/types.ts';
1414

1515
// ── Tokens ──
16+
const word = oneOf(range('A', 'Z'), range('a', 'z'), range('0', '9'), '_');
17+
const whitespace = oneOf('\t', '\n', '\f', '\r', ' ');
18+
1619
// Tag and attribute names: a letter, then name chars (incl. `-` for custom
1720
// 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 });
1922
// An OPEN void-element name (`br`, `img`, `meta`, …). The lexer retags these from
2023
// Name (driven by `markup.voidTags`); the pattern is a placeholder, never matched
2124
// fresh. A distinct token lets the parser's void branch match void elements without
2225
// 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' });
2427
// 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 });
2629
// Unquoted attribute value (`colspan=2`, `value=5px`, `href=https://x/`, `href=/a/b.css`):
2730
// per WHATWG, an unquoted value ends ONLY at whitespace or `>`, so `/` is a legal value char
2831
// (URLs / paths). The lexer scans the whole value as ONE token the moment it follows `=` (see
2932
// markup.unquotedValueToken below) — so the leading `/` of a path and the trailing `/` of a URL
3033
// stay in the value, while a `/>` self-close (where no value is being read) stays punctuation.
3134
// `\`` excluded to mirror the highlighter's value pattern. The leading-char-class scan in the
3235
// 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' });
3437
// Markup-mode content tokens — emitted by the lexer state machine, not matched by
3538
// 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' });
3942

4043
// ── Rules ──
4144
// An attribute: a name, optionally `= value` (quoted, or an unquoted name/number).

javascript.language-configuration.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124
"end": "^\\s*//\\s*#?endregion\\b"
125125
}
126126
},
127-
"wordPattern": "(?:[a-zA-Z_$]|\\\\u[0-9a-fA-F]{4}|\\\\u\\{[0-9a-fA-F]+\\})(?:[a-zA-Z0-9_$]|\\\\u[0-9a-fA-F]{4}|\\\\u\\{[0-9a-fA-F]+\\})*",
127+
"wordPattern": "(?:[a-zA-Z_$]|\\\\u[0-9A-Fa-f]{4}|\\\\u\\{[0-9A-Fa-f]+\\})(?:[a-zA-Z0-9_$]|\\\\u[0-9A-Fa-f]{4}|\\\\u\\{[0-9A-Fa-f]+\\})*",
128128
"indentationRules": {
129129
"decreaseIndentPattern": "^\\s*[)}\\]].*$",
130130
"increaseIndentPattern": "^.*(\\([^)]*|\\{[^}]*|\\[[^\\]]*)$"

0 commit comments

Comments
 (0)