Skip to content

Commit 270e48f

Browse files
authored
Support XML parse (#50)
1 parent e17f750 commit 270e48f

6 files changed

Lines changed: 141 additions & 90 deletions

File tree

packages/angular-html-parser/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@
2323
"test": "vitest",
2424
"release": "release-it",
2525
"fix": "prettier . --write",
26-
"lint": "prettier . --check"
26+
"lint": "yarn lint:prettier && yarn lint:types",
27+
"lint:prettier": "prettier . --check",
28+
"lint:types": "tsc"
2729
},
2830
"devDependencies": {
2931
"@types/node": "25.0.2",
3032
"@vitest/coverage-v8": "4.0.15",
33+
"outdent": "0.8.0",
3134
"prettier": "3.7.4",
3235
"release-it": "19.1.0",
3336
"tsconfig-paths": "4.2.0",

packages/angular-html-parser/src/index.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
1-
import { HtmlParser } from "../../compiler/src/ml_parser/html_parser.js";
2-
import { TagContentType } from "../../compiler/src/ml_parser/tags.js";
3-
import { ParseTreeResult } from "../../compiler/src/ml_parser/parser.js";
1+
import { HtmlParser } from "../../compiler/src/ml_parser/html_parser.ts";
2+
import { XmlParser } from "../../compiler/src/ml_parser/xml_parser.ts";
3+
import type { TagContentType } from "../../compiler/src/ml_parser/tags.ts";
4+
import { ParseTreeResult as HtmlParseTreeResult } from "../../compiler/src/ml_parser/parser.ts";
45

5-
let parser: HtmlParser | null = null;
6-
7-
const getParser = () => {
8-
if (!parser) {
9-
parser = new HtmlParser();
10-
}
11-
return parser;
12-
};
13-
14-
export interface ParseOptions {
6+
export interface HtmlParseOptions {
157
/**
168
* any element can self close
179
*
@@ -56,10 +48,11 @@ export interface ParseOptions {
5648
enableAngularSelectorlessSyntax?: boolean;
5749
}
5850

59-
export function parse(
51+
let htmlParser: HtmlParser;
52+
export function parseHtml(
6053
input: string,
61-
options: ParseOptions = {},
62-
): ParseTreeResult {
54+
options: HtmlParseOptions = {},
55+
): HtmlParseTreeResult {
6356
const {
6457
canSelfClose = false,
6558
allowHtmComponentClosingTags = false,
@@ -69,7 +62,9 @@ export function parse(
6962
tokenizeAngularLetDeclaration = false,
7063
enableAngularSelectorlessSyntax = false,
7164
} = options;
72-
return getParser().parse(
65+
htmlParser ??= new HtmlParser();
66+
67+
return htmlParser.parse(
7368
input,
7469
"angular-html-parser",
7570
{
@@ -85,19 +80,30 @@ export function parse(
8580
);
8681
}
8782

83+
let xmlParser: XmlParser;
84+
export function parseXml(input: string) {
85+
xmlParser ??= new XmlParser();
86+
87+
return xmlParser.parse(input, "angular-xml-parser");
88+
}
89+
8890
// For prettier
89-
export { TagContentType };
91+
export { TagContentType } from "../../compiler/src/ml_parser/tags.ts";
9092
export {
9193
RecursiveVisitor,
9294
visitAll,
93-
} from "../../compiler/src/ml_parser/ast.js";
95+
} from "../../compiler/src/ml_parser/ast.ts";
9496
export {
9597
ParseSourceSpan,
9698
ParseLocation,
9799
ParseSourceFile,
98-
} from "../../compiler/src/parse_util.js";
99-
export { getHtmlTagDefinition } from "../../compiler/src/ml_parser/html_tags.js";
100+
} from "../../compiler/src/parse_util.ts";
101+
export { getHtmlTagDefinition } from "../../compiler/src/ml_parser/html_tags.ts";
100102

101103
// Types
102-
export type { ParseTreeResult } from "../../compiler/src/ml_parser/parser.js";
103-
export type * as Ast from "../../compiler/src/ml_parser/ast.js";
104+
export type { ParseTreeResult } from "../../compiler/src/ml_parser/parser.ts";
105+
export type * as Ast from "../../compiler/src/ml_parser/ast.ts";
106+
107+
// Remove these alias in next major release
108+
export type { HtmlParseOptions as ParseOptions };
109+
export { parseHtml as parse };

packages/angular-html-parser/test/index_spec.ts

Lines changed: 65 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { parse, TagContentType } from "../src/index.js";
2-
import { humanizeDom } from "../../compiler/test/ml_parser/ast_spec_utils.js";
3-
import * as html from "../../compiler/src/ml_parser/ast.js";
1+
import { describe, it, expect } from "vitest";
2+
import { parse, TagContentType } from "../src/index.ts";
3+
import { humanizeDom } from "../../compiler/test/ml_parser/ast_spec_utils.ts";
4+
import * as ast from "../../compiler/src/ml_parser/ast.ts";
45

56
describe("options", () => {
67
describe("getTagContentType", () => {
@@ -35,17 +36,17 @@ describe("options", () => {
3536
}
3637
};
3738
expect(humanizeDom(parse(input, { getTagContentType }))).toEqual([
38-
[html.Element, "template", 0],
39-
[html.Element, "MyComponent", 1],
40-
[html.Element, "template", 2],
41-
[html.Attribute, "#content", ""],
42-
[html.Text, "text", 3, ["text"]],
43-
[html.Element, "template", 0],
44-
[html.Attribute, "lang", "something-else", ["something-else"]],
45-
[html.Text, "<div>", 1, ["<div>"]],
46-
[html.Element, "custom", 0],
47-
[html.Attribute, "lang", "babel", ["babel"]],
48-
[html.Text, 'const foo = "</";', 1, ['const foo = "</";']],
39+
[ast.Element, "template", 0],
40+
[ast.Element, "MyComponent", 1],
41+
[ast.Element, "template", 2],
42+
[ast.Attribute, "#content", ""],
43+
[ast.Text, "text", 3, ["text"]],
44+
[ast.Element, "template", 0],
45+
[ast.Attribute, "lang", "something-else", ["something-else"]],
46+
[ast.Text, "<div>", 1, ["<div>"]],
47+
[ast.Element, "custom", 0],
48+
[ast.Attribute, "lang", "babel", ["babel"]],
49+
[ast.Text, 'const foo = "</";', 1, ['const foo = "</";']],
4950
]);
5051
});
5152

@@ -56,8 +57,8 @@ describe("options", () => {
5657
MJML_RAW_TAGS.has(tagName) ? TagContentType.RAW_TEXT : undefined,
5758
});
5859
expect(humanizeDom(result)).toEqual([
59-
[html.Element, "mj-raw", 0],
60-
[html.Text, "</p>", 1, ["</p>"]],
60+
[ast.Element, "mj-raw", 0],
61+
[ast.Text, "</p>", 1, ["</p>"]],
6162
]);
6263
});
6364
});
@@ -66,8 +67,8 @@ describe("options", () => {
6667
describe("AST format", () => {
6768
it("should have `type` property", () => {
6869
const input = `<!DOCTYPE html> <el attr></el>txt<!-- --><![CDATA[foo]]>`;
69-
const ast = parse(input);
70-
expect(ast.rootNodes).toEqual([
70+
const result = parse(input);
71+
expect(result.rootNodes).toEqual([
7172
expect.objectContaining({ kind: "docType" }),
7273
expect.objectContaining({ kind: "text" }),
7374
expect.objectContaining({
@@ -82,8 +83,8 @@ describe("AST format", () => {
8283

8384
it("should support 'tokenizeAngularBlocks'", () => {
8485
const input = `@if (user.isHuman) { <p>Hello human</p> }`;
85-
const ast = parse(input, { tokenizeAngularBlocks: true });
86-
expect(ast.rootNodes).toEqual([
86+
const result = parse(input, { tokenizeAngularBlocks: true });
87+
expect(result.rootNodes).toEqual([
8788
expect.objectContaining({
8889
name: "if",
8990
kind: "block",
@@ -122,43 +123,43 @@ describe("AST format", () => {
122123
}
123124
}
124125
`;
125-
const ast = parse(input, { tokenizeAngularBlocks: true });
126-
expect(humanizeDom(ast)).toEqual([
127-
[html.Text, "\n", 0, ["\n"]],
128-
[html.Block, "switch", 0],
129-
[html.BlockParameter, "case"],
130-
[html.Text, "\n ", 1, ["\n "]],
131-
[html.Block, "case", 1],
132-
[html.BlockParameter, "0"],
133-
[html.Block, "case", 1],
134-
[html.BlockParameter, "1"],
135-
[html.Text, "\n ", 2, ["\n "]],
136-
[html.Element, "div", 2],
137-
[html.Text, "case 0 or 1", 3, ["case 0 or 1"]],
138-
[html.Text, "\n ", 2, ["\n "]],
139-
[html.Text, "\n ", 1, ["\n "]],
140-
[html.Block, "case", 1],
141-
[html.BlockParameter, "2"],
142-
[html.Text, "\n ", 2, ["\n "]],
143-
[html.Element, "div", 2],
144-
[html.Text, "case 2", 3, ["case 2"]],
145-
[html.Text, "\n ", 2, ["\n "]],
146-
[html.Text, "\n ", 1, ["\n "]],
147-
[html.Block, "default", 1],
148-
[html.Text, "\n ", 2, ["\n "]],
149-
[html.Element, "div", 2],
150-
[html.Text, "default", 3, ["default"]],
151-
[html.Text, "\n ", 2, ["\n "]],
152-
[html.Text, "\n", 1, ["\n"]],
153-
[html.Text, "\n ", 0, ["\n "]],
126+
const result = parse(input, { tokenizeAngularBlocks: true });
127+
expect(humanizeDom(result)).toEqual([
128+
[ast.Text, "\n", 0, ["\n"]],
129+
[ast.Block, "switch", 0],
130+
[ast.BlockParameter, "case"],
131+
[ast.Text, "\n ", 1, ["\n "]],
132+
[ast.Block, "case", 1],
133+
[ast.BlockParameter, "0"],
134+
[ast.Block, "case", 1],
135+
[ast.BlockParameter, "1"],
136+
[ast.Text, "\n ", 2, ["\n "]],
137+
[ast.Element, "div", 2],
138+
[ast.Text, "case 0 or 1", 3, ["case 0 or 1"]],
139+
[ast.Text, "\n ", 2, ["\n "]],
140+
[ast.Text, "\n ", 1, ["\n "]],
141+
[ast.Block, "case", 1],
142+
[ast.BlockParameter, "2"],
143+
[ast.Text, "\n ", 2, ["\n "]],
144+
[ast.Element, "div", 2],
145+
[ast.Text, "case 2", 3, ["case 2"]],
146+
[ast.Text, "\n ", 2, ["\n "]],
147+
[ast.Text, "\n ", 1, ["\n "]],
148+
[ast.Block, "default", 1],
149+
[ast.Text, "\n ", 2, ["\n "]],
150+
[ast.Element, "div", 2],
151+
[ast.Text, "default", 3, ["default"]],
152+
[ast.Text, "\n ", 2, ["\n "]],
153+
[ast.Text, "\n", 1, ["\n"]],
154+
[ast.Text, "\n ", 0, ["\n "]],
154155
]);
155156
}
156157
});
157158

158159
it("should support 'tokenizeAngularLetDeclaration'", () => {
159160
const input = `@let foo = 'bar';`;
160-
const ast = parse(input, { tokenizeAngularLetDeclaration: true });
161-
expect(ast.rootNodes).toEqual([
161+
const result = parse(input, { tokenizeAngularLetDeclaration: true });
162+
expect(result.rootNodes).toEqual([
162163
expect.objectContaining({
163164
name: "foo",
164165
kind: "letDeclaration",
@@ -170,10 +171,10 @@ describe("AST format", () => {
170171
// https://github.com/angular/angular/pull/60724
171172
it("should support 'enableAngularSelectorlessSyntax'", () => {
172173
{
173-
const ast = parse("<div @Dir></div>", {
174+
const result = parse("<div @Dir></div>", {
174175
enableAngularSelectorlessSyntax: true,
175176
});
176-
expect(ast.rootNodes).toEqual([
177+
expect(result.rootNodes).toEqual([
177178
expect.objectContaining({
178179
name: "div",
179180
kind: "element",
@@ -188,11 +189,11 @@ describe("AST format", () => {
188189
}
189190

190191
{
191-
const ast = parse("<MyComp>Hello</MyComp>", {
192+
const result = parse("<MyComp>Hello</MyComp>", {
192193
enableAngularSelectorlessSyntax: true,
193194
});
194195

195-
expect(ast.rootNodes).toEqual([
196+
expect(result.rootNodes).toEqual([
196197
expect.objectContaining({
197198
fullName: "MyComp",
198199
componentName: "MyComp",
@@ -202,8 +203,10 @@ describe("AST format", () => {
202203
}
203204

204205
{
205-
const ast = parse("<MyComp/>", { enableAngularSelectorlessSyntax: true });
206-
expect(ast.rootNodes).toEqual([
206+
const result = parse("<MyComp/>", {
207+
enableAngularSelectorlessSyntax: true,
208+
});
209+
expect(result.rootNodes).toEqual([
207210
expect.objectContaining({
208211
fullName: "MyComp",
209212
componentName: "MyComp",
@@ -213,10 +216,10 @@ describe("AST format", () => {
213216
}
214217

215218
{
216-
const ast = parse("<MyComp:button>Hello</MyComp:button>", {
219+
const result = parse("<MyComp:button>Hello</MyComp:button>", {
217220
enableAngularSelectorlessSyntax: true,
218221
});
219-
expect(ast.rootNodes).toEqual([
222+
expect(result.rootNodes).toEqual([
220223
expect.objectContaining({
221224
fullName: "MyComp:button",
222225
componentName: "MyComp",
@@ -226,10 +229,10 @@ describe("AST format", () => {
226229
}
227230

228231
{
229-
const ast = parse("<MyComp:svg:title>Hello</MyComp:svg:title>", {
232+
const result = parse("<MyComp:svg:title>Hello</MyComp:svg:title>", {
230233
enableAngularSelectorlessSyntax: true,
231234
});
232-
expect(ast.rootNodes).toEqual([
235+
expect(result.rootNodes).toEqual([
233236
expect.objectContaining({
234237
fullName: "MyComp:svg:title",
235238
componentName: "MyComp",
@@ -242,6 +245,6 @@ describe("AST format", () => {
242245

243246
it("Edge cases", () => {
244247
expect(humanizeDom(parse("<html:style></html:style>"))).toEqual([
245-
[html.Element, ":html:style", 0],
248+
[ast.Element, ":html:style", 0],
246249
]);
247250
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { it, expect } from "vitest";
2+
import { outdent } from "outdent";
3+
import { parseXml } from "../src/index.ts";
4+
import { humanizeDom } from "../../compiler/test/ml_parser/ast_spec_utils.ts";
5+
import * as ast from "../../compiler/src/ml_parser/ast.ts";
6+
7+
it("parseXml", () => {
8+
const input = outdent`
9+
<?xml version="1.0" encoding="UTF-8"?>
10+
<message>
11+
<warning>
12+
Hello World
13+
</warning>
14+
</message>
15+
`;
16+
expect(humanizeDom(parseXml(input))).toEqual([
17+
[ast.Comment, '?xml version="1.0" encoding="UTF-8"?', 0],
18+
[ast.Text, "\n", 0, ["\n"]],
19+
[ast.Element, "message", 0],
20+
[ast.Text, "\n ", 1, ["\n "]],
21+
[ast.Element, "warning", 1],
22+
[
23+
ast.Text,
24+
"\n Hello World\n ",
25+
2,
26+
["\n Hello World\n "],
27+
],
28+
[ast.Text, "\n", 1, ["\n"]],
29+
]);
30+
});
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
{
22
"compilerOptions": {
3-
"target": "ES2020",
4-
"module": "commonjs",
5-
"baseUrl": ".",
3+
"target": "esnext",
4+
"module": "esnext",
65
"allowImportingTsExtensions": true,
76
"rewriteRelativeImportExtensions": true,
87
"paths": {
98
"@angular/*": ["../*"]
109
},
11-
"types": ["vitest/globals"]
10+
"skipLibCheck": true,
11+
"noEmit": true,
12+
"moduleResolution": "bundler"
1213
}
1314
}

packages/angular-html-parser/yarn.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,7 @@ __metadata:
11971197
dependencies:
11981198
"@types/node": "npm:25.0.2"
11991199
"@vitest/coverage-v8": "npm:4.0.15"
1200+
outdent: "npm:0.8.0"
12001201
prettier: "npm:3.7.4"
12011202
release-it: "npm:19.1.0"
12021203
tsconfig-paths: "npm:4.2.0"
@@ -2722,6 +2723,13 @@ __metadata:
27222723
languageName: node
27232724
linkType: hard
27242725

2726+
"outdent@npm:0.8.0":
2727+
version: 0.8.0
2728+
resolution: "outdent@npm:0.8.0"
2729+
checksum: 10/a556c5c308705ad4e3441be435f2b2cf014cb5f9753a24cbd080eadc473b988c77d0d529a6a9a57c3931fb4178e5a81d668cc4bc49892b668191a5d0ba3df76e
2730+
languageName: node
2731+
linkType: hard
2732+
27252733
"p-map@npm:^7.0.2":
27262734
version: 7.0.2
27272735
resolution: "p-map@npm:7.0.2"

0 commit comments

Comments
 (0)