/** * 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 }> }; 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, """); } function renderBlock(block: { type: string; data: Record }): string { const { type, data } = block; switch (type) { case "paragraph": { const text = data.text as string ?? ""; return `

${text}

`; } case "header": { const level = Number(data.level) || 2; const tag = `h${Math.min(Math.max(level, 1), 6)}`; return `<${tag}>${data.text}`; } case "list": { const style = data.style === "ordered" ? "ol" : "ul"; const items = (data.items as string[]) ?? []; const lis = items.map((item) => `
  • ${item}
  • `).join("\n"); return `<${style}>\n${lis}\n`; } case "checklist": { const items = (data.items as Array<{ text: string; checked: boolean }>) ?? []; const lis = items .map( (item) => `
  • ` + `${item.checked ? "✓" : "○"} ${item.text}` + `
  • ` ) .join("\n"); return ``; } 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 ( `` + `${esc(caption)}` + (caption ? `
    ${caption}
    ` : "") + `` ); } case "quote": { const text = data.text as string ?? ""; const caption = data.caption as string ?? ""; return ( `
    ` + `

    ${text}

    ` + (caption ? `${caption}` : "") + `
    ` ); } case "code": { return `
    ${esc(data.code)}
    `; } case "delimiter": { return `
    `; } 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}`).join(""); return `${cells}`; }); return `\n${rows.join("\n")}\n
    `; } 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 ( `
    ` + `` + (caption ? `
    ${caption}
    ` : "") + `
    ` ); } default: // Unknown block — skip silently return ""; } }