@rohal12/spindle 0.52.4 → 0.52.6

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.
Files changed (47) hide show
  1. package/dist/pkg/format.js +1 -1
  2. package/dist/pkg/headless.js +1502 -1521
  3. package/dist/pkg/macro-registry.json +255 -0
  4. package/package.json +1 -1
  5. package/src/class-registry.ts +6 -182
  6. package/src/components/macros/Button.tsx +2 -4
  7. package/src/components/macros/Checkbox.tsx +6 -13
  8. package/src/components/macros/Computed.tsx +38 -55
  9. package/src/components/macros/Cycle.tsx +1 -0
  10. package/src/components/macros/Dialog.tsx +7 -8
  11. package/src/components/macros/Do.tsx +2 -1
  12. package/src/components/macros/For.tsx +27 -78
  13. package/src/components/macros/Goto.tsx +6 -9
  14. package/src/components/macros/Include.tsx +10 -57
  15. package/src/components/macros/Listbox.tsx +1 -0
  16. package/src/components/macros/MacroError.tsx +37 -0
  17. package/src/components/macros/MacroLink.tsx +9 -24
  18. package/src/components/macros/Meter.tsx +19 -37
  19. package/src/components/macros/Numberbox.tsx +5 -32
  20. package/src/components/macros/PassageDisplay.tsx +37 -47
  21. package/src/components/macros/Print.tsx +1 -0
  22. package/src/components/macros/Radiobutton.tsx +8 -38
  23. package/src/components/macros/Repeat.tsx +3 -3
  24. package/src/components/macros/Set.tsx +3 -29
  25. package/src/components/macros/Switch.tsx +36 -37
  26. package/src/components/macros/Textarea.tsx +2 -28
  27. package/src/components/macros/Textbox.tsx +2 -29
  28. package/src/components/macros/Type.tsx +3 -3
  29. package/src/components/macros/Unset.tsx +17 -40
  30. package/src/components/macros/Watch.tsx +23 -91
  31. package/src/components/macros/WidgetInvocation.tsx +9 -71
  32. package/src/components/macros/detached-body.tsx +2 -2
  33. package/src/components/macros/input-macro.ts +48 -0
  34. package/src/components/macros/locals-scope.tsx +60 -0
  35. package/src/components/macros/macro-args.ts +280 -0
  36. package/src/components/macros/option-utils.ts +7 -12
  37. package/src/define-macro.ts +47 -30
  38. package/src/execute-mutation.ts +17 -97
  39. package/src/hooks/use-render-options.ts +26 -0
  40. package/src/markup/render.tsx +2 -4
  41. package/src/registry.ts +48 -0
  42. package/src/saves/save-manager.ts +190 -259
  43. package/src/saves/storage.ts +229 -402
  44. package/src/store.ts +244 -310
  45. package/src/structural.ts +388 -0
  46. package/types/index.d.ts +69 -6
  47. package/types/tooling.d.ts +33 -1
@@ -3,6 +3,13 @@
3
3
  "name": "set",
4
4
  "block": false,
5
5
  "subMacros": [],
6
+ "parameters": [
7
+ {
8
+ "name": "code",
9
+ "type": "expression",
10
+ "required": true
11
+ }
12
+ ],
6
13
  "source": "builtin"
7
14
  },
8
15
  {
@@ -11,6 +18,13 @@
11
18
  "subMacros": [],
12
19
  "interpolate": true,
13
20
  "merged": true,
21
+ "parameters": [
22
+ {
23
+ "name": "expression",
24
+ "type": "expression",
25
+ "required": true
26
+ }
27
+ ],
14
28
  "source": "builtin"
15
29
  },
16
30
  {
@@ -18,6 +32,22 @@
18
32
  "block": false,
19
33
  "subMacros": [],
20
34
  "merged": true,
35
+ "parameters": [
36
+ {
37
+ "name": "target",
38
+ "type": "variable",
39
+ "required": true
40
+ },
41
+ {
42
+ "name": "=",
43
+ "type": "separator"
44
+ },
45
+ {
46
+ "name": "expression",
47
+ "type": "expression",
48
+ "required": true
49
+ }
50
+ ],
21
51
  "source": "builtin"
22
52
  },
23
53
  {
@@ -26,6 +56,22 @@
26
56
  "subMacros": [],
27
57
  "interpolate": true,
28
58
  "merged": true,
59
+ "parameters": [
60
+ {
61
+ "name": "current",
62
+ "type": "expression",
63
+ "required": true
64
+ },
65
+ {
66
+ "name": "max",
67
+ "type": "expression",
68
+ "required": true
69
+ },
70
+ {
71
+ "name": "label",
72
+ "type": "string"
73
+ }
74
+ ],
29
75
  "source": "builtin"
30
76
  },
31
77
  {
@@ -42,6 +88,22 @@
42
88
  "subMacros": [],
43
89
  "interpolate": true,
44
90
  "merged": true,
91
+ "parameters": [
92
+ {
93
+ "name": "variables",
94
+ "type": "names",
95
+ "required": true
96
+ },
97
+ {
98
+ "name": "of",
99
+ "type": "separator"
100
+ },
101
+ {
102
+ "name": "list",
103
+ "type": "expression",
104
+ "required": true
105
+ }
106
+ ],
45
107
  "source": "builtin"
46
108
  },
47
109
  {
@@ -55,6 +117,13 @@
55
117
  "block": true,
56
118
  "subMacros": [],
57
119
  "interpolate": true,
120
+ "parameters": [
121
+ {
122
+ "name": "delay",
123
+ "type": "delay",
124
+ "required": true
125
+ }
126
+ ],
58
127
  "source": "builtin"
59
128
  },
60
129
  {
@@ -62,6 +131,13 @@
62
131
  "block": true,
63
132
  "subMacros": [],
64
133
  "interpolate": true,
134
+ "parameters": [
135
+ {
136
+ "name": "label",
137
+ "type": "text",
138
+ "required": true
139
+ }
140
+ ],
65
141
  "source": "builtin"
66
142
  },
67
143
  {
@@ -69,6 +145,17 @@
69
145
  "block": true,
70
146
  "subMacros": [],
71
147
  "interpolate": true,
148
+ "parameters": [
149
+ {
150
+ "name": "text",
151
+ "type": "string",
152
+ "required": true
153
+ },
154
+ {
155
+ "name": "passage",
156
+ "type": "string"
157
+ }
158
+ ],
72
159
  "source": "builtin"
73
160
  },
74
161
  {
@@ -79,6 +166,13 @@
79
166
  "default"
80
167
  ],
81
168
  "merged": true,
169
+ "parameters": [
170
+ {
171
+ "name": "expression",
172
+ "type": "expression",
173
+ "required": true
174
+ }
175
+ ],
82
176
  "source": "builtin"
83
177
  },
84
178
  {
@@ -101,6 +195,13 @@
101
195
  "block": true,
102
196
  "subMacros": [],
103
197
  "interpolate": true,
198
+ "parameters": [
199
+ {
200
+ "name": "delay",
201
+ "type": "delay",
202
+ "required": true
203
+ }
204
+ ],
104
205
  "source": "builtin"
105
206
  },
106
207
  {
@@ -162,6 +263,17 @@
162
263
  "block": true,
163
264
  "subMacros": [],
164
265
  "interpolate": true,
266
+ "parameters": [
267
+ {
268
+ "name": "label",
269
+ "type": "text",
270
+ "required": true
271
+ },
272
+ {
273
+ "name": "noclose",
274
+ "type": "flag"
275
+ }
276
+ ],
165
277
  "source": "builtin"
166
278
  },
167
279
  {
@@ -182,6 +294,17 @@
182
294
  "subMacros": [],
183
295
  "interpolate": true,
184
296
  "merged": true,
297
+ "parameters": [
298
+ {
299
+ "name": "inline",
300
+ "type": "flag"
301
+ },
302
+ {
303
+ "name": "passage",
304
+ "type": "expression",
305
+ "required": true
306
+ }
307
+ ],
185
308
  "source": "builtin"
186
309
  },
187
310
  {
@@ -195,12 +318,26 @@
195
318
  "block": false,
196
319
  "subMacros": [],
197
320
  "merged": true,
321
+ "parameters": [
322
+ {
323
+ "name": "passage",
324
+ "type": "expression",
325
+ "required": true
326
+ }
327
+ ],
198
328
  "source": "builtin"
199
329
  },
200
330
  {
201
331
  "name": "unset",
202
332
  "block": false,
203
333
  "subMacros": [],
334
+ "parameters": [
335
+ {
336
+ "name": "variable",
337
+ "type": "variable",
338
+ "required": true
339
+ }
340
+ ],
204
341
  "source": "builtin"
205
342
  },
206
343
  {
@@ -208,6 +345,17 @@
208
345
  "block": false,
209
346
  "subMacros": [],
210
347
  "storeVar": true,
348
+ "parameters": [
349
+ {
350
+ "name": "variable",
351
+ "type": "variable",
352
+ "required": true
353
+ },
354
+ {
355
+ "name": "placeholder",
356
+ "type": "string"
357
+ }
358
+ ],
211
359
  "source": "builtin"
212
360
  },
213
361
  {
@@ -215,6 +363,17 @@
215
363
  "block": false,
216
364
  "subMacros": [],
217
365
  "storeVar": true,
366
+ "parameters": [
367
+ {
368
+ "name": "variable",
369
+ "type": "variable",
370
+ "required": true
371
+ },
372
+ {
373
+ "name": "placeholder",
374
+ "type": "string"
375
+ }
376
+ ],
218
377
  "source": "builtin"
219
378
  },
220
379
  {
@@ -222,6 +381,17 @@
222
381
  "block": false,
223
382
  "subMacros": [],
224
383
  "storeVar": true,
384
+ "parameters": [
385
+ {
386
+ "name": "variable",
387
+ "type": "variable",
388
+ "required": true
389
+ },
390
+ {
391
+ "name": "placeholder",
392
+ "type": "string"
393
+ }
394
+ ],
225
395
  "source": "builtin"
226
396
  },
227
397
  {
@@ -229,6 +399,17 @@
229
399
  "block": false,
230
400
  "subMacros": [],
231
401
  "storeVar": true,
402
+ "parameters": [
403
+ {
404
+ "name": "variable",
405
+ "type": "variable",
406
+ "required": true
407
+ },
408
+ {
409
+ "name": "label",
410
+ "type": "text"
411
+ }
412
+ ],
232
413
  "source": "builtin"
233
414
  },
234
415
  {
@@ -236,6 +417,22 @@
236
417
  "block": false,
237
418
  "subMacros": [],
238
419
  "storeVar": true,
420
+ "parameters": [
421
+ {
422
+ "name": "variable",
423
+ "type": "variable",
424
+ "required": true
425
+ },
426
+ {
427
+ "name": "value",
428
+ "type": "text",
429
+ "required": true
430
+ },
431
+ {
432
+ "name": "label",
433
+ "type": "text"
434
+ }
435
+ ],
239
436
  "source": "builtin"
240
437
  },
241
438
  {
@@ -245,6 +442,13 @@
245
442
  "option"
246
443
  ],
247
444
  "storeVar": true,
445
+ "parameters": [
446
+ {
447
+ "name": "variable",
448
+ "type": "variable",
449
+ "required": true
450
+ }
451
+ ],
248
452
  "source": "builtin"
249
453
  },
250
454
  {
@@ -252,6 +456,13 @@
252
456
  "block": true,
253
457
  "subMacros": [],
254
458
  "storeVar": true,
459
+ "parameters": [
460
+ {
461
+ "name": "variable",
462
+ "type": "variable",
463
+ "required": true
464
+ }
465
+ ],
255
466
  "source": "builtin"
256
467
  },
257
468
  {
@@ -271,12 +482,56 @@
271
482
  "name": "watch",
272
483
  "block": false,
273
484
  "subMacros": [],
485
+ "parameters": [
486
+ {
487
+ "name": "condition",
488
+ "type": "string",
489
+ "required": true
490
+ },
491
+ {
492
+ "name": "options",
493
+ "type": "options",
494
+ "parameters": [
495
+ {
496
+ "name": "goto",
497
+ "type": "string"
498
+ },
499
+ {
500
+ "name": "dialog",
501
+ "type": "string"
502
+ },
503
+ {
504
+ "name": "run",
505
+ "type": "string"
506
+ },
507
+ {
508
+ "name": "name",
509
+ "type": "string"
510
+ },
511
+ {
512
+ "name": "priority",
513
+ "type": "number"
514
+ },
515
+ {
516
+ "name": "once",
517
+ "type": "flag"
518
+ }
519
+ ]
520
+ }
521
+ ],
274
522
  "source": "builtin"
275
523
  },
276
524
  {
277
525
  "name": "unwatch",
278
526
  "block": false,
279
527
  "subMacros": [],
528
+ "parameters": [
529
+ {
530
+ "name": "name",
531
+ "type": "text",
532
+ "required": true
533
+ }
534
+ ],
280
535
  "source": "builtin"
281
536
  }
282
537
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.52.4",
3
+ "version": "0.52.6",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -1,5 +1,11 @@
1
1
  // Class registry for preserving class instances across clone/save/load cycles.
2
2
 
3
+ import { setOwn } from './structural';
4
+ import { hasOwn } from './utils/namespace';
5
+
6
+ // Copying and comparing story values live with the structural diff
7
+ export { deepClone, deepEqual, type DeepCloneOptions } from './structural';
8
+
3
9
  type Constructor = new (...args: any[]) => any;
4
10
 
5
11
  const registry = new Map<string, Constructor>();
@@ -33,188 +39,6 @@ export function clearRegistry(): void {
33
39
  ctorToName.clear();
34
40
  }
35
41
 
36
- // --- Deep Clone ---
37
-
38
- function isPlainObject(value: unknown): value is Record<string, unknown> {
39
- if (typeof value !== 'object' || value === null) return false;
40
- const proto = Object.getPrototypeOf(value);
41
- return proto === Object.prototype || proto === null;
42
- }
43
-
44
- /**
45
- * Set an own enumerable property. A "__proto__" key is defined as an own
46
- * property (as JSON.parse does) instead of replacing the prototype.
47
- */
48
- function setOwn(target: object, key: string, value: unknown): void {
49
- if (key === '__proto__') {
50
- Object.defineProperty(target, key, {
51
- value,
52
- enumerable: true,
53
- writable: true,
54
- configurable: true,
55
- });
56
- } else {
57
- (target as Record<string, unknown>)[key] = value;
58
- }
59
- }
60
-
61
- export interface DeepCloneOptions {
62
- /**
63
- * Return instances of unregistered classes (DOM nodes, promises, other
64
- * library objects) by reference instead of copying their own keys into a
65
- * plain object, which would lose their prototype and identity.
66
- */
67
- keepUnregistered?: boolean;
68
- }
69
-
70
- export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
71
- const seen = new Map<object, object>();
72
-
73
- function clone(val: unknown): unknown {
74
- if (val === null || typeof val !== 'object') return val;
75
-
76
- const obj = val as object;
77
- if (seen.has(obj)) return seen.get(obj);
78
-
79
- if (val instanceof Date) return new Date(val.getTime()) as unknown;
80
- if (val instanceof RegExp)
81
- return new RegExp(val.source, val.flags) as unknown;
82
-
83
- if (Array.isArray(val)) {
84
- const arr: unknown[] = [];
85
- seen.set(obj, arr);
86
- for (let i = 0; i < val.length; i++) {
87
- arr[i] = clone(val[i]);
88
- }
89
- return arr;
90
- }
91
-
92
- if (val instanceof Map) {
93
- const copy = new Map();
94
- seen.set(obj, copy);
95
- for (const [k, v] of val) {
96
- copy.set(clone(k), clone(v));
97
- }
98
- return copy;
99
- }
100
-
101
- if (val instanceof Set) {
102
- const copy = new Set();
103
- seen.set(obj, copy);
104
- for (const v of val) {
105
- copy.add(clone(v));
106
- }
107
- return copy;
108
- }
109
-
110
- // Registered class instance
111
- const name = registeredClassName(obj);
112
- if (name !== undefined) {
113
- const copy = Object.create(Object.getPrototypeOf(obj)) as Record<
114
- string,
115
- unknown
116
- >;
117
- seen.set(obj, copy);
118
- for (const key of Object.keys(obj)) {
119
- setOwn(copy, key, clone((obj as Record<string, unknown>)[key]));
120
- }
121
- return copy;
122
- }
123
-
124
- // Plain object (or unregistered class — treat as plain). A plain object
125
- // keeps its prototype, which may be null.
126
- const plain = isPlainObject(val);
127
- if (!plain && options.keepUnregistered) return val;
128
- const copy = (
129
- plain ? Object.create(Object.getPrototypeOf(obj) as object | null) : {}
130
- ) as Record<string, unknown>;
131
- seen.set(obj, copy);
132
- for (const key of Object.keys(obj)) {
133
- setOwn(copy, key, clone((obj as Record<string, unknown>)[key]));
134
- }
135
- return copy;
136
- }
137
-
138
- return clone(value) as T;
139
- }
140
-
141
- // --- Deep Equal ---
142
-
143
- /**
144
- * Structural equality over the value types deepClone() supports: primitives,
145
- * arrays, plain objects, class instances, Date, RegExp, Map and Set (nested
146
- * at any depth). Map and Set entries are compared in insertion order. Arrays
147
- * are compared by length and index, so a hole equals an undefined element
148
- * (deepClone() and save/load turn holes into undefined elements).
149
- */
150
- export function deepEqual(a: unknown, b: unknown): boolean {
151
- return equal(a, b, new Map());
152
- }
153
-
154
- /**
155
- * `assumed` holds the pairs already being compared: meeting one again (a
156
- * cycle) assumes it equal. Every object may pair with several others, since
157
- * cycles of different lengths can still unfold to the same value.
158
- */
159
- function equal(
160
- a: unknown,
161
- b: unknown,
162
- assumed: Map<object, Set<object>>,
163
- ): boolean {
164
- if (Object.is(a, b)) return true;
165
- if (
166
- a === null ||
167
- b === null ||
168
- typeof a !== 'object' ||
169
- typeof b !== 'object'
170
- ) {
171
- return false;
172
- }
173
- if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
174
- const pairs = assumed.get(a);
175
- if (pairs?.has(b)) return true;
176
- if (pairs) pairs.add(b);
177
- else assumed.set(a, new Set([b]));
178
-
179
- if (a instanceof Date) return Object.is(a.getTime(), (b as Date).getTime());
180
- if (a instanceof RegExp) return String(a) === String(b);
181
- if (a instanceof Map || a instanceof Set) {
182
- const bc = b as Map<unknown, unknown> | Set<unknown>;
183
- if (a.size !== bc.size) return false;
184
- const ai = a.entries();
185
- const bi = bc.entries();
186
- for (
187
- let x = ai.next(), y = bi.next();
188
- !x.done;
189
- x = ai.next(), y = bi.next()
190
- ) {
191
- if (!equal(x.value, y.value, assumed)) return false;
192
- }
193
- return true;
194
- }
195
-
196
- if (Array.isArray(a)) {
197
- const bc = b as unknown[];
198
- if (a.length !== bc.length) return false;
199
- for (let i = 0; i < a.length; i++) {
200
- if (!equal(a[i], bc[i], assumed)) return false;
201
- }
202
- return true;
203
- }
204
-
205
- const ao = a as Record<string, unknown>;
206
- const bo = b as Record<string, unknown>;
207
- const keys = Object.keys(ao);
208
- if (keys.length !== Object.keys(bo).length) return false;
209
- for (const key of keys) {
210
- if (!hasOwn(bo, key) || !equal(ao[key], bo[key], assumed)) return false;
211
- }
212
- return true;
213
- }
214
-
215
- const hasOwn = (obj: object, key: string): boolean =>
216
- Object.prototype.hasOwnProperty.call(obj, key);
217
-
218
42
  // --- Serialize ---
219
43
 
220
44
  const CLASS_TAG = '__spindle_class__';
@@ -1,15 +1,13 @@
1
1
  import { defineMacro } from '../../define-macro';
2
- import { readWholeQuoted } from './arg-utils';
3
2
  import { useDetachedBody } from './detached-body';
4
3
 
5
4
  defineMacro({
6
5
  name: 'button',
7
6
  block: true,
8
7
  interpolate: true,
8
+ parameters: [{ name: 'label', type: 'text', required: true }],
9
9
  render({ rawArgs, children = [] }, ctx) {
10
- const text =
11
- readWholeQuoted(rawArgs.trim()) ?? rawArgs.replace(/^["']|["']$/g, '');
12
- const label = ctx.resolve?.(text) ?? rawArgs;
10
+ const label = ctx.resolve?.(ctx.args.label ?? '') ?? rawArgs;
13
11
  const runBody = useDetachedBody();
14
12
 
15
13
  // Run the body outside the passage tree: all macro side effects ({set},
@@ -1,21 +1,14 @@
1
1
  import { defineMacro } from '../../define-macro';
2
- import { readWholeQuoted, stripLooseQuotes } from './arg-utils';
3
-
4
- export function parseCheckboxLabel(rawArgs: string): string {
5
- // `s`: a quoted label may span lines. Its trailing whitespace is trimmed
6
- // here, not by the regex: `(.+?)\s*$` tries every position of a run of
7
- // spaces in the label, taking time quadratic in its length.
8
- const match = rawArgs.match(/^\s*["']?\$?[\w.]+["']?\s+(.+)$/s);
9
- if (!match) return '';
10
- const rest = match[1]!.trimEnd() || match[1]!;
11
- return readWholeQuoted(rest) ?? stripLooseQuotes(rest);
12
- }
13
2
 
14
3
  defineMacro({
15
4
  name: 'checkbox',
16
5
  storeVar: true,
17
- render({ rawArgs }, ctx) {
18
- const label = parseCheckboxLabel(rawArgs);
6
+ parameters: [
7
+ { name: 'variable', type: 'variable', required: true },
8
+ { name: 'label', type: 'text' },
9
+ ],
10
+ render(_props, ctx) {
11
+ const label = ctx.args.label ?? '';
19
12
 
20
13
  ctx.useAction({
21
14
  type: 'checkbox',