@kudzujs/core 0.4.5 → 0.4.6
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 +9 -9
- package/framework/build.mjs +1 -1
- package/framework/core.mjs +45 -12
- package/framework/list-runtime.js +102 -19
- package/framework/shared-runtime.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -279,15 +279,15 @@ The list starts with 1,000 keyed items, then updates every label, reverses the o
|
|
|
279
279
|
|
|
280
280
|
| Framework | Initial content | Initial JS gzip | Total output | Build | Update | Reverse | Remove | Add | Operations total |
|
|
281
281
|
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
282
|
-
| Astro | Yes | **324 B** | **43.6 KB** |
|
|
283
|
-
| Kudzu | Yes |
|
|
284
|
-
|
|
|
285
|
-
| React CSR | No | 59.3 KB | 189.4 KB |
|
|
286
|
-
|
|
|
287
|
-
| Qwik CSR | No | 22.2 KB | 64.1 KB |
|
|
288
|
-
| Svelte CSR | No | 12.9 KB | 33.1 KB |
|
|
289
|
-
|
|
290
|
-
Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. Kudzu's keyed-list operations total
|
|
282
|
+
| Astro | Yes | **324 B** | **43.6 KB** | 852 ms | **3.5 ms** | 25.8 ms | 15.8 ms | **17.9 ms** | **63.0 ms** |
|
|
283
|
+
| Kudzu | Yes | 6.3 KB | 69.3 KB | **406 ms** | 6.0 ms | 28.5 ms | 14.7 ms | 19.1 ms | **68.3 ms** |
|
|
284
|
+
| Next.js | Yes | 182.2 KB | 695.2 KB | 2950 ms | 6.8 ms | 34.1 ms | **9.0 ms** | 19.3 ms | 69.2 ms |
|
|
285
|
+
| React CSR | No | 59.3 KB | 189.4 KB | 1028 ms | 9.5 ms | 35.3 ms | 10.1 ms | **17.8 ms** | 72.7 ms |
|
|
286
|
+
| Vue CSR | No | 24.3 KB | 61.3 KB | 759 ms | 10.2 ms | 33.9 ms | 9.3 ms | 19.7 ms | 73.1 ms |
|
|
287
|
+
| Qwik CSR | No | 22.2 KB | 64.1 KB | 604 ms | 15.6 ms | **20.0 ms** | 30.4 ms | 18.1 ms | 84.1 ms |
|
|
288
|
+
| Svelte CSR | No | 12.9 KB | 33.1 KB | 845 ms | 5.4 ms | 64.4 ms | 11.2 ms | 19.1 ms | 100.1 ms |
|
|
289
|
+
|
|
290
|
+
Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. Kudzu's keyed-list operations total 68.3 ms, ahead of the React fixture's 72.7 ms and within 5.3 ms of Astro across all four operations.
|
|
291
291
|
|
|
292
292
|
Benchmark snapshot collected on July 22, 2026 with Node 24.14.0 on an Intel i5-9500. These results compare the selected one-page fixtures, not ecosystem maturity, browser interaction speed beyond the listed operations, or each framework's full rendering options. Build times vary with machine load and filesystem cache.
|
|
293
293
|
|
package/framework/build.mjs
CHANGED
|
@@ -116,7 +116,7 @@ function specializeRuntime(source, events, hasStateSeed) {
|
|
|
116
116
|
if (hasStateSeed) return specialized
|
|
117
117
|
return specialized
|
|
118
118
|
.replace(" const initialState = document.body.dataset.kState\n", "")
|
|
119
|
-
.replace(" if (initialState) for (const [id, value] of JSON.parse(initialState)) browserState.set(id, value)\n", "")
|
|
119
|
+
.replace(" if (initialState) for (const [id, value, compact] of JSON.parse(initialState)) browserState.set(id, compact ? value[1].map(row => Object.fromEntries(value[0].map((field, index) => [field, row[index]]))) : value)\n", "")
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
export function parseDevPort(value) {
|
package/framework/core.mjs
CHANGED
|
@@ -187,7 +187,7 @@ function serializeCapture(name, value, seen) {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
export async function renderPage(component, metadata = {}) {
|
|
190
|
-
renderContext = { nextState: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, states: {}, textStates: new Set(), conditionStates: new Set(), events: [], bindings: [], conditions: [], lists: [], hasBehaviors: false, hasNativeBehaviors: false, hasBindings: false, hasLists: false }
|
|
190
|
+
renderContext = { nextState: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listFields: undefined, states: {}, textStates: new Set(), conditionStates: new Set(), events: [], bindings: [], conditions: [], lists: [], hasBehaviors: false, hasNativeBehaviors: false, hasBindings: false, hasLists: false }
|
|
191
191
|
|
|
192
192
|
try {
|
|
193
193
|
const body = await renderNode({ type: component, props: {} })
|
|
@@ -208,8 +208,13 @@ export async function renderPage(component, metadata = {}) {
|
|
|
208
208
|
const listRuntime = renderContext.hasLists
|
|
209
209
|
? '<script type="module" src="/assets/kudzu-list.js"></script>'
|
|
210
210
|
: ""
|
|
211
|
+
const listStates = new Set(renderContext.lists.map(list => list.state))
|
|
212
|
+
const seededListStates = new Set(renderContext.lists.filter(list => list.seed && !renderContext.textStates.has(list.state) && !renderContext.conditionStates.has(list.state)).map(list => list.state))
|
|
211
213
|
const initialState = renderContext.hasBehaviors
|
|
212
|
-
? Object.entries(renderContext.states).filter(([id]) => !renderContext.textStates.has(id) || renderContext.conditionStates.has(id)).map(([id, entry]) =>
|
|
214
|
+
? Object.entries(renderContext.states).filter(([id]) => (!renderContext.textStates.has(id) || renderContext.conditionStates.has(id)) && !seededListStates.has(id)).map(([id, entry]) => {
|
|
215
|
+
const compact = listStates.has(id) && compactListState(entry.initialValue)
|
|
216
|
+
return compact ? [id, compact, 1] : [id, entry.initialValue]
|
|
217
|
+
})
|
|
213
218
|
: []
|
|
214
219
|
const state = initialState.length
|
|
215
220
|
? ` data-k-state='${escapeJsonAttribute(initialState)}'`
|
|
@@ -305,11 +310,14 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
305
310
|
}
|
|
306
311
|
if (node?.[listMarker]) return renderList(node, namespace, selectValue)
|
|
307
312
|
if (node?.[listFieldMarker]) {
|
|
308
|
-
|
|
313
|
+
if (renderContext.listTemplate) renderContext.listFields?.add(node.field)
|
|
314
|
+
const marker = renderContext.listTemplate ? ` data-k-list-text="${escapeAttribute(node.field)}"` : ""
|
|
315
|
+
return `<template${marker}></template>${escapeHtml(node.value ?? "")}<template data-k-list-text-end></template>`
|
|
309
316
|
}
|
|
310
317
|
if (node?.[listExpressionMarker]) {
|
|
311
318
|
const descriptor = { module: node.module, handler: node.handler }
|
|
312
|
-
|
|
319
|
+
const marker = renderContext.listTemplate ? ` data-k-list-expression='${escapeJsonAttribute(descriptor)}'` : ""
|
|
320
|
+
return `<template${marker}></template>${escapeHtml(node.value ?? "")}<template data-k-list-expression-end></template>`
|
|
313
321
|
}
|
|
314
322
|
if (!node || typeof node !== "object" || !("type" in node)) {
|
|
315
323
|
throw new Error(`Cannot render ${String(node)}`)
|
|
@@ -336,9 +344,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
336
344
|
if (renderContext.listRoot) {
|
|
337
345
|
const root = renderContext.listRoot
|
|
338
346
|
renderContext.listRoot = undefined
|
|
339
|
-
attributes += root.
|
|
340
|
-
? ` data-k-list-root="${root.id}"`
|
|
341
|
-
: ` data-k-list-item='${escapeJsonAttribute([root.id, root.key])}'`
|
|
347
|
+
if (root.template) attributes += ` data-k-list-root="${root.id}"`
|
|
342
348
|
}
|
|
343
349
|
|
|
344
350
|
for (const [rawName, value] of Object.entries(props)) {
|
|
@@ -412,10 +418,13 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
412
418
|
}
|
|
413
419
|
|
|
414
420
|
if (attributeBindings.length) attributes += ` data-k-bind-attrs='${escapeJsonAttribute(attributeBindings)}'`
|
|
415
|
-
if (listAttributes.length) attributes += ` data-k-list-attrs='${escapeJsonAttribute(listAttributes)}'`
|
|
416
|
-
if (listExpressionAttributes.length) attributes += ` data-k-list-expression-attrs='${escapeJsonAttribute(listExpressionAttributes)}'`
|
|
417
|
-
if (listEvents.length) attributes += ` data-k-list-events='${escapeJsonAttribute(listEvents)}'`
|
|
418
|
-
if (directListText)
|
|
421
|
+
if (renderContext.listTemplate && listAttributes.length) attributes += ` data-k-list-attrs='${escapeJsonAttribute(listAttributes)}'`
|
|
422
|
+
if (renderContext.listTemplate && listExpressionAttributes.length) attributes += ` data-k-list-expression-attrs='${escapeJsonAttribute(listExpressionAttributes)}'`
|
|
423
|
+
if (renderContext.listTemplate && listEvents.length) attributes += ` data-k-list-events='${escapeJsonAttribute(listEvents)}'`
|
|
424
|
+
if (renderContext.listTemplate && directListText) {
|
|
425
|
+
renderContext.listFields?.add(directListText.field)
|
|
426
|
+
attributes += ` data-k-list-text="${escapeAttribute(directListText.field)}"`
|
|
427
|
+
}
|
|
419
428
|
|
|
420
429
|
if (tag === "option" && selectValue !== noSelectValue && String(optionValue(props)) === (selectValue == null ? "" : String(selectValue))) attributes += " selected"
|
|
421
430
|
|
|
@@ -428,12 +437,17 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
428
437
|
async function renderList(node, namespace, selectValue) {
|
|
429
438
|
if (namespace) throw new Error(`Reactive keyed lists are not supported inside ${namespace}`)
|
|
430
439
|
const id = `l${renderContext.nextList++}`
|
|
431
|
-
const descriptor = { id, state: node.items.id, key: node.keyField }
|
|
440
|
+
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.items.value.map(item => item[node.keyField]) }
|
|
432
441
|
renderContext.listDepth++
|
|
442
|
+
const previousListFields = renderContext.listFields
|
|
433
443
|
try {
|
|
434
444
|
renderContext.listTemplate = true
|
|
445
|
+
renderContext.listFields = new Set([node.keyField])
|
|
435
446
|
renderContext.listRoot = { id, template: true }
|
|
436
447
|
const template = await renderNode(node.render({}), namespace, selectValue)
|
|
448
|
+
if (template.includes("data-k-native-")) descriptor.mount = true
|
|
449
|
+
const seed = listSeed(node.items.value, renderContext.listFields)
|
|
450
|
+
if (seed) descriptor.seed = seed
|
|
437
451
|
let current = ""
|
|
438
452
|
renderContext.listTemplate = false
|
|
439
453
|
for (const item of node.items.value) {
|
|
@@ -447,6 +461,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
447
461
|
} finally {
|
|
448
462
|
renderContext.listRoot = undefined
|
|
449
463
|
renderContext.listTemplate = false
|
|
464
|
+
renderContext.listFields = previousListFields
|
|
450
465
|
renderContext.listDepth--
|
|
451
466
|
}
|
|
452
467
|
}
|
|
@@ -493,6 +508,24 @@ function escapeJsonAttribute(value) {
|
|
|
493
508
|
return JSON.stringify(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll("'", "'")
|
|
494
509
|
}
|
|
495
510
|
|
|
511
|
+
function compactListState(value) {
|
|
512
|
+
if (!Array.isArray(value) || !value.length) return undefined
|
|
513
|
+
const fields = Object.keys(value[0])
|
|
514
|
+
if (!fields.length || !value.every(item => Object.keys(item).length === fields.length && fields.every(field => Object.hasOwn(item, field)))) return undefined
|
|
515
|
+
return [fields, value.map(item => fields.map(field => item[field]))]
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function listSeed(items, fields) {
|
|
519
|
+
if (!items.length || !items.every(item => Object.keys(item).length === fields.size && Object.keys(item).every(field => fields.has(field)))) return undefined
|
|
520
|
+
const seed = {}
|
|
521
|
+
for (const field of fields) {
|
|
522
|
+
const types = new Set(items.map(item => item[field] === null ? "null" : typeof item[field]))
|
|
523
|
+
if (types.size !== 1 || !["string", "number", "boolean", "null"].includes([...types][0])) return undefined
|
|
524
|
+
seed[field] = [...types][0]
|
|
525
|
+
}
|
|
526
|
+
return seed
|
|
527
|
+
}
|
|
528
|
+
|
|
496
529
|
function toKebabCase(value) {
|
|
497
530
|
return value.replace(/[A-Z]/g, character => `-${character.toLowerCase()}`)
|
|
498
531
|
}
|
|
@@ -28,16 +28,22 @@ function mountLists(root) {
|
|
|
28
28
|
if (mountedLists.has(start)) continue
|
|
29
29
|
mountedLists.add(start)
|
|
30
30
|
const descriptor = JSON.parse(start.dataset.kList)
|
|
31
|
-
const
|
|
31
|
+
const end = findEnd(start, descriptor.id)
|
|
32
|
+
const roots = listRoots(start, end)
|
|
33
|
+
const templateRoot = start.content.firstElementChild
|
|
34
|
+
const parts = listItemPartPlan(templateRoot)
|
|
35
|
+
for (const root of roots) mapListItemParts(parts, root)
|
|
36
|
+
if (descriptor.seed && !browserState.has(descriptor.state)) browserState.set(descriptor.state, roots.map((root, index) => seedListItem(root, descriptor, index)))
|
|
32
37
|
const items = browserState.get(descriptor.state)
|
|
33
|
-
for (const root of roots) listItemParts(root)
|
|
34
38
|
const list = {
|
|
35
39
|
start,
|
|
36
40
|
descriptor,
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
parts,
|
|
42
|
+
seedFields: descriptor.seed && Object.keys(descriptor.seed),
|
|
43
|
+
roots: new Map(roots.map((node, index) => [keyToken(descriptor.keys[index]), node])),
|
|
44
|
+
values: new Map(),
|
|
39
45
|
container: roots[0]?.parentNode,
|
|
40
|
-
boundary:
|
|
46
|
+
boundary: end
|
|
41
47
|
}
|
|
42
48
|
register(listTargets, descriptor.state, list)
|
|
43
49
|
listRegistrations.set(start, { state: descriptor.state, list })
|
|
@@ -70,11 +76,12 @@ function updateList(list) {
|
|
|
70
76
|
const key = item?.[list.descriptor.key]
|
|
71
77
|
if (!validListKey(key)) throw new Error(`Keyed list key "${list.descriptor.key}" must be a string or finite number`)
|
|
72
78
|
assertListItem(item)
|
|
73
|
-
|
|
79
|
+
const seededValue = list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed)
|
|
80
|
+
if (seededValue === undefined) assertListValue(item, seen, true)
|
|
74
81
|
const token = keyToken(key)
|
|
75
82
|
if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
76
83
|
keys.add(token)
|
|
77
|
-
entries.push({ item, key, token, value: JSON.stringify(item) })
|
|
84
|
+
entries.push({ item, key, token, value: seededValue ?? JSON.stringify(item) })
|
|
78
85
|
}
|
|
79
86
|
const next = []
|
|
80
87
|
const values = new Map()
|
|
@@ -88,7 +95,7 @@ function updateList(list) {
|
|
|
88
95
|
if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
89
96
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
90
97
|
node.removeAttribute("data-k-list-root")
|
|
91
|
-
|
|
98
|
+
mapListItemParts(list.parts, node)
|
|
92
99
|
fillListItem(node, item)
|
|
93
100
|
additions.append(node)
|
|
94
101
|
added = true
|
|
@@ -98,13 +105,18 @@ function updateList(list) {
|
|
|
98
105
|
next.push([token, node])
|
|
99
106
|
values.set(token, value)
|
|
100
107
|
}
|
|
108
|
+
const removals = parent.ownerDocument.createDocumentFragment()
|
|
101
109
|
for (const [token, node] of list.roots) {
|
|
102
110
|
if (keys.has(token)) continue
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
if (list.descriptor.mount) {
|
|
112
|
+
unmountDom(node)
|
|
113
|
+
node.remove()
|
|
114
|
+
} else {
|
|
115
|
+
removals.append(node)
|
|
116
|
+
}
|
|
105
117
|
}
|
|
106
118
|
if (added) {
|
|
107
|
-
mountDom(additions)
|
|
119
|
+
if (list.descriptor.mount) mountDom(additions)
|
|
108
120
|
parent.insertBefore(additions, list.boundary)
|
|
109
121
|
list.container ??= parent
|
|
110
122
|
}
|
|
@@ -135,8 +147,8 @@ function fillListItem(root, item) {
|
|
|
135
147
|
const revision = (revisions.get(root) ?? 0) + 1
|
|
136
148
|
revisions.set(root, revision)
|
|
137
149
|
const parts = listItemParts(root)
|
|
138
|
-
for (const node of parts.directTexts) {
|
|
139
|
-
const text = item?.[
|
|
150
|
+
for (const [node, field] of parts.directTexts) {
|
|
151
|
+
const text = item?.[field]
|
|
140
152
|
const value = text == null ? "" : String(text)
|
|
141
153
|
const current = node.firstChild
|
|
142
154
|
if (current?.nodeType === Node.TEXT_NODE && !current.nextSibling) {
|
|
@@ -145,14 +157,14 @@ function fillListItem(root, item) {
|
|
|
145
157
|
node.textContent = value
|
|
146
158
|
}
|
|
147
159
|
}
|
|
148
|
-
for (const marker of parts.texts) {
|
|
149
|
-
patchListText(marker, "template[data-k-list-text-end]", item?.[
|
|
160
|
+
for (const [marker, field] of parts.texts) {
|
|
161
|
+
patchListText(marker, "template[data-k-list-text-end]", item?.[field])
|
|
150
162
|
}
|
|
151
163
|
for (const [node, attributes] of parts.attributes) {
|
|
152
164
|
for (const [target, field] of attributes) patchBinding(node, target, item?.[field])
|
|
153
165
|
}
|
|
154
|
-
for (const node of parts.events) {
|
|
155
|
-
for (const [event, native] of JSON.parse(
|
|
166
|
+
for (const [node, events] of parts.events) {
|
|
167
|
+
for (const [event, native] of JSON.parse(events)) {
|
|
156
168
|
native.scope = Object.fromEntries(Object.entries(native.scope).map(([name, value]) => [name, value?.type === "list-item" ? serializeItem(item) : value]))
|
|
157
169
|
node.dataset[`kNative${capitalize(event)}`] = JSON.stringify(native)
|
|
158
170
|
}
|
|
@@ -176,9 +188,9 @@ function listItemParts(root) {
|
|
|
176
188
|
if (parts) return parts
|
|
177
189
|
parts = { directTexts: [], texts: [], attributes: [], events: [], expressions: [], expressionAttributes: [] }
|
|
178
190
|
for (const node of matching(root, itemPartsSelector)) {
|
|
179
|
-
if (node.hasAttribute("data-k-list-text")) (node.tagName === "TEMPLATE" ? parts.texts : parts.directTexts).push(node)
|
|
191
|
+
if (node.hasAttribute("data-k-list-text")) (node.tagName === "TEMPLATE" ? parts.texts : parts.directTexts).push([node, node.dataset.kListText])
|
|
180
192
|
if (node.hasAttribute("data-k-list-attrs")) parts.attributes.push([node, JSON.parse(node.dataset.kListAttrs)])
|
|
181
|
-
if (node.hasAttribute("data-k-list-events")) parts.events.push(node)
|
|
193
|
+
if (node.hasAttribute("data-k-list-events")) parts.events.push([node, node.dataset.kListEvents])
|
|
182
194
|
if (node.hasAttribute("data-k-list-expression")) parts.expressions.push([node, JSON.parse(node.dataset.kListExpression)])
|
|
183
195
|
if (node.hasAttribute("data-k-list-expression-attrs")) parts.expressionAttributes.push([node, JSON.parse(node.dataset.kListExpressionAttrs)])
|
|
184
196
|
}
|
|
@@ -186,6 +198,61 @@ function listItemParts(root) {
|
|
|
186
198
|
return parts
|
|
187
199
|
}
|
|
188
200
|
|
|
201
|
+
function listItemPartPlan(template) {
|
|
202
|
+
const source = [template, ...template.querySelectorAll("*")]
|
|
203
|
+
const indexes = new Map(source.map((node, index) => [node, index]))
|
|
204
|
+
const parts = listItemParts(template)
|
|
205
|
+
return {
|
|
206
|
+
directTexts: parts.directTexts.map(([node, field]) => [indexes.get(node), field]),
|
|
207
|
+
texts: parts.texts.map(([node, field]) => [indexes.get(node), field]),
|
|
208
|
+
attributes: parts.attributes.map(([node, attributes]) => [indexes.get(node), attributes]),
|
|
209
|
+
events: parts.events.map(([node, events]) => [indexes.get(node), events]),
|
|
210
|
+
expressions: parts.expressions.map(([node, descriptor]) => [indexes.get(node), descriptor]),
|
|
211
|
+
expressionAttributes: parts.expressionAttributes.map(([node, attributes]) => [indexes.get(node), attributes])
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function mapListItemParts(parts, root) {
|
|
216
|
+
const target = [root, ...root.querySelectorAll("*")]
|
|
217
|
+
itemParts.set(root, {
|
|
218
|
+
directTexts: parts.directTexts.map(([index, field]) => [target[index], field]),
|
|
219
|
+
texts: parts.texts.map(([index, field]) => [target[index], field]),
|
|
220
|
+
attributes: parts.attributes.map(([index, attributes]) => [target[index], attributes]),
|
|
221
|
+
events: parts.events.map(([index, events]) => [target[index], events]),
|
|
222
|
+
expressions: parts.expressions.map(([index, descriptor]) => [target[index], descriptor]),
|
|
223
|
+
expressionAttributes: parts.expressionAttributes.map(([index, attributes]) => [target[index], attributes])
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function listRoots(start, end) {
|
|
228
|
+
const roots = []
|
|
229
|
+
for (let node = start.nextSibling; node && node !== end; node = node.nextSibling) {
|
|
230
|
+
if (node.nodeType === Node.ELEMENT_NODE) roots.push(node)
|
|
231
|
+
}
|
|
232
|
+
return roots
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function seedListItem(root, descriptor, index) {
|
|
236
|
+
const item = { [descriptor.key]: descriptor.keys[index] }
|
|
237
|
+
const parts = itemParts.get(root)
|
|
238
|
+
for (const [node, field] of parts.directTexts) item[field] = seedValue(node.textContent, descriptor.seed[field])
|
|
239
|
+
for (const [marker, field] of parts.texts) item[field] = seedValue(rangeText(marker, "template[data-k-list-text-end]"), descriptor.seed[field])
|
|
240
|
+
return item
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function seedValue(value, type) {
|
|
244
|
+
if (type === "number") return Number(value)
|
|
245
|
+
if (type === "boolean") return value === "true"
|
|
246
|
+
if (type === "null") return null
|
|
247
|
+
return value
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function rangeText(marker, endSelector) {
|
|
251
|
+
let value = ""
|
|
252
|
+
for (let node = marker.nextSibling; node && !(node.nodeType === Node.ELEMENT_NODE && node.matches(endSelector)); node = node.nextSibling) value += node.textContent
|
|
253
|
+
return value
|
|
254
|
+
}
|
|
255
|
+
|
|
189
256
|
function patchListText(marker, endSelector, value) {
|
|
190
257
|
let end = marker.nextSibling
|
|
191
258
|
while (end && !(end.nodeType === Node.ELEMENT_NODE && end.matches(endSelector))) end = end.nextSibling
|
|
@@ -254,6 +321,22 @@ function assertListItem(item) {
|
|
|
254
321
|
if (!item || Array.isArray(item) || prototype !== Object.prototype) throw new Error("Keyed list items must be ordinary plain objects")
|
|
255
322
|
}
|
|
256
323
|
|
|
324
|
+
function seededListValue(item, fields, seed) {
|
|
325
|
+
const keys = Reflect.ownKeys(item)
|
|
326
|
+
if (keys.length !== fields.length || keys.some(key => typeof key !== "string" || !fields.includes(key))) return undefined
|
|
327
|
+
const values = []
|
|
328
|
+
for (const field of fields) {
|
|
329
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, field)
|
|
330
|
+
if (!descriptor.enumerable) throw new Error("Keyed list items must not contain non-enumerable properties")
|
|
331
|
+
if (!("value" in descriptor)) throw new Error("Keyed list items must not contain accessors")
|
|
332
|
+
const value = descriptor.value
|
|
333
|
+
const type = value === null ? "null" : typeof value
|
|
334
|
+
if (type !== seed[field] || type === "number" && (!Number.isFinite(value) || Object.is(value, -0))) return undefined
|
|
335
|
+
values.push(value)
|
|
336
|
+
}
|
|
337
|
+
return JSON.stringify(values)
|
|
338
|
+
}
|
|
339
|
+
|
|
257
340
|
function assertListValue(value, seen, root = false) {
|
|
258
341
|
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0)) return
|
|
259
342
|
if (!value || typeof value !== "object") throw new Error("Keyed list items must contain only JSON-safe values")
|
|
@@ -55,7 +55,7 @@ export function unmountDom(root) {
|
|
|
55
55
|
|
|
56
56
|
if (typeof document !== "undefined") {
|
|
57
57
|
const initialState = document.body.dataset.kState
|
|
58
|
-
if (initialState) for (const [id, value] of JSON.parse(initialState)) browserState.set(id, value)
|
|
58
|
+
if (initialState) for (const [id, value, compact] of JSON.parse(initialState)) browserState.set(id, compact ? value[1].map(row => Object.fromEntries(value[0].map((field, index) => [field, row[index]]))) : value)
|
|
59
59
|
mountText(document)
|
|
60
60
|
|
|
61
61
|
const eventNames = ["click", "input", "change"]
|