repoview 0.1.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/src/views.js ADDED
@@ -0,0 +1,373 @@
1
+ import path from "node:path";
2
+
3
+ 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-light.css" />
48
+ <link rel="stylesheet" href="/static/vendor/highlight.js/styles/github.css" />
49
+ <link rel="stylesheet" href="/static/vendor/katex/katex.min.css" />
50
+ <link rel="stylesheet" href="/static/app.css" />
51
+ </head>
52
+ <body>
53
+ <header class="topbar">
54
+ <div class="topbar-row">
55
+ <a class="brand" href="/tree/">${escapeHtml(repoName)}</a>
56
+ <div class="meta">
57
+ <span class="pill">${branch}</span>
58
+ ${commit ? `<span class="pill mono">${commit}</span>` : ""}
59
+ </div>
60
+ </div>
61
+ ${renderBreadcrumbs(relPathPosix, "")}
62
+ </header>
63
+ <main class="container">
64
+ ${bodyHtml}
65
+ </main>
66
+ <script defer src="/static/vendor/katex/katex.min.js"></script>
67
+ <script defer src="/static/vendor/katex/contrib/auto-render.min.js"></script>
68
+ <script type="module" src="/static/app.js"></script>
69
+ </body>
70
+ </html>`;
71
+ }
72
+
73
+ function renderBrokenLinksPill(brokenLinks, querySuffix) {
74
+ const state = brokenLinks;
75
+ if (!state) return "";
76
+ const status = state.status;
77
+ const count = state.lastResult?.broken?.length ?? 0;
78
+ const href = `/broken-links${querySuffix || ""}`;
79
+ if (status === "running") return `<a class="pill link" href="${href}">Scanning links…</a>`;
80
+ if (state.lastResult) {
81
+ return `<a class="pill link" href="${href}">Broken: ${count}</a>`;
82
+ }
83
+ if (state.lastError) return `<a class="pill link" href="${href}">Broken: ?</a>`;
84
+ return "";
85
+ }
86
+
87
+ function renderIgnoredTogglePill({ toggleIgnoredHref, showIgnored }) {
88
+ const href = toggleIgnoredHref || "#";
89
+ const label = showIgnored ? "Ignored: on" : "Ignored: off";
90
+ return `<a class="pill link" data-no-preserve="ignored" href="${href}">${label}</a>`;
91
+ }
92
+
93
+ function renderMetaMenu({ gitInfo, brokenLinks, querySuffix, toggleIgnoredHref, showIgnored }) {
94
+ const commit = gitInfo?.commit ? escapeHtml(gitInfo.commit.slice(0, 7)) : "";
95
+ const brokenState = brokenLinks;
96
+ const brokenCount = brokenState?.lastResult?.broken?.length ?? null;
97
+ const brokenLabel =
98
+ brokenState?.status === "running"
99
+ ? "Broken links: scanning…"
100
+ : brokenCount == null
101
+ ? "Broken links"
102
+ : `Broken links: ${brokenCount}`;
103
+ const brokenHref = `/broken-links${querySuffix || ""}`;
104
+ const ignoredHref = toggleIgnoredHref || "#";
105
+ const ignoredLabel = showIgnored ? "Hide ignored files" : "Show ignored files";
106
+
107
+ return `<details class="meta-menu">
108
+ <summary class="pill link" aria-label="More">More</summary>
109
+ <div class="menu-panel" role="menu">
110
+ <a class="menu-item link" href="${brokenHref}" role="menuitem">${escapeHtml(brokenLabel)}</a>
111
+ <a class="menu-item link" data-no-preserve="ignored" href="${ignoredHref}" role="menuitem">${escapeHtml(
112
+ ignoredLabel,
113
+ )}</a>
114
+ ${commit ? `<div class="menu-item mono" role="menuitem">Commit: ${commit}</div>` : ""}
115
+ </div>
116
+ </details>`;
117
+ }
118
+
119
+ function pageTemplateWithLinks({
120
+ title,
121
+ repoName,
122
+ gitInfo,
123
+ relPathPosix,
124
+ bodyHtml,
125
+ brokenLinks,
126
+ querySuffix,
127
+ toggleIgnoredHref,
128
+ showIgnored,
129
+ }) {
130
+ const branch = gitInfo?.branch ? escapeHtml(gitInfo.branch) : "no-git";
131
+ const commit = gitInfo?.commit ? escapeHtml(gitInfo.commit.slice(0, 7)) : "";
132
+ const brokenPill = renderBrokenLinksPill(brokenLinks, querySuffix);
133
+ const ignoredPill = renderIgnoredTogglePill({ toggleIgnoredHref, showIgnored });
134
+ const metaMenu = renderMetaMenu({ gitInfo, brokenLinks, querySuffix, toggleIgnoredHref, showIgnored });
135
+ return `<!doctype html>
136
+ <html lang="en">
137
+ <head>
138
+ <meta charset="utf-8" />
139
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
140
+ <title>${escapeHtml(title)}</title>
141
+ <link rel="stylesheet" href="/static/vendor/github-markdown-css/github-markdown-light.css" />
142
+ <link rel="stylesheet" href="/static/vendor/highlight.js/styles/github.css" />
143
+ <link rel="stylesheet" href="/static/vendor/katex/katex.min.css" />
144
+ <link rel="stylesheet" href="/static/app.css" />
145
+ </head>
146
+ <body>
147
+ <header class="topbar">
148
+ <div class="topbar-row">
149
+ <a class="brand" href="/tree/${querySuffix || ""}">${escapeHtml(repoName)}</a>
150
+ <div class="meta">
151
+ <span class="pill">${branch}</span>
152
+ ${commit ? `<span class="pill mono meta-commit">${commit}</span>` : ""}
153
+ <span class="meta-actions">
154
+ ${brokenPill}
155
+ ${ignoredPill}
156
+ </span>
157
+ ${metaMenu}
158
+ </div>
159
+ </div>
160
+ ${renderBreadcrumbs(relPathPosix, querySuffix)}
161
+ </header>
162
+ <main class="container">
163
+ ${bodyHtml}
164
+ </main>
165
+ <script defer src="/static/vendor/katex/katex.min.js"></script>
166
+ <script defer src="/static/vendor/katex/contrib/auto-render.min.js"></script>
167
+ <script type="module" src="/static/app.js"></script>
168
+ </body>
169
+ </html>`;
170
+ }
171
+
172
+ export function renderTreePage({
173
+ title,
174
+ repoName,
175
+ gitInfo,
176
+ brokenLinks,
177
+ querySuffix,
178
+ toggleIgnoredHref,
179
+ showIgnored,
180
+ relPathPosix,
181
+ rows,
182
+ readmeHtml,
183
+ }) {
184
+ const tableRows = rows
185
+ .map((r) => {
186
+ const icon = r.isDir ? "dir" : "file";
187
+ const name = escapeHtml(r.name);
188
+ return `<tr>
189
+ <td class="name"><a class="item ${icon}" href="${r.href}">${name}</a></td>
190
+ <td class="mtime">${escapeHtml(r.mtime)}</td>
191
+ <td class="size">${escapeHtml(r.size)}</td>
192
+ </tr>`;
193
+ })
194
+ .join("\n");
195
+
196
+ const readmeSection = readmeHtml
197
+ ? `<section class="panel readme">
198
+ <div class="panel-title">README</div>
199
+ <div class="markdown-body markdown-wrap">${readmeHtml}</div>
200
+ </section>`
201
+ : "";
202
+
203
+ const body = `<section class="panel">
204
+ <div class="panel-title">Files</div>
205
+ <div class="table-wrap">
206
+ <table class="file-table">
207
+ <thead>
208
+ <tr><th class="name">Name</th><th class="mtime">Last modified</th><th class="size">Size</th></tr>
209
+ </thead>
210
+ <tbody>
211
+ ${tableRows || `<tr><td colspan="3" class="empty">Empty directory</td></tr>`}
212
+ </tbody>
213
+ </table>
214
+ </div>
215
+ </section>
216
+ ${readmeSection}`;
217
+
218
+ return pageTemplateWithLinks({
219
+ title,
220
+ repoName,
221
+ gitInfo,
222
+ brokenLinks,
223
+ toggleIgnoredHref,
224
+ showIgnored,
225
+ querySuffix,
226
+ relPathPosix,
227
+ bodyHtml: body,
228
+ });
229
+ }
230
+
231
+ export function renderFilePage({
232
+ title,
233
+ repoName,
234
+ gitInfo,
235
+ brokenLinks,
236
+ querySuffix,
237
+ toggleIgnoredHref,
238
+ showIgnored,
239
+ relPathPosix,
240
+ fileName,
241
+ isMarkdown,
242
+ renderedHtml,
243
+ }) {
244
+ const relDir = path.posix.dirname(relPathPosix || "");
245
+ const suffix = querySuffix || "";
246
+ const rawHref = `/raw/${encodePathForUrl(relPathPosix || "")}${suffix}`;
247
+ const treeHref = `/tree/${encodePathForUrl(relDir === "." ? "" : relDir)}${suffix}`;
248
+
249
+ const body = `<section class="panel">
250
+ <div class="panel-title">
251
+ <span class="filename">${escapeHtml(fileName)}</span>
252
+ <span class="spacer"></span>
253
+ <a class="btn" href="${treeHref}">Back</a>
254
+ <a class="btn" href="${rawHref}">Raw</a>
255
+ </div>
256
+ <div class="${isMarkdown ? "markdown-body markdown-wrap" : "code-wrap"}">
257
+ ${renderedHtml}
258
+ </div>
259
+ </section>`;
260
+
261
+ return pageTemplateWithLinks({
262
+ title,
263
+ repoName,
264
+ gitInfo,
265
+ brokenLinks,
266
+ toggleIgnoredHref,
267
+ showIgnored,
268
+ querySuffix,
269
+ relPathPosix,
270
+ bodyHtml: body,
271
+ });
272
+ }
273
+
274
+ export function renderErrorPage({ title, message }) {
275
+ return `<!doctype html>
276
+ <html lang="en">
277
+ <head>
278
+ <meta charset="utf-8" />
279
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
280
+ <title>${escapeHtml(title)}</title>
281
+ <link rel="stylesheet" href="/static/app.css" />
282
+ </head>
283
+ <body>
284
+ <main class="container">
285
+ <section class="panel">
286
+ <div class="panel-title">Error</div>
287
+ <div class="error">${escapeHtml(message)}</div>
288
+ </section>
289
+ </main>
290
+ </body>
291
+ </html>`;
292
+ }
293
+
294
+ export function renderBrokenLinksPage({
295
+ title,
296
+ repoName,
297
+ gitInfo,
298
+ relPathPosix,
299
+ scanState,
300
+ querySuffix,
301
+ toggleIgnoredHref,
302
+ showIgnored,
303
+ }) {
304
+ const state = scanState || {};
305
+ const result = state.lastResult;
306
+ const broken = result?.broken || [];
307
+ const statusLine =
308
+ state.status === "running"
309
+ ? "Scanning…"
310
+ : result
311
+ ? `Last scan: ${new Date(result.finishedAt).toLocaleString()} · Files: ${
312
+ result.filesScanned
313
+ } · URLs: ${result.urlsChecked} · Broken: ${broken.length} · ${result.durationMs}ms`
314
+ : state.lastError
315
+ ? `Last error: ${escapeHtml(state.lastError)}`
316
+ : "No scan yet.";
317
+
318
+ const grouped = new Map();
319
+ for (const b of broken) {
320
+ const arr = grouped.get(b.source) || [];
321
+ arr.push(b);
322
+ grouped.set(b.source, arr);
323
+ }
324
+
325
+ const sections = Array.from(grouped.entries())
326
+ .sort((a, b) => a[0].localeCompare(b[0]))
327
+ .map(([source, items]) => {
328
+ const sourceHref = `/blob/${source.split("/").map(encodeURIComponent).join("/")}${
329
+ querySuffix || ""
330
+ }`;
331
+ const rows = items
332
+ .map((i) => {
333
+ const reason = escapeHtml(i.reason || "");
334
+ const kind = escapeHtml(i.kind || "");
335
+ const url = escapeHtml(i.url || "");
336
+ const target = i.target ? escapeHtml(i.target) : "";
337
+ return `<tr><td class="mono">${kind}</td><td class="mono">${reason}</td><td class="mono">${url}</td><td class="mono">${target}</td></tr>`;
338
+ })
339
+ .join("\n");
340
+ return `<section class="panel">
341
+ <div class="panel-title">
342
+ <span class="filename"><a class="link" href="${sourceHref}">${escapeHtml(source)}</a></span>
343
+ <span class="spacer"></span>
344
+ <span class="pill">${items.length}</span>
345
+ </div>
346
+ <div class="table-wrap">
347
+ <table class="file-table linkcheck">
348
+ <thead><tr><th>Kind</th><th>Reason</th><th>URL</th><th>Target</th></tr></thead>
349
+ <tbody>${rows}</tbody>
350
+ </table>
351
+ </div>
352
+ </section>`;
353
+ })
354
+ .join("\n");
355
+
356
+ const body = `<section class="panel">
357
+ <div class="panel-title">Broken links</div>
358
+ <div class="note">${escapeHtml(statusLine)}</div>
359
+ </section>
360
+ ${sections || `<section class="panel"><div class="panel-title">All good</div><div class="note">No broken internal links found.</div></section>`}`;
361
+
362
+ return pageTemplateWithLinks({
363
+ title,
364
+ repoName,
365
+ gitInfo,
366
+ brokenLinks: scanState,
367
+ toggleIgnoredHref,
368
+ showIgnored,
369
+ querySuffix,
370
+ relPathPosix,
371
+ bodyHtml: body,
372
+ });
373
+ }