@rohal12/spindle 0.59.12 → 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.
@@ -40,6 +40,25 @@ export function hasInterpolation(s: string): boolean {
40
40
  return s.includes('{') || s.includes('}');
41
41
  }
42
42
 
43
+ /**
44
+ * The children an invocation passes to a block widget, for `{@children}`.
45
+ * `outer` is the children context the invocation itself was written in: a
46
+ * `{@children}` among `nodes` forwards the enclosing widget's children, so it
47
+ * resolves there, not against `nodes` again (#386).
48
+ */
49
+ export interface WidgetChildren {
50
+ readonly nodes: ASTNode[];
51
+ readonly outer: WidgetChildren | null;
52
+ }
53
+
54
+ /** The children context for an invocation's `children`, or null if none. */
55
+ export function widgetChildrenOf(
56
+ children: ASTNode[] | undefined,
57
+ outer: WidgetChildren | null | undefined,
58
+ ): WidgetChildren | null {
59
+ return children?.length ? { nodes: children, outer: outer ?? null } : null;
60
+ }
61
+
43
62
  /** The scope text-only markup is evaluated in. */
44
63
  export interface TextScope {
45
64
  variables: Record<string, unknown>;
@@ -47,7 +66,7 @@ export interface TextScope {
47
66
  locals: Record<string, unknown>;
48
67
  transient: Record<string, unknown>;
49
68
  /** Invocation children of the enclosing block widget, for `{@children}`. */
50
- widgetChildren?: ASTNode[] | null;
69
+ widgetChildren?: WidgetChildren | null;
51
70
  }
52
71
 
53
72
  /** A macro, expression or markup error met while evaluating text. */
@@ -177,11 +196,12 @@ class TextEvaluator {
177
196
  case 'variable':
178
197
  if (node.scope === 'local' && node.name === 'children') {
179
198
  // As {@children} in a widget body: the invocation's children, in
180
- // the scope of the body.
199
+ // the scope of the body, and in the children context they were
200
+ // written in.
181
201
  const children = scope.widgetChildren;
182
202
  if (!children) return '';
183
- return this.nested('children', children, scope, depth, {
184
- widgetChildren: null,
203
+ return this.nested('children', children.nodes, scope, depth, {
204
+ widgetChildren: children.outer,
185
205
  });
186
206
  }
187
207
  return this.variable(node.scope, node.name, scope);
@@ -262,7 +282,7 @@ class TextEvaluator {
262
282
  }
263
283
  return this.nested(node.name, widget.body, scope, depth, {
264
284
  locals,
265
- widgetChildren: node.children.length > 0 ? node.children : null,
285
+ widgetChildren: widgetChildrenOf(node.children, scope.widgetChildren),
266
286
  });
267
287
  }
268
288
 
@@ -9,6 +9,7 @@ import { markdownToHtml } from './markdown';
9
9
  import { h } from 'preact';
10
10
  import type { ASTNode, HtmlNode, MacroNode } from './ast';
11
11
  import { useTextScope } from '../hooks/use-interpolate';
12
+ import type { WidgetChildren } from '../interpolation';
12
13
  import { useRenderOptions } from '../hooks/use-render-options';
13
14
  import {
14
15
  hasInterpolation,
@@ -52,7 +53,7 @@ export const InlineContext = createContext(false);
52
53
  /**
53
54
  * True while rendering inside an element whose content is not markdown: SVG
54
55
  * (whose namespace `<p>` wrappers would break) and the preformatted `<pre>`
55
- * and `<textarea>`, whose text (indentation, `#`, `*`, ...) is literal.
56
+ * `<textarea>` and `<style>`, whose text (indentation, `#`, `*`, ...) is literal.
56
57
  * Macro and widget bodies read it so their content stays literal too.
57
58
  */
58
59
  export const RawTextContext = createContext(false);
@@ -66,8 +67,8 @@ const SvgContext = createContext(false);
66
67
  const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
67
68
 
68
69
  /** Elements whose content is literal text, not markdown. */
69
- const PREFORMATTED_ELEMENTS = new Set(['pre', 'textarea']);
70
- export const WidgetChildrenContext = createContext<ASTNode[] | null>(null);
70
+ const PREFORMATTED_ELEMENTS = new Set(['pre', 'textarea', 'style']);
71
+ export const WidgetChildrenContext = createContext<WidgetChildren | null>(null);
71
72
 
72
73
  /**
73
74
  * Components rendered for the non-text nodes of one renderNodes() call. Each
@@ -392,6 +393,7 @@ const INLINE_ELEMENTS = new Set([
392
393
  'span',
393
394
  'strong',
394
395
  'sub',
396
+ 'summary',
395
397
  'sup',
396
398
  'time',
397
399
  'u',
@@ -632,6 +634,15 @@ function resolveAttributeValue(
632
634
  return result.text;
633
635
  }
634
636
 
637
+ /** The index of the summary element opening `nodes`, or -1 when none does. */
638
+ function leadingSummaryIndex(nodes: ASTNode[]): number {
639
+ const at = nodes.findIndex((n) => n.type !== 'text' || n.value.trim() !== '');
640
+ const first = nodes[at];
641
+ return first?.type === 'html' && first.tag.toLowerCase() === 'summary'
642
+ ? at
643
+ : -1;
644
+ }
645
+
635
646
  function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
636
647
  const scope = useTextScope();
637
648
  const { nobr, locals, raw: inRaw, inline: parentInline } = useRenderOptions();
@@ -658,7 +669,22 @@ function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
658
669
  if (inRaw || isRawRoot) {
659
670
  children = renderInlineNodes(node.children);
660
671
  } else {
661
- children = renderNodes(node.children, { nobr, locals, inline: isInline });
672
+ // A details element's summary stays its direct child: it is rendered
673
+ // apart from the markdown of the rest of the content.
674
+ const summaryAt =
675
+ tag === 'details' ? leadingSummaryIndex(node.children) : -1;
676
+ const body = (nodes: ASTNode[]) =>
677
+ renderNodes(nodes, { nobr, locals, inline: isInline });
678
+ children =
679
+ summaryAt === -1 ? (
680
+ body(node.children)
681
+ ) : (
682
+ <>
683
+ {body(node.children.slice(0, summaryAt))}
684
+ {renderSingleNode(node.children[summaryAt]!)}
685
+ {body(node.children.slice(summaryAt + 1))}
686
+ </>
687
+ );
662
688
  if (isInline !== parentInline) {
663
689
  children = (
664
690
  <InlineContext.Provider value={isInline}>
@@ -683,10 +709,16 @@ function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
683
709
  }
684
710
 
685
711
  function ChildrenSlot() {
686
- const childrenAST = useContext(WidgetChildrenContext);
712
+ const children = useContext(WidgetChildrenContext);
687
713
  const renderOptions = useRenderOptions();
688
- if (!childrenAST || childrenAST.length === 0) return null;
689
- return <>{renderNodes(childrenAST, renderOptions)}</>;
714
+ if (!children) return null;
715
+ // The children render with the widget's locals, but a {@children} among
716
+ // them forwards the enclosing widget's children (#386).
717
+ return (
718
+ <WidgetChildrenContext.Provider value={children.outer}>
719
+ {renderNodes(children.nodes, renderOptions)}
720
+ </WidgetChildrenContext.Provider>
721
+ );
690
722
  }
691
723
 
692
724
  /**
package/src/story-api.ts CHANGED
@@ -23,6 +23,7 @@ import { getBackendType } from './saves/storage';
23
23
  import { registerClass } from './class-registry';
24
24
  import {
25
25
  frozenCopy,
26
+ readOnlyValue,
26
27
  getActiveMutationScope,
27
28
  mutateState,
28
29
  } from './execute-mutation';
@@ -286,7 +287,7 @@ function createStoryAPI(): StoryAPI {
286
287
  const value = key.includes('.')
287
288
  ? getByPath(namespace, key.split('.'))
288
289
  : ownValue(namespace, key);
289
- return scope ? frozenCopy(value) : value;
290
+ return scope ? frozenCopy(value) : readOnlyValue(value);
290
291
  },
291
292
 
292
293
  set(nameOrVars: string | Record<string, unknown>, value?: unknown): void {
@@ -32,7 +32,13 @@ function tokensOf(text: string, textMode: boolean): Token[] {
32
32
  }
33
33
  }
34
34
 
35
- export type VarType = 'number' | 'string' | 'boolean' | 'array' | 'object';
35
+ export type VarType =
36
+ | 'number'
37
+ | 'string'
38
+ | 'boolean'
39
+ | 'array'
40
+ | 'object'
41
+ | 'null';
36
42
 
37
43
  export interface FieldSchema {
38
44
  type: VarType;
@@ -66,7 +72,9 @@ function inferSchema(value: unknown): FieldSchema {
66
72
  if (Array.isArray(value)) {
67
73
  return { type: 'array' };
68
74
  }
69
- if (value !== null && typeof value === 'object') {
75
+ // A default of null means "nothing yet"; the value may later be anything
76
+ if (value === null) return { type: 'null' };
77
+ if (typeof value === 'object') {
70
78
  const fields = new Map<string, FieldSchema>();
71
79
  for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
72
80
  fields.set(key, inferSchema(val));
@@ -156,6 +164,8 @@ function validateRef(
156
164
  // Arrays have built-in methods/properties (push, find, length, etc.)
157
165
  // so any field access on an array is allowed.
158
166
  if (current.type === 'array') return null;
167
+ // A null default may later hold a value of any shape
168
+ if (current.type === 'null') return null;
159
169
 
160
170
  // Primitives expose their wrapper's built-ins (length, toUpperCase,
161
171
  // toFixed, etc.). Keep validating past properties of primitive type.
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) &&
@@ -1,15 +1,48 @@
1
+ import { getByPath } from './object-path';
2
+
1
3
  /**
2
- * Counts the edits a reader made through a control bound to a story
4
+ * Records the edits a reader made through a control bound to a story
3
5
  * variable (`{textbox}`, `{checkbox}`, ...). {for} tells these apart from a
4
6
  * list that is replaced: typing into a control inside an iteration changes
5
7
  * its item, but must not remount the iteration and drop the control's focus.
8
+ * An edit is attributed to the variable path it wrote, so a replacement made
9
+ * elsewhere (a watcher reacting to the edit) still remounts.
6
10
  */
7
11
  let version = 0;
12
+ const MAX_LOG = 256;
13
+ const log: { version: number; segments: readonly string[] }[] = [];
8
14
 
9
- export function noteControlEdit(): void {
10
- version++;
15
+ export function noteControlEdit(segments: readonly string[]): void {
16
+ log.push({ version: ++version, segments });
17
+ if (log.length > MAX_LOG) log.shift();
11
18
  }
12
19
 
13
20
  export function controlEditVersion(): number {
14
21
  return version;
15
22
  }
23
+
24
+ /**
25
+ * Whether an edit since `since` wrote inside `item`, element `index` of
26
+ * `list`: the path leads through the item (compared by reference in
27
+ * `variables`), or, for a primitive item, ends at its slot in the list.
28
+ */
29
+ export function editedItem(
30
+ since: number,
31
+ variables: object,
32
+ list: readonly unknown[],
33
+ item: unknown,
34
+ index: number,
35
+ ): boolean {
36
+ return log.some(({ version: v, segments }) => {
37
+ if (v <= since) return false;
38
+ if (typeof item === 'object' && item !== null) {
39
+ return segments.some(
40
+ (_, n) => getByPath(variables, segments.slice(0, n + 1)) === item,
41
+ );
42
+ }
43
+ return (
44
+ segments[segments.length - 1] === String(index) &&
45
+ getByPath(variables, segments.slice(0, -1)) === list
46
+ );
47
+ });
48
+ }
@@ -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
  }
@@ -100,7 +100,13 @@ export declare function defineMacro(config: MacroDefinition): void;
100
100
  export declare function getMacroRegistry(): MacroMetadata[];
101
101
 
102
102
  /** Variable type inferred from a StoryVariables/StoryTransients default value. */
103
- export type VarType = 'number' | 'string' | 'boolean' | 'array' | 'object';
103
+ export type VarType =
104
+ | 'number'
105
+ | 'string'
106
+ | 'boolean'
107
+ | 'array'
108
+ | 'object'
109
+ | 'null';
104
110
 
105
111
  /** Inferred shape of a declared variable (or one of its object fields). */
106
112
  export interface FieldSchema {