@symbo.ls/brender 3.14.7 → 3.14.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @symbo.ls/brender
2
2
 
3
+ ## 3.14.8
4
+
5
+ ### Patch Changes
6
+
7
+ - Manual patch bump triggered via workflow_dispatch (scope: @symbo.ls).
8
+ No source change behind this bump — released to refresh dist or
9
+ coordinate a cross-package version line.
10
+ - Updated dependencies
11
+ - @symbo.ls/css@3.14.10
12
+ - @symbo.ls/helmet@3.14.8
13
+
3
14
  ## 3.14.7
4
15
 
5
16
  ### Patch Changes
package/env.js ADDED
@@ -0,0 +1,76 @@
1
+ import { parseHTML } from 'linkedom'
2
+
3
+ /**
4
+ * Creates a virtual DOM environment for server-side rendering.
5
+ * Returns window and document that DOMQL can use as context.
6
+ */
7
+ export const createEnv = (html = '<!DOCTYPE html><html><head></head><body></body></html>') => {
8
+ const { window, document } = parseHTML(html)
9
+
10
+ // Stub APIs that DOMQL/smbls may call during rendering
11
+ if (!window.requestAnimationFrame) {
12
+ window.requestAnimationFrame = (fn) => {
13
+ const id = setTimeout(fn, 0)
14
+ if (id?.unref) id.unref()
15
+ return id
16
+ }
17
+ }
18
+ if (!window.cancelAnimationFrame) {
19
+ window.cancelAnimationFrame = (id) => clearTimeout(id)
20
+ }
21
+ if (!window.history) {
22
+ window.history = {
23
+ pushState: () => {},
24
+ replaceState: () => {},
25
+ state: null
26
+ }
27
+ }
28
+ if (!window.location) {
29
+ window.location = { pathname: '/', search: '', hash: '', origin: 'http://localhost' }
30
+ }
31
+ if (!window.URL) {
32
+ window.URL = URL
33
+ }
34
+ if (!window.scrollTo) {
35
+ window.scrollTo = () => {}
36
+ }
37
+
38
+ // Storage stubs
39
+ const createStorage = () => {
40
+ const store = {}
41
+ return {
42
+ getItem: (k) => store[k] ?? null,
43
+ setItem: (k, v) => { store[k] = String(v) },
44
+ removeItem: (k) => { delete store[k] },
45
+ clear: () => { for (const k in store) delete store[k] },
46
+ get length () { return Object.keys(store).length },
47
+ key: (i) => Object.keys(store)[i] ?? null
48
+ }
49
+ }
50
+ if (!window.localStorage) window.localStorage = createStorage()
51
+ if (!window.sessionStorage) window.sessionStorage = createStorage()
52
+ if (!globalThis.localStorage) globalThis.localStorage = window.localStorage
53
+ if (!globalThis.sessionStorage) globalThis.sessionStorage = window.sessionStorage
54
+
55
+ // Stub MutationObserver for SSR (not provided by linkedom)
56
+ if (!window.MutationObserver) {
57
+ window.MutationObserver = class MutationObserver {
58
+ constructor () {}
59
+ observe () {}
60
+ disconnect () {}
61
+ takeRecords () { return [] }
62
+ }
63
+ }
64
+
65
+ // Expose linkedom constructors on globalThis so @symbo.ls/utils isDOMNode
66
+ // can use instanceof checks (it reads from globalThis.Node, etc.)
67
+ globalThis.window = window
68
+ globalThis.document = document
69
+ globalThis.Node = window.Node || globalThis.Node
70
+ globalThis.Element = window.Element || globalThis.Element
71
+ globalThis.HTMLElement = window.HTMLElement || globalThis.HTMLElement
72
+ globalThis.MutationObserver = window.MutationObserver
73
+ globalThis.Window = window.constructor
74
+
75
+ return { window, document }
76
+ }
package/hydrate.js ADDED
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Client-side hydration — reconnects pre-rendered HTML (with data-br keys)
3
+ * to a live DOMQL element tree, attaches events, and fires lifecycle hooks.
4
+ *
5
+ * After hydration the DOMQL tree owns every DOM node:
6
+ * - el.node points to the real DOM element
7
+ * - node.ref points back to the DOMQL element
8
+ * - CSS classes are generated via emotion and applied
9
+ * - DOM events (click, input, etc.) are bound
10
+ * - on.render / on.renderRouter callbacks fire
11
+ */
12
+
13
+ /**
14
+ * Collects all elements with data-br attributes from the document.
15
+ * Returns a map of brKey -> DOM node.
16
+ */
17
+ export const collectBrNodes = (root) => {
18
+ const container = root || document
19
+ const nodes = container.querySelectorAll('[data-br]')
20
+ const map = {}
21
+ nodes.forEach(node => {
22
+ map[node.getAttribute('data-br')] = node
23
+ })
24
+ return map
25
+ }
26
+
27
+ /**
28
+ * Walks a DOMQL element tree that was created with onlyResolveExtends.
29
+ * For each element with a __brKey, attaches the matching real DOM node,
30
+ * renders CSS via emotion, binds DOM events, and fires lifecycle hooks.
31
+ *
32
+ * @param {object} element - Root DOMQL element (from create with onlyResolveExtends)
33
+ * @param {object} [options]
34
+ * @param {Element} [options.root] - Root DOM element to scan for data-br nodes
35
+ * @param {boolean} [options.events=true] - Attach DOM events (click, input, etc.)
36
+ * @param {boolean} [options.renderEvents=true] - Fire on.render / on.renderRouter
37
+ * @param {object} [options.emotion] - Emotion instance for CSS class generation
38
+ * @param {object} [options.designSystem] - Design system with color/media/spacing definitions
39
+ * @returns {{ element: object, linked: number, unlinked: number }}
40
+ */
41
+ export const hydrate = (element, options = {}) => {
42
+ const {
43
+ root,
44
+ events: attachEvents = true,
45
+ renderEvents: fireRenderEvents = true,
46
+ emotion,
47
+ designSystem
48
+ } = options
49
+
50
+ const brNodes = collectBrNodes(root)
51
+ const colorMap = designSystem?.color || {}
52
+ const mediaMap = designSystem?.media || {}
53
+ let linked = 0
54
+ let unlinked = 0
55
+
56
+ const walk = (el) => {
57
+ if (!el || !el.__ref) return
58
+
59
+ const brKey = el.__ref.__brKey
60
+ if (brKey) {
61
+ const node = brNodes[brKey]
62
+ if (node) {
63
+ el.node = node
64
+ node.ref = el
65
+
66
+ if (emotion) {
67
+ renderCSS(el, emotion, colorMap, mediaMap)
68
+ }
69
+
70
+ if (attachEvents) {
71
+ bindEvents(el)
72
+ }
73
+
74
+ linked++
75
+ } else {
76
+ unlinked++
77
+ }
78
+ }
79
+
80
+ if (el.__ref.__children) {
81
+ for (const childKey of el.__ref.__children) {
82
+ const child = el[childKey]
83
+ if (child && child.__ref) walk(child)
84
+ }
85
+ }
86
+ }
87
+
88
+ walk(element)
89
+
90
+ if (fireRenderEvents) {
91
+ fireLifecycle(element)
92
+ }
93
+
94
+ return { element, linked, unlinked }
95
+ }
96
+
97
+ /**
98
+ * Renders CSS for an element: resolves props into a CSS object,
99
+ * resolves design system values (colors, media queries, pseudo-classes),
100
+ * generates emotion class name, and applies it to the DOM node.
101
+ */
102
+ const renderCSS = (el, emotion, colorMap, mediaMap) => {
103
+ const { node } = el
104
+ if (!node) return
105
+
106
+ const css = {}
107
+ let hasCss = false
108
+
109
+ for (const key in el) {
110
+ const val = el[key]
111
+
112
+ // @media breakpoint objects: @mobile, @tablet, etc.
113
+ if (key.charCodeAt(0) === 64) {
114
+ const breakpoint = mediaMap[key.slice(1)]
115
+ if (breakpoint && typeof val === 'object') {
116
+ const mediaCss = resolvePropsToCSS(val, colorMap)
117
+ if (Object.keys(mediaCss).length) {
118
+ css[breakpoint] = mediaCss
119
+ hasCss = true
120
+ }
121
+ }
122
+ continue
123
+ }
124
+
125
+ // :pseudo-class objects: :hover, :focus, :active, etc.
126
+ if (key.charCodeAt(0) === 58) {
127
+ if (typeof val === 'object') {
128
+ const pseudoCss = resolvePropsToCSS(val, colorMap)
129
+ if (Object.keys(pseudoCss).length) {
130
+ css['&' + key] = pseudoCss
131
+ hasCss = true
132
+ }
133
+ }
134
+ continue
135
+ }
136
+
137
+ // Resolve DOMQL shorthands (flexAlign, round, boxSize, etc.)
138
+ const expanded = resolveShorthand(key, val)
139
+ if (expanded) {
140
+ for (const ek in expanded) {
141
+ css[ek] = resolveValue(ek, expanded[ek], colorMap)
142
+ }
143
+ hasCss = true
144
+ continue
145
+ }
146
+
147
+ // Skip non-CSS props
148
+ if (!isCSS(key)) continue
149
+
150
+ // Resolve design system color values
151
+ css[key] = resolveValue(key, val, colorMap)
152
+ hasCss = true
153
+ }
154
+
155
+ // Inject CSS from extends chain (e.g. extends: 'Flex' → display: flex)
156
+ const extsCss = getExtendsCSS(el)
157
+ if (extsCss) {
158
+ for (const [k, v] of Object.entries(extsCss)) {
159
+ if (!css[k]) { css[k] = v; hasCss = true }
160
+ }
161
+ }
162
+
163
+ // Handle element.style object
164
+ if (el.style && typeof el.style === 'object') {
165
+ Object.assign(css, el.style)
166
+ hasCss = true
167
+ }
168
+
169
+ if (!hasCss) return
170
+
171
+ // Generate emotion class
172
+ const emotionClass = emotion.css(css)
173
+
174
+ // Build final class string
175
+ const classes = []
176
+ if (emotionClass) classes.push(emotionClass)
177
+
178
+ // Preserve key-based classname (keys starting with _ become class names)
179
+ if (typeof el.key === 'string' && el.key.charCodeAt(0) === 95 && el.key.charCodeAt(1) !== 95) {
180
+ classes.push(el.key.slice(1))
181
+ }
182
+
183
+ // Preserve explicit class from props/attr
184
+ if (el.class) classes.push(el.class)
185
+ if (el.attr?.class) classes.push(el.attr.class)
186
+
187
+ // Handle classlist
188
+ const classlist = el.classlist
189
+ if (classlist) {
190
+ if (typeof classlist === 'string') classes.push(classlist)
191
+ else if (typeof classlist === 'object') {
192
+ for (const k in classlist) {
193
+ const v = classlist[k]
194
+ if (typeof v === 'boolean' && v) classes.push(k)
195
+ else if (typeof v === 'string') classes.push(v)
196
+ else if (typeof v === 'object' && v) classes.push(emotion.css(v))
197
+ }
198
+ }
199
+ }
200
+
201
+ if (classes.length) {
202
+ node.setAttribute('class', classes.join(' '))
203
+ }
204
+
205
+ // Clean up CSS prop attributes that leaked into HTML
206
+ for (const key in el) {
207
+ if (isCSS(key) && node.hasAttribute(key)) {
208
+ node.removeAttribute(key)
209
+ }
210
+ }
211
+ }
212
+
213
+ const resolvePropsToCSS = (propsObj, colorMap) => {
214
+ const css = {}
215
+ for (const key in propsObj) {
216
+ const expanded = resolveShorthand(key, propsObj[key])
217
+ if (expanded) {
218
+ for (const ek in expanded) css[ek] = resolveValue(ek, expanded[ek], colorMap)
219
+ continue
220
+ }
221
+ if (!isCSS(key)) continue
222
+ css[key] = resolveValue(key, propsObj[key], colorMap)
223
+ }
224
+ return css
225
+ }
226
+
227
+ const COLOR_PROPS = new Set([
228
+ 'color', 'background', 'backgroundColor', 'borderColor',
229
+ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor',
230
+ 'outlineColor', 'fill', 'stroke', 'caretColor', 'columnRuleColor',
231
+ 'textDecorationColor', 'boxShadow', 'textShadow'
232
+ ])
233
+
234
+ const resolveValue = (key, val, colorMap) => {
235
+ if (typeof val !== 'string') return val
236
+ if (COLOR_PROPS.has(key) && colorMap[val]) return colorMap[val]
237
+ return val
238
+ }
239
+
240
+ const NON_CSS_PROPS = new Set([
241
+ 'href', 'src', 'alt', 'title', 'id', 'name', 'type', 'value', 'placeholder',
242
+ 'target', 'rel', 'loading', 'srcset', 'sizes', 'media', 'role', 'tabindex',
243
+ 'for', 'action', 'method', 'enctype', 'autocomplete', 'autofocus',
244
+ 'theme', '__element', 'update'
245
+ ])
246
+
247
+ // Map of component names to their implicit CSS from extends
248
+ const EXTENDS_CSS = {
249
+ Flex: { display: 'flex' },
250
+ InlineFlex: { display: 'inline-flex' },
251
+ Grid: { display: 'grid' },
252
+ InlineGrid: { display: 'inline-grid' },
253
+ Block: { display: 'block' },
254
+ Inline: { display: 'inline' }
255
+ }
256
+
257
+ const getExtendsCSS = (el) => {
258
+ const exts = el.__ref?.__extends
259
+ if (!exts || !Array.isArray(exts)) return null
260
+ for (const ext of exts) {
261
+ if (EXTENDS_CSS[ext]) return EXTENDS_CSS[ext]
262
+ }
263
+ return null
264
+ }
265
+
266
+ // DOMQL shorthand props that expand to multiple CSS properties
267
+ const resolveShorthand = (key, val) => {
268
+ if (typeof val === 'undefined' || val === null) return null
269
+
270
+ // Flex shorthands
271
+ if (key === 'flow' && typeof val === 'string') {
272
+ let [direction, wrap] = (val || 'row').split(' ')
273
+ if (val.startsWith('x') || val === 'row') direction = 'row'
274
+ if (val.startsWith('y') || val === 'column') direction = 'column'
275
+ return { display: 'flex', flexFlow: (direction || '') + ' ' + (wrap || '') }
276
+ }
277
+ if (key === 'wrap') {
278
+ return { display: 'flex', flexWrap: val }
279
+ }
280
+ if ((key === 'align' || key === 'flexAlign') && typeof val === 'string') {
281
+ const [alignItems, justifyContent] = val.split(' ')
282
+ return { display: 'flex', alignItems, justifyContent }
283
+ }
284
+ if (key === 'gridAlign' && typeof val === 'string') {
285
+ const [alignItems, justifyContent] = val.split(' ')
286
+ return { display: 'grid', alignItems, justifyContent }
287
+ }
288
+ if (key === 'flexFlow' && typeof val === 'string') {
289
+ let [direction, wrap] = (val || 'row').split(' ')
290
+ if (val.startsWith('x') || val === 'row') direction = 'row'
291
+ if (val.startsWith('y') || val === 'column') direction = 'column'
292
+ return { display: 'flex', flexFlow: (direction || '') + ' ' + (wrap || '') }
293
+ }
294
+ if (key === 'flexWrap') {
295
+ return { display: 'flex', flexWrap: val }
296
+ }
297
+
298
+ // Box/size shorthands
299
+ if (key === 'round' || (key === 'borderRadius' && val)) {
300
+ return { borderRadius: typeof val === 'number' ? val + 'px' : val }
301
+ }
302
+ if (key === 'boxSize' && typeof val === 'string') {
303
+ const [height, width] = val.split(' ')
304
+ return { height, width: width || height }
305
+ }
306
+ if (key === 'widthRange' && typeof val === 'string') {
307
+ const [minWidth, maxWidth] = val.split(' ')
308
+ return { minWidth, maxWidth: maxWidth || minWidth }
309
+ }
310
+ if (key === 'heightRange' && typeof val === 'string') {
311
+ const [minHeight, maxHeight] = val.split(' ')
312
+ return { minHeight, maxHeight: maxHeight || minHeight }
313
+ }
314
+
315
+ // Grid aliases
316
+ if (key === 'column') return { gridColumn: val }
317
+ if (key === 'columns') return { gridTemplateColumns: val }
318
+ if (key === 'templateColumns') return { gridTemplateColumns: val }
319
+ if (key === 'row') return { gridRow: val }
320
+ if (key === 'rows') return { gridTemplateRows: val }
321
+ if (key === 'templateRows') return { gridTemplateRows: val }
322
+ if (key === 'area') return { gridArea: val }
323
+ if (key === 'template') return { gridTemplate: val }
324
+ if (key === 'templateAreas') return { gridTemplateAreas: val }
325
+ if (key === 'autoColumns') return { gridAutoColumns: val }
326
+ if (key === 'autoRows') return { gridAutoRows: val }
327
+ if (key === 'autoFlow') return { gridAutoFlow: val }
328
+ if (key === 'columnStart') return { gridColumnStart: val }
329
+ if (key === 'rowStart') return { gridRowStart: val }
330
+
331
+ return null
332
+ }
333
+
334
+ const isCSS = (key) => {
335
+ const ch = key.charCodeAt(0)
336
+ if (ch === 95 || ch === 64 || ch === 58) return false
337
+ if (ch >= 65 && ch <= 90) return false
338
+ if (NON_CSS_PROPS.has(key)) return false
339
+ return CSS_PROPERTIES.has(key)
340
+ }
341
+
342
+ const CSS_PROPERTIES = new Set([
343
+ 'display', 'position', 'top', 'right', 'bottom', 'left',
344
+ 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight',
345
+ 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
346
+ 'marginBlock', 'marginInline',
347
+ 'padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
348
+ 'paddingBlock', 'paddingInline',
349
+ 'border', 'borderTop', 'borderRight', 'borderBottom', 'borderLeft',
350
+ 'borderRadius', 'borderColor', 'borderWidth', 'borderStyle',
351
+ 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth',
352
+ 'borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle',
353
+ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor',
354
+ 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomLeftRadius', 'borderBottomRightRadius',
355
+ 'background', 'backgroundColor', 'backgroundImage', 'backgroundSize', 'backgroundPosition',
356
+ 'backgroundRepeat', 'backgroundAttachment',
357
+ 'color', 'fontSize', 'fontWeight', 'fontFamily', 'fontStyle',
358
+ 'lineHeight', 'letterSpacing', 'textAlign', 'textDecoration', 'textTransform',
359
+ 'textIndent', 'textOverflow', 'textShadow',
360
+ 'opacity', 'overflow', 'overflowX', 'overflowY',
361
+ 'zIndex', 'cursor', 'pointerEvents', 'userSelect',
362
+ 'flex', 'flexDirection', 'flexWrap', 'flexFlow', 'flexGrow', 'flexShrink', 'flexBasis',
363
+ 'alignItems', 'alignContent', 'alignSelf',
364
+ 'justifyContent', 'justifyItems', 'justifySelf',
365
+ 'gap', 'rowGap', 'columnGap',
366
+ 'gridTemplateColumns', 'gridTemplateRows', 'gridColumn', 'gridRow',
367
+ 'gridArea', 'gridAutoFlow', 'gridAutoColumns', 'gridAutoRows',
368
+ 'inset',
369
+ 'inlineSize', 'blockSize', 'minInlineSize', 'maxInlineSize', 'minBlockSize', 'maxBlockSize',
370
+ 'paddingBlockStart', 'paddingBlockEnd', 'paddingInlineStart', 'paddingInlineEnd',
371
+ 'marginBlockStart', 'marginBlockEnd', 'marginInlineStart', 'marginInlineEnd',
372
+ 'transform', 'transformOrigin', 'transition',
373
+ 'animation', 'animationName', 'animationDuration', 'animationDelay',
374
+ 'animationTimingFunction', 'animationFillMode', 'animationIterationCount',
375
+ 'animationPlayState', 'animationDirection',
376
+ 'gridTemplate', 'gridTemplateAreas', 'gridColumnStart', 'gridRowStart',
377
+ 'boxShadow', 'outline', 'outlineColor', 'outlineWidth', 'outlineStyle', 'outlineOffset',
378
+ 'whiteSpace', 'wordBreak', 'wordWrap', 'overflowWrap',
379
+ 'visibility', 'boxSizing', 'objectFit', 'objectPosition',
380
+ 'filter', 'backdropFilter', 'mixBlendMode',
381
+ 'fill', 'stroke', 'strokeWidth',
382
+ 'listStyle', 'listStyleType', 'listStylePosition',
383
+ 'counterReset', 'counterIncrement', 'content',
384
+ 'aspectRatio', 'resize', 'appearance',
385
+ 'scrollBehavior', 'scrollMargin', 'scrollPadding',
386
+ 'willChange', 'contain', 'isolation',
387
+ 'caretColor', 'accentColor',
388
+ 'columnCount', 'columnGap', 'columnRuleColor', 'columnRuleStyle', 'columnRuleWidth',
389
+ 'textDecorationColor', 'textDecorationStyle', 'textDecorationThickness',
390
+ 'clipPath', 'shapeOutside'
391
+ ])
392
+
393
+ /**
394
+ * Binds DOM events from element's onX properties onto the real node.
395
+ */
396
+ const DOMQL_LIFECYCLE = new Set([
397
+ 'render', 'create', 'init', 'start', 'complete', 'done',
398
+ 'beforeClassAssign', 'attachNode', 'stateInit', 'stateCreated',
399
+ 'renderRouter', 'lazyLoad', 'error'
400
+ ])
401
+
402
+ const bindEvents = (el) => {
403
+ const { node } = el
404
+ if (!node) return
405
+
406
+ if (!el.__ref.__eventCleanup) el.__ref.__eventCleanup = []
407
+
408
+ // v3.14: event handlers are flat on element as onX properties
409
+ for (const key in el) {
410
+ if (key.length <= 2 || key[0] !== 'o' || key[1] !== 'n') continue
411
+ if (typeof el[key] !== 'function') continue
412
+ const third = key[2]
413
+ if (third !== third.toUpperCase()) continue
414
+ const eventName = third.toLowerCase() + key.slice(3)
415
+ if (DOMQL_LIFECYCLE.has(eventName)) continue
416
+ addListener(node, eventName, el[key], el)
417
+ }
418
+ }
419
+
420
+ const addListener = (node, eventName, handler, el) => {
421
+ const listener = (event) => {
422
+ try {
423
+ handler.call(el, event, el, el.state, el.context)
424
+ } catch (e) {
425
+ console.warn('[brender hydrate]', eventName, e.message)
426
+ }
427
+ }
428
+ node.addEventListener(eventName, listener)
429
+ el.__ref.__eventCleanup.push(() => node.removeEventListener(eventName, listener))
430
+ }
431
+
432
+ /**
433
+ * Walks the tree and fires onRender, onRenderRouter, onDone, onCreate
434
+ * lifecycle events — the same ones that fire during normal DOMQL create.
435
+ */
436
+ const fireLifecycle = (el) => {
437
+ if (!el || !el.__ref || !el.node) return
438
+
439
+ fireEvent(el.onRender, el)
440
+ fireEvent(el.onRenderRouter, el)
441
+ fireEvent(el.onDone, el)
442
+ fireEvent(el.onCreate, el)
443
+
444
+ if (el.__ref.__children) {
445
+ for (const childKey of el.__ref.__children) {
446
+ const child = el[childKey]
447
+ if (child && child.__ref) fireLifecycle(child)
448
+ }
449
+ }
450
+ }
451
+
452
+ const fireEvent = (fn, el) => {
453
+ if (typeof fn !== 'function') return
454
+ try {
455
+ const result = fn.call(el, el, el.state, el.context)
456
+ if (result && typeof result.then === 'function') {
457
+ result.catch(() => {})
458
+ }
459
+ } catch (e) {
460
+ console.warn('[brender hydrate]', el.key, e.message)
461
+ }
462
+ }
package/index.js ADDED
@@ -0,0 +1,54 @@
1
+ import { createEnv } from './env.js'
2
+ import { resetKeys, assignKeys, mapKeysToElements } from './keys.js'
3
+ import { loadProject, loadAndRenderAll } from './load.js'
4
+ import { render, renderElement, renderRoute, renderPage, resetGlobalCSSCache, getAccumulatedEmotionCSS, replaceEmotionCSS } from './render.js'
5
+ import { extractMetadata, generateHeadHtml } from './metadata.js'
6
+ import { collectBrNodes, hydrate } from './hydrate.js'
7
+ import { generateSitemap } from './sitemap.js'
8
+ import { prefetchPageData, injectPrefetchedState } from './prefetch.js'
9
+
10
+ export {
11
+ createEnv,
12
+ resetKeys,
13
+ assignKeys,
14
+ mapKeysToElements,
15
+ loadProject,
16
+ loadAndRenderAll,
17
+ render,
18
+ renderElement,
19
+ renderRoute,
20
+ renderPage,
21
+ resetGlobalCSSCache,
22
+ getAccumulatedEmotionCSS,
23
+ replaceEmotionCSS,
24
+ extractMetadata,
25
+ generateHeadHtml,
26
+ collectBrNodes,
27
+ hydrate,
28
+ generateSitemap,
29
+ prefetchPageData,
30
+ injectPrefetchedState
31
+ }
32
+
33
+ export default {
34
+ createEnv,
35
+ resetKeys,
36
+ assignKeys,
37
+ mapKeysToElements,
38
+ loadProject,
39
+ loadAndRenderAll,
40
+ render,
41
+ renderElement,
42
+ renderRoute,
43
+ renderPage,
44
+ resetGlobalCSSCache,
45
+ getAccumulatedEmotionCSS,
46
+ replaceEmotionCSS,
47
+ extractMetadata,
48
+ generateHeadHtml,
49
+ collectBrNodes,
50
+ hydrate,
51
+ generateSitemap,
52
+ prefetchPageData,
53
+ injectPrefetchedState
54
+ }
package/keys.js ADDED
@@ -0,0 +1,54 @@
1
+ let _keyCounter = 0
2
+
3
+ export const resetKeys = () => {
4
+ _keyCounter = 0
5
+ }
6
+
7
+ /**
8
+ * Recursively assigns `data-br` attributes to all element nodes.
9
+ * These keys allow qsql to remap static HTML back onto DOMQL elements.
10
+ */
11
+ export const assignKeys = (node) => {
12
+ if (!node) return
13
+
14
+ if (node.nodeType === 1) {
15
+ const key = `br-${_keyCounter++}`
16
+ node.setAttribute('data-br', key)
17
+ }
18
+
19
+ const children = node.childNodes
20
+ if (children) {
21
+ for (let i = 0; i < children.length; i++) {
22
+ assignKeys(children[i])
23
+ }
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Walks a DOMQL element tree and builds a registry
29
+ * mapping data-br keys to DOMQL elements.
30
+ */
31
+ export const mapKeysToElements = (element, registry = {}) => {
32
+ if (!element) return registry
33
+
34
+ const node = element.node
35
+ if (node && node.getAttribute) {
36
+ const brKey = node.getAttribute('data-br')
37
+ if (brKey) {
38
+ if (!element.__ref) element.__ref = {}
39
+ element.__ref.__brKey = brKey
40
+ registry[brKey] = element
41
+ }
42
+ }
43
+
44
+ if (element.__ref && element.__ref.__children) {
45
+ for (const childKey of element.__ref.__children) {
46
+ const child = element[childKey]
47
+ if (child && child.__ref) {
48
+ mapKeysToElements(child, registry)
49
+ }
50
+ }
51
+ }
52
+
53
+ return registry
54
+ }