Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
Svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210<script lang="ts"> import { untrack } from "svelte"; import type { ResolvedPathname } from "$app/types"; import { hasTextView, type BlobKind, type RepoBlobView } from "$lib/api/blob"; import BlobHeader from "$lib/components/repo/BlobHeader.svelte"; import { languageForPath } from "$lib/components/repo/languageColors"; import LastCommitPanel from "$lib/components/repo/LastCommitPanel.svelte"; import { enhanceCodeblocks } from "$lib/markup/highlight"; import PierreFile from "$lib/components/repo/PierreFile.svelte"; import { baseName, fileHistoryHref } from "$lib/components/repo/urls"; import { createBlame, type BlameSource } from "$lib/components/repo/blame/request.svelte"; import Separator from "$lib/components/ui/Separator.svelte"; import Card from "$lib/components/ui/Card.svelte";
interface Props { ownerHandle: string; repoName?: string; rawHref: ResolvedPathname; blob: RepoBlobView; // enry language name override, defaults to the extension guess language?: string | null; timeLabel?: string | null; blameSource?: BlameSource; }
let { ownerHandle, repoName, rawHref, blob, language, timeLabel = null, blameSource }: Props = $props();
const historyHref = $derived( repoName ? fileHistoryHref(ownerHandle, repoName, blob.ref, blob.path) : null ); const fileName = $derived(baseName(blob.path)); // the xrpc doesn't name a language, fall back to the extension map const languageName = $derived(language ?? languageForPath(blob.path));
// view is seeded from the load and reseeds only when the kind category // changes, a "rendered" view must not leak onto a plain code file type ViewCategory = "code" | "markup" | "media" | "other"; const categoryOf = (kind: BlobKind): ViewCategory => { switch (kind) { case "code": return "code"; case "markup": return "markup"; case "image": case "svg": case "video": return "media"; default: return "other"; } };
let view = $state(untrack(() => blob.defaultView)); let lastCategory = untrack(() => categoryOf(blob.kind));
$effect(() => { const category = categoryOf(blob.kind); const defaultView = blob.defaultView; untrack(() => { if (category === lastCategory) return; lastCategory = category; view = defaultView; }); });
let wrap = $state(false); let blame = $state(false); // blame the commit these lines came from, not the branch name they were requested by: the // mirror can be a push behind the knot that served the file, and an immutable sha makes it // answer RefNotFound instead of blaming an older snapshot onto the rendered lines const blameRequest = createBlame({ enabled: () => blame, source: () => blameSource, ref: () => blob.lastCommit?.hash ?? blob.ref, path: () => blob.path });
const textViewActive = $derived( hasTextView(blob.kind) && view === "code" && !blob.fileTooLarge );
let overflows = $state(false);
// pierre renders into a shadow root, measure its scroller element // directly const measure = (host: HTMLElement | undefined) => { const scroller = host?.shadowRoot?.querySelector("[data-code]"); overflows = scroller ? scroller.scrollWidth > scroller.clientWidth : false; };
// rAF races pierre's async shiki pass, the measurement happens in // pierre's own post-render hook const onPostRender = (node: HTMLElement) => measure(node);
// sveltekit reuses this component across [...path] navigations, drop the // previous file's measurement before pierre reports the new one $effect(() => { void blob.path; void blob.contents; overflows = false; });
let codeHost: HTMLElement | undefined = $state();
$effect(() => { const host = codeHost; if (!host || wrap) return; const onResize = () => measure(host.querySelector("diffs-container") ?? undefined); window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); });
const showWrap = $derived(textViewActive && (overflows || wrap));</script>
<Card padding="none" class="relative mx-auto w-full p-md"> <BlobHeader path={blob.path} repo={repoName ? { ownerHandle, repoName, ref: blob.ref } : null} {rawHref} {historyHref} kind={blob.kind} language={languageName} sizeLabel={blob.sizeLabel} lines={blob.lines} {timeLabel} bind:view bind:wrap bind:blame {showWrap} showBlame={blameSource !== undefined} blameLoading={blameRequest.loading} blameError={blameRequest.error} copyText={blob.contents} />
{#if blameRequest.error} <p role="alert" class="px-md py-xs text-foreground-danger">{blameRequest.error}</p> {/if}
{#if blob.lastCommit && repoName} <LastCommitPanel {ownerHandle} {repoName} commit={blob.lastCommit} /> {:else} <Separator class="my-md" /> {/if}
{#if blob.kind === "submodule" && blob.submodule} <p class="text-center text-foreground-muted"> This directory is a git submodule of <a href={blob.submodule.url} rel="external ugc nofollow noopener noreferrer" class="text-foreground-default no-underline hover:underline">{blob.submodule.url}</a >. </p> {:else if blob.fileTooLarge} <p class="py-2xl text-center text-foreground-muted"> This file is too large to render. <a href={rawHref} class="text-foreground-default no-underline hover:underline" >View raw.</a > </p> {:else if blob.kind === "image" || (blob.kind === "svg" && view === "rendered")} <div class="text-center"> <img src={rawHref} alt={blob.path} class="mx-auto h-auto max-w-full rounded-sm border border-border-default" /> </div> {:else if blob.kind === "video"} <div class="text-center"> <video controls class="mx-auto h-auto max-w-full rounded-sm border border-border-default" > <source src={rawHref} /> </video> </div> {:else if blob.kind === "markup" && view === "rendered" && blob.renderedHtml !== null} <div class="markup overflow-x-auto" use:enhanceCodeblocks={blob.renderedHtml}> <!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in $lib/markup --> {@html blob.renderedHtml} </div> {:else if blob.contents !== null} <div bind:this={codeHost}> <PierreFile name={fileName} contents={blob.contents} {wrap} prerenderedHTML={blob.prerenderedHTML} {onPostRender} blame={blame ? blameRequest.runs : undefined} /> </div> {:else} <p class="py-2xl text-center text-foreground-muted"> Previews are not supported for this file type. </p> {/if}</Card>