Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/angular-html-parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@
"test": "vitest",
"release": "release-it",
"fix": "prettier . --write",
"lint": "prettier . --check"
"lint": "yarn lint:prettier && yarn lint:types",
"lint:prettier": "prettier . --check",
"lint:types": "tsc"
},
"devDependencies": {
"@types/node": "25.0.2",
"@vitest/coverage-v8": "4.0.15",
"outdent": "0.8.0",
"prettier": "3.7.4",
"release-it": "19.1.0",
"tsconfig-paths": "4.2.0",
Expand Down
52 changes: 29 additions & 23 deletions packages/angular-html-parser/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import { HtmlParser } from "../../compiler/src/ml_parser/html_parser.js";
import { TagContentType } from "../../compiler/src/ml_parser/tags.js";
import { ParseTreeResult } from "../../compiler/src/ml_parser/parser.js";
import { HtmlParser } from "../../compiler/src/ml_parser/html_parser.ts";
import { XmlParser } from "../../compiler/src/ml_parser/xml_parser.ts";
import type { TagContentType } from "../../compiler/src/ml_parser/tags.ts";
import { ParseTreeResult as HtmlParseTreeResult } from "../../compiler/src/ml_parser/parser.ts";

let parser: HtmlParser | null = null;

const getParser = () => {
if (!parser) {
parser = new HtmlParser();
}
return parser;
};

export interface ParseOptions {
export interface HtmlParseOptions {
/**
* any element can self close
*
Expand Down Expand Up @@ -56,10 +48,11 @@ export interface ParseOptions {
enableAngularSelectorlessSyntax?: boolean;
}

export function parse(
let htmlParser: HtmlParser;
export function parseHtml(
input: string,
options: ParseOptions = {},
): ParseTreeResult {
options: HtmlParseOptions = {},
): HtmlParseTreeResult {
const {
canSelfClose = false,
allowHtmComponentClosingTags = false,
Expand All @@ -69,7 +62,9 @@ export function parse(
tokenizeAngularLetDeclaration = false,
enableAngularSelectorlessSyntax = false,
} = options;
return getParser().parse(
htmlParser ??= new HtmlParser();

return htmlParser.parse(
input,
"angular-html-parser",
{
Expand All @@ -85,19 +80,30 @@ export function parse(
);
}

let xmlParser: XmlParser;
export function parseXml(input: string) {
xmlParser ??= new XmlParser();

return xmlParser.parse(input, "angular-xml-parser");
}

// For prettier
export { TagContentType };
export { TagContentType } from "../../compiler/src/ml_parser/tags.ts";
export {
RecursiveVisitor,
visitAll,
} from "../../compiler/src/ml_parser/ast.js";
} from "../../compiler/src/ml_parser/ast.ts";
export {
ParseSourceSpan,
ParseLocation,
ParseSourceFile,
} from "../../compiler/src/parse_util.js";
export { getHtmlTagDefinition } from "../../compiler/src/ml_parser/html_tags.js";
} from "../../compiler/src/parse_util.ts";
export { getHtmlTagDefinition } from "../../compiler/src/ml_parser/html_tags.ts";

// Types
export type { ParseTreeResult } from "../../compiler/src/ml_parser/parser.js";
export type * as Ast from "../../compiler/src/ml_parser/ast.js";
export type { ParseTreeResult } from "../../compiler/src/ml_parser/parser.ts";
export type * as Ast from "../../compiler/src/ml_parser/ast.ts";

// Remove these alias in next major release
export type { HtmlParseOptions as ParseOptions };
export { parseHtml as parse };
127 changes: 65 additions & 62 deletions packages/angular-html-parser/test/index_spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { parse, TagContentType } from "../src/index.js";
import { humanizeDom } from "../../compiler/test/ml_parser/ast_spec_utils.js";
import * as html from "../../compiler/src/ml_parser/ast.js";
import { describe, it, expect } from "vitest";
import { parse, TagContentType } from "../src/index.ts";
import { humanizeDom } from "../../compiler/test/ml_parser/ast_spec_utils.ts";
import * as ast from "../../compiler/src/ml_parser/ast.ts";

describe("options", () => {
describe("getTagContentType", () => {
Expand Down Expand Up @@ -35,17 +36,17 @@ describe("options", () => {
}
};
expect(humanizeDom(parse(input, { getTagContentType }))).toEqual([
[html.Element, "template", 0],
[html.Element, "MyComponent", 1],
[html.Element, "template", 2],
[html.Attribute, "#content", ""],
[html.Text, "text", 3, ["text"]],
[html.Element, "template", 0],
[html.Attribute, "lang", "something-else", ["something-else"]],
[html.Text, "<div>", 1, ["<div>"]],
[html.Element, "custom", 0],
[html.Attribute, "lang", "babel", ["babel"]],
[html.Text, 'const foo = "</";', 1, ['const foo = "</";']],
[ast.Element, "template", 0],
[ast.Element, "MyComponent", 1],
[ast.Element, "template", 2],
[ast.Attribute, "#content", ""],
[ast.Text, "text", 3, ["text"]],
[ast.Element, "template", 0],
[ast.Attribute, "lang", "something-else", ["something-else"]],
[ast.Text, "<div>", 1, ["<div>"]],
[ast.Element, "custom", 0],
[ast.Attribute, "lang", "babel", ["babel"]],
[ast.Text, 'const foo = "</";', 1, ['const foo = "</";']],
]);
});

Expand All @@ -56,8 +57,8 @@ describe("options", () => {
MJML_RAW_TAGS.has(tagName) ? TagContentType.RAW_TEXT : undefined,
});
expect(humanizeDom(result)).toEqual([
[html.Element, "mj-raw", 0],
[html.Text, "</p>", 1, ["</p>"]],
[ast.Element, "mj-raw", 0],
[ast.Text, "</p>", 1, ["</p>"]],
]);
});
});
Expand All @@ -66,8 +67,8 @@ describe("options", () => {
describe("AST format", () => {
it("should have `type` property", () => {
const input = `<!DOCTYPE html> <el attr></el>txt<!-- --><![CDATA[foo]]>`;
const ast = parse(input);
expect(ast.rootNodes).toEqual([
const result = parse(input);
expect(result.rootNodes).toEqual([
expect.objectContaining({ kind: "docType" }),
expect.objectContaining({ kind: "text" }),
expect.objectContaining({
Expand All @@ -82,8 +83,8 @@ describe("AST format", () => {

it("should support 'tokenizeAngularBlocks'", () => {
const input = `@if (user.isHuman) { <p>Hello human</p> }`;
const ast = parse(input, { tokenizeAngularBlocks: true });
expect(ast.rootNodes).toEqual([
const result = parse(input, { tokenizeAngularBlocks: true });
expect(result.rootNodes).toEqual([
expect.objectContaining({
name: "if",
kind: "block",
Expand Down Expand Up @@ -122,43 +123,43 @@ describe("AST format", () => {
}
}
`;
const ast = parse(input, { tokenizeAngularBlocks: true });
expect(humanizeDom(ast)).toEqual([
[html.Text, "\n", 0, ["\n"]],
[html.Block, "switch", 0],
[html.BlockParameter, "case"],
[html.Text, "\n ", 1, ["\n "]],
[html.Block, "case", 1],
[html.BlockParameter, "0"],
[html.Block, "case", 1],
[html.BlockParameter, "1"],
[html.Text, "\n ", 2, ["\n "]],
[html.Element, "div", 2],
[html.Text, "case 0 or 1", 3, ["case 0 or 1"]],
[html.Text, "\n ", 2, ["\n "]],
[html.Text, "\n ", 1, ["\n "]],
[html.Block, "case", 1],
[html.BlockParameter, "2"],
[html.Text, "\n ", 2, ["\n "]],
[html.Element, "div", 2],
[html.Text, "case 2", 3, ["case 2"]],
[html.Text, "\n ", 2, ["\n "]],
[html.Text, "\n ", 1, ["\n "]],
[html.Block, "default", 1],
[html.Text, "\n ", 2, ["\n "]],
[html.Element, "div", 2],
[html.Text, "default", 3, ["default"]],
[html.Text, "\n ", 2, ["\n "]],
[html.Text, "\n", 1, ["\n"]],
[html.Text, "\n ", 0, ["\n "]],
const result = parse(input, { tokenizeAngularBlocks: true });
expect(humanizeDom(result)).toEqual([
[ast.Text, "\n", 0, ["\n"]],
[ast.Block, "switch", 0],
[ast.BlockParameter, "case"],
[ast.Text, "\n ", 1, ["\n "]],
[ast.Block, "case", 1],
[ast.BlockParameter, "0"],
[ast.Block, "case", 1],
[ast.BlockParameter, "1"],
[ast.Text, "\n ", 2, ["\n "]],
[ast.Element, "div", 2],
[ast.Text, "case 0 or 1", 3, ["case 0 or 1"]],
[ast.Text, "\n ", 2, ["\n "]],
[ast.Text, "\n ", 1, ["\n "]],
[ast.Block, "case", 1],
[ast.BlockParameter, "2"],
[ast.Text, "\n ", 2, ["\n "]],
[ast.Element, "div", 2],
[ast.Text, "case 2", 3, ["case 2"]],
[ast.Text, "\n ", 2, ["\n "]],
[ast.Text, "\n ", 1, ["\n "]],
[ast.Block, "default", 1],
[ast.Text, "\n ", 2, ["\n "]],
[ast.Element, "div", 2],
[ast.Text, "default", 3, ["default"]],
[ast.Text, "\n ", 2, ["\n "]],
[ast.Text, "\n", 1, ["\n"]],
[ast.Text, "\n ", 0, ["\n "]],
]);
}
});

it("should support 'tokenizeAngularLetDeclaration'", () => {
const input = `@let foo = 'bar';`;
const ast = parse(input, { tokenizeAngularLetDeclaration: true });
expect(ast.rootNodes).toEqual([
const result = parse(input, { tokenizeAngularLetDeclaration: true });
expect(result.rootNodes).toEqual([
expect.objectContaining({
name: "foo",
kind: "letDeclaration",
Expand All @@ -170,10 +171,10 @@ describe("AST format", () => {
// https://github.com/angular/angular/pull/60724
it("should support 'enableAngularSelectorlessSyntax'", () => {
{
const ast = parse("<div @Dir></div>", {
const result = parse("<div @Dir></div>", {
enableAngularSelectorlessSyntax: true,
});
expect(ast.rootNodes).toEqual([
expect(result.rootNodes).toEqual([
expect.objectContaining({
name: "div",
kind: "element",
Expand All @@ -188,11 +189,11 @@ describe("AST format", () => {
}

{
const ast = parse("<MyComp>Hello</MyComp>", {
const result = parse("<MyComp>Hello</MyComp>", {
enableAngularSelectorlessSyntax: true,
});

expect(ast.rootNodes).toEqual([
expect(result.rootNodes).toEqual([
expect.objectContaining({
fullName: "MyComp",
componentName: "MyComp",
Expand All @@ -202,8 +203,10 @@ describe("AST format", () => {
}

{
const ast = parse("<MyComp/>", { enableAngularSelectorlessSyntax: true });
expect(ast.rootNodes).toEqual([
const result = parse("<MyComp/>", {
enableAngularSelectorlessSyntax: true,
});
expect(result.rootNodes).toEqual([
expect.objectContaining({
fullName: "MyComp",
componentName: "MyComp",
Expand All @@ -213,10 +216,10 @@ describe("AST format", () => {
}

{
const ast = parse("<MyComp:button>Hello</MyComp:button>", {
const result = parse("<MyComp:button>Hello</MyComp:button>", {
enableAngularSelectorlessSyntax: true,
});
expect(ast.rootNodes).toEqual([
expect(result.rootNodes).toEqual([
expect.objectContaining({
fullName: "MyComp:button",
componentName: "MyComp",
Expand All @@ -226,10 +229,10 @@ describe("AST format", () => {
}

{
const ast = parse("<MyComp:svg:title>Hello</MyComp:svg:title>", {
const result = parse("<MyComp:svg:title>Hello</MyComp:svg:title>", {
enableAngularSelectorlessSyntax: true,
});
expect(ast.rootNodes).toEqual([
expect(result.rootNodes).toEqual([
expect.objectContaining({
fullName: "MyComp:svg:title",
componentName: "MyComp",
Expand All @@ -242,6 +245,6 @@ describe("AST format", () => {

it("Edge cases", () => {
expect(humanizeDom(parse("<html:style></html:style>"))).toEqual([
[html.Element, ":html:style", 0],
[ast.Element, ":html:style", 0],
]);
});
30 changes: 30 additions & 0 deletions packages/angular-html-parser/test/xml_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { it, expect } from "vitest";
import { outdent } from "outdent";
import { parseXml } from "../src/index.ts";
import { humanizeDom } from "../../compiler/test/ml_parser/ast_spec_utils.ts";
import * as ast from "../../compiler/src/ml_parser/ast.ts";

it("parseXml", () => {
const input = outdent`
<?xml version="1.0" encoding="UTF-8"?>
<message>
<warning>
Hello World
</warning>
</message>
`;
expect(humanizeDom(parseXml(input))).toEqual([
[ast.Comment, '?xml version="1.0" encoding="UTF-8"?', 0],
[ast.Text, "\n", 0, ["\n"]],
[ast.Element, "message", 0],
[ast.Text, "\n ", 1, ["\n "]],
[ast.Element, "warning", 1],
[
ast.Text,
"\n Hello World\n ",
2,
["\n Hello World\n "],
],
[ast.Text, "\n", 1, ["\n"]],
]);
});
9 changes: 5 additions & 4 deletions packages/angular-html-parser/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"baseUrl": ".",
"target": "esnext",
"module": "esnext",
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"paths": {
"@angular/*": ["../*"]
},
"types": ["vitest/globals"]
"skipLibCheck": true,
"noEmit": true,
"moduleResolution": "bundler"
}
}
8 changes: 8 additions & 0 deletions packages/angular-html-parser/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,7 @@ __metadata:
dependencies:
"@types/node": "npm:25.0.2"
"@vitest/coverage-v8": "npm:4.0.15"
outdent: "npm:0.8.0"
prettier: "npm:3.7.4"
release-it: "npm:19.1.0"
tsconfig-paths: "npm:4.2.0"
Expand Down Expand Up @@ -2722,6 +2723,13 @@ __metadata:
languageName: node
linkType: hard

"outdent@npm:0.8.0":
version: 0.8.0
resolution: "outdent@npm:0.8.0"
checksum: 10/a556c5c308705ad4e3441be435f2b2cf014cb5f9753a24cbd080eadc473b988c77d0d529a6a9a57c3931fb4178e5a81d668cc4bc49892b668191a5d0ba3df76e
languageName: node
linkType: hard

"p-map@npm:^7.0.2":
version: 7.0.2
resolution: "p-map@npm:7.0.2"
Expand Down