@withwiz/block-editor 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.
Files changed (40) hide show
  1. package/README.md +219 -0
  2. package/dist/blocks/built-in.d.ts +6 -0
  3. package/dist/blocks/built-in.js +11 -0
  4. package/dist/chunk-3R3HAGQL.js +102 -0
  5. package/dist/chunk-62BAOSP6.js +100 -0
  6. package/dist/chunk-CJGZUEQO.js +270 -0
  7. package/dist/chunk-CLC3FEL2.js +313 -0
  8. package/dist/chunk-CYMYM7LP.js +25 -0
  9. package/dist/chunk-EERQYNER.js +123 -0
  10. package/dist/chunk-G6J2DCC5.js +77 -0
  11. package/dist/chunk-N3ETBM74.js +24 -0
  12. package/dist/chunk-PPVXNJWI.js +28 -0
  13. package/dist/chunk-QR225IXX.js +148 -0
  14. package/dist/chunk-VIJV6FLT.js +250 -0
  15. package/dist/components/ArtistEditor.d.ts +12 -0
  16. package/dist/components/ArtistEditor.js +11 -0
  17. package/dist/components/BlockEditor.d.ts +24 -0
  18. package/dist/components/BlockEditor.js +16 -0
  19. package/dist/components/BlockRenderer.d.ts +10 -0
  20. package/dist/components/BlockRenderer.js +12 -0
  21. package/dist/components/ImageUploadField.d.ts +11 -0
  22. package/dist/components/ImageUploadField.js +11 -0
  23. package/dist/context/BlockEditorProvider.d.ts +21 -0
  24. package/dist/context/BlockEditorProvider.js +10 -0
  25. package/dist/core/html-renderer.d.ts +13 -0
  26. package/dist/core/html-renderer.js +11 -0
  27. package/dist/core/image-resize.d.ts +17 -0
  28. package/dist/core/image-resize.js +11 -0
  29. package/dist/core/serializer.d.ts +9 -0
  30. package/dist/core/serializer.js +7 -0
  31. package/dist/hooks/useImageDropZone.d.ts +23 -0
  32. package/dist/hooks/useImageDropZone.js +10 -0
  33. package/dist/index.d.ts +11 -0
  34. package/dist/index.js +57 -0
  35. package/dist/types.d.ts +67 -0
  36. package/dist/types.js +0 -0
  37. package/package.json +43 -0
  38. package/styles/artist.css +332 -0
  39. package/styles/editor.css +394 -0
  40. package/styles/preview.css +203 -0
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # @withwiz/block-editor
2
+
3
+ Block-based content editor for web publishing. A React component library providing a flexible, configurable block editor with 22 built-in block types, serialization, and HTML rendering.
4
+
5
+ ## Features
6
+
7
+ - 22 built-in block types (text, images, quotes, stats, timelines, and more)
8
+ - Drag-and-drop block reordering
9
+ - Category-based block filtering
10
+ - Two operation modes: serialized (HTML string) or raw (block array)
11
+ - Image upload with resize support
12
+ - Artist/bio editor component
13
+ - HTML renderer for preview and server-side rendering
14
+ - TypeScript support with full type definitions
15
+ - CSS themes included
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @withwiz/block-editor
21
+ ```
22
+
23
+ > **Peer dependency:** React 18 or higher is required.
24
+
25
+ ## Quick Start
26
+
27
+ ```tsx
28
+ import { BlockEditor, BlockEditorProvider, BUILT_IN_BLOCKS } from "@withwiz/block-editor";
29
+ import "@withwiz/block-editor/styles/editor.css";
30
+
31
+ const config = {
32
+ blocks: BUILT_IN_BLOCKS,
33
+ marker: "nbe-blocks:",
34
+ cssPrefix: "nbe-pvb",
35
+ };
36
+
37
+ function MyEditor() {
38
+ const [content, setContent] = useState("");
39
+
40
+ return (
41
+ <BlockEditorProvider uploadFn={myUploadFn}>
42
+ <BlockEditor
43
+ config={config}
44
+ content={content}
45
+ onChange={setContent}
46
+ />
47
+ </BlockEditorProvider>
48
+ );
49
+ }
50
+ ```
51
+
52
+ ## Built-in Block Types
53
+
54
+ | Type | Label | Description |
55
+ |------|-------|-------------|
56
+ | `lead` | Lead paragraph | Opening paragraph summarizing key content |
57
+ | `paragraph` | Paragraph | Body text |
58
+ | `subheading` | Subheading | Section divider heading |
59
+ | `subheading-label` | Subheading + Label | Subheading with a small label above |
60
+ | `divider` | Divider | Visual separator between sections |
61
+ | `spacer` | Spacer | Adjustable blank space |
62
+ | `img-full` | Full-width image | Image spanning the full page width |
63
+ | `img-inline` | Inline image | Image within body width, resizable |
64
+ | `img-pair` | Image pair | Two images side by side |
65
+ | `gallery` | Gallery (3) | Three square images in a row |
66
+ | `img-text` | Person intro | Profile photo + name, role, bio |
67
+ | `quote` | Quote | Interview or speech highlight |
68
+ | `quote-large` | Large quote | Centered oversized pull quote |
69
+ | `stats` | Number highlight | Key figures (e.g. 3 shows, 1,200 attendees) |
70
+ | `infobox` | Info box | Key–value information table |
71
+ | `callout` | Callout | Important notice for readers |
72
+ | `numcards` | Numbered cards | Sequentially numbered guide cards (01, 02, 03…) |
73
+ | `qa` | Q&A | Interview-style question and answer |
74
+ | `press-list` | Press list | Media coverage list (outlet, date, title, link) |
75
+ | `timeline` | Timeline | Chronological event sequence |
76
+ | `video` | Video | Embedded YouTube video |
77
+ | `cta` | CTA button | Call-to-action button |
78
+
79
+ ## API Reference
80
+
81
+ ### `<BlockEditor />`
82
+
83
+ The main editor component. Operates in two modes:
84
+
85
+ **Serialized mode** (recommended for database persistence):
86
+
87
+ ```tsx
88
+ <BlockEditor
89
+ config={config}
90
+ content={serializedHtmlString}
91
+ onChange={(html) => saveToDb(html)}
92
+ />
93
+ ```
94
+
95
+ **Raw mode** (for custom serialization):
96
+
97
+ ```tsx
98
+ <BlockEditor
99
+ config={config}
100
+ blocks={blockArray}
101
+ onBlocksChange={(blocks) => setBlocks(blocks)}
102
+ />
103
+ ```
104
+
105
+ **Props:**
106
+
107
+ | Prop | Type | Description |
108
+ |------|------|-------------|
109
+ | `config` | `BlockEditorConfig` | Editor configuration |
110
+ | `content` | `string` | *(Serialized mode)* HTML+marker string |
111
+ | `onChange` | `(html: string) => void` | *(Serialized mode)* Called on change |
112
+ | `blocks` | `BlockData[]` | *(Raw mode)* Block array |
113
+ | `onBlocksChange` | `(blocks: BlockData[]) => void` | *(Raw mode)* Called on change |
114
+ | `category` | `string` | Current category (with `enableCategoryFilter`) |
115
+ | `onImageUploaded` | `(key: string) => void` | Track uploaded image keys |
116
+ | `onModeChange` | `(mode: "template" \| "sample") => void` | Template/sample load notification |
117
+
118
+ ### `<BlockRenderer />`
119
+
120
+ Read-only block renderer for preview or published content.
121
+
122
+ ```tsx
123
+ import { BlockRenderer } from "@withwiz/block-editor/components/BlockRenderer";
124
+
125
+ <BlockRenderer blocks={blocks} config={config} />
126
+ ```
127
+
128
+ ### `<ArtistEditor />`
129
+
130
+ Specialized editor for artist bio pages with main image and gallery.
131
+
132
+ ```tsx
133
+ import { ArtistEditor } from "@withwiz/block-editor/components/ArtistEditor";
134
+ import "@withwiz/block-editor/styles/artist.css";
135
+
136
+ <ArtistEditor value={bioData} onChange={setBioData} />
137
+ ```
138
+
139
+ ### `<BlockEditorProvider />`
140
+
141
+ Context provider for image upload and error handling.
142
+
143
+ ```tsx
144
+ import { BlockEditorProvider } from "@withwiz/block-editor/context/BlockEditorProvider";
145
+
146
+ <BlockEditorProvider
147
+ uploadFn={async (file) => ({ url: "https://...", key: "file-key" })}
148
+ onError={(msg) => console.error(msg)}
149
+ >
150
+ {children}
151
+ </BlockEditorProvider>
152
+ ```
153
+
154
+ ### `BlockEditorConfig`
155
+
156
+ ```ts
157
+ interface BlockEditorConfig {
158
+ blocks: BlockDef[]; // Available block types
159
+ marker: string; // Serialization marker (e.g. "nbe-blocks:")
160
+ cssPrefix: string; // CSS class prefix for rendered HTML
161
+ enableDragDrop?: boolean; // Enable drag-and-drop (default: true)
162
+ enableCategoryFilter?: boolean; // Enable category filtering (default: false)
163
+ categories?: string[]; // Category list
164
+ catClasses?: Record<string, string>; // Category → CSS class mapping
165
+ templates?: Record<string, Omit<BlockData, "id">[]>;
166
+ samples?: Record<string, Omit<BlockData, "id">[]>;
167
+ }
168
+ ```
169
+
170
+ ### Core Utilities
171
+
172
+ ```ts
173
+ import { createSerializer } from "@withwiz/block-editor/core/serializer";
174
+ import { createHtmlRenderer, h, nl2br } from "@withwiz/block-editor/core/html-renderer";
175
+ import { resizeImageIfNeeded, validateImageFile } from "@withwiz/block-editor/core/image-resize";
176
+ ```
177
+
178
+ ### Hooks
179
+
180
+ ```ts
181
+ import { useImageDropZone } from "@withwiz/block-editor/hooks/useImageDropZone";
182
+ ```
183
+
184
+ ## Styles
185
+
186
+ Three CSS files are included:
187
+
188
+ ```ts
189
+ import "@withwiz/block-editor/styles/editor.css"; // Editor UI styles
190
+ import "@withwiz/block-editor/styles/preview.css"; // Published content styles
191
+ import "@withwiz/block-editor/styles/artist.css"; // Artist editor styles
192
+ ```
193
+
194
+ ## Custom Block Types
195
+
196
+ You can extend or replace the built-in blocks with custom `BlockDef` objects:
197
+
198
+ ```ts
199
+ import { BUILT_IN_BLOCKS } from "@withwiz/block-editor";
200
+ import type { BlockDef } from "@withwiz/block-editor/types";
201
+
202
+ const customBlock: BlockDef = {
203
+ type: "my-block",
204
+ label: "My Block",
205
+ icon: "★",
206
+ desc: "A custom block type",
207
+ createEmpty: (id) => ({ id, type: "my-block", text: "" }),
208
+ };
209
+
210
+ const config = {
211
+ blocks: [...BUILT_IN_BLOCKS, customBlock],
212
+ marker: "nbe-blocks:",
213
+ cssPrefix: "nbe-pvb",
214
+ };
215
+ ```
216
+
217
+ ## License
218
+
219
+ MIT
@@ -0,0 +1,6 @@
1
+ import type { BlockData, BlockDef } from "../types";
2
+ /** Standalone factory for creating an empty block by type */
3
+ export declare function createEmptyBlock(type: string, id: number): BlockData;
4
+ export declare const BUILT_IN_BLOCKS: BlockDef[];
5
+ /** Lookup helper: type → BlockDef */
6
+ export declare function getBlockDef(type: string): BlockDef | undefined;
@@ -0,0 +1,11 @@
1
+ import {
2
+ BUILT_IN_BLOCKS,
3
+ createEmptyBlock,
4
+ getBlockDef
5
+ } from "../chunk-EERQYNER.js";
6
+ import "../chunk-N3ETBM74.js";
7
+ export {
8
+ BUILT_IN_BLOCKS,
9
+ createEmptyBlock,
10
+ getBlockDef
11
+ };
@@ -0,0 +1,102 @@
1
+ // src/core/image-resize.ts
2
+ var RESIZE_THRESHOLD = 10 * 1024 * 1024;
3
+ var ABSOLUTE_MAX_SIZE = 50 * 1024 * 1024;
4
+ var SKIP_RESIZE_TYPES = ["image/gif"];
5
+ var ALLOWED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"];
6
+ async function resizeImageIfNeeded(file) {
7
+ const originalSize = file.size;
8
+ if (file.size <= RESIZE_THRESHOLD) {
9
+ return { file, wasResized: false, originalSize, newSize: originalSize };
10
+ }
11
+ if (SKIP_RESIZE_TYPES.includes(file.type)) {
12
+ return { file, wasResized: false, originalSize, newSize: originalSize };
13
+ }
14
+ const img = await loadImage(file);
15
+ const { width, height } = img;
16
+ const outputMime = file.type === "image/png" ? "image/jpeg" : file.type || "image/jpeg";
17
+ const qualitySteps = [0.85, 0.75, 0.65, 0.55, 0.5];
18
+ for (const quality of qualitySteps) {
19
+ const blob = await canvasToBlob(img, width, height, outputMime, quality);
20
+ if (blob.size <= RESIZE_THRESHOLD) {
21
+ return toResult(blob, file.name, outputMime, originalSize);
22
+ }
23
+ }
24
+ const scaleSteps = [0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3];
25
+ for (const scale of scaleSteps) {
26
+ const newW = Math.round(width * scale);
27
+ const newH = Math.round(height * scale);
28
+ const blob = await canvasToBlob(img, newW, newH, outputMime, 0.75);
29
+ if (blob.size <= RESIZE_THRESHOLD) {
30
+ return toResult(blob, file.name, outputMime, originalSize);
31
+ }
32
+ }
33
+ const finalW = Math.round(width * 0.3);
34
+ const finalH = Math.round(height * 0.3);
35
+ const finalBlob = await canvasToBlob(img, finalW, finalH, outputMime, 0.5);
36
+ return toResult(finalBlob, file.name, outputMime, originalSize);
37
+ }
38
+ function validateImageFile(file) {
39
+ if (!ALLOWED_IMAGE_TYPES.includes(file.type)) {
40
+ const allowed = ALLOWED_IMAGE_TYPES.map((t) => t.split("/")[1]).join(", ");
41
+ return `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4. (\uD5C8\uC6A9: ${allowed})`;
42
+ }
43
+ if (file.size > ABSOLUTE_MAX_SIZE) {
44
+ return `\uD30C\uC77C \uD06C\uAE30\uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4. (${(file.size / 1024 / 1024).toFixed(0)}MB, \uCD5C\uB300 50MB)`;
45
+ }
46
+ if (SKIP_RESIZE_TYPES.includes(file.type) && file.size > RESIZE_THRESHOLD) {
47
+ return `GIF \uD30C\uC77C\uC740 10MB \uC774\uD558\uB9CC \uC5C5\uB85C\uB4DC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. (${(file.size / 1024 / 1024).toFixed(1)}MB)`;
48
+ }
49
+ return null;
50
+ }
51
+ function loadImage(file) {
52
+ return new Promise((resolve, reject) => {
53
+ const img = new Image();
54
+ const url = URL.createObjectURL(file);
55
+ img.onload = () => {
56
+ URL.revokeObjectURL(url);
57
+ resolve(img);
58
+ };
59
+ img.onerror = () => {
60
+ URL.revokeObjectURL(url);
61
+ reject(new Error("\uC774\uBBF8\uC9C0 \uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4."));
62
+ };
63
+ img.src = url;
64
+ });
65
+ }
66
+ function canvasToBlob(img, width, height, mime, quality) {
67
+ return new Promise((resolve, reject) => {
68
+ const canvas = document.createElement("canvas");
69
+ canvas.width = width;
70
+ canvas.height = height;
71
+ const ctx = canvas.getContext("2d");
72
+ if (!ctx) {
73
+ reject(new Error("\uC774\uBBF8\uC9C0 \uCC98\uB9AC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4."));
74
+ return;
75
+ }
76
+ if (mime === "image/jpeg") {
77
+ ctx.fillStyle = "#FFFFFF";
78
+ ctx.fillRect(0, 0, width, height);
79
+ }
80
+ ctx.drawImage(img, 0, 0, width, height);
81
+ canvas.toBlob(
82
+ (blob) => {
83
+ if (blob) resolve(blob);
84
+ else reject(new Error("\uC774\uBBF8\uC9C0 \uBCC0\uD658\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4."));
85
+ },
86
+ mime,
87
+ quality
88
+ );
89
+ });
90
+ }
91
+ function toResult(blob, originalName, mime, originalSize) {
92
+ const ext = mime === "image/jpeg" ? ".jpg" : mime === "image/webp" ? ".webp" : ".png";
93
+ const baseName = originalName.replace(/\.[^.]+$/, "");
94
+ const file = new File([blob], `${baseName}${ext}`, { type: mime });
95
+ return { file, wasResized: true, originalSize, newSize: blob.size };
96
+ }
97
+
98
+ export {
99
+ ALLOWED_IMAGE_TYPES,
100
+ resizeImageIfNeeded,
101
+ validateImageFile
102
+ };
@@ -0,0 +1,100 @@
1
+ // src/core/html-renderer.ts
2
+ function h(s) {
3
+ return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4
+ }
5
+ function nl2br(s) {
6
+ return h(s).replace(/\n/g, "<br>");
7
+ }
8
+ function createHtmlRenderer(prefix, catClass) {
9
+ const p = prefix;
10
+ const cc = catClass || "";
11
+ function renderBlock(b) {
12
+ var _a, _b, _c, _d;
13
+ switch (b.type) {
14
+ case "lead":
15
+ return b.text ? `<div class="${p}-lead">${nl2br(b.text)}</div>` : "";
16
+ case "paragraph":
17
+ return b.text ? `<p class="${p}-p">${nl2br(b.text)}</p>` : "";
18
+ case "subheading":
19
+ return b.text ? `<div class="${p}-sh">${h(b.text)}</div>` : "";
20
+ case "subheading-label":
21
+ if (b.en) {
22
+ return `<div class="${p}-sh-en">${h(b.en)}</div>${b.text ? `<div class="${p}-sh">${h(b.text)}</div>` : ""}`;
23
+ }
24
+ return b.text ? `<div class="${p}-sh">${h(b.text)}</div>` : "";
25
+ case "divider":
26
+ return `<div class="${p}-hr"></div>`;
27
+ case "spacer": {
28
+ const sh = b.size === "small" ? 16 : b.size === "large" ? 56 : 32;
29
+ return `<div style="height:${sh}px"></div>`;
30
+ }
31
+ case "img-full":
32
+ return b.src ? `<div class="${p}-imgf"><img src="${b.src}" alt="">${b.cap ? `<div class="${p}-cap">${h(b.cap)}</div>` : ""}</div>` : "";
33
+ case "img-inline":
34
+ if (!b.src) return "";
35
+ {
36
+ const w = b.size === "small" ? "50%" : b.size === "medium" ? "70%" : "100%";
37
+ return `<div class="${p}-imgi" style="width:${w}"><img src="${b.src}" alt="">${b.cap ? `<div class="${p}-cap">${h(b.cap)}</div>` : ""}</div>`;
38
+ }
39
+ case "img-pair":
40
+ return b.src1 || b.src2 ? `<div class="${p}-pair">${b.src1 ? `<img src="${b.src1}" alt="">` : ""}${b.src2 ? `<img src="${b.src2}" alt="">` : ""}${b.cap ? `<div class="${p}-cap">${h(b.cap)}</div>` : ""}</div>` : "";
41
+ case "gallery":
42
+ return `<div class="${p}-gal">${b.src1 ? `<img src="${b.src1}" alt="">` : ""}${b.src2 ? `<img src="${b.src2}" alt="">` : ""}${b.src3 ? `<img src="${b.src3}" alt="">` : ""}${b.cap ? `<div class="${p}-cap">${h(b.cap)}</div>` : ""}</div>`;
43
+ case "img-text":
44
+ return b.src || b.name ? `<div class="${p}-prof">${b.src ? `<img src="${b.src}" alt="">` : ""}<div>${b.name ? `<div class="${p}-nm">${h(b.name)}</div>` : ""}${b.role ? `<div class="${p}-rl">${h(b.role)}</div>` : ""}${b.bio ? `<div class="${p}-bio">${nl2br(b.bio)}</div>` : ""}</div></div>` : "";
45
+ case "quote":
46
+ return b.text ? `<div class="${p}-q"><p>${nl2br(b.text)}</p>${b.attr ? `<div class="${p}-at">${h(b.attr)}</div>` : ""}</div>` : "";
47
+ case "quote-large":
48
+ return b.text ? `<div class="${p}-ql"><div class="${p}-mk">&ldquo;</div><p>${nl2br(b.text)}</p>${b.attr ? `<div class="${p}-at">${h(b.attr)}</div>` : ""}</div>` : "";
49
+ case "stats":
50
+ if (!((_a = b.items) == null ? void 0 : _a.length)) return "";
51
+ return `<div class="${p}-stats">${b.items.map((it) => `<div class="${p}-stat"><div class="${p}-n">${h(it.num || "\u2014")}</div><div class="${p}-l">${h(it.label || "")}</div></div>`).join("")}</div>`;
52
+ case "infobox": {
53
+ let out = `<div class="${p}-ib"><div class="${p}-lbl${cc ? ` ${cc}` : ""}">${h(b.label || "Info")}</div>`;
54
+ if (b.items) for (const it of b.items) {
55
+ if (it.k || it.v) out += `<strong>${h(it.k)}</strong> &middot; ${h(it.v)}<br>`;
56
+ }
57
+ return out + `</div>`;
58
+ }
59
+ case "callout":
60
+ return b.text ? `<div class="${p}-co">${b.title ? `<div class="${p}-ct">${h(b.title)}</div>` : ""}<p>${nl2br(b.text)}</p></div>` : "";
61
+ case "numcards":
62
+ if (!((_b = b.items) == null ? void 0 : _b.length)) return "";
63
+ return `<div class="${p}-nc">${b.items.map((it, i) => `<div class="${p}-nci"><div class="${p}-num">${String(i + 1).padStart(2, "0")}</div>${it.title ? `<h3>${h(it.title)}</h3>` : ""}${it.desc ? `<p>${h(it.desc)}</p>` : ""}</div>`).join("")}</div>`;
64
+ case "qa":
65
+ return b.q || b.a ? `<div class="${p}-qa"><div class="${p}-qai">${b.q ? `<div class="${p}-qaq">${h(b.q)}</div>` : ""}${b.a ? `<p class="${p}-qaa">${nl2br(b.a)}</p>` : ""}</div></div>` : "";
66
+ case "press-list":
67
+ if (!((_c = b.items) == null ? void 0 : _c.length)) return "";
68
+ {
69
+ let out = `<div class="${p}-pl">`;
70
+ for (const it of b.items) {
71
+ if (!it.src && !it.title) continue;
72
+ out += `<div class="${p}-pli"><div class="${p}-src">${h(it.src)}<span class="${p}-pd">${h(it.date)}</span></div><div><div class="${p}-ptt">${h(it.title)}</div><div class="${p}-pex">${h(it.ex)}</div>${it.link ? `<a class="${p}-plk" href="${h(it.link)}" target="_blank" rel="noopener noreferrer">\uC6D0\uBB38 \uBCF4\uAE30 &rarr;</a>` : ""}</div></div>`;
73
+ }
74
+ return out + `</div>`;
75
+ }
76
+ case "timeline":
77
+ if (!((_d = b.items) == null ? void 0 : _d.length)) return "";
78
+ return `<div class="${p}-tl">${b.items.map((it) => `<div class="${p}-tli">${it.date ? `<div class="${p}-td">${h(it.date)}</div>` : ""}${it.title ? `<div class="${p}-tt">${h(it.title)}</div>` : ""}${it.desc ? `<p>${h(it.desc)}</p>` : ""}</div>`).join("")}</div>`;
79
+ case "video":
80
+ return b.url ? `<div class="${p}-vid"><div class="${p}-vw"><iframe src="${b.url}" allowfullscreen></iframe></div>${b.cap ? `<div class="${p}-cap">${h(b.cap)}</div>` : ""}</div>` : "";
81
+ case "cta":
82
+ return b.text || b.label ? `<div class="${p}-cta${cc ? ` ${cc}` : ""}"><p>${h(b.text || "")}</p>${b.label ? `<a class="${p}-cta-btn" href="${b.url || "#"}">${h(b.label)}</a>` : ""}</div>` : "";
83
+ default:
84
+ return "";
85
+ }
86
+ }
87
+ function renderBlocks(blocks) {
88
+ return blocks.map(renderBlock).filter(Boolean).join("\n");
89
+ }
90
+ function renderBlocksWrapped(blocks) {
91
+ return `<div class="${p}-body">${renderBlocks(blocks)}</div>`;
92
+ }
93
+ return { renderBlock, renderBlocks, renderBlocksWrapped };
94
+ }
95
+
96
+ export {
97
+ h,
98
+ nl2br,
99
+ createHtmlRenderer
100
+ };