From a11c1f1157d7cfaf0e971ee4756b55425cd60b79 Mon Sep 17 00:00:00 2001 From: dpiercey Date: Mon, 9 Feb 2026 16:23:00 -0700 Subject: [PATCH] fix: add more info to attr value / statement validity --- .changeset/eleven-chicken-share.md | 5 ++++ src/__tests__/validate.test.ts | 40 ++++++++++++++++++------------ src/util/validators.ts | 25 ++++++++++++++----- 3 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 .changeset/eleven-chicken-share.md diff --git a/.changeset/eleven-chicken-share.md b/.changeset/eleven-chicken-share.md new file mode 100644 index 00000000..46f0c745 --- /dev/null +++ b/.changeset/eleven-chicken-share.md @@ -0,0 +1,5 @@ +--- +"htmljs-parser": patch +--- + +Expose extra information about statement and attr value validity. diff --git a/src/__tests__/validate.test.ts b/src/__tests__/validate.test.ts index 6cee8d0e..a3d6ae7a 100644 --- a/src/__tests__/validate.test.ts +++ b/src/__tests__/validate.test.ts @@ -4,69 +4,77 @@ import { isValidAttrValue, isValidStatement } from ".."; describe("validation helpers", () => { describe("isValidStatement", () => { it("accepts single-line expressions", () => { - assert.equal(isValidStatement("foo + bar"), true); + assert.equal(isValidStatement("foo + bar"), 2); }); it("accepts indented continuation lines", () => { - assert.equal(isValidStatement("foo\n + bar"), true); + assert.equal(isValidStatement("foo\n + bar"), 1); }); it("rejects unindented continuation lines", () => { - assert.equal(isValidStatement("foo\nbar"), false); + assert.equal(isValidStatement("foo\nbar"), 0); }); it("accepts indented ternary continuation", () => { - assert.equal(isValidStatement("foo ?\n bar : baz"), true); + assert.equal(isValidStatement("foo ?\n bar : baz"), 2); }); it("rejects unterminated groups", () => { - assert.equal(isValidStatement("(foo"), false); + assert.equal(isValidStatement("(foo"), 0); }); it("rejects mismatched closing groups", () => { - assert.equal(isValidStatement(")"), false); + assert.equal(isValidStatement(")"), 0); }); }); describe("isValidAttrValue", () => { it("accepts html attr values with operators", () => { - assert.equal(isValidAttrValue("foo + bar", false), true); + assert.equal(isValidAttrValue("foo + bar", false), 2); }); it("accepts html attr values containing =>", () => { - assert.equal(isValidAttrValue("foo=>bar", false), true); + assert.equal(isValidAttrValue("foo=>bar", false), 2); }); it("rejects html attr values terminated by >", () => { - assert.equal(isValidAttrValue("foo >", false), false); + assert.equal(isValidAttrValue("foo >", false), 0); }); it("accepts concise attr values with >", () => { - assert.equal(isValidAttrValue("foo > bar", true), true); + assert.equal(isValidAttrValue("foo > bar", true), 2); }); it("rejects html attr values terminated by commas", () => { - assert.equal(isValidAttrValue("foo, bar", false), false); + assert.equal(isValidAttrValue("foo, bar", false), 0); }); it("accepts html attr values containing semicolons", () => { - assert.equal(isValidAttrValue("foo;", false), true); + assert.equal(isValidAttrValue("foo;", false), 2); }); it("rejects concise attr values terminated by semicolons", () => { - assert.equal(isValidAttrValue("foo;", true), false); + assert.equal(isValidAttrValue("foo;", true), 0); }); it("accepts html attr values with decrement operator", () => { - assert.equal(isValidAttrValue("foo --", false), true); + assert.equal(isValidAttrValue("foo --", false), 2); }); it("rejects concise attr values with decrement operator", () => { - assert.equal(isValidAttrValue("foo --", true), false); + assert.equal(isValidAttrValue("foo --", true), 0); }); it("rejects attr values separated only by whitespace", () => { - assert.equal(isValidAttrValue("foo bar", false), false); + assert.equal(isValidAttrValue("foo bar", false), 0); + }); + + it("accepts continued multiline logical expression", () => { + assert.equal(isValidAttrValue("a &&\nb", true), 1); + }); + + it("accepts continued multiline enclosed logical expression", () => { + assert.equal(isValidAttrValue("a && (\nb\n)", true), 2); }); }); }); diff --git a/src/util/validators.ts b/src/util/validators.ts index 18b03c4c..bb0e8105 100644 --- a/src/util/validators.ts +++ b/src/util/validators.ts @@ -28,7 +28,13 @@ const ROOT_RANGE = { end: 0, }; -export function isValidStatement(code: string): boolean { +export enum Validity { + invalid, + valid, + enclosed, +} + +export function isValidStatement(code: string): Validity { return isValid(code, true, prepareStatement); } @@ -38,7 +44,7 @@ function prepareStatement(expr: STATE.ExpressionMeta) { expr.consumeIndentedContent = true; } -export function isValidAttrValue(code: string, concise: boolean): boolean { +export function isValidAttrValue(code: string, concise: boolean): Validity { return isValid(code, concise, prepareAttrValue); } @@ -54,7 +60,7 @@ function isValid( data: string, concise: boolean, prepare: (expr: STATE.ExpressionMeta, concise: boolean) => void, -) { +): Validity { const parser = new Parser({}); const maxPos = (parser.maxPos = data.length); parser.pos = 0; @@ -68,18 +74,21 @@ function isValid( parser.activeState = ROOT_STATE; parser.activeRange = ROOT_RANGE; const expr = parser.enterState(STATE.EXPRESSION); + let isEnclosed = true; prepare(expr, concise); while (parser.pos < maxPos) { const code = data.charCodeAt(parser.pos); if (code === CODE.NEWLINE) { + if (isEnclosed && !expr.groupStack.length) isEnclosed = false; parser.forward = 1; parser.activeState.eol.call(parser, 1, parser.activeRange); } else if ( code === CODE.CARRIAGE_RETURN && data.charCodeAt(parser.pos + 1) === CODE.NEWLINE ) { + if (isEnclosed && !expr.groupStack.length) isEnclosed = false; parser.forward = 2; parser.activeState.eol.call(parser, 2, parser.activeRange); } else { @@ -88,15 +97,19 @@ function isValid( } if (parser.activeRange === ROOT_RANGE) { - return false; + return Validity.invalid; } parser.pos += parser.forward; } - return ( + if ( parser.pos === maxPos && parser.activeRange === expr && !expr.groupStack.length - ); + ) { + return isEnclosed ? Validity.enclosed : Validity.valid; + } + + return Validity.invalid; }