Spaces:
Runtime error
Runtime error
| <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"} | |
| { 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"} | |
| { 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} | |