repoview 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/app.css +131 -10
- package/public/app.js +64 -1
- package/src/markdown.js +20 -0
- package/src/server.js +111 -1
- package/src/views.js +14 -8
package/package.json
CHANGED
package/public/app.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
color-scheme: light;
|
|
2
|
+
color-scheme: light dark;
|
|
3
3
|
--bg: #ffffff;
|
|
4
4
|
--panel: #ffffff;
|
|
5
5
|
--text: #24292f;
|
|
@@ -11,6 +11,20 @@
|
|
|
11
11
|
--accent: #0969da;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
@media (prefers-color-scheme: dark) {
|
|
15
|
+
:root {
|
|
16
|
+
--bg: #0d1117;
|
|
17
|
+
--panel: #151b23;
|
|
18
|
+
--text: #f0f6fc;
|
|
19
|
+
--muted: #9198a1;
|
|
20
|
+
--border: #3d444d;
|
|
21
|
+
--subtleBg: #151b23;
|
|
22
|
+
--btn: #21262d;
|
|
23
|
+
--btnHover: #30363d;
|
|
24
|
+
--accent: #4493f8;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
html,
|
|
15
29
|
body {
|
|
16
30
|
height: 100%;
|
|
@@ -39,7 +53,7 @@ a {
|
|
|
39
53
|
position: sticky;
|
|
40
54
|
top: 0;
|
|
41
55
|
z-index: 10;
|
|
42
|
-
background:
|
|
56
|
+
background: color-mix(in srgb, var(--bg) 92%, transparent);
|
|
43
57
|
backdrop-filter: saturate(180%) blur(8px);
|
|
44
58
|
border-bottom: 1px solid var(--border);
|
|
45
59
|
}
|
|
@@ -75,6 +89,88 @@ a {
|
|
|
75
89
|
align-items: center;
|
|
76
90
|
}
|
|
77
91
|
|
|
92
|
+
.conn-status {
|
|
93
|
+
width: 8px;
|
|
94
|
+
height: 8px;
|
|
95
|
+
border-radius: 50%;
|
|
96
|
+
background: var(--muted);
|
|
97
|
+
flex-shrink: 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.conn-status[data-status="connected"] {
|
|
101
|
+
background: #2da44e;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.conn-status[data-status="polling"] {
|
|
105
|
+
background: #bf8700;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.conn-status[data-status="disconnected"] {
|
|
109
|
+
background: #cf222e;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.pdf-wrap {
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-direction: column;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.pdf-frame {
|
|
118
|
+
width: 100%;
|
|
119
|
+
height: 80vh;
|
|
120
|
+
border: none;
|
|
121
|
+
border-radius: 6px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.image-wrap {
|
|
125
|
+
display: flex;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
align-items: center;
|
|
128
|
+
padding: 16px;
|
|
129
|
+
background: var(--subtleBg);
|
|
130
|
+
border-radius: 6px;
|
|
131
|
+
min-height: 200px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.image-preview {
|
|
135
|
+
max-width: 100%;
|
|
136
|
+
max-height: 80vh;
|
|
137
|
+
object-fit: contain;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.csv-wrap {
|
|
141
|
+
overflow: auto;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.csv-table-wrap {
|
|
145
|
+
overflow-x: auto;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.csv-table {
|
|
149
|
+
width: 100%;
|
|
150
|
+
border-collapse: collapse;
|
|
151
|
+
font-size: 13px;
|
|
152
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.csv-table th,
|
|
156
|
+
.csv-table td {
|
|
157
|
+
padding: 6px 12px;
|
|
158
|
+
border: 1px solid var(--border);
|
|
159
|
+
text-align: left;
|
|
160
|
+
white-space: nowrap;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.csv-table th {
|
|
164
|
+
background: var(--subtleBg);
|
|
165
|
+
font-weight: 600;
|
|
166
|
+
position: sticky;
|
|
167
|
+
top: 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.csv-table tr:hover td {
|
|
171
|
+
background: var(--subtleBg);
|
|
172
|
+
}
|
|
173
|
+
|
|
78
174
|
.meta-menu {
|
|
79
175
|
display: none;
|
|
80
176
|
position: relative;
|
|
@@ -117,7 +213,7 @@ a {
|
|
|
117
213
|
padding: 3px 8px;
|
|
118
214
|
border: 1px solid var(--border);
|
|
119
215
|
border-radius: 999px;
|
|
120
|
-
background:
|
|
216
|
+
background: var(--panel);
|
|
121
217
|
}
|
|
122
218
|
|
|
123
219
|
.mono {
|
|
@@ -149,7 +245,8 @@ a {
|
|
|
149
245
|
}
|
|
150
246
|
|
|
151
247
|
.crumb-sep {
|
|
152
|
-
color:
|
|
248
|
+
color: var(--muted);
|
|
249
|
+
opacity: 0.5;
|
|
153
250
|
}
|
|
154
251
|
|
|
155
252
|
.panel {
|
|
@@ -247,8 +344,26 @@ a {
|
|
|
247
344
|
width: 160px;
|
|
248
345
|
}
|
|
249
346
|
|
|
347
|
+
.tz-toggle {
|
|
348
|
+
margin-left: 6px;
|
|
349
|
+
padding: 2px 6px;
|
|
350
|
+
font-size: 11px;
|
|
351
|
+
font-weight: 500;
|
|
352
|
+
color: var(--muted);
|
|
353
|
+
background: var(--btn);
|
|
354
|
+
border: 1px solid var(--border);
|
|
355
|
+
border-radius: 4px;
|
|
356
|
+
cursor: pointer;
|
|
357
|
+
vertical-align: middle;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.tz-toggle:hover {
|
|
361
|
+
background: var(--btnHover);
|
|
362
|
+
color: var(--text);
|
|
363
|
+
}
|
|
364
|
+
|
|
250
365
|
.file-table tr:hover td {
|
|
251
|
-
background:
|
|
366
|
+
background: var(--subtleBg);
|
|
252
367
|
}
|
|
253
368
|
|
|
254
369
|
.item {
|
|
@@ -300,19 +415,25 @@ a {
|
|
|
300
415
|
margin: 0;
|
|
301
416
|
padding: 16px;
|
|
302
417
|
overflow: auto;
|
|
303
|
-
background:
|
|
418
|
+
background: var(--subtleBg);
|
|
304
419
|
}
|
|
305
420
|
|
|
306
421
|
.note {
|
|
307
422
|
padding: 12px;
|
|
308
423
|
border: 1px solid var(--border);
|
|
309
424
|
border-radius: 6px;
|
|
310
|
-
background:
|
|
425
|
+
background: var(--subtleBg);
|
|
311
426
|
}
|
|
312
427
|
|
|
313
428
|
.error {
|
|
314
429
|
padding: 12px;
|
|
315
|
-
color: #cf222e;
|
|
430
|
+
color: var(--error, #cf222e);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
@media (prefers-color-scheme: dark) {
|
|
434
|
+
.error {
|
|
435
|
+
color: #f85149;
|
|
436
|
+
}
|
|
316
437
|
}
|
|
317
438
|
|
|
318
439
|
.markdown-body .markdown-alert {
|
|
@@ -321,7 +442,7 @@ a {
|
|
|
321
442
|
border: 1px solid var(--border);
|
|
322
443
|
border-left-width: 4px;
|
|
323
444
|
border-radius: 6px;
|
|
324
|
-
background:
|
|
445
|
+
background: var(--subtleBg);
|
|
325
446
|
}
|
|
326
447
|
|
|
327
448
|
.markdown-body .markdown-alert-title {
|
|
@@ -353,7 +474,7 @@ a {
|
|
|
353
474
|
padding: 12px;
|
|
354
475
|
border: 1px solid var(--border);
|
|
355
476
|
border-radius: 12px;
|
|
356
|
-
background:
|
|
477
|
+
background: var(--subtleBg);
|
|
357
478
|
overflow: auto;
|
|
358
479
|
}
|
|
359
480
|
|
package/public/app.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
const shouldWatch = new URLSearchParams(location.search).get("watch") !== "0";
|
|
3
|
-
|
|
3
|
+
const statusEl = document.getElementById("conn-status");
|
|
4
|
+
|
|
5
|
+
function setStatus(state) {
|
|
6
|
+
if (!statusEl) return;
|
|
7
|
+
statusEl.dataset.status = state;
|
|
8
|
+
const titles = {
|
|
9
|
+
connected: "Live reload: connected",
|
|
10
|
+
connecting: "Live reload: connecting...",
|
|
11
|
+
polling: "Live reload: polling",
|
|
12
|
+
disconnected: "Live reload: disconnected",
|
|
13
|
+
};
|
|
14
|
+
statusEl.title = titles[state] || "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!shouldWatch) {
|
|
18
|
+
if (statusEl) statusEl.style.display = "none";
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
4
21
|
|
|
5
22
|
let pollingTimer = null;
|
|
6
23
|
let lastRevision = null;
|
|
@@ -14,6 +31,7 @@
|
|
|
14
31
|
|
|
15
32
|
async function ensurePolling() {
|
|
16
33
|
if (pollingTimer) return;
|
|
34
|
+
setStatus("polling");
|
|
17
35
|
try {
|
|
18
36
|
lastRevision = await fetchRevision();
|
|
19
37
|
} catch {
|
|
@@ -32,10 +50,14 @@
|
|
|
32
50
|
|
|
33
51
|
try {
|
|
34
52
|
const es = new EventSource("/events");
|
|
53
|
+
es.addEventListener("open", () => {
|
|
54
|
+
setStatus("connected");
|
|
55
|
+
});
|
|
35
56
|
es.addEventListener("reload", () => {
|
|
36
57
|
location.reload();
|
|
37
58
|
});
|
|
38
59
|
es.addEventListener("error", () => {
|
|
60
|
+
setStatus("disconnected");
|
|
39
61
|
// Some environments/proxies break SSE; fall back to polling.
|
|
40
62
|
void ensurePolling();
|
|
41
63
|
});
|
|
@@ -113,8 +135,49 @@ function renderMath() {
|
|
|
113
135
|
}
|
|
114
136
|
}
|
|
115
137
|
|
|
138
|
+
function formatDateTime(ms, useUtc) {
|
|
139
|
+
const d = new Date(ms);
|
|
140
|
+
const opts = {
|
|
141
|
+
year: "numeric",
|
|
142
|
+
month: "short",
|
|
143
|
+
day: "2-digit",
|
|
144
|
+
hour: "2-digit",
|
|
145
|
+
minute: "2-digit",
|
|
146
|
+
timeZone: useUtc ? "UTC" : undefined,
|
|
147
|
+
};
|
|
148
|
+
const formatted = d.toLocaleString(undefined, opts);
|
|
149
|
+
return useUtc ? `${formatted} UTC` : formatted;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function initTimezoneToggle() {
|
|
153
|
+
const toggle = document.querySelector(".tz-toggle");
|
|
154
|
+
if (!toggle) return;
|
|
155
|
+
|
|
156
|
+
const cells = document.querySelectorAll(".mtime[data-ts]");
|
|
157
|
+
if (!cells.length) return;
|
|
158
|
+
|
|
159
|
+
let useUtc = localStorage.getItem("tz") === "utc";
|
|
160
|
+
|
|
161
|
+
function update() {
|
|
162
|
+
toggle.textContent = useUtc ? "UTC" : "Local";
|
|
163
|
+
for (const cell of cells) {
|
|
164
|
+
const ts = Number(cell.dataset.ts);
|
|
165
|
+
if (ts) cell.textContent = formatDateTime(ts, useUtc);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
toggle.addEventListener("click", () => {
|
|
170
|
+
useUtc = !useUtc;
|
|
171
|
+
localStorage.setItem("tz", useUtc ? "utc" : "local");
|
|
172
|
+
update();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
update();
|
|
176
|
+
}
|
|
177
|
+
|
|
116
178
|
window.addEventListener("load", () => {
|
|
117
179
|
preserveQueryParamsOnInternalLinks(["ignored", "watch"]);
|
|
118
180
|
renderMath();
|
|
119
181
|
renderMermaid();
|
|
182
|
+
initTimezoneToggle();
|
|
120
183
|
});
|
package/src/markdown.js
CHANGED
|
@@ -16,6 +16,26 @@ function escapeHtml(s) {
|
|
|
16
16
|
.replaceAll("'", "'");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// CommonMark only allows "1." to interrupt a paragraph, but GitHub allows any number.
|
|
20
|
+
// This preprocessor adds blank lines before ordered lists starting with numbers other than 1.
|
|
21
|
+
function normalizeOrderedLists(text) {
|
|
22
|
+
const lines = text.split("\n");
|
|
23
|
+
const result = [];
|
|
24
|
+
for (let i = 0; i < lines.length; i++) {
|
|
25
|
+
const line = lines[i];
|
|
26
|
+
const prevLine = i > 0 ? lines[i - 1] : "";
|
|
27
|
+
// Check if current line starts an ordered list with number > 1
|
|
28
|
+
if (/^[2-9]\d*\. /.test(line)) {
|
|
29
|
+
// Insert blank line if previous line is non-empty and not a list item
|
|
30
|
+
if (prevLine.trim() && !/^\d+\. /.test(prevLine) && !/^[-*+] /.test(prevLine)) {
|
|
31
|
+
result.push("");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
result.push(line);
|
|
35
|
+
}
|
|
36
|
+
return result.join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
function isExternalHref(href) {
|
|
20
40
|
return /^(?:[a-z]+:)?\/\//i.test(href) || href.startsWith("mailto:") || href.startsWith("tel:");
|
|
21
41
|
}
|
package/src/server.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createMarkdownRenderer } from "./markdown.js";
|
|
|
12
12
|
import { loadGitIgnoreMatcher } from "./gitignore.js";
|
|
13
13
|
import { createRepoLinkScanner } from "./linkcheck.js";
|
|
14
14
|
import {
|
|
15
|
+
escapeHtml,
|
|
15
16
|
renderBrokenLinksPage,
|
|
16
17
|
renderErrorPage,
|
|
17
18
|
renderFilePage,
|
|
@@ -35,6 +36,63 @@ function isWithinRoot(rootReal, candidateReal) {
|
|
|
35
36
|
return candidateReal.startsWith(rootWithSep);
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function parseCsv(text, delimiter = ",") {
|
|
40
|
+
const rows = [];
|
|
41
|
+
let current = [];
|
|
42
|
+
let cell = "";
|
|
43
|
+
let inQuotes = false;
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < text.length; i++) {
|
|
46
|
+
const ch = text[i];
|
|
47
|
+
if (inQuotes) {
|
|
48
|
+
if (ch === '"' && text[i + 1] === '"') {
|
|
49
|
+
cell += '"';
|
|
50
|
+
i++;
|
|
51
|
+
} else if (ch === '"') {
|
|
52
|
+
inQuotes = false;
|
|
53
|
+
} else {
|
|
54
|
+
cell += ch;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
if (ch === '"') {
|
|
58
|
+
inQuotes = true;
|
|
59
|
+
} else if (ch === delimiter) {
|
|
60
|
+
current.push(cell);
|
|
61
|
+
cell = "";
|
|
62
|
+
} else if (ch === "\n" || (ch === "\r" && text[i + 1] === "\n")) {
|
|
63
|
+
if (ch === "\r") i++;
|
|
64
|
+
current.push(cell);
|
|
65
|
+
rows.push(current);
|
|
66
|
+
current = [];
|
|
67
|
+
cell = "";
|
|
68
|
+
} else if (ch === "\r") {
|
|
69
|
+
current.push(cell);
|
|
70
|
+
rows.push(current);
|
|
71
|
+
current = [];
|
|
72
|
+
cell = "";
|
|
73
|
+
} else {
|
|
74
|
+
cell += ch;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (cell || current.length) {
|
|
79
|
+
current.push(cell);
|
|
80
|
+
rows.push(current);
|
|
81
|
+
}
|
|
82
|
+
return rows;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderCsvTable(rows, escFn) {
|
|
86
|
+
if (!rows.length) return "<p>Empty file</p>";
|
|
87
|
+
const header = rows[0];
|
|
88
|
+
const body = rows.slice(1);
|
|
89
|
+
const ths = header.map((h) => `<th>${escFn(h)}</th>`).join("");
|
|
90
|
+
const trs = body
|
|
91
|
+
.map((row) => `<tr>${row.map((c) => `<td>${escFn(c)}</td>`).join("")}</tr>`)
|
|
92
|
+
.join("\n");
|
|
93
|
+
return `<div class="csv-table-wrap"><table class="csv-table"><thead><tr>${ths}</tr></thead><tbody>${trs}</tbody></table></div>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
38
96
|
async function getGitInfo(repoRootReal) {
|
|
39
97
|
const gitDir = path.join(repoRootReal, ".git");
|
|
40
98
|
try {
|
|
@@ -311,6 +369,7 @@ export async function startServer({ repoRoot, host, port, watch }) {
|
|
|
311
369
|
href,
|
|
312
370
|
size: isDir ? "" : formatBytes(info.size),
|
|
313
371
|
mtime: formatDate(info.mtimeMs),
|
|
372
|
+
mtimeMs: info.mtimeMs,
|
|
314
373
|
};
|
|
315
374
|
}),
|
|
316
375
|
);
|
|
@@ -388,7 +447,51 @@ export async function startServer({ repoRoot, host, port, watch }) {
|
|
|
388
447
|
const fileName = path.basename(resolved);
|
|
389
448
|
const ext = path.extname(fileName).toLowerCase();
|
|
390
449
|
const isMarkdown = [".md", ".markdown", ".mdown", ".mkd", ".mkdn"].includes(ext);
|
|
450
|
+
const isPdf = ext === ".pdf";
|
|
451
|
+
const isImage = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".bmp"].includes(ext);
|
|
452
|
+
const isCsv = [".csv", ".tsv"].includes(ext);
|
|
391
453
|
const maxBytes = 2 * 1024 * 1024;
|
|
454
|
+
const rawSrc = `/raw/${encodePathForUrl(toPosixPath(stripped))}`;
|
|
455
|
+
|
|
456
|
+
if (isPdf) {
|
|
457
|
+
res.status(200).send(
|
|
458
|
+
renderFilePage({
|
|
459
|
+
title: `${repoName}/${stripped}`,
|
|
460
|
+
repoName,
|
|
461
|
+
gitInfo,
|
|
462
|
+
brokenLinks: linkScanner.getState(),
|
|
463
|
+
relPathPosix: toPosixPath(stripped),
|
|
464
|
+
querySuffix,
|
|
465
|
+
toggleIgnoredHref,
|
|
466
|
+
showIgnored,
|
|
467
|
+
fileName,
|
|
468
|
+
isMarkdown: false,
|
|
469
|
+
mediaType: "pdf",
|
|
470
|
+
renderedHtml: `<iframe class="pdf-frame" src="${rawSrc}"></iframe>`,
|
|
471
|
+
}),
|
|
472
|
+
);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (isImage) {
|
|
477
|
+
res.status(200).send(
|
|
478
|
+
renderFilePage({
|
|
479
|
+
title: `${repoName}/${stripped}`,
|
|
480
|
+
repoName,
|
|
481
|
+
gitInfo,
|
|
482
|
+
brokenLinks: linkScanner.getState(),
|
|
483
|
+
relPathPosix: toPosixPath(stripped),
|
|
484
|
+
querySuffix,
|
|
485
|
+
toggleIgnoredHref,
|
|
486
|
+
showIgnored,
|
|
487
|
+
fileName,
|
|
488
|
+
isMarkdown: false,
|
|
489
|
+
mediaType: "image",
|
|
490
|
+
renderedHtml: `<img class="image-preview" src="${rawSrc}" alt="${escapeHtml(fileName)}" />`,
|
|
491
|
+
}),
|
|
492
|
+
);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
392
495
|
|
|
393
496
|
if (st.size > maxBytes) {
|
|
394
497
|
res.status(200).send(
|
|
@@ -417,7 +520,13 @@ export async function startServer({ repoRoot, host, port, watch }) {
|
|
|
417
520
|
const text = raw.toString("utf8");
|
|
418
521
|
|
|
419
522
|
let renderedHtml;
|
|
420
|
-
|
|
523
|
+
let mediaType;
|
|
524
|
+
if (isCsv) {
|
|
525
|
+
const delimiter = ext === ".tsv" ? "\t" : ",";
|
|
526
|
+
const rows = parseCsv(text, delimiter);
|
|
527
|
+
renderedHtml = renderCsvTable(rows, escapeHtml);
|
|
528
|
+
mediaType = "csv";
|
|
529
|
+
} else if (isMarkdown) {
|
|
421
530
|
const baseDir = toPosixPath(path.posix.dirname(toPosixPath(stripped)));
|
|
422
531
|
renderedHtml = md.render(text, { baseDirPosix: baseDir === "." ? "" : baseDir });
|
|
423
532
|
} else {
|
|
@@ -438,6 +547,7 @@ export async function startServer({ repoRoot, host, port, watch }) {
|
|
|
438
547
|
showIgnored,
|
|
439
548
|
fileName,
|
|
440
549
|
isMarkdown,
|
|
550
|
+
mediaType,
|
|
441
551
|
renderedHtml,
|
|
442
552
|
}),
|
|
443
553
|
);
|
package/src/views.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
function escapeHtml(s) {
|
|
3
|
+
export function escapeHtml(s) {
|
|
4
4
|
return String(s)
|
|
5
5
|
.replaceAll("&", "&")
|
|
6
6
|
.replaceAll("<", "<")
|
|
@@ -44,8 +44,9 @@ function pageTemplate({ title, repoName, gitInfo, relPathPosix, bodyHtml }) {
|
|
|
44
44
|
<meta charset="utf-8" />
|
|
45
45
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
46
46
|
<title>${escapeHtml(title)}</title>
|
|
47
|
-
<link rel="stylesheet" href="/static/vendor/github-markdown-css/github-markdown
|
|
48
|
-
<link rel="stylesheet" href="/static/vendor/highlight.js/styles/github.css" />
|
|
47
|
+
<link rel="stylesheet" href="/static/vendor/github-markdown-css/github-markdown.css" />
|
|
48
|
+
<link rel="stylesheet" href="/static/vendor/highlight.js/styles/github.css" media="(prefers-color-scheme: light)" />
|
|
49
|
+
<link rel="stylesheet" href="/static/vendor/highlight.js/styles/github-dark.css" media="(prefers-color-scheme: dark)" />
|
|
49
50
|
<link rel="stylesheet" href="/static/vendor/katex/katex.min.css" />
|
|
50
51
|
<link rel="stylesheet" href="/static/app.css" />
|
|
51
52
|
</head>
|
|
@@ -138,8 +139,9 @@ function pageTemplateWithLinks({
|
|
|
138
139
|
<meta charset="utf-8" />
|
|
139
140
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
140
141
|
<title>${escapeHtml(title)}</title>
|
|
141
|
-
<link rel="stylesheet" href="/static/vendor/github-markdown-css/github-markdown
|
|
142
|
-
<link rel="stylesheet" href="/static/vendor/highlight.js/styles/github.css" />
|
|
142
|
+
<link rel="stylesheet" href="/static/vendor/github-markdown-css/github-markdown.css" />
|
|
143
|
+
<link rel="stylesheet" href="/static/vendor/highlight.js/styles/github.css" media="(prefers-color-scheme: light)" />
|
|
144
|
+
<link rel="stylesheet" href="/static/vendor/highlight.js/styles/github-dark.css" media="(prefers-color-scheme: dark)" />
|
|
143
145
|
<link rel="stylesheet" href="/static/vendor/katex/katex.min.css" />
|
|
144
146
|
<link rel="stylesheet" href="/static/app.css" />
|
|
145
147
|
</head>
|
|
@@ -154,6 +156,7 @@ function pageTemplateWithLinks({
|
|
|
154
156
|
${brokenPill}
|
|
155
157
|
${ignoredPill}
|
|
156
158
|
</span>
|
|
159
|
+
<span id="conn-status" class="conn-status" title="Live reload: connecting..."></span>
|
|
157
160
|
${metaMenu}
|
|
158
161
|
</div>
|
|
159
162
|
</div>
|
|
@@ -185,9 +188,10 @@ export function renderTreePage({
|
|
|
185
188
|
.map((r) => {
|
|
186
189
|
const icon = r.isDir ? "dir" : "file";
|
|
187
190
|
const name = escapeHtml(r.name);
|
|
191
|
+
const tsAttr = r.mtimeMs ? ` data-ts="${r.mtimeMs}"` : "";
|
|
188
192
|
return `<tr>
|
|
189
193
|
<td class="name"><a class="item ${icon}" href="${r.href}">${name}</a></td>
|
|
190
|
-
<td class="mtime">${escapeHtml(r.mtime)}</td>
|
|
194
|
+
<td class="mtime"${tsAttr}>${escapeHtml(r.mtime)}</td>
|
|
191
195
|
<td class="size">${escapeHtml(r.size)}</td>
|
|
192
196
|
</tr>`;
|
|
193
197
|
})
|
|
@@ -205,7 +209,7 @@ export function renderTreePage({
|
|
|
205
209
|
<div class="table-wrap">
|
|
206
210
|
<table class="file-table">
|
|
207
211
|
<thead>
|
|
208
|
-
<tr><th class="name">Name</th><th class="mtime">Last modified</th><th class="size">Size</th></tr>
|
|
212
|
+
<tr><th class="name">Name</th><th class="mtime">Last modified <button type="button" class="tz-toggle" title="Toggle local/UTC time">Local</button></th><th class="size">Size</th></tr>
|
|
209
213
|
</thead>
|
|
210
214
|
<tbody>
|
|
211
215
|
${tableRows || `<tr><td colspan="3" class="empty">Empty directory</td></tr>`}
|
|
@@ -239,6 +243,7 @@ export function renderFilePage({
|
|
|
239
243
|
relPathPosix,
|
|
240
244
|
fileName,
|
|
241
245
|
isMarkdown,
|
|
246
|
+
mediaType,
|
|
242
247
|
renderedHtml,
|
|
243
248
|
}) {
|
|
244
249
|
const relDir = path.posix.dirname(relPathPosix || "");
|
|
@@ -246,6 +251,7 @@ export function renderFilePage({
|
|
|
246
251
|
const rawHref = `/raw/${encodePathForUrl(relPathPosix || "")}${suffix}`;
|
|
247
252
|
const treeHref = `/tree/${encodePathForUrl(relDir === "." ? "" : relDir)}${suffix}`;
|
|
248
253
|
|
|
254
|
+
const wrapClass = mediaType ? `${mediaType}-wrap` : isMarkdown ? "markdown-body markdown-wrap" : "code-wrap";
|
|
249
255
|
const body = `<section class="panel">
|
|
250
256
|
<div class="panel-title">
|
|
251
257
|
<span class="filename">${escapeHtml(fileName)}</span>
|
|
@@ -253,7 +259,7 @@ export function renderFilePage({
|
|
|
253
259
|
<a class="btn" href="${treeHref}">Back</a>
|
|
254
260
|
<a class="btn" href="${rawHref}">Raw</a>
|
|
255
261
|
</div>
|
|
256
|
-
<div class="${
|
|
262
|
+
<div class="${wrapClass}">
|
|
257
263
|
${renderedHtml}
|
|
258
264
|
</div>
|
|
259
265
|
</section>`;
|