> > />
+ │ ││ │ ││ ╰─ 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();
+ }
}
}