Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106<script lang="ts"> import { hydratable, untrack } from "svelte"; import { browser } from "$app/env"; import { codeblockToolbars } from "#lib/components/ui/codeblockToolbars.ts"; import { mermaidDiagrams } from "#lib/components/ui/mermaidDiagrams.ts"; import { renderPreview, type MarkupContext } from "#lib/markup/index.ts"; import { getSsrBudget, PLAIN_PREVIEW_CHARS, renderOnServer } from "#lib/ssr/render.ts";
interface Props { source: string; ctx: MarkupContext; document?: boolean; tag?: "article" | "div"; class?: string; plainClass?: string; [attribute: `data-${string}`]: string | undefined; }
let { source, ctx, document = false, tag = "div", class: className, plainClass, ...attributes }: Props = $props();
const id = $props.id(); const SERVER_RENDERED = "<!-- server rendered -->"; const budget = getSsrBudget(); const text = $derived(source); let last: MarkupContext | undefined; const options = $derived.by(() => { const next = { ...ctx, collapseCode: !document }; const keys = Object.keys({ ...last, ...next }) as (keyof MarkupContext)[]; if (last && keys.every((key) => last?.[key] === next[key])) return last; return (last = next); });
// hydration keeps the server's dom nodes regardless of {@html}'s value, which lets page data carry only a boolean let serverHtml: string | null = null; const inPage = await untrack(() => text ? hydratable(`markup:${id}`, async () => { if (!budget) return false; serverHtml = await renderOnServer(budget, text.length, () => ({ kind: "markup", source: text, ctx: options })); return serverHtml !== null; }) : false ); const initial = browser ? (inPage ? SERVER_RENDERED : null) : serverHtml; let rendered = $state.raw(untrack(() => ({ source: text, ctx: options, html: initial }))); const shown = $derived( rendered.source === text && rendered.ctx === options ? rendered.html : null );
const preview = $derived.by(() => { if (text.length <= PLAIN_PREVIEW_CHARS) return text; const cut = text.slice(0, PLAIN_PREVIEW_CHARS); return `${/[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut}…`; });
let plain: HTMLElement | undefined = $state(); $effect(() => { if (shown !== null || !text || !plain) return; const from = text; const at = options; const observer = new IntersectionObserver( (entries) => { if (!entries.some((entry) => entry.isIntersecting)) return; observer.disconnect(); renderPreview(from, at).then( (html) => (rendered = { source: from, ctx: at, html }), () => undefined ); }, { rootMargin: "1000px 0px" } ); observer.observe(plain); return () => observer.disconnect(); });</script>
{#if shown} <svelte:element this={tag} class={className} {...attributes} use:codeblockToolbars={shown} use:mermaidDiagrams={shown} > <!-- svelte-ignore hydration_html_changed --> <!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in $lib/markup --> {@html shown} </svelte:element>{:else if text} <svelte:element this={tag} bind:this={plain} class={plainClass} {...attributes} >{preview}</svelte:element >{/if}