Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/eleven-chicken-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"htmljs-parser": patch
---

Expose extra information about statement and attr value validity.
40 changes: 24 additions & 16 deletions src/__tests__/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
25 changes: 19 additions & 6 deletions src/util/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -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;
Expand All @@ -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 {
Expand All @@ -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;
}