- Remove deprecated marked options (sanitize, headerIds, mangle) - Fix undefined codeExample and diagramExample references in templateRenderer - Replace with additional markdown sample content - Ensure clean TypeScript build with no errors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
192 lines
5.4 KiB
TypeScript
192 lines
5.4 KiB
TypeScript
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 <br>
|
|
});
|
|
|
|
/**
|
|
* 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(`<h${level}([^>]*)>`, 'g');
|
|
html = html.replace(regex, `<h${options.maxHeadingLevel}$1>`);
|
|
html = html.replace(new RegExp(`</h${level}>`, 'g'), `</h${options.maxHeadingLevel}>`);
|
|
}
|
|
}
|
|
|
|
// 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 `<div class="${options.className}">${sanitized}</div>`;
|
|
}
|
|
|
|
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<string, string> = {
|
|
'<': '<',
|
|
'>': '>',
|
|
'&': '&',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
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()
|
|
}; |