ai / frontend /src /lib /components /chat /Messages /Markdown /MarkdownInlineTokens.svelte
fdrah
initial-commit
9e93b10
Raw
History Blame Contribute Delete
4.5 kB
<script lang="ts">
import DOMPurify from "dompurify";
import { toast } from "svelte-sonner";
import type { Token } from "marked";
import { getContext } from "svelte";
import { goto } from "$app/navigation";
const i18n = getContext("i18n");
import { REXPRO_BASE_URL } from "$lib/constants";
import { copyToClipboard, unescapeHtml } from "$lib/utils";
import Image from "$lib/components/common/Image.svelte";
import KatexRenderer from "./KatexRenderer.svelte";
import Source from "./Source.svelte";
import HtmlToken from "./HTMLToken.svelte";
import TextToken from "./MarkdownInlineTokens/TextToken.svelte";
import CodespanToken from "./MarkdownInlineTokens/CodespanToken.svelte";
import MentionToken from "./MarkdownInlineTokens/MentionToken.svelte";
import NoteLinkToken from "./MarkdownInlineTokens/NoteLinkToken.svelte";
import SourceToken from "./SourceToken.svelte";
export let id: string;
export let done = true;
export let tokens: Token[];
export let sourceIds = [];
export let onSourceClick: Function = () => {};
/**
* Check if a URL is a same-origin note link and return the note ID if so.
*/
const getNoteIdFromHref = (href: string): string | null => {
try {
const url = new URL(href, window.location.origin);
if (url.origin === window.location.origin) {
const match = url.pathname.match(/^\/notes\/([^/]+)$/);
if (match) {
return match[1];
}
}
} catch {
// Invalid URL
}
return null;
};
/**
* Handle link clicks - intercept same-origin app URLs for in-app navigation
*/
const handleLinkClick = (e: MouseEvent, href: string) => {
try {
const url = new URL(href, window.location.origin);
// Check if same origin and an in-app route
if (
url.origin === window.location.origin &&
(url.pathname.startsWith("/notes/") ||
url.pathname.startsWith("/c/") ||
url.pathname.startsWith("/channels/"))
) {
e.preventDefault();
goto(url.pathname + url.search + url.hash);
}
} catch {
// Invalid URL, let browser handle it
}
};
</script>
{#each tokens as token, tokenIdx (tokenIdx)}
{#if token.type === "escape"}
{unescapeHtml(token.text)}
{:else if token.type === "html"}
<HtmlToken {id} {token} {onSourceClick} />
{:else if token.type === "link"}
{@const noteId = getNoteIdFromHref(token.href)}
{#if noteId}
<NoteLinkToken {noteId} href={token.href} />
{:else if token.tokens}
<a
href={token.href}
target="_blank"
rel="nofollow"
title={token.title}
on:click={(e) => handleLinkClick(e, token.href)}
>
<svelte:self
id={`${id}-a`}
tokens={token.tokens}
{onSourceClick}
{done}
/>
</a>
{:else}
<a
href={token.href}
target="_blank"
rel="nofollow"
title={token.title}
on:click={(e) => handleLinkClick(e, token.href)}>{token.text}</a
>
{/if}
{:else if token.type === "image"}
<Image src={token.href} alt={token.text} />
{:else if token.type === "strong"}
<strong
><svelte:self
id={`${id}-strong`}
tokens={token.tokens}
{onSourceClick}
/></strong
>
{:else if token.type === "em"}
<em
><svelte:self
id={`${id}-em`}
tokens={token.tokens}
{onSourceClick}
/></em
>
{:else if token.type === "codespan"}
<CodespanToken {token} {done} />
{:else if token.type === "br"}
<br />
{:else if token.type === "del"}
<del
><svelte:self
id={`${id}-del`}
tokens={token.tokens}
{onSourceClick}
/></del
>
{:else if token.type === "inlineKatex"}
{#if token.text}
<KatexRenderer
content={token.text}
displayMode={token?.displayMode ?? false}
/>
{/if}
{:else if token.type === "iframe"}
<iframe
src="{REXPRO_BASE_URL}/api/v1/files/{token.fileId}/content"
title={token.fileId}
width="100%"
frameborder="0"
on:load={(e) => {
try {
e.currentTarget.style.height =
e.currentTarget.contentWindow.document.body
.scrollHeight +
20 +
"px";
} catch {}
}}
></iframe>
{:else if token.type === "mention"}
<MentionToken {token} />
{:else if token.type === "footnote"}
{@html DOMPurify.sanitize(
`<sup class="footnote-ref footnote-ref-text">${token.escapedText}</sup>`,
) || ""}
{:else if token.type === "citation"}
{#if (sourceIds ?? []).length > 0}
<SourceToken {id} {token} {sourceIds} onClick={onSourceClick} />
{:else}
<TextToken {token} {done} />
{/if}
{:else if token.type === "text"}
<TextToken {token} {done} />
{/if}
{/each}