From 5aa01f4dec507739500b9143d1922ddc10fea7f7 Mon Sep 17 00:00:00 2001 From: dtoro Date: Sat, 7 Mar 2026 15:20:48 +0100 Subject: [PATCH] vars --- src/components/graph/ConfigNode.tsx | 4 +-- src/components/graph/RenderingNode.tsx | 35 ++++---------------------- src/lib/nunjucksAutocomplete.ts | 24 +++++++++++------- 3 files changed, 22 insertions(+), 41 deletions(-) diff --git a/src/components/graph/ConfigNode.tsx b/src/components/graph/ConfigNode.tsx index fb94239..233d947 100644 --- a/src/components/graph/ConfigNode.tsx +++ b/src/components/graph/ConfigNode.tsx @@ -106,14 +106,14 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: const insertVariableReference = useCallback( (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => { - insertAt(`\${${sourceNode.id}}`, mode) + insertAt(`{{ ${sourceNode.id} }}`, mode) }, [insertAt] ) const insertFunctionCall = useCallback( (sourceNode: any, mode: 'prepend' | 'append' | 'cursor') => { - insertAt(`\${${sourceNode.id}()}`, mode) + insertAt(`{{ ${sourceNode.id}() }}`, mode) }, [insertAt] ) diff --git a/src/components/graph/RenderingNode.tsx b/src/components/graph/RenderingNode.tsx index a9681ed..c254e02 100644 --- a/src/components/graph/RenderingNode.tsx +++ b/src/components/graph/RenderingNode.tsx @@ -191,8 +191,8 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: const resolvedIncludes = resolveIncludes(plantumlText) - const varMap: Record = {} - // Use null prototype so keys like "var", "constructor", "toString" don't collide with Object.prototype + // Use null prototype so keys like "var", "constructor", "toString" don't collide with Object.prototype. + // Only Nunjucks {{ var }} / {% if var %} are supported; context is built from connected variable/function nodes. const nunjucksContext = Object.create(null) as Record for (const e of edges) { @@ -201,7 +201,6 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: if (src?.type === 'variable') { const v = src.data?.value const str = v === undefined || v === null ? '' : String(v) - varMap[src.id] = str // Keep booleans/numbers for {% if %} etc.; Nunjucks treats "false" as truthy nunjucksContext[src.id] = v === undefined || v === null ? '' : typeof v === 'boolean' || typeof v === 'number' ? v : str @@ -229,33 +228,9 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: throw new Error(`Nunjucks: ${nunjucksErr?.message ?? String(nunjucksErr)}`) } - const resolveFunctionCalls = (text: string): string => { - const fnCallRegex = /\$\{([\w-]+)\s*\(([^)]*)\)\}/g - return text.replace(fnCallRegex, (match, funcId: string, argsStr: string) => { - const fnNode = nodes.find((n: any) => n.id === funcId && n.type === 'function') - if (!fnNode) return match - const body = fnNode.data?.body ?? 'return args[0];' - const argIds = argsStr.split(',').map((s: string) => s.trim()).filter(Boolean) - const argValues = argIds.map((argId: string) => varMap[argId] ?? '') - try { - const fn = new Function('args', body) - const result = fn(argValues) - if (result === undefined || result === null) return '' - if (typeof result === 'string' || typeof result === 'number' || typeof result === 'boolean') return String(result) - return String(result) - } catch { - return match - } - }) - } - - const resolveVariables = (text: string): string => - text.replace(/\$\{([^}]+)\}/g, (_, varId: string) => varMap[varId.trim()] ?? '') - - const afterFunctions = resolveFunctionCalls(afterNunjucks) - let resolvedWithVars = resolveVariables(afterFunctions) + let resolvedPlantuml = afterNunjucks if (theme === 'dark') { - resolvedWithVars = resolvedWithVars.replace( + resolvedPlantuml = resolvedPlantuml.replace( /^(\s*@startuml\s*\n)/i, `$1${DARK_SKINPARAMS}\n` ) @@ -264,7 +239,7 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: const res = await fetch(KROKI_PLANTUML_SVG, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, - body: resolvedWithVars, + body: resolvedPlantuml, }) if (cancelled || thisRunId !== runIdRef.current) return diff --git a/src/lib/nunjucksAutocomplete.ts b/src/lib/nunjucksAutocomplete.ts index 1bd6515..2d13581 100644 --- a/src/lib/nunjucksAutocomplete.ts +++ b/src/lib/nunjucksAutocomplete.ts @@ -1,4 +1,5 @@ import { CompletionContext, CompletionResult } from '@codemirror/autocomplete' +import type { EditorState } from '@codemirror/state' const NUNJUCKS_KEYWORDS = [ 'if', 'endif', 'elif', 'else', 'for', 'endfor', 'in', 'and', 'or', 'not', @@ -12,10 +13,15 @@ const NUNJUCKS_FILTERS = [ 'trim', 'escape', 'safe', 'striptags', 'capitalize', 'reverse', 'batch', 'slice', ] +/** Get line text (CodeMirror 6: doc.line(n) is 1-based) */ +function getLineText(state: EditorState, lineNo0Based: number): string { + return state.doc.line(lineNo0Based + 1).text +} + /** Detect if position is inside {{ or {% from the start of the line */ -function insideNunjucks(doc: { getLine: (n: number) => string }, lineNo: number, pos: number): boolean { - const line = doc.getLine(lineNo) - const before = line.slice(0, pos) +function insideNunjucks(state: EditorState, lineNo0Based: number, posInLine: number): boolean { + const line = getLineText(state, lineNo0Based) + const before = line.slice(0, posInLine) const openVar = before.lastIndexOf('{{') const openTag = before.lastIndexOf('{%') const closeVar = before.lastIndexOf('}}') @@ -26,11 +32,11 @@ function insideNunjucks(doc: { getLine: (n: number) => string }, lineNo: number, } /** Get the word fragment before the cursor for matching */ -function wordBefore(doc: { getLine: (n: number) => string }, lineNo: number, pos: number): string { - const line = doc.getLine(lineNo) - let start = pos +function wordBefore(state: EditorState, lineNo0Based: number, posInLine: number): string { + const line = getLineText(state, lineNo0Based) + let start = posInLine while (start > 0 && /[\w.-]/.test(line[start - 1])) start -= 1 - return line.slice(start, pos) + return line.slice(start, posInLine) } export function nunjucksCompletionSource( @@ -41,9 +47,9 @@ export function nunjucksCompletionSource( return (context: CompletionContext) => { const { state, pos } = context const line = state.doc.lineAt(pos) - if (!insideNunjucks(state.doc, line.number - 1, pos - line.from)) return null + if (!insideNunjucks(state, line.number - 1, pos - line.from)) return null - const word = wordBefore(state.doc, line.number - 1, pos - line.from) + const word = wordBefore(state, line.number - 1, pos - line.from) const from = pos - word.length const options: { label: string; type?: string; info?: string }[] = []