import { Context } from "runed"; import type { ReadableBoxedValues, WritableBoxedValues } from "svelte-toolbelt"; import type { CodeRootProps } from "./types"; import { highlighter } from "./shiki"; import createDOMPurify from "dompurify"; import type { HighlighterCore } from "shiki"; type CodeOverflowStateProps = WritableBoxedValues<{ collapsed: boolean; }>; // Bind DOMPurify only in the browser const DOMPurify = typeof window !== "undefined" ? createDOMPurify(window) : null; type CodeRootStateProps = ReadableBoxedValues<{ code: string; lang: NonNullable; hideLines: boolean; highlight: CodeRootProps["highlight"]; }>; class CodeOverflowState { constructor(readonly opts: CodeOverflowStateProps) { this.toggleCollapsed = this.toggleCollapsed.bind(this); } toggleCollapsed() { this.opts.collapsed.current = !this.opts.collapsed.current; } get collapsed() { return this.opts.collapsed.current; } } class CodeRootState { highlighter: HighlighterCore | null = $state(null); constructor( readonly opts: CodeRootStateProps, readonly overflow?: CodeOverflowState ) { highlighter.then((hl) => (this.highlighter = hl)); } highlight(code: string) { return this.highlighter?.codeToHtml(code, { lang: this.opts.lang.current, themes: { light: "github-light-default", dark: "github-dark-default", }, transformers: [ { pre: (el) => { el.properties.style = ""; if (!this.opts.hideLines.current) { el.properties.class += " line-numbers"; } return el; }, line: (node, line) => { if (within(line, this.opts.highlight.current)) { node.properties.class = node.properties.class + " line--highlighted"; } return node; }, }, ], }); } get code() { return this.opts.code.current; } // Use DOMPurify in the browser, raw HTML as a fallback during SSR highlighted = $derived.by(() => { const html = this.highlight(this.code) ?? ""; if (DOMPurify) { return DOMPurify.sanitize(html); } return html; }); } function within(num: number, range: CodeRootProps["highlight"]) { if (!range) return false; let within = false; for (const r of range) { if (typeof r === "number") { if (num === r) { within = true; break; } continue; } if (r[0] <= num && num <= r[1]) { within = true; break; } } return within; } class CodeCopyButtonState { constructor(readonly root: CodeRootState) {} get code() { return this.root.opts.code.current; } } const overflowCtx = new Context("code-overflow-state"); const ctx = new Context("code-root-state"); export function useCodeOverflow(props: CodeOverflowStateProps) { return overflowCtx.set(new CodeOverflowState(props)); } export function useCode(props: CodeRootStateProps) { return ctx.set(new CodeRootState(props, overflowCtx.getOr(undefined))); } export function useCodeCopyButton() { return new CodeCopyButtonState(ctx.get()); }