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/markdown.js DELETED
@@ -1,364 +0,0 @@
1
- import path from "node:path";
2
- import hljs from "highlight.js";
3
- import GithubSlugger from "github-slugger";
4
- import MarkdownIt from "markdown-it";
5
- import { full as emoji } from "markdown-it-emoji";
6
- import footnote from "markdown-it-footnote";
7
- import taskLists from "markdown-it-task-lists";
8
- import sanitizeHtml from "sanitize-html";
9
-
10
- function escapeHtml(s) {
11
- return String(s)
12
- .replaceAll("&", "&")
13
- .replaceAll("<", "&lt;")
14
- .replaceAll(">", "&gt;")
15
- .replaceAll('"', "&quot;")
16
- .replaceAll("'", "&#39;");
17
- }
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
-
39
- function isExternalHref(href) {
40
- return /^(?:[a-z]+:)?\/\//i.test(href) || href.startsWith("mailto:") || href.startsWith("tel:");
41
- }
42
-
43
- function toPosixPath(p) {
44
- return p.split(path.sep).join("/");
45
- }
46
-
47
- function normalizeRepoPath(posixPath) {
48
- const stripped = String(posixPath || "").replace(/^\/+/, "");
49
- let normalized = path.posix.normalize(stripped);
50
- if (normalized === "." || normalized === "./") return "";
51
- // GitHub effectively clamps links so they can't escape the repo root.
52
- while (normalized === ".." || normalized.startsWith("../")) {
53
- normalized = normalized === ".." ? "" : normalized.slice(3);
54
- }
55
- return normalized;
56
- }
57
-
58
- function encodePathForUrl(posixPath) {
59
- return posixPath
60
- .split("/")
61
- .map((segment) => encodeURIComponent(segment))
62
- .join("/");
63
- }
64
-
65
- function rewriteLinkHref(href, baseDirPosix) {
66
- if (!href) return href;
67
- if (href.startsWith("#") || isExternalHref(href)) return href;
68
-
69
- const trimmed = href.trim();
70
- const hashIndex = trimmed.indexOf("#");
71
- const beforeHash = hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed;
72
- const hash = hashIndex >= 0 ? trimmed.slice(hashIndex + 1) : "";
73
-
74
- const queryIndex = beforeHash.indexOf("?");
75
- const rawPath = queryIndex >= 0 ? beforeHash.slice(0, queryIndex) : beforeHash;
76
- const query = queryIndex >= 0 ? beforeHash.slice(queryIndex) : "";
77
-
78
- if (/^\/(?:blob|tree|raw|static)(?:\/|$)/.test(rawPath) || rawPath === "/events") {
79
- return href;
80
- }
81
-
82
- const raw = rawPath.trim();
83
- if (!raw) return href;
84
-
85
- const isRooted = raw.startsWith("/");
86
- const targetPosix = isRooted
87
- ? normalizeRepoPath(raw)
88
- : normalizeRepoPath(path.posix.join(baseDirPosix || "", raw));
89
- if (targetPosix == null) return href;
90
-
91
- const isTree = raw.endsWith("/") || targetPosix === "";
92
- const newPath = `/${isTree ? "tree" : "blob"}/${encodePathForUrl(targetPosix)}`;
93
- const withQuery = query ? `${newPath}${query}` : newPath;
94
- return hash ? `${withQuery}#${hash}` : withQuery;
95
- }
96
-
97
- function rewriteImageSrc(src, baseDirPosix) {
98
- if (!src) return src;
99
- if (isExternalHref(src) || src.startsWith("data:")) return src;
100
-
101
- const trimmed = src.trim();
102
- const hashIndex = trimmed.indexOf("#");
103
- const beforeHash = hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed;
104
- const hash = hashIndex >= 0 ? trimmed.slice(hashIndex + 1) : "";
105
-
106
- const queryIndex = beforeHash.indexOf("?");
107
- const rawPath = queryIndex >= 0 ? beforeHash.slice(0, queryIndex) : beforeHash;
108
- const query = queryIndex >= 0 ? beforeHash.slice(queryIndex) : "";
109
-
110
- if (/^\/(?:raw|static)(?:\/|$)/.test(rawPath)) return src;
111
-
112
- const isRooted = rawPath.startsWith("/");
113
- const targetPosix = isRooted
114
- ? normalizeRepoPath(rawPath)
115
- : normalizeRepoPath(path.posix.join(baseDirPosix || "", rawPath));
116
- if (targetPosix == null) return src;
117
- const newPath = `/raw/${encodePathForUrl(targetPosix)}`;
118
- const withQuery = query ? `${newPath}${query}` : newPath;
119
- return hash ? `${withQuery}#${hash}` : withQuery;
120
- }
121
-
122
- export function createMarkdownRenderer() {
123
- const md = new MarkdownIt({
124
- html: true,
125
- linkify: true,
126
- typographer: false,
127
- highlight(code, lang) {
128
- if (lang && hljs.getLanguage(lang)) {
129
- return hljs.highlight(code, { language: lang }).value;
130
- }
131
- return hljs.highlightAuto(code).value;
132
- },
133
- })
134
- .use(emoji, { shortcuts: {} })
135
- .use(footnote)
136
- .use(taskLists, { enabled: true, label: false, labelAfter: false })
137
- .enable(["table", "strikethrough"]);
138
-
139
- const defaultFence = md.renderer.rules.fence;
140
- md.renderer.rules.fence = function (tokens, idx, options, env, self) {
141
- const token = tokens[idx];
142
- const info = (token.info || "").trim();
143
- const lang = info.split(/\s+/g)[0]?.toLowerCase() || "";
144
- if (lang === "mermaid") {
145
- return `<pre class="mermaid">${escapeHtml(token.content || "")}</pre>\n`;
146
- }
147
- if (typeof defaultFence === "function") return defaultFence(tokens, idx, options, env, self);
148
- return self.renderToken(tokens, idx, options);
149
- };
150
-
151
- const defaultLinkOpen =
152
- md.renderer.rules.link_open ||
153
- function (tokens, idx, options, env, self) {
154
- return self.renderToken(tokens, idx, options);
155
- };
156
- md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
157
- const token = tokens[idx];
158
- const hrefIndex = token.attrIndex("href");
159
- if (hrefIndex >= 0) {
160
- const href = token.attrs[hrefIndex][1];
161
- const rewritten = rewriteLinkHref(href, env.baseDirPosix || "");
162
- token.attrs[hrefIndex][1] = rewritten;
163
- if (isExternalHref(href)) {
164
- token.attrSet("target", "_blank");
165
- token.attrSet("rel", "noreferrer noopener");
166
- }
167
- }
168
- return defaultLinkOpen(tokens, idx, options, env, self);
169
- };
170
-
171
- const defaultImage =
172
- md.renderer.rules.image ||
173
- function (tokens, idx, options, env, self) {
174
- return self.renderToken(tokens, idx, options);
175
- };
176
- md.renderer.rules.image = function (tokens, idx, options, env, self) {
177
- const token = tokens[idx];
178
- const srcIndex = token.attrIndex("src");
179
- if (srcIndex >= 0) {
180
- const src = token.attrs[srcIndex][1];
181
- token.attrs[srcIndex][1] = rewriteImageSrc(src, env.baseDirPosix || "");
182
- }
183
- return defaultImage(tokens, idx, options, env, self);
184
- };
185
-
186
- const defaultHeadingOpen =
187
- md.renderer.rules.heading_open ||
188
- function (tokens, idx, options, env, self) {
189
- return self.renderToken(tokens, idx, options);
190
- };
191
- md.renderer.rules.heading_open = function (tokens, idx, options, env, self) {
192
- const token = tokens[idx];
193
- if (token.attrIndex("id") < 0 && token.tag && token.tag.startsWith("h")) {
194
- const titleToken = tokens[idx + 1];
195
- const headingText = titleToken?.content || "";
196
- const slugger = env.__slugger || (env.__slugger = new GithubSlugger());
197
- const slug = slugger.slug(headingText);
198
- if (slug) token.attrSet("id", slug);
199
- }
200
- const idIndex = token.attrIndex("id");
201
- const id = idIndex >= 0 ? token.attrs[idIndex][1] : "";
202
- const rendered = defaultHeadingOpen(tokens, idx, options, env, self);
203
- if (!id) return rendered;
204
- return `${rendered}<a class="anchor" aria-hidden="true" href="#${escapeHtml(id)}"></a>`;
205
- };
206
-
207
- const alertTypes = new Map([
208
- ["NOTE", { classSuffix: "note", title: "Note" }],
209
- ["TIP", { classSuffix: "tip", title: "Tip" }],
210
- ["IMPORTANT", { classSuffix: "important", title: "Important" }],
211
- ["WARNING", { classSuffix: "warning", title: "Warning" }],
212
- ["CAUTION", { classSuffix: "caution", title: "Caution" }],
213
- ]);
214
-
215
- md.core.ruler.after("inline", "github-alerts", (state) => {
216
- const tokens = state.tokens;
217
- for (let i = 0; i < tokens.length; i++) {
218
- if (tokens[i].type !== "blockquote_open") continue;
219
-
220
- let level = 1;
221
- let closeIndex = -1;
222
- for (let j = i + 1; j < tokens.length; j++) {
223
- if (tokens[j].type === "blockquote_open") level++;
224
- else if (tokens[j].type === "blockquote_close") level--;
225
- if (level === 0) {
226
- closeIndex = j;
227
- break;
228
- }
229
- }
230
- if (closeIndex === -1) continue;
231
-
232
- const paragraphOpen = tokens[i + 1];
233
- const inline = tokens[i + 2];
234
- if (paragraphOpen?.type !== "paragraph_open" || inline?.type !== "inline") continue;
235
- const children = inline.children || [];
236
-
237
- const firstTextIndex = children.findIndex(
238
- (t) => t.type === "text" && /^\s*\[!\w+\]/.test(t.content),
239
- );
240
- if (firstTextIndex === -1) continue;
241
-
242
- const match = children[firstTextIndex].content.match(
243
- /^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/i,
244
- );
245
- if (!match) continue;
246
- const typeKey = match[1].toUpperCase();
247
- const alert = alertTypes.get(typeKey);
248
- if (!alert) continue;
249
-
250
- children[firstTextIndex].content = children[firstTextIndex].content.slice(match[0].length);
251
- if (children[firstTextIndex].content.length === 0) {
252
- children.splice(firstTextIndex, 1);
253
- if (children[firstTextIndex]?.type === "softbreak") children.splice(firstTextIndex, 1);
254
- }
255
- while (children[0]?.type === "softbreak") children.shift();
256
-
257
- const open = tokens[i];
258
- open.type = "html_block";
259
- open.tag = "";
260
- open.nesting = 0;
261
- open.markup = "";
262
- open.content = `<div class="markdown-alert markdown-alert-${alert.classSuffix}">\n`;
263
- open.block = true;
264
-
265
- const close = tokens[closeIndex];
266
- close.type = "html_block";
267
- close.tag = "";
268
- close.nesting = 0;
269
- close.markup = "";
270
- close.content = `</div>\n`;
271
- close.block = true;
272
-
273
- const titleToken = new state.Token("html_block", "", 0);
274
- titleToken.content = `<p class="markdown-alert-title">${escapeHtml(alert.title)}</p>\n`;
275
- titleToken.block = true;
276
- tokens.splice(i + 1, 0, titleToken);
277
- i++;
278
- }
279
- });
280
-
281
- function sanitize(html, env) {
282
- const baseDirPosix = env?.baseDirPosix || "";
283
- return sanitizeHtml(html, {
284
- allowedTags: [
285
- ...sanitizeHtml.defaults.allowedTags,
286
- "details",
287
- "summary",
288
- "img",
289
- "section",
290
- "sup",
291
- "h1",
292
- "h2",
293
- "h3",
294
- "h4",
295
- "h5",
296
- "h6",
297
- "table",
298
- "thead",
299
- "tbody",
300
- "tr",
301
- "th",
302
- "td",
303
- "pre",
304
- "code",
305
- "span",
306
- "div",
307
- "kbd",
308
- "input",
309
- ],
310
- allowedAttributes: {
311
- "*": ["class", "id", "aria-label", "aria-hidden", "role", "align"],
312
- a: ["href", "name", "title", "target", "rel", "tabindex"],
313
- img: ["src", "alt", "title", "width", "height", "loading"],
314
- input: ["type", "checked", "disabled"],
315
- details: ["open"],
316
- },
317
- allowedSchemes: ["http", "https", "mailto", "tel", "data"],
318
- allowProtocolRelative: true,
319
- transformTags: {
320
- a: (tagName, attribs) => {
321
- const next = { ...attribs };
322
- if (next.href) {
323
- const originalHref = next.href;
324
- next.href = rewriteLinkHref(originalHref, baseDirPosix);
325
- if (isExternalHref(originalHref)) {
326
- next.target = "_blank";
327
- next.rel = "noreferrer noopener";
328
- }
329
- }
330
- return { tagName, attribs: next };
331
- },
332
- img: (tagName, attribs) => {
333
- const next = { ...attribs };
334
- if (next.src) next.src = rewriteImageSrc(next.src, baseDirPosix);
335
- if (!next.loading) next.loading = "lazy";
336
- return { tagName, attribs: next };
337
- },
338
- input: (tagName, attribs) => {
339
- const next = { ...attribs };
340
- if (next.type === "checkbox") next.disabled = "disabled";
341
- return { tagName, attribs: next };
342
- },
343
- },
344
- });
345
- }
346
-
347
- return {
348
- render(markdown, env) {
349
- const e = env ?? {};
350
- const html = md.render(markdown ?? "", e);
351
- return sanitize(html, e);
352
- },
353
- renderCodeBlock(text, { languageHint } = {}) {
354
- const lang = languageHint && hljs.getLanguage(languageHint) ? languageHint : "";
355
- const highlighted = lang
356
- ? hljs.highlight(text, { language: lang }).value
357
- : hljs.highlightAuto(text).value;
358
- return sanitize(
359
- `<pre class="hljs"><code>${highlighted || escapeHtml(text)}</code></pre>`,
360
- { baseDirPosix: "" },
361
- );
362
- },
363
- };
364
- }