From 4a800cbeec71c02591d4a96a954574d32fd56c57 Mon Sep 17 00:00:00 2001 From: dpiercey Date: Fri, 8 Aug 2025 09:06:26 -0700 Subject: [PATCH] fix: improve typescript generic parsing --- .changeset/major-experts-start.md | 5 + README.md | 5 +- package.json | 8 +- .../ts-function-type.expected.txt | 7 + .../fixtures/ts-function-type/input.marko | 1 + .../ts-generic-complex.expected.txt | 15 ++ .../fixtures/ts-generic-complex/input.marko | 2 + .../ts-generic-function-type.expected.txt | 15 ++ .../ts-generic-function-type/input.marko | 2 + .../ts-generic-simple.expected.txt | 15 ++ .../fixtures/ts-generic-simple/input.marko | 2 + .../ts-intersection-type.expected.txt | 14 ++ .../fixtures/ts-intersection-type/input.marko | 2 + .../ts-keyof-typeof.expected.txt | 8 + .../fixtures/ts-keyof-typeof/input.marko | 1 + .../ts-nested-generics.expected.txt | 15 ++ .../fixtures/ts-nested-generics/input.marko | 2 + .../__snapshots__/ts-static-type.expected.txt | 11 ++ .../fixtures/ts-static-type/input.marko | 5 + .../ts-tag-var-type-generic.expected.txt | 13 ++ .../ts-tag-var-type-generic/input.marko | 4 + .../ts-type-statement.expected.txt | 31 ++++ .../fixtures/ts-type-statement/input.marko | 22 +++ src/states/EXPRESSION.ts | 155 ++++++++++++++---- src/states/OPEN_TAG.ts | 21 ++- src/states/TAG_NAME.ts | 12 ++ 26 files changed, 347 insertions(+), 46 deletions(-) create mode 100644 .changeset/major-experts-start.md create mode 100644 src/__tests__/fixtures/ts-function-type/__snapshots__/ts-function-type.expected.txt create mode 100644 src/__tests__/fixtures/ts-function-type/input.marko create mode 100644 src/__tests__/fixtures/ts-generic-complex/__snapshots__/ts-generic-complex.expected.txt create mode 100644 src/__tests__/fixtures/ts-generic-complex/input.marko create mode 100644 src/__tests__/fixtures/ts-generic-function-type/__snapshots__/ts-generic-function-type.expected.txt create mode 100644 src/__tests__/fixtures/ts-generic-function-type/input.marko create mode 100644 src/__tests__/fixtures/ts-generic-simple/__snapshots__/ts-generic-simple.expected.txt create mode 100644 src/__tests__/fixtures/ts-generic-simple/input.marko create mode 100644 src/__tests__/fixtures/ts-intersection-type/__snapshots__/ts-intersection-type.expected.txt create mode 100644 src/__tests__/fixtures/ts-intersection-type/input.marko create mode 100644 src/__tests__/fixtures/ts-keyof-typeof/__snapshots__/ts-keyof-typeof.expected.txt create mode 100644 src/__tests__/fixtures/ts-keyof-typeof/input.marko create mode 100644 src/__tests__/fixtures/ts-nested-generics/__snapshots__/ts-nested-generics.expected.txt create mode 100644 src/__tests__/fixtures/ts-nested-generics/input.marko create mode 100644 src/__tests__/fixtures/ts-static-type/__snapshots__/ts-static-type.expected.txt create mode 100644 src/__tests__/fixtures/ts-static-type/input.marko create mode 100644 src/__tests__/fixtures/ts-tag-var-type-generic/__snapshots__/ts-tag-var-type-generic.expected.txt create mode 100644 src/__tests__/fixtures/ts-tag-var-type-generic/input.marko create mode 100644 src/__tests__/fixtures/ts-type-statement/__snapshots__/ts-type-statement.expected.txt create mode 100644 src/__tests__/fixtures/ts-type-statement/input.marko diff --git a/.changeset/major-experts-start.md b/.changeset/major-experts-start.md new file mode 100644 index 0000000..1cce7fb --- /dev/null +++ b/.changeset/major-experts-start.md @@ -0,0 +1,5 @@ +--- +"htmljs-parser": minor +--- + +Improve expression parsing for typescript, especially around multi line generics. diff --git a/README.md b/README.md index db845f0..70b79aa 100644 --- a/README.md +++ b/README.md @@ -201,15 +201,18 @@ const parser = createParser({ // TagType.void makes this a void element (cannot have children). return TagType.void; case "html-comment": + case "html-script": + case "html-style": case "script": case "style": case "textarea": // TagType.text makes the child content text only (with placeholders). return TagType.text; - case "class": case "export": case "import": case "static": + case "client": + case "server": // TagType.statement makes this a statement tag where the content following the tag name will be parsed as script code until we reach a new line, eg for `import x from "y"`). return TagType.statement; } diff --git a/package.json b/package.json index aa9e5f9..d85e156 100644 --- a/package.json +++ b/package.json @@ -63,18 +63,16 @@ "bench": "tsx bench.mts", "build": "tsc -b && tsx build.mts", "change": "changeset add", - "ci:test": "nyc npm run mocha -- --forbid-only", + "ci:test": "nyc npm test -- --forbid-only", "format": "npm run lint:eslint -- --fix && npm run lint:prettier -- --write && (fixpack || true)", "lint": "tsc -b && npm run lint:eslint && npm run lint:prettier -- -l && fixpack", "lint:eslint": "eslint -f visualstudio .", "lint:prettier": "prettier \"./**/*{.ts,.js,.json,.md,.yml,rc}\"", - "mocha": "cross-env NODE_ENV=test mocha \"./src/**/__tests__/*.test.ts\"", "prepare": "husky", "release": "npm run build && changeset publish", "report": "open ./coverage/lcov-report/index.html", - "test": "npm run mocha -- --watch", - "test:inspect": "npm test -- --inspect", - "test:update": "npm run mocha -- --update", + "test": "cross-env NODE_ENV=test mocha \"./src/**/__tests__/*.test.ts\"", + "test:update": "npm test -- --update", "version": "changeset version && npm i --package-lock-only" }, "types": "dist/index.d.ts" diff --git a/src/__tests__/fixtures/ts-function-type/__snapshots__/ts-function-type.expected.txt b/src/__tests__/fixtures/ts-function-type/__snapshots__/ts-function-type.expected.txt new file mode 100644 index 0000000..b91992d --- /dev/null +++ b/src/__tests__/fixtures/ts-function-type/__snapshots__/ts-function-type.expected.txt @@ -0,0 +1,7 @@ +1╭─
string /> + │ ││ │ ││ ╰─ openTagEnd:selfClosed "/>" + │ ││ │ │╰─ attrValue.value "fn as (x: number) => string" + │ ││ │ ╰─ attrValue "=fn as (x: number) => string" + │ ││ ╰─ attrName "value" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart \ No newline at end of file diff --git a/src/__tests__/fixtures/ts-function-type/input.marko b/src/__tests__/fixtures/ts-function-type/input.marko new file mode 100644 index 0000000..1b79059 --- /dev/null +++ b/src/__tests__/fixtures/ts-function-type/input.marko @@ -0,0 +1 @@ +
string /> \ No newline at end of file diff --git a/src/__tests__/fixtures/ts-generic-complex/__snapshots__/ts-generic-complex.expected.txt b/src/__tests__/fixtures/ts-generic-complex/__snapshots__/ts-generic-complex.expected.txt new file mode 100644 index 0000000..f2eb62c --- /dev/null +++ b/src/__tests__/fixtures/ts-generic-complex/__snapshots__/ts-generic-complex.expected.txt @@ -0,0 +1,15 @@ +1╭─
>> /> + │ ││ │ ││ ╰─ openTagEnd:selfClosed "/>" + │ ││ │ │╰─ attrValue.value "x as Map>>" + │ ││ │ ╰─ attrValue "=x as Map>>" + │ ││ ╰─ attrName "value" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +2╭─
>> /> + │ ││ │ ││ ╰─ openTagEnd:selfClosed "/>" + │ ││ │ │╰─ attrValue.value "x satisfies Map>>" + │ ││ │ ╰─ attrValue "=x satisfies Map>>" + │ ││ ╰─ attrName "value" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +3╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/ts-generic-complex/input.marko b/src/__tests__/fixtures/ts-generic-complex/input.marko new file mode 100644 index 0000000..547c4f8 --- /dev/null +++ b/src/__tests__/fixtures/ts-generic-complex/input.marko @@ -0,0 +1,2 @@ +
>> /> +
>> /> diff --git a/src/__tests__/fixtures/ts-generic-function-type/__snapshots__/ts-generic-function-type.expected.txt b/src/__tests__/fixtures/ts-generic-function-type/__snapshots__/ts-generic-function-type.expected.txt new file mode 100644 index 0000000..0605ed6 --- /dev/null +++ b/src/__tests__/fixtures/ts-generic-function-type/__snapshots__/ts-generic-function-type.expected.txt @@ -0,0 +1,15 @@ +1╭─
(x: T) => T /> + │ ││ │ ││ ╰─ openTagEnd:selfClosed "/>" + │ ││ │ │╰─ attrValue.value "fn as (x: T) => T" + │ ││ │ ╰─ attrValue "=fn as (x: T) => T" + │ ││ ╰─ attrName "value" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +2╭─
(x: T) => T /> + │ ││ │ ││ ╰─ openTagEnd:selfClosed "/>" + │ ││ │ │╰─ attrValue.value "fn as < T >(x: T) => T" + │ ││ │ ╰─ attrValue "=fn as < T >(x: T) => T" + │ ││ ╰─ attrName "value" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +3╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/ts-generic-function-type/input.marko b/src/__tests__/fixtures/ts-generic-function-type/input.marko new file mode 100644 index 0000000..b7322e9 --- /dev/null +++ b/src/__tests__/fixtures/ts-generic-function-type/input.marko @@ -0,0 +1,2 @@ +
(x: T) => T /> +
(x: T) => T /> diff --git a/src/__tests__/fixtures/ts-generic-simple/__snapshots__/ts-generic-simple.expected.txt b/src/__tests__/fixtures/ts-generic-simple/__snapshots__/ts-generic-simple.expected.txt new file mode 100644 index 0000000..2e4bb19 --- /dev/null +++ b/src/__tests__/fixtures/ts-generic-simple/__snapshots__/ts-generic-simple.expected.txt @@ -0,0 +1,15 @@ +1╭─
/> + │ ││ │ ││ ╰─ openTagEnd:selfClosed "/>" + │ ││ │ │╰─ attrValue.value "x as Array" + │ ││ │ ╰─ attrValue "=x as Array" + │ ││ ╰─ attrName "value" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +2╭─
/> + │ ││ │ ││ ╰─ openTagEnd:selfClosed "/>" + │ ││ │ │╰─ attrValue.value "x as Array< T >" + │ ││ │ ╰─ attrValue "=x as Array< T >" + │ ││ ╰─ attrName "value" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +3╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/ts-generic-simple/input.marko b/src/__tests__/fixtures/ts-generic-simple/input.marko new file mode 100644 index 0000000..6c7f6e2 --- /dev/null +++ b/src/__tests__/fixtures/ts-generic-simple/input.marko @@ -0,0 +1,2 @@ +
/> +
/> diff --git a/src/__tests__/fixtures/ts-intersection-type/__snapshots__/ts-intersection-type.expected.txt b/src/__tests__/fixtures/ts-intersection-type/__snapshots__/ts-intersection-type.expected.txt new file mode 100644 index 0000000..382d831 --- /dev/null +++ b/src/__tests__/fixtures/ts-intersection-type/__snapshots__/ts-intersection-type.expected.txt @@ -0,0 +1,14 @@ +1╭─
/> + │ ││ │ ││ ╰─ openTagEnd:selfClosed "/>" + │ ││ │ │╰─ attrValue.value "x as A & B" + │ ││ │ ╰─ attrValue "=x as A & B" + │ ││ ╰─ attrName "value" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +2╭─
/> + │ ││ │ ││ ╰─ openTagEnd:selfClosed "/>" + │ ││ │ │╰─ attrValue.value "x as A & B< T >" + │ ││ │ ╰─ attrValue "=x as A & B< T >" + │ ││ ╰─ attrName "value" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart \ No newline at end of file diff --git a/src/__tests__/fixtures/ts-intersection-type/input.marko b/src/__tests__/fixtures/ts-intersection-type/input.marko new file mode 100644 index 0000000..a0883c3 --- /dev/null +++ b/src/__tests__/fixtures/ts-intersection-type/input.marko @@ -0,0 +1,2 @@ +
/> +
/> \ No newline at end of file diff --git a/src/__tests__/fixtures/ts-keyof-typeof/__snapshots__/ts-keyof-typeof.expected.txt b/src/__tests__/fixtures/ts-keyof-typeof/__snapshots__/ts-keyof-typeof.expected.txt new file mode 100644 index 0000000..e3be098 --- /dev/null +++ b/src/__tests__/fixtures/ts-keyof-typeof/__snapshots__/ts-keyof-typeof.expected.txt @@ -0,0 +1,8 @@ +1╭─
+ │ ││ │ ││ ╰─ openTagEnd:selfClosed "/>" + │ ││ │ │╰─ attrValue.value "x as keyof typeof T" + │ ││ │ ╰─ attrValue "=x as keyof typeof T" + │ ││ ╰─ attrName "value" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +2╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/ts-keyof-typeof/input.marko b/src/__tests__/fixtures/ts-keyof-typeof/input.marko new file mode 100644 index 0000000..d27b912 --- /dev/null +++ b/src/__tests__/fixtures/ts-keyof-typeof/input.marko @@ -0,0 +1 @@ +
diff --git a/src/__tests__/fixtures/ts-nested-generics/__snapshots__/ts-nested-generics.expected.txt b/src/__tests__/fixtures/ts-nested-generics/__snapshots__/ts-nested-generics.expected.txt new file mode 100644 index 0000000..e241cf5 --- /dev/null +++ b/src/__tests__/fixtures/ts-nested-generics/__snapshots__/ts-nested-generics.expected.txt @@ -0,0 +1,15 @@ +1╭─
>> /> + │ ││ │ ││ ╰─ openTagEnd:selfClosed "/>" + │ ││ │ │╰─ attrValue.value "x as Promise>>" + │ ││ │ ╰─ attrValue "=x as Promise>>" + │ ││ ╰─ attrName "value" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +2╭─
> > /> + │ ││ │ ││ ╰─ openTagEnd:selfClosed "/>" + │ ││ │ │╰─ attrValue.value "x as Promise< Array< Record< string, number > > >" + │ ││ │ ╰─ attrValue "=x as Promise< Array< Record< string, number > > >" + │ ││ ╰─ attrName "value" + │ │╰─ tagName "div" + ╰─ ╰─ openTagStart +3╰─ \ No newline at end of file diff --git a/src/__tests__/fixtures/ts-nested-generics/input.marko b/src/__tests__/fixtures/ts-nested-generics/input.marko new file mode 100644 index 0000000..0e2e7e5 --- /dev/null +++ b/src/__tests__/fixtures/ts-nested-generics/input.marko @@ -0,0 +1,2 @@ +
>> /> +
> > /> diff --git a/src/__tests__/fixtures/ts-static-type/__snapshots__/ts-static-type.expected.txt b/src/__tests__/fixtures/ts-static-type/__snapshots__/ts-static-type.expected.txt new file mode 100644 index 0000000..d147c8e --- /dev/null +++ b/src/__tests__/fixtures/ts-static-type/__snapshots__/ts-static-type.expected.txt @@ -0,0 +1,11 @@ +1╭─ static type Foo = Array< + ╰─ ╰─ tagName "static" +2├─ Bar +3├─ > +4╭─ + ╰─ ╰─ openTagEnd +5╭─ div + ╰─ ╰─ tagName "div" +6╭─ + │ ├─ openTagEnd + ╰─ ╰─ closeTagEnd(div) \ No newline at end of file diff --git a/src/__tests__/fixtures/ts-static-type/input.marko b/src/__tests__/fixtures/ts-static-type/input.marko new file mode 100644 index 0000000..68c4ecf --- /dev/null +++ b/src/__tests__/fixtures/ts-static-type/input.marko @@ -0,0 +1,5 @@ +static type Foo = Array< + Bar +> + +div diff --git a/src/__tests__/fixtures/ts-tag-var-type-generic/__snapshots__/ts-tag-var-type-generic.expected.txt b/src/__tests__/fixtures/ts-tag-var-type-generic/__snapshots__/ts-tag-var-type-generic.expected.txt new file mode 100644 index 0000000..584ce89 --- /dev/null +++ b/src/__tests__/fixtures/ts-tag-var-type-generic/__snapshots__/ts-tag-var-type-generic.expected.txt @@ -0,0 +1,13 @@ +1╭─ const/foo:Bar< + │ │ │╰─ tagVar.value "foo:Bar<\n A,\n B\n>" + │ │ ╰─ tagVar "/foo:Bar<\n A,\n B\n>" + ╰─ ╰─ tagName "const" +2├─ A, +3├─ B +4╭─ > = baz + │ │ ╰─ attrValue.value "baz" + │ ├─ attrValue "= baz" + ╰─ ╰─ attrName +5╭─ + │ ├─ openTagEnd + ╰─ ╰─ closeTagEnd(const) \ No newline at end of file diff --git a/src/__tests__/fixtures/ts-tag-var-type-generic/input.marko b/src/__tests__/fixtures/ts-tag-var-type-generic/input.marko new file mode 100644 index 0000000..c624d6c --- /dev/null +++ b/src/__tests__/fixtures/ts-tag-var-type-generic/input.marko @@ -0,0 +1,4 @@ +const/foo:Bar< + A, + B +> = baz diff --git a/src/__tests__/fixtures/ts-type-statement/__snapshots__/ts-type-statement.expected.txt b/src/__tests__/fixtures/ts-type-statement/__snapshots__/ts-type-statement.expected.txt new file mode 100644 index 0000000..95dcb1f --- /dev/null +++ b/src/__tests__/fixtures/ts-type-statement/__snapshots__/ts-type-statement.expected.txt @@ -0,0 +1,31 @@ +1╭─ static interface Foo< + ╰─ ╰─ tagName "static" +2├─ A, +3├─ B, +4├─ > { +5├─ x: 1 +6├─ } +7╭─ + ╰─ ╰─ openTagEnd +8╭─ static type Bar< + ╰─ ╰─ tagName "static" +9├─ A, +10├─ B, +11├─ > = A & +12├─ B +13╭─ + ╰─ ╰─ openTagEnd +14╭─ static declare const x: Foo< + ╰─ ╰─ tagName "static" +15├─ 1, +16├─ 2 +17├─ >; +18╭─ + ╰─ ╰─ openTagEnd +19╭─ static const x: Foo< + ╰─ ╰─ tagName "static" +20├─ 1, +21├─ 2 +22├─ > = baz; +23╭─ + ╰─ ╰─ openTagEnd \ No newline at end of file diff --git a/src/__tests__/fixtures/ts-type-statement/input.marko b/src/__tests__/fixtures/ts-type-statement/input.marko new file mode 100644 index 0000000..b5442ea --- /dev/null +++ b/src/__tests__/fixtures/ts-type-statement/input.marko @@ -0,0 +1,22 @@ +static interface Foo< + A, + B, +> { + x: 1 +} + +static type Bar< + A, + B, +> = A & + B + +static declare const x: Foo< + 1, + 2 +>; + +static const x: Foo< + 1, + 2 +> = baz; diff --git a/src/states/EXPRESSION.ts b/src/states/EXPRESSION.ts index 51311cc..fbcdc11 100644 --- a/src/states/EXPRESSION.ts +++ b/src/states/EXPRESSION.ts @@ -13,31 +13,44 @@ export interface ExpressionMeta extends Meta { groupStack: number[]; operators: boolean; wasComment: boolean; + inType: boolean; + forceType: boolean; + ternaryDepth: number; terminatedByEOL: boolean; terminatedByWhitespace: boolean; consumeIndentedContent: boolean; - shouldTerminate(code: number, data: string, pos: number): boolean; + shouldTerminate( + code: number, + data: string, + pos: number, + expression: ExpressionMeta, + ): boolean; } // Never terminate early by default. const shouldTerminate = () => false; const unaryKeywords = [ + "asserts", "async", "await", - "keyof", "class", "function", + "infer", + "is", + "keyof", "new", + "readonly", "typeof", + "unique", "void", ] as const; const binaryKeywords = [ - "instanceof", - "in", "as", "extends", + "instanceof", // Note: instanceof must be checked before `in` + "in", "satisfies", ] as const; @@ -54,6 +67,9 @@ export const EXPRESSION: StateDefinition = { shouldTerminate, operators: false, wasComment: false, + inType: false, + forceType: false, + ternaryDepth: 0, terminatedByEOL: false, terminatedByWhitespace: false, consumeIndentedContent: false, @@ -71,7 +87,7 @@ export const EXPRESSION: StateDefinition = { return; } - if (expression.shouldTerminate(code, this.data, this.pos)) { + if (expression.shouldTerminate(code, this.data, this.pos, expression)) { let wasExpression = false; if (expression.operators) { const prevNonWhitespacePos = lookBehindWhile( @@ -81,7 +97,11 @@ export const EXPRESSION: StateDefinition = { ); if (prevNonWhitespacePos > expression.start) { wasExpression = - lookBehindForOperator(this.data, prevNonWhitespacePos) !== -1; + lookBehindForOperator( + expression, + this.data, + prevNonWhitespacePos, + ) !== -1; } } @@ -102,6 +122,40 @@ export const EXPRESSION: StateDefinition = { case CODE.BACKTICK: this.enterState(STATE.TEMPLATE_STRING); break; + case CODE.QUESTION: + if (expression.operators && !expression.groupStack.length) { + expression.ternaryDepth++; + this.pos++; + this.forward = 0; + this.consumeWhitespace(); + } + break; + case CODE.COLON: + if (expression.operators && !expression.groupStack.length) { + if (expression.ternaryDepth) { + expression.ternaryDepth--; + } else { + expression.inType = true; + } + + this.pos++; + this.forward = 0; + this.consumeWhitespace(); + } + break; + case CODE.EQUAL: + if (expression.operators && !expression.groupStack.length) { + if (this.lookAtCharCodeAhead(1) === CODE.CLOSE_ANGLE_BRACKET) { + this.pos++; + } else if (!expression.forceType) { + expression.inType = false; + } + + this.pos++; + this.forward = 0; + this.consumeWhitespace(); + } + break; case CODE.FORWARD_SLASH: // Check next character to see if we are in a comment or regexp switch (this.lookAtCharCodeAhead(1)) { @@ -134,10 +188,20 @@ export const EXPRESSION: StateDefinition = { case CODE.OPEN_CURLY_BRACE: expression.groupStack.push(CODE.CLOSE_CURLY_BRACE); break; + case CODE.OPEN_ANGLE_BRACKET: + if (expression.inType) { + expression.groupStack.push(CODE.CLOSE_ANGLE_BRACKET); + } else if (expression.operators && !expression.groupStack.length) { + this.pos++; + this.forward = 0; + this.consumeWhitespace(); + } + break; case CODE.CLOSE_PAREN: case CODE.CLOSE_SQUARE_BRACKET: - case CODE.CLOSE_CURLY_BRACE: { + case CODE.CLOSE_CURLY_BRACE: + case expression.inType && CODE.CLOSE_ANGLE_BRACKET: { if (!expression.groupStack.length) { return this.emitError( expression, @@ -254,7 +318,7 @@ function checkForOperators( if (!expression.operators) return false; const { pos, data } = parser; - if (lookBehindForOperator(data, pos) !== -1) { + if (lookBehindForOperator(expression, data, pos) !== -1) { parser.consumeWhitespace(); parser.forward = 0; return true; @@ -273,9 +337,10 @@ function checkForOperators( data.charCodeAt(nextNonSpace), data, nextNonSpace, + expression, ) ) { - const lookAheadPos = lookAheadForOperator(data, nextNonSpace); + const lookAheadPos = lookAheadForOperator(expression, data, nextNonSpace); if (lookAheadPos !== -1) { parser.pos = lookAheadPos; parser.forward = 0; @@ -287,7 +352,11 @@ function checkForOperators( return false; } -function lookBehindForOperator(data: string, pos: number): number { +function lookBehindForOperator( + expression: ExpressionMeta, + data: string, + pos: number, +): number { const curPos = pos - 1; const code = data.charCodeAt(curPos); @@ -299,13 +368,19 @@ function lookBehindForOperator(data: string, pos: number): number { case CODE.EQUAL: case CODE.EXCLAMATION: case CODE.OPEN_ANGLE_BRACKET: - case CODE.CLOSE_ANGLE_BRACKET: case CODE.PERCENT: case CODE.PIPE: case CODE.QUESTION: case CODE.TILDE: return curPos; + case CODE.CLOSE_ANGLE_BRACKET: + return data.charCodeAt(curPos - 1) === CODE.EQUAL + ? curPos - 1 + : expression.inType + ? -1 + : curPos; + case CODE.PERIOD: { // Only matches `.` followed by something that could be an identifier. const nextPos = lookAheadWhile(isWhitespaceCode, data, pos); @@ -319,6 +394,7 @@ function lookBehindForOperator(data: string, pos: number): number { // Check if we should continue for another reason. // eg "typeof++ x" return lookBehindForOperator( + expression, data, lookBehindWhile(isWhitespaceCode, data, curPos - 2), ); @@ -331,8 +407,7 @@ function lookBehindForOperator(data: string, pos: number): number { for (const keyword of unaryKeywords) { const keywordPos = lookBehindFor(data, curPos, keyword); if (keywordPos !== -1) { - const prevCode = data.charCodeAt(keywordPos - 1); - return prevCode === CODE.PERIOD || isWordCode(prevCode) + return isWordOrPeriodCode(data.charCodeAt(keywordPos - 1)) ? -1 : keywordPos; } @@ -342,7 +417,11 @@ function lookBehindForOperator(data: string, pos: number): number { } } -function lookAheadForOperator(data: string, pos: number): number { +function lookAheadForOperator( + expression: ExpressionMeta, + data: string, + pos: number, +): number { switch (data.charCodeAt(pos)) { case CODE.AMPERSAND: case CODE.ASTERISK: @@ -351,18 +430,18 @@ function lookAheadForOperator(data: string, pos: number): number { case CODE.OPEN_ANGLE_BRACKET: case CODE.PERCENT: case CODE.PIPE: - case CODE.QUESTION: case CODE.TILDE: case CODE.PLUS: case CODE.HYPHEN: - case CODE.COLON: - case CODE.CLOSE_ANGLE_BRACKET: - case CODE.EQUAL: return pos + 1; case CODE.FORWARD_SLASH: case CODE.OPEN_CURLY_BRACE: case CODE.OPEN_PAREN: + case CODE.CLOSE_ANGLE_BRACKET: + case CODE.QUESTION: + case CODE.COLON: + case CODE.EQUAL: return pos; // defers to base expression state to track block groups. case CODE.PERIOD: { @@ -373,33 +452,33 @@ function lookAheadForOperator(data: string, pos: number): number { default: { for (const keyword of binaryKeywords) { - let nextPos = lookAheadFor(data, pos, keyword); - if (nextPos === -1) continue; - - const max = data.length - 1; - if (nextPos === max) return -1; - - let nextCode = data.charCodeAt(nextPos + 1); - if (isWhitespaceCode(nextCode)) { - // skip any whitespace after the operator - nextPos = lookAheadWhile(isWhitespaceCode, data, nextPos + 2); - if (nextPos === max) return -1; - nextCode = data.charCodeAt(nextPos); - } else { - // bail if we didn't match a space keyword. - return -1; - } + const keywordPos = lookAheadFor(data, pos, keyword); + if (keywordPos === -1) continue; + if (!isWhitespaceCode(data.charCodeAt(keywordPos + 1))) break; + + // skip any whitespace after the operator + const nextPos = lookAheadWhile(isWhitespaceCode, data, keywordPos + 2); + if (nextPos === data.length - 1) break; // finally check that this is not followed by a terminator. - switch (nextCode) { + switch (data.charCodeAt(nextPos)) { case CODE.COLON: case CODE.COMMA: case CODE.EQUAL: case CODE.FORWARD_SLASH: case CODE.CLOSE_ANGLE_BRACKET: case CODE.SEMICOLON: - return -1; + break; default: + if ( + !expression.inType && + (keyword === "as" || keyword === "satisfies") + ) { + expression.inType = true; + if (!(expression.ternaryDepth || expression.groupStack.length)) { + expression.forceType = true; + } + } return nextPos; } } @@ -427,6 +506,10 @@ function canFollowDivision(code: number) { } } +function isWordOrPeriodCode(code: number) { + return code === CODE.PERIOD || isWordCode(code); +} + function isWordCode(code: number) { return ( (code >= CODE.UPPER_A && code <= CODE.UPPER_Z) || diff --git a/src/states/OPEN_TAG.ts b/src/states/OPEN_TAG.ts index 39d35f6..69093b4 100644 --- a/src/states/OPEN_TAG.ts +++ b/src/states/OPEN_TAG.ts @@ -12,6 +12,7 @@ import { matchesCloseAngleBracket, isIndentCode, } from "../internal"; +import type { ExpressionMeta } from "./EXPRESSION"; export enum TAG_STAGE { UNKNOWN, @@ -476,15 +477,21 @@ export const OPEN_TAG: StateDefinition = { }, }; -function shouldTerminateConciseTagVar(code: number, data: string, pos: number) { +function shouldTerminateConciseTagVar( + code: number, + data: string, + pos: number, + expression: ExpressionMeta, +) { switch (code) { case CODE.COMMA: case CODE.EQUAL: case CODE.PIPE: case CODE.OPEN_PAREN: case CODE.SEMICOLON: - case CODE.OPEN_ANGLE_BRACKET: return true; + case CODE.OPEN_ANGLE_BRACKET: + return !expression.inType; case CODE.HYPHEN: return data.charCodeAt(pos + 1) === CODE.HYPHEN; case CODE.COLON: @@ -494,15 +501,21 @@ function shouldTerminateConciseTagVar(code: number, data: string, pos: number) { } } -function shouldTerminateHtmlTagVar(code: number, data: string, pos: number) { +function shouldTerminateHtmlTagVar( + code: number, + data: string, + pos: number, + expression: ExpressionMeta, +) { switch (code) { case CODE.PIPE: case CODE.COMMA: case CODE.EQUAL: case CODE.OPEN_PAREN: case CODE.CLOSE_ANGLE_BRACKET: - case CODE.OPEN_ANGLE_BRACKET: return true; + case CODE.OPEN_ANGLE_BRACKET: + return !expression.inType; case CODE.COLON: return data.charCodeAt(pos + 1) === CODE.EQUAL; case CODE.FORWARD_SLASH: diff --git a/src/states/TAG_NAME.ts b/src/states/TAG_NAME.ts index b33b0e1..68d091b 100644 --- a/src/states/TAG_NAME.ts +++ b/src/states/TAG_NAME.ts @@ -100,6 +100,18 @@ export const TAG_NAME: StateDefinition = { 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.forward = 0; + this.consumeWhitespace(); + } } }