gh-here 2.0.0 → 3.0.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/.claude/settings.local.json +20 -11
- package/README.md +30 -101
- package/SAMPLE.md +287 -0
- package/lib/constants.js +38 -0
- package/lib/error-handler.js +55 -0
- package/lib/file-tree-builder.js +81 -0
- package/lib/file-utils.js +43 -12
- package/lib/renderers.js +440 -194
- package/lib/server.js +120 -32
- package/lib/validation.js +77 -0
- package/package.json +1 -1
- package/public/app.js +199 -1825
- package/public/app.js.backup +1902 -0
- package/public/js/clipboard-utils.js +45 -0
- package/public/js/constants.js +60 -0
- package/public/js/draft-manager.js +36 -0
- package/public/js/editor-manager.js +159 -0
- package/public/js/file-tree.js +321 -0
- package/public/js/keyboard-handler.js +41 -0
- package/public/js/modal-manager.js +70 -0
- package/public/js/navigation.js +254 -0
- package/public/js/notification.js +23 -0
- package/public/js/search-handler.js +238 -0
- package/public/js/theme-manager.js +108 -0
- package/public/js/utils.js +123 -0
- package/public/styles.css +874 -570
- package/.channels_cache_v2.json +0 -10882
- package/.users_cache.json +0 -16187
- package/blog-post.md +0 -100
package/lib/renderers.js
CHANGED
|
@@ -8,6 +8,23 @@ const { exec } = require('child_process');
|
|
|
8
8
|
const { getFileIcon, getLanguageFromExtension, getLanguageColor, formatBytes, isImageFile, isBinaryFile, isTextFile } = require('./file-utils');
|
|
9
9
|
const { getGitStatusIcon, getGitStatusDescription } = require('./git');
|
|
10
10
|
|
|
11
|
+
// Configure marked to use highlight.js for syntax highlighting
|
|
12
|
+
marked.use({
|
|
13
|
+
renderer: {
|
|
14
|
+
code(code, language) {
|
|
15
|
+
if (language && hljs.getLanguage(language)) {
|
|
16
|
+
try {
|
|
17
|
+
return `<pre><code class="hljs language-${language}">${hljs.highlight(code, { language }).value}</code></pre>`;
|
|
18
|
+
} catch (err) {
|
|
19
|
+
// Fall back to auto-detection if language-specific highlighting fails
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Auto-detect language if not specified or language not found
|
|
23
|
+
return `<pre><code class="hljs">${hljs.highlightAuto(code).value}</code></pre>`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
11
28
|
/**
|
|
12
29
|
* HTML rendering module
|
|
13
30
|
* Handles all HTML template generation for different views
|
|
@@ -21,8 +38,21 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
21
38
|
// Only show language stats on root/top-level directory
|
|
22
39
|
const languageStats = (!currentPath || currentPath === '.') ? generateLanguageStats(items) : '';
|
|
23
40
|
|
|
24
|
-
const itemsHtml = items.map(item =>
|
|
25
|
-
|
|
41
|
+
const itemsHtml = items.map(item => {
|
|
42
|
+
const statusMap = {
|
|
43
|
+
'A': 'added',
|
|
44
|
+
'M': 'modified',
|
|
45
|
+
'D': 'deleted',
|
|
46
|
+
'R': 'renamed',
|
|
47
|
+
'??': 'untracked',
|
|
48
|
+
'MM': 'mixed',
|
|
49
|
+
'AM': 'mixed',
|
|
50
|
+
'AD': 'mixed'
|
|
51
|
+
};
|
|
52
|
+
const statusKey = item.gitStatus ? statusMap[item.gitStatus.status] || '' : '';
|
|
53
|
+
const rowStatusClass = statusKey ? ` file-row--${statusKey}` : '';
|
|
54
|
+
return `
|
|
55
|
+
<tr class="file-row${rowStatusClass}" data-name="${item.name.toLowerCase()}" data-type="${item.isDirectory ? 'dir' : 'file'}" data-path="${item.path}">
|
|
26
56
|
<td class="icon">
|
|
27
57
|
${item.isDirectory ? octicons['file-directory-fill'].toSVG({ class: 'octicon-directory' }) : getFileIcon(item.name)}
|
|
28
58
|
</td>
|
|
@@ -66,7 +96,7 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
66
96
|
${item.modified.toLocaleDateString()}
|
|
67
97
|
</td>
|
|
68
98
|
</tr>
|
|
69
|
-
`).join('');
|
|
99
|
+
`}).join('');
|
|
70
100
|
|
|
71
101
|
return `
|
|
72
102
|
<!DOCTYPE html>
|
|
@@ -74,7 +104,23 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
74
104
|
<head>
|
|
75
105
|
<title>gh-here: ${currentPath || 'Root'}</title>
|
|
76
106
|
<link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
|
|
77
|
-
<script
|
|
107
|
+
<script>
|
|
108
|
+
// Check localStorage and add showGitignored param if needed (before page renders)
|
|
109
|
+
(function() {
|
|
110
|
+
const showGitignored = localStorage.getItem('gh-here-show-gitignored') === 'true';
|
|
111
|
+
const url = new URL(window.location.href);
|
|
112
|
+
const hasParam = url.searchParams.has('showGitignored');
|
|
113
|
+
|
|
114
|
+
if (showGitignored && !hasParam) {
|
|
115
|
+
url.searchParams.set('showGitignored', 'true');
|
|
116
|
+
window.location.replace(url.toString());
|
|
117
|
+
} else if (!showGitignored && hasParam) {
|
|
118
|
+
url.searchParams.delete('showGitignored');
|
|
119
|
+
window.location.replace(url.toString());
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
122
|
+
</script>
|
|
123
|
+
<script type="module" src="/static/app.js"></script>
|
|
78
124
|
</head>
|
|
79
125
|
<body>
|
|
80
126
|
<header>
|
|
@@ -83,13 +129,7 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
83
129
|
<h1>gh-here</h1>
|
|
84
130
|
</div>
|
|
85
131
|
<div class="header-right">
|
|
86
|
-
|
|
87
|
-
<button id="commit-btn" class="commit-btn" title="Commit changes">
|
|
88
|
-
${octicons['git-commit'].toSVG({ class: 'commit-icon' })}
|
|
89
|
-
<span class="commit-text">Commit</span>
|
|
90
|
-
</button>
|
|
91
|
-
` : ''}
|
|
92
|
-
<button id="gitignore-toggle" class="gitignore-toggle ${showGitignored ? 'showing-ignored' : ''}" aria-label="Toggle .gitignore filtering" title="${showGitignored ? 'Hide' : 'Show'} gitignored files">
|
|
132
|
+
<button id="gitignore-toggle" class="gitignore-toggle" aria-label="Toggle .gitignore filtering" title="Toggle gitignored files">
|
|
93
133
|
${octicons.eye.toSVG({ class: 'gitignore-icon' })}
|
|
94
134
|
</button>
|
|
95
135
|
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
@@ -98,41 +138,60 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
98
138
|
</div>
|
|
99
139
|
</div>
|
|
100
140
|
</header>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
` : ''}
|
|
117
|
-
${languageStats}
|
|
118
|
-
</div>
|
|
119
|
-
<div class="repo-controls-right">
|
|
120
|
-
<div class="search-container">
|
|
121
|
-
${octicons.search.toSVG({ class: 'search-icon' })}
|
|
122
|
-
<input type="text" id="file-search" placeholder="Go to file" class="search-input">
|
|
123
|
-
<kbd class="search-hotkey">t</kbd>
|
|
124
|
-
</div>
|
|
125
|
-
<button id="new-file-btn" class="btn btn-outline">
|
|
126
|
-
<span class="btn-text">New file</span>
|
|
141
|
+
|
|
142
|
+
<main>
|
|
143
|
+
<aside class="file-tree-sidebar ${!currentPath || currentPath === '' ? 'hidden' : ''}">
|
|
144
|
+
<div class="file-tree-header">
|
|
145
|
+
<svg class="files-icon" viewBox="0 0 16 16" width="16" height="16">
|
|
146
|
+
<path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"></path>
|
|
147
|
+
</svg>
|
|
148
|
+
<span>Files</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="sidebar-controls">
|
|
151
|
+
${gitBranch ? `
|
|
152
|
+
<button class="branch-button sidebar-branch">
|
|
153
|
+
${octicons['git-branch'].toSVG({ class: 'octicon-branch' })}
|
|
154
|
+
<span class="branch-name">${gitBranch}</span>
|
|
155
|
+
${octicons['chevron-down'].toSVG({ class: 'octicon-chevron' })}
|
|
127
156
|
</button>
|
|
157
|
+
` : ''}
|
|
158
|
+
<div class="search-container sidebar-search">
|
|
159
|
+
${octicons.search.toSVG({ class: 'search-icon' })}
|
|
160
|
+
<input type="text" id="file-search" placeholder="Go to file" class="search-input">
|
|
161
|
+
<kbd class="search-hotkey">t</kbd>
|
|
128
162
|
</div>
|
|
129
163
|
</div>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
164
|
+
<div id="file-tree" class="file-tree-container"></div>
|
|
165
|
+
</aside>
|
|
166
|
+
<div class="main-content-wrapper ${!currentPath || currentPath === '' ? 'no-sidebar' : ''}">
|
|
167
|
+
<div class="repo-canvas">
|
|
168
|
+
<div class="repo-canvas-content">
|
|
169
|
+
<div class="breadcrumb-section">${breadcrumbs}</div>
|
|
170
|
+
${(!currentPath || currentPath === '.') ? `
|
|
171
|
+
<hr class="repo-divider">
|
|
172
|
+
|
|
173
|
+
<div class="repo-controls">
|
|
174
|
+
<div class="repo-controls-left">
|
|
175
|
+
${gitBranch ? `
|
|
176
|
+
<button class="branch-button">
|
|
177
|
+
${octicons['git-branch'].toSVG({ class: 'octicon-branch' })}
|
|
178
|
+
<span class="branch-name">${gitBranch}</span>
|
|
179
|
+
${octicons['chevron-down'].toSVG({ class: 'octicon-chevron' })}
|
|
180
|
+
</button>
|
|
181
|
+
` : ''}
|
|
182
|
+
${languageStats}
|
|
183
|
+
</div>
|
|
184
|
+
<div class="repo-controls-right">
|
|
185
|
+
<div class="search-container">
|
|
186
|
+
${octicons.search.toSVG({ class: 'search-icon' })}
|
|
187
|
+
<input type="text" id="root-file-search" placeholder="Go to file" class="search-input">
|
|
188
|
+
<kbd class="search-hotkey">/</kbd>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
` : ''}
|
|
193
|
+
|
|
194
|
+
<div class="file-table-container">
|
|
136
195
|
<table class="file-table" id="file-table">
|
|
137
196
|
<thead>
|
|
138
197
|
<tr>
|
|
@@ -159,7 +218,8 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
159
218
|
</tbody>
|
|
160
219
|
</table>
|
|
161
220
|
</div>
|
|
162
|
-
|
|
221
|
+
${readmePreview}
|
|
222
|
+
</div>
|
|
163
223
|
</div>
|
|
164
224
|
</div>
|
|
165
225
|
</main>
|
|
@@ -243,7 +303,7 @@ function generateLanguageStats(items) {
|
|
|
243
303
|
`;
|
|
244
304
|
}
|
|
245
305
|
|
|
246
|
-
async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot, workingDir = null) {
|
|
306
|
+
async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot, workingDir = null, gitBranch = null) {
|
|
247
307
|
const workingDirName = workingDir ? path.basename(workingDir) : null;
|
|
248
308
|
const breadcrumbs = generateBreadcrumbs(filePath, null, workingDirName);
|
|
249
309
|
|
|
@@ -260,26 +320,88 @@ async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot, workingDir =
|
|
|
260
320
|
|
|
261
321
|
const diffContent = renderRawDiff(stdout, ext);
|
|
262
322
|
const currentParams = new URLSearchParams({ path: filePath });
|
|
263
|
-
const viewUrl = `/?${currentParams.toString()}
|
|
264
|
-
const rawUrl = `/?${currentParams.toString()}&view=raw`;
|
|
323
|
+
const viewUrl = `/?${currentParams.toString()}`;
|
|
265
324
|
const diffUrl = `/?${currentParams.toString()}&view=diff`;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
325
|
+
|
|
326
|
+
// Get file stats for header
|
|
327
|
+
const fullPath = path.join(workingDir, filePath);
|
|
328
|
+
let fileSize = '';
|
|
329
|
+
let lineCount = 0;
|
|
330
|
+
let locCount = 0;
|
|
331
|
+
try {
|
|
332
|
+
const stats = fs.statSync(fullPath);
|
|
333
|
+
fileSize = formatBytes(stats.size);
|
|
334
|
+
if (isTextFile(ext)) {
|
|
335
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
336
|
+
lineCount = content.split('\n').length;
|
|
337
|
+
locCount = content.split('\n').filter(line => line.trim().length > 0).length;
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
// File stats optional
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const rawUrlApi = `/api/file-content?path=${encodeURIComponent(filePath)}`;
|
|
344
|
+
const downloadUrl = `/download?path=${encodeURIComponent(filePath)}`;
|
|
345
|
+
|
|
346
|
+
// Clean file header with separated view toggle and action buttons
|
|
347
|
+
const fileHeader = `
|
|
348
|
+
<div class="file-header">
|
|
349
|
+
<div class="file-header-main">
|
|
350
|
+
<div class="file-path-info">
|
|
351
|
+
<span class="file-path-text">${filePath}</span>
|
|
352
|
+
<button class="file-path-copy-btn" data-path="${filePath}" title="Copy file path">
|
|
353
|
+
${octicons.copy.toSVG({ class: 'octicon-copy', width: 16, height: 16 })}
|
|
354
|
+
</button>
|
|
355
|
+
</div>
|
|
356
|
+
${isTextFile(ext) && lineCount > 0 ? `
|
|
357
|
+
<div class="file-stats">
|
|
358
|
+
<span class="file-stat">${lineCount} line${lineCount !== 1 ? 's' : ''}</span>
|
|
359
|
+
<span class="file-stat-separator">·</span>
|
|
360
|
+
<span class="file-stat">${locCount} loc</span>
|
|
361
|
+
<span class="file-stat-separator">·</span>
|
|
362
|
+
<span class="file-stat">${fileSize}</span>
|
|
363
|
+
</div>
|
|
364
|
+
` : fileSize ? `
|
|
365
|
+
<div class="file-stats">
|
|
366
|
+
<span class="file-stat">${fileSize}</span>
|
|
367
|
+
</div>
|
|
368
|
+
` : ''}
|
|
369
|
+
</div>
|
|
370
|
+
<div class="file-header-actions">
|
|
371
|
+
<div class="view-toggle">
|
|
372
|
+
<a href="${viewUrl}" class="view-btn">
|
|
373
|
+
${octicons.eye.toSVG({ class: 'view-icon' })}
|
|
374
|
+
<span>View</span>
|
|
375
|
+
</a>
|
|
376
|
+
<a href="${diffUrl}" class="view-btn active">
|
|
377
|
+
${octicons.diff.toSVG({ class: 'view-icon' })}
|
|
378
|
+
<span>Diff</span>
|
|
379
|
+
</a>
|
|
380
|
+
</div>
|
|
381
|
+
${isTextFile(ext) ? `
|
|
382
|
+
<div class="file-action-group">
|
|
383
|
+
<a href="${rawUrlApi}" class="file-action-btn" target="_blank" title="View raw file">
|
|
384
|
+
${octicons['file-code'].toSVG({ class: 'file-action-icon', width: 16, height: 16 })}
|
|
385
|
+
</a>
|
|
386
|
+
<button class="file-action-btn copy-raw-btn" data-path="${filePath}" title="Copy raw content">
|
|
387
|
+
${octicons.copy.toSVG({ class: 'file-action-icon', width: 16, height: 16 })}
|
|
388
|
+
</button>
|
|
389
|
+
<a href="${downloadUrl}" class="file-action-btn" download="${path.basename(filePath)}" title="Download file">
|
|
390
|
+
${octicons.download.toSVG({ class: 'file-action-icon', width: 16, height: 16 })}
|
|
391
|
+
</a>
|
|
392
|
+
</div>
|
|
393
|
+
` : `
|
|
394
|
+
<div class="file-action-group">
|
|
395
|
+
<a href="${downloadUrl}" class="file-action-btn" download="${path.basename(filePath)}" title="Download file">
|
|
396
|
+
${octicons.download.toSVG({ class: 'file-action-icon', width: 16, height: 16 })}
|
|
397
|
+
</a>
|
|
398
|
+
</div>
|
|
399
|
+
`}
|
|
400
|
+
</div>
|
|
280
401
|
</div>
|
|
281
402
|
`;
|
|
282
403
|
|
|
404
|
+
// Use same structure as renderFile - with sidebar
|
|
283
405
|
const html = `
|
|
284
406
|
<!DOCTYPE html>
|
|
285
407
|
<html data-theme="dark">
|
|
@@ -287,7 +409,23 @@ async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot, workingDir =
|
|
|
287
409
|
<title>gh-here: ${path.basename(filePath)} (diff)</title>
|
|
288
410
|
<link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
|
|
289
411
|
<link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
|
|
290
|
-
<script
|
|
412
|
+
<script>
|
|
413
|
+
// Check localStorage and add showGitignored param if needed (before page renders)
|
|
414
|
+
(function() {
|
|
415
|
+
const showGitignored = localStorage.getItem('gh-here-show-gitignored') === 'true';
|
|
416
|
+
const url = new URL(window.location.href);
|
|
417
|
+
const hasParam = url.searchParams.has('showGitignored');
|
|
418
|
+
|
|
419
|
+
if (showGitignored && !hasParam) {
|
|
420
|
+
url.searchParams.set('showGitignored', 'true');
|
|
421
|
+
window.location.replace(url.toString());
|
|
422
|
+
} else if (!showGitignored && hasParam) {
|
|
423
|
+
url.searchParams.delete('showGitignored');
|
|
424
|
+
window.location.replace(url.toString());
|
|
425
|
+
}
|
|
426
|
+
})();
|
|
427
|
+
</script>
|
|
428
|
+
<script type="module" src="/static/app.js"></script>
|
|
291
429
|
</head>
|
|
292
430
|
<body>
|
|
293
431
|
<header>
|
|
@@ -296,7 +434,9 @@ async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot, workingDir =
|
|
|
296
434
|
<h1>gh-here</h1>
|
|
297
435
|
</div>
|
|
298
436
|
<div class="header-right">
|
|
299
|
-
|
|
437
|
+
<button id="gitignore-toggle" class="gitignore-toggle" aria-label="Toggle .gitignore filtering" title="Toggle gitignored files">
|
|
438
|
+
${octicons.eye.toSVG({ class: 'gitignore-icon' })}
|
|
439
|
+
</button>
|
|
300
440
|
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
301
441
|
${octicons.moon.toSVG({ class: 'theme-icon' })}
|
|
302
442
|
</button>
|
|
@@ -304,11 +444,32 @@ async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot, workingDir =
|
|
|
304
444
|
</div>
|
|
305
445
|
</header>
|
|
306
446
|
<main>
|
|
307
|
-
<
|
|
308
|
-
<div class="
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
447
|
+
<aside class="file-tree-sidebar">
|
|
448
|
+
<div class="file-tree-header">
|
|
449
|
+
<svg class="files-icon" viewBox="0 0 16 16" width="16" height="16">
|
|
450
|
+
<path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"></path>
|
|
451
|
+
</svg>
|
|
452
|
+
<span>Files</span>
|
|
453
|
+
</div>
|
|
454
|
+
<div class="sidebar-controls">
|
|
455
|
+
<div class="search-container sidebar-search">
|
|
456
|
+
${octicons.search.toSVG({ class: 'search-icon' })}
|
|
457
|
+
<input type="text" id="file-search" placeholder="Go to file" class="search-input">
|
|
458
|
+
<kbd class="search-hotkey">t</kbd>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
<div id="file-tree" class="file-tree-container"></div>
|
|
462
|
+
</aside>
|
|
463
|
+
<div class="main-content-wrapper">
|
|
464
|
+
<div class="main-content">
|
|
465
|
+
<div class="breadcrumb-section">${breadcrumbs}</div>
|
|
466
|
+
${fileHeader}
|
|
467
|
+
<div class="file-content">
|
|
468
|
+
<div class="diff-container">
|
|
469
|
+
<div class="diff-content">
|
|
470
|
+
${diffContent}
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
312
473
|
</div>
|
|
313
474
|
</div>
|
|
314
475
|
</div>
|
|
@@ -316,7 +477,7 @@ async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot, workingDir =
|
|
|
316
477
|
</body>
|
|
317
478
|
</html>
|
|
318
479
|
`;
|
|
319
|
-
|
|
480
|
+
|
|
320
481
|
resolve(html);
|
|
321
482
|
});
|
|
322
483
|
});
|
|
@@ -326,51 +487,111 @@ function renderRawDiff(diffOutput, ext) {
|
|
|
326
487
|
if (!diffOutput.trim()) {
|
|
327
488
|
return '<div class="no-changes">No changes to display</div>';
|
|
328
489
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
let
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
highlighted = hljs.highlight(diffOutput, { language: 'diff' }).value;
|
|
337
|
-
} catch {
|
|
338
|
-
// Fallback to plain text if diff highlighting fails
|
|
339
|
-
highlighted = diffOutput.replace(/&/g, '&')
|
|
340
|
-
.replace(/</g, '<')
|
|
341
|
-
.replace(/>/g, '>');
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Split into lines and add line numbers
|
|
345
|
-
const lines = highlighted.split('\n');
|
|
346
|
-
let lineNumber = 1;
|
|
347
|
-
|
|
490
|
+
|
|
491
|
+
// Parse diff to extract line numbers and render properly
|
|
492
|
+
const lines = diffOutput.split('\n');
|
|
493
|
+
let oldLineNum = 0;
|
|
494
|
+
let newLineNum = 0;
|
|
495
|
+
let inHunk = false;
|
|
496
|
+
|
|
348
497
|
const linesHtml = lines.map(line => {
|
|
349
|
-
//
|
|
498
|
+
// Detect hunk header: @@ -oldStart,oldCount +newStart,newCount @@
|
|
499
|
+
if (line.startsWith('@@')) {
|
|
500
|
+
const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
501
|
+
if (match) {
|
|
502
|
+
oldLineNum = parseInt(match[1], 10);
|
|
503
|
+
newLineNum = parseInt(match[2], 10);
|
|
504
|
+
inHunk = true;
|
|
505
|
+
}
|
|
506
|
+
// Hunk headers get no line numbers, just styling
|
|
507
|
+
const escapedLine = escapeHtml(line);
|
|
508
|
+
return `<div class="diff-line diff-line-hunk">
|
|
509
|
+
<span class="diff-line-number old"></span>
|
|
510
|
+
<span class="diff-line-number new"></span>
|
|
511
|
+
<span class="diff-line-content">${escapedLine}</span>
|
|
512
|
+
</div>`;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// File headers (diff --git, index, ---, +++)
|
|
516
|
+
if (line.startsWith('diff --git') || line.startsWith('index ') ||
|
|
517
|
+
line.startsWith('---') || line.startsWith('+++')) {
|
|
518
|
+
const escapedLine = escapeHtml(line);
|
|
519
|
+
return `<div class="diff-line diff-line-header">
|
|
520
|
+
<span class="diff-line-number old"></span>
|
|
521
|
+
<span class="diff-line-number new"></span>
|
|
522
|
+
<span class="diff-line-content">${escapedLine}</span>
|
|
523
|
+
</div>`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (!inHunk || line.length === 0) {
|
|
527
|
+
// Empty lines or lines before first hunk
|
|
528
|
+
const escapedLine = escapeHtml(line);
|
|
529
|
+
return `<div class="diff-line diff-line-context">
|
|
530
|
+
<span class="diff-line-number old"></span>
|
|
531
|
+
<span class="diff-line-number new"></span>
|
|
532
|
+
<span class="diff-line-content">${escapedLine}</span>
|
|
533
|
+
</div>`;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const firstChar = line.charAt(0);
|
|
350
537
|
let lineType = 'context';
|
|
351
|
-
let
|
|
352
|
-
|
|
353
|
-
|
|
538
|
+
let oldNum = '';
|
|
539
|
+
let newNum = '';
|
|
540
|
+
let content = line;
|
|
541
|
+
|
|
542
|
+
if (firstChar === '-') {
|
|
543
|
+
// Removed line: show old number, blank new number
|
|
354
544
|
lineType = 'removed';
|
|
355
|
-
|
|
545
|
+
oldNum = oldLineNum.toString();
|
|
546
|
+
newNum = '';
|
|
547
|
+
oldLineNum++;
|
|
548
|
+
// Keep the - prefix in content for visual consistency
|
|
549
|
+
content = line;
|
|
550
|
+
} else if (firstChar === '+') {
|
|
551
|
+
// Added line: blank old number, show new number
|
|
356
552
|
lineType = 'added';
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
553
|
+
oldNum = '';
|
|
554
|
+
newNum = newLineNum.toString();
|
|
555
|
+
newLineNum++;
|
|
556
|
+
// Keep the + prefix in content
|
|
557
|
+
content = line;
|
|
558
|
+
} else {
|
|
559
|
+
// Context line: show both numbers
|
|
560
|
+
lineType = 'context';
|
|
561
|
+
oldNum = oldLineNum.toString();
|
|
562
|
+
newNum = newLineNum.toString();
|
|
563
|
+
oldLineNum++;
|
|
564
|
+
newLineNum++;
|
|
565
|
+
content = line;
|
|
361
566
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
567
|
+
|
|
568
|
+
// Apply syntax highlighting to the content
|
|
569
|
+
let highlightedContent;
|
|
570
|
+
try {
|
|
571
|
+
highlightedContent = hljs.highlight(content, { language: 'diff' }).value;
|
|
572
|
+
} catch {
|
|
573
|
+
highlightedContent = escapeHtml(content);
|
|
574
|
+
}
|
|
575
|
+
|
|
365
576
|
return `<div class="diff-line diff-line-${lineType}">
|
|
366
|
-
<span class="diff-line-number">${
|
|
367
|
-
<span class="diff-line-
|
|
577
|
+
<span class="diff-line-number old">${oldNum}</span>
|
|
578
|
+
<span class="diff-line-number new">${newNum}</span>
|
|
579
|
+
<span class="diff-line-content">${highlightedContent}</span>
|
|
368
580
|
</div>`;
|
|
369
581
|
}).join('');
|
|
370
|
-
|
|
582
|
+
|
|
371
583
|
return `<div class="raw-diff-container">${linesHtml}</div>`;
|
|
372
584
|
}
|
|
373
585
|
|
|
586
|
+
// Helper function to escape HTML
|
|
587
|
+
function escapeHtml(text) {
|
|
588
|
+
return text.replace(/&/g, '&')
|
|
589
|
+
.replace(/</g, '<')
|
|
590
|
+
.replace(/>/g, '>')
|
|
591
|
+
.replace(/"/g, '"')
|
|
592
|
+
.replace(/'/g, ''');
|
|
593
|
+
}
|
|
594
|
+
|
|
374
595
|
|
|
375
596
|
async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStatus = null, workingDir = null) {
|
|
376
597
|
const workingDirName = workingDir ? path.basename(workingDir) : null;
|
|
@@ -378,10 +599,21 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
378
599
|
let displayContent;
|
|
379
600
|
let viewToggle = '';
|
|
380
601
|
|
|
602
|
+
// Get file stats
|
|
603
|
+
const fullPath = path.join(workingDir, filePath);
|
|
604
|
+
const stats = fs.statSync(fullPath);
|
|
605
|
+
const fileSize = formatBytes(stats.size);
|
|
606
|
+
const lineCount = isTextFile(ext) ? content.split('\n').length : 0;
|
|
607
|
+
const locCount = isTextFile(ext) ? content.split('\n').filter(line => line.trim().length > 0).length : 0;
|
|
608
|
+
|
|
381
609
|
// Check if file has git changes
|
|
382
610
|
const absolutePath = path.resolve(path.join(workingDir, filePath));
|
|
383
611
|
const hasGitChanges = gitStatus && gitStatus[absolutePath];
|
|
384
612
|
|
|
613
|
+
// URLs for file actions (will be used in fileHeader after viewToggle is set)
|
|
614
|
+
const rawUrl = `/api/file-content?path=${encodeURIComponent(filePath)}`;
|
|
615
|
+
const downloadUrl = `/download?path=${encodeURIComponent(filePath)}`;
|
|
616
|
+
|
|
385
617
|
// Determine file category and handle accordingly
|
|
386
618
|
if (isImageFile(ext)) {
|
|
387
619
|
// Handle image files
|
|
@@ -399,14 +631,16 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
399
631
|
if (hasGitChanges) {
|
|
400
632
|
const currentParams = new URLSearchParams({ path: filePath });
|
|
401
633
|
const diffUrl = `/?${currentParams.toString()}&view=diff`;
|
|
402
|
-
|
|
634
|
+
|
|
403
635
|
viewToggle = `
|
|
404
636
|
<div class="view-toggle">
|
|
405
637
|
<a href="/?path=${encodeURIComponent(filePath)}" class="view-btn active">
|
|
406
|
-
${octicons.eye.toSVG({ class: 'view-icon' })}
|
|
638
|
+
${octicons.eye.toSVG({ class: 'view-icon' })}
|
|
639
|
+
<span>View</span>
|
|
407
640
|
</a>
|
|
408
641
|
<a href="${diffUrl}" class="view-btn">
|
|
409
|
-
${octicons.diff.toSVG({ class: 'view-icon' })}
|
|
642
|
+
${octicons.diff.toSVG({ class: 'view-icon' })}
|
|
643
|
+
<span>Diff</span>
|
|
410
644
|
</a>
|
|
411
645
|
</div>
|
|
412
646
|
`;
|
|
@@ -427,35 +661,38 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
427
661
|
} else if (ext === 'md') {
|
|
428
662
|
if (viewMode === 'raw') {
|
|
429
663
|
const highlighted = hljs.highlight(content, { language: 'markdown' }).value;
|
|
430
|
-
|
|
664
|
+
|
|
431
665
|
// Add line numbers for raw markdown view
|
|
432
666
|
const lines = highlighted.split('\n');
|
|
433
667
|
const numberedLines = lines.map((line, index) => {
|
|
434
668
|
const lineNum = index + 1;
|
|
435
669
|
return `<span class="line-container" data-line="${lineNum}"><a class="line-number" href="#L${lineNum}" id="L${lineNum}">${lineNum}</a><span class="line-content">${line}</span></span>`;
|
|
436
670
|
}).join('');
|
|
437
|
-
|
|
671
|
+
|
|
438
672
|
displayContent = `<pre><code class="hljs with-line-numbers">${numberedLines}</code></pre>`;
|
|
439
673
|
} else {
|
|
440
674
|
displayContent = `<div class="markdown">${marked.parse(content)}</div>`;
|
|
441
675
|
}
|
|
442
|
-
|
|
676
|
+
|
|
443
677
|
const currentParams = new URLSearchParams({ path: filePath });
|
|
444
|
-
const
|
|
678
|
+
const rawUrlView = `/?${currentParams.toString()}&view=raw`;
|
|
445
679
|
const renderedUrl = `/?${currentParams.toString()}&view=rendered`;
|
|
446
680
|
const diffUrl = `/?${currentParams.toString()}&view=diff`;
|
|
447
|
-
|
|
681
|
+
|
|
448
682
|
viewToggle = `
|
|
449
683
|
<div class="view-toggle">
|
|
450
684
|
<a href="${renderedUrl}" class="view-btn ${viewMode === 'rendered' ? 'active' : ''}">
|
|
451
|
-
${octicons.eye.toSVG({ class: 'view-icon' })}
|
|
685
|
+
${octicons.eye.toSVG({ class: 'view-icon' })}
|
|
686
|
+
<span>View</span>
|
|
452
687
|
</a>
|
|
453
|
-
<a href="${
|
|
454
|
-
${octicons['file-code'].toSVG({ class: 'view-icon' })}
|
|
688
|
+
<a href="${rawUrlView}" class="view-btn ${viewMode === 'raw' ? 'active' : ''}">
|
|
689
|
+
${octicons['file-code'].toSVG({ class: 'view-icon' })}
|
|
690
|
+
<span>Raw</span>
|
|
455
691
|
</a>
|
|
456
692
|
${hasGitChanges ? `
|
|
457
693
|
<a href="${diffUrl}" class="view-btn ${viewMode === 'diff' ? 'active' : ''}">
|
|
458
|
-
${octicons.diff.toSVG({ class: 'view-icon' })}
|
|
694
|
+
${octicons.diff.toSVG({ class: 'view-icon' })}
|
|
695
|
+
<span>Diff</span>
|
|
459
696
|
</a>
|
|
460
697
|
` : ''}
|
|
461
698
|
</div>
|
|
@@ -480,93 +717,95 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
480
717
|
const currentParams = new URLSearchParams({ path: filePath });
|
|
481
718
|
const viewUrl = `/?${currentParams.toString()}&view=rendered`;
|
|
482
719
|
const diffUrl = `/?${currentParams.toString()}&view=diff`;
|
|
483
|
-
|
|
720
|
+
|
|
484
721
|
viewToggle = `
|
|
485
722
|
<div class="view-toggle">
|
|
486
723
|
<a href="${viewUrl}" class="view-btn ${viewMode === 'rendered' ? 'active' : ''}">
|
|
487
|
-
${octicons.eye.toSVG({ class: 'view-icon' })}
|
|
724
|
+
${octicons.eye.toSVG({ class: 'view-icon' })}
|
|
725
|
+
<span>View</span>
|
|
488
726
|
</a>
|
|
489
727
|
<a href="${diffUrl}" class="view-btn ${viewMode === 'diff' ? 'active' : ''}">
|
|
490
|
-
${octicons.diff.toSVG({ class: 'view-icon' })}
|
|
728
|
+
${octicons.diff.toSVG({ class: 'view-icon' })}
|
|
729
|
+
<span>Diff</span>
|
|
491
730
|
</a>
|
|
492
731
|
</div>
|
|
493
732
|
`;
|
|
494
733
|
}
|
|
495
734
|
}
|
|
496
735
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
<
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
</head>
|
|
507
|
-
<body>
|
|
508
|
-
<header>
|
|
509
|
-
<div class="header-content">
|
|
510
|
-
<div class="header-left">
|
|
511
|
-
<h1>gh-here</h1>
|
|
512
|
-
</div>
|
|
513
|
-
<div class="header-right">
|
|
514
|
-
<div id="filename-input-container" class="filename-input-container" style="display: none;">
|
|
515
|
-
<input type="text" id="filename-input" class="filename-input" placeholder="Name your file...">
|
|
516
|
-
</div>
|
|
517
|
-
${isTextFile(ext) ? `
|
|
518
|
-
<button id="edit-btn" class="edit-btn" aria-label="Edit file">
|
|
519
|
-
${octicons.pencil.toSVG({ class: 'edit-icon' })}
|
|
520
|
-
</button>
|
|
521
|
-
` : ''}
|
|
522
|
-
${viewToggle}
|
|
523
|
-
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
524
|
-
${octicons.moon.toSVG({ class: 'theme-icon' })}
|
|
525
|
-
</button>
|
|
526
|
-
</div>
|
|
736
|
+
// Build file header after viewToggle is set (so it includes diff button if applicable)
|
|
737
|
+
const fileHeader = `
|
|
738
|
+
<div class="file-header">
|
|
739
|
+
<div class="file-header-main">
|
|
740
|
+
<div class="file-path-info">
|
|
741
|
+
<span class="file-path-text">${filePath}</span>
|
|
742
|
+
<button class="file-path-copy-btn" data-path="${filePath}" title="Copy file path">
|
|
743
|
+
${octicons.copy.toSVG({ class: 'octicon-copy', width: 16, height: 16 })}
|
|
744
|
+
</button>
|
|
527
745
|
</div>
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
746
|
+
${isTextFile(ext) ? `
|
|
747
|
+
<div class="file-stats">
|
|
748
|
+
<span class="file-stat">${lineCount} line${lineCount !== 1 ? 's' : ''}</span>
|
|
749
|
+
<span class="file-stat-separator">·</span>
|
|
750
|
+
<span class="file-stat">${locCount} loc</span>
|
|
751
|
+
<span class="file-stat-separator">·</span>
|
|
752
|
+
<span class="file-stat">${fileSize}</span>
|
|
753
|
+
</div>
|
|
754
|
+
` : `
|
|
755
|
+
<div class="file-stats">
|
|
756
|
+
<span class="file-stat">${fileSize}</span>
|
|
534
757
|
</div>
|
|
758
|
+
`}
|
|
759
|
+
</div>
|
|
760
|
+
<div class="file-header-actions">
|
|
761
|
+
${viewToggle}
|
|
535
762
|
${isTextFile(ext) ? `
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
763
|
+
<div class="file-action-group">
|
|
764
|
+
<a href="${rawUrl}" class="file-action-btn" target="_blank" title="View raw file">
|
|
765
|
+
${octicons['file-code'].toSVG({ class: 'file-action-icon', width: 16, height: 16 })}
|
|
766
|
+
</a>
|
|
767
|
+
<button class="file-action-btn copy-raw-btn" data-path="${filePath}" title="Copy raw content">
|
|
768
|
+
${octicons.copy.toSVG({ class: 'file-action-icon', width: 16, height: 16 })}
|
|
769
|
+
</button>
|
|
770
|
+
<a href="${downloadUrl}" class="file-action-btn" download="${path.basename(filePath)}" title="Download file">
|
|
771
|
+
${octicons.download.toSVG({ class: 'file-action-icon', width: 16, height: 16 })}
|
|
772
|
+
</a>
|
|
544
773
|
</div>
|
|
545
|
-
|
|
546
|
-
|
|
774
|
+
` : `
|
|
775
|
+
<div class="file-action-group">
|
|
776
|
+
<a href="${downloadUrl}" class="file-action-btn" download="${path.basename(filePath)}" title="Download file">
|
|
777
|
+
${octicons.download.toSVG({ class: 'file-action-icon', width: 16, height: 16 })}
|
|
778
|
+
</a>
|
|
547
779
|
</div>
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
</main>
|
|
552
|
-
</body>
|
|
553
|
-
</html>
|
|
780
|
+
`}
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
554
783
|
`;
|
|
555
|
-
}
|
|
556
784
|
|
|
557
|
-
function renderNewFile(currentPath, workingDir = null) {
|
|
558
|
-
const workingDirName = workingDir ? path.basename(workingDir) : null;
|
|
559
|
-
const breadcrumbs = generateBreadcrumbs(currentPath, null, workingDirName);
|
|
560
|
-
|
|
561
785
|
return `
|
|
562
786
|
<!DOCTYPE html>
|
|
563
787
|
<html data-theme="dark">
|
|
564
788
|
<head>
|
|
565
|
-
<title>gh-here:
|
|
789
|
+
<title>gh-here: ${path.basename(filePath)}</title>
|
|
566
790
|
<link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
|
|
567
791
|
<link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
|
|
568
|
-
<script
|
|
569
|
-
|
|
792
|
+
<script>
|
|
793
|
+
// Check localStorage and add showGitignored param if needed (before page renders)
|
|
794
|
+
(function() {
|
|
795
|
+
const showGitignored = localStorage.getItem('gh-here-show-gitignored') === 'true';
|
|
796
|
+
const url = new URL(window.location.href);
|
|
797
|
+
const hasParam = url.searchParams.has('showGitignored');
|
|
798
|
+
|
|
799
|
+
if (showGitignored && !hasParam) {
|
|
800
|
+
url.searchParams.set('showGitignored', 'true');
|
|
801
|
+
window.location.replace(url.toString());
|
|
802
|
+
} else if (!showGitignored && hasParam) {
|
|
803
|
+
url.searchParams.delete('showGitignored');
|
|
804
|
+
window.location.replace(url.toString());
|
|
805
|
+
}
|
|
806
|
+
})();
|
|
807
|
+
</script>
|
|
808
|
+
<script type="module" src="/static/app.js"></script>
|
|
570
809
|
</head>
|
|
571
810
|
<body>
|
|
572
811
|
<header>
|
|
@@ -575,6 +814,9 @@ function renderNewFile(currentPath, workingDir = null) {
|
|
|
575
814
|
<h1>gh-here</h1>
|
|
576
815
|
</div>
|
|
577
816
|
<div class="header-right">
|
|
817
|
+
<button id="gitignore-toggle" class="gitignore-toggle" aria-label="Toggle .gitignore filtering" title="Toggle gitignored files">
|
|
818
|
+
${octicons.eye.toSVG({ class: 'gitignore-icon' })}
|
|
819
|
+
</button>
|
|
578
820
|
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
579
821
|
${octicons.moon.toSVG({ class: 'theme-icon' })}
|
|
580
822
|
</button>
|
|
@@ -582,26 +824,31 @@ function renderNewFile(currentPath, workingDir = null) {
|
|
|
582
824
|
</div>
|
|
583
825
|
</header>
|
|
584
826
|
<main>
|
|
585
|
-
<
|
|
586
|
-
<div class="
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
<div class="
|
|
594
|
-
|
|
595
|
-
<
|
|
827
|
+
<aside class="file-tree-sidebar">
|
|
828
|
+
<div class="file-tree-header">
|
|
829
|
+
<svg class="files-icon" viewBox="0 0 16 16" width="16" height="16">
|
|
830
|
+
<path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"></path>
|
|
831
|
+
</svg>
|
|
832
|
+
<span>Files</span>
|
|
833
|
+
</div>
|
|
834
|
+
<div class="sidebar-controls">
|
|
835
|
+
<div class="search-container sidebar-search">
|
|
836
|
+
${octicons.search.toSVG({ class: 'search-icon' })}
|
|
837
|
+
<input type="text" id="file-search" placeholder="Go to file" class="search-input">
|
|
838
|
+
<kbd class="search-hotkey">t</kbd>
|
|
596
839
|
</div>
|
|
597
840
|
</div>
|
|
598
|
-
<div class="
|
|
599
|
-
|
|
600
|
-
|
|
841
|
+
<div id="file-tree" class="file-tree-container"></div>
|
|
842
|
+
</aside>
|
|
843
|
+
<div class="main-content-wrapper">
|
|
844
|
+
<div class="main-content">
|
|
845
|
+
<div class="breadcrumb-section">${breadcrumbs}</div>
|
|
846
|
+
${fileHeader}
|
|
847
|
+
<div class="file-content">
|
|
848
|
+
${displayContent}
|
|
601
849
|
</div>
|
|
602
850
|
</div>
|
|
603
851
|
</div>
|
|
604
|
-
</div>
|
|
605
852
|
</main>
|
|
606
853
|
</body>
|
|
607
854
|
</html>
|
|
@@ -644,7 +891,6 @@ module.exports = {
|
|
|
644
891
|
renderDirectory,
|
|
645
892
|
renderFileDiff,
|
|
646
893
|
renderFile,
|
|
647
|
-
renderNewFile,
|
|
648
894
|
generateBreadcrumbs,
|
|
649
895
|
findReadmeFile,
|
|
650
896
|
generateReadmePreview,
|