mirror of
https://github.com/duhanbalci/dexpr.git
synced 2026-08-30 16:01:41 +00:00
initial commit
This commit is contained in:
@@ -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 };
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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: ["(", '"', "'"] },
|
||||
},
|
||||
});
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user