@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/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 = keep(obj, new Array(val.length) as unknown[]);
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 = keep(obj, new Map());
151
- for (const [k, v] of val) copy.set(settled(clone(k), k), clone(v));
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 = keep(obj, new Set());
157
- for (const v of val) copy.add(settled(clone(v), v));
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
- done.set(c, out);
301
- for (const [k, v] of c) out.set(fresh(k), share(p.get(k), v));
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 cr = c as Record<string, unknown>;
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(a: object, b: object, assumed: Pairs): boolean {
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 = Object.keys(ao);
389
- if (keys.length !== Object.keys(bo).length) return false;
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 true;
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 true;
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 Array.isArray(a) && Array.isArray(b);
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) &&
@@ -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
- for (const match of passage.content.matchAll(WIDGET_DEFINITION)) {
63
- if (!CHILDREN_PLACEHOLDER.test(match[2]!)) continue;
64
- try {
65
- names.push(parseWidgetDef(match[1]!).name);
66
- } catch {
67
- // The {widget} macro reports a definition it cannot read
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
  }