peekmd 1.0.0 → 1.0.1

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 (6) hide show
  1. package/README.md +127 -36
  2. package/cli.ts +7 -0
  3. package/index.ts +978 -0
  4. package/package.json +8 -11
  5. package/dist/cli.js +0 -51307
  6. package/dist/index.js +0 -51303
package/index.ts ADDED
@@ -0,0 +1,978 @@
1
+ import { readFileSync, existsSync, readdirSync, lstatSync } from "fs";
2
+ import { extname, join, dirname, sep } from "path";
3
+ import { marked } from "marked";
4
+ import hljs from "highlight.js";
5
+
6
+ interface FileNode {
7
+ name: string;
8
+ type: "file" | "folder";
9
+ children?: FileNode[];
10
+ size?: string;
11
+ }
12
+
13
+ function getFileTree(
14
+ dir: string,
15
+ maxDepth: number = 3,
16
+ currentDepth: number = 0,
17
+ ): FileNode[] {
18
+ if (currentDepth >= maxDepth) return [];
19
+
20
+ try {
21
+ const items = readdirSync(dir);
22
+ return items
23
+ .slice(0, 20)
24
+ .map((item) => {
25
+ try {
26
+ const fullPath = join(dir, item);
27
+ const stats = lstatSync(fullPath);
28
+ const isDir = stats.isDirectory();
29
+ return {
30
+ name: item,
31
+ type: isDir ? "folder" : "file",
32
+ children: isDir
33
+ ? getFileTree(fullPath, maxDepth, currentDepth + 1)
34
+ : undefined,
35
+ size: isDir ? "" : formatSize(stats.size),
36
+ };
37
+ } catch {
38
+ // Skip files/directories we can't access
39
+ return null;
40
+ }
41
+ })
42
+ .filter((node): node is FileNode => node !== null);
43
+ } catch {
44
+ return [];
45
+ }
46
+ }
47
+
48
+ function formatSize(bytes: number): string {
49
+ if (bytes < 1024) return `${bytes} B`;
50
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
51
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
52
+ }
53
+
54
+ function renderFileTree(nodes: FileNode[], depth: number = 0): string {
55
+ return nodes
56
+ .map((node) => {
57
+ const indent = " ".repeat(depth);
58
+ const icon = node.type === "folder" ? "📁" : "📄";
59
+ const size = node.size
60
+ ? ` <span class="file-size">${node.size}</span>`
61
+ : "";
62
+ const children =
63
+ node.children && node.children.length > 0
64
+ ? `\n${renderFileTree(node.children, depth + 1)}`
65
+ : "";
66
+ return `${indent}<li class="${node.type}"><span class="icon">${icon}</span><a href="#">${node.name}</a>${size}${children}</li>`;
67
+ })
68
+ .join("\n");
69
+ }
70
+
71
+ function getDirName(filePath: string): string {
72
+ const dir = dirname(filePath);
73
+ return dir.split(sep).pop() || "peekmd";
74
+ }
75
+
76
+ function getRelativePath(filePath: string): string {
77
+ const dir = dirname(filePath);
78
+ return dir === "." ? "" : dir;
79
+ }
80
+
81
+ const HTML_TEMPLATE = `<!DOCTYPE html>
82
+ <html lang="en">
83
+ <head>
84
+ <meta charset="UTF-8">
85
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
86
+ <title>{{filename}} - {{repoName}}</title>
87
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📝</text></svg>">
88
+ <style>
89
+ * { box-sizing: border-box; margin: 0; padding: 0; }
90
+ body {
91
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
92
+ font-size: 14px;
93
+ line-height: 1.5;
94
+ color: #1f2328;
95
+ background-color: #f6f8fa;
96
+ }
97
+
98
+ /* GitHub Header */
99
+ .AppHeader {
100
+ background: #24292f;
101
+ padding: 16px;
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 16px;
105
+ }
106
+ .AppHeader-logo svg { fill: #fff; width: 32px; height: 32px; }
107
+ .AppHeader-search {
108
+ flex: 1;
109
+ max-width: 272px;
110
+ background: hsla(0,0%,100%,.12);
111
+ border: 1px solid #57606a;
112
+ border-radius: 6px;
113
+ padding: 5px 12px;
114
+ color: #fff;
115
+ font-size: 14px;
116
+ }
117
+ .AppHeader-search::placeholder { color: hsla(0,0%,100%,.7); }
118
+ .AppHeader-nav { display: flex; gap: 16px; margin-left: auto; }
119
+ .AppHeader-nav a { color: #fff; text-decoration: none; font-size: 14px; font-weight: 600; }
120
+ .AppHeader-nav a:hover { color: hsla(0,0%,100%,.7); }
121
+
122
+ /* Repository Header */
123
+ .repohead {
124
+ background: #fff;
125
+ border-bottom: 1px solid #d0d7de;
126
+ padding: 16px 24px;
127
+ }
128
+ .repohead-details-container {
129
+ max-width: 1280px;
130
+ margin: 0 auto;
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 8px;
134
+ }
135
+ .repohead svg { fill: #656d76; width: 16px; height: 16px; flex-shrink: 0; }
136
+ .repohead-name { font-size: 20px; }
137
+ .repohead-name a { color: #0969da; text-decoration: none; font-weight: 600; }
138
+ .repohead-name a:hover { text-decoration: underline; }
139
+ .repohead-name .separator { color: #656d76; margin: 0 4px; font-weight: 300; }
140
+
141
+ /* Underline Nav (tabs) */
142
+ .UnderlineNav {
143
+ background: #fff;
144
+ border-bottom: 1px solid #d0d7de;
145
+ padding: 0 24px;
146
+ }
147
+ .UnderlineNav-body {
148
+ max-width: 1280px;
149
+ margin: 0 auto;
150
+ display: flex;
151
+ gap: 8px;
152
+ overflow-x: auto;
153
+ }
154
+ .UnderlineNav-item {
155
+ display: flex;
156
+ align-items: center;
157
+ gap: 8px;
158
+ padding: 8px 16px;
159
+ color: #656d76;
160
+ text-decoration: none;
161
+ font-size: 14px;
162
+ border-bottom: 2px solid transparent;
163
+ white-space: nowrap;
164
+ }
165
+ .UnderlineNav-item:hover { color: #1f2328; }
166
+ .UnderlineNav-item.selected { color: #1f2328; border-bottom-color: #fd8c73; font-weight: 600; }
167
+ .UnderlineNav-item svg { width: 16px; height: 16px; fill: currentColor; }
168
+ .Counter {
169
+ background: rgba(175,184,193,0.2);
170
+ border-radius: 2em;
171
+ padding: 0 6px;
172
+ font-size: 12px;
173
+ font-weight: 500;
174
+ line-height: 18px;
175
+ color: #1f2328;
176
+ }
177
+
178
+ /* Main Content - Two Column Layout */
179
+ .container-xl {
180
+ max-width: 1280px;
181
+ margin: 0 auto;
182
+ padding: 24px;
183
+ }
184
+ .Layout {
185
+ display: flex;
186
+ gap: 24px;
187
+ }
188
+ .Layout-main {
189
+ flex: 1;
190
+ min-width: 0;
191
+ }
192
+ .Layout-sidebar {
193
+ width: 296px;
194
+ flex-shrink: 0;
195
+ }
196
+
197
+ /* Sidebar */
198
+ .BorderGrid { border-top: 1px solid #d0d7de; }
199
+ .BorderGrid-row { padding: 16px 0; border-bottom: 1px solid #d0d7de; }
200
+ .BorderGrid-row:first-child { border-top: none; }
201
+ .BorderGrid-cell h2 { font-size: 14px; font-weight: 600; margin-bottom: 8px; }
202
+ .sidebar-about p { font-size: 14px; color: #1f2328; margin-bottom: 16px; }
203
+ .sidebar-link {
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 8px;
207
+ color: #656d76;
208
+ text-decoration: none;
209
+ font-size: 14px;
210
+ margin-top: 8px;
211
+ }
212
+ .sidebar-link:hover { color: #0969da; }
213
+ .sidebar-link svg { width: 16px; height: 16px; fill: currentColor; flex-shrink: 0; }
214
+ .sidebar-link strong { color: #1f2328; }
215
+ .topic-tag {
216
+ display: inline-block;
217
+ padding: 0 10px;
218
+ font-size: 12px;
219
+ font-weight: 500;
220
+ line-height: 22px;
221
+ color: #0969da;
222
+ background-color: #ddf4ff;
223
+ border-radius: 2em;
224
+ text-decoration: none;
225
+ margin: 0 4px 4px 0;
226
+ }
227
+ .topic-tag:hover { background-color: #b6e3ff; }
228
+ .Progress {
229
+ display: flex;
230
+ height: 8px;
231
+ overflow: hidden;
232
+ background-color: #e6e8eb;
233
+ border-radius: 6px;
234
+ margin-bottom: 8px;
235
+ }
236
+ .Progress-item { height: 100%; }
237
+ .lang-list { list-style: none; }
238
+ .lang-item {
239
+ display: inline-flex;
240
+ align-items: center;
241
+ font-size: 12px;
242
+ margin-right: 16px;
243
+ margin-bottom: 4px;
244
+ }
245
+ .lang-dot {
246
+ width: 8px;
247
+ height: 8px;
248
+ border-radius: 50%;
249
+ margin-right: 4px;
250
+ }
251
+ .lang-name { font-weight: 600; color: #1f2328; margin-right: 4px; }
252
+ .lang-percent { color: #656d76; }
253
+
254
+ /* File Box */
255
+ .Box {
256
+ background: #fff;
257
+ border: 1px solid #d0d7de;
258
+ border-radius: 6px;
259
+ overflow: hidden;
260
+ }
261
+ .Box-header {
262
+ display: flex;
263
+ align-items: center;
264
+ justify-content: space-between;
265
+ padding: 8px 16px;
266
+ background: #f6f8fa;
267
+ border-bottom: 1px solid #d0d7de;
268
+ }
269
+ .Box-header-title {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 8px;
273
+ font-size: 14px;
274
+ font-weight: 600;
275
+ }
276
+ .Box-header-title svg { width: 16px; height: 16px; fill: #656d76; }
277
+ .Box-header .btn-sm {
278
+ display: inline-flex;
279
+ align-items: center;
280
+ gap: 4px;
281
+ padding: 3px 12px;
282
+ font-size: 12px;
283
+ font-weight: 500;
284
+ background: #f6f8fa;
285
+ border: 1px solid rgba(31,35,40,0.15);
286
+ border-radius: 6px;
287
+ color: #1f2328;
288
+ cursor: pointer;
289
+ }
290
+ .Box-header .btn-sm:hover { background: #f3f4f6; border-color: rgba(31,35,40,0.15); }
291
+ .Box-header .btn-sm svg { width: 16px; height: 16px; fill: currentColor; }
292
+
293
+ /* Markdown Body - GitHub's exact styling */
294
+ .markdown-body {
295
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
296
+ font-size: 16px;
297
+ line-height: 1.5;
298
+ word-wrap: break-word;
299
+ padding: 32px;
300
+ max-width: 1012px;
301
+ }
302
+
303
+ /* Headings */
304
+ .markdown-body h1, .markdown-body h2 {
305
+ padding-bottom: 0.3em;
306
+ border-bottom: 1px solid hsla(210,18%,87%,1);
307
+ }
308
+ .markdown-body h1 { font-size: 2em; margin: 0.67em 0 16px; font-weight: 600; line-height: 1.25; }
309
+ .markdown-body h1:first-child { margin-top: 0; }
310
+ .markdown-body h2 { font-size: 1.5em; margin: 24px 0 16px; font-weight: 600; line-height: 1.25; }
311
+ .markdown-body h3 { font-size: 1.25em; margin: 24px 0 16px; font-weight: 600; line-height: 1.25; }
312
+ .markdown-body h4 { font-size: 1em; margin: 24px 0 16px; font-weight: 600; line-height: 1.25; }
313
+ .markdown-body h5 { font-size: 0.875em; margin: 24px 0 16px; font-weight: 600; line-height: 1.25; }
314
+ .markdown-body h6 { font-size: 0.85em; margin: 24px 0 16px; font-weight: 600; line-height: 1.25; color: #656d76; }
315
+
316
+ /* Anchor links */
317
+ .markdown-body h1 .anchor, .markdown-body h2 .anchor, .markdown-body h3 .anchor,
318
+ .markdown-body h4 .anchor, .markdown-body h5 .anchor, .markdown-body h6 .anchor {
319
+ float: left;
320
+ padding-right: 4px;
321
+ margin-left: -20px;
322
+ line-height: 1;
323
+ opacity: 0;
324
+ text-decoration: none;
325
+ }
326
+ .markdown-body h1:hover .anchor, .markdown-body h2:hover .anchor, .markdown-body h3:hover .anchor,
327
+ .markdown-body h4:hover .anchor, .markdown-body h5:hover .anchor, .markdown-body h6:hover .anchor {
328
+ opacity: 1;
329
+ }
330
+ .markdown-body .anchor svg { fill: #1f2328; }
331
+
332
+ /* Paragraphs and text */
333
+ .markdown-body p { margin: 0 0 16px; }
334
+ .markdown-body a { color: #0969da; text-decoration: none; }
335
+ .markdown-body a:hover { text-decoration: underline; }
336
+ .markdown-body strong, .markdown-body b { font-weight: 600; }
337
+ .markdown-body em, .markdown-body i { font-style: italic; }
338
+ .markdown-body del { text-decoration: line-through; }
339
+ .markdown-body mark { background-color: #fff8c5; padding: 0.1em 0.2em; }
340
+ .markdown-body sub, .markdown-body sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
341
+ .markdown-body sup { top: -0.5em; }
342
+ .markdown-body sub { bottom: -0.25em; }
343
+
344
+ /* Code */
345
+ .markdown-body code, .markdown-body tt {
346
+ padding: 0.2em 0.4em;
347
+ margin: 0;
348
+ font-size: 85%;
349
+ white-space: break-spaces;
350
+ background-color: rgba(175,184,193,0.2);
351
+ border-radius: 6px;
352
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
353
+ }
354
+ .markdown-body pre {
355
+ padding: 16px;
356
+ overflow: auto;
357
+ font-size: 85%;
358
+ line-height: 1.45;
359
+ color: #1f2328;
360
+ background-color: #f6f8fa;
361
+ border-radius: 6px;
362
+ margin-bottom: 16px;
363
+ word-wrap: normal;
364
+ }
365
+ .markdown-body pre code {
366
+ display: inline;
367
+ max-width: auto;
368
+ padding: 0;
369
+ margin: 0;
370
+ overflow: visible;
371
+ line-height: inherit;
372
+ word-wrap: normal;
373
+ background-color: transparent;
374
+ border: 0;
375
+ font-size: 100%;
376
+ white-space: pre;
377
+ }
378
+
379
+ /* Lists */
380
+ .markdown-body ul, .markdown-body ol { padding-left: 2em; margin-bottom: 16px; }
381
+ .markdown-body ul ul, .markdown-body ul ol, .markdown-body ol ol, .markdown-body ol ul { margin-top: 0; margin-bottom: 0; }
382
+ .markdown-body li { margin: 0.25em 0; }
383
+ .markdown-body li > p { margin-top: 16px; }
384
+ .markdown-body li + li { margin-top: 0.25em; }
385
+
386
+ /* Task lists */
387
+ .markdown-body .task-list-item { list-style-type: none; }
388
+ .markdown-body .task-list-item label { font-weight: 400; }
389
+ .markdown-body .task-list-item.enabled label { cursor: pointer; }
390
+ .markdown-body .task-list-item + .task-list-item { margin-top: 4px; }
391
+ .markdown-body .task-list-item-checkbox {
392
+ margin: 0 0.2em 0.25em -1.4em;
393
+ vertical-align: middle;
394
+ }
395
+ .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { margin: 0 -1.6em 0.25em 0.2em; }
396
+ .markdown-body input[type="checkbox"] {
397
+ appearance: none;
398
+ width: 16px;
399
+ height: 16px;
400
+ border: 1px solid #d0d7de;
401
+ border-radius: 3px;
402
+ background: #fff;
403
+ vertical-align: middle;
404
+ cursor: pointer;
405
+ }
406
+ .markdown-body input[type="checkbox"]:checked {
407
+ background-color: #0969da;
408
+ border-color: #0969da;
409
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='white' d='M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z'/%3E%3C/svg%3E");
410
+ background-size: 12px 12px;
411
+ background-position: center;
412
+ background-repeat: no-repeat;
413
+ }
414
+
415
+ /* Blockquotes */
416
+ .markdown-body blockquote {
417
+ padding: 0 1em;
418
+ color: #656d76;
419
+ border-left: 0.25em solid #d0d7de;
420
+ margin: 0 0 16px;
421
+ }
422
+ .markdown-body blockquote > :first-child { margin-top: 0; }
423
+ .markdown-body blockquote > :last-child { margin-bottom: 0; }
424
+
425
+ /* GitHub Alerts */
426
+ .markdown-body .markdown-alert {
427
+ padding: 8px 16px;
428
+ margin-bottom: 16px;
429
+ color: inherit;
430
+ border-left: 0.25em solid;
431
+ border-radius: 6px;
432
+ }
433
+ .markdown-body .markdown-alert > :first-child { margin-top: 0; }
434
+ .markdown-body .markdown-alert > :last-child { margin-bottom: 0; }
435
+ .markdown-body .markdown-alert-title {
436
+ display: flex;
437
+ align-items: center;
438
+ gap: 8px;
439
+ font-weight: 600;
440
+ margin-bottom: 4px;
441
+ }
442
+ .markdown-body .markdown-alert-title svg { width: 16px; height: 16px; fill: currentColor; }
443
+ .markdown-body .markdown-alert-note { border-left-color: #0969da; background: #ddf4ff; }
444
+ .markdown-body .markdown-alert-note .markdown-alert-title { color: #0969da; }
445
+ .markdown-body .markdown-alert-tip { border-left-color: #1a7f37; background: #dafbe1; }
446
+ .markdown-body .markdown-alert-tip .markdown-alert-title { color: #1a7f37; }
447
+ .markdown-body .markdown-alert-important { border-left-color: #8250df; background: #fbefff; }
448
+ .markdown-body .markdown-alert-important .markdown-alert-title { color: #8250df; }
449
+ .markdown-body .markdown-alert-warning { border-left-color: #9a6700; background: #fff8c5; }
450
+ .markdown-body .markdown-alert-warning .markdown-alert-title { color: #9a6700; }
451
+ .markdown-body .markdown-alert-caution { border-left-color: #cf222e; background: #ffebe9; }
452
+ .markdown-body .markdown-alert-caution .markdown-alert-title { color: #cf222e; }
453
+
454
+ /* Tables */
455
+ .markdown-body table {
456
+ display: block;
457
+ width: max-content;
458
+ max-width: 100%;
459
+ overflow: auto;
460
+ margin-bottom: 16px;
461
+ border-spacing: 0;
462
+ border-collapse: collapse;
463
+ }
464
+ .markdown-body table th, .markdown-body table td {
465
+ padding: 6px 13px;
466
+ border: 1px solid #d0d7de;
467
+ }
468
+ .markdown-body table th {
469
+ font-weight: 600;
470
+ background-color: #f6f8fa;
471
+ }
472
+ .markdown-body table tr {
473
+ background-color: #fff;
474
+ border-top: 1px solid hsla(210,18%,87%,1);
475
+ }
476
+ .markdown-body table tr:nth-child(2n) {
477
+ background-color: #f6f8fa;
478
+ }
479
+
480
+ /* Images */
481
+ .markdown-body img {
482
+ max-width: 100%;
483
+ box-sizing: border-box;
484
+ background-color: #fff;
485
+ }
486
+ .markdown-body img[align=right] { padding-left: 20px; }
487
+ .markdown-body img[align=left] { padding-right: 20px; }
488
+
489
+ /* HR */
490
+ .markdown-body hr {
491
+ height: 0.25em;
492
+ padding: 0;
493
+ margin: 24px 0;
494
+ background-color: #d0d7de;
495
+ border: 0;
496
+ }
497
+
498
+ /* Details/Summary */
499
+ .markdown-body details { margin-bottom: 16px; }
500
+ .markdown-body details summary { cursor: pointer; font-weight: 600; }
501
+ .markdown-body details summary:focus { outline: none; }
502
+ .markdown-body details[open] summary { margin-bottom: 8px; }
503
+
504
+ /* Footnotes */
505
+ .markdown-body .footnotes { font-size: 12px; color: #656d76; border-top: 1px solid #d0d7de; padding-top: 16px; margin-top: 32px; }
506
+ .markdown-body .footnotes ol { padding-left: 16px; }
507
+ .markdown-body .footnotes li { margin-bottom: 8px; }
508
+ .markdown-body .footnotes li:target { background-color: #fff8c5; }
509
+ .markdown-body sup a { font-size: 12px; }
510
+ .markdown-body .data-footnote-backref { font-family: inherit; }
511
+
512
+ /* Keyboard */
513
+ .markdown-body kbd {
514
+ display: inline-block;
515
+ padding: 3px 5px;
516
+ font: 11px ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
517
+ line-height: 10px;
518
+ color: #1f2328;
519
+ vertical-align: middle;
520
+ background-color: #f6f8fa;
521
+ border: 1px solid rgba(175,184,193,0.2);
522
+ border-bottom-color: rgba(175,184,193,0.2);
523
+ border-radius: 6px;
524
+ box-shadow: inset 0 -1px 0 rgba(175,184,193,0.2);
525
+ }
526
+
527
+ /* Syntax Highlighting - highlight.js */
528
+ .hljs { color: #1f2328; background: #f6f8fa; }
529
+ .hljs-doctag, .hljs-keyword, .hljs-meta .hljs-keyword, .hljs-template-tag, .hljs-template-variable, .hljs-type, .hljs-variable.language_ { color: #cf222e; }
530
+ .hljs-title, .hljs-title.class_, .hljs-title.class_.inherited__, .hljs-title.function_ { color: #8250df; }
531
+ .hljs-attr, .hljs-attribute, .hljs-literal, .hljs-meta, .hljs-number, .hljs-operator, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-id, .hljs-variable { color: #0550ae; }
532
+ .hljs-meta .hljs-string, .hljs-regexp, .hljs-string { color: #0a3069; }
533
+ .hljs-built_in, .hljs-symbol { color: #e36209; }
534
+ .hljs-code, .hljs-comment, .hljs-formula { color: #6e7781; }
535
+ .hljs-name, .hljs-quote, .hljs-selector-pseudo, .hljs-selector-tag { color: #116329; }
536
+ .hljs-subst { color: #1f2328; }
537
+ .hljs-section { color: #0550ae; font-weight: 700; }
538
+ .hljs-bullet { color: #953800; }
539
+ .hljs-emphasis { color: #1f2328; font-style: italic; }
540
+ .hljs-strong { color: #1f2328; font-weight: 700; }
541
+ .hljs-addition { color: #116329; background-color: #dafbe1; }
542
+ .hljs-deletion { color: #82071e; background-color: #ffebe9; }
543
+
544
+ /* Toast notification */
545
+ .toast {
546
+ position: fixed;
547
+ bottom: 24px;
548
+ right: 24px;
549
+ background: #1f2328;
550
+ color: #fff;
551
+ padding: 12px 16px;
552
+ border-radius: 6px;
553
+ font-size: 14px;
554
+ opacity: 0;
555
+ transition: opacity 0.3s;
556
+ z-index: 1000;
557
+ }
558
+ .toast.show { opacity: 1; }
559
+
560
+ /* Responsive */
561
+ @media (max-width: 1012px) {
562
+ .container-xl { padding: 16px; }
563
+ .Layout { flex-direction: column-reverse; }
564
+ .Layout-sidebar { width: 100%; }
565
+ .markdown-body { padding: 24px 16px; }
566
+ }
567
+ @media (max-width: 768px) {
568
+ .repohead, .UnderlineNav { padding-left: 16px; padding-right: 16px; }
569
+ .markdown-body { padding: 16px; font-size: 14px; }
570
+ .markdown-body h1 { font-size: 1.75em; }
571
+ .markdown-body h2 { font-size: 1.35em; }
572
+ .AppHeader-nav { display: none; }
573
+ }
574
+ </style>
575
+ </head>
576
+ <body>
577
+ <header class="AppHeader">
578
+ <div class="AppHeader-logo">
579
+ <svg viewBox="0 0 16 16"><path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path></svg>
580
+ </div>
581
+ <input type="text" class="AppHeader-search" placeholder="Search or jump to...">
582
+ <nav class="AppHeader-nav">
583
+ <a href="#">Pull requests</a>
584
+ <a href="#">Issues</a>
585
+ <a href="#">Marketplace</a>
586
+ <a href="#">Explore</a>
587
+ </nav>
588
+ </header>
589
+
590
+ <div class="repohead">
591
+ <div class="repohead-details-container">
592
+ <svg viewBox="0 0 16 16"><path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 0 1 1-1ZM5 12.25v3.25a.25.25 0 0 0 .4.2l1.45-1.087a.25.25 0 0 1 .3 0L8.6 15.7a.25.25 0 0 0 .4-.2v-3.25a.25.25 0 0 0-.25-.25h-3.5a.25.25 0 0 0-.25.25Z"></path></svg>
593
+ <div class="repohead-name">
594
+ <a href="#">{{repoName}}</a>
595
+ <span class="separator">/</span>
596
+ <a href="#">{{filename}}</a>
597
+ </div>
598
+ </div>
599
+ </div>
600
+
601
+ <nav class="UnderlineNav">
602
+ <div class="UnderlineNav-body">
603
+ <a href="#" class="UnderlineNav-item selected">
604
+ <svg viewBox="0 0 16 16"><path d="m11.28 3.22 4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L13.94 8l-3.72-3.72a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215Zm-6.56 0a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L2.06 8l3.72 3.72a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L.47 8.53a.75.75 0 0 1 0-1.06Z"></path></svg>
605
+ Code
606
+ </a>
607
+ <a href="#" class="UnderlineNav-item">
608
+ <svg viewBox="0 0 16 16"><path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"></path><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"></path></svg>
609
+ Issues
610
+ </a>
611
+ <a href="#" class="UnderlineNav-item">
612
+ <svg viewBox="0 0 16 16"><path d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z"></path></svg>
613
+ Pull requests
614
+ </a>
615
+ <a href="#" class="UnderlineNav-item">
616
+ <svg viewBox="0 0 16 16"><path d="M8 0a8.2 8.2 0 0 1 .701.031C6.444.095 4.07.52 2.392 1.699c-.428.301-.986.156-1.248-.308a.75.75 0 0 1 .191-.94C3.312-.567 6.186-.984 8.001.031A8.003 8.003 0 0 1 15.969 8 8.003 8.003 0 0 1 8 16a8.002 8.002 0 0 1-7.997-7.562.75.75 0 0 1 1.5-.063A6.502 6.502 0 0 0 14.5 8a6.502 6.502 0 0 0-6.5-6.5c-.364 0-.72.03-1.07.088ZM8 4.5a.75.75 0 0 1 .75.75v2.5h2a.75.75 0 0 1 0 1.5h-2.75a.75.75 0 0 1-.75-.75v-3.25A.75.75 0 0 1 8 4.5Z"></path></svg>
617
+ Actions
618
+ </a>
619
+ <a href="#" class="UnderlineNav-item">
620
+ <svg viewBox="0 0 16 16"><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25ZM6.5 6.5v8h7.75a.25.25 0 0 0 .25-.25V6.5Zm8-1.5V1.75a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25V5Zm-8 1.5v8H1.75a.25.25 0 0 1-.25-.25V6.5Z"></path></svg>
621
+ Projects
622
+ </a>
623
+ <a href="#" class="UnderlineNav-item">
624
+ <svg viewBox="0 0 16 16"><path d="M7.467.133a1.748 1.748 0 0 1 1.066 0l5.25 1.68A1.75 1.75 0 0 1 15 3.48V7c0 1.566-.32 3.182-1.303 4.682-.983 1.498-2.585 2.813-5.032 3.855a1.697 1.697 0 0 1-1.33 0c-2.447-1.042-4.049-2.357-5.032-3.855C1.32 10.182 1 8.566 1 7V3.48a1.75 1.75 0 0 1 1.217-1.667Zm.61 1.429a.25.25 0 0 0-.153 0l-5.25 1.68a.25.25 0 0 0-.174.238V7c0 1.358.275 2.666 1.057 3.86.784 1.194 2.121 2.34 4.366 3.297a.196.196 0 0 0 .154 0c2.245-.956 3.582-2.104 4.366-3.298C13.225 9.666 13.5 8.36 13.5 7V3.48a.251.251 0 0 0-.174-.237l-5.25-1.68ZM8.75 4.75v3a.75.75 0 0 1-1.5 0v-3a.75.75 0 0 1 1.5 0ZM9 10.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>
625
+ Security
626
+ </a>
627
+ <a href="#" class="UnderlineNav-item">
628
+ <svg viewBox="0 0 16 16"><path d="M.75 8a.75.75 0 0 0 0 1.5h4a.75.75 0 0 0 0-1.5h-4Zm8 0a.75.75 0 0 0 0 1.5h4a.75.75 0 0 0 0-1.5h-4Zm-8 4a.75.75 0 0 0 0 1.5h4a.75.75 0 0 0 0-1.5h-4Zm8 0a.75.75 0 0 0 0 1.5h4a.75.75 0 0 0 0-1.5h-4Z"></path></svg>
629
+ Insights
630
+ </a>
631
+ </div>
632
+ </nav>
633
+
634
+ <div class="container-xl">
635
+ <div class="Layout">
636
+ <div class="Layout-main">
637
+ <article class="Box">
638
+ <div class="Box-header">
639
+ <div class="Box-header-title">
640
+ <svg viewBox="0 0 16 16"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"></path></svg>
641
+ {{filename}}
642
+ </div>
643
+ <button class="btn-sm">
644
+ <svg viewBox="0 0 16 16"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25ZM5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>
645
+ Copy
646
+ </button>
647
+ </div>
648
+ <div class="markdown-body">{{content}}</div>
649
+ </article>
650
+ </div>
651
+ <div class="Layout-sidebar">
652
+ <div class="BorderGrid">
653
+ <div class="BorderGrid-row">
654
+ <div class="BorderGrid-cell sidebar-about">
655
+ <h2>About</h2>
656
+ <p>{{description}}</p>
657
+ <div class="my-3">
658
+ {{topics}}
659
+ </div>
660
+ <a class="sidebar-link" href="#readme-ov-file">
661
+ <svg viewBox="0 0 16 16"><path d="M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.743 3.743 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75Zm7.251 10.324.004-5.073-.002-2.253A2.25 2.25 0 0 0 5.003 2.5H1.5v9h3.757a3.75 3.75 0 0 1 1.994.574ZM8.755 4.75l-.004 7.322a3.752 3.752 0 0 1 1.992-.572H14.5v-9h-3.495a2.25 2.25 0 0 0-2.25 2.25Z"></path></svg>
662
+ Readme
663
+ </a>
664
+ <a class="sidebar-link" href="#">
665
+ <svg viewBox="0 0 16 16"><path d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z"></path></svg>
666
+ MIT license
667
+ </a>
668
+ <a class="sidebar-link" href="#">
669
+ <svg viewBox="0 0 16 16"><path d="M6 2c.306 0 .582.187.696.471L10 10.731l1.304-3.26A.751.751 0 0 1 12 7h3.25a.75.75 0 0 1 0 1.5h-2.742l-1.812 4.528a.751.751 0 0 1-1.392 0L6 4.77 4.696 8.03A.75.75 0 0 1 4 8.5H.75a.75.75 0 0 1 0-1.5h2.742l1.812-4.529A.751.751 0 0 1 6 2Z"></path></svg>
670
+ Activity
671
+ </a>
672
+ <a class="sidebar-link" href="#">
673
+ <svg viewBox="0 0 16 16"><path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z"></path></svg>
674
+ <strong>{{stars}}</strong> stars
675
+ </a>
676
+ <a class="sidebar-link" href="#">
677
+ <svg viewBox="0 0 16 16"><path d="M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.671-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.677 1.367-1.931 2.637-3.022C4.33 2.992 6.019 2 8 2ZM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z"></path></svg>
678
+ <strong>{{watchers}}</strong> watching
679
+ </a>
680
+ <a class="sidebar-link" href="#">
681
+ <svg viewBox="0 0 16 16"><path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path></svg>
682
+ <strong>{{forks}}</strong> forks
683
+ </a>
684
+ </div>
685
+ </div>
686
+ <div class="BorderGrid-row">
687
+ <div class="BorderGrid-cell">
688
+ <h2>Languages</h2>
689
+ <div class="Progress">
690
+ <span class="Progress-item" style="width: 100%; background-color: #083fa1;"></span>
691
+ </div>
692
+ <ul class="lang-list">
693
+ <li class="lang-item">
694
+ <span class="lang-dot" style="background-color: #083fa1;"></span>
695
+ <span class="lang-name">Markdown</span>
696
+ <span class="lang-percent">100%</span>
697
+ </li>
698
+ </ul>
699
+ </div>
700
+ </div>
701
+ </div>
702
+ </div>
703
+ </div>
704
+ </div>
705
+ <div class="toast" id="toast"></div>
706
+ <script>
707
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') window.close(); });
708
+ window.addEventListener('beforeunload', () => { fetch('/close'); });
709
+ </script>
710
+ </body>
711
+ </html>`;
712
+
713
+ function highlightCode(code: string, language: string): string {
714
+ if (language && hljs.getLanguage(language)) {
715
+ return hljs.highlight(code, { language }).value;
716
+ }
717
+ return hljs.highlightAuto(code).value;
718
+ }
719
+
720
+ function slugify(text: string): string {
721
+ return text
722
+ .toLowerCase()
723
+ .trim()
724
+ .replace(/<[^>]*>/g, "")
725
+ .replace(/[^\w\s-]/g, "")
726
+ .replace(/\s+/g, "-");
727
+ }
728
+
729
+ const ALERT_ICONS: Record<string, string> = {
730
+ note: '<svg viewBox="0 0 16 16" width="16" height="16"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg>',
731
+ tip: '<svg viewBox="0 0 16 16" width="16" height="16"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg>',
732
+ important:
733
+ '<svg viewBox="0 0 16 16" width="16" height="16"><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>',
734
+ warning:
735
+ '<svg viewBox="0 0 16 16" width="16" height="16"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>',
736
+ caution:
737
+ '<svg viewBox="0 0 16 16" width="16" height="16"><path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg>',
738
+ };
739
+
740
+ function processAlerts(html: string): string {
741
+ const alertBlockRegex = /<blockquote>([\s\S]*?)<\/blockquote>/gi;
742
+ const alertPrefixRegex =
743
+ /^\s*<p>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?:<br\s*\/?>(?:\s*)?|\s)*/i;
744
+
745
+ return html.replace(alertBlockRegex, (match, inner) => {
746
+ const prefixMatch = inner.match(alertPrefixRegex);
747
+ if (!prefixMatch) {
748
+ return match;
749
+ }
750
+
751
+ const typeKey = prefixMatch[1].toLowerCase();
752
+ const icon = ALERT_ICONS[typeKey] || "";
753
+ const title =
754
+ prefixMatch[1].charAt(0) + prefixMatch[1].slice(1).toLowerCase();
755
+ const cleanedContent = inner.replace(alertPrefixRegex, "<p>").trim();
756
+
757
+ return `<div class="markdown-alert markdown-alert-${typeKey}">
758
+ <p class="markdown-alert-title">${icon}${title}</p>
759
+ ${cleanedContent}
760
+ </div>`;
761
+ });
762
+ }
763
+
764
+ function renderMarkdown(content: string): string {
765
+ marked.setOptions({
766
+ gfm: true, // GitHub Flavored Markdown
767
+ breaks: false, // Match GitHub's single-line break behavior
768
+ pedantic: false, // Conform to original markdown.pl
769
+ smartLists: true, // Use smarter list behavior
770
+ smartypants: false, // Use "smart" typographic punctuation
771
+ mangle: false, // Don't mangle emails
772
+ headerIds: false, // We handle header IDs manually for GitHub-style anchors
773
+ });
774
+
775
+ const renderer = new marked.Renderer();
776
+
777
+ // Code blocks with syntax highlighting
778
+ renderer.code = ({ text, lang }) => {
779
+ if (lang) {
780
+ return `<pre><code class="hljs language-${lang}">${highlightCode(text, lang)}</code></pre>`;
781
+ }
782
+ return `<pre><code class="hljs">${highlightCode(text, "")}</code></pre>`;
783
+ };
784
+
785
+ // Headings with anchor links
786
+ renderer.heading = ({ text, depth }) => {
787
+ const slug = slugify(text);
788
+ const anchorIcon =
789
+ '<svg class="octicon" viewBox="0 0 16 16" width="16" height="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>';
790
+ return `<h${depth} id="${slug}"><a class="anchor" href="#${slug}">${anchorIcon}</a>${text}</h${depth}>\n`;
791
+ };
792
+
793
+ // List with task list support
794
+ renderer.list = (token) => {
795
+ const body = token.items
796
+ .map((item: { task?: boolean; checked?: boolean; tokens: unknown[] }) => {
797
+ // Parse tokens as block content (handles nested lists properly)
798
+ const text = marked.parser(item.tokens as marked.Token[], { renderer });
799
+ if (item.task) {
800
+ const checkbox = item.checked
801
+ ? '<input type="checkbox" class="task-list-item-checkbox" checked disabled>'
802
+ : '<input type="checkbox" class="task-list-item-checkbox" disabled>';
803
+ return `<li class="task-list-item">${checkbox}${text}</li>\n`;
804
+ }
805
+ return `<li>${text}</li>\n`;
806
+ })
807
+ .join("");
808
+ const isTaskList = token.items.some(
809
+ (item: { task?: boolean }) => item.task,
810
+ );
811
+ const listClass = isTaskList ? ' class="contains-task-list"' : "";
812
+ if (token.ordered) {
813
+ const startAttr = token.start !== 1 ? ` start="${token.start}"` : "";
814
+ return `<ol${startAttr}${listClass}>\n${body}</ol>\n`;
815
+ }
816
+ return `<ul${listClass}>\n${body}</ul>\n`;
817
+ };
818
+
819
+ let html = marked.parse(content, { renderer }) as string;
820
+ html = processAlerts(html);
821
+
822
+ return html;
823
+ }
824
+
825
+ function extractDescription(content: string): string {
826
+ // Try to find first paragraph after any heading
827
+ const lines = content.split("\n");
828
+ let foundHeading = false;
829
+ let description = "";
830
+
831
+ for (const line of lines) {
832
+ const trimmed = line.trim();
833
+ if (trimmed.startsWith("#")) {
834
+ foundHeading = true;
835
+ continue;
836
+ }
837
+ if (
838
+ foundHeading &&
839
+ trimmed &&
840
+ !trimmed.startsWith("#") &&
841
+ !trimmed.startsWith("-") &&
842
+ !trimmed.startsWith("*") &&
843
+ !trimmed.startsWith("`")
844
+ ) {
845
+ description = trimmed.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); // Remove markdown links
846
+ break;
847
+ }
848
+ }
849
+
850
+ return description || "No description provided.";
851
+ }
852
+
853
+ function extractTopics(repoName: string): string {
854
+ // Generate some placeholder topics based on repo name
855
+ const topics = ["markdown", "preview", "documentation"];
856
+ return topics.map((t) => `<a href="#" class="topic-tag">${t}</a>`).join("");
857
+ }
858
+
859
+ function getHtml(
860
+ filename: string,
861
+ content: string,
862
+ fileTree: string,
863
+ repoName: string,
864
+ dirPath: string,
865
+ ): string {
866
+ const rendered = renderMarkdown(content);
867
+ const description = extractDescription(content);
868
+ const topics = extractTopics(repoName);
869
+
870
+ return HTML_TEMPLATE.replace(/\{\{filename\}\}/g, filename)
871
+ .replace("{{content}}", rendered)
872
+ .replace("{{fileTree}}", fileTree)
873
+ .replace(/\{\{repoName\}\}/g, repoName)
874
+ .replace("{{dirPath}}", dirPath)
875
+ .replace("{{description}}", description)
876
+ .replace("{{topics}}", topics)
877
+ .replace("{{stars}}", "0")
878
+ .replace("{{watchers}}", "1")
879
+ .replace("{{forks}}", "0");
880
+ }
881
+
882
+ function showToast(message: string): void {
883
+ console.log(`[peekmd] ${message}`);
884
+ }
885
+
886
+ async function openBrowser(url: string): Promise<void> {
887
+ const { execSync } = await import("child_process");
888
+ const platform = process.platform;
889
+
890
+ try {
891
+ if (platform === "darwin") {
892
+ execSync(`open "${url}"`, { stdio: "ignore" });
893
+ } else if (platform === "win32") {
894
+ execSync(`start "" "${url}"`, { stdio: "ignore" });
895
+ } else {
896
+ execSync(`xdg-open "${url}"`, { stdio: "ignore" });
897
+ }
898
+ } catch {
899
+ showToast(
900
+ "Could not open browser automatically. Please open the URL manually.",
901
+ );
902
+ }
903
+ }
904
+
905
+ interface ServerState {
906
+ port: number;
907
+ isOpen: boolean;
908
+ }
909
+
910
+ const state: ServerState = { port: 0, isOpen: false };
911
+
912
+ export async function main(): Promise<void> {
913
+ const args = process.argv.slice(2);
914
+
915
+ if (args.length === 0) {
916
+ console.log("Usage: peekmd <file.md>");
917
+ console.log(
918
+ " Opens a GitHub-style preview of a markdown file in your default browser.",
919
+ );
920
+ process.exit(1);
921
+ }
922
+
923
+ const filePath = args[0];
924
+
925
+ if (!existsSync(filePath)) {
926
+ console.error(`Error: File not found: ${filePath}`);
927
+ process.exit(1);
928
+ }
929
+
930
+ const ext = extname(filePath).toLowerCase();
931
+ if (ext !== ".md" && ext !== ".markdown" && ext !== ".mdown") {
932
+ console.warn(`Warning: File '${filePath}' may not be a markdown file.`);
933
+ }
934
+
935
+ const content = readFileSync(filePath, "utf-8");
936
+ const filename = filePath.split(sep).pop() || filePath;
937
+ const repoName = getDirName(filePath);
938
+ const dirPath = getRelativePath(filePath) || "";
939
+ const fileTree = renderFileTree(getFileTree(process.cwd(), 3));
940
+
941
+ const port = 3456;
942
+ state.port = port;
943
+
944
+ const server = Bun.serve({
945
+ port,
946
+ routes: {
947
+ "/": {
948
+ GET: () => {
949
+ const html = getHtml(filename, content, fileTree, repoName, dirPath);
950
+ return new Response(html, {
951
+ headers: { "Content-Type": "text/html; charset=utf-8" },
952
+ });
953
+ },
954
+ },
955
+ "/close": {
956
+ GET: () => {
957
+ state.isOpen = false;
958
+ setTimeout(() => {
959
+ if (!state.isOpen) {
960
+ server.stop();
961
+ showToast("Server closed.");
962
+ }
963
+ }, 1000);
964
+ return new Response("ok");
965
+ },
966
+ },
967
+ },
968
+ development: false,
969
+ });
970
+
971
+ const url = `http://localhost:${port}`;
972
+ showToast(`Serving ${filename} at ${url}`);
973
+ showToast("Press ESC or close this window to exit.");
974
+
975
+ await openBrowser(url);
976
+ state.isOpen = true;
977
+ }
978
+