repo.ts

4.6 KB · raw

1import type { Hono } from "hono";2import {3	fileAt,4	filesAt,5	headId,6	history,7	objectBytes,8	repoStats,9	timeline,10} from "../mirror/queries.ts";11import { setting } from "../server/db.ts";12import { type Html, html } from "./html.ts";13import {14	cacheFor,15	notFound,16	openPublicRepo,17	type PageContext,18	type PublicRepo,19	page,20	plural,21	relativeTime,22} from "./layout.ts";23import { firstParagraph, renderMarkdown } from "./markdown.ts";24import { timelineSvg } from "./timeline.ts";2526const PAGE_SIZE = 50;27const HTTP_URL = /^https?:\/\//i;2829export function registerRepoPage(app: Hono, ctx: PageContext): void {30	app.get("/:repo", (c) => {31		const repo = openPublicRepo(ctx, c.req.param("repo"));32		if (!repo) return notFound(c, ctx);33		const pageNumber = Math.max(1, Number.parseInt(c.req.query("page") ?? "1", 10) || 1);34		const readme = readmeAtHead(repo);35		const name = repo.settings.name;36		const intro =37			readme === null ? null : firstParagraph(readme, { repo: name, dir: "", at: null });38		cacheFor(c, false);39		return c.html(40			page(ctx, {41				title: name,42				description: intro?.text ?? "",43				path: `/${name}`,44				body: html`${header(ctx, repo, intro?.html ?? null)}45${headId(repo.mirror) && sections(repo, pageNumber)}46${readme && details(renderMarkdown(readme, { repo: name, dir: "", at: null }))}`,47			}),48		);49	});50}5152function readmeAtHead(repo: PublicRepo): string | null {53	const head = headId(repo.mirror);54	const path =55		head && filesAt(repo.mirror, head).find((f) => f.path.toLowerCase() === "readme.md")?.path;56	const file = head && path ? fileAt(repo.mirror, head, path) : null;57	const bytes = file && objectBytes(repo.mirror, file.hash);58	return bytes ? new TextDecoder().decode(bytes) : null;59}6061function header(ctx: PageContext, repo: PublicRepo, intro: Html | null): Html {62	const { name, homepage, installLines } = repo.settings;63	const stats = repoStats(repo.mirror);64	const handle = setting(ctx.kepr, "handle");65	const meta = [66		plural(stats.saves, "save"),67		stats.lines > 0 && plural(stats.lines, "line"),68		stats.updated !== null && `updated ${relativeTime(stats.updated)}`,69	].filter(Boolean);70	return html`<header class="repo-head">71<h1 class="repo-name">${name}</h1>72${handle && html`<p class="repo-contributor mono"><a href="/@${handle}">@${handle}</a></p>`}73${intro && html`<div class="repo-desc"><p>${intro}</p></div>`}74${HTTP_URL.test(homepage) && html`<p class="repo-homepage"><a href="${homepage}">${homepage}</a></p>`}75${stats.language && html`<div class="lang-badges"><span class="lang-badge">${stats.language}</span></div>`}76<p class="repo-meta">${meta.join(" · ")}</p>77<div class="repo-tabbar">78<nav class="repo-tabs" aria-label="Repository panels"><a class="repo-tab" href="/${name}/issues">issues</a><a class="repo-tab" href="/${name}/releases">builds</a><a class="repo-tab" href="/${name}/archives">archives</a></nav>79${installLines.length > 0 && html`<div class="install-lines">${installLines.map((line) => html`<div class="install-line"><code>${line}</code><button type="button" class="copy-btn" data-copy="${line}" hidden>copy</button></div>`)}</div>`}80</div>81</header>`;82}8384function sections(repo: PublicRepo, pageNumber: number): Html {85	const name = repo.settings.name;86	const saves = timeline(repo.mirror);87	const rows = history(repo.mirror, PAGE_SIZE, (pageNumber - 1) * PAGE_SIZE);88	const count = plural(saves.length, "save");89	const older = pageNumber * PAGE_SIZE < saves.length;90	return html`<section class="repo-section">91<details class="repo-section-toggle" open>92<summary class="section-head"><span class="section-label">timeline</span><span class="section-count">${count}</span></summary>93${timelineSvg(name, saves)}94</details>95</section>96<section class="repo-section" id="history">97<details class="repo-section-toggle" open>98<summary class="section-head"><span class="section-label">history</span><span class="section-count">${count}</span></summary>99${rows.map((save) => html`<a class="history-row" href="/${name}/saves/${save.id}"><span class="history-id">${save.id}</span><span class="history-msg">${save.message.split("\n")[0]}</span><span class="history-when">${relativeTime(save.created_at)}</span></a>`)}100${(pageNumber > 1 || older) && html`<p class="pager mono">${pageNumber > 1 && html`<a href="?page=${pageNumber - 1}#history">← newer</a>`}${older && html`<a href="?page=${pageNumber + 1}#history">older →</a>`}</p>`}101</details>102</section>`;103}104105function details(readme: Html): Html {106	return html`<section class="repo-details">107<details class="repo-details-toggle" open>108<summary class="repo-details-head">details</summary>109<div class="repo-details-body markdown">${readme}</div>110</details>111</section>`;112}