@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.
- package/README.md +219 -0
- package/dist/blocks/built-in.d.ts +6 -0
- package/dist/blocks/built-in.js +11 -0
- package/dist/chunk-3R3HAGQL.js +102 -0
- package/dist/chunk-62BAOSP6.js +100 -0
- package/dist/chunk-CJGZUEQO.js +270 -0
- package/dist/chunk-CLC3FEL2.js +313 -0
- package/dist/chunk-CYMYM7LP.js +25 -0
- package/dist/chunk-EERQYNER.js +123 -0
- package/dist/chunk-G6J2DCC5.js +77 -0
- package/dist/chunk-N3ETBM74.js +24 -0
- package/dist/chunk-PPVXNJWI.js +28 -0
- package/dist/chunk-QR225IXX.js +148 -0
- package/dist/chunk-VIJV6FLT.js +250 -0
- package/dist/components/ArtistEditor.d.ts +12 -0
- package/dist/components/ArtistEditor.js +11 -0
- package/dist/components/BlockEditor.d.ts +24 -0
- package/dist/components/BlockEditor.js +16 -0
- package/dist/components/BlockRenderer.d.ts +10 -0
- package/dist/components/BlockRenderer.js +12 -0
- package/dist/components/ImageUploadField.d.ts +11 -0
- package/dist/components/ImageUploadField.js +11 -0
- package/dist/context/BlockEditorProvider.d.ts +21 -0
- package/dist/context/BlockEditorProvider.js +10 -0
- package/dist/core/html-renderer.d.ts +13 -0
- package/dist/core/html-renderer.js +11 -0
- package/dist/core/image-resize.d.ts +17 -0
- package/dist/core/image-resize.js +11 -0
- package/dist/core/serializer.d.ts +9 -0
- package/dist/core/serializer.js +7 -0
- package/dist/hooks/useImageDropZone.d.ts +23 -0
- package/dist/hooks/useImageDropZone.js +10 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +57 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.js +0 -0
- package/package.json +43 -0
- package/styles/artist.css +332 -0
- package/styles/editor.css +394 -0
- 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,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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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">“</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> · ${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 →</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
|
+
};
|