src/lib/components/chat/MarkdownRenderer.svelte (77 lines of code) (raw):
<script lang="ts">
import type { WebSearchSource } from "$lib/types/WebSearch";
import { processTokens, processTokensSync, type Token } from "$lib/utils/marked";
// import MarkdownWorker from "$lib/workers/markdownWorker?worker";
import CodeBlock from "../CodeBlock.svelte";
import type { IncomingMessage, OutgoingMessage } from "$lib/workers/markdownWorker";
import { browser } from "$app/environment";
import DOMPurify from "isomorphic-dompurify";
import { onMount } from "svelte";
import { updateDebouncer } from "$lib/utils/updates";
interface Props {
content: string;
sources?: WebSearchSource[];
}
let worker: Worker | null = null;
let { content, sources = [] }: Props = $props();
let tokens: Token[] = $state(processTokensSync(content, sources));
async function processContent(content: string, sources: WebSearchSource[]): Promise<Token[]> {
if (worker) {
return new Promise((resolve) => {
if (!worker) {
throw new Error("Worker not initialized");
}
worker.onmessage = (event: MessageEvent<OutgoingMessage>) => {
if (event.data.type !== "processed") {
throw new Error("Invalid message type");
}
resolve(event.data.tokens);
};
worker.postMessage(
JSON.parse(JSON.stringify({ content, sources, type: "process" })) as IncomingMessage
);
});
} else {
return processTokens(content, sources);
}
}
$effect(() => {
if (!browser) {
tokens = processTokensSync(content, sources);
} else {
(async () => {
updateDebouncer.startRender();
tokens = await processContent(content, sources).then(
async (tokens) =>
await Promise.all(
tokens.map(async (token) => {
if (token.type === "text") {
token.html = DOMPurify.sanitize(await token.html);
}
return token;
})
)
);
updateDebouncer.endRender();
})();
}
});
onMount(() => {
// todo: fix worker, seems to be transmitting a lot of data
// worker = browser && window.Worker ? new MarkdownWorker() : null;
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
if (node.tagName === "A") {
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noreferrer");
}
});
});
</script>
{#each tokens as token}
{#if token.type === "text"}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html token.html}
{:else if token.type === "code"}
<CodeBlock code={token.code} rawCode={token.rawCode} />
{/if}
{/each}