/** * Markdown render helper for cards. * * Pipeline: marked (GFM) → DOMPurify. Used by the card face for basic / * type-in / basic-reverse, and by the cloze post-processor. * * Cloze callers should pass `{ skipParagraph: true }` so a single-line * fragment doesn't get wrapped in

. */ import { marked } from 'marked'; import DOMPurify from 'isomorphic-dompurify'; marked.setOptions({ gfm: true, breaks: true }); export interface RenderOptions { skipParagraph?: boolean; } export function renderMarkdown(source: string, opts: RenderOptions = {}): string { if (!source) return ''; const raw = marked.parse(source, { async: false }) as string; let html = DOMPurify.sanitize(raw, { // `mark` for cloze highlights; `audio`/`source`/`video` for media // attachments inserted via the editor (the Markdown renderer // passes inline HTML through, sanitizer is the gate). ADD_TAGS: ['mark', 'audio', 'source', 'video'], ADD_ATTR: ['class', 'controls', 'preload', 'src', 'type'], }); if (opts.skipParagraph) { html = html.replace(/^\s*

/, '').replace(/<\/p>\s*$/, ''); } return html; }