@webmate-studio/builder 0.2.112 → 0.2.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/expression-evaluator.js +1234 -0
- package/src/template-evaluator.js +379 -0
- package/src/template-parser.js +597 -0
- package/src/template-processor.js +87 -617
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Parser — Parses Svelte-style template syntax into an AST.
|
|
3
|
+
*
|
|
4
|
+
* Supported syntax:
|
|
5
|
+
* - {expression} → ExpressionNode
|
|
6
|
+
* - {#if cond}...{:else if}...{:else}...{/if} → IfBlock
|
|
7
|
+
* - {#each expr as item, index (key)}...{:else}...{/each} → EachBlock
|
|
8
|
+
* - {@html expr} → HtmlExpression
|
|
9
|
+
* - {@const name = expr} → ConstTag
|
|
10
|
+
* - {@debug var1, var2} → DebugTag
|
|
11
|
+
* - class:name={condition} → ClassDirective (inside tags)
|
|
12
|
+
* - attr={expression} → DynamicAttribute (inside tags)
|
|
13
|
+
* - Everything else → Text
|
|
14
|
+
*
|
|
15
|
+
* The parser does NOT fully parse HTML — it only recognizes opening tags
|
|
16
|
+
* enough to extract class: directives and dynamic attributes.
|
|
17
|
+
*
|
|
18
|
+
* @module template-parser
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} ASTNode
|
|
23
|
+
* @property {string} type
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
class TemplateParser {
|
|
27
|
+
/**
|
|
28
|
+
* Parse a template string into an AST
|
|
29
|
+
* @param {string} template
|
|
30
|
+
* @returns {ASTNode} Root Fragment node
|
|
31
|
+
*/
|
|
32
|
+
parse(template) {
|
|
33
|
+
this.input = template;
|
|
34
|
+
this.pos = 0;
|
|
35
|
+
this.length = template.length;
|
|
36
|
+
|
|
37
|
+
const children = this.parseFragment([]);
|
|
38
|
+
return { type: 'Fragment', children };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse a sequence of nodes until we hit a stop token.
|
|
43
|
+
* @param {string[]} stopTokens - e.g. ['{/if}', '{:else}', '{:else if']
|
|
44
|
+
* @returns {ASTNode[]}
|
|
45
|
+
*/
|
|
46
|
+
parseFragment(stopTokens) {
|
|
47
|
+
const children = [];
|
|
48
|
+
|
|
49
|
+
while (this.pos < this.length) {
|
|
50
|
+
// Check for stop tokens
|
|
51
|
+
if (stopTokens.length > 0) {
|
|
52
|
+
for (const stop of stopTokens) {
|
|
53
|
+
if (this.lookingAt(stop)) {
|
|
54
|
+
return children;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const node = this.parseNext();
|
|
60
|
+
if (node) {
|
|
61
|
+
// Merge adjacent text nodes
|
|
62
|
+
if (node.type === 'Text' && children.length > 0 && children[children.length - 1].type === 'Text') {
|
|
63
|
+
children[children.length - 1].value += node.value;
|
|
64
|
+
} else {
|
|
65
|
+
children.push(node);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return children;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse the next node at current position
|
|
75
|
+
* @returns {ASTNode|null}
|
|
76
|
+
*/
|
|
77
|
+
parseNext() {
|
|
78
|
+
if (this.pos >= this.length) return null;
|
|
79
|
+
|
|
80
|
+
const ch = this.input[this.pos];
|
|
81
|
+
|
|
82
|
+
// Template syntax: { ... }
|
|
83
|
+
if (ch === '{') {
|
|
84
|
+
const next = this.input[this.pos + 1];
|
|
85
|
+
|
|
86
|
+
// Block opening: {#if ...} or {#each ...}
|
|
87
|
+
if (next === '#') {
|
|
88
|
+
if (this.lookingAt('{#if ') || this.lookingAt('{#if}')) {
|
|
89
|
+
return this.parseIfBlock();
|
|
90
|
+
}
|
|
91
|
+
if (this.lookingAt('{#each ')) {
|
|
92
|
+
return this.parseEachBlock();
|
|
93
|
+
}
|
|
94
|
+
// Unknown block — treat as text
|
|
95
|
+
return this.parseText();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Special tags: {@html}, {@const}, {@debug}
|
|
99
|
+
if (next === '@') {
|
|
100
|
+
if (this.lookingAt('{@html ')) {
|
|
101
|
+
return this.parseHtmlTag();
|
|
102
|
+
}
|
|
103
|
+
if (this.lookingAt('{@const ')) {
|
|
104
|
+
return this.parseConstTag();
|
|
105
|
+
}
|
|
106
|
+
if (this.lookingAt('{@debug ')) {
|
|
107
|
+
return this.parseDebugTag();
|
|
108
|
+
}
|
|
109
|
+
return this.parseText();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Closing/continuation tags — should be handled by parent
|
|
113
|
+
if (next === '/' || next === ':') {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Expression: {expr}
|
|
118
|
+
return this.parseExpression();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// HTML tag: < ... >
|
|
122
|
+
if (ch === '<') {
|
|
123
|
+
const next = this.input[this.pos + 1];
|
|
124
|
+
// Only parse opening tags (not closing tags or comments)
|
|
125
|
+
if (next && /[a-zA-Z]/.test(next)) {
|
|
126
|
+
return this.parseTag();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Plain text
|
|
131
|
+
return this.parseText();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ========================================
|
|
135
|
+
// {#if} Block
|
|
136
|
+
// ========================================
|
|
137
|
+
|
|
138
|
+
parseIfBlock() {
|
|
139
|
+
// {#if condition}
|
|
140
|
+
this.pos += 4; // skip '{#if'
|
|
141
|
+
const condition = this.readUntilCloseBrace();
|
|
142
|
+
|
|
143
|
+
// Parse consequent (the "then" branch)
|
|
144
|
+
const consequent = this.parseFragment(['{:else if ', '{:else if\t', '{:else if\n', '{:else}', '{/if}']);
|
|
145
|
+
|
|
146
|
+
// Collect else-if branches
|
|
147
|
+
const elseIfBranches = [];
|
|
148
|
+
while (this.lookingAt('{:else if ') || this.lookingAt('{:else if\t') || this.lookingAt('{:else if\n')) {
|
|
149
|
+
this.pos += 9; // skip '{:else if'
|
|
150
|
+
const elseIfCondition = this.readUntilCloseBrace();
|
|
151
|
+
const elseIfBody = this.parseFragment(['{:else if ', '{:else if\t', '{:else if\n', '{:else}', '{/if}']);
|
|
152
|
+
elseIfBranches.push({
|
|
153
|
+
condition: elseIfCondition,
|
|
154
|
+
body: { type: 'Fragment', children: elseIfBody },
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Optional else branch
|
|
159
|
+
let alternate = null;
|
|
160
|
+
if (this.lookingAt('{:else}')) {
|
|
161
|
+
this.pos += 7; // skip '{:else}'
|
|
162
|
+
const elseChildren = this.parseFragment(['{/if}']);
|
|
163
|
+
alternate = { type: 'Fragment', children: elseChildren };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Consume {/if}
|
|
167
|
+
if (this.lookingAt('{/if}')) {
|
|
168
|
+
this.pos += 5;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
type: 'IfBlock',
|
|
173
|
+
condition,
|
|
174
|
+
consequent: { type: 'Fragment', children: consequent },
|
|
175
|
+
elseIfBranches,
|
|
176
|
+
alternate,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ========================================
|
|
181
|
+
// {#each} Block
|
|
182
|
+
// ========================================
|
|
183
|
+
|
|
184
|
+
parseEachBlock() {
|
|
185
|
+
// {#each expression as item, index (key)}
|
|
186
|
+
this.pos += 7; // skip '{#each '
|
|
187
|
+
|
|
188
|
+
// Read everything until closing }
|
|
189
|
+
const fullExpr = this.readUntilCloseBrace();
|
|
190
|
+
|
|
191
|
+
// Parse: expression as item, index (key)
|
|
192
|
+
const asIndex = fullExpr.indexOf(' as ');
|
|
193
|
+
if (asIndex === -1) {
|
|
194
|
+
// Malformed each — treat as text
|
|
195
|
+
return { type: 'Text', value: `{#each ${fullExpr}}` };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const iterableExpr = fullExpr.substring(0, asIndex).trim();
|
|
199
|
+
let rest = fullExpr.substring(asIndex + 4).trim();
|
|
200
|
+
|
|
201
|
+
// Extract optional (key) at the end
|
|
202
|
+
let keyExpr = null;
|
|
203
|
+
const keyMatch = rest.match(/\(([^)]+)\)\s*$/);
|
|
204
|
+
if (keyMatch) {
|
|
205
|
+
keyExpr = keyMatch[1].trim();
|
|
206
|
+
rest = rest.substring(0, keyMatch.index).trim();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Extract item and optional index: "item" or "item, index"
|
|
210
|
+
const parts = rest.split(',').map(s => s.trim());
|
|
211
|
+
const itemVar = parts[0];
|
|
212
|
+
const indexVar = parts[1] || null;
|
|
213
|
+
|
|
214
|
+
// Parse body
|
|
215
|
+
const body = this.parseFragment(['{:else}', '{/each}']);
|
|
216
|
+
|
|
217
|
+
// Optional else
|
|
218
|
+
let elseBlock = null;
|
|
219
|
+
if (this.lookingAt('{:else}')) {
|
|
220
|
+
this.pos += 7;
|
|
221
|
+
const elseChildren = this.parseFragment(['{/each}']);
|
|
222
|
+
elseBlock = { type: 'Fragment', children: elseChildren };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Consume {/each}
|
|
226
|
+
if (this.lookingAt('{/each}')) {
|
|
227
|
+
this.pos += 7;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
type: 'EachBlock',
|
|
232
|
+
expression: iterableExpr,
|
|
233
|
+
itemVar,
|
|
234
|
+
indexVar,
|
|
235
|
+
keyExpr,
|
|
236
|
+
body: { type: 'Fragment', children: body },
|
|
237
|
+
else: elseBlock,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ========================================
|
|
242
|
+
// {@html expression}
|
|
243
|
+
// ========================================
|
|
244
|
+
|
|
245
|
+
parseHtmlTag() {
|
|
246
|
+
this.pos += 7; // skip '{@html '
|
|
247
|
+
const expression = this.readUntilCloseBrace();
|
|
248
|
+
return { type: 'HtmlExpression', expression };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ========================================
|
|
252
|
+
// {@const name = expression}
|
|
253
|
+
// ========================================
|
|
254
|
+
|
|
255
|
+
parseConstTag() {
|
|
256
|
+
this.pos += 8; // skip '{@const '
|
|
257
|
+
const content = this.readUntilCloseBrace();
|
|
258
|
+
const eqIndex = content.indexOf('=');
|
|
259
|
+
if (eqIndex === -1) {
|
|
260
|
+
return { type: 'Text', value: '' };
|
|
261
|
+
}
|
|
262
|
+
const name = content.substring(0, eqIndex).trim();
|
|
263
|
+
const expression = content.substring(eqIndex + 1).trim();
|
|
264
|
+
return { type: 'ConstTag', name, expression };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ========================================
|
|
268
|
+
// {@debug var1, var2}
|
|
269
|
+
// ========================================
|
|
270
|
+
|
|
271
|
+
parseDebugTag() {
|
|
272
|
+
this.pos += 8; // skip '{@debug '
|
|
273
|
+
const content = this.readUntilCloseBrace();
|
|
274
|
+
const variables = content.split(',').map(s => s.trim()).filter(Boolean);
|
|
275
|
+
return { type: 'DebugTag', variables };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ========================================
|
|
279
|
+
// {expression} — value interpolation
|
|
280
|
+
// ========================================
|
|
281
|
+
|
|
282
|
+
parseExpression() {
|
|
283
|
+
this.pos++; // skip '{'
|
|
284
|
+
const expression = this.readUntilCloseBraceRaw();
|
|
285
|
+
return { type: 'Expression', expression };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ========================================
|
|
289
|
+
// HTML Tag parsing (for class: and attr={})
|
|
290
|
+
// ========================================
|
|
291
|
+
|
|
292
|
+
parseTag() {
|
|
293
|
+
const start = this.pos;
|
|
294
|
+
this.pos++; // skip '<'
|
|
295
|
+
|
|
296
|
+
// Read tag name
|
|
297
|
+
const tagName = this.readWhile(/[a-zA-Z0-9-]/);
|
|
298
|
+
if (!tagName) {
|
|
299
|
+
// Not a valid tag, treat as text
|
|
300
|
+
this.pos = start;
|
|
301
|
+
return this.parseTextChar();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Parse attributes
|
|
305
|
+
const attributes = [];
|
|
306
|
+
const classDirectives = [];
|
|
307
|
+
|
|
308
|
+
this.skipWhitespace();
|
|
309
|
+
|
|
310
|
+
while (this.pos < this.length && this.input[this.pos] !== '>' && !this.lookingAt('/>')) {
|
|
311
|
+
this.skipWhitespace();
|
|
312
|
+
if (this.pos >= this.length || this.input[this.pos] === '>' || this.lookingAt('/>')) break;
|
|
313
|
+
|
|
314
|
+
// class:name={condition}
|
|
315
|
+
if (this.lookingAt('class:')) {
|
|
316
|
+
this.pos += 6; // skip 'class:'
|
|
317
|
+
// Read class name (can contain brackets for Tailwind: e.g. lg:bg-[#fff])
|
|
318
|
+
let className = '';
|
|
319
|
+
while (this.pos < this.length) {
|
|
320
|
+
const c = this.input[this.pos];
|
|
321
|
+
if (c === '=') break;
|
|
322
|
+
if (c === ' ' || c === '>' || c === '/') break;
|
|
323
|
+
if (c === '[') {
|
|
324
|
+
// Bracket notation (Tailwind arbitrary values)
|
|
325
|
+
className += c;
|
|
326
|
+
this.pos++;
|
|
327
|
+
while (this.pos < this.length && this.input[this.pos] !== ']') {
|
|
328
|
+
className += this.input[this.pos];
|
|
329
|
+
this.pos++;
|
|
330
|
+
}
|
|
331
|
+
if (this.pos < this.length) {
|
|
332
|
+
className += this.input[this.pos]; // closing ]
|
|
333
|
+
this.pos++;
|
|
334
|
+
}
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
className += c;
|
|
338
|
+
this.pos++;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (this.input[this.pos] === '=' && this.input[this.pos + 1] === '{') {
|
|
342
|
+
this.pos += 2; // skip ={
|
|
343
|
+
const condition = this.readUntilCloseBraceRaw();
|
|
344
|
+
classDirectives.push({ name: className, condition });
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Regular attribute
|
|
350
|
+
const attrName = this.readWhile(/[a-zA-Z0-9_:@.-]/);
|
|
351
|
+
if (!attrName) {
|
|
352
|
+
// Skip unknown character
|
|
353
|
+
this.pos++;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this.skipWhitespace();
|
|
358
|
+
|
|
359
|
+
if (this.input[this.pos] === '=') {
|
|
360
|
+
this.pos++; // skip '='
|
|
361
|
+
this.skipWhitespace();
|
|
362
|
+
|
|
363
|
+
if (this.input[this.pos] === '{') {
|
|
364
|
+
// Dynamic attribute: attr={expression}
|
|
365
|
+
this.pos++; // skip '{'
|
|
366
|
+
const expression = this.readUntilCloseBraceRaw();
|
|
367
|
+
attributes.push({ type: 'DynamicAttribute', name: attrName, expression });
|
|
368
|
+
} else if (this.input[this.pos] === '"') {
|
|
369
|
+
// Quoted attribute: attr="value" (may contain {expr} inside)
|
|
370
|
+
this.pos++; // skip opening "
|
|
371
|
+
const value = this.readQuotedAttributeValue('"');
|
|
372
|
+
attributes.push(value.dynamic
|
|
373
|
+
? { type: 'DynamicQuotedAttribute', name: attrName, parts: value.parts }
|
|
374
|
+
: { type: 'StaticAttribute', name: attrName, value: value.text }
|
|
375
|
+
);
|
|
376
|
+
} else if (this.input[this.pos] === "'") {
|
|
377
|
+
// Single-quoted attribute
|
|
378
|
+
this.pos++; // skip opening '
|
|
379
|
+
const value = this.readQuotedAttributeValue("'");
|
|
380
|
+
attributes.push(value.dynamic
|
|
381
|
+
? { type: 'DynamicQuotedAttribute', name: attrName, parts: value.parts }
|
|
382
|
+
: { type: 'StaticAttribute', name: attrName, value: value.text }
|
|
383
|
+
);
|
|
384
|
+
} else {
|
|
385
|
+
// Unquoted attribute value
|
|
386
|
+
const value = this.readWhile(/[^\s>]/);
|
|
387
|
+
attributes.push({ type: 'StaticAttribute', name: attrName, value });
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
// Boolean attribute: disabled, checked, etc.
|
|
391
|
+
attributes.push({ type: 'StaticAttribute', name: attrName, value: true });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Consume closing > or />
|
|
396
|
+
let selfClosing = false;
|
|
397
|
+
if (this.lookingAt('/>')) {
|
|
398
|
+
selfClosing = true;
|
|
399
|
+
this.pos += 2;
|
|
400
|
+
} else if (this.input[this.pos] === '>') {
|
|
401
|
+
this.pos++;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
type: 'Tag',
|
|
406
|
+
tagName,
|
|
407
|
+
attributes,
|
|
408
|
+
classDirectives,
|
|
409
|
+
selfClosing,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Read a quoted attribute value, detecting {expression} inside quotes.
|
|
415
|
+
* e.g. "Hello {name}, welcome" → parts with text and expressions
|
|
416
|
+
*/
|
|
417
|
+
readQuotedAttributeValue(quote) {
|
|
418
|
+
const parts = [];
|
|
419
|
+
let text = '';
|
|
420
|
+
let hasDynamic = false;
|
|
421
|
+
|
|
422
|
+
while (this.pos < this.length && this.input[this.pos] !== quote) {
|
|
423
|
+
if (this.input[this.pos] === '{' && this.input[this.pos + 1] !== '#' &&
|
|
424
|
+
this.input[this.pos + 1] !== '/' && this.input[this.pos + 1] !== ':' &&
|
|
425
|
+
this.input[this.pos + 1] !== '@') {
|
|
426
|
+
// Expression inside quoted attribute
|
|
427
|
+
if (text) {
|
|
428
|
+
parts.push({ type: 'text', value: text });
|
|
429
|
+
text = '';
|
|
430
|
+
}
|
|
431
|
+
this.pos++; // skip {
|
|
432
|
+
const expr = this.readUntilCloseBraceRaw();
|
|
433
|
+
parts.push({ type: 'expr', value: expr });
|
|
434
|
+
hasDynamic = true;
|
|
435
|
+
} else {
|
|
436
|
+
text += this.input[this.pos];
|
|
437
|
+
this.pos++;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this.pos++; // skip closing quote
|
|
442
|
+
|
|
443
|
+
if (text) {
|
|
444
|
+
parts.push({ type: 'text', value: text });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!hasDynamic) {
|
|
448
|
+
return { dynamic: false, text: parts.map(p => p.value).join('') };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return { dynamic: true, parts };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ========================================
|
|
455
|
+
// Text
|
|
456
|
+
// ========================================
|
|
457
|
+
|
|
458
|
+
parseText() {
|
|
459
|
+
let text = '';
|
|
460
|
+
while (this.pos < this.length) {
|
|
461
|
+
const ch = this.input[this.pos];
|
|
462
|
+
|
|
463
|
+
// Stop at template syntax
|
|
464
|
+
if (ch === '{') break;
|
|
465
|
+
|
|
466
|
+
// Stop at HTML tags that we want to parse for attributes
|
|
467
|
+
if (ch === '<' && this.pos + 1 < this.length) {
|
|
468
|
+
const next = this.input[this.pos + 1];
|
|
469
|
+
if (/[a-zA-Z]/.test(next)) break;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
text += ch;
|
|
473
|
+
this.pos++;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!text) return null;
|
|
477
|
+
return { type: 'Text', value: text };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
parseTextChar() {
|
|
481
|
+
const ch = this.input[this.pos];
|
|
482
|
+
this.pos++;
|
|
483
|
+
return { type: 'Text', value: ch };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ========================================
|
|
487
|
+
// Utilities
|
|
488
|
+
// ========================================
|
|
489
|
+
|
|
490
|
+
lookingAt(str) {
|
|
491
|
+
return this.input.substring(this.pos, this.pos + str.length) === str;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Read until matching } (handles nested {} in expressions)
|
|
496
|
+
* Skips the closing }
|
|
497
|
+
* Trims the result
|
|
498
|
+
*/
|
|
499
|
+
readUntilCloseBrace() {
|
|
500
|
+
return this.readUntilCloseBraceRaw().trim();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Read until matching } (handles nested {}, strings, template literals)
|
|
505
|
+
* Skips the closing }
|
|
506
|
+
*/
|
|
507
|
+
readUntilCloseBraceRaw() {
|
|
508
|
+
let depth = 1;
|
|
509
|
+
let result = '';
|
|
510
|
+
|
|
511
|
+
while (this.pos < this.length && depth > 0) {
|
|
512
|
+
const ch = this.input[this.pos];
|
|
513
|
+
|
|
514
|
+
// Handle strings (don't count {} inside strings)
|
|
515
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
516
|
+
result += ch;
|
|
517
|
+
this.pos++;
|
|
518
|
+
// Read until matching quote
|
|
519
|
+
while (this.pos < this.length && this.input[this.pos] !== ch) {
|
|
520
|
+
if (this.input[this.pos] === '\\') {
|
|
521
|
+
result += this.input[this.pos];
|
|
522
|
+
this.pos++;
|
|
523
|
+
if (this.pos < this.length) {
|
|
524
|
+
result += this.input[this.pos];
|
|
525
|
+
this.pos++;
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
// Handle ${...} in template literals
|
|
529
|
+
if (ch === '`' && this.input[this.pos] === '$' && this.input[this.pos + 1] === '{') {
|
|
530
|
+
result += '${';
|
|
531
|
+
this.pos += 2;
|
|
532
|
+
// Read the inner expression
|
|
533
|
+
let innerDepth = 1;
|
|
534
|
+
while (this.pos < this.length && innerDepth > 0) {
|
|
535
|
+
if (this.input[this.pos] === '{') innerDepth++;
|
|
536
|
+
else if (this.input[this.pos] === '}') innerDepth--;
|
|
537
|
+
if (innerDepth > 0) {
|
|
538
|
+
result += this.input[this.pos];
|
|
539
|
+
this.pos++;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (this.pos < this.length) {
|
|
543
|
+
result += '}';
|
|
544
|
+
this.pos++; // skip closing }
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
result += this.input[this.pos];
|
|
548
|
+
this.pos++;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (this.pos < this.length) {
|
|
553
|
+
result += this.input[this.pos]; // closing quote
|
|
554
|
+
this.pos++;
|
|
555
|
+
}
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (ch === '{') {
|
|
560
|
+
depth++;
|
|
561
|
+
result += ch;
|
|
562
|
+
this.pos++;
|
|
563
|
+
} else if (ch === '}') {
|
|
564
|
+
depth--;
|
|
565
|
+
if (depth === 0) {
|
|
566
|
+
this.pos++; // skip closing }
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
result += ch;
|
|
570
|
+
this.pos++;
|
|
571
|
+
} else {
|
|
572
|
+
result += ch;
|
|
573
|
+
this.pos++;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return result;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
readWhile(regex) {
|
|
581
|
+
let result = '';
|
|
582
|
+
while (this.pos < this.length && regex.test(this.input[this.pos])) {
|
|
583
|
+
result += this.input[this.pos];
|
|
584
|
+
this.pos++;
|
|
585
|
+
}
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
skipWhitespace() {
|
|
590
|
+
while (this.pos < this.length && /\s/.test(this.input[this.pos])) {
|
|
591
|
+
this.pos++;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export { TemplateParser };
|
|
597
|
+
export default TemplateParser;
|