forks/sorcery

git clone https://git.gae.moe/forks/sorcery

Charlotte Somrender tip tree/blob views on clientside (big rework)7e9177b

grace-patches
5.1 KiB121 linesraw
1/**
2 * where views live. the static page is the fallback and the hash is a delta
3 * on it: pieces missing from the hash (revision, path, kind) come from the
4 * pathname, so `/u/r/ref/main/blob/x#history` is x's history from main's tip,
5 * `/u/r/ref/main/blob/x#<oid>` is x at that commit, and `/u/r/ref/main/blob/x`
6 * alone is x at the tip.
7 */
8import type { GitRepo } from "./git/repo.ts";
9
10export type ViewKind = "tree" | "blob" | "history" | "commit" | "language";
11/** for `language`, `path` is the single language id */
12export interface View {
13  kind: ViewKind;
14  oid: string;
15  path: string[];
16}
17
18/** what the pathname says, plus the ref it stands for */
19export interface Page {
20  base: string;
21  /** the ref's root page: `/u/r/` when that is where we are, else `/u/r/ref/<ref>/` */
22  rootBase: string;
23  /** where the ref's tree and blob pages live: always `/u/r/ref/<ref>/` */
24  refBase: string;
25  tip: string | null;
26  kind: "tree" | "blob";
27  path: string[];
28}
29
30export interface Site {
31  repo: GitRepo;
32  page: Page;
33  name: string;
34}
35
36const OID = "([0-9a-f]{40}|[0-9a-f]{64})";
37export const encodePath = (path: string[]) => path.map(encodeURIComponent).join("/");
38const decodePath = (path: string) => path.split("/").filter(Boolean).map(decodeURIComponent);
39
40/** `ref` and `tip` come from `<main data-ref data-tip>`; the ref's pages are
41 * the only ones `tip` speaks for, see `withinRef` */
42export function pageAt(pathname: string, ref: string | null, tip: string | null): Page {
43  const [user, repo, ...rest] = pathname.split("/").filter(Boolean);
44  const base = `/${user}/${repo}`;
45  if (rest[0] !== "ref") {
46    const refBase = ref ? `${base}/ref/${encodePath(ref.split("/"))}/` : `${base}/`;
47    // only the root page shows the ref; `/u/r/refs` and the like do not
48    return { base, rootBase: `${base}/`, refBase, tip: rest.length === 0 ? tip : null, kind: "tree", path: [] };
49  }
50  // ref names may contain slashes, so the ref runs up to the tree/blob marker
51  const marker = rest.findIndex((seg, i) => i > 1 && (seg === "tree" || seg === "blob"));
52  const refBase = `${base}/${(marker < 0 ? rest : rest.slice(0, marker)).join("/")}/`;
53  const page = { base, rootBase: refBase, refBase, tip };
54  if (marker < 0) return { ...page, kind: "tree", path: [] };
55  return { ...page, kind: rest[marker] as "tree" | "blob", path: decodePath(rest.slice(marker + 1).join("/")) };
56}
57
58/** whether `pathname` is a page of the same ref as `page`, so that `page.tip`
59 * stands for it too */
60export const withinRef = (page: Page, pathname: string) =>
61  page.tip !== null && (pathname === page.rootBase || pathname.startsWith(page.refBase));
62
63/** what the pathname alone shows: the page's own path at the tip */
64export const pageView = (page: Page): View | null =>
65  page.tip ? { kind: page.kind, oid: page.tip, path: page.path } : null;
66
67export const viewKey = (view: View | null) => view ? [view.kind, view.oid, ...view.path].join("/") : "";
68
69/** whether `view` is what the page's pathname alone shows, i.e. its url has no hash */
70export const isPageView = (page: Page, view: View) => viewKey(view) === viewKey(pageView(page));
71
72/** the hash as a delta on the page; a hash that is no route (`#L12`) leaves
73 * the page's own view */
74export function viewAt(page: Page, hash: string): View | null {
75  const match = (pattern: string) => hash.match(new RegExp(`^#${pattern}$`));
76  let m: RegExpMatchArray | null;
77  if ((m = match(OID))) return { kind: page.kind, oid: m[1], path: page.path };
78  if ((m = match(`commit/${OID}`))) return { kind: "commit", oid: m[1], path: [] };
79  if ((m = match(`history(?:/${OID})?`))) {
80    const oid = m[1] ?? page.tip;
81    return oid ? { kind: "history", oid, path: page.path } : null;
82  }
83  if ((m = match(`language/([a-z0-9-]+)(?:/${OID})?`))) {
84    const oid = m[2] ?? page.tip;
85    return oid ? { kind: "language", oid, path: [m[1]] } : null;
86  }
87  if ((m = match(`(tree|blob|history)/${OID}/(.*)`))) {
88    return { kind: m[1] as ViewKind, oid: m[2], path: decodePath(m[3]) };
89  }
90  if ((m = match(`tree/${OID}`))) return { kind: "tree", oid: m[1], path: [] };
91  return pageView(page);
92}
93
94/**
95 * the shortest url for `view` that is still a real page without JS: the
96 * static page itself when it shows the same thing, else the closest static
97 * page with the difference in the hash. `atTip` is what `view.path` is at
98 * the page's tip, if anything; hash-only results keep the current pathname.
99 */
100export function canonical(page: Page, view: View, atTip: "tree" | "blob" | null): string {
101  const { kind, oid, path } = view;
102  const rev = oid === page.tip ? "" : `/${oid}`;
103  const pageFor = (kind: "tree" | "blob") =>
104    kind === "blob"
105      ? `${page.refBase}blob/${encodePath(path)}`
106      : path.length > 0
107      ? `${page.refBase}tree/${encodePath(path)}/`
108      : page.rootBase;
109  switch (kind) {
110    case "commit":
111      return `#commit/${oid}`;
112    case "language":
113      return `${page.rootBase}#language/${path[0]}${rev}`;
114    case "history":
115      return atTip ? `${pageFor(atTip)}#history${rev}` : `#history/${oid}/${encodePath(path)}`;
116    default:
117      return atTip === kind
118        ? `${pageFor(kind)}${oid === page.tip ? "" : `#${oid}`}`
119        : `#${kind}/${oid}/${encodePath(path)}`;
120  }
121}