2024-08-05 15:47:18 +00:00
|
|
|
<script lang="ts">
|
2024-08-06 21:34:51 +00:00
|
|
|
import { marked } from 'marked';
|
2024-08-05 15:47:18 +00:00
|
|
|
import type { Token } from 'marked';
|
2024-08-06 09:43:47 +00:00
|
|
|
import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
|
2024-08-06 23:55:37 +00:00
|
|
|
|
2024-08-06 09:43:47 +00:00
|
|
|
import { onMount } from 'svelte';
|
2024-08-06 23:55:37 +00:00
|
|
|
|
|
|
|
import Image from '$lib/components/common/Image.svelte';
|
|
|
|
import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
|
|
|
|
|
2024-08-05 15:47:18 +00:00
|
|
|
import MarkdownInlineTokens from '$lib/components/chat/Messages/MarkdownInlineTokens.svelte';
|
|
|
|
|
|
|
|
export let id: string;
|
|
|
|
export let tokens: Token[];
|
|
|
|
export let top = true;
|
|
|
|
|
2024-08-06 23:55:37 +00:00
|
|
|
let containerElement;
|
|
|
|
|
2024-08-05 15:47:18 +00:00
|
|
|
const headerComponent = (depth: number) => {
|
|
|
|
return 'h' + depth;
|
|
|
|
};
|
|
|
|
|
2024-08-06 21:34:51 +00:00
|
|
|
const renderer = new marked.Renderer();
|
|
|
|
// For code blocks with simple backticks
|
|
|
|
renderer.codespan = (code) => {
|
|
|
|
return `<code class="codespan">${code.replaceAll('&', '&')}</code>`;
|
|
|
|
};
|
|
|
|
|
2024-08-06 23:55:37 +00:00
|
|
|
let codes = [];
|
2024-08-06 21:34:51 +00:00
|
|
|
renderer.code = (code, lang) => {
|
2024-08-06 23:55:37 +00:00
|
|
|
codes.push({
|
|
|
|
code: code,
|
|
|
|
lang: lang
|
|
|
|
});
|
|
|
|
codes = codes;
|
|
|
|
const codeId = `${id}-${codes.length}`;
|
|
|
|
|
|
|
|
const interval = setInterval(() => {
|
2024-08-07 00:11:37 +00:00
|
|
|
const codeElement = document.getElementById(`code-${codeId}`);
|
|
|
|
if (codeElement) {
|
2024-08-06 23:55:37 +00:00
|
|
|
clearInterval(interval);
|
2024-08-07 00:18:29 +00:00
|
|
|
// If the code is already loaded, don't load it again
|
|
|
|
if (codeElement.innerHTML) {
|
|
|
|
return;
|
|
|
|
}
|
2024-08-06 23:55:37 +00:00
|
|
|
|
|
|
|
new CodeBlock({
|
2024-08-07 00:11:37 +00:00
|
|
|
target: codeElement,
|
2024-08-06 23:55:37 +00:00
|
|
|
props: {
|
|
|
|
id: `${id}-${codes.length}`,
|
|
|
|
lang: lang,
|
|
|
|
code: revertSanitizedResponseContent(code)
|
|
|
|
},
|
|
|
|
hydrate: true,
|
|
|
|
$$inline: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, 10);
|
|
|
|
|
2024-08-07 00:11:37 +00:00
|
|
|
return `<div id="code-${id}-${codes.length}"></div>`;
|
2024-08-06 23:55:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
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(() => {
|
2024-08-07 00:11:37 +00:00
|
|
|
const imageElement = document.getElementById(`image-${imageId}`);
|
|
|
|
if (imageElement) {
|
2024-08-06 23:55:37 +00:00
|
|
|
clearInterval(interval);
|
2024-08-07 00:11:37 +00:00
|
|
|
|
|
|
|
// If the image is already loaded, don't load it again
|
|
|
|
if (imageElement.innerHTML) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('image', href, text);
|
2024-08-06 23:55:37 +00:00
|
|
|
new Image({
|
2024-08-07 00:11:37 +00:00
|
|
|
target: imageElement,
|
2024-08-06 23:55:37 +00:00
|
|
|
props: {
|
|
|
|
src: href,
|
|
|
|
alt: text
|
|
|
|
},
|
2024-08-07 00:11:37 +00:00
|
|
|
$$inline: true
|
2024-08-06 23:55:37 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}, 10);
|
|
|
|
|
2024-08-07 00:11:37 +00:00
|
|
|
return `<div id="image-${id}-${images.length}"></div>`;
|
2024-08-06 21:34:51 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// 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" ');
|
|
|
|
};
|
2024-08-06 23:55:37 +00:00
|
|
|
|
2024-08-06 21:34:51 +00:00
|
|
|
const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
extensions: any;
|
|
|
|
};
|
2024-08-06 23:55:37 +00:00
|
|
|
|
|
|
|
$: if (tokens) {
|
|
|
|
images = [];
|
|
|
|
codes = [];
|
|
|
|
}
|
2024-08-05 15:47:18 +00:00
|
|
|
</script>
|
|
|
|
|
2024-08-06 23:55:37 +00:00
|
|
|
<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>
|