@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.
@@ -10431,28 +10431,34 @@ function parseWidgetDef(rawArgs) {
10431
10431
  return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
10432
10432
  }
10433
10433
  /**
10434
- * A definition: its arguments (read like a {widget} macro's, see
10435
- * parseWidgetDef) and its body. Macro names are not case-sensitive.
10436
- */
10437
- var WIDGET_DEFINITION = /\{widget\s+([^}]*)\}([\s\S]*?)\{\/widget\}/gi;
10438
- /** `{@children}`, also with selectors such as `{.highlight @children}`. */
10439
- var CHILDREN_PLACEHOLDER = /\{[^{}]*@children\s*\}/;
10440
- /**
10441
10434
  * The names of the block widgets (those whose body renders `{@children}`)
10442
10435
  * that StoryInit and the passages tagged `widget` define. They must be
10443
10436
  * known as block macros before any passage is parsed, so that passages
10444
10437
  * invoking them nest their content whatever the passage order.
10438
+ *
10439
+ * The passages are read as flat tokens, which need no block macros to be
10440
+ * known, so a `{@children}` in an HTML comment or in a `{do}` body is text
10441
+ * here as it is to the parser, and does not make a block widget (#387).
10445
10442
  */
10446
10443
  function blockWidgetNames(passages) {
10447
10444
  const names = [];
10448
10445
  for (const passage of passages) {
10449
10446
  if (passage.name !== "StoryInit" && !passage.tags?.includes("widget")) continue;
10450
- for (const match of passage.content.matchAll(WIDGET_DEFINITION)) {
10451
- if (!CHILDREN_PLACEHOLDER.test(match[2])) continue;
10447
+ const open = [];
10448
+ for (const token of tokenizeMarkupTolerant(passage.content).tokens) if (token.type === "macro" && token.name.toLowerCase() === "widget") {
10449
+ if (!token.isClose) {
10450
+ open.push({
10451
+ rawArgs: token.rawArgs,
10452
+ isBlock: false
10453
+ });
10454
+ continue;
10455
+ }
10456
+ const def = open.pop();
10457
+ if (!def?.isBlock) continue;
10452
10458
  try {
10453
- names.push(parseWidgetDef(match[1]).name);
10459
+ names.push(parseWidgetDef(def.rawArgs).name);
10454
10460
  } catch {}
10455
- }
10461
+ } else if (token.type === "variable" && token.scope === "local" && token.name === "children" && open.length > 0) open[open.length - 1].isBlock = true;
10456
10462
  }
10457
10463
  return names;
10458
10464
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.59.13",
3
+ "version": "0.59.14",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { parse, stringify } from 'devalue';
4
4
  import { hasOwn } from './utils/namespace';
5
+ import { extraKeys } from './utils/value-kinds';
5
6
 
6
7
  type Constructor = new (...args: any[]) => any;
7
8
 
@@ -46,7 +47,10 @@ export function clearRegistry(): void {
46
47
  // values. Spindle adds, with reducers and revivers:
47
48
  //
48
49
  // - `c:<name>`: an instance of the class registered as <name>, with its own
49
- // enumerable keys (and, for Error subclasses, its message and cause);
50
+ // enumerable keys (and, for Error subclasses, its message and cause); for
51
+ // a subclass of Array, Map or Set, `[kind, contents, keys]`: the kind of
52
+ // collection, its elements (Map entries as pairs) and its other own
53
+ // enumerable keys;
50
54
  // - `E:<name>`: a built-in error (Error, TypeError, ..., AggregateError),
51
55
  // with its message, cause, errors and own enumerable keys (not its stack);
52
56
  // - `S`: a symbol from the global registry (Symbol.for);
@@ -103,6 +107,13 @@ const ERROR_HIDDEN_KEYS = ['message', 'cause', 'errors'] as const;
103
107
  const isObject = (v: unknown): v is object =>
104
108
  typeof v === 'object' && v !== null;
105
109
 
110
+ /** Whether `v` is a plain object (as devalue revives one). */
111
+ const isPlainData = (v: unknown): v is Record<string, unknown> =>
112
+ isObject(v) && Object.getPrototypeOf(v) === Object.prototype;
113
+
114
+ const malformedClass = (name: string): TypeError =>
115
+ new TypeError(`spindle: Malformed data for class "${name}"`);
116
+
106
117
  const defineData = (target: object, key: string, value: unknown): void => {
107
118
  // Define, so that a "__proto__" key stays a key (devalue refuses it)
108
119
  Object.defineProperty(target, key, {
@@ -127,7 +138,10 @@ function refuseSymbolKeys(value: object): void {
127
138
  * Own enumerable keys of `value`, as a plain object; for an error also its
128
139
  * message, cause and (AggregateError) errors, which are not enumerable.
129
140
  */
130
- function ownData(value: object): Record<string, unknown> {
141
+ function ownData(
142
+ value: object,
143
+ keys = Object.keys(value),
144
+ ): Record<string, unknown> {
131
145
  refuseSymbolKeys(value);
132
146
  const data: Record<string, unknown> = {};
133
147
  if (value instanceof Error) {
@@ -135,12 +149,44 @@ function ownData(value: object): Record<string, unknown> {
135
149
  if (hasOwn(value, key)) defineData(data, key, value[key as keyof Error]);
136
150
  }
137
151
  }
138
- for (const key of Object.keys(value)) {
152
+ for (const key of keys) {
139
153
  defineData(data, key, (value as Record<string, unknown>)[key]);
140
154
  }
141
155
  return data;
142
156
  }
143
157
 
158
+ /** The kinds of collection a registered class may extend. */
159
+ const COLLECTION_KINDS = {
160
+ Array: Array as unknown as Constructor,
161
+ Map: Map as Constructor,
162
+ Set: Set as Constructor,
163
+ };
164
+ type CollectionKind = keyof typeof COLLECTION_KINDS;
165
+
166
+ /**
167
+ * The saved form of a registered class instance: its own data (see
168
+ * ownData), or for a collection its kind, elements and extra keys, read
169
+ * with the built-in methods, which a subclass may override (#391).
170
+ */
171
+ function classData(value: object): unknown {
172
+ const extra = () => ownData(value, extraKeys(value));
173
+ if (Array.isArray(value)) {
174
+ // Holes stay holes
175
+ const items = new Array(value.length) as unknown[];
176
+ for (let i = 0; i < value.length; i++) {
177
+ if (i in value) items[i] = value[i] as unknown;
178
+ }
179
+ return ['Array', items, extra()];
180
+ }
181
+ if (value instanceof Map) {
182
+ return ['Map', Array.from(Map.prototype.entries.call(value)), extra()];
183
+ }
184
+ if (value instanceof Set) {
185
+ return ['Set', Array.from(Set.prototype.values.call(value)), extra()];
186
+ }
187
+ return ownData(value);
188
+ }
189
+
144
190
  /** Make an error's message, cause and errors non-enumerable again. */
145
191
  function hideErrorKeys(value: object): void {
146
192
  for (const key of ERROR_HIDDEN_KEYS) {
@@ -158,7 +204,9 @@ function reducers(): Record<string, (value: unknown) => unknown> {
158
204
  const out: Record<string, (value: unknown) => unknown> = {};
159
205
  for (const [name, ctor] of registry) {
160
206
  out[CLASS_PREFIX + name] = (v) =>
161
- isObject(v) && Object.getPrototypeOf(v) === ctor.prototype && ownData(v);
207
+ isObject(v) &&
208
+ Object.getPrototypeOf(v) === ctor.prototype &&
209
+ classData(v);
162
210
  }
163
211
  for (const [proto, name] of BUILTIN_ERRORS) {
164
212
  out[ERROR_PREFIX + name] = (v) =>
@@ -175,7 +223,7 @@ function reducers(): Record<string, (value: unknown) => unknown> {
175
223
  v.lastIndex !== 0 && [v.source, v.flags, v.lastIndex];
176
224
  if (GUARD_V8_KEYS) {
177
225
  out[KEYS_TAG] = (v) => {
178
- if (!isObject(v) || Object.getPrototypeOf(v) !== Object.prototype) {
226
+ if (!isPlainData(v)) {
179
227
  return false;
180
228
  }
181
229
  const keys = Object.keys(v);
@@ -192,19 +240,79 @@ function reducers(): Record<string, (value: unknown) => unknown> {
192
240
  return out;
193
241
  }
194
242
 
243
+ /**
244
+ * The collection of a registered class that a `[kind, contents, keys]`
245
+ * list holds (see classData). A cycle through the collection revives it
246
+ * before its list is complete (devalue holds unread parts as holes), so the
247
+ * collection is kept per list and filled with what the list holds so far.
248
+ */
249
+ const collections = new WeakMap<unknown[], object>();
250
+ function reviveCollection(name: string, ctor: Constructor, list: unknown[]) {
251
+ const [kind, contents, keys] = list;
252
+ const base =
253
+ typeof kind === 'string' && hasOwn(COLLECTION_KINDS, kind)
254
+ ? COLLECTION_KINDS[kind as CollectionKind]
255
+ : undefined;
256
+ if (
257
+ list.length !== 3 ||
258
+ !base ||
259
+ !(ctor.prototype instanceof base) ||
260
+ (hasOwn(list, 1) && !Array.isArray(contents)) ||
261
+ (hasOwn(list, 2) && !isPlainData(keys))
262
+ ) {
263
+ throw malformedClass(name);
264
+ }
265
+ let made = collections.get(list);
266
+ if (!made) {
267
+ made = Object.setPrototypeOf(new base(), ctor.prototype) as object;
268
+ collections.set(list, made);
269
+ }
270
+ if (Array.isArray(contents)) fillCollection(made, contents);
271
+ if (isPlainData(keys)) {
272
+ for (const key of Object.keys(keys)) defineData(made, key, keys[key]);
273
+ }
274
+ return made;
275
+ }
276
+
277
+ /** Set the elements of a revived collection, with the built-in methods. */
278
+ function fillCollection(made: object, contents: unknown[]): void {
279
+ if (Array.isArray(made)) {
280
+ made.length = contents.length;
281
+ for (let i = 0; i < contents.length; i++) {
282
+ if (hasOwn(contents, i)) made[i] = contents[i];
283
+ else delete made[i];
284
+ }
285
+ } else if (made instanceof Map) {
286
+ Map.prototype.clear.call(made);
287
+ for (const entry of contents) {
288
+ if (!Array.isArray(entry) || entry.length !== 2) {
289
+ throw new TypeError('spindle: Malformed map entries');
290
+ }
291
+ Map.prototype.set.call(made, entry[0], entry[1]);
292
+ }
293
+ } else {
294
+ Set.prototype.clear.call(made);
295
+ for (const member of contents) Set.prototype.add.call(made, member);
296
+ }
297
+ }
298
+
195
299
  /** Restore a registered class instance in place, so cycles through it hold. */
196
300
  function reviveClass(name: string) {
197
- return (data: Record<string, unknown>): unknown => {
198
- if (!isObject(data) || Object.getPrototypeOf(data) !== Object.prototype) {
301
+ return (data: Record<string, unknown> | unknown[]): unknown => {
302
+ if (Array.isArray(data)) {
303
+ return reviveCollection(name, registeredCtor(name), data);
304
+ }
305
+ if (!isPlainData(data)) {
199
306
  // Already revived: a cycle through the instance revives it twice
200
307
  if (isObject(data) && registeredClassName(data) === name) return data;
201
- throw new TypeError(`spindle: Malformed data for class "${name}"`);
308
+ throw malformedClass(name);
202
309
  }
203
- const ctor = registry.get(name);
204
- if (!ctor) {
205
- throw new TypeError(
206
- `spindle: The save holds an instance of class "${name}", which is not registered`,
207
- );
310
+ const ctor = registeredCtor(name);
311
+ // A plain object cannot be made a collection (see classData)
312
+ if (
313
+ Object.values(COLLECTION_KINDS).some((c) => ctor.prototype instanceof c)
314
+ ) {
315
+ throw malformedClass(name);
208
316
  }
209
317
  Object.setPrototypeOf(data, ctor.prototype as object);
210
318
  if (data instanceof Error) hideErrorKeys(data);
@@ -212,13 +320,24 @@ function reviveClass(name: string) {
212
320
  };
213
321
  }
214
322
 
323
+ /** The class registered as `name`; throws if there is none. */
324
+ function registeredCtor(name: string): Constructor {
325
+ const ctor = registry.get(name);
326
+ if (!ctor) {
327
+ throw new TypeError(
328
+ `spindle: The save holds an instance of class "${name}", which is not registered`,
329
+ );
330
+ }
331
+ return ctor;
332
+ }
333
+
215
334
  /** Restore a built-in error in place, like a class instance. */
216
335
  function reviveError(name: string) {
217
336
  const ctor = ERROR_CTORS.get(name);
218
337
  if (!ctor) return undefined;
219
338
  return (data: Record<string, unknown>): unknown => {
220
339
  if (Object.getPrototypeOf(data) === ctor.prototype) return data;
221
- if (!isObject(data) || Object.getPrototypeOf(data) !== Object.prototype) {
340
+ if (!isPlainData(data)) {
222
341
  throw new TypeError(`spindle: Malformed data for "${name}"`);
223
342
  }
224
343
  Object.setPrototypeOf(data, ctor.prototype);
@@ -1,4 +1,4 @@
1
- import { useMemo } from 'preact/hooks';
1
+ import { useContext, useMemo } from 'preact/hooks';
2
2
  import { WidgetChildrenContext, renderNodes } from '../../markup/render';
3
3
  import { useMergedLocals } from '../../hooks/use-merged-locals';
4
4
  import { evaluate } from '../../expression';
@@ -9,6 +9,7 @@ import { useRenderOptions } from '../../hooks/use-render-options';
9
9
  import { LocalsScope } from './locals-scope';
10
10
  import { useInterpolate } from '../../hooks/use-interpolate';
11
11
  import { wrapContent } from './display';
12
+ import { widgetChildrenOf } from '../../interpolation';
12
13
 
13
14
  export { splitArgs };
14
15
 
@@ -44,7 +45,11 @@ function WidgetContent({
44
45
  const [mergedVars, mergedTemps, mergedLocals, mergedTrans] =
45
46
  useMergedLocals();
46
47
 
47
- const childrenValue = invocationChildren?.length ? invocationChildren : null;
48
+ const outerChildren = useContext(WidgetChildrenContext);
49
+ const childrenValue = useMemo(
50
+ () => widgetChildrenOf(invocationChildren, outerChildren),
51
+ [invocationChildren, outerChildren],
52
+ );
48
53
 
49
54
  // Every widget gets its own local scope, even when it declares no
50
55
  // parameters or is invoked without arguments: missing parameters shadow
@@ -148,6 +148,16 @@ function deeplyFrozen(value: unknown, seen = new Set<object>()): boolean {
148
148
  if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return true;
149
149
  if (!Object.isFrozen(value)) return false;
150
150
  seen.add(value);
151
+ // What a Map or Set holds is not in its properties (#388)
152
+ if (value instanceof Map) {
153
+ for (const [key, entry] of value) {
154
+ if (!deeplyFrozen(key, seen) || !deeplyFrozen(entry, seen)) return false;
155
+ }
156
+ } else if (value instanceof Set) {
157
+ for (const member of value) {
158
+ if (!deeplyFrozen(member, seen)) return false;
159
+ }
160
+ }
151
161
  return Reflect.ownKeys(value).every((key) => {
152
162
  const desc = Object.getOwnPropertyDescriptor(value, key);
153
163
  return !desc || !('value' in desc) || deeplyFrozen(desc.value, seen);
@@ -14,10 +14,26 @@ const FOCUSABLE = [
14
14
  '[tabindex]',
15
15
  ].join(',');
16
16
 
17
+ /**
18
+ * Whether `el` is in a closed `<details>`, other than in the summary that
19
+ * toggles it: the browser renders none of that content (#389).
20
+ */
21
+ function inClosedDetails(el: HTMLElement): boolean {
22
+ for (let e = el.parentElement; e; e = e.parentElement) {
23
+ if (e.localName !== 'details' || e.hasAttribute('open')) continue;
24
+ const summary = Array.from(e.children).find(
25
+ (c) => c.localName === 'summary',
26
+ );
27
+ if (!summary?.contains(el)) return true;
28
+ }
29
+ return false;
30
+ }
31
+
17
32
  /**
18
33
  * Whether CSS leaves `el` in the tab sequence: not `visibility: hidden`
19
- * (inherited, so the computed value covers ancestors) and not inside a
20
- * `display: none` element.
34
+ * (inherited, so the computed value covers ancestors), not inside a
35
+ * `display: none` element and not in the hidden content of a closed
36
+ * `<details>`.
21
37
  */
22
38
  function isRendered(el: HTMLElement): boolean {
23
39
  const { visibility } = getComputedStyle(el);
@@ -25,7 +41,21 @@ function isRendered(el: HTMLElement): boolean {
25
41
  for (let e: HTMLElement | null = el; e; e = e.parentElement) {
26
42
  if (getComputedStyle(e).display === 'none') return false;
27
43
  }
28
- return true;
44
+ return !inClosedDetails(el);
45
+ }
46
+
47
+ /**
48
+ * Whether `el` can be focused at all: shown, rendered, enabled (also by its
49
+ * fieldset) and not inert.
50
+ */
51
+ function isAvailable(el: HTMLElement): boolean {
52
+ return (
53
+ !el.hidden &&
54
+ !el.closest('[hidden], [inert]') &&
55
+ // Also covers controls disabled by a <fieldset disabled> ancestor
56
+ !el.matches(':disabled') &&
57
+ isRendered(el)
58
+ );
29
59
  }
30
60
 
31
61
  /** The `tabindex` of `el` as an integer, or null when absent or invalid. */
@@ -36,9 +66,10 @@ function tabIndexAttr(el: HTMLElement): number | null {
36
66
  }
37
67
 
38
68
  /**
39
- * Whether the tab key can reach `el`: a negative tabindex only allows
40
- * scripted focus, and of a radio group the browser tabs to the checked
41
- * member only (the first one when none is checked).
69
+ * Whether the tab key can reach `el`, an available element (see
70
+ * isAvailable): a negative tabindex only allows scripted focus, and of a
71
+ * radio group the browser tabs to the checked member only (the first one
72
+ * when none is checked), of the members it can reach.
42
73
  */
43
74
  function isTabStop(el: HTMLElement): boolean {
44
75
  const tabIndex = tabIndexAttr(el);
@@ -67,7 +98,14 @@ function isTabStop(el: HTMLElement): boolean {
67
98
  (
68
99
  el.form ?? (el.getRootNode() as ParentNode)
69
100
  ).querySelectorAll<HTMLInputElement>('input[type="radio"]'),
70
- ).filter((r) => r.name === el.name && r.form === el.form && !r.disabled);
101
+ ).filter(
102
+ (r) =>
103
+ r.name === el.name &&
104
+ r.form === el.form &&
105
+ // Of the members Tab can reach (#390)
106
+ (tabIndexAttr(r) ?? 0) >= 0 &&
107
+ isAvailable(r),
108
+ );
71
109
  const stop = group.find((r) => r.checked) ?? group[0];
72
110
  return el === stop;
73
111
  }
@@ -81,15 +119,7 @@ function isTabStop(el: HTMLElement): boolean {
81
119
  function focusables(root: HTMLElement): HTMLElement[] {
82
120
  const stops = Array.from(
83
121
  root.querySelectorAll<HTMLElement>(FOCUSABLE),
84
- ).filter(
85
- (el) =>
86
- !el.hidden &&
87
- !el.closest('[hidden], [inert]') &&
88
- // Also covers controls disabled by a <fieldset disabled> ancestor
89
- !el.matches(':disabled') &&
90
- isRendered(el) &&
91
- isTabStop(el),
92
- );
122
+ ).filter((el) => isAvailable(el) && isTabStop(el));
93
123
  const positive = (el: HTMLElement) => Math.max(tabIndexAttr(el) ?? 0, 0);
94
124
  // Array.prototype.sort is stable: equal tabindexes keep document order
95
125
  return [
@@ -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,
@@ -67,7 +68,7 @@ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
67
68
 
68
69
  /** Elements whose content is literal text, not markdown. */
69
70
  const PREFORMATTED_ELEMENTS = new Set(['pre', 'textarea', 'style']);
70
- export const WidgetChildrenContext = createContext<ASTNode[] | null>(null);
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
@@ -708,10 +709,16 @@ function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
708
709
  }
709
710
 
710
711
  function ChildrenSlot() {
711
- const childrenAST = useContext(WidgetChildrenContext);
712
+ const children = useContext(WidgetChildrenContext);
712
713
  const renderOptions = useRenderOptions();
713
- if (!childrenAST || childrenAST.length === 0) return null;
714
- 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
+ );
715
722
  }
716
723
 
717
724
  /**