This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / components / repo / BlobView.svelte
5.1 kB 157 lines
1<script lang="ts"> 2 import { untrack } from "svelte"; 3 import { hasTextView, type BlobKind, type RepoBlobView } from "$lib/api/blob"; 4 import BlobHeader from "./BlobHeader.svelte"; 5 import { languageForPath } from "./language-colors"; 6 import LastCommitPanel from "./LastCommitPanel.svelte"; 7 import PierreFile from "./PierreFile.svelte"; 8 import { baseName, rawBlobHref } from "./urls"; 9 import TabPanel from "$lib/components/ui/TabPanel.svelte"; 10 11 interface Props { 12 ownerHandle: string; 13 repoName: string; 14 blob: RepoBlobView; 15 // server-prerendered shadow dom for the code view, only for the initial ssr 16 prerenderedHTML?: string; 17 // enry language name override, defaults to the extension guess 18 language?: string | null; 19 } 20 21 let { ownerHandle, repoName, blob, prerenderedHTML, language }: Props = $props(); 22 23 const rawHref = $derived(rawBlobHref(ownerHandle, repoName, blob.ref, blob.path)); 24 const fileName = $derived(baseName(blob.path)); 25 // the xrpc doesn't name a language, fall back to the extension map 26 const languageName = $derived(language ?? languageForPath(blob.path)); 27 28 // view is seeded from the load and reseeds only when the kind category 29 // changes, a "rendered" view must not leak onto a plain code file 30 type ViewCategory = "code" | "markup" | "media" | "other"; 31 const categoryOf = (kind: BlobKind): ViewCategory => { 32 switch (kind) { 33 case "code": 34 return "code"; 35 case "markup": 36 return "markup"; 37 case "image": 38 case "svg": 39 case "video": 40 return "media"; 41 default: 42 return "other"; 43 } 44 }; 45 46 let view = $state(untrack(() => blob.defaultView)); 47 let lastCategory = untrack(() => categoryOf(blob.kind)); 48 49 $effect(() => { 50 const category = categoryOf(blob.kind); 51 const defaultView = blob.defaultView; 52 untrack(() => { 53 if (category === lastCategory) return; 54 lastCategory = category; 55 view = defaultView; 56 }); 57 }); 58 59 let wrap = $state(false); 60 61 const textViewActive = $derived(hasTextView(blob.kind) && view === "code" && !blob.fileTooLarge); 62 63 let overflows = $state(false); 64 65 // pierre renders into a shadow root, measure its scroller element 66 // directly 67 const measure = (host: HTMLElement | undefined) => { 68 const scroller = host?.shadowRoot?.querySelector("[data-code]"); 69 overflows = scroller ? scroller.scrollWidth > scroller.clientWidth : false; 70 }; 71 72 // rAF races pierre's async shiki pass, the measurement happens in 73 // pierre's own post-render hook 74 const onPostRender = (node: HTMLElement) => measure(node); 75 76 // sveltekit reuses this component across [...path] navigations, drop the 77 // previous file's measurement before pierre reports the new one 78 $effect(() => { 79 void blob.path; 80 void blob.contents; 81 overflows = false; 82 }); 83 84 let codeHost: HTMLElement | undefined = $state(); 85 86 $effect(() => { 87 const host = codeHost; 88 if (!host || wrap) return; 89 const onResize = () => measure(host.querySelector("diffs-container") ?? undefined); 90 window.addEventListener("resize", onResize); 91 return () => window.removeEventListener("resize", onResize); 92 }); 93 94 const showWrap = $derived(textViewActive && (overflows || wrap)); 95</script> 96 97<TabPanel padded={false} class="relative mx-auto w-full p-4"> 98 <BlobHeader 99 {ownerHandle} 100 {repoName} 101 ref={blob.ref} 102 path={blob.path} 103 kind={blob.kind} 104 language={languageName} 105 sizeLabel={blob.sizeLabel} 106 lines={blob.lines} 107 bind:view 108 bind:wrap 109 {showWrap} 110 copyText={blob.contents} 111 /> 112 113 {#if blob.lastCommit} 114 <LastCommitPanel {ownerHandle} {repoName} commit={blob.lastCommit} /> 115 {/if} 116 117 {#if blob.kind === "submodule" && blob.submodule} 118 <p class="text-center text-foreground-muted"> 119 This directory is a git submodule of 120 <a 121 href={blob.submodule.url} 122 rel="external ugc nofollow noopener noreferrer" 123 class="text-foreground-default no-underline hover:underline">{blob.submodule.url}</a 124 >. 125 </p> 126 {:else if blob.fileTooLarge} 127 <p class="py-8 text-center text-foreground-muted"> 128 This file is too large to render. 129 <a href={rawHref} class="text-foreground-default no-underline hover:underline">View raw.</a> 130 </p> 131 {:else if blob.kind === "image" || (blob.kind === "svg" && view === "rendered")} 132 <div class="text-center"> 133 <img 134 src={rawHref} 135 alt={blob.path} 136 class="mx-auto h-auto max-w-full rounded border border-border-default" 137 /> 138 </div> 139 {:else if blob.kind === "video"} 140 <div class="text-center"> 141 <video controls class="mx-auto h-auto max-w-full rounded border border-border-default"> 142 <source src={rawHref} /> 143 </video> 144 </div> 145 {:else if blob.kind === "markup" && view === "rendered" && blob.renderedHtml !== null} 146 <!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in $lib/markup --> 147 <div class="markup overflow-x-auto">{@html blob.renderedHtml}</div> 148 {:else if blob.contents !== null} 149 <div bind:this={codeHost}> 150 <PierreFile name={fileName} contents={blob.contents} {wrap} {prerenderedHTML} {onPostRender} /> 151 </div> 152 {:else} 153 <p class="py-8 text-center text-foreground-muted"> 154 Previews are not supported for this file type. 155 </p> 156 {/if} 157</TabPanel>