@rohal12/spindle 0.59.13 → 0.59.14
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/dist/pkg/headless.js +320 -74
- package/dist/pkg/story-variables.js +17 -11
- package/package.json +1 -1
- package/src/class-registry.ts +133 -14
- package/src/components/macros/WidgetInvocation.tsx +7 -2
- package/src/execute-mutation.ts +10 -0
- package/src/hooks/use-modal-focus.ts +46 -16
- package/src/interpolation.ts +25 -5
- package/src/markup/render.tsx +11 -4
- package/src/structural.ts +85 -21
- package/src/utils/value-kinds.ts +14 -0
- package/src/widgets/widget-def.ts +27 -15
package/src/structural.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { registeredClassName } from './class-registry';
|
|
13
13
|
import { hasOwn, setOwn } from './utils/namespace';
|
|
14
14
|
import { deleteByPath, getByPath, setByPath } from './utils/object-path';
|
|
15
|
-
import { isAtomic, isBoxed, isTemporal } from './utils/value-kinds';
|
|
15
|
+
import { extraKeys, isAtomic, isBoxed, isTemporal } from './utils/value-kinds';
|
|
16
16
|
|
|
17
17
|
export { isAtomic };
|
|
18
18
|
|
|
@@ -24,6 +24,26 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
24
24
|
return proto === Object.prototype || proto === null;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* `empty`, a new collection (Array, Map or Set) standing for `obj`, made
|
|
29
|
+
* like it: an instance of a registered subclass keeps its class and its
|
|
30
|
+
* extra keys (#391), with the values `value` gives for them; other
|
|
31
|
+
* collections are plain. `made` gets it before its keys are filled, so
|
|
32
|
+
* that cycles through it find it.
|
|
33
|
+
*/
|
|
34
|
+
function collectionLike<C extends object>(
|
|
35
|
+
obj: object,
|
|
36
|
+
empty: C,
|
|
37
|
+
made: (copy: C) => void,
|
|
38
|
+
value: (key: string) => unknown,
|
|
39
|
+
): C {
|
|
40
|
+
made(empty);
|
|
41
|
+
if (registeredClassName(obj) === undefined) return empty;
|
|
42
|
+
Object.setPrototypeOf(empty, Object.getPrototypeOf(obj) as object);
|
|
43
|
+
for (const key of extraKeys(obj)) setOwn(empty, key, value(key));
|
|
44
|
+
return empty;
|
|
45
|
+
}
|
|
46
|
+
|
|
27
47
|
/** Own keys of an error that are not enumerable but are data. */
|
|
28
48
|
const ERROR_HIDDEN_KEYS = ['message', 'cause', 'errors'] as const;
|
|
29
49
|
|
|
@@ -131,6 +151,15 @@ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
|
|
|
131
151
|
: copy;
|
|
132
152
|
}
|
|
133
153
|
|
|
154
|
+
/** `empty`, made the copy of the collection `obj` (see collectionLike). */
|
|
155
|
+
const collection = <C extends object>(obj: object, empty: C): C =>
|
|
156
|
+
collectionLike(
|
|
157
|
+
obj,
|
|
158
|
+
empty,
|
|
159
|
+
(copy) => keep(obj, copy),
|
|
160
|
+
(key) => clone((obj as Record<string, unknown>)[key]),
|
|
161
|
+
);
|
|
162
|
+
|
|
134
163
|
function clone(val: unknown): unknown {
|
|
135
164
|
if (val === null || typeof val !== 'object') return val;
|
|
136
165
|
|
|
@@ -139,22 +168,25 @@ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
|
|
|
139
168
|
|
|
140
169
|
if (Array.isArray(val)) {
|
|
141
170
|
// Holes stay holes
|
|
142
|
-
const arr =
|
|
171
|
+
const arr = collection(obj, new Array(val.length) as unknown[]);
|
|
143
172
|
for (let i = 0; i < val.length; i++) {
|
|
144
173
|
if (i in val) arr[i] = clone(val[i]);
|
|
145
174
|
}
|
|
146
175
|
return arr;
|
|
147
176
|
}
|
|
148
177
|
|
|
178
|
+
// Filled with the built-in methods: a subclass may override them
|
|
149
179
|
if (val instanceof Map) {
|
|
150
|
-
const copy =
|
|
151
|
-
for (const [k, v] of val)
|
|
180
|
+
const copy = collection(obj, new Map());
|
|
181
|
+
for (const [k, v] of val) {
|
|
182
|
+
Map.prototype.set.call(copy, settled(clone(k), k), clone(v));
|
|
183
|
+
}
|
|
152
184
|
return copy;
|
|
153
185
|
}
|
|
154
186
|
|
|
155
187
|
if (val instanceof Set) {
|
|
156
|
-
const copy =
|
|
157
|
-
for (const v of val)
|
|
188
|
+
const copy = collection(obj, new Set());
|
|
189
|
+
for (const v of val) Set.prototype.add.call(copy, settled(clone(v), v));
|
|
158
190
|
return copy;
|
|
159
191
|
}
|
|
160
192
|
|
|
@@ -295,15 +327,23 @@ export function shareEqual<T>(prev: unknown, curr: T): T {
|
|
|
295
327
|
if (placed !== undefined) return placed;
|
|
296
328
|
if (!isObjectValue(p)) return fresh(c);
|
|
297
329
|
if (reuse(p, c)) return p;
|
|
330
|
+
const pr = p as Record<string, unknown>;
|
|
331
|
+
const cr = c as Record<string, unknown>;
|
|
332
|
+
/** What the result holds at `key` of `c`. */
|
|
333
|
+
const shareKey = (key: string) =>
|
|
334
|
+
share(hasOwn(pr, key) ? pr[key] : undefined, cr[key]);
|
|
335
|
+
/** `empty`, made the result for the collection `c` (see collectionLike). */
|
|
336
|
+
const collection = <C extends object>(empty: C): C =>
|
|
337
|
+
collectionLike(c, empty, (out) => done.set(c, out), shareKey);
|
|
298
338
|
if (c instanceof Map && p instanceof Map) {
|
|
299
|
-
const out = new Map();
|
|
300
|
-
|
|
301
|
-
|
|
339
|
+
const out = collection(new Map());
|
|
340
|
+
for (const [k, v] of c) {
|
|
341
|
+
Map.prototype.set.call(out, fresh(k), share(p.get(k), v));
|
|
342
|
+
}
|
|
302
343
|
return out;
|
|
303
344
|
}
|
|
304
345
|
if (Array.isArray(c) && Array.isArray(p)) {
|
|
305
|
-
const out = new Array(c.length) as unknown[];
|
|
306
|
-
done.set(c, out);
|
|
346
|
+
const out = collection(new Array(c.length) as unknown[]);
|
|
307
347
|
for (let i = 0; i < c.length; i++) {
|
|
308
348
|
if (i in c) out[i] = share(p[i], c[i]);
|
|
309
349
|
}
|
|
@@ -312,10 +352,7 @@ export function shareEqual<T>(prev: unknown, curr: T): T {
|
|
|
312
352
|
if (mergesWith(p, c, false)) {
|
|
313
353
|
const out = Object.create(Object.getPrototypeOf(c) as object | null);
|
|
314
354
|
done.set(c, out);
|
|
315
|
-
const
|
|
316
|
-
for (const key of Object.keys(cr)) {
|
|
317
|
-
setOwn(out, key, share(hasOwn(p, key) ? p[key] : undefined, cr[key]));
|
|
318
|
-
}
|
|
355
|
+
for (const key of Object.keys(cr)) setOwn(out, key, shareKey(key));
|
|
319
356
|
return out;
|
|
320
357
|
}
|
|
321
358
|
return fresh(c);
|
|
@@ -382,17 +419,40 @@ function equalBuiltin(
|
|
|
382
419
|
return undefined;
|
|
383
420
|
}
|
|
384
421
|
|
|
385
|
-
function equalKeys(
|
|
422
|
+
function equalKeys(
|
|
423
|
+
a: object,
|
|
424
|
+
b: object,
|
|
425
|
+
assumed: Pairs,
|
|
426
|
+
keysOf: (o: object) => string[] = Object.keys,
|
|
427
|
+
): boolean {
|
|
386
428
|
const ao = a as Record<string, unknown>;
|
|
387
429
|
const bo = b as Record<string, unknown>;
|
|
388
|
-
const keys =
|
|
389
|
-
if (keys.length !==
|
|
430
|
+
const keys = keysOf(ao);
|
|
431
|
+
if (keys.length !== keysOf(bo).length) return false;
|
|
390
432
|
for (const key of keys) {
|
|
391
433
|
if (!hasOwn(bo, key) || !equal(ao[key], bo[key], assumed)) return false;
|
|
392
434
|
}
|
|
393
435
|
return true;
|
|
394
436
|
}
|
|
395
437
|
|
|
438
|
+
/** Built-in collection prototypes, whose instances hold no extra keys. */
|
|
439
|
+
const COLLECTION_PROTOS: ReadonlySet<object | null> = new Set([
|
|
440
|
+
Array.prototype,
|
|
441
|
+
Map.prototype,
|
|
442
|
+
Set.prototype,
|
|
443
|
+
]);
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Equality of the extra keys (see extraKeys) of two collections of one
|
|
447
|
+
* class: those of a subclass instance are part of its value.
|
|
448
|
+
*/
|
|
449
|
+
function equalExtraKeys(a: object, b: object, assumed: Pairs): boolean {
|
|
450
|
+
return (
|
|
451
|
+
COLLECTION_PROTOS.has(Object.getPrototypeOf(a) as object | null) ||
|
|
452
|
+
equalKeys(a, b, assumed, extraKeys)
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
396
456
|
/** `assumed` tracks the pairs of objects met (see Pairs). */
|
|
397
457
|
function equal(a: unknown, b: unknown, assumed: Pairs): boolean {
|
|
398
458
|
if (Object.is(a, b) && !(assumed.strict && isObjectValue(a))) return true;
|
|
@@ -420,7 +480,7 @@ function equal(a: unknown, b: unknown, assumed: Pairs): boolean {
|
|
|
420
480
|
) {
|
|
421
481
|
if (!equal(x.value, y.value, assumed)) return false;
|
|
422
482
|
}
|
|
423
|
-
return
|
|
483
|
+
return equalExtraKeys(a, b, assumed);
|
|
424
484
|
}
|
|
425
485
|
|
|
426
486
|
const builtin = equalBuiltin(a, b, assumed);
|
|
@@ -432,7 +492,7 @@ function equal(a: unknown, b: unknown, assumed: Pairs): boolean {
|
|
|
432
492
|
for (let i = 0; i < a.length; i++) {
|
|
433
493
|
if (i in a !== i in bc || !equal(a[i], bc[i], assumed)) return false;
|
|
434
494
|
}
|
|
435
|
-
return
|
|
495
|
+
return equalExtraKeys(a, b, assumed);
|
|
436
496
|
}
|
|
437
497
|
|
|
438
498
|
return equalKeys(a, b, assumed);
|
|
@@ -466,7 +526,11 @@ export function mergesWith(
|
|
|
466
526
|
arrays: boolean,
|
|
467
527
|
): a is Record<string, unknown> {
|
|
468
528
|
if (arrays && (Array.isArray(a) || Array.isArray(b))) {
|
|
469
|
-
return
|
|
529
|
+
return (
|
|
530
|
+
Array.isArray(a) &&
|
|
531
|
+
Array.isArray(b) &&
|
|
532
|
+
Object.getPrototypeOf(a) === Object.getPrototypeOf(b)
|
|
533
|
+
);
|
|
470
534
|
}
|
|
471
535
|
return (
|
|
472
536
|
isMergeable(a) &&
|
package/src/utils/value-kinds.ts
CHANGED
|
@@ -43,3 +43,17 @@ export function atomicName(value: object): string | undefined {
|
|
|
43
43
|
const tag = value instanceof Error ? 'Error' : toStringTag(value);
|
|
44
44
|
return `${/^[AEIOU]/.test(tag) ? 'an' : 'a'} ${tag}`;
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/** Whether `key` is an index of an array element. */
|
|
48
|
+
const isIndexKey = (key: string): boolean =>
|
|
49
|
+
key !== '4294967295' && String(Number(key) >>> 0) === key;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The own enumerable string keys of a collection (an Array, Map or Set) that
|
|
53
|
+
* are not its elements: what an instance of a subclass may hold besides
|
|
54
|
+
* them.
|
|
55
|
+
*/
|
|
56
|
+
export function extraKeys(collection: object): string[] {
|
|
57
|
+
const keys = Object.keys(collection);
|
|
58
|
+
return Array.isArray(collection) ? keys.filter((k) => !isIndexKey(k)) : keys;
|
|
59
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { parseMacroArgs } from '../components/macros/macro-args';
|
|
6
6
|
import type { MacroArgs } from '../registry';
|
|
7
|
+
import { tokenizeMarkupTolerant } from '../markup/parse';
|
|
7
8
|
|
|
8
9
|
/** A {widget} definition's name, then its `@` parameters. */
|
|
9
10
|
export const WIDGET_PARAMETERS = [
|
|
@@ -36,20 +37,15 @@ export function parseWidgetDef(rawArgs: string): WidgetDef {
|
|
|
36
37
|
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
/**
|
|
40
|
-
* A definition: its arguments (read like a {widget} macro's, see
|
|
41
|
-
* parseWidgetDef) and its body. Macro names are not case-sensitive.
|
|
42
|
-
*/
|
|
43
|
-
const WIDGET_DEFINITION = /\{widget\s+([^}]*)\}([\s\S]*?)\{\/widget\}/gi;
|
|
44
|
-
|
|
45
|
-
/** `{@children}`, also with selectors such as `{.highlight @children}`. */
|
|
46
|
-
const CHILDREN_PLACEHOLDER = /\{[^{}]*@children\s*\}/;
|
|
47
|
-
|
|
48
40
|
/**
|
|
49
41
|
* The names of the block widgets (those whose body renders `{@children}`)
|
|
50
42
|
* that StoryInit and the passages tagged `widget` define. They must be
|
|
51
43
|
* known as block macros before any passage is parsed, so that passages
|
|
52
44
|
* invoking them nest their content whatever the passage order.
|
|
45
|
+
*
|
|
46
|
+
* The passages are read as flat tokens, which need no block macros to be
|
|
47
|
+
* known, so a `{@children}` in an HTML comment or in a `{do}` body is text
|
|
48
|
+
* here as it is to the parser, and does not make a block widget (#387).
|
|
53
49
|
*/
|
|
54
50
|
export function blockWidgetNames(
|
|
55
51
|
passages: Iterable<{ name: string; tags?: string[]; content: string }>,
|
|
@@ -59,12 +55,28 @@ export function blockWidgetNames(
|
|
|
59
55
|
if (passage.name !== 'StoryInit' && !passage.tags?.includes('widget')) {
|
|
60
56
|
continue;
|
|
61
57
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
// The open definitions: their arguments, and whether a slot was seen
|
|
59
|
+
const open: { rawArgs: string; isBlock: boolean }[] = [];
|
|
60
|
+
for (const token of tokenizeMarkupTolerant(passage.content).tokens) {
|
|
61
|
+
if (token.type === 'macro' && token.name.toLowerCase() === 'widget') {
|
|
62
|
+
if (!token.isClose) {
|
|
63
|
+
open.push({ rawArgs: token.rawArgs, isBlock: false });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const def = open.pop();
|
|
67
|
+
if (!def?.isBlock) continue;
|
|
68
|
+
try {
|
|
69
|
+
names.push(parseWidgetDef(def.rawArgs).name);
|
|
70
|
+
} catch {
|
|
71
|
+
// The {widget} macro reports a definition it cannot read
|
|
72
|
+
}
|
|
73
|
+
} else if (
|
|
74
|
+
token.type === 'variable' &&
|
|
75
|
+
token.scope === 'local' &&
|
|
76
|
+
token.name === 'children' &&
|
|
77
|
+
open.length > 0
|
|
78
|
+
) {
|
|
79
|
+
open[open.length - 1]!.isBlock = true;
|
|
68
80
|
}
|
|
69
81
|
}
|
|
70
82
|
}
|