@rohal12/spindle 0.29.1 → 0.30.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/action-registry.ts +9 -0
- package/src/class-registry.ts +38 -0
- package/src/components/PassageDialog.tsx +9 -4
- package/src/components/macros/Unset.tsx +3 -1
- package/src/define-macro.ts +5 -7
- package/src/expression.ts +8 -0
- package/src/hooks/use-action.ts +13 -2
- package/src/index.tsx +21 -1
- package/src/story-api.ts +7 -1
package/package.json
CHANGED
package/src/action-registry.ts
CHANGED
|
@@ -53,6 +53,15 @@ export function registerAction(action: StoryAction): () => void {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export function updateAction(action: StoryAction): () => void {
|
|
57
|
+
actions.set(action.id, action);
|
|
58
|
+
notify();
|
|
59
|
+
return () => {
|
|
60
|
+
actions.delete(action.id);
|
|
61
|
+
notify();
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
export function getActions(): StoryAction[] {
|
|
57
66
|
return Array.from(actions.values());
|
|
58
67
|
}
|
package/src/class-registry.ts
CHANGED
|
@@ -49,6 +49,24 @@ export function deepClone<T>(value: T): T {
|
|
|
49
49
|
return arr;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
if (val instanceof Map) {
|
|
53
|
+
const copy = new Map();
|
|
54
|
+
seen.set(obj, copy);
|
|
55
|
+
for (const [k, v] of val) {
|
|
56
|
+
copy.set(clone(k), clone(v));
|
|
57
|
+
}
|
|
58
|
+
return copy;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (val instanceof Set) {
|
|
62
|
+
const copy = new Set();
|
|
63
|
+
seen.set(obj, copy);
|
|
64
|
+
for (const v of val) {
|
|
65
|
+
copy.add(clone(v));
|
|
66
|
+
}
|
|
67
|
+
return copy;
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
// Registered class instance
|
|
53
71
|
const ctor = obj.constructor as Constructor;
|
|
54
72
|
const name = ctorToName.get(ctor);
|
|
@@ -119,6 +137,18 @@ export function serialize<T>(value: T): T {
|
|
|
119
137
|
return result;
|
|
120
138
|
}
|
|
121
139
|
|
|
140
|
+
if (val instanceof Map) {
|
|
141
|
+
const entries = [...val].map(([k, v]) => [ser(k), ser(v)]);
|
|
142
|
+
seen.delete(obj);
|
|
143
|
+
return { [CLASS_TAG]: '__Map__', [DATA_TAG]: { entries } };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (val instanceof Set) {
|
|
147
|
+
const entries = [...val].map((v) => ser(v));
|
|
148
|
+
seen.delete(obj);
|
|
149
|
+
return { [CLASS_TAG]: '__Set__', [DATA_TAG]: { entries } };
|
|
150
|
+
}
|
|
151
|
+
|
|
122
152
|
// Registered class instance
|
|
123
153
|
const ctor = obj.constructor as Constructor;
|
|
124
154
|
const name = ctorToName.get(ctor);
|
|
@@ -167,6 +197,14 @@ export function deserialize<T>(value: T): T {
|
|
|
167
197
|
if (name === '__RegExp__') {
|
|
168
198
|
return new RegExp(data.source as string, data.flags as string);
|
|
169
199
|
}
|
|
200
|
+
if (name === '__Map__') {
|
|
201
|
+
const entries = data.entries as [unknown, unknown][];
|
|
202
|
+
return new Map(entries.map(([k, v]) => [deser(k), deser(v)]));
|
|
203
|
+
}
|
|
204
|
+
if (name === '__Set__') {
|
|
205
|
+
const entries = data.entries as unknown[];
|
|
206
|
+
return new Set(entries.map((v) => deser(v)));
|
|
207
|
+
}
|
|
170
208
|
|
|
171
209
|
const ctor = registry.get(name);
|
|
172
210
|
if (!ctor) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext } from 'preact';
|
|
2
|
-
import { useMemo } from 'preact/hooks';
|
|
2
|
+
import { useCallback, useMemo, useRef } from 'preact/hooks';
|
|
3
3
|
import { tokenize } from '../markup/tokenizer';
|
|
4
4
|
import { buildAST } from '../markup/ast';
|
|
5
5
|
import { renderNodes } from '../markup/render';
|
|
@@ -22,6 +22,11 @@ export function PassageDialog({
|
|
|
22
22
|
onClose,
|
|
23
23
|
showCloseButton = true,
|
|
24
24
|
}: PassageDialogProps) {
|
|
25
|
+
// Stabilize onClose so DialogCloseContext value doesn't change identity
|
|
26
|
+
const onCloseRef = useRef(onClose);
|
|
27
|
+
onCloseRef.current = onClose;
|
|
28
|
+
const stableOnClose = useCallback(() => onCloseRef.current(), []);
|
|
29
|
+
|
|
25
30
|
const storyData = useStoryStore((s) => s.storyData);
|
|
26
31
|
|
|
27
32
|
const passage = passageName
|
|
@@ -48,14 +53,14 @@ export function PassageDialog({
|
|
|
48
53
|
|
|
49
54
|
const handleBackdrop = (e: MouseEvent) => {
|
|
50
55
|
if ((e.target as HTMLElement).classList.contains('dialog-overlay')) {
|
|
51
|
-
|
|
56
|
+
stableOnClose();
|
|
52
57
|
}
|
|
53
58
|
};
|
|
54
59
|
|
|
55
60
|
const cls = panelClass ? `dialog-panel ${panelClass}` : 'dialog-panel';
|
|
56
61
|
|
|
57
62
|
return (
|
|
58
|
-
<DialogCloseContext.Provider value={
|
|
63
|
+
<DialogCloseContext.Provider value={stableOnClose}>
|
|
59
64
|
<div
|
|
60
65
|
class="dialog-overlay"
|
|
61
66
|
onClick={handleBackdrop}
|
|
@@ -64,7 +69,7 @@ export function PassageDialog({
|
|
|
64
69
|
{showCloseButton && (
|
|
65
70
|
<button
|
|
66
71
|
class="dialog-close"
|
|
67
|
-
onClick={
|
|
72
|
+
onClick={stableOnClose}
|
|
68
73
|
>
|
|
69
74
|
✕
|
|
70
75
|
</button>
|
|
@@ -15,9 +15,11 @@ defineMacro({
|
|
|
15
15
|
state.deleteVariable(name.slice(1));
|
|
16
16
|
} else if (name.startsWith('_')) {
|
|
17
17
|
state.deleteTemporary(name.slice(1));
|
|
18
|
+
} else if (name.startsWith('@')) {
|
|
19
|
+
ctx.update(name.slice(1), undefined);
|
|
18
20
|
} else {
|
|
19
21
|
console.error(
|
|
20
|
-
`spindle: {unset} expects a variable ($name or
|
|
22
|
+
`spindle: {unset} expects a variable ($name, _name, or @name), got "${name}"`,
|
|
21
23
|
);
|
|
22
24
|
}
|
|
23
25
|
}
|
package/src/define-macro.ts
CHANGED
|
@@ -130,7 +130,11 @@ export function defineMacro(config: MacroDefinition): void {
|
|
|
130
130
|
mutate: (code: string) => executeMutation(code, getValues(), update),
|
|
131
131
|
update,
|
|
132
132
|
getValues,
|
|
133
|
-
wrap:
|
|
133
|
+
wrap: (content: ComponentChildren): VNode<any> => {
|
|
134
|
+
if (className || id)
|
|
135
|
+
return h('span', { id, class: className }, content);
|
|
136
|
+
return h(Fragment, null, content);
|
|
137
|
+
},
|
|
134
138
|
};
|
|
135
139
|
|
|
136
140
|
if (config.merged) {
|
|
@@ -149,12 +153,6 @@ export function defineMacro(config: MacroDefinition): void {
|
|
|
149
153
|
ctx.setValue = (value: unknown) => setVariable(varName, value);
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
ctx.wrap = (content: ComponentChildren): VNode<any> => {
|
|
153
|
-
if (ctx.className || ctx.id)
|
|
154
|
-
return h('span', { id: ctx.id, class: ctx.className }, content);
|
|
155
|
-
return h(Fragment, null, content);
|
|
156
|
-
};
|
|
157
|
-
|
|
158
156
|
return config.render(props, ctx);
|
|
159
157
|
}
|
|
160
158
|
|
package/src/expression.ts
CHANGED
|
@@ -323,6 +323,14 @@ export function execute(
|
|
|
323
323
|
/**
|
|
324
324
|
* Convenience: evaluate using store state directly.
|
|
325
325
|
*/
|
|
326
|
+
/** Clear the compiled expression cache. Useful for testing and HMR. */
|
|
327
|
+
export function clearExpressionCache(): void {
|
|
328
|
+
fnCache.clear();
|
|
329
|
+
cachedFns = null;
|
|
330
|
+
cachedVisitCounts = null;
|
|
331
|
+
cachedRenderCounts = null;
|
|
332
|
+
}
|
|
333
|
+
|
|
326
334
|
export function evaluateWithState(expr: string, state: StoryState): unknown {
|
|
327
335
|
return evaluate(expr, state.variables, state.temporary, {});
|
|
328
336
|
}
|
package/src/hooks/use-action.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface UseActionOptions {
|
|
|
21
21
|
|
|
22
22
|
export function useAction(opts: UseActionOptions): string {
|
|
23
23
|
const idRef = useRef<string>('');
|
|
24
|
+
const performRef = useRef(opts.perform);
|
|
25
|
+
performRef.current = opts.perform;
|
|
24
26
|
|
|
25
27
|
// Generate ID only once on first call
|
|
26
28
|
if (!idRef.current) {
|
|
@@ -34,7 +36,7 @@ export function useAction(opts: UseActionOptions): string {
|
|
|
34
36
|
id,
|
|
35
37
|
type: opts.type,
|
|
36
38
|
label: opts.label,
|
|
37
|
-
perform:
|
|
39
|
+
perform: (...args) => performRef.current(...args),
|
|
38
40
|
};
|
|
39
41
|
if (opts.target !== undefined) action.target = opts.target;
|
|
40
42
|
if (opts.variable !== undefined) action.variable = opts.variable;
|
|
@@ -43,7 +45,16 @@ export function useAction(opts: UseActionOptions): string {
|
|
|
43
45
|
if (opts.disabled !== undefined) action.disabled = opts.disabled;
|
|
44
46
|
|
|
45
47
|
return registerAction(action);
|
|
46
|
-
}
|
|
48
|
+
}, [
|
|
49
|
+
id,
|
|
50
|
+
opts.type,
|
|
51
|
+
opts.label,
|
|
52
|
+
opts.target,
|
|
53
|
+
opts.variable,
|
|
54
|
+
opts.disabled,
|
|
55
|
+
opts.value,
|
|
56
|
+
JSON.stringify(opts.options),
|
|
57
|
+
]);
|
|
47
58
|
|
|
48
59
|
return id;
|
|
49
60
|
}
|
package/src/index.tsx
CHANGED
|
@@ -108,7 +108,27 @@ function boot() {
|
|
|
108
108
|
useStoryStore.getState().loadFromPayload(sessionPayload);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
//
|
|
111
|
+
// Pass 1: Pre-scan all widget passages to discover block widgets.
|
|
112
|
+
// Register them as block macros BEFORE any tokenize/buildAST calls,
|
|
113
|
+
// so that widget bodies using other block widgets parse correctly
|
|
114
|
+
// regardless of passage order.
|
|
115
|
+
const blockWidgetPattern =
|
|
116
|
+
/\{widget\s+["']?(\w+)["']?[^}]*\}([\s\S]*?)\{\/widget\}/g;
|
|
117
|
+
for (const [, passage] of storyData.passages) {
|
|
118
|
+
if (passage.tags.includes('widget')) {
|
|
119
|
+
let match;
|
|
120
|
+
while ((match = blockWidgetPattern.exec(passage.content)) !== null) {
|
|
121
|
+
const name = match[1]!;
|
|
122
|
+
const body = match[2]!;
|
|
123
|
+
if (/\{@children\}/.test(body)) {
|
|
124
|
+
registerBlockMacro(name);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
blockWidgetPattern.lastIndex = 0;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Pass 2: Full parse and register widgets from passages tagged "widget"
|
|
112
132
|
for (const [, passage] of storyData.passages) {
|
|
113
133
|
if (passage.tags.includes('widget')) {
|
|
114
134
|
const widgetTokens = tokenize(passage.content);
|
package/src/story-api.ts
CHANGED
|
@@ -383,6 +383,12 @@ function createStoryAPI(): StoryAPI {
|
|
|
383
383
|
};
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
declare global {
|
|
387
|
+
interface Window {
|
|
388
|
+
Story: StoryAPI;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
386
392
|
export function installStoryAPI(): void {
|
|
387
|
-
|
|
393
|
+
window.Story = createStoryAPI();
|
|
388
394
|
}
|