open-webui/src/lib/components/chat/Messages/MarkdownTokens.svelte

211 lines
5.2 KiB
Svelte
Raw Normal View History

<script lang="ts">
2024-08-06 21:34:51 +00:00
import { marked } from 'marked';
import type { Token } from 'marked';
import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
2024-08-06 23:55:37 +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';
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;
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('&amp;', '&')}</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(() => {
if (document.getElementById(`code-${codeId}`)) {
clearInterval(interval);
new CodeBlock({
target: document.getElementById(`code-${codeId}`),
props: {
id: `${id}-${codes.length}`,
lang: lang,
code: revertSanitizedResponseContent(code)
},
hydrate: true,
$$inline: true
});
}
}, 10);
return `<div id="code-${id}-${codes.length}" />`;
};
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(() => {
if (document.getElementById(`image-${imageId}`)) {
clearInterval(interval);
new Image({
target: document.getElementById(`image-${imageId}`),
props: {
src: href,
alt: text
},
hydrate: true
});
}
}, 10);
return `<div id="image-${id}-${images.length}" />`;
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 = [];
}
</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 if token.type === 'heading'}
<svelte:element this={headerComponent(token.depth)}>
<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} />
</svelte:element>
2024-08-06 21:34:51 +00:00
{:else if token.type === 'hr'}
<hr />
{:else if token.type === 'blockquote'}
<blockquote>
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} />
</blockquote>
2024-08-06 21:34:51 +00:00
{:else if token.type === 'html'}
{@html token.text}
{:else if token.type === 'paragraph'}
<p>
<MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
</p>
{:else if token.type === 'list'}
{#if token.ordered}
<ol start={token.start || 1}>
{#each token.items as item, itemIdx}
<li>
<svelte:self
id={`${id}-${tokenIdx}-${itemIdx}`}
tokens={item.tokens}
top={token.loose}
/>
</li>
{/each}
</ol>
{:else}
<ul>
{#each token.items as item, itemIdx}
<li>
<svelte:self
id={`${id}-${tokenIdx}-${itemIdx}`}
tokens={item.tokens}
top={token.loose}
/>
</li>
{/each}
</ul>
{/if}
2024-08-06 21:34:51 +00:00
{:else if token.type === 'table'}
<table>
<thead>
<tr>
{#each token.header as header, headerIdx}
<th style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}>
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-header-${headerIdx}`}
tokens={header.tokens}
/>
</th>
{/each}
</tr>
</thead>
<tbody>
{#each token.rows as row, rowIdx}
<tr>
{#each row ?? [] as cell, cellIdx}
<td style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}>
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
tokens={cell.tokens}
/>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
{:else if token.type === 'text'} -->
2024-08-06 23:55:37 +00:00
<!-- {#if top}
<p>
{#if token.tokens}
<MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} />
{:else}
{unescapeHtml(token.text)}
{/if}
</p>
{:else if token.tokens}
<MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
{:else}
{unescapeHtml(token.text)}
2024-08-06 21:34:51 +00:00
{/if} -->
2024-08-06 23:55:37 +00:00
{:else}
{@html marked.parse(token.raw, {
...defaults,
gfm: true,
breaks: true,
renderer
})}
{/if}
{/each}
</div>