Spaces:
Running
Running
File size: 3,816 Bytes
1bcd186 ef367c0 1bcd186 ef367c0 b157e67 ef367c0 1bcd186 ef367c0 1bcd186 ef367c0 1bcd186 ef367c0 1bcd186 252913a |
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 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
<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("&", "&")}</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,
};
function escapeHTML(content: string) {
return content.replace(
/[<>&\n]/g,
(x) =>
({
"<": "<",
">": ">",
"&": "&",
}[x] || x)
);
}
$: 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(escapeHTML(token.raw)), options)}
{#await parsed then parsed}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html DOMPurify.sanitize(parsed)}
{/await}
{/if}
{/each}
</div>
<style lang="postcss">
:global(.katex-display) {
overflow: auto hidden;
}
</style>
|