File size: 3,539 Bytes
1bcd186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<script lang="ts">
	import type { WebSearchSource } from "$lib/types/WebSearch";
	import katex from "katex";
	import DOMPurify from "isomorphic-dompurify";
	import { marked, type MarkedOptions } from "marked";
	import CodeBlock from "../CodeBlock.svelte";

	export let content: string;
	export let sources: WebSearchSource[] = [];

	function addInlineCitations(md: string, webSearchSources: WebSearchSource[] = []): string {
		const linkStyle =
			"color: rgb(59, 130, 246); text-decoration: none; hover:text-decoration: underline;";

		return md.replace(/\[(\d+)\]/g, (match: string) => {
			const indices: number[] = (match.match(/\d+/g) || []).map(Number);
			const links: string = indices
				.map((index: number) => {
					if (index === 0) return false;
					const source = webSearchSources[index - 1];
					if (source) {
						return `<a href="${source.link}" target="_blank" rel="noreferrer" style="${linkStyle}">${index}</a>`;
					}
					return "";
				})
				.filter(Boolean)
				.join(", ");

			return links ? ` <sup>${links}</sup>` : match;
		});
	}

	const renderer = new marked.Renderer();

	// For code blocks with simple backticks
	renderer.codespan = (code) => {
		// Unsanitize double-sanitized code
		return `<code>${code.replaceAll("&amp;", "&")}</code>`;
	};

	renderer.link = (href, title, text) => {
		return `<a href="${href?.replace(/>$/, "")}" target="_blank" rel="noreferrer">${text}</a>`;
	};

	const options: MarkedOptions = {
		gfm: true,
		// breaks: true,
		renderer,
	};

	$: tokens = marked.lexer(addInlineCitations(content, sources));

	function processLatex(parsed: string) {
		const delimiters = [
			{ left: "$$", right: "$$", display: true },
			{ left: "$", right: "$", display: false },
			{ left: "\\(", right: "\\)", display: false },
			{ left: "\\[", right: "\\]", display: true },
		];

		for (const { left, right, display } of delimiters) {
			// Escape special regex characters in the delimiters
			const escapedLeft = left.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
			const escapedRight = right.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

			// Create regex pattern that matches content between delimiters
			const pattern = new RegExp(`${escapedLeft}([^]*?)${escapedRight}`, "g");

			parsed = parsed.replace(pattern, (match, latex) => {
				try {
					// Remove the delimiters from the latex content
					const cleanLatex = latex.trim();
					const rendered = katex.renderToString(cleanLatex, { displayMode: display });

					// For display mode, wrap in centered paragraph
					if (display) {
						return `<p style="width:100%;text-align:center;">${rendered}</p>`;
					}
					return rendered;
				} catch (error) {
					console.error("KaTeX error:", error);
					return match; // Return original on error
				}
			});
		}
		return parsed;
	}

	DOMPurify.addHook("afterSanitizeAttributes", (node) => {
		if (node.tagName === "A") {
			node.setAttribute("rel", "noreferrer");
			node.setAttribute("target", "_blank");
		}
	});
</script>

<div
	class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
>
	{#each tokens as token}
		{#if token.type === "code"}
			<CodeBlock lang={token.lang} code={token.text} />
		{:else}
			{@const parsed = marked.parse(processLatex(token.raw), options)}
			{#await parsed then parsed}
				<!-- eslint-disable-next-line svelte/no-at-html-tags -->
				{@html DOMPurify.sanitize(parsed)}
			{/await}
		{/if}
	{/each}
</div>