@rohal12/spindle 0.13.0 → 0.15.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.13.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -1,8 +1,7 @@
1
1
  import { useContext } from 'preact/hooks';
2
- import { renderInlineNodes, LocalsContext } from '../../markup/render';
2
+ import { renderInlineNodes, LocalsUpdateContext } from '../../markup/render';
3
3
  import { collectText } from '../../utils/extract-text';
4
4
  import { useAction } from '../../hooks/use-action';
5
- import { stripLocalsPrefix } from '../../hooks/use-merged-locals';
6
5
  import { useInterpolate } from '../../hooks/use-interpolate';
7
6
  import { executeMutation } from '../../execute-mutation';
8
7
  import { currentSourceLocation } from '../../utils/source-location';
@@ -19,11 +18,11 @@ export function Button({ rawArgs, children, className, id }: ButtonProps) {
19
18
  const resolve = useInterpolate();
20
19
  className = resolve(className);
21
20
  id = resolve(id);
22
- const scope = useContext(LocalsContext);
21
+ const { update, getValues } = useContext(LocalsUpdateContext);
23
22
 
24
23
  const handleClick = () => {
25
24
  try {
26
- executeMutation(rawArgs, stripLocalsPrefix(scope.values), scope.update);
25
+ executeMutation(rawArgs, getValues(), update);
27
26
  } catch (err) {
28
27
  console.error(
29
28
  `spindle: Error in {button ${rawArgs}}${currentSourceLocation()}:`,
@@ -1,28 +1,21 @@
1
1
  import { useLayoutEffect, useContext } from 'preact/hooks';
2
2
  import type { ASTNode } from '../../markup/ast';
3
- import { LocalsContext } from '../../markup/render';
4
- import { stripLocalsPrefix } from '../../hooks/use-merged-locals';
3
+ import { LocalsUpdateContext } from '../../markup/render';
5
4
  import { executeMutation } from '../../execute-mutation';
6
5
  import { currentSourceLocation } from '../../utils/source-location';
6
+ import { collectText } from '../../utils/extract-text';
7
7
 
8
8
  interface DoProps {
9
9
  children: ASTNode[];
10
10
  }
11
11
 
12
- /**
13
- * Concatenate text children into a single JS string for execution.
14
- */
15
- function collectText(nodes: ASTNode[]): string {
16
- return nodes.map((n) => (n.type === 'text' ? n.value : '')).join('');
17
- }
18
-
19
12
  export function Do({ children }: DoProps) {
20
13
  const code = collectText(children);
21
- const scope = useContext(LocalsContext);
14
+ const { update, getValues } = useContext(LocalsUpdateContext);
22
15
 
23
16
  useLayoutEffect(() => {
24
17
  try {
25
- executeMutation(code, stripLocalsPrefix(scope.values), scope.update);
18
+ executeMutation(code, getValues(), update);
26
19
  } catch (err) {
27
20
  console.error(`spindle: Error in {do}${currentSourceLocation()}:`, err);
28
21
  }
@@ -1,7 +1,16 @@
1
- import { useContext, useState, useCallback } from 'preact/hooks';
1
+ import {
2
+ useContext,
3
+ useState,
4
+ useCallback,
5
+ useRef,
6
+ useMemo,
7
+ } from 'preact/hooks';
2
8
  import { evaluate } from '../../expression';
3
- import { LocalsContext, renderNodes } from '../../markup/render';
4
- import type { LocalsScope } from '../../markup/render';
9
+ import {
10
+ LocalsValuesContext,
11
+ LocalsUpdateContext,
12
+ renderNodes,
13
+ } from '../../markup/render';
5
14
  import { useMergedLocals } from '../../hooks/use-merged-locals';
6
15
  import { useInterpolate } from '../../hooks/use-interpolate';
7
16
  import { currentSourceLocation } from '../../utils/source-location';
@@ -43,7 +52,11 @@ function parseForArgs(rawArgs: string): {
43
52
  );
44
53
  }
45
54
 
46
- return { itemVar, indexVar, listExpr };
55
+ return {
56
+ itemVar: itemVar.slice(1),
57
+ indexVar: indexVar ? indexVar.slice(1) : null,
58
+ listExpr,
59
+ };
47
60
  }
48
61
 
49
62
  function ForIteration({
@@ -63,16 +76,21 @@ function ForIteration({
63
76
  ...initialValues,
64
77
  }));
65
78
 
79
+ const valuesRef = useRef(localState);
80
+ valuesRef.current = localState;
81
+
82
+ const getValues = useCallback(() => valuesRef.current, []);
66
83
  const update = useCallback((key: string, value: unknown) => {
67
84
  setLocalState((prev) => ({ ...prev, [key]: value }));
68
85
  }, []);
69
-
70
- const scope: LocalsScope = { values: localState, update };
86
+ const updater = useMemo(() => ({ update, getValues }), [update, getValues]);
71
87
 
72
88
  return (
73
- <LocalsContext.Provider value={scope}>
74
- {renderNodes(children)}
75
- </LocalsContext.Provider>
89
+ <LocalsUpdateContext.Provider value={updater}>
90
+ <LocalsValuesContext.Provider value={localState}>
91
+ {renderNodes(children)}
92
+ </LocalsValuesContext.Provider>
93
+ </LocalsUpdateContext.Provider>
76
94
  );
77
95
  }
78
96
 
@@ -80,7 +98,7 @@ export function For({ rawArgs, children, className, id }: ForProps) {
80
98
  const resolve = useInterpolate();
81
99
  className = resolve(className);
82
100
  id = resolve(id);
83
- const parentScope = useContext(LocalsContext);
101
+ const parentValues = useContext(LocalsValuesContext);
84
102
  const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
85
103
 
86
104
  let parsed: ReturnType<typeof parseForArgs>;
@@ -130,7 +148,7 @@ export function For({ rawArgs, children, className, id }: ForProps) {
130
148
  return (
131
149
  <ForIteration
132
150
  key={i}
133
- parentValues={parentScope.values}
151
+ parentValues={parentValues}
134
152
  ownKeys={ownKeys}
135
153
  initialValues={{}}
136
154
  children={children}
@@ -1,8 +1,7 @@
1
1
  import { useContext } from 'preact/hooks';
2
2
  import { useStoryStore } from '../../store';
3
3
  import type { ASTNode } from '../../markup/ast';
4
- import { LocalsContext } from '../../markup/render';
5
- import { stripLocalsPrefix } from '../../hooks/use-merged-locals';
4
+ import { LocalsUpdateContext } from '../../markup/render';
6
5
  import { useInterpolate } from '../../hooks/use-interpolate';
7
6
  import { executeMutation } from '../../execute-mutation';
8
7
  import { currentSourceLocation } from '../../utils/source-location';
@@ -81,11 +80,11 @@ export function MacroLink({
81
80
  className = resolve(className);
82
81
  id = resolve(id);
83
82
  const { display, passage } = parseArgs(rawArgs);
84
- const scope = useContext(LocalsContext);
83
+ const { update, getValues } = useContext(LocalsUpdateContext);
85
84
 
86
85
  const handleClick = (e: Event) => {
87
86
  e.preventDefault();
88
- executeChildren(children, stripLocalsPrefix(scope.values), scope.update);
87
+ executeChildren(children, getValues(), update);
89
88
  if (passage) {
90
89
  useStoryStore.getState().navigate(passage);
91
90
  }
@@ -98,7 +97,7 @@ export function MacroLink({
98
97
  label: display,
99
98
  target: passage ?? undefined,
100
99
  perform: () => {
101
- executeChildren(children, stripLocalsPrefix(scope.values), scope.update);
100
+ executeChildren(children, getValues(), update);
102
101
  if (passage) {
103
102
  useStoryStore.getState().navigate(passage);
104
103
  }
@@ -1,6 +1,5 @@
1
1
  import { useRef, useContext } from 'preact/hooks';
2
- import { LocalsContext } from '../../markup/render';
3
- import { stripLocalsPrefix } from '../../hooks/use-merged-locals';
2
+ import { LocalsUpdateContext } from '../../markup/render';
4
3
  import { executeMutation } from '../../execute-mutation';
5
4
  import { currentSourceLocation } from '../../utils/source-location';
6
5
 
@@ -9,14 +8,14 @@ interface SetProps {
9
8
  }
10
9
 
11
10
  export function Set({ rawArgs }: SetProps) {
12
- const scope = useContext(LocalsContext);
11
+ const { update, getValues } = useContext(LocalsUpdateContext);
13
12
  const ran = useRef(false);
14
13
 
15
14
  if (!ran.current) {
16
15
  ran.current = true;
17
16
 
18
17
  try {
19
- executeMutation(rawArgs, stripLocalsPrefix(scope.values), scope.update);
18
+ executeMutation(rawArgs, getValues(), update);
20
19
  } catch (err) {
21
20
  console.error(
22
21
  `spindle: Error in {set ${rawArgs}}${currentSourceLocation()}:`,
@@ -1,6 +1,6 @@
1
1
  import { useStoryStore } from '../../store';
2
2
  import { useContext } from 'preact/hooks';
3
- import { LocalsContext } from '../../markup/render';
3
+ import { LocalsValuesContext } from '../../markup/render';
4
4
  import { useInterpolate } from '../../hooks/use-interpolate';
5
5
 
6
6
  interface VarDisplayProps {
@@ -14,7 +14,7 @@ export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
14
14
  const resolve = useInterpolate();
15
15
  className = resolve(className);
16
16
  id = resolve(id);
17
- const localsScope = useContext(LocalsContext);
17
+ const localsValues = useContext(LocalsValuesContext);
18
18
  const parts = name.split('.');
19
19
  const root = parts[0]!;
20
20
  const storeValue = useStoryStore((s) =>
@@ -27,8 +27,7 @@ export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
27
27
 
28
28
  let value: unknown;
29
29
  if (scope === 'local') {
30
- const key = `@${root}`;
31
- value = key in localsScope.values ? localsScope.values[key] : undefined;
30
+ value = root in localsValues ? localsValues[root] : undefined;
32
31
  } else {
33
32
  value = storeValue;
34
33
  }
@@ -1,6 +1,15 @@
1
- import { useContext, useState, useCallback } from 'preact/hooks';
2
- import { LocalsContext, renderNodes } from '../../markup/render';
3
- import type { LocalsScope } from '../../markup/render';
1
+ import {
2
+ useContext,
3
+ useState,
4
+ useCallback,
5
+ useRef,
6
+ useMemo,
7
+ } from 'preact/hooks';
8
+ import {
9
+ LocalsValuesContext,
10
+ LocalsUpdateContext,
11
+ renderNodes,
12
+ } from '../../markup/render';
4
13
  import { useMergedLocals } from '../../hooks/use-merged-locals';
5
14
  import { evaluate } from '../../expression';
6
15
  import type { ASTNode } from '../../markup/ast';
@@ -75,16 +84,21 @@ function WidgetBody({
75
84
  ...ownKeys,
76
85
  }));
77
86
 
87
+ const valuesRef = useRef(localState);
88
+ valuesRef.current = localState;
89
+
90
+ const getValues = useCallback(() => valuesRef.current, []);
78
91
  const update = useCallback((key: string, value: unknown) => {
79
92
  setLocalState((prev) => ({ ...prev, [key]: value }));
80
93
  }, []);
81
-
82
- const scope: LocalsScope = { values: localState, update };
94
+ const updater = useMemo(() => ({ update, getValues }), [update, getValues]);
83
95
 
84
96
  return (
85
- <LocalsContext.Provider value={scope}>
86
- {renderNodes(body)}
87
- </LocalsContext.Provider>
97
+ <LocalsUpdateContext.Provider value={updater}>
98
+ <LocalsValuesContext.Provider value={localState}>
99
+ {renderNodes(body)}
100
+ </LocalsValuesContext.Provider>
101
+ </LocalsUpdateContext.Provider>
88
102
  );
89
103
  }
90
104
 
@@ -93,7 +107,7 @@ export function WidgetInvocation({
93
107
  params,
94
108
  rawArgs,
95
109
  }: WidgetInvocationProps) {
96
- const parentScope = useContext(LocalsContext);
110
+ const parentValues = useContext(LocalsValuesContext);
97
111
  const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
98
112
 
99
113
  if (params.length === 0 || !rawArgs) {
@@ -114,13 +128,13 @@ export function WidgetInvocation({
114
128
  value = undefined;
115
129
  }
116
130
  }
117
- ownKeys[param] = value;
131
+ ownKeys[param.startsWith('@') ? param.slice(1) : param] = value;
118
132
  }
119
133
 
120
134
  return (
121
135
  <WidgetBody
122
136
  body={body}
123
- parentValues={parentScope.values}
137
+ parentValues={parentValues}
124
138
  ownKeys={ownKeys}
125
139
  />
126
140
  );
@@ -26,7 +26,7 @@ export function executeMutation(
26
26
  }
27
27
  for (const key of Object.keys(localsClone)) {
28
28
  if (localsClone[key] !== mergedLocals[key]) {
29
- scopeUpdate(`@${key}`, localsClone[key]);
29
+ scopeUpdate(key, localsClone[key]);
30
30
  }
31
31
  }
32
32
  }
@@ -1,27 +1,10 @@
1
1
  import { useContext, useMemo } from 'preact/hooks';
2
2
  import { useStoryStore } from '../store';
3
- import { LocalsContext } from '../markup/render';
3
+ import { LocalsValuesContext } from '../markup/render';
4
4
 
5
5
  /**
6
- * Strip `@` prefix from locals scope values so they can be passed
7
- * directly to evaluate/execute.
8
- */
9
- export function stripLocalsPrefix(
10
- values: Record<string, unknown>,
11
- ): Record<string, unknown> {
12
- const locals: Record<string, unknown> = {};
13
- for (const [key, val] of Object.entries(values)) {
14
- if (key.startsWith('@')) {
15
- locals[key.slice(1)] = val;
16
- }
17
- }
18
- return locals;
19
- }
20
-
21
- /**
22
- * Return store variables, temporary, and @-prefixed locals from context.
23
- * Locals use `@` prefix keys internally; the returned locals dict has
24
- * the prefix stripped so it can be passed directly to evaluate/execute.
6
+ * Return store variables, temporary, and locals from context.
7
+ * All three dicts use unprefixed keys suitable for evaluate/execute.
25
8
  */
26
9
  export function useMergedLocals(): readonly [
27
10
  Record<string, unknown>,
@@ -30,9 +13,9 @@ export function useMergedLocals(): readonly [
30
13
  ] {
31
14
  const variables = useStoryStore((s) => s.variables);
32
15
  const temporary = useStoryStore((s) => s.temporary);
33
- const scope = useContext(LocalsContext);
16
+ const localsValues = useContext(LocalsValuesContext);
34
17
 
35
18
  return useMemo(() => {
36
- return [variables, temporary, stripLocalsPrefix(scope.values)] as const;
37
- }, [variables, temporary, scope.values]);
19
+ return [variables, temporary, localsValues] as const;
20
+ }, [variables, temporary, localsValues]);
38
21
  }
@@ -44,17 +44,18 @@ import type { ASTNode, Branch, HtmlNode, MacroNode, VariableNode } from './ast';
44
44
  import { useStoryStore } from '../store';
45
45
  import { useInterpolate } from '../hooks/use-interpolate';
46
46
 
47
- export interface LocalsScope {
48
- values: Record<string, unknown>;
47
+ export interface LocalsUpdater {
49
48
  update: (key: string, value: unknown) => void;
49
+ getValues: () => Record<string, unknown>;
50
50
  }
51
51
 
52
- const defaultLocalsScope: LocalsScope = {
53
- values: {},
52
+ const defaultUpdater: LocalsUpdater = {
54
53
  update: () => {},
54
+ getValues: () => ({}),
55
55
  };
56
56
 
57
- export const LocalsContext = createContext<LocalsScope>(defaultLocalsScope);
57
+ export const LocalsValuesContext = createContext<Record<string, unknown>>({});
58
+ export const LocalsUpdateContext = createContext<LocalsUpdater>(defaultUpdater);
58
59
 
59
60
  const EMPTY_BRANCHES: Branch[] = [];
60
61