@brightsideoy/kama 1.0.0-alpha.20 → 1.0.0-alpha.21

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.
Files changed (65) hide show
  1. package/content/definitions/blocks/image-gallery.yaml +34 -0
  2. package/content/definitions/blocks/youtube.yaml +8 -0
  3. package/content/definitions/field-sets/access.yaml +10 -0
  4. package/content/definitions/field-sets/page-base.yaml +16 -1
  5. package/content/definitions/taxonomies/media-tag.yaml +15 -0
  6. package/definitions/editor/default.ts +15 -1
  7. package/dist/assets/AssetEditor-B4W7tV1a.js +1 -0
  8. package/dist/assets/AssetEditor-D9sz6bfN.css +1 -0
  9. package/dist/assets/CmsFieldTaxonomy-CVlKL-l2.css +1 -0
  10. package/dist/assets/CmsFieldTaxonomy-C_AwHDwY.js +1 -0
  11. package/dist/assets/{CmsHelp-DyPJLGGi.js → CmsHelp-BQkr9KyU.js} +1 -1
  12. package/dist/assets/{CmsPublisher-DNLwjirk.js → CmsPublisher-XSCPrp5L.js} +1 -1
  13. package/dist/assets/NodeEditor-4TsNvk2f.css +1 -0
  14. package/dist/assets/NodeEditor-DA5GRtRP.js +161 -0
  15. package/dist/assets/{TrashWorkspace-DgIg1v6t.js → TrashWorkspace-CATe46WF.js} +1 -1
  16. package/dist/assets/index-CY0VYSyt.css +1 -0
  17. package/dist/assets/index-DdG9Pozx.js +14 -0
  18. package/dist/assets/{plus-FQRsZBoJ.js → plus-CX_GHzXs.js} +1 -1
  19. package/dist/assets/useNodes-CqFlc72m.js +1 -0
  20. package/dist/{dist-D_Q1y4Kh.js → dist-3Xc2TTZw.js} +1117 -456
  21. package/dist/index.html +4 -4
  22. package/dist/index.js +1117 -456
  23. package/dist/schema-compiler.js +1 -1
  24. package/package.json +5 -5
  25. package/schema.sql +10 -0
  26. package/templates/astro-starter/src/components/global/NavigationMenu.astro +11 -3
  27. package/dist/assets/AssetEditor-D5j7pwcK.css +0 -1
  28. package/dist/assets/AssetEditor-Dt1zCR7_.js +0 -1
  29. package/dist/assets/NodeEditor-CiXQKXK1.js +0 -147
  30. package/dist/assets/NodeEditor-pZI3gu4a.css +0 -1
  31. package/dist/assets/index-DQ6_RmCI.js +0 -14
  32. package/dist/assets/index-DcKiaRjI.css +0 -1
  33. package/dist/assets/useNodes-D2-VxsxR.js +0 -1
  34. package/templates/astro-starter/old_src/components/blocks/BlockRenderer.astro +0 -143
  35. package/templates/astro-starter/old_src/components/blocks/ColumnLayout.astro +0 -67
  36. package/templates/astro-starter/old_src/components/blocks/HeaderImage.astro +0 -126
  37. package/templates/astro-starter/old_src/components/blocks/Hero.astro +0 -218
  38. package/templates/astro-starter/old_src/components/blocks/HtmlBlock.astro +0 -25
  39. package/templates/astro-starter/old_src/components/blocks/ImageBlock.astro +0 -91
  40. package/templates/astro-starter/old_src/components/blocks/NodeList.astro +0 -274
  41. package/templates/astro-starter/old_src/components/blocks/TextBlock.astro +0 -15
  42. package/templates/astro-starter/old_src/components/global/Breadcrumb.astro +0 -108
  43. package/templates/astro-starter/old_src/components/global/Footer.astro +0 -50
  44. package/templates/astro-starter/old_src/components/global/Header.astro +0 -171
  45. package/templates/astro-starter/old_src/components/global/MetaHead.astro +0 -266
  46. package/templates/astro-starter/old_src/components/global/NavigationMenu.astro +0 -413
  47. package/templates/astro-starter/old_src/components/ui/KamaImage.astro +0 -180
  48. package/templates/astro-starter/old_src/layouts/SkeletonLayout.astro +0 -56
  49. package/templates/astro-starter/old_src/layouts/StandardLayout.astro +0 -63
  50. package/templates/astro-starter/old_src/lib/cms.ts +0 -219
  51. package/templates/astro-starter/old_src/lib/media/kama-image-service.ts +0 -67
  52. package/templates/astro-starter/old_src/lib/media/media-proxy.ts +0 -148
  53. package/templates/astro-starter/old_src/lib/media/media-url.ts +0 -112
  54. package/templates/astro-starter/old_src/middleware.ts +0 -536
  55. package/templates/astro-starter/old_src/pages/404.astro +0 -46
  56. package/templates/astro-starter/old_src/pages/[...slug].astro +0 -311
  57. package/templates/astro-starter/old_src/pages/api/load-nodes.ts +0 -68
  58. package/templates/astro-starter/old_src/pages/api/partials/node-cards.astro +0 -101
  59. package/templates/astro-starter/old_src/pages/llms-full.txt.ts +0 -171
  60. package/templates/astro-starter/old_src/pages/llms.txt.ts +0 -113
  61. package/templates/astro-starter/old_src/pages/media/[...path].ts +0 -14
  62. package/templates/astro-starter/old_src/pages/partials/footer.astro +0 -69
  63. package/templates/astro-starter/old_src/pages/partials/nav.astro +0 -141
  64. package/templates/astro-starter/old_src/styles/standard-theme.css +0 -157
  65. package/templates/astro-starter/old_src/utils/tiptap.ts +0 -113
@@ -1,113 +0,0 @@
1
- // src/pages/llms.txt.ts
2
- import type { APIRoute } from "astro";
3
- import { getTree, getTreeId, getChannelId, getNodeByPath } from "../lib/cms";
4
-
5
- // Ensure this route is dynamically rendered (SSR)
6
- export const prerender = false;
7
-
8
- export const GET: APIRoute = async (context) => {
9
- // 1. Setup Cloudflare Worker Fetch Binding safely
10
- const locals = (context.locals as any) || {};
11
- const cfEnv = locals.cfContext?.env || {};
12
- const cmsWorker = cfEnv.CMS_WORKER;
13
-
14
- const envApiUrl = import.meta.env.PUBLIC_API_URL;
15
- const workerApiUrl = cfEnv.PUBLIC_API_URL || (typeof process !== "undefined" ? process.env?.PUBLIC_API_URL : undefined);
16
- const baseApiUrl = String(envApiUrl || workerApiUrl || "http://localhost:8787").replace(/\/$/, "");
17
-
18
- const customFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
19
- let urlStr = input instanceof URL ? input.toString() : String(input);
20
- if (urlStr.startsWith("/")) urlStr = `${baseApiUrl}${urlStr}`;
21
-
22
- const req = new Request(urlStr, init);
23
- if (cmsWorker && typeof cmsWorker.fetch === "function") {
24
- return cmsWorker.fetch(req);
25
- }
26
- return fetch(req);
27
- };
28
-
29
- // 2. Load keys from environment or fallback defaults
30
- const channelId = getChannelId() || import.meta.env.PUBLIC_CHANNEL_ID || "main-channel";
31
- const treeId = getTreeId() || import.meta.env.PUBLIC_TREE_ID || "main-tree";
32
-
33
- // 🎯 Extract configurable fallback home slug
34
- const envHomeSlug = import.meta.env.PUBLIC_HOME_SLUG;
35
- const workerHomeSlug = cfEnv.PUBLIC_HOME_SLUG || (typeof process !== "undefined" ? process.env?.PUBLIC_HOME_SLUG : undefined);
36
- const homeSlug = String(envHomeSlug || workerHomeSlug || "home").trim();
37
-
38
- const rawOrigin = import.meta.env.PUBLIC_SITE_URL || context.url.origin;
39
- const siteOrigin = String(rawOrigin).replace(/\/$/, "");
40
- const brandName = import.meta.env.PUBLIC_SITE_NAME || "KAMA Starter";
41
-
42
- try {
43
- // 3. Parallel fetch for Navigation Tree first to resolve root node slug
44
- const rawNavTree = await getTree(treeId, channelId, customFetch).catch(() => []);
45
- const navTree = Array.isArray(rawNavTree)
46
- ? rawNavTree
47
- : (rawNavTree?.tree || rawNavTree?.items || rawNavTree?.data || []);
48
-
49
- const rootTreeSlug = navTree.length > 0 ? (navTree[0].slug || navTree[0].key || "") : "";
50
- const resolvedHomeSlug = rootTreeSlug || homeSlug;
51
-
52
- // Fetch Home Node summary
53
- const homeData = await getNodeByPath(resolvedHomeSlug, channelId, false, customFetch).catch(() => null);
54
-
55
- const siteSummary = homeData?.node?.abstract || homeData?.node?.summary || "Global Content Delivery at the Edge";
56
-
57
- // 4. Build standard-compliant llms.txt Markdown
58
- let markdown = `# ${brandName}\n\n`;
59
- markdown += `> ${siteSummary}\n\n`;
60
- markdown += `This file serves as a structured directory for AI agents and language models.\n\n`;
61
- markdown += `## Site Directory\n\n`;
62
-
63
- // Recursive tree flattener with link annotations
64
- const buildList = (nodes: any[], depth = 0) => {
65
- let list = "";
66
- const indent = " ".repeat(depth);
67
-
68
- for (const item of nodes) {
69
- const nodeData = item.node || item;
70
- if (!nodeData || nodeData.isHidden) continue;
71
-
72
- const itemSlug = nodeData.slug || nodeData.path || "";
73
- const isRootItem = itemSlug === resolvedHomeSlug || itemSlug === "" || itemSlug === "/";
74
- const itemUrl = isRootItem ? siteOrigin : `${siteOrigin}/${itemSlug.replace(/^\//, "")}`;
75
-
76
- const itemTitle = nodeData.title || nodeData.label || nodeData.menuTitle || "Untitled";
77
- const abstract = nodeData.abstract || nodeData.summary || nodeData.description;
78
- const annotation = abstract ? `: ${abstract.trim()}` : "";
79
-
80
- list += `${indent}- [${itemTitle}](${itemUrl})${annotation}\n`;
81
-
82
- if (Array.isArray(item.children) && item.children.length > 0) {
83
- list += buildList(item.children, depth + 1);
84
- }
85
- }
86
- return list;
87
- };
88
-
89
- if (navTree.length > 0) {
90
- markdown += buildList(navTree);
91
- } else {
92
- const homeTitle = homeData?.node?.title || brandName;
93
- markdown += `- [${homeTitle}](${siteOrigin}): Main landing page.\n`;
94
- }
95
-
96
- markdown += `\n---\n*Generated dynamically by Kama Engine.*`;
97
-
98
- return new Response(markdown, {
99
- status: 200,
100
- headers: {
101
- "Content-Type": "text/plain; charset=utf-8",
102
- "Cache-Control": "public, max-age=3600, s-maxage=86400"
103
- },
104
- });
105
-
106
- } catch (error) {
107
- console.error("[llms.txt Generator Error]:", error);
108
- return new Response("# Error\nCould not generate AI directory.", {
109
- status: 500,
110
- headers: { "Content-Type": "text/plain; charset=utf-8" }
111
- });
112
- }
113
- };
@@ -1,14 +0,0 @@
1
- // src/pages/media/[...path].ts
2
- import type { APIRoute } from "astro";
3
- import { handleMediaProxy } from "../../lib/media/media-proxy";
4
-
5
- export const prerender = false;
6
-
7
- export const GET: APIRoute = async (context) => {
8
- try {
9
- return await handleMediaProxy(context);
10
- } catch (error: any) {
11
- console.error("❌ [Media Proxy Error]:", error?.message || error);
12
- return new Response("Media Proxy Failed", { status: 500 });
13
- }
14
- };
@@ -1,69 +0,0 @@
1
- ---
2
- // src/pages/partials/footer.astro
3
- export const partial = true;
4
-
5
- import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
6
- import { getChannelId } from "@/lib/cms";
7
-
8
- const safeEnv = (Astro.locals as any).safeEnv || {};
9
- const cmsWorker = safeEnv.CMS_WORKER;
10
- const channelId = getChannelId() || "website";
11
-
12
- const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
13
- const reqInit = { ...init };
14
- delete reqInit.cache;
15
-
16
- let urlStr = input instanceof URL ? input.toString() : String(input);
17
- if (urlStr.startsWith("/")) {
18
- const baseUrl =
19
- import.meta.env.PUBLIC_API_URL ||
20
- safeEnv.PUBLIC_API_URL ||
21
- "http://127.0.0.1:8787";
22
- urlStr = `${baseUrl}${urlStr}`;
23
- }
24
-
25
- const req = new Request(urlStr, reqInit);
26
- if (cmsWorker && typeof cmsWorker.fetch === "function")
27
- return cmsWorker.fetch(req);
28
- return fetch(req);
29
- };
30
-
31
- // 🎯 Resolve the Home Page slug
32
- const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "home").trim();
33
-
34
- let footerBlocks: any[] = [];
35
- let contentMap: Record<string, any[]> = {};
36
-
37
- try {
38
- // 🎯 Fetch the Home Page node from Kama CMS Public API
39
- const res = await customFetch(
40
- `/api/v1/node/${homeSlug}?channelId=${channelId}`,
41
- );
42
- if (res.ok) {
43
- const data = await res.json();
44
-
45
- // Kama CMS public node endpoints usually organize blocks inside a 'content' object mapped by region
46
- const allContent = data.content || {};
47
-
48
- // Extract specifically the footer region from the Home page
49
- footerBlocks = allContent.footer || [];
50
- contentMap = allContent;
51
- }
52
- } catch (e) {
53
- console.error("[Partials/Footer] Failed to fetch home page blocks:", e);
54
- }
55
- ---
56
-
57
- {
58
- footerBlocks && footerBlocks.length > 0 ? (
59
- <BlockRenderer
60
- blocks={footerBlocks}
61
- contentMap={contentMap}
62
- region="footer"
63
- />
64
- ) : (
65
- <div class="footer-default">
66
- <p>&copy; {new Date().getFullYear()} all rights reserved.</p>
67
- </div>
68
- )
69
- }
@@ -1,141 +0,0 @@
1
- ---
2
- // src/pages/partials/nav.astro
3
- export const partial = true; // 🎯 Tells Astro not to wrap this in <html><body>
4
-
5
- import { getTree, getTreeId, getChannelId } from "@/lib/cms";
6
- import type { TreeNode } from "@/lib/cms";
7
-
8
- const safeEnv = (Astro.locals as any).safeEnv || {};
9
- const cmsWorker = safeEnv.CMS_WORKER;
10
-
11
- const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
12
- const reqInit = { ...init };
13
- delete reqInit.cache;
14
-
15
- let urlStr = input instanceof URL ? input.toString() : String(input);
16
- if (urlStr.startsWith("/")) {
17
- const baseUrl =
18
- import.meta.env.PUBLIC_API_URL ||
19
- safeEnv.PUBLIC_API_URL ||
20
- "http://127.0.0.1:8787";
21
- urlStr = `${baseUrl}${urlStr}`;
22
- }
23
-
24
- const req = new Request(urlStr, reqInit);
25
- if (cmsWorker && typeof cmsWorker.fetch === "function")
26
- return cmsWorker.fetch(req);
27
- return fetch(req);
28
- };
29
-
30
- // Fetch fresh tree from CMS
31
- const channelId = getChannelId() || "website";
32
- const treeId = getTreeId() || "main-nav";
33
- let tree: TreeNode[] = [];
34
- try {
35
- tree = (await getTree(treeId, channelId, customFetch)) || [];
36
- } catch (e) {
37
- console.error("[Partials/Nav] Failed to fetch tree:", e);
38
- }
39
-
40
- // Get the current path from the client's fetch request
41
- const currentPath =
42
- (Astro.url.searchParams.get("path") || "/").replace(/\/$/, "") || "/";
43
- const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "home").trim();
44
-
45
- const homeNode = tree.find(
46
- item =>
47
- item.slug === homeSlug ||
48
- item.slug === "" ||
49
- item.path === "/" ||
50
- item.path === "",
51
- );
52
-
53
- const topLevelNavItems =
54
- homeNode?.children && homeNode.children.length > 0
55
- ? homeNode.children
56
- : tree.filter(
57
- item =>
58
- item.slug !== homeSlug &&
59
- item.slug !== "" &&
60
- item.path !== "/" &&
61
- item.path !== "",
62
- );
63
-
64
- const isPathActive = (itemPath?: string) => {
65
- const normItem = (itemPath || "").replace(/\/$/, "") || "/";
66
- if (normItem === "/") return currentPath === "/";
67
- return currentPath === normItem || currentPath.startsWith(`${normItem}/`);
68
- };
69
-
70
- const isExactActive = (itemPath?: string) => {
71
- return currentPath === ((itemPath || "").replace(/\/$/, "") || "/");
72
- };
73
- ---
74
-
75
- <ul class="nav-list">
76
- {
77
- topLevelNavItems.map(item => {
78
- const hasChildren = item.children && item.children.length > 0;
79
- const isParentActive = isPathActive(item.path);
80
-
81
- if (hasChildren) {
82
- return (
83
- <li class="nav-item">
84
- <details class="nav-details">
85
- <summary
86
- class:list={[
87
- "nav-button",
88
- "dropdown-summary",
89
- { active: isParentActive },
90
- ]}
91
- >
92
- <span>{item.title}</span>
93
- <span class="indicator">▼</span>
94
- </summary>
95
- <div class="submenu-container">
96
- <ul class="dropdown-panel">
97
- <li>
98
- <a
99
- href={item.path}
100
- class:list={[
101
- "dropdown-item",
102
- "overview-item",
103
- { active: isExactActive(item.path) },
104
- ]}
105
- >
106
- {item.title} Overview
107
- </a>
108
- </li>
109
- {item.children?.map(child => (
110
- <li>
111
- <a
112
- href={child.path}
113
- class:list={[
114
- "dropdown-item",
115
- { active: isExactActive(child.path) },
116
- ]}
117
- >
118
- {child.title}
119
- </a>
120
- </li>
121
- ))}
122
- </ul>
123
- </div>
124
- </details>
125
- </li>
126
- );
127
- }
128
-
129
- return (
130
- <li class="nav-item">
131
- <a
132
- href={item.path}
133
- class:list={["nav-button", { active: isExactActive(item.path) }]}
134
- >
135
- {item.title}
136
- </a>
137
- </li>
138
- );
139
- })
140
- }
141
- </ul>
@@ -1,157 +0,0 @@
1
- /* =======================================
2
- 1. GLOBAL RESETS & TYPOGRAPHY
3
- ======================================= */
4
- *,
5
- *::before,
6
- *::after {
7
- box-sizing: border-box;
8
- border-radius: 0 !important;
9
- }
10
-
11
- html {
12
- font-size: 16px;
13
- -webkit-text-size-adjust: 100%;
14
- -webkit-font-smoothing: antialiased;
15
- -moz-osx-font-smoothing: grayscale;
16
- text-rendering: optimizeLegibility;
17
- }
18
-
19
- body {
20
- margin: 0;
21
- padding: 0;
22
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
23
- font-display: swap;
24
- background-color: #ffffff;
25
- color: #1e293b;
26
- min-height: 100vh;
27
- display: flex;
28
- flex-direction: column;
29
- line-height: 1.65;
30
- letter-spacing: -0.011em;
31
- }
32
-
33
- h1,
34
- h2,
35
- h3,
36
- h4,
37
- h5,
38
- h6 {
39
- color: #0f172a;
40
- font-weight: 700;
41
- line-height: 1.25;
42
- letter-spacing: -0.022em;
43
- margin-top: 1.6em;
44
- margin-bottom: 0.6em;
45
- }
46
-
47
- h1 {
48
- font-size: 2.25rem;
49
- }
50
-
51
- h2 {
52
- font-size: 1.65rem;
53
- }
54
-
55
- h3 {
56
- font-size: 1.35rem;
57
- }
58
-
59
- p {
60
- margin-top: 0;
61
- margin-bottom: 1.25em;
62
- color: #334155;
63
- font-size: 1.03rem;
64
- }
65
-
66
- a {
67
- color: #0284c7;
68
- text-decoration-thickness: 1px;
69
- text-underline-offset: 3px;
70
- transition: color 0.15s ease;
71
- }
72
-
73
- a:hover {
74
- color: #0369a1;
75
- }
76
-
77
- li>p:last-child {
78
- margin-bottom: 0;
79
- }
80
-
81
- /* =======================================
82
- 2. MAIN LAYOUT SHELL
83
- ======================================= */
84
- .content-block {
85
- flex: 1;
86
- padding: 40px 0 64px 0;
87
- }
88
-
89
- .content-container {
90
- max-width: 1400px;
91
- margin: 0 auto;
92
- padding: 0 24px;
93
- width: 100%;
94
- box-sizing: border-box;
95
- }
96
-
97
- /* =======================================
98
- 3. ROUTER REGIONS (from slug.astro)
99
- ======================================= */
100
- .region-header {
101
- margin-bottom: 24px;
102
- }
103
-
104
- .breadcrumb-wrapper {
105
- margin-bottom: 24px;
106
- }
107
-
108
- .content-layout {
109
- width: 100%;
110
- display: block;
111
- }
112
-
113
- .main-column {
114
- width: 100%;
115
- min-width: 0;
116
- }
117
-
118
- @media (min-width: 768px) {
119
- .content-layout.has-sidebar {
120
- display: grid !important;
121
- grid-template-columns: calc(100% - 360px) 310px !important;
122
- column-gap: 48px !important;
123
- align-items: start !important;
124
- }
125
-
126
- .main-column {
127
- grid-column: 1 / 2 !important;
128
- }
129
-
130
- .sidebar-column {
131
- grid-column: 2 / 3 !important;
132
- position: sticky;
133
- top: 32px;
134
- }
135
- }
136
-
137
- .sidebar-column {
138
- background: #f8fafc;
139
- border: 1px solid #e2e8f0;
140
- padding: 28px;
141
- box-sizing: border-box;
142
- }
143
-
144
- @media (max-width: 767px) {
145
- .sidebar-column {
146
- margin-top: 40px;
147
- }
148
- }
149
-
150
- .empty-content {
151
- padding: 32px;
152
- border: 1px dashed #cbd5e1;
153
- background: #f8fafc;
154
- color: #64748b;
155
- font-size: 0.95rem;
156
- text-align: center;
157
- }
@@ -1,113 +0,0 @@
1
- // src/utils/tiptap.ts
2
-
3
- export interface TiptapNode {
4
- type: string;
5
- text?: string;
6
- attrs?: Record<string, any>;
7
- marks?: Array<{ type: string; attrs?: Record<string, any> }>;
8
- content?: TiptapNode[];
9
- }
10
-
11
- /**
12
- * Traverses a ProseMirror / Tiptap AST node and converts it into HTML.
13
- */
14
- export function renderTiptapNode(node?: TiptapNode | null): string {
15
- if (!node) return "";
16
- if (typeof node === "string") return node;
17
-
18
- // 1. Text Node Handling (with Marks)
19
- if (node.type === "text") {
20
- let text = escapeHtml(node.text || "");
21
- if (node.marks && Array.isArray(node.marks)) {
22
- node.marks.forEach((mark) => {
23
- switch (mark.type) {
24
- case "bold":
25
- case "strong":
26
- text = `<strong>${text}</strong>`;
27
- break;
28
- case "italic":
29
- case "em":
30
- text = `<em>${text}</em>`;
31
- break;
32
- case "underline":
33
- text = `<u>${text}</u>`;
34
- break;
35
- case "strike":
36
- text = `<s>${text}</s>`;
37
- break;
38
- case "code":
39
- text = `<code>${text}</code>`;
40
- break;
41
- case "link": {
42
- const href = mark.attrs?.href ? escapeHtml(mark.attrs.href) : "#";
43
- const target = mark.attrs?.target ? ` target="${escapeHtml(mark.attrs.target)}"` : "";
44
- const rel = mark.attrs?.target === "_blank" ? ' rel="noopener noreferrer"' : "";
45
- text = `<a href="${href}"${target}${rel}>${text}</a>`;
46
- break;
47
- }
48
- }
49
- });
50
- }
51
- return text;
52
- }
53
-
54
- // 2. Traversal of Child Nodes
55
- const childrenHtml = Array.isArray(node.content)
56
- ? node.content.map(renderTiptapNode).join("")
57
- : "";
58
-
59
- // 3. Block Level Mapping
60
- switch (node.type) {
61
- case "doc":
62
- return childrenHtml;
63
- case "paragraph":
64
- return childrenHtml.trim() ? `<p>${childrenHtml}</p>` : "";
65
- case "heading": {
66
- const level = Math.min(Math.max(node.attrs?.level || 2, 1), 6);
67
- return `<h${level}>${childrenHtml}</h${level}>`;
68
- }
69
- case "bulletList":
70
- return `<ul>${childrenHtml}</ul>`;
71
- case "orderedList": {
72
- const start = node.attrs?.start ? ` start="${node.attrs.start}"` : "";
73
- return `<ol${start}>${childrenHtml}</ol>`;
74
- }
75
- case "listItem":
76
- return `<li>${childrenHtml}</li>`;
77
- case "blockquote":
78
- return `<blockquote>${childrenHtml}</blockquote>`;
79
- case "codeBlock": {
80
- const lang = node.attrs?.language ? ` class="language-${escapeHtml(node.attrs.language)}"` : "";
81
- return `<pre><code${lang}>${childrenHtml}</code></pre>`;
82
- }
83
- case "horizontalRule":
84
- return "<hr />";
85
- case "hardBreak":
86
- return "<br />";
87
- default:
88
- return childrenHtml;
89
- }
90
- }
91
-
92
- /**
93
- * Universal helper that handles plain strings, legacy objects, and Tiptap ASTs safely.
94
- */
95
- export function extractHtml(value: any): string {
96
- if (!value) return "";
97
- if (typeof value === "string") return value; // Already HTML/string
98
- if (typeof value === "object") {
99
- if (value.type === "doc") return renderTiptapNode(value);
100
- if (value.html) return value.html;
101
- if (value.markup) return value.markup;
102
- }
103
- return String(value);
104
- }
105
-
106
- function escapeHtml(str: string): string {
107
- return str
108
- .replace(/&/g, "&amp;")
109
- .replace(/</g, "&lt;")
110
- .replace(/>/g, "&gt;")
111
- .replace(/"/g, "&quot;")
112
- .replace(/'/g, "&#039;");
113
- }