Blame Raw
· · · 227 lines (9.8 KB)
0 contributors
1import { chevrotain } from '../../../lib.js';
2import { MacroLexer } from './MacroLexer.js';
3
4const { CstParser } = chevrotain;
5
6/** @typedef {import('chevrotain').TokenType} TokenType */
7/** @typedef {import('chevrotain').CstNode} CstNode */
8/** @typedef {import('chevrotain').ILexingError} ILexingError */
9/** @typedef {import('chevrotain').IRecognitionException} IRecognitionException */
10
11/**
12 * The singleton instance of the MacroParser.
13 *
14 * @type {MacroParser}
15 */
16let instance;
17export { instance as MacroParser };
18
19class MacroParser extends CstParser {
20 /** @type {MacroParser} */ static #instance;
21 /** @type {MacroParser} */ static get instance() { return MacroParser.#instance ?? (MacroParser.#instance = new MacroParser()); }
22
23 /** @private */
24 constructor() {
25 super(MacroLexer.def, {
26 traceInitPerf: false,
27 nodeLocationTracking: 'full',
28 recoveryEnabled: true,
29 });
30 const Tokens = MacroLexer.tokens;
31
32 const $ = this;
33
34 // Top-level document rule that can handle both plaintext and macros
35 $.document = $.RULE('document', () => {
36 $.MANY(() => {
37 $.OR([
38 { ALT: () => $.CONSUME(Tokens.Plaintext, { LABEL: 'plaintext' }) },
39 { ALT: () => $.CONSUME(Tokens.PlaintextOpenBrace, { LABEL: 'plaintext' }) },
40 { ALT: () => $.SUBRULE($.macro) },
41 { ALT: () => $.CONSUME(Tokens.Macro.Start, { LABEL: 'plaintext' }) },
42 ]);
43 });
44 });
45
46 // Basic Macro Structure - can be either a regular macro or a variable expression
47 $.macro = $.RULE('macro', () => {
48 $.CONSUME(Tokens.Macro.Start);
49
50 // Optional flags before the identifier (e.g., {{!user}}, {{?~macro}}, {{>filtered}})
51 // Both regular flags and filter flag are captured under the 'flags' label
52 $.MANY(() => {
53 $.OR1([
54 { ALT: () => $.CONSUME(Tokens.Macro.Flags, { LABEL: 'flags' }) },
55 { ALT: () => $.CONSUME(Tokens.Macro.FilterFlag, { LABEL: 'flags' }) },
56 ]);
57 });
58
59 // Branch: either a variable expression (starts with . or $) or a regular macro
60 $.OR([
61 // Variable expression branch
62 { ALT: () => $.SUBRULE($.variableExpr) },
63 // Regular macro branch
64 { ALT: () => $.SUBRULE($.macroBody) },
65 ]);
66
67 $.CONSUME(Tokens.Macro.End);
68 });
69
70 // Regular macro body (flags + identifier + optional arguments)
71 $.macroBody = $.RULE('macroBody', () => {
72 // Macro identifier (name)
73 $.OR2([
74 { ALT: () => $.CONSUME(Tokens.Macro.DoubleSlash, { LABEL: 'Macro.identifier' }) },
75 { ALT: () => $.CONSUME(Tokens.Macro.Identifier, { LABEL: 'Macro.identifier' }) },
76 ]);
77 $.OPTION(() => $.SUBRULE($.arguments));
78 });
79
80 // Variable expression: .varName or $varName with optional operator
81 $.variableExpr = $.RULE('variableExpr', () => {
82 // Variable scope prefix
83 $.OR3([
84 { ALT: () => $.CONSUME(Tokens.Var.LocalPrefix, { LABEL: 'Var.scope' }) },
85 { ALT: () => $.CONSUME(Tokens.Var.GlobalPrefix, { LABEL: 'Var.scope' }) },
86 ]);
87
88 // Variable identifier (name)
89 $.CONSUME(Tokens.Var.Identifier, { LABEL: 'Var.identifier' });
90
91 // Optional operator (and expression, if operator requires one)
92 $.OPTION2(() => $.SUBRULE($.variableOperator));
93 });
94
95 // Variable operator: ++, --, = value, += value, -= value, ||, ??, ||=, ??=, ==, !=, >, >=, <, <=
96 $.variableOperator = $.RULE('variableOperator', () => {
97 $.OR4([
98 { ALT: () => $.CONSUME(Tokens.Var.Operators.Increment, { LABEL: 'Var.operator' }) },
99 { ALT: () => $.CONSUME(Tokens.Var.Operators.Decrement, { LABEL: 'Var.operator' }) },
100 {
101 ALT: () => {
102 $.OR5([
103 { ALT: () => $.CONSUME(Tokens.Var.Operators.NullishCoalescingEquals, { LABEL: 'Var.operator' }) },
104 { ALT: () => $.CONSUME(Tokens.Var.Operators.NullishCoalescing, { LABEL: 'Var.operator' }) },
105 { ALT: () => $.CONSUME(Tokens.Var.Operators.LogicalOrEquals, { LABEL: 'Var.operator' }) },
106 { ALT: () => $.CONSUME(Tokens.Var.Operators.LogicalOr, { LABEL: 'Var.operator' }) },
107 { ALT: () => $.CONSUME(Tokens.Var.Operators.MinusEquals, { LABEL: 'Var.operator' }) },
108 { ALT: () => $.CONSUME(Tokens.Var.Operators.DoubleEquals, { LABEL: 'Var.operator' }) },
109 { ALT: () => $.CONSUME(Tokens.Var.Operators.NotEquals, { LABEL: 'Var.operator' }) },
110 { ALT: () => $.CONSUME(Tokens.Var.Operators.GreaterThanOrEqual, { LABEL: 'Var.operator' }) },
111 { ALT: () => $.CONSUME(Tokens.Var.Operators.GreaterThan, { LABEL: 'Var.operator' }) },
112 { ALT: () => $.CONSUME(Tokens.Var.Operators.LessThanOrEqual, { LABEL: 'Var.operator' }) },
113 { ALT: () => $.CONSUME(Tokens.Var.Operators.LessThan, { LABEL: 'Var.operator' }) },
114 { ALT: () => $.CONSUME(Tokens.Var.Operators.PlusEquals, { LABEL: 'Var.operator' }) },
115 { ALT: () => $.CONSUME(Tokens.Var.Operators.Equals, { LABEL: 'Var.operator' }) },
116 ]);
117 $.SUBRULE($.variableValue, { LABEL: 'Var.value' });
118 },
119 },
120 ]);
121 });
122
123 // Variable value: everything after = or += until the end
124 // Can contain nested macros and any other tokens
125 $.variableValue = $.RULE('variableValue', () => {
126 $.MANY2(() => {
127 $.OR5([
128 { ALT: () => $.SUBRULE($.macro) }, // Nested macros
129 { ALT: () => $.CONSUME(Tokens.Identifier) },
130 { ALT: () => $.CONSUME(Tokens.Unknown) },
131 ]);
132 });
133 });
134
135 // Arguments Parsing
136 $.arguments = $.RULE('arguments', () => {
137 $.OR([
138 {
139 ALT: () => {
140 $.CONSUME(Tokens.Args.DoubleColon, { LABEL: 'separator' });
141 $.AT_LEAST_ONE_SEP({
142 SEP: Tokens.Args.DoubleColon,
143 DEF: () => $.SUBRULE($.argument, { LABEL: 'argument' }),
144 });
145 },
146 },
147 {
148 ALT: () => {
149 $.OPTION(() => {
150 $.CONSUME(Tokens.Args.Colon, { LABEL: 'separator' });
151 });
152 $.SUBRULE($.argumentAllowingColons, { LABEL: 'argument' });
153 },
154 // So, this is a bit hacky. But implemented below, the argument capture does explicitly exclude double colons
155 // from being captured as the first token. The potential ambiguity chevrotain claims here is not possible.
156 // It says stuff like <Args.DoubleColon, Identifier/Macro/Unknown> is possible in both branches, but it is not.
157 IGNORE_AMBIGUITIES: true,
158 },
159 ]);
160 });
161
162 // List the argument tokens here, as we need two rules, one to be able to parse with double colons and one without
163 const validArgumentTokens = [
164 { ALT: () => $.SUBRULE($.macro) }, // Nested Macros
165 { ALT: () => $.CONSUME(Tokens.Identifier) },
166 { ALT: () => $.CONSUME(Tokens.Unknown) },
167 { ALT: () => $.CONSUME(Tokens.Args.Colon) },
168 { ALT: () => $.CONSUME(Tokens.Args.Equals) },
169 { ALT: () => $.CONSUME(Tokens.Args.Quote) },
170 ];
171
172 $.argument = $.RULE('argument', () => {
173 $.MANY(() => {
174 $.OR([...validArgumentTokens]);
175 });
176 });
177 $.argumentAllowingColons = $.RULE('argumentAllowingColons', () => {
178 $.AT_LEAST_ONE(() => {
179 $.OR([
180 ...validArgumentTokens,
181 { ALT: () => $.CONSUME(Tokens.Args.DoubleColon) },
182 ]);
183 });
184 });
185
186 this.performSelfAnalysis();
187 }
188
189 /**
190 * Parses a document into a CST.
191 *
192 * @param {string} input
193 * @returns {{ cst: CstNode|null, errors: ({ message: string }|ILexingError|IRecognitionException)[] , lexingErrors: ILexingError[], parserErrors: IRecognitionException[] }}
194 */
195 parseDocument(input) {
196 if (!input) {
197 return { cst: null, errors: [{ message: 'Input is empty' }], lexingErrors: [], parserErrors: [] };
198 }
199
200 const lexingResult = MacroLexer.tokenize(input);
201
202 this.input = lexingResult.tokens;
203 const cst = this.document();
204
205 const errors = [
206 ...lexingResult.errors,
207 ...this.errors,
208 ];
209
210 return { cst, errors, lexingErrors: lexingResult.errors, parserErrors: this.errors };
211 }
212
213 test(input) {
214 const lexingResult = MacroLexer.tokenize(input);
215 // "input" is a setter which will reset the parser's state.
216 this.input = lexingResult.tokens;
217 const cst = this.macro();
218
219 // For testing purposes we need to actually persist the error messages in the object,
220 // otherwise the test cases cannot read those, as they don't have access to the exception object type.
221 const errors = this.errors.map(x => ({ message: x.message, ...x, stack: x.stack }));
222
223 return { cst, errors: errors };
224 }
225}
226
227instance = MacroParser.instance;