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