aboutsummaryrefslogtreecommitdiffstats
path: root/src/ui/html.ts
blob: 230757fa754a506e1dda98aa6bb48273db00c2d2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
export function esc(s: string): string {
  return s
    .replaceAll("&", "&")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;");
}
 
/** cgit-style relative age with its color class, e.g. "3 days" / age-days. */
export function age(unixSecs: number): string {
  if (!unixSecs) return "";
  const secs = Math.max(0, Math.floor(Date.now() / 1000) - unixSecs);
  const fmt = (n: number, unit: string, cls: string) =>
    `<span class='age-${cls}'>${n} ${unit}${n === 1 ? "" : unit.endsWith(".") ? "" : "s"}</span>`;
  if (secs < 3600) return fmt(Math.max(1, Math.floor(secs / 60)), "min.", "mins");
  if (secs < 24 * 3600) return fmt(Math.floor(secs / 3600), "hour", "hours");
  if (secs < 7 * 24 * 3600) return fmt(Math.floor(secs / (24 * 3600)), "day", "days");
  if (secs < 31 * 24 * 3600) return fmt(Math.floor(secs / (7 * 24 * 3600)), "week", "weeks");
  if (secs < 365 * 24 * 3600) return fmt(Math.floor(secs / (30 * 24 * 3600)), "month", "months");
  return fmt(Math.floor(secs / (365 * 24 * 3600)), "year", "years");
}
 
export function fmtDate(unixSecs: number, tz: string): string {
  const d = new Date(unixSecs * 1000);
  const pad = (n: number) => String(n).padStart(2, "0");
  return (
    `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ` +
    `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())} ${tz}`
  );
}
 
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
 
/** RFC 2822 date in the person's own timezone (for patch/atom output). */
export function fmtDate2822(unixSecs: number, tz: string): string {
  const offMin = (parseInt(tz.slice(0, 3), 10) || 0) * 60 + (parseInt(tz.slice(0, 1) + tz.slice(3), 10) || 0);
  const d = new Date((unixSecs + offMin * 60) * 1000);
  const pad = (n: number) => String(n).padStart(2, "0");
  return (
    `${DAYS[d.getUTCDay()]}, ${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()} ` +
    `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())} ${tz}`
  );
}
 
export interface LayoutOpts {
  site: string;
  siteDesc: string;
  title: string;
  /** repo name when rendering a repo page */
  repo?: string;
  /** description line under the title */
  sub: string;
  /** active tab id */
  tab?: string;
  /** current ref for tab links */
  ref?: string;
  /** breadcrumb path html (tree pages) */
  pathBar?: string;
  /** show the about tab (repo has a README) */
  hasAbout?: boolean;
  /** branch names for the switcher dropdown */
  branches?: string[];
  /** form action for the branch switcher (current page path) */
  formAction?: string;
  body: string;
}
 
export function layout(o: LayoutOpts): string {
  const r = o.repo ? `/${encodeURIComponent(o.repo)}` : "";
  const q = o.ref ? `?h=${encodeURIComponent(o.ref)}` : "";
  const tabs: [string, string][] = o.repo
    ? [
        ...(o.hasAbout ? [["about", `${r}/about/${q}`] as [string, string]] : []),
        ["summary", `${r}/${q}`],
        ["refs", `${r}/refs/${q}`],
        ["log", `${r}/log/${q}`],
        ["tree", `${r}/tree/${q}`],
        ["commit", `${r}/commit/${q}`],
        ["diff", `${r}/diff/${q}`],
        ["stats", `${r}/stats/${q}`],
      ]
    : [["index", "/"]];
  const tabHtml = tabs
    .map(([id, href]) => `<a${id === o.tab ? " class='active'" : ""} href='${href}'>${id}</a>`)
    .join("");
  const crumb = o.repo ? ` : <a href='${r}/'>${esc(o.repo)}</a>` : "";
  const switcher =
    o.repo && o.branches && o.branches.length
      ? `<td class='form'><form method='get' action='${esc(o.formAction ?? `${r}/`)}'>` +
        `<select name='h' onchange='this.form.submit();'>` +
        o.branches
          .map((b) => `<option value='${esc(b)}'${b === o.ref ? " selected='selected'" : ""}>${esc(b)}</option>`)
          .join("") +
        `</select> <input type='submit' value='switch'/></form></td>`
      : "";
  const atom = o.repo
    ? `<link rel='alternate' title='Atom feed' href='${r}/atom/${q}' type='application/atom+xml'/>`
    : "";
  return `<!DOCTYPE html>
<html lang='en'>
<head>
<title>${esc(o.title)}</title>
<meta name='generator' content='dgit'/>
<meta name='viewport' content='width=device-width, initial-scale=1'/>
<link rel='stylesheet' type='text/css' href='/cgit.css'/>
${atom}
</head>
<body>
<div id='cgit'>
<table id='header'>
<tr>
<td class='main'><a href='/'>${esc(o.site)}</a>${crumb}</td>
</tr>
<tr><td class='sub'>${esc(o.sub)}</td></tr>
</table>
<table class='tabs'><tr><td>${tabHtml}</td>${switcher}</tr></table>
${o.pathBar ? `<div class='path'>${o.pathBar}</div>` : ""}
<div class='content'>
${o.body}
</div>
<div class='footer'>generated by dgit 0.2 (cgit on Cloudflare Workers / celld)</div>
</div>
</body>
</html>
`;
}
 
export function htmlResponse(html: string, status = 200): Response {
  return new Response(html, {
    status,
    headers: { "content-type": "text/html; charset=utf-8" },
  });
}
 
export function errorPage(o: Omit<LayoutOpts, "body">, msg: string, status = 404): Response {
  return htmlResponse(layout({ ...o, body: `<div class='error'>${esc(msg)}</div>` }), status);
}