mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-03 02:11:07 +05:30
feat(renderer): markdown lib (marked + DOMPurify + mermaid placeholders)
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
import { marked } from 'marked';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
|
marked.setOptions({
|
||||||
|
gfm: true,
|
||||||
|
breaks: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const MERMAID_RE = /```mermaid\n([\s\S]*?)```/g;
|
||||||
|
|
||||||
|
export function renderMarkdown(source: string): string {
|
||||||
|
// Mark mermaid blocks with a placeholder we can replace client-side.
|
||||||
|
const placeholders: string[] = [];
|
||||||
|
const withPlaceholders = source.replace(MERMAID_RE, (_m, code) => {
|
||||||
|
const idx = placeholders.length;
|
||||||
|
placeholders.push(code.trim());
|
||||||
|
return `<div class="mermaid-block" data-mermaid-source="${idx}"></div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const rawHtml = marked.parse(withPlaceholders, { async: false }) as string;
|
||||||
|
const clean = DOMPurify.sanitize(rawHtml, {
|
||||||
|
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'data-mermaid-source', 'data-language', 'id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// The sanitized HTML still has placeholders; we leave the actual mermaid
|
||||||
|
// rendering to the React layer (MermaidLazy component) so the heavy
|
||||||
|
// mermaid library only loads when needed.
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { renderMarkdown } from '@/lib/markdown';
|
||||||
|
|
||||||
|
describe('renderMarkdown', () => {
|
||||||
|
it('renders headings', () => {
|
||||||
|
const html = renderMarkdown('# Hello');
|
||||||
|
expect(html).toContain('<h1>Hello</h1>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders bold and italic', () => {
|
||||||
|
const html = renderMarkdown('**bold** and *italic*');
|
||||||
|
expect(html).toContain('<strong>bold</strong>');
|
||||||
|
expect(html).toContain('<em>italic</em>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders code blocks with language class', () => {
|
||||||
|
const html = renderMarkdown('```js\nconst x = 1;\n```');
|
||||||
|
expect(html).toContain('language-js');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders mermaid blocks with a placeholder', () => {
|
||||||
|
const html = renderMarkdown('```mermaid\ngraph TD\nA-->B\n```');
|
||||||
|
expect(html).toContain('data-mermaid-source=');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sanitizes dangerous HTML', () => {
|
||||||
|
const html = renderMarkdown('<script>alert(1)</script>');
|
||||||
|
expect(html).not.toContain('<script>');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user