123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- <script lang="ts">
- import { marked } from 'marked';
- import type { Token } from 'marked';
- import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
- import { onMount } from 'svelte';
- 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;
- let containerElement;
- const headerComponent = (depth: number) => {
- return 'h' + depth;
- };
- const renderer = new marked.Renderer();
- // For code blocks with simple backticks
- renderer.codespan = (code) => {
- return `<code class="codespan">${code.replaceAll('&', '&')}</code>`;
- };
- let codes = [];
- renderer.code = (code, lang) => {
- 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}" />`;
- };
- // 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" ');
- };
- const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- extensions: any;
- };
- $: if (tokens) {
- images = [];
- codes = [];
- }
- </script>
- <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>
- {:else if token.type === 'hr'}
- <hr />
- {:else if token.type === 'blockquote'}
- <blockquote>
- <svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} />
- </blockquote>
- {: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}
- {: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'} -->
- <!-- {#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)}
- {/if} -->
- {:else}
- {@html marked.parse(token.raw, {
- ...defaults,
- gfm: true,
- breaks: true,
- renderer
- })}
- {/if}
- {/each}
- </div>
|