|
2 | 2 | import type { WebSearchSource } from "$lib/types/WebSearch";
|
3 | 3 | import katex from "katex";
|
4 | 4 | import DOMPurify from "isomorphic-dompurify";
|
5 |
| - import { marked, type MarkedOptions } from "marked"; |
| 5 | + import { Marked } from "marked"; |
6 | 6 | import CodeBlock from "../CodeBlock.svelte";
|
7 | 7 |
|
8 | 8 | export let content: string;
|
|
30 | 30 | });
|
31 | 31 | }
|
32 | 32 |
|
33 |
| - const renderer = new marked.Renderer(); |
34 |
| -
|
35 |
| - // For code blocks with simple backticks |
36 |
| - renderer.codespan = (code) => { |
37 |
| - // Unsanitize double-sanitized code |
38 |
| - return `<code>${code.replaceAll("&", "&")}</code>`; |
39 |
| - }; |
40 |
| -
|
41 |
| - renderer.link = (href, title, text) => { |
42 |
| - return `<a href="${href?.replace(/>$/, "")}" target="_blank" rel="noreferrer">${text}</a>`; |
43 |
| - }; |
44 |
| -
|
45 |
| - const options: MarkedOptions = { |
46 |
| - gfm: true, |
47 |
| - // breaks: true, |
48 |
| - renderer, |
49 |
| - }; |
50 |
| -
|
51 | 33 | function escapeHTML(content: string) {
|
52 | 34 | return content.replace(
|
53 | 35 | /[<>&\n]/g,
|
|
60 | 42 | );
|
61 | 43 | }
|
62 | 44 |
|
63 |
| - $: tokens = marked.lexer(addInlineCitations(content, sources)); |
64 |
| -
|
65 | 45 | function processLatex(parsed: string) {
|
66 | 46 | const delimiters = [
|
67 | 47 | { left: "$$", right: "$$", display: true },
|
|
98 | 78 | return parsed;
|
99 | 79 | }
|
100 | 80 |
|
| 81 | + const marked = new Marked({ |
| 82 | + hooks: { |
| 83 | + preprocess: (md) => addInlineCitations(escapeHTML(md), sources), |
| 84 | + postprocess: (html) => { |
| 85 | + return DOMPurify.sanitize(processLatex(html)); |
| 86 | + }, |
| 87 | + }, |
| 88 | + renderer: { |
| 89 | + codespan: (code) => `<code>${code.replaceAll("&", "&")}</code>`, |
| 90 | + link: (href, title, text) => |
| 91 | + `<a href="${href?.replace(/>$/, "")}" target="_blank" rel="noreferrer">${text}</a>`, |
| 92 | + }, |
| 93 | + gfm: true, |
| 94 | + }); |
| 95 | +
|
101 | 96 | DOMPurify.addHook("afterSanitizeAttributes", (node) => {
|
102 | 97 | if (node.tagName === "A") {
|
103 | 98 | node.setAttribute("rel", "noreferrer");
|
|
106 | 101 | });
|
107 | 102 | </script>
|
108 | 103 |
|
109 |
| -{#each tokens as token} |
110 |
| - {#if token.type === "code"} |
111 |
| - <CodeBlock lang={token.lang} code={token.text} /> |
112 |
| - {:else} |
113 |
| - {@const parsed = marked.parse(processLatex(escapeHTML(token.raw)), options)} |
114 |
| - {#await parsed then parsed} |
115 |
| - <!-- eslint-disable-next-line svelte/no-at-html-tags --> |
116 |
| - {@html DOMPurify.sanitize(parsed)} |
117 |
| - {/await} |
118 |
| - {/if} |
119 |
| -{/each} |
| 104 | +<div |
| 105 | + 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" |
| 106 | +> |
| 107 | + {#each marked.lexer(content) as token} |
| 108 | + {#if token.type === "code"} |
| 109 | + <CodeBlock lang={token.lang} code={token.text} /> |
| 110 | + {:else} |
| 111 | + {#await marked.parse(token.raw) then parsed} |
| 112 | + <!-- eslint-disable-next-line svelte/no-at-html-tags --> |
| 113 | + {@html parsed} |
| 114 | + {/await} |
| 115 | + {/if} |
| 116 | + {/each} |
| 117 | +</div> |
120 | 118 |
|
121 | 119 | <style lang="postcss">
|
122 | 120 | :global(.katex-display) {
|
|
0 commit comments