@symbo.ls/brender 3.14.7 → 3.14.9
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 +22 -0
- package/env.js +76 -0
- package/hydrate.js +462 -0
- package/index.js +54 -0
- package/keys.js +54 -0
- package/load.js +141 -0
- package/metadata.js +5 -0
- package/package.json +4 -3
- package/prefetch.js +363 -0
- package/render.js +1894 -0
- package/sitemap.js +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @symbo.ls/brender
|
|
2
2
|
|
|
3
|
+
## 3.14.9
|
|
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.13
|
|
12
|
+
- @symbo.ls/helmet@3.14.9
|
|
13
|
+
|
|
14
|
+
## 3.14.8
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Manual patch bump triggered via workflow_dispatch (scope: @symbo.ls).
|
|
19
|
+
No source change behind this bump — released to refresh dist or
|
|
20
|
+
coordinate a cross-package version line.
|
|
21
|
+
- Updated dependencies
|
|
22
|
+
- @symbo.ls/css@3.14.10
|
|
23
|
+
- @symbo.ls/helmet@3.14.8
|
|
24
|
+
|
|
3
25
|
## 3.14.7
|
|
4
26
|
|
|
5
27
|
### 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
|
+
}
|