@rohal12/spindle 0.42.0 → 0.43.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/dist/pkg/format.js +1 -1
- package/package.json +1 -1
- package/src/components/macros/Computed.tsx +6 -3
- package/src/components/macros/ExprDisplay.tsx +8 -1
- package/src/components/macros/Unset.tsx +3 -1
- package/src/components/macros/VarDisplay.tsx +4 -2
- package/src/components/macros/WidgetInvocation.tsx +11 -3
- package/src/define-macro.ts +11 -1
- package/src/execute-mutation.ts +12 -1
- package/src/expression.ts +14 -7
- package/src/hooks/use-interpolate.ts +3 -3
- package/src/hooks/use-merged-locals.ts +6 -4
- package/src/index.tsx +22 -1
- package/src/interpolation.ts +18 -7
- package/src/markup/ast.ts +1 -1
- package/src/markup/render.tsx +1 -0
- package/src/markup/tokenizer.ts +86 -1
- package/src/store.ts +26 -1
- package/src/story-api.ts +32 -4
- package/src/story-variables.ts +22 -9
- package/src/triggers.ts +7 -1
- package/types/index.d.ts +12 -3
package/src/story-api.ts
CHANGED
|
@@ -76,20 +76,37 @@ function ensureVariableChangedSubscription(): void {
|
|
|
76
76
|
if (variableChangedSubActive) return;
|
|
77
77
|
variableChangedSubActive = true;
|
|
78
78
|
let prevVars = { ...useStoryStore.getState().variables };
|
|
79
|
+
let prevTrans = { ...useStoryStore.getState().transient };
|
|
79
80
|
useStoryStore.subscribe((state) => {
|
|
80
81
|
const changed: Record<string, { from: unknown; to: unknown }> = {};
|
|
81
82
|
let hasChanges = false;
|
|
82
|
-
|
|
83
|
+
|
|
84
|
+
// Check $variables
|
|
85
|
+
const allVarKeys = new Set([
|
|
83
86
|
...Object.keys(prevVars),
|
|
84
87
|
...Object.keys(state.variables),
|
|
85
88
|
]);
|
|
86
|
-
for (const key of
|
|
89
|
+
for (const key of allVarKeys) {
|
|
87
90
|
if (state.variables[key] !== prevVars[key]) {
|
|
88
91
|
changed[key] = { from: prevVars[key], to: state.variables[key] };
|
|
89
92
|
hasChanges = true;
|
|
90
93
|
}
|
|
91
94
|
}
|
|
95
|
+
|
|
96
|
+
// Check %transient
|
|
97
|
+
const allTransKeys = new Set([
|
|
98
|
+
...Object.keys(prevTrans),
|
|
99
|
+
...Object.keys(state.transient),
|
|
100
|
+
]);
|
|
101
|
+
for (const key of allTransKeys) {
|
|
102
|
+
if (state.transient[key] !== prevTrans[key]) {
|
|
103
|
+
changed[`%${key}`] = { from: prevTrans[key], to: state.transient[key] };
|
|
104
|
+
hasChanges = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
prevVars = { ...state.variables };
|
|
109
|
+
prevTrans = { ...state.transient };
|
|
93
110
|
if (hasChanges) {
|
|
94
111
|
emit('variableChanged', changed);
|
|
95
112
|
}
|
|
@@ -178,16 +195,27 @@ export interface StoryAPI {
|
|
|
178
195
|
function createStoryAPI(): StoryAPI {
|
|
179
196
|
return {
|
|
180
197
|
get(name: string): unknown {
|
|
198
|
+
if (name.startsWith('%')) {
|
|
199
|
+
return useStoryStore.getState().transient[name.slice(1)];
|
|
200
|
+
}
|
|
181
201
|
return useStoryStore.getState().variables[name];
|
|
182
202
|
},
|
|
183
203
|
|
|
184
204
|
set(nameOrVars: string | Record<string, unknown>, value?: unknown): void {
|
|
185
205
|
const state = useStoryStore.getState();
|
|
186
206
|
if (typeof nameOrVars === 'string') {
|
|
187
|
-
|
|
207
|
+
if (nameOrVars.startsWith('%')) {
|
|
208
|
+
state.setTransient(nameOrVars.slice(1), value);
|
|
209
|
+
} else {
|
|
210
|
+
state.setVariable(nameOrVars, value);
|
|
211
|
+
}
|
|
188
212
|
} else {
|
|
189
213
|
for (const [k, v] of Object.entries(nameOrVars)) {
|
|
190
|
-
|
|
214
|
+
if (k.startsWith('%')) {
|
|
215
|
+
state.setTransient(k.slice(1), v);
|
|
216
|
+
} else {
|
|
217
|
+
state.setVariable(k, v);
|
|
218
|
+
}
|
|
191
219
|
}
|
|
192
220
|
}
|
|
193
221
|
},
|
package/src/story-variables.ts
CHANGED
|
@@ -12,7 +12,10 @@ export interface VariableSchema extends FieldSchema {
|
|
|
12
12
|
default: unknown;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
function declarationRegex(sigil: string): RegExp {
|
|
16
|
+
const escaped = sigil.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
17
|
+
return new RegExp(`^${escaped}(\\w+)\\s*=\\s*(.+)$`);
|
|
18
|
+
}
|
|
16
19
|
const VAR_REF_RE = /\$(\w+(?:\.\w+)*)/g;
|
|
17
20
|
const FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
|
|
18
21
|
|
|
@@ -32,20 +35,23 @@ function inferSchema(value: unknown): FieldSchema {
|
|
|
32
35
|
const jsType = typeof value;
|
|
33
36
|
if (!VALID_VAR_TYPES.has(jsType)) {
|
|
34
37
|
throw new Error(
|
|
35
|
-
`
|
|
38
|
+
`Unsupported type "${jsType}" for value ${String(value)}. Expected number, string, boolean, array, or object.`,
|
|
36
39
|
);
|
|
37
40
|
}
|
|
38
41
|
return { type: jsType as VarType };
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
|
-
* Parse a StoryVariables passage content into a schema map.
|
|
43
|
-
* Each line: `$varName = expression`
|
|
45
|
+
* Parse a StoryVariables or StoryTransients passage content into a schema map.
|
|
46
|
+
* Each line: `$varName = expression` (or `%varName = expression` for transients)
|
|
44
47
|
*/
|
|
45
48
|
export function parseStoryVariables(
|
|
46
49
|
content: string,
|
|
50
|
+
sigil: '$' | '%' = '$',
|
|
47
51
|
): Map<string, VariableSchema> {
|
|
48
52
|
const schema = new Map<string, VariableSchema>();
|
|
53
|
+
const DECLARATION_RE = declarationRegex(sigil);
|
|
54
|
+
const passageName = sigil === '%' ? 'StoryTransients' : 'StoryVariables';
|
|
49
55
|
|
|
50
56
|
for (const rawLine of content.split('\n')) {
|
|
51
57
|
const line = rawLine.trim();
|
|
@@ -54,7 +60,7 @@ export function parseStoryVariables(
|
|
|
54
60
|
const match = line.match(DECLARATION_RE);
|
|
55
61
|
if (!match) {
|
|
56
62
|
throw new Error(
|
|
57
|
-
|
|
63
|
+
`${passageName}: Invalid declaration: "${line}". Expected: ${sigil}name = value`,
|
|
58
64
|
);
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -64,11 +70,18 @@ export function parseStoryVariables(
|
|
|
64
70
|
value = new Function('return (' + expr + ')')();
|
|
65
71
|
} catch (err) {
|
|
66
72
|
throw new Error(
|
|
67
|
-
|
|
73
|
+
`${passageName}: Failed to evaluate "${sigil}${name} = ${expr}": ${err instanceof Error ? err.message : err}`,
|
|
68
74
|
);
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
|
|
77
|
+
let fieldSchema: FieldSchema;
|
|
78
|
+
try {
|
|
79
|
+
fieldSchema = inferSchema(value);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${passageName}: ${err instanceof Error ? err.message : err}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
72
85
|
schema.set(name, { ...fieldSchema, name, default: value });
|
|
73
86
|
}
|
|
74
87
|
|
|
@@ -146,8 +159,8 @@ export function validatePassages(
|
|
|
146
159
|
const errors: string[] = [];
|
|
147
160
|
|
|
148
161
|
for (const [name, passage] of passages) {
|
|
149
|
-
// Don't validate the StoryVariables
|
|
150
|
-
if (name === 'StoryVariables') continue;
|
|
162
|
+
// Don't validate the StoryVariables/StoryTransients passages themselves
|
|
163
|
+
if (name === 'StoryVariables' || name === 'StoryTransients') continue;
|
|
151
164
|
|
|
152
165
|
const forLocals = extractForLocals(passage.content);
|
|
153
166
|
|
package/src/triggers.ts
CHANGED
|
@@ -45,7 +45,13 @@ let dialogHostCallbacks: DialogHostCallbacks | null = null;
|
|
|
45
45
|
function evalCondition(condition: string): boolean {
|
|
46
46
|
const state = useStoryStore.getState();
|
|
47
47
|
try {
|
|
48
|
-
return !!evaluate(
|
|
48
|
+
return !!evaluate(
|
|
49
|
+
condition,
|
|
50
|
+
state.variables,
|
|
51
|
+
state.temporary,
|
|
52
|
+
{},
|
|
53
|
+
state.transient,
|
|
54
|
+
);
|
|
49
55
|
} catch {
|
|
50
56
|
return false;
|
|
51
57
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -294,6 +294,7 @@ export interface MacroContext {
|
|
|
294
294
|
Record<string, unknown>,
|
|
295
295
|
Record<string, unknown>,
|
|
296
296
|
Record<string, unknown>,
|
|
297
|
+
Record<string, unknown>,
|
|
297
298
|
];
|
|
298
299
|
varName?: string;
|
|
299
300
|
value?: unknown;
|
|
@@ -364,12 +365,20 @@ export interface SaveInfo {
|
|
|
364
365
|
* @see {@link ../../src/story-api.ts} for the implementation.
|
|
365
366
|
*/
|
|
366
367
|
export interface StoryAPI {
|
|
367
|
-
/**
|
|
368
|
+
/**
|
|
369
|
+
* Get a variable value. Use '%name' prefix for transient variables.
|
|
370
|
+
* @example Story.get('health') // $health
|
|
371
|
+
* @example Story.get('%npcList') // %npcList (transient)
|
|
372
|
+
*/
|
|
368
373
|
get(name: string): unknown;
|
|
369
374
|
|
|
370
|
-
/**
|
|
375
|
+
/**
|
|
376
|
+
* Set one or more variables. Use '%name' prefix for transient variables.
|
|
377
|
+
* @example Story.set('health', 100)
|
|
378
|
+
* @example Story.set('%npcList', [...])
|
|
379
|
+
* @example Story.set({ health: 100, '%npcList': [...] })
|
|
380
|
+
*/
|
|
371
381
|
set(name: string, value: unknown): void;
|
|
372
|
-
/** Set multiple story variables at once. */
|
|
373
382
|
set(vars: Record<string, unknown>): void;
|
|
374
383
|
|
|
375
384
|
/** Navigate to a passage by name. */
|