@rohal12/spindle 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -1,9 +1,11 @@
1
+ import { useContext } from 'preact/hooks';
1
2
  import { useStoryStore } from '../../store';
2
3
  import { execute } from '../../expression';
3
- import { renderInlineNodes } from '../../markup/render';
4
+ import { renderInlineNodes, LocalsContext } from '../../markup/render';
4
5
  import { deepClone } from '../../class-registry';
5
6
  import { collectText } from '../../utils/extract-text';
6
7
  import { useAction } from '../../hooks/use-action';
8
+ import { useMergedLocals } from '../../hooks/use-merged-locals';
7
9
  import type { ASTNode } from '../../markup/ast';
8
10
 
9
11
  interface ButtonProps {
@@ -14,13 +16,17 @@ interface ButtonProps {
14
16
  }
15
17
 
16
18
  export function Button({ rawArgs, children, className, id }: ButtonProps) {
19
+ const scope = useContext(LocalsContext);
20
+ const [, , mergedLocals] = useMergedLocals();
21
+
17
22
  const handleClick = () => {
18
23
  const state = useStoryStore.getState();
19
24
  const vars = deepClone(state.variables);
20
25
  const temps = deepClone(state.temporary);
26
+ const localsClone = { ...mergedLocals };
21
27
 
22
28
  try {
23
- execute(rawArgs, vars, temps);
29
+ execute(rawArgs, vars, temps, localsClone);
24
30
  } catch (err) {
25
31
  console.error(`spindle: Error in {button ${rawArgs}}:`, err);
26
32
  return;
@@ -36,6 +42,11 @@ export function Button({ rawArgs, children, className, id }: ButtonProps) {
36
42
  state.setTemporary(key, temps[key]);
37
43
  }
38
44
  }
45
+ for (const key of Object.keys(localsClone)) {
46
+ if (localsClone[key] !== mergedLocals[key]) {
47
+ scope.update(`@${key}`, localsClone[key]);
48
+ }
49
+ }
39
50
  };
40
51
 
41
52
  useAction({
@@ -59,7 +59,7 @@ function valuesEqual(a: unknown, b: unknown): boolean {
59
59
  }
60
60
 
61
61
  export function Computed({ rawArgs }: ComputedProps) {
62
- const [mergedVars, mergedTemps] = useMergedLocals();
62
+ const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
63
63
 
64
64
  let target: string;
65
65
  let expr: string;
@@ -84,7 +84,7 @@ export function Computed({ rawArgs }: ComputedProps) {
84
84
 
85
85
  let newValue: unknown;
86
86
  try {
87
- newValue = evaluate(expr, mergedVars, mergedTemps);
87
+ newValue = evaluate(expr, mergedVars, mergedTemps, mergedLocals);
88
88
  } catch (err) {
89
89
  console.error(`spindle: Error in {computed ${rawArgs}}:`, err);
90
90
  return;
@@ -1,8 +1,10 @@
1
- import { useLayoutEffect } from 'preact/hooks';
1
+ import { useLayoutEffect, useContext } from 'preact/hooks';
2
2
  import { useStoryStore } from '../../store';
3
3
  import { execute } from '../../expression';
4
4
  import type { ASTNode } from '../../markup/ast';
5
5
  import { deepClone } from '../../class-registry';
6
+ import { LocalsContext } from '../../markup/render';
7
+ import { useMergedLocals } from '../../hooks/use-merged-locals';
6
8
 
7
9
  interface DoProps {
8
10
  children: ASTNode[];
@@ -17,14 +19,17 @@ function collectText(nodes: ASTNode[]): string {
17
19
 
18
20
  export function Do({ children }: DoProps) {
19
21
  const code = collectText(children);
22
+ const scope = useContext(LocalsContext);
23
+ const [, , mergedLocals] = useMergedLocals();
20
24
 
21
25
  useLayoutEffect(() => {
22
26
  const state = useStoryStore.getState();
23
27
  const vars = deepClone(state.variables);
24
28
  const temps = deepClone(state.temporary);
29
+ const localsClone = { ...mergedLocals };
25
30
 
26
31
  try {
27
- execute(code, vars, temps);
32
+ execute(code, vars, temps, localsClone);
28
33
  } catch (err) {
29
34
  console.error(`spindle: Error in {do}:`, err);
30
35
  return;
@@ -41,6 +46,11 @@ export function Do({ children }: DoProps) {
41
46
  state.setTemporary(key, temps[key]);
42
47
  }
43
48
  }
49
+ for (const key of Object.keys(localsClone)) {
50
+ if (localsClone[key] !== mergedLocals[key]) {
51
+ scope.update(`@${key}`, localsClone[key]);
52
+ }
53
+ }
44
54
  }, []);
45
55
 
46
56
  return null;
@@ -1,6 +1,7 @@
1
- import { useContext } from 'preact/hooks';
1
+ import { useContext, useState, useCallback } from 'preact/hooks';
2
2
  import { evaluate } from '../../expression';
3
3
  import { LocalsContext, renderNodes } from '../../markup/render';
4
+ import type { LocalsScope } from '../../markup/render';
4
5
  import { useMergedLocals } from '../../hooks/use-merged-locals';
5
6
  import type { ASTNode } from '../../markup/ast';
6
7
 
@@ -12,7 +13,7 @@ interface ForProps {
12
13
  }
13
14
 
14
15
  /**
15
- * Parse for-loop args: "$item, $i of $list" or "$item of $list"
16
+ * Parse for-loop args: "@item, @i of $list" or "@item of $list"
16
17
  */
17
18
  function parseForArgs(rawArgs: string): {
18
19
  itemVar: string;
@@ -31,12 +32,51 @@ function parseForArgs(rawArgs: string): {
31
32
  const itemVar = vars[0]!;
32
33
  const indexVar = vars.length > 1 ? vars[1]! : null;
33
34
 
35
+ if (!itemVar.startsWith('@')) {
36
+ throw new Error(`{for} loop variable must use @ prefix: got "${itemVar}"`);
37
+ }
38
+ if (indexVar && !indexVar.startsWith('@')) {
39
+ throw new Error(
40
+ `{for} index variable must use @ prefix: got "${indexVar}"`,
41
+ );
42
+ }
43
+
34
44
  return { itemVar, indexVar, listExpr };
35
45
  }
36
46
 
47
+ function ForIteration({
48
+ parentValues,
49
+ ownKeys,
50
+ initialValues,
51
+ children,
52
+ }: {
53
+ parentValues: Record<string, unknown>;
54
+ ownKeys: Record<string, unknown>;
55
+ initialValues: Record<string, unknown>;
56
+ children: ASTNode[];
57
+ }) {
58
+ const [localState, setLocalState] = useState<Record<string, unknown>>(() => ({
59
+ ...parentValues,
60
+ ...ownKeys,
61
+ ...initialValues,
62
+ }));
63
+
64
+ const update = useCallback((key: string, value: unknown) => {
65
+ setLocalState((prev) => ({ ...prev, [key]: value }));
66
+ }, []);
67
+
68
+ const scope: LocalsScope = { values: localState, update };
69
+
70
+ return (
71
+ <LocalsContext.Provider value={scope}>
72
+ {renderNodes(children)}
73
+ </LocalsContext.Provider>
74
+ );
75
+ }
76
+
37
77
  export function For({ rawArgs, children, className, id }: ForProps) {
38
- const parentLocals = useContext(LocalsContext);
39
- const [mergedVars, mergedTemps] = useMergedLocals();
78
+ const parentScope = useContext(LocalsContext);
79
+ const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
40
80
 
41
81
  let parsed: ReturnType<typeof parseForArgs>;
42
82
  try {
@@ -56,7 +96,7 @@ export function For({ rawArgs, children, className, id }: ForProps) {
56
96
 
57
97
  let list: unknown[];
58
98
  try {
59
- const result = evaluate(listExpr, mergedVars, mergedTemps);
99
+ const result = evaluate(listExpr, mergedVars, mergedTemps, mergedLocals);
60
100
  if (!Array.isArray(result)) {
61
101
  return (
62
102
  <span class="error">
@@ -77,19 +117,19 @@ export function For({ rawArgs, children, className, id }: ForProps) {
77
117
  }
78
118
 
79
119
  const content = list.map((item, i) => {
80
- const locals = {
81
- ...parentLocals,
120
+ const ownKeys: Record<string, unknown> = {
82
121
  [itemVar]: item,
83
122
  ...(indexVar ? { [indexVar]: i } : undefined),
84
123
  };
85
124
 
86
125
  return (
87
- <LocalsContext.Provider
126
+ <ForIteration
88
127
  key={i}
89
- value={locals}
90
- >
91
- {renderNodes(children)}
92
- </LocalsContext.Provider>
128
+ parentValues={parentScope.values}
129
+ ownKeys={ownKeys}
130
+ initialValues={{}}
131
+ children={children}
132
+ />
93
133
  );
94
134
  });
95
135
 
@@ -1,22 +1,24 @@
1
1
  import { useLayoutEffect } from 'preact/hooks';
2
2
  import { useStoryStore } from '../../store';
3
3
  import { evaluate } from '../../expression';
4
+ import { useMergedLocals } from '../../hooks/use-merged-locals';
4
5
 
5
6
  interface GotoProps {
6
7
  rawArgs: string;
7
8
  }
8
9
 
9
10
  export function Goto({ rawArgs }: GotoProps) {
11
+ const [variables, temporary, locals] = useMergedLocals();
12
+
10
13
  useLayoutEffect(() => {
11
- const state = useStoryStore.getState();
12
14
  let passageName: string;
13
15
  try {
14
- const result = evaluate(rawArgs, state.variables, state.temporary);
16
+ const result = evaluate(rawArgs, variables, temporary, locals);
15
17
  passageName = String(result);
16
18
  } catch {
17
19
  passageName = rawArgs.replace(/^["']|["']$/g, '');
18
20
  }
19
- state.navigate(passageName);
21
+ useStoryStore.getState().navigate(passageName);
20
22
  }, []);
21
23
 
22
24
  return null;
@@ -8,7 +8,7 @@ interface IfProps {
8
8
  }
9
9
 
10
10
  export function If({ branches }: IfProps) {
11
- const [mergedVars, mergedTemps] = useMergedLocals();
11
+ const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
12
12
 
13
13
  function renderBranch(branch: Branch) {
14
14
  const children = renderNodes(branch.children);
@@ -31,7 +31,12 @@ export function If({ branches }: IfProps) {
31
31
  }
32
32
 
33
33
  try {
34
- const result = evaluate(branch.rawArgs, mergedVars, mergedTemps);
34
+ const result = evaluate(
35
+ branch.rawArgs,
36
+ mergedVars,
37
+ mergedTemps,
38
+ mergedLocals,
39
+ );
35
40
  if (result) {
36
41
  return renderBranch(branch);
37
42
  }
@@ -3,6 +3,7 @@ import { evaluate } from '../../expression';
3
3
  import { tokenize } from '../../markup/tokenizer';
4
4
  import { buildAST } from '../../markup/ast';
5
5
  import { renderNodes } from '../../markup/render';
6
+ import { useMergedLocals } from '../../hooks/use-merged-locals';
6
7
 
7
8
  interface IncludeProps {
8
9
  rawArgs: string;
@@ -12,14 +13,13 @@ interface IncludeProps {
12
13
 
13
14
  export function Include({ rawArgs, className, id }: IncludeProps) {
14
15
  const storyData = useStoryStore((s) => s.storyData);
15
- const variables = useStoryStore((s) => s.variables);
16
- const temporary = useStoryStore((s) => s.temporary);
16
+ const [variables, temporary, locals] = useMergedLocals();
17
17
 
18
18
  if (!storyData) return null;
19
19
 
20
20
  let passageName: string;
21
21
  try {
22
- const result = evaluate(rawArgs, variables, temporary);
22
+ const result = evaluate(rawArgs, variables, temporary, locals);
23
23
  passageName = String(result);
24
24
  } catch {
25
25
  passageName = rawArgs.replace(/^["']|["']$/g, '');
@@ -1,7 +1,10 @@
1
+ import { useContext } from 'preact/hooks';
1
2
  import { useStoryStore } from '../../store';
2
3
  import { execute } from '../../expression';
3
4
  import type { ASTNode } from '../../markup/ast';
4
5
  import { deepClone } from '../../class-registry';
6
+ import { LocalsContext } from '../../markup/render';
7
+ import { useMergedLocals } from '../../hooks/use-merged-locals';
5
8
 
6
9
  interface MacroLinkProps {
7
10
  rawArgs: string;
@@ -37,23 +40,28 @@ import { useAction } from '../../hooks/use-action';
37
40
  /**
38
41
  * Execute the children imperatively: walk AST for {set} and {do} macros.
39
42
  */
40
- function executeChildren(children: ASTNode[]) {
43
+ function executeChildren(
44
+ children: ASTNode[],
45
+ mergedLocals: Record<string, unknown>,
46
+ scopeUpdate: (key: string, value: unknown) => void,
47
+ ) {
41
48
  const state = useStoryStore.getState();
42
49
  const vars = deepClone(state.variables);
43
50
  const temps = deepClone(state.temporary);
51
+ const localsClone = { ...mergedLocals };
44
52
 
45
53
  for (const node of children) {
46
54
  if (node.type !== 'macro') continue;
47
55
  if (node.name === 'set') {
48
56
  try {
49
- execute(node.rawArgs, vars, temps);
57
+ execute(node.rawArgs, vars, temps, localsClone);
50
58
  } catch (err) {
51
59
  console.error(`spindle: Error in {link} child {set}:`, err);
52
60
  }
53
61
  } else if (node.name === 'do') {
54
62
  const code = collectText(node.children);
55
63
  try {
56
- execute(code, vars, temps);
64
+ execute(code, vars, temps, localsClone);
57
65
  } catch (err) {
58
66
  console.error(`spindle: Error in {link} child {do}:`, err);
59
67
  }
@@ -71,6 +79,11 @@ function executeChildren(children: ASTNode[]) {
71
79
  state.setTemporary(key, temps[key]);
72
80
  }
73
81
  }
82
+ for (const key of Object.keys(localsClone)) {
83
+ if (localsClone[key] !== mergedLocals[key]) {
84
+ scopeUpdate(`@${key}`, localsClone[key]);
85
+ }
86
+ }
74
87
  }
75
88
 
76
89
  export function MacroLink({
@@ -80,10 +93,12 @@ export function MacroLink({
80
93
  id,
81
94
  }: MacroLinkProps) {
82
95
  const { display, passage } = parseArgs(rawArgs);
96
+ const scope = useContext(LocalsContext);
97
+ const [, , mergedLocals] = useMergedLocals();
83
98
 
84
99
  const handleClick = (e: Event) => {
85
100
  e.preventDefault();
86
- executeChildren(children);
101
+ executeChildren(children, mergedLocals, scope.update);
87
102
  if (passage) {
88
103
  useStoryStore.getState().navigate(passage);
89
104
  }
@@ -96,7 +111,7 @@ export function MacroLink({
96
111
  label: display,
97
112
  target: passage ?? undefined,
98
113
  perform: () => {
99
- executeChildren(children);
114
+ executeChildren(children, mergedLocals, scope.update);
100
115
  if (passage) {
101
116
  useStoryStore.getState().navigate(passage);
102
117
  }
@@ -83,12 +83,16 @@ function formatLabel(
83
83
  }
84
84
 
85
85
  export function Meter({ rawArgs, className, id }: MeterProps) {
86
- const [mergedVars, mergedTemps] = useMergedLocals();
86
+ const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
87
87
 
88
88
  try {
89
89
  const { currentExpr, maxExpr, labelMode } = parseArgs(rawArgs);
90
- const current = Number(evaluate(currentExpr, mergedVars, mergedTemps));
91
- const max = Number(evaluate(maxExpr, mergedVars, mergedTemps));
90
+ const current = Number(
91
+ evaluate(currentExpr, mergedVars, mergedTemps, mergedLocals),
92
+ );
93
+ const max = Number(
94
+ evaluate(maxExpr, mergedVars, mergedTemps, mergedLocals),
95
+ );
92
96
  const pct =
93
97
  max === 0 ? 0 : Math.max(0, Math.min(100, (current / max) * 100));
94
98
  const label = formatLabel(current, max, labelMode);
@@ -8,10 +8,10 @@ interface PrintProps {
8
8
  }
9
9
 
10
10
  export function Print({ rawArgs, className, id }: PrintProps) {
11
- const [mergedVars, mergedTemps] = useMergedLocals();
11
+ const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
12
12
 
13
13
  try {
14
- const result = evaluate(rawArgs, mergedVars, mergedTemps);
14
+ const result = evaluate(rawArgs, mergedVars, mergedTemps, mergedLocals);
15
15
  const display = result == null ? '' : String(result);
16
16
  if (className || id)
17
17
  return (
@@ -1,26 +1,32 @@
1
- import { useLayoutEffect } from 'preact/hooks';
1
+ import { useLayoutEffect, useContext } from 'preact/hooks';
2
2
  import { useStoryStore } from '../../store';
3
3
  import { execute } from '../../expression';
4
4
  import { deepClone } from '../../class-registry';
5
+ import { LocalsContext } from '../../markup/render';
6
+ import { useMergedLocals } from '../../hooks/use-merged-locals';
5
7
 
6
8
  interface SetProps {
7
9
  rawArgs: string;
8
10
  }
9
11
 
10
12
  export function Set({ rawArgs }: SetProps) {
13
+ const scope = useContext(LocalsContext);
14
+ const [, , mergedLocals] = useMergedLocals();
15
+
11
16
  useLayoutEffect(() => {
12
17
  const state = useStoryStore.getState();
13
18
  const vars = deepClone(state.variables);
14
19
  const temps = deepClone(state.temporary);
20
+ const localsClone = { ...mergedLocals };
15
21
 
16
22
  try {
17
- execute(rawArgs, vars, temps);
23
+ execute(rawArgs, vars, temps, localsClone);
18
24
  } catch (err) {
19
25
  console.error(`spindle: Error in {set ${rawArgs}}:`, err);
20
26
  return;
21
27
  }
22
28
 
23
- // Diff and apply changes
29
+ // Diff and apply store changes
24
30
  for (const key of Object.keys(vars)) {
25
31
  if (vars[key] !== state.variables[key]) {
26
32
  state.setVariable(key, vars[key]);
@@ -31,6 +37,13 @@ export function Set({ rawArgs }: SetProps) {
31
37
  state.setTemporary(key, temps[key]);
32
38
  }
33
39
  }
40
+
41
+ // Diff and apply locals changes
42
+ for (const key of Object.keys(localsClone)) {
43
+ if (localsClone[key] !== mergedLocals[key]) {
44
+ scope.update(`@${key}`, localsClone[key]);
45
+ }
46
+ }
34
47
  }, []);
35
48
 
36
49
  return null;
@@ -9,11 +9,11 @@ interface SwitchProps {
9
9
  }
10
10
 
11
11
  export function Switch({ rawArgs, branches }: SwitchProps) {
12
- const [mergedVars, mergedTemps] = useMergedLocals();
12
+ const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
13
13
 
14
14
  let switchValue: unknown;
15
15
  try {
16
- switchValue = evaluate(rawArgs, mergedVars, mergedTemps);
16
+ switchValue = evaluate(rawArgs, mergedVars, mergedTemps, mergedLocals);
17
17
  } catch (err) {
18
18
  return (
19
19
  <span
@@ -35,7 +35,12 @@ export function Switch({ rawArgs, branches }: SwitchProps) {
35
35
  }
36
36
 
37
37
  try {
38
- const caseValue = evaluate(branch.rawArgs, mergedVars, mergedTemps);
38
+ const caseValue = evaluate(
39
+ branch.rawArgs,
40
+ mergedVars,
41
+ mergedTemps,
42
+ mergedLocals,
43
+ );
39
44
  if (switchValue === caseValue) {
40
45
  return <>{renderNodes(branch.children)}</>;
41
46
  }
@@ -4,22 +4,30 @@ import { LocalsContext } from '../../markup/render';
4
4
 
5
5
  interface VarDisplayProps {
6
6
  name: string;
7
- scope: 'variable' | 'temporary';
7
+ scope: 'variable' | 'temporary' | 'local';
8
8
  className?: string;
9
9
  id?: string;
10
10
  }
11
11
 
12
12
  export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
13
- const locals = useContext(LocalsContext);
13
+ const localsScope = useContext(LocalsContext);
14
14
  const parts = name.split('.');
15
15
  const root = parts[0]!;
16
16
  const storeValue = useStoryStore((s) =>
17
- scope === 'variable' ? s.variables[root] : s.temporary[root],
17
+ scope === 'variable'
18
+ ? s.variables[root]
19
+ : scope === 'temporary'
20
+ ? s.temporary[root]
21
+ : undefined,
18
22
  );
19
23
 
20
- // Locals (from for-loops) override store values
21
- const key = scope === 'variable' ? `$${root}` : `_${root}`;
22
- let value = key in locals ? locals[key] : storeValue;
24
+ let value: unknown;
25
+ if (scope === 'local') {
26
+ const key = `@${root}`;
27
+ value = key in localsScope.values ? localsScope.values[key] : undefined;
28
+ } else {
29
+ value = storeValue;
30
+ }
23
31
 
24
32
  // Resolve dot path (e.g. "character.name" → character['name'])
25
33
  for (let i = 1; i < parts.length; i++) {
@@ -13,9 +13,7 @@ interface WidgetProps {
13
13
  function parseWidgetDef(rawArgs: string): { name: string; params: string[] } {
14
14
  const tokens = rawArgs.trim().split(/\s+/);
15
15
  const name = tokens[0]!.replace(/["']/g, '');
16
- const params = tokens
17
- .slice(1)
18
- .filter((t) => t.startsWith('$') || t.startsWith('_'));
16
+ const params = tokens.slice(1).filter((t) => t.startsWith('@'));
19
17
  return { name, params };
20
18
  }
21
19
 
@@ -1,5 +1,6 @@
1
- import { useContext } from 'preact/hooks';
1
+ import { useContext, useState, useCallback } from 'preact/hooks';
2
2
  import { LocalsContext, renderNodes } from '../../markup/render';
3
+ import type { LocalsScope } from '../../markup/render';
3
4
  import { useMergedLocals } from '../../hooks/use-merged-locals';
4
5
  import { evaluate } from '../../expression';
5
6
  import type { ASTNode } from '../../markup/ast';
@@ -60,20 +61,47 @@ function splitArgs(raw: string): string[] {
60
61
  return args;
61
62
  }
62
63
 
64
+ function WidgetBody({
65
+ body,
66
+ parentValues,
67
+ ownKeys,
68
+ }: {
69
+ body: ASTNode[];
70
+ parentValues: Record<string, unknown>;
71
+ ownKeys: Record<string, unknown>;
72
+ }) {
73
+ const [localState, setLocalState] = useState<Record<string, unknown>>(() => ({
74
+ ...parentValues,
75
+ ...ownKeys,
76
+ }));
77
+
78
+ const update = useCallback((key: string, value: unknown) => {
79
+ setLocalState((prev) => ({ ...prev, [key]: value }));
80
+ }, []);
81
+
82
+ const scope: LocalsScope = { values: localState, update };
83
+
84
+ return (
85
+ <LocalsContext.Provider value={scope}>
86
+ {renderNodes(body)}
87
+ </LocalsContext.Provider>
88
+ );
89
+ }
90
+
63
91
  export function WidgetInvocation({
64
92
  body,
65
93
  params,
66
94
  rawArgs,
67
95
  }: WidgetInvocationProps) {
68
- const parentLocals = useContext(LocalsContext);
69
- const [mergedVars, mergedTemps] = useMergedLocals();
96
+ const parentScope = useContext(LocalsContext);
97
+ const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
70
98
 
71
99
  if (params.length === 0 || !rawArgs) {
72
100
  return <>{renderNodes(body)}</>;
73
101
  }
74
102
 
75
103
  const argExprs = splitArgs(rawArgs);
76
- const locals: Record<string, unknown> = { ...parentLocals };
104
+ const ownKeys: Record<string, unknown> = {};
77
105
 
78
106
  for (let i = 0; i < params.length; i++) {
79
107
  const param = params[i]!;
@@ -81,17 +109,19 @@ export function WidgetInvocation({
81
109
  let value: unknown;
82
110
  if (expr !== undefined) {
83
111
  try {
84
- value = evaluate(expr, mergedVars, mergedTemps);
112
+ value = evaluate(expr, mergedVars, mergedTemps, mergedLocals);
85
113
  } catch {
86
114
  value = undefined;
87
115
  }
88
116
  }
89
- locals[param] = value;
117
+ ownKeys[param] = value;
90
118
  }
91
119
 
92
120
  return (
93
- <LocalsContext.Provider value={locals}>
94
- {renderNodes(body)}
95
- </LocalsContext.Provider>
121
+ <WidgetBody
122
+ body={body}
123
+ parentValues={parentScope.values}
124
+ ownKeys={ownKeys}
125
+ />
96
126
  );
97
127
  }