| import { checksum } from "@opencode-ai/core/util/encode" |
| import DOMPurify from "dompurify" |
| import { parseMarkdown } from "./markdown-worker" |
|
|
| export type MarkdownCacheEntry = { |
| raw: string |
| hash: string |
| html: string |
| } |
|
|
| const max = 200 |
| const cache = new Map<string, MarkdownCacheEntry>() |
| const config = { |
| USE_PROFILES: { html: true, mathMl: true }, |
| SANITIZE_NAMED_PROPS: true, |
| FORBID_TAGS: ["style"], |
| FORBID_CONTENTS: ["style", "script"], |
| ADD_TAGS: ["svg", "path"], |
| ADD_ATTR: ["d", "viewBox", "preserveAspectRatio", "xmlns", "target"], |
| } |
|
|
| if (typeof window !== "undefined" && DOMPurify.isSupported) { |
| DOMPurify.addHook("afterSanitizeAttributes", (node: Element) => { |
| if (!(node instanceof HTMLAnchorElement)) return |
| if (node.target !== "_blank") return |
|
|
| const rel = node.getAttribute("rel") ?? "" |
| const set = new Set(rel.split(/\s+/).filter(Boolean)) |
| set.add("noopener") |
| set.add("noreferrer") |
| node.setAttribute("rel", Array.from(set).join(" ")) |
| }) |
| } |
|
|
| export function sanitizeMarkdown(html: string) { |
| if (!DOMPurify.isSupported) return "" |
| return DOMPurify.sanitize(html, config) |
| } |
|
|
| export function getCachedMarkdown(key: string) { |
| return cache.get(key) |
| } |
|
|
| export function touchCachedMarkdown(key: string, value: MarkdownCacheEntry) { |
| cache.delete(key) |
| cache.set(key, value) |
|
|
| if (cache.size <= max) return |
|
|
| const first = cache.keys().next().value |
| if (!first) return |
| cache.delete(first) |
| } |
|
|
| export async function preloadMarkdown(text: string, cacheKey: string) { |
| const key = `${cacheKey}:0:full` |
| const cached = getCachedMarkdown(key) |
| if (cached?.raw === text) { |
| touchCachedMarkdown(key, cached) |
| return |
| } |
| const hash = checksum(text) |
| if (!hash) return |
| touchCachedMarkdown(key, { |
| raw: text, |
| hash, |
| html: sanitizeMarkdown(await parseMarkdown(text)), |
| }) |
| } |
|
|