mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-08 17:34:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
586b605429 | ||
|
|
b03695d787 |
@@ -46,6 +46,7 @@ const LANGUAGE_ALIASES: Record<string, string> = {
|
||||
"objective-c": "m",
|
||||
markdown: "md",
|
||||
elixir: "ex",
|
||||
terraform: "tf",
|
||||
};
|
||||
|
||||
function fenceLanguageToExtension(info: string | null | undefined): string | null {
|
||||
|
||||
@@ -430,20 +430,6 @@ function agentPermission(id: string): AgentPermissionRequest {
|
||||
return { id, provider: "codex", name: id, kind: "tool", title: id };
|
||||
}
|
||||
|
||||
const relayPrimary: HostConnection = {
|
||||
id: "relay:relay.paseo.sh:443",
|
||||
type: "relay",
|
||||
relayEndpoint: "relay.paseo.sh:443",
|
||||
daemonPublicKeyB64: "pk_test",
|
||||
};
|
||||
|
||||
const relayAlternate: HostConnection = {
|
||||
id: "relay:relay-next.paseo.sh:443",
|
||||
type: "relay",
|
||||
relayEndpoint: "relay-next.paseo.sh:443",
|
||||
daemonPublicKeyB64: "pk_test",
|
||||
};
|
||||
|
||||
function makeHost(input?: Partial<HostProfile>): HostProfile {
|
||||
const direct: HostConnection = {
|
||||
id: "direct:lan:6767",
|
||||
@@ -1580,233 +1566,6 @@ describe("HostRuntimeController", () => {
|
||||
expect(switched).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the live connection while one alternate sample is re-read at active ticks", async () => {
|
||||
// Replays the recorded Android sequence: both relays probed at startup, the alternate
|
||||
// measured once, then only the active connection is re-read every ten seconds.
|
||||
useHostRuntimeClock();
|
||||
const host = makeHost({
|
||||
connections: [relayPrimary, relayAlternate],
|
||||
preferredConnectionId: relayPrimary.id,
|
||||
});
|
||||
const clients: FakeDaemonClient[] = [];
|
||||
const latencies: Record<string, number | Error> = {
|
||||
[relayPrimary.id]: 1053,
|
||||
[relayAlternate.id]: 251,
|
||||
};
|
||||
const controller = new HostRuntimeController({
|
||||
host,
|
||||
deps: makeDeps(latencies, clients),
|
||||
});
|
||||
|
||||
await controller.start();
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe(relayPrimary.id);
|
||||
const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient;
|
||||
const clientsAfterStart = clients.length;
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10_500);
|
||||
activeClient.heartbeatReportsRtt(376);
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
|
||||
expect(clients.length).toBe(clientsAfterStart);
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe(relayPrimary.id);
|
||||
expect(controller.getSnapshot().client).toBe(activeClient);
|
||||
expect(activeClient.isDisposed()).toBe(false);
|
||||
await controller.stop();
|
||||
});
|
||||
|
||||
it("does not hold the previous socket's RTT against a reconnected active connection", async () => {
|
||||
// Replays the recorded foreground recovery: the active client reconnects on its own while an
|
||||
// alternate probe is in flight. The recovered socket has no heartbeat RTT yet.
|
||||
useHostRuntimeClock();
|
||||
const host = makeHost({
|
||||
connections: [relayPrimary, relayAlternate],
|
||||
preferredConnectionId: relayPrimary.id,
|
||||
});
|
||||
const clients: FakeDaemonClient[] = [];
|
||||
const latencies: Record<string, number | Error> = {
|
||||
[relayPrimary.id]: 300,
|
||||
[relayAlternate.id]: 392,
|
||||
};
|
||||
let connectGate: Promise<void> | null = null;
|
||||
const baseDeps = makeDeps(latencies, clients);
|
||||
const controller = new HostRuntimeController({
|
||||
host,
|
||||
deps: {
|
||||
...baseDeps,
|
||||
connectToDaemon: async (input) => {
|
||||
if (connectGate) await connectGate;
|
||||
return baseDeps.connectToDaemon(input);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await controller.start();
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe(relayPrimary.id);
|
||||
const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient;
|
||||
activeClient.heartbeatReportsRtt(403);
|
||||
await vi.advanceTimersByTimeAsync(150_500);
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe(relayPrimary.id);
|
||||
|
||||
activeClient.setConnectionState({ status: "disconnected", reason: "liveness_timeout" });
|
||||
activeClient.setConnectionState({ status: "connecting", attempt: 2 });
|
||||
activeClient.heartbeatReportsRtt(null);
|
||||
latencies[relayAlternate.id] = 280;
|
||||
const alternateConnect = createDeferred<void>();
|
||||
connectGate = alternateConnect.promise;
|
||||
await vi.advanceTimersByTimeAsync(2_800);
|
||||
activeClient.setConnectionState({ status: "connected" });
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
alternateConnect.resolve();
|
||||
connectGate = null;
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
expect(controller.getSnapshot().probeByConnectionId.get(relayAlternate.id)).toEqual({
|
||||
status: "available",
|
||||
latencyMs: 280,
|
||||
});
|
||||
const clientsAfterProbe = clients.length;
|
||||
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
|
||||
expect(clients.length).toBe(clientsAfterProbe);
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe(relayPrimary.id);
|
||||
expect(controller.getSnapshot().client).toBe(activeClient);
|
||||
expect(activeClient.isDisposed()).toBe(false);
|
||||
await controller.stop();
|
||||
});
|
||||
|
||||
it("starts the switch streak over when the active socket reconnects", async () => {
|
||||
useHostRuntimeClock();
|
||||
const host = makeHost({ preferredConnectionId: "direct:lan:6767" });
|
||||
const clients: FakeDaemonClient[] = [];
|
||||
const latencies: Record<string, number | Error> = {
|
||||
"direct:lan:6767": 15,
|
||||
"relay:relay.paseo.sh:443": 60,
|
||||
};
|
||||
const controller = new HostRuntimeController({
|
||||
host,
|
||||
deps: makeDeps(latencies, clients),
|
||||
});
|
||||
|
||||
await controller.start({ autoProbe: false });
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
|
||||
const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient;
|
||||
|
||||
latencies["relay:relay.paseo.sh:443"] = 30;
|
||||
activeClient.heartbeatReportsRtt(95);
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
await controller.runProbeCycleNow();
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
|
||||
}
|
||||
|
||||
activeClient.setConnectionState({ status: "disconnected", reason: "network changed" });
|
||||
activeClient.setConnectionState({ status: "connected" });
|
||||
activeClient.heartbeatReportsRtt(95);
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
await controller.runProbeCycleNow();
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
|
||||
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
await controller.runProbeCycleNow();
|
||||
}
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe("relay:relay.paseo.sh:443");
|
||||
await controller.stop();
|
||||
});
|
||||
|
||||
it("ends the switch streak when a fresh probe of the alternate fails", async () => {
|
||||
useHostRuntimeClock();
|
||||
const host = makeHost({ preferredConnectionId: "direct:lan:6767" });
|
||||
const clients: FakeDaemonClient[] = [];
|
||||
const latencies: Record<string, number | Error> = {
|
||||
"direct:lan:6767": 15,
|
||||
"relay:relay.paseo.sh:443": 60,
|
||||
};
|
||||
const controller = new HostRuntimeController({
|
||||
host,
|
||||
deps: makeDeps(latencies, clients),
|
||||
});
|
||||
|
||||
await controller.start({ autoProbe: false });
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
|
||||
const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient;
|
||||
activeClient.heartbeatReportsRtt(95);
|
||||
|
||||
latencies["relay:relay.paseo.sh:443"] = 30;
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
await controller.runProbeCycleNow();
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
|
||||
}
|
||||
|
||||
latencies["relay:relay.paseo.sh:443"] = new Error("relay unreachable");
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
await controller.runProbeCycleNow();
|
||||
expect(controller.getSnapshot().probeByConnectionId.get("relay:relay.paseo.sh:443")).toEqual({
|
||||
status: "unavailable",
|
||||
latencyMs: null,
|
||||
});
|
||||
|
||||
latencies["relay:relay.paseo.sh:443"] = 30;
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
await controller.runProbeCycleNow();
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
|
||||
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
await controller.runProbeCycleNow();
|
||||
}
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe("relay:relay.paseo.sh:443");
|
||||
await controller.stop();
|
||||
});
|
||||
|
||||
it("restarts the streak when a different candidate becomes the fastest fresh sample", async () => {
|
||||
useHostRuntimeClock();
|
||||
const direct: HostConnection = {
|
||||
id: "direct:lan:6767",
|
||||
type: "directTcp",
|
||||
endpoint: "lan:6767",
|
||||
};
|
||||
const host = makeHost({
|
||||
connections: [direct, relayPrimary, relayAlternate],
|
||||
preferredConnectionId: direct.id,
|
||||
});
|
||||
const clients: FakeDaemonClient[] = [];
|
||||
const latencies: Record<string, number | Error> = {
|
||||
[direct.id]: 15,
|
||||
[relayPrimary.id]: 60,
|
||||
[relayAlternate.id]: 70,
|
||||
};
|
||||
const controller = new HostRuntimeController({
|
||||
host,
|
||||
deps: makeDeps(latencies, clients),
|
||||
});
|
||||
|
||||
await controller.start({ autoProbe: false });
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe(direct.id);
|
||||
const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient;
|
||||
activeClient.heartbeatReportsRtt(95);
|
||||
|
||||
latencies[relayPrimary.id] = 30;
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
await controller.runProbeCycleNow();
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe(direct.id);
|
||||
}
|
||||
|
||||
latencies[relayAlternate.id] = 20;
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
await controller.runProbeCycleNow();
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe(direct.id);
|
||||
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
await controller.runProbeCycleNow();
|
||||
}
|
||||
expect(controller.getSnapshot().activeConnectionId).toBe(relayAlternate.id);
|
||||
await controller.stop();
|
||||
});
|
||||
|
||||
it("exposes one snapshot with active connection and status from same source", async () => {
|
||||
const host = makeHost();
|
||||
const clients: FakeDaemonClient[] = [];
|
||||
|
||||
@@ -1063,46 +1063,37 @@ export class HostRuntimeController {
|
||||
expectedProbeVersion: requestVersion,
|
||||
});
|
||||
}
|
||||
this.resetSwitchCandidate();
|
||||
this.switchCandidateConnectionId = null;
|
||||
this.switchCandidateHitCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Adaptive switching counts fresh comparisons only. The probe map keeps the last known
|
||||
// sample per connection for display and failover; a streak hit needs a candidate measured
|
||||
// in this cycle and a heartbeat RTT from the socket that is active right now. Re-reading
|
||||
// the cached map at every ten-second active tick turned one idle-probe sample into three
|
||||
// "consecutive" wins, and a reconnected socket inherited its predecessor's RTT, so a
|
||||
// working relay connection was replaced within seconds of coming online.
|
||||
const sampledCandidates = connectionsToProbe.filter(
|
||||
(connection) => connection.id !== currentActiveConnectionId,
|
||||
);
|
||||
if (sampledCandidates.length === 0) {
|
||||
// An active-only tick carries no evidence about any candidate.
|
||||
return;
|
||||
}
|
||||
const fastest =
|
||||
sampledCandidates
|
||||
.flatMap((connection) => {
|
||||
const probe = probeByConnectionId.get(connection.id);
|
||||
return probe?.status === "available"
|
||||
? [{ connectionId: connection.id, latencyMs: probe.latencyMs }]
|
||||
: [];
|
||||
})
|
||||
.sort((left, right) => left.latencyMs - right.latencyMs)[0] ?? null;
|
||||
if (!fastest) {
|
||||
// Every candidate sampled this cycle failed: that breaks the streak.
|
||||
this.resetSwitchCandidate();
|
||||
if (!activeProbe || activeProbe.status !== "available") {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeRttMs = this.currentActiveSocketRttMs();
|
||||
if (activeRttMs === null) {
|
||||
const available = Array.from(probeByConnectionId.entries())
|
||||
.filter(
|
||||
(entry): entry is [string, Extract<ConnectionProbeState, { status: "available" }>] =>
|
||||
entry[1].status === "available",
|
||||
)
|
||||
.map(([connectionId, probe]) => ({
|
||||
connectionId,
|
||||
latencyMs: probe.latencyMs,
|
||||
}))
|
||||
.sort((left, right) => left.latencyMs - right.latencyMs);
|
||||
|
||||
const fastest = available[0] ?? null;
|
||||
if (!fastest || fastest.connectionId === currentActiveConnectionId) {
|
||||
this.switchCandidateConnectionId = null;
|
||||
this.switchCandidateHitCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const improvement = activeRttMs - fastest.latencyMs;
|
||||
const improvement = activeProbe.latencyMs - fastest.latencyMs;
|
||||
if (improvement < ADAPTIVE_SWITCH_THRESHOLD_MS) {
|
||||
this.resetSwitchCandidate();
|
||||
this.switchCandidateConnectionId = null;
|
||||
this.switchCandidateHitCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1114,7 +1105,8 @@ export class HostRuntimeController {
|
||||
}
|
||||
|
||||
if (this.switchCandidateHitCount >= ADAPTIVE_SWITCH_CONSECUTIVE_PROBES) {
|
||||
this.resetSwitchCandidate();
|
||||
this.switchCandidateConnectionId = null;
|
||||
this.switchCandidateHitCount = 0;
|
||||
await this.switchToConnection({
|
||||
connectionId: fastest.connectionId,
|
||||
expectedProbeVersion: requestVersion,
|
||||
@@ -1225,19 +1217,6 @@ export class HostRuntimeController {
|
||||
});
|
||||
}
|
||||
|
||||
// The client clears its heartbeat RTT when a socket connects, so a connected client's RTT
|
||||
// describes the socket in use right now; a reconnecting client still carries the old one.
|
||||
private currentActiveSocketRttMs(): number | null {
|
||||
const client = this.snapshot.client;
|
||||
if (!client || client.getConnectionState().status !== "connected") return null;
|
||||
return client.getLastLivenessRttMs();
|
||||
}
|
||||
|
||||
private resetSwitchCandidate(): void {
|
||||
this.switchCandidateConnectionId = null;
|
||||
this.switchCandidateHitCount = 0;
|
||||
}
|
||||
|
||||
// The active connection is probed through its own client, which may still be
|
||||
// reconnecting when the probe starts. A verdict taken then is stale if the client
|
||||
// has since connected, so the decision uses the client's current state.
|
||||
@@ -1527,8 +1506,6 @@ export class HostRuntimeController {
|
||||
return;
|
||||
}
|
||||
if (state.status === "connected") this.learnRelaySecurity(client, connection.id);
|
||||
// Switch evidence belongs to the socket it was measured against.
|
||||
if (state.status !== "connected") this.resetSwitchCandidate();
|
||||
this.applyConnectionEvent({ type: "client_state", state, lastError: client.lastError });
|
||||
this.updateSnapshot({
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch),
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map",
|
||||
"src/astro/LICENSE"
|
||||
"src/astro/LICENSE",
|
||||
"src/hcl/LICENSE"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { setImmediate } from "node:timers/promises";
|
||||
import { highlightCodeWithBudget, HIGHLIGHT_MAX_SOURCE_BYTES } from "../budgeted-highlighter.js";
|
||||
import type { HighlightToken } from "../types.js";
|
||||
import { highlightCode, highlightCodeInSteps, highlightLine } from "../highlighter.js";
|
||||
import { getParserForFile } from "../parsers.js";
|
||||
|
||||
describe("highlightCode", () => {
|
||||
it("highlights JavaScript code with correct token styles", () => {
|
||||
@@ -344,6 +345,137 @@ const title = "Hello";
|
||||
});
|
||||
});
|
||||
|
||||
describe("Terraform", () => {
|
||||
function styleOf(line: HighlightToken[], text: string) {
|
||||
return line.find((token) => token.text === text)?.style;
|
||||
}
|
||||
|
||||
it("highlights blocks, labels, attributes, and values", () => {
|
||||
const code = [
|
||||
"# The web server",
|
||||
'resource "aws_instance" "web" {',
|
||||
" count = 2",
|
||||
" enabled = true",
|
||||
' tags = merge(var.labels, { Name = "web" })',
|
||||
"}",
|
||||
].join("\n");
|
||||
const result = highlightCode(code, "main.tf");
|
||||
|
||||
expect(result[0][0]).toEqual({ text: "# The web server", style: "comment" });
|
||||
expect(styleOf(result[1], "resource")).toBe("type");
|
||||
expect(styleOf(result[1], '"aws_instance"')).toBe("string");
|
||||
expect(styleOf(result[1], '"web"')).toBe("string");
|
||||
expect(styleOf(result[2], "count")).toBe("definition");
|
||||
expect(styleOf(result[2], "2")).toBe("number");
|
||||
expect(styleOf(result[3], "true")).toBe("literal");
|
||||
expect(styleOf(result[4], "merge")).toBe("function");
|
||||
expect(styleOf(result[4], "var")).toBe("variable");
|
||||
expect(styleOf(result[4], "labels")).toBe("property");
|
||||
});
|
||||
|
||||
it("highlights expressions interpolated inside strings", () => {
|
||||
const result = highlightCode('name = "app-${var.env}-web"', "main.tf");
|
||||
|
||||
expect(result[0].map((token) => [token.text, token.style])).toEqual([
|
||||
["name", "definition"],
|
||||
[" ", null],
|
||||
["=", "operator"],
|
||||
[" ", null],
|
||||
['"app-', "string"],
|
||||
["${", "punctuation"],
|
||||
["var", "variable"],
|
||||
[".", "operator"],
|
||||
["env", "property"],
|
||||
["}", "punctuation"],
|
||||
['-web"', "string"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("highlights heredoc bodies as strings and resumes after the terminator", () => {
|
||||
const code = ["policy = <<-EOT", ' { "Version": "2012" }', " EOT", "retries = 3"].join("\n");
|
||||
const result = highlightCode(code, "main.tf");
|
||||
|
||||
expect(styleOf(result[0], "EOT")).toBe("keyword");
|
||||
expect(result[1].map((token) => token.style)).toEqual(["string"]);
|
||||
expect(styleOf(result[3], "retries")).toBe("definition");
|
||||
expect(styleOf(result[3], "3")).toBe("number");
|
||||
});
|
||||
|
||||
it("ends a heredoc with a Unicode delimiter", () => {
|
||||
const result = highlightCode("x = <<ÉOF\nfoo\nÉOF\ny = 1", "main.tf");
|
||||
|
||||
expect(styleOf(result[3], "y")).toBe("definition");
|
||||
expect(styleOf(result[3], "1")).toBe("number");
|
||||
});
|
||||
|
||||
it.each([
|
||||
['x = "$$${var.name}"', "$"],
|
||||
['x = "%%%{ if true }"', "%"],
|
||||
])("keeps escaped template text in %s as a string", (code) => {
|
||||
const line = highlightCode(code, "main.tf")[0];
|
||||
|
||||
expect(line[line.length - 1]).toEqual({ text: code.slice(4), style: "string" });
|
||||
});
|
||||
|
||||
it("parses a real configuration without error nodes", () => {
|
||||
const code = [
|
||||
"terraform {",
|
||||
' required_version = ">= 1.5"',
|
||||
' backend "s3" {}',
|
||||
"}",
|
||||
"",
|
||||
'variable "subnets" {',
|
||||
" type = map(object({ cidr = string }))",
|
||||
" default = {}",
|
||||
"}",
|
||||
"",
|
||||
"locals {",
|
||||
" names = [for key, subnet in var.subnets : upper(key) if subnet.cidr != null]",
|
||||
' first = length(local.names) > 0 ? local.names[0] : "none"',
|
||||
" arns = aws_instance.web[*].arn",
|
||||
" /* block comment */",
|
||||
" path = provider::time::rfc3339_parse(timestamp()) // trailing",
|
||||
"}",
|
||||
"",
|
||||
'data "aws_iam_policy_document" "assume" {',
|
||||
' dynamic "statement" {',
|
||||
" for_each = var.subnets",
|
||||
" content {",
|
||||
' sid = "${statement.key}-%{ if statement.value.cidr != "" }set%{ else }unset%{ endif }"',
|
||||
" }",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
'output "script" {',
|
||||
" value = <<EOT",
|
||||
"#!/bin/sh",
|
||||
"echo ${local.first}",
|
||||
"EOT",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
const parser = getParserForFile("main.tf");
|
||||
if (!parser) throw new Error("Expected a parser for .tf files");
|
||||
const errors: number[] = [];
|
||||
parser.parse(code).iterate({
|
||||
enter(node) {
|
||||
if (node.type.isError) errors.push(node.from);
|
||||
},
|
||||
});
|
||||
|
||||
expect(errors.map((from) => code.slice(from, from + 30))).toEqual([]);
|
||||
});
|
||||
|
||||
it("uses the same grammar for tfvars and hcl files", () => {
|
||||
expect(highlightCode('region = "eu-west-1"', "prod.tfvars")).toEqual(
|
||||
highlightCode('region = "eu-west-1"', "terragrunt.hcl"),
|
||||
);
|
||||
expect(styleOf(highlightLine('region = "eu-west-1"', "prod.tfvars"), "region")).toBe(
|
||||
"definition",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("highlightCodeInSteps", () => {
|
||||
it("splits a large file into many bounded steps with the same result", () => {
|
||||
const code = Array.from(
|
||||
|
||||
@@ -22,6 +22,9 @@ describe("isLanguageSupported", () => {
|
||||
expect(isLanguageSupported("test.dart")).toBe(true);
|
||||
expect(isLanguageSupported("test.cs")).toBe(true);
|
||||
expect(isLanguageSupported("test.nix")).toBe(true);
|
||||
expect(isLanguageSupported("main.tf")).toBe(true);
|
||||
expect(isLanguageSupported("prod.tfvars")).toBe(true);
|
||||
expect(isLanguageSupported("terragrunt.hcl")).toBe(true);
|
||||
expect(isLanguageSupported("test.ex")).toBe(true);
|
||||
expect(isLanguageSupported("Counter.svelte")).toBe(true);
|
||||
expect(isLanguageSupported("Page.astro")).toBe(true);
|
||||
@@ -65,6 +68,9 @@ describe("getSupportedExtensions", () => {
|
||||
expect(extensions).toContain("dart");
|
||||
expect(extensions).toContain("cs");
|
||||
expect(extensions).toContain("nix");
|
||||
expect(extensions).toContain("tf");
|
||||
expect(extensions).toContain("tfvars");
|
||||
expect(extensions).toContain("hcl");
|
||||
expect(extensions).toContain("json");
|
||||
expect(extensions).toContain("svelte");
|
||||
expect(extensions).toContain("astro");
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (C) 2021 by Marijn Haverbeke <marijnh@gmail.com> and others
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,41 @@
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
|
||||
export const hclHighlighting = styleTags({
|
||||
"for in if else endfor endif": t.controlKeyword,
|
||||
"Attribute/Identifier": t.definition(t.propertyName),
|
||||
"ObjectKey/Expression/VariableExpr/Identifier": t.definition(t.propertyName),
|
||||
"ObjectKey/Expression/LiteralValue/StringLit!": t.definition(t.propertyName),
|
||||
"ForIntro/Identifier TemplateForStart/Identifier": t.definition(t.variableName),
|
||||
"BlockType!": t.typeName,
|
||||
// Paseo themes have no role for labelName; labels render as the strings they are.
|
||||
"BlockLabel!": t.string,
|
||||
"FunctionNamespace/Identifier": t.namespace,
|
||||
"FunctionName/Identifier": t.function(t.variableName),
|
||||
"VariableExpr/Identifier": t.variableName,
|
||||
"GetAttr/Identifier": t.propertyName,
|
||||
Identifier: t.name,
|
||||
"QuotedTemplateStart QuotedTemplateEnd TemplateLiteral": t.string,
|
||||
NumericLit: t.number,
|
||||
"true false": t.bool,
|
||||
null: t.null,
|
||||
LineComment: t.lineComment,
|
||||
BlockComment: t.blockComment,
|
||||
"( )": t.paren,
|
||||
"{ }": t.brace,
|
||||
"[ ]": t.squareBracket,
|
||||
ArithOp: t.arithmeticOperator,
|
||||
LogicOp: t.logicOperator,
|
||||
CompareOp: t.compareOperator,
|
||||
ControlOp: t.controlOperator,
|
||||
"ControlOp/Colon": t.controlOperator,
|
||||
AssignOp: t.definitionOperator,
|
||||
DerefOp: t.derefOperator,
|
||||
HeredocIdentifier: t.special(t.controlKeyword),
|
||||
HeredocStart: t.operator,
|
||||
"TemplateInterpolationStart TemplateInterpolationEnd": t.special(t.brace),
|
||||
"TemplateDirectiveStart TemplateDirectiveEnd": t.special(t.brace),
|
||||
StripMarker: t.special(t.brace),
|
||||
"AttrSplat FullSplat LegacyIndex": t.derefOperator,
|
||||
"Arrow Ellipsis": t.punctuation,
|
||||
"ObjectElem/Colon ForIntro/Colon Comma NamespaceSeparator": t.separator,
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
delimitedIndent,
|
||||
foldInside,
|
||||
foldNodeProp,
|
||||
indentNodeProp,
|
||||
LRLanguage,
|
||||
} from "@codemirror/language";
|
||||
import type { SyntaxNode } from "@lezer/common";
|
||||
import { parser } from "./parser.js";
|
||||
|
||||
export const hclLanguage = LRLanguage.define({
|
||||
name: "HCL",
|
||||
parser: parser.configure({
|
||||
props: [
|
||||
indentNodeProp.add({
|
||||
Block: delimitedIndent({ closing: "}", align: false }),
|
||||
}),
|
||||
foldNodeProp.add({
|
||||
"Object Tuple TemplateFor TemplateIf": foldInside,
|
||||
BlockComment: (tree: SyntaxNode) => {
|
||||
return { from: tree.from + 2, to: tree.to - 2 };
|
||||
},
|
||||
HeredocTemplate(tree: SyntaxNode) {
|
||||
const start = tree.getChild("HeredocIdentifier");
|
||||
if (!start) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
from: start.to,
|
||||
to: tree.to,
|
||||
};
|
||||
},
|
||||
FunctionCall(tree: SyntaxNode) {
|
||||
const start = tree.getChild("(");
|
||||
const end = tree.getChild(")");
|
||||
if (!start || !end) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
from: start.from + 1,
|
||||
to: end.to - 1,
|
||||
};
|
||||
},
|
||||
Block(tree: SyntaxNode) {
|
||||
const start = tree.getChild("{");
|
||||
const end = tree.getChild("}");
|
||||
if (!start || !end) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
from: start.from + 1,
|
||||
to: end.to - 1,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
languageData: {
|
||||
commentTokens: { line: "#", block: { open: "/*", close: "*/" } },
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
// Generated by lezer-generator, taken from codemirror-lang-hcl@0.2.0 (MIT, see LICENSE).
|
||||
// Vendored for the same reason as packages/highlight/src/nix: only Lezer primitives load here.
|
||||
import { LRParser } from "@lezer/lr";
|
||||
import { hclHighlighting } from "./highlight.js";
|
||||
import {
|
||||
scanQuotedTemplateStart,
|
||||
scanQuotedTemplateEnd,
|
||||
scanTemplateInterpolationStart,
|
||||
scanTemplateInterpolationEnd,
|
||||
scanTemplateDirectiveStart,
|
||||
scanTemplateDirectiveEnd,
|
||||
scanHeredocIdentifier,
|
||||
scanTemplateLiteralChunk,
|
||||
trackTemplate,
|
||||
} from "./tokens.js";
|
||||
|
||||
const spec_Identifier = new Map<string, number>([
|
||||
["true", 41],
|
||||
["false", 43],
|
||||
["null", 47],
|
||||
["for", 66],
|
||||
["in", 70],
|
||||
["endfor", 74],
|
||||
["if", 80],
|
||||
["else", 84],
|
||||
["endif", 88],
|
||||
]);
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states:
|
||||
"GxQ]Q&jOOPeO&jOOOmQ&kO'#DcO!kQ&kO'#E_O!yQ&kO'#E^OOQO'#Ea'#EaO#UQ&jO'#ChQOQ&jOOPOOO'#Cd'#CdPOOO)C@`)C@`OOQy'#Co'#CoOOQy'#Cr'#CrO#aQ)vO'#CwOOQy'#Cm'#CmOOQ!b'#D['#D[O#oQ'|O'#DZOOQy'#Cv'#CvO$rQ&kO'#D`O$yQ&jO'#DeO%UQ&jO'#DdO&XQ&kO'#EqO&cQ&kO'#DcOOQy,59},59}O&jQ&jO,59}OOQy'#D]'#D]O(YQ'dO'#DgOOQO'#Ee'#EeO)PQ&jO'#DiO)UQ&jO'#DiOOQy'#Dp'#DpOOQP'#Dx'#DxOOQP'#Dy'#DyO%^Q&kO'#DwO*nQ'dO'#ClOOQy'#Dv'#DvOOQy'#El'#ElO%^Q&kO'#ElOOQy'#Cl'#ClO%^Q&kO,59TO#jQ)`O'#CtOOQP'#E`'#E`OOQP'#Eg'#EgO+eQ&kO,5:xO+pQ&jO,5:xOOQO-E8_-E8_O+xQ+YO'#CuO,ZQ)vO,59`O,iQ&sO'#CxO,sQ)tO'#C{O-OQ)tO'#DSOOQ%n'#Cz'#CzOOQ%n'#Eb'#EbO-ZQ)vO,59cOOQy,59c,59cO-iQ&jO'#DTO-tQ+WO,59uO.SQ&jO'#EpOOQy,59z,59zO._Q&jO,59zO%^Q&kO,5:]O.dQ&jO'#DrOOQP'#E['#E[O%^Q&kO,5:uO.iQ&kO,5:OOOQP'#Ed'#EdO/dQ&kO,5;]O/kQ&kO,5;]O/uQ&kO,5:`OOQy1G/i1G/iOOQO'#Dk'#DkOOQO,5:U,5:UO0pQ&jO'#DlOOQO-E8c-E8cO0xQ&jO,5:TO0}Q&kO,5:TOOQy,5:c,5:cOOQP'#D{'#D{OOQP'#D|'#D|OOQP'#D}'#D}OOQP'#EO'#EOOOQP'#EP'#EPOOQP'#EQ'#EQO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO'#ESOOQy'#ER'#ERO1UQ&jO'#EUOOQy'#EX'#EXOOQy'#EY'#EYOOQy'#EW'#EWOOQy,5;W,5;WO1ZQ&jO,5;WO1cQ&jO1G.oO1qQ&lO,59`OOQP-E8e-E8eO1vQ&jO1G0dOOQO1G0d1G0dO1yQ&jO1G0dO+xQ+YO'#EmOOQ%n,59a,59aOOQy1G.z1G.zOOQ%n,59d,59dO2OQ&sO,59dO2YQ&rO,59dOOQ%n,59g,59gO,sQ)tO,59gO2eQ&jO'#DQOOQ%n,59n,59nO2sQ)tO,59nO-OQ)tO,59nO3OQ&jO'#DVOOQ%n-E8`-E8`OOQy1G.}1G.}O3aQ&jO,59hO3fQ&jO,59oO%^Q&kO,59oO3nQ+WO1G/aOOQy1G/a1G/aO3|Q&kO,5;[O4TQ&jO,5;[OOQy1G/f1G/fO4]Q&jO1G/wO4hQ&jO,5:^O4pQ&jO1G0aO4xQ&kO1G/jOOQP,5;O,5;OO5|Q&kO1G0wOOQP-E8b-E8bO7eQ,UO'#DgO7oQ,UO1G/zO9RQ,UO'#ClP9]Q&jO'#DjO9bQ&kO1G/oO9iQ&jO'#DnOOQy1G/o1G/oO9wQ&jO1G/oOOQy1G0Q1G0QO;|Q'dO1G0QO<WQ'dO1G0QO>bQ'dO1G0QO>lQ'dO1G0QO@^Q'dO1G0QOAQQ&jO,5:nOOQy,5:p,5:pPOQy'#Ef'#EfPOQy,5:s,5:sPOQy,5:t,5:tOOQy1G0r1G0rOOQO7+&O7+&OOAYQ&jO7+&OOOQ%n,5;X,5;XOOQ%n1G/O1G/OOA_Q&rO1G/OOAdQ&rO1G/OOOQ%n1G/R1G/ROAoQ&jO,59lOAzQ'[O,59lOOQ%n1G/Y1G/YO2sQ)tO1G/YOBSQ&jO'#DXO2sQ)tO1G/YOBbQ&jO,59qOBpQ'[O,59qOBxQ'[O,59sOCQQ&jO1G/SOCYQ&jO1G/SO%^Q&kO1G/ZOC_Q'[O1G/ZOOQy7+${7+${OCjQ&jO,5:}OC{Q&kO1G0vOOQO-E8a-E8aOOQy7+%c7+%cODSQ&jO7+%cO%^Q&kO'#DsO%^Q&kO1G/xODXQ&jO1G/xOOQP'#E]'#E]O%^Q&kO7+%{P%^Q&kO'#EdO%^Q&kO7+%fOOQy7+%Z7+%ZOD^Q&jO7+%ZODcQ&kO,5:YODjQ&jO,5:YOOQO,5:Y,5:YOOQy1G0Y1G0YPOQy-E8d-E8dOOQO<<Ij<<IjOOQ%n7+$j7+$jODuQ&rO7+$jODzQ'[O1G/WOOQ%n1G/W1G/WOESQ'[O1G/WOOQ%n7+$t7+$tOEXQ&jO,59sO2sQ)tO7+$tOEdQ'[O1G/]OElQ'[O1G/_OOQ$Y1G/]1G/]OEtQ'[O1G/]OOQ%n1G/_1G/_OEyQ'[O1G/_O%^Q&kO7+$nOFOQ&jO7+$nOFTQ&jO7+$nOF]Q'[O7+$uOOQ$Y7+$u7+$uOFhQ'[O7+$uP%^Q&kO'#EcOOQy<<H}<<H}OFmQ&jO,5:_OFxQ&jO7+%dOGQQ&jO7+%dOOQy<<Ig<<IgOGVQ&jO<<IQOHrQ,UO1G0QOJWQ,UO1G0QOKlQ,UO1G0QOMQQ,UO1G0QOMXQ,UO1G0QOOQy<<Hu<<HuOM`Q&kO1G/tOOQO1G/t1G/tOOQ%n<<HU<<HUOOQ%n7+$r7+$rOMgQ'[O7+$rOOQ%n<<H`<<H`OOQ$Y7+$w7+$wOMlQ'[O7+$wOOQ%n7+$y7+$yOMqQ'[O7+$yOMvQ'[O<<HYONRQ&jO<<HYO%^Q&kO<<HYONWQ&jO<<HYOOQ$Y<<Ha<<HaON]Q'[O<<HaOOQP<<IO<<IOO%^Q&kO<<IOOOQyAN>lAN>lONbQ&jOAN>lONgQ&jOAN>lOOQ%n<<H^<<H^OOQ$Y<<Hc<<HcOOQ%n<<He<<HeOOQ$YAN=tAN=tONoQ'[OAN=tO%^Q&kOAN=tONtQ'[OAN=tO! PQ&jOAN=tOOQ$YAN={AN={O! UQ&jOAN>jOOQyG24WG24WO! ^Q&jOG24WOOQ$YG23`G23`O! cQ'[OG23`O! nQ'[OG23`O%^Q&kOG23`OOQPG24UG24UOOQyLD)rLD)rOOQ$YLD(zLD(zO! sQ'[OLD(zO! xQ'[OLD(zOOQ$Y!$'Lf!$'LfO!!TQ'[O!$'LfOOQ$Y!)9BQ!)9BQO!!YQ&kO'#DgO.iQ&kO'#DwO/uQ&kO'#DwO!!|Q&kO'#ClO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO!#sQ&kO1G0QO!$pQ&kO1G0QO!%mQ&kO1G0QO!(mQ&kO1G0QO!(tQ&kO1G0QO.iQ&kO7+%{O/uQ&kO7+%{O4pQ&jO1G0aO4pQ&jO1G0aO%^Q&kO,5:uO%^Q&kO,5:u",
|
||||
stateData:
|
||||
"!)T~O#_OSXPQYPQ~O^RO!UQO~OXWOYWO~OP[O^iOb]OdYOeYOgZO!RaO!TfO!UeO!atO#b^O#c^O#gnO#hoO~O_vOP#RX^#RX!U#RX~OPwO^xO!U{O~O^RO#[[X!T[X~OQ!VOR!POT!WO#]}O~OV!XO~OP[O^iOb]OdYOeYOgZOq!^O!RaO!UeO!atO#b^O#c^O#gnO#hoO~O!Q!ZO~P#tO#w!_O_!XX!Y!XX~O_!aO!Y!aO~OP[O^iOb]OdYOeYOgZO!RaO!UeO!atO#b^O#c^O#gnO#hoO~Or!cO!T#eX~P%^Oq!^O~PmO!T!fO~O#f!gO!R!ZX!w!ZX!y!ZX#g!ZX#i!ZX#j!ZX#k!ZX#l!ZX#m!ZX#n!ZX#o!ZX#p!ZX#q!ZX#r!ZX#s!ZX#t!ZX#u!ZX#v!ZX#w!ZXr!ZX^!ZX!T!ZX~O_!ZX!Y!ZX!a!`X!Q!ZX![!ZX#[!ZXS!ZXm!ZXx!ZX!c!ZXU!ZX~P&oO^!iO~O!a!lO~O!w!{O!y!|O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qO#s!rO#t!sO#u!}O#v#OO#w`Xr`X^`X!T`X~O!R!zO_`X!Y`X!Q`X![`X#[`XS`Xm`Xx`X!c`XU`X~P)ZOPwO^xO!U#VO~O^RO!T#WO~O#]#YOQ#aPR#aPT#aPV#aP~OQ#[OR#UXT#UX#]#UX~OS#]Om#^O~P%^OR!POT#bO#]}O~OR!POT#fO#]}O~OQ#hOR!POT!WO#]}O~Om#jOq#iOx#kO~OR!POT!WOV#mO#]}O~Or#nO#w!_O!Q#dX~O!Q#pO~O^#rO~OP[O^&yOb]OdYOeYOgZO!RaO!UeO!atO#b^O#c^O#gnO#hoO~O!T#ea~P%^Or#vO!T#ea~P%^OP[O^#xOb]OdYOeYOgZO!RaO!UeO!atO#b^O#c^O#gnO#hoO~O#f!gO!a!`X~O!a#|O~O![$OO~P%^O^$XO~O![$]O#w!_O~O#w!_O^]i#[]i!T]i~OQ#[O~O^RO!T$^O~OS$aOm$bO~P%^OS$aOm$bO#w!_O~Om$eOq#iOu$fOx#kO~OR!POT$iO#]}O~Om$kOq#iOx#kOz$lO|$mO~O^$nO~Oq$oOx$pO~OR!POT!WOV$rO#]}O~O!Q#da~P%^Or$tO!Q#da~Ox$xO!Q$vO#w!_O~Or$zOs$yO~O!Y${O#w!_O~O#w!_OP!Wi^!Wib!Wid!Wie!Wig!Wir!Wi!R!Wi!T!Wi!U!Wi!a!Wi#b!Wi#c!Wi#g!Wi#h!Wi~O!T#ei~P%^O#f!gO!R!ZX!w!ZX!y!ZX#g!ZX#i!ZX#j!ZX#k!ZX#l!ZX#m!ZX#n!ZX#o!ZX#p!ZX#q!ZX#r!ZX#s!ZX#t!ZX#u!ZX#v!ZX#w!ZX~O!a!`X!i!ZX~P6TO!i%OO#w!_O~O!w!{O!y!|O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qO#s!rO#t!sO#u!}O#v#OO#w`X~O!R!zO!i`X~P7wO#f!gO~O![%PO~P%^Or%RO!c%TO#w!_O![!bX~O![%PO~O#i!nO#j!nO#k!nO_!ni!R!ni!Y!ni!w!ni!y!ni#m!ni#n!ni#o!ni#p!ni#q!ni#r!ni#s!ni#t!ni#u!ni#v!ni#w!nir!ni!Q!ni![!ni^!ni#[!niS!nim!nix!ni!c!niU!ni!T!ni~O#g!ni#l!ni~P9|O#g!oO#l!oO~P9|O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO_!ni!R!ni!Y!ni!w!ni!y!ni#s!ni#t!ni#u!ni#v!ni#w!nir!ni!Q!ni![!ni^!ni#[!niS!nim!nix!ni!c!niU!ni!T!ni~O#q!ni#r!ni~P<bO#q!qO#r!qO~P<bO#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qO#s!rO!R!ni!w!ni!y!ni#t!ni#u!ni#v!ni#w!nir!ni^!ni!T!ni~O_!ni!Y!ni!Q!ni![!ni#[!niS!nim!nix!ni!c!niU!ni~P>vO!Q%UO#w!_O~O!T%WO~OS%XO~OS%XOm%YO#w!_O~Oq$oOu%ZOx$pO~OU%[Om%]O~Om%_Oq#iOx#kO|$mO~Oq$oOx$pOz%aO|%bO~OU%cOm%dO~OU%eOm%fO~Or%hOs%gO~O^%iO~OU%kOm%lO#w!_O~O#w!_Or#Va!Q#Va![#Va!c#Va~O!Q#di~P%^O!Q%nO~O^%qO~O![%yO~O![!ba~P%^Or%zO!c%{O![!ba~OS%|O~OU%}Om&OO~OU%}O~Oq$oOx$pO|%bO~OU&QOm&RO~OU&SOm&TO~OU&QO~OU&SO~O^&VO~Or&XOs&WO~OU&YOm&ZO#w!_O~OU&YO~O#w!_O!Q!ga!T!ga~O!Y&[O#w!_O~Os&]O~Ox$xO!T&^O!c&`O#w!_O~O#i!nO#j!nO#k!nO!R!ni!w!ni!y!ni#g!ni#l!ni#m!ni#n!ni#o!ni#p!ni#q!ni#r!ni#s!ni#t!ni#u!ni#v!ni#w!ni~O!i!ni~PGeO#g!oO#i!nO#j!nO#k!nO#l!oO!R!ni!w!ni!y!ni#m!ni#n!ni#o!ni#p!ni#q!ni#r!ni#s!ni#t!ni#u!ni#v!ni#w!ni~O!i!ni~PHyO#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO!R!ni!w!ni!y!ni#q!ni#r!ni#s!ni#t!ni#u!ni#v!ni#w!ni~O!i!ni~PJ_O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qO!R!ni!i!ni!w!ni!y!ni#t!ni#u!ni#v!ni#w!ni~O#s!ni~PKsO#s!rO~PKsO![!bi~P%^OU&aO~OU&bO~OU&cO~OU&dOm&eO#w!_O~Os&fO~O^&hO~OU&iO~O!T&kO~Ox$xO!T&kO~OU&mO~OU&mOm&oO#w!_O~Os&pO~O!Y&qO#w!_O~O!T&rO~OU&sOm&tO#w!_O~OU&sO~OU&vO~OU&vOm&wO#w!_O~OU&xO~OP!ZXb!ZXd!ZXe!ZXg!ZX!U!ZX!a!ZX#b!ZX#c!ZX#h!ZX~P&oOP`Xb`Xd`Xe`Xg`X!R`X!U`X!a`X#b`X#c`X#h`X~P)ZOP!ni^!nib!nid!nie!nig!nir!ni!T!ni!U!ni!a!ni#b!ni#c!ni#h!ni~PGeOP!ni^!nib!nid!nie!nig!nir!ni!T!ni!U!ni!a!ni#b!ni#c!ni#h!ni~PHyOP!ni^!nib!nid!nie!nig!nir!ni!T!ni!U!ni!a!ni#b!ni#c!ni#h!ni~PJ_O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qOP!ni^!nib!nid!nie!nig!nir!ni!R!ni!T!ni!U!ni!a!ni!w!ni!y!ni#b!ni#c!ni#h!ni#t!ni#u!ni#v!ni#w!ni~O#s!ni~P!&jO#s!rO~P!&jOb!w!y^X#j^~",
|
||||
goto: "CW#fPPPPPPPP#gPPP#j#sPP#y%xP'YPP'YP(j*O%x*c+sP+s,R,aPPP,oP,R,uP-TP-ZP*c-i%xPP.yPP0Z1n1{P%xP%x2U3g3nP5RP%x5X6i6o5XP%x6x8Y8Y6x9n:W:m;P;`;l;u;zP;uP;u<O<O<T=O=o#s=x>O>S>[>v?Q?WP@kPPPP@qByPPCPCSRXPQVOQ#X{R$_#VXTOU{#V^bQde!c!d#v$}Q!YaQ#RtQ#SvQ#_!PQ#q!]Q#s!`Q#t!aQ#y!eS#}!l#|Q$W!zQ$c#^Q$q#kY$s#n$t%R%m%zQ%j$pQ%o$xQ%p$yU%r$|'`'aQ%s%OQ&U%gQ&g&WQ&j&]Q&n&fQ&u&pQ'b'dR'c'e#msQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'e#m]Qadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'e#l]Qadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eTxSzQ!O[f!T!Q!R!U!X#a#d#e#l$h$j%`R#Tw#m`Qadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'ei!T[!Q!R!U!X#a#d#e#l$h$j%`i!S[!Q!R!U!X#a#d#e#l$h$j%`i!Q[!Q!R!U!X#a#d#e#l$h$j%`Q#`!QR$d#ai!R[!Q!R!U!X#a#d#e#l$h$j%`Q#d!RR$j#eQ#c!RS$g#d#eS%^$h$jR&P%`#m_Qadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'e#mhQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eQVO#mhQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eSdQeS!bd!dV#u!c#v$}_cQde!c!d#v$}#ojQadekptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eZ!hi!i#x#{&y#llQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eR!kkQ$P!lR%Q#|#mmQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eQ!]aR!eeQ$w#qQ&_%sR&l&`#mrQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'e!vpQadeptv!P!]!`!c!d!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p'd'eb&z!a&z&}'P'R'T'V'X'`c&{!e&{'O'Q'S'U'W'Y'a[!tq$R$S$T$U$V[&}&|'Z'[']'^'_]'O#z%t%u%v%w%xY!uq$S$T$U$VY'P&|'[']'^'_Z'Q#z%u%v%w%xW!vq$T$U$VW'R&|']'^'_X'S#z%v%w%xU!wq$U$VU'T&|'^'_V'U#z%w%xS!xq$VS'V&|'_T'W#z%xQ!yqQ'X&|R'Y#zV#Qq#z&|T!{q#zV#Pq#z&|!quQadetv!P!]!`!a!c!d!e!l!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p'`'a'd'e!O!`b!Y#R#S#_#q#s#}$W$c$q$s%j%o%p%s&U&g&j&n&u'b'cQ'd#tR'e#yQ$|#sQ'`'bR'a'cXSOU{#VTySzUUO{#VR|UQ!U[Q#a!QQ#e!R[#g!U#a#e#l$h%`Q#l!XQ$h#dR%`$jQ#o!YS$u#o%SR%S#}Q!ddR#w!d#lkQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eR!jkQzSR#Uz!hqQadetv!P!]!`!c!d!l!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p'd'eU!mp&z&{S#z!e'aU$Q!t&}'OQ$R!uQ$S!vQ$T!wQ$U!xQ$V!yQ%t'QQ%u'SQ%v'UQ%w'WQ%x'YS&|!a'`Q'Z'PQ'['RQ']'TQ'^'VR'_'XQ#Z}R$`#YR![aTgQe",
|
||||
nodeNames:
|
||||
"⚠ QuotedTemplateStart QuotedTemplateEnd TemplateInterpolationStart TemplateInterpolationEnd TemplateDirectiveStart TemplateDirectiveEnd HeredocIdentifier Comment LineComment BlockComment ConfigFile Body Attribute Identifier AssignOp Expression LiteralValue NumericLit BoolLit true false NullLit null StringLit TemplateLiteral TemplateExpr QuotedTemplate TemplateInterpolation StripMarker TemplateDirective TemplateFor TemplateForStart for Comma in TemplateForEnd endfor TemplateIf TemplateIfIntro if TemplateElseIntro else TemplateIfEnd endif HeredocTemplate HeredocStart CollectionValue ] [ Tuple } { Object ObjectElem ObjectKey Colon VariableExpr ) FunctionCall FunctionNamespace NamespaceSeparator FunctionName ( FunctionArguments Ellipsis ForExpr ForTupleExpr ForIntro ForCond ForObjectExpr Arrow Operation UnaryOperation ArithOp LogicOp BinaryOperation ArithOp ArithOp CompareOp CompareOp LogicOp LogicOp Index NewIndex LegacyIndex GetAttr DerefOp Splat AttrSplat FullSplat Conditional ControlOp ControlOp Block BlockType BlockLabel",
|
||||
maxTerm: 131,
|
||||
context: trackTemplate,
|
||||
nodeProps: [
|
||||
["openedBy", 48, "[", 51, "{"],
|
||||
["closedBy", 49, "]", 52, "}"],
|
||||
],
|
||||
skippedNodes: [0, 8, 9, 10],
|
||||
repeatNodeCount: 7,
|
||||
tokenData:
|
||||
"-p~RvX^#ipq#iqr$^st$kuv%Svw%Xxy%dyz%iz{%n{|%s|}%x}!O%}!O!P&S!P!Q&z!Q!R(^!R![)l![!]*c!^!_*p!_!`+_!`!a+t!a!b,R!c!},W!}#O,l#P#Q-P#R#S,W#T#o,W#o#p-U#p#q-Z#q#r-f#r#s-k#y#z#i$f$g#i#BY#BZ#i$IS$I_#i$I|$JO#i$JT$JU#i$KV$KW#i&FU&FV#i~#nY#_~X^#ipq#i#y#z#i$f$g#i#BY#BZ#i$IS$I_#i$I|$JO#i$JT$JU#i$KV$KW#i&FU&FV#i~$cP#h~!_!`$f~$kO#r~~$pSX~OY$kZ;'S$k;'S;=`$|<%lO$k~%PP;=`<%l$k~%XO#k~~%[Pvw%_~%dO#s~~%iO!a~~%nO![~~%sO#i~~%xO#l~~%}Or~~&SO#g~~&XR!y~z{&b!O!P&g!Q![&r~&gO#u~~&jP!O!P&m~&rO!c~~&wP!w~!Q![&r~'PQ#j~z{'V!P!Q$k~'YTOz'Vz{'i{;'S'V;'S;=`(W<%lO'V~'lVOz'Vz{'i{!P'V!P!Q(R!Q;'S'V;'S;=`(W<%lO'V~(WOY~~(ZP;=`<%l'V~(cRb~!O!P(l!Q![)l#l#m)w~(oP!Q![(r~(wRb~!Q![(r!g!h)Q#X#Y)Q~)TR{|)^}!O)^!Q![)d~)aP!Q![)d~)iPb~!Q![)d~)qQb~!O!P(l!Q![)l~)zR!Q![*T!c!i*T#T#Z*T~*YRb~!Q![*T!c!i*T#T#Z*T~*hP!Y~![!]*k~*pO#f~~*uQ#o~!^!_*{!_!`+Y~+QP#b~}!O+T~+YO#c~~+_O#p~~+dQ_P!_!`+j!`!a+o~+oO#q~Q+tO!iQ~+yP#m~!_!`+|~,RO#n~~,WO#w~~,]T^~}!O,W!Q![,W!c!},W#R#S,W#T#o,W~,qP!R~z{,t~,wP#P#Q,z~-PO#v~~-UO!Q~~-ZO!U~~-^P#p#q-a~-fO#t~~-kO!T~~-pOm~",
|
||||
tokenizers: [
|
||||
scanQuotedTemplateStart,
|
||||
scanQuotedTemplateEnd,
|
||||
scanTemplateInterpolationStart,
|
||||
scanTemplateInterpolationEnd,
|
||||
scanTemplateDirectiveStart,
|
||||
scanTemplateDirectiveEnd,
|
||||
scanHeredocIdentifier,
|
||||
scanTemplateLiteralChunk,
|
||||
0,
|
||||
1,
|
||||
],
|
||||
topRules: { ConfigFile: [0, 11] },
|
||||
dynamicPrecedences: { "19": 1, "22": 1 },
|
||||
specialized: [{ term: 14, get: (value: string) => spec_Identifier.get(value) ?? -1 }],
|
||||
propSources: [hclHighlighting],
|
||||
tokenPrec: 2481,
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
// Generated by lezer-generator, taken from codemirror-lang-hcl@0.2.0 (MIT, see LICENSE).
|
||||
// Vendored for the same reason as packages/highlight/src/nix: only Lezer primitives load here.
|
||||
export const QuotedTemplateStart = 1;
|
||||
export const QuotedTemplateEnd = 2;
|
||||
export const TemplateInterpolationStart = 3;
|
||||
export const TemplateInterpolationEnd = 4;
|
||||
export const TemplateDirectiveStart = 5;
|
||||
export const TemplateDirectiveEnd = 6;
|
||||
export const HeredocIdentifier = 7;
|
||||
export const templateLiteralChunk = 105;
|
||||
@@ -0,0 +1,317 @@
|
||||
// Taken from codemirror-lang-hcl@0.2.0 (MIT, see LICENSE).
|
||||
// Vendored for the same reason as packages/highlight/src/nix: only Lezer primitives load here.
|
||||
import { ContextTracker, ExternalTokenizer, type InputStream } from "@lezer/lr";
|
||||
import {
|
||||
QuotedTemplateStart,
|
||||
QuotedTemplateEnd,
|
||||
TemplateInterpolationStart,
|
||||
TemplateInterpolationEnd,
|
||||
TemplateDirectiveStart,
|
||||
TemplateDirectiveEnd,
|
||||
HeredocIdentifier,
|
||||
templateLiteralChunk,
|
||||
} from "./terms.js";
|
||||
|
||||
const ContextType = {
|
||||
QUOTED_TEMPLATE: 1,
|
||||
TEMPLATE_INTERPOLATION: 2,
|
||||
TEMPLATE_DIRECTIVE: 4,
|
||||
HEREDOC_TEMPLATE: 8,
|
||||
};
|
||||
|
||||
const hashStr = function (str: string) {
|
||||
var hash = 0,
|
||||
i,
|
||||
chr;
|
||||
if (str.length === 0) return hash;
|
||||
for (i = 0; i < str.length; i++) {
|
||||
chr = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
|
||||
class Context {
|
||||
readonly parent: Context | null;
|
||||
readonly type: number;
|
||||
readonly heredoc_identifier: string;
|
||||
readonly hash: number;
|
||||
|
||||
constructor(parent: Context | null, type: number, heredoc_identifier: string) {
|
||||
this.parent = parent;
|
||||
this.type = type;
|
||||
this.heredoc_identifier = heredoc_identifier;
|
||||
this.hash =
|
||||
(parent ? (parent.hash + parent.hash) << 8 : 0) +
|
||||
(type << 4) +
|
||||
(this.heredoc_identifier === "" ? 0 : hashStr(this.heredoc_identifier));
|
||||
}
|
||||
}
|
||||
|
||||
function readTokenText(input: InputStream, end: number): string {
|
||||
let text = "";
|
||||
for (let offset = 0; offset < end - input.pos; offset++) {
|
||||
text += String.fromCharCode(input.peek(offset));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
const topCtx = new Context(null, 0, "");
|
||||
|
||||
export const trackTemplate = new ContextTracker({
|
||||
start: topCtx,
|
||||
shift(context, term, stack, input) {
|
||||
if (term === QuotedTemplateStart) {
|
||||
return new Context(context, ContextType.QUOTED_TEMPLATE, "");
|
||||
}
|
||||
if (term === TemplateInterpolationStart) {
|
||||
return new Context(context, ContextType.TEMPLATE_INTERPOLATION, "");
|
||||
}
|
||||
if (term === TemplateDirectiveStart) {
|
||||
return new Context(context, ContextType.TEMPLATE_DIRECTIVE, "");
|
||||
}
|
||||
if (term === HeredocIdentifier && context.type !== ContextType.HEREDOC_TEMPLATE) {
|
||||
const ctx = new Context(
|
||||
context,
|
||||
ContextType.HEREDOC_TEMPLATE,
|
||||
readTokenText(input, stack.pos),
|
||||
);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
if (term === HeredocIdentifier && context.type === ContextType.HEREDOC_TEMPLATE) {
|
||||
return context.parent ?? context;
|
||||
}
|
||||
switch (term) {
|
||||
case QuotedTemplateEnd:
|
||||
case TemplateInterpolationEnd:
|
||||
case TemplateDirectiveEnd:
|
||||
return context.parent ?? context;
|
||||
}
|
||||
|
||||
return context;
|
||||
},
|
||||
hash(context) {
|
||||
return context.hash;
|
||||
},
|
||||
});
|
||||
|
||||
const HEX_DIGIT = /[0-9a-fA-F]/;
|
||||
|
||||
function scanEscapeSequence(input: InputStream): void {
|
||||
input.advance();
|
||||
const kind = String.fromCharCode(input.next);
|
||||
if (kind === "u" || kind === "U") {
|
||||
const digits = kind === "u" ? 4 : 8;
|
||||
for (let i = 0; i < digits; i++) {
|
||||
input.advance();
|
||||
if (!HEX_DIGIT.test(String.fromCharCode(input.next))) return;
|
||||
}
|
||||
} else if (!'"nrt\\'.includes(kind)) {
|
||||
return;
|
||||
}
|
||||
input.advance();
|
||||
input.acceptToken(templateLiteralChunk);
|
||||
}
|
||||
|
||||
function scanHeredocBody(input: InputStream, expected: string): void {
|
||||
let has_leading_whitespace_with_newline = false;
|
||||
let has_leading_whitespace = false;
|
||||
let next = String.fromCharCode(input.next);
|
||||
while (/\s/.test(next)) {
|
||||
has_leading_whitespace = true;
|
||||
if (next === "\n") {
|
||||
has_leading_whitespace_with_newline = true;
|
||||
}
|
||||
input.advance();
|
||||
next = String.fromCharCode(input.next);
|
||||
}
|
||||
if (has_leading_whitespace) {
|
||||
input.acceptToken(templateLiteralChunk);
|
||||
}
|
||||
|
||||
if (!has_leading_whitespace_with_newline) {
|
||||
return;
|
||||
}
|
||||
|
||||
let advance = 0;
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
if (String.fromCharCode(input.next) === expected[i]) {
|
||||
input.advance();
|
||||
advance++;
|
||||
} else {
|
||||
if (advance) {
|
||||
input.acceptToken(templateLiteralChunk);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// check if the identifier is on a line of its own
|
||||
let i = 0;
|
||||
while (true) {
|
||||
const ch = input.peek(i);
|
||||
if (ch == -1 || String.fromCharCode(ch) === "\n") {
|
||||
return input.acceptToken(HeredocIdentifier);
|
||||
}
|
||||
|
||||
if (!/\s/.test(String.fromCharCode(ch))) {
|
||||
return input.acceptToken(templateLiteralChunk);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// `$${` and `%%{` are escapes. The doubled marker counts only when `{` follows it, so in `$$${`
|
||||
// the first `$` is literal text and the rest is the escape.
|
||||
function scanTemplateMarker(input: InputStream, marker: string, startToken: number): void {
|
||||
input.advance();
|
||||
if (String.fromCharCode(input.next) === "{") {
|
||||
input.advance();
|
||||
return input.acceptToken(startToken);
|
||||
}
|
||||
if (String.fromCharCode(input.next) === marker && String.fromCharCode(input.peek(1)) === "{") {
|
||||
input.advance();
|
||||
input.advance();
|
||||
}
|
||||
input.acceptToken(templateLiteralChunk);
|
||||
}
|
||||
|
||||
export const scanQuotedTemplateStart = new ExternalTokenizer(
|
||||
(input, stack) => {
|
||||
if (
|
||||
stack.canShift(QuotedTemplateStart) &&
|
||||
!(stack.context && stack.context.type === ContextType.QUOTED_TEMPLATE) &&
|
||||
String.fromCharCode(input.next) === '"'
|
||||
) {
|
||||
input.advance();
|
||||
return input.acceptToken(QuotedTemplateStart);
|
||||
}
|
||||
},
|
||||
{ contextual: true, fallback: true },
|
||||
);
|
||||
export const scanQuotedTemplateEnd = new ExternalTokenizer(
|
||||
(input, stack) => {
|
||||
if (
|
||||
stack.canShift(QuotedTemplateEnd) &&
|
||||
stack.context &&
|
||||
stack.context.type === ContextType.QUOTED_TEMPLATE &&
|
||||
String.fromCharCode(input.next) === '"'
|
||||
) {
|
||||
input.advance();
|
||||
return input.acceptToken(QuotedTemplateEnd);
|
||||
}
|
||||
},
|
||||
{ contextual: true, fallback: true },
|
||||
);
|
||||
|
||||
export const scanTemplateLiteralChunk = new ExternalTokenizer(
|
||||
(input, stack) => {
|
||||
if (
|
||||
stack.canShift(templateLiteralChunk) &&
|
||||
stack.context &&
|
||||
stack.context.type === ContextType.QUOTED_TEMPLATE
|
||||
) {
|
||||
if (String.fromCharCode(input.next) === "\\") {
|
||||
return scanEscapeSequence(input);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
stack.canShift(templateLiteralChunk) &&
|
||||
stack.context &&
|
||||
(stack.context.type === ContextType.QUOTED_TEMPLATE ||
|
||||
stack.context.type === ContextType.HEREDOC_TEMPLATE) &&
|
||||
input.next !== -1
|
||||
) {
|
||||
input.advance();
|
||||
return input.acceptToken(templateLiteralChunk);
|
||||
}
|
||||
},
|
||||
{ contextual: true, fallback: true },
|
||||
);
|
||||
export const scanTemplateInterpolationStart = new ExternalTokenizer(
|
||||
(input, stack) => {
|
||||
if (
|
||||
stack.canShift(TemplateInterpolationStart) &&
|
||||
stack.canShift(templateLiteralChunk) &&
|
||||
!(stack.context && stack.context.type === ContextType.TEMPLATE_INTERPOLATION) &&
|
||||
String.fromCharCode(input.next) === "$"
|
||||
) {
|
||||
return scanTemplateMarker(input, "$", TemplateInterpolationStart);
|
||||
}
|
||||
},
|
||||
{ contextual: true, fallback: true },
|
||||
);
|
||||
export const scanTemplateInterpolationEnd = new ExternalTokenizer(
|
||||
(input, stack) => {
|
||||
if (
|
||||
stack.canShift(TemplateInterpolationEnd) &&
|
||||
stack.context &&
|
||||
stack.context.type === ContextType.TEMPLATE_INTERPOLATION &&
|
||||
String.fromCharCode(input.next) === "}"
|
||||
) {
|
||||
input.advance();
|
||||
return input.acceptToken(TemplateInterpolationEnd);
|
||||
}
|
||||
},
|
||||
{ contextual: true, fallback: true },
|
||||
);
|
||||
export const scanTemplateDirectiveStart = new ExternalTokenizer(
|
||||
(input, stack) => {
|
||||
if (
|
||||
stack.canShift(TemplateDirectiveStart) &&
|
||||
stack.canShift(templateLiteralChunk) &&
|
||||
!(stack.context && stack.context.type === ContextType.TEMPLATE_DIRECTIVE) &&
|
||||
String.fromCharCode(input.next) === "%"
|
||||
) {
|
||||
return scanTemplateMarker(input, "%", TemplateDirectiveStart);
|
||||
}
|
||||
},
|
||||
{ contextual: true, fallback: true },
|
||||
);
|
||||
export const scanTemplateDirectiveEnd = new ExternalTokenizer(
|
||||
(input, stack) => {
|
||||
if (
|
||||
stack.canShift(TemplateDirectiveEnd) &&
|
||||
stack.context &&
|
||||
stack.context.type === ContextType.TEMPLATE_DIRECTIVE &&
|
||||
String.fromCharCode(input.next) === "}"
|
||||
) {
|
||||
input.advance();
|
||||
return input.acceptToken(TemplateDirectiveEnd);
|
||||
}
|
||||
},
|
||||
{ contextual: true, fallback: true },
|
||||
);
|
||||
|
||||
export const scanHeredocIdentifier = new ExternalTokenizer(
|
||||
(input, stack) => {
|
||||
if (
|
||||
stack.canShift(HeredocIdentifier) &&
|
||||
!(stack.context && stack.context.type === ContextType.HEREDOC_TEMPLATE) &&
|
||||
input.next !== -1
|
||||
) {
|
||||
let next = String.fromCharCode(input.next);
|
||||
while (/[\p{L}\p{N}_-]/u.test(next)) {
|
||||
input.advance();
|
||||
next = String.fromCharCode(input.next);
|
||||
}
|
||||
|
||||
return input.acceptToken(HeredocIdentifier);
|
||||
}
|
||||
|
||||
if (
|
||||
stack.canShift(HeredocIdentifier) &&
|
||||
stack.canShift(templateLiteralChunk) &&
|
||||
stack.context &&
|
||||
stack.context.type === ContextType.HEREDOC_TEMPLATE &&
|
||||
input.next != -1
|
||||
) {
|
||||
return scanHeredocBody(input, stack.context.heredoc_identifier);
|
||||
}
|
||||
},
|
||||
{ contextual: true, fallback: true },
|
||||
);
|
||||
@@ -20,6 +20,7 @@ import { csharpLanguage } from "./csharp/language.js";
|
||||
import { astroParser } from "./astro/parser.js";
|
||||
import { vueParser } from "./vue/parser.js";
|
||||
import { nixLanguage } from "./nix/language.js";
|
||||
import { hclLanguage } from "./hcl/language.js";
|
||||
import { parser as svelteBaseParser } from "./svelte/parser.js";
|
||||
import { configureNesting, defaultNesting } from "./svelte/nesting.js";
|
||||
|
||||
@@ -82,6 +83,10 @@ const languagesByExtension: Record<string, Language> = {
|
||||
cs: csharpLanguage,
|
||||
// Nix
|
||||
nix: nixLanguage,
|
||||
// Terraform / HCL
|
||||
tf: hclLanguage,
|
||||
tfvars: hclLanguage,
|
||||
hcl: hclLanguage,
|
||||
// Elixir
|
||||
ex: language(elixirParser),
|
||||
exs: language(elixirParser),
|
||||
|
||||
Reference in New Issue
Block a user