Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,6 @@ dist
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

# IDE files
.idea
36 changes: 15 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,8 @@
},
"peerDependencies": {
"prettier": "^3.0.0"
},
"dependencies": {
"angular-html-parser": "^10.6.1"
}
}
40 changes: 40 additions & 0 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Placeholder,
RootNode,
} from "./jte";
import {parseHtml} from "angular-html-parser";

const NOT_FOUND = -1;
const IGNORE_START = /^<!--\s*prettier-ignore-start\s*-->/;
Expand All @@ -30,6 +31,26 @@ export const parse: Parser<Node>["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;
};

Expand Down Expand Up @@ -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;
}

}
20 changes: 19 additions & 1 deletion test/parser.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -14,3 +14,21 @@ test("keeps broken directive text untouched", async () => {
.content,
).toEqual("<div>@for(var entry : entries </div>");
});
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("<p><ul><li>item</li></ul></p>", {} 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("<strong>Title</", {} as ParserOptions)
).toThrow(expectedException)
});