Blame Raw
· · · 393 lines (19.0 KB)
0 contributors
1import { chevrotain } from '../../../lib.js';
2const { createToken, Lexer } = chevrotain;
3
4/** @typedef {import('chevrotain').TokenType} TokenType */
5
6
7/** Regex for lexer token matching (no anchors). */
8const IDENTIFIER_LEXER_PATTERN = /[a-zA-Z][\w-_]*/;
9
10/**
11 * Pattern for valid macro identifiers.
12 * Must start with a letter, followed by word chars (letters, digits, underscore) or hyphens.
13 * Used by both the lexer token and the validation regex.
14 *
15 * Regex for full-string validation (with anchors). Exported for macro registration.
16 */
17export const MACRO_IDENTIFIER_PATTERN = /^[a-zA-Z][\w-_]*$/;
18
19/**
20 * Pattern for valid variable shorthand identifiers.
21 * Must start with a letter, followed by word chars (letters, digits, underscore) or hyphens,
22 * but must end with a word character (not a hyphen).
23 *
24 * Used for variable shorthand syntax like .varName or $varName.
25 */
26export const MACRO_VARIABLE_SHORTHAND_PATTERN = /[a-zA-Z](?:[\w\-_]*[\w])?/;
27
28/** @enum {string} */
29const modes = Object.freeze({
30 plaintext: 'plaintext_mode',
31 macro_def: 'macro_def_mode',
32 macro_identifier_end: 'macro_identifier_end_mode',
33 macro_args: 'macro_args_mode',
34 macro_filter_modifer: 'macro_filter_modifer_mode',
35 macro_filter_modifier_end: 'macro_filter_modifier_end_mode',
36 // Variable shorthand modes
37 var_identifier: 'var_identifier_mode',
38 var_after_identifier: 'var_after_identifier_mode',
39 var_value: 'var_value_mode',
40});
41
42/**
43 * All lexer tokens used by the macro parser.
44 * @readonly
45 */
46const Tokens = Object.freeze({
47/** General capture-all plaintext without macros. Consumes any character that is not the first '{' of a macro opener '{{'. */
48 Plaintext: createToken({ name: 'Plaintext', pattern: /(?:[^{]|\{(?!\{))+/u, line_breaks: true }),
49 /** Single literal '{' that appears immediately before a macro opener '{{' */
50 PlaintextOpenBrace: createToken({ name: 'Plaintext.OpenBrace', pattern: /\{(?=\{\{)/ }),
51
52 /** General macro capture */
53 Macro: {
54 Start: createToken({ name: 'Macro.Start', pattern: /\{\{/ }),
55 /**
56 * Macro execution flags - special symbols that modify macro resolution behavior.
57 * - `!` = immediate resolve (TBD)
58 * - `?` = delayed resolve (TBD)
59 * - `~` = re-evaluate (TBD)
60 * - `/` = closing block marker for scoped macros
61 * - `#` = preserve whitespace (don't auto-trim scoped content), also legacy handlebars compatibility
62 */
63 Flags: createToken({ name: 'Macro.Flag', pattern: /[!?~#/]/ }),
64 /**
65 * Filter flag (`>`) - separate token because it changes parsing behavior.
66 * When present, `|` characters inside the macro are treated as filter/pipe operators.
67 */
68 FilterFlag: createToken({ name: 'Macro.FilterFlag', pattern: />/ }),
69 DoubleSlash: createToken({ name: 'Macro.DoubleSlash', pattern: /\/\// }),
70 /**
71 * Separate macro identifier needed, that is similar to the global indentifier, but captures the actual macro "name"
72 * We need this, because this token is going to switch lexer mode, while the general identifier does not.
73 */
74 Identifier: createToken({ name: 'Macro.Identifier', pattern: IDENTIFIER_LEXER_PATTERN }),
75 /** At the end of an identifier, there has to be whitspace, or must be directly followed by colon/double-colon separator, output modifier or closing braces */
76 EndOfIdentifier: createToken({ name: 'Macro.EndOfIdentifier', pattern: /(?:\s+|(?=:{1,2})|(?=[|}]))/, group: Lexer.SKIPPED }),
77 BeforeEnd: createToken({ name: 'Macro.BeforeEnd', pattern: /(?=\}\})/, group: Lexer.SKIPPED }),
78 End: createToken({ name: 'Macro.End', pattern: /\}\}/ }),
79 },
80
81 /** Captures that only appear inside arguments */
82 Args: {
83 DoubleColon: createToken({ name: 'Args.DoubleColon', pattern: /::/ }),
84 Colon: createToken({ name: 'Args.Colon', pattern: /:/ }),
85 Equals: createToken({ name: 'Args.Equals', pattern: /=/ }),
86 Quote: createToken({ name: 'Args.Quote', pattern: /"/ }),
87 },
88
89 Filter: {
90 EscapedPipe: createToken({ name: 'Filter.EscapedPipe', pattern: /\\\|/ }),
91 Pipe: createToken({ name: 'Filter.Pipe', pattern: /\|/ }),
92 Identifier: createToken({ name: 'Filter.Identifier', pattern: IDENTIFIER_LEXER_PATTERN }),
93 /** At the end of an identifier, there has to be whitspace, or must be directly followed by colon/double-colon separator, output modifier or closing braces */
94 EndOfIdentifier: createToken({ name: 'Filter.EndOfIdentifier', pattern: /(?:\s+|(?=:{1,2})|(?=[|}]))/, group: Lexer.SKIPPED }),
95 },
96
97 // All tokens that can be captured inside a macro
98 Identifier: createToken({ name: 'Identifier', pattern: IDENTIFIER_LEXER_PATTERN }),
99 WhiteSpace: createToken({ name: 'WhiteSpace', pattern: /\s+/, group: Lexer.SKIPPED }),
100
101 /** Variable shorthand tokens */
102 Var: {
103 /** Local variable prefix (`.`) - triggers variable shorthand for local variables */
104 LocalPrefix: createToken({ name: 'Var.LocalPrefix', pattern: /\./ }),
105 /** Global variable prefix (`$`) - triggers variable shorthand for global variables */
106 GlobalPrefix: createToken({ name: 'Var.GlobalPrefix', pattern: /\$/ }),
107 /**
108 * Variable identifier - allows hyphens inside but not at the end to avoid conflict with -- operator.
109 * Pattern: starts with letter, optionally followed by word chars/hyphens, but must end with word char.
110 * Examples: myVar, my-var, my_var, myVar123, my-long-var-name
111 * Invalid: my-, my--, -var
112 */
113 Identifier: createToken({ name: 'Var.Identifier', pattern: MACRO_VARIABLE_SHORTHAND_PATTERN }),
114
115 /** All tokens that are valid operators inside a variable shorthand expression */
116 Operators: {
117 /** Increment operator (`++`) */
118 Increment: createToken({ name: 'Var.Increment', pattern: /\+\+/ }),
119 /** Decrement operator (`--`) */
120 Decrement: createToken({ name: 'Var.Decrement', pattern: /--/ }),
121 /** Nullish coalescing assignment operator (`??=`) - sets var if undefined, must come before NullishCoalescing */
122 NullishCoalescingEquals: createToken({ name: 'Var.NullishCoalescingEquals', pattern: /\?\?=/ }),
123 /** Nullish coalescing operator (`??`) - returns default if var undefined */
124 NullishCoalescing: createToken({ name: 'Var.NullishCoalescing', pattern: /\?\?/ }),
125 /** Logical OR assignment operator (`||=`) - sets var if falsy, must come before LogicalOr */
126 LogicalOrEquals: createToken({ name: 'Var.LogicalOrEquals', pattern: /\|\|=/ }),
127 /** Logical OR operator (`||`) - returns default if var falsy */
128 LogicalOr: createToken({ name: 'Var.LogicalOr', pattern: /\|\|/ }),
129 /** Subtract operator (`-=`) - subtracts value from variable */
130 MinusEquals: createToken({ name: 'Var.MinusEquals', pattern: /-=/ }),
131 /** Equality comparison operator (`==`) - compares variable to value */
132 DoubleEquals: createToken({ name: 'Var.DoubleEquals', pattern: /==/ }),
133 /** Not equals comparison operator (`!=`) - compares variable to value, returns inverted result */
134 NotEquals: createToken({ name: 'Var.NotEquals', pattern: /!=/ }),
135 /** Greater than or equal comparison operator (`>=`) - must come before GreaterThan */
136 GreaterThanOrEqual: createToken({ name: 'Var.GreaterThanOrEqual', pattern: />=/ }),
137 /** Greater than comparison operator (`>`) */
138 GreaterThan: createToken({ name: 'Var.GreaterThan', pattern: />/ }),
139 /** Less than or equal comparison operator (`<=`) - must come before LessThan */
140 LessThanOrEqual: createToken({ name: 'Var.LessThanOrEqual', pattern: /<=/ }),
141 /** Less than comparison operator (`<`) */
142 LessThan: createToken({ name: 'Var.LessThan', pattern: /</ }),
143 /** Add/append operator (`+=`) - must come before Equals to avoid conflict */
144 PlusEquals: createToken({ name: 'Var.PlusEquals', pattern: /\+=/ }),
145 /** Set operator (`=`) */
146 Equals: createToken({ name: 'Var.Equals', pattern: /=/ }),
147 },
148 },
149
150 /**
151 * Capture unknown characters one by one, to still allow other tokens being matched once they are there.
152 * This includes any possible braces that is not the double closing braces as MacroEnd.
153 */
154 Unknown: createToken({ name: 'Unknown', pattern: /([^}]|\}(?!\}))/ }),
155
156 /** TODO: Capture-all rest for now, that is not the macro end or opening of a new macro. Might be replaced later down the line. */
157 Text: createToken({ name: 'Text', pattern: /.+(?=\}\}|\{\{)/, line_breaks: true }),
158
159 /**
160 * DANGER ZONE: Careful with this token. This is used as a way to pop the current mode, if no other token matches.
161 * Can be used in modes that don't have a "defined" end really, like when capturing a single argument, argument list, etc.
162 * Has to ALWAYS be the last token.
163 */
164 ModePopper: createToken({ name: 'ModePopper', pattern: () => [''], line_breaks: false, group: Lexer.SKIPPED }),
165});
166
167/** @type {Map<string,string>} Saves all token definitions that are marked as entering modes */
168const enterModesMap = new Map();
169
170/**
171 * Lexer definition object that maps states/modes to their token rules.
172 * Each mode defines which tokens are valid in that context and how to transition between modes.
173 * @readonly
174 */
175const Def = {
176 modes: {
177 [modes.plaintext]: [
178 using(Tokens.Plaintext),
179 using(Tokens.PlaintextOpenBrace),
180 enter(Tokens.Macro.Start, modes.macro_def),
181 ],
182 [modes.macro_def]: [
183 exits(Tokens.Macro.End, modes.macro_def),
184
185 // An explicit double-slash will be treated above flags to consume, as it'll introduce a comment macro. Directly following is the args then.
186 enter(Tokens.Macro.DoubleSlash, modes.macro_args),
187
188 // Variable shorthand prefixes - must come before flags to take precedence
189 // These enter the variable identifier mode to parse variable expressions
190 enter(Tokens.Var.LocalPrefix, modes.var_identifier),
191 enter(Tokens.Var.GlobalPrefix, modes.var_identifier),
192
193 using(Tokens.Macro.Flags),
194 // Filter flag is separate because it affects parsing behavior for pipes
195 using(Tokens.Macro.FilterFlag),
196
197 // We allow whitspaces inbetween flags or in front of the modifier
198 using(Tokens.WhiteSpace),
199
200 // Inside a macro, we will match the identifier
201 // Enter 'macro_identifier_end' mode automatically at the end of the identifier, so we don't match more than one identifier
202 enter(Tokens.Macro.Identifier, modes.macro_identifier_end),
203
204 // If none of the tokens above are found, this is an invalid macro at runtime.
205 // We still need to exit the mode to prevent lexer errors
206 exits(Tokens.ModePopper, modes.macro_def),
207 ],
208 [modes.macro_identifier_end]: [
209 // Valid options after a macro identifier: whitespace, colon/double-colon (captured), macro end braces, or output modifier pipe.
210 exits(Tokens.Macro.BeforeEnd, modes.macro_identifier_end),
211 enter(Tokens.Macro.EndOfIdentifier, modes.macro_args, { andExits: modes.macro_identifier_end }),
212 ],
213 [modes.macro_args]: [
214 // Macro args allow nested macros
215 enter(Tokens.Macro.Start, modes.macro_def),
216
217 // We allow escaped pipes to not start output modifiers. We need to capture this first, before the pipe
218 using(Tokens.Filter.EscapedPipe),
219
220 // If at any place during args writing there is a pipe, we lex it as an output identifier, and then continue with lex its args
221 enter(Tokens.Filter.Pipe, modes.macro_filter_modifer),
222
223 using(Tokens.Args.DoubleColon),
224 using(Tokens.Args.Colon),
225 using(Tokens.Args.Equals),
226 using(Tokens.Args.Quote),
227 using(Tokens.Identifier),
228
229 using(Tokens.WhiteSpace),
230
231 // Last fallback, before we need to exit the mode, as we might have characters we (wrongly) haven't defined yet
232 using(Tokens.Unknown),
233
234 // Args are optional, and we don't know how long, so exit the mode to be able to capture the actual macro end
235 exits(Tokens.ModePopper, modes.macro_args),
236 ],
237 [modes.macro_filter_modifer]: [
238 using(Tokens.WhiteSpace),
239
240 enter(Tokens.Filter.Identifier, modes.macro_filter_modifier_end, { andExits: modes.macro_filter_modifer }),
241 ],
242 [modes.macro_filter_modifier_end]: [
243 // Valid options after a filter itenfier: whitespace, colon/double-colon (captured), macro end braces, or output modifier pipe.
244 exits(Tokens.Macro.BeforeEnd, modes.macro_identifier_end),
245 exits(Tokens.Filter.EndOfIdentifier, modes.macro_filter_modifer),
246 ],
247
248 // After seeing `.` or `$`, expect a variable identifier
249 [modes.var_identifier]: [
250 using(Tokens.WhiteSpace),
251 // Consume the variable identifier and move to operator detection
252 enter(Tokens.Var.Identifier, modes.var_after_identifier, { andExits: modes.var_identifier }),
253 // If no valid identifier found, exit back (will result in parser error)
254 exits(Tokens.ModePopper, modes.var_identifier),
255 ],
256 // After the variable identifier, look for operators or end
257 [modes.var_after_identifier]: [
258 using(Tokens.WhiteSpace),
259 // Check for operators - order matters: longer patterns first
260 using(Tokens.Var.Operators.Increment),
261 using(Tokens.Var.Operators.Decrement),
262 enter(Tokens.Var.Operators.NullishCoalescingEquals, modes.var_value, { andExits: modes.var_after_identifier }),
263 enter(Tokens.Var.Operators.NullishCoalescing, modes.var_value, { andExits: modes.var_after_identifier }),
264 enter(Tokens.Var.Operators.LogicalOrEquals, modes.var_value, { andExits: modes.var_after_identifier }),
265 enter(Tokens.Var.Operators.LogicalOr, modes.var_value, { andExits: modes.var_after_identifier }),
266 enter(Tokens.Var.Operators.MinusEquals, modes.var_value, { andExits: modes.var_after_identifier }),
267 enter(Tokens.Var.Operators.DoubleEquals, modes.var_value, { andExits: modes.var_after_identifier }),
268 enter(Tokens.Var.Operators.NotEquals, modes.var_value, { andExits: modes.var_after_identifier }),
269 enter(Tokens.Var.Operators.GreaterThanOrEqual, modes.var_value, { andExits: modes.var_after_identifier }),
270 enter(Tokens.Var.Operators.GreaterThan, modes.var_value, { andExits: modes.var_after_identifier }),
271 enter(Tokens.Var.Operators.LessThanOrEqual, modes.var_value, { andExits: modes.var_after_identifier }),
272 enter(Tokens.Var.Operators.LessThan, modes.var_value, { andExits: modes.var_after_identifier }),
273 enter(Tokens.Var.Operators.PlusEquals, modes.var_value, { andExits: modes.var_after_identifier }),
274 enter(Tokens.Var.Operators.Equals, modes.var_value, { andExits: modes.var_after_identifier }),
275 // If we see the end, exit
276 exits(Tokens.Macro.BeforeEnd, modes.var_after_identifier),
277 // Fallback exit
278 exits(Tokens.ModePopper, modes.var_after_identifier),
279 ],
280 // After `=` or `+=`, capture the value (can contain nested macros)
281 [modes.var_value]: [
282 // Nested macros in value
283 enter(Tokens.Macro.Start, modes.macro_def),
284
285 using(Tokens.Identifier),
286 using(Tokens.WhiteSpace),
287 using(Tokens.Unknown),
288
289 // Exit when we're about to see the end
290 exits(Tokens.ModePopper, modes.var_value),
291 ],
292 },
293 defaultMode: modes.plaintext,
294};
295
296/**
297 * The singleton instance of the MacroLexer.
298 *
299 * @type {MacroLexer}
300 */
301let instance;
302export { instance as MacroLexer };
303
304class MacroLexer extends Lexer {
305 /** @type {MacroLexer} */ static #instance;
306 /** @type {MacroLexer} */ static get instance() { return MacroLexer.#instance ?? (MacroLexer.#instance = new MacroLexer()); }
307
308 // Define the tokens
309 /** @readonly */ static tokens = Tokens;
310 /** @readonly */ static def = Def;
311 /** @readonly */ tokens = Tokens;
312 /** @readonly */ def = MacroLexer.def;
313
314 /** @private */
315 constructor() {
316 super(MacroLexer.def, {
317 traceInitPerf: false,
318 });
319 }
320
321 test(input) {
322 const result = this.tokenize(input);
323 return {
324 errors: result.errors,
325 groups: result.groups,
326 tokens: result.tokens.map(({ tokenType, ...rest }) => ({ type: tokenType.name, ...rest, tokenType: tokenType })),
327 };
328 }
329}
330
331instance = MacroLexer.instance;
332
333/**
334 * [Utility]
335 * Set push mode on the token definition.
336 * Can be used inside the token mode definition block.
337 *
338 * Marks the token to **enter** the following lexer mode.
339 *
340 * Optionally, you can specify the modes to exit when entering this mode.
341 *
342 * @param {TokenType} token - The token to modify
343 * @param {string} mode - The mode to set
344 * @param {object} [options={}] - Additional options
345 * @param {string} [options.andExits] - The modes to exit when entering this mode
346 * @returns {TokenType} The token again
347 */
348function enter(token, mode, { andExits = undefined } = {}) {
349 if (!token) throw new Error('Token must not be undefined');
350 if (enterModesMap.has(token.name) && enterModesMap.get(token.name) !== mode) {
351 throw new Error(`Token ${token.name} already is set to enter mode ${enterModesMap.get(token.name)}. The token definition are global, so they cannot be used to lead to different modes.`);
352 }
353
354 if (andExits) exits(token, andExits);
355
356 token.PUSH_MODE = mode;
357 enterModesMap.set(token.name, mode);
358 return token;
359}
360
361/**
362 * [Utility]
363 * Set pop mode on the token definition.
364 * Can be used inside the token mode definition block.
365 *
366 * Marks the token to **exit** the following lexer mode.
367 *
368 * @param {TokenType} token - The token to modify
369 * @param {string} mode - The mode to leave
370 * @returns {TokenType} The token again
371 */
372function exits(token, mode) {
373 if (!token) throw new Error('Token must not be undefined');
374 token.POP_MODE = !!mode; // Always set to true. We just use the mode here, so the linter thinks it was used. We just pass it in for clarity in the definition
375 return token;
376}
377
378/**
379 * [Utility]
380 * Can be used inside the token mode definition block.
381 *
382 * Marks the token to to just be used/consumed, and not exit or enter a mode.
383 *
384 * @param {TokenType} token - The token to modify
385 * @returns {TokenType} The token again
386 */
387function using(token) {
388 if (!token) throw new Error('Token must not be undefined');
389 if (enterModesMap.has(token.name)) {
390 throw new Error(`Token ${token.name} is already marked to enter a mode (${enterModesMap.get(token.name)}). The token definition are global, so they cannot be used to lead or stay differently.`);
391 }
392 return token;
393}