@domql/brender 3.2.7

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.
@@ -0,0 +1,311 @@
1
+ import { createEnv } from "./env.js";
2
+ import { resetKeys, assignKeys, mapKeysToElements } from "./keys.js";
3
+ import { extractMetadata, generateHeadHtml } from "./metadata.js";
4
+ import { hydrate } from "./hydrate.js";
5
+ import { parseHTML } from "linkedom";
6
+ const render = async (data, options = {}) => {
7
+ const { route = "/", state: stateOverrides, context: contextOverrides } = options;
8
+ const { window, document } = createEnv();
9
+ const body = document.body;
10
+ window.location.pathname = route;
11
+ const smblsSrc = new URL("../../packages/smbls/src/createDomql.js", import.meta.url);
12
+ const { createDomqlElement } = await import(smblsSrc.href);
13
+ const app = data.app || {};
14
+ const ctx = {
15
+ state: { ...data.state, ...stateOverrides || {} },
16
+ dependencies: data.dependencies || {},
17
+ components: data.components || {},
18
+ snippets: data.snippets || {},
19
+ pages: data.pages || {},
20
+ functions: data.functions || {},
21
+ methods: data.methods || {},
22
+ designSystem: data.designSystem || {},
23
+ files: data.files || {},
24
+ ...data.config || data.settings || {},
25
+ // Virtual DOM environment
26
+ document,
27
+ window,
28
+ parent: { node: body },
29
+ // Caller overrides
30
+ ...contextOverrides || {}
31
+ };
32
+ resetKeys();
33
+ const element = await createDomqlElement(app, ctx);
34
+ assignKeys(body);
35
+ const registry = mapKeysToElements(element);
36
+ const metadata = extractMetadata(data, route);
37
+ const html = body.innerHTML;
38
+ return { html, metadata, registry, element };
39
+ };
40
+ const renderElement = async (elementDef, options = {}) => {
41
+ const { context = {} } = options;
42
+ const { window, document } = createEnv();
43
+ const body = document.body;
44
+ const { create } = await import("@domql/element");
45
+ resetKeys();
46
+ const element = create(elementDef, { node: body }, "root", {
47
+ context: { document, window, ...context }
48
+ });
49
+ assignKeys(body);
50
+ const registry = mapKeysToElements(element);
51
+ const html = body.innerHTML;
52
+ return { html, registry, element };
53
+ };
54
+ const renderRoute = async (data, options = {}) => {
55
+ const { route = "/" } = options;
56
+ const ds = data.designSystem || {};
57
+ const pageDef = (data.pages || {})[route];
58
+ if (!pageDef) return null;
59
+ const result = await renderElement(pageDef, {
60
+ context: {
61
+ components: data.components || {},
62
+ snippets: data.snippets || {},
63
+ designSystem: ds,
64
+ state: data.state || {},
65
+ functions: data.functions || {},
66
+ methods: data.methods || {}
67
+ }
68
+ });
69
+ const { document: cssDoc } = parseHTML(`<html><head></head><body>${result.html}</body></html>`);
70
+ let emotionInstance;
71
+ try {
72
+ const { default: createInstance } = await import("@emotion/css/create-instance");
73
+ emotionInstance = createInstance({ key: "smbls", container: cssDoc.head });
74
+ } catch {
75
+ }
76
+ hydrate(result.element, {
77
+ root: cssDoc.body,
78
+ renderEvents: false,
79
+ events: false,
80
+ emotion: emotionInstance,
81
+ designSystem: ds
82
+ });
83
+ return {
84
+ html: cssDoc.body.innerHTML,
85
+ css: extractCSS(result.element, ds),
86
+ resetCss: generateResetCSS(ds.reset),
87
+ fontLinks: generateFontLinks(ds),
88
+ metadata: extractMetadata(data, route),
89
+ brKeyCount: Object.keys(result.registry).length
90
+ };
91
+ };
92
+ const renderPage = async (data, route = "/", options = {}) => {
93
+ const { lang = "en", themeColor } = options;
94
+ const result = await renderRoute(data, { route });
95
+ if (!result) return null;
96
+ const metadata = { ...result.metadata };
97
+ if (themeColor) metadata["theme-color"] = themeColor;
98
+ const headTags = generateHeadHtml(metadata);
99
+ const html = `<!DOCTYPE html>
100
+ <html lang="${lang}">
101
+ <head>
102
+ ${headTags}
103
+ ${result.fontLinks}
104
+ <style>${result.resetCss}</style>
105
+ <style data-emotion="smbls">
106
+ ${result.css}
107
+ </style>
108
+ </head>
109
+ <body>
110
+ ${result.html}
111
+ </body>
112
+ </html>`;
113
+ return { html, route, brKeyCount: result.brKeyCount };
114
+ };
115
+ const CSS_COLOR_PROPS = /* @__PURE__ */ new Set([
116
+ "color",
117
+ "background",
118
+ "backgroundColor",
119
+ "borderColor",
120
+ "borderTopColor",
121
+ "borderRightColor",
122
+ "borderBottomColor",
123
+ "borderLeftColor",
124
+ "outlineColor",
125
+ "fill",
126
+ "stroke"
127
+ ]);
128
+ const NON_CSS_PROPS = /* @__PURE__ */ new Set([
129
+ "href",
130
+ "src",
131
+ "alt",
132
+ "title",
133
+ "id",
134
+ "name",
135
+ "type",
136
+ "value",
137
+ "placeholder",
138
+ "target",
139
+ "rel",
140
+ "loading",
141
+ "srcset",
142
+ "sizes",
143
+ "media",
144
+ "role",
145
+ "tabindex",
146
+ "for",
147
+ "action",
148
+ "method",
149
+ "enctype",
150
+ "autocomplete",
151
+ "autofocus",
152
+ "theme",
153
+ "__element",
154
+ "update"
155
+ ]);
156
+ const camelToKebab = (str) => str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
157
+ const resolveShorthand = (key, val) => {
158
+ if (key === "flexAlign" && typeof val === "string") {
159
+ const [alignItems, justifyContent] = val.split(" ");
160
+ return { display: "flex", "align-items": alignItems, "justify-content": justifyContent };
161
+ }
162
+ if (key === "gridAlign" && typeof val === "string") {
163
+ const [alignItems, justifyContent] = val.split(" ");
164
+ return { display: "grid", "align-items": alignItems, "justify-content": justifyContent };
165
+ }
166
+ if (key === "round" && val) {
167
+ return { "border-radius": typeof val === "number" ? val + "px" : val };
168
+ }
169
+ if (key === "boxSize" && val) {
170
+ return { width: val, height: val };
171
+ }
172
+ return null;
173
+ };
174
+ const resolveInnerProps = (obj, colorMap) => {
175
+ const result = {};
176
+ for (const k in obj) {
177
+ const v = obj[k];
178
+ const expanded = resolveShorthand(k, v);
179
+ if (expanded) {
180
+ Object.assign(result, expanded);
181
+ continue;
182
+ }
183
+ if (typeof v !== "string" && typeof v !== "number") continue;
184
+ result[camelToKebab(k)] = CSS_COLOR_PROPS.has(k) && colorMap[v] ? colorMap[v] : v;
185
+ }
186
+ return result;
187
+ };
188
+ const buildCSSFromProps = (props, colorMap, mediaMap) => {
189
+ const base = {};
190
+ const mediaRules = {};
191
+ const pseudoRules = {};
192
+ for (const key in props) {
193
+ const val = props[key];
194
+ if (key.charCodeAt(0) === 64 && typeof val === "object") {
195
+ const bp = mediaMap?.[key.slice(1)];
196
+ if (bp) {
197
+ const inner = resolveInnerProps(val, colorMap);
198
+ if (Object.keys(inner).length) mediaRules[bp] = inner;
199
+ }
200
+ continue;
201
+ }
202
+ if (key.charCodeAt(0) === 58 && typeof val === "object") {
203
+ const inner = resolveInnerProps(val, colorMap);
204
+ if (Object.keys(inner).length) pseudoRules[key] = inner;
205
+ continue;
206
+ }
207
+ if (typeof val !== "string" && typeof val !== "number") continue;
208
+ if (key.charCodeAt(0) >= 65 && key.charCodeAt(0) <= 90) continue;
209
+ if (NON_CSS_PROPS.has(key)) continue;
210
+ const expanded = resolveShorthand(key, val);
211
+ if (expanded) {
212
+ Object.assign(base, expanded);
213
+ continue;
214
+ }
215
+ base[camelToKebab(key)] = CSS_COLOR_PROPS.has(key) && colorMap[val] ? colorMap[val] : val;
216
+ }
217
+ return { base, mediaRules, pseudoRules };
218
+ };
219
+ const renderCSSRule = (selector, { base, mediaRules, pseudoRules }) => {
220
+ const lines = [];
221
+ const baseDecls = Object.entries(base).map(([k, v]) => `${k}: ${v}`).join("; ");
222
+ if (baseDecls) lines.push(`${selector} { ${baseDecls}; }`);
223
+ for (const [pseudo, p] of Object.entries(pseudoRules)) {
224
+ const decls = Object.entries(p).map(([k, v]) => `${k}: ${v}`).join("; ");
225
+ if (decls) lines.push(`${selector}${pseudo} { ${decls}; }`);
226
+ }
227
+ for (const [query, p] of Object.entries(mediaRules)) {
228
+ const decls = Object.entries(p).map(([k, v]) => `${k}: ${v}`).join("; ");
229
+ const mq = query.startsWith("@") ? query : `@media ${query}`;
230
+ if (decls) lines.push(`${mq} { ${selector} { ${decls}; } }`);
231
+ }
232
+ return lines.join("\n");
233
+ };
234
+ const extractCSS = (element, ds) => {
235
+ const colorMap = ds?.color || {};
236
+ const mediaMap = ds?.media || {};
237
+ const animations = ds?.animation || {};
238
+ const rules = [];
239
+ const seen = /* @__PURE__ */ new Set();
240
+ const usedAnimations = /* @__PURE__ */ new Set();
241
+ const walk = (el) => {
242
+ if (!el || !el.__ref) return;
243
+ const { props } = el;
244
+ if (props && el.node) {
245
+ const cls = el.node.getAttribute?.("class");
246
+ if (cls && !seen.has(cls)) {
247
+ seen.add(cls);
248
+ const cssResult = buildCSSFromProps(props, colorMap, mediaMap);
249
+ const has = Object.keys(cssResult.base).length || Object.keys(cssResult.mediaRules).length || Object.keys(cssResult.pseudoRules).length;
250
+ if (has) rules.push(renderCSSRule("." + cls.split(" ")[0], cssResult));
251
+ const anim = props.animation || props.animationName;
252
+ if (typeof anim === "string") {
253
+ const name = anim.split(" ")[0];
254
+ if (animations[name]) usedAnimations.add(name);
255
+ }
256
+ }
257
+ }
258
+ if (el.__ref.__children) {
259
+ for (const ck of el.__ref.__children) {
260
+ if (el[ck]?.__ref) walk(el[ck]);
261
+ }
262
+ }
263
+ };
264
+ walk(element);
265
+ const keyframes = [];
266
+ for (const name of usedAnimations) {
267
+ const frames = animations[name];
268
+ const frameRules = Object.entries(frames).map(([step, p]) => {
269
+ const decls = Object.entries(p).map(([k, v]) => `${camelToKebab(k)}: ${v}`).join("; ");
270
+ return ` ${step} { ${decls}; }`;
271
+ }).join("\n");
272
+ keyframes.push(`@keyframes ${name} {
273
+ ${frameRules}
274
+ }`);
275
+ }
276
+ return [...keyframes, ...rules].join("\n");
277
+ };
278
+ const generateResetCSS = (reset) => {
279
+ if (!reset) return "";
280
+ const rules = [];
281
+ for (const [selector, props] of Object.entries(reset)) {
282
+ const decls = Object.entries(props).map(([k, v]) => `${camelToKebab(k)}: ${v}`).join("; ");
283
+ if (decls) rules.push(`${selector} { ${decls}; }`);
284
+ }
285
+ return rules.join("\n");
286
+ };
287
+ const generateFontLinks = (ds) => {
288
+ if (!ds) return "";
289
+ const families = ds.font_family || ds.fontFamily || {};
290
+ const fontNames = /* @__PURE__ */ new Set();
291
+ for (const val of Object.values(families)) {
292
+ const match = val.match(/'([^']+)'/);
293
+ if (match) fontNames.add(match[1]);
294
+ }
295
+ if (!fontNames.size) return "";
296
+ const params = [...fontNames].map((name) => {
297
+ const slug = name.replace(/\s+/g, "+");
298
+ return `family=${slug}:wght@300;400;500;600;700`;
299
+ }).join("&");
300
+ return [
301
+ '<link rel="preconnect" href="https://fonts.googleapis.com">',
302
+ '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',
303
+ `<link href="https://fonts.googleapis.com/css2?${params}&display=swap" rel="stylesheet">`
304
+ ].join("\n");
305
+ };
306
+ export {
307
+ render,
308
+ renderElement,
309
+ renderPage,
310
+ renderRoute
311
+ };
package/env.js ADDED
@@ -0,0 +1,43 @@
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) => setTimeout(fn, 0)
13
+ }
14
+ if (!window.cancelAnimationFrame) {
15
+ window.cancelAnimationFrame = (id) => clearTimeout(id)
16
+ }
17
+ if (!window.history) {
18
+ window.history = {
19
+ pushState: () => {},
20
+ replaceState: () => {},
21
+ state: null
22
+ }
23
+ }
24
+ if (!window.location) {
25
+ window.location = { pathname: '/', search: '', hash: '', origin: 'http://localhost' }
26
+ }
27
+ if (!window.URL) {
28
+ window.URL = URL
29
+ }
30
+ if (!window.scrollTo) {
31
+ window.scrollTo = () => {}
32
+ }
33
+
34
+ // Expose linkedom constructors on globalThis so @domql/utils isDOMNode
35
+ // can use instanceof checks (it reads from globalThis.Node, etc.)
36
+ globalThis.window = window
37
+ globalThis.document = document
38
+ globalThis.Node = window.Node || globalThis.Node
39
+ globalThis.HTMLElement = window.HTMLElement || globalThis.HTMLElement
40
+ globalThis.Window = window.constructor
41
+
42
+ return { window, document }
43
+ }