@lucidkiln/core 1.0.0 → 1.1.0

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.
@@ -0,0 +1,2667 @@
1
+ //#region src/parser/ast.ts
2
+ /** The name of the default slot, filled by content outside any `fill`. */
3
+ const DEFAULT_SLOT = "default";
4
+ const ESCAPE_STRATEGIES = [
5
+ "html",
6
+ "attr",
7
+ "js",
8
+ "css",
9
+ "url"
10
+ ];
11
+ /** The value types a prop can declare, named as in the docs' table of types. */
12
+ const TYPE_NAMES = [
13
+ "null",
14
+ "boolean",
15
+ "number",
16
+ "string",
17
+ "array",
18
+ "hash",
19
+ "set",
20
+ "date",
21
+ "markup",
22
+ "object"
23
+ ];
24
+ const PUNCTUATORS = [
25
+ "...",
26
+ "?.",
27
+ "??",
28
+ "?:",
29
+ "**",
30
+ "//",
31
+ "==",
32
+ "!=",
33
+ "<=",
34
+ ">=",
35
+ "=>",
36
+ "..",
37
+ "+",
38
+ "-",
39
+ "*",
40
+ "/",
41
+ "%",
42
+ "~",
43
+ "<",
44
+ ">",
45
+ "=",
46
+ "(",
47
+ ")",
48
+ "[",
49
+ "]",
50
+ "{",
51
+ "}",
52
+ ",",
53
+ ":",
54
+ ".",
55
+ "|",
56
+ "?"
57
+ ];
58
+ /** Punctuators by their first character's code, longest first as in `PUNCTUATORS`, so lexing tries only a few. */
59
+ const PUNCTUATORS_BY_CHARACTER = /* @__PURE__ */ new Map();
60
+ for (const punct of PUNCTUATORS) {
61
+ const code = punct.charCodeAt(0);
62
+ const list = PUNCTUATORS_BY_CHARACTER.get(code);
63
+ if (list) list.push(punct);
64
+ else PUNCTUATORS_BY_CHARACTER.set(code, [punct]);
65
+ }
66
+ /**
67
+ * The punctuator at `pos`, whose first two character codes are `code` and `next`, or null. Longest first, so `...` wins
68
+ * over `..` and `.`, as in `PUNCTUATORS`.
69
+ */
70
+ const punctuatorAt = (text, pos, code, next) => {
71
+ switch (code) {
72
+ case 46:
73
+ if (next === 46) return text.charCodeAt(pos + 2) === 46 ? "..." : "..";
74
+ return ".";
75
+ case 63: return next === 46 ? "?." : next === 63 ? "??" : next === 58 ? "?:" : "?";
76
+ case 42: return next === 42 ? "**" : "*";
77
+ case 47: return next === 47 ? "//" : "/";
78
+ case 61: return next === 61 ? "==" : next === 62 ? "=>" : "=";
79
+ case 33: return next === 61 ? "!=" : null;
80
+ case 60: return next === 61 ? "<=" : "<";
81
+ case 62: return next === 61 ? ">=" : ">";
82
+ default: {
83
+ const single = SINGLE_PUNCTUATORS[code];
84
+ return single === void 0 ? null : single;
85
+ }
86
+ }
87
+ };
88
+ /** Punctuators of one character that start no longer one, by character code. */
89
+ const SINGLE_PUNCTUATORS = [];
90
+ for (const punct of PUNCTUATORS) {
91
+ const code = punct.charCodeAt(0);
92
+ if (PUNCTUATORS_BY_CHARACTER.get(code).length === 1) SINGLE_PUNCTUATORS[code] = punct;
93
+ }
94
+ const CLOSED = {
95
+ closed: true,
96
+ trim: false
97
+ };
98
+ const CLOSED_TRIM = {
99
+ closed: true,
100
+ trim: true
101
+ };
102
+ const NOT_CLOSED = {
103
+ closed: false,
104
+ trim: false
105
+ };
106
+ const ESCAPES = {
107
+ n: "\n",
108
+ r: "\r",
109
+ t: " ",
110
+ "\\": "\\",
111
+ "'": "'",
112
+ "\"": "\""
113
+ };
114
+ const VERBATIM_END = /\{%(-?)\s*endverbatim\s*(-?)%\}/g;
115
+ const isInlineSpace = (code) => code === 32 || code === 9 || code === 13 || code === 11 || code === 12;
116
+ const isSpace = (code) => code === 10 || isInlineSpace(code);
117
+ const isNameStart = (code) => code >= 97 && code <= 122 || code >= 65 && code <= 90 || code === 95;
118
+ const isDigit = (code) => code >= 48 && code <= 57;
119
+ const isNamePart = (code) => isNameStart(code) || isDigit(code);
120
+ /**
121
+ * Turns template source into tokens in one pass, then applies whitespace control.
122
+ *
123
+ * Text outside delimiters becomes `text` tokens. Comments produce no tokens. Lines holding only block tags and comments are removed, and `-` markers trim neighbouring whitespace.
124
+ */
125
+ const lex = (text) => new Lexer(text).run();
126
+ /**
127
+ * Splits source into text, outputs, tags and comments as written, before whitespace control. Tools such as the formatter use it. Comments have no tokens.
128
+ */
129
+ const scanParts = (text) => new Lexer(text).scan();
130
+ /** Where a template's content starts: after a byte order mark, which is never content. */
131
+ const contentStart = (text) => text.charCodeAt(0) === 65279 ? 1 : 0;
132
+ var Lexer = class {
133
+ #text;
134
+ #pos;
135
+ /** How many string interpolations the lexer is inside. */
136
+ #interpolations = 0;
137
+ #tokens = [];
138
+ #problems = [];
139
+ constructor(text) {
140
+ this.#text = text;
141
+ this.#pos = contentStart(text);
142
+ }
143
+ scan() {
144
+ return {
145
+ parts: this.#scanParts(),
146
+ problems: this.#problems
147
+ };
148
+ }
149
+ run() {
150
+ const parts = this.#scanParts();
151
+ applyTrimMarkers(this.#text, parts);
152
+ const standalone = applyStandaloneLines(this.#text, parts);
153
+ const lines = textLines(this.#text, parts, standalone);
154
+ const tokens = [];
155
+ let line = 0;
156
+ for (const part of parts) {
157
+ const text = part.kind === "text";
158
+ const at = text ? part.from : part.start;
159
+ if (text && part.from < part.to) tokens.push({
160
+ type: "text",
161
+ value: this.#text.slice(part.from, part.to),
162
+ start: part.from,
163
+ end: part.to
164
+ });
165
+ else if (lines[line]?.start === at) tokens.push({
166
+ type: "text",
167
+ value: "",
168
+ start: at,
169
+ end: at
170
+ });
171
+ if (!text && part.kind !== "comment") for (const token of part.tokens) tokens.push(token);
172
+ const end = text ? part.to : part.start;
173
+ while ((lines[line]?.start ?? Infinity) <= end) line++;
174
+ }
175
+ tokens.push({
176
+ type: "eof",
177
+ value: "",
178
+ start: this.#text.length,
179
+ end: this.#text.length
180
+ });
181
+ return {
182
+ tokens,
183
+ problems: this.#problems,
184
+ standalone,
185
+ lines
186
+ };
187
+ }
188
+ #scanParts() {
189
+ const text = this.#text;
190
+ const parts = [];
191
+ const pushText = (start, end) => {
192
+ if (end > start) parts.push({
193
+ kind: "text",
194
+ start,
195
+ end,
196
+ from: start,
197
+ to: end
198
+ });
199
+ };
200
+ while (this.#pos < text.length) {
201
+ const open = this.#findOpen(this.#pos);
202
+ if (open === -1) {
203
+ pushText(this.#pos, text.length);
204
+ break;
205
+ }
206
+ pushText(this.#pos, open);
207
+ const kind = text[open + 1];
208
+ if (kind === "#") {
209
+ parts.push(this.#scanComment(open));
210
+ continue;
211
+ }
212
+ const part = this.#scanDelimited(open, kind === "{" ? "output" : "tag");
213
+ parts.push(part);
214
+ const inner = part.tokens;
215
+ if (part.kind === "tag" && inner.length === 3 && inner[1]?.type === "name" && inner[1].value === "verbatim") this.#scanVerbatim(part, parts);
216
+ }
217
+ return parts;
218
+ }
219
+ #findOpen(from) {
220
+ const text = this.#text;
221
+ let index = text.indexOf("{", from);
222
+ while (index !== -1) {
223
+ const next = text.charCodeAt(index + 1);
224
+ if (next === 123 || next === 37 || next === 35) return index;
225
+ index = text.indexOf("{", index + 1);
226
+ }
227
+ return -1;
228
+ }
229
+ #scanComment(open) {
230
+ const text = this.#text;
231
+ const close = text.indexOf("#}", open + 2);
232
+ const trimBefore = text[open + 2] === "-";
233
+ if (close === -1) {
234
+ this.#problem("unclosed-comment", "Comment is missing its closing \"#}\"", open, open + 2);
235
+ this.#pos = text.length;
236
+ return {
237
+ kind: "comment",
238
+ start: open,
239
+ end: text.length,
240
+ trimBefore,
241
+ trimAfter: false,
242
+ tokens: []
243
+ };
244
+ }
245
+ this.#pos = close + 2;
246
+ return {
247
+ kind: "comment",
248
+ start: open,
249
+ end: close + 2,
250
+ trimBefore,
251
+ trimAfter: close > open + 2 && text[close - 1] === "-",
252
+ tokens: []
253
+ };
254
+ }
255
+ #scanDelimited(open, kind) {
256
+ const text = this.#text;
257
+ const trimBefore = text[open + 2] === "-";
258
+ this.#tokens = [{
259
+ type: kind === "output" ? "outputOpen" : "tagOpen",
260
+ value: text.slice(open, open + (trimBefore ? 3 : 2)),
261
+ start: open,
262
+ end: open + 2
263
+ }];
264
+ this.#pos = open + (trimBefore ? 3 : 2);
265
+ const closeStart = this.#pos;
266
+ const { trim, closed } = this.#lexExpression(kind);
267
+ const end = this.#pos;
268
+ if (!closed) this.#problem("unclosed-tag", kind === "output" ? "Output is missing its closing \"}}\"" : "Tag is missing its closing \"%}\"", open, open + 2);
269
+ this.#tokens.push({
270
+ type: kind === "output" ? "outputClose" : "tagClose",
271
+ value: closed ? text.slice(trim ? end - 3 : end - 2, end) : "",
272
+ start: closed ? end - 2 : Math.max(closeStart, end),
273
+ end
274
+ });
275
+ return {
276
+ kind,
277
+ start: open,
278
+ end,
279
+ trimBefore,
280
+ trimAfter: trim,
281
+ tokens: this.#tokens
282
+ };
283
+ }
284
+ /** Treats everything up to `{% endverbatim %}` as text. */
285
+ #scanVerbatim(tag, parts) {
286
+ const text = this.#text;
287
+ VERBATIM_END.lastIndex = this.#pos;
288
+ const match = VERBATIM_END.exec(text);
289
+ if (!match) {
290
+ this.#problem("unclosed-block", "\"verbatim\" is missing its \"endverbatim\" tag", tag.start, tag.end);
291
+ parts.push({
292
+ kind: "text",
293
+ start: this.#pos,
294
+ end: text.length,
295
+ from: this.#pos,
296
+ to: text.length
297
+ });
298
+ this.#pos = text.length;
299
+ return;
300
+ }
301
+ const start = match.index;
302
+ const end = start + match[0].length;
303
+ if (start > this.#pos) parts.push({
304
+ kind: "text",
305
+ start: this.#pos,
306
+ end: start,
307
+ from: this.#pos,
308
+ to: start
309
+ });
310
+ const nameStart = text.indexOf("endverbatim", start);
311
+ const trimBefore = match[1] === "-";
312
+ const trimAfter = match[2] === "-";
313
+ parts.push({
314
+ kind: "tag",
315
+ start,
316
+ end,
317
+ trimBefore,
318
+ trimAfter,
319
+ tokens: [
320
+ {
321
+ type: "tagOpen",
322
+ value: trimBefore ? "{%-" : "{%",
323
+ start,
324
+ end: start + 2
325
+ },
326
+ {
327
+ type: "name",
328
+ value: "endverbatim",
329
+ start: nameStart,
330
+ end: nameStart + 11
331
+ },
332
+ {
333
+ type: "tagClose",
334
+ value: trimAfter ? "-%}" : "%}",
335
+ start: end - 2,
336
+ end
337
+ }
338
+ ]
339
+ });
340
+ this.#pos = end;
341
+ }
342
+ /**
343
+ * Lexes expression tokens until the closing delimiter for `mode`.
344
+ *
345
+ * `closed` is false when the source ended, or a new tag started, before the delimiter.
346
+ */
347
+ #lexExpression(mode) {
348
+ const text = this.#text;
349
+ const length = text.length;
350
+ const tokens = this.#tokens;
351
+ let depth = 0;
352
+ let pos = this.#pos;
353
+ for (;;) {
354
+ while (pos < length && isSpace(text.charCodeAt(pos))) pos++;
355
+ if (pos >= length) {
356
+ this.#pos = pos;
357
+ return NOT_CLOSED;
358
+ }
359
+ const code = text.charCodeAt(pos);
360
+ const next = text.charCodeAt(pos + 1);
361
+ if (mode === "tag") {
362
+ if (code === 45 && next === 37 && text.charCodeAt(pos + 2) === 125) {
363
+ this.#pos = pos + 3;
364
+ return CLOSED_TRIM;
365
+ }
366
+ if (code === 37 && next === 125) {
367
+ this.#pos = pos + 2;
368
+ return CLOSED;
369
+ }
370
+ } else if (mode === "output" && depth === 0) {
371
+ if (code === 45 && next === 125 && text.charCodeAt(pos + 2) === 125) {
372
+ this.#pos = pos + 3;
373
+ return CLOSED_TRIM;
374
+ }
375
+ if (code === 125 && next === 125) {
376
+ this.#pos = pos + 2;
377
+ return CLOSED;
378
+ }
379
+ } else if (mode === "interp" && depth === 0 && code === 125) {
380
+ tokens.push({
381
+ type: "interpClose",
382
+ value: "}",
383
+ start: pos,
384
+ end: pos + 1
385
+ });
386
+ this.#pos = pos + 1;
387
+ return CLOSED;
388
+ }
389
+ if (code === 123 && (next === 123 || next === 37 || next === 35)) {
390
+ this.#pos = pos;
391
+ return NOT_CLOSED;
392
+ }
393
+ if (mode === "interp" && code === 37 && next === 125) {
394
+ this.#pos = pos;
395
+ return NOT_CLOSED;
396
+ }
397
+ if (isNameStart(code)) {
398
+ let end = pos + 1;
399
+ while (end < length && isNamePart(text.charCodeAt(end))) end++;
400
+ tokens.push({
401
+ type: "name",
402
+ value: text.slice(pos, end),
403
+ start: pos,
404
+ end
405
+ });
406
+ pos = end;
407
+ continue;
408
+ }
409
+ if (isDigit(code)) {
410
+ this.#pos = pos;
411
+ this.#lexNumber();
412
+ pos = this.#pos;
413
+ continue;
414
+ }
415
+ if (code === 34 || code === 39) {
416
+ this.#pos = pos;
417
+ this.#lexString();
418
+ pos = this.#pos;
419
+ continue;
420
+ }
421
+ const punct = punctuatorAt(text, pos, code, next);
422
+ if (punct !== null) {
423
+ if (code === 123) depth++;
424
+ else if (code === 125 && depth > 0) depth--;
425
+ tokens.push({
426
+ type: "punct",
427
+ value: punct,
428
+ start: pos,
429
+ end: pos + punct.length
430
+ });
431
+ pos += punct.length;
432
+ continue;
433
+ }
434
+ const character = String.fromCodePoint(text.codePointAt(pos));
435
+ this.#problem("unexpected-character", `Unexpected character "${character}"`, pos, pos + character.length);
436
+ pos += character.length;
437
+ }
438
+ }
439
+ #lexNumber() {
440
+ const text = this.#text;
441
+ const start = this.#pos;
442
+ const previous = this.#tokens[this.#tokens.length - 1];
443
+ const integerOnly = previous?.type === "punct" && (previous.value === "." || previous.value === "?.");
444
+ const readDigits = () => {
445
+ while (this.#pos < text.length) {
446
+ const code = text.charCodeAt(this.#pos);
447
+ if (isDigit(code)) this.#pos++;
448
+ else if (code === 95 && isDigit(text.charCodeAt(this.#pos + 1))) this.#pos++;
449
+ else break;
450
+ }
451
+ };
452
+ readDigits();
453
+ if (!integerOnly && text[this.#pos] === "." && isDigit(text.charCodeAt(this.#pos + 1))) {
454
+ this.#pos++;
455
+ readDigits();
456
+ }
457
+ const raw = text.slice(start, this.#pos);
458
+ this.#push("number", raw.replaceAll("_", ""), start, this.#pos);
459
+ }
460
+ #lexString() {
461
+ const text = this.#text;
462
+ const start = this.#pos;
463
+ const quote = text[start];
464
+ let value = "";
465
+ let segmentStart = start;
466
+ let interpolated = false;
467
+ this.#pos++;
468
+ while (this.#pos < text.length) {
469
+ const char = text[this.#pos];
470
+ if (char === quote) {
471
+ this.#pos++;
472
+ if (interpolated) {
473
+ if (value !== "") this.#push("string", value, segmentStart, this.#pos - 1);
474
+ this.#push("stringEnd", quote, this.#pos - 1, this.#pos);
475
+ } else this.#push("string", value, start, this.#pos);
476
+ return;
477
+ }
478
+ if (char === "\\") {
479
+ value += this.#readEscape(quote);
480
+ continue;
481
+ }
482
+ if (quote === "\"" && char === "#" && text[this.#pos + 1] === "{") {
483
+ if (!interpolated) {
484
+ interpolated = true;
485
+ this.#push("stringStart", quote, start, start + 1);
486
+ }
487
+ if (value !== "") {
488
+ this.#push("string", value, segmentStart, this.#pos);
489
+ value = "";
490
+ }
491
+ this.#push("interpOpen", "#{", this.#pos, this.#pos + 2);
492
+ this.#pos += 2;
493
+ const interpStart = this.#pos;
494
+ if (this.#interpolations === 200) {
495
+ this.#problem("too-deep", `Template is nested more than 200 levels deep`, interpStart - 2, interpStart);
496
+ this.#skipInterpolation();
497
+ segmentStart = this.#pos;
498
+ continue;
499
+ }
500
+ this.#interpolations++;
501
+ const { closed } = this.#lexExpression("interp");
502
+ this.#interpolations--;
503
+ if (!closed) {
504
+ this.#problem("unclosed-interpolation", "Interpolation is missing its closing \"}\"", interpStart - 2, interpStart);
505
+ this.#push("interpClose", "", this.#pos, this.#pos);
506
+ this.#push("stringEnd", "", this.#pos, this.#pos);
507
+ return;
508
+ }
509
+ segmentStart = this.#pos;
510
+ continue;
511
+ }
512
+ value += char;
513
+ this.#pos++;
514
+ }
515
+ this.#problem("unclosed-string", "String is missing its closing quote", start, start + 1);
516
+ if (interpolated) {
517
+ if (value !== "") this.#push("string", value, segmentStart, this.#pos);
518
+ this.#push("stringEnd", "", this.#pos, this.#pos);
519
+ } else this.#push("string", value, start, this.#pos);
520
+ }
521
+ /** Skips the rest of an interpolation without lexing it, up to the `}` that balances its `#{`. */
522
+ #skipInterpolation() {
523
+ const text = this.#text;
524
+ let depth = 0;
525
+ while (this.#pos < text.length) {
526
+ const char = text[this.#pos++];
527
+ if (char === "{") depth++;
528
+ else if (char === "}" && depth-- === 0) {
529
+ this.#push("interpClose", "}", this.#pos - 1, this.#pos);
530
+ return;
531
+ }
532
+ }
533
+ }
534
+ /** Reads a backslash escape. `\#{` is only an escape in double-quoted strings, where `#{` would start interpolation. */
535
+ #readEscape(quote) {
536
+ const text = this.#text;
537
+ const start = this.#pos;
538
+ const next = text[start + 1];
539
+ if (next === void 0) {
540
+ this.#pos++;
541
+ return "";
542
+ }
543
+ if (next === "#" && quote === "\"" && text[start + 2] === "{") {
544
+ this.#pos += 2;
545
+ return "#";
546
+ }
547
+ const simple = ESCAPES[next];
548
+ if (simple !== void 0) {
549
+ this.#pos += 2;
550
+ return simple;
551
+ }
552
+ if (next === "u" && text[start + 2] === "{") {
553
+ const brace = text.slice(start + 3, start + 10).indexOf("}");
554
+ const close = brace === -1 ? -1 : start + 3 + brace;
555
+ const hex = close === -1 ? "" : text.slice(start + 3, close);
556
+ if (/^[0-9a-fA-F]{1,6}$/.test(hex)) {
557
+ const code = Number.parseInt(hex, 16);
558
+ if (code <= 1114111) {
559
+ this.#pos = close + 1;
560
+ return String.fromCodePoint(code);
561
+ }
562
+ }
563
+ }
564
+ this.#problem("invalid-escape", `Unknown escape sequence "\\${next}"`, start, start + 2);
565
+ this.#pos += 2;
566
+ return next;
567
+ }
568
+ #push(type, value, start, end) {
569
+ this.#tokens.push({
570
+ type,
571
+ value,
572
+ start,
573
+ end
574
+ });
575
+ }
576
+ #problem(code, message, start, end) {
577
+ this.#problems.push({
578
+ code,
579
+ message,
580
+ span: {
581
+ start,
582
+ end
583
+ }
584
+ });
585
+ }
586
+ };
587
+ /** Applies `-` markers, which remove all whitespace on their side. */
588
+ const applyTrimMarkers = (text, parts) => {
589
+ for (let i = 0; i < parts.length; i++) {
590
+ const part = parts[i];
591
+ if (part.kind === "text") continue;
592
+ const previous = parts[i - 1];
593
+ if (part.trimBefore && previous?.kind === "text") {
594
+ let end = previous.end;
595
+ while (end > previous.start && isSpace(text.charCodeAt(end - 1))) end--;
596
+ previous.to = Math.min(previous.to, end);
597
+ }
598
+ const next = parts[i + 1];
599
+ if (part.trimAfter && next?.kind === "text") {
600
+ let start = next.start;
601
+ while (start < next.end && isSpace(text.charCodeAt(start))) start++;
602
+ next.from = Math.max(next.from, start);
603
+ }
604
+ }
605
+ };
606
+ const isInlineBlank = (text, start, end) => {
607
+ for (let i = start; i < end; i++) if (!isInlineSpace(text.charCodeAt(i))) return false;
608
+ return true;
609
+ };
610
+ /**
611
+ * The first line feed from `start` up to `end`, or -1. The search stops at `end`, unlike `indexOf`, so a template with
612
+ * many tags on one long line stays linear.
613
+ */
614
+ const newlineIn = (text, start, end) => {
615
+ for (let i = start; i < end; i++) if (text.charCodeAt(i) === 10) return i;
616
+ return -1;
617
+ };
618
+ /** The last line feed from `start` up to `end`, or -1. Stops at `start`, as `newlineIn` stops at `end`. */
619
+ const lastNewlineIn = (text, start, end) => {
620
+ for (let i = end - 1; i >= start; i--) if (text.charCodeAt(i) === 10) return i;
621
+ return -1;
622
+ };
623
+ /**
624
+ * Removes lines that hold only block tags and comments, including their indentation and line ending.
625
+ *
626
+ * Decisions use the original text, so they do not depend on what trim markers removed. Returns the start of each tag and comment it removed.
627
+ */
628
+ const applyStandaloneLines = (text, parts) => {
629
+ const removed = /* @__PURE__ */ new Map();
630
+ const isBlockLike = (part) => part !== void 0 && (part.kind === "tag" || part.kind === "comment");
631
+ let i = 0;
632
+ while (i < parts.length) {
633
+ if (!isBlockLike(parts[i])) {
634
+ i++;
635
+ continue;
636
+ }
637
+ let last = i;
638
+ for (;;) {
639
+ const next = parts[last + 1];
640
+ if (isBlockLike(next)) {
641
+ last++;
642
+ continue;
643
+ }
644
+ if (next?.kind === "text" && isInlineBlank(text, next.start, next.end) && isBlockLike(parts[last + 2])) {
645
+ last += 2;
646
+ continue;
647
+ }
648
+ break;
649
+ }
650
+ let standalone = true;
651
+ let cutBefore = -1;
652
+ const previous = parts[i - 1];
653
+ if (previous) {
654
+ if (previous.kind !== "text") standalone = false;
655
+ else {
656
+ const newline = lastNewlineIn(text, previous.start, previous.end);
657
+ if (newline !== -1) {
658
+ if (isInlineBlank(text, newline + 1, previous.end)) cutBefore = newline + 1;
659
+ else standalone = false;
660
+ } else if (i - 1 === 0 && isInlineBlank(text, previous.start, previous.end)) cutBefore = previous.start;
661
+ else standalone = false;
662
+ }
663
+ }
664
+ let cutAfter = -1;
665
+ const next = parts[last + 1];
666
+ if (standalone && next) {
667
+ if (next.kind !== "text") standalone = false;
668
+ else {
669
+ const newline = newlineIn(text, next.start, next.end);
670
+ if (newline !== -1) {
671
+ if (isInlineBlank(text, next.start, newline)) cutAfter = newline + 1;
672
+ else standalone = false;
673
+ } else if (last + 1 === parts.length - 1 && isInlineBlank(text, next.start, next.end)) cutAfter = next.end;
674
+ else standalone = false;
675
+ }
676
+ }
677
+ if (standalone) {
678
+ if (previous?.kind === "text") previous.to = Math.min(previous.to, cutBefore);
679
+ for (let k = i; k <= last; k++) {
680
+ const part = parts[k];
681
+ if (part.kind === "text") part.from = part.to;
682
+ else removed.set(part.start, part.end);
683
+ }
684
+ if (next?.kind === "text") next.from = Math.max(next.from, cutAfter);
685
+ }
686
+ i = last + 1;
687
+ }
688
+ return removed;
689
+ };
690
+ /** Finds the lines of text that hold content. See `TextLine`. */
691
+ const textLines = (text, parts, removed) => {
692
+ const lines = [];
693
+ const add = (start) => {
694
+ let indentEnd = start;
695
+ while (text[indentEnd] === " " || text[indentEnd] === " ") indentEnd++;
696
+ const next = text[indentEnd];
697
+ if (next === void 0 || next === "\n" || next === "\r") return;
698
+ if (removed.has(indentEnd)) return;
699
+ lines.push({
700
+ start,
701
+ indentEnd
702
+ });
703
+ };
704
+ const first = parts[0];
705
+ const start = contentStart(text);
706
+ if (first && (first.kind !== "text" || first.from === start)) add(start);
707
+ for (const part of parts) {
708
+ if (part.kind !== "text") continue;
709
+ for (let newline = newlineIn(text, part.start, part.end); newline !== -1; newline = newlineIn(text, newline + 1, part.end)) {
710
+ const start = newline + 1;
711
+ if (start < part.from || start > part.to) continue;
712
+ add(start);
713
+ }
714
+ }
715
+ return lines;
716
+ };
717
+ //#endregion
718
+ //#region src/parser/indent.ts
719
+ /** The bodies a statement holds. Branches share their tag's body. A component's fills are bodies of their own, inside the component's. */
720
+ const bodiesOf = (node) => {
721
+ const own = (lists, full = false) => ({
722
+ open: node.span.start,
723
+ end: node.span.end,
724
+ lists,
725
+ full
726
+ });
727
+ switch (node.type) {
728
+ case "If": return [own([...node.branches.map((branch) => branch.body), node.alternate ?? []])];
729
+ case "For": return [own([node.body, node.alternate ?? []])];
730
+ case "Component": return [...node.fills.map((fill) => ({
731
+ open: fill.span.start,
732
+ end: fill.span.end,
733
+ lists: [fill.body],
734
+ full: true
735
+ })), own([node.body, ...node.fills.map((fill) => fill.body)], true)];
736
+ case "Island": return [{
737
+ ...own([node.body]),
738
+ keep: true
739
+ }];
740
+ case "Block":
741
+ case "Slot":
742
+ case "Add": return [own([node.body], true)];
743
+ case "SetBlock":
744
+ case "Macro":
745
+ case "Cache":
746
+ case "With":
747
+ case "Apply":
748
+ case "Autoescape": return [own([node.body])];
749
+ default: return NO_BODIES;
750
+ }
751
+ };
752
+ const NO_BODIES = [];
753
+ /** The statement lists inside a statement, each once. Only a component's fills appear in more than one body. */
754
+ const childLists = (bodies) => bodies.length === 1 ? bodies[0].lists : new Set(bodies.flatMap((body) => body.lists));
755
+ /** The index of the first item whose key is at least `value`, in a list sorted by that key. */
756
+ const firstAtLeast = (items, value, key) => {
757
+ let low = 0;
758
+ let high = items.length;
759
+ while (low < high) {
760
+ const middle = low + high >>> 1;
761
+ if (key(items[middle]) < value) low = middle + 1;
762
+ else high = middle;
763
+ }
764
+ return low;
765
+ };
766
+ /**
767
+ * Finds `<pre>` and `<textarea>` tags, and the HTML comments, scripts and styles to skip, whose `<pre` is not a tag.
768
+ * Each region runs to its end, or to the end of the source when it is not closed.
769
+ */
770
+ const MARKUP = /<!--[\s\S]*?(?:-->|$)|<(script|style)(?=[\s/>])[\s\S]*?(?:<\/\1\s*>|$)|<(\/?)(pre|textarea)(?=[\s/>])/gi;
771
+ const MAY_PREFORMAT = /<(?:pre|textarea)/i;
772
+ /** Where `<pre>` and `<textarea>` elements start and end in the template's text, whose whitespace is content. */
773
+ const preformatted = (text, textTokens) => {
774
+ if (!MAY_PREFORMAT.test(text)) return [];
775
+ let visible = "";
776
+ for (const token of textTokens) {
777
+ visible += "\0".repeat(token.start - visible.length);
778
+ visible += text.slice(token.start, token.end);
779
+ }
780
+ const ranges = [];
781
+ let open = null;
782
+ MARKUP.lastIndex = 0;
783
+ for (let match = MARKUP.exec(visible); match; match = MARKUP.exec(visible)) {
784
+ const tag = match[3];
785
+ if (tag === void 0) continue;
786
+ const name = tag.toLowerCase();
787
+ if (open === null && match[2] === "") open = {
788
+ name,
789
+ start: match.index
790
+ };
791
+ else if (open?.name === name && match[2] === "/") {
792
+ ranges.push([open.start, match.index]);
793
+ open = null;
794
+ }
795
+ }
796
+ if (open) ranges.push([open.start, text.length]);
797
+ return ranges;
798
+ };
799
+ /**
800
+ * Works out the indentation of a template's text, so content can be inserted elsewhere and still line up.
801
+ *
802
+ * - Removes the extra indentation inside tags that stand on their own line: the spaces and tabs that every content line inside shares, beyond the tag's own. Blocks, slots and a component's content can be inserted where another tag stands, so they lose all of it and take the indentation of where they land.
803
+ * - Sets `indent` on the tags in `placedNodes`, which insert content, when they stand on their own line: the line's indentation once that removal is done.
804
+ * - Sets `lines` on text nodes: where each line of text starts, to take the indentation of where the text is inserted.
805
+ *
806
+ * Lines inside `<pre>` and `<textarea>` keep their whitespace. Amounts come from the source alone, so nested tags each remove their own step whatever order they are visited in.
807
+ */
808
+ const arrangeIndentation = (ast, text, { tokens, standalone, lines }, placedNodes) => {
809
+ const textTokens = tokens.filter((token) => token.type === "text");
810
+ const pre = preformatted(text, textTokens);
811
+ const inPre = (offset) => {
812
+ const range = pre[firstAtLeast(pre, offset, (range) => range[1])];
813
+ return range !== void 0 && range[0] < offset;
814
+ };
815
+ const textLines = pre.length > 0 ? lines.filter((line) => !inPre(line.start)) : lines;
816
+ const lineStart = (offset) => text.lastIndexOf("\n", offset - 1) + 1 || contentStart(text);
817
+ const lineOf = (offset) => {
818
+ const start = lineStart(offset);
819
+ let indentEnd = start;
820
+ while (text[indentEnd] === " " || text[indentEnd] === " ") indentEnd++;
821
+ return {
822
+ start,
823
+ indentEnd
824
+ };
825
+ };
826
+ const placed = [];
827
+ const islandEnds = [];
828
+ for (const node of placedNodes) {
829
+ if (inPre(node.span.start)) continue;
830
+ if (node.type === "Island") {
831
+ const end = text.lastIndexOf("{%", node.span.end - 1);
832
+ if (standalone.has(end)) islandEnds.push({
833
+ node,
834
+ line: lineOf(end)
835
+ });
836
+ }
837
+ if (!standalone.has(node.span.start)) continue;
838
+ placed.push({
839
+ node,
840
+ line: lineOf(node.span.start)
841
+ });
842
+ }
843
+ const contentLines = placed.length > 0 || islandEnds.length > 0 ? [
844
+ ...textLines,
845
+ ...placed.map((item) => item.line),
846
+ ...islandEnds.map((item) => item.line)
847
+ ].sort((a, b) => a.start - b.start) : textLines;
848
+ const cuts = [];
849
+ const indentOf = (offset) => {
850
+ const line = lineOf(offset);
851
+ return text.slice(line.start, line.indentEnd);
852
+ };
853
+ const trim = (body) => {
854
+ if (body.keep) return;
855
+ const openEnd = standalone.get(body.open);
856
+ if (openEnd === void 0) return;
857
+ const newline = text.indexOf("\n", openEnd);
858
+ if (newline === -1) return;
859
+ const to = text.lastIndexOf("{%", body.end - 1);
860
+ const first = firstAtLeast(contentLines, newline + 1, (line) => line.start);
861
+ const last = firstAtLeast(contentLines, to, (line) => line.start);
862
+ if (first >= last) return;
863
+ let shared = Infinity;
864
+ const firstLine = contentLines[first];
865
+ for (let i = first; i < last; i++) {
866
+ const line = contentLines[i];
867
+ let length = 0;
868
+ while (length < line.indentEnd - line.start && text[line.start + length] === text[firstLine.start + length]) length++;
869
+ shared = Math.min(shared, length);
870
+ }
871
+ let keep = 0;
872
+ if (!body.full) {
873
+ const own = indentOf(body.open);
874
+ if (shared <= own.length) return;
875
+ if (text.slice(firstLine.start, firstLine.start + own.length) !== own) return;
876
+ keep = own.length;
877
+ } else if (shared === 0) return;
878
+ for (let i = first; i < last; i++) {
879
+ const line = contentLines[i];
880
+ cuts.push([line.start + keep, line.start + shared]);
881
+ }
882
+ };
883
+ const visit = (list) => {
884
+ for (const node of list) {
885
+ const bodies = bodiesOf(node);
886
+ if (bodies.length === 0) continue;
887
+ for (const child of childLists(bodies)) visit(child);
888
+ for (const body of bodies) trim(body);
889
+ }
890
+ };
891
+ visit(ast.body);
892
+ cuts.sort((a, b) => a[0] - b[0]);
893
+ const merged = [];
894
+ for (const range of cuts) {
895
+ const previous = merged[merged.length - 1];
896
+ if (previous && range[0] <= previous[1]) previous[1] = Math.max(previous[1], range[1]);
897
+ else merged.push(range);
898
+ }
899
+ /** The source from `start` to `end`, without the cut ranges. */
900
+ const uncut = (start, end) => {
901
+ let out = "";
902
+ let at = start;
903
+ for (let i = firstAtLeast(merged, start, (range) => range[1] - 1); i < merged.length; i++) {
904
+ const [from, to] = merged[i];
905
+ if (from >= end) break;
906
+ out += text.slice(at, Math.max(at, from));
907
+ at = Math.max(at, to);
908
+ }
909
+ return out + text.slice(Math.min(at, end), end);
910
+ };
911
+ for (const { node, line } of placed) node.indent = uncut(line.start, line.indentEnd);
912
+ for (const { node, line } of islandEnds) node.closing = {
913
+ indent: line.start === lineOf(node.span.start).start ? null : uncut(line.start, line.indentEnd),
914
+ newline: text.includes("\n", node.span.end)
915
+ };
916
+ const hasCut = (start, end) => {
917
+ const range = merged[firstAtLeast(merged, start, (range) => range[1] - 1)];
918
+ return range !== void 0 && range[0] < end;
919
+ };
920
+ const rewrite = (list) => {
921
+ for (const node of list) {
922
+ if (node.type === "Text") {
923
+ let line = firstAtLeast(textLines, node.span.start, (l) => l.start);
924
+ const hasLines = (textLines[line]?.start ?? Infinity) <= node.span.end;
925
+ const cut = hasCut(node.span.start, node.span.end);
926
+ if (!hasLines && !cut) continue;
927
+ let value = "";
928
+ let size = 0;
929
+ const starts = [];
930
+ for (let i = firstAtLeast(textTokens, node.span.start, (t) => t.start); i < textTokens.length; i++) {
931
+ const token = textTokens[i];
932
+ if (token.end > node.span.end) break;
933
+ for (let next = textLines[line]; next !== void 0 && next.start <= token.end; next = textLines[++line]) starts.push(size + (cut ? uncut(token.start, next.start).length : next.start - token.start));
934
+ if (cut) {
935
+ const piece = uncut(token.start, token.end);
936
+ value += piece;
937
+ size += piece.length;
938
+ } else size += token.end - token.start;
939
+ }
940
+ if (cut) node.value = value;
941
+ if (starts.length > 0) node.lines = starts;
942
+ }
943
+ const bodies = bodiesOf(node);
944
+ if (bodies.length > 0) for (const child of childLists(bodies)) rewrite(child);
945
+ }
946
+ };
947
+ rewrite(ast.body);
948
+ };
949
+ //#endregion
950
+ //#region src/parser/parser.ts
951
+ const isTypeName = (name) => TYPE_NAMES.includes(name);
952
+ /** Names that cannot be declared as variables, because Kiln uses them itself. */
953
+ const RESERVED_NAMES = /* @__PURE__ */ new Set([
954
+ "true",
955
+ "false",
956
+ "null",
957
+ "and",
958
+ "or",
959
+ "not",
960
+ "in",
961
+ "is",
962
+ "loop",
963
+ "slots"
964
+ ]);
965
+ /** Parses template source into a syntax tree, collecting every problem instead of stopping at the first. */
966
+ const parse = (text, options) => {
967
+ const lexed = lex(text);
968
+ const parser = new Parser(lexed.tokens, options);
969
+ const ast = parser.parseTemplate(text.length);
970
+ arrangeIndentation(ast, text, lexed, parser.placed);
971
+ return {
972
+ ast,
973
+ problems: [...lexed.problems, ...parser.problems]
974
+ };
975
+ };
976
+ var ParseFailure = class {
977
+ code;
978
+ message;
979
+ span;
980
+ constructor(code, message, span) {
981
+ this.code = code;
982
+ this.message = message;
983
+ this.span = span;
984
+ }
985
+ };
986
+ /** Words that can follow a one-word test, so they never form part of a two-word test name. */
987
+ const NOT_TEST_WORDS = /* @__PURE__ */ new Set([
988
+ "and",
989
+ "or",
990
+ "not",
991
+ "in",
992
+ "is",
993
+ "starts",
994
+ "ends",
995
+ "with",
996
+ "ignore",
997
+ "as"
998
+ ]);
999
+ const COMPARISON_PUNCTUATORS = /* @__PURE__ */ new Set([
1000
+ "==",
1001
+ "!=",
1002
+ "<",
1003
+ ">",
1004
+ "<=",
1005
+ ">="
1006
+ ]);
1007
+ const describeToken = (token) => {
1008
+ switch (token.type) {
1009
+ case "eof": return "the end of the template";
1010
+ case "tagClose": return "\"%}\"";
1011
+ case "outputClose": return "\"}}\"";
1012
+ case "text": return "text";
1013
+ case "string": return "a string";
1014
+ case "stringStart": return "an interpolated string";
1015
+ case "number": return `number ${token.value}`;
1016
+ default: return `"${token.value}"`;
1017
+ }
1018
+ };
1019
+ var Parser = class {
1020
+ problems = [];
1021
+ /** Every tag that inserts content, for `arrangeIndentation`. */
1022
+ placed = [];
1023
+ #tokens;
1024
+ #options;
1025
+ #index = 0;
1026
+ #lastEnd = 0;
1027
+ #nesting = 0;
1028
+ /** End tag names of every open block tag, innermost last. */
1029
+ #endStack = [];
1030
+ constructor(tokens, options) {
1031
+ this.#tokens = tokens;
1032
+ this.#options = options;
1033
+ }
1034
+ parseTemplate(length) {
1035
+ const { body } = this.#parseBody([]);
1036
+ return {
1037
+ type: "Template",
1038
+ body,
1039
+ span: {
1040
+ start: 0,
1041
+ end: length
1042
+ }
1043
+ };
1044
+ }
1045
+ #peek(offset = 0) {
1046
+ return this.#tokens[this.#index + offset] ?? this.#tokens[this.#tokens.length - 1];
1047
+ }
1048
+ #next() {
1049
+ const token = this.#peek();
1050
+ if (token.type !== "eof") this.#index++;
1051
+ this.#lastEnd = token.end;
1052
+ return token;
1053
+ }
1054
+ #isPunct(value, offset = 0) {
1055
+ const token = this.#peek(offset);
1056
+ return token.type === "punct" && token.value === value;
1057
+ }
1058
+ #isName(value, offset = 0) {
1059
+ const token = this.#peek(offset);
1060
+ return token.type === "name" && token.value === value;
1061
+ }
1062
+ #fail(message, token = this.#peek(), code = "unexpected-token") {
1063
+ throw new ParseFailure(code, message, {
1064
+ start: token.start,
1065
+ end: Math.max(token.end, token.start + 1)
1066
+ });
1067
+ }
1068
+ #expectPunct(value) {
1069
+ if (!this.#isPunct(value)) this.#fail(`Expected "${value}" but found ${describeToken(this.#peek())}`);
1070
+ return this.#next();
1071
+ }
1072
+ #expectName(what = "a name") {
1073
+ const token = this.#peek();
1074
+ if (token.type !== "name") this.#fail(`Expected ${what} but found ${describeToken(token)}`);
1075
+ return this.#next();
1076
+ }
1077
+ #expectKeyword(value) {
1078
+ if (!this.#isName(value)) this.#fail(`Expected "${value}" but found ${describeToken(this.#peek())}`);
1079
+ return this.#next();
1080
+ }
1081
+ #expectTagEnd() {
1082
+ const token = this.#peek();
1083
+ if (token.type !== "tagClose") this.#fail(`Expected "%}" but found ${describeToken(token)}`);
1084
+ return this.#next();
1085
+ }
1086
+ #expectDeclarableName(what) {
1087
+ const token = this.#expectName(what);
1088
+ if (RESERVED_NAMES.has(token.value)) this.#problem("reserved-name", `"${token.value}" is reserved and cannot be used as a name`, token);
1089
+ return token;
1090
+ }
1091
+ #problem(code, message, span) {
1092
+ this.problems.push({
1093
+ code,
1094
+ message,
1095
+ span: {
1096
+ start: span.start,
1097
+ end: span.end
1098
+ }
1099
+ });
1100
+ }
1101
+ /** Skips to just after the end of the current tag or output. */
1102
+ #recover() {
1103
+ for (;;) {
1104
+ const token = this.#peek();
1105
+ if (token.type === "eof") return;
1106
+ if (token.type === "tagClose" || token.type === "outputClose") {
1107
+ this.#next();
1108
+ return;
1109
+ }
1110
+ if (token.type === "text" || token.type === "tagOpen" || token.type === "outputOpen") return;
1111
+ this.#next();
1112
+ }
1113
+ }
1114
+ #record(failure) {
1115
+ if (!(failure instanceof ParseFailure)) throw failure;
1116
+ this.#problem(failure.code, failure.message, failure.span);
1117
+ this.#recover();
1118
+ }
1119
+ /**
1120
+ * Parses a block tag's header and its `%}`. If that fails, records the problem and skips to the end of the tag, so the body still parses.
1121
+ */
1122
+ #header(parse, fallback) {
1123
+ try {
1124
+ const result = parse();
1125
+ this.#expectTagEnd();
1126
+ return result;
1127
+ } catch (failure) {
1128
+ this.#record(failure);
1129
+ return fallback;
1130
+ }
1131
+ }
1132
+ /** Whether the current tag holds the punctuator `value` before its `%}`. */
1133
+ #tagHasPunct(value) {
1134
+ for (let offset = 0;; offset++) {
1135
+ const token = this.#peek(offset);
1136
+ if (token.type === "punct" && token.value === value) return true;
1137
+ if (token.type === "tagClose" || token.type === "eof" || token.type === "text" || token.type === "tagOpen" || token.type === "outputOpen") return false;
1138
+ }
1139
+ }
1140
+ #invalid() {
1141
+ const token = this.#peek();
1142
+ return {
1143
+ type: "Invalid",
1144
+ span: {
1145
+ start: token.start,
1146
+ end: token.end
1147
+ }
1148
+ };
1149
+ }
1150
+ #parseBody(ends) {
1151
+ this.#enter();
1152
+ const body = [];
1153
+ this.#endStack.push(ends);
1154
+ try {
1155
+ for (;;) {
1156
+ const token = this.#peek();
1157
+ switch (token.type) {
1158
+ case "eof": return {
1159
+ body,
1160
+ end: null
1161
+ };
1162
+ case "text": {
1163
+ this.#next();
1164
+ const previous = body[body.length - 1];
1165
+ if (previous?.type === "Text") {
1166
+ previous.value += token.value;
1167
+ previous.span = {
1168
+ start: previous.span.start,
1169
+ end: token.end
1170
+ };
1171
+ } else body.push({
1172
+ type: "Text",
1173
+ value: token.value,
1174
+ span: {
1175
+ start: token.start,
1176
+ end: token.end
1177
+ }
1178
+ });
1179
+ break;
1180
+ }
1181
+ case "outputOpen":
1182
+ this.#parseOutput(body);
1183
+ break;
1184
+ case "tagOpen": {
1185
+ const name = this.#peek(1);
1186
+ if (name.type === "name") {
1187
+ if (ends.includes(name.value)) {
1188
+ this.#next();
1189
+ this.#next();
1190
+ return {
1191
+ body,
1192
+ end: name
1193
+ };
1194
+ }
1195
+ if (this.#endStack.some((outer) => outer.includes(name.value))) return {
1196
+ body,
1197
+ end: null
1198
+ };
1199
+ }
1200
+ this.#parseTag(body);
1201
+ break;
1202
+ }
1203
+ default: this.#next();
1204
+ }
1205
+ }
1206
+ } finally {
1207
+ this.#endStack.pop();
1208
+ this.#nesting--;
1209
+ }
1210
+ }
1211
+ #parseOutput(body) {
1212
+ const open = this.#next();
1213
+ try {
1214
+ const expression = this.#parseExpression();
1215
+ const close = this.#peek();
1216
+ if (close.type !== "outputClose") this.#fail(`Expected "}}" but found ${describeToken(close)}`);
1217
+ this.#next();
1218
+ body.push({
1219
+ type: "Output",
1220
+ expression,
1221
+ span: {
1222
+ start: open.start,
1223
+ end: close.end
1224
+ }
1225
+ });
1226
+ } catch (failure) {
1227
+ this.#record(failure);
1228
+ }
1229
+ }
1230
+ #parseTag(body) {
1231
+ const open = this.#next();
1232
+ const nameToken = this.#peek();
1233
+ if (nameToken.type !== "name") {
1234
+ this.#problem("unexpected-token", `Expected a tag name but found ${describeToken(nameToken)}`, nameToken);
1235
+ this.#recover();
1236
+ return;
1237
+ }
1238
+ this.#next();
1239
+ const name = nameToken.value;
1240
+ try {
1241
+ const statement = this.#parseNamedTag(name, open, nameToken);
1242
+ if (statement) body.push(statement);
1243
+ } catch (failure) {
1244
+ this.#record(failure);
1245
+ }
1246
+ }
1247
+ #parseNamedTag(name, open, nameToken) {
1248
+ switch (name) {
1249
+ case "if": return this.#parseIf(open);
1250
+ case "for": return this.#parseFor(open);
1251
+ case "set": return this.#parseSet(open);
1252
+ case "props": return this.#parseProps(open);
1253
+ case "extends": {
1254
+ const template = this.#parseExpression();
1255
+ const close = this.#expectTagEnd();
1256
+ return {
1257
+ type: "Extends",
1258
+ template,
1259
+ span: {
1260
+ start: open.start,
1261
+ end: close.end
1262
+ }
1263
+ };
1264
+ }
1265
+ case "block": return this.#parseBlock(open);
1266
+ case "include": return this.#parseInclude(open);
1267
+ case "component": return this.#parseComponent(open);
1268
+ case "slot": return this.#parseSlot(open);
1269
+ case "macro": return this.#parseMacro(open);
1270
+ case "import": return this.#parseImport(open);
1271
+ case "from": return this.#parseFromImport(open);
1272
+ case "with": return this.#parseWith(open);
1273
+ case "cache": return this.#parseCache(open);
1274
+ case "island": return this.#parseIsland(open);
1275
+ case "outlet": {
1276
+ const name = this.#parseStringLiteral("an outlet name");
1277
+ const close = this.#expectTagEnd();
1278
+ return this.#placed({
1279
+ type: "Outlet",
1280
+ name,
1281
+ span: {
1282
+ start: open.start,
1283
+ end: close.end
1284
+ }
1285
+ });
1286
+ }
1287
+ case "flush": {
1288
+ const close = this.#expectTagEnd();
1289
+ return {
1290
+ type: "Flush",
1291
+ span: {
1292
+ start: open.start,
1293
+ end: close.end
1294
+ }
1295
+ };
1296
+ }
1297
+ case "add": return this.#parseAdd(open);
1298
+ case "apply": return this.#parseApply(open);
1299
+ case "autoescape": return this.#parseAutoescape(open);
1300
+ case "verbatim": return this.#parseVerbatim(open);
1301
+ default: {
1302
+ const stray = name.startsWith("end") || name === "else" || name === "elseif" || name === "fill";
1303
+ this.#problem(stray ? "unexpected-tag" : "unknown-tag", stray ? `"${name}" has no matching opening tag here` : `Unknown tag "${name}"`, nameToken);
1304
+ this.#recover();
1305
+ return null;
1306
+ }
1307
+ }
1308
+ }
1309
+ /** Reports a block tag whose end tag never came. */
1310
+ #unclosed(name, open, endName) {
1311
+ this.#problem("unclosed-block", `"${name}" is missing its "${endName}" tag`, {
1312
+ start: open.start,
1313
+ end: open.end
1314
+ });
1315
+ }
1316
+ /** Consumes an end tag's `%}`, plus an optional repeated name such as `endblock content`. */
1317
+ #finishEndTag(expectedName) {
1318
+ try {
1319
+ if (expectedName !== void 0 && this.#peek().type === "name") {
1320
+ const repeated = this.#next();
1321
+ if (repeated.value !== expectedName) this.#problem("mismatched-end", `End tag names "${repeated.value}" but closes "${expectedName}"`, repeated);
1322
+ }
1323
+ this.#expectTagEnd();
1324
+ } catch (failure) {
1325
+ this.#record(failure);
1326
+ }
1327
+ }
1328
+ #parseIf(open) {
1329
+ const branches = [];
1330
+ let test = this.#header(() => this.#parseExpression(), this.#invalid());
1331
+ let alternate = null;
1332
+ for (;;) {
1333
+ const { body, end } = this.#parseBody([
1334
+ "elseif",
1335
+ "else",
1336
+ "endif"
1337
+ ]);
1338
+ branches.push({
1339
+ test,
1340
+ body
1341
+ });
1342
+ if (!end) {
1343
+ this.#unclosed("if", open, "endif");
1344
+ break;
1345
+ }
1346
+ if (end.value === "elseif") {
1347
+ test = this.#header(() => this.#parseExpression(), this.#invalid());
1348
+ continue;
1349
+ }
1350
+ if (end.value === "else") {
1351
+ this.#finishEndTag();
1352
+ alternate = this.#parseAlternate("if", open, ["elseif", "else"]);
1353
+ break;
1354
+ }
1355
+ this.#finishEndTag();
1356
+ break;
1357
+ }
1358
+ return {
1359
+ type: "If",
1360
+ branches,
1361
+ alternate,
1362
+ span: {
1363
+ start: open.start,
1364
+ end: this.#lastEnd
1365
+ }
1366
+ };
1367
+ }
1368
+ /**
1369
+ * Parses the `else` branch of `if` or `for` up to its end tag. A branch tag after `else`, such as a second `else`,
1370
+ * is reported where it stands, so it can't close an outer tag instead.
1371
+ */
1372
+ #parseAlternate(name, open, branches) {
1373
+ const endName = `end${name}`;
1374
+ const alternate = [];
1375
+ for (;;) {
1376
+ const { body, end } = this.#parseBody([endName, ...branches]);
1377
+ alternate.push(...body);
1378
+ if (!end) {
1379
+ this.#unclosed(name, open, endName);
1380
+ return alternate;
1381
+ }
1382
+ if (end.value === endName) {
1383
+ this.#finishEndTag();
1384
+ return alternate;
1385
+ }
1386
+ this.#problem("unexpected-tag", `"${end.value}" cannot follow "else" in "${name}"`, end);
1387
+ this.#recover();
1388
+ }
1389
+ }
1390
+ #parseFor(open) {
1391
+ const header = this.#header(() => {
1392
+ let keyName = null;
1393
+ let valueName = this.#expectDeclarableName("a loop variable").value;
1394
+ if (this.#isPunct(",")) {
1395
+ this.#next();
1396
+ keyName = valueName;
1397
+ valueName = this.#expectDeclarableName("a loop variable").value;
1398
+ }
1399
+ this.#expectKeyword("in");
1400
+ const iterable = this.#parseExpression();
1401
+ return {
1402
+ keyName,
1403
+ valueName,
1404
+ iterable
1405
+ };
1406
+ }, {
1407
+ keyName: null,
1408
+ valueName: "_",
1409
+ iterable: this.#invalid()
1410
+ });
1411
+ const { body, end } = this.#parseBody(["else", "endfor"]);
1412
+ let alternate = null;
1413
+ if (!end) this.#unclosed("for", open, "endfor");
1414
+ else if (end.value === "else") {
1415
+ this.#finishEndTag();
1416
+ alternate = this.#parseAlternate("for", open, ["else"]);
1417
+ } else this.#finishEndTag();
1418
+ return {
1419
+ type: "For",
1420
+ ...header,
1421
+ body,
1422
+ alternate,
1423
+ span: {
1424
+ start: open.start,
1425
+ end: this.#lastEnd
1426
+ }
1427
+ };
1428
+ }
1429
+ #parseSet(open) {
1430
+ if (this.#tagHasPunct("=")) {
1431
+ const name = this.#expectDeclarableName("a variable name").value;
1432
+ this.#expectPunct("=");
1433
+ const value = this.#parseExpression();
1434
+ const close = this.#expectTagEnd();
1435
+ return {
1436
+ type: "Set",
1437
+ name,
1438
+ value,
1439
+ span: {
1440
+ start: open.start,
1441
+ end: close.end
1442
+ }
1443
+ };
1444
+ }
1445
+ const name = this.#header(() => this.#expectDeclarableName("a variable name").value, "");
1446
+ const { body, end } = this.#parseBody(["endset"]);
1447
+ if (end) this.#finishEndTag();
1448
+ else this.#unclosed("set", open, "endset");
1449
+ return {
1450
+ type: "SetBlock",
1451
+ name,
1452
+ body,
1453
+ span: {
1454
+ start: open.start,
1455
+ end: this.#lastEnd
1456
+ }
1457
+ };
1458
+ }
1459
+ /** Reads parameters for `macro` and `props`. Only `props` may declare types, so macros' `type` is always null. */
1460
+ #parseParams(closing, typed = false) {
1461
+ const params = [];
1462
+ const done = () => closing === null ? this.#peek().type === "tagClose" || this.#isPunct("...") : this.#isPunct(closing);
1463
+ while (!done()) {
1464
+ const nameToken = this.#expectDeclarableName("a parameter name");
1465
+ if (params.some((param) => param.name === nameToken.value)) this.#problem("duplicate-parameter", `"${nameToken.value}" is declared more than once`, nameToken);
1466
+ let type = null;
1467
+ if (typed && this.#isPunct(":")) {
1468
+ this.#next();
1469
+ type = this.#parsePropType();
1470
+ }
1471
+ let defaultValue = null;
1472
+ if (this.#isPunct("=")) {
1473
+ this.#next();
1474
+ defaultValue = this.#parseExpression();
1475
+ }
1476
+ params.push({
1477
+ name: nameToken.value,
1478
+ type,
1479
+ default: defaultValue,
1480
+ span: {
1481
+ start: nameToken.start,
1482
+ end: this.#lastEnd
1483
+ }
1484
+ });
1485
+ if (!this.#isPunct(",")) break;
1486
+ this.#next();
1487
+ }
1488
+ return params;
1489
+ }
1490
+ /** Reads a prop type such as `string | null`, `"primary" | "secondary"`, `Post[]` or `{ name: string }`. */
1491
+ #parsePropType() {
1492
+ this.#enter();
1493
+ const nesting = this.#nesting;
1494
+ try {
1495
+ const members = [];
1496
+ for (;;) {
1497
+ let type = this.#parsePropTypeTerm();
1498
+ while (this.#isPunct("[") && this.#isPunct("]", 1)) {
1499
+ this.#enter();
1500
+ this.#next();
1501
+ this.#next();
1502
+ type = [{ list: type }];
1503
+ }
1504
+ this.#nesting = nesting;
1505
+ members.push(...type);
1506
+ if (!this.#isPunct("|")) return members;
1507
+ this.#next();
1508
+ }
1509
+ } finally {
1510
+ this.#nesting = nesting - 1;
1511
+ }
1512
+ }
1513
+ /** Reads one part of a union. Brackets return the union inside them, so `(2 | 3)[]` lists either. */
1514
+ #parsePropTypeTerm() {
1515
+ const token = this.#peek();
1516
+ if (token.type === "punct" && token.value === "(") {
1517
+ this.#next();
1518
+ const inner = this.#parsePropType();
1519
+ this.#expectPunct(")");
1520
+ return inner;
1521
+ }
1522
+ if (token.type === "punct" && token.value === "{") return [{ shape: this.#parseShape() }];
1523
+ this.#next();
1524
+ if (token.type === "string") return [{ literal: token.value }];
1525
+ if (token.type === "number") return [{ literal: Number(token.value) }];
1526
+ if (token.type === "name" && isTypeName(token.value)) return [token.value];
1527
+ if (token.type === "name" && /^[A-Z]/.test(token.value)) return [{ named: token.value }];
1528
+ if (token.type === "name") {
1529
+ this.#problem("unknown-type", `Unknown type "${token.value}"`, token);
1530
+ return [];
1531
+ }
1532
+ return this.#fail(`Expected a type but found ${describeToken(token)}`, token);
1533
+ }
1534
+ /** Reads a shape type such as `{ name: string, url?: string }`. */
1535
+ #parseShape() {
1536
+ this.#expectPunct("{");
1537
+ const keys = [];
1538
+ while (!this.#isPunct("}")) {
1539
+ const token = this.#next();
1540
+ if (token.type !== "name" && token.type !== "string") this.#fail(`Expected a key but found ${describeToken(token)}`, token);
1541
+ if (keys.some((key) => key.name === token.value)) this.#problem("duplicate-key", `"${token.value}" is declared more than once`, token);
1542
+ const optional = this.#isPunct("?:") || this.#isPunct("?");
1543
+ if (this.#isPunct("?:")) this.#next();
1544
+ else {
1545
+ if (optional) this.#next();
1546
+ this.#expectPunct(":");
1547
+ }
1548
+ keys.push({
1549
+ name: token.value,
1550
+ optional,
1551
+ type: this.#parsePropType()
1552
+ });
1553
+ if (!this.#isPunct(",")) break;
1554
+ this.#next();
1555
+ }
1556
+ this.#expectPunct("}");
1557
+ return keys;
1558
+ }
1559
+ #parseProps(open) {
1560
+ const props = this.#parseParams(null, true);
1561
+ let rest = null;
1562
+ if (this.#isPunct("...")) {
1563
+ this.#next();
1564
+ const nameToken = this.#expectDeclarableName("a rest prop name");
1565
+ if (props.some((prop) => prop.name === nameToken.value)) this.#problem("duplicate-parameter", `"${nameToken.value}" is declared more than once`, nameToken);
1566
+ rest = nameToken.value;
1567
+ if (this.#isPunct(",")) this.#next();
1568
+ if (this.#peek().type !== "tagClose") this.#fail("The rest prop must come last, with no type or default");
1569
+ }
1570
+ const close = this.#expectTagEnd();
1571
+ return {
1572
+ type: "Props",
1573
+ props,
1574
+ rest,
1575
+ span: {
1576
+ start: open.start,
1577
+ end: close.end
1578
+ }
1579
+ };
1580
+ }
1581
+ #parseBlock(open) {
1582
+ const first = this.#peek();
1583
+ if (first.type !== "name" && (first.type === "tagClose" || this.#peek(1).type === "tagClose")) {
1584
+ this.#header(() => {
1585
+ this.#expectName("a block name");
1586
+ }, void 0);
1587
+ const { end } = this.#parseBody(["endblock"]);
1588
+ if (end) this.#finishEndTag();
1589
+ else this.#unclosed("block", open, "endblock");
1590
+ return null;
1591
+ }
1592
+ const name = this.#expectName("a block name").value;
1593
+ if (this.#peek().type !== "tagClose") {
1594
+ const expression = this.#parseExpression();
1595
+ const close = this.#expectTagEnd();
1596
+ return this.#placed({
1597
+ type: "Block",
1598
+ name,
1599
+ body: [{
1600
+ type: "Output",
1601
+ expression,
1602
+ span: {
1603
+ start: expression.span.start,
1604
+ end: expression.span.end
1605
+ }
1606
+ }],
1607
+ span: {
1608
+ start: open.start,
1609
+ end: close.end
1610
+ }
1611
+ });
1612
+ }
1613
+ this.#expectTagEnd();
1614
+ const { body, end } = this.#parseBody(["endblock"]);
1615
+ if (end) this.#finishEndTag(name);
1616
+ else this.#unclosed("block", open, "endblock");
1617
+ return this.#placed({
1618
+ type: "Block",
1619
+ name,
1620
+ body,
1621
+ span: {
1622
+ start: open.start,
1623
+ end: this.#lastEnd
1624
+ }
1625
+ });
1626
+ }
1627
+ #parseInclude(open) {
1628
+ const template = this.#parseExpression();
1629
+ let withValue = null;
1630
+ let ignoreMissing = false;
1631
+ if (this.#isName("with")) {
1632
+ this.#next();
1633
+ withValue = this.#parseExpression();
1634
+ }
1635
+ if (this.#isName("ignore")) {
1636
+ this.#next();
1637
+ this.#expectKeyword("missing");
1638
+ ignoreMissing = true;
1639
+ }
1640
+ const close = this.#expectTagEnd();
1641
+ return this.#placed({
1642
+ type: "Include",
1643
+ template,
1644
+ with: withValue,
1645
+ ignoreMissing,
1646
+ span: {
1647
+ start: open.start,
1648
+ end: close.end
1649
+ }
1650
+ });
1651
+ }
1652
+ #parseComponent(open) {
1653
+ const header = this.#header(() => {
1654
+ const template = this.#parseExpression();
1655
+ let withValue = null;
1656
+ if (this.#isName("with")) {
1657
+ this.#next();
1658
+ withValue = this.#parseExpression();
1659
+ }
1660
+ return {
1661
+ template,
1662
+ with: withValue
1663
+ };
1664
+ }, {
1665
+ template: this.#invalid(),
1666
+ with: null
1667
+ });
1668
+ const body = [];
1669
+ const fills = [];
1670
+ for (;;) {
1671
+ const part = this.#parseBody(["fill", "endcomponent"]);
1672
+ body.push(...part.body);
1673
+ if (!part.end) {
1674
+ this.#unclosed("component", open, "endcomponent");
1675
+ break;
1676
+ }
1677
+ if (part.end.value === "endcomponent") {
1678
+ this.#finishEndTag();
1679
+ break;
1680
+ }
1681
+ const fillStart = this.#tokens[this.#index - 2] ?? part.end;
1682
+ const name = this.#header(() => this.#parseStringLiteral("a slot name"), "");
1683
+ const fill = this.#parseBody(["endfill"]);
1684
+ if (fill.end) this.#finishEndTag();
1685
+ else this.#unclosed("fill", fillStart, "endfill");
1686
+ if (fills.some((existing) => existing.name === name)) this.#problem("duplicate-fill", `Slot "${name}" is filled more than once`, {
1687
+ start: fillStart.start,
1688
+ end: fillStart.end
1689
+ });
1690
+ fills.push({
1691
+ name,
1692
+ body: fill.body,
1693
+ span: {
1694
+ start: fillStart.start,
1695
+ end: this.#lastEnd
1696
+ }
1697
+ });
1698
+ }
1699
+ return this.#placed({
1700
+ type: "Component",
1701
+ ...header,
1702
+ fills,
1703
+ body,
1704
+ span: {
1705
+ start: open.start,
1706
+ end: this.#lastEnd
1707
+ }
1708
+ });
1709
+ }
1710
+ #placed(node) {
1711
+ this.placed.push(node);
1712
+ return node;
1713
+ }
1714
+ #parseStringLiteral(what) {
1715
+ const token = this.#peek();
1716
+ if (token.type !== "string") this.#fail(`Expected ${what} as a string but found ${describeToken(token)}`);
1717
+ this.#next();
1718
+ return token.value;
1719
+ }
1720
+ #parseSlot(open) {
1721
+ const name = this.#header(() => this.#peek().type === "tagClose" ? "default" : this.#parseStringLiteral("a slot name"), "default");
1722
+ const { body, end } = this.#parseBody(["endslot"]);
1723
+ if (end) this.#finishEndTag();
1724
+ else this.#unclosed("slot", open, "endslot");
1725
+ return this.#placed({
1726
+ type: "Slot",
1727
+ name,
1728
+ body,
1729
+ span: {
1730
+ start: open.start,
1731
+ end: this.#lastEnd
1732
+ }
1733
+ });
1734
+ }
1735
+ #parseMacro(open) {
1736
+ const header = this.#header(() => {
1737
+ const name = this.#expectDeclarableName("a macro name").value;
1738
+ this.#expectPunct("(");
1739
+ const params = this.#parseParams(")");
1740
+ this.#expectPunct(")");
1741
+ return {
1742
+ name,
1743
+ params
1744
+ };
1745
+ }, {
1746
+ name: "",
1747
+ params: []
1748
+ });
1749
+ const { body, end } = this.#parseBody(["endmacro"]);
1750
+ if (end) this.#finishEndTag(header.name);
1751
+ else this.#unclosed("macro", open, "endmacro");
1752
+ return {
1753
+ type: "Macro",
1754
+ ...header,
1755
+ body,
1756
+ span: {
1757
+ start: open.start,
1758
+ end: this.#lastEnd
1759
+ }
1760
+ };
1761
+ }
1762
+ #parseImport(open) {
1763
+ const template = this.#parseStringLiteral("a template name");
1764
+ this.#expectKeyword("as");
1765
+ const alias = this.#expectDeclarableName("an alias").value;
1766
+ const close = this.#expectTagEnd();
1767
+ return {
1768
+ type: "Import",
1769
+ template,
1770
+ alias,
1771
+ span: {
1772
+ start: open.start,
1773
+ end: close.end
1774
+ }
1775
+ };
1776
+ }
1777
+ #parseFromImport(open) {
1778
+ const template = this.#parseStringLiteral("a template name");
1779
+ this.#expectKeyword("import");
1780
+ const names = [];
1781
+ do {
1782
+ if (names.length > 0) this.#next();
1783
+ const nameToken = this.#expectName("a macro name");
1784
+ let alias = nameToken.value;
1785
+ if (this.#isName("as")) {
1786
+ this.#next();
1787
+ alias = this.#expectDeclarableName("an alias").value;
1788
+ }
1789
+ names.push({
1790
+ name: nameToken.value,
1791
+ alias,
1792
+ span: {
1793
+ start: nameToken.start,
1794
+ end: this.#lastEnd
1795
+ }
1796
+ });
1797
+ } while (this.#isPunct(","));
1798
+ const close = this.#expectTagEnd();
1799
+ return {
1800
+ type: "FromImport",
1801
+ template,
1802
+ names,
1803
+ span: {
1804
+ start: open.start,
1805
+ end: close.end
1806
+ }
1807
+ };
1808
+ }
1809
+ #parseWith(open) {
1810
+ const value = this.#header(() => this.#parseExpression(), this.#invalid());
1811
+ const { body, end } = this.#parseBody(["endwith"]);
1812
+ if (end) this.#finishEndTag();
1813
+ else this.#unclosed("with", open, "endwith");
1814
+ return {
1815
+ type: "With",
1816
+ value,
1817
+ body,
1818
+ span: {
1819
+ start: open.start,
1820
+ end: this.#lastEnd
1821
+ }
1822
+ };
1823
+ }
1824
+ #parseAdd(open) {
1825
+ const { outlet, once } = this.#header(() => {
1826
+ this.#expectKeyword("to");
1827
+ const outlet = this.#parseStringLiteral("an outlet name");
1828
+ const once = this.#isName("once");
1829
+ if (once) this.#next();
1830
+ return {
1831
+ outlet,
1832
+ once
1833
+ };
1834
+ }, {
1835
+ outlet: "",
1836
+ once: false
1837
+ });
1838
+ const { body, end } = this.#parseBody(["endadd"]);
1839
+ if (end) this.#finishEndTag();
1840
+ else this.#unclosed("add", open, "endadd");
1841
+ return {
1842
+ type: "Add",
1843
+ outlet,
1844
+ once,
1845
+ body,
1846
+ span: {
1847
+ start: open.start,
1848
+ end: this.#lastEnd
1849
+ }
1850
+ };
1851
+ }
1852
+ #parseCache(open) {
1853
+ const { key, seconds } = this.#header(() => {
1854
+ const key = this.#parseExpression();
1855
+ let seconds = null;
1856
+ if (this.#isName("for")) {
1857
+ this.#next();
1858
+ seconds = this.#parseExpression();
1859
+ }
1860
+ return {
1861
+ key,
1862
+ seconds
1863
+ };
1864
+ }, {
1865
+ key: this.#invalid(),
1866
+ seconds: null
1867
+ });
1868
+ const { body, end } = this.#parseBody(["endcache"]);
1869
+ if (end) this.#finishEndTag();
1870
+ else this.#unclosed("cache", open, "endcache");
1871
+ return {
1872
+ type: "Cache",
1873
+ key,
1874
+ seconds,
1875
+ body,
1876
+ span: {
1877
+ start: open.start,
1878
+ end: this.#lastEnd
1879
+ }
1880
+ };
1881
+ }
1882
+ #parseIsland(open) {
1883
+ const header = this.#header(() => {
1884
+ const template = this.#parseExpression();
1885
+ let withValue = null;
1886
+ if (this.#isName("with")) {
1887
+ this.#next();
1888
+ withValue = this.#parseExpression();
1889
+ }
1890
+ return {
1891
+ template,
1892
+ with: withValue
1893
+ };
1894
+ }, {
1895
+ template: this.#invalid(),
1896
+ with: null
1897
+ });
1898
+ const { body, end } = this.#parseBody(["endisland"]);
1899
+ if (end) this.#finishEndTag();
1900
+ else this.#unclosed("island", open, "endisland");
1901
+ return this.#placed({
1902
+ type: "Island",
1903
+ ...header,
1904
+ body,
1905
+ span: {
1906
+ start: open.start,
1907
+ end: this.#lastEnd
1908
+ }
1909
+ });
1910
+ }
1911
+ #parseApply(open) {
1912
+ const filters = this.#header(() => {
1913
+ const list = [];
1914
+ do {
1915
+ if (list.length > 0) this.#next();
1916
+ const nameToken = this.#expectName("a filter name");
1917
+ const args = this.#isPunct("(") ? this.#parseArguments() : [];
1918
+ list.push({
1919
+ name: nameToken.value,
1920
+ args,
1921
+ span: {
1922
+ start: nameToken.start,
1923
+ end: this.#lastEnd
1924
+ }
1925
+ });
1926
+ } while (this.#isPunct("|"));
1927
+ return list;
1928
+ }, []);
1929
+ const { body, end } = this.#parseBody(["endapply"]);
1930
+ if (end) this.#finishEndTag();
1931
+ else this.#unclosed("apply", open, "endapply");
1932
+ return {
1933
+ type: "Apply",
1934
+ filters,
1935
+ body,
1936
+ span: {
1937
+ start: open.start,
1938
+ end: this.#lastEnd
1939
+ }
1940
+ };
1941
+ }
1942
+ #parseAutoescape(open) {
1943
+ const strategy = this.#header(() => {
1944
+ const token = this.#peek();
1945
+ if (token.type === "tagClose") return "html";
1946
+ if (token.type === "name" && token.value === "false") {
1947
+ this.#next();
1948
+ return false;
1949
+ }
1950
+ const value = this.#parseStringLiteral("an escaping strategy");
1951
+ if (!ESCAPE_STRATEGIES.includes(value)) this.#fail(`Unknown escaping strategy "${value}". Use ${ESCAPE_STRATEGIES.map((s) => `"${s}"`).join(", ")} or false`, token, "unknown-strategy");
1952
+ return value;
1953
+ }, "html");
1954
+ const { body, end } = this.#parseBody(["endautoescape"]);
1955
+ if (end) this.#finishEndTag();
1956
+ else this.#unclosed("autoescape", open, "endautoescape");
1957
+ return {
1958
+ type: "Autoescape",
1959
+ strategy,
1960
+ body,
1961
+ span: {
1962
+ start: open.start,
1963
+ end: this.#lastEnd
1964
+ }
1965
+ };
1966
+ }
1967
+ #parseVerbatim(open) {
1968
+ this.#expectTagEnd();
1969
+ let value = "";
1970
+ let start = this.#lastEnd;
1971
+ let end = start;
1972
+ const token = this.#peek();
1973
+ if (token.type === "text") {
1974
+ this.#next();
1975
+ value = token.value;
1976
+ start = token.start;
1977
+ end = token.end;
1978
+ }
1979
+ if (this.#peek().type === "tagOpen" && this.#isName("endverbatim", 1)) {
1980
+ this.#next();
1981
+ this.#next();
1982
+ this.#finishEndTag();
1983
+ }
1984
+ if (value === "") return null;
1985
+ return {
1986
+ type: "Text",
1987
+ value,
1988
+ span: {
1989
+ start: Math.max(start, open.end),
1990
+ end
1991
+ }
1992
+ };
1993
+ }
1994
+ /** Guards against source nested so deeply that parsing it would overflow the call stack. */
1995
+ #enter() {
1996
+ if (++this.#nesting > 200) {
1997
+ this.#nesting--;
1998
+ this.#fail(`Template is nested more than 200 levels deep`, this.#peek(), "too-deep");
1999
+ }
2000
+ }
2001
+ #parseExpression() {
2002
+ this.#enter();
2003
+ try {
2004
+ return this.#parseConditional();
2005
+ } finally {
2006
+ this.#nesting--;
2007
+ }
2008
+ }
2009
+ #parseConditional() {
2010
+ const test = this.#parseCoalesce();
2011
+ if (this.#isPunct("?")) {
2012
+ this.#next();
2013
+ const consequent = this.#parseExpression();
2014
+ let alternate = null;
2015
+ if (this.#isPunct(":")) {
2016
+ this.#next();
2017
+ alternate = this.#parseExpression();
2018
+ }
2019
+ return {
2020
+ type: "Conditional",
2021
+ test,
2022
+ consequent,
2023
+ alternate,
2024
+ span: {
2025
+ start: test.span.start,
2026
+ end: this.#lastEnd
2027
+ }
2028
+ };
2029
+ }
2030
+ if (this.#isPunct("?:")) {
2031
+ this.#next();
2032
+ const right = this.#parseExpression();
2033
+ return this.#binary("?:", test, right);
2034
+ }
2035
+ return test;
2036
+ }
2037
+ #binary(operator, left, right) {
2038
+ return {
2039
+ type: "Binary",
2040
+ operator,
2041
+ left,
2042
+ right,
2043
+ span: {
2044
+ start: left.span.start,
2045
+ end: right.span.end
2046
+ }
2047
+ };
2048
+ }
2049
+ #parseCoalesce() {
2050
+ const left = this.#parseOr();
2051
+ if (!this.#isPunct("??")) return left;
2052
+ this.#next();
2053
+ this.#enter();
2054
+ try {
2055
+ return this.#binary("??", left, this.#parseCoalesce());
2056
+ } finally {
2057
+ this.#nesting--;
2058
+ }
2059
+ }
2060
+ #parseOr() {
2061
+ let left = this.#parseAnd();
2062
+ while (this.#isName("or")) {
2063
+ this.#next();
2064
+ left = this.#binary("or", left, this.#parseAnd());
2065
+ }
2066
+ return left;
2067
+ }
2068
+ #parseAnd() {
2069
+ let left = this.#parseNot();
2070
+ while (this.#isName("and")) {
2071
+ this.#next();
2072
+ left = this.#binary("and", left, this.#parseNot());
2073
+ }
2074
+ return left;
2075
+ }
2076
+ #parseNot() {
2077
+ this.#enter();
2078
+ try {
2079
+ return this.#parseNotInner();
2080
+ } finally {
2081
+ this.#nesting--;
2082
+ }
2083
+ }
2084
+ #parseNotInner() {
2085
+ if (this.#isName("not")) {
2086
+ const token = this.#next();
2087
+ const operand = this.#parseNot();
2088
+ return {
2089
+ type: "Unary",
2090
+ operator: "not",
2091
+ operand,
2092
+ span: {
2093
+ start: token.start,
2094
+ end: operand.span.end
2095
+ }
2096
+ };
2097
+ }
2098
+ return this.#parseComparison();
2099
+ }
2100
+ /** Reads a comparison operator at the cursor without consuming it, and how many tokens it spans. */
2101
+ #comparisonAhead() {
2102
+ const token = this.#peek();
2103
+ if (token.type === "punct" && COMPARISON_PUNCTUATORS.has(token.value)) return {
2104
+ operator: token.value,
2105
+ length: 1
2106
+ };
2107
+ if (token.type !== "name") return null;
2108
+ switch (token.value) {
2109
+ case "in": return {
2110
+ operator: "in",
2111
+ length: 1
2112
+ };
2113
+ case "is": return {
2114
+ operator: "is",
2115
+ length: 1
2116
+ };
2117
+ case "not": return this.#isName("in", 1) ? {
2118
+ operator: "not in",
2119
+ length: 2
2120
+ } : null;
2121
+ case "starts": return this.#isName("with", 1) ? {
2122
+ operator: "starts with",
2123
+ length: 2
2124
+ } : null;
2125
+ case "ends": return this.#isName("with", 1) ? {
2126
+ operator: "ends with",
2127
+ length: 2
2128
+ } : null;
2129
+ default: return null;
2130
+ }
2131
+ }
2132
+ #parseComparison() {
2133
+ const left = this.#parseRange();
2134
+ const ahead = this.#comparisonAhead();
2135
+ if (!ahead) return left;
2136
+ let result;
2137
+ if (ahead.operator === "is") result = this.#parseTest(left);
2138
+ else {
2139
+ for (let i = 0; i < ahead.length; i++) this.#next();
2140
+ result = this.#binary(ahead.operator, left, this.#parseRange());
2141
+ }
2142
+ if (this.#comparisonAhead()) this.#fail("Comparisons cannot be chained. Join them with \"and\" or \"or\"", this.#peek(), "chained-comparison");
2143
+ return result;
2144
+ }
2145
+ #parseTest(input) {
2146
+ this.#next();
2147
+ let negated = false;
2148
+ if (this.#isName("not")) {
2149
+ this.#next();
2150
+ negated = true;
2151
+ }
2152
+ const first = this.#expectName("a test name");
2153
+ let name = first.value;
2154
+ let nameEnd = first.end;
2155
+ const second = this.#peek();
2156
+ if (second.type === "name" && (this.#options.isTest(`${name} ${second.value}`) || !this.#options.isTest(name) && !NOT_TEST_WORDS.has(second.value))) {
2157
+ this.#next();
2158
+ name = `${name} ${second.value}`;
2159
+ nameEnd = second.end;
2160
+ }
2161
+ const args = this.#isPunct("(") ? this.#parseArguments() : [];
2162
+ return {
2163
+ type: "Test",
2164
+ name,
2165
+ nameSpan: {
2166
+ start: first.start,
2167
+ end: nameEnd
2168
+ },
2169
+ input,
2170
+ args,
2171
+ negated,
2172
+ span: {
2173
+ start: input.span.start,
2174
+ end: this.#lastEnd
2175
+ }
2176
+ };
2177
+ }
2178
+ #parseRange() {
2179
+ const left = this.#parseConcat();
2180
+ if (this.#isPunct("..")) {
2181
+ this.#next();
2182
+ return this.#binary("..", left, this.#parseConcat());
2183
+ }
2184
+ return left;
2185
+ }
2186
+ #parseConcat() {
2187
+ let left = this.#parseAdditive();
2188
+ while (this.#isPunct("~")) {
2189
+ this.#next();
2190
+ left = this.#binary("~", left, this.#parseAdditive());
2191
+ }
2192
+ return left;
2193
+ }
2194
+ #parseAdditive() {
2195
+ let left = this.#parseMultiplicative();
2196
+ while (this.#isPunct("+") || this.#isPunct("-")) {
2197
+ const operator = this.#next().value;
2198
+ left = this.#binary(operator, left, this.#parseMultiplicative());
2199
+ }
2200
+ return left;
2201
+ }
2202
+ #parseMultiplicative() {
2203
+ let left = this.#parseUnary();
2204
+ for (;;) {
2205
+ const token = this.#peek();
2206
+ if (token.type === "punct" && (token.value === "*" || token.value === "/" || token.value === "//" || token.value === "%")) {
2207
+ this.#next();
2208
+ left = this.#binary(token.value, left, this.#parseUnary());
2209
+ } else return left;
2210
+ }
2211
+ }
2212
+ #parseUnary() {
2213
+ this.#enter();
2214
+ try {
2215
+ return this.#parseUnaryInner();
2216
+ } finally {
2217
+ this.#nesting--;
2218
+ }
2219
+ }
2220
+ #parseUnaryInner() {
2221
+ if (this.#isPunct("-") || this.#isPunct("+")) {
2222
+ const token = this.#next();
2223
+ const operand = this.#parseUnary();
2224
+ return {
2225
+ type: "Unary",
2226
+ operator: token.value,
2227
+ operand,
2228
+ span: {
2229
+ start: token.start,
2230
+ end: operand.span.end
2231
+ }
2232
+ };
2233
+ }
2234
+ return this.#parsePower();
2235
+ }
2236
+ #parsePower() {
2237
+ const base = this.#parsePostfix();
2238
+ if (this.#isPunct("**")) {
2239
+ this.#next();
2240
+ return this.#binary("**", base, this.#parseUnary());
2241
+ }
2242
+ return base;
2243
+ }
2244
+ #parsePostfix() {
2245
+ let expression = this.#parsePrimary();
2246
+ let optional = false;
2247
+ const start = expression.span.start;
2248
+ const span = () => ({
2249
+ start,
2250
+ end: this.#lastEnd
2251
+ });
2252
+ const closeChain = () => {
2253
+ if (optional) {
2254
+ expression = {
2255
+ type: "OptionalChain",
2256
+ expression,
2257
+ span: span()
2258
+ };
2259
+ optional = false;
2260
+ }
2261
+ };
2262
+ for (;;) {
2263
+ if (this.#isPunct(".") || this.#isPunct("?.")) {
2264
+ const isOptional = this.#next().value === "?.";
2265
+ if (isOptional) optional = true;
2266
+ if (isOptional && this.#isPunct("[")) {
2267
+ this.#next();
2268
+ const property = this.#parseExpression();
2269
+ this.#expectPunct("]");
2270
+ expression = {
2271
+ type: "Member",
2272
+ object: expression,
2273
+ property,
2274
+ optional: true,
2275
+ span: span()
2276
+ };
2277
+ continue;
2278
+ }
2279
+ const token = this.#peek();
2280
+ if (token.type === "name") {
2281
+ this.#next();
2282
+ expression = {
2283
+ type: "Member",
2284
+ object: expression,
2285
+ property: {
2286
+ type: "Literal",
2287
+ value: token.value,
2288
+ span: {
2289
+ start: token.start,
2290
+ end: token.end
2291
+ }
2292
+ },
2293
+ optional: isOptional,
2294
+ span: span()
2295
+ };
2296
+ continue;
2297
+ }
2298
+ if (token.type === "number" && /^\d+$/.test(token.value)) {
2299
+ this.#next();
2300
+ expression = {
2301
+ type: "Member",
2302
+ object: expression,
2303
+ property: {
2304
+ type: "Literal",
2305
+ value: Number(token.value),
2306
+ span: {
2307
+ start: token.start,
2308
+ end: token.end
2309
+ }
2310
+ },
2311
+ optional: isOptional,
2312
+ span: span()
2313
+ };
2314
+ continue;
2315
+ }
2316
+ this.#fail(`Expected a property name but found ${describeToken(token)}`);
2317
+ }
2318
+ if (this.#isPunct("[")) {
2319
+ this.#next();
2320
+ const property = this.#parseExpression();
2321
+ this.#expectPunct("]");
2322
+ expression = {
2323
+ type: "Member",
2324
+ object: expression,
2325
+ property,
2326
+ optional: false,
2327
+ span: span()
2328
+ };
2329
+ continue;
2330
+ }
2331
+ if (this.#isPunct("(")) {
2332
+ const callee = expression;
2333
+ if (callee.type !== "Name" && callee.type !== "Member") this.#fail("Only named functions, macros and exposed methods can be called", this.#peek(), "invalid-call");
2334
+ expression = {
2335
+ type: "Call",
2336
+ callee,
2337
+ args: this.#parseArguments(),
2338
+ span: span()
2339
+ };
2340
+ continue;
2341
+ }
2342
+ if (this.#isPunct("|")) {
2343
+ closeChain();
2344
+ this.#next();
2345
+ const nameToken = this.#expectName("a filter name");
2346
+ const args = this.#isPunct("(") ? this.#parseArguments() : [];
2347
+ expression = {
2348
+ type: "Filter",
2349
+ name: nameToken.value,
2350
+ nameSpan: {
2351
+ start: nameToken.start,
2352
+ end: nameToken.end
2353
+ },
2354
+ input: expression,
2355
+ args,
2356
+ span: span()
2357
+ };
2358
+ continue;
2359
+ }
2360
+ closeChain();
2361
+ return expression;
2362
+ }
2363
+ }
2364
+ #parsePrimary() {
2365
+ const token = this.#peek();
2366
+ const tokenSpan = {
2367
+ start: token.start,
2368
+ end: token.end
2369
+ };
2370
+ switch (token.type) {
2371
+ case "number":
2372
+ this.#next();
2373
+ return {
2374
+ type: "Literal",
2375
+ value: Number(token.value),
2376
+ span: tokenSpan
2377
+ };
2378
+ case "string":
2379
+ this.#next();
2380
+ return {
2381
+ type: "Literal",
2382
+ value: token.value,
2383
+ span: tokenSpan
2384
+ };
2385
+ case "stringStart": return this.#parseInterpolation();
2386
+ case "name":
2387
+ switch (token.value) {
2388
+ case "true":
2389
+ case "false":
2390
+ this.#next();
2391
+ return {
2392
+ type: "Literal",
2393
+ value: token.value === "true",
2394
+ span: tokenSpan
2395
+ };
2396
+ case "null":
2397
+ this.#next();
2398
+ return {
2399
+ type: "Literal",
2400
+ value: null,
2401
+ span: tokenSpan
2402
+ };
2403
+ case "and":
2404
+ case "or":
2405
+ case "not":
2406
+ case "in":
2407
+ case "is": this.#fail(`Expected a value but found "${token.value}"`);
2408
+ }
2409
+ if (this.#isPunct("=>", 1)) this.#fail("Arrow functions can only be passed as arguments to filters, functions and methods", token, "invalid-arrow");
2410
+ this.#next();
2411
+ return {
2412
+ type: "Name",
2413
+ name: token.value,
2414
+ span: tokenSpan
2415
+ };
2416
+ case "punct": switch (token.value) {
2417
+ case "[": return this.#parseArray();
2418
+ case "{": return this.#parseHash();
2419
+ case "(": {
2420
+ if (this.#arrowAhead()) this.#fail("Arrow functions can only be passed as arguments to filters, functions and methods", token, "invalid-arrow");
2421
+ this.#next();
2422
+ const inner = this.#parseExpression();
2423
+ this.#expectPunct(")");
2424
+ return {
2425
+ ...inner,
2426
+ span: {
2427
+ start: token.start,
2428
+ end: this.#lastEnd
2429
+ }
2430
+ };
2431
+ }
2432
+ }
2433
+ }
2434
+ return this.#fail(`Expected a value but found ${describeToken(token)}`);
2435
+ }
2436
+ #parseInterpolation() {
2437
+ const start = this.#next();
2438
+ const parts = [];
2439
+ for (;;) {
2440
+ const token = this.#peek();
2441
+ if (token.type === "stringEnd") {
2442
+ this.#next();
2443
+ break;
2444
+ }
2445
+ if (token.type === "string") {
2446
+ this.#next();
2447
+ parts.push({
2448
+ type: "Literal",
2449
+ value: token.value,
2450
+ span: {
2451
+ start: token.start,
2452
+ end: token.end
2453
+ }
2454
+ });
2455
+ continue;
2456
+ }
2457
+ if (token.type === "interpOpen") {
2458
+ this.#next();
2459
+ parts.push(this.#parseExpression());
2460
+ const close = this.#peek();
2461
+ if (close.type !== "interpClose") this.#fail(`Expected "}" but found ${describeToken(close)}`);
2462
+ this.#next();
2463
+ continue;
2464
+ }
2465
+ this.#fail(`Unexpected ${describeToken(token)} in string`);
2466
+ }
2467
+ return {
2468
+ type: "Interpolation",
2469
+ parts,
2470
+ span: {
2471
+ start: start.start,
2472
+ end: this.#lastEnd
2473
+ }
2474
+ };
2475
+ }
2476
+ #parseSpread() {
2477
+ const token = this.#next();
2478
+ const value = this.#parseExpression();
2479
+ return {
2480
+ type: "Spread",
2481
+ value,
2482
+ span: {
2483
+ start: token.start,
2484
+ end: value.span.end
2485
+ }
2486
+ };
2487
+ }
2488
+ #parseArray() {
2489
+ const open = this.#next();
2490
+ const elements = [];
2491
+ while (!this.#isPunct("]")) {
2492
+ elements.push(this.#isPunct("...") ? this.#parseSpread() : this.#parseExpression());
2493
+ if (!this.#isPunct(",")) break;
2494
+ this.#next();
2495
+ }
2496
+ this.#expectPunct("]");
2497
+ return {
2498
+ type: "Array",
2499
+ elements,
2500
+ span: {
2501
+ start: open.start,
2502
+ end: this.#lastEnd
2503
+ }
2504
+ };
2505
+ }
2506
+ #parseHash() {
2507
+ const open = this.#next();
2508
+ const entries = [];
2509
+ while (!this.#isPunct("}")) {
2510
+ if (this.#isPunct("...")) entries.push(this.#parseSpread());
2511
+ else {
2512
+ const token = this.#peek();
2513
+ let key;
2514
+ if (token.type === "name") {
2515
+ this.#next();
2516
+ key = {
2517
+ type: "Literal",
2518
+ value: token.value,
2519
+ span: {
2520
+ start: token.start,
2521
+ end: token.end
2522
+ }
2523
+ };
2524
+ if (!this.#isPunct(":")) {
2525
+ if (RESERVED_NAMES.has(token.value) && token.value !== "loop" && token.value !== "slots") this.#fail(`Expected ":" after "${token.value}"`);
2526
+ entries.push({
2527
+ type: "Property",
2528
+ key,
2529
+ value: {
2530
+ type: "Name",
2531
+ name: token.value,
2532
+ span: {
2533
+ start: token.start,
2534
+ end: token.end
2535
+ }
2536
+ },
2537
+ span: {
2538
+ start: token.start,
2539
+ end: token.end
2540
+ }
2541
+ });
2542
+ if (!this.#isPunct(",")) break;
2543
+ this.#next();
2544
+ continue;
2545
+ }
2546
+ } else if (token.type === "string" || token.type === "number") {
2547
+ this.#next();
2548
+ key = {
2549
+ type: "Literal",
2550
+ value: token.type === "number" ? String(Number(token.value)) : token.value,
2551
+ span: {
2552
+ start: token.start,
2553
+ end: token.end
2554
+ }
2555
+ };
2556
+ } else if (this.#isPunct("(")) {
2557
+ this.#next();
2558
+ key = this.#parseExpression();
2559
+ this.#expectPunct(")");
2560
+ } else this.#fail(`Expected a hash key but found ${describeToken(token)}`);
2561
+ this.#expectPunct(":");
2562
+ const value = this.#parseExpression();
2563
+ entries.push({
2564
+ type: "Property",
2565
+ key,
2566
+ value,
2567
+ span: {
2568
+ start: token.start,
2569
+ end: value.span.end
2570
+ }
2571
+ });
2572
+ }
2573
+ if (!this.#isPunct(",")) break;
2574
+ this.#next();
2575
+ }
2576
+ this.#expectPunct("}");
2577
+ return {
2578
+ type: "Hash",
2579
+ entries,
2580
+ span: {
2581
+ start: open.start,
2582
+ end: this.#lastEnd
2583
+ }
2584
+ };
2585
+ }
2586
+ /** Checks whether the tokens at the cursor start an arrow function such as `(a, b) =>`. */
2587
+ #arrowAhead() {
2588
+ if (this.#peek().type === "name") return this.#isPunct("=>", 1);
2589
+ if (!this.#isPunct("(")) return false;
2590
+ let offset = 1;
2591
+ if (this.#isPunct(")", offset)) return this.#isPunct("=>", offset + 1);
2592
+ for (;;) {
2593
+ if (this.#peek(offset).type !== "name") return false;
2594
+ offset++;
2595
+ if (this.#isPunct(")", offset)) return this.#isPunct("=>", offset + 1);
2596
+ if (!this.#isPunct(",", offset)) return false;
2597
+ offset++;
2598
+ }
2599
+ }
2600
+ #parseArrow() {
2601
+ const start = this.#peek();
2602
+ const params = [];
2603
+ if (start.type === "name") params.push(this.#expectDeclarableName("a parameter name").value);
2604
+ else {
2605
+ this.#next();
2606
+ while (!this.#isPunct(")")) {
2607
+ params.push(this.#expectDeclarableName("a parameter name").value);
2608
+ if (!this.#isPunct(",")) break;
2609
+ this.#next();
2610
+ }
2611
+ this.#expectPunct(")");
2612
+ }
2613
+ this.#expectPunct("=>");
2614
+ const body = this.#parseExpression();
2615
+ return {
2616
+ type: "Arrow",
2617
+ params,
2618
+ body,
2619
+ span: {
2620
+ start: start.start,
2621
+ end: body.span.end
2622
+ }
2623
+ };
2624
+ }
2625
+ #parseArguments() {
2626
+ this.#expectPunct("(");
2627
+ const args = [];
2628
+ let named = false;
2629
+ while (!this.#isPunct(")")) {
2630
+ const token = this.#peek();
2631
+ if (this.#isPunct("...")) {
2632
+ if (named) this.#fail("Positional arguments cannot follow named arguments", this.#peek(), "positional-after-named");
2633
+ args.push(this.#parseSpread());
2634
+ } else if (token.type === "name" && this.#isPunct(":", 1)) {
2635
+ this.#next();
2636
+ this.#next();
2637
+ named = true;
2638
+ const value = this.#arrowAhead() ? this.#parseArrow() : this.#parseExpression();
2639
+ args.push({
2640
+ type: "Named",
2641
+ name: token.value,
2642
+ value,
2643
+ span: {
2644
+ start: token.start,
2645
+ end: value.span.end
2646
+ }
2647
+ });
2648
+ } else {
2649
+ if (named) this.#fail("Positional arguments cannot follow named arguments", token, "positional-after-named");
2650
+ const value = this.#arrowAhead() ? this.#parseArrow() : this.#parseExpression();
2651
+ args.push({
2652
+ type: "Positional",
2653
+ value,
2654
+ span: value.span
2655
+ });
2656
+ }
2657
+ if (!this.#isPunct(",")) break;
2658
+ this.#next();
2659
+ }
2660
+ this.#expectPunct(")");
2661
+ return args;
2662
+ }
2663
+ };
2664
+ //#endregion
2665
+ export { ESCAPE_STRATEGIES as a, DEFAULT_SLOT as i, contentStart as n, scanParts as r, parse as t };
2666
+
2667
+ //# sourceMappingURL=parser-BU5dThuk.js.map