/** * Convert EditorJS JSON data to HTML * Supports: header, paragraph, list, image, quote, marker, embed, code, table, delimiter, * checklist, link, inlineCode, warning, alert, raw, personality, and more */ interface EditorJSBlockData { text?: string; level?: number; items?: string[] | Array<{ text?: string; checked?: boolean }>; style?: 'ordered' | 'unordered'; file?: { url?: string }; url?: string; caption?: string; withBorder?: boolean; withBackground?: boolean; stretched?: boolean; alignment?: 'left' | 'center' | 'right'; service?: string; source?: string; embed?: string; width?: number; height?: number; code?: string; language?: string; content?: string[][]; link?: string; meta?: { url?: string; title?: string; description?: string; image?: string }; title?: string; message?: string; html?: string; htmlCode?: string; name?: string; description?: string; photo?: string; linkText?: string; linkUrl?: string; type?: string; } interface EditorJSBlock { id?: string; type: string; data: EditorJSBlockData; } interface EditorJSData { time?: number; blocks: EditorJSBlock[]; version?: string; } /** * Convert EditorJS block to HTML */ function blockToHtml(block: EditorJSBlock, baseUrl?: string): string { const { type, data } = block; switch (type) { case 'header': { const level = data.level || 2; const text = escapeHtml(data.text || ''); return `${text}`; } case 'paragraph': { let text = data.text || ''; // Convert markdown-style links to HTML text = text.replace( /\[([^\]]+)\]\(([^)]+)\)/g, '$1' ); // Convert **bold** to text = text.replace(/\*\*([^*]+)\*\*/g, '$1'); // Convert *italic* to text = text.replace(/\*([^*]+)\*/g, '$1'); // Convert `inline code` to text = text.replace(/`([^`]+)`/g, '$1'); // Convert line breaks text = text.replace(/\n/g, '
'); return `

${text}

`; } case 'list': { const items = data.items || []; const style = data.style || 'unordered'; const tag = style === 'ordered' ? 'ol' : 'ul'; const itemsHtml = items .map((item: { text?: string; checked?: boolean }) => { const escapedItem = escapeHtml(item.text || ''); return `
  • ${escapedItem}
  • `; }) .join(''); return `<${tag}>${itemsHtml}`; } case 'image': { const url = data.file?.url || data.url || ''; const caption = data.caption || ''; const withBorder = data.withBorder || false; const withBackground = data.withBackground || false; const stretched = data.stretched || false; let imageUrl = url; if (baseUrl && imageUrl && !imageUrl.startsWith('http')) { if (imageUrl.startsWith('/')) { imageUrl = `${baseUrl}${imageUrl}`; } else { imageUrl = `${baseUrl}/${imageUrl}`; } } const classes: string[] = []; if (withBorder) classes.push('with-border'); if (withBackground) classes.push('with-background'); if (stretched) classes.push('stretched'); const classAttr = classes.length > 0 ? ` class="${classes.join(' ')}"` : ''; const captionHtml = caption ? `
    ${escapeHtml(caption)}
    ` : ''; return ` ${escapeHtml(caption)} ${captionHtml} `; } case 'quote': { const text = escapeHtml(data.text || ''); const caption = escapeHtml(data.caption || ''); const alignment = data.alignment || 'left'; const captionHtml = caption ? `${caption}` : ''; return `

    ${text}

    ${captionHtml}
    `; } case 'marker': { const text = data.text || ''; // Marker typically highlights text, so we'll use return `${escapeHtml(text)}`; } case 'embed': { const service = data.service || ''; const embed = data.embed || ''; const width = data.width || 600; const height = data.height || 400; const caption = data.caption || ''; if (service === 'youtube' || service === 'vimeo') { const captionHtml = caption ? `
    ${escapeHtml(caption)}
    ` : ''; return `
    ${captionHtml}
    `; } // Fallback for other embed types return `
    ${embed} ${caption ? `

    ${escapeHtml(caption)}

    ` : ''}
    `; } case 'code': { const code = data.code || ''; const language = data.language || ''; const langAttr = language ? ` class="language-${language}"` : ''; return `
    ${escapeHtml(code)}
    `; } case 'delimiter': { return '
    ***
    '; } case 'table': { const content = data.content || []; if (content.length === 0) return ''; // First row is usually header const headerRow = content[0] || []; const headerCells = headerRow .map((cell: string) => `${escapeHtml(cell)}`) .join(''); // Rest are body rows const bodyRows = content.slice(1) .map((row: string[]) => { const cells = row .map((cell: string) => `${escapeHtml(cell)}`) .join(''); return `${cells}`; }) .join(''); return ` ${headerCells ? `${headerCells}` : ''} ${bodyRows}
    `; } case 'checklist': { const items = data.items || []; const itemsHtml = items .map((item: { text?: string; checked?: boolean }) => { const text = escapeHtml(item.text || ''); const checked = item.checked ? ' checked' : ''; return `
    `; }) .join(''); return `
    ${itemsHtml}
    `; } case 'linkTool': case 'link': { const link = data.link || data.url || ''; const meta = data.meta || {}; const title = meta.title || data.title || link; const description = meta.description || ''; const image = meta.image || ''; let imageHtml = ''; if (image) { let imageUrl = image; if (baseUrl && imageUrl && !imageUrl.startsWith('http')) { imageUrl = imageUrl.startsWith('/') ? `${baseUrl}${imageUrl}` : `${baseUrl}/${imageUrl}`; } imageHtml = ``; } return ``; } case 'inlineCode': { const code = data.text || ''; return `${escapeHtml(code)}`; } case 'warning': { const title = data.title || 'Warning'; const message = data.message || ''; return `
    ⚠️ ${escapeHtml(title)}
    ${escapeHtml(message)}
    `; } case 'alert': { const type = data.type || 'info'; const title = data.title || ''; const message = data.message || ''; return `
    ${title ? `
    ${escapeHtml(title)}
    ` : ''}
    ${escapeHtml(message)}
    `; } case 'raw': { const html = data.html || data.htmlCode || ''; // Return raw HTML (be careful with XSS) return html; } case 'personality': { const name = data.name || ''; const description = data.description || ''; const photo = data.photo || ''; const linkText = data.linkText || ''; const linkUrl = data.linkUrl || ''; let photoHtml = ''; if (photo) { let photoUrl = photo; if (baseUrl && photoUrl && !photoUrl.startsWith('http')) { photoUrl = photoUrl.startsWith('/') ? `${baseUrl}${photoUrl}` : `${baseUrl}/${photoUrl}`; } photoHtml = `
    ${escapeHtml(name)}
    `; } const linkHtml = linkUrl && linkText ? `${escapeHtml(linkText)}` : ''; return `
    ${photoHtml}
    ${escapeHtml(name)}
    ${description ? `
    ${escapeHtml(description)}
    ` : ''} ${linkHtml}
    `; } default: // Unknown block type - return empty or log warning console.warn(`Unknown EditorJS block type: ${type}`); return ''; } } /** * Escape HTML special characters */ function escapeHtml(text: string): string { const map: { [key: string]: string } = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }; return text.replace(/[&<>"']/g, (m) => map[m]); } /** * Convert EditorJS JSON data to HTML string * @param editorData - EditorJS JSON data (can be string or object) * @param baseUrl - Base URL for images (optional) * @returns HTML string */ export function editorjsToHtml( editorData: string | EditorJSData | null | undefined, baseUrl?: string ): string { if (!editorData) { return ''; } // If it's a string, try to parse it as JSON let parsedData: EditorJSData; if (typeof editorData === 'string') { try { parsedData = JSON.parse(editorData); } catch { // If parsing fails, assume it's already HTML return editorData; } } else { parsedData = editorData; } // If it doesn't have blocks, assume it's already HTML if (!parsedData.blocks || !Array.isArray(parsedData.blocks)) { if (typeof editorData === 'string') { return editorData; } return ''; } // Convert each block to HTML const htmlBlocks = parsedData.blocks .map((block) => blockToHtml(block, baseUrl)) .filter((html) => html.length > 0); return htmlBlocks.join('\n'); } /** * Check if content is EditorJS format */ export function isEditorJSFormat( content: string | EditorJSData | null | undefined ): boolean { if (!content) return false; try { const parsed = typeof content === 'string' ? JSON.parse(content) : content; return ( parsed && typeof parsed === 'object' && Array.isArray(parsed.blocks) && parsed.blocks.length > 0 ); } catch { return false; } }