Skip to content

Commit a939fc4

Browse files
Make token-pattern literals bare strings (#16)
A token-pattern literal is now a bare `string` instead of a `lit('x')` wrapper. This removes the entire layer that existed only to box a string into a TokenPattern: `lit`, `TokenPatternInput`, `toTokenPattern` / `toPatternInput`, `isTokenPattern`, `TokenCharClassInput`, `mark` + the `__kind: 'token-pattern'` marker, and `NormalizedTokenOptions` / `normalizeTokenOptions`. Because a bare string now inhabits both the token-pattern world (a literal) and the rule world (a token reference), the first-argument overload that `alt`/`opt` relied on can no longer dispatch. They are split into token-pattern combinators `altPattern`/`optPattern` and the rule combinators `alt`/`opt`. Grammars, tests, and README examples are updated accordingly. All 7 generated grammars are byte-identical; conformance unchanged (5386/5659).
1 parent ad0415c commit a939fc4

10 files changed

Lines changed: 159 additions & 196 deletions

File tree

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -242,15 +242,15 @@ A grammar is a TypeScript module: tokens, operator precedence, and rules built f
242242
```ts
243243
import {
244244
token, rule, defineGrammar, left, op, sep,
245-
seq, oneOf, range, plus, star, opt,
245+
seq, oneOf, range, plus, star, optPattern,
246246
} from './src/api.ts';
247247

248248
const digit = range('0', '9');
249249
const Ident = token(seq(
250250
oneOf(range('a', 'z'), range('A', 'Z'), '_', '$'),
251251
star(oneOf(range('a', 'z'), range('A', 'Z'), digit, '_', '$')),
252252
), { identifier: true });
253-
const Number = token(seq(plus(digit), opt(seq('.', plus(digit)))));
253+
const Number = token(seq(plus(digit), optPattern(seq('.', plus(digit)))));
254254

255255
const Expr = rule($ => [
256256
Ident,
@@ -283,23 +283,23 @@ Flat, irreducible facts — which keywords are control flow, which punctuation i
283283
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*:
284284

285285
```ts
286-
import { token, seq, alt, noneOf, anyChar, oneOf, plus, star, notFollowedBy } from './src/api.ts';
286+
import { token, seq, altPattern, noneOf, anyChar, oneOf, plus, star, notFollowedBy } from './src/api.ts';
287287

288288
const escaped = seq('\\', anyChar());
289289

290290
const Template = token(seq(
291291
'`',
292-
star(alt(noneOf('`', '\\', '$'), escaped, seq('$', notFollowedBy('{')))),
292+
star(altPattern(noneOf('`', '\\', '$'), escaped, seq('$', notFollowedBy('{')))),
293293
'`',
294294
), {
295295
template: { open: '`', interpOpen: '${', interpClose: '}' },
296296
});
297297
const Regex = token(seq(
298298
'/',
299-
plus(alt(
299+
plus(altPattern(
300300
noneOf('/', '\\', '[', '\n'),
301301
escaped,
302-
seq('[', star(alt(noneOf(']', '\\', '\n'), escaped)), ']'),
302+
seq('[', star(altPattern(noneOf(']', '\\', '\n'), escaped)), ']'),
303303
)),
304304
'/',
305305
star(oneOf('g', 'i', 'm', 's', 'u', 'y', 'd', 'v')),

html.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
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, lit, seq, oneOf, noneOf, range, anyChar, star, plus, notFollowedBy } from './src/api.ts';
12+
import { token, rule, defineGrammar, many, opt, alt, altPattern, seq, oneOf, noneOf, range, anyChar, star, plus, notFollowedBy } from './src/api.ts';
1313
import type { MarkupConfig } from './src/types.ts';
1414

1515
// ── Tokens ──
@@ -27,7 +27,7 @@ const Name = token(seq(oneOf(range('a', 'z'), range('A', 'Z')), star(oneOf(word,
2727
// the generic engine knowing any tag names.
2828
const VoidName = token(seq(oneOf(range('a', 'z'), range('A', 'Z')), star(oneOf(word, ':', '.', '-'))), { scope: 'entity.name.tag' });
2929
// Quoted attribute value (double or single).
30-
const AttrValue = token(alt(seq('"', star(noneOf('"')), '"'), seq("'", star(noneOf("'")), "'")), { string: true });
30+
const AttrValue = token(altPattern(seq('"', star(noneOf('"')), '"'), seq("'", star(noneOf("'")), "'")), { string: true });
3131
// Unquoted attribute value (`colspan=2`, `value=5px`, `href=https://x/`, `href=/a/b.css`):
3232
// per WHATWG, an unquoted value ends ONLY at whitespace or `>`, so `/` is a legal value char
3333
// (URLs / paths). The lexer scans the whole value as ONE token the moment it follows `=` (see
@@ -122,8 +122,8 @@ export const markup: MarkupConfig = {
122122
// VS Code's own HTML grammar (a flat source.css blob there) and matching the hand-written Vue
123123
// grammar's `#vue-directives-style-attr` (its "Copy from source.css#rule-list-innards").
124124
attributeEmbed: [
125-
{ namePattern: seq(lit('on'), plus(word)), embed: 'source.js' },
126-
{ namePattern: lit('style'), embed: 'source.css', include: 'source.css#rule-list-innards' },
125+
{ namePattern: seq('on', plus(word)), embed: 'source.js' },
126+
{ namePattern: 'style', embed: 'source.css', include: 'source.css#rule-list-innards' },
127127
],
128128
// Raw-text element bodies are scanned verbatim by the parser, but the HIGHLIGHTER
129129
// delegates `<script>`/`<style>` to the platform's real JS/CSS grammars (exactly as

javascript.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
left, right, none, noUnaryLhs,
3030
op, prefix, postfix, sameLine,
3131
sep, opt, many, many1, alt, exclude, not,
32-
lit, seq, oneOf, noneOf, range, anyChar, star, plus, repeat, notFollowedBy, start,
32+
altPattern, optPattern, seq, oneOf, noneOf, range, anyChar, star, plus, repeat, notFollowedBy, start,
3333
} from './src/api.ts';
3434

3535
// ── Tokens ──
@@ -46,9 +46,9 @@ const idStart = oneOf(range('a', 'z'), range('A', 'Z'), '_', '$');
4646
const idCont = oneOf(range('a', 'z'), range('A', 'Z'), digit, '_', '$');
4747
const lineTerminator = oneOf('\n', '\r', '\u2028', '\u2029');
4848
const hspace = oneOf(' ', '\t');
49-
const uEsc = alt(seq('\\u', repeat(hexDigit, 4, 4)), seq('\\u{', plus(hexDigit), '}'));
50-
const identStart = alt(idStart, uEsc);
51-
const identPart = alt(idCont, uEsc);
49+
const uEsc = altPattern(seq('\\u', repeat(hexDigit, 4, 4)), seq('\\u{', plus(hexDigit), '}'));
50+
const identStart = altPattern(idStart, uEsc);
51+
const identPart = altPattern(idCont, uEsc);
5252
const numericTailGuard = notFollowedBy(oneOf(range('0', '9'), range('A', 'Z'), range('a', 'z'), '_', '$', '\\'));
5353
const digits = seq(plus(digit), star(seq('_', plus(digit))));
5454
const Ident = token(seq(identStart, star(identPart)), { identifier: true });
@@ -63,39 +63,39 @@ const Ident = token(seq(identStart, star(identPart)), { identifier: true
6363
// only valid here (radix forms have no fractional part), so it lives on each radix token rather
6464
// than on the decimal `BigInt_` below. The shared `(?!IdentifierStart|DecimalDigit)` tail still
6565
// rejects a stray trailing identifier (`0x5anabc`).
66-
const HexNumber = token(seq('0', oneOf('x', 'X'), plus(hexDigit), star(seq('_', plus(hexDigit))), opt(lit('n')), numericTailGuard), { scope: 'constant.numeric.hex' });
67-
const OctalNumber = token(seq('0', oneOf('o', 'O'), plus(range('0', '7')), star(seq('_', plus(range('0', '7')))), opt(lit('n')), numericTailGuard), { scope: 'constant.numeric.octal' });
68-
const BinaryNumber = token(seq('0', oneOf('b', 'B'), plus(oneOf('0', '1')), star(seq('_', plus(oneOf('0', '1')))), opt(lit('n')), numericTailGuard), { scope: 'constant.numeric.binary' });
66+
const HexNumber = token(seq('0', oneOf('x', 'X'), plus(hexDigit), star(seq('_', plus(hexDigit))), optPattern('n'), numericTailGuard), { scope: 'constant.numeric.hex' });
67+
const OctalNumber = token(seq('0', oneOf('o', 'O'), plus(range('0', '7')), star(seq('_', plus(range('0', '7')))), optPattern('n'), numericTailGuard), { scope: 'constant.numeric.octal' });
68+
const BinaryNumber = token(seq('0', oneOf('b', 'B'), plus(oneOf('0', '1')), star(seq('_', plus(oneOf('0', '1')))), optPattern('n'), numericTailGuard), { scope: 'constant.numeric.binary' });
6969
const BigInt_ = token(seq(digits, 'n', numericTailGuard), { scope: 'constant.numeric.bigint' });
7070
// DecimalLiteral, including the leading-dot form (`.5`, `.0e1`): an integer part with optional
7171
// fraction/exponent, OR a bare fraction `.digits` with optional exponent. Same trailing guard.
7272
// Scope is set explicitly (not inferred from a `[0-9]`-leading pattern) because the leading-dot
7373
// alternative makes the pattern start with `(?:` — gen-tm's decimal-numeric detector keys on a
7474
// `[0-9]`/`\d` prefix, so without this the token would lose its `constant.numeric` scope.
7575
const fracTail = seq('.', star(digit), star(seq('_', plus(digit))));
76-
const expTail = seq(oneOf('e', 'E'), opt(oneOf('+', '-')), digits);
77-
const Number_ = token(seq(alt(seq(digits, opt(fracTail)), seq('.', digits)), opt(expTail), numericTailGuard), { scope: 'constant.numeric.decimal' });
76+
const expTail = seq(oneOf('e', 'E'), optPattern(oneOf('+', '-')), digits);
77+
const Number_ = token(seq(altPattern(seq(digits, optPattern(fracTail)), seq('.', digits)), optPattern(expTail), numericTailGuard), { scope: 'constant.numeric.decimal' });
7878
// A well-formed JS escape, used in the string-body pattern below. `\u`/`\x` must
7979
// match their strict forms — a `\u{cp}` with cp ≤ 0x10FFFF, a 4-hex `\uXXXX`, or a
8080
// 2-hex `\xXX` — while `\` + any *other* char (\n, \\, \q non-escape, line
8181
// continuation) stays valid via `[^ux]`. A malformed `\u`/`\x` (e.g. `\u{110000}`,
8282
// `\u{r}`, `\u{}`, `\u{67`) matches no escape, so the string matches no token and the
8383
// lexer throws — TS's exact rejection. The in-range codepoint is `0*` leading zeros
8484
// then 1–5 hex (0–0xFFFFF) or `10`+4 hex (0x100000–0x10FFFF).
85-
const codePoint = seq(star('0'), alt(repeat(hexDigit, 1, 5), seq('10', repeat(hexDigit, 4, 4))));
86-
const escape = seq('\\', alt(seq('u{', codePoint, '}'), seq('u', repeat(hexDigit, 4, 4)), seq('x', repeat(hexDigit, 2, 2)), noneOf('u', 'x')));
87-
const highlightedEscape = seq('\\', alt(
85+
const codePoint = seq(star('0'), altPattern(repeat(hexDigit, 1, 5), seq('10', repeat(hexDigit, 4, 4))));
86+
const escape = seq('\\', altPattern(seq('u{', codePoint, '}'), seq('u', repeat(hexDigit, 4, 4)), seq('x', repeat(hexDigit, 2, 2)), noneOf('u', 'x')));
87+
const highlightedEscape = seq('\\', altPattern(
8888
oneOf('n', 'r', 't', 'b', 'f', 'v', '0', "'", '"', '\\'),
8989
seq('x', repeat(hexDigit, 2, 2)),
9090
seq('u', repeat(hexDigit, 4, 4)),
9191
seq('u{', plus(hexDigit), '}'),
9292
));
93-
const String_ = token(alt(seq('"', star(alt(noneOf('"', '\\'), escape)), '"'), seq("'", star(alt(noneOf("'", '\\'), escape)), "'")), {
93+
const String_ = token(altPattern(seq('"', star(altPattern(noneOf('"', '\\'), escape)), '"'), seq("'", star(altPattern(noneOf("'", '\\'), escape)), "'")), {
9494
string: true,
9595
escape: highlightedEscape,
9696
});
97-
const Template = token(seq('`', star(alt(noneOf('`', '\\', '$'), seq('\\', noneOf(lineTerminator)), seq('$', notFollowedBy('{')))), '`'), {
98-
escape: seq('\\', alt(
97+
const Template = token(seq('`', star(altPattern(noneOf('`', '\\', '$'), seq('\\', noneOf(lineTerminator)), seq('$', notFollowedBy('{')))), '`'), {
98+
escape: seq('\\', altPattern(
9999
oneOf('n', 'r', 't', 'b', 'f', 'v', '0', "'", '"', '\\', '`', '$'),
100100
seq('x', repeat(hexDigit, 2, 2)),
101101
seq('u', repeat(hexDigit, 4, 4)),
@@ -107,8 +107,8 @@ const Template = token(seq('`', star(alt(noneOf('`', '\\', '$'), seq('\\', n
107107
template: { open: '`', interpOpen: '${', interpClose: '}' },
108108
});
109109
const regexEscape = seq('\\', noneOf(lineTerminator));
110-
const regexClassBody = star(alt(noneOf(']', '\\', '\n'), regexEscape));
111-
const Regex_ = token(seq('/', plus(alt(noneOf('/', '\\', '[', '\n'), regexEscape, seq('[', regexClassBody, ']'))), '/', star(oneOf('g', 'i', 'm', 's', 'u', 'y', 'd', 'v'))), {
110+
const regexClassBody = star(altPattern(noneOf(']', '\\', '\n'), regexEscape));
111+
const Regex_ = token(seq('/', plus(altPattern(noneOf('/', '\\', '[', '\n'), regexEscape, seq('[', regexClassBody, ']'))), '/', star(oneOf('g', 'i', 'm', 's', 'u', 'y', 'd', 'v'))), {
112112
regex: true,
113113
regexContext: {
114114
divisionAfterTypes: ['Ident', 'Number', 'String', 'Template', 'BigInt'],
@@ -131,7 +131,7 @@ const Regex_ = token(seq('/', plus(alt(noneOf('/', '\\', '[', '\n'), regex
131131
});
132132
// `@name` / `@ns.name` — each dotted segment is an IdentifierName, so it admits the same
133133
// `\u`-escape forms as `Ident` (`@℘`, `@ZW_‌_NJ`); the parser owns the `(args)` tail.
134-
const Decorator = token(seq('@', opt(seq(identStart, star(alt(identPart, '.'))))), { scope: 'entity.name.function.decorator' });
134+
const Decorator = token(seq('@', optPattern(seq(identStart, star(altPattern(identPart, '.'))))), { scope: 'entity.name.function.decorator' });
135135
// PrivateIdentifier: `#` + an IdentifierName, so it admits the same `\u`-escape forms as `Ident`
136136
// (`#\u{6F}_`). A non-ASCII literal `#name` (`#℘`, `#ZWNJ`) is handled by the lexer's Unicode
137137
// fallback, which recognises this token's leading `#` as a name prefix.

src/api.ts

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,29 @@
11
import type { CstGrammar, TokenDecl, PrecLevel, PrecOperator, RuleDecl, RuleExpr, MarkupConfig, IndentConfig, NewlineConfig, StringInterpolation, TokenPattern } from './types.ts';
22
import {
3-
altPattern, anyChar, followedBy, isTokenPattern, lit, never, noneOf, notFollowedBy,
3+
altPattern, anyChar, followedBy, never, noneOf, notFollowedBy,
44
notPrecededBy, oneOf, optPattern, plus, precededBy, range, repeat,
5-
seq, star, start, end, toTokenPattern,
6-
type TokenPatternInput,
5+
seq, star, start, end,
76
} from './token-pattern.ts';
87

98
export {
10-
anyChar, followedBy, lit, never, noneOf, notFollowedBy, notPrecededBy, oneOf,
11-
plus, precededBy, range, repeat, seq, star, start, end,
9+
altPattern, anyChar, followedBy, never, noneOf, notFollowedBy, notPrecededBy, oneOf,
10+
optPattern, plus, precededBy, range, repeat, seq, star, start, end,
1211
};
1312

1413
// ── Token ──
1514

1615
interface TokenOptions {
1716
skip?: boolean;
1817
scope?: string;
19-
escape?: TokenPatternInput;
18+
escape?: TokenPattern;
2019
// Highlight-only interpolation regions for ordinary string tokens (e.g. env-spec `${…}` / `$(…)`).
2120
// The parser/lexer stay token-based; generators re-express these as nested regions.
2221
interpolation?: StringInterpolation | StringInterpolation[];
2322
// A regex matching exactly one well-formed escape sequence. Engine-scanned tokens
2423
// (templates) validate each `\`-escape against it and reject any that don't match —
2524
// unlike `escape` (highlight-only), this drives tokenization. Skipped in tag
2625
// position, where invalid escapes are legal (cooked = undefined). Optional.
27-
escapeValid?: TokenPatternInput;
26+
escapeValid?: TokenPattern;
2827
regex?: boolean;
2928
embed?: string;
3029
// ── Lexer hints (keep gen-parser language-agnostic; all optional) ──
@@ -45,38 +44,23 @@ interface TokenOptions {
4544
};
4645
string?: boolean;
4746
// Block-context (flowDepth===0) pattern variant for indentation grammars — see TokenDecl.blockPattern.
48-
blockPattern?: TokenPatternInput;
47+
blockPattern?: TokenPattern;
4948
// Block-context ONLY (indentation grammars): match this token only outside flow — see TokenDecl.blockOnly.
5049
blockOnly?: boolean;
5150
}
5251

53-
type NormalizedTokenOptions = Omit<TokenOptions, 'escape' | 'escapeValid' | 'blockPattern'> & {
54-
escape?: TokenPattern;
55-
escapeValid?: TokenPattern;
56-
blockPattern?: TokenPattern;
57-
};
58-
5952
export class TokenRef {
6053
readonly __kind = 'token' as const;
6154
readonly pattern: TokenPattern;
62-
readonly opts: NormalizedTokenOptions;
63-
constructor(pattern: TokenPattern, opts: NormalizedTokenOptions) {
55+
readonly opts: TokenOptions;
56+
constructor(pattern: TokenPattern, opts: TokenOptions) {
6457
this.pattern = pattern;
6558
this.opts = opts;
6659
}
6760
}
6861

69-
export function token(pattern: TokenPatternInput, opts?: TokenOptions): TokenRef {
70-
return new TokenRef(toTokenPattern(pattern), normalizeTokenOptions(opts ?? {}));
71-
}
72-
73-
function normalizeTokenOptions(opts: TokenOptions): NormalizedTokenOptions {
74-
return {
75-
...opts,
76-
escape: opts.escape ? toTokenPattern(opts.escape) : undefined,
77-
escapeValid: opts.escapeValid ? toTokenPattern(opts.escapeValid) : undefined,
78-
blockPattern: opts.blockPattern ? toTokenPattern(opts.blockPattern) : undefined,
79-
};
62+
export function token(pattern: TokenPattern, opts?: TokenOptions): TokenRef {
63+
return new TokenRef(pattern, opts ?? {});
8064
}
8165

8266
// ── Rule ──
@@ -215,11 +199,8 @@ export function sep(item: Element, delimiter: string): SepNode {
215199
return new SepNode(item, delimiter);
216200
}
217201

218-
export function opt(...items: [TokenPattern, ...TokenPatternInput[]]): TokenPattern;
219-
export function opt(...items: Element[]): OptNode;
220-
export function opt(...items: (Element | TokenPatternInput)[]): OptNode | TokenPattern {
221-
if (items.some(isTokenPattern)) return optPattern(...items as TokenPatternInput[]);
222-
return new OptNode(items as Element[]);
202+
export function opt(...items: Element[]): OptNode {
203+
return new OptNode(items);
223204
}
224205

225206
export function many(...items: Element[]): ManyNode {
@@ -230,11 +211,8 @@ export function many1(...items: Element[]): Many1Node {
230211
return new Many1Node(items);
231212
}
232213

233-
export function alt(...items: [TokenPattern, ...TokenPatternInput[]]): TokenPattern;
234-
export function alt(...items: Alternative[]): AltNode;
235-
export function alt(...items: (Alternative | TokenPatternInput)[]): AltNode | TokenPattern {
236-
if (items.some(isTokenPattern)) return altPattern(...items as TokenPatternInput[]);
237-
return new AltNode(items as Alternative[]);
214+
export function alt(...items: Alternative[]): AltNode {
215+
return new AltNode(items);
238216
}
239217

240218
// Parse `items` with the given LED connector(s) disabled at the top level (a

0 commit comments

Comments
 (0)