diff --git a/.gitignore b/.gitignore index b7ed95d..36ea00b 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,6 @@ dist # Vite logs files vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# IDE files +.idea diff --git a/package-lock.json b/package-lock.json index 4a41efc..9196714 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "prettier-plugin-jte", "version": "1.2.0", "license": "MIT", + "dependencies": { + "angular-html-parser": "^10.6.1" + }, "devDependencies": { "@types/node": "^24.12.2", "prettier": "^3.8.3", @@ -373,6 +376,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -483,6 +487,15 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/angular-html-parser": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-10.6.1.tgz", + "integrity": "sha512-d49rV6cIh8yEYhEs2+YYDOiDLzlRYTKwbBxzitNo21nksKmi0sdU2pypsYi5i6bzV/sL743XrrV/jZZA0VUVIw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -877,6 +890,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -1064,6 +1078,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "dev": true, + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", diff --git a/package.json b/package.json index efa5ce6..6f03344 100644 --- a/package.json +++ b/package.json @@ -42,5 +42,8 @@ }, "peerDependencies": { "prettier": "^3.0.0" + }, + "dependencies": { + "angular-html-parser": "^10.6.1" } } diff --git a/src/parser.ts b/src/parser.ts index 79e7a39..e0eaecb 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -10,6 +10,7 @@ import { Placeholder, RootNode, } from "./jte"; +import {parseHtml} from "angular-html-parser"; const NOT_FOUND = -1; const IGNORE_START = /^/; @@ -30,6 +31,26 @@ export const parse: Parser["parse"] = (text) => { const generatePlaceholder = placeholderGenerator(text); root.content = parseFragment(text, root.nodes, generatePlaceholder); + // Validate HTML using the same parser prettier uses internally + const { errors } = parseHtml(root.content, { + canSelfClose: true, + allowHtmComponentClosingTags: true, + }); + + if (errors.length > 0) { + const error = errors[0]; + const { msg, span: { start, end } } = error; + const startLine = start.line + 1; + const startCol = start.col + 1; + + throw new PrettierParseError( + `${msg} (${startLine}:${startCol})`, + { + start: {line: startLine, column: startCol}, + end: {line: end.line + 1, column: end.col + 1}, + } + ); + } return root; }; @@ -675,3 +696,22 @@ const replaceAt = ( ): string => { return str.slice(0, start) + replacement + str.slice(start + length); }; + +type ParseErrorLocation = { + start: { line: number; column: number }; + end: { line: number; column: number }; +}; + +export class PrettierParseError extends SyntaxError { + loc: ParseErrorLocation; + + constructor( + message: string, + loc: ParseErrorLocation + ) { + super(message); + this.name = "PrettierParseError"; + this.loc = loc; + } + +} \ No newline at end of file diff --git a/test/parser.test.ts b/test/parser.test.ts index 8682d2a..b172c88 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { parse } from "../src/parser"; +import { parse, PrettierParseError } from "../src/parser"; import { ParserOptions } from "prettier"; test("keeps broken expression text untouched", async () => { @@ -14,3 +14,21 @@ test("keeps broken directive text untouched", async () => { .content, ).toEqual("
@for(var entry : entries
"); }); +test("throws on invalid HTML nesting", () => { + const expectedException = new PrettierParseError("Unexpected closing tag \"p\". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags (1:26)", { + start: { line: 1, column: 26 }, + end: { line: 1, column: 30 }, + }); + expect(() => + parse("

", {} as ParserOptions) + ).toThrow(expectedException); +}); +test("throws on malformed tag", () => { + const expectedException = new PrettierParseError("Unexpected character \"EOF\" (1:16)", { + start: { line: 1, column: 16 }, + end: { line: 1, column: 16 }, + }); + expect(() => + parse("Title