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}${tag}>`;
}
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(' ')} editorjs-image"` : ' class="editorjs-image"';
const captionHtml = caption
? `${escapeHtml(caption)} `
: '';
return `
${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 ``;
}
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)}
`;
}
case 'alert': {
const type = data.type || 'info';
const title = data.title || '';
const message = data.message || '';
return `
${title ? `${escapeHtml(title)}` : ''}
`;
}
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 = `
`;
}
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;
}
}