import ReactMarkdown, { type Components } from 'react-markdown' import rehypeRaw from 'rehype-raw' import rehypeSanitize from 'rehype-sanitize' import remarkGfm from 'remark-gfm' export interface MarkdownContentProps { /** Markdown source. */ content: string /** * Element overrides. Consumers (`MarkdownUI`, `Note`, …) supply their own * typography / link styling here; the engine itself stays opinion-free. */ components?: Components /** * When `true`, raw HTML in the source is rendered. Defaults to `false` so * caption slots like `Note` stay safe-by-default; the document-style * Markdown widget opts in. */ allowHtml?: boolean /** * When `true`, `` / `![alt](url)` markdown produces an `` tag. * Defaults to `false` so caption slots stay text-only by default. */ allowImages?: boolean /** * When `true`, enables GitHub Flavored Markdown (tables, strikethrough, * task lists, autolinks) via `remark-gfm`. Defaults to `false` so existing * consumers keep CommonMark-only parsing. */ gfm?: boolean } // Allowlist of URL schemes considered safe for `` and ``. // Anything outside this set (e.g. `data:`, `javascript:`, `vbscript:`) is // stripped by `safeUrlTransform`. Belt-and-braces with `rehype-sanitize`'s // `defaultSchema` (which already restricts protocols when raw HTML is parsed) // — this layer also covers the `allowHtml=false` path, where the sanitizer // doesn't run but `react-markdown`'s default `urlTransform` only blocks // `javascript:`. const SAFE_URL_PREFIX = /^(https?:|mailto:|tel:|#|\/|\.\/|\.\.\/)/i function safeUrlTransform(url: string): string { const trimmed = (url ?? '').trim() if (!trimmed) return '' if (SAFE_URL_PREFIX.test(trimmed)) return url return '' } /** * Low-level Markdown rendering primitive. Wraps `react-markdown` with two * opt-in capabilities (`allowHtml`, `allowImages`) and a consumer-supplied * component map for typography. Both default OFF — Note-style consumers * are safe-by-default; MarkdownUI opts both ON to render rich content. * * `allowHtml` plugs in `rehype-raw` so raw HTML nodes in the source are * actually parsed, then `rehype-sanitize` with the default schema strips * `