repoview 0.5.1 → 0.6.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/CHANGELOG.md +53 -0
- package/CONTRIBUTING.md +4 -3
- package/DEVELOPMENT.md +70 -16
- package/README.md +36 -5
- package/dist/api.js +58 -0
- package/dist/api.js.map +1 -0
- package/dist/cli.js +224 -0
- package/dist/cli.js.map +1 -0
- package/dist/csv.js +64 -0
- package/dist/csv.js.map +1 -0
- package/dist/format.js +25 -0
- package/dist/format.js.map +1 -0
- package/dist/git.js +67 -0
- package/dist/git.js.map +1 -0
- package/dist/gitignore.js +34 -0
- package/dist/gitignore.js.map +1 -0
- package/dist/linkcheck.js +310 -0
- package/dist/linkcheck.js.map +1 -0
- package/dist/markdown.js +493 -0
- package/dist/markdown.js.map +1 -0
- package/dist/net.js +10 -0
- package/dist/net.js.map +1 -0
- package/dist/paths.js +59 -0
- package/dist/paths.js.map +1 -0
- package/dist/reload.js +36 -0
- package/dist/reload.js.map +1 -0
- package/dist/repo-context.js +73 -0
- package/dist/repo-context.js.map +1 -0
- package/dist/repo-router.js +801 -0
- package/dist/repo-router.js.map +1 -0
- package/dist/review-cli.js +228 -0
- package/dist/review-cli.js.map +1 -0
- package/dist/server.js +116 -0
- package/dist/server.js.map +1 -0
- package/dist/session.js +86 -0
- package/dist/session.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/views.js +633 -0
- package/dist/views.js.map +1 -0
- package/package.json +20 -9
- package/public/app.css +82 -0
- package/public/app.js +4 -2
- package/public/review.js +9 -6
- package/public/session.js +61 -0
- package/src/cli.js +0 -91
- package/src/gitignore.js +0 -34
- package/src/linkcheck.js +0 -312
- package/src/markdown.js +0 -518
- package/src/review-cli.js +0 -245
- package/src/server.js +0 -1126
- package/src/views.js +0 -657
package/src/views.js
DELETED
|
@@ -1,657 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
|
|
3
|
-
function formatReviewTime(isoString) {
|
|
4
|
-
if (!isoString) return "";
|
|
5
|
-
const d = new Date(isoString);
|
|
6
|
-
const now = Date.now();
|
|
7
|
-
const diff = now - d.getTime();
|
|
8
|
-
if (diff < 60000) return "just now";
|
|
9
|
-
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
10
|
-
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
11
|
-
if (diff < 604800000) return `${Math.floor(diff / 86400000)}d ago`;
|
|
12
|
-
return d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function escapeHtml(s) {
|
|
16
|
-
return String(s)
|
|
17
|
-
.replaceAll("&", "&")
|
|
18
|
-
.replaceAll("<", "<")
|
|
19
|
-
.replaceAll(">", ">")
|
|
20
|
-
.replaceAll('"', """)
|
|
21
|
-
.replaceAll("'", "'");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function encodePathForUrl(posixPath) {
|
|
25
|
-
return posixPath
|
|
26
|
-
.split("/")
|
|
27
|
-
.map((segment) => encodeURIComponent(segment))
|
|
28
|
-
.join("/");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function renderBreadcrumbs(relPathPosix, querySuffix) {
|
|
32
|
-
const parts = (relPathPosix || "").split("/").filter(Boolean);
|
|
33
|
-
const suffix = querySuffix || "";
|
|
34
|
-
const crumbs = [{ name: "", href: `/tree/${suffix}` }];
|
|
35
|
-
let cursor = "";
|
|
36
|
-
for (const p of parts) {
|
|
37
|
-
cursor = cursor ? `${cursor}/${p}` : p;
|
|
38
|
-
crumbs.push({ name: p, href: `/tree/${encodePathForUrl(cursor)}${suffix}` });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const html = crumbs
|
|
42
|
-
.map((c, idx) => {
|
|
43
|
-
const label = idx === 0 ? "root" : escapeHtml(c.name);
|
|
44
|
-
return `<a class="crumb" href="${c.href}">${label}</a>`;
|
|
45
|
-
})
|
|
46
|
-
.join(`<span class="crumb-sep">/</span>`);
|
|
47
|
-
return `<nav class="breadcrumbs" aria-label="Breadcrumbs">${html}</nav>`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function pageTemplate({ title, repoName, gitInfo, relPathPosix, bodyHtml }) {
|
|
51
|
-
const branch = gitInfo?.branch ? escapeHtml(gitInfo.branch) : "no-git";
|
|
52
|
-
const commit = gitInfo?.commit ? escapeHtml(gitInfo.commit.slice(0, 7)) : "";
|
|
53
|
-
return `<!doctype html>
|
|
54
|
-
<html lang="en">
|
|
55
|
-
<head>
|
|
56
|
-
<meta charset="utf-8" />
|
|
57
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
58
|
-
<title>${escapeHtml(title)}</title>
|
|
59
|
-
<link rel="stylesheet" href="/static/vendor/github-markdown-css/github-markdown.css" />
|
|
60
|
-
<link rel="stylesheet" href="/static/vendor/highlight.js/styles/github.css" media="(prefers-color-scheme: light)" />
|
|
61
|
-
<link rel="stylesheet" href="/static/vendor/highlight.js/styles/github-dark.css" media="(prefers-color-scheme: dark)" />
|
|
62
|
-
<link rel="stylesheet" href="/static/vendor/katex/katex.min.css" />
|
|
63
|
-
<link rel="stylesheet" href="/static/app.css" />
|
|
64
|
-
</head>
|
|
65
|
-
<body>
|
|
66
|
-
<header class="topbar">
|
|
67
|
-
<div class="topbar-row">
|
|
68
|
-
<a class="brand" href="/tree/">${escapeHtml(repoName)}</a>
|
|
69
|
-
<div class="meta">
|
|
70
|
-
<span class="pill">${branch}</span>
|
|
71
|
-
${commit ? `<span class="pill mono">${commit}</span>` : ""}
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
${renderBreadcrumbs(relPathPosix, "")}
|
|
75
|
-
</header>
|
|
76
|
-
<main class="container">
|
|
77
|
-
${bodyHtml}
|
|
78
|
-
</main>
|
|
79
|
-
<script defer src="/static/vendor/katex/katex.min.js"></script>
|
|
80
|
-
<script defer src="/static/vendor/katex/contrib/auto-render.min.js"></script>
|
|
81
|
-
<script type="module" src="/static/app.js"></script>
|
|
82
|
-
</body>
|
|
83
|
-
</html>`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function renderBrokenLinksPill(brokenLinks, querySuffix) {
|
|
87
|
-
const state = brokenLinks;
|
|
88
|
-
if (!state) return "";
|
|
89
|
-
const status = state.status;
|
|
90
|
-
const count = state.lastResult?.broken?.length ?? 0;
|
|
91
|
-
const href = `/broken-links${querySuffix || ""}`;
|
|
92
|
-
if (status === "running") return `<a class="pill link" href="${href}">Scanning links…</a>`;
|
|
93
|
-
if (state.lastResult) {
|
|
94
|
-
return `<a class="pill link" href="${href}">Broken: ${count}</a>`;
|
|
95
|
-
}
|
|
96
|
-
if (state.lastError) return `<a class="pill link" href="${href}">Broken: ?</a>`;
|
|
97
|
-
return "";
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function renderIgnoredTogglePill({ toggleIgnoredHref, showIgnored }) {
|
|
101
|
-
const href = toggleIgnoredHref || "#";
|
|
102
|
-
const label = showIgnored ? "Ignored: on" : "Ignored: off";
|
|
103
|
-
return `<a class="pill link" data-no-preserve="ignored" href="${href}">${label}</a>`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function renderMetaMenu({ gitInfo, brokenLinks, querySuffix, toggleIgnoredHref, showIgnored }) {
|
|
107
|
-
const commit = gitInfo?.commit ? escapeHtml(gitInfo.commit.slice(0, 7)) : "";
|
|
108
|
-
const brokenState = brokenLinks;
|
|
109
|
-
const brokenCount = brokenState?.lastResult?.broken?.length ?? null;
|
|
110
|
-
const brokenLabel =
|
|
111
|
-
brokenState?.status === "running"
|
|
112
|
-
? "Broken links: scanning…"
|
|
113
|
-
: brokenCount == null
|
|
114
|
-
? "Broken links"
|
|
115
|
-
: `Broken links: ${brokenCount}`;
|
|
116
|
-
const brokenHref = `/broken-links${querySuffix || ""}`;
|
|
117
|
-
const ignoredHref = toggleIgnoredHref || "#";
|
|
118
|
-
const ignoredLabel = showIgnored ? "Hide ignored files" : "Show ignored files";
|
|
119
|
-
|
|
120
|
-
const diffHref = `/diff${querySuffix || ""}`;
|
|
121
|
-
|
|
122
|
-
return `<details class="meta-menu">
|
|
123
|
-
<summary class="pill link" aria-label="More">More</summary>
|
|
124
|
-
<div class="menu-panel" role="menu">
|
|
125
|
-
<a class="menu-item link" href="${diffHref}" role="menuitem">Diff view</a>
|
|
126
|
-
<a class="menu-item link" href="/review/" role="menuitem">Reviews</a>
|
|
127
|
-
<a class="menu-item link" href="${brokenHref}" role="menuitem">${escapeHtml(brokenLabel)}</a>
|
|
128
|
-
<a class="menu-item link" data-no-preserve="ignored" href="${ignoredHref}" role="menuitem">${escapeHtml(
|
|
129
|
-
ignoredLabel,
|
|
130
|
-
)}</a>
|
|
131
|
-
${commit ? `<div class="menu-item mono" role="menuitem">Commit: ${commit}</div>` : ""}
|
|
132
|
-
</div>
|
|
133
|
-
</details>`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function pageTemplateWithLinks({
|
|
137
|
-
title,
|
|
138
|
-
repoName,
|
|
139
|
-
gitInfo,
|
|
140
|
-
relPathPosix,
|
|
141
|
-
bodyHtml,
|
|
142
|
-
brokenLinks,
|
|
143
|
-
querySuffix,
|
|
144
|
-
toggleIgnoredHref,
|
|
145
|
-
showIgnored,
|
|
146
|
-
}) {
|
|
147
|
-
const branch = gitInfo?.branch ? escapeHtml(gitInfo.branch) : "no-git";
|
|
148
|
-
const commit = gitInfo?.commit ? escapeHtml(gitInfo.commit.slice(0, 7)) : "";
|
|
149
|
-
const brokenPill = renderBrokenLinksPill(brokenLinks, querySuffix);
|
|
150
|
-
const ignoredPill = renderIgnoredTogglePill({ toggleIgnoredHref, showIgnored });
|
|
151
|
-
const metaMenu = renderMetaMenu({ gitInfo, brokenLinks, querySuffix, toggleIgnoredHref, showIgnored });
|
|
152
|
-
return `<!doctype html>
|
|
153
|
-
<html lang="en">
|
|
154
|
-
<head>
|
|
155
|
-
<meta charset="utf-8" />
|
|
156
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
157
|
-
<title>${escapeHtml(title)}</title>
|
|
158
|
-
<link rel="stylesheet" href="/static/vendor/github-markdown-css/github-markdown.css" />
|
|
159
|
-
<link rel="stylesheet" href="/static/vendor/highlight.js/styles/github.css" media="(prefers-color-scheme: light)" />
|
|
160
|
-
<link rel="stylesheet" href="/static/vendor/highlight.js/styles/github-dark.css" media="(prefers-color-scheme: dark)" />
|
|
161
|
-
<link rel="stylesheet" href="/static/vendor/katex/katex.min.css" />
|
|
162
|
-
<link rel="stylesheet" href="/static/app.css" />
|
|
163
|
-
</head>
|
|
164
|
-
<body>
|
|
165
|
-
<header class="topbar">
|
|
166
|
-
<div class="topbar-row">
|
|
167
|
-
<a class="brand" href="/tree/${querySuffix || ""}">${escapeHtml(repoName)}</a>
|
|
168
|
-
<div class="meta">
|
|
169
|
-
<span class="pill">${branch}</span>
|
|
170
|
-
${commit ? `<span class="pill mono meta-commit">${commit}</span>` : ""}
|
|
171
|
-
<span class="meta-actions">
|
|
172
|
-
<a class="pill link" href="/diff${querySuffix || ""}">Diff</a>
|
|
173
|
-
<a class="pill link" href="/review/">Review</a>
|
|
174
|
-
${brokenPill}
|
|
175
|
-
${ignoredPill}
|
|
176
|
-
</span>
|
|
177
|
-
<span id="conn-status" class="conn-status" title="Live reload: connecting..."></span>
|
|
178
|
-
${metaMenu}
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
${renderBreadcrumbs(relPathPosix, querySuffix)}
|
|
182
|
-
</header>
|
|
183
|
-
<main class="container">
|
|
184
|
-
${bodyHtml}
|
|
185
|
-
</main>
|
|
186
|
-
<script defer src="/static/vendor/katex/katex.min.js"></script>
|
|
187
|
-
<script defer src="/static/vendor/katex/contrib/auto-render.min.js"></script>
|
|
188
|
-
<script type="module" src="/static/app.js"></script>
|
|
189
|
-
</body>
|
|
190
|
-
</html>`;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export function renderTreePage({
|
|
194
|
-
title,
|
|
195
|
-
repoName,
|
|
196
|
-
gitInfo,
|
|
197
|
-
brokenLinks,
|
|
198
|
-
querySuffix,
|
|
199
|
-
toggleIgnoredHref,
|
|
200
|
-
showIgnored,
|
|
201
|
-
relPathPosix,
|
|
202
|
-
rows,
|
|
203
|
-
readmeHtml,
|
|
204
|
-
}) {
|
|
205
|
-
const tableRows = rows
|
|
206
|
-
.map((r) => {
|
|
207
|
-
const icon = r.isDir ? "dir" : "file";
|
|
208
|
-
const name = escapeHtml(r.name);
|
|
209
|
-
const tsAttr = r.mtimeMs ? ` data-ts="${r.mtimeMs}"` : "";
|
|
210
|
-
return `<tr>
|
|
211
|
-
<td class="name"><a class="item ${icon}" href="${r.href}">${name}</a></td>
|
|
212
|
-
<td class="mtime"${tsAttr}>${escapeHtml(r.mtime)}</td>
|
|
213
|
-
<td class="size">${escapeHtml(r.size)}</td>
|
|
214
|
-
</tr>`;
|
|
215
|
-
})
|
|
216
|
-
.join("\n");
|
|
217
|
-
|
|
218
|
-
const readmeSection = readmeHtml
|
|
219
|
-
? `<section class="panel readme">
|
|
220
|
-
<div class="panel-title">README</div>
|
|
221
|
-
<div class="markdown-body markdown-wrap">${readmeHtml}</div>
|
|
222
|
-
</section>`
|
|
223
|
-
: "";
|
|
224
|
-
|
|
225
|
-
const body = `<section class="panel">
|
|
226
|
-
<div class="panel-title">Files</div>
|
|
227
|
-
<div class="table-wrap">
|
|
228
|
-
<table class="file-table">
|
|
229
|
-
<thead>
|
|
230
|
-
<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>
|
|
231
|
-
</thead>
|
|
232
|
-
<tbody>
|
|
233
|
-
${tableRows || `<tr><td colspan="3" class="empty">Empty directory</td></tr>`}
|
|
234
|
-
</tbody>
|
|
235
|
-
</table>
|
|
236
|
-
</div>
|
|
237
|
-
</section>
|
|
238
|
-
${readmeSection}`;
|
|
239
|
-
|
|
240
|
-
return pageTemplateWithLinks({
|
|
241
|
-
title,
|
|
242
|
-
repoName,
|
|
243
|
-
gitInfo,
|
|
244
|
-
brokenLinks,
|
|
245
|
-
toggleIgnoredHref,
|
|
246
|
-
showIgnored,
|
|
247
|
-
querySuffix,
|
|
248
|
-
relPathPosix,
|
|
249
|
-
bodyHtml: body,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export function renderFilePage({
|
|
254
|
-
title,
|
|
255
|
-
repoName,
|
|
256
|
-
gitInfo,
|
|
257
|
-
brokenLinks,
|
|
258
|
-
querySuffix,
|
|
259
|
-
toggleIgnoredHref,
|
|
260
|
-
showIgnored,
|
|
261
|
-
relPathPosix,
|
|
262
|
-
fileName,
|
|
263
|
-
isMarkdown,
|
|
264
|
-
mediaType,
|
|
265
|
-
renderedHtml,
|
|
266
|
-
}) {
|
|
267
|
-
const relDir = path.posix.dirname(relPathPosix || "");
|
|
268
|
-
const suffix = querySuffix || "";
|
|
269
|
-
const rawHref = `/raw/${encodePathForUrl(relPathPosix || "")}${suffix}`;
|
|
270
|
-
const treeHref = `/tree/${encodePathForUrl(relDir === "." ? "" : relDir)}${suffix}`;
|
|
271
|
-
|
|
272
|
-
const wrapClass = mediaType ? `${mediaType}-wrap` : isMarkdown ? "markdown-body markdown-wrap" : "code-wrap";
|
|
273
|
-
const body = `<section class="panel">
|
|
274
|
-
<div class="panel-title">
|
|
275
|
-
<span class="filename">${escapeHtml(fileName)}</span>
|
|
276
|
-
<span class="spacer"></span>
|
|
277
|
-
<a class="btn" href="${treeHref}">Back</a>
|
|
278
|
-
<a class="btn" href="${rawHref}">Raw</a>
|
|
279
|
-
</div>
|
|
280
|
-
<div class="${wrapClass}">
|
|
281
|
-
${renderedHtml}
|
|
282
|
-
</div>
|
|
283
|
-
</section>`;
|
|
284
|
-
|
|
285
|
-
return pageTemplateWithLinks({
|
|
286
|
-
title,
|
|
287
|
-
repoName,
|
|
288
|
-
gitInfo,
|
|
289
|
-
brokenLinks,
|
|
290
|
-
toggleIgnoredHref,
|
|
291
|
-
showIgnored,
|
|
292
|
-
querySuffix,
|
|
293
|
-
relPathPosix,
|
|
294
|
-
bodyHtml: body,
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export function renderDiffPage({
|
|
299
|
-
title,
|
|
300
|
-
repoName,
|
|
301
|
-
gitInfo,
|
|
302
|
-
relPathPosix,
|
|
303
|
-
querySuffix,
|
|
304
|
-
base,
|
|
305
|
-
branches,
|
|
306
|
-
tags,
|
|
307
|
-
diffHtml,
|
|
308
|
-
tooLarge,
|
|
309
|
-
empty,
|
|
310
|
-
fileCount,
|
|
311
|
-
showAll,
|
|
312
|
-
}) {
|
|
313
|
-
const branchOptions = branches
|
|
314
|
-
.map((b) => {
|
|
315
|
-
const sel = b === base ? " selected" : "";
|
|
316
|
-
return `<option value="${escapeHtml(b)}"${sel}>${escapeHtml(b)}</option>`;
|
|
317
|
-
})
|
|
318
|
-
.join("\n");
|
|
319
|
-
const tagOptions = tags
|
|
320
|
-
.map((t) => {
|
|
321
|
-
const sel = t === base ? " selected" : "";
|
|
322
|
-
return `<option value="${escapeHtml(t)}"${sel}>${escapeHtml(t)}</option>`;
|
|
323
|
-
})
|
|
324
|
-
.join("\n");
|
|
325
|
-
const headSelected = base === "HEAD" ? " selected" : "";
|
|
326
|
-
|
|
327
|
-
const selector = `<select id="base-selector" class="base-selector">
|
|
328
|
-
<option value="HEAD"${headSelected}>HEAD</option>
|
|
329
|
-
${branches.length ? `<optgroup label="Branches">${branchOptions}</optgroup>` : ""}
|
|
330
|
-
${tags.length ? `<optgroup label="Tags">${tagOptions}</optgroup>` : ""}
|
|
331
|
-
</select>`;
|
|
332
|
-
|
|
333
|
-
let content = "";
|
|
334
|
-
if (tooLarge) {
|
|
335
|
-
content = `<div class="diff-empty note">Diff output exceeded 512KB and was truncated. Try narrowing the comparison range.</div>`;
|
|
336
|
-
} else if (empty) {
|
|
337
|
-
content = `<div class="diff-empty note">No changes found.</div>`;
|
|
338
|
-
} else {
|
|
339
|
-
content = diffHtml;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const MAX_DIFF_FILES = 30;
|
|
343
|
-
let truncatedMsg = "";
|
|
344
|
-
if (fileCount > MAX_DIFF_FILES && !showAll) {
|
|
345
|
-
const hidden = fileCount - MAX_DIFF_FILES;
|
|
346
|
-
const showAllQuery = new URLSearchParams(querySuffix ? querySuffix.slice(1) : "");
|
|
347
|
-
showAllQuery.set("show_all", "1");
|
|
348
|
-
const showAllHref = `/diff?${showAllQuery.toString()}`;
|
|
349
|
-
truncatedMsg = `<div class="diff-truncated note">${hidden} more file${hidden === 1 ? "" : "s"} not shown. <a class="link" href="${showAllHref}">Show all ${fileCount} files</a></div>`;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const body = `<section class="panel">
|
|
353
|
-
<div class="panel-title">
|
|
354
|
-
<span>Compare working tree against</span>
|
|
355
|
-
${selector}
|
|
356
|
-
<span class="spacer"></span>
|
|
357
|
-
<a class="btn" href="/tree/${querySuffix || ""}">Back</a>
|
|
358
|
-
</div>
|
|
359
|
-
<div class="diff-wrap">
|
|
360
|
-
${content}
|
|
361
|
-
${truncatedMsg}
|
|
362
|
-
</div>
|
|
363
|
-
</section>`;
|
|
364
|
-
|
|
365
|
-
const branch = gitInfo?.branch ? escapeHtml(gitInfo.branch) : "no-git";
|
|
366
|
-
const commit = gitInfo?.commit ? escapeHtml(gitInfo.commit.slice(0, 7)) : "";
|
|
367
|
-
return `<!doctype html>
|
|
368
|
-
<html lang="en">
|
|
369
|
-
<head>
|
|
370
|
-
<meta charset="utf-8" />
|
|
371
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
372
|
-
<title>${escapeHtml(title)}</title>
|
|
373
|
-
<link rel="stylesheet" href="/static/vendor/diff2html/diff2html.min.css" />
|
|
374
|
-
<link rel="stylesheet" href="/static/app.css" />
|
|
375
|
-
</head>
|
|
376
|
-
<body>
|
|
377
|
-
<header class="topbar">
|
|
378
|
-
<div class="topbar-row">
|
|
379
|
-
<a class="brand" href="/tree/${querySuffix || ""}">${escapeHtml(repoName)}</a>
|
|
380
|
-
<div class="meta">
|
|
381
|
-
<span class="pill">${branch}</span>
|
|
382
|
-
${commit ? `<span class="pill mono">${commit}</span>` : ""}
|
|
383
|
-
</div>
|
|
384
|
-
</div>
|
|
385
|
-
</header>
|
|
386
|
-
<main class="container">
|
|
387
|
-
${body}
|
|
388
|
-
</main>
|
|
389
|
-
<script type="module" src="/static/app.js"></script>
|
|
390
|
-
</body>
|
|
391
|
-
</html>`;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export function renderReviewListPage({
|
|
395
|
-
title,
|
|
396
|
-
repoName,
|
|
397
|
-
gitInfo,
|
|
398
|
-
threads,
|
|
399
|
-
}) {
|
|
400
|
-
const branch = gitInfo?.branch ? escapeHtml(gitInfo.branch) : "no-git";
|
|
401
|
-
const commit = gitInfo?.commit ? escapeHtml(gitInfo.commit.slice(0, 7)) : "";
|
|
402
|
-
|
|
403
|
-
const threadRows = threads.length
|
|
404
|
-
? threads
|
|
405
|
-
.map((t) => {
|
|
406
|
-
const unread = t.readUntil
|
|
407
|
-
? t.lastMessageId && t.lastMessageId > t.readUntil
|
|
408
|
-
: t.messageCount > 0;
|
|
409
|
-
const badge = unread ? `<span class="review-unread-badge">${t.unreadCount || "new"}</span>` : "";
|
|
410
|
-
const timeAgo = escapeHtml(formatReviewTime(t.lastActivityAt || t.createdAt));
|
|
411
|
-
return `<a class="review-thread-row" href="/review/${encodeURIComponent(t.id)}">
|
|
412
|
-
<div class="review-thread-info">
|
|
413
|
-
<span class="review-thread-title">${escapeHtml(t.title)}${badge}</span>
|
|
414
|
-
<span class="review-thread-meta">${t.messageCount} message${t.messageCount !== 1 ? "s" : ""} · ${timeAgo}</span>
|
|
415
|
-
</div>
|
|
416
|
-
<span class="review-thread-arrow">›</span>
|
|
417
|
-
</a>`;
|
|
418
|
-
})
|
|
419
|
-
.join("\n")
|
|
420
|
-
: `<div class="review-empty">No review threads yet.</div>`;
|
|
421
|
-
|
|
422
|
-
const body = `<section class="panel">
|
|
423
|
-
<div class="panel-title">Review Threads</div>
|
|
424
|
-
<div class="review-thread-list">
|
|
425
|
-
${threadRows}
|
|
426
|
-
</div>
|
|
427
|
-
</section>`;
|
|
428
|
-
|
|
429
|
-
return `<!doctype html>
|
|
430
|
-
<html lang="en">
|
|
431
|
-
<head>
|
|
432
|
-
<meta charset="utf-8" />
|
|
433
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
|
434
|
-
<title>${escapeHtml(title)}</title>
|
|
435
|
-
<link rel="stylesheet" href="/static/app.css" />
|
|
436
|
-
</head>
|
|
437
|
-
<body>
|
|
438
|
-
<header class="topbar">
|
|
439
|
-
<div class="topbar-row">
|
|
440
|
-
<a class="brand" href="/tree/">${escapeHtml(repoName)}</a>
|
|
441
|
-
<div class="meta">
|
|
442
|
-
<span class="pill">${branch}</span>
|
|
443
|
-
${commit ? `<span class="pill mono">${commit}</span>` : ""}
|
|
444
|
-
</div>
|
|
445
|
-
</div>
|
|
446
|
-
</header>
|
|
447
|
-
<main class="container">
|
|
448
|
-
${body}
|
|
449
|
-
</main>
|
|
450
|
-
<script type="module" src="/static/app.js"></script>
|
|
451
|
-
</body>
|
|
452
|
-
</html>`;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function renderCommentCard(c, msgId) {
|
|
456
|
-
return `<div class="review-comment-card${c.resolved ? " resolved" : ""}" data-comment-id="${escapeHtml(c.id)}" data-message-id="${escapeHtml(msgId)}" data-anchor-line="${c.anchorLine || ""}" data-anchor-end-line="${c.anchorEndLine || ""}">
|
|
457
|
-
<div class="review-comment-header">
|
|
458
|
-
<span class="review-comment-anchor">${c.anchorLine ? `Line ${c.anchorLine}${c.anchorEndLine && c.anchorEndLine !== c.anchorLine ? `-${c.anchorEndLine}` : ""}` : "General"}</span>
|
|
459
|
-
<span class="review-comment-time" title="${escapeHtml(c.createdAt)}">${escapeHtml(formatReviewTime(c.createdAt))}</span>
|
|
460
|
-
<span class="review-comment-actions">
|
|
461
|
-
${!c.resolved ? `<button class="btn btn-sm review-resolve-btn" data-comment-id="${escapeHtml(c.id)}">Resolve</button>` : `<span class="review-resolved-label">Resolved</span>`}
|
|
462
|
-
<button class="btn btn-sm review-delete-comment-btn" data-comment-id="${escapeHtml(c.id)}">Delete</button>
|
|
463
|
-
</span>
|
|
464
|
-
</div>
|
|
465
|
-
<div class="review-comment-body">${escapeHtml(c.body)}</div>
|
|
466
|
-
</div>`;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
export function renderReviewThreadPage({
|
|
470
|
-
title,
|
|
471
|
-
repoName,
|
|
472
|
-
gitInfo,
|
|
473
|
-
thread,
|
|
474
|
-
messages,
|
|
475
|
-
comments,
|
|
476
|
-
renderedMessages,
|
|
477
|
-
}) {
|
|
478
|
-
const branch = gitInfo?.branch ? escapeHtml(gitInfo.branch) : "no-git";
|
|
479
|
-
const commit = gitInfo?.commit ? escapeHtml(gitInfo.commit.slice(0, 7)) : "";
|
|
480
|
-
|
|
481
|
-
const messageBlocks = messages
|
|
482
|
-
.map((msg, idx) => {
|
|
483
|
-
const isAgent = msg.role === "agent";
|
|
484
|
-
const roleClass = isAgent ? "review-msg-agent" : "review-msg-user";
|
|
485
|
-
const roleLabel = isAgent ? "Agent" : "You";
|
|
486
|
-
const rendered = renderedMessages[idx];
|
|
487
|
-
|
|
488
|
-
// Gather comments for this message
|
|
489
|
-
const msgComments = comments.filter((c) => c.messageId === msg.id);
|
|
490
|
-
const commentHtml = msgComments.length
|
|
491
|
-
? `<div class="review-inline-comments">${msgComments.map((c) => renderCommentCard(c, msg.id)).join("\n")}</div>`
|
|
492
|
-
: "";
|
|
493
|
-
|
|
494
|
-
const contentWrapper = isAgent
|
|
495
|
-
? `<div class="markdown-body markdown-wrap review-msg-content" data-message-id="${escapeHtml(msg.id)}">${rendered}</div>`
|
|
496
|
-
: `<div class="review-msg-content review-msg-text" data-message-id="${escapeHtml(msg.id)}">${escapeHtml(rendered)}</div>`;
|
|
497
|
-
|
|
498
|
-
return `<div class="review-message ${roleClass}" data-message-id="${escapeHtml(msg.id)}">
|
|
499
|
-
<div class="review-msg-header">
|
|
500
|
-
<span class="review-msg-role">${roleLabel}</span>
|
|
501
|
-
<span class="review-msg-time" title="${escapeHtml(msg.createdAt)}">${escapeHtml(formatReviewTime(msg.createdAt))}</span>
|
|
502
|
-
</div>
|
|
503
|
-
${contentWrapper}
|
|
504
|
-
${commentHtml}
|
|
505
|
-
</div>`;
|
|
506
|
-
})
|
|
507
|
-
.join("\n");
|
|
508
|
-
|
|
509
|
-
const body = `<section class="panel review-thread-panel">
|
|
510
|
-
<div class="panel-title review-thread-header">
|
|
511
|
-
<a class="btn" href="/review/">← Back</a>
|
|
512
|
-
<span class="review-thread-title-text">${escapeHtml(thread.title)}</span>
|
|
513
|
-
<span class="spacer"></span>
|
|
514
|
-
</div>
|
|
515
|
-
<div class="review-messages">
|
|
516
|
-
${messageBlocks || `<div class="review-empty">No messages yet.</div>`}
|
|
517
|
-
</div>
|
|
518
|
-
<div class="review-reply-form">
|
|
519
|
-
<textarea id="review-reply-text" class="review-reply-textarea" placeholder="Write a reply..." rows="4"></textarea>
|
|
520
|
-
<button id="review-reply-submit" class="btn review-reply-btn" data-thread-id="${escapeHtml(thread.id)}">Submit Reply</button>
|
|
521
|
-
</div>
|
|
522
|
-
</section>`;
|
|
523
|
-
|
|
524
|
-
return `<!doctype html>
|
|
525
|
-
<html lang="en">
|
|
526
|
-
<head>
|
|
527
|
-
<meta charset="utf-8" />
|
|
528
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
|
529
|
-
<title>${escapeHtml(title)}</title>
|
|
530
|
-
<link rel="stylesheet" href="/static/vendor/github-markdown-css/github-markdown.css" />
|
|
531
|
-
<link rel="stylesheet" href="/static/vendor/highlight.js/styles/github.css" media="(prefers-color-scheme: light)" />
|
|
532
|
-
<link rel="stylesheet" href="/static/vendor/highlight.js/styles/github-dark.css" media="(prefers-color-scheme: dark)" />
|
|
533
|
-
<link rel="stylesheet" href="/static/vendor/katex/katex.min.css" />
|
|
534
|
-
<link rel="stylesheet" href="/static/app.css" />
|
|
535
|
-
</head>
|
|
536
|
-
<body>
|
|
537
|
-
<header class="topbar">
|
|
538
|
-
<div class="topbar-row">
|
|
539
|
-
<a class="brand" href="/tree/">${escapeHtml(repoName)}</a>
|
|
540
|
-
<div class="meta">
|
|
541
|
-
<span class="pill">${branch}</span>
|
|
542
|
-
${commit ? `<span class="pill mono">${commit}</span>` : ""}
|
|
543
|
-
<span id="conn-status" class="conn-status" title="Live reload: connecting..."></span>
|
|
544
|
-
</div>
|
|
545
|
-
</div>
|
|
546
|
-
</header>
|
|
547
|
-
<main class="container">
|
|
548
|
-
${body}
|
|
549
|
-
</main>
|
|
550
|
-
<script defer src="/static/vendor/katex/katex.min.js"></script>
|
|
551
|
-
<script defer src="/static/vendor/katex/contrib/auto-render.min.js"></script>
|
|
552
|
-
<script type="module" src="/static/app.js"></script>
|
|
553
|
-
<script type="module" src="/static/review.js"></script>
|
|
554
|
-
</body>
|
|
555
|
-
</html>`;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
export function renderErrorPage({ title, message }) {
|
|
559
|
-
return `<!doctype html>
|
|
560
|
-
<html lang="en">
|
|
561
|
-
<head>
|
|
562
|
-
<meta charset="utf-8" />
|
|
563
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
564
|
-
<title>${escapeHtml(title)}</title>
|
|
565
|
-
<link rel="stylesheet" href="/static/app.css" />
|
|
566
|
-
</head>
|
|
567
|
-
<body>
|
|
568
|
-
<main class="container">
|
|
569
|
-
<section class="panel">
|
|
570
|
-
<div class="panel-title">Error</div>
|
|
571
|
-
<div class="error">${escapeHtml(message)}</div>
|
|
572
|
-
</section>
|
|
573
|
-
</main>
|
|
574
|
-
</body>
|
|
575
|
-
</html>`;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
export function renderBrokenLinksPage({
|
|
579
|
-
title,
|
|
580
|
-
repoName,
|
|
581
|
-
gitInfo,
|
|
582
|
-
relPathPosix,
|
|
583
|
-
scanState,
|
|
584
|
-
querySuffix,
|
|
585
|
-
toggleIgnoredHref,
|
|
586
|
-
showIgnored,
|
|
587
|
-
}) {
|
|
588
|
-
const state = scanState || {};
|
|
589
|
-
const result = state.lastResult;
|
|
590
|
-
const broken = result?.broken || [];
|
|
591
|
-
const statusLine =
|
|
592
|
-
state.status === "running"
|
|
593
|
-
? "Scanning…"
|
|
594
|
-
: result
|
|
595
|
-
? `Last scan: ${new Date(result.finishedAt).toLocaleString()} · Files: ${
|
|
596
|
-
result.filesScanned
|
|
597
|
-
} · URLs: ${result.urlsChecked} · Broken: ${broken.length} · ${result.durationMs}ms`
|
|
598
|
-
: state.lastError
|
|
599
|
-
? `Last error: ${escapeHtml(state.lastError)}`
|
|
600
|
-
: "No scan yet.";
|
|
601
|
-
|
|
602
|
-
const grouped = new Map();
|
|
603
|
-
for (const b of broken) {
|
|
604
|
-
const arr = grouped.get(b.source) || [];
|
|
605
|
-
arr.push(b);
|
|
606
|
-
grouped.set(b.source, arr);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
const sections = Array.from(grouped.entries())
|
|
610
|
-
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
611
|
-
.map(([source, items]) => {
|
|
612
|
-
const sourceHref = `/blob/${source.split("/").map(encodeURIComponent).join("/")}${
|
|
613
|
-
querySuffix || ""
|
|
614
|
-
}`;
|
|
615
|
-
const rows = items
|
|
616
|
-
.map((i) => {
|
|
617
|
-
const reason = escapeHtml(i.reason || "");
|
|
618
|
-
const kind = escapeHtml(i.kind || "");
|
|
619
|
-
const url = escapeHtml(i.url || "");
|
|
620
|
-
const target = i.target ? escapeHtml(i.target) : "";
|
|
621
|
-
return `<tr><td class="mono">${kind}</td><td class="mono">${reason}</td><td class="mono">${url}</td><td class="mono">${target}</td></tr>`;
|
|
622
|
-
})
|
|
623
|
-
.join("\n");
|
|
624
|
-
return `<section class="panel">
|
|
625
|
-
<div class="panel-title">
|
|
626
|
-
<span class="filename"><a class="link" href="${sourceHref}">${escapeHtml(source)}</a></span>
|
|
627
|
-
<span class="spacer"></span>
|
|
628
|
-
<span class="pill">${items.length}</span>
|
|
629
|
-
</div>
|
|
630
|
-
<div class="table-wrap">
|
|
631
|
-
<table class="file-table linkcheck">
|
|
632
|
-
<thead><tr><th>Kind</th><th>Reason</th><th>URL</th><th>Target</th></tr></thead>
|
|
633
|
-
<tbody>${rows}</tbody>
|
|
634
|
-
</table>
|
|
635
|
-
</div>
|
|
636
|
-
</section>`;
|
|
637
|
-
})
|
|
638
|
-
.join("\n");
|
|
639
|
-
|
|
640
|
-
const body = `<section class="panel">
|
|
641
|
-
<div class="panel-title">Broken links</div>
|
|
642
|
-
<div class="note">${escapeHtml(statusLine)}</div>
|
|
643
|
-
</section>
|
|
644
|
-
${sections || `<section class="panel"><div class="panel-title">All good</div><div class="note">No broken internal links found.</div></section>`}`;
|
|
645
|
-
|
|
646
|
-
return pageTemplateWithLinks({
|
|
647
|
-
title,
|
|
648
|
-
repoName,
|
|
649
|
-
gitInfo,
|
|
650
|
-
brokenLinks: scanState,
|
|
651
|
-
toggleIgnoredHref,
|
|
652
|
-
showIgnored,
|
|
653
|
-
querySuffix,
|
|
654
|
-
relPathPosix,
|
|
655
|
-
bodyHtml: body,
|
|
656
|
-
});
|
|
657
|
-
}
|