import { marked } from 'marked'; import DOMPurify from 'dompurify'; /** * Secure markdown processor for slide content * Uses marked for parsing and DOMPurify for sanitization */ // Configure marked for slide-safe markdown marked.setOptions({ gfm: true, // GitHub Flavored Markdown breaks: true, // Convert line breaks to
}); /** * DOMPurify configuration for slides * Allows safe HTML elements commonly used in presentations */ const PURIFY_CONFIG = { ALLOWED_TAGS: [ // Text formatting 'p', 'br', 'strong', 'em', 'b', 'i', 'u', 's', 'mark', 'small', 'sub', 'sup', // Headers 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', // Lists 'ul', 'ol', 'li', // Links (without javascript: or data: schemes) 'a', // Code 'code', 'pre', // Tables 'table', 'thead', 'tbody', 'tr', 'th', 'td', // Quotes 'blockquote', 'cite', // Semantic elements 'span', 'div', ], ALLOWED_ATTR: [ 'href', 'title', 'class', 'id', 'data-*', // Table attributes 'colspan', 'rowspan', // Link attributes (but we'll filter URLs) 'target', 'rel', // Code highlighting attributes 'style' // Allow style attribute for syntax highlighting ], ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i, FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'applet', 'form', 'input', 'button'], FORBID_ATTR: ['onclick', 'onload', 'onerror', 'onmouseover', 'onfocus', 'onblur', 'onchange', 'onsubmit'], KEEP_CONTENT: true, RETURN_DOM: false, RETURN_DOM_FRAGMENT: false, SANITIZE_DOM: true, }; /** * Safely renders markdown to HTML * @param markdown - The markdown content to render * @param options - Additional rendering options * @returns Sanitized HTML string */ export const renderMarkdown = ( markdown: string, options: { allowImages?: boolean; maxHeadingLevel?: number; className?: string; } = {} ): string => { if (!markdown || typeof markdown !== 'string') { return ''; } try { // Parse markdown to HTML let html = marked.parse(markdown) as string; // Apply heading level restrictions if (options.maxHeadingLevel && options.maxHeadingLevel < 6) { for (let level = options.maxHeadingLevel + 1; level <= 6; level++) { const regex = new RegExp(`]*)>`, 'g'); html = html.replace(regex, ``); html = html.replace(new RegExp(``, 'g'), ``); } } // Configure DOMPurify for this render const config = { ...PURIFY_CONFIG }; // Conditionally allow images if (options.allowImages) { config.ALLOWED_TAGS = [...config.ALLOWED_TAGS, 'img']; config.ALLOWED_ATTR = [...config.ALLOWED_ATTR, 'src', 'alt', 'width', 'height']; } // Sanitize the HTML const sanitized = DOMPurify.sanitize(html, config); // Wrap in container if className provided if (options.className) { return `
${sanitized}
`; } return sanitized; } catch (error) { console.warn('Markdown rendering failed:', error); // Return escaped plain text as fallback return DOMPurify.sanitize(markdown.replace(/[<>&"']/g, (char) => { const escapeMap: Record = { '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' }; return escapeMap[char]; })); } }; /** * Renders markdown specifically for slide content * Applies slide-appropriate restrictions and styling */ export const renderSlideMarkdown = (markdown: string, slotType?: string): string => { const options = { allowImages: slotType === 'image' || slotType === 'content', maxHeadingLevel: 3, // Limit to h1, h2, h3 for slides className: `markdown-content slot-${slotType || 'text'}` }; return renderMarkdown(markdown, options); }; /** * Checks if content appears to be markdown * Used to determine whether to process content as markdown */ export const isMarkdownContent = (content: string): boolean => { if (!content || typeof content !== 'string') { return false; } // Common markdown patterns const markdownPatterns = [ /^#{1,6}\s/m, // Headers /\*\*.*\*\*/, // Bold /\*.*\*/, // Italic /^\s*[-*+]\s/m, // Unordered lists /^\s*\d+\.\s/m, // Ordered lists /```/, // Code blocks /`[^`]+`/, // Inline code /\[.*\]\(.*\)/, // Links /!\[.*\]\(.*\)/, // Images /^>\s/m, // Blockquotes ]; return markdownPatterns.some(pattern => pattern.test(content)); }; /** * Sample markdown content for testing and previews */ export const SAMPLE_MARKDOWN_CONTENT = { title: '# Quarterly **Sales** Review', subtitle: '## Q4 Results and *Future Outlook*', content: ` ## Key Highlights - **Revenue Growth**: 25% increase over last quarter - **Customer Satisfaction**: 94% positive feedback - **Market Expansion**: 3 new regions launched ### Action Items 1. Finalize Q1 budget allocation 2. Launch customer feedback program 3. Prepare expansion strategy > "Our team's dedication to excellence continues to drive exceptional results." [View Full Report](https://example.com/report) `.trim(), list: ` - Increase market share by **15%** - Launch *3 new products* - Expand to **5 new regions** - Improve customer satisfaction - \`Optimize\` operational efficiency `.trim() };