diff --git a/packages/angular-html-parser/package.json b/packages/angular-html-parser/package.json index 3949d9c6d384..725932f3e299 100644 --- a/packages/angular-html-parser/package.json +++ b/packages/angular-html-parser/package.json @@ -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", diff --git a/packages/angular-html-parser/src/index.ts b/packages/angular-html-parser/src/index.ts index 1684624cdceb..3bc402d45ed8 100644 --- a/packages/angular-html-parser/src/index.ts +++ b/packages/angular-html-parser/src/index.ts @@ -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 * @@ -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, @@ -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", { @@ -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 }; diff --git a/packages/angular-html-parser/test/index_spec.ts b/packages/angular-html-parser/test/index_spec.ts index c6d9ce20001a..41da232a1905 100644 --- a/packages/angular-html-parser/test/index_spec.ts +++ b/packages/angular-html-parser/test/index_spec.ts @@ -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", () => { @@ -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, "
", 1, ["
"]], - [html.Element, "custom", 0], - [html.Attribute, "lang", "babel", ["babel"]], - [html.Text, 'const foo = "", 1, ["
"]], + [ast.Element, "custom", 0], + [ast.Attribute, "lang", "babel", ["babel"]], + [ast.Text, 'const foo = " { MJML_RAW_TAGS.has(tagName) ? TagContentType.RAW_TEXT : undefined, }); expect(humanizeDom(result)).toEqual([ - [html.Element, "mj-raw", 0], - [html.Text, "

", 1, ["

"]], + [ast.Element, "mj-raw", 0], + [ast.Text, "

", 1, ["

"]], ]); }); }); @@ -66,8 +67,8 @@ describe("options", () => { describe("AST format", () => { it("should have `type` property", () => { const input = ` txt`; - 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({ @@ -82,8 +83,8 @@ describe("AST format", () => { it("should support 'tokenizeAngularBlocks'", () => { const input = `@if (user.isHuman) {

Hello human

}`; - 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", @@ -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", @@ -170,10 +171,10 @@ describe("AST format", () => { // https://github.com/angular/angular/pull/60724 it("should support 'enableAngularSelectorlessSyntax'", () => { { - const ast = parse("
", { + const result = parse("
", { enableAngularSelectorlessSyntax: true, }); - expect(ast.rootNodes).toEqual([ + expect(result.rootNodes).toEqual([ expect.objectContaining({ name: "div", kind: "element", @@ -188,11 +189,11 @@ describe("AST format", () => { } { - const ast = parse("Hello", { + const result = parse("Hello", { enableAngularSelectorlessSyntax: true, }); - expect(ast.rootNodes).toEqual([ + expect(result.rootNodes).toEqual([ expect.objectContaining({ fullName: "MyComp", componentName: "MyComp", @@ -202,8 +203,10 @@ describe("AST format", () => { } { - const ast = parse("", { enableAngularSelectorlessSyntax: true }); - expect(ast.rootNodes).toEqual([ + const result = parse("", { + enableAngularSelectorlessSyntax: true, + }); + expect(result.rootNodes).toEqual([ expect.objectContaining({ fullName: "MyComp", componentName: "MyComp", @@ -213,10 +216,10 @@ describe("AST format", () => { } { - const ast = parse("Hello", { + const result = parse("Hello", { enableAngularSelectorlessSyntax: true, }); - expect(ast.rootNodes).toEqual([ + expect(result.rootNodes).toEqual([ expect.objectContaining({ fullName: "MyComp:button", componentName: "MyComp", @@ -226,10 +229,10 @@ describe("AST format", () => { } { - const ast = parse("Hello", { + const result = parse("Hello", { enableAngularSelectorlessSyntax: true, }); - expect(ast.rootNodes).toEqual([ + expect(result.rootNodes).toEqual([ expect.objectContaining({ fullName: "MyComp:svg:title", componentName: "MyComp", @@ -242,6 +245,6 @@ describe("AST format", () => { it("Edge cases", () => { expect(humanizeDom(parse(""))).toEqual([ - [html.Element, ":html:style", 0], + [ast.Element, ":html:style", 0], ]); }); diff --git a/packages/angular-html-parser/test/xml_spec.ts b/packages/angular-html-parser/test/xml_spec.ts new file mode 100644 index 000000000000..d50de28301ad --- /dev/null +++ b/packages/angular-html-parser/test/xml_spec.ts @@ -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` + + + + Hello World + + +`; + 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"]], + ]); +}); diff --git a/packages/angular-html-parser/tsconfig.json b/packages/angular-html-parser/tsconfig.json index b734b9e851e3..0235ab0dd383 100644 --- a/packages/angular-html-parser/tsconfig.json +++ b/packages/angular-html-parser/tsconfig.json @@ -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" } } diff --git a/packages/angular-html-parser/yarn.lock b/packages/angular-html-parser/yarn.lock index 95fa6d79e9bf..f19708e6d9dc 100644 --- a/packages/angular-html-parser/yarn.lock +++ b/packages/angular-html-parser/yarn.lock @@ -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" @@ -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"