/** * Markdown Detection and Conversion Utilities * * Handles detection of markdown vs HTML content and conversion between formats */ /** * Detect if content is markdown or HTML * * @param content - The content to check * @returns 'markdown' | 'html' */ export function detectContentType(content: string): 'markdown' | 'html' { if (!content || content.trim() === '') { return 'html'; } // Check for markdown-specific patterns const markdownPatterns = [ /^\*\*[^*]+\*\*/m, // **bold** /^__[^_]+__/m, // __bold__ /^\*[^*]+\*/m, // *italic* /^_[^_]+_/m, // _italic_ /^#{1,6}\s/m, // # headings /^\[card[^\]]*\]/m, // [card] syntax /^\[button\s+url=/m, // [button url=...] syntax /^---$/m, // horizontal rules /^[\*\-•✓]\s/m, // bullet points ]; // Check for HTML-specific patterns const htmlPatterns = [ /<[a-z][\s\S]*>/i, // HTML tags /<\/[a-z]+>/i, // Closing tags /&[a-z]+;/i, // HTML entities ]; // Count markdown vs HTML indicators let markdownScore = 0; let htmlScore = 0; for (const pattern of markdownPatterns) { if (pattern.test(content)) { markdownScore++; } } for (const pattern of htmlPatterns) { if (pattern.test(content)) { htmlScore++; } } // If content has [card] or [button] syntax, it's definitely our markdown format if (/\[card[^\]]*\]/.test(content) || /\[button\s+url=/.test(content)) { return 'markdown'; } // If content has HTML tags but no markdown, it's HTML if (htmlScore > 0 && markdownScore === 0) { return 'html'; } // If content has markdown indicators, it's markdown if (markdownScore > 0) { return 'markdown'; } // Default to HTML for safety return 'html'; } /** * Convert markdown to HTML for display * * @param markdown - Markdown content * @returns HTML content */ export function markdownToHtml(markdown: string): string { if (!markdown) return ''; let html = markdown; // Parse [card:type] blocks (new syntax) html = html.replace(/\[card:(\w+)\]([\s\S]*?)\[\/card\]/g, (match, type, content) => { const cardClass = `card card-${type}`; const parsedContent = parseMarkdownBasics(content.trim()); return `
${paragraphContent}
`); paragraphContent = ''; } }; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); // Empty line - close paragraph or list if (!trimmed) { if (inList) { processedLines.push(''); inList = false; } closeParagraph(); processedLines.push(''); continue; } // Check if line is a list item if (/^[\*\-•✓]\s/.test(trimmed)) { closeParagraph(); const content = trimmed.replace(/^[\*\-•✓]\s/, ''); if (!inList) { processedLines.push(']*>]*>([^<]+)<\/a><\/p>/g, (match, url, className, text) => {
const style = className.includes('outline') ? ' style="outline"' : '';
return `[button url="${url}"${style}]${text.trim()}[/button]`;
});
// Convert remaining HTML to markdown
markdown = parseHtmlToMarkdownBasics(markdown);
return markdown;
}
/**
* Parse HTML back to basic markdown
*
* @param html - HTML text
* @returns Markdown text
*/
function parseHtmlToMarkdownBasics(html: string): string {
let markdown = html;
// Headings
markdown = markdown.replace(/ (.*?)<\/p>/gi, '$1\n\n');
// Clean up extra newlines
markdown = markdown.replace(/\n{3,}/g, '\n\n');
return markdown.trim();
}
(.*?)<\/h1>/gi, '# $1');
markdown = markdown.replace(/
(.*?)<\/h2>/gi, '## $1');
markdown = markdown.replace(/
(.*?)<\/h3>/gi, '### $1');
markdown = markdown.replace(/
(.*?)<\/h4>/gi, '#### $1');
// Bold
markdown = markdown.replace(/(.*?)<\/strong>/gi, '**$1**');
markdown = markdown.replace(/(.*?)<\/b>/gi, '**$1**');
// Italic
markdown = markdown.replace(/(.*?)<\/em>/gi, '*$1*');
markdown = markdown.replace(/(.*?)<\/i>/gi, '*$1*');
// Links
markdown = markdown.replace(/]*>(.*?)<\/a>/gi, '[$2]($1)');
// Horizontal rules
markdown = markdown.replace(/
/gi, '\n---\n');
// Lists
markdown = markdown.replace(/([\s\S]*?)<\/ul>/gi, (match, content) => {
return content.replace(/