repoview 0.5.0 → 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.
Files changed (51) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/CONTRIBUTING.md +4 -3
  3. package/DEVELOPMENT.md +70 -16
  4. package/README.md +36 -5
  5. package/dist/api.js +58 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/cli.js +224 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/csv.js +64 -0
  10. package/dist/csv.js.map +1 -0
  11. package/dist/format.js +25 -0
  12. package/dist/format.js.map +1 -0
  13. package/dist/git.js +67 -0
  14. package/dist/git.js.map +1 -0
  15. package/dist/gitignore.js +34 -0
  16. package/dist/gitignore.js.map +1 -0
  17. package/dist/linkcheck.js +310 -0
  18. package/dist/linkcheck.js.map +1 -0
  19. package/dist/markdown.js +493 -0
  20. package/dist/markdown.js.map +1 -0
  21. package/dist/net.js +10 -0
  22. package/dist/net.js.map +1 -0
  23. package/dist/paths.js +59 -0
  24. package/dist/paths.js.map +1 -0
  25. package/dist/reload.js +36 -0
  26. package/dist/reload.js.map +1 -0
  27. package/dist/repo-context.js +73 -0
  28. package/dist/repo-context.js.map +1 -0
  29. package/dist/repo-router.js +801 -0
  30. package/dist/repo-router.js.map +1 -0
  31. package/dist/review-cli.js +228 -0
  32. package/dist/review-cli.js.map +1 -0
  33. package/dist/server.js +116 -0
  34. package/dist/server.js.map +1 -0
  35. package/dist/session.js +86 -0
  36. package/dist/session.js.map +1 -0
  37. package/dist/types.js +2 -0
  38. package/dist/types.js.map +1 -0
  39. package/dist/views.js +633 -0
  40. package/dist/views.js.map +1 -0
  41. package/package.json +22 -6
  42. package/public/app.css +842 -0
  43. package/public/app.js +35 -2
  44. package/public/review.js +587 -0
  45. package/public/session.js +61 -0
  46. package/src/cli.js +0 -73
  47. package/src/gitignore.js +0 -34
  48. package/src/linkcheck.js +0 -312
  49. package/src/markdown.js +0 -364
  50. package/src/server.js +0 -760
  51. package/src/views.js +0 -479
package/src/views.js DELETED
@@ -1,479 +0,0 @@
1
- import path from "node:path";
2
-
3
- export function escapeHtml(s) {
4
- return String(s)
5
- .replaceAll("&", "&")
6
- .replaceAll("<", "&lt;")
7
- .replaceAll(">", "&gt;")
8
- .replaceAll('"', "&quot;")
9
- .replaceAll("'", "&#39;");
10
- }
11
-
12
- function encodePathForUrl(posixPath) {
13
- return posixPath
14
- .split("/")
15
- .map((segment) => encodeURIComponent(segment))
16
- .join("/");
17
- }
18
-
19
- function renderBreadcrumbs(relPathPosix, querySuffix) {
20
- const parts = (relPathPosix || "").split("/").filter(Boolean);
21
- const suffix = querySuffix || "";
22
- const crumbs = [{ name: "", href: `/tree/${suffix}` }];
23
- let cursor = "";
24
- for (const p of parts) {
25
- cursor = cursor ? `${cursor}/${p}` : p;
26
- crumbs.push({ name: p, href: `/tree/${encodePathForUrl(cursor)}${suffix}` });
27
- }
28
-
29
- const html = crumbs
30
- .map((c, idx) => {
31
- const label = idx === 0 ? "root" : escapeHtml(c.name);
32
- return `<a class="crumb" href="${c.href}">${label}</a>`;
33
- })
34
- .join(`<span class="crumb-sep">/</span>`);
35
- return `<nav class="breadcrumbs" aria-label="Breadcrumbs">${html}</nav>`;
36
- }
37
-
38
- function pageTemplate({ title, repoName, gitInfo, relPathPosix, bodyHtml }) {
39
- const branch = gitInfo?.branch ? escapeHtml(gitInfo.branch) : "no-git";
40
- const commit = gitInfo?.commit ? escapeHtml(gitInfo.commit.slice(0, 7)) : "";
41
- return `<!doctype html>
42
- <html lang="en">
43
- <head>
44
- <meta charset="utf-8" />
45
- <meta name="viewport" content="width=device-width, initial-scale=1" />
46
- <title>${escapeHtml(title)}</title>
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)" />
50
- <link rel="stylesheet" href="/static/vendor/katex/katex.min.css" />
51
- <link rel="stylesheet" href="/static/app.css" />
52
- </head>
53
- <body>
54
- <header class="topbar">
55
- <div class="topbar-row">
56
- <a class="brand" href="/tree/">${escapeHtml(repoName)}</a>
57
- <div class="meta">
58
- <span class="pill">${branch}</span>
59
- ${commit ? `<span class="pill mono">${commit}</span>` : ""}
60
- </div>
61
- </div>
62
- ${renderBreadcrumbs(relPathPosix, "")}
63
- </header>
64
- <main class="container">
65
- ${bodyHtml}
66
- </main>
67
- <script defer src="/static/vendor/katex/katex.min.js"></script>
68
- <script defer src="/static/vendor/katex/contrib/auto-render.min.js"></script>
69
- <script type="module" src="/static/app.js"></script>
70
- </body>
71
- </html>`;
72
- }
73
-
74
- function renderBrokenLinksPill(brokenLinks, querySuffix) {
75
- const state = brokenLinks;
76
- if (!state) return "";
77
- const status = state.status;
78
- const count = state.lastResult?.broken?.length ?? 0;
79
- const href = `/broken-links${querySuffix || ""}`;
80
- if (status === "running") return `<a class="pill link" href="${href}">Scanning links…</a>`;
81
- if (state.lastResult) {
82
- return `<a class="pill link" href="${href}">Broken: ${count}</a>`;
83
- }
84
- if (state.lastError) return `<a class="pill link" href="${href}">Broken: ?</a>`;
85
- return "";
86
- }
87
-
88
- function renderIgnoredTogglePill({ toggleIgnoredHref, showIgnored }) {
89
- const href = toggleIgnoredHref || "#";
90
- const label = showIgnored ? "Ignored: on" : "Ignored: off";
91
- return `<a class="pill link" data-no-preserve="ignored" href="${href}">${label}</a>`;
92
- }
93
-
94
- function renderMetaMenu({ gitInfo, brokenLinks, querySuffix, toggleIgnoredHref, showIgnored }) {
95
- const commit = gitInfo?.commit ? escapeHtml(gitInfo.commit.slice(0, 7)) : "";
96
- const brokenState = brokenLinks;
97
- const brokenCount = brokenState?.lastResult?.broken?.length ?? null;
98
- const brokenLabel =
99
- brokenState?.status === "running"
100
- ? "Broken links: scanning…"
101
- : brokenCount == null
102
- ? "Broken links"
103
- : `Broken links: ${brokenCount}`;
104
- const brokenHref = `/broken-links${querySuffix || ""}`;
105
- const ignoredHref = toggleIgnoredHref || "#";
106
- const ignoredLabel = showIgnored ? "Hide ignored files" : "Show ignored files";
107
-
108
- const diffHref = `/diff${querySuffix || ""}`;
109
-
110
- return `<details class="meta-menu">
111
- <summary class="pill link" aria-label="More">More</summary>
112
- <div class="menu-panel" role="menu">
113
- <a class="menu-item link" href="${diffHref}" role="menuitem">Diff view</a>
114
- <a class="menu-item link" href="${brokenHref}" role="menuitem">${escapeHtml(brokenLabel)}</a>
115
- <a class="menu-item link" data-no-preserve="ignored" href="${ignoredHref}" role="menuitem">${escapeHtml(
116
- ignoredLabel,
117
- )}</a>
118
- ${commit ? `<div class="menu-item mono" role="menuitem">Commit: ${commit}</div>` : ""}
119
- </div>
120
- </details>`;
121
- }
122
-
123
- function pageTemplateWithLinks({
124
- title,
125
- repoName,
126
- gitInfo,
127
- relPathPosix,
128
- bodyHtml,
129
- brokenLinks,
130
- querySuffix,
131
- toggleIgnoredHref,
132
- showIgnored,
133
- }) {
134
- const branch = gitInfo?.branch ? escapeHtml(gitInfo.branch) : "no-git";
135
- const commit = gitInfo?.commit ? escapeHtml(gitInfo.commit.slice(0, 7)) : "";
136
- const brokenPill = renderBrokenLinksPill(brokenLinks, querySuffix);
137
- const ignoredPill = renderIgnoredTogglePill({ toggleIgnoredHref, showIgnored });
138
- const metaMenu = renderMetaMenu({ gitInfo, brokenLinks, querySuffix, toggleIgnoredHref, showIgnored });
139
- return `<!doctype html>
140
- <html lang="en">
141
- <head>
142
- <meta charset="utf-8" />
143
- <meta name="viewport" content="width=device-width, initial-scale=1" />
144
- <title>${escapeHtml(title)}</title>
145
- <link rel="stylesheet" href="/static/vendor/github-markdown-css/github-markdown.css" />
146
- <link rel="stylesheet" href="/static/vendor/highlight.js/styles/github.css" media="(prefers-color-scheme: light)" />
147
- <link rel="stylesheet" href="/static/vendor/highlight.js/styles/github-dark.css" media="(prefers-color-scheme: dark)" />
148
- <link rel="stylesheet" href="/static/vendor/katex/katex.min.css" />
149
- <link rel="stylesheet" href="/static/app.css" />
150
- </head>
151
- <body>
152
- <header class="topbar">
153
- <div class="topbar-row">
154
- <a class="brand" href="/tree/${querySuffix || ""}">${escapeHtml(repoName)}</a>
155
- <div class="meta">
156
- <span class="pill">${branch}</span>
157
- ${commit ? `<span class="pill mono meta-commit">${commit}</span>` : ""}
158
- <span class="meta-actions">
159
- <a class="pill link" href="/diff${querySuffix || ""}">Diff</a>
160
- ${brokenPill}
161
- ${ignoredPill}
162
- </span>
163
- <span id="conn-status" class="conn-status" title="Live reload: connecting..."></span>
164
- ${metaMenu}
165
- </div>
166
- </div>
167
- ${renderBreadcrumbs(relPathPosix, querySuffix)}
168
- </header>
169
- <main class="container">
170
- ${bodyHtml}
171
- </main>
172
- <script defer src="/static/vendor/katex/katex.min.js"></script>
173
- <script defer src="/static/vendor/katex/contrib/auto-render.min.js"></script>
174
- <script type="module" src="/static/app.js"></script>
175
- </body>
176
- </html>`;
177
- }
178
-
179
- export function renderTreePage({
180
- title,
181
- repoName,
182
- gitInfo,
183
- brokenLinks,
184
- querySuffix,
185
- toggleIgnoredHref,
186
- showIgnored,
187
- relPathPosix,
188
- rows,
189
- readmeHtml,
190
- }) {
191
- const tableRows = rows
192
- .map((r) => {
193
- const icon = r.isDir ? "dir" : "file";
194
- const name = escapeHtml(r.name);
195
- const tsAttr = r.mtimeMs ? ` data-ts="${r.mtimeMs}"` : "";
196
- return `<tr>
197
- <td class="name"><a class="item ${icon}" href="${r.href}">${name}</a></td>
198
- <td class="mtime"${tsAttr}>${escapeHtml(r.mtime)}</td>
199
- <td class="size">${escapeHtml(r.size)}</td>
200
- </tr>`;
201
- })
202
- .join("\n");
203
-
204
- const readmeSection = readmeHtml
205
- ? `<section class="panel readme">
206
- <div class="panel-title">README</div>
207
- <div class="markdown-body markdown-wrap">${readmeHtml}</div>
208
- </section>`
209
- : "";
210
-
211
- const body = `<section class="panel">
212
- <div class="panel-title">Files</div>
213
- <div class="table-wrap">
214
- <table class="file-table">
215
- <thead>
216
- <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>
217
- </thead>
218
- <tbody>
219
- ${tableRows || `<tr><td colspan="3" class="empty">Empty directory</td></tr>`}
220
- </tbody>
221
- </table>
222
- </div>
223
- </section>
224
- ${readmeSection}`;
225
-
226
- return pageTemplateWithLinks({
227
- title,
228
- repoName,
229
- gitInfo,
230
- brokenLinks,
231
- toggleIgnoredHref,
232
- showIgnored,
233
- querySuffix,
234
- relPathPosix,
235
- bodyHtml: body,
236
- });
237
- }
238
-
239
- export function renderFilePage({
240
- title,
241
- repoName,
242
- gitInfo,
243
- brokenLinks,
244
- querySuffix,
245
- toggleIgnoredHref,
246
- showIgnored,
247
- relPathPosix,
248
- fileName,
249
- isMarkdown,
250
- mediaType,
251
- renderedHtml,
252
- }) {
253
- const relDir = path.posix.dirname(relPathPosix || "");
254
- const suffix = querySuffix || "";
255
- const rawHref = `/raw/${encodePathForUrl(relPathPosix || "")}${suffix}`;
256
- const treeHref = `/tree/${encodePathForUrl(relDir === "." ? "" : relDir)}${suffix}`;
257
-
258
- const wrapClass = mediaType ? `${mediaType}-wrap` : isMarkdown ? "markdown-body markdown-wrap" : "code-wrap";
259
- const body = `<section class="panel">
260
- <div class="panel-title">
261
- <span class="filename">${escapeHtml(fileName)}</span>
262
- <span class="spacer"></span>
263
- <a class="btn" href="${treeHref}">Back</a>
264
- <a class="btn" href="${rawHref}">Raw</a>
265
- </div>
266
- <div class="${wrapClass}">
267
- ${renderedHtml}
268
- </div>
269
- </section>`;
270
-
271
- return pageTemplateWithLinks({
272
- title,
273
- repoName,
274
- gitInfo,
275
- brokenLinks,
276
- toggleIgnoredHref,
277
- showIgnored,
278
- querySuffix,
279
- relPathPosix,
280
- bodyHtml: body,
281
- });
282
- }
283
-
284
- export function renderDiffPage({
285
- title,
286
- repoName,
287
- gitInfo,
288
- relPathPosix,
289
- querySuffix,
290
- base,
291
- branches,
292
- tags,
293
- diffHtml,
294
- tooLarge,
295
- empty,
296
- fileCount,
297
- showAll,
298
- }) {
299
- const branchOptions = branches
300
- .map((b) => {
301
- const sel = b === base ? " selected" : "";
302
- return `<option value="${escapeHtml(b)}"${sel}>${escapeHtml(b)}</option>`;
303
- })
304
- .join("\n");
305
- const tagOptions = tags
306
- .map((t) => {
307
- const sel = t === base ? " selected" : "";
308
- return `<option value="${escapeHtml(t)}"${sel}>${escapeHtml(t)}</option>`;
309
- })
310
- .join("\n");
311
- const headSelected = base === "HEAD" ? " selected" : "";
312
-
313
- const selector = `<select id="base-selector" class="base-selector">
314
- <option value="HEAD"${headSelected}>HEAD</option>
315
- ${branches.length ? `<optgroup label="Branches">${branchOptions}</optgroup>` : ""}
316
- ${tags.length ? `<optgroup label="Tags">${tagOptions}</optgroup>` : ""}
317
- </select>`;
318
-
319
- let content = "";
320
- if (tooLarge) {
321
- content = `<div class="diff-empty note">Diff output exceeded 512KB and was truncated. Try narrowing the comparison range.</div>`;
322
- } else if (empty) {
323
- content = `<div class="diff-empty note">No changes found.</div>`;
324
- } else {
325
- content = diffHtml;
326
- }
327
-
328
- const MAX_DIFF_FILES = 30;
329
- let truncatedMsg = "";
330
- if (fileCount > MAX_DIFF_FILES && !showAll) {
331
- const hidden = fileCount - MAX_DIFF_FILES;
332
- const showAllQuery = new URLSearchParams(querySuffix ? querySuffix.slice(1) : "");
333
- showAllQuery.set("show_all", "1");
334
- const showAllHref = `/diff?${showAllQuery.toString()}`;
335
- 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>`;
336
- }
337
-
338
- const body = `<section class="panel">
339
- <div class="panel-title">
340
- <span>Compare working tree against</span>
341
- ${selector}
342
- <span class="spacer"></span>
343
- <a class="btn" href="/tree/${querySuffix || ""}">Back</a>
344
- </div>
345
- <div class="diff-wrap">
346
- ${content}
347
- ${truncatedMsg}
348
- </div>
349
- </section>`;
350
-
351
- const branch = gitInfo?.branch ? escapeHtml(gitInfo.branch) : "no-git";
352
- const commit = gitInfo?.commit ? escapeHtml(gitInfo.commit.slice(0, 7)) : "";
353
- return `<!doctype html>
354
- <html lang="en">
355
- <head>
356
- <meta charset="utf-8" />
357
- <meta name="viewport" content="width=device-width, initial-scale=1" />
358
- <title>${escapeHtml(title)}</title>
359
- <link rel="stylesheet" href="/static/vendor/diff2html/diff2html.min.css" />
360
- <link rel="stylesheet" href="/static/app.css" />
361
- </head>
362
- <body>
363
- <header class="topbar">
364
- <div class="topbar-row">
365
- <a class="brand" href="/tree/${querySuffix || ""}">${escapeHtml(repoName)}</a>
366
- <div class="meta">
367
- <span class="pill">${branch}</span>
368
- ${commit ? `<span class="pill mono">${commit}</span>` : ""}
369
- </div>
370
- </div>
371
- </header>
372
- <main class="container">
373
- ${body}
374
- </main>
375
- <script type="module" src="/static/app.js"></script>
376
- </body>
377
- </html>`;
378
- }
379
-
380
- export function renderErrorPage({ title, message }) {
381
- return `<!doctype html>
382
- <html lang="en">
383
- <head>
384
- <meta charset="utf-8" />
385
- <meta name="viewport" content="width=device-width, initial-scale=1" />
386
- <title>${escapeHtml(title)}</title>
387
- <link rel="stylesheet" href="/static/app.css" />
388
- </head>
389
- <body>
390
- <main class="container">
391
- <section class="panel">
392
- <div class="panel-title">Error</div>
393
- <div class="error">${escapeHtml(message)}</div>
394
- </section>
395
- </main>
396
- </body>
397
- </html>`;
398
- }
399
-
400
- export function renderBrokenLinksPage({
401
- title,
402
- repoName,
403
- gitInfo,
404
- relPathPosix,
405
- scanState,
406
- querySuffix,
407
- toggleIgnoredHref,
408
- showIgnored,
409
- }) {
410
- const state = scanState || {};
411
- const result = state.lastResult;
412
- const broken = result?.broken || [];
413
- const statusLine =
414
- state.status === "running"
415
- ? "Scanning…"
416
- : result
417
- ? `Last scan: ${new Date(result.finishedAt).toLocaleString()} · Files: ${
418
- result.filesScanned
419
- } · URLs: ${result.urlsChecked} · Broken: ${broken.length} · ${result.durationMs}ms`
420
- : state.lastError
421
- ? `Last error: ${escapeHtml(state.lastError)}`
422
- : "No scan yet.";
423
-
424
- const grouped = new Map();
425
- for (const b of broken) {
426
- const arr = grouped.get(b.source) || [];
427
- arr.push(b);
428
- grouped.set(b.source, arr);
429
- }
430
-
431
- const sections = Array.from(grouped.entries())
432
- .sort((a, b) => a[0].localeCompare(b[0]))
433
- .map(([source, items]) => {
434
- const sourceHref = `/blob/${source.split("/").map(encodeURIComponent).join("/")}${
435
- querySuffix || ""
436
- }`;
437
- const rows = items
438
- .map((i) => {
439
- const reason = escapeHtml(i.reason || "");
440
- const kind = escapeHtml(i.kind || "");
441
- const url = escapeHtml(i.url || "");
442
- const target = i.target ? escapeHtml(i.target) : "";
443
- return `<tr><td class="mono">${kind}</td><td class="mono">${reason}</td><td class="mono">${url}</td><td class="mono">${target}</td></tr>`;
444
- })
445
- .join("\n");
446
- return `<section class="panel">
447
- <div class="panel-title">
448
- <span class="filename"><a class="link" href="${sourceHref}">${escapeHtml(source)}</a></span>
449
- <span class="spacer"></span>
450
- <span class="pill">${items.length}</span>
451
- </div>
452
- <div class="table-wrap">
453
- <table class="file-table linkcheck">
454
- <thead><tr><th>Kind</th><th>Reason</th><th>URL</th><th>Target</th></tr></thead>
455
- <tbody>${rows}</tbody>
456
- </table>
457
- </div>
458
- </section>`;
459
- })
460
- .join("\n");
461
-
462
- const body = `<section class="panel">
463
- <div class="panel-title">Broken links</div>
464
- <div class="note">${escapeHtml(statusLine)}</div>
465
- </section>
466
- ${sections || `<section class="panel"><div class="panel-title">All good</div><div class="note">No broken internal links found.</div></section>`}`;
467
-
468
- return pageTemplateWithLinks({
469
- title,
470
- repoName,
471
- gitInfo,
472
- brokenLinks: scanState,
473
- toggleIgnoredHref,
474
- showIgnored,
475
- querySuffix,
476
- relPathPosix,
477
- bodyHtml: body,
478
- });
479
- }