import type { Hono } from "hono"; import { describeFile } from "../describe/describe.ts"; import { fileAt, filesAt, headId, history, objectBytes, repoStats, saveByPrefix, timeline, } from "../mirror/queries.ts"; import { setting } from "../server/db.ts"; import { atlasSection } from "./atlas.ts"; import { type Html, html } from "./html.ts"; import { cacheFor, notFound, openPublicRepo, type PageContext, type PublicRepo, page, plural, relativeTime, } from "./layout.ts"; import { renderMarkdown, renderParagraph } from "./markdown.ts"; import { timelineSvg } from "./timeline.ts"; const PAGE_SIZE = 50; const HTTP_URL = /^https?:\/\//i; export function registerRepoPage(app: Hono, ctx: PageContext): void { app.get("/:repo", (c) => { const repo = openPublicRepo(ctx, c.req.param("repo")); if (!repo) return notFound(c, ctx); // `?at=` shows the Atlas at that save (§21.5); the rest of the page stays at HEAD. const at = c.req.query("at"); const atSave = at === undefined ? null : saveByPrefix(repo.mirror, at); if (at !== undefined && !atSave) return notFound(c, ctx); const pageNumber = Math.max(1, Number.parseInt(c.req.query("page") ?? "1", 10) || 1); const readme = readmeAtHead(repo); const name = repo.settings.name; const paragraph = readme === null ? null : describeFile("README.md", readme); const intro = paragraph === null ? null : renderParagraph(paragraph, { repo: name, dir: "", at: null }); cacheFor(c, false); return c.html( page(ctx, { title: name, description: intro?.text ?? "", path: `/${name}`, body: html`${header(ctx, repo, intro?.html ?? null)} ${headId(repo.mirror) && sections(repo, pageNumber, atSave?.id ?? null)} ${readme && details(renderMarkdown(readme, { repo: name, dir: "", at: null }))}`, }), ); }); } function readmeAtHead(repo: PublicRepo): string | null { const head = headId(repo.mirror); const path = head && filesAt(repo.mirror, head).find((f) => f.path.toLowerCase() === "readme.md")?.path; const file = head && path ? fileAt(repo.mirror, head, path) : null; const bytes = file && objectBytes(repo.mirror, file.hash); return bytes ? new TextDecoder().decode(bytes) : null; } function header(ctx: PageContext, repo: PublicRepo, intro: Html | null): Html { const { name, homepage, installLines } = repo.settings; const stats = repoStats(repo.mirror); const handle = setting(ctx.kepr, "handle"); const meta = [ plural(stats.saves, "save"), stats.lines > 0 && plural(stats.lines, "line"), stats.updated !== null && `updated ${relativeTime(stats.updated)}`, ].filter(Boolean); return html`

${name}

${handle && html`

@${handle}

`} ${intro && html`

${intro}

`} ${HTTP_URL.test(homepage) && html`

${homepage}

`} ${stats.language && html`
${stats.language}
`}

${meta.join(" · ")}

${installLines.length > 0 && html`
${installLines.map((line) => html`
${line}
`)}
`}
`; } function sections(repo: PublicRepo, pageNumber: number, at: string | null): Html { const name = repo.settings.name; const atlas = atlasSection(repo, at ?? (headId(repo.mirror) as string), at); const saves = timeline(repo.mirror); const rows = history(repo.mirror, PAGE_SIZE, (pageNumber - 1) * PAGE_SIZE); const count = plural(saves.length, "save"); const older = pageNumber * PAGE_SIZE < saves.length; return html`
${count} ${timelineSvg(name, saves)}
${atlas}
${count} ${rows.map((save) => html`${save.id}${save.message.split("\n")[0]}${relativeTime(save.created_at)}`)} ${(pageNumber > 1 || older) && html`

${pageNumber > 1 && html`← newer`}${older && html`older →`}

`}
`; } function details(readme: Html): Html { return html`
details
${readme}
`; }