import { useMarked } from "../context/marked" import DOMPurify from "dompurify" import { checksum } from "@opencode-ai/util/encode" import { ComponentProps, createResource, splitProps } from "solid-js" import { isServer } from "solid-js/web" type Entry = { hash: string html: string } const max = 200 const cache = new Map() 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(" ")) }) } const config = { USE_PROFILES: { html: true, mathMl: true }, SANITIZE_NAMED_PROPS: true, FORBID_TAGS: ["style"], FORBID_CONTENTS: ["style", "script"], } function sanitize(html: string) { if (!DOMPurify.isSupported) return "" return DOMPurify.sanitize(html, config) } function touch(key: string, value: Entry) { 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 function Markdown( props: ComponentProps<"div"> & { text: string cacheKey?: string class?: string classList?: Record }, ) { const [local, others] = splitProps(props, ["text", "cacheKey", "class", "classList"]) const marked = useMarked() const [html] = createResource( () => local.text, async (markdown) => { if (isServer) return "" const hash = checksum(markdown) const key = local.cacheKey ?? hash if (key && hash) { const cached = cache.get(key) if (cached && cached.hash === hash) { touch(key, cached) return cached.html } } const next = await marked.parse(markdown) const safe = sanitize(next) if (key && hash) touch(key, { hash, html: safe }) return safe }, { initialValue: "" }, ) return (
) }