function inside function
This commit is contained in:
@@ -11,7 +11,7 @@ const DOT_MARKER_R = 1.5
|
|||||||
|
|
||||||
function getEdgeLabelByTargetType(targetType: string | undefined): string | undefined {
|
function getEdgeLabelByTargetType(targetType: string | undefined): string | undefined {
|
||||||
if (targetType === 'render') return 'render'
|
if (targetType === 'render') return 'render'
|
||||||
if (targetType === 'config') return 'add input'
|
if (targetType === 'config' || targetType === 'function') return 'add input'
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ export function AnimatedEdge({
|
|||||||
r={DOT_MARKER_R}
|
r={DOT_MARKER_R}
|
||||||
cx={DOT_MARKER_R}
|
cx={DOT_MARKER_R}
|
||||||
cy={DOT_MARKER_R}
|
cy={DOT_MARKER_R}
|
||||||
className="fill-primary stroke-background"
|
className="fill-primary"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
/>
|
/>
|
||||||
</marker>
|
</marker>
|
||||||
@@ -80,7 +80,7 @@ export function AnimatedEdge({
|
|||||||
r={DOT_MARKER_R}
|
r={DOT_MARKER_R}
|
||||||
cx={DOT_MARKER_R}
|
cx={DOT_MARKER_R}
|
||||||
cy={DOT_MARKER_R}
|
cy={DOT_MARKER_R}
|
||||||
className="fill-primary stroke-background"
|
className="fill-primary"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
/>
|
/>
|
||||||
</marker>
|
</marker>
|
||||||
|
|||||||
@@ -39,7 +39,13 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
|
|||||||
() => (nodes as any[]).filter((n: any) => incomingIds.includes(n.id) && n.type === 'variable'),
|
() => (nodes as any[]).filter((n: any) => incomingIds.includes(n.id) && n.type === 'variable'),
|
||||||
[nodes, incomingIds]
|
[nodes, incomingIds]
|
||||||
)
|
)
|
||||||
|
const connectedFunctionNodes = useMemo(
|
||||||
|
() => (nodes as any[]).filter((n: any) => incomingIds.includes(n.id) && n.type === 'function'),
|
||||||
|
[nodes, incomingIds]
|
||||||
|
)
|
||||||
const hasConnectedVariables = connectedVariableNodes.length > 0
|
const hasConnectedVariables = connectedVariableNodes.length > 0
|
||||||
|
const hasConnectedFunctions = connectedFunctionNodes.length > 0
|
||||||
|
const hasConnectedInputs = hasConnectedVariables || hasConnectedFunctions
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(val: string) => {
|
(val: string) => {
|
||||||
@@ -80,6 +86,13 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
|
|||||||
[insertAt]
|
[insertAt]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const insertFunctionAtCursor = useCallback(
|
||||||
|
(functionNode: any) => {
|
||||||
|
insertAt(functionNode.id, 'cursor')
|
||||||
|
},
|
||||||
|
[insertAt]
|
||||||
|
)
|
||||||
|
|
||||||
const extensions = useMemo(() => [javascript()], [])
|
const extensions = useMemo(() => [javascript()], [])
|
||||||
const [editorHeight, editorContainerRef] = useResizeHeight(120)
|
const [editorHeight, editorContainerRef] = useResizeHeight(120)
|
||||||
const dimensions =
|
const dimensions =
|
||||||
@@ -97,7 +110,7 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
|
|||||||
nodeId={id}
|
nodeId={id}
|
||||||
nodeType="function"
|
nodeType="function"
|
||||||
editInputsContent={
|
editInputsContent={
|
||||||
hasConnectedVariables ? (
|
hasConnectedInputs ? (
|
||||||
<>
|
<>
|
||||||
{connectedVariableNodes.map((n: any) => (
|
{connectedVariableNodes.map((n: any) => (
|
||||||
<MenubarSub key={n.id}>
|
<MenubarSub key={n.id}>
|
||||||
@@ -115,6 +128,22 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
|
|||||||
</MenubarSubContent>
|
</MenubarSubContent>
|
||||||
</MenubarSub>
|
</MenubarSub>
|
||||||
))}
|
))}
|
||||||
|
{connectedFunctionNodes.map((n: any) => (
|
||||||
|
<MenubarSub key={n.id}>
|
||||||
|
<MenubarSubTrigger className="text-xs flex items-center gap-2">
|
||||||
|
<Code2 className="size-3.5 shrink-0" />
|
||||||
|
{n.id}
|
||||||
|
</MenubarSubTrigger>
|
||||||
|
<MenubarSubContent>
|
||||||
|
<MenubarItem
|
||||||
|
className="text-xs"
|
||||||
|
onClick={() => insertFunctionAtCursor(n)}
|
||||||
|
>
|
||||||
|
Insert at cursor
|
||||||
|
</MenubarItem>
|
||||||
|
</MenubarSubContent>
|
||||||
|
</MenubarSub>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,8 +247,24 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }:
|
|||||||
const src = nodes.find((n: any) => n.id === e.source)
|
const src = nodes.find((n: any) => n.id === e.source)
|
||||||
if (src?.type === 'variable') setVarInContext(src)
|
if (src?.type === 'variable') setVarInContext(src)
|
||||||
}
|
}
|
||||||
|
// All function node ids that feed (directly or transitively) into config — need to register them and collect their variables
|
||||||
|
const functionIdsToRegister = new Set<string>()
|
||||||
|
let added = true
|
||||||
|
while (added) {
|
||||||
|
added = false
|
||||||
|
for (const e of edges) {
|
||||||
|
const src = nodes.find((n: any) => n.id === e.source)
|
||||||
|
if (src?.type !== 'function') continue
|
||||||
|
const targetInScope = configIdsUsed.has(e.target) || functionIdsToRegister.has(e.target)
|
||||||
|
if (!targetInScope) continue
|
||||||
|
if (!functionIdsToRegister.has(src.id)) {
|
||||||
|
functionIdsToRegister.add(src.id)
|
||||||
|
added = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const e of edges) {
|
for (const e of edges) {
|
||||||
if (!configIdsUsed.has(e.target)) continue
|
if (!configIdsUsed.has(e.target) && !functionIdsToRegister.has(e.target)) continue
|
||||||
const src = nodes.find((n: any) => n.id === e.source)
|
const src = nodes.find((n: any) => n.id === e.source)
|
||||||
if (src?.type === 'function') {
|
if (src?.type === 'function') {
|
||||||
for (const e2 of edges) {
|
for (const e2 of edges) {
|
||||||
@@ -292,65 +308,90 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }:
|
|||||||
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
||||||
typeof v === 'object' && v !== null && !Array.isArray(v)
|
typeof v === 'object' && v !== null && !Array.isArray(v)
|
||||||
|
|
||||||
// For each function node that feeds a config: which variable node ids are connected to that function?
|
// For each registered function: which variable/function node ids are connected to it?
|
||||||
const functionConnectedVariableIds = Object.create(null) as Record<string, string[]>
|
const functionConnectedVariableIds = Object.create(null) as Record<string, string[]>
|
||||||
for (const e of edges) {
|
const functionConnectedFunctionIds = Object.create(null) as Record<string, string[]>
|
||||||
if (!configIdsUsed.has(e.target)) continue
|
for (const fid of functionIdsToRegister) {
|
||||||
const src = nodes.find((n: any) => n.id === e.source)
|
for (const e of edges) {
|
||||||
if (src?.type === 'function') {
|
if (e.target !== fid) continue
|
||||||
const fid = src.id
|
const src = nodes.find((n: any) => n.id === e.source)
|
||||||
for (const e2 of edges) {
|
if (src?.type === 'variable') {
|
||||||
if (e2.target !== fid) continue
|
if (!functionConnectedVariableIds[fid]) functionConnectedVariableIds[fid] = []
|
||||||
const vNode = nodes.find((n: any) => n.id === e2.source)
|
functionConnectedVariableIds[fid].push(src.id)
|
||||||
if (vNode?.type === 'variable') {
|
} else if (src?.type === 'function') {
|
||||||
if (!functionConnectedVariableIds[fid]) functionConnectedVariableIds[fid] = []
|
if (!functionConnectedFunctionIds[fid]) functionConnectedFunctionIds[fid] = []
|
||||||
functionConnectedVariableIds[fid].push(vNode.id)
|
functionConnectedFunctionIds[fid].push(src.id)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const e of edges) {
|
for (const fid of functionIdsToRegister) {
|
||||||
if (!configIdsUsed.has(e.target)) continue
|
const src = nodes.find((n: any) => n.id === fid)
|
||||||
const src = nodes.find((n: any) => n.id === e.source)
|
if (!src || src.type !== 'function') continue
|
||||||
if (src?.type === 'function') {
|
const body = src.data?.body ?? 'return args[0];'
|
||||||
const body = src.data?.body ?? 'return args[0];'
|
const parsed = parseFunctionSignature(body)
|
||||||
const parsed = parseFunctionSignature(body)
|
const connectedVarIds = new Set<string>(functionConnectedVariableIds[fid] ?? [])
|
||||||
const connectedVarIds = new Set<string>(functionConnectedVariableIds[src.id] ?? [])
|
const connectedFuncIds = functionConnectedFunctionIds[fid] ?? []
|
||||||
env.addFilter(
|
env.addFilter(
|
||||||
src.id,
|
src.id,
|
||||||
(value: unknown, ...args: unknown[]) => {
|
(value: unknown, ...args: unknown[]) => {
|
||||||
const callback = args[args.length - 1] as (err: Error | null, res: string) => void
|
const callback = args[args.length - 1] as (err: Error | null, res: string) => void
|
||||||
const raw = [value, ...args.slice(0, -1)]
|
const raw = [value, ...args.slice(0, -1)]
|
||||||
const hasKwargs = raw.length > 0 && isPlainObject(raw[raw.length - 1])
|
const hasKwargs = raw.length > 0 && isPlainObject(raw[raw.length - 1])
|
||||||
const positionals = hasKwargs ? raw.slice(0, -1) : raw
|
const positionals = hasKwargs ? raw.slice(0, -1) : raw
|
||||||
const kwargs = hasKwargs ? (raw[raw.length - 1] as Record<string, unknown>) : Object.create(null)
|
const kwargs = hasKwargs ? (raw[raw.length - 1] as Record<string, unknown>) : Object.create(null)
|
||||||
|
|
||||||
let invoke: () => unknown
|
// Cache for nested filter results so sync-looking code like `num + fn_003(4)` works:
|
||||||
if (parsed) {
|
// callable throws Suspend when result isn't ready; we await, cache, then re-run.
|
||||||
const { paramNames, innerBody } = parsed
|
// Cached values are strings (Nunjucks); coerce to number when numeric so 2 + fn_003(4) => 10 not "28".
|
||||||
const lastParam = paramNames[paramNames.length - 1]
|
const nestedCache = new Map<string, string>()
|
||||||
const invocationArgs = paramNames.map((name, i) => {
|
const coerceCached = (s: string): string | number => {
|
||||||
if (name === lastParam && lastParam === 'kwargs') return kwargs
|
const n = Number(s)
|
||||||
// Connected variables are available as constants: use variable value from context
|
return s.trim() !== '' && !Number.isNaN(n) ? n : s
|
||||||
if (connectedVarIds.has(name) && name in nunjucksContext)
|
}
|
||||||
return nunjucksContext[name]
|
const makeCallable = (filterId: string) => (input: unknown) => {
|
||||||
return positionals[i]
|
const key = `${filterId}::${JSON.stringify(input)}`
|
||||||
})
|
if (nestedCache.has(key)) return coerceCached(nestedCache.get(key)!)
|
||||||
// Inject connected variables as extra params so they're in scope in the body (e.g. (value) => value + var_001)
|
const p = new Promise<string>((resolve, reject) => {
|
||||||
const extraVarIds = [...connectedVarIds].filter((vid) => !paramNames.includes(vid))
|
env.getFilter(filterId)(input, (err: Error | null, res: string) =>
|
||||||
const allParamNames = [...paramNames, ...extraVarIds]
|
err ? reject(err) : resolve(res)
|
||||||
const allArgs = [...invocationArgs, ...extraVarIds.map((vid) => nunjucksContext[vid])]
|
)
|
||||||
const fn = new Function(...allParamNames, innerBody)
|
})
|
||||||
invoke = () => fn(...allArgs)
|
p.then((res) => nestedCache.set(key, res))
|
||||||
} else {
|
const suspend = { __suspend: true as const, promise: p, key }
|
||||||
const fn = new Function('args', body)
|
throw suspend
|
||||||
invoke = () => fn(positionals)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const done = (err: Error | null, res: string) => {
|
let invoke: () => unknown
|
||||||
callback(err, res)
|
if (parsed) {
|
||||||
}
|
const { paramNames, innerBody } = parsed
|
||||||
|
const lastParam = paramNames[paramNames.length - 1]
|
||||||
|
const invocationArgs = paramNames.map((name, i) => {
|
||||||
|
if (name === lastParam && lastParam === 'kwargs') return kwargs
|
||||||
|
if (connectedVarIds.has(name) && name in nunjucksContext)
|
||||||
|
return nunjucksContext[name]
|
||||||
|
if (connectedFuncIds.includes(name)) return makeCallable(name)
|
||||||
|
return positionals[i]
|
||||||
|
})
|
||||||
|
const extraVarIds = [...connectedVarIds].filter((vid) => !paramNames.includes(vid))
|
||||||
|
const extraFuncIds = connectedFuncIds.filter((fid2) => !paramNames.includes(fid2))
|
||||||
|
const allParamNames = [...paramNames, ...extraVarIds, ...extraFuncIds]
|
||||||
|
const allArgs = [
|
||||||
|
...invocationArgs,
|
||||||
|
...extraVarIds.map((vid) => nunjucksContext[vid]),
|
||||||
|
...extraFuncIds.map((fid2) => makeCallable(fid2)),
|
||||||
|
]
|
||||||
|
const fn = new Function(...allParamNames, innerBody)
|
||||||
|
invoke = () => fn(...allArgs)
|
||||||
|
} else {
|
||||||
|
const fn = new Function('args', body)
|
||||||
|
invoke = () => fn(positionals)
|
||||||
|
}
|
||||||
|
|
||||||
|
const done = (err: Error | null, res: string) => {
|
||||||
|
callback(err, res)
|
||||||
|
}
|
||||||
|
const runInvoke = () => {
|
||||||
try {
|
try {
|
||||||
const result = invoke()
|
const result = invoke()
|
||||||
if (result != null && typeof (result as Promise<unknown>).then === 'function') {
|
if (result != null && typeof (result as Promise<unknown>).then === 'function') {
|
||||||
@@ -361,13 +402,21 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }:
|
|||||||
} else {
|
} else {
|
||||||
done(null, formatFilterResult(result))
|
done(null, formatFilterResult(result))
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (e: unknown) {
|
||||||
done(err instanceof Error ? err : new Error(String(err)), '')
|
const s = e as { __suspend?: boolean; promise?: Promise<string>; key?: string }
|
||||||
|
if (s?.__suspend && s.promise) {
|
||||||
|
s.promise.then(() => runInvoke(), (err) =>
|
||||||
|
done(err instanceof Error ? err : new Error(String(err)), '')
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
done(e instanceof Error ? e : new Error(String(e)), '')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
true
|
runInvoke()
|
||||||
)
|
},
|
||||||
}
|
true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
env.render(srcId!, nunjucksContext, async (nunjucksErr: Error | null, afterNunjucks: string) => {
|
env.render(srcId!, nunjucksContext, async (nunjucksErr: Error | null, afterNunjucks: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user