From 2ddf2742701afe0ecaf177124d60513f5fb69481 Mon Sep 17 00:00:00 2001 From: Matthieu Fillon Date: Thu, 21 May 2026 16:33:37 +0200 Subject: [PATCH 1/2] Fail parsing on HTML syntax error --- .gitignore | 3 +++ package-lock.json | 36 +++++++++++++++--------------------- package.json | 3 +++ src/parser.ts | 23 +++++++++++++++++++++++ test/parser.test.ts | 10 ++++++++++ 5 files changed, 54 insertions(+), 21 deletions(-) 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 bddc420..f28f60d 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", @@ -18,27 +21,6 @@ "prettier": "^3.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -373,6 +355,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 +466,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 +869,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 +1057,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", diff --git a/package.json b/package.json index b77658a..cc12ddb 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..c562a02 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,28 @@ 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 line = start.line + 1; + const col = start.col; + + const err = new SyntaxError( + `${msg} (${line}:${col + 1})` + ); + (err as any).loc = { + start: { line, column: col + 1 }, + end: { line: end.line + 1, column: end.col }, + }; + throw err; + } + return root; }; diff --git a/test/parser.test.ts b/test/parser.test.ts index 8682d2a..5b0a142 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -14,3 +14,13 @@ test("keeps broken directive text untouched", async () => { .content, ).toEqual("
@for(var entry : entries
"); }); +test("throws on invalid HTML nesting", () => { + expect(() => + parse("

", {} as ParserOptions) + ).toThrow(SyntaxError); +}); +test("throws on malformed tag", () => { + expect(() => + parse("Title\n
  • item
  • ", {} as ParserOptions) + ).toThrow(SyntaxError); +}); From cee8b076c6eec0de2265c61bb9c426bee7e7ccd7 Mon Sep 17 00:00:00 2001 From: Matthieu Fillon Date: Fri, 22 May 2026 10:57:14 +0200 Subject: [PATCH 2/2] Fix parse error location + test error content --- src/parser.ts | 39 ++++++++++++++++++++++++++++----------- test/parser.test.ts | 16 ++++++++++++---- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index c562a02..e0eaecb 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -40,19 +40,17 @@ export const parse: Parser["parse"] = (text) => { if (errors.length > 0) { const error = errors[0]; const { msg, span: { start, end } } = error; - const line = start.line + 1; - const col = start.col; - - const err = new SyntaxError( - `${msg} (${line}:${col + 1})` + 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}, + } ); - (err as any).loc = { - start: { line, column: col + 1 }, - end: { line: end.line + 1, column: end.col }, - }; - throw err; } - return root; }; @@ -698,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 5b0a142..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 () => { @@ -15,12 +15,20 @@ test("keeps broken directive text untouched", async () => { ).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("

    • item

    ", {} as ParserOptions) - ).toThrow(SyntaxError); + ).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\n
  • item
  • ", {} as ParserOptions) - ).toThrow(SyntaxError); + parse("Title