MarkdownTokens.svelte 3.42 KB
Newer Older
Timothy J. Baek's avatar
Timothy J. Baek committed
1
<script lang="ts">
Timothy J. Baek's avatar
Timothy J. Baek committed
2
	import { marked } from 'marked';
Timothy J. Baek's avatar
Timothy J. Baek committed
3
	import type { Token } from 'marked';
Timothy J. Baek's avatar
Timothy J. Baek committed
4
	import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
5

Timothy J. Baek's avatar
Timothy J. Baek committed
6
7
8
9
	import { onMount, getContext, getAllContexts } from 'svelte';

	const context = getAllContexts();
	const i18n = getContext('i18n');
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
10
11
12
13

	import Image from '$lib/components/common/Image.svelte';
	import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';

Timothy J. Baek's avatar
Timothy J. Baek committed
14
15
16
17
18
19
	import MarkdownInlineTokens from '$lib/components/chat/Messages/MarkdownInlineTokens.svelte';

	export let id: string;
	export let tokens: Token[];
	export let top = true;

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
20
21
	let containerElement;

Timothy J. Baek's avatar
Timothy J. Baek committed
22
23
24
25
	const headerComponent = (depth: number) => {
		return 'h' + depth;
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
26
27
28
29
30
31
	const renderer = new marked.Renderer();
	// For code blocks with simple backticks
	renderer.codespan = (code) => {
		return `<code class="codespan">${code.replaceAll('&amp;', '&')}</code>`;
	};

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
32
	let codes = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
33
	renderer.code = (code, lang) => {
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
34
35
36
37
38
39
40
41
		codes.push({
			code: code,
			lang: lang
		});
		codes = codes;
		const codeId = `${id}-${codes.length}`;

		const interval = setInterval(() => {
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
42
43
			const codeElement = document.getElementById(`code-${codeId}`);
			if (codeElement) {
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
44
				clearInterval(interval);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
45
46
47
48
				// If the code is already loaded, don't load it again
				if (codeElement.innerHTML) {
					return;
				}
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
49

Timothy J. Baek's avatar
Timothy J. Baek committed
50
				const element = new CodeBlock({
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
51
					target: codeElement,
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
52
53
54
55
56
57
					props: {
						id: `${id}-${codes.length}`,
						lang: lang,
						code: revertSanitizedResponseContent(code)
					},
					hydrate: true,
Timothy J. Baek's avatar
Timothy J. Baek committed
58
59
					$$inline: true,
					context: context
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
60
61
62
63
				});
			}
		}, 10);

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
64
		return `<div id="code-${id}-${codes.length}"></div>`;
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
65
66
67
68
69
70
71
72
73
74
75
76
77
	};

	let images = [];
	renderer.image = (href, title, text) => {
		images.push({
			href: href,
			title: title,
			text: text
		});
		images = images;

		const imageId = `${id}-${images.length}`;
		const interval = setInterval(() => {
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
78
79
			const imageElement = document.getElementById(`image-${imageId}`);
			if (imageElement) {
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
80
				clearInterval(interval);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
81
82
83
84
85
86
87

				// If the image is already loaded, don't load it again
				if (imageElement.innerHTML) {
					return;
				}

				console.log('image', href, text);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
88
				new Image({
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
89
					target: imageElement,
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
90
91
92
93
					props: {
						src: href,
						alt: text
					},
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
94
					$$inline: true
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
95
96
97
98
				});
			}
		}, 10);

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
99
		return `<div id="image-${id}-${images.length}"></div>`;
Timothy J. Baek's avatar
Timothy J. Baek committed
100
101
102
103
104
105
106
107
	};

	// Open all links in a new tab/window (from https://github.com/markedjs/marked/issues/655#issuecomment-383226346)
	const origLinkRenderer = renderer.link;
	renderer.link = (href, title, text) => {
		const html = origLinkRenderer.call(renderer, href, title, text);
		return html.replace(/^<a /, '<a target="_blank" rel="nofollow" ');
	};
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
108

Timothy J. Baek's avatar
Timothy J. Baek committed
109
110
111
112
	const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		extensions: any;
	};
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
113
114
115
116
117

	$: if (tokens) {
		images = [];
		codes = [];
	}
Timothy J. Baek's avatar
Timothy J. Baek committed
118
119
</script>

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
<div bind:this={containerElement} class="flex flex-col">
	{#each tokens as token, tokenIdx (`${id}-${tokenIdx}`)}
		{#if token.type === 'code'}
			{#if token.lang === 'mermaid'}
				<pre class="mermaid">{revertSanitizedResponseContent(token.text)}</pre>
			{:else}
				<CodeBlock
					id={`${id}-${tokenIdx}`}
					lang={token?.lang ?? ''}
					code={revertSanitizedResponseContent(token?.text ?? '')}
				/>
			{/if}
		{:else}
			{@html marked.parse(token.raw, {
				...defaults,
				gfm: true,
				breaks: true,
				renderer
			})}
		{/if}
	{/each}
</div>