@lucidkiln/core 1.0.0 → 1.2.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.
- package/CHANGELOG.md +14 -0
- package/dist/ast-DDhHuwaY.d.ts +350 -0
- package/dist/ast-DDhHuwaY.d.ts.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/{environment-BlQ8V4tX.d.ts → environment-DBvRrOn0.d.ts} +51 -80
- package/dist/environment-DBvRrOn0.d.ts.map +1 -0
- package/dist/{format-CJoqhehX.js → format-Di6-GUtH.js} +4 -3
- package/dist/{format-CJoqhehX.js.map → format-Di6-GUtH.js.map} +1 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +76 -158
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/parser-isboTef0.js +2627 -0
- package/dist/parser-isboTef0.js.map +1 -0
- package/dist/{source-BQxJGo5Y.js → source-BbikGqIb.js} +40 -2765
- package/dist/source-BbikGqIb.js.map +1 -0
- package/dist/syntax.d.ts +22 -0
- package/dist/syntax.d.ts.map +1 -0
- package/dist/syntax.js +1 -0
- package/package.json +5 -1
- package/dist/environment-BlQ8V4tX.d.ts.map +0 -1
- package/dist/source-BQxJGo5Y.js.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as contentStart } from "./parser-isboTef0.js";
|
|
1
2
|
//#region src/errors.ts
|
|
2
3
|
/** Creates a diagnostic for a span of a source. */
|
|
3
4
|
const createDiagnostic = (source, code, message, span, severity = "error") => ({
|
|
@@ -372,8 +373,6 @@ const variableNames = (scope) => {
|
|
|
372
373
|
/** An empty output buffer. Captures start with one too, since what they capture is printed as a value. */
|
|
373
374
|
const emptyOut = () => ({
|
|
374
375
|
s: "",
|
|
375
|
-
indent: "",
|
|
376
|
-
inline: false,
|
|
377
376
|
ahead: null
|
|
378
377
|
});
|
|
379
378
|
/** The scope a block's body renders in: its own when it may set variables. */
|
|
@@ -469,13 +468,16 @@ const startCapture = (run, section) => {
|
|
|
469
468
|
};
|
|
470
469
|
/**
|
|
471
470
|
* Ends a capture that started inside `run`'s. One that added nothing leaves the log around it when it is last there,
|
|
472
|
-
* as it is unless captures overlap, and a cached section's islands join the section around it.
|
|
471
|
+
* as it is unless captures overlap, and a cached section's islands and tags join the section around it.
|
|
473
472
|
*/
|
|
474
473
|
const endCapture = (run, capture) => {
|
|
475
474
|
const outer = capture.outer;
|
|
476
475
|
if (capture.additions === null && outer.at(-1) === capture) outer.pop();
|
|
477
476
|
const section = run.capture?.section;
|
|
478
|
-
if (section && capture.section && capture.section !== section)
|
|
477
|
+
if (section && capture.section && capture.section !== section) {
|
|
478
|
+
for (const id of capture.section.deferred) section.deferred.push(id);
|
|
479
|
+
for (const tag of capture.section.tags) section.tags.add(tag);
|
|
480
|
+
}
|
|
479
481
|
};
|
|
480
482
|
const NO_BLOCKS = /* @__PURE__ */ new Map();
|
|
481
483
|
/** The imports of a template that imports nothing, shared. */
|
|
@@ -519,22 +521,20 @@ var Run = class Run {
|
|
|
519
521
|
locale: this.state.locale,
|
|
520
522
|
timeZone: this.state.timeZone,
|
|
521
523
|
template: this.template.name,
|
|
522
|
-
signal: this.state.signal
|
|
524
|
+
signal: this.state.signal,
|
|
525
|
+
tagCache: (...tags) => {
|
|
526
|
+
const section = this.capture?.section;
|
|
527
|
+
for (const tag of tags) {
|
|
528
|
+
if (typeof tag !== "string") throw new TypeError("Cache tags must be strings");
|
|
529
|
+
section?.tags.add(tag);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
523
532
|
};
|
|
524
533
|
return this.#info;
|
|
525
534
|
}
|
|
526
535
|
};
|
|
527
536
|
//#endregion
|
|
528
537
|
//#region src/runtime/outlets.ts
|
|
529
|
-
/**
|
|
530
|
-
* Stands for the outlet's indentation in added content, which is rendered before the outlet may be reached. The
|
|
531
|
-
* outlet puts its own indentation in place when it closes, so content lines up the same whichever runs first, and a
|
|
532
|
-
* cached section's additions line up wherever they are replayed. Two Unicode noncharacters, meant for uses like
|
|
533
|
-
* this. Indentation can land mid-line, where a tag's line ending was removed, so the marker is replaced wherever it
|
|
534
|
-
* is. Data holding one of the characters on its own is left alone.
|
|
535
|
-
*/
|
|
536
|
-
const OUTLET_INDENT = "";
|
|
537
|
-
const INDENT_MARKERS = /\uFDD0\uFDD1/g;
|
|
538
538
|
/** The additions in a log, in order, with each capture's in its place. See `Capture`. */
|
|
539
539
|
const additionsIn = (log, into = []) => {
|
|
540
540
|
for (const entry of log) if (entry instanceof Capture) {
|
|
@@ -596,10 +596,10 @@ const closedError = (run, span, outlet) => fail(run, span, "outlet-closed", `Out
|
|
|
596
596
|
/** Closes every outlet reached so far, fixing its content. */
|
|
597
597
|
const closeOutlets = (state) => {
|
|
598
598
|
if (state.outlets === null) return;
|
|
599
|
-
for (const [name, mark] of state.outlets) if (mark.content === null) mark.content = contentFor(state, name
|
|
599
|
+
for (const [name, mark] of state.outlets) if (mark.content === null) mark.content = contentFor(state, name);
|
|
600
600
|
};
|
|
601
601
|
/** An outlet's content: what was added to it, in order, without repeats of `once` tags. */
|
|
602
|
-
const contentFor = (state, name
|
|
602
|
+
const contentFor = (state, name) => {
|
|
603
603
|
const seen = /* @__PURE__ */ new Set();
|
|
604
604
|
let content = "";
|
|
605
605
|
for (const addition of additionsIn(state.additions ?? [])) {
|
|
@@ -608,8 +608,7 @@ const contentFor = (state, name, mark) => {
|
|
|
608
608
|
if (seen.has(addition.once)) continue;
|
|
609
609
|
seen.add(addition.once);
|
|
610
610
|
}
|
|
611
|
-
|
|
612
|
-
content += html.replace(INDENT_MARKERS, mark.indent);
|
|
611
|
+
content += addition.html;
|
|
613
612
|
}
|
|
614
613
|
return content;
|
|
615
614
|
};
|
|
@@ -648,20 +647,14 @@ const takeOutput = (state, output) => {
|
|
|
648
647
|
return text;
|
|
649
648
|
};
|
|
650
649
|
const NOTHING = () => {};
|
|
650
|
+
/** Whether parts may start early ahead of a part that waits: the render runs fewer parts ahead than it may. */
|
|
651
|
+
const mayRunAhead = (state) => state.ahead < 64;
|
|
651
652
|
/**
|
|
652
|
-
*
|
|
653
|
-
* part
|
|
654
|
-
*
|
|
655
|
-
* and so does the indentation of what comes after it. Check before starting anything that waits for the part.
|
|
656
|
-
*/
|
|
657
|
-
const mayRunAhead = (state, inline) => state.ahead < 64 && !inline;
|
|
658
|
-
/**
|
|
659
|
-
* Starts `rest` while `pending`, the promise of the part before it, waits, and returns a promise for both. `indent` is
|
|
660
|
-
* the indentation `out` had before the part started, which `rest` writes at. Rejects with the part's error when it
|
|
661
|
-
* fails, or else with the rest's, after both settle, so a failed render has nothing still running. Only call it when
|
|
662
|
-
* `mayRunAhead` allows.
|
|
653
|
+
* Starts `rest` while `pending`, the promise of the part before it, waits, and returns a promise for both. Rejects with
|
|
654
|
+
* the part's error when it fails, or else with the rest's, after both settle, so a failed render has nothing still
|
|
655
|
+
* running. Only call it when `mayRunAhead` allows.
|
|
663
656
|
*/
|
|
664
|
-
const runAhead = (run, out, pending,
|
|
657
|
+
const runAhead = (run, out, pending, rest, span) => {
|
|
665
658
|
const state = run.state;
|
|
666
659
|
state.ahead++;
|
|
667
660
|
const log = additionLog(run);
|
|
@@ -672,8 +665,6 @@ const runAhead = (run, out, pending, indent, rest, span) => {
|
|
|
672
665
|
});
|
|
673
666
|
const ahead = {
|
|
674
667
|
s: "",
|
|
675
|
-
indent,
|
|
676
|
-
inline: false,
|
|
677
668
|
ahead: out.ahead ?? {
|
|
678
669
|
base: out,
|
|
679
670
|
size: 0
|
|
@@ -748,17 +739,15 @@ const runGated = (sequence, index, gate, run, scope, out) => {
|
|
|
748
739
|
*/
|
|
749
740
|
const runSequence = (sequence, index, last, run, scope, out, gate) => {
|
|
750
741
|
const statements = sequence.statements;
|
|
751
|
-
const indent = out.indent;
|
|
752
742
|
for (let i = index; i <= last; i++) {
|
|
753
|
-
const inline = out.inline;
|
|
754
743
|
const result = gate === null ? statements[i](run, scope, out) : runGated(sequence, i, gate, run, scope, out);
|
|
755
744
|
if (result === void 0) continue;
|
|
756
745
|
const plan = sequence.plan(run, gate !== null);
|
|
757
746
|
const after = Math.min(plan.after[i], last);
|
|
758
747
|
const end = Math.min(plan.reach[i], last);
|
|
759
|
-
if (end > after && mayRunAhead(run.state
|
|
748
|
+
if (end > after && mayRunAhead(run.state)) {
|
|
760
749
|
const pending = after > i ? result.then(() => runSequence(sequence, i + 1, after, run, scope, out, gate)) : result;
|
|
761
|
-
const joined = runAhead(run, out, pending,
|
|
750
|
+
const joined = runAhead(run, out, pending, (ahead, aheadOut) => runSequence(sequence, after + 1, end, ahead, scope, aheadOut, gate), plan.spans[i]);
|
|
762
751
|
return end === last ? joined : joined.then(() => runSequence(sequence, end + 1, last, run, scope, out, gate));
|
|
763
752
|
}
|
|
764
753
|
return result.then(() => runSequence(sequence, i + 1, last, run, scope, out, gate));
|
|
@@ -768,24 +757,22 @@ const runSequence = (sequence, index, last, run, scope, out, gate) => {
|
|
|
768
757
|
* Runs passes `from` to `to` of a loop in order, waiting only when one returns a promise. Then the passes after it
|
|
769
758
|
* continue as `passesFrom` runs them. `ahead` is whether the loop's passes may run ahead of each other.
|
|
770
759
|
*/
|
|
771
|
-
const runPasses = (run, out, from, to,
|
|
760
|
+
const runPasses = (run, out, from, to, pass, span, ahead) => {
|
|
772
761
|
for (let index = from; index < to; index++) {
|
|
773
|
-
const inline = out.inline;
|
|
774
762
|
const result = pass(run, out, index);
|
|
775
763
|
if (result === void 0) continue;
|
|
776
|
-
return index + 1 < to ? passesFrom(run, out, result,
|
|
764
|
+
return index + 1 < to ? passesFrom(run, out, result, index + 1, to, pass, span, ahead) : result;
|
|
777
765
|
}
|
|
778
766
|
};
|
|
779
767
|
/**
|
|
780
|
-
* Runs passes `from` to `to` of a loop, which come after `pending`, a pass that waits
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
* pass started is done.
|
|
768
|
+
* Runs passes `from` to `to` of a loop, which come after `pending`, a pass that waits. When `ahead`, they start at
|
|
769
|
+
* once: each pass that waits lets the passes after it start in a buffer and capture of their own, and every buffer
|
|
770
|
+
* joins `out` in order once all the passes are done. Otherwise, or when `mayRunAhead` refuses, they run once the pass
|
|
771
|
+
* is done. Rejects with the first error in the loop's order, once every pass started is done.
|
|
785
772
|
*/
|
|
786
|
-
const passesFrom = (run, out, pending,
|
|
773
|
+
const passesFrom = (run, out, pending, from, to, pass, span, ahead) => {
|
|
787
774
|
const state = run.state;
|
|
788
|
-
if (!ahead || !mayRunAhead(state
|
|
775
|
+
if (!ahead || !mayRunAhead(state)) return pending.then(() => runPasses(run, out, from, to, pass, span, ahead));
|
|
789
776
|
const log = additionLog(run);
|
|
790
777
|
const outer = run.capture;
|
|
791
778
|
const section = outer?.section ?? null;
|
|
@@ -851,8 +838,6 @@ const passesFrom = (run, out, pending, inline, from, to, indent, pass, span, ahe
|
|
|
851
838
|
const capture = new Capture(log, section, behind);
|
|
852
839
|
const segment = {
|
|
853
840
|
s: "",
|
|
854
|
-
indent,
|
|
855
|
-
inline: false,
|
|
856
841
|
ahead: group
|
|
857
842
|
};
|
|
858
843
|
const segmentRun = run.within(capture);
|
|
@@ -874,7 +859,7 @@ const passesFrom = (run, out, pending, inline, from, to, indent, pass, span, ahe
|
|
|
874
859
|
if (waiting === void 0) break;
|
|
875
860
|
if (index < to && state.ahead >= 64) {
|
|
876
861
|
const next = index;
|
|
877
|
-
waiting = waiting.then(() => runPasses(segmentRun, segment, next, to,
|
|
862
|
+
waiting = waiting.then(() => runPasses(segmentRun, segment, next, to, pass, span, true));
|
|
878
863
|
index = to;
|
|
879
864
|
}
|
|
880
865
|
watch(waiting, at);
|
|
@@ -1286,55 +1271,10 @@ const checkLength = (run, text) => {
|
|
|
1286
1271
|
*/
|
|
1287
1272
|
const write = (run, out, text, span) => {
|
|
1288
1273
|
out.s += text;
|
|
1289
|
-
if (out.inline && text !== "") out.inline = false;
|
|
1290
1274
|
const state = run.state;
|
|
1291
1275
|
const ahead = out.ahead;
|
|
1292
1276
|
if (out.s.length > state.maxSize || ahead !== null && (ahead.size += text.length) + ahead.base.s.length > state.maxSize) throw fail(run, span, "limit-output", `Render stopped after ${state.maxSize} characters of output`);
|
|
1293
1277
|
};
|
|
1294
|
-
/**
|
|
1295
|
-
* Starts inserting content for an include, component, slot or block. `at` is the tag's `indent` when it stands on a
|
|
1296
|
-
* line of its own, which the content's lines get on top of the current indentation. Without it, the tag shares its
|
|
1297
|
-
* line, so the content's first line carries on from it. Tags at the start of a line, with `indent` empty, need
|
|
1298
|
-
* nothing.
|
|
1299
|
-
*
|
|
1300
|
-
* Callers keep `out.indent` and `out.inline` from before, and pass them to `endPlace` with the render's result. A
|
|
1301
|
-
* render that throws stops, so nothing needs restoring then.
|
|
1302
|
-
*/
|
|
1303
|
-
const startPlace = (out, at) => {
|
|
1304
|
-
if (at === void 0) out.inline = true;
|
|
1305
|
-
else out.indent += at;
|
|
1306
|
-
};
|
|
1307
|
-
/** Ends `startPlace` once `result` settles. */
|
|
1308
|
-
const endPlace = (out, indent, inline, result) => {
|
|
1309
|
-
if (result instanceof Promise) return result.then(() => endPlace(out, indent, inline, void 0));
|
|
1310
|
-
out.indent = indent;
|
|
1311
|
-
if (out.inline) out.inline = inline;
|
|
1312
|
-
};
|
|
1313
|
-
const indentedText = (text, lines) => ({
|
|
1314
|
-
text,
|
|
1315
|
-
lines,
|
|
1316
|
-
indent: "",
|
|
1317
|
-
indented: text
|
|
1318
|
-
});
|
|
1319
|
-
/**
|
|
1320
|
-
* The text with `indent` at the start of each line. `inline` is set when it is the first thing written since content
|
|
1321
|
-
* was inserted mid-line, so a line starting straight away gets no indentation. Remembers the last result, since text
|
|
1322
|
-
* is usually inserted at the same indentation each time.
|
|
1323
|
-
*/
|
|
1324
|
-
const indentText = (entry, indent, inline) => {
|
|
1325
|
-
if (entry.indent !== indent) {
|
|
1326
|
-
const { text, lines } = entry;
|
|
1327
|
-
let result = "";
|
|
1328
|
-
let at = 0;
|
|
1329
|
-
for (const start of lines) {
|
|
1330
|
-
result += text.slice(at, start) + indent;
|
|
1331
|
-
at = start;
|
|
1332
|
-
}
|
|
1333
|
-
entry.indented = result + text.slice(at);
|
|
1334
|
-
entry.indent = indent;
|
|
1335
|
-
}
|
|
1336
|
-
return inline && entry.lines[0] === 0 ? entry.indented.slice(indent.length) : entry.indented;
|
|
1337
|
-
};
|
|
1338
1278
|
/** Records the caller's location on a render error that passes through an include, component or `extends`. */
|
|
1339
1279
|
const markCaller = (run, span, error) => {
|
|
1340
1280
|
if (error instanceof KilnRenderError) {
|
|
@@ -1590,16 +1530,15 @@ const renderBody = (run, scope, out, imports, block, gate) => {
|
|
|
1590
1530
|
const { template, state } = run;
|
|
1591
1531
|
const parent = template.parent;
|
|
1592
1532
|
if (!parent && block !== void 0) return renderOnlyBlock(run, scope, out, block);
|
|
1593
|
-
const { indent, inline } = out;
|
|
1594
1533
|
const top = template.top;
|
|
1595
1534
|
const result = gate === null || top === null ? template.body(run, scope, out) : runSequence(top, 0, top.statements.length - 1, run, scope, out, gate);
|
|
1596
1535
|
if (!parent) return result;
|
|
1597
1536
|
const span = template.parentSpan;
|
|
1598
|
-
if (result !== void 0 && !parent.async && block === void 0 && mayRunAhead(state
|
|
1537
|
+
if (result !== void 0 && !parent.async && block === void 0 && mayRunAhead(state)) {
|
|
1599
1538
|
const effects = template.early?.() ?? null;
|
|
1600
1539
|
if (effects !== null) {
|
|
1601
1540
|
const next = gateFor(result, effects, gate);
|
|
1602
|
-
return runAhead(run, out, result,
|
|
1541
|
+
return runAhead(run, out, result, (ahead, aheadOut) => after(loadTemplate(ahead, parent.ev(ahead, scope), span), (layout) => withCaller(ahead, span, () => renderTemplate(state, layout, scope, aheadOut, run.depth + 1, run.slots, null, run.blocks, ahead, span, void 0, next))), span);
|
|
1603
1542
|
}
|
|
1604
1543
|
}
|
|
1605
1544
|
if (result === void 0 && !parent.async) {
|
|
@@ -1953,7 +1892,7 @@ const toPlainData = (value, onCycle, { what, charge = () => {}, maxLength = Infi
|
|
|
1953
1892
|
return convert(value, 0);
|
|
1954
1893
|
};
|
|
1955
1894
|
const isAsciiLetter = (code) => code >= 65 && code <= 90 || code >= 97 && code <= 122;
|
|
1956
|
-
const isSpace
|
|
1895
|
+
const isSpace = (char) => char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
|
|
1957
1896
|
/**
|
|
1958
1897
|
* Finds the `>` that ends a tag, or -1 when the tag never ends. A quote starts a value only straight after `=`, as in
|
|
1959
1898
|
* HTML, and only when the same quote appears again later, so an unclosed quote can't hide the rest of the text.
|
|
@@ -1970,7 +1909,7 @@ const tagEnd = (html, from, lastDouble, lastSingle) => {
|
|
|
1970
1909
|
else if (afterEquals && (char === "\"" && i < lastDouble || char === "'" && i < lastSingle)) {
|
|
1971
1910
|
quote = char;
|
|
1972
1911
|
afterEquals = false;
|
|
1973
|
-
} else if (!isSpace
|
|
1912
|
+
} else if (!isSpace(char)) afterEquals = false;
|
|
1974
1913
|
}
|
|
1975
1914
|
return -1;
|
|
1976
1915
|
};
|
|
@@ -2308,2670 +2247,6 @@ const builtinTests = {
|
|
|
2308
2247
|
}, ["divisor"])
|
|
2309
2248
|
};
|
|
2310
2249
|
//#endregion
|
|
2311
|
-
//#region src/parser/ast.ts
|
|
2312
|
-
/** The name of the default slot, filled by content outside any `fill`. */
|
|
2313
|
-
const DEFAULT_SLOT = "default";
|
|
2314
|
-
const ESCAPE_STRATEGIES = [
|
|
2315
|
-
"html",
|
|
2316
|
-
"attr",
|
|
2317
|
-
"js",
|
|
2318
|
-
"css",
|
|
2319
|
-
"url"
|
|
2320
|
-
];
|
|
2321
|
-
/** The value types a prop can declare, named as in the docs' table of types. */
|
|
2322
|
-
const TYPE_NAMES = [
|
|
2323
|
-
"null",
|
|
2324
|
-
"boolean",
|
|
2325
|
-
"number",
|
|
2326
|
-
"string",
|
|
2327
|
-
"array",
|
|
2328
|
-
"hash",
|
|
2329
|
-
"set",
|
|
2330
|
-
"date",
|
|
2331
|
-
"markup",
|
|
2332
|
-
"object"
|
|
2333
|
-
];
|
|
2334
|
-
const PUNCTUATORS = [
|
|
2335
|
-
"...",
|
|
2336
|
-
"?.",
|
|
2337
|
-
"??",
|
|
2338
|
-
"?:",
|
|
2339
|
-
"**",
|
|
2340
|
-
"//",
|
|
2341
|
-
"==",
|
|
2342
|
-
"!=",
|
|
2343
|
-
"<=",
|
|
2344
|
-
">=",
|
|
2345
|
-
"=>",
|
|
2346
|
-
"..",
|
|
2347
|
-
"+",
|
|
2348
|
-
"-",
|
|
2349
|
-
"*",
|
|
2350
|
-
"/",
|
|
2351
|
-
"%",
|
|
2352
|
-
"~",
|
|
2353
|
-
"<",
|
|
2354
|
-
">",
|
|
2355
|
-
"=",
|
|
2356
|
-
"(",
|
|
2357
|
-
")",
|
|
2358
|
-
"[",
|
|
2359
|
-
"]",
|
|
2360
|
-
"{",
|
|
2361
|
-
"}",
|
|
2362
|
-
",",
|
|
2363
|
-
":",
|
|
2364
|
-
".",
|
|
2365
|
-
"|",
|
|
2366
|
-
"?"
|
|
2367
|
-
];
|
|
2368
|
-
/** Punctuators by their first character's code, longest first as in `PUNCTUATORS`, so lexing tries only a few. */
|
|
2369
|
-
const PUNCTUATORS_BY_CHARACTER = /* @__PURE__ */ new Map();
|
|
2370
|
-
for (const punct of PUNCTUATORS) {
|
|
2371
|
-
const code = punct.charCodeAt(0);
|
|
2372
|
-
const list = PUNCTUATORS_BY_CHARACTER.get(code);
|
|
2373
|
-
if (list) list.push(punct);
|
|
2374
|
-
else PUNCTUATORS_BY_CHARACTER.set(code, [punct]);
|
|
2375
|
-
}
|
|
2376
|
-
/**
|
|
2377
|
-
* The punctuator at `pos`, whose first two character codes are `code` and `next`, or null. Longest first, so `...` wins
|
|
2378
|
-
* over `..` and `.`, as in `PUNCTUATORS`.
|
|
2379
|
-
*/
|
|
2380
|
-
const punctuatorAt = (text, pos, code, next) => {
|
|
2381
|
-
switch (code) {
|
|
2382
|
-
case 46:
|
|
2383
|
-
if (next === 46) return text.charCodeAt(pos + 2) === 46 ? "..." : "..";
|
|
2384
|
-
return ".";
|
|
2385
|
-
case 63: return next === 46 ? "?." : next === 63 ? "??" : next === 58 ? "?:" : "?";
|
|
2386
|
-
case 42: return next === 42 ? "**" : "*";
|
|
2387
|
-
case 47: return next === 47 ? "//" : "/";
|
|
2388
|
-
case 61: return next === 61 ? "==" : next === 62 ? "=>" : "=";
|
|
2389
|
-
case 33: return next === 61 ? "!=" : null;
|
|
2390
|
-
case 60: return next === 61 ? "<=" : "<";
|
|
2391
|
-
case 62: return next === 61 ? ">=" : ">";
|
|
2392
|
-
default: {
|
|
2393
|
-
const single = SINGLE_PUNCTUATORS[code];
|
|
2394
|
-
return single === void 0 ? null : single;
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
};
|
|
2398
|
-
/** Punctuators of one character that start no longer one, by character code. */
|
|
2399
|
-
const SINGLE_PUNCTUATORS = [];
|
|
2400
|
-
for (const punct of PUNCTUATORS) {
|
|
2401
|
-
const code = punct.charCodeAt(0);
|
|
2402
|
-
if (PUNCTUATORS_BY_CHARACTER.get(code).length === 1) SINGLE_PUNCTUATORS[code] = punct;
|
|
2403
|
-
}
|
|
2404
|
-
const CLOSED = {
|
|
2405
|
-
closed: true,
|
|
2406
|
-
trim: false
|
|
2407
|
-
};
|
|
2408
|
-
const CLOSED_TRIM = {
|
|
2409
|
-
closed: true,
|
|
2410
|
-
trim: true
|
|
2411
|
-
};
|
|
2412
|
-
const NOT_CLOSED = {
|
|
2413
|
-
closed: false,
|
|
2414
|
-
trim: false
|
|
2415
|
-
};
|
|
2416
|
-
const ESCAPES = {
|
|
2417
|
-
n: "\n",
|
|
2418
|
-
r: "\r",
|
|
2419
|
-
t: " ",
|
|
2420
|
-
"\\": "\\",
|
|
2421
|
-
"'": "'",
|
|
2422
|
-
"\"": "\""
|
|
2423
|
-
};
|
|
2424
|
-
const VERBATIM_END = /\{%(-?)\s*endverbatim\s*(-?)%\}/g;
|
|
2425
|
-
const isInlineSpace = (code) => code === 32 || code === 9 || code === 13 || code === 11 || code === 12;
|
|
2426
|
-
const isSpace = (code) => code === 10 || isInlineSpace(code);
|
|
2427
|
-
const isNameStart = (code) => code >= 97 && code <= 122 || code >= 65 && code <= 90 || code === 95;
|
|
2428
|
-
const isDigit = (code) => code >= 48 && code <= 57;
|
|
2429
|
-
const isNamePart = (code) => isNameStart(code) || isDigit(code);
|
|
2430
|
-
/**
|
|
2431
|
-
* Turns template source into tokens in one pass, then applies whitespace control.
|
|
2432
|
-
*
|
|
2433
|
-
* Text outside delimiters becomes `text` tokens. Comments produce no tokens. Lines holding only block tags and comments are removed, and `-` markers trim neighbouring whitespace.
|
|
2434
|
-
*/
|
|
2435
|
-
const lex = (text) => new Lexer(text).run();
|
|
2436
|
-
/**
|
|
2437
|
-
* Splits source into text, outputs, tags and comments as written, before whitespace control. Tools such as the formatter use it. Comments have no tokens.
|
|
2438
|
-
*/
|
|
2439
|
-
const scanParts = (text) => new Lexer(text).scan();
|
|
2440
|
-
/** Where a template's content starts: after a byte order mark, which is never content. */
|
|
2441
|
-
const contentStart = (text) => text.charCodeAt(0) === 65279 ? 1 : 0;
|
|
2442
|
-
var Lexer = class {
|
|
2443
|
-
#text;
|
|
2444
|
-
#pos;
|
|
2445
|
-
/** How many string interpolations the lexer is inside. */
|
|
2446
|
-
#interpolations = 0;
|
|
2447
|
-
#tokens = [];
|
|
2448
|
-
#problems = [];
|
|
2449
|
-
constructor(text) {
|
|
2450
|
-
this.#text = text;
|
|
2451
|
-
this.#pos = contentStart(text);
|
|
2452
|
-
}
|
|
2453
|
-
scan() {
|
|
2454
|
-
return {
|
|
2455
|
-
parts: this.#scanParts(),
|
|
2456
|
-
problems: this.#problems
|
|
2457
|
-
};
|
|
2458
|
-
}
|
|
2459
|
-
run() {
|
|
2460
|
-
const parts = this.#scanParts();
|
|
2461
|
-
applyTrimMarkers(this.#text, parts);
|
|
2462
|
-
const standalone = applyStandaloneLines(this.#text, parts);
|
|
2463
|
-
const lines = textLines(this.#text, parts, standalone);
|
|
2464
|
-
const tokens = [];
|
|
2465
|
-
let line = 0;
|
|
2466
|
-
for (const part of parts) {
|
|
2467
|
-
const text = part.kind === "text";
|
|
2468
|
-
const at = text ? part.from : part.start;
|
|
2469
|
-
if (text && part.from < part.to) tokens.push({
|
|
2470
|
-
type: "text",
|
|
2471
|
-
value: this.#text.slice(part.from, part.to),
|
|
2472
|
-
start: part.from,
|
|
2473
|
-
end: part.to
|
|
2474
|
-
});
|
|
2475
|
-
else if (lines[line]?.start === at) tokens.push({
|
|
2476
|
-
type: "text",
|
|
2477
|
-
value: "",
|
|
2478
|
-
start: at,
|
|
2479
|
-
end: at
|
|
2480
|
-
});
|
|
2481
|
-
if (!text && part.kind !== "comment") for (const token of part.tokens) tokens.push(token);
|
|
2482
|
-
const end = text ? part.to : part.start;
|
|
2483
|
-
while ((lines[line]?.start ?? Infinity) <= end) line++;
|
|
2484
|
-
}
|
|
2485
|
-
tokens.push({
|
|
2486
|
-
type: "eof",
|
|
2487
|
-
value: "",
|
|
2488
|
-
start: this.#text.length,
|
|
2489
|
-
end: this.#text.length
|
|
2490
|
-
});
|
|
2491
|
-
return {
|
|
2492
|
-
tokens,
|
|
2493
|
-
problems: this.#problems,
|
|
2494
|
-
standalone,
|
|
2495
|
-
lines
|
|
2496
|
-
};
|
|
2497
|
-
}
|
|
2498
|
-
#scanParts() {
|
|
2499
|
-
const text = this.#text;
|
|
2500
|
-
const parts = [];
|
|
2501
|
-
const pushText = (start, end) => {
|
|
2502
|
-
if (end > start) parts.push({
|
|
2503
|
-
kind: "text",
|
|
2504
|
-
start,
|
|
2505
|
-
end,
|
|
2506
|
-
from: start,
|
|
2507
|
-
to: end
|
|
2508
|
-
});
|
|
2509
|
-
};
|
|
2510
|
-
while (this.#pos < text.length) {
|
|
2511
|
-
const open = this.#findOpen(this.#pos);
|
|
2512
|
-
if (open === -1) {
|
|
2513
|
-
pushText(this.#pos, text.length);
|
|
2514
|
-
break;
|
|
2515
|
-
}
|
|
2516
|
-
pushText(this.#pos, open);
|
|
2517
|
-
const kind = text[open + 1];
|
|
2518
|
-
if (kind === "#") {
|
|
2519
|
-
parts.push(this.#scanComment(open));
|
|
2520
|
-
continue;
|
|
2521
|
-
}
|
|
2522
|
-
const part = this.#scanDelimited(open, kind === "{" ? "output" : "tag");
|
|
2523
|
-
parts.push(part);
|
|
2524
|
-
const inner = part.tokens;
|
|
2525
|
-
if (part.kind === "tag" && inner.length === 3 && inner[1]?.type === "name" && inner[1].value === "verbatim") this.#scanVerbatim(part, parts);
|
|
2526
|
-
}
|
|
2527
|
-
return parts;
|
|
2528
|
-
}
|
|
2529
|
-
#findOpen(from) {
|
|
2530
|
-
const text = this.#text;
|
|
2531
|
-
let index = text.indexOf("{", from);
|
|
2532
|
-
while (index !== -1) {
|
|
2533
|
-
const next = text.charCodeAt(index + 1);
|
|
2534
|
-
if (next === 123 || next === 37 || next === 35) return index;
|
|
2535
|
-
index = text.indexOf("{", index + 1);
|
|
2536
|
-
}
|
|
2537
|
-
return -1;
|
|
2538
|
-
}
|
|
2539
|
-
#scanComment(open) {
|
|
2540
|
-
const text = this.#text;
|
|
2541
|
-
const close = text.indexOf("#}", open + 2);
|
|
2542
|
-
const trimBefore = text[open + 2] === "-";
|
|
2543
|
-
if (close === -1) {
|
|
2544
|
-
this.#problem("unclosed-comment", "Comment is missing its closing \"#}\"", open, open + 2);
|
|
2545
|
-
this.#pos = text.length;
|
|
2546
|
-
return {
|
|
2547
|
-
kind: "comment",
|
|
2548
|
-
start: open,
|
|
2549
|
-
end: text.length,
|
|
2550
|
-
trimBefore,
|
|
2551
|
-
trimAfter: false,
|
|
2552
|
-
tokens: []
|
|
2553
|
-
};
|
|
2554
|
-
}
|
|
2555
|
-
this.#pos = close + 2;
|
|
2556
|
-
return {
|
|
2557
|
-
kind: "comment",
|
|
2558
|
-
start: open,
|
|
2559
|
-
end: close + 2,
|
|
2560
|
-
trimBefore,
|
|
2561
|
-
trimAfter: close > open + 2 && text[close - 1] === "-",
|
|
2562
|
-
tokens: []
|
|
2563
|
-
};
|
|
2564
|
-
}
|
|
2565
|
-
#scanDelimited(open, kind) {
|
|
2566
|
-
const text = this.#text;
|
|
2567
|
-
const trimBefore = text[open + 2] === "-";
|
|
2568
|
-
this.#tokens = [{
|
|
2569
|
-
type: kind === "output" ? "outputOpen" : "tagOpen",
|
|
2570
|
-
value: text.slice(open, open + (trimBefore ? 3 : 2)),
|
|
2571
|
-
start: open,
|
|
2572
|
-
end: open + 2
|
|
2573
|
-
}];
|
|
2574
|
-
this.#pos = open + (trimBefore ? 3 : 2);
|
|
2575
|
-
const closeStart = this.#pos;
|
|
2576
|
-
const { trim, closed } = this.#lexExpression(kind);
|
|
2577
|
-
const end = this.#pos;
|
|
2578
|
-
if (!closed) this.#problem("unclosed-tag", kind === "output" ? "Output is missing its closing \"}}\"" : "Tag is missing its closing \"%}\"", open, open + 2);
|
|
2579
|
-
this.#tokens.push({
|
|
2580
|
-
type: kind === "output" ? "outputClose" : "tagClose",
|
|
2581
|
-
value: closed ? text.slice(trim ? end - 3 : end - 2, end) : "",
|
|
2582
|
-
start: closed ? end - 2 : Math.max(closeStart, end),
|
|
2583
|
-
end
|
|
2584
|
-
});
|
|
2585
|
-
return {
|
|
2586
|
-
kind,
|
|
2587
|
-
start: open,
|
|
2588
|
-
end,
|
|
2589
|
-
trimBefore,
|
|
2590
|
-
trimAfter: trim,
|
|
2591
|
-
tokens: this.#tokens
|
|
2592
|
-
};
|
|
2593
|
-
}
|
|
2594
|
-
/** Treats everything up to `{% endverbatim %}` as text. */
|
|
2595
|
-
#scanVerbatim(tag, parts) {
|
|
2596
|
-
const text = this.#text;
|
|
2597
|
-
VERBATIM_END.lastIndex = this.#pos;
|
|
2598
|
-
const match = VERBATIM_END.exec(text);
|
|
2599
|
-
if (!match) {
|
|
2600
|
-
this.#problem("unclosed-block", "\"verbatim\" is missing its \"endverbatim\" tag", tag.start, tag.end);
|
|
2601
|
-
parts.push({
|
|
2602
|
-
kind: "text",
|
|
2603
|
-
start: this.#pos,
|
|
2604
|
-
end: text.length,
|
|
2605
|
-
from: this.#pos,
|
|
2606
|
-
to: text.length
|
|
2607
|
-
});
|
|
2608
|
-
this.#pos = text.length;
|
|
2609
|
-
return;
|
|
2610
|
-
}
|
|
2611
|
-
const start = match.index;
|
|
2612
|
-
const end = start + match[0].length;
|
|
2613
|
-
if (start > this.#pos) parts.push({
|
|
2614
|
-
kind: "text",
|
|
2615
|
-
start: this.#pos,
|
|
2616
|
-
end: start,
|
|
2617
|
-
from: this.#pos,
|
|
2618
|
-
to: start
|
|
2619
|
-
});
|
|
2620
|
-
const nameStart = text.indexOf("endverbatim", start);
|
|
2621
|
-
const trimBefore = match[1] === "-";
|
|
2622
|
-
const trimAfter = match[2] === "-";
|
|
2623
|
-
parts.push({
|
|
2624
|
-
kind: "tag",
|
|
2625
|
-
start,
|
|
2626
|
-
end,
|
|
2627
|
-
trimBefore,
|
|
2628
|
-
trimAfter,
|
|
2629
|
-
tokens: [
|
|
2630
|
-
{
|
|
2631
|
-
type: "tagOpen",
|
|
2632
|
-
value: trimBefore ? "{%-" : "{%",
|
|
2633
|
-
start,
|
|
2634
|
-
end: start + 2
|
|
2635
|
-
},
|
|
2636
|
-
{
|
|
2637
|
-
type: "name",
|
|
2638
|
-
value: "endverbatim",
|
|
2639
|
-
start: nameStart,
|
|
2640
|
-
end: nameStart + 11
|
|
2641
|
-
},
|
|
2642
|
-
{
|
|
2643
|
-
type: "tagClose",
|
|
2644
|
-
value: trimAfter ? "-%}" : "%}",
|
|
2645
|
-
start: end - 2,
|
|
2646
|
-
end
|
|
2647
|
-
}
|
|
2648
|
-
]
|
|
2649
|
-
});
|
|
2650
|
-
this.#pos = end;
|
|
2651
|
-
}
|
|
2652
|
-
/**
|
|
2653
|
-
* Lexes expression tokens until the closing delimiter for `mode`.
|
|
2654
|
-
*
|
|
2655
|
-
* `closed` is false when the source ended, or a new tag started, before the delimiter.
|
|
2656
|
-
*/
|
|
2657
|
-
#lexExpression(mode) {
|
|
2658
|
-
const text = this.#text;
|
|
2659
|
-
const length = text.length;
|
|
2660
|
-
const tokens = this.#tokens;
|
|
2661
|
-
let depth = 0;
|
|
2662
|
-
let pos = this.#pos;
|
|
2663
|
-
for (;;) {
|
|
2664
|
-
while (pos < length && isSpace(text.charCodeAt(pos))) pos++;
|
|
2665
|
-
if (pos >= length) {
|
|
2666
|
-
this.#pos = pos;
|
|
2667
|
-
return NOT_CLOSED;
|
|
2668
|
-
}
|
|
2669
|
-
const code = text.charCodeAt(pos);
|
|
2670
|
-
const next = text.charCodeAt(pos + 1);
|
|
2671
|
-
if (mode === "tag") {
|
|
2672
|
-
if (code === 45 && next === 37 && text.charCodeAt(pos + 2) === 125) {
|
|
2673
|
-
this.#pos = pos + 3;
|
|
2674
|
-
return CLOSED_TRIM;
|
|
2675
|
-
}
|
|
2676
|
-
if (code === 37 && next === 125) {
|
|
2677
|
-
this.#pos = pos + 2;
|
|
2678
|
-
return CLOSED;
|
|
2679
|
-
}
|
|
2680
|
-
} else if (mode === "output" && depth === 0) {
|
|
2681
|
-
if (code === 45 && next === 125 && text.charCodeAt(pos + 2) === 125) {
|
|
2682
|
-
this.#pos = pos + 3;
|
|
2683
|
-
return CLOSED_TRIM;
|
|
2684
|
-
}
|
|
2685
|
-
if (code === 125 && next === 125) {
|
|
2686
|
-
this.#pos = pos + 2;
|
|
2687
|
-
return CLOSED;
|
|
2688
|
-
}
|
|
2689
|
-
} else if (mode === "interp" && depth === 0 && code === 125) {
|
|
2690
|
-
tokens.push({
|
|
2691
|
-
type: "interpClose",
|
|
2692
|
-
value: "}",
|
|
2693
|
-
start: pos,
|
|
2694
|
-
end: pos + 1
|
|
2695
|
-
});
|
|
2696
|
-
this.#pos = pos + 1;
|
|
2697
|
-
return CLOSED;
|
|
2698
|
-
}
|
|
2699
|
-
if (code === 123 && (next === 123 || next === 37 || next === 35)) {
|
|
2700
|
-
this.#pos = pos;
|
|
2701
|
-
return NOT_CLOSED;
|
|
2702
|
-
}
|
|
2703
|
-
if (mode === "interp" && code === 37 && next === 125) {
|
|
2704
|
-
this.#pos = pos;
|
|
2705
|
-
return NOT_CLOSED;
|
|
2706
|
-
}
|
|
2707
|
-
if (isNameStart(code)) {
|
|
2708
|
-
let end = pos + 1;
|
|
2709
|
-
while (end < length && isNamePart(text.charCodeAt(end))) end++;
|
|
2710
|
-
tokens.push({
|
|
2711
|
-
type: "name",
|
|
2712
|
-
value: text.slice(pos, end),
|
|
2713
|
-
start: pos,
|
|
2714
|
-
end
|
|
2715
|
-
});
|
|
2716
|
-
pos = end;
|
|
2717
|
-
continue;
|
|
2718
|
-
}
|
|
2719
|
-
if (isDigit(code)) {
|
|
2720
|
-
this.#pos = pos;
|
|
2721
|
-
this.#lexNumber();
|
|
2722
|
-
pos = this.#pos;
|
|
2723
|
-
continue;
|
|
2724
|
-
}
|
|
2725
|
-
if (code === 34 || code === 39) {
|
|
2726
|
-
this.#pos = pos;
|
|
2727
|
-
this.#lexString();
|
|
2728
|
-
pos = this.#pos;
|
|
2729
|
-
continue;
|
|
2730
|
-
}
|
|
2731
|
-
const punct = punctuatorAt(text, pos, code, next);
|
|
2732
|
-
if (punct !== null) {
|
|
2733
|
-
if (code === 123) depth++;
|
|
2734
|
-
else if (code === 125 && depth > 0) depth--;
|
|
2735
|
-
tokens.push({
|
|
2736
|
-
type: "punct",
|
|
2737
|
-
value: punct,
|
|
2738
|
-
start: pos,
|
|
2739
|
-
end: pos + punct.length
|
|
2740
|
-
});
|
|
2741
|
-
pos += punct.length;
|
|
2742
|
-
continue;
|
|
2743
|
-
}
|
|
2744
|
-
const character = String.fromCodePoint(text.codePointAt(pos));
|
|
2745
|
-
this.#problem("unexpected-character", `Unexpected character "${character}"`, pos, pos + character.length);
|
|
2746
|
-
pos += character.length;
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
#lexNumber() {
|
|
2750
|
-
const text = this.#text;
|
|
2751
|
-
const start = this.#pos;
|
|
2752
|
-
const previous = this.#tokens[this.#tokens.length - 1];
|
|
2753
|
-
const integerOnly = previous?.type === "punct" && (previous.value === "." || previous.value === "?.");
|
|
2754
|
-
const readDigits = () => {
|
|
2755
|
-
while (this.#pos < text.length) {
|
|
2756
|
-
const code = text.charCodeAt(this.#pos);
|
|
2757
|
-
if (isDigit(code)) this.#pos++;
|
|
2758
|
-
else if (code === 95 && isDigit(text.charCodeAt(this.#pos + 1))) this.#pos++;
|
|
2759
|
-
else break;
|
|
2760
|
-
}
|
|
2761
|
-
};
|
|
2762
|
-
readDigits();
|
|
2763
|
-
if (!integerOnly && text[this.#pos] === "." && isDigit(text.charCodeAt(this.#pos + 1))) {
|
|
2764
|
-
this.#pos++;
|
|
2765
|
-
readDigits();
|
|
2766
|
-
}
|
|
2767
|
-
const raw = text.slice(start, this.#pos);
|
|
2768
|
-
this.#push("number", raw.replaceAll("_", ""), start, this.#pos);
|
|
2769
|
-
}
|
|
2770
|
-
#lexString() {
|
|
2771
|
-
const text = this.#text;
|
|
2772
|
-
const start = this.#pos;
|
|
2773
|
-
const quote = text[start];
|
|
2774
|
-
let value = "";
|
|
2775
|
-
let segmentStart = start;
|
|
2776
|
-
let interpolated = false;
|
|
2777
|
-
this.#pos++;
|
|
2778
|
-
while (this.#pos < text.length) {
|
|
2779
|
-
const char = text[this.#pos];
|
|
2780
|
-
if (char === quote) {
|
|
2781
|
-
this.#pos++;
|
|
2782
|
-
if (interpolated) {
|
|
2783
|
-
if (value !== "") this.#push("string", value, segmentStart, this.#pos - 1);
|
|
2784
|
-
this.#push("stringEnd", quote, this.#pos - 1, this.#pos);
|
|
2785
|
-
} else this.#push("string", value, start, this.#pos);
|
|
2786
|
-
return;
|
|
2787
|
-
}
|
|
2788
|
-
if (char === "\\") {
|
|
2789
|
-
value += this.#readEscape(quote);
|
|
2790
|
-
continue;
|
|
2791
|
-
}
|
|
2792
|
-
if (quote === "\"" && char === "#" && text[this.#pos + 1] === "{") {
|
|
2793
|
-
if (!interpolated) {
|
|
2794
|
-
interpolated = true;
|
|
2795
|
-
this.#push("stringStart", quote, start, start + 1);
|
|
2796
|
-
}
|
|
2797
|
-
if (value !== "") {
|
|
2798
|
-
this.#push("string", value, segmentStart, this.#pos);
|
|
2799
|
-
value = "";
|
|
2800
|
-
}
|
|
2801
|
-
this.#push("interpOpen", "#{", this.#pos, this.#pos + 2);
|
|
2802
|
-
this.#pos += 2;
|
|
2803
|
-
const interpStart = this.#pos;
|
|
2804
|
-
if (this.#interpolations === 200) {
|
|
2805
|
-
this.#problem("too-deep", `Template is nested more than 200 levels deep`, interpStart - 2, interpStart);
|
|
2806
|
-
this.#skipInterpolation();
|
|
2807
|
-
segmentStart = this.#pos;
|
|
2808
|
-
continue;
|
|
2809
|
-
}
|
|
2810
|
-
this.#interpolations++;
|
|
2811
|
-
const { closed } = this.#lexExpression("interp");
|
|
2812
|
-
this.#interpolations--;
|
|
2813
|
-
if (!closed) {
|
|
2814
|
-
this.#problem("unclosed-interpolation", "Interpolation is missing its closing \"}\"", interpStart - 2, interpStart);
|
|
2815
|
-
this.#push("interpClose", "", this.#pos, this.#pos);
|
|
2816
|
-
this.#push("stringEnd", "", this.#pos, this.#pos);
|
|
2817
|
-
return;
|
|
2818
|
-
}
|
|
2819
|
-
segmentStart = this.#pos;
|
|
2820
|
-
continue;
|
|
2821
|
-
}
|
|
2822
|
-
value += char;
|
|
2823
|
-
this.#pos++;
|
|
2824
|
-
}
|
|
2825
|
-
this.#problem("unclosed-string", "String is missing its closing quote", start, start + 1);
|
|
2826
|
-
if (interpolated) {
|
|
2827
|
-
if (value !== "") this.#push("string", value, segmentStart, this.#pos);
|
|
2828
|
-
this.#push("stringEnd", "", this.#pos, this.#pos);
|
|
2829
|
-
} else this.#push("string", value, start, this.#pos);
|
|
2830
|
-
}
|
|
2831
|
-
/** Skips the rest of an interpolation without lexing it, up to the `}` that balances its `#{`. */
|
|
2832
|
-
#skipInterpolation() {
|
|
2833
|
-
const text = this.#text;
|
|
2834
|
-
let depth = 0;
|
|
2835
|
-
while (this.#pos < text.length) {
|
|
2836
|
-
const char = text[this.#pos++];
|
|
2837
|
-
if (char === "{") depth++;
|
|
2838
|
-
else if (char === "}" && depth-- === 0) {
|
|
2839
|
-
this.#push("interpClose", "}", this.#pos - 1, this.#pos);
|
|
2840
|
-
return;
|
|
2841
|
-
}
|
|
2842
|
-
}
|
|
2843
|
-
}
|
|
2844
|
-
/** Reads a backslash escape. `\#{` is only an escape in double-quoted strings, where `#{` would start interpolation. */
|
|
2845
|
-
#readEscape(quote) {
|
|
2846
|
-
const text = this.#text;
|
|
2847
|
-
const start = this.#pos;
|
|
2848
|
-
const next = text[start + 1];
|
|
2849
|
-
if (next === void 0) {
|
|
2850
|
-
this.#pos++;
|
|
2851
|
-
return "";
|
|
2852
|
-
}
|
|
2853
|
-
if (next === "#" && quote === "\"" && text[start + 2] === "{") {
|
|
2854
|
-
this.#pos += 2;
|
|
2855
|
-
return "#";
|
|
2856
|
-
}
|
|
2857
|
-
const simple = ESCAPES[next];
|
|
2858
|
-
if (simple !== void 0) {
|
|
2859
|
-
this.#pos += 2;
|
|
2860
|
-
return simple;
|
|
2861
|
-
}
|
|
2862
|
-
if (next === "u" && text[start + 2] === "{") {
|
|
2863
|
-
const brace = text.slice(start + 3, start + 10).indexOf("}");
|
|
2864
|
-
const close = brace === -1 ? -1 : start + 3 + brace;
|
|
2865
|
-
const hex = close === -1 ? "" : text.slice(start + 3, close);
|
|
2866
|
-
if (/^[0-9a-fA-F]{1,6}$/.test(hex)) {
|
|
2867
|
-
const code = Number.parseInt(hex, 16);
|
|
2868
|
-
if (code <= 1114111) {
|
|
2869
|
-
this.#pos = close + 1;
|
|
2870
|
-
return String.fromCodePoint(code);
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2873
|
-
}
|
|
2874
|
-
this.#problem("invalid-escape", `Unknown escape sequence "\\${next}"`, start, start + 2);
|
|
2875
|
-
this.#pos += 2;
|
|
2876
|
-
return next;
|
|
2877
|
-
}
|
|
2878
|
-
#push(type, value, start, end) {
|
|
2879
|
-
this.#tokens.push({
|
|
2880
|
-
type,
|
|
2881
|
-
value,
|
|
2882
|
-
start,
|
|
2883
|
-
end
|
|
2884
|
-
});
|
|
2885
|
-
}
|
|
2886
|
-
#problem(code, message, start, end) {
|
|
2887
|
-
this.#problems.push({
|
|
2888
|
-
code,
|
|
2889
|
-
message,
|
|
2890
|
-
span: {
|
|
2891
|
-
start,
|
|
2892
|
-
end
|
|
2893
|
-
}
|
|
2894
|
-
});
|
|
2895
|
-
}
|
|
2896
|
-
};
|
|
2897
|
-
/** Applies `-` markers, which remove all whitespace on their side. */
|
|
2898
|
-
const applyTrimMarkers = (text, parts) => {
|
|
2899
|
-
for (let i = 0; i < parts.length; i++) {
|
|
2900
|
-
const part = parts[i];
|
|
2901
|
-
if (part.kind === "text") continue;
|
|
2902
|
-
const previous = parts[i - 1];
|
|
2903
|
-
if (part.trimBefore && previous?.kind === "text") {
|
|
2904
|
-
let end = previous.end;
|
|
2905
|
-
while (end > previous.start && isSpace(text.charCodeAt(end - 1))) end--;
|
|
2906
|
-
previous.to = Math.min(previous.to, end);
|
|
2907
|
-
}
|
|
2908
|
-
const next = parts[i + 1];
|
|
2909
|
-
if (part.trimAfter && next?.kind === "text") {
|
|
2910
|
-
let start = next.start;
|
|
2911
|
-
while (start < next.end && isSpace(text.charCodeAt(start))) start++;
|
|
2912
|
-
next.from = Math.max(next.from, start);
|
|
2913
|
-
}
|
|
2914
|
-
}
|
|
2915
|
-
};
|
|
2916
|
-
const isInlineBlank = (text, start, end) => {
|
|
2917
|
-
for (let i = start; i < end; i++) if (!isInlineSpace(text.charCodeAt(i))) return false;
|
|
2918
|
-
return true;
|
|
2919
|
-
};
|
|
2920
|
-
/**
|
|
2921
|
-
* The first line feed from `start` up to `end`, or -1. The search stops at `end`, unlike `indexOf`, so a template with
|
|
2922
|
-
* many tags on one long line stays linear.
|
|
2923
|
-
*/
|
|
2924
|
-
const newlineIn = (text, start, end) => {
|
|
2925
|
-
for (let i = start; i < end; i++) if (text.charCodeAt(i) === 10) return i;
|
|
2926
|
-
return -1;
|
|
2927
|
-
};
|
|
2928
|
-
/** The last line feed from `start` up to `end`, or -1. Stops at `start`, as `newlineIn` stops at `end`. */
|
|
2929
|
-
const lastNewlineIn = (text, start, end) => {
|
|
2930
|
-
for (let i = end - 1; i >= start; i--) if (text.charCodeAt(i) === 10) return i;
|
|
2931
|
-
return -1;
|
|
2932
|
-
};
|
|
2933
|
-
/**
|
|
2934
|
-
* Removes lines that hold only block tags and comments, including their indentation and line ending.
|
|
2935
|
-
*
|
|
2936
|
-
* 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.
|
|
2937
|
-
*/
|
|
2938
|
-
const applyStandaloneLines = (text, parts) => {
|
|
2939
|
-
const removed = /* @__PURE__ */ new Map();
|
|
2940
|
-
const isBlockLike = (part) => part !== void 0 && (part.kind === "tag" || part.kind === "comment");
|
|
2941
|
-
let i = 0;
|
|
2942
|
-
while (i < parts.length) {
|
|
2943
|
-
if (!isBlockLike(parts[i])) {
|
|
2944
|
-
i++;
|
|
2945
|
-
continue;
|
|
2946
|
-
}
|
|
2947
|
-
let last = i;
|
|
2948
|
-
for (;;) {
|
|
2949
|
-
const next = parts[last + 1];
|
|
2950
|
-
if (isBlockLike(next)) {
|
|
2951
|
-
last++;
|
|
2952
|
-
continue;
|
|
2953
|
-
}
|
|
2954
|
-
if (next?.kind === "text" && isInlineBlank(text, next.start, next.end) && isBlockLike(parts[last + 2])) {
|
|
2955
|
-
last += 2;
|
|
2956
|
-
continue;
|
|
2957
|
-
}
|
|
2958
|
-
break;
|
|
2959
|
-
}
|
|
2960
|
-
let standalone = true;
|
|
2961
|
-
let cutBefore = -1;
|
|
2962
|
-
const previous = parts[i - 1];
|
|
2963
|
-
if (previous) {
|
|
2964
|
-
if (previous.kind !== "text") standalone = false;
|
|
2965
|
-
else {
|
|
2966
|
-
const newline = lastNewlineIn(text, previous.start, previous.end);
|
|
2967
|
-
if (newline !== -1) {
|
|
2968
|
-
if (isInlineBlank(text, newline + 1, previous.end)) cutBefore = newline + 1;
|
|
2969
|
-
else standalone = false;
|
|
2970
|
-
} else if (i - 1 === 0 && isInlineBlank(text, previous.start, previous.end)) cutBefore = previous.start;
|
|
2971
|
-
else standalone = false;
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
2974
|
-
let cutAfter = -1;
|
|
2975
|
-
const next = parts[last + 1];
|
|
2976
|
-
if (standalone && next) {
|
|
2977
|
-
if (next.kind !== "text") standalone = false;
|
|
2978
|
-
else {
|
|
2979
|
-
const newline = newlineIn(text, next.start, next.end);
|
|
2980
|
-
if (newline !== -1) {
|
|
2981
|
-
if (isInlineBlank(text, next.start, newline)) cutAfter = newline + 1;
|
|
2982
|
-
else standalone = false;
|
|
2983
|
-
} else if (last + 1 === parts.length - 1 && isInlineBlank(text, next.start, next.end)) cutAfter = next.end;
|
|
2984
|
-
else standalone = false;
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
if (standalone) {
|
|
2988
|
-
if (previous?.kind === "text") previous.to = Math.min(previous.to, cutBefore);
|
|
2989
|
-
for (let k = i; k <= last; k++) {
|
|
2990
|
-
const part = parts[k];
|
|
2991
|
-
if (part.kind === "text") part.from = part.to;
|
|
2992
|
-
else removed.set(part.start, part.end);
|
|
2993
|
-
}
|
|
2994
|
-
if (next?.kind === "text") next.from = Math.max(next.from, cutAfter);
|
|
2995
|
-
}
|
|
2996
|
-
i = last + 1;
|
|
2997
|
-
}
|
|
2998
|
-
return removed;
|
|
2999
|
-
};
|
|
3000
|
-
/** Finds the lines of text that hold content. See `TextLine`. */
|
|
3001
|
-
const textLines = (text, parts, removed) => {
|
|
3002
|
-
const lines = [];
|
|
3003
|
-
const add = (start) => {
|
|
3004
|
-
let indentEnd = start;
|
|
3005
|
-
while (text[indentEnd] === " " || text[indentEnd] === " ") indentEnd++;
|
|
3006
|
-
const next = text[indentEnd];
|
|
3007
|
-
if (next === void 0 || next === "\n" || next === "\r") return;
|
|
3008
|
-
if (removed.has(indentEnd)) return;
|
|
3009
|
-
lines.push({
|
|
3010
|
-
start,
|
|
3011
|
-
indentEnd
|
|
3012
|
-
});
|
|
3013
|
-
};
|
|
3014
|
-
const first = parts[0];
|
|
3015
|
-
const start = contentStart(text);
|
|
3016
|
-
if (first && (first.kind !== "text" || first.from === start)) add(start);
|
|
3017
|
-
for (const part of parts) {
|
|
3018
|
-
if (part.kind !== "text") continue;
|
|
3019
|
-
for (let newline = newlineIn(text, part.start, part.end); newline !== -1; newline = newlineIn(text, newline + 1, part.end)) {
|
|
3020
|
-
const start = newline + 1;
|
|
3021
|
-
if (start < part.from || start > part.to) continue;
|
|
3022
|
-
add(start);
|
|
3023
|
-
}
|
|
3024
|
-
}
|
|
3025
|
-
return lines;
|
|
3026
|
-
};
|
|
3027
|
-
//#endregion
|
|
3028
|
-
//#region src/parser/indent.ts
|
|
3029
|
-
/** The bodies a statement holds. Branches share their tag's body. A component's fills are bodies of their own, inside the component's. */
|
|
3030
|
-
const bodiesOf = (node) => {
|
|
3031
|
-
const own = (lists, full = false) => ({
|
|
3032
|
-
open: node.span.start,
|
|
3033
|
-
end: node.span.end,
|
|
3034
|
-
lists,
|
|
3035
|
-
full
|
|
3036
|
-
});
|
|
3037
|
-
switch (node.type) {
|
|
3038
|
-
case "If": return [own([...node.branches.map((branch) => branch.body), node.alternate ?? []])];
|
|
3039
|
-
case "For": return [own([node.body, node.alternate ?? []])];
|
|
3040
|
-
case "Component": return [...node.fills.map((fill) => ({
|
|
3041
|
-
open: fill.span.start,
|
|
3042
|
-
end: fill.span.end,
|
|
3043
|
-
lists: [fill.body],
|
|
3044
|
-
full: true
|
|
3045
|
-
})), own([node.body, ...node.fills.map((fill) => fill.body)], true)];
|
|
3046
|
-
case "Island": return [{
|
|
3047
|
-
...own([node.body]),
|
|
3048
|
-
keep: true
|
|
3049
|
-
}];
|
|
3050
|
-
case "Block":
|
|
3051
|
-
case "Slot":
|
|
3052
|
-
case "Add": return [own([node.body], true)];
|
|
3053
|
-
case "SetBlock":
|
|
3054
|
-
case "Macro":
|
|
3055
|
-
case "Cache":
|
|
3056
|
-
case "With":
|
|
3057
|
-
case "Apply":
|
|
3058
|
-
case "Autoescape": return [own([node.body])];
|
|
3059
|
-
default: return NO_BODIES;
|
|
3060
|
-
}
|
|
3061
|
-
};
|
|
3062
|
-
const NO_BODIES = [];
|
|
3063
|
-
/** The statement lists inside a statement, each once. Only a component's fills appear in more than one body. */
|
|
3064
|
-
const childLists = (bodies) => bodies.length === 1 ? bodies[0].lists : new Set(bodies.flatMap((body) => body.lists));
|
|
3065
|
-
/** The index of the first item whose key is at least `value`, in a list sorted by that key. */
|
|
3066
|
-
const firstAtLeast = (items, value, key) => {
|
|
3067
|
-
let low = 0;
|
|
3068
|
-
let high = items.length;
|
|
3069
|
-
while (low < high) {
|
|
3070
|
-
const middle = low + high >>> 1;
|
|
3071
|
-
if (key(items[middle]) < value) low = middle + 1;
|
|
3072
|
-
else high = middle;
|
|
3073
|
-
}
|
|
3074
|
-
return low;
|
|
3075
|
-
};
|
|
3076
|
-
/**
|
|
3077
|
-
* Finds `<pre>` and `<textarea>` tags, and the HTML comments, scripts and styles to skip, whose `<pre` is not a tag.
|
|
3078
|
-
* Each region runs to its end, or to the end of the source when it is not closed.
|
|
3079
|
-
*/
|
|
3080
|
-
const MARKUP = /<!--[\s\S]*?(?:-->|$)|<(script|style)(?=[\s/>])[\s\S]*?(?:<\/\1\s*>|$)|<(\/?)(pre|textarea)(?=[\s/>])/gi;
|
|
3081
|
-
const MAY_PREFORMAT = /<(?:pre|textarea)/i;
|
|
3082
|
-
/** Where `<pre>` and `<textarea>` elements start and end in the template's text, whose whitespace is content. */
|
|
3083
|
-
const preformatted = (text, textTokens) => {
|
|
3084
|
-
if (!MAY_PREFORMAT.test(text)) return [];
|
|
3085
|
-
let visible = "";
|
|
3086
|
-
for (const token of textTokens) {
|
|
3087
|
-
visible += "\0".repeat(token.start - visible.length);
|
|
3088
|
-
visible += text.slice(token.start, token.end);
|
|
3089
|
-
}
|
|
3090
|
-
const ranges = [];
|
|
3091
|
-
let open = null;
|
|
3092
|
-
MARKUP.lastIndex = 0;
|
|
3093
|
-
for (let match = MARKUP.exec(visible); match; match = MARKUP.exec(visible)) {
|
|
3094
|
-
const tag = match[3];
|
|
3095
|
-
if (tag === void 0) continue;
|
|
3096
|
-
const name = tag.toLowerCase();
|
|
3097
|
-
if (open === null && match[2] === "") open = {
|
|
3098
|
-
name,
|
|
3099
|
-
start: match.index
|
|
3100
|
-
};
|
|
3101
|
-
else if (open?.name === name && match[2] === "/") {
|
|
3102
|
-
ranges.push([open.start, match.index]);
|
|
3103
|
-
open = null;
|
|
3104
|
-
}
|
|
3105
|
-
}
|
|
3106
|
-
if (open) ranges.push([open.start, text.length]);
|
|
3107
|
-
return ranges;
|
|
3108
|
-
};
|
|
3109
|
-
/**
|
|
3110
|
-
* Works out the indentation of a template's text, so content can be inserted elsewhere and still line up.
|
|
3111
|
-
*
|
|
3112
|
-
* - 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.
|
|
3113
|
-
* - 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.
|
|
3114
|
-
* - Sets `lines` on text nodes: where each line of text starts, to take the indentation of where the text is inserted.
|
|
3115
|
-
*
|
|
3116
|
-
* 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.
|
|
3117
|
-
*/
|
|
3118
|
-
const arrangeIndentation = (ast, text, { tokens, standalone, lines }, placedNodes) => {
|
|
3119
|
-
const textTokens = tokens.filter((token) => token.type === "text");
|
|
3120
|
-
const pre = preformatted(text, textTokens);
|
|
3121
|
-
const inPre = (offset) => {
|
|
3122
|
-
const range = pre[firstAtLeast(pre, offset, (range) => range[1])];
|
|
3123
|
-
return range !== void 0 && range[0] < offset;
|
|
3124
|
-
};
|
|
3125
|
-
const textLines = pre.length > 0 ? lines.filter((line) => !inPre(line.start)) : lines;
|
|
3126
|
-
const lineStart = (offset) => text.lastIndexOf("\n", offset - 1) + 1 || contentStart(text);
|
|
3127
|
-
const lineOf = (offset) => {
|
|
3128
|
-
const start = lineStart(offset);
|
|
3129
|
-
let indentEnd = start;
|
|
3130
|
-
while (text[indentEnd] === " " || text[indentEnd] === " ") indentEnd++;
|
|
3131
|
-
return {
|
|
3132
|
-
start,
|
|
3133
|
-
indentEnd
|
|
3134
|
-
};
|
|
3135
|
-
};
|
|
3136
|
-
const placed = [];
|
|
3137
|
-
const islandEnds = [];
|
|
3138
|
-
for (const node of placedNodes) {
|
|
3139
|
-
if (inPre(node.span.start)) continue;
|
|
3140
|
-
if (node.type === "Island") {
|
|
3141
|
-
const end = text.lastIndexOf("{%", node.span.end - 1);
|
|
3142
|
-
if (standalone.has(end)) islandEnds.push({
|
|
3143
|
-
node,
|
|
3144
|
-
line: lineOf(end)
|
|
3145
|
-
});
|
|
3146
|
-
}
|
|
3147
|
-
if (!standalone.has(node.span.start)) continue;
|
|
3148
|
-
placed.push({
|
|
3149
|
-
node,
|
|
3150
|
-
line: lineOf(node.span.start)
|
|
3151
|
-
});
|
|
3152
|
-
}
|
|
3153
|
-
const contentLines = placed.length > 0 || islandEnds.length > 0 ? [
|
|
3154
|
-
...textLines,
|
|
3155
|
-
...placed.map((item) => item.line),
|
|
3156
|
-
...islandEnds.map((item) => item.line)
|
|
3157
|
-
].sort((a, b) => a.start - b.start) : textLines;
|
|
3158
|
-
const cuts = [];
|
|
3159
|
-
const indentOf = (offset) => {
|
|
3160
|
-
const line = lineOf(offset);
|
|
3161
|
-
return text.slice(line.start, line.indentEnd);
|
|
3162
|
-
};
|
|
3163
|
-
const trim = (body) => {
|
|
3164
|
-
if (body.keep) return;
|
|
3165
|
-
const openEnd = standalone.get(body.open);
|
|
3166
|
-
if (openEnd === void 0) return;
|
|
3167
|
-
const newline = text.indexOf("\n", openEnd);
|
|
3168
|
-
if (newline === -1) return;
|
|
3169
|
-
const to = text.lastIndexOf("{%", body.end - 1);
|
|
3170
|
-
const first = firstAtLeast(contentLines, newline + 1, (line) => line.start);
|
|
3171
|
-
const last = firstAtLeast(contentLines, to, (line) => line.start);
|
|
3172
|
-
if (first >= last) return;
|
|
3173
|
-
let shared = Infinity;
|
|
3174
|
-
const firstLine = contentLines[first];
|
|
3175
|
-
for (let i = first; i < last; i++) {
|
|
3176
|
-
const line = contentLines[i];
|
|
3177
|
-
let length = 0;
|
|
3178
|
-
while (length < line.indentEnd - line.start && text[line.start + length] === text[firstLine.start + length]) length++;
|
|
3179
|
-
shared = Math.min(shared, length);
|
|
3180
|
-
}
|
|
3181
|
-
let keep = 0;
|
|
3182
|
-
if (!body.full) {
|
|
3183
|
-
const own = indentOf(body.open);
|
|
3184
|
-
if (shared <= own.length) return;
|
|
3185
|
-
if (text.slice(firstLine.start, firstLine.start + own.length) !== own) return;
|
|
3186
|
-
keep = own.length;
|
|
3187
|
-
} else if (shared === 0) return;
|
|
3188
|
-
for (let i = first; i < last; i++) {
|
|
3189
|
-
const line = contentLines[i];
|
|
3190
|
-
cuts.push([line.start + keep, line.start + shared]);
|
|
3191
|
-
}
|
|
3192
|
-
};
|
|
3193
|
-
const visit = (list) => {
|
|
3194
|
-
for (const node of list) {
|
|
3195
|
-
const bodies = bodiesOf(node);
|
|
3196
|
-
if (bodies.length === 0) continue;
|
|
3197
|
-
for (const child of childLists(bodies)) visit(child);
|
|
3198
|
-
for (const body of bodies) trim(body);
|
|
3199
|
-
}
|
|
3200
|
-
};
|
|
3201
|
-
visit(ast.body);
|
|
3202
|
-
cuts.sort((a, b) => a[0] - b[0]);
|
|
3203
|
-
const merged = [];
|
|
3204
|
-
for (const range of cuts) {
|
|
3205
|
-
const previous = merged[merged.length - 1];
|
|
3206
|
-
if (previous && range[0] <= previous[1]) previous[1] = Math.max(previous[1], range[1]);
|
|
3207
|
-
else merged.push(range);
|
|
3208
|
-
}
|
|
3209
|
-
/** The source from `start` to `end`, without the cut ranges. */
|
|
3210
|
-
const uncut = (start, end) => {
|
|
3211
|
-
let out = "";
|
|
3212
|
-
let at = start;
|
|
3213
|
-
for (let i = firstAtLeast(merged, start, (range) => range[1] - 1); i < merged.length; i++) {
|
|
3214
|
-
const [from, to] = merged[i];
|
|
3215
|
-
if (from >= end) break;
|
|
3216
|
-
out += text.slice(at, Math.max(at, from));
|
|
3217
|
-
at = Math.max(at, to);
|
|
3218
|
-
}
|
|
3219
|
-
return out + text.slice(Math.min(at, end), end);
|
|
3220
|
-
};
|
|
3221
|
-
for (const { node, line } of placed) node.indent = uncut(line.start, line.indentEnd);
|
|
3222
|
-
for (const { node, line } of islandEnds) node.closing = {
|
|
3223
|
-
indent: line.start === lineOf(node.span.start).start ? null : uncut(line.start, line.indentEnd),
|
|
3224
|
-
newline: text.includes("\n", node.span.end)
|
|
3225
|
-
};
|
|
3226
|
-
const hasCut = (start, end) => {
|
|
3227
|
-
const range = merged[firstAtLeast(merged, start, (range) => range[1] - 1)];
|
|
3228
|
-
return range !== void 0 && range[0] < end;
|
|
3229
|
-
};
|
|
3230
|
-
const rewrite = (list) => {
|
|
3231
|
-
for (const node of list) {
|
|
3232
|
-
if (node.type === "Text") {
|
|
3233
|
-
let line = firstAtLeast(textLines, node.span.start, (l) => l.start);
|
|
3234
|
-
const hasLines = (textLines[line]?.start ?? Infinity) <= node.span.end;
|
|
3235
|
-
const cut = hasCut(node.span.start, node.span.end);
|
|
3236
|
-
if (!hasLines && !cut) continue;
|
|
3237
|
-
let value = "";
|
|
3238
|
-
let size = 0;
|
|
3239
|
-
const starts = [];
|
|
3240
|
-
for (let i = firstAtLeast(textTokens, node.span.start, (t) => t.start); i < textTokens.length; i++) {
|
|
3241
|
-
const token = textTokens[i];
|
|
3242
|
-
if (token.end > node.span.end) break;
|
|
3243
|
-
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));
|
|
3244
|
-
if (cut) {
|
|
3245
|
-
const piece = uncut(token.start, token.end);
|
|
3246
|
-
value += piece;
|
|
3247
|
-
size += piece.length;
|
|
3248
|
-
} else size += token.end - token.start;
|
|
3249
|
-
}
|
|
3250
|
-
if (cut) node.value = value;
|
|
3251
|
-
if (starts.length > 0) node.lines = starts;
|
|
3252
|
-
}
|
|
3253
|
-
const bodies = bodiesOf(node);
|
|
3254
|
-
if (bodies.length > 0) for (const child of childLists(bodies)) rewrite(child);
|
|
3255
|
-
}
|
|
3256
|
-
};
|
|
3257
|
-
rewrite(ast.body);
|
|
3258
|
-
};
|
|
3259
|
-
//#endregion
|
|
3260
|
-
//#region src/parser/parser.ts
|
|
3261
|
-
const isTypeName = (name) => TYPE_NAMES.includes(name);
|
|
3262
|
-
/** Names that cannot be declared as variables, because Kiln uses them itself. */
|
|
3263
|
-
const RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
3264
|
-
"true",
|
|
3265
|
-
"false",
|
|
3266
|
-
"null",
|
|
3267
|
-
"and",
|
|
3268
|
-
"or",
|
|
3269
|
-
"not",
|
|
3270
|
-
"in",
|
|
3271
|
-
"is",
|
|
3272
|
-
"loop",
|
|
3273
|
-
"slots"
|
|
3274
|
-
]);
|
|
3275
|
-
/** Parses template source into a syntax tree, collecting every problem instead of stopping at the first. */
|
|
3276
|
-
const parse = (text, options) => {
|
|
3277
|
-
const lexed = lex(text);
|
|
3278
|
-
const parser = new Parser(lexed.tokens, options);
|
|
3279
|
-
const ast = parser.parseTemplate(text.length);
|
|
3280
|
-
arrangeIndentation(ast, text, lexed, parser.placed);
|
|
3281
|
-
return {
|
|
3282
|
-
ast,
|
|
3283
|
-
problems: [...lexed.problems, ...parser.problems]
|
|
3284
|
-
};
|
|
3285
|
-
};
|
|
3286
|
-
var ParseFailure = class {
|
|
3287
|
-
code;
|
|
3288
|
-
message;
|
|
3289
|
-
span;
|
|
3290
|
-
constructor(code, message, span) {
|
|
3291
|
-
this.code = code;
|
|
3292
|
-
this.message = message;
|
|
3293
|
-
this.span = span;
|
|
3294
|
-
}
|
|
3295
|
-
};
|
|
3296
|
-
/** Words that can follow a one-word test, so they never form part of a two-word test name. */
|
|
3297
|
-
const NOT_TEST_WORDS = /* @__PURE__ */ new Set([
|
|
3298
|
-
"and",
|
|
3299
|
-
"or",
|
|
3300
|
-
"not",
|
|
3301
|
-
"in",
|
|
3302
|
-
"is",
|
|
3303
|
-
"starts",
|
|
3304
|
-
"ends",
|
|
3305
|
-
"with",
|
|
3306
|
-
"ignore",
|
|
3307
|
-
"as"
|
|
3308
|
-
]);
|
|
3309
|
-
const COMPARISON_PUNCTUATORS = /* @__PURE__ */ new Set([
|
|
3310
|
-
"==",
|
|
3311
|
-
"!=",
|
|
3312
|
-
"<",
|
|
3313
|
-
">",
|
|
3314
|
-
"<=",
|
|
3315
|
-
">="
|
|
3316
|
-
]);
|
|
3317
|
-
const describeToken = (token) => {
|
|
3318
|
-
switch (token.type) {
|
|
3319
|
-
case "eof": return "the end of the template";
|
|
3320
|
-
case "tagClose": return "\"%}\"";
|
|
3321
|
-
case "outputClose": return "\"}}\"";
|
|
3322
|
-
case "text": return "text";
|
|
3323
|
-
case "string": return "a string";
|
|
3324
|
-
case "stringStart": return "an interpolated string";
|
|
3325
|
-
case "number": return `number ${token.value}`;
|
|
3326
|
-
default: return `"${token.value}"`;
|
|
3327
|
-
}
|
|
3328
|
-
};
|
|
3329
|
-
var Parser = class {
|
|
3330
|
-
problems = [];
|
|
3331
|
-
/** Every tag that inserts content, for `arrangeIndentation`. */
|
|
3332
|
-
placed = [];
|
|
3333
|
-
#tokens;
|
|
3334
|
-
#options;
|
|
3335
|
-
#index = 0;
|
|
3336
|
-
#lastEnd = 0;
|
|
3337
|
-
#nesting = 0;
|
|
3338
|
-
/** End tag names of every open block tag, innermost last. */
|
|
3339
|
-
#endStack = [];
|
|
3340
|
-
constructor(tokens, options) {
|
|
3341
|
-
this.#tokens = tokens;
|
|
3342
|
-
this.#options = options;
|
|
3343
|
-
}
|
|
3344
|
-
parseTemplate(length) {
|
|
3345
|
-
const { body } = this.#parseBody([]);
|
|
3346
|
-
return {
|
|
3347
|
-
type: "Template",
|
|
3348
|
-
body,
|
|
3349
|
-
span: {
|
|
3350
|
-
start: 0,
|
|
3351
|
-
end: length
|
|
3352
|
-
}
|
|
3353
|
-
};
|
|
3354
|
-
}
|
|
3355
|
-
#peek(offset = 0) {
|
|
3356
|
-
return this.#tokens[this.#index + offset] ?? this.#tokens[this.#tokens.length - 1];
|
|
3357
|
-
}
|
|
3358
|
-
#next() {
|
|
3359
|
-
const token = this.#peek();
|
|
3360
|
-
if (token.type !== "eof") this.#index++;
|
|
3361
|
-
this.#lastEnd = token.end;
|
|
3362
|
-
return token;
|
|
3363
|
-
}
|
|
3364
|
-
#isPunct(value, offset = 0) {
|
|
3365
|
-
const token = this.#peek(offset);
|
|
3366
|
-
return token.type === "punct" && token.value === value;
|
|
3367
|
-
}
|
|
3368
|
-
#isName(value, offset = 0) {
|
|
3369
|
-
const token = this.#peek(offset);
|
|
3370
|
-
return token.type === "name" && token.value === value;
|
|
3371
|
-
}
|
|
3372
|
-
#fail(message, token = this.#peek(), code = "unexpected-token") {
|
|
3373
|
-
throw new ParseFailure(code, message, {
|
|
3374
|
-
start: token.start,
|
|
3375
|
-
end: Math.max(token.end, token.start + 1)
|
|
3376
|
-
});
|
|
3377
|
-
}
|
|
3378
|
-
#expectPunct(value) {
|
|
3379
|
-
if (!this.#isPunct(value)) this.#fail(`Expected "${value}" but found ${describeToken(this.#peek())}`);
|
|
3380
|
-
return this.#next();
|
|
3381
|
-
}
|
|
3382
|
-
#expectName(what = "a name") {
|
|
3383
|
-
const token = this.#peek();
|
|
3384
|
-
if (token.type !== "name") this.#fail(`Expected ${what} but found ${describeToken(token)}`);
|
|
3385
|
-
return this.#next();
|
|
3386
|
-
}
|
|
3387
|
-
#expectKeyword(value) {
|
|
3388
|
-
if (!this.#isName(value)) this.#fail(`Expected "${value}" but found ${describeToken(this.#peek())}`);
|
|
3389
|
-
return this.#next();
|
|
3390
|
-
}
|
|
3391
|
-
#expectTagEnd() {
|
|
3392
|
-
const token = this.#peek();
|
|
3393
|
-
if (token.type !== "tagClose") this.#fail(`Expected "%}" but found ${describeToken(token)}`);
|
|
3394
|
-
return this.#next();
|
|
3395
|
-
}
|
|
3396
|
-
#expectDeclarableName(what) {
|
|
3397
|
-
const token = this.#expectName(what);
|
|
3398
|
-
if (RESERVED_NAMES.has(token.value)) this.#problem("reserved-name", `"${token.value}" is reserved and cannot be used as a name`, token);
|
|
3399
|
-
return token;
|
|
3400
|
-
}
|
|
3401
|
-
#problem(code, message, span) {
|
|
3402
|
-
this.problems.push({
|
|
3403
|
-
code,
|
|
3404
|
-
message,
|
|
3405
|
-
span: {
|
|
3406
|
-
start: span.start,
|
|
3407
|
-
end: span.end
|
|
3408
|
-
}
|
|
3409
|
-
});
|
|
3410
|
-
}
|
|
3411
|
-
/** Skips to just after the end of the current tag or output. */
|
|
3412
|
-
#recover() {
|
|
3413
|
-
for (;;) {
|
|
3414
|
-
const token = this.#peek();
|
|
3415
|
-
if (token.type === "eof") return;
|
|
3416
|
-
if (token.type === "tagClose" || token.type === "outputClose") {
|
|
3417
|
-
this.#next();
|
|
3418
|
-
return;
|
|
3419
|
-
}
|
|
3420
|
-
if (token.type === "text" || token.type === "tagOpen" || token.type === "outputOpen") return;
|
|
3421
|
-
this.#next();
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
#record(failure) {
|
|
3425
|
-
if (!(failure instanceof ParseFailure)) throw failure;
|
|
3426
|
-
this.#problem(failure.code, failure.message, failure.span);
|
|
3427
|
-
this.#recover();
|
|
3428
|
-
}
|
|
3429
|
-
/**
|
|
3430
|
-
* 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.
|
|
3431
|
-
*/
|
|
3432
|
-
#header(parse, fallback) {
|
|
3433
|
-
try {
|
|
3434
|
-
const result = parse();
|
|
3435
|
-
this.#expectTagEnd();
|
|
3436
|
-
return result;
|
|
3437
|
-
} catch (failure) {
|
|
3438
|
-
this.#record(failure);
|
|
3439
|
-
return fallback;
|
|
3440
|
-
}
|
|
3441
|
-
}
|
|
3442
|
-
/** Whether the current tag holds the punctuator `value` before its `%}`. */
|
|
3443
|
-
#tagHasPunct(value) {
|
|
3444
|
-
for (let offset = 0;; offset++) {
|
|
3445
|
-
const token = this.#peek(offset);
|
|
3446
|
-
if (token.type === "punct" && token.value === value) return true;
|
|
3447
|
-
if (token.type === "tagClose" || token.type === "eof" || token.type === "text" || token.type === "tagOpen" || token.type === "outputOpen") return false;
|
|
3448
|
-
}
|
|
3449
|
-
}
|
|
3450
|
-
#invalid() {
|
|
3451
|
-
const token = this.#peek();
|
|
3452
|
-
return {
|
|
3453
|
-
type: "Invalid",
|
|
3454
|
-
span: {
|
|
3455
|
-
start: token.start,
|
|
3456
|
-
end: token.end
|
|
3457
|
-
}
|
|
3458
|
-
};
|
|
3459
|
-
}
|
|
3460
|
-
#parseBody(ends) {
|
|
3461
|
-
this.#enter();
|
|
3462
|
-
const body = [];
|
|
3463
|
-
this.#endStack.push(ends);
|
|
3464
|
-
try {
|
|
3465
|
-
for (;;) {
|
|
3466
|
-
const token = this.#peek();
|
|
3467
|
-
switch (token.type) {
|
|
3468
|
-
case "eof": return {
|
|
3469
|
-
body,
|
|
3470
|
-
end: null
|
|
3471
|
-
};
|
|
3472
|
-
case "text": {
|
|
3473
|
-
this.#next();
|
|
3474
|
-
const previous = body[body.length - 1];
|
|
3475
|
-
if (previous?.type === "Text") {
|
|
3476
|
-
previous.value += token.value;
|
|
3477
|
-
previous.span = {
|
|
3478
|
-
start: previous.span.start,
|
|
3479
|
-
end: token.end
|
|
3480
|
-
};
|
|
3481
|
-
} else body.push({
|
|
3482
|
-
type: "Text",
|
|
3483
|
-
value: token.value,
|
|
3484
|
-
span: {
|
|
3485
|
-
start: token.start,
|
|
3486
|
-
end: token.end
|
|
3487
|
-
}
|
|
3488
|
-
});
|
|
3489
|
-
break;
|
|
3490
|
-
}
|
|
3491
|
-
case "outputOpen":
|
|
3492
|
-
this.#parseOutput(body);
|
|
3493
|
-
break;
|
|
3494
|
-
case "tagOpen": {
|
|
3495
|
-
const name = this.#peek(1);
|
|
3496
|
-
if (name.type === "name") {
|
|
3497
|
-
if (ends.includes(name.value)) {
|
|
3498
|
-
this.#next();
|
|
3499
|
-
this.#next();
|
|
3500
|
-
return {
|
|
3501
|
-
body,
|
|
3502
|
-
end: name
|
|
3503
|
-
};
|
|
3504
|
-
}
|
|
3505
|
-
if (this.#endStack.some((outer) => outer.includes(name.value))) return {
|
|
3506
|
-
body,
|
|
3507
|
-
end: null
|
|
3508
|
-
};
|
|
3509
|
-
}
|
|
3510
|
-
this.#parseTag(body);
|
|
3511
|
-
break;
|
|
3512
|
-
}
|
|
3513
|
-
default: this.#next();
|
|
3514
|
-
}
|
|
3515
|
-
}
|
|
3516
|
-
} finally {
|
|
3517
|
-
this.#endStack.pop();
|
|
3518
|
-
this.#nesting--;
|
|
3519
|
-
}
|
|
3520
|
-
}
|
|
3521
|
-
#parseOutput(body) {
|
|
3522
|
-
const open = this.#next();
|
|
3523
|
-
try {
|
|
3524
|
-
const expression = this.#parseExpression();
|
|
3525
|
-
const close = this.#peek();
|
|
3526
|
-
if (close.type !== "outputClose") this.#fail(`Expected "}}" but found ${describeToken(close)}`);
|
|
3527
|
-
this.#next();
|
|
3528
|
-
body.push({
|
|
3529
|
-
type: "Output",
|
|
3530
|
-
expression,
|
|
3531
|
-
span: {
|
|
3532
|
-
start: open.start,
|
|
3533
|
-
end: close.end
|
|
3534
|
-
}
|
|
3535
|
-
});
|
|
3536
|
-
} catch (failure) {
|
|
3537
|
-
this.#record(failure);
|
|
3538
|
-
}
|
|
3539
|
-
}
|
|
3540
|
-
#parseTag(body) {
|
|
3541
|
-
const open = this.#next();
|
|
3542
|
-
const nameToken = this.#peek();
|
|
3543
|
-
if (nameToken.type !== "name") {
|
|
3544
|
-
this.#problem("unexpected-token", `Expected a tag name but found ${describeToken(nameToken)}`, nameToken);
|
|
3545
|
-
this.#recover();
|
|
3546
|
-
return;
|
|
3547
|
-
}
|
|
3548
|
-
this.#next();
|
|
3549
|
-
const name = nameToken.value;
|
|
3550
|
-
try {
|
|
3551
|
-
const statement = this.#parseNamedTag(name, open, nameToken);
|
|
3552
|
-
if (statement) body.push(statement);
|
|
3553
|
-
} catch (failure) {
|
|
3554
|
-
this.#record(failure);
|
|
3555
|
-
}
|
|
3556
|
-
}
|
|
3557
|
-
#parseNamedTag(name, open, nameToken) {
|
|
3558
|
-
switch (name) {
|
|
3559
|
-
case "if": return this.#parseIf(open);
|
|
3560
|
-
case "for": return this.#parseFor(open);
|
|
3561
|
-
case "set": return this.#parseSet(open);
|
|
3562
|
-
case "props": return this.#parseProps(open);
|
|
3563
|
-
case "extends": {
|
|
3564
|
-
const template = this.#parseExpression();
|
|
3565
|
-
const close = this.#expectTagEnd();
|
|
3566
|
-
return {
|
|
3567
|
-
type: "Extends",
|
|
3568
|
-
template,
|
|
3569
|
-
span: {
|
|
3570
|
-
start: open.start,
|
|
3571
|
-
end: close.end
|
|
3572
|
-
}
|
|
3573
|
-
};
|
|
3574
|
-
}
|
|
3575
|
-
case "block": return this.#parseBlock(open);
|
|
3576
|
-
case "include": return this.#parseInclude(open);
|
|
3577
|
-
case "component": return this.#parseComponent(open);
|
|
3578
|
-
case "slot": return this.#parseSlot(open);
|
|
3579
|
-
case "macro": return this.#parseMacro(open);
|
|
3580
|
-
case "import": return this.#parseImport(open);
|
|
3581
|
-
case "from": return this.#parseFromImport(open);
|
|
3582
|
-
case "with": return this.#parseWith(open);
|
|
3583
|
-
case "cache": return this.#parseCache(open);
|
|
3584
|
-
case "island": return this.#parseIsland(open);
|
|
3585
|
-
case "outlet": {
|
|
3586
|
-
const name = this.#parseStringLiteral("an outlet name");
|
|
3587
|
-
const close = this.#expectTagEnd();
|
|
3588
|
-
return this.#placed({
|
|
3589
|
-
type: "Outlet",
|
|
3590
|
-
name,
|
|
3591
|
-
span: {
|
|
3592
|
-
start: open.start,
|
|
3593
|
-
end: close.end
|
|
3594
|
-
}
|
|
3595
|
-
});
|
|
3596
|
-
}
|
|
3597
|
-
case "flush": {
|
|
3598
|
-
const close = this.#expectTagEnd();
|
|
3599
|
-
return {
|
|
3600
|
-
type: "Flush",
|
|
3601
|
-
span: {
|
|
3602
|
-
start: open.start,
|
|
3603
|
-
end: close.end
|
|
3604
|
-
}
|
|
3605
|
-
};
|
|
3606
|
-
}
|
|
3607
|
-
case "add": return this.#parseAdd(open);
|
|
3608
|
-
case "apply": return this.#parseApply(open);
|
|
3609
|
-
case "autoescape": return this.#parseAutoescape(open);
|
|
3610
|
-
case "verbatim": return this.#parseVerbatim(open);
|
|
3611
|
-
default: {
|
|
3612
|
-
const stray = name.startsWith("end") || name === "else" || name === "elseif" || name === "fill";
|
|
3613
|
-
this.#problem(stray ? "unexpected-tag" : "unknown-tag", stray ? `"${name}" has no matching opening tag here` : `Unknown tag "${name}"`, nameToken);
|
|
3614
|
-
this.#recover();
|
|
3615
|
-
return null;
|
|
3616
|
-
}
|
|
3617
|
-
}
|
|
3618
|
-
}
|
|
3619
|
-
/** Reports a block tag whose end tag never came. */
|
|
3620
|
-
#unclosed(name, open, endName) {
|
|
3621
|
-
this.#problem("unclosed-block", `"${name}" is missing its "${endName}" tag`, {
|
|
3622
|
-
start: open.start,
|
|
3623
|
-
end: open.end
|
|
3624
|
-
});
|
|
3625
|
-
}
|
|
3626
|
-
/** Consumes an end tag's `%}`, plus an optional repeated name such as `endblock content`. */
|
|
3627
|
-
#finishEndTag(expectedName) {
|
|
3628
|
-
try {
|
|
3629
|
-
if (expectedName !== void 0 && this.#peek().type === "name") {
|
|
3630
|
-
const repeated = this.#next();
|
|
3631
|
-
if (repeated.value !== expectedName) this.#problem("mismatched-end", `End tag names "${repeated.value}" but closes "${expectedName}"`, repeated);
|
|
3632
|
-
}
|
|
3633
|
-
this.#expectTagEnd();
|
|
3634
|
-
} catch (failure) {
|
|
3635
|
-
this.#record(failure);
|
|
3636
|
-
}
|
|
3637
|
-
}
|
|
3638
|
-
#parseIf(open) {
|
|
3639
|
-
const branches = [];
|
|
3640
|
-
let test = this.#header(() => this.#parseExpression(), this.#invalid());
|
|
3641
|
-
let alternate = null;
|
|
3642
|
-
for (;;) {
|
|
3643
|
-
const { body, end } = this.#parseBody([
|
|
3644
|
-
"elseif",
|
|
3645
|
-
"else",
|
|
3646
|
-
"endif"
|
|
3647
|
-
]);
|
|
3648
|
-
branches.push({
|
|
3649
|
-
test,
|
|
3650
|
-
body
|
|
3651
|
-
});
|
|
3652
|
-
if (!end) {
|
|
3653
|
-
this.#unclosed("if", open, "endif");
|
|
3654
|
-
break;
|
|
3655
|
-
}
|
|
3656
|
-
if (end.value === "elseif") {
|
|
3657
|
-
test = this.#header(() => this.#parseExpression(), this.#invalid());
|
|
3658
|
-
continue;
|
|
3659
|
-
}
|
|
3660
|
-
if (end.value === "else") {
|
|
3661
|
-
this.#finishEndTag();
|
|
3662
|
-
alternate = this.#parseAlternate("if", open, ["elseif", "else"]);
|
|
3663
|
-
break;
|
|
3664
|
-
}
|
|
3665
|
-
this.#finishEndTag();
|
|
3666
|
-
break;
|
|
3667
|
-
}
|
|
3668
|
-
return {
|
|
3669
|
-
type: "If",
|
|
3670
|
-
branches,
|
|
3671
|
-
alternate,
|
|
3672
|
-
span: {
|
|
3673
|
-
start: open.start,
|
|
3674
|
-
end: this.#lastEnd
|
|
3675
|
-
}
|
|
3676
|
-
};
|
|
3677
|
-
}
|
|
3678
|
-
/**
|
|
3679
|
-
* Parses the `else` branch of `if` or `for` up to its end tag. A branch tag after `else`, such as a second `else`,
|
|
3680
|
-
* is reported where it stands, so it can't close an outer tag instead.
|
|
3681
|
-
*/
|
|
3682
|
-
#parseAlternate(name, open, branches) {
|
|
3683
|
-
const endName = `end${name}`;
|
|
3684
|
-
const alternate = [];
|
|
3685
|
-
for (;;) {
|
|
3686
|
-
const { body, end } = this.#parseBody([endName, ...branches]);
|
|
3687
|
-
alternate.push(...body);
|
|
3688
|
-
if (!end) {
|
|
3689
|
-
this.#unclosed(name, open, endName);
|
|
3690
|
-
return alternate;
|
|
3691
|
-
}
|
|
3692
|
-
if (end.value === endName) {
|
|
3693
|
-
this.#finishEndTag();
|
|
3694
|
-
return alternate;
|
|
3695
|
-
}
|
|
3696
|
-
this.#problem("unexpected-tag", `"${end.value}" cannot follow "else" in "${name}"`, end);
|
|
3697
|
-
this.#recover();
|
|
3698
|
-
}
|
|
3699
|
-
}
|
|
3700
|
-
#parseFor(open) {
|
|
3701
|
-
const header = this.#header(() => {
|
|
3702
|
-
let keyName = null;
|
|
3703
|
-
let valueName = this.#expectDeclarableName("a loop variable").value;
|
|
3704
|
-
if (this.#isPunct(",")) {
|
|
3705
|
-
this.#next();
|
|
3706
|
-
keyName = valueName;
|
|
3707
|
-
valueName = this.#expectDeclarableName("a loop variable").value;
|
|
3708
|
-
}
|
|
3709
|
-
this.#expectKeyword("in");
|
|
3710
|
-
const iterable = this.#parseExpression();
|
|
3711
|
-
return {
|
|
3712
|
-
keyName,
|
|
3713
|
-
valueName,
|
|
3714
|
-
iterable
|
|
3715
|
-
};
|
|
3716
|
-
}, {
|
|
3717
|
-
keyName: null,
|
|
3718
|
-
valueName: "_",
|
|
3719
|
-
iterable: this.#invalid()
|
|
3720
|
-
});
|
|
3721
|
-
const { body, end } = this.#parseBody(["else", "endfor"]);
|
|
3722
|
-
let alternate = null;
|
|
3723
|
-
if (!end) this.#unclosed("for", open, "endfor");
|
|
3724
|
-
else if (end.value === "else") {
|
|
3725
|
-
this.#finishEndTag();
|
|
3726
|
-
alternate = this.#parseAlternate("for", open, ["else"]);
|
|
3727
|
-
} else this.#finishEndTag();
|
|
3728
|
-
return {
|
|
3729
|
-
type: "For",
|
|
3730
|
-
...header,
|
|
3731
|
-
body,
|
|
3732
|
-
alternate,
|
|
3733
|
-
span: {
|
|
3734
|
-
start: open.start,
|
|
3735
|
-
end: this.#lastEnd
|
|
3736
|
-
}
|
|
3737
|
-
};
|
|
3738
|
-
}
|
|
3739
|
-
#parseSet(open) {
|
|
3740
|
-
if (this.#tagHasPunct("=")) {
|
|
3741
|
-
const name = this.#expectDeclarableName("a variable name").value;
|
|
3742
|
-
this.#expectPunct("=");
|
|
3743
|
-
const value = this.#parseExpression();
|
|
3744
|
-
const close = this.#expectTagEnd();
|
|
3745
|
-
return {
|
|
3746
|
-
type: "Set",
|
|
3747
|
-
name,
|
|
3748
|
-
value,
|
|
3749
|
-
span: {
|
|
3750
|
-
start: open.start,
|
|
3751
|
-
end: close.end
|
|
3752
|
-
}
|
|
3753
|
-
};
|
|
3754
|
-
}
|
|
3755
|
-
const name = this.#header(() => this.#expectDeclarableName("a variable name").value, "");
|
|
3756
|
-
const { body, end } = this.#parseBody(["endset"]);
|
|
3757
|
-
if (end) this.#finishEndTag();
|
|
3758
|
-
else this.#unclosed("set", open, "endset");
|
|
3759
|
-
return {
|
|
3760
|
-
type: "SetBlock",
|
|
3761
|
-
name,
|
|
3762
|
-
body,
|
|
3763
|
-
span: {
|
|
3764
|
-
start: open.start,
|
|
3765
|
-
end: this.#lastEnd
|
|
3766
|
-
}
|
|
3767
|
-
};
|
|
3768
|
-
}
|
|
3769
|
-
/** Reads parameters for `macro` and `props`. Only `props` may declare types, so macros' `type` is always null. */
|
|
3770
|
-
#parseParams(closing, typed = false) {
|
|
3771
|
-
const params = [];
|
|
3772
|
-
const done = () => closing === null ? this.#peek().type === "tagClose" || this.#isPunct("...") : this.#isPunct(closing);
|
|
3773
|
-
while (!done()) {
|
|
3774
|
-
const nameToken = this.#expectDeclarableName("a parameter name");
|
|
3775
|
-
if (params.some((param) => param.name === nameToken.value)) this.#problem("duplicate-parameter", `"${nameToken.value}" is declared more than once`, nameToken);
|
|
3776
|
-
let type = null;
|
|
3777
|
-
if (typed && this.#isPunct(":")) {
|
|
3778
|
-
this.#next();
|
|
3779
|
-
type = this.#parsePropType();
|
|
3780
|
-
}
|
|
3781
|
-
let defaultValue = null;
|
|
3782
|
-
if (this.#isPunct("=")) {
|
|
3783
|
-
this.#next();
|
|
3784
|
-
defaultValue = this.#parseExpression();
|
|
3785
|
-
}
|
|
3786
|
-
params.push({
|
|
3787
|
-
name: nameToken.value,
|
|
3788
|
-
type,
|
|
3789
|
-
default: defaultValue,
|
|
3790
|
-
span: {
|
|
3791
|
-
start: nameToken.start,
|
|
3792
|
-
end: this.#lastEnd
|
|
3793
|
-
}
|
|
3794
|
-
});
|
|
3795
|
-
if (!this.#isPunct(",")) break;
|
|
3796
|
-
this.#next();
|
|
3797
|
-
}
|
|
3798
|
-
return params;
|
|
3799
|
-
}
|
|
3800
|
-
/** Reads a prop type such as `string | null`, `"primary" | "secondary"`, `Post[]` or `{ name: string }`. */
|
|
3801
|
-
#parsePropType() {
|
|
3802
|
-
this.#enter();
|
|
3803
|
-
const nesting = this.#nesting;
|
|
3804
|
-
try {
|
|
3805
|
-
const members = [];
|
|
3806
|
-
for (;;) {
|
|
3807
|
-
let type = this.#parsePropTypeTerm();
|
|
3808
|
-
while (this.#isPunct("[") && this.#isPunct("]", 1)) {
|
|
3809
|
-
this.#enter();
|
|
3810
|
-
this.#next();
|
|
3811
|
-
this.#next();
|
|
3812
|
-
type = [{ list: type }];
|
|
3813
|
-
}
|
|
3814
|
-
this.#nesting = nesting;
|
|
3815
|
-
members.push(...type);
|
|
3816
|
-
if (!this.#isPunct("|")) return members;
|
|
3817
|
-
this.#next();
|
|
3818
|
-
}
|
|
3819
|
-
} finally {
|
|
3820
|
-
this.#nesting = nesting - 1;
|
|
3821
|
-
}
|
|
3822
|
-
}
|
|
3823
|
-
/** Reads one part of a union. Brackets return the union inside them, so `(2 | 3)[]` lists either. */
|
|
3824
|
-
#parsePropTypeTerm() {
|
|
3825
|
-
const token = this.#peek();
|
|
3826
|
-
if (token.type === "punct" && token.value === "(") {
|
|
3827
|
-
this.#next();
|
|
3828
|
-
const inner = this.#parsePropType();
|
|
3829
|
-
this.#expectPunct(")");
|
|
3830
|
-
return inner;
|
|
3831
|
-
}
|
|
3832
|
-
if (token.type === "punct" && token.value === "{") return [{ shape: this.#parseShape() }];
|
|
3833
|
-
this.#next();
|
|
3834
|
-
if (token.type === "string") return [{ literal: token.value }];
|
|
3835
|
-
if (token.type === "number") return [{ literal: Number(token.value) }];
|
|
3836
|
-
if (token.type === "name" && isTypeName(token.value)) return [token.value];
|
|
3837
|
-
if (token.type === "name" && /^[A-Z]/.test(token.value)) return [{ named: token.value }];
|
|
3838
|
-
if (token.type === "name") {
|
|
3839
|
-
this.#problem("unknown-type", `Unknown type "${token.value}"`, token);
|
|
3840
|
-
return [];
|
|
3841
|
-
}
|
|
3842
|
-
return this.#fail(`Expected a type but found ${describeToken(token)}`, token);
|
|
3843
|
-
}
|
|
3844
|
-
/** Reads a shape type such as `{ name: string, url?: string }`. */
|
|
3845
|
-
#parseShape() {
|
|
3846
|
-
this.#expectPunct("{");
|
|
3847
|
-
const keys = [];
|
|
3848
|
-
while (!this.#isPunct("}")) {
|
|
3849
|
-
const token = this.#next();
|
|
3850
|
-
if (token.type !== "name" && token.type !== "string") this.#fail(`Expected a key but found ${describeToken(token)}`, token);
|
|
3851
|
-
if (keys.some((key) => key.name === token.value)) this.#problem("duplicate-key", `"${token.value}" is declared more than once`, token);
|
|
3852
|
-
const optional = this.#isPunct("?:") || this.#isPunct("?");
|
|
3853
|
-
if (this.#isPunct("?:")) this.#next();
|
|
3854
|
-
else {
|
|
3855
|
-
if (optional) this.#next();
|
|
3856
|
-
this.#expectPunct(":");
|
|
3857
|
-
}
|
|
3858
|
-
keys.push({
|
|
3859
|
-
name: token.value,
|
|
3860
|
-
optional,
|
|
3861
|
-
type: this.#parsePropType()
|
|
3862
|
-
});
|
|
3863
|
-
if (!this.#isPunct(",")) break;
|
|
3864
|
-
this.#next();
|
|
3865
|
-
}
|
|
3866
|
-
this.#expectPunct("}");
|
|
3867
|
-
return keys;
|
|
3868
|
-
}
|
|
3869
|
-
#parseProps(open) {
|
|
3870
|
-
const props = this.#parseParams(null, true);
|
|
3871
|
-
let rest = null;
|
|
3872
|
-
if (this.#isPunct("...")) {
|
|
3873
|
-
this.#next();
|
|
3874
|
-
const nameToken = this.#expectDeclarableName("a rest prop name");
|
|
3875
|
-
if (props.some((prop) => prop.name === nameToken.value)) this.#problem("duplicate-parameter", `"${nameToken.value}" is declared more than once`, nameToken);
|
|
3876
|
-
rest = nameToken.value;
|
|
3877
|
-
if (this.#isPunct(",")) this.#next();
|
|
3878
|
-
if (this.#peek().type !== "tagClose") this.#fail("The rest prop must come last, with no type or default");
|
|
3879
|
-
}
|
|
3880
|
-
const close = this.#expectTagEnd();
|
|
3881
|
-
return {
|
|
3882
|
-
type: "Props",
|
|
3883
|
-
props,
|
|
3884
|
-
rest,
|
|
3885
|
-
span: {
|
|
3886
|
-
start: open.start,
|
|
3887
|
-
end: close.end
|
|
3888
|
-
}
|
|
3889
|
-
};
|
|
3890
|
-
}
|
|
3891
|
-
#parseBlock(open) {
|
|
3892
|
-
const first = this.#peek();
|
|
3893
|
-
if (first.type !== "name" && (first.type === "tagClose" || this.#peek(1).type === "tagClose")) {
|
|
3894
|
-
this.#header(() => {
|
|
3895
|
-
this.#expectName("a block name");
|
|
3896
|
-
}, void 0);
|
|
3897
|
-
const { end } = this.#parseBody(["endblock"]);
|
|
3898
|
-
if (end) this.#finishEndTag();
|
|
3899
|
-
else this.#unclosed("block", open, "endblock");
|
|
3900
|
-
return null;
|
|
3901
|
-
}
|
|
3902
|
-
const name = this.#expectName("a block name").value;
|
|
3903
|
-
if (this.#peek().type !== "tagClose") {
|
|
3904
|
-
const expression = this.#parseExpression();
|
|
3905
|
-
const close = this.#expectTagEnd();
|
|
3906
|
-
return this.#placed({
|
|
3907
|
-
type: "Block",
|
|
3908
|
-
name,
|
|
3909
|
-
body: [{
|
|
3910
|
-
type: "Output",
|
|
3911
|
-
expression,
|
|
3912
|
-
span: {
|
|
3913
|
-
start: expression.span.start,
|
|
3914
|
-
end: expression.span.end
|
|
3915
|
-
}
|
|
3916
|
-
}],
|
|
3917
|
-
span: {
|
|
3918
|
-
start: open.start,
|
|
3919
|
-
end: close.end
|
|
3920
|
-
}
|
|
3921
|
-
});
|
|
3922
|
-
}
|
|
3923
|
-
this.#expectTagEnd();
|
|
3924
|
-
const { body, end } = this.#parseBody(["endblock"]);
|
|
3925
|
-
if (end) this.#finishEndTag(name);
|
|
3926
|
-
else this.#unclosed("block", open, "endblock");
|
|
3927
|
-
return this.#placed({
|
|
3928
|
-
type: "Block",
|
|
3929
|
-
name,
|
|
3930
|
-
body,
|
|
3931
|
-
span: {
|
|
3932
|
-
start: open.start,
|
|
3933
|
-
end: this.#lastEnd
|
|
3934
|
-
}
|
|
3935
|
-
});
|
|
3936
|
-
}
|
|
3937
|
-
#parseInclude(open) {
|
|
3938
|
-
const template = this.#parseExpression();
|
|
3939
|
-
let withValue = null;
|
|
3940
|
-
let ignoreMissing = false;
|
|
3941
|
-
if (this.#isName("with")) {
|
|
3942
|
-
this.#next();
|
|
3943
|
-
withValue = this.#parseExpression();
|
|
3944
|
-
}
|
|
3945
|
-
if (this.#isName("ignore")) {
|
|
3946
|
-
this.#next();
|
|
3947
|
-
this.#expectKeyword("missing");
|
|
3948
|
-
ignoreMissing = true;
|
|
3949
|
-
}
|
|
3950
|
-
const close = this.#expectTagEnd();
|
|
3951
|
-
return this.#placed({
|
|
3952
|
-
type: "Include",
|
|
3953
|
-
template,
|
|
3954
|
-
with: withValue,
|
|
3955
|
-
ignoreMissing,
|
|
3956
|
-
span: {
|
|
3957
|
-
start: open.start,
|
|
3958
|
-
end: close.end
|
|
3959
|
-
}
|
|
3960
|
-
});
|
|
3961
|
-
}
|
|
3962
|
-
#parseComponent(open) {
|
|
3963
|
-
const header = this.#header(() => {
|
|
3964
|
-
const template = this.#parseExpression();
|
|
3965
|
-
let withValue = null;
|
|
3966
|
-
if (this.#isName("with")) {
|
|
3967
|
-
this.#next();
|
|
3968
|
-
withValue = this.#parseExpression();
|
|
3969
|
-
}
|
|
3970
|
-
return {
|
|
3971
|
-
template,
|
|
3972
|
-
with: withValue
|
|
3973
|
-
};
|
|
3974
|
-
}, {
|
|
3975
|
-
template: this.#invalid(),
|
|
3976
|
-
with: null
|
|
3977
|
-
});
|
|
3978
|
-
const body = [];
|
|
3979
|
-
const fills = [];
|
|
3980
|
-
for (;;) {
|
|
3981
|
-
const part = this.#parseBody(["fill", "endcomponent"]);
|
|
3982
|
-
body.push(...part.body);
|
|
3983
|
-
if (!part.end) {
|
|
3984
|
-
this.#unclosed("component", open, "endcomponent");
|
|
3985
|
-
break;
|
|
3986
|
-
}
|
|
3987
|
-
if (part.end.value === "endcomponent") {
|
|
3988
|
-
this.#finishEndTag();
|
|
3989
|
-
break;
|
|
3990
|
-
}
|
|
3991
|
-
const fillStart = this.#tokens[this.#index - 2] ?? part.end;
|
|
3992
|
-
const name = this.#header(() => this.#parseStringLiteral("a slot name"), "");
|
|
3993
|
-
const fill = this.#parseBody(["endfill"]);
|
|
3994
|
-
if (fill.end) this.#finishEndTag();
|
|
3995
|
-
else this.#unclosed("fill", fillStart, "endfill");
|
|
3996
|
-
if (fills.some((existing) => existing.name === name)) this.#problem("duplicate-fill", `Slot "${name}" is filled more than once`, {
|
|
3997
|
-
start: fillStart.start,
|
|
3998
|
-
end: fillStart.end
|
|
3999
|
-
});
|
|
4000
|
-
fills.push({
|
|
4001
|
-
name,
|
|
4002
|
-
body: fill.body,
|
|
4003
|
-
span: {
|
|
4004
|
-
start: fillStart.start,
|
|
4005
|
-
end: this.#lastEnd
|
|
4006
|
-
}
|
|
4007
|
-
});
|
|
4008
|
-
}
|
|
4009
|
-
return this.#placed({
|
|
4010
|
-
type: "Component",
|
|
4011
|
-
...header,
|
|
4012
|
-
fills,
|
|
4013
|
-
body,
|
|
4014
|
-
span: {
|
|
4015
|
-
start: open.start,
|
|
4016
|
-
end: this.#lastEnd
|
|
4017
|
-
}
|
|
4018
|
-
});
|
|
4019
|
-
}
|
|
4020
|
-
#placed(node) {
|
|
4021
|
-
this.placed.push(node);
|
|
4022
|
-
return node;
|
|
4023
|
-
}
|
|
4024
|
-
#parseStringLiteral(what) {
|
|
4025
|
-
const token = this.#peek();
|
|
4026
|
-
if (token.type !== "string") this.#fail(`Expected ${what} as a string but found ${describeToken(token)}`);
|
|
4027
|
-
this.#next();
|
|
4028
|
-
return token.value;
|
|
4029
|
-
}
|
|
4030
|
-
#parseSlot(open) {
|
|
4031
|
-
const name = this.#header(() => this.#peek().type === "tagClose" ? "default" : this.#parseStringLiteral("a slot name"), "default");
|
|
4032
|
-
const { body, end } = this.#parseBody(["endslot"]);
|
|
4033
|
-
if (end) this.#finishEndTag();
|
|
4034
|
-
else this.#unclosed("slot", open, "endslot");
|
|
4035
|
-
return this.#placed({
|
|
4036
|
-
type: "Slot",
|
|
4037
|
-
name,
|
|
4038
|
-
body,
|
|
4039
|
-
span: {
|
|
4040
|
-
start: open.start,
|
|
4041
|
-
end: this.#lastEnd
|
|
4042
|
-
}
|
|
4043
|
-
});
|
|
4044
|
-
}
|
|
4045
|
-
#parseMacro(open) {
|
|
4046
|
-
const header = this.#header(() => {
|
|
4047
|
-
const name = this.#expectDeclarableName("a macro name").value;
|
|
4048
|
-
this.#expectPunct("(");
|
|
4049
|
-
const params = this.#parseParams(")");
|
|
4050
|
-
this.#expectPunct(")");
|
|
4051
|
-
return {
|
|
4052
|
-
name,
|
|
4053
|
-
params
|
|
4054
|
-
};
|
|
4055
|
-
}, {
|
|
4056
|
-
name: "",
|
|
4057
|
-
params: []
|
|
4058
|
-
});
|
|
4059
|
-
const { body, end } = this.#parseBody(["endmacro"]);
|
|
4060
|
-
if (end) this.#finishEndTag(header.name);
|
|
4061
|
-
else this.#unclosed("macro", open, "endmacro");
|
|
4062
|
-
return {
|
|
4063
|
-
type: "Macro",
|
|
4064
|
-
...header,
|
|
4065
|
-
body,
|
|
4066
|
-
span: {
|
|
4067
|
-
start: open.start,
|
|
4068
|
-
end: this.#lastEnd
|
|
4069
|
-
}
|
|
4070
|
-
};
|
|
4071
|
-
}
|
|
4072
|
-
#parseImport(open) {
|
|
4073
|
-
const template = this.#parseStringLiteral("a template name");
|
|
4074
|
-
this.#expectKeyword("as");
|
|
4075
|
-
const alias = this.#expectDeclarableName("an alias").value;
|
|
4076
|
-
const close = this.#expectTagEnd();
|
|
4077
|
-
return {
|
|
4078
|
-
type: "Import",
|
|
4079
|
-
template,
|
|
4080
|
-
alias,
|
|
4081
|
-
span: {
|
|
4082
|
-
start: open.start,
|
|
4083
|
-
end: close.end
|
|
4084
|
-
}
|
|
4085
|
-
};
|
|
4086
|
-
}
|
|
4087
|
-
#parseFromImport(open) {
|
|
4088
|
-
const template = this.#parseStringLiteral("a template name");
|
|
4089
|
-
this.#expectKeyword("import");
|
|
4090
|
-
const names = [];
|
|
4091
|
-
do {
|
|
4092
|
-
if (names.length > 0) this.#next();
|
|
4093
|
-
const nameToken = this.#expectName("a macro name");
|
|
4094
|
-
let alias = nameToken.value;
|
|
4095
|
-
if (this.#isName("as")) {
|
|
4096
|
-
this.#next();
|
|
4097
|
-
alias = this.#expectDeclarableName("an alias").value;
|
|
4098
|
-
}
|
|
4099
|
-
names.push({
|
|
4100
|
-
name: nameToken.value,
|
|
4101
|
-
alias,
|
|
4102
|
-
span: {
|
|
4103
|
-
start: nameToken.start,
|
|
4104
|
-
end: this.#lastEnd
|
|
4105
|
-
}
|
|
4106
|
-
});
|
|
4107
|
-
} while (this.#isPunct(","));
|
|
4108
|
-
const close = this.#expectTagEnd();
|
|
4109
|
-
return {
|
|
4110
|
-
type: "FromImport",
|
|
4111
|
-
template,
|
|
4112
|
-
names,
|
|
4113
|
-
span: {
|
|
4114
|
-
start: open.start,
|
|
4115
|
-
end: close.end
|
|
4116
|
-
}
|
|
4117
|
-
};
|
|
4118
|
-
}
|
|
4119
|
-
#parseWith(open) {
|
|
4120
|
-
const value = this.#header(() => this.#parseExpression(), this.#invalid());
|
|
4121
|
-
const { body, end } = this.#parseBody(["endwith"]);
|
|
4122
|
-
if (end) this.#finishEndTag();
|
|
4123
|
-
else this.#unclosed("with", open, "endwith");
|
|
4124
|
-
return {
|
|
4125
|
-
type: "With",
|
|
4126
|
-
value,
|
|
4127
|
-
body,
|
|
4128
|
-
span: {
|
|
4129
|
-
start: open.start,
|
|
4130
|
-
end: this.#lastEnd
|
|
4131
|
-
}
|
|
4132
|
-
};
|
|
4133
|
-
}
|
|
4134
|
-
#parseAdd(open) {
|
|
4135
|
-
const { outlet, once } = this.#header(() => {
|
|
4136
|
-
this.#expectKeyword("to");
|
|
4137
|
-
const outlet = this.#parseStringLiteral("an outlet name");
|
|
4138
|
-
const once = this.#isName("once");
|
|
4139
|
-
if (once) this.#next();
|
|
4140
|
-
return {
|
|
4141
|
-
outlet,
|
|
4142
|
-
once
|
|
4143
|
-
};
|
|
4144
|
-
}, {
|
|
4145
|
-
outlet: "",
|
|
4146
|
-
once: false
|
|
4147
|
-
});
|
|
4148
|
-
const { body, end } = this.#parseBody(["endadd"]);
|
|
4149
|
-
if (end) this.#finishEndTag();
|
|
4150
|
-
else this.#unclosed("add", open, "endadd");
|
|
4151
|
-
return {
|
|
4152
|
-
type: "Add",
|
|
4153
|
-
outlet,
|
|
4154
|
-
once,
|
|
4155
|
-
body,
|
|
4156
|
-
span: {
|
|
4157
|
-
start: open.start,
|
|
4158
|
-
end: this.#lastEnd
|
|
4159
|
-
}
|
|
4160
|
-
};
|
|
4161
|
-
}
|
|
4162
|
-
#parseCache(open) {
|
|
4163
|
-
const { key, seconds } = this.#header(() => {
|
|
4164
|
-
const key = this.#parseExpression();
|
|
4165
|
-
let seconds = null;
|
|
4166
|
-
if (this.#isName("for")) {
|
|
4167
|
-
this.#next();
|
|
4168
|
-
seconds = this.#parseExpression();
|
|
4169
|
-
}
|
|
4170
|
-
return {
|
|
4171
|
-
key,
|
|
4172
|
-
seconds
|
|
4173
|
-
};
|
|
4174
|
-
}, {
|
|
4175
|
-
key: this.#invalid(),
|
|
4176
|
-
seconds: null
|
|
4177
|
-
});
|
|
4178
|
-
const { body, end } = this.#parseBody(["endcache"]);
|
|
4179
|
-
if (end) this.#finishEndTag();
|
|
4180
|
-
else this.#unclosed("cache", open, "endcache");
|
|
4181
|
-
return {
|
|
4182
|
-
type: "Cache",
|
|
4183
|
-
key,
|
|
4184
|
-
seconds,
|
|
4185
|
-
body,
|
|
4186
|
-
span: {
|
|
4187
|
-
start: open.start,
|
|
4188
|
-
end: this.#lastEnd
|
|
4189
|
-
}
|
|
4190
|
-
};
|
|
4191
|
-
}
|
|
4192
|
-
#parseIsland(open) {
|
|
4193
|
-
const header = this.#header(() => {
|
|
4194
|
-
const template = this.#parseExpression();
|
|
4195
|
-
let withValue = null;
|
|
4196
|
-
if (this.#isName("with")) {
|
|
4197
|
-
this.#next();
|
|
4198
|
-
withValue = this.#parseExpression();
|
|
4199
|
-
}
|
|
4200
|
-
return {
|
|
4201
|
-
template,
|
|
4202
|
-
with: withValue
|
|
4203
|
-
};
|
|
4204
|
-
}, {
|
|
4205
|
-
template: this.#invalid(),
|
|
4206
|
-
with: null
|
|
4207
|
-
});
|
|
4208
|
-
const { body, end } = this.#parseBody(["endisland"]);
|
|
4209
|
-
if (end) this.#finishEndTag();
|
|
4210
|
-
else this.#unclosed("island", open, "endisland");
|
|
4211
|
-
return this.#placed({
|
|
4212
|
-
type: "Island",
|
|
4213
|
-
...header,
|
|
4214
|
-
body,
|
|
4215
|
-
span: {
|
|
4216
|
-
start: open.start,
|
|
4217
|
-
end: this.#lastEnd
|
|
4218
|
-
}
|
|
4219
|
-
});
|
|
4220
|
-
}
|
|
4221
|
-
#parseApply(open) {
|
|
4222
|
-
const filters = this.#header(() => {
|
|
4223
|
-
const list = [];
|
|
4224
|
-
do {
|
|
4225
|
-
if (list.length > 0) this.#next();
|
|
4226
|
-
const nameToken = this.#expectName("a filter name");
|
|
4227
|
-
const args = this.#isPunct("(") ? this.#parseArguments() : [];
|
|
4228
|
-
list.push({
|
|
4229
|
-
name: nameToken.value,
|
|
4230
|
-
args,
|
|
4231
|
-
span: {
|
|
4232
|
-
start: nameToken.start,
|
|
4233
|
-
end: this.#lastEnd
|
|
4234
|
-
}
|
|
4235
|
-
});
|
|
4236
|
-
} while (this.#isPunct("|"));
|
|
4237
|
-
return list;
|
|
4238
|
-
}, []);
|
|
4239
|
-
const { body, end } = this.#parseBody(["endapply"]);
|
|
4240
|
-
if (end) this.#finishEndTag();
|
|
4241
|
-
else this.#unclosed("apply", open, "endapply");
|
|
4242
|
-
return {
|
|
4243
|
-
type: "Apply",
|
|
4244
|
-
filters,
|
|
4245
|
-
body,
|
|
4246
|
-
span: {
|
|
4247
|
-
start: open.start,
|
|
4248
|
-
end: this.#lastEnd
|
|
4249
|
-
}
|
|
4250
|
-
};
|
|
4251
|
-
}
|
|
4252
|
-
#parseAutoescape(open) {
|
|
4253
|
-
const strategy = this.#header(() => {
|
|
4254
|
-
const token = this.#peek();
|
|
4255
|
-
if (token.type === "tagClose") return "html";
|
|
4256
|
-
if (token.type === "name" && token.value === "false") {
|
|
4257
|
-
this.#next();
|
|
4258
|
-
return false;
|
|
4259
|
-
}
|
|
4260
|
-
const value = this.#parseStringLiteral("an escaping strategy");
|
|
4261
|
-
if (!ESCAPE_STRATEGIES.includes(value)) this.#fail(`Unknown escaping strategy "${value}". Use ${ESCAPE_STRATEGIES.map((s) => `"${s}"`).join(", ")} or false`, token, "unknown-strategy");
|
|
4262
|
-
return value;
|
|
4263
|
-
}, "html");
|
|
4264
|
-
const { body, end } = this.#parseBody(["endautoescape"]);
|
|
4265
|
-
if (end) this.#finishEndTag();
|
|
4266
|
-
else this.#unclosed("autoescape", open, "endautoescape");
|
|
4267
|
-
return {
|
|
4268
|
-
type: "Autoescape",
|
|
4269
|
-
strategy,
|
|
4270
|
-
body,
|
|
4271
|
-
span: {
|
|
4272
|
-
start: open.start,
|
|
4273
|
-
end: this.#lastEnd
|
|
4274
|
-
}
|
|
4275
|
-
};
|
|
4276
|
-
}
|
|
4277
|
-
#parseVerbatim(open) {
|
|
4278
|
-
this.#expectTagEnd();
|
|
4279
|
-
let value = "";
|
|
4280
|
-
let start = this.#lastEnd;
|
|
4281
|
-
let end = start;
|
|
4282
|
-
const token = this.#peek();
|
|
4283
|
-
if (token.type === "text") {
|
|
4284
|
-
this.#next();
|
|
4285
|
-
value = token.value;
|
|
4286
|
-
start = token.start;
|
|
4287
|
-
end = token.end;
|
|
4288
|
-
}
|
|
4289
|
-
if (this.#peek().type === "tagOpen" && this.#isName("endverbatim", 1)) {
|
|
4290
|
-
this.#next();
|
|
4291
|
-
this.#next();
|
|
4292
|
-
this.#finishEndTag();
|
|
4293
|
-
}
|
|
4294
|
-
if (value === "") return null;
|
|
4295
|
-
return {
|
|
4296
|
-
type: "Text",
|
|
4297
|
-
value,
|
|
4298
|
-
span: {
|
|
4299
|
-
start: Math.max(start, open.end),
|
|
4300
|
-
end
|
|
4301
|
-
}
|
|
4302
|
-
};
|
|
4303
|
-
}
|
|
4304
|
-
/** Guards against source nested so deeply that parsing it would overflow the call stack. */
|
|
4305
|
-
#enter() {
|
|
4306
|
-
if (++this.#nesting > 200) {
|
|
4307
|
-
this.#nesting--;
|
|
4308
|
-
this.#fail(`Template is nested more than 200 levels deep`, this.#peek(), "too-deep");
|
|
4309
|
-
}
|
|
4310
|
-
}
|
|
4311
|
-
#parseExpression() {
|
|
4312
|
-
this.#enter();
|
|
4313
|
-
try {
|
|
4314
|
-
return this.#parseConditional();
|
|
4315
|
-
} finally {
|
|
4316
|
-
this.#nesting--;
|
|
4317
|
-
}
|
|
4318
|
-
}
|
|
4319
|
-
#parseConditional() {
|
|
4320
|
-
const test = this.#parseCoalesce();
|
|
4321
|
-
if (this.#isPunct("?")) {
|
|
4322
|
-
this.#next();
|
|
4323
|
-
const consequent = this.#parseExpression();
|
|
4324
|
-
let alternate = null;
|
|
4325
|
-
if (this.#isPunct(":")) {
|
|
4326
|
-
this.#next();
|
|
4327
|
-
alternate = this.#parseExpression();
|
|
4328
|
-
}
|
|
4329
|
-
return {
|
|
4330
|
-
type: "Conditional",
|
|
4331
|
-
test,
|
|
4332
|
-
consequent,
|
|
4333
|
-
alternate,
|
|
4334
|
-
span: {
|
|
4335
|
-
start: test.span.start,
|
|
4336
|
-
end: this.#lastEnd
|
|
4337
|
-
}
|
|
4338
|
-
};
|
|
4339
|
-
}
|
|
4340
|
-
if (this.#isPunct("?:")) {
|
|
4341
|
-
this.#next();
|
|
4342
|
-
const right = this.#parseExpression();
|
|
4343
|
-
return this.#binary("?:", test, right);
|
|
4344
|
-
}
|
|
4345
|
-
return test;
|
|
4346
|
-
}
|
|
4347
|
-
#binary(operator, left, right) {
|
|
4348
|
-
return {
|
|
4349
|
-
type: "Binary",
|
|
4350
|
-
operator,
|
|
4351
|
-
left,
|
|
4352
|
-
right,
|
|
4353
|
-
span: {
|
|
4354
|
-
start: left.span.start,
|
|
4355
|
-
end: right.span.end
|
|
4356
|
-
}
|
|
4357
|
-
};
|
|
4358
|
-
}
|
|
4359
|
-
#parseCoalesce() {
|
|
4360
|
-
const left = this.#parseOr();
|
|
4361
|
-
if (!this.#isPunct("??")) return left;
|
|
4362
|
-
this.#next();
|
|
4363
|
-
this.#enter();
|
|
4364
|
-
try {
|
|
4365
|
-
return this.#binary("??", left, this.#parseCoalesce());
|
|
4366
|
-
} finally {
|
|
4367
|
-
this.#nesting--;
|
|
4368
|
-
}
|
|
4369
|
-
}
|
|
4370
|
-
#parseOr() {
|
|
4371
|
-
let left = this.#parseAnd();
|
|
4372
|
-
while (this.#isName("or")) {
|
|
4373
|
-
this.#next();
|
|
4374
|
-
left = this.#binary("or", left, this.#parseAnd());
|
|
4375
|
-
}
|
|
4376
|
-
return left;
|
|
4377
|
-
}
|
|
4378
|
-
#parseAnd() {
|
|
4379
|
-
let left = this.#parseNot();
|
|
4380
|
-
while (this.#isName("and")) {
|
|
4381
|
-
this.#next();
|
|
4382
|
-
left = this.#binary("and", left, this.#parseNot());
|
|
4383
|
-
}
|
|
4384
|
-
return left;
|
|
4385
|
-
}
|
|
4386
|
-
#parseNot() {
|
|
4387
|
-
this.#enter();
|
|
4388
|
-
try {
|
|
4389
|
-
return this.#parseNotInner();
|
|
4390
|
-
} finally {
|
|
4391
|
-
this.#nesting--;
|
|
4392
|
-
}
|
|
4393
|
-
}
|
|
4394
|
-
#parseNotInner() {
|
|
4395
|
-
if (this.#isName("not")) {
|
|
4396
|
-
const token = this.#next();
|
|
4397
|
-
const operand = this.#parseNot();
|
|
4398
|
-
return {
|
|
4399
|
-
type: "Unary",
|
|
4400
|
-
operator: "not",
|
|
4401
|
-
operand,
|
|
4402
|
-
span: {
|
|
4403
|
-
start: token.start,
|
|
4404
|
-
end: operand.span.end
|
|
4405
|
-
}
|
|
4406
|
-
};
|
|
4407
|
-
}
|
|
4408
|
-
return this.#parseComparison();
|
|
4409
|
-
}
|
|
4410
|
-
/** Reads a comparison operator at the cursor without consuming it, and how many tokens it spans. */
|
|
4411
|
-
#comparisonAhead() {
|
|
4412
|
-
const token = this.#peek();
|
|
4413
|
-
if (token.type === "punct" && COMPARISON_PUNCTUATORS.has(token.value)) return {
|
|
4414
|
-
operator: token.value,
|
|
4415
|
-
length: 1
|
|
4416
|
-
};
|
|
4417
|
-
if (token.type !== "name") return null;
|
|
4418
|
-
switch (token.value) {
|
|
4419
|
-
case "in": return {
|
|
4420
|
-
operator: "in",
|
|
4421
|
-
length: 1
|
|
4422
|
-
};
|
|
4423
|
-
case "is": return {
|
|
4424
|
-
operator: "is",
|
|
4425
|
-
length: 1
|
|
4426
|
-
};
|
|
4427
|
-
case "not": return this.#isName("in", 1) ? {
|
|
4428
|
-
operator: "not in",
|
|
4429
|
-
length: 2
|
|
4430
|
-
} : null;
|
|
4431
|
-
case "starts": return this.#isName("with", 1) ? {
|
|
4432
|
-
operator: "starts with",
|
|
4433
|
-
length: 2
|
|
4434
|
-
} : null;
|
|
4435
|
-
case "ends": return this.#isName("with", 1) ? {
|
|
4436
|
-
operator: "ends with",
|
|
4437
|
-
length: 2
|
|
4438
|
-
} : null;
|
|
4439
|
-
default: return null;
|
|
4440
|
-
}
|
|
4441
|
-
}
|
|
4442
|
-
#parseComparison() {
|
|
4443
|
-
const left = this.#parseRange();
|
|
4444
|
-
const ahead = this.#comparisonAhead();
|
|
4445
|
-
if (!ahead) return left;
|
|
4446
|
-
let result;
|
|
4447
|
-
if (ahead.operator === "is") result = this.#parseTest(left);
|
|
4448
|
-
else {
|
|
4449
|
-
for (let i = 0; i < ahead.length; i++) this.#next();
|
|
4450
|
-
result = this.#binary(ahead.operator, left, this.#parseRange());
|
|
4451
|
-
}
|
|
4452
|
-
if (this.#comparisonAhead()) this.#fail("Comparisons cannot be chained. Join them with \"and\" or \"or\"", this.#peek(), "chained-comparison");
|
|
4453
|
-
return result;
|
|
4454
|
-
}
|
|
4455
|
-
#parseTest(input) {
|
|
4456
|
-
this.#next();
|
|
4457
|
-
let negated = false;
|
|
4458
|
-
if (this.#isName("not")) {
|
|
4459
|
-
this.#next();
|
|
4460
|
-
negated = true;
|
|
4461
|
-
}
|
|
4462
|
-
const first = this.#expectName("a test name");
|
|
4463
|
-
let name = first.value;
|
|
4464
|
-
let nameEnd = first.end;
|
|
4465
|
-
const second = this.#peek();
|
|
4466
|
-
if (second.type === "name" && (this.#options.isTest(`${name} ${second.value}`) || !this.#options.isTest(name) && !NOT_TEST_WORDS.has(second.value))) {
|
|
4467
|
-
this.#next();
|
|
4468
|
-
name = `${name} ${second.value}`;
|
|
4469
|
-
nameEnd = second.end;
|
|
4470
|
-
}
|
|
4471
|
-
const args = this.#isPunct("(") ? this.#parseArguments() : [];
|
|
4472
|
-
return {
|
|
4473
|
-
type: "Test",
|
|
4474
|
-
name,
|
|
4475
|
-
nameSpan: {
|
|
4476
|
-
start: first.start,
|
|
4477
|
-
end: nameEnd
|
|
4478
|
-
},
|
|
4479
|
-
input,
|
|
4480
|
-
args,
|
|
4481
|
-
negated,
|
|
4482
|
-
span: {
|
|
4483
|
-
start: input.span.start,
|
|
4484
|
-
end: this.#lastEnd
|
|
4485
|
-
}
|
|
4486
|
-
};
|
|
4487
|
-
}
|
|
4488
|
-
#parseRange() {
|
|
4489
|
-
const left = this.#parseConcat();
|
|
4490
|
-
if (this.#isPunct("..")) {
|
|
4491
|
-
this.#next();
|
|
4492
|
-
return this.#binary("..", left, this.#parseConcat());
|
|
4493
|
-
}
|
|
4494
|
-
return left;
|
|
4495
|
-
}
|
|
4496
|
-
#parseConcat() {
|
|
4497
|
-
let left = this.#parseAdditive();
|
|
4498
|
-
while (this.#isPunct("~")) {
|
|
4499
|
-
this.#next();
|
|
4500
|
-
left = this.#binary("~", left, this.#parseAdditive());
|
|
4501
|
-
}
|
|
4502
|
-
return left;
|
|
4503
|
-
}
|
|
4504
|
-
#parseAdditive() {
|
|
4505
|
-
let left = this.#parseMultiplicative();
|
|
4506
|
-
while (this.#isPunct("+") || this.#isPunct("-")) {
|
|
4507
|
-
const operator = this.#next().value;
|
|
4508
|
-
left = this.#binary(operator, left, this.#parseMultiplicative());
|
|
4509
|
-
}
|
|
4510
|
-
return left;
|
|
4511
|
-
}
|
|
4512
|
-
#parseMultiplicative() {
|
|
4513
|
-
let left = this.#parseUnary();
|
|
4514
|
-
for (;;) {
|
|
4515
|
-
const token = this.#peek();
|
|
4516
|
-
if (token.type === "punct" && (token.value === "*" || token.value === "/" || token.value === "//" || token.value === "%")) {
|
|
4517
|
-
this.#next();
|
|
4518
|
-
left = this.#binary(token.value, left, this.#parseUnary());
|
|
4519
|
-
} else return left;
|
|
4520
|
-
}
|
|
4521
|
-
}
|
|
4522
|
-
#parseUnary() {
|
|
4523
|
-
this.#enter();
|
|
4524
|
-
try {
|
|
4525
|
-
return this.#parseUnaryInner();
|
|
4526
|
-
} finally {
|
|
4527
|
-
this.#nesting--;
|
|
4528
|
-
}
|
|
4529
|
-
}
|
|
4530
|
-
#parseUnaryInner() {
|
|
4531
|
-
if (this.#isPunct("-") || this.#isPunct("+")) {
|
|
4532
|
-
const token = this.#next();
|
|
4533
|
-
const operand = this.#parseUnary();
|
|
4534
|
-
return {
|
|
4535
|
-
type: "Unary",
|
|
4536
|
-
operator: token.value,
|
|
4537
|
-
operand,
|
|
4538
|
-
span: {
|
|
4539
|
-
start: token.start,
|
|
4540
|
-
end: operand.span.end
|
|
4541
|
-
}
|
|
4542
|
-
};
|
|
4543
|
-
}
|
|
4544
|
-
return this.#parsePower();
|
|
4545
|
-
}
|
|
4546
|
-
#parsePower() {
|
|
4547
|
-
const base = this.#parsePostfix();
|
|
4548
|
-
if (this.#isPunct("**")) {
|
|
4549
|
-
this.#next();
|
|
4550
|
-
return this.#binary("**", base, this.#parseUnary());
|
|
4551
|
-
}
|
|
4552
|
-
return base;
|
|
4553
|
-
}
|
|
4554
|
-
#parsePostfix() {
|
|
4555
|
-
let expression = this.#parsePrimary();
|
|
4556
|
-
let optional = false;
|
|
4557
|
-
const start = expression.span.start;
|
|
4558
|
-
const span = () => ({
|
|
4559
|
-
start,
|
|
4560
|
-
end: this.#lastEnd
|
|
4561
|
-
});
|
|
4562
|
-
const closeChain = () => {
|
|
4563
|
-
if (optional) {
|
|
4564
|
-
expression = {
|
|
4565
|
-
type: "OptionalChain",
|
|
4566
|
-
expression,
|
|
4567
|
-
span: span()
|
|
4568
|
-
};
|
|
4569
|
-
optional = false;
|
|
4570
|
-
}
|
|
4571
|
-
};
|
|
4572
|
-
for (;;) {
|
|
4573
|
-
if (this.#isPunct(".") || this.#isPunct("?.")) {
|
|
4574
|
-
const isOptional = this.#next().value === "?.";
|
|
4575
|
-
if (isOptional) optional = true;
|
|
4576
|
-
if (isOptional && this.#isPunct("[")) {
|
|
4577
|
-
this.#next();
|
|
4578
|
-
const property = this.#parseExpression();
|
|
4579
|
-
this.#expectPunct("]");
|
|
4580
|
-
expression = {
|
|
4581
|
-
type: "Member",
|
|
4582
|
-
object: expression,
|
|
4583
|
-
property,
|
|
4584
|
-
optional: true,
|
|
4585
|
-
span: span()
|
|
4586
|
-
};
|
|
4587
|
-
continue;
|
|
4588
|
-
}
|
|
4589
|
-
const token = this.#peek();
|
|
4590
|
-
if (token.type === "name") {
|
|
4591
|
-
this.#next();
|
|
4592
|
-
expression = {
|
|
4593
|
-
type: "Member",
|
|
4594
|
-
object: expression,
|
|
4595
|
-
property: {
|
|
4596
|
-
type: "Literal",
|
|
4597
|
-
value: token.value,
|
|
4598
|
-
span: {
|
|
4599
|
-
start: token.start,
|
|
4600
|
-
end: token.end
|
|
4601
|
-
}
|
|
4602
|
-
},
|
|
4603
|
-
optional: isOptional,
|
|
4604
|
-
span: span()
|
|
4605
|
-
};
|
|
4606
|
-
continue;
|
|
4607
|
-
}
|
|
4608
|
-
if (token.type === "number" && /^\d+$/.test(token.value)) {
|
|
4609
|
-
this.#next();
|
|
4610
|
-
expression = {
|
|
4611
|
-
type: "Member",
|
|
4612
|
-
object: expression,
|
|
4613
|
-
property: {
|
|
4614
|
-
type: "Literal",
|
|
4615
|
-
value: Number(token.value),
|
|
4616
|
-
span: {
|
|
4617
|
-
start: token.start,
|
|
4618
|
-
end: token.end
|
|
4619
|
-
}
|
|
4620
|
-
},
|
|
4621
|
-
optional: isOptional,
|
|
4622
|
-
span: span()
|
|
4623
|
-
};
|
|
4624
|
-
continue;
|
|
4625
|
-
}
|
|
4626
|
-
this.#fail(`Expected a property name but found ${describeToken(token)}`);
|
|
4627
|
-
}
|
|
4628
|
-
if (this.#isPunct("[")) {
|
|
4629
|
-
this.#next();
|
|
4630
|
-
const property = this.#parseExpression();
|
|
4631
|
-
this.#expectPunct("]");
|
|
4632
|
-
expression = {
|
|
4633
|
-
type: "Member",
|
|
4634
|
-
object: expression,
|
|
4635
|
-
property,
|
|
4636
|
-
optional: false,
|
|
4637
|
-
span: span()
|
|
4638
|
-
};
|
|
4639
|
-
continue;
|
|
4640
|
-
}
|
|
4641
|
-
if (this.#isPunct("(")) {
|
|
4642
|
-
const callee = expression;
|
|
4643
|
-
if (callee.type !== "Name" && callee.type !== "Member") this.#fail("Only named functions, macros and exposed methods can be called", this.#peek(), "invalid-call");
|
|
4644
|
-
expression = {
|
|
4645
|
-
type: "Call",
|
|
4646
|
-
callee,
|
|
4647
|
-
args: this.#parseArguments(),
|
|
4648
|
-
span: span()
|
|
4649
|
-
};
|
|
4650
|
-
continue;
|
|
4651
|
-
}
|
|
4652
|
-
if (this.#isPunct("|")) {
|
|
4653
|
-
closeChain();
|
|
4654
|
-
this.#next();
|
|
4655
|
-
const nameToken = this.#expectName("a filter name");
|
|
4656
|
-
const args = this.#isPunct("(") ? this.#parseArguments() : [];
|
|
4657
|
-
expression = {
|
|
4658
|
-
type: "Filter",
|
|
4659
|
-
name: nameToken.value,
|
|
4660
|
-
nameSpan: {
|
|
4661
|
-
start: nameToken.start,
|
|
4662
|
-
end: nameToken.end
|
|
4663
|
-
},
|
|
4664
|
-
input: expression,
|
|
4665
|
-
args,
|
|
4666
|
-
span: span()
|
|
4667
|
-
};
|
|
4668
|
-
continue;
|
|
4669
|
-
}
|
|
4670
|
-
closeChain();
|
|
4671
|
-
return expression;
|
|
4672
|
-
}
|
|
4673
|
-
}
|
|
4674
|
-
#parsePrimary() {
|
|
4675
|
-
const token = this.#peek();
|
|
4676
|
-
const tokenSpan = {
|
|
4677
|
-
start: token.start,
|
|
4678
|
-
end: token.end
|
|
4679
|
-
};
|
|
4680
|
-
switch (token.type) {
|
|
4681
|
-
case "number":
|
|
4682
|
-
this.#next();
|
|
4683
|
-
return {
|
|
4684
|
-
type: "Literal",
|
|
4685
|
-
value: Number(token.value),
|
|
4686
|
-
span: tokenSpan
|
|
4687
|
-
};
|
|
4688
|
-
case "string":
|
|
4689
|
-
this.#next();
|
|
4690
|
-
return {
|
|
4691
|
-
type: "Literal",
|
|
4692
|
-
value: token.value,
|
|
4693
|
-
span: tokenSpan
|
|
4694
|
-
};
|
|
4695
|
-
case "stringStart": return this.#parseInterpolation();
|
|
4696
|
-
case "name":
|
|
4697
|
-
switch (token.value) {
|
|
4698
|
-
case "true":
|
|
4699
|
-
case "false":
|
|
4700
|
-
this.#next();
|
|
4701
|
-
return {
|
|
4702
|
-
type: "Literal",
|
|
4703
|
-
value: token.value === "true",
|
|
4704
|
-
span: tokenSpan
|
|
4705
|
-
};
|
|
4706
|
-
case "null":
|
|
4707
|
-
this.#next();
|
|
4708
|
-
return {
|
|
4709
|
-
type: "Literal",
|
|
4710
|
-
value: null,
|
|
4711
|
-
span: tokenSpan
|
|
4712
|
-
};
|
|
4713
|
-
case "and":
|
|
4714
|
-
case "or":
|
|
4715
|
-
case "not":
|
|
4716
|
-
case "in":
|
|
4717
|
-
case "is": this.#fail(`Expected a value but found "${token.value}"`);
|
|
4718
|
-
}
|
|
4719
|
-
if (this.#isPunct("=>", 1)) this.#fail("Arrow functions can only be passed as arguments to filters, functions and methods", token, "invalid-arrow");
|
|
4720
|
-
this.#next();
|
|
4721
|
-
return {
|
|
4722
|
-
type: "Name",
|
|
4723
|
-
name: token.value,
|
|
4724
|
-
span: tokenSpan
|
|
4725
|
-
};
|
|
4726
|
-
case "punct": switch (token.value) {
|
|
4727
|
-
case "[": return this.#parseArray();
|
|
4728
|
-
case "{": return this.#parseHash();
|
|
4729
|
-
case "(": {
|
|
4730
|
-
if (this.#arrowAhead()) this.#fail("Arrow functions can only be passed as arguments to filters, functions and methods", token, "invalid-arrow");
|
|
4731
|
-
this.#next();
|
|
4732
|
-
const inner = this.#parseExpression();
|
|
4733
|
-
this.#expectPunct(")");
|
|
4734
|
-
return {
|
|
4735
|
-
...inner,
|
|
4736
|
-
span: {
|
|
4737
|
-
start: token.start,
|
|
4738
|
-
end: this.#lastEnd
|
|
4739
|
-
}
|
|
4740
|
-
};
|
|
4741
|
-
}
|
|
4742
|
-
}
|
|
4743
|
-
}
|
|
4744
|
-
return this.#fail(`Expected a value but found ${describeToken(token)}`);
|
|
4745
|
-
}
|
|
4746
|
-
#parseInterpolation() {
|
|
4747
|
-
const start = this.#next();
|
|
4748
|
-
const parts = [];
|
|
4749
|
-
for (;;) {
|
|
4750
|
-
const token = this.#peek();
|
|
4751
|
-
if (token.type === "stringEnd") {
|
|
4752
|
-
this.#next();
|
|
4753
|
-
break;
|
|
4754
|
-
}
|
|
4755
|
-
if (token.type === "string") {
|
|
4756
|
-
this.#next();
|
|
4757
|
-
parts.push({
|
|
4758
|
-
type: "Literal",
|
|
4759
|
-
value: token.value,
|
|
4760
|
-
span: {
|
|
4761
|
-
start: token.start,
|
|
4762
|
-
end: token.end
|
|
4763
|
-
}
|
|
4764
|
-
});
|
|
4765
|
-
continue;
|
|
4766
|
-
}
|
|
4767
|
-
if (token.type === "interpOpen") {
|
|
4768
|
-
this.#next();
|
|
4769
|
-
parts.push(this.#parseExpression());
|
|
4770
|
-
const close = this.#peek();
|
|
4771
|
-
if (close.type !== "interpClose") this.#fail(`Expected "}" but found ${describeToken(close)}`);
|
|
4772
|
-
this.#next();
|
|
4773
|
-
continue;
|
|
4774
|
-
}
|
|
4775
|
-
this.#fail(`Unexpected ${describeToken(token)} in string`);
|
|
4776
|
-
}
|
|
4777
|
-
return {
|
|
4778
|
-
type: "Interpolation",
|
|
4779
|
-
parts,
|
|
4780
|
-
span: {
|
|
4781
|
-
start: start.start,
|
|
4782
|
-
end: this.#lastEnd
|
|
4783
|
-
}
|
|
4784
|
-
};
|
|
4785
|
-
}
|
|
4786
|
-
#parseSpread() {
|
|
4787
|
-
const token = this.#next();
|
|
4788
|
-
const value = this.#parseExpression();
|
|
4789
|
-
return {
|
|
4790
|
-
type: "Spread",
|
|
4791
|
-
value,
|
|
4792
|
-
span: {
|
|
4793
|
-
start: token.start,
|
|
4794
|
-
end: value.span.end
|
|
4795
|
-
}
|
|
4796
|
-
};
|
|
4797
|
-
}
|
|
4798
|
-
#parseArray() {
|
|
4799
|
-
const open = this.#next();
|
|
4800
|
-
const elements = [];
|
|
4801
|
-
while (!this.#isPunct("]")) {
|
|
4802
|
-
elements.push(this.#isPunct("...") ? this.#parseSpread() : this.#parseExpression());
|
|
4803
|
-
if (!this.#isPunct(",")) break;
|
|
4804
|
-
this.#next();
|
|
4805
|
-
}
|
|
4806
|
-
this.#expectPunct("]");
|
|
4807
|
-
return {
|
|
4808
|
-
type: "Array",
|
|
4809
|
-
elements,
|
|
4810
|
-
span: {
|
|
4811
|
-
start: open.start,
|
|
4812
|
-
end: this.#lastEnd
|
|
4813
|
-
}
|
|
4814
|
-
};
|
|
4815
|
-
}
|
|
4816
|
-
#parseHash() {
|
|
4817
|
-
const open = this.#next();
|
|
4818
|
-
const entries = [];
|
|
4819
|
-
while (!this.#isPunct("}")) {
|
|
4820
|
-
if (this.#isPunct("...")) entries.push(this.#parseSpread());
|
|
4821
|
-
else {
|
|
4822
|
-
const token = this.#peek();
|
|
4823
|
-
let key;
|
|
4824
|
-
if (token.type === "name") {
|
|
4825
|
-
this.#next();
|
|
4826
|
-
key = {
|
|
4827
|
-
type: "Literal",
|
|
4828
|
-
value: token.value,
|
|
4829
|
-
span: {
|
|
4830
|
-
start: token.start,
|
|
4831
|
-
end: token.end
|
|
4832
|
-
}
|
|
4833
|
-
};
|
|
4834
|
-
if (!this.#isPunct(":")) {
|
|
4835
|
-
if (RESERVED_NAMES.has(token.value) && token.value !== "loop" && token.value !== "slots") this.#fail(`Expected ":" after "${token.value}"`);
|
|
4836
|
-
entries.push({
|
|
4837
|
-
type: "Property",
|
|
4838
|
-
key,
|
|
4839
|
-
value: {
|
|
4840
|
-
type: "Name",
|
|
4841
|
-
name: token.value,
|
|
4842
|
-
span: {
|
|
4843
|
-
start: token.start,
|
|
4844
|
-
end: token.end
|
|
4845
|
-
}
|
|
4846
|
-
},
|
|
4847
|
-
span: {
|
|
4848
|
-
start: token.start,
|
|
4849
|
-
end: token.end
|
|
4850
|
-
}
|
|
4851
|
-
});
|
|
4852
|
-
if (!this.#isPunct(",")) break;
|
|
4853
|
-
this.#next();
|
|
4854
|
-
continue;
|
|
4855
|
-
}
|
|
4856
|
-
} else if (token.type === "string" || token.type === "number") {
|
|
4857
|
-
this.#next();
|
|
4858
|
-
key = {
|
|
4859
|
-
type: "Literal",
|
|
4860
|
-
value: token.type === "number" ? String(Number(token.value)) : token.value,
|
|
4861
|
-
span: {
|
|
4862
|
-
start: token.start,
|
|
4863
|
-
end: token.end
|
|
4864
|
-
}
|
|
4865
|
-
};
|
|
4866
|
-
} else if (this.#isPunct("(")) {
|
|
4867
|
-
this.#next();
|
|
4868
|
-
key = this.#parseExpression();
|
|
4869
|
-
this.#expectPunct(")");
|
|
4870
|
-
} else this.#fail(`Expected a hash key but found ${describeToken(token)}`);
|
|
4871
|
-
this.#expectPunct(":");
|
|
4872
|
-
const value = this.#parseExpression();
|
|
4873
|
-
entries.push({
|
|
4874
|
-
type: "Property",
|
|
4875
|
-
key,
|
|
4876
|
-
value,
|
|
4877
|
-
span: {
|
|
4878
|
-
start: token.start,
|
|
4879
|
-
end: value.span.end
|
|
4880
|
-
}
|
|
4881
|
-
});
|
|
4882
|
-
}
|
|
4883
|
-
if (!this.#isPunct(",")) break;
|
|
4884
|
-
this.#next();
|
|
4885
|
-
}
|
|
4886
|
-
this.#expectPunct("}");
|
|
4887
|
-
return {
|
|
4888
|
-
type: "Hash",
|
|
4889
|
-
entries,
|
|
4890
|
-
span: {
|
|
4891
|
-
start: open.start,
|
|
4892
|
-
end: this.#lastEnd
|
|
4893
|
-
}
|
|
4894
|
-
};
|
|
4895
|
-
}
|
|
4896
|
-
/** Checks whether the tokens at the cursor start an arrow function such as `(a, b) =>`. */
|
|
4897
|
-
#arrowAhead() {
|
|
4898
|
-
if (this.#peek().type === "name") return this.#isPunct("=>", 1);
|
|
4899
|
-
if (!this.#isPunct("(")) return false;
|
|
4900
|
-
let offset = 1;
|
|
4901
|
-
if (this.#isPunct(")", offset)) return this.#isPunct("=>", offset + 1);
|
|
4902
|
-
for (;;) {
|
|
4903
|
-
if (this.#peek(offset).type !== "name") return false;
|
|
4904
|
-
offset++;
|
|
4905
|
-
if (this.#isPunct(")", offset)) return this.#isPunct("=>", offset + 1);
|
|
4906
|
-
if (!this.#isPunct(",", offset)) return false;
|
|
4907
|
-
offset++;
|
|
4908
|
-
}
|
|
4909
|
-
}
|
|
4910
|
-
#parseArrow() {
|
|
4911
|
-
const start = this.#peek();
|
|
4912
|
-
const params = [];
|
|
4913
|
-
if (start.type === "name") params.push(this.#expectDeclarableName("a parameter name").value);
|
|
4914
|
-
else {
|
|
4915
|
-
this.#next();
|
|
4916
|
-
while (!this.#isPunct(")")) {
|
|
4917
|
-
params.push(this.#expectDeclarableName("a parameter name").value);
|
|
4918
|
-
if (!this.#isPunct(",")) break;
|
|
4919
|
-
this.#next();
|
|
4920
|
-
}
|
|
4921
|
-
this.#expectPunct(")");
|
|
4922
|
-
}
|
|
4923
|
-
this.#expectPunct("=>");
|
|
4924
|
-
const body = this.#parseExpression();
|
|
4925
|
-
return {
|
|
4926
|
-
type: "Arrow",
|
|
4927
|
-
params,
|
|
4928
|
-
body,
|
|
4929
|
-
span: {
|
|
4930
|
-
start: start.start,
|
|
4931
|
-
end: body.span.end
|
|
4932
|
-
}
|
|
4933
|
-
};
|
|
4934
|
-
}
|
|
4935
|
-
#parseArguments() {
|
|
4936
|
-
this.#expectPunct("(");
|
|
4937
|
-
const args = [];
|
|
4938
|
-
let named = false;
|
|
4939
|
-
while (!this.#isPunct(")")) {
|
|
4940
|
-
const token = this.#peek();
|
|
4941
|
-
if (this.#isPunct("...")) {
|
|
4942
|
-
if (named) this.#fail("Positional arguments cannot follow named arguments", this.#peek(), "positional-after-named");
|
|
4943
|
-
args.push(this.#parseSpread());
|
|
4944
|
-
} else if (token.type === "name" && this.#isPunct(":", 1)) {
|
|
4945
|
-
this.#next();
|
|
4946
|
-
this.#next();
|
|
4947
|
-
named = true;
|
|
4948
|
-
const value = this.#arrowAhead() ? this.#parseArrow() : this.#parseExpression();
|
|
4949
|
-
args.push({
|
|
4950
|
-
type: "Named",
|
|
4951
|
-
name: token.value,
|
|
4952
|
-
value,
|
|
4953
|
-
span: {
|
|
4954
|
-
start: token.start,
|
|
4955
|
-
end: value.span.end
|
|
4956
|
-
}
|
|
4957
|
-
});
|
|
4958
|
-
} else {
|
|
4959
|
-
if (named) this.#fail("Positional arguments cannot follow named arguments", token, "positional-after-named");
|
|
4960
|
-
const value = this.#arrowAhead() ? this.#parseArrow() : this.#parseExpression();
|
|
4961
|
-
args.push({
|
|
4962
|
-
type: "Positional",
|
|
4963
|
-
value,
|
|
4964
|
-
span: value.span
|
|
4965
|
-
});
|
|
4966
|
-
}
|
|
4967
|
-
if (!this.#isPunct(",")) break;
|
|
4968
|
-
this.#next();
|
|
4969
|
-
}
|
|
4970
|
-
this.#expectPunct(")");
|
|
4971
|
-
return args;
|
|
4972
|
-
}
|
|
4973
|
-
};
|
|
4974
|
-
//#endregion
|
|
4975
2250
|
//#region src/source.ts
|
|
4976
2251
|
/**
|
|
4977
2252
|
* A template's text and resolved name.
|
|
@@ -5054,6 +2329,6 @@ var Source = class {
|
|
|
5054
2329
|
}
|
|
5055
2330
|
};
|
|
5056
2331
|
//#endregion
|
|
5057
|
-
export {
|
|
2332
|
+
export { compare as $, escapeUrl as $t, consume as A, additionsIn as At, loadTemplate as B, fillScope as Bt, getAttr as C, contentSets as Ct, after as D, passesFrom as Dt, addSlotFlags as E, passesAhead as Et, fail as F, RenderState as Ft, renderTemplate as G, sectionCopy as Gt, locating as H, childScope as Ht, hold as I, Run as It, unhold as J, suggest as Jt, silence as K, sectionScope as Kt, holdData as L, blockScope as Lt, embed as M, closeOutlets as Mt, embedKeyed as N, recordAddition as Nt, all as O, runAhead as Ot, embedScope as P, takeOutput as Pt, MISSING as Q, escapeHtml as Qt, importsFor as R, emptyOut as Rt, toTime as S, atFront as St, readableNames as T, mayRunAhead as Tt, markCaller as U, lookup$1 as Ut, locate as V, startCapture as Vt, passData as W, ownerOf as Wt, write as X, isSafe as Xt, withCaller as Y, Markup as Yt, FORMATTERS as Z, safe as Zt, markupText as _, unwrap as _t, operatorFunction as a, KilnSyntaxError as an, isHashLike as at, toPieces as b, expose as bt, checkDepth as c, mapKeys as ct, functionArgument as d, templateFunctions as dt, escapeWith as en, describeType as et, insertedPiece as f, toNumber as ft, mapContents as g, typeName as gt, listOf as h, truthy as ht, contains as i, KilnRenderError as in, isEmpty as it, consumeAt as j, checkLate as jt, checkLength as k, runSequence as kt, entriesOf as l, matchesType as lt, isHtmlStrategy as m, toStringArgument as mt, builtinFunctions as n, resolveTemplateName as nn, equals as nt, rangeOperator as o, createDiagnostic as on, isPlainObject as ot, integerArgument as p, toStr as pt, step as q, variableNames as qt, builtinTests as r, KilnIssue as rn, hashKeys as rt, Kept as s, issue as sn, literalOptions as st, Source as t, TEMPLATE_EXTENSION as tn, describeValue as tt, fromPieces as u, setOwn as ut, readProperty as v, valueLabel as vt, getMethod as w, isRoot as wt, toPlainData as x, lookup as xt, scanHtml as y, BLOCKED_NAMES as yt, isPromise as z, endCapture as zt };
|
|
5058
2333
|
|
|
5059
|
-
//# sourceMappingURL=source-
|
|
2334
|
+
//# sourceMappingURL=source-BbikGqIb.js.map
|