@kubuild/renderer 0.0.1 → 0.1.0
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/LICENSE +21 -0
- package/dist/animation.d.ts +26 -0
- package/dist/animation.d.ts.map +1 -0
- package/dist/code-generator.d.ts +47 -0
- package/dist/code-generator.d.ts.map +1 -0
- package/dist/error-boundary.d.ts.map +1 -1
- package/dist/index.cjs +751 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +741 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +44754 -0
- package/dist/index.mjs.map +1 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/styles.d.ts +8 -3
- package/dist/styles.d.ts.map +1 -1
- package/package.json +13 -14
package/dist/index.js
CHANGED
|
@@ -215,7 +215,7 @@ function dispatchAction(options) {
|
|
|
215
215
|
|
|
216
216
|
// src/renderer.tsx
|
|
217
217
|
import { resolveBinding as resolveBinding3, sanitizeUrl, sanitizeHtml } from "@kubuild/core";
|
|
218
|
-
import { icons as lucideIcons } from "lucide-react";
|
|
218
|
+
import { icons as lucideIcons, Package, Puzzle, AlertTriangle as AlertTriangle2 } from "lucide-react";
|
|
219
219
|
|
|
220
220
|
// src/styles.ts
|
|
221
221
|
function resolveNodeStyles(styles, viewport = "desktop") {
|
|
@@ -230,17 +230,18 @@ function escapeCssValue(value) {
|
|
|
230
230
|
function escapeCssIdent(value) {
|
|
231
231
|
return value.replace(/["\\\]]/g, "\\$&");
|
|
232
232
|
}
|
|
233
|
-
function styleDefinitionToCssDeclarations(styleDefinition) {
|
|
233
|
+
function styleDefinitionToCssDeclarations(styleDefinition, options) {
|
|
234
234
|
if (!styleDefinition || typeof styleDefinition !== "object") return "";
|
|
235
235
|
const declarations = [];
|
|
236
|
+
const suffix = options?.important ? " !important" : "";
|
|
236
237
|
for (const [key, value] of Object.entries(styleDefinition)) {
|
|
237
238
|
if (value === null || value === void 0 || value === "") continue;
|
|
238
239
|
const property = key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
239
|
-
declarations.push(`${property}: ${escapeCssValue(value)};`);
|
|
240
|
+
declarations.push(`${property}: ${escapeCssValue(value)}${suffix};`);
|
|
240
241
|
}
|
|
241
242
|
return declarations.join(" ");
|
|
242
243
|
}
|
|
243
|
-
function collectStateStylesCss(document) {
|
|
244
|
+
function collectStateStylesCss(document, options = { important: true }) {
|
|
244
245
|
if (!document?.document) return "";
|
|
245
246
|
const rules = [];
|
|
246
247
|
const walk = (node) => {
|
|
@@ -248,7 +249,8 @@ function collectStateStylesCss(document) {
|
|
|
248
249
|
if (states && typeof states === "object") {
|
|
249
250
|
for (const [state, styleDefinition] of Object.entries(states)) {
|
|
250
251
|
const declarations = styleDefinitionToCssDeclarations(
|
|
251
|
-
styleDefinition
|
|
252
|
+
styleDefinition,
|
|
253
|
+
{ important: options.important !== false }
|
|
252
254
|
);
|
|
253
255
|
if (!declarations) continue;
|
|
254
256
|
const safeState = /^::?[a-zA-Z-]+$/.test(state) ? state : null;
|
|
@@ -289,8 +291,208 @@ p, h1, h2, h3, h4, h5, h6 {
|
|
|
289
291
|
}
|
|
290
292
|
`.trim();
|
|
291
293
|
|
|
294
|
+
// src/animation.ts
|
|
295
|
+
var ANIMATION_KEYFRAMES_CSS = `
|
|
296
|
+
/* Entrance / AOS Keyframes */
|
|
297
|
+
@keyframes kb-anim-fade {
|
|
298
|
+
from { opacity: 0; }
|
|
299
|
+
to { opacity: 1; }
|
|
300
|
+
}
|
|
301
|
+
@keyframes kb-anim-fade-up {
|
|
302
|
+
from { opacity: 0; transform: translateY(24px); }
|
|
303
|
+
to { opacity: 1; transform: translateY(0); }
|
|
304
|
+
}
|
|
305
|
+
@keyframes kb-anim-fade-down {
|
|
306
|
+
from { opacity: 0; transform: translateY(-24px); }
|
|
307
|
+
to { opacity: 1; transform: translateY(0); }
|
|
308
|
+
}
|
|
309
|
+
@keyframes kb-anim-fade-left {
|
|
310
|
+
from { opacity: 0; transform: translateX(24px); }
|
|
311
|
+
to { opacity: 1; transform: translateX(0); }
|
|
312
|
+
}
|
|
313
|
+
@keyframes kb-anim-fade-right {
|
|
314
|
+
from { opacity: 0; transform: translateX(-24px); }
|
|
315
|
+
to { opacity: 1; transform: translateX(0); }
|
|
316
|
+
}
|
|
317
|
+
@keyframes kb-anim-zoom-in {
|
|
318
|
+
from { opacity: 0; transform: scale(0.92); }
|
|
319
|
+
to { opacity: 1; transform: scale(1); }
|
|
320
|
+
}
|
|
321
|
+
@keyframes kb-anim-zoom-out {
|
|
322
|
+
from { opacity: 0; transform: scale(1.08); }
|
|
323
|
+
to { opacity: 1; transform: scale(1); }
|
|
324
|
+
}
|
|
325
|
+
@keyframes kb-anim-slide-up {
|
|
326
|
+
from { transform: translateY(100%); }
|
|
327
|
+
to { transform: translateY(0); }
|
|
328
|
+
}
|
|
329
|
+
@keyframes kb-anim-slide-down {
|
|
330
|
+
from { transform: translateY(-100%); }
|
|
331
|
+
to { transform: translateY(0); }
|
|
332
|
+
}
|
|
333
|
+
@keyframes kb-anim-slide-left {
|
|
334
|
+
from { transform: translateX(100%); }
|
|
335
|
+
to { transform: translateX(0); }
|
|
336
|
+
}
|
|
337
|
+
@keyframes kb-anim-slide-right {
|
|
338
|
+
from { transform: translateX(-100%); }
|
|
339
|
+
to { transform: translateX(0); }
|
|
340
|
+
}
|
|
341
|
+
@keyframes kb-anim-flip-up {
|
|
342
|
+
from { opacity: 0; transform: perspective(600px) rotateX(45deg); }
|
|
343
|
+
to { opacity: 1; transform: perspective(600px) rotateX(0deg); }
|
|
344
|
+
}
|
|
345
|
+
@keyframes kb-anim-flip-down {
|
|
346
|
+
from { opacity: 0; transform: perspective(600px) rotateX(-45deg); }
|
|
347
|
+
to { opacity: 1; transform: perspective(600px) rotateX(0deg); }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* Continuous Loop Keyframes */
|
|
351
|
+
@keyframes kb-loop-pulse {
|
|
352
|
+
0%, 100% { transform: scale(1); opacity: 1; }
|
|
353
|
+
50% { transform: scale(1.05); opacity: 0.9; }
|
|
354
|
+
}
|
|
355
|
+
@keyframes kb-loop-bounce {
|
|
356
|
+
0%, 100% { transform: translateY(0); }
|
|
357
|
+
50% { transform: translateY(-8px); }
|
|
358
|
+
}
|
|
359
|
+
@keyframes kb-loop-spin {
|
|
360
|
+
from { transform: rotate(0deg); }
|
|
361
|
+
to { transform: rotate(360deg); }
|
|
362
|
+
}
|
|
363
|
+
@keyframes kb-loop-float {
|
|
364
|
+
0%, 100% { transform: translateY(0); }
|
|
365
|
+
50% { transform: translateY(-6px); }
|
|
366
|
+
}
|
|
367
|
+
@keyframes kb-loop-shimmer {
|
|
368
|
+
0%, 100% { opacity: 0.75; }
|
|
369
|
+
50% { opacity: 1; }
|
|
370
|
+
}
|
|
371
|
+
`.trim();
|
|
372
|
+
function escapeCssIdent2(value) {
|
|
373
|
+
return value.replace(/["\\\]]/g, "\\$&");
|
|
374
|
+
}
|
|
375
|
+
function getHoverEffectCss(nodeId, hoverEffect) {
|
|
376
|
+
const safeId = escapeCssIdent2(nodeId);
|
|
377
|
+
const rules = [];
|
|
378
|
+
switch (hoverEffect) {
|
|
379
|
+
case "lift":
|
|
380
|
+
rules.push(
|
|
381
|
+
`[data-kubuild-node="${safeId}"] { transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s ease !important; will-change: transform; }`,
|
|
382
|
+
`[data-kubuild-node="${safeId}"]:hover { transform: translateY(-4px) !important; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1) !important; }`
|
|
383
|
+
);
|
|
384
|
+
break;
|
|
385
|
+
case "scale":
|
|
386
|
+
rules.push(
|
|
387
|
+
`[data-kubuild-node="${safeId}"] { transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important; will-change: transform; }`,
|
|
388
|
+
`[data-kubuild-node="${safeId}"]:hover { transform: scale(1.04) !important; }`
|
|
389
|
+
);
|
|
390
|
+
break;
|
|
391
|
+
case "glow":
|
|
392
|
+
rules.push(
|
|
393
|
+
`[data-kubuild-node="${safeId}"] { transition: box-shadow 0.25s ease !important; }`,
|
|
394
|
+
`[data-kubuild-node="${safeId}"]:hover { box-shadow: 0 0 20px 2px rgba(59, 130, 246, 0.5) !important; }`
|
|
395
|
+
);
|
|
396
|
+
break;
|
|
397
|
+
case "tilt":
|
|
398
|
+
rules.push(
|
|
399
|
+
`[data-kubuild-node="${safeId}"] { transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important; will-change: transform; }`,
|
|
400
|
+
`[data-kubuild-node="${safeId}"]:hover { transform: rotate(2deg) scale(1.02) !important; }`
|
|
401
|
+
);
|
|
402
|
+
break;
|
|
403
|
+
default:
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
return rules;
|
|
407
|
+
}
|
|
408
|
+
function getLoopEffectCss(nodeId, loopEffect) {
|
|
409
|
+
const safeId = escapeCssIdent2(nodeId);
|
|
410
|
+
const rules = [];
|
|
411
|
+
switch (loopEffect) {
|
|
412
|
+
case "pulse":
|
|
413
|
+
rules.push(
|
|
414
|
+
`[data-kubuild-node="${safeId}"] { animation: kb-loop-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important; }`
|
|
415
|
+
);
|
|
416
|
+
break;
|
|
417
|
+
case "bounce":
|
|
418
|
+
rules.push(
|
|
419
|
+
`[data-kubuild-node="${safeId}"] { animation: kb-loop-bounce 1.5s ease-in-out infinite !important; }`
|
|
420
|
+
);
|
|
421
|
+
break;
|
|
422
|
+
case "spin":
|
|
423
|
+
rules.push(
|
|
424
|
+
`[data-kubuild-node="${safeId}"] { animation: kb-loop-spin 3s linear infinite !important; }`
|
|
425
|
+
);
|
|
426
|
+
break;
|
|
427
|
+
case "float":
|
|
428
|
+
rules.push(
|
|
429
|
+
`[data-kubuild-node="${safeId}"] { animation: kb-loop-float 3s ease-in-out infinite !important; }`
|
|
430
|
+
);
|
|
431
|
+
break;
|
|
432
|
+
case "shimmer":
|
|
433
|
+
rules.push(
|
|
434
|
+
`[data-kubuild-node="${safeId}"] { animation: kb-loop-shimmer 2s ease-in-out infinite !important; }`
|
|
435
|
+
);
|
|
436
|
+
break;
|
|
437
|
+
default:
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
return rules;
|
|
441
|
+
}
|
|
442
|
+
function getEntranceAnimationCss(nodeId, anim) {
|
|
443
|
+
if (!anim.type || anim.type === "none") return [];
|
|
444
|
+
const safeId = escapeCssIdent2(nodeId);
|
|
445
|
+
const duration = typeof anim.duration === "number" ? anim.duration : 600;
|
|
446
|
+
const delay = typeof anim.delay === "number" ? anim.delay : 0;
|
|
447
|
+
const easing = anim.easing || "ease-out";
|
|
448
|
+
return [
|
|
449
|
+
`[data-kubuild-node="${safeId}"] { animation-name: kb-anim-${anim.type} !important; animation-duration: ${duration}ms !important; animation-delay: ${delay}ms !important; animation-timing-function: ${easing} !important; animation-fill-mode: both !important; }`
|
|
450
|
+
];
|
|
451
|
+
}
|
|
452
|
+
function collectAnimationStylesCss(document) {
|
|
453
|
+
if (!document?.document) return "";
|
|
454
|
+
const rules = [];
|
|
455
|
+
let hasAnyAnimation = false;
|
|
456
|
+
const walk = (node) => {
|
|
457
|
+
const anim = node.animation;
|
|
458
|
+
if (anim) {
|
|
459
|
+
if (anim.hoverEffect && anim.hoverEffect !== "none") {
|
|
460
|
+
rules.push(...getHoverEffectCss(node.id, anim.hoverEffect));
|
|
461
|
+
hasAnyAnimation = true;
|
|
462
|
+
}
|
|
463
|
+
if (anim.loopEffect && anim.loopEffect !== "none") {
|
|
464
|
+
rules.push(...getLoopEffectCss(node.id, anim.loopEffect));
|
|
465
|
+
hasAnyAnimation = true;
|
|
466
|
+
}
|
|
467
|
+
if (anim.type && anim.type !== "none") {
|
|
468
|
+
rules.push(...getEntranceAnimationCss(node.id, anim));
|
|
469
|
+
hasAnyAnimation = true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
node.children?.forEach(walk);
|
|
473
|
+
};
|
|
474
|
+
walk(document.document);
|
|
475
|
+
if (!hasAnyAnimation) return "";
|
|
476
|
+
return `${ANIMATION_KEYFRAMES_CSS}
|
|
477
|
+
|
|
478
|
+
${rules.join("\n")}`;
|
|
479
|
+
}
|
|
480
|
+
function replayNodeAnimation(nodeId, rootElement) {
|
|
481
|
+
const root = rootElement || (typeof window !== "undefined" ? window.document : null);
|
|
482
|
+
if (!root) return false;
|
|
483
|
+
const el = root.querySelector(`[data-kubuild-node="${escapeCssIdent2(nodeId)}"]`);
|
|
484
|
+
if (!el) return false;
|
|
485
|
+
const currentAnimation = el.style.animation;
|
|
486
|
+
el.style.animation = "none";
|
|
487
|
+
void el.offsetWidth;
|
|
488
|
+
el.style.animation = currentAnimation;
|
|
489
|
+
el.dispatchEvent(new CustomEvent("kubuild:replay-animation", { bubbles: true, detail: { nodeId } }));
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
|
|
292
493
|
// src/error-boundary.tsx
|
|
293
494
|
import { Component } from "react";
|
|
495
|
+
import { AlertTriangle } from "lucide-react";
|
|
294
496
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
295
497
|
var ComponentErrorBoundary = class extends Component {
|
|
296
498
|
constructor(props) {
|
|
@@ -327,10 +529,13 @@ var ComponentErrorBoundary = class extends Component {
|
|
|
327
529
|
lineHeight: "1.4"
|
|
328
530
|
},
|
|
329
531
|
children: [
|
|
330
|
-
/* @__PURE__ */ jsxs("div", { style: { fontWeight: 600, marginBottom: "4px" }, children: [
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
532
|
+
/* @__PURE__ */ jsxs("div", { style: { fontWeight: 600, marginBottom: "4px", display: "flex", alignItems: "center", gap: "6px" }, children: [
|
|
533
|
+
/* @__PURE__ */ jsx2(AlertTriangle, { size: 14, "aria-hidden": "true" }),
|
|
534
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
535
|
+
"Component Render Error: <",
|
|
536
|
+
componentType,
|
|
537
|
+
">"
|
|
538
|
+
] })
|
|
334
539
|
] }),
|
|
335
540
|
/* @__PURE__ */ jsxs("div", { style: { fontSize: "11px", color: "#7f1d1d", wordBreak: "break-all" }, children: [
|
|
336
541
|
"Node ID: ",
|
|
@@ -1589,9 +1794,12 @@ function NodeRenderer({
|
|
|
1589
1794
|
fontFamily: "system-ui, -apple-system, sans-serif"
|
|
1590
1795
|
},
|
|
1591
1796
|
onClick: handleClick,
|
|
1592
|
-
children: /* @__PURE__ */ jsxs2("div", { style: { fontWeight: 600, fontSize: "13px" }, children: [
|
|
1593
|
-
|
|
1594
|
-
/* @__PURE__ */
|
|
1797
|
+
children: /* @__PURE__ */ jsxs2("div", { style: { fontWeight: 600, fontSize: "13px", display: "flex", alignItems: "center", gap: "6px" }, children: [
|
|
1798
|
+
/* @__PURE__ */ jsx3(Package, { size: 14, "aria-hidden": "true" }),
|
|
1799
|
+
/* @__PURE__ */ jsxs2("span", { children: [
|
|
1800
|
+
"Collection: expected an array at ",
|
|
1801
|
+
/* @__PURE__ */ jsx3("code", { children: sourceKey ?? "(missing sourceKey)" })
|
|
1802
|
+
] })
|
|
1595
1803
|
] })
|
|
1596
1804
|
}
|
|
1597
1805
|
);
|
|
@@ -1642,9 +1850,12 @@ function NodeRenderer({
|
|
|
1642
1850
|
},
|
|
1643
1851
|
onClick: handleClick,
|
|
1644
1852
|
children: [
|
|
1645
|
-
/* @__PURE__ */ jsxs2("div", { style: { fontWeight: 600, fontSize: "13px", marginBottom: "4px" }, children: [
|
|
1646
|
-
|
|
1647
|
-
/* @__PURE__ */
|
|
1853
|
+
/* @__PURE__ */ jsxs2("div", { style: { fontWeight: 600, fontSize: "13px", marginBottom: "4px", display: "flex", alignItems: "center", gap: "6px" }, children: [
|
|
1854
|
+
/* @__PURE__ */ jsx3(Puzzle, { size: 14, "aria-hidden": "true" }),
|
|
1855
|
+
/* @__PURE__ */ jsxs2("span", { children: [
|
|
1856
|
+
"Unknown Component: ",
|
|
1857
|
+
/* @__PURE__ */ jsx3("code", { children: node.type })
|
|
1858
|
+
] })
|
|
1648
1859
|
] }),
|
|
1649
1860
|
/* @__PURE__ */ jsxs2("div", { style: { fontSize: "11px", color: "#b45309", marginBottom: childrenElements ? "8px" : 0 }, children: [
|
|
1650
1861
|
"Node ID: ",
|
|
@@ -1690,10 +1901,13 @@ function NodeRenderer({
|
|
|
1690
1901
|
lineHeight: "1.4"
|
|
1691
1902
|
},
|
|
1692
1903
|
children: [
|
|
1693
|
-
/* @__PURE__ */ jsxs2("div", { style: { fontWeight: 600, marginBottom: "4px" }, children: [
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1904
|
+
/* @__PURE__ */ jsxs2("div", { style: { fontWeight: 600, marginBottom: "4px", display: "flex", alignItems: "center", gap: "6px" }, children: [
|
|
1905
|
+
/* @__PURE__ */ jsx3(AlertTriangle2, { size: 14, "aria-hidden": "true" }),
|
|
1906
|
+
/* @__PURE__ */ jsxs2("span", { children: [
|
|
1907
|
+
"Component Render Error: <",
|
|
1908
|
+
node.type,
|
|
1909
|
+
">"
|
|
1910
|
+
] })
|
|
1697
1911
|
] }),
|
|
1698
1912
|
/* @__PURE__ */ jsxs2("div", { style: { fontSize: "11px", color: "#7f1d1d", wordBreak: "break-all" }, children: [
|
|
1699
1913
|
"Node ID: ",
|
|
@@ -1738,6 +1952,10 @@ var KubuildRenderer = ({
|
|
|
1738
1952
|
const css = collectStateStylesCss(document);
|
|
1739
1953
|
return css ? /* @__PURE__ */ jsx3("style", { "data-kubuild-state-styles": true, children: css }) : null;
|
|
1740
1954
|
})(),
|
|
1955
|
+
(() => {
|
|
1956
|
+
const animCss = collectAnimationStylesCss(document);
|
|
1957
|
+
return animCss ? /* @__PURE__ */ jsx3("style", { "data-kubuild-animation-styles": true, children: animCss }) : null;
|
|
1958
|
+
})(),
|
|
1741
1959
|
/* @__PURE__ */ jsx3(
|
|
1742
1960
|
NodeRenderer,
|
|
1743
1961
|
{
|
|
@@ -1997,7 +2215,503 @@ var PreviewViewportAdapter = ({
|
|
|
1997
2215
|
);
|
|
1998
2216
|
};
|
|
1999
2217
|
var KubuildPreviewViewport = PreviewViewportAdapter;
|
|
2218
|
+
|
|
2219
|
+
// src/code-generator.ts
|
|
2220
|
+
function escapeHtml(str) {
|
|
2221
|
+
if (str === null || str === void 0) return "";
|
|
2222
|
+
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2223
|
+
}
|
|
2224
|
+
function escapeAttr(str) {
|
|
2225
|
+
return escapeHtml(str);
|
|
2226
|
+
}
|
|
2227
|
+
function getYouTubeId2(url) {
|
|
2228
|
+
if (!url || typeof url !== "string") return null;
|
|
2229
|
+
const match = url.match(
|
|
2230
|
+
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i
|
|
2231
|
+
);
|
|
2232
|
+
return match ? match[1] : null;
|
|
2233
|
+
}
|
|
2234
|
+
function getVimeoId2(url) {
|
|
2235
|
+
if (!url || typeof url !== "string") return null;
|
|
2236
|
+
const match = url.match(
|
|
2237
|
+
/(?:vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|album\/(\d+)\/video\/|video\/|))(\d+)/i
|
|
2238
|
+
);
|
|
2239
|
+
return match ? match[3] : null;
|
|
2240
|
+
}
|
|
2241
|
+
function renderNodeToHtml(node, indentLevel, options) {
|
|
2242
|
+
const indent = " ".repeat(indentLevel * (options.indentSize ?? 2));
|
|
2243
|
+
const innerIndent = " ".repeat((indentLevel + 1) * (options.indentSize ?? 2));
|
|
2244
|
+
const props = node.props || {};
|
|
2245
|
+
const children = node.children || [];
|
|
2246
|
+
const includeNodeClass = options.includeNodeClasses !== false;
|
|
2247
|
+
const nodeClass = includeNodeClass ? `kb-node-${node.id}` : "";
|
|
2248
|
+
const buildClass = (...classes) => {
|
|
2249
|
+
return classes.filter(Boolean).join(" ");
|
|
2250
|
+
};
|
|
2251
|
+
const idAttr = props.id ? ` id="${escapeAttr(props.id)}"` : "";
|
|
2252
|
+
switch (node.type) {
|
|
2253
|
+
case "page": {
|
|
2254
|
+
const tag = options.rootTag || "main";
|
|
2255
|
+
const cls = buildClass("kb-page", nodeClass);
|
|
2256
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2257
|
+
if (!childHtml) {
|
|
2258
|
+
return `${indent}<${tag} class="${cls}"${idAttr}></${tag}>`;
|
|
2259
|
+
}
|
|
2260
|
+
return `${indent}<${tag} class="${cls}"${idAttr}>
|
|
2261
|
+
${childHtml}
|
|
2262
|
+
${indent}</${tag}>`;
|
|
2263
|
+
}
|
|
2264
|
+
case "section": {
|
|
2265
|
+
const ariaLabel = props.ariaLabel ? ` aria-label="${escapeAttr(props.ariaLabel)}"` : "";
|
|
2266
|
+
const cls = buildClass("kb-section", nodeClass);
|
|
2267
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2268
|
+
if (!childHtml) {
|
|
2269
|
+
return `${indent}<section class="${cls}"${idAttr}${ariaLabel}></section>`;
|
|
2270
|
+
}
|
|
2271
|
+
return `${indent}<section class="${cls}"${idAttr}${ariaLabel}>
|
|
2272
|
+
${childHtml}
|
|
2273
|
+
${indent}</section>`;
|
|
2274
|
+
}
|
|
2275
|
+
case "container": {
|
|
2276
|
+
const cls = buildClass("kb-container", nodeClass);
|
|
2277
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2278
|
+
if (!childHtml) {
|
|
2279
|
+
return `${indent}<div class="${cls}"${idAttr}></div>`;
|
|
2280
|
+
}
|
|
2281
|
+
return `${indent}<div class="${cls}"${idAttr}>
|
|
2282
|
+
${childHtml}
|
|
2283
|
+
${indent}</div>`;
|
|
2284
|
+
}
|
|
2285
|
+
case "columns": {
|
|
2286
|
+
const cls = buildClass("kb-columns", nodeClass);
|
|
2287
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2288
|
+
if (!childHtml) {
|
|
2289
|
+
return `${indent}<div class="${cls}"${idAttr}></div>`;
|
|
2290
|
+
}
|
|
2291
|
+
return `${indent}<div class="${cls}"${idAttr}>
|
|
2292
|
+
${childHtml}
|
|
2293
|
+
${indent}</div>`;
|
|
2294
|
+
}
|
|
2295
|
+
case "heading": {
|
|
2296
|
+
let level = "h2";
|
|
2297
|
+
if (typeof props.level === "string" && /^h[1-6]$/i.test(props.level)) {
|
|
2298
|
+
level = props.level.toLowerCase();
|
|
2299
|
+
} else if (typeof props.level === "number" && props.level >= 1 && props.level <= 6) {
|
|
2300
|
+
level = `h${props.level}`;
|
|
2301
|
+
} else if (typeof props.tag === "string" && /^h[1-6]$/i.test(props.tag)) {
|
|
2302
|
+
level = props.tag.toLowerCase();
|
|
2303
|
+
}
|
|
2304
|
+
const text = props.text ?? props.value ?? props.content ?? "Heading";
|
|
2305
|
+
const cls = buildClass("kb-heading", nodeClass);
|
|
2306
|
+
return `${indent}<${level} class="${cls}"${idAttr}>${escapeHtml(text)}</${level}>`;
|
|
2307
|
+
}
|
|
2308
|
+
case "paragraph": {
|
|
2309
|
+
const text = props.text ?? props.value ?? props.content ?? "";
|
|
2310
|
+
const cls = buildClass("kb-paragraph", nodeClass);
|
|
2311
|
+
return `${indent}<p class="${cls}"${idAttr}>${escapeHtml(text)}</p>`;
|
|
2312
|
+
}
|
|
2313
|
+
case "text": {
|
|
2314
|
+
const tag = props.as || "p";
|
|
2315
|
+
const text = props.text ?? props.value ?? props.content ?? "";
|
|
2316
|
+
const cls = buildClass("kb-text", nodeClass);
|
|
2317
|
+
return `${indent}<${tag} class="${cls}"${idAttr}>${escapeHtml(text)}</${tag}>`;
|
|
2318
|
+
}
|
|
2319
|
+
case "link": {
|
|
2320
|
+
const href = props.href ? ` href="${escapeAttr(props.href)}"` : ' href="#"';
|
|
2321
|
+
const target = props.target ? ` target="${escapeAttr(props.target)}"` : "";
|
|
2322
|
+
const rel = props.rel ? ` rel="${escapeAttr(props.rel)}"` : target.includes("_blank") ? ' rel="noopener noreferrer"' : "";
|
|
2323
|
+
const text = props.text ?? props.label ?? props.value;
|
|
2324
|
+
const cls = buildClass("kb-link", nodeClass);
|
|
2325
|
+
if (children.length > 0) {
|
|
2326
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2327
|
+
return `${indent}<a class="${cls}"${idAttr}${href}${target}${rel}>
|
|
2328
|
+
${childHtml}
|
|
2329
|
+
${indent}</a>`;
|
|
2330
|
+
}
|
|
2331
|
+
return `${indent}<a class="${cls}"${idAttr}${href}${target}${rel}>${escapeHtml(text ?? "Link")}</a>`;
|
|
2332
|
+
}
|
|
2333
|
+
case "blockquote": {
|
|
2334
|
+
const cite = props.cite ? ` cite="${escapeAttr(props.cite)}"` : "";
|
|
2335
|
+
const quote = props.quote ?? props.text ?? props.value;
|
|
2336
|
+
const author = props.author ?? props.citeAuthor;
|
|
2337
|
+
const cls = buildClass("kb-blockquote", nodeClass);
|
|
2338
|
+
if (quote || author) {
|
|
2339
|
+
const quoteHtml = quote ? `${innerIndent}<p>${escapeHtml(quote)}</p>` : "";
|
|
2340
|
+
const authorHtml = author ? `${innerIndent}<cite>${escapeHtml(author)}</cite>` : "";
|
|
2341
|
+
const parts = [quoteHtml, authorHtml].filter(Boolean).join("\n");
|
|
2342
|
+
return `${indent}<blockquote class="${cls}"${idAttr}${cite}>
|
|
2343
|
+
${parts}
|
|
2344
|
+
${indent}</blockquote>`;
|
|
2345
|
+
}
|
|
2346
|
+
if (children.length > 0) {
|
|
2347
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2348
|
+
return `${indent}<blockquote class="${cls}"${idAttr}${cite}>
|
|
2349
|
+
${childHtml}
|
|
2350
|
+
${indent}</blockquote>`;
|
|
2351
|
+
}
|
|
2352
|
+
return `${indent}<blockquote class="${cls}"${idAttr}${cite}></blockquote>`;
|
|
2353
|
+
}
|
|
2354
|
+
case "badge": {
|
|
2355
|
+
const text = props.text ?? props.label ?? props.value ?? "Badge";
|
|
2356
|
+
const cls = buildClass("kb-badge", nodeClass);
|
|
2357
|
+
return `${indent}<span class="${cls}"${idAttr}>${escapeHtml(text)}</span>`;
|
|
2358
|
+
}
|
|
2359
|
+
case "code-block": {
|
|
2360
|
+
const code = props.code ?? props.text ?? props.value ?? "";
|
|
2361
|
+
const lang = props.language || props.lang;
|
|
2362
|
+
const langClass = lang ? ` class="language-${escapeAttr(lang)}"` : "";
|
|
2363
|
+
const cls = buildClass("kb-code-block", nodeClass);
|
|
2364
|
+
return `${indent}<pre class="${cls}"${idAttr}><code${langClass}>${escapeHtml(code)}</code></pre>`;
|
|
2365
|
+
}
|
|
2366
|
+
case "divider": {
|
|
2367
|
+
const cls = buildClass("kb-divider", nodeClass);
|
|
2368
|
+
const text = props.text ?? props.label;
|
|
2369
|
+
if (text) {
|
|
2370
|
+
return `${indent}<div class="${cls}"${idAttr} role="separator"><span>${escapeHtml(text)}</span></div>`;
|
|
2371
|
+
}
|
|
2372
|
+
return `${indent}<hr class="${cls}"${idAttr} />`;
|
|
2373
|
+
}
|
|
2374
|
+
case "spacer": {
|
|
2375
|
+
const cls = buildClass("kb-spacer", nodeClass);
|
|
2376
|
+
return `${indent}<div class="${cls}"${idAttr} aria-hidden="true"></div>`;
|
|
2377
|
+
}
|
|
2378
|
+
case "image": {
|
|
2379
|
+
const src = props.src ? ` src="${escapeAttr(props.src)}"` : ' src=""';
|
|
2380
|
+
const alt = props.alt ? ` alt="${escapeAttr(props.alt)}"` : ' alt=""';
|
|
2381
|
+
const loading = props.loading ? ` loading="${escapeAttr(props.loading)}"` : ' loading="lazy"';
|
|
2382
|
+
const cls = buildClass("kb-image", nodeClass);
|
|
2383
|
+
return `${indent}<img class="${cls}"${idAttr}${src}${alt}${loading} />`;
|
|
2384
|
+
}
|
|
2385
|
+
case "video": {
|
|
2386
|
+
const src = props.src || "";
|
|
2387
|
+
const ytId = getYouTubeId2(src);
|
|
2388
|
+
const vmId = getVimeoId2(src);
|
|
2389
|
+
const cls = buildClass("kb-video", nodeClass);
|
|
2390
|
+
if (ytId) {
|
|
2391
|
+
return `${indent}<div class="kb-video-wrapper ${nodeClass}"${idAttr}>
|
|
2392
|
+
${innerIndent}<iframe src="https://www.youtube-nocookie.com/embed/${escapeAttr(ytId)}" title="${escapeAttr(props.title || "Video player")}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
|
2393
|
+
${indent}</div>`;
|
|
2394
|
+
}
|
|
2395
|
+
if (vmId) {
|
|
2396
|
+
return `${indent}<div class="kb-video-wrapper ${nodeClass}"${idAttr}>
|
|
2397
|
+
${innerIndent}<iframe src="https://player.vimeo.com/video/${escapeAttr(vmId)}" title="${escapeAttr(props.title || "Video player")}" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>
|
|
2398
|
+
${indent}</div>`;
|
|
2399
|
+
}
|
|
2400
|
+
const poster = props.poster ? ` poster="${escapeAttr(props.poster)}"` : "";
|
|
2401
|
+
const controls = props.controls !== false ? " controls" : "";
|
|
2402
|
+
const autoplay = props.autoplay ? " autoplay" : "";
|
|
2403
|
+
const loop = props.loop ? " loop" : "";
|
|
2404
|
+
const muted = props.muted ? " muted" : "";
|
|
2405
|
+
const videoSrc = src ? ` src="${escapeAttr(src)}"` : "";
|
|
2406
|
+
return `${indent}<video class="${cls}"${idAttr}${videoSrc}${poster}${controls}${autoplay}${loop}${muted}></video>`;
|
|
2407
|
+
}
|
|
2408
|
+
case "icon": {
|
|
2409
|
+
const name = props.name || props.icon || "star";
|
|
2410
|
+
const ariaLabel = props.ariaLabel ? ` aria-label="${escapeAttr(props.ariaLabel)}"` : ' aria-hidden="true"';
|
|
2411
|
+
const cls = buildClass("kb-icon", nodeClass);
|
|
2412
|
+
return `${indent}<span class="${cls}"${idAttr}${ariaLabel} data-icon="${escapeAttr(name)}"></span>`;
|
|
2413
|
+
}
|
|
2414
|
+
case "html-embed": {
|
|
2415
|
+
const rawHtml = props.html ?? props.content ?? "";
|
|
2416
|
+
const cls = buildClass("kb-html-embed", nodeClass);
|
|
2417
|
+
if (!rawHtml) {
|
|
2418
|
+
return `${indent}<div class="${cls}"${idAttr}></div>`;
|
|
2419
|
+
}
|
|
2420
|
+
return `${indent}<div class="${cls}"${idAttr}>
|
|
2421
|
+
${innerIndent}${rawHtml}
|
|
2422
|
+
${indent}</div>`;
|
|
2423
|
+
}
|
|
2424
|
+
case "button": {
|
|
2425
|
+
const label = props.label ?? props.text ?? props.value ?? "Button";
|
|
2426
|
+
const type = props.type ? ` type="${escapeAttr(props.type)}"` : ' type="button"';
|
|
2427
|
+
const disabled = props.disabled ? " disabled" : "";
|
|
2428
|
+
const cls = buildClass("kb-button", nodeClass);
|
|
2429
|
+
if (props.href) {
|
|
2430
|
+
const href = ` href="${escapeAttr(props.href)}"`;
|
|
2431
|
+
const target = props.target ? ` target="${escapeAttr(props.target)}"` : "";
|
|
2432
|
+
return `${indent}<a class="${cls}"${idAttr}${href}${target}>${escapeHtml(label)}</a>`;
|
|
2433
|
+
}
|
|
2434
|
+
return `${indent}<button class="${cls}"${idAttr}${type}${disabled}>${escapeHtml(label)}</button>`;
|
|
2435
|
+
}
|
|
2436
|
+
case "form": {
|
|
2437
|
+
const action = props.action ? ` action="${escapeAttr(props.action)}"` : "";
|
|
2438
|
+
const method = props.method ? ` method="${escapeAttr(props.method)}"` : ' method="POST"';
|
|
2439
|
+
const cls = buildClass("kb-form", nodeClass);
|
|
2440
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2441
|
+
if (!childHtml) {
|
|
2442
|
+
return `${indent}<form class="${cls}"${idAttr}${action}${method}></form>`;
|
|
2443
|
+
}
|
|
2444
|
+
return `${indent}<form class="${cls}"${idAttr}${action}${method}>
|
|
2445
|
+
${childHtml}
|
|
2446
|
+
${indent}</form>`;
|
|
2447
|
+
}
|
|
2448
|
+
case "input": {
|
|
2449
|
+
const type = props.type ? ` type="${escapeAttr(props.type)}"` : ' type="text"';
|
|
2450
|
+
const name = props.name ? ` name="${escapeAttr(props.name)}"` : "";
|
|
2451
|
+
const placeholder = props.placeholder ? ` placeholder="${escapeAttr(props.placeholder)}"` : "";
|
|
2452
|
+
const value = props.value !== void 0 ? ` value="${escapeAttr(props.value)}"` : "";
|
|
2453
|
+
const required = props.required ? " required" : "";
|
|
2454
|
+
const disabled = props.disabled ? " disabled" : "";
|
|
2455
|
+
const cls = buildClass("kb-input", nodeClass);
|
|
2456
|
+
return `${indent}<input class="${cls}"${idAttr}${type}${name}${placeholder}${value}${required}${disabled} />`;
|
|
2457
|
+
}
|
|
2458
|
+
case "textarea": {
|
|
2459
|
+
const name = props.name ? ` name="${escapeAttr(props.name)}"` : "";
|
|
2460
|
+
const placeholder = props.placeholder ? ` placeholder="${escapeAttr(props.placeholder)}"` : "";
|
|
2461
|
+
const rows = props.rows ? ` rows="${escapeAttr(props.rows)}"` : ' rows="4"';
|
|
2462
|
+
const value = props.value ?? props.defaultValue ?? "";
|
|
2463
|
+
const required = props.required ? " required" : "";
|
|
2464
|
+
const disabled = props.disabled ? " disabled" : "";
|
|
2465
|
+
const cls = buildClass("kb-textarea", nodeClass);
|
|
2466
|
+
return `${indent}<textarea class="${cls}"${idAttr}${name}${placeholder}${rows}${required}${disabled}>${escapeHtml(value)}</textarea>`;
|
|
2467
|
+
}
|
|
2468
|
+
case "select": {
|
|
2469
|
+
const name = props.name ? ` name="${escapeAttr(props.name)}"` : "";
|
|
2470
|
+
const required = props.required ? " required" : "";
|
|
2471
|
+
const disabled = props.disabled ? " disabled" : "";
|
|
2472
|
+
const cls = buildClass("kb-select", nodeClass);
|
|
2473
|
+
const optionsList = Array.isArray(props.options) ? props.options : [];
|
|
2474
|
+
const optionIndent = " ".repeat((indentLevel + 1) * (options.indentSize ?? 2));
|
|
2475
|
+
const optionsHtml = optionsList.map((opt) => {
|
|
2476
|
+
const optVal = typeof opt === "object" ? opt.value : opt;
|
|
2477
|
+
const optLabel = typeof opt === "object" ? opt.label : opt;
|
|
2478
|
+
const selected = props.value === optVal || props.defaultValue === optVal ? " selected" : "";
|
|
2479
|
+
return `${optionIndent}<option value="${escapeAttr(optVal)}"${selected}>${escapeHtml(optLabel)}</option>`;
|
|
2480
|
+
}).join("\n");
|
|
2481
|
+
if (!optionsHtml) {
|
|
2482
|
+
return `${indent}<select class="${cls}"${idAttr}${name}${required}${disabled}></select>`;
|
|
2483
|
+
}
|
|
2484
|
+
return `${indent}<select class="${cls}"${idAttr}${name}${required}${disabled}>
|
|
2485
|
+
${optionsHtml}
|
|
2486
|
+
${indent}</select>`;
|
|
2487
|
+
}
|
|
2488
|
+
case "checkbox": {
|
|
2489
|
+
const name = props.name ? ` name="${escapeAttr(props.name)}"` : "";
|
|
2490
|
+
const checked = props.checked || props.defaultChecked ? " checked" : "";
|
|
2491
|
+
const label = props.label ?? props.text ?? "";
|
|
2492
|
+
const cls = buildClass("kb-checkbox-label", nodeClass);
|
|
2493
|
+
return `${indent}<label class="${cls}"${idAttr}><input type="checkbox"${name}${checked} /><span>${escapeHtml(label)}</span></label>`;
|
|
2494
|
+
}
|
|
2495
|
+
case "radio": {
|
|
2496
|
+
const name = props.name ? ` name="${escapeAttr(props.name)}"` : "";
|
|
2497
|
+
const value = props.value ? ` value="${escapeAttr(props.value)}"` : "";
|
|
2498
|
+
const checked = props.checked || props.defaultChecked ? " checked" : "";
|
|
2499
|
+
const label = props.label ?? props.text ?? "";
|
|
2500
|
+
const cls = buildClass("kb-radio-label", nodeClass);
|
|
2501
|
+
return `${indent}<label class="${cls}"${idAttr}><input type="radio"${name}${value}${checked} /><span>${escapeHtml(label)}</span></label>`;
|
|
2502
|
+
}
|
|
2503
|
+
case "list": {
|
|
2504
|
+
const tag = props.tag === "ol" || props.type === "ol" || props.ordered ? "ol" : "ul";
|
|
2505
|
+
const cls = buildClass("kb-list", nodeClass);
|
|
2506
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2507
|
+
if (!childHtml) {
|
|
2508
|
+
return `${indent}<${tag} class="${cls}"${idAttr}></${tag}>`;
|
|
2509
|
+
}
|
|
2510
|
+
return `${indent}<${tag} class="${cls}"${idAttr}>
|
|
2511
|
+
${childHtml}
|
|
2512
|
+
${indent}</${tag}>`;
|
|
2513
|
+
}
|
|
2514
|
+
case "list-item": {
|
|
2515
|
+
const text = props.text ?? props.value;
|
|
2516
|
+
const cls = buildClass("kb-list-item", nodeClass);
|
|
2517
|
+
if (children.length > 0) {
|
|
2518
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2519
|
+
return `${indent}<li class="${cls}"${idAttr}>
|
|
2520
|
+
${childHtml}
|
|
2521
|
+
${indent}</li>`;
|
|
2522
|
+
}
|
|
2523
|
+
return `${indent}<li class="${cls}"${idAttr}>${escapeHtml(text ?? "List item")}</li>`;
|
|
2524
|
+
}
|
|
2525
|
+
case "table": {
|
|
2526
|
+
const cls = buildClass("kb-table", nodeClass);
|
|
2527
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2528
|
+
if (!childHtml) {
|
|
2529
|
+
return `${indent}<table class="${cls}"${idAttr}></table>`;
|
|
2530
|
+
}
|
|
2531
|
+
return `${indent}<table class="${cls}"${idAttr}>
|
|
2532
|
+
${childHtml}
|
|
2533
|
+
${indent}</table>`;
|
|
2534
|
+
}
|
|
2535
|
+
case "table-row": {
|
|
2536
|
+
const cls = buildClass("kb-table-row", nodeClass);
|
|
2537
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2538
|
+
if (!childHtml) {
|
|
2539
|
+
return `${indent}<tr class="${cls}"${idAttr}></tr>`;
|
|
2540
|
+
}
|
|
2541
|
+
return `${indent}<tr class="${cls}"${idAttr}>
|
|
2542
|
+
${childHtml}
|
|
2543
|
+
${indent}</tr>`;
|
|
2544
|
+
}
|
|
2545
|
+
case "table-cell": {
|
|
2546
|
+
const isHeader = props.isHeader || props.type === "header" || props.tag === "th";
|
|
2547
|
+
const tag = isHeader ? "th" : "td";
|
|
2548
|
+
const colSpan = props.colSpan && Number(props.colSpan) > 1 ? ` colspan="${escapeAttr(props.colSpan)}"` : "";
|
|
2549
|
+
const rowSpan = props.rowSpan && Number(props.rowSpan) > 1 ? ` rowspan="${escapeAttr(props.rowSpan)}"` : "";
|
|
2550
|
+
const text = props.text ?? props.value ?? "";
|
|
2551
|
+
const cls = buildClass("kb-table-cell", nodeClass);
|
|
2552
|
+
if (children.length > 0) {
|
|
2553
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2554
|
+
return `${indent}<${tag} class="${cls}"${idAttr}${colSpan}${rowSpan}>
|
|
2555
|
+
${childHtml}
|
|
2556
|
+
${indent}</${tag}>`;
|
|
2557
|
+
}
|
|
2558
|
+
return `${indent}<${tag} class="${cls}"${idAttr}${colSpan}${rowSpan}>${escapeHtml(text)}</${tag}>`;
|
|
2559
|
+
}
|
|
2560
|
+
case "collection": {
|
|
2561
|
+
const cls = buildClass("kb-collection", nodeClass);
|
|
2562
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2563
|
+
if (!childHtml) {
|
|
2564
|
+
return `${indent}<div class="${cls}"${idAttr}></div>`;
|
|
2565
|
+
}
|
|
2566
|
+
return `${indent}<div class="${cls}"${idAttr}>
|
|
2567
|
+
${childHtml}
|
|
2568
|
+
${indent}</div>`;
|
|
2569
|
+
}
|
|
2570
|
+
default: {
|
|
2571
|
+
const cls = buildClass(`kb-${node.type}`, nodeClass);
|
|
2572
|
+
const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
|
|
2573
|
+
if (!childHtml) {
|
|
2574
|
+
return `${indent}<div class="${cls}"${idAttr}></div>`;
|
|
2575
|
+
}
|
|
2576
|
+
return `${indent}<div class="${cls}"${idAttr}>
|
|
2577
|
+
${childHtml}
|
|
2578
|
+
${indent}</div>`;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
function generateSemanticHtml(docOrNode, options = {}) {
|
|
2583
|
+
const rootNode = "document" in docOrNode ? docOrNode.document : docOrNode;
|
|
2584
|
+
if (!rootNode) return "";
|
|
2585
|
+
return renderNodeToHtml(rootNode, 0, options);
|
|
2586
|
+
}
|
|
2587
|
+
function formatCssRule(selector, declarationsStr, indent = "") {
|
|
2588
|
+
if (!declarationsStr.trim()) return "";
|
|
2589
|
+
const rules = declarationsStr.split(";").map((r) => r.trim()).filter(Boolean);
|
|
2590
|
+
if (rules.length === 0) return "";
|
|
2591
|
+
const indentedRules = rules.map((r) => `${indent} ${r};`).join("\n");
|
|
2592
|
+
return `${indent}${selector} {
|
|
2593
|
+
${indentedRules}
|
|
2594
|
+
${indent}}`;
|
|
2595
|
+
}
|
|
2596
|
+
function generateDocumentCss(docOrNode, options = {}) {
|
|
2597
|
+
const rootNode = "document" in docOrNode ? docOrNode.document : docOrNode;
|
|
2598
|
+
if (!rootNode) return "";
|
|
2599
|
+
const classPrefix = options.classPrefix || "kb-node-";
|
|
2600
|
+
const baseRules = [];
|
|
2601
|
+
const tabletRules = [];
|
|
2602
|
+
const mobileRules = [];
|
|
2603
|
+
const stateRules = [];
|
|
2604
|
+
const walk = (node) => {
|
|
2605
|
+
const selector = `.${classPrefix}${node.id}`;
|
|
2606
|
+
if (node.styles) {
|
|
2607
|
+
const baseStyles = {
|
|
2608
|
+
...node.styles.base || {},
|
|
2609
|
+
...node.styles.desktop || {}
|
|
2610
|
+
};
|
|
2611
|
+
const baseDecls = styleDefinitionToCssDeclarations(baseStyles);
|
|
2612
|
+
if (baseDecls) {
|
|
2613
|
+
baseRules.push(formatCssRule(selector, baseDecls));
|
|
2614
|
+
}
|
|
2615
|
+
if (node.styles.tablet) {
|
|
2616
|
+
const tabletDecls = styleDefinitionToCssDeclarations(node.styles.tablet);
|
|
2617
|
+
if (tabletDecls) {
|
|
2618
|
+
tabletRules.push(formatCssRule(selector, tabletDecls, " "));
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
if (node.styles.mobile) {
|
|
2622
|
+
const mobileDecls = styleDefinitionToCssDeclarations(node.styles.mobile);
|
|
2623
|
+
if (mobileDecls) {
|
|
2624
|
+
mobileRules.push(formatCssRule(selector, mobileDecls, " "));
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
if (node.styles.states && typeof node.styles.states === "object") {
|
|
2628
|
+
for (const [state, styleDef] of Object.entries(node.styles.states)) {
|
|
2629
|
+
if (!styleDef) continue;
|
|
2630
|
+
const safeState = /^::?[a-zA-Z-]+$/.test(state) ? state : null;
|
|
2631
|
+
if (!safeState) continue;
|
|
2632
|
+
const stateDecls = styleDefinitionToCssDeclarations(styleDef);
|
|
2633
|
+
if (stateDecls) {
|
|
2634
|
+
stateRules.push(formatCssRule(`${selector}${safeState}`, stateDecls));
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
node.children?.forEach(walk);
|
|
2640
|
+
};
|
|
2641
|
+
walk(rootNode);
|
|
2642
|
+
const sections = [];
|
|
2643
|
+
if (options.includeReset !== false) {
|
|
2644
|
+
sections.push(`/* ==========================================================================
|
|
2645
|
+
Baseline Reset & Typography Standards
|
|
2646
|
+
========================================================================== */
|
|
2647
|
+
${DEFAULT_CSS_RESET}`);
|
|
2648
|
+
}
|
|
2649
|
+
if (baseRules.length > 0) {
|
|
2650
|
+
sections.push(`/* ==========================================================================
|
|
2651
|
+
Component Styles
|
|
2652
|
+
========================================================================== */
|
|
2653
|
+
${baseRules.join("\n\n")}`);
|
|
2654
|
+
}
|
|
2655
|
+
if (stateRules.length > 0) {
|
|
2656
|
+
sections.push(`/* ==========================================================================
|
|
2657
|
+
Interactive & Hover States
|
|
2658
|
+
========================================================================== */
|
|
2659
|
+
${stateRules.join("\n\n")}`);
|
|
2660
|
+
}
|
|
2661
|
+
if (tabletRules.length > 0) {
|
|
2662
|
+
sections.push(`/* ==========================================================================
|
|
2663
|
+
Tablet Breakpoint (max-width: 1024px)
|
|
2664
|
+
========================================================================== */
|
|
2665
|
+
@media (max-width: 1024px) {
|
|
2666
|
+
${tabletRules.join("\n\n")}
|
|
2667
|
+
}`);
|
|
2668
|
+
}
|
|
2669
|
+
if (mobileRules.length > 0) {
|
|
2670
|
+
sections.push(`/* ==========================================================================
|
|
2671
|
+
Mobile Breakpoint (max-width: 640px)
|
|
2672
|
+
========================================================================== */
|
|
2673
|
+
@media (max-width: 640px) {
|
|
2674
|
+
${mobileRules.join("\n\n")}
|
|
2675
|
+
}`);
|
|
2676
|
+
}
|
|
2677
|
+
const pageDoc = "document" in docOrNode ? docOrNode : { schema: "stora.page", version: "1.0.0", document: rootNode };
|
|
2678
|
+
const animStyles = collectAnimationStylesCss(pageDoc);
|
|
2679
|
+
if (animStyles) {
|
|
2680
|
+
sections.push(`/* ==========================================================================
|
|
2681
|
+
Animations & Motion Keyframes
|
|
2682
|
+
========================================================================== */
|
|
2683
|
+
${animStyles}`);
|
|
2684
|
+
}
|
|
2685
|
+
return sections.join("\n\n");
|
|
2686
|
+
}
|
|
2687
|
+
function generateStandaloneHtml(doc, options = {}) {
|
|
2688
|
+
const title = escapeHtml(doc.metadata?.title || "KUBUILD Page");
|
|
2689
|
+
const description = doc.metadata?.description ? escapeAttr(doc.metadata.description) : "";
|
|
2690
|
+
const author = doc.metadata?.author ? escapeAttr(doc.metadata.author) : "";
|
|
2691
|
+
const lang = options.lang || "en";
|
|
2692
|
+
const css = generateDocumentCss(doc, options.cssOptions);
|
|
2693
|
+
const html = generateSemanticHtml(doc, options.htmlOptions);
|
|
2694
|
+
const metaDescription = description ? ` <meta name="description" content="${description}">
|
|
2695
|
+
` : "";
|
|
2696
|
+
const metaAuthor = author ? ` <meta name="author" content="${author}">
|
|
2697
|
+
` : "";
|
|
2698
|
+
return `<!DOCTYPE html>
|
|
2699
|
+
<html lang="${lang}">
|
|
2700
|
+
<head>
|
|
2701
|
+
<meta charset="UTF-8">
|
|
2702
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2703
|
+
<title>${title}</title>
|
|
2704
|
+
${metaDescription}${metaAuthor} <style>
|
|
2705
|
+
${css}
|
|
2706
|
+
</style>
|
|
2707
|
+
</head>
|
|
2708
|
+
<body>
|
|
2709
|
+
${html}
|
|
2710
|
+
</body>
|
|
2711
|
+
</html>`;
|
|
2712
|
+
}
|
|
2000
2713
|
export {
|
|
2714
|
+
ANIMATION_KEYFRAMES_CSS,
|
|
2001
2715
|
ComponentErrorBoundary,
|
|
2002
2716
|
DEFAULT_BREAKPOINTS,
|
|
2003
2717
|
DEFAULT_CSS_RESET,
|
|
@@ -2010,11 +2724,19 @@ export {
|
|
|
2010
2724
|
NodeRenderer,
|
|
2011
2725
|
PreviewViewportAdapter,
|
|
2012
2726
|
RenderContextProvider,
|
|
2727
|
+
collectAnimationStylesCss,
|
|
2013
2728
|
collectStateStylesCss,
|
|
2014
2729
|
createMinimalRenderContext,
|
|
2015
2730
|
createRenderContext,
|
|
2016
2731
|
dispatchAction,
|
|
2732
|
+
generateDocumentCss,
|
|
2733
|
+
generateSemanticHtml,
|
|
2734
|
+
generateStandaloneHtml,
|
|
2735
|
+
getEntranceAnimationCss,
|
|
2736
|
+
getHoverEffectCss,
|
|
2737
|
+
getLoopEffectCss,
|
|
2017
2738
|
isActionRegistered,
|
|
2739
|
+
replayNodeAnimation,
|
|
2018
2740
|
resolveActionPayload,
|
|
2019
2741
|
resolveActionPayloadDetailed,
|
|
2020
2742
|
resolveAssetSync,
|