@jarenjs/view 0.73.0 → 0.83.2
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/README.md +7 -0
- package/dist/types/dom.d.ts +9 -0
- package/dist/types/index.d.ts +2 -0
- package/docs/VIEW-FORMAT.md +31 -17
- package/package.json +2 -2
- package/src/dom.js +116 -14
- package/src/html.js +2 -0
- package/src/index.js +2 -0
package/README.md
CHANGED
|
@@ -50,6 +50,13 @@ render.destroy(); // terminal teardown, idempotent
|
|
|
50
50
|
|
|
51
51
|
`render.destroy()` unmounts every mounted widget exactly once (pending mounts are canceled), empties the container, and turns every later `render` call into an exact no-op — including a scheduled flush that fires after destruction. The render boundary is **serialized**: a `render` entered synchronously from inside a widget hook or event callback (an `emit` chain) never nests — it queues behind the running patch, multiple nested requests coalesce to the latest vnode, and the queued tree is applied against the committed baseline, so no widget sees `update` before its `mount` returned or receives stale previous props.
|
|
52
52
|
|
|
53
|
+
Both renderers accept fragment roots without a wrapper. Pass `hydrate: true`
|
|
54
|
+
to adopt matching server DOM on the first trusted render; local mismatches
|
|
55
|
+
are repaired and widgets get fresh hosts. Safe mode rebuilds existing DOM.
|
|
56
|
+
Controlled text writes defer through composition and the final input event,
|
|
57
|
+
then settle with the caret preserved. The browser tests cover these paths in
|
|
58
|
+
Chromium, Firefox and WebKit; native OS IME audits remain separate.
|
|
59
|
+
|
|
53
60
|
### On the server
|
|
54
61
|
|
|
55
62
|
```javascript
|
package/dist/types/dom.d.ts
CHANGED
|
@@ -76,6 +76,12 @@ export type DomRendererOptions = {
|
|
|
76
76
|
* (defaults to `container.ownerDocument`).
|
|
77
77
|
*/
|
|
78
78
|
document?: any;
|
|
79
|
+
/**
|
|
80
|
+
* - Adopt matching server DOM on the
|
|
81
|
+
* first render. Mismatches are replaced locally. Safe mode rebuilds
|
|
82
|
+
* existing markup because the renderer cannot trust its provenance.
|
|
83
|
+
*/
|
|
84
|
+
hydrate?: boolean;
|
|
79
85
|
/**
|
|
80
86
|
* - Render under the SAFE policy
|
|
81
87
|
* ({@link createSafePolicy}): treat the vnode as untrusted. Tags are
|
|
@@ -167,6 +173,9 @@ export type DomRenderer = ((vnode: any) => void) & {
|
|
|
167
173
|
* definitions by name (VIEW-FORMAT §7).
|
|
168
174
|
* @property {any} [document] - The document to create nodes with
|
|
169
175
|
* (defaults to `container.ownerDocument`).
|
|
176
|
+
* @property {boolean} [hydrate=false] - Adopt matching server DOM on the
|
|
177
|
+
* first render. Mismatches are replaced locally. Safe mode rebuilds
|
|
178
|
+
* existing markup because the renderer cannot trust its provenance.
|
|
170
179
|
* @property {boolean} [safe=false] - Render under the SAFE policy
|
|
171
180
|
* ({@link createSafePolicy}): treat the vnode as untrusted. Tags are
|
|
172
181
|
* restricted to an inert HTML/SVG allow-list, scripting-sink and inline
|
package/dist/types/index.d.ts
CHANGED
|
@@ -6,3 +6,5 @@ export { EMPTY_PROPS, WIDGET_TAG, h, isTextNode, isElementNode, isSkippedNode, i
|
|
|
6
6
|
export { createDomRenderer, styleToString, } from './dom.js';
|
|
7
7
|
export { renderToString, escapeText, escapeAttribute, } from './html.js';
|
|
8
8
|
export { createSafePolicy, } from './safe.js';
|
|
9
|
+
export type DomRendererOptions = import('./dom.js').DomRendererOptions;
|
|
10
|
+
/** @typedef {import('./dom.js').DomRendererOptions} DomRendererOptions */
|
package/docs/VIEW-FORMAT.md
CHANGED
|
@@ -190,8 +190,9 @@ reorder efficiency is only specified for fully keyed lists.
|
|
|
190
190
|
### 5.4 Namespaces
|
|
191
191
|
|
|
192
192
|
An element with tag `svg` and its descendants are created in the SVG
|
|
193
|
-
namespace.
|
|
194
|
-
|
|
193
|
+
namespace. Children of SVG `foreignObject` re-enter the HTML namespace;
|
|
194
|
+
a nested `svg` enters SVG again. `xlink:*` and `xml:*` attributes use their
|
|
195
|
+
namespace URIs for setting and removal. Safe mode excludes `foreignObject`.
|
|
195
196
|
|
|
196
197
|
### 5.5 The memo marker
|
|
197
198
|
|
|
@@ -238,9 +239,18 @@ and works through `optgroup`. HTML cannot encode a single select's
|
|
|
238
239
|
option until the DOM renderer reconciles the control.
|
|
239
240
|
Safe mode keeps its attribute-only policy (§8).
|
|
240
241
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
Both renderers accept a single vnode, a root list, or an empty/skipped root.
|
|
243
|
+
Lists flatten with the ordinary child rules and need no wrapper element.
|
|
244
|
+
|
|
245
|
+
`createDomRenderer(container, { hydrate: true })` adopts matching text and
|
|
246
|
+
element nodes on its first trusted render, patches props and handlers,
|
|
247
|
+
reconciles controlled values, repairs mismatches locally and removes surplus
|
|
248
|
+
nodes and attributes. Initial matching is positional by tag and namespace;
|
|
249
|
+
subsequent keyed patches preserve adopted identities. Widgets mount once on
|
|
250
|
+
fresh hosts because their server-side markup has no transferable handle.
|
|
251
|
+
Without `hydrate`, the first render rebuilds. Safe mode also rebuilds: existing
|
|
252
|
+
DOM does not inherit trust from a sanitized vnode. Hydration does not remove
|
|
253
|
+
listeners installed by another owner, so the host must own the adopted DOM.
|
|
244
254
|
|
|
245
255
|
## 7. Widgets
|
|
246
256
|
|
|
@@ -524,21 +534,25 @@ SHOULD do both.
|
|
|
524
534
|
Safe mode reduces an untrusted view to a display; it is **not a complete
|
|
525
535
|
sandbox**. The tag allow-list still admits anchors, forms, controls and media,
|
|
526
536
|
so native navigation, form submission, focus and network loads remain
|
|
527
|
-
possible. It is also a **renderer** policy
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
+
possible. It is also a **renderer** policy. `@jarenjs/app` forwards `safe`
|
|
538
|
+
and `onUnsafe` and provides explicit effect/subscription/widget/event-field
|
|
539
|
+
grants (APP-FORMAT §8.2.1). Those grants bound named host access; they do not
|
|
540
|
+
isolate computation, navigation or network activity.
|
|
541
|
+
|
|
542
|
+
The browser suite exercises safe create→update→remove→reinsert, URL/style
|
|
543
|
+
reflection, controlled inputs, multiple selects and composition in Chromium,
|
|
544
|
+
Firefox and WebKit. Trusted controlled text writes defer through composition
|
|
545
|
+
and the final input event, then settle while retaining a clamped caret range.
|
|
546
|
+
Composition cleanup cancels pending settlement on removal or destruction.
|
|
547
|
+
Safe controls retain the attribute-only display policy rather than the trusted
|
|
548
|
+
controlled-input policy.
|
|
549
|
+
|
|
550
|
+
Native OS IME fidelity and assistive-technology behavior still require manual
|
|
551
|
+
audits. MathML and Trusted Types integration remain outside this version.
|
|
552
|
+
Safe mode is one layer under a Content-Security-Policy, not a substitute for one.
|
|
537
553
|
|
|
538
554
|
## 9. Open items (roadmap, non-normative)
|
|
539
555
|
|
|
540
|
-
- **Fragment / multi-root documents** — a list at the root.
|
|
541
|
-
- **DOM-adopting hydration** (§6).
|
|
542
556
|
- ~~Memoized rule outputs~~ — **shipped**: the JSLT engine's `memo`
|
|
543
557
|
option (on by default in `@jarenjs/app`) caches rule outputs by
|
|
544
558
|
(location, value reference) with compile-time eligibility analysis,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/view",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.83.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"ssr"
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@jarenjs/core": "^0.
|
|
56
|
+
"@jarenjs/core": "^0.83.2"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "npm run build:types",
|
package/src/dom.js
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
propsOf,
|
|
47
47
|
keyOf,
|
|
48
48
|
childrenOf,
|
|
49
|
+
isSkippedNode,
|
|
49
50
|
EMPTY_PROPS,
|
|
50
51
|
WIDGET_TAG,
|
|
51
52
|
} from './vnode.js';
|
|
@@ -113,6 +114,9 @@ const WIDGET_SKIP_PROPS = { name: true, props: true, tag: true };
|
|
|
113
114
|
* definitions by name (VIEW-FORMAT §7).
|
|
114
115
|
* @property {any} [document] - The document to create nodes with
|
|
115
116
|
* (defaults to `container.ownerDocument`).
|
|
117
|
+
* @property {boolean} [hydrate=false] - Adopt matching server DOM on the
|
|
118
|
+
* first render. Mismatches are replaced locally. Safe mode rebuilds
|
|
119
|
+
* existing markup because the renderer cannot trust its provenance.
|
|
116
120
|
* @property {boolean} [safe=false] - Render under the SAFE policy
|
|
117
121
|
* ({@link createSafePolicy}): treat the vnode as untrusted. Tags are
|
|
118
122
|
* restricted to an inert HTML/SVG allow-list, scripting-sink and inline
|
|
@@ -238,9 +242,10 @@ export function createDomRenderer(container, options = {}) {
|
|
|
238
242
|
|
|
239
243
|
function render(vnode) {
|
|
240
244
|
if (ctx.destroyed) return; // a scheduled flush after destroy is a no-op
|
|
241
|
-
if (!isTextNode(vnode) && !
|
|
242
|
-
throw new TypeError('view: the root
|
|
245
|
+
if (!isTextNode(vnode) && !Array.isArray(vnode) && !isSkippedNode(vnode)) {
|
|
246
|
+
throw new TypeError('view: the root must be a vnode or a list of vnodes');
|
|
243
247
|
}
|
|
248
|
+
if (isSkippedNode(vnode)) vnode = [];
|
|
244
249
|
if (rendering) {
|
|
245
250
|
// re-entrant call (a widget mount/update emitted synchronously):
|
|
246
251
|
// queue behind the current patch — it applies after this frame,
|
|
@@ -254,12 +259,17 @@ export function createDomRenderer(container, options = {}) {
|
|
|
254
259
|
do {
|
|
255
260
|
pendingVnode = undefined;
|
|
256
261
|
if (rootNode === null) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
262
|
+
if (options.hydrate && ctx.policy === null) {
|
|
263
|
+
adoptChildren(ctx, container, childrenOf(['root', {}, next]), null);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
container.textContent = '';
|
|
267
|
+
for (const child of childrenOf(['root', {}, next])) container.appendChild(createNode(ctx, child, null));
|
|
268
|
+
}
|
|
269
|
+
rootNode = container;
|
|
260
270
|
}
|
|
261
271
|
else {
|
|
262
|
-
|
|
272
|
+
patchChildren(ctx, container, childrenOf(['root', {}, oldVnode]), childrenOf(['root', {}, next]), null);
|
|
263
273
|
}
|
|
264
274
|
oldVnode = next;
|
|
265
275
|
// mount flush: after the patch completes every queued host is
|
|
@@ -321,7 +331,7 @@ export function createDomRenderer(container, options = {}) {
|
|
|
321
331
|
ctx.mountQueue.length = 0;
|
|
322
332
|
// Release the controlled-node registry: a destroyed renderer must not
|
|
323
333
|
// retain detached form controls (nor their stored values) past teardown.
|
|
324
|
-
ctx.controlled
|
|
334
|
+
for (const node of ctx.controlled) releaseControlled(ctx, node);
|
|
325
335
|
if (rootNode !== null) {
|
|
326
336
|
/** @type {unknown[]} */
|
|
327
337
|
const failures = [];
|
|
@@ -439,12 +449,49 @@ function createNode(ctx, vnode, ns) {
|
|
|
439
449
|
}
|
|
440
450
|
const children = childrenOf(vnode);
|
|
441
451
|
for (let i = 0; i < children.length; i++) {
|
|
442
|
-
node.appendChild(createNode(ctx, children[i], ns));
|
|
452
|
+
node.appendChild(createNode(ctx, children[i], tag === 'foreignObject' ? null : ns));
|
|
443
453
|
}
|
|
444
454
|
registerControlled(ctx, node, props);
|
|
445
455
|
return node;
|
|
446
456
|
}
|
|
447
457
|
|
|
458
|
+
/** Adopt matching nodes positionally once; subsequent keyed reconciliation
|
|
459
|
+
* has the same vnode baseline as a fresh mount. Browser parser repairs and
|
|
460
|
+
* missing/extra nodes are local mismatches, never a second tree of state. */
|
|
461
|
+
function adoptChildren(ctx, parent, children, ns) {
|
|
462
|
+
for (let i = 0; i < children.length; i++) {
|
|
463
|
+
const vnode = children[i];
|
|
464
|
+
const node = parent.childNodes[i];
|
|
465
|
+
if (node && isTextNode(vnode) && node.nodeType === 3) {
|
|
466
|
+
if (node.nodeValue !== String(vnode)) node.nodeValue = String(vnode);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const tag = isElementNode(vnode) ? vnode[0] : null;
|
|
470
|
+
const namespace = tag === 'svg' ? SVG_NS : ns;
|
|
471
|
+
if (node && tag && tag !== WIDGET_TAG && node.nodeType === 1
|
|
472
|
+
&& (node.localName ?? node.tagName?.toLowerCase()) === tag
|
|
473
|
+
&& (namespace === null ? node.namespaceURI == null || node.namespaceURI === 'http://www.w3.org/1999/xhtml' : node.namespaceURI === namespace)) {
|
|
474
|
+
const props = propsOf(vnode);
|
|
475
|
+
const names = new Set(Object.keys(props).map((name) => name === 'className' ? 'class' : name === 'htmlFor' ? 'for' : name.toLowerCase()));
|
|
476
|
+
for (const name of node.getAttributeNames()) {
|
|
477
|
+
if (!names.has(name.toLowerCase())) node.removeAttribute(name);
|
|
478
|
+
}
|
|
479
|
+
for (const name in props) setProp(ctx, node, name, undefined, props[name], namespace);
|
|
480
|
+
// A textarea's server text is its default value, not a vnode child
|
|
481
|
+
// when `value` supplies the authoritative content.
|
|
482
|
+
if (!(tag === 'textarea' && Object.hasOwn(props, 'value')))
|
|
483
|
+
adoptChildren(ctx, node, childrenOf(vnode), tag === 'foreignObject' ? null : namespace);
|
|
484
|
+
registerControlled(ctx, node, props);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
const fresh = createNode(ctx, vnode, ns);
|
|
488
|
+
if (node) parent.replaceChild(fresh, node);
|
|
489
|
+
else parent.appendChild(fresh);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
while (parent.childNodes.length > children.length) parent.removeChild(parent.childNodes[parent.childNodes.length - 1]);
|
|
493
|
+
}
|
|
494
|
+
|
|
448
495
|
/**
|
|
449
496
|
* Map a vnode the safe policy rejects — a widget (imperative JS) or an
|
|
450
497
|
* element whose tag is off the allow-list or is injection-shaped — to a
|
|
@@ -530,7 +577,7 @@ function patchNode(ctx, parent, node, oldV, newV, ns) {
|
|
|
530
577
|
return node;
|
|
531
578
|
}
|
|
532
579
|
patchProps(ctx, node, propsOf(oldV), propsOf(newV), ns);
|
|
533
|
-
patchChildren(ctx, node, childrenOf(oldV), childrenOf(newV), ns);
|
|
580
|
+
patchChildren(ctx, node, childrenOf(oldV), childrenOf(newV), newV[0] === 'foreignObject' ? null : ns);
|
|
534
581
|
return node;
|
|
535
582
|
}
|
|
536
583
|
destroyNode(ctx, node);
|
|
@@ -584,11 +631,32 @@ function registerControlled(ctx, node, props) {
|
|
|
584
631
|
const hasChecked = kind === 'INPUT' && 'checked' in props;
|
|
585
632
|
if (!hasValue && !hasChecked) {
|
|
586
633
|
if (node.__jarenControlled !== undefined) {
|
|
587
|
-
node
|
|
588
|
-
ctx.controlled.delete(node);
|
|
634
|
+
releaseControlled(ctx, node);
|
|
589
635
|
}
|
|
590
636
|
return;
|
|
591
637
|
}
|
|
638
|
+
if (node.__jarenComposition === undefined && kind !== 'SELECT') {
|
|
639
|
+
let settlement;
|
|
640
|
+
const start = () => {
|
|
641
|
+
clearTimeout(settlement);
|
|
642
|
+
node.__jarenComposing = true;
|
|
643
|
+
node.__jarenCompositionDirty = false;
|
|
644
|
+
};
|
|
645
|
+
const end = () => {
|
|
646
|
+
clearTimeout(settlement);
|
|
647
|
+
// Engines can checkpoint microtasks between compositionend and the
|
|
648
|
+
// final input. Keep writes deferred through that event sequence, or
|
|
649
|
+
// the stale value clears Firefox's dirty flag and loses change-on-blur.
|
|
650
|
+
settlement = setTimeout(() => {
|
|
651
|
+
node.__jarenComposing = false;
|
|
652
|
+
if (node.__jarenCompositionDirty && !ctx.destroyed && ctx.controlled.has(node) && node.parentNode !== null)
|
|
653
|
+
reconcileControlled(node);
|
|
654
|
+
}, 0);
|
|
655
|
+
};
|
|
656
|
+
node.__jarenComposition = { start, end, cancel: () => clearTimeout(settlement) };
|
|
657
|
+
node.addEventListener('compositionstart', start, true);
|
|
658
|
+
node.addEventListener('compositionend', end, true);
|
|
659
|
+
}
|
|
592
660
|
node.__jarenControlled = {
|
|
593
661
|
hasValue,
|
|
594
662
|
hasChecked,
|
|
@@ -598,14 +666,27 @@ function registerControlled(ctx, node, props) {
|
|
|
598
666
|
ctx.controlled.add(node);
|
|
599
667
|
}
|
|
600
668
|
|
|
669
|
+
/** Release listeners as well as registry ownership, including detached nodes. */
|
|
670
|
+
function releaseControlled(ctx, node) {
|
|
671
|
+
const listeners = node.__jarenComposition;
|
|
672
|
+
if (listeners !== undefined) {
|
|
673
|
+
listeners.cancel();
|
|
674
|
+
node.removeEventListener('compositionstart', listeners.start, true);
|
|
675
|
+
node.removeEventListener('compositionend', listeners.end, true);
|
|
676
|
+
}
|
|
677
|
+
node.__jarenComposition = undefined;
|
|
678
|
+
node.__jarenComposing = false;
|
|
679
|
+
node.__jarenControlled = undefined;
|
|
680
|
+
ctx.controlled.delete(node);
|
|
681
|
+
}
|
|
682
|
+
|
|
601
683
|
/** Reconcile every registered controlled node against the live DOM, once per
|
|
602
684
|
* settled render pass. A node no longer connected to the container is dropped
|
|
603
685
|
* from the registry here, which is why registration needs no destroy hook. */
|
|
604
686
|
function reconcileControlledSet(ctx, container) {
|
|
605
687
|
for (const node of ctx.controlled) {
|
|
606
688
|
if (!isConnectedTo(node, container)) {
|
|
607
|
-
node
|
|
608
|
-
ctx.controlled.delete(node);
|
|
689
|
+
releaseControlled(ctx, node);
|
|
609
690
|
continue;
|
|
610
691
|
}
|
|
611
692
|
reconcileControlled(node);
|
|
@@ -628,6 +709,7 @@ function reconcileControlled(node) {
|
|
|
628
709
|
if (node.checked !== want) node.checked = want;
|
|
629
710
|
}
|
|
630
711
|
if (!c.hasValue) return;
|
|
712
|
+
if (node.__jarenComposing) { node.__jarenCompositionDirty = true; return; }
|
|
631
713
|
const isMultiple = node.multiple === true
|
|
632
714
|
|| (typeof node.getAttribute === 'function' && node.getAttribute('multiple') != null);
|
|
633
715
|
if (node.nodeName === 'SELECT' && isMultiple && Array.isArray(c.value)) {
|
|
@@ -643,7 +725,17 @@ function reconcileControlled(node) {
|
|
|
643
725
|
return;
|
|
644
726
|
}
|
|
645
727
|
const want = c.value == null ? '' : String(c.value);
|
|
646
|
-
if (node.value !== want)
|
|
728
|
+
if (node.value !== want) {
|
|
729
|
+
// Preserve a live edit's caret. A newly created or unfocused control
|
|
730
|
+
// keeps the browser's normal assignment behavior (caret at the end).
|
|
731
|
+
const focused = node.ownerDocument?.activeElement === node;
|
|
732
|
+
const start = focused ? node.selectionStart : null;
|
|
733
|
+
const end = focused ? node.selectionEnd : null;
|
|
734
|
+
const direction = node.selectionDirection;
|
|
735
|
+
node.value = want;
|
|
736
|
+
if (typeof start === 'number' && typeof end === 'number' && typeof node.setSelectionRange === 'function')
|
|
737
|
+
node.setSelectionRange(Math.min(start, want.length), Math.min(end, want.length), direction ?? 'none');
|
|
738
|
+
}
|
|
647
739
|
}
|
|
648
740
|
|
|
649
741
|
/** Whether `node` is still attached beneath `root` (the render container).
|
|
@@ -703,6 +795,16 @@ function setProp(ctx, node, name, oldValue, newValue, ns) {
|
|
|
703
795
|
}
|
|
704
796
|
// Trusted path (unchanged): a property where the node has one, else an
|
|
705
797
|
// attribute — the equivalent of writing the DOM by hand.
|
|
798
|
+
// Controlled values settle after children and never interrupt composition.
|
|
799
|
+
if ((name === 'value' || name === 'checked')
|
|
800
|
+
&& (node.nodeName === 'INPUT' || node.nodeName === 'TEXTAREA' || node.nodeName === 'SELECT')) return;
|
|
801
|
+
const attrNs = name.startsWith('xlink:') ? 'http://www.w3.org/1999/xlink'
|
|
802
|
+
: name.startsWith('xml:') ? 'http://www.w3.org/XML/1998/namespace' : null;
|
|
803
|
+
if (attrNs !== null && typeof node.setAttributeNS === 'function') {
|
|
804
|
+
if (newValue == null || newValue === false) node.removeAttributeNS(attrNs, name.slice(name.indexOf(':') + 1));
|
|
805
|
+
else node.setAttributeNS(attrNs, name, String(newValue));
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
706
808
|
if (name === 'style' && typeof newValue === 'object' && newValue !== null) {
|
|
707
809
|
newValue = styleToString(newValue);
|
|
708
810
|
}
|
package/src/html.js
CHANGED
|
@@ -158,6 +158,8 @@ function renderNode(vnode, widgets, policy, onUnsafe, selection = null) {
|
|
|
158
158
|
return escapeText(String(vnode));
|
|
159
159
|
}
|
|
160
160
|
if (!isElementNode(vnode)) {
|
|
161
|
+
if (Array.isArray(vnode)) return childrenOf(['root', {}, vnode])
|
|
162
|
+
.map((child) => renderNode(child, widgets, policy, onUnsafe, selection)).join('');
|
|
161
163
|
return '';
|
|
162
164
|
}
|
|
163
165
|
const tag = vnode[0];
|