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()
};