@rohal12/spindle 0.13.0 → 0.14.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.14.0",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -1,5 +1,5 @@
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
5
  import { stripLocalsPrefix } from '../../hooks/use-merged-locals';
@@ -19,11 +19,11 @@ export function Button({ rawArgs, children, className, id }: ButtonProps) {
19
19
  const resolve = useInterpolate();
20
20
  className = resolve(className);
21
21
  id = resolve(id);
22
- const scope = useContext(LocalsContext);
22
+ const { update, getValues } = useContext(LocalsUpdateContext);
23
23
 
24
24
  const handleClick = () => {
25
25
  try {
26
- executeMutation(rawArgs, stripLocalsPrefix(scope.values), scope.update);
26
+ executeMutation(rawArgs, stripLocalsPrefix(getValues()), update);
27
27
  } catch (err) {
28
28
  console.error(
29
29
  `spindle: Error in {button ${rawArgs}}${currentSourceLocation()}:`,
@@ -1,28 +1,22 @@
1
1
  import { useLayoutEffect, useContext } from 'preact/hooks';
2
2
  import type { ASTNode } from '../../markup/ast';
3
- import { LocalsContext } from '../../markup/render';
3
+ import { LocalsUpdateContext } from '../../markup/render';
4
4
  import { stripLocalsPrefix } from '../../hooks/use-merged-locals';
5
5
  import { executeMutation } from '../../execute-mutation';
6
6
  import { currentSourceLocation } from '../../utils/source-location';
7
+ import { collectText } from '../../utils/extract-text';
7
8
 
8
9
  interface DoProps {
9
10
  children: ASTNode[];
10
11
  }
11
12
 
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
13
  export function Do({ children }: DoProps) {
20
14
  const code = collectText(children);
21
- const scope = useContext(LocalsContext);
15
+ const { update, getValues } = useContext(LocalsUpdateContext);
22
16
 
23
17
  useLayoutEffect(() => {
24
18
  try {
25
- executeMutation(code, stripLocalsPrefix(scope.values), scope.update);
19
+ executeMutation(code, stripLocalsPrefix(getValues()), update);
26
20
  } catch (err) {
27
21
  console.error(`spindle: Error in {do}${currentSourceLocation()}:`, err);
28
22
  }
@@ -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';
@@ -63,16 +72,21 @@ function ForIteration({
63
72
  ...initialValues,
64
73
  }));
65
74
 
75
+ const valuesRef = useRef(localState);
76
+ valuesRef.current = localState;
77
+
78
+ const getValues = useCallback(() => valuesRef.current, []);
66
79
  const update = useCallback((key: string, value: unknown) => {
67
80
  setLocalState((prev) => ({ ...prev, [key]: value }));
68
81
  }, []);
69
-
70
- const scope: LocalsScope = { values: localState, update };
82
+ const updater = useMemo(() => ({ update, getValues }), [update, getValues]);
71
83
 
72
84
  return (
73
- <LocalsContext.Provider value={scope}>
74
- {renderNodes(children)}
75
- </LocalsContext.Provider>
85
+ <LocalsUpdateContext.Provider value={updater}>
86
+ <LocalsValuesContext.Provider value={localState}>
87
+ {renderNodes(children)}
88
+ </LocalsValuesContext.Provider>
89
+ </LocalsUpdateContext.Provider>
76
90
  );
77
91
  }
78
92
 
@@ -80,7 +94,7 @@ export function For({ rawArgs, children, className, id }: ForProps) {
80
94
  const resolve = useInterpolate();
81
95
  className = resolve(className);
82
96
  id = resolve(id);
83
- const parentScope = useContext(LocalsContext);
97
+ const parentValues = useContext(LocalsValuesContext);
84
98
  const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
85
99
 
86
100
  let parsed: ReturnType<typeof parseForArgs>;
@@ -130,7 +144,7 @@ export function For({ rawArgs, children, className, id }: ForProps) {
130
144
  return (
131
145
  <ForIteration
132
146
  key={i}
133
- parentValues={parentScope.values}
147
+ parentValues={parentValues}
134
148
  ownKeys={ownKeys}
135
149
  initialValues={{}}
136
150
  children={children}
@@ -1,7 +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';
4
+ import { LocalsUpdateContext } from '../../markup/render';
5
5
  import { stripLocalsPrefix } from '../../hooks/use-merged-locals';
6
6
  import { useInterpolate } from '../../hooks/use-interpolate';
7
7
  import { executeMutation } from '../../execute-mutation';
@@ -81,11 +81,11 @@ export function MacroLink({
81
81
  className = resolve(className);
82
82
  id = resolve(id);
83
83
  const { display, passage } = parseArgs(rawArgs);
84
- const scope = useContext(LocalsContext);
84
+ const { update, getValues } = useContext(LocalsUpdateContext);
85
85
 
86
86
  const handleClick = (e: Event) => {
87
87
  e.preventDefault();
88
- executeChildren(children, stripLocalsPrefix(scope.values), scope.update);
88
+ executeChildren(children, stripLocalsPrefix(getValues()), update);
89
89
  if (passage) {
90
90
  useStoryStore.getState().navigate(passage);
91
91
  }
@@ -98,7 +98,7 @@ export function MacroLink({
98
98
  label: display,
99
99
  target: passage ?? undefined,
100
100
  perform: () => {
101
- executeChildren(children, stripLocalsPrefix(scope.values), scope.update);
101
+ executeChildren(children, stripLocalsPrefix(getValues()), update);
102
102
  if (passage) {
103
103
  useStoryStore.getState().navigate(passage);
104
104
  }
@@ -1,5 +1,5 @@
1
1
  import { useRef, useContext } from 'preact/hooks';
2
- import { LocalsContext } from '../../markup/render';
2
+ import { LocalsUpdateContext } from '../../markup/render';
3
3
  import { stripLocalsPrefix } from '../../hooks/use-merged-locals';
4
4
  import { executeMutation } from '../../execute-mutation';
5
5
  import { currentSourceLocation } from '../../utils/source-location';
@@ -9,14 +9,14 @@ interface SetProps {
9
9
  }
10
10
 
11
11
  export function Set({ rawArgs }: SetProps) {
12
- const scope = useContext(LocalsContext);
12
+ const { update, getValues } = useContext(LocalsUpdateContext);
13
13
  const ran = useRef(false);
14
14
 
15
15
  if (!ran.current) {
16
16
  ran.current = true;
17
17
 
18
18
  try {
19
- executeMutation(rawArgs, stripLocalsPrefix(scope.values), scope.update);
19
+ executeMutation(rawArgs, stripLocalsPrefix(getValues()), update);
20
20
  } catch (err) {
21
21
  console.error(
22
22
  `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) =>
@@ -28,7 +28,7 @@ export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
28
28
  let value: unknown;
29
29
  if (scope === 'local') {
30
30
  const key = `@${root}`;
31
- value = key in localsScope.values ? localsScope.values[key] : undefined;
31
+ value = key in localsValues ? localsValues[key] : undefined;
32
32
  } else {
33
33
  value = storeValue;
34
34
  }
@@ -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) {
@@ -120,7 +134,7 @@ export function WidgetInvocation({
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
  );
@@ -1,6 +1,6 @@
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
6
  * Strip `@` prefix from locals scope values so they can be passed
@@ -30,9 +30,9 @@ export function useMergedLocals(): readonly [
30
30
  ] {
31
31
  const variables = useStoryStore((s) => s.variables);
32
32
  const temporary = useStoryStore((s) => s.temporary);
33
- const scope = useContext(LocalsContext);
33
+ const localsValues = useContext(LocalsValuesContext);
34
34
 
35
35
  return useMemo(() => {
36
- return [variables, temporary, stripLocalsPrefix(scope.values)] as const;
37
- }, [variables, temporary, scope.values]);
36
+ return [variables, temporary, stripLocalsPrefix(localsValues)] as const;
37
+ }, [variables, temporary, localsValues]);
38
38
  }
@@ -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