/** * where views live. the static page is the fallback and the hash is a delta * on it: pieces missing from the hash (revision, path, kind) come from the * pathname, so `/u/r/ref/main/blob/x#history` is x's history from main's tip * and `/u/r/ref/main/blob/x#` is x at that commit. */ import type { GitRepo } from "./git/repo.ts"; import { objectAt } from "./git/walk.ts"; export type ViewKind = "tree" | "blob" | "history" | "commit" | "language"; /** for `language`, `path` is the single language id */ export interface View { kind: ViewKind; oid: string; path: string[]; } /** what the pathname says, plus the ref it stands for */ export interface Page { base: string; /** the ref's root page: `/u/r/` when that is where we are, else `/u/r/ref//` */ rootBase: string; /** where the ref's tree and blob pages live: always `/u/r/ref//` */ refBase: string; tip: string | null; kind: "tree" | "blob"; path: string[]; } export interface Site { repo: GitRepo; page: Page; name: string; } const OID = "([0-9a-f]{40}|[0-9a-f]{64})"; export const encodePath = (path: string[]) => path.map(encodeURIComponent).join("/"); const decodePath = (path: string) => path.split("/").filter(Boolean).map(decodeURIComponent); /** `ref` and `tip` come from `
` */ export function pageAt(pathname: string, ref: string | null, tip: string | null): Page { const [user, repo, ...rest] = pathname.split("/").filter(Boolean); const base = `/${user}/${repo}`; if (rest[0] !== "ref") { const refBase = ref ? `${base}/ref/${encodePath(ref.split("/"))}/` : `${base}/`; return { base, rootBase: `${base}/`, refBase, tip, kind: "tree", path: [] }; } // ref names may contain slashes, so the ref runs up to the tree/blob marker const marker = rest.findIndex((seg, i) => i > 1 && (seg === "tree" || seg === "blob")); const refBase = `${base}/${(marker < 0 ? rest : rest.slice(0, marker)).join("/")}/`; const page = { base, rootBase: refBase, refBase, tip }; if (marker < 0) return { ...page, kind: "tree", path: [] }; return { ...page, kind: rest[marker] as "tree" | "blob", path: decodePath(rest.slice(marker + 1).join("/")) }; } export function viewAt(page: Page, hash: string): View | null { const match = (pattern: string) => hash.match(new RegExp(`^#${pattern}$`)); let m: RegExpMatchArray | null; if ((m = match(OID))) return { kind: page.kind, oid: m[1], path: page.path }; if ((m = match(`commit/${OID}`))) return { kind: "commit", oid: m[1], path: [] }; if ((m = match(`history(?:/${OID})?`))) { const oid = m[1] ?? page.tip; return oid ? { kind: "history", oid, path: page.path } : null; } if ((m = match(`language/([a-z0-9-]+)(?:/${OID})?`))) { const oid = m[2] ?? page.tip; return oid ? { kind: "language", oid, path: [m[1]] } : null; } if ((m = match(`(tree|blob|history)/${OID}/(.*)`))) { return { kind: m[1] as ViewKind, oid: m[2], path: decodePath(m[3]) }; } if ((m = match(`tree/${OID}`))) return { kind: "tree", oid: m[1], path: [] }; return null; } /** * the shortest url for `view` that is still a real page without JS: the * static page itself when it shows the same thing, else the closest static * page with the difference in the hash. `atTip` is what `view.path` is at * the page's tip, if anything; hash-only results keep the current pathname. */ export function canonical(page: Page, view: View, atTip: "tree" | "blob" | null): string { const { kind, oid, path } = view; const rev = oid === page.tip ? "" : `/${oid}`; const pageFor = (kind: "tree" | "blob") => kind === "blob" ? `${page.refBase}blob/${encodePath(path)}` : path.length > 0 ? `${page.refBase}tree/${encodePath(path)}/` : page.rootBase; switch (kind) { case "commit": return `#commit/${oid}`; case "language": return `${page.rootBase}#language/${path[0]}${rev}`; case "history": return atTip ? `${pageFor(atTip)}#history${rev}` : `#history/${oid}/${encodePath(path)}`; default: return atTip === kind ? `${pageFor(kind)}${oid === page.tip ? "" : `#${oid}`}` : `#${kind}/${oid}/${encodePath(path)}`; } } export async function kindAtTip(site: Site, path: string[]): Promise<"tree" | "blob" | null> { if (!site.page.tip) return null; if (path.length === 0) return "tree"; const commit = await site.repo.commit(site.page.tip); const object = await objectAt(site.repo, commit.tree, path); if (!object) return null; const kind = object.mode & 0o170000; return kind === 0o040000 ? "tree" : kind === 0o160000 ? null : "blob"; } export async function href(site: Site, view: View): Promise { const needsTip = view.kind === "tree" || view.kind === "blob" || view.kind === "history"; return canonical(site.page, view, needsTip ? await kindAtTip(site, view.path) : null); }