// src/index.ts import { LanguageSupport } from "@codemirror/language"; // src/language.ts import { LRLanguage } from "@codemirror/language"; import { styleTags, tags } from "@lezer/highlight"; // src/parser.js import { LRParser } from "@lezer/lr"; // src/tokens.ts import { ExternalTokenizer } from "@lezer/lr"; // src/parser.terms.js var elseIf = 44; // src/tokens.ts var CH_e = 101; var CH_l = 108; var CH_s = 115; var CH_i = 105; var CH_f = 102; var CH_SPACE = 32; var CH_TAB = 9; var CH_NL = 10; var CH_CR = 13; var elseIfTokenizer = new ExternalTokenizer((input) => { 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; let pos = 4; const ch = input.peek(pos); if (ch !== CH_SPACE && ch !== CH_TAB && ch !== CH_NL && ch !== CH_CR) return; while (true) { const c = input.peek(pos); if (c === CH_SPACE || c === CH_TAB || c === CH_NL || c === CH_CR) { pos++; } else { break; } } if (input.peek(pos) !== CH_i) return; if (input.peek(pos + 1) !== CH_f) return; 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); }); // src/parser.js var spec_identifier = { __proto__: null, if: 11, true: 21, false: 21, in: 43, then: 69, else: 71, end: 73 }; var 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< spec_identifier[value] || -1 }], tokenPrec: 1033 }); // src/language.ts var 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 }); var dexprLanguage = LRLanguage.define({ name: "dexpr", parser: parser.configure({ props: [dexprHighlighting] }), languageData: { commentTokens: { line: "//", block: { open: "/*", close: "*/" } }, closeBrackets: { brackets: ["(", '"', "'"] } } }); // src/completions.ts import { autocompletion } from "@codemirror/autocomplete"; import { syntaxTree } from "@codemirror/language"; function funcToCompletion(f) { return { label: f.name, type: "function", detail: f.signature, info: f.doc }; } function methodToCompletion(m) { return { label: m.name, type: "method", detail: m.signature, info: m.doc }; } function varToCompletion(v) { return { label: v.name, type: "variable", detail: v.type, info: v.doc }; } var KEYWORDS = [ { 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" } ]; function inferVariableTypes(context, knownVars) { const types = new Map(knownVars); const tree = syntaxTree(context.state); const doc = context.state.doc; tree.iterate({ enter(node) { if (node.name !== "Assignment") return; const varNode = node.node.firstChild; if (!varNode || varNode.name !== "VariableName") return; const varName = doc.sliceString(varNode.from, varNode.to); 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, doc, knownTypes) { 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": { const propNode = findChild(node, "PropertyName"); if (!propNode) return null; const method = doc.sliceString(propNode.from, propNode.to); return inferMethodReturnType(method); } case "BinaryExpression": { 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": { 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); const rootType = knownTypes.get(varName); if (rootType === "Object") { return knownTypes.get(`${varName}.${fieldName}`) ?? null; } } return null; } case "FunctionCall": case "ParenExpression": return null; default: return null; } } function findChild(node, name) { let child = node.firstChild; while (child) { if (child.name === name) return child; child = child.nextSibling; } return null; } function inferMethodReturnType(method) { 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"; // List methods case "map": return null; // depends on field type (NumberList, StringList, or List) case "filter": return "List"; case "find": return null; // returns single element default: return null; } } function dedup(items) { const seen = /* @__PURE__ */ new Set(); return items.filter((item) => { if (seen.has(item.label)) return false; seen.add(item.label); return true; }); } function dexprCompletion(info) { const functionCompletions = info.functions.map(funcToCompletion); const variableCompletions = (info.variables ?? []).map(varToCompletion); const methodsByType = {}; 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 ]; const configVarTypes = /* @__PURE__ */ new Map(); for (const v of info.variables ?? []) { configVarTypes.set(v.name, v.type); } const objectFieldCompletions = /* @__PURE__ */ new Map(); for (const v of info.variables ?? []) { if ((v.type === "Object" || v.type === "List") && v.fields) { const fieldItems = []; for (const f of v.fields) { configVarTypes.set(`${v.name}.${f.name}`, f.type); fieldItems.push({ label: f.name, type: "property", detail: f.type }); } objectFieldCompletions.set(v.name, fieldItems); } } function resolveDotPath(context, dotPos, varTypes) { const tree = syntaxTree(context.state); const doc = context.state.doc; const path = []; let pos = dotPos; while (true) { const nodeAtPos = tree.resolveInner(pos, -1); if (nodeAtPos.name === "PropertyName") { path.unshift(doc.sliceString(nodeAtPos.from, nodeAtPos.to)); 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 }; const rootType = varTypes.get(path[0]) ?? null; if (path.length === 1) return { type: rootType, path }; let currentType = rootType; for (let i = 1; i < path.length; i++) { if (currentType !== "Object") { return { type: currentType, path }; } const key = `${path[i - 1]}.${path[i]}`; const fieldType = varTypes.get(key) ?? null; currentType = fieldType; } return { type: currentType, path }; } function completions(context) { const tree = syntaxTree(context.state); const node = tree.resolveInner(context.pos, -1); if (node.name === "String" || node.name === "LineComment" || node.name === "BlockComment") return null; 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; } const { type: resolvedType, path } = resolveDotPath(context, dotPos, varTypes); let finalType = resolvedType; if (!finalType) { if (beforeNode.name === "String") finalType = "String"; else if (beforeNode.name === "BooleanLiteral") finalType = "Boolean"; } let options; if (finalType === "Object") { const rootVarName = path[0]; const fieldItems = objectFieldCompletions.get(rootVarName) ?? []; const objMethods = methodsByType["Object"] ?? []; options = [...fieldItems, ...objMethods]; } else if (finalType === "List") { const rootVarName = path[0]; const listMethods = methodsByType["List"] ?? []; options = [...listMethods]; } else if (finalType) { options = methodsByType[finalType] ?? allMethods; } else { options = allMethods; } if (options.length === 0) return null; return { from: dotMatch.from + 1, options, validFor: /^\w*$/ }; } 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] }); } // src/highlight.ts import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { tags as tags2 } from "@lezer/highlight"; var dexprHighlightStyle = HighlightStyle.define([ { tag: tags2.keyword, color: "#7c3aed" }, { tag: tags2.bool, color: "#d97706" }, { tag: tags2.string, color: "#059669" }, { tag: tags2.number, color: "#2563eb" }, { tag: tags2.lineComment, color: "#9ca3af", fontStyle: "italic" }, { tag: tags2.blockComment, color: "#9ca3af", fontStyle: "italic" }, { tag: tags2.operator, color: "#dc2626" }, { tag: tags2.compareOperator, color: "#dc2626" }, { tag: tags2.variableName, color: "#1f2937" }, { tag: tags2.propertyName, color: "#0891b2" }, { tag: tags2.function(tags2.variableName), color: "#9333ea" }, { tag: tags2.paren, color: "#6b7280" }, { tag: tags2.separator, color: "#6b7280" }, { tag: tags2.derefOperator, color: "#6b7280" } ]); function dexprHighlighting2() { return syntaxHighlighting(dexprHighlightStyle); } // src/index.ts function dexpr(config) { const extensions = [ new LanguageSupport(dexprLanguage), dexprCompletion(config) ]; if (config.highlighting !== false) { extensions.push(dexprHighlighting2()); } return extensions; } export { KEYWORDS, dexpr, dexprCompletion, dexprHighlightStyle, dexprHighlighting2 as dexprHighlighting, dexprLanguage };