diff --git a/.changeset/delete-unary-keyword.md b/.changeset/delete-unary-keyword.md
new file mode 100644
index 00000000..0e68726d
--- /dev/null
+++ b/.changeset/delete-unary-keyword.md
@@ -0,0 +1,5 @@
+---
+"htmljs-parser": minor
+---
+
+Read `delete` as a prefix operator, like `typeof` and `void`, so `
` no longer fails as an unterminated regular expression.
diff --git a/.changeset/type-ends-at-void.md b/.changeset/type-ends-at-void.md
new file mode 100644
index 00000000..2c9a1271
--- /dev/null
+++ b/.changeset/type-ends-at-void.md
@@ -0,0 +1,5 @@
+---
+"htmljs-parser": minor
+---
+
+End a type at a line-final `void`. A type statement such as `export type H = () => void` carried on into the next line, and a `void` return type before `{` (`function foo(): void {`) read the body as a type; `void` inside a type is now always the `void` type rather than a prefix operator.
diff --git a/.changeset/valid-type-statement.md b/.changeset/valid-type-statement.md
new file mode 100644
index 00000000..07380ca6
--- /dev/null
+++ b/.changeset/valid-type-statement.md
@@ -0,0 +1,5 @@
+---
+"htmljs-parser": minor
+---
+
+`isValidStatement` now reads a statement starting with `type `, `interface ` or `declare ` as a type, as the parser does, so a multi-line type argument list is `enclosed` and a type followed by an unindented line is `invalid`.
diff --git a/AGENTS.md b/AGENTS.md
index f6cda945..5277f11f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,7 +4,7 @@ An HTML parser recognizes content and string placeholders and allows JavaScript
## Conventions
-When the parser misreports a structure, fix it here rather than working around it in prettier, the compiler, or language-tools. Changing which events existing input produces breaks every consumer: treat it as a breaking change.
+When the parser misreports a structure, fix it here rather than working around it in prettier, the compiler, or language-tools. Changing the events for input that already parses and compiles correctly breaks every consumer: treat it as a breaking change. Fixing a misparse (input that errored, or compiled to the wrong code) is a feature, not a breaking change: release it as a minor, and raise the htmljs-parser floor in the compiler and prettier-plugin-marko together, since both decide what is valid with this parser.
## Agent feedback
diff --git a/src/__tests__/fixtures/attr-operators-space-after/__snapshots__/attr-operators-space-after.expected.txt b/src/__tests__/fixtures/attr-operators-space-after/__snapshots__/attr-operators-space-after.expected.txt
index 0da82f15..e0be3d07 100644
--- a/src/__tests__/fixtures/attr-operators-space-after/__snapshots__/attr-operators-space-after.expected.txt
+++ b/src/__tests__/fixtures/attr-operators-space-after/__snapshots__/attr-operators-space-after.expected.txt
@@ -5,9 +5,10 @@
│ │╰─ attrName
╰─ ╰─ tagName
2╭─ a=x! y a
- │ │││ ╰─ attrName
- │ ││╰─ attrValue.value "x! y"
- │ │├─ attrValue "=x! y"
+ │ │││ │ ╰─ attrName
+ │ │││ ╰─ attrName
+ │ ││╰─ attrValue.value "x!"
+ │ │├─ attrValue "=x!"
│ │╰─ attrName
│ ├─ closeTagEnd(a)
│ ├─ openTagEnd
@@ -368,10 +369,11 @@
│ ├─ closeTagEnd(a)
╰─ ╰─ openTagStart
48╭─
- │ ││││ │╰─ openTagEnd:selfClosed "/>"
- │ ││││ ╰─ attrName
- │ │││╰─ attrValue.value "x! y"
- │ ││├─ attrValue "=x! y"
+ │ ││││ │ │╰─ openTagEnd:selfClosed "/>"
+ │ ││││ │ ╰─ attrName
+ │ ││││ ╰─ attrName
+ │ │││╰─ attrValue.value "x!"
+ │ ││├─ attrValue "=x!"
│ ││╰─ attrName
│ │╰─ tagName
╰─ ╰─ openTagStart
diff --git a/src/__tests__/fixtures/attr-value-delete/__snapshots__/attr-value-delete.expected.txt b/src/__tests__/fixtures/attr-value-delete/__snapshots__/attr-value-delete.expected.txt
new file mode 100644
index 00000000..4d3259d2
--- /dev/null
+++ b/src/__tests__/fixtures/attr-value-delete/__snapshots__/attr-value-delete.expected.txt
@@ -0,0 +1,16 @@
+1╭─
+ │ ││ │││ │││ │╰─ openTagEnd:selfClosed "/>"
+ │ ││ │││ │││ ╰─ attrName
+ │ ││ │││ ││╰─ attrValue.value "delete! y"
+ │ ││ │││ │╰─ attrValue "=delete! y"
+ │ ││ │││ ╰─ attrName
+ │ ││ ││╰─ attrValue.value "delete x.y"
+ │ ││ │╰─ attrValue "=delete x.y"
+ │ ││ ╰─ attrName
+ │ │╰─ tagName "div"
+ ╰─ ╰─ openTagStart
+2╭─ static const d = delete
+ ╰─ ╰─ tagName "static"
+3├─ e.f
+4╭─
+ ╰─ ╰─ openTagEnd
\ No newline at end of file
diff --git a/src/__tests__/fixtures/attr-value-delete/input.marko b/src/__tests__/fixtures/attr-value-delete/input.marko
new file mode 100644
index 00000000..abf04526
--- /dev/null
+++ b/src/__tests__/fixtures/attr-value-delete/input.marko
@@ -0,0 +1,3 @@
+
+static const d = delete
+e.f
diff --git a/src/__tests__/fixtures/attr-value-non-null-assertion-before-attr/__snapshots__/attr-value-non-null-assertion-before-attr.expected.txt b/src/__tests__/fixtures/attr-value-non-null-assertion-before-attr/__snapshots__/attr-value-non-null-assertion-before-attr.expected.txt
new file mode 100644
index 00000000..711daead
--- /dev/null
+++ b/src/__tests__/fixtures/attr-value-non-null-assertion-before-attr/__snapshots__/attr-value-non-null-assertion-before-attr.expected.txt
@@ -0,0 +1,11 @@
+1╭─
+ │ ││ │ ││ │ ││ ╰─ openTagEnd:selfClosed "/>"
+ │ ││ │ ││ │ │╰─ attrValue.value "\"a\""
+ │ ││ │ ││ │ ╰─ attrValue "=\"a\""
+ │ ││ │ ││ ╰─ attrName "id"
+ │ ││ │ │╰─ attrValue.value "input.x!"
+ │ ││ │ ╰─ attrValue "=input.x!"
+ │ ││ ╰─ attrName "title"
+ │ │╰─ tagName "div"
+ ╰─ ╰─ openTagStart
+2╰─
\ No newline at end of file
diff --git a/src/__tests__/fixtures/attr-value-non-null-assertion-before-attr/input.marko b/src/__tests__/fixtures/attr-value-non-null-assertion-before-attr/input.marko
new file mode 100644
index 00000000..5da827b5
--- /dev/null
+++ b/src/__tests__/fixtures/attr-value-non-null-assertion-before-attr/input.marko
@@ -0,0 +1 @@
+
diff --git a/src/__tests__/fixtures/attr-value-non-null-assertion-before-open-tag-end/__snapshots__/attr-value-non-null-assertion-before-open-tag-end.expected.txt b/src/__tests__/fixtures/attr-value-non-null-assertion-before-open-tag-end/__snapshots__/attr-value-non-null-assertion-before-open-tag-end.expected.txt
new file mode 100644
index 00000000..9636cc45
--- /dev/null
+++ b/src/__tests__/fixtures/attr-value-non-null-assertion-before-open-tag-end/__snapshots__/attr-value-non-null-assertion-before-open-tag-end.expected.txt
@@ -0,0 +1,15 @@
+1╭─
+ │ ││ ││││ ╰─ openTagEnd
+ │ ││ │││╰─ attrValue.value "input.x!"
+ │ ││ ││├─ attrValue "=input.x!"
+ │ ││ ││╰─ attrName
+ │ ││ │╰─ tagVar.value
+ │ ││ ╰─ tagVar "/z"
+ │ │╰─ tagName "const"
+ ╰─ ╰─ openTagStart
+2╭─
+ │ │ │ ╰─ closeTagEnd(const)
+ │ │ ╰─ closeTagName "const"
+ │ ├─ text "\n"
+ ╰─ ╰─ closeTagStart ""
+3╰─
\ No newline at end of file
diff --git a/src/__tests__/fixtures/attr-value-non-null-assertion-before-open-tag-end/input.marko b/src/__tests__/fixtures/attr-value-non-null-assertion-before-open-tag-end/input.marko
new file mode 100644
index 00000000..c621939c
--- /dev/null
+++ b/src/__tests__/fixtures/attr-value-non-null-assertion-before-open-tag-end/input.marko
@@ -0,0 +1,2 @@
+
+
diff --git a/src/__tests__/fixtures/attr-value-non-null-assertion-before-self-close/__snapshots__/attr-value-non-null-assertion-before-self-close.expected.txt b/src/__tests__/fixtures/attr-value-non-null-assertion-before-self-close/__snapshots__/attr-value-non-null-assertion-before-self-close.expected.txt
new file mode 100644
index 00000000..503a7df4
--- /dev/null
+++ b/src/__tests__/fixtures/attr-value-non-null-assertion-before-self-close/__snapshots__/attr-value-non-null-assertion-before-self-close.expected.txt
@@ -0,0 +1,11 @@
+1╭─
+ │ ││ │││ │││ ╰─ openTagEnd:selfClosed "/>"
+ │ ││ │││ ││╰─ attrValue.value "f()!"
+ │ ││ │││ │╰─ attrValue "=f()!"
+ │ ││ │││ ╰─ attrName
+ │ ││ ││╰─ attrValue.value "a[0]!"
+ │ ││ │╰─ attrValue "=a[0]!"
+ │ ││ ╰─ attrName
+ │ │╰─ tagName "div"
+ ╰─ ╰─ openTagStart
+2╰─
\ No newline at end of file
diff --git a/src/__tests__/fixtures/attr-value-non-null-assertion-before-self-close/input.marko b/src/__tests__/fixtures/attr-value-non-null-assertion-before-self-close/input.marko
new file mode 100644
index 00000000..f1f82ef6
--- /dev/null
+++ b/src/__tests__/fixtures/attr-value-non-null-assertion-before-self-close/input.marko
@@ -0,0 +1 @@
+
diff --git a/src/__tests__/fixtures/non-null-assertion-at-eol/__snapshots__/non-null-assertion-at-eol.expected.txt b/src/__tests__/fixtures/non-null-assertion-at-eol/__snapshots__/non-null-assertion-at-eol.expected.txt
new file mode 100644
index 00000000..174570ee
--- /dev/null
+++ b/src/__tests__/fixtures/non-null-assertion-at-eol/__snapshots__/non-null-assertion-at-eol.expected.txt
@@ -0,0 +1,15 @@
+1╭─ static const a = b!
+ ╰─ ╰─ tagName "static"
+2╭─ div title=input.x!
+ │ │ │ │╰─ attrValue.value "input.x!"
+ │ │ │ ╰─ attrValue "=input.x!"
+ │ │ ╰─ attrName "title"
+ │ ├─ openTagEnd
+ ╰─ ╰─ tagName "div"
+3╭─ span
+ │ │ ╰─ tagName "span"
+ ╰─ ╰─ openTagEnd
+4╭─
+ │ ├─ openTagEnd
+ │ ├─ closeTagEnd(div)
+ ╰─ ╰─ closeTagEnd(span)
\ No newline at end of file
diff --git a/src/__tests__/fixtures/non-null-assertion-at-eol/input.marko b/src/__tests__/fixtures/non-null-assertion-at-eol/input.marko
new file mode 100644
index 00000000..05fa8067
--- /dev/null
+++ b/src/__tests__/fixtures/non-null-assertion-at-eol/input.marko
@@ -0,0 +1,3 @@
+static const a = b!
+div title=input.x!
+ span
diff --git a/src/__tests__/fixtures/placeholder-empty/__snapshots__/placeholder-empty.expected.txt b/src/__tests__/fixtures/placeholder-empty/__snapshots__/placeholder-empty.expected.txt
index 61314626..d23c7ba3 100644
--- a/src/__tests__/fixtures/placeholder-empty/__snapshots__/placeholder-empty.expected.txt
+++ b/src/__tests__/fixtures/placeholder-empty/__snapshots__/placeholder-empty.expected.txt
@@ -1,7 +1,5 @@
1╭─
${}
- │ ││ ││ ├─ placeholder:escape.value "}
"
- │ ││ ││ ╰─ error(MALFORMED_PLACEHOLDER:Invalid placeholder, the expression cannot be missing)
- │ ││ │╰─ placeholder:escape "${}
"
+ │ ││ │ ╰─ error(MALFORMED_PLACEHOLDER:Invalid placeholder, the expression cannot be missing)
│ ││ ╰─ openTagEnd
│ │╰─ tagName "div"
╰─ ╰─ openTagStart
\ No newline at end of file
diff --git a/src/__tests__/fixtures/prefix-not-after-keyword-operator/__snapshots__/prefix-not-after-keyword-operator.expected.txt b/src/__tests__/fixtures/prefix-not-after-keyword-operator/__snapshots__/prefix-not-after-keyword-operator.expected.txt
new file mode 100644
index 00000000..daeae6ab
--- /dev/null
+++ b/src/__tests__/fixtures/prefix-not-after-keyword-operator/__snapshots__/prefix-not-after-keyword-operator.expected.txt
@@ -0,0 +1,30 @@
+1╭─ static const a = typeof!
+ ╰─ ╰─ tagName "static"
+2├─ b
+3╭─ static const c = d in!
+ │ ├─ openTagEnd
+ ╰─ ╰─ tagName "static"
+4├─ e
+5╭─ static const f = g instanceof!
+ │ ├─ openTagEnd
+ ╰─ ╰─ tagName "static"
+6├─ h
+7╭─ static const i = main!
+ │ ├─ openTagEnd
+ ╰─ ╰─ tagName "static"
+8╭─
+ │ ││ │││ │││ │││ │╰─ openTagEnd:selfClosed "/>"
+ │ ││ │││ │││ │││ ╰─ attrName
+ │ ││ │││ │││ ││╰─ attrValue.value "input.new!"
+ │ ││ │││ │││ │╰─ attrValue "=input.new!"
+ │ ││ │││ │││ ╰─ attrName
+ │ ││ │││ ││╰─ attrValue.value "void! y"
+ │ ││ │││ │╰─ attrValue "=void! y"
+ │ ││ │││ ╰─ attrName
+ │ ││ ││╰─ attrValue.value "typeof! x"
+ │ ││ │╰─ attrValue "=typeof! x"
+ │ ││ ╰─ attrName
+ │ │╰─ tagName "div"
+ │ ├─ openTagEnd
+ ╰─ ╰─ openTagStart
+9╰─
\ No newline at end of file
diff --git a/src/__tests__/fixtures/prefix-not-after-keyword-operator/input.marko b/src/__tests__/fixtures/prefix-not-after-keyword-operator/input.marko
new file mode 100644
index 00000000..77e17679
--- /dev/null
+++ b/src/__tests__/fixtures/prefix-not-after-keyword-operator/input.marko
@@ -0,0 +1,8 @@
+static const a = typeof!
+b
+static const c = d in!
+e
+static const f = g instanceof!
+h
+static const i = main!
+
diff --git a/src/__tests__/fixtures/ts-tag-var-type-void/__snapshots__/ts-tag-var-type-void.expected.txt b/src/__tests__/fixtures/ts-tag-var-type-void/__snapshots__/ts-tag-var-type-void.expected.txt
new file mode 100644
index 00000000..a4905fee
--- /dev/null
+++ b/src/__tests__/fixtures/ts-tag-var-type-void/__snapshots__/ts-tag-var-type-void.expected.txt
@@ -0,0 +1,12 @@
+1╭─
+ │ ││ ││ ╰─ openTagEnd
+ │ ││ │╰─ tagVar.value "x: void"
+ │ ││ ╰─ tagVar "/x: void"
+ │ │╰─ tagName "let"
+ ╰─ ╰─ openTagStart
+2╭─
+ │ │ │ ╰─ closeTagEnd(let)
+ │ │ ╰─ closeTagName "let"
+ │ ├─ text "\n"
+ ╰─ ╰─ closeTagStart ""
+3╰─
\ No newline at end of file
diff --git a/src/__tests__/fixtures/ts-tag-var-type-void/input.marko b/src/__tests__/fixtures/ts-tag-var-type-void/input.marko
new file mode 100644
index 00000000..2789e678
--- /dev/null
+++ b/src/__tests__/fixtures/ts-tag-var-type-void/input.marko
@@ -0,0 +1,2 @@
+
+
diff --git a/src/__tests__/fixtures/ts-type-statement-void-before-statement/__snapshots__/ts-type-statement-void-before-statement.expected.txt b/src/__tests__/fixtures/ts-type-statement-void-before-statement/__snapshots__/ts-type-statement-void-before-statement.expected.txt
new file mode 100644
index 00000000..039ab0e8
--- /dev/null
+++ b/src/__tests__/fixtures/ts-type-statement-void-before-statement/__snapshots__/ts-type-statement-void-before-statement.expected.txt
@@ -0,0 +1,7 @@
+1╭─ static type F = () => void
+ ╰─ ╰─ tagName "static"
+2╭─ static const x = 1
+ │ ├─ openTagEnd
+ ╰─ ╰─ tagName "static"
+3╭─
+ ╰─ ╰─ openTagEnd
\ No newline at end of file
diff --git a/src/__tests__/fixtures/ts-type-statement-void-before-statement/input.marko b/src/__tests__/fixtures/ts-type-statement-void-before-statement/input.marko
new file mode 100644
index 00000000..e68705b7
--- /dev/null
+++ b/src/__tests__/fixtures/ts-type-statement-void-before-statement/input.marko
@@ -0,0 +1,2 @@
+static type F = () => void
+static const x = 1
diff --git a/src/__tests__/fixtures/ts-type-statement-void-before-tag/__snapshots__/ts-type-statement-void-before-tag.expected.txt b/src/__tests__/fixtures/ts-type-statement-void-before-tag/__snapshots__/ts-type-statement-void-before-tag.expected.txt
new file mode 100644
index 00000000..29af8c7a
--- /dev/null
+++ b/src/__tests__/fixtures/ts-type-statement-void-before-tag/__snapshots__/ts-type-statement-void-before-tag.expected.txt
@@ -0,0 +1,8 @@
+1╭─ export type H = (v: string) => void
+ ╰─ ╰─ tagName "export"
+2╭─
+ │ ││ ╰─ openTagEnd:selfClosed "/>"
+ │ │╰─ tagName "div"
+ │ ├─ openTagEnd
+ ╰─ ╰─ openTagStart
+3╰─
\ No newline at end of file
diff --git a/src/__tests__/fixtures/ts-type-statement-void-before-tag/input.marko b/src/__tests__/fixtures/ts-type-statement-void-before-tag/input.marko
new file mode 100644
index 00000000..8a192ee0
--- /dev/null
+++ b/src/__tests__/fixtures/ts-type-statement-void-before-tag/input.marko
@@ -0,0 +1,2 @@
+export type H = (v: string) => void
+
diff --git a/src/__tests__/fixtures/ts-void-return-type-before-body/__snapshots__/ts-void-return-type-before-body.expected.txt b/src/__tests__/fixtures/ts-void-return-type-before-body/__snapshots__/ts-void-return-type-before-body.expected.txt
new file mode 100644
index 00000000..5b679656
--- /dev/null
+++ b/src/__tests__/fixtures/ts-void-return-type-before-body/__snapshots__/ts-void-return-type-before-body.expected.txt
@@ -0,0 +1,10 @@
+1╭─ static function foo(): void {
+ ╰─ ╰─ tagName "static"
+2├─ if (a < b) {}
+3├─ }
+4╭─
+ │ ││ ╰─ openTagEnd:selfClosed "/>"
+ │ │╰─ tagName "div"
+ │ ├─ openTagEnd
+ ╰─ ╰─ openTagStart
+5╰─
\ No newline at end of file
diff --git a/src/__tests__/fixtures/ts-void-return-type-before-body/input.marko b/src/__tests__/fixtures/ts-void-return-type-before-body/input.marko
new file mode 100644
index 00000000..d18db89f
--- /dev/null
+++ b/src/__tests__/fixtures/ts-void-return-type-before-body/input.marko
@@ -0,0 +1,4 @@
+static function foo(): void {
+ if (a < b) {}
+}
+
diff --git a/src/__tests__/validate.test.ts b/src/__tests__/validate.test.ts
index 9c4c471d..90fc0ed6 100644
--- a/src/__tests__/validate.test.ts
+++ b/src/__tests__/validate.test.ts
@@ -40,6 +40,17 @@ describe("validation helpers", () => {
it("treats newlines in enclosed template literals as guarded", () => {
assert.equal(isValidStatement("(`foo\nbar`)"), 2);
});
+
+ it("reads type arguments in a type statement as a group", () => {
+ assert.equal(
+ isValidStatement("type A = Record<\n string,\n number\n>"),
+ 2,
+ );
+ });
+
+ it("ends a type statement at an unindented newline", () => {
+ assert.equal(isValidStatement("type A = B
\nfoo"), 0);
+ });
});
describe("isValidScriptlet", () => {
diff --git a/src/states/EXPRESSION.ts b/src/states/EXPRESSION.ts
index 2f9fd156..c3fd96fe 100644
--- a/src/states/EXPRESSION.ts
+++ b/src/states/EXPRESSION.ts
@@ -39,9 +39,13 @@ const unaryKeywords = [
"function",
"new",
"typeof",
- "void",
] as const;
+// Only JavaScript has `delete` and `void` operators; in a type `void` is the
+// `void` type. `return`, `throw` and `yield` are left out: JavaScript allows
+// no line break after them, so they never continue an expression.
+const jsUnaryKeywords = [...unaryKeywords, "delete", "void"] as const;
+
const tsUnaryKeywords = [
...unaryKeywords,
"asserts",
@@ -60,6 +64,8 @@ const binaryKeywords = [
"satisfies",
] as const;
+const relationalKeywords = ["in", "instanceof"] as const;
+
export const EXPRESSION: StateDefinition = {
name: "EXPRESSION",
@@ -469,7 +475,6 @@ function lookBehindForOperator(
case CODE.CARET:
case CODE.COLON:
case CODE.EQUAL:
- case CODE.EXCLAMATION:
case CODE.OPEN_ANGLE_BRACKET:
case CODE.PERCENT:
case CODE.PIPE:
@@ -477,6 +482,23 @@ function lookBehindForOperator(
case CODE.TILDE:
return curPos;
+ case CODE.EXCLAMATION: {
+ // After an operand, `!` is a TypeScript non-null assertion (postfix);
+ // after a keyword operator (`typeof!a`, `a in!b`) it is the prefix `!`.
+ const prevCode = data.charCodeAt(curPos - 1);
+ switch (prevCode) {
+ case CODE.CLOSE_PAREN:
+ case CODE.CLOSE_SQUARE_BRACKET:
+ return -1;
+ default:
+ return isWordCode(prevCode) &&
+ lookBehindForOperator(expression, data, curPos) === -1 &&
+ lookBehindForKeyword(data, curPos - 1, relationalKeywords) === -1
+ ? -1
+ : curPos;
+ }
+ }
+
case CODE.CLOSE_ANGLE_BRACKET:
return data.charCodeAt(curPos - 1) === CODE.EQUAL
? curPos - 1
@@ -511,17 +533,11 @@ function lookBehindForOperator(
// before `pos` is not one, no keyword can match.
if (code < CODE.LOWER_A || code > CODE.LOWER_Z) return -1;
- for (const keyword of expression.inType
- ? tsUnaryKeywords
- : unaryKeywords) {
- const keywordPos = lookBehindFor(data, curPos, keyword);
- if (keywordPos !== -1) {
- return isWordOrPeriodCode(data.charCodeAt(keywordPos - 1))
- ? -1
- : keywordPos;
- }
- }
- return -1;
+ return lookBehindForKeyword(
+ data,
+ curPos,
+ expression.inType ? tsUnaryKeywords : jsUnaryKeywords,
+ );
}
}
}
@@ -654,6 +670,23 @@ function lookBehindWhile(
return 0;
}
+// Returns where a whole keyword ending at `pos` starts, or -1.
+function lookBehindForKeyword(
+ data: string,
+ pos: number,
+ keywords: readonly string[],
+) {
+ for (const keyword of keywords) {
+ const keywordPos = lookBehindFor(data, pos, keyword);
+ if (keywordPos !== -1) {
+ return isWordOrPeriodCode(data.charCodeAt(keywordPos - 1))
+ ? -1
+ : keywordPos;
+ }
+ }
+ return -1;
+}
+
function lookBehindFor(data: string, pos: number, str: string) {
let i = str.length;
const endPos = pos - i + 1;
diff --git a/src/states/PLACEHOLDER.ts b/src/states/PLACEHOLDER.ts
index 750aa2f2..082687d4 100644
--- a/src/states/PLACEHOLDER.ts
+++ b/src/states/PLACEHOLDER.ts
@@ -43,7 +43,7 @@ export const PLACEHOLDER: StateDefinition = {
return(child) {
if (child.start === child.end) {
- this.emitError(
+ return this.emitError(
child,
ErrorCode.MALFORMED_PLACEHOLDER,
"Invalid placeholder, the expression cannot be missing",
diff --git a/src/states/TAG_NAME.ts b/src/states/TAG_NAME.ts
index 13272e52..13a768c9 100644
--- a/src/states/TAG_NAME.ts
+++ b/src/states/TAG_NAME.ts
@@ -2,6 +2,7 @@ import {
isWhitespaceCode,
matchesCloseCurlyBrace,
type Meta,
+ Parser,
type Ranges,
STATE,
type StateDefinition,
@@ -96,21 +97,11 @@ export const TAG_NAME: StateDefinition = {
);
}
- const expr = this.enterState(STATE.EXPRESSION);
- expr.operators = true;
- expr.terminatedByEOL = true;
- expr.consumeIndentedContent = true;
-
- const typeStatementMatch =
- this.lookAheadFor("declare ") ||
- this.lookAheadFor("interface ") ||
- this.lookAheadFor("type ");
- if (typeStatementMatch) {
- expr.inType = true;
- expr.forceType = true;
- this.pos += typeStatementMatch.length;
- this.consumeWhitespace();
- }
+ prepareStatement(
+ this.enterState(STATE.EXPRESSION),
+ this,
+ this.pos + 1,
+ );
}
}
@@ -197,3 +188,25 @@ export const TAG_NAME: StateDefinition = {
quasis.push({ start: nextStart, end: nextStart });
},
};
+
+// Sets up a root statement's expression, whose code starts at `pos`.
+export function prepareStatement(
+ expr: STATE.ExpressionMeta,
+ parser: Parser,
+ pos = parser.pos,
+) {
+ expr.operators = true;
+ expr.terminatedByEOL = true;
+ expr.consumeIndentedContent = true;
+
+ const typeStatementMatch =
+ parser.lookAheadFor("declare ", pos) ||
+ parser.lookAheadFor("interface ", pos) ||
+ parser.lookAheadFor("type ", pos);
+ if (typeStatementMatch) {
+ expr.inType = true;
+ expr.forceType = true;
+ parser.pos = pos + typeStatementMatch.length;
+ parser.consumeWhitespace();
+ }
+}
diff --git a/src/util/validators.ts b/src/util/validators.ts
index 740970b6..40dcab06 100644
--- a/src/util/validators.ts
+++ b/src/util/validators.ts
@@ -1,5 +1,6 @@
import { type Meta, Parser, STATE, type StateDefinition } from "../internal.ts";
import {
+ prepareStatement,
shouldTerminateConciseAttrValue,
shouldTerminateHtmlAttrValue,
} from "../states/index.ts";
@@ -33,12 +34,6 @@ export function isValidStatement(code: string): Validity {
return isValid(code, true, prepareStatement);
}
-function prepareStatement(expr: STATE.ExpressionMeta) {
- expr.operators = true;
- expr.terminatedByEOL = true;
- expr.consumeIndentedContent = true;
-}
-
export function isValidScriptlet(code: string): Validity {
return isValid(code, true, prepareScriptlet);
}
@@ -52,10 +47,10 @@ export function isValidAttrValue(code: string, concise: boolean): Validity {
return isValid(code, concise, prepareAttrValue);
}
-function prepareAttrValue(expr: STATE.ExpressionMeta, concise: boolean) {
+function prepareAttrValue(expr: STATE.ExpressionMeta, parser: Parser) {
expr.operators = true;
expr.terminatedByWhitespace = true;
- expr.shouldTerminate = concise
+ expr.shouldTerminate = parser.isConcise
? shouldTerminateConciseAttrValue
: shouldTerminateHtmlAttrValue;
}
@@ -63,7 +58,7 @@ function prepareAttrValue(expr: STATE.ExpressionMeta, concise: boolean) {
function isValid(
data: string,
concise: boolean,
- prepare: (expr: STATE.ExpressionMeta, concise: boolean) => void,
+ prepare: (expr: STATE.ExpressionMeta, parser: Parser) => void,
): Validity {
let hasError = false;
const parser = new Parser({
@@ -85,7 +80,7 @@ function isValid(
parser.activeState = ROOT_STATE;
parser.activeRange = ROOT_RANGE;
const expr = parser.enterState(STATE.EXPRESSION);
- prepare(expr, concise);
+ prepare(expr, parser);
while (parser.pos <= maxPos) {
const childActive = parser.activeRange !== expr;