forked from UKSOURCE/ipv6
147 lines
4.6 KiB
TypeScript
147 lines
4.6 KiB
TypeScript
/**
|
|
* Converts an EditorJS JSON output (string or object) into an HTML string.
|
|
* Supports all block types used in the CMS blog editor.
|
|
*/
|
|
export function editorjsToHtml(raw: string | object | null | undefined): string {
|
|
if (!raw) return "";
|
|
|
|
let data: { blocks?: Array<{ type: string; data: Record<string, unknown> }> };
|
|
|
|
if (typeof raw === "string") {
|
|
try {
|
|
data = JSON.parse(raw);
|
|
} catch {
|
|
// Not JSON — treat as plain HTML string (legacy content)
|
|
return raw;
|
|
}
|
|
} else {
|
|
data = raw as typeof data;
|
|
}
|
|
|
|
if (!data || !Array.isArray(data.blocks) || data.blocks.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
return data.blocks.map(renderBlock).join("\n");
|
|
}
|
|
|
|
function esc(text: unknown): string {
|
|
return String(text ?? "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function renderBlock(block: { type: string; data: Record<string, unknown> }): string {
|
|
const { type, data } = block;
|
|
|
|
switch (type) {
|
|
case "paragraph": {
|
|
const text = data.text as string ?? "";
|
|
return `<p>${text}</p>`;
|
|
}
|
|
|
|
case "header": {
|
|
const level = Number(data.level) || 2;
|
|
const tag = `h${Math.min(Math.max(level, 1), 6)}`;
|
|
return `<${tag}>${data.text}</${tag}>`;
|
|
}
|
|
|
|
case "list": {
|
|
const style = data.style === "ordered" ? "ol" : "ul";
|
|
const items = (data.items as string[]) ?? [];
|
|
const lis = items.map((item) => `<li>${item}</li>`).join("\n");
|
|
return `<${style}>\n${lis}\n</${style}>`;
|
|
}
|
|
|
|
case "checklist": {
|
|
const items = (data.items as Array<{ text: string; checked: boolean }>) ?? [];
|
|
const lis = items
|
|
.map(
|
|
(item) =>
|
|
`<li class="editorjs-checklist-item${item.checked ? " checked" : ""}">` +
|
|
`<span class="editorjs-checkbox">${item.checked ? "✓" : "○"}</span> ${item.text}` +
|
|
`</li>`
|
|
)
|
|
.join("\n");
|
|
return `<ul class="editorjs-checklist">\n${lis}\n</ul>`;
|
|
}
|
|
|
|
case "image": {
|
|
const file = (data.file as { url?: string }) ?? {};
|
|
const url = file.url ?? (data.url as string) ?? "";
|
|
const caption = (data.caption as string) ?? "";
|
|
const stretched = data.stretched ? ' class="editorjs-image-stretched"' : "";
|
|
const withBg = data.withBackground ? ' style="background:#f5f5f5; padding:1rem;"' : "";
|
|
return (
|
|
`<figure${withBg}>` +
|
|
`<img src="${esc(url)}" alt="${esc(caption)}"${stretched} loading="lazy" />` +
|
|
(caption ? `<figcaption>${caption}</figcaption>` : "") +
|
|
`</figure>`
|
|
);
|
|
}
|
|
|
|
case "quote": {
|
|
const text = data.text as string ?? "";
|
|
const caption = data.caption as string ?? "";
|
|
return (
|
|
`<blockquote class="editorjs-quote">` +
|
|
`<p>${text}</p>` +
|
|
(caption ? `<cite>${caption}</cite>` : "") +
|
|
`</blockquote>`
|
|
);
|
|
}
|
|
|
|
case "code": {
|
|
return `<pre><code>${esc(data.code)}</code></pre>`;
|
|
}
|
|
|
|
case "delimiter": {
|
|
return `<hr class="editorjs-delimiter" />`;
|
|
}
|
|
|
|
case "table": {
|
|
const content = (data.content as string[][]) ?? [];
|
|
const withHeadings = data.withHeadings as boolean;
|
|
const rows = content.map((row, rowIdx) => {
|
|
const tag = withHeadings && rowIdx === 0 ? "th" : "td";
|
|
const cells = row.map((cell) => `<${tag}>${cell}</${tag}>`).join("");
|
|
return `<tr>${cells}</tr>`;
|
|
});
|
|
return `<table class="editorjs-table">\n${rows.join("\n")}\n</table>`;
|
|
}
|
|
|
|
case "embed": {
|
|
const service = (data.service as string ?? "").toLowerCase();
|
|
const embedUrl = (data.embed as string) ?? "";
|
|
const caption = (data.caption as string) ?? "";
|
|
const width = (data.width as number) ?? 560;
|
|
const height = (data.height as number) ?? 315;
|
|
|
|
// EditorJS Embed stores the ready-to-use iframe src in data.embed
|
|
let iframeSrc = esc(embedUrl);
|
|
|
|
// Fallback: if data.embed is the watch URL, convert it
|
|
if (service === "youtube" && iframeSrc.includes("watch?v=")) {
|
|
iframeSrc = iframeSrc.replace("watch?v=", "embed/").split("&")[0];
|
|
} else if (service === "vimeo" && !iframeSrc.includes("player.vimeo.com")) {
|
|
const vimeoId = iframeSrc.split("/").pop() ?? "";
|
|
iframeSrc = `https://player.vimeo.com/video/${vimeoId}`;
|
|
}
|
|
|
|
return (
|
|
`<figure class="editorjs-embed">` +
|
|
`<iframe src="${iframeSrc}" width="${width}" height="${height}" frameborder="0" allowfullscreen loading="lazy"></iframe>` +
|
|
(caption ? `<figcaption>${caption}</figcaption>` : "") +
|
|
`</figure>`
|
|
);
|
|
}
|
|
|
|
|
|
default:
|
|
// Unknown block — skip silently
|
|
return "";
|
|
}
|
|
}
|