initial commit

This commit is contained in:
2026-04-05 16:08:59 +03:00
commit 75ab9bec9f
1117 changed files with 789034 additions and 0 deletions
+442
View File
@@ -0,0 +1,442 @@
import {
autocompletion,
} from "@codemirror/autocomplete";
import type {
CompletionContext,
CompletionResult,
Completion,
} from "@codemirror/autocomplete";
import { syntaxTree } from "@codemirror/language";
import type { Extension } from "@codemirror/state";
// --- dexpr type system ---
export type DexprType =
| "String"
| "Number"
| "Boolean"
| "NumberList"
| "StringList"
| "Object";
export interface FunctionInfo {
name: string;
signature: string;
doc?: string;
}
export interface MethodInfo {
name: string;
signature: string;
doc?: string;
}
export interface FieldInfo {
name: string;
type: DexprType;
}
export interface VariableInfo {
name: string;
type: DexprType;
doc?: string;
fields?: FieldInfo[];
}
/**
* Language metadata — generated by Rust `LanguageInfo::to_json()`,
* extended with host-registered functions/methods/variables.
*/
export interface DexprLanguageInfo {
functions: FunctionInfo[];
methods: Partial<Record<DexprType, MethodInfo[]>>;
variables?: VariableInfo[];
}
// --- Build completions from metadata ---
function funcToCompletion(f: FunctionInfo): Completion {
return {
label: f.name,
type: "function",
detail: f.signature,
info: f.doc,
};
}
function methodToCompletion(m: MethodInfo): Completion {
return {
label: m.name,
type: "method",
detail: m.signature,
info: m.doc,
};
}
function varToCompletion(v: VariableInfo): Completion {
return {
label: v.name,
type: "variable",
detail: v.type,
info: v.doc,
};
}
const KEYWORDS: Completion[] = [
{ label: "if", type: "keyword" },
{ label: "then", type: "keyword" },
{ label: "else", type: "keyword" },
{ label: "end", type: "keyword" },
{ label: "true", type: "keyword", detail: "Boolean" },
{ label: "false", type: "keyword", detail: "Boolean" },
{ label: "in", type: "keyword", detail: "membership test" },
];
// --- Type inference from Lezer tree ---
/**
* Walk the tree to infer variable types from assignments.
* Scans `Assignment` nodes: `VariableName AssignOp expression`
* and determines the type of the right-hand side expression.
*/
function inferVariableTypes(
context: CompletionContext,
knownVars: Map<string, DexprType>
): Map<string, DexprType> {
const types = new Map(knownVars);
const tree = syntaxTree(context.state);
const doc = context.state.doc;
tree.iterate({
enter(node) {
if (node.name !== "Assignment") return;
// First child: VariableName
const varNode = node.node.firstChild;
if (!varNode || varNode.name !== "VariableName") return;
const varName = doc.sliceString(varNode.from, varNode.to);
// Third child (skip AssignOp): expression
const assignOp = varNode.nextSibling;
if (!assignOp) return;
const exprNode = assignOp.nextSibling;
if (!exprNode) return;
const exprType = inferExprType(exprNode, doc, types);
if (exprType) types.set(varName, exprType);
},
});
return types;
}
function inferExprType(
node: { name: string; from: number; to: number; firstChild: any },
doc: { sliceString(from: number, to: number): string },
knownTypes: Map<string, DexprType>
): DexprType | null {
switch (node.name) {
case "String":
return "String";
case "Number":
return "Number";
case "BooleanLiteral":
return "Boolean";
case "VariableName": {
const name = doc.sliceString(node.from, node.to);
return knownTypes.get(name) ?? null;
}
case "MethodCall": {
// Infer return type from method name
const propNode = findChild(node, "PropertyName");
if (!propNode) return null;
const method = doc.sliceString(propNode.from, propNode.to);
return inferMethodReturnType(method);
}
case "BinaryExpression": {
// String + anything = String, Number ops = Number
const first = node.firstChild;
if (first) {
const t = inferExprType(first, doc, knownTypes);
if (t === "String") return "String";
if (t === "Number") return "Number";
}
return null;
}
case "PropertyAccess": {
// Resolve: obj.field → look up field type from config
const objNode = node.firstChild;
const propNode = findChild(node, "PropertyName");
if (!objNode || !propNode) return null;
if (objNode.name === "VariableName") {
const varName = doc.sliceString(objNode.from, objNode.to);
const fieldName = doc.sliceString(propNode.from, propNode.to);
// Look up from objectFieldTypes via the global lookup
// (We use knownTypes to check if root is Object, then check field)
const rootType = knownTypes.get(varName);
if (rootType === "Object") {
// Field type needs to come from config — stored as "varName.fieldName" key
// We can't access objectFieldTypes here, so use the convention
// that knownTypes may contain "varName.fieldName" entries
return knownTypes.get(`${varName}.${fieldName}`) ?? null;
}
}
return null;
}
case "FunctionCall":
case "ParenExpression":
return null;
default:
return null;
}
}
function findChild(
node: { firstChild: any },
name: string
): { name: string; from: number; to: number } | null {
let child = node.firstChild;
while (child) {
if (child.name === name) return child;
child = child.nextSibling;
}
return null;
}
/** Infer return type from known method names */
function inferMethodReturnType(method: string): DexprType | null {
switch (method) {
// String -> String
case "upper":
case "lower":
case "trim":
case "trimStart":
case "trimEnd":
case "replace":
case "charAt":
case "substring":
return "String";
// String -> Boolean
case "contains":
case "startsWith":
case "endsWith":
case "isEmpty":
return "Boolean";
// String -> Number
case "length":
case "len":
case "indexOf":
return "Number";
// String -> StringList
case "split":
return "StringList";
// List -> aggregate
case "sum":
case "avg":
case "min":
case "max":
case "first":
case "last":
return "Number";
// List methods returning lists
case "reverse":
case "sort":
case "slice":
return null; // depends on input type
case "join":
return "String";
default:
return null;
}
}
// --- Autocomplete ---
function dedup(items: Completion[]): Completion[] {
const seen = new Set<string>();
return items.filter((item) => {
if (seen.has(item.label)) return false;
seen.add(item.label);
return true;
});
}
export function dexprCompletion(info: DexprLanguageInfo): Extension {
const functionCompletions = info.functions.map(funcToCompletion);
const variableCompletions = (info.variables ?? []).map(varToCompletion);
// Methods per type
const methodsByType: Record<string, Completion[]> = {};
for (const [type, methods] of Object.entries(info.methods)) {
methodsByType[type] = (methods ?? []).map(methodToCompletion);
}
const allMethods = dedup(
Object.values(methodsByType).flat()
);
const allIdentifiers = [
...KEYWORDS,
...functionCompletions,
...variableCompletions,
];
// Build known variable type map from config
const configVarTypes = new Map<string, DexprType>();
for (const v of info.variables ?? []) {
configVarTypes.set(v.name, v.type);
}
// Build Object field type lookup: "varName.fieldName" → DexprType
// and field completions per Object variable
const objectFieldCompletions = new Map<string, Completion[]>();
for (const v of info.variables ?? []) {
if (v.type === "Object" && v.fields) {
const fieldItems: Completion[] = [];
for (const f of v.fields) {
// Store "customer.name" → "String" in configVarTypes for type inference
configVarTypes.set(`${v.name}.${f.name}`, f.type);
fieldItems.push({
label: f.name,
type: "property",
detail: f.type,
});
}
objectFieldCompletions.set(v.name, fieldItems);
}
}
/**
* Resolve the type of a dotted path expression before the cursor.
* Walks the Lezer tree backwards from a dot position to build
* the full path (e.g. "customer.address") and looks up field types.
*/
function resolveDotPath(
context: CompletionContext,
dotPos: number,
varTypes: Map<string, DexprType>
): { type: DexprType | null; path: string[] } {
const tree = syntaxTree(context.state);
const doc = context.state.doc;
// Collect the chain of identifiers before the dot
// e.g. for "customer.address.|" we want ["customer", "address"]
const path: string[] = [];
let pos = dotPos;
// Walk backwards through PropertyAccess / MethodCall nodes
while (true) {
const nodeAtPos = tree.resolveInner(pos, -1);
if (nodeAtPos.name === "PropertyName") {
path.unshift(doc.sliceString(nodeAtPos.from, nodeAtPos.to));
// Skip backwards past the "." to the expression before it
const dotCharPos = nodeAtPos.from - 1;
if (dotCharPos >= 0 && doc.sliceString(dotCharPos, dotCharPos + 1) === ".") {
pos = dotCharPos;
continue;
}
break;
} else if (nodeAtPos.name === "VariableName") {
path.unshift(doc.sliceString(nodeAtPos.from, nodeAtPos.to));
break;
} else {
break;
}
}
if (path.length === 0) return { type: null, path };
// Resolve the type by walking the path
const rootType = varTypes.get(path[0]) ?? null;
if (path.length === 1) return { type: rootType, path };
// For multi-segment paths, look up field types
// e.g. path=["customer","name"] → look up "customer.name" in varTypes
let currentType = rootType;
for (let i = 1; i < path.length; i++) {
if (currentType !== "Object") {
return { type: currentType, path };
}
// "customer.name" key convention
const key = `${path[i - 1]}.${path[i]}`;
const fieldType = varTypes.get(key) ?? null;
currentType = fieldType;
}
return { type: currentType, path };
}
function completions(context: CompletionContext): CompletionResult | null {
const tree = syntaxTree(context.state);
const node = tree.resolveInner(context.pos, -1);
// Don't complete inside strings or comments
if (
node.name === "String" ||
node.name === "LineComment" ||
node.name === "BlockComment"
)
return null;
// Method/property completion: after "."
const dotMatch = context.matchBefore(/\.\w*/);
if (dotMatch) {
const dotPos = dotMatch.from;
const beforeNode = tree.resolveInner(dotPos, -1);
const varTypes = inferVariableTypes(context, configVarTypes);
if (beforeNode.name === "Number") {
return null; // could be decimal
}
// Try to resolve the full dotted path for type info
const { type: resolvedType, path } = resolveDotPath(context, dotPos, varTypes);
// For literal types, resolve directly
let finalType = resolvedType;
if (!finalType) {
if (beforeNode.name === "String") finalType = "String";
else if (beforeNode.name === "BooleanLiteral") finalType = "Boolean";
}
let options: Completion[];
if (finalType === "Object") {
// Show field names + Object methods
const rootVarName = path[0];
const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
const objMethods = methodsByType["Object"] ?? [];
options = [...fieldItems, ...objMethods];
} else if (finalType) {
options = methodsByType[finalType] ?? allMethods;
} else {
options = allMethods;
}
if (options.length === 0) return null;
return {
from: dotMatch.from + 1,
options,
validFor: /^\w*$/,
};
}
// Identifier/keyword completion
const wordMatch = context.matchBefore(/[a-zA-Z_]\w*/);
if (!wordMatch && !context.explicit) return null;
if (wordMatch && wordMatch.from === wordMatch.to && !context.explicit)
return null;
return {
from: wordMatch?.from ?? context.pos,
options: allIdentifiers,
validFor: /^\w*$/,
};
}
return autocompletion({ override: [completions] });
}
export { KEYWORDS };
+124
View File
@@ -0,0 +1,124 @@
@top Program { statement* }
@skip { space | LineComment | BlockComment }
@external tokens elseIfTokenizer from "./tokens" { elseIf }
@precedence {
member @left,
call,
prefix @right,
power @right,
times @left,
plus @left,
compare @left,
and @left,
or @left
}
statement[@isGroup=Statement] {
IfStatement |
Assignment |
ExprStatement
}
IfStatement {
kw<"if"> expression kw<"then"> statement*
(elseIf expression kw<"then"> statement*)*
(kw<"else"> statement*)?
kw<"end">
}
Assignment {
VariableName AssignOp expression |
PropertyAccess AssignOp expression
}
ExprStatement {
expression
}
expression[@isGroup=Expression] {
VariableName |
Number |
String |
BooleanLiteral |
ParenExpression |
UnaryExpression |
BinaryExpression |
MethodCall |
PropertyAccess |
FunctionCall
}
ParenExpression { "(" expression ")" }
UnaryExpression {
!prefix ("-" | "!") expression
}
BinaryExpression {
expression !or "||" expression |
expression !and "&&" expression |
expression !compare CompareOp expression |
expression !compare kw<"in"> expression |
expression !plus ("+" | !prefix "-") expression |
expression !times ("*" | "/" | "%") expression |
expression !power Power expression
}
MethodCall {
expression !member "." PropertyName ArgList
}
PropertyAccess {
expression !member "." PropertyName
}
FunctionCall {
VariableName !call ArgList
}
ArgList { "(" commaSep<expression>? ")" }
commaSep<expr> { expr ("," expr)* }
PropertyName { identifier }
VariableName { identifier }
kw<term> { @extend[@name={term}]<identifier, term> }
BooleanLiteral { @extend[@name=BooleanLiteral]<identifier, "true" | "false"> }
@tokens {
space { $[ \t\n\r]+ }
LineComment { "//" ![\n]* }
BlockComment { "/*" blockCommentRest }
blockCommentRest { ![*] blockCommentRest | "*" blockCommentAfterStar }
blockCommentAfterStar { "/" | ![/] blockCommentRest }
Number { @digit+ ("." @digit+)? }
String {
'"' (![\\\"\n] | "\\" _)* '"' |
"'" (![\\\'\n] | "\\" _)* "'"
}
identifier { $[a-zA-Z_] $[a-zA-Z0-9_]* }
AssignOp { "=" | "+=" | "-=" | "*=" | "/=" | "%=" }
CompareOp { "==" | "!=" | "<=" | ">=" | "<" | ">" }
Power { "**" }
"+" "-" "*" "/" "%" "!" "." "," "(" ")" "||" "&&"
@precedence { AssignOp, CompareOp }
@precedence { BlockComment, LineComment, "/" }
@precedence { Power, "*" }
}
@detectDelim
+24
View File
@@ -0,0 +1,24 @@
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { tags } from "@lezer/highlight";
import type { Extension } from "@codemirror/state";
export const dexprHighlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: "#7c3aed" },
{ tag: tags.bool, color: "#d97706" },
{ tag: tags.string, color: "#059669" },
{ tag: tags.number, color: "#2563eb" },
{ tag: tags.lineComment, color: "#9ca3af", fontStyle: "italic" },
{ tag: tags.blockComment, color: "#9ca3af", fontStyle: "italic" },
{ tag: tags.operator, color: "#dc2626" },
{ tag: tags.compareOperator, color: "#dc2626" },
{ tag: tags.variableName, color: "#1f2937" },
{ tag: tags.propertyName, color: "#0891b2" },
{ tag: tags.function(tags.variableName), color: "#9333ea" },
{ tag: tags.paren, color: "#6b7280" },
{ tag: tags.separator, color: "#6b7280" },
{ tag: tags.derefOperator, color: "#6b7280" },
]);
export function dexprHighlighting(): Extension {
return syntaxHighlighting(dexprHighlightStyle);
}
+49
View File
@@ -0,0 +1,49 @@
import { LanguageSupport } from "@codemirror/language";
import type { Extension } from "@codemirror/state";
import { dexprLanguage } from "./language";
import { dexprCompletion } from "./completions";
import type { DexprLanguageInfo } from "./completions";
import { dexprHighlighting } from "./highlight";
export interface DexprConfig extends DexprLanguageInfo {
/** Include default syntax highlighting theme (default: true) */
highlighting?: boolean;
}
/**
* All-in-one dexpr language support for CodeMirror 6.
*
* @example
* ```ts
* import { dexpr } from "codemirror-lang-dexpr";
*
* // languageInfo comes from Rust's LanguageInfo::to_json()
* // extended with host-registered functions/methods/variables
* const extensions = [basicSetup, dexpr(languageInfo)];
* ```
*/
export function dexpr(config: DexprConfig): Extension {
const extensions: Extension[] = [
new LanguageSupport(dexprLanguage),
dexprCompletion(config),
];
if (config.highlighting !== false) {
extensions.push(dexprHighlighting());
}
return extensions;
}
// Granular exports
export { dexprLanguage } from "./language";
export { dexprCompletion, KEYWORDS } from "./completions";
export type {
DexprLanguageInfo,
DexprType,
FieldInfo,
FunctionInfo,
MethodInfo,
VariableInfo,
} from "./completions";
export { dexprHighlighting, dexprHighlightStyle } from "./highlight";
+32
View File
@@ -0,0 +1,32 @@
import { LRLanguage } from "@codemirror/language";
import { styleTags, tags } from "@lezer/highlight";
// @ts-ignore - generated parser
import { parser } from "./parser.js";
const dexprHighlighting = styleTags({
"if then else end in elseIf": tags.keyword,
BooleanLiteral: tags.bool,
String: tags.string,
Number: tags.number,
LineComment: tags.lineComment,
BlockComment: tags.blockComment,
"CompareOp AssignOp Power": tags.compareOperator,
'"+" "-" "*" "/" "%" "!" "||" "&&"': tags.operator,
VariableName: tags.variableName,
PropertyName: tags.propertyName,
"FunctionCall/VariableName": tags.function(tags.variableName),
'"(" ")"': tags.paren,
'","': tags.separator,
'"."': tags.derefOperator,
});
export const dexprLanguage = LRLanguage.define({
name: "dexpr",
parser: parser.configure({
props: [dexprHighlighting],
}),
languageData: {
commentTokens: { line: "//", block: { open: "/*", close: "*/" } },
closeBrackets: { brackets: ["(", '"', "'"] },
},
});
+24
View File
@@ -0,0 +1,24 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {elseIfTokenizer} from "./tokens"
const spec_identifier = {__proto__:null,if:11, true:21, false:21, in:43, then:69, else:71, end:73}
export const parser = LRParser.deserialize({
version: 14,
states: "+^Q]QQOOOOQP'#Cb'#CbOOQP'#Ce'#CeOwQQO'#CiO`QQO'#CjO#TQRO'#DTO%bQRO'#D_OOQP'#D_'#D_O%iQRO'#D_OOQP'#D]'#D]OOQP'#DU'#DUQ]QQOOOwQQO'#C`O&eQQO,59TO&lQRO'#D_O(zQRO,59UO`QQO,59XO`QQO,59XO`QQO,59XO`QQO,59XO`QQO,59XO)wQQO,59hO)|QQO'#CzOOQP,59i,59iO`QQO,59mOOQP-E7S-E7SO*TQQO,58zOOQP1G.o1G.oO+oQRO1G.sO+vQRO1G.sO-bQRO1G.sO-iQRO1G.sO.[QRO1G.sOOQP'#Cy'#CyOOQP1G/S1G/SO/[QQO'#D`OOQP,59f,59fO/fQQO,59fO/kQRO1G/XO0bQRO1G.fO0oQQO1G/SOOQP7+$i7+$iOwQQO'#DVO1pQQO,59zOOQP1G/Q1G/QO1xQRO7+$QO2TQRO7+$QOwQQO'#DWOOQP7+$Q7+$QO2bQQO7+$QO2iQQO,59qOOQO-E7T-E7TOOQP-E7U-E7UOOQP<<Gl<<GlO2sQQO<<GlO2zQRO<<GlO3VQQO,59rO2sQQO<<GlO3^QQOAN=WOOQPAN=WAN=WO3^QQOAN=WO3eQRO1G/^OOQPG22rG22rO3rQQOG22rO3yQRO7+$xOOQPLD(^LD(^O)wQQO,59hO5RQQO1G.sO5YQQO1G.sO6[QQO1G.sO6cQQO1G.sO6jQQO1G.sO7QQQO,59UOwQQO,59XOwQQO,59XOwQQO,59XOwQQO,59XOwQQO,59XOwQQO'#Cj",
stateData: "7n~O!OOSPOSQOS~OT[OVVOWVOYQO[RO_SO`SO!QPO~OVVOWVOYQO[RO_!pO`!pO!QPO~O_cOb`OcaOdbOebOfcOgdOhdOidOjdOleO~OTwXVwXWwXYwX[wX`wX{wX!QwXswXtwX|wX~P!`OvhOT!RXV!RXW!RXY!RX_!RX`!RXb!RXc!RXd!RXe!RXf!RXg!RXh!RXi!RXj!RXl!RX{!RX!Q!RXs!RXt!RX|!RX~O[fO~P#zO[!RX~P#zO_!nOb!kOc!lOd!mOe!mOf!nOg!oOh!oOi!oOj!oOl!dO~OZkO~P%pO[fOZ!RX_!RXb!RXc!RXd!RXe!RXf!RXg!RXh!RXi!RXj!RXl!RXT!RXV!RXW!RXY!RX`!RX{!RX!Q!RXr!RXo!RXs!RXt!RX|!RX~Ob^ac^ad^ae^af^ag^ah^ai^aj^a~O_cOleOT^aV^aW^aY^a[^a`^a{^a!Q^as^at^a|^a~P(]O!QqO~OZtO~PwOrwO~P%pO_cOdbOebOfcOgdOhdOidOjdOleOTaiVaiWaiYai[ai`aibai{ai!Qaisaitai|ai~OcaO~P*[Ocai~P*[O_cOgdOhdOidOjdOleOTaiVaiWaiYai[ai`aibaicaidaieai{ai!Qaisaitai|ai~OfcO~P+}Ofai~P+}Obaicaidaieaifaigaihaiiai~O_cOjdOleOTaiVaiWaiYai[ai`ai{ai!Qaisaitai|ai~P-pOozOZ!SX~P%pOZ|O~OTuiVuiWuiYui[ui`ui{ui!Quisuitui|ui~P!`Os!ROt!QO|!PO~P]O[fOZpi_pibpicpidpiepifpigpihpiipijpilpirpiopi~OozOZ!Sa~Os!WOt!VO|!PO~Os!WOt!VO|!PO~P]Ot!VO~P]OZyaoya~P%pOt!]O~P]Os!^Ot!]O|!PO~Or!_O~P%pOt!`O~P]Oszitzi|zi~P]Ot!cO~P]Oszqtzq|zq~P]O_!nOd!mOe!mOf!nOg!oOh!oOi!oOj!oOl!dOZaibairaioai~Oc!lO~P4WOcai~P4WO_!nOg!oOh!oOi!oOj!oOl!dOZaibaicaidaieairaioai~Of!nO~P5aOfai~P5aO_!nOj!oOl!dOZairaioai~P-pO_!nOl!dOZ^ar^ao^a~P(]OvdjgQPQh~",
goto: "'n!TPPPP!UP!dPP#WPPP#W#WPP#WPPPPPPPPP#WP#x$OP$V#WPPP!UP!U$y%e%kPPPP%uP&T'kiXOZw!O!R!W!Z![!^!_!a!bhUOZw!O!R!W!Z![!^!_!a!bu^RS[`abcdfhz!P!k!l!m!n!o!p!_VORSZ[`abcdfhwz!O!P!R!W!Z![!^!_!a!b!k!l!m!n!o!pQreRx!dSgU^RyxtVRS[`abcdfhz!P!k!l!m!n!o!piWOZw!O!R!W!Z![!^!_!a!bQZO[iZ!O!Z![!a!bQ!OwQ!Z!RQ![!WQ!a!^R!b!_Q{sR!T{Q}wS!U}!XR!X!OiYOZw!O!R!W!Z![!^!_!a!bhTOZw!O!R!W!Z![!^!_!a!bQ]RQ_SQj[Ql`QmaQnbQocQpdQsfQvhQ!SzQ!Y!PQ!e!kQ!f!lQ!g!mQ!h!nQ!i!oR!j!pRuf",
nodeNames: "⚠ LineComment BlockComment Program IfStatement if VariableName Number String BooleanLiteral BooleanLiteral ) ( ParenExpression UnaryExpression - ! BinaryExpression || && CompareOp in + * / % Power MethodCall . PropertyName ArgList , PropertyAccess FunctionCall then else end Assignment AssignOp ExprStatement",
maxTerm: 50,
nodeProps: [
["group", -3,4,37,39,"Statement",-10,6,7,8,9,13,14,17,27,32,33,"Expression"],
["openedBy", 11,"("],
["closedBy", 12,")"]
],
skippedNodes: [0,1,2],
repeatNodeCount: 3,
tokenData: "+p~RiXY!pYZ!p]^!ppq!pqr#Rrs#`uv%Svw%awx%lxy'Zyz'`z{'e{|'u|}'}}!O(S!O!P([!P!Q(a!Q![*X!^!_*r!_!`*z!`!a*r!c!}+S#R#S+S#T#o+S#p#q+e~!uS!O~XY!pYZ!p]^!ppq!p~#WP`~!_!`#Z~#`Od~~#cWOY#`Zr#`rs#{s#O#`#O#P$Q#P;'S#`;'S;=`$|<%lO#`~$QOW~~$TRO;'S#`;'S;=`$^;=`O#`~$aXOY#`Zr#`rs#{s#O#`#O#P$Q#P;'S#`;'S;=`$|;=`<%l#`<%lO#`~%PP;=`<%l#`~%XPi~!_!`%[~%aOv~~%dPvw%g~%lOc~~%oWOY%lZw%lwx#{x#O%l#O#P&X#P;'S%l;'S;=`'T<%lO%l~&[RO;'S%l;'S;=`&e;=`O%l~&hXOY%lZw%lwx#{x#O%l#O#P&X#P;'S%l;'S;=`'T;=`<%l%l<%lO%l~'WP;=`<%l%l~'`O[~~'eOZ~~'jQg~z{'p!_!`%[~'uOj~~'zPf~!_!`%[~(SOo~~(XP_~!_!`%[~(aOl~~(fRh~z{(o!P!Q)p!_!`%[~(rTOz(oz{)R{;'S(o;'S;=`)j<%lO(o~)UTO!P(o!P!Q)e!Q;'S(o;'S;=`)j<%lO(o~)jOQ~~)mP;=`<%l(o~)uSP~OY)pZ;'S)p;'S;=`*R<%lO)p~*UP;=`<%l)p~*^QV~!O!P*d!Q![*X~*gP!Q![*j~*oPV~!Q![*j~*wPd~!_!`#Z~+PPv~!_!`#Z~+XS!Q~!Q![+S!c!}+S#R#S+S#T#o+S~+hP#p#q+k~+pOb~",
tokenizers: [elseIfTokenizer, 0],
topRules: {"Program":[0,3]},
specialized: [{term: 48, get: (value) => spec_identifier[value] || -1}],
tokenPrec: 1033
})
+29
View File
@@ -0,0 +1,29 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
elseIf = 44,
LineComment = 1,
BlockComment = 2,
Program = 3,
IfStatement = 4,
_if = 5,
VariableName = 6,
Number = 7,
String = 8,
BooleanLiteral = 10,
ParenExpression = 13,
UnaryExpression = 14,
BinaryExpression = 17,
CompareOp = 20,
_in = 21,
Power = 26,
MethodCall = 27,
PropertyName = 29,
ArgList = 30,
PropertyAccess = 32,
FunctionCall = 33,
then = 34,
_else = 35,
end = 36,
Assignment = 37,
AssignOp = 38,
ExprStatement = 39
+53
View File
@@ -0,0 +1,53 @@
import { ExternalTokenizer } from "@lezer/lr";
// @ts-ignore - generated terms
import { elseIf } from "./parser.terms";
const CH_e = 101,
CH_l = 108,
CH_s = 115,
CH_i = 105,
CH_f = 102,
CH_SPACE = 32,
CH_TAB = 9,
CH_NL = 10,
CH_CR = 13;
/** Matches `else` followed by whitespace then `if` as a single token */
export const elseIfTokenizer = new ExternalTokenizer((input) => {
// Match "else"
if (input.next !== CH_e) return;
if (input.peek(1) !== CH_l) return;
if (input.peek(2) !== CH_s) return;
if (input.peek(3) !== CH_e) return;
// Must have at least one whitespace
let pos = 4;
const ch = input.peek(pos);
if (ch !== CH_SPACE && ch !== CH_TAB && ch !== CH_NL && ch !== CH_CR) return;
// Skip whitespace
while (true) {
const c = input.peek(pos);
if (c === CH_SPACE || c === CH_TAB || c === CH_NL || c === CH_CR) {
pos++;
} else {
break;
}
}
// Match "if"
if (input.peek(pos) !== CH_i) return;
if (input.peek(pos + 1) !== CH_f) return;
// Make sure "if" is not part of a longer identifier
const after = input.peek(pos + 2);
if (
(after >= 97 && after <= 122) || // a-z
(after >= 65 && after <= 90) || // A-Z
(after >= 48 && after <= 57) || // 0-9
after === 95 // _
)
return;
input.acceptToken(elseIf, pos + 2);
});