repo.ts

5.8 KB · raw

1import type { Hono } from "hono";2import { describeFile } from "../describe/describe.ts";3import {4	fileAt,5	filesAt,6	headId,7	history,8	objectBytes,9	repoStats,10	saveByPrefix,11	timeline,12} from "../mirror/queries.ts";13import { visibleReleases } from "../releases/releases.ts";14import { setting } from "../server/db.ts";15import { atlasSection } from "./atlas.ts";16import { type Html, html } from "./html.ts";17import {18	cacheFor,19	notFound,20	openPublicRepo,21	type PageContext,22	type PublicRepo,23	page,24	plural,25	relativeTime,26} from "./layout.ts";27import { renderMarkdown, renderParagraph } from "./markdown.ts";28import { timelineSvg } from "./timeline.ts";2930const PAGE_SIZE = 50;31const HTTP_URL = /^https?:\/\//i;3233export function registerRepoPage(app: Hono, ctx: PageContext): void {34	app.get("/:repo", (c) => {35		const repo = openPublicRepo(ctx, c.req.param("repo"));36		if (!repo) return notFound(c, ctx);37		// `?at=<id>` shows the Atlas at that save (§21.5); the rest of the page stays at HEAD.38		const at = c.req.query("at");39		const atSave = at === undefined ? null : saveByPrefix(repo.mirror, at);40		if (at !== undefined && !atSave) return notFound(c, ctx);41		const pageNumber = Math.max(1, Number.parseInt(c.req.query("page") ?? "1", 10) || 1);42		const readme = readmeAtHead(repo);43		const name = repo.settings.name;44		const intro = readmeIntro(name, readme);45		cacheFor(c, false);46		return c.html(47			page(ctx, {48				title: name,49				description: intro?.text ?? "",50				path: `/${name}`,51				body: html`${header(ctx, repo, intro?.html ?? null)}52${headId(repo.mirror) && sections(repo, pageNumber, atSave?.id ?? null)}53${readme && details(renderMarkdown(readme, { repo: name, dir: "", at: null }))}`,54			}),55		);56	});57}5859/** The README's first paragraph (§21.4's description), as markup and as plain text. */60export function readmeIntro(61	repo: string,62	readme: string | null,63): { html: Html; text: string } | null {64	const paragraph = readme === null ? null : describeFile("README.md", readme);65	return paragraph === null ? null : renderParagraph(paragraph, { repo, dir: "", at: null });66}6768export function readmeAtHead(repo: PublicRepo): string | null {69	const head = headId(repo.mirror);70	const path =71		head && filesAt(repo.mirror, head).find((f) => f.path.toLowerCase() === "readme.md")?.path;72	const file = head && path ? fileAt(repo.mirror, head, path) : null;73	const bytes = file && objectBytes(repo.mirror, file.hash);74	return bytes ? new TextDecoder().decode(bytes) : null;75}7677function header(ctx: PageContext, repo: PublicRepo, intro: Html | null): Html {78	const { name, homepage, installLines } = repo.settings;79	const stats = repoStats(repo.mirror);80	const handle = setting(ctx.kepr, "handle");81	// §21.4: the version is the newest release; with none, the header shows no version.82	const newest = visibleReleases(ctx.kepr, repo.mirror, name)[0];83	const meta = [84		newest && `v${newest.version}`,85		plural(stats.saves, "save"),86		stats.lines > 0 && plural(stats.lines, "line"),87		stats.updated !== null && `updated ${relativeTime(stats.updated)}`,88	].filter(Boolean);89	return html`<header class="repo-head">90<h1 class="repo-name">${name}</h1>91${handle && html`<p class="repo-contributor mono"><a href="/@${handle}">@${handle}</a></p>`}92${intro && html`<div class="repo-desc"><p>${intro}</p></div>`}93${94	HTTP_URL.test(homepage) &&95	html`<p class="repo-homepage"><a href="${homepage}">${homepage}</a></p>`96}97${98	stats.language &&99	html`<div class="lang-badges"><span class="lang-badge">${stats.language}</span></div>`100}101<p class="repo-meta">${meta.join(" · ")}</p>102<div class="repo-tabbar">103<nav class="repo-tabs" aria-label="Repository panels"><a class="repo-tab" href="/${104		name105	}/issues">issues</a><a class="repo-tab" href="/${name}/releases">builds</a><a class="repo-tab" href="/${name}/archives">archives</a></nav>106${107	installLines.length > 0 &&108	html`<div class="install-lines">${installLines.map(109		(line) =>110			html`<div class="install-line"><code>${line}</code>${html`<button type="button" class="copy-btn" data-copy="${line}" hidden>copy</button>`}</div>`,111	)}</div>`112}113</div>114</header>`;115}116117function sections(repo: PublicRepo, requestedPage: number, at: string | null): Html {118	const name = repo.settings.name;119	const atlas = atlasSection(repo, at ?? (headId(repo.mirror) as string), at);120	const saves = timeline(repo.mirror);121	// Past the last page, even at an unbindable number like Infinity, is page 1.122	const pageNumber = (requestedPage - 1) * PAGE_SIZE < saves.length ? requestedPage : 1;123	const rows = history(repo.mirror, PAGE_SIZE, (pageNumber - 1) * PAGE_SIZE);124	const count = plural(saves.length, "save");125	const older = pageNumber * PAGE_SIZE < saves.length;126	return html`<section class="repo-section">127<details class="repo-section-toggle" open>128<summary class="section-head"><span class="section-label">timeline</span><span class="section-count">${129		count130	}</span></summary>131${timelineSvg(name, saves)}132</details>133</section>134${atlas}135<section class="repo-section" id="history">136<details class="repo-section-toggle" open>137<summary class="section-head"><span class="section-label">history</span><span class="section-count">${138		count139	}</span></summary>140${rows.map(141	(save) =>142		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>`,143)}144${145	(pageNumber > 1 || older) &&146	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>`147}148</details>149</section>`;150}151152function details(readme: Html): Html {153	return html`<section class="repo-details">154<details class="repo-details-toggle" open>155<summary class="repo-details-head">details</summary>156<div class="repo-details-body markdown">${readme}</div>157</details>158</section>`;159}