Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252<script lang="ts"> import { untrack } from "svelte"; import type { SelectedLineRange } from "@pierre/diffs"; import { goto } from "$app/navigation"; import { page } from "$app/state"; import type { ResolvedPathname } from "$app/types"; import { hasTextView, type BlobKind, type RepoBlobView } from "#lib/api/blob.ts"; import BlobHeader from "#lib/components/repo/BlobHeader.svelte"; import { languageForPath } from "#lib/components/repo/languageColors.ts"; import LastCommitPanel from "#lib/components/repo/LastCommitPanel.svelte"; import PierreFile from "#lib/components/repo/PierreFile.svelte"; import { lineAnchor, parseLineAnchor } from "#lib/components/repo/lineAnchor.ts"; import { baseName, fileHistoryHref } from "#lib/components/repo/urls.ts"; import { createBlame, type BlameSource } from "#lib/components/repo/blame/request.svelte.ts"; import Separator from "#lib/components/ui/Separator.svelte"; import Card from "#lib/components/ui/Card.svelte"; import { mermaidDiagrams } from "#lib/components/ui/mermaidDiagrams.ts"; import { safeHref } from "#lib/safeHref.ts";
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; };
let selectedLines = $derived(parseLineAnchor(page.url.hash)); let landed: string | undefined; const landing = (): string => `${blob.path}${lineAnchor(selectedLines)}`;
const selectLines = (range: SelectedLineRange | null): void => { selectedLines = range; landed = landing(); // eslint-disable-next-line svelte/no-navigation-without-resolve void goto(`${location.pathname}${location.search}${lineAnchor(range)}`, { shallow: true, replace: true, state: page.state }); };
const scrollToSelection = (node: HTMLElement): void => { if (!selectedLines || landed === landing()) return; landed = landing(); node.shadowRoot ?.querySelector(`[data-line="${selectedLines.start}"]`) ?.scrollIntoView({ block: "center" }); };
// rAF races pierre's async shiki pass, the measurement happens in // pierre's own post-render hook const onPostRender = (node: HTMLElement) => { measure(node); scrollToSelection(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} {@const submoduleHref = safeHref(blob.submodule.url)} <p class="text-center text-foreground-muted"> This directory is a git submodule of {#if submoduleHref} <a href={submoduleHref} rel="external ugc nofollow noopener noreferrer" class="text-foreground-default no-underline hover:underline" >{blob.submodule.url}</a >. {:else} <span class="text-foreground-default">{blob.submodule.url}</span>. {/if} </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-md 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-md border border-border-default" > <source src={rawHref} /> </video> </div> {:else if blob.kind === "markup" && view === "rendered" && blob.renderedHtml !== null} <div data-testid="renderedMarkdownBody" class="markup overflow-x-auto" use:mermaidDiagrams={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} {onPostRender} blame={blame ? blameRequest.runs : undefined} {selectedLines} onLineSelected={selectLines} /> </div> {:else} <p class="py-2xl text-center text-foreground-muted"> Previews are not supported for this file type. </p> {/if}</Card>