// Render Edra/BlockNote JSON content to HTML export const renderEdraContent = (content: any): string => { if (!content) return '' // Handle Tiptap format first (has type: 'doc') if (content.type === 'doc' && content.content) { return renderTiptapContent(content) } // Handle both { blocks: [...] } and { content: [...] } formats const blocks = content.blocks || content.content || [] if (!Array.isArray(blocks)) return '' const renderBlock = (block: any): string => { switch (block.type) { case 'heading': { const level = block.attrs?.level || block.level || 1 const headingText = block.content || block.text || '' return `${headingText}` } case 'paragraph': { const paragraphText = block.content || block.text || '' if (!paragraphText) return '


' return `

${paragraphText}

` } case 'bulletList': case 'ul': { const listItems = (block.content || []) .map((item: any) => { const itemText = item.content || item.text || '' return `
  • ${itemText}
  • ` }) .join('') return `` } case 'orderedList': case 'ol': { const orderedItems = (block.content || []) .map((item: any) => { const itemText = item.content || item.text || '' return `
  • ${itemText}
  • ` }) .join('') return `
      ${orderedItems}
    ` } case 'blockquote': { const quoteText = block.content || block.text || '' return `

    ${quoteText}

    ` } case 'codeBlock': case 'code': { const codeText = block.content || block.text || '' const language = block.attrs?.language || block.language || '' return `
    ${codeText}
    ` } case 'image': { const src = block.attrs?.src || block.src || '' const alt = block.attrs?.alt || block.alt || '' const caption = block.attrs?.caption || block.caption || '' return `
    ${alt}${caption ? `
    ${caption}
    ` : ''}
    ` } case 'hr': case 'horizontalRule': return '
    ' default: { // For simple text content const text = block.content || block.text || '' if (text) { return `

    ${text}

    ` } return '' } } } return blocks.map(renderBlock).join('') } // Render Tiptap JSON content to HTML function renderTiptapContent(doc: any): string { if (!doc || !doc.content) return '' const renderNode = (node: any): string => { switch (node.type) { case 'paragraph': { const content = renderInlineContent(node.content || []) if (!content) return '


    ' return `

    ${content}

    ` } case 'heading': { const level = node.attrs?.level || 1 const content = renderInlineContent(node.content || []) return `${content}` } case 'bulletList': { const items = (node.content || []) .map((item: any) => { const itemContent = item.content?.map(renderNode).join('') || '' return `
  • ${itemContent}
  • ` }) .join('') return `` } case 'orderedList': { const items = (node.content || []) .map((item: any) => { const itemContent = item.content?.map(renderNode).join('') || '' return `
  • ${itemContent}
  • ` }) .join('') return `
      ${items}
    ` } case 'listItem': { // List items are handled by their parent list return node.content?.map(renderNode).join('') || '' } case 'blockquote': { const content = node.content?.map(renderNode).join('') || '' return `
    ${content}
    ` } case 'codeBlock': { const language = node.attrs?.language || '' const content = node.content?.[0]?.text || '' return `
    ${escapeHtml(content)}
    ` } case 'image': { const src = node.attrs?.src || '' const alt = node.attrs?.alt || '' const title = node.attrs?.title || '' const width = node.attrs?.width const height = node.attrs?.height const widthAttr = width ? ` width="${width}"` : '' const heightAttr = height ? ` height="${height}"` : '' return `
    ${alt}${title ? `
    ${title}
    ` : ''}
    ` } case 'horizontalRule': { return '
    ' } case 'hardBreak': { return '
    ' } case 'urlEmbed': { const url = node.attrs?.url || '' const title = node.attrs?.title || '' const description = node.attrs?.description || '' const image = node.attrs?.image || '' const favicon = node.attrs?.favicon || '' const siteName = node.attrs?.siteName || '' // Helper to get domain from URL const getDomain = (url: string) => { try { const urlObj = new URL(url) return urlObj.hostname.replace('www.', '') } catch { return '' } } let embedHtml = '
    ' embedHtml += `` if (image) { embedHtml += `
    ${title || 'Link preview'}
    ` } embedHtml += '
    ' embedHtml += '
    ' if (favicon) { embedHtml += `` } embedHtml += `${siteName || getDomain(url)}` embedHtml += '
    ' if (title) { embedHtml += `

    ${title}

    ` } if (description) { embedHtml += `

    ${description}

    ` } embedHtml += '
    ' embedHtml += '
    ' embedHtml += '
    ' return embedHtml } default: { // For any unknown block types, try to render their content if (node.content) { return node.content.map(renderNode).join('') } return '' } } } // Render inline content (text nodes with marks) const renderInlineContent = (content: any[]): string => { return content .map((node: any) => { if (node.type === 'text') { let text = escapeHtml(node.text || '') // Apply marks (bold, italic, etc.) if (node.marks) { node.marks.forEach((mark: any) => { switch (mark.type) { case 'bold': text = `${text}` break case 'italic': text = `${text}` break case 'underline': text = `${text}` break case 'strike': text = `${text}` break case 'code': text = `${text}` break case 'link': const href = mark.attrs?.href || '#' const target = mark.attrs?.target || '_blank' text = `${text}` break case 'highlight': text = `${text}` break } }) } return text } // Handle other inline nodes return renderNode(node) }) .join('') } // Helper to escape HTML const escapeHtml = (str: string): string => { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } return doc.content.map(renderNode).join('') } // Extract text content from Edra JSON for excerpt export const getContentExcerpt = (content: any, maxLength = 200): string => { if (!content) return '' // Handle Tiptap format first (has type: 'doc') if (content.type === 'doc' && content.content) { return extractTiptapText(content, maxLength) } // Handle both { blocks: [...] } and { content: [...] } formats const blocks = content.blocks || content.content || [] if (!Array.isArray(blocks)) return '' const extractText = (node: any): string => { // For block-level content if (node.type && node.content && typeof node.content === 'string') { return node.content } // For inline content with text property if (node.text) return node.text // For nested content if (node.content && Array.isArray(node.content)) { return node.content.map(extractText).join(' ') } return '' } const text = blocks.map(extractText).join(' ').trim() if (text.length <= maxLength) return text return text.substring(0, maxLength).trim() + '...' } // Extract text from Tiptap content function extractTiptapText(doc: any, maxLength: number): string { const extractFromNode = (node: any): string => { if (node.type === 'text') { return node.text || '' } if (node.content && Array.isArray(node.content)) { return node.content.map(extractFromNode).join(' ') } return '' } const text = doc.content.map(extractFromNode).join(' ').trim() if (text.length <= maxLength) return text return text.substring(0, maxLength).trim() + '...' }