This commit is contained in:
Timothy Jaeryang Baek 2026-10-16 14:34:35 +04:00
parent 92e64c1d59
commit 7c3980338b
2 changed files with 56 additions and 4 deletions

View file

@ -16,7 +16,9 @@
</script>
<script lang="ts">
import { setContext, onDestroy } from 'svelte';
import { getAllContexts, mount, unmount, setContext, onDestroy } from 'svelte';
import DOMPurify from 'dompurify';
import { v4 as uuidv4 } from 'uuid';
import { downloadFileBlob } from '$lib/apis/terminal';
import MarkdownTokens from '../Messages/Markdown/MarkdownTokens.svelte';
import type { Token } from 'marked';
@ -98,8 +100,48 @@
allowRemoteContent: true,
lexer: (source: string) => documentMarkdown.lexer(source),
parseInline: (tokens: Token[]) =>
documentMarkdown.Parser.parseInline(tokens, documentMarkdown.defaults)
documentMarkdown.Parser.parseInline(tokens, documentMarkdown.defaults),
renderHtml
});
const context = getAllContexts();
function renderHtml(node: HTMLElement, props: { id: string; tokens: Token[]; top: boolean }) {
let components: ReturnType<typeof mount>[] = [];
const destroy = () => {
components.forEach((component) => unmount(component));
components = [];
};
const update = ({ id, tokens, top }: typeof props) => {
destroy();
const prefix = `markdown-${uuidv4()}`;
// Parse HTML together; keep Markdown components in slots the document cannot forge.
node.replaceChildren(
DOMPurify.sanitize(
tokens
.map((token, index) =>
token.type === 'html'
? token.text
: `<div id="${prefix}-${index}" style="display: contents"></div>`
)
.join(''),
{ RETURN_DOM_FRAGMENT: true }
)
);
tokens.forEach((token, index) => {
const target = node.querySelector(`#${prefix}-${index}`);
if (target) {
components.push(
mount(MarkdownTokens, {
target,
context,
props: { id: `${id}-${index}`, tokens: [token], top, editCodeBlock: false }
})
);
}
});
};
update(props);
return { update, destroy };
}
$: tokens = documentMarkdown.lexer(content);
const resolveHtmlImages = (node: HTMLElement) => {
let alive = true;

View file

@ -1,9 +1,15 @@
<script lang="ts">
import { decode } from 'html-entities';
import { onMount, getContext } from 'svelte';
import type { Action } from 'svelte/action';
const i18n = getContext('i18n');
const markdown = getContext<
{ document?: boolean; lexer?: (source: string) => Token[] } | undefined
| {
document?: boolean;
lexer?: (source: string) => Token[];
renderHtml?: Action<HTMLElement, { id: string; tokens: Token[]; top: boolean }>;
}
| undefined
>('markdown');
import fileSaver from 'file-saver';
@ -135,6 +141,7 @@
} text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition`;
$: displayTokens = getDisplayTokens(tokens);
$: renderHtml = tokens.some((token) => token.type === 'html') ? markdown?.renderHtml : undefined;
$: singlePlainBlock =
displayTokens.length === 1 &&
(displayTokens[0]?.type === 'paragraph' || displayTokens[0]?.type === 'text');
@ -173,7 +180,10 @@
</script>
<!-- {JSON.stringify(tokens)} -->
{#each displayTokens as token, tokenIdx (tokenIdx)}
{#if renderHtml}
<div class="contents" use:renderHtml={{ id, tokens, top }}></div>
{/if}
{#each renderHtml ? [] : displayTokens as token, tokenIdx (tokenIdx)}
{#if token.type === 'hr'}
<hr class="border-gray-50 dark:border-gray-850/30" />
{:else if token.type === 'heading'}