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/scriptlet-and-spaced-type-statements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"htmljs-parser": minor
---

Read a `type`, `interface` or `declare` scriptlet as a type, as statements already are, so `$ type H = () => void` or `$ type A = B<C>` no longer swallows the next line; `$ type = x` stays JavaScript. A statement also finds its type keyword after extra whitespace (`static type F = () => void`), and `isValidScriptlet` matches the parser.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
1╭─ $ type = a < b
│ │ ╰─ scriptlet.value "type = a < b"
╰─ ╰─ scriptlet "$ type = a < b"
2╭─ $ type in obj && go()
│ │ ╰─ scriptlet.value "type in obj && go()"
╰─ ╰─ scriptlet "$ type in obj && go()"
3╭─ $ type (x)
│ │ ╰─ scriptlet.value "type (x)"
╰─ ╰─ scriptlet "$ type (x)"
4╭─ <div/>
│ ││ ╰─ openTagEnd:selfClosed "/>"
│ │╰─ tagName "div"
╰─ ╰─ openTagStart
5╰─
4 changes: 4 additions & 0 deletions src/__tests__/fixtures/scriptlet-type-name-guard/input.marko
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
$ type = a < b
$ type in obj && go()
$ type (x)
<div/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
1╭─ $ type H = () => void
│ │ ╰─ scriptlet.value "type H = () => void"
╰─ ╰─ scriptlet "$ type H = () => void"
2╭─ <div>
│ ││ ╰─ openTagEnd
│ │╰─ tagName "div"
╰─ ╰─ openTagStart
3╭─ $ type A = B<C>
│ │ │ ╰─ scriptlet.value "type A = B<C>"
│ │ ╰─ scriptlet "$ type A = B<C>"
╰─ ╰─ text "\n "
4╭─ </div>
│ │ │ ╰─ closeTagEnd(div)
│ │ ╰─ closeTagName "div"
│ ├─ text "\n"
╰─ ╰─ closeTagStart "</"
5╭─ $ interface I { a(): void }
│ │ ╰─ scriptlet.value "interface I { a(): void }"
╰─ ╰─ scriptlet "$ interface I { a(): void }"
6╭─ $ declare const d: void
│ │ ╰─ scriptlet.value "declare const d: void"
╰─ ╰─ scriptlet "$ declare const d: void"
7╭─ <span/>
│ ││ ╰─ openTagEnd:selfClosed "/>"
│ │╰─ tagName "span"
╰─ ╰─ openTagStart
8╰─
7 changes: 7 additions & 0 deletions src/__tests__/fixtures/scriptlet-type-void/input.marko
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
$ type H = () => void
<div>
$ type A = B<C>
</div>
$ interface I { a(): void }
$ declare const d: void
<span/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
1╭─ static type F = () => void
╰─ ╰─ tagName "static"
2╭─ static const x = 1
│ ├─ openTagEnd
╰─ ╰─ tagName "static"
3╭─
╰─ ╰─ openTagEnd
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
static type F = () => void
static const x = 1
12 changes: 12 additions & 0 deletions src/__tests__/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,25 @@ describe("validation helpers", () => {
it("ends a type statement at an unindented newline", () => {
assert.equal(isValidStatement("type A = B<C>\nfoo"), 0);
});

it("reads a type statement after extra whitespace", () => {
assert.equal(isValidStatement(" type A = B<C>\nfoo"), 0);
});
});

describe("isValidScriptlet", () => {
it("accepts single-line expressions", () => {
assert.equal(isValidScriptlet("foo + bar"), 2);
});

it("ends a type scriptlet at a line-final void", () => {
assert.equal(isValidScriptlet("type H = () => void\nfoo"), 0);
});

it("reads an assignment to a variable named type as JavaScript", () => {
assert.equal(isValidScriptlet("type = a < b"), 2);
});

it("rejects indented continuation lines", () => {
assert.equal(isValidScriptlet("foo\n + bar"), 0);
});
Expand Down
2 changes: 1 addition & 1 deletion src/states/EXPRESSION.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const tsUnaryKeywords = [
"unique",
] as const;

const binaryKeywords = [
export const binaryKeywords = [
"as",
"extends",
"instanceof", // Note: instanceof must be checked before `in`
Expand Down
64 changes: 61 additions & 3 deletions src/states/INLINE_SCRIPT.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import {
isIndentCode,
isWordCode,
matchesCloseCurlyBrace,
type Meta,
Parser,
type Range,
STATE,
type StateDefinition,
} from "../internal.ts";
import * as CODE from "../util/codes.ts";
import { binaryKeywords } from "./EXPRESSION.ts";

interface ScriptletMeta extends Meta {
block: boolean;
Expand Down Expand Up @@ -49,9 +53,7 @@ export const INLINE_SCRIPT: StateDefinition<ScriptletMeta> = {
this.enterState(STATE.EXPRESSION).shouldTerminate =
matchesCloseCurlyBrace;
} else {
const expr = this.enterState(STATE.EXPRESSION);
expr.operators = true;
expr.terminatedByEOL = true;
prepareScriptlet(this.enterState(STATE.EXPRESSION), this);
}
},

Expand All @@ -70,3 +72,59 @@ export const INLINE_SCRIPT: StateDefinition<ScriptletMeta> = {
this.exitState();
},
};

const typeKeywords = ["declare", "interface", "type"] as const;

// Sets up an unenclosed scriptlet's expression, whose code starts at `pos`.
// `declare`, `interface` or `type` before a name (or `type` before `{`/`*`,
// as in `import type { A }`) starts a type, which ends differently than
// JavaScript: a trailing `void` or `>` does not continue it. `type = 1` and
// `type in x` stay JavaScript.
export function prepareScriptlet(
expr: STATE.ExpressionMeta,
parser: Parser,
pos = parser.pos,
) {
expr.operators = true;
expr.terminatedByEOL = true;

const { data } = parser;
while (isIndentCode(data.charCodeAt(pos))) pos++;

for (const keyword of typeKeywords) {
if (!parser.lookAheadFor(keyword, pos)) continue;

let namePos = pos + keyword.length;
if (!isIndentCode(data.charCodeAt(namePos))) return;
while (isIndentCode(data.charCodeAt(namePos))) namePos++;

if (startsTypeName(parser, namePos, keyword === "type")) {
expr.inType = true;
expr.forceType = true;
parser.pos = namePos;
}
return;
}
}

function startsTypeName(parser: Parser, pos: number, allowGroup: boolean) {
const code = parser.data.charCodeAt(pos);
if (code === CODE.OPEN_CURLY_BRACE || code === CODE.ASTERISK) {
return allowGroup;
}

if (!isWordCode(code) || (code >= CODE.NUMBER_0 && code <= CODE.NUMBER_9)) {
return false;
}

for (const keyword of binaryKeywords) {
if (
parser.lookAheadFor(keyword, pos) &&
!isWordCode(parser.data.charCodeAt(pos + keyword.length))
) {
return false;
}
}

return true;
}
15 changes: 2 additions & 13 deletions src/states/TAG_NAME.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import * as CODE from "../util/codes.ts";
import * as ErrorCode from "../util/error-code.ts";
import * as TagType from "../util/tag-type.ts";
import { prepareScriptlet } from "./INLINE_SCRIPT.ts";

export interface TagNameMeta extends Meta, Ranges.Template {
shorthandCode: -1 | typeof CODE.NUMBER_SIGN | typeof CODE.PERIOD;
Expand Down Expand Up @@ -195,18 +196,6 @@ export function prepareStatement(
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();
}
prepareScriptlet(expr, parser, pos);
}
6 changes: 1 addition & 5 deletions src/util/validators.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type Meta, Parser, STATE, type StateDefinition } from "../internal.ts";
import {
prepareScriptlet,
prepareStatement,
shouldTerminateConciseAttrValue,
shouldTerminateHtmlAttrValue,
Expand Down Expand Up @@ -38,11 +39,6 @@ export function isValidScriptlet(code: string): Validity {
return isValid(code, true, prepareScriptlet);
}

function prepareScriptlet(expr: STATE.ExpressionMeta) {
expr.operators = true;
expr.terminatedByEOL = true;
}

export function isValidAttrValue(code: string, concise: boolean): Validity {
return isValid(code, concise, prepareAttrValue);
}
Expand Down
Loading