Files
ipv6-sims/app/utils/editorjsToHtml.ts
T
2026-05-11 16:24:14 +07:00

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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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 "";
}
}