File size: 2,735 Bytes
1bcd186
7d6fc19
47f81d6
1bcd186
be6d225
7d6fc19
 
be6d225
 
1bcd186
6e2a902
 
a1a6daf
 
7bf1507
 
a1a6daf
 
be6d225
cbd723d
7bf1507
cbd723d
7d6fc19
1bcd186
7bf1507
 
 
 
be6d225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1bcd186
 
7d6fc19
 
 
 
 
be6d225
 
 
 
 
6e2a902
be6d225
 
 
 
 
 
 
 
7d6fc19
 
6f93b29
 
6e2a902
47f81d6
 
be6d225
6e2a902
 
 
 
be6d225
 
 
 
 
 
1bcd186
 
 
7d6fc19
 
be6d225
 
7d6fc19
3b53c7a
6f7beab
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<script lang="ts">
	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 { onMount } from "svelte";
	import { updateDebouncer } from "$lib/utils/updates";

	let DOMPurify: typeof import("isomorphic-dompurify").default | null = null;

	interface Props {
		content: string;
		sources?: { title?: string; link: string }[];
		loading?: boolean;
	}

	let worker: Worker | null = null;

	let { content, sources = [], loading = false }: Props = $props();

	let tokens: Token[] = $state(processTokensSync(content, sources));

	async function processContent(
		content: string,
		sources: { title?: string; link: string }[]
	): 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" && DOMPurify) {
									token.html = DOMPurify.sanitize(await token.html);
								}
								return token;
							})
						)
				);

				updateDebouncer.endRender();
			})();
		}
	});

	onMount(async () => {
		// todo: fix worker, seems to be transmitting a lot of data
		// worker = browser && window.Worker ? new MarkdownWorker() : null;

		// Dynamically import DOMPurify only on the client
		const { default: purify } = await import("isomorphic-dompurify");
		DOMPurify = purify;

		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} loading={loading && !token.isClosed} />
	{/if}
{/each}