perf(angular): parse only text with Angular syntax as Angular

Every text node of a template got its own nested Angular parse, including
the whitespace between tags, which is most of them. Text without a `{`, `}`
or `@` holds no interpolation or block, so it stays HTML text. A full parse
of a 12k-line template takes about a fifth less work.
Цей коміт міститься в:
l0ng-ai committed 2026-11-06 13:22:53 +08:00
1 джерело 75718a6d21
коміт d533750d1c
2 files changed
+25 -6

No files matched your search

+12
Переглянути файл
@@ -556,6 +556,18 @@ describe("Angular templates", () => {
}
});
it("parses only text with Angular syntax as Angular", () => {
const doc = "<ul>\n <li>Tom &amp; Jerry</li>\n <li>{{ a }} &amp; {{ b }}</li>\n</ul>\n@if (x) {\n}";
const tree = angularLanguage.parser.parse(doc);
const parentAt = (text: string) => tree.resolveInner(doc.indexOf(text) + 1, 1).parent?.name;
// Plain text and the whitespace between tags stay HTML text.
expect(parentAt("Tom")).toBe("Element");
expect(parentAt("\n <li>")).toBe("Element");
expect(parentAt("{{ a")).toBe("Interpolation");
expect(tokens(doc)).toContain("@if:control");
expect(tokens(doc)).toContain("b:var");
});
it("is generated from angular.grammar", () => {
const dir = import.meta.dirname;
const { parser, terms } = buildParserFile(readFileSync(resolve(dir, "angular.grammar"), "utf8"), {
+13 -6
Переглянути файл
@@ -93,8 +93,15 @@ const textMixed = { parser: textParser },
// document) holding it.
const TEXT_NODES = new Set(["Text", "EntityReference", "CharacterReference", "InvalidEntity"]);
/** The runs of text directly in `node` that entities split, `null` without any. */
function entityRuns(node: SyntaxNode): { from: number; to: number }[] | null {
/**
* Whether text holds Angular syntax: interpolations, blocks and their braces all
* need a `{`, `}` or `@`. Other text (mostly the whitespace between tags) stays
* plain HTML text instead of costing a nested parse each.
*/
const ANGULAR_TEXT = /[{}@]/;
/** The runs of Angular text directly in `node` that entities split, `null` without any. */
function entityRuns(node: SyntaxNode, input: Input): { from: number; to: number }[] | null {
const runs: { from: number; to: number }[] = [];
let from = -1,
to = -1,
@@ -108,12 +115,12 @@ function entityRuns(node: SyntaxNode): { from: number; to: number }[] | null {
parts++;
continue;
}
if (parts > 1) runs.push({ from, to });
if (parts > 1 && ANGULAR_TEXT.test(input.read(from, to))) runs.push({ from, to });
({ from, to } = child);
parts = 1;
} while (child.nextSibling());
}
if (parts > 1) runs.push({ from, to });
if (parts > 1 && ANGULAR_TEXT.test(input.read(from, to))) runs.push({ from, to });
return runs.length ? runs : null;
}
@@ -125,10 +132,10 @@ function mixAngular(node: SyntaxNodeRef, input: Input): NestedParse | null {
return /^[*#([]|\{\{/.test(input.read(node.from, node.to)) ? attrMixed : null;
// Text in a run is covered by its parent's overlay, so not asked about.
case "Text":
return textMixed;
return ANGULAR_TEXT.test(input.read(node.from, node.to)) ? textMixed : null;
case "Document":
case "Element": {
const overlay = entityRuns(node.node);
const overlay = entityRuns(node.node, input);
return overlay && { parser: node.name == "Document" ? documentRunsParser : runsParser, overlay };
}
}