gh-here 1.0.1 → 1.0.4

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.
@@ -0,0 +1,569 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const hljs = require('highlight.js');
4
+ const marked = require('marked');
5
+ const octicons = require('@primer/octicons');
6
+ const { exec } = require('child_process');
7
+
8
+ const { getFileIcon, getLanguageFromExtension, getLanguageColor, formatBytes } = require('./file-utils');
9
+ const { getGitStatusIcon, getGitStatusDescription } = require('./git');
10
+
11
+ /**
12
+ * HTML rendering module
13
+ * Handles all HTML template generation for different views
14
+ */
15
+
16
+ function renderDirectory(currentPath, items, showGitignored = false, gitBranch = null, gitStatus = {}) {
17
+ const breadcrumbs = generateBreadcrumbs(currentPath, gitBranch);
18
+ const readmeFile = findReadmeFile(items);
19
+ const readmePreview = readmeFile ? generateReadmePreview(currentPath, readmeFile) : '';
20
+ const languageStats = generateLanguageStats(items);
21
+
22
+ const itemsHtml = items.map(item => `
23
+ <tr class="file-row" data-name="${item.name.toLowerCase()}" data-type="${item.isDirectory ? 'dir' : 'file'}" data-path="${item.path}">
24
+ <td class="icon">
25
+ ${item.isDirectory ? octicons['file-directory'].toSVG({ class: 'octicon-directory' }) : getFileIcon(item.name)}
26
+ </td>
27
+ <td class="git-status-col">
28
+ ${item.gitStatus ? (item.gitStatus.status === '??' ? `<span class="git-status git-status-untracked" title="Untracked file">${require('./git').getGitStatusIcon('??')}</span>` : `<span class="git-status git-status-${item.gitStatus.status.replace(' ', '')}" title="Git Status: ${getGitStatusDescription(item.gitStatus.status)}">${getGitStatusIcon(item.gitStatus.status)}</span>`) : ''}
29
+ </td>
30
+ <td class="name">
31
+ <a href="/?path=${encodeURIComponent(item.path)}">${item.name}</a>
32
+ <div class="quick-actions">
33
+ <button class="quick-btn copy-path-btn" title="Copy path" data-path="${item.path}">
34
+ ${octicons.copy.toSVG({ class: 'quick-icon' })}
35
+ </button>
36
+ ${!item.isDirectory && item.gitStatus ? `
37
+ <button class="quick-btn diff-btn" title="Show diff" data-path="${item.path}">
38
+ ${octicons.diff.toSVG({ class: 'quick-icon' })}
39
+ </button>
40
+ ` : ''}
41
+ ${!item.isDirectory ? `
42
+ <a class="quick-btn download-btn" href="/download?path=${encodeURIComponent(item.path)}" title="Download" download="${item.name}">
43
+ ${octicons.download.toSVG({ class: 'quick-icon' })}
44
+ </a>
45
+ ` : ''}
46
+ ${!item.isDirectory ? `
47
+ <button class="quick-btn edit-file-btn" title="Edit file" data-path="${item.path}">
48
+ ${octicons.pencil.toSVG({ class: 'quick-icon' })}
49
+ </button>
50
+ ` : `
51
+ <button class="quick-btn rename-btn" title="Rename" data-path="${item.path}" data-name="${item.name}" data-is-directory="${item.isDirectory}">
52
+ ${octicons.pencil.toSVG({ class: 'quick-icon' })}
53
+ </button>
54
+ `}
55
+ <button class="quick-btn delete-btn" title="Delete" data-path="${item.path}" data-name="${item.name}" data-is-directory="${item.isDirectory}">
56
+ ${octicons.trash.toSVG({ class: 'quick-icon' })}
57
+ </button>
58
+ </div>
59
+ </td>
60
+ <td class="size">
61
+ ${item.isDirectory ? '-' : formatBytes(item.size)}
62
+ </td>
63
+ <td class="modified">
64
+ ${item.modified.toLocaleDateString()}
65
+ </td>
66
+ </tr>
67
+ `).join('');
68
+
69
+ return `
70
+ <!DOCTYPE html>
71
+ <html data-theme="dark">
72
+ <head>
73
+ <title>gh-here: ${currentPath || 'Root'}</title>
74
+ <link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
75
+ <script src="/static/app.js"></script>
76
+ </head>
77
+ <body>
78
+ <header>
79
+ <div class="header-content">
80
+ <div class="header-left">
81
+ <h1 class="header-path">${breadcrumbs}</h1>
82
+ </div>
83
+ <div class="header-right">
84
+ <div class="search-container">
85
+ ${octicons.search.toSVG({ class: 'search-icon' })}
86
+ <input type="text" id="file-search" placeholder="Find files..." class="search-input">
87
+ </div>
88
+ ${Object.keys(gitStatus).length > 0 ? `
89
+ <button id="commit-btn" class="commit-btn" title="Commit changes">
90
+ ${octicons['git-commit'].toSVG({ class: 'commit-icon' })}
91
+ <span class="commit-text">Commit</span>
92
+ </button>
93
+ ` : ''}
94
+ <button id="gitignore-toggle" class="gitignore-toggle ${showGitignored ? 'showing-ignored' : ''}" aria-label="Toggle .gitignore filtering" title="${showGitignored ? 'Hide' : 'Show'} gitignored files">
95
+ ${octicons.eye.toSVG({ class: 'gitignore-icon' })}
96
+ </button>
97
+ <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
98
+ ${octicons.moon.toSVG({ class: 'theme-icon' })}
99
+ </button>
100
+ </div>
101
+ </div>
102
+ </header>
103
+ <main>
104
+ ${languageStats}
105
+ <div class="directory-actions">
106
+ <button id="new-file-btn" class="btn btn-secondary">
107
+ ${octicons['file-added'].toSVG({ class: 'btn-icon' })} New file
108
+ </button>
109
+ </div>
110
+ <div class="file-table-container">
111
+ <table class="file-table" id="file-table">
112
+ <thead>
113
+ <tr>
114
+ <th></th>
115
+ <th></th>
116
+ <th>Name</th>
117
+ <th>Size</th>
118
+ <th>Modified</th>
119
+ </tr>
120
+ </thead>
121
+ <tbody>
122
+ ${currentPath && currentPath !== '.' ? `
123
+ <tr class="file-row" data-name=".." data-type="dir">
124
+ <td class="icon">${octicons['arrow-up'].toSVG({ class: 'octicon-directory' })}</td>
125
+ <td class="git-status-col"></td>
126
+ <td class="name">
127
+ <a href="/?path=${encodeURIComponent(path.dirname(currentPath))}">.</a>
128
+ </td>
129
+ <td class="size">-</td>
130
+ <td class="modified">-</td>
131
+ </tr>
132
+ ` : ''}
133
+ ${itemsHtml}
134
+ </tbody>
135
+ </table>
136
+ </div>
137
+ ${readmePreview}
138
+ </main>
139
+ </body>
140
+ </html>
141
+ `;
142
+ }
143
+
144
+ function findReadmeFile(items) {
145
+ const readmeNames = ['README.md', 'readme.md', 'README.rst', 'readme.rst', 'README.txt', 'readme.txt', 'README'];
146
+ return items.find(item => !item.isDirectory && readmeNames.includes(item.name));
147
+ }
148
+
149
+ function generateReadmePreview(currentPath, readmeFile) {
150
+ try {
151
+ const readmePath = path.join(process.cwd(), currentPath, readmeFile.name);
152
+ const content = fs.readFileSync(readmePath, 'utf8');
153
+ const ext = path.extname(readmeFile.name).slice(1).toLowerCase();
154
+
155
+ let renderedContent;
156
+ if (ext === 'md' || ext === '') {
157
+ renderedContent = `<div class="markdown">${marked.parse(content)}</div>`;
158
+ } else {
159
+ const highlighted = hljs.highlightAuto(content).value;
160
+ renderedContent = `<pre><code class="hljs">${highlighted}</code></pre>`;
161
+ }
162
+
163
+ return `
164
+ <div class="readme-section">
165
+ <div class="readme-header">
166
+ <h2>
167
+ ${octicons.book.toSVG({ class: 'readme-icon' })}
168
+ ${readmeFile.name}
169
+ </h2>
170
+ </div>
171
+ <div class="readme-content">
172
+ ${renderedContent}
173
+ </div>
174
+ </div>
175
+ `;
176
+ } catch (error) {
177
+ return '';
178
+ }
179
+ }
180
+
181
+ function generateLanguageStats(items) {
182
+ const languages = {};
183
+ let totalFiles = 0;
184
+
185
+ items.forEach(item => {
186
+ if (!item.isDirectory) {
187
+ const ext = path.extname(item.name).slice(1).toLowerCase();
188
+ const lang = getLanguageFromExtension(ext) || 'other';
189
+ languages[lang] = (languages[lang] || 0) + 1;
190
+ totalFiles++;
191
+ }
192
+ });
193
+
194
+ if (totalFiles === 0) return '';
195
+
196
+ const sortedLangs = Object.entries(languages)
197
+ .sort(([,a], [,b]) => b - a)
198
+ .slice(0, 5);
199
+
200
+ const statsHtml = sortedLangs.map(([lang, count]) => {
201
+ const percentage = ((count / totalFiles) * 100).toFixed(1);
202
+ const color = getLanguageColor(lang);
203
+ return `
204
+ <div class="lang-stat">
205
+ <span class="lang-dot" style="background-color: ${color}"></span>
206
+ <span class="lang-name">${lang}</span>
207
+ <span class="lang-percent">${percentage}%</span>
208
+ </div>
209
+ `;
210
+ }).join('');
211
+
212
+ return `
213
+ <div class="language-stats">
214
+ ${statsHtml}
215
+ </div>
216
+ `;
217
+ }
218
+
219
+ async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot) {
220
+ const breadcrumbs = generateBreadcrumbs(filePath);
221
+
222
+ // Get git diff for the file
223
+ return new Promise((resolve, reject) => {
224
+ const diffCommand = gitInfo.staged ?
225
+ `git diff --cached "${filePath}"` :
226
+ `git diff "${filePath}"`;
227
+
228
+ exec(diffCommand, { cwd: gitRepoRoot }, (error, stdout) => {
229
+ if (error) {
230
+ return reject(error);
231
+ }
232
+
233
+ const diffContent = renderRawDiff(stdout, ext);
234
+ const currentParams = new URLSearchParams({ path: filePath });
235
+ const viewUrl = `/?${currentParams.toString()}&view=rendered`;
236
+ const rawUrl = `/?${currentParams.toString()}&view=raw`;
237
+ const diffUrl = `/?${currentParams.toString()}&view=diff`;
238
+
239
+ const viewToggle = `
240
+ <div class="view-toggle">
241
+ <a href="${viewUrl}" class="view-btn">
242
+ ${octicons.eye.toSVG({ class: 'view-icon' })} View
243
+ </a>
244
+ ${ext === 'md' ? `
245
+ <a href="${rawUrl}" class="view-btn">
246
+ ${octicons['file-code'].toSVG({ class: 'view-icon' })} Raw
247
+ </a>
248
+ ` : ''}
249
+ <a href="${diffUrl}" class="view-btn active">
250
+ ${octicons.diff.toSVG({ class: 'view-icon' })} Diff
251
+ </a>
252
+ </div>
253
+ `;
254
+
255
+ const html = `
256
+ <!DOCTYPE html>
257
+ <html data-theme="dark">
258
+ <head>
259
+ <title>gh-here: ${path.basename(filePath)} (diff)</title>
260
+ <link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
261
+ <link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
262
+ <script src="/static/app.js"></script>
263
+ </head>
264
+ <body>
265
+ <header>
266
+ <div class="header-content">
267
+ <div class="header-left">
268
+ <h1 class="header-path">${breadcrumbs}</h1>
269
+ </div>
270
+ <div class="header-right">
271
+ ${viewToggle}
272
+ <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
273
+ ${octicons.moon.toSVG({ class: 'theme-icon' })}
274
+ </button>
275
+ </div>
276
+ </div>
277
+ </header>
278
+ <main>
279
+ <div class="diff-container">
280
+ <div class="diff-content">
281
+ ${diffContent}
282
+ </div>
283
+ </div>
284
+ </main>
285
+ </body>
286
+ </html>
287
+ `;
288
+
289
+ resolve(html);
290
+ });
291
+ });
292
+ }
293
+
294
+ function renderRawDiff(diffOutput, ext) {
295
+ if (!diffOutput.trim()) {
296
+ return '<div class="no-changes">No changes to display</div>';
297
+ }
298
+
299
+ const language = getLanguageFromExtension(ext);
300
+
301
+ // Apply syntax highlighting to the entire diff
302
+ let highlighted;
303
+ try {
304
+ // Use diff language for syntax highlighting if available, otherwise use the file's language
305
+ highlighted = hljs.highlight(diffOutput, { language: 'diff' }).value;
306
+ } catch {
307
+ // Fallback to plain text if diff highlighting fails
308
+ highlighted = diffOutput.replace(/&/g, '&amp;')
309
+ .replace(/</g, '&lt;')
310
+ .replace(/>/g, '&gt;');
311
+ }
312
+
313
+ // Split into lines and add line numbers
314
+ const lines = highlighted.split('\n');
315
+ let lineNumber = 1;
316
+
317
+ const linesHtml = lines.map(line => {
318
+ // Determine line type based on first character
319
+ let lineType = 'context';
320
+ let displayLine = line;
321
+
322
+ if (line.startsWith('<span class="hljs-deletion">-') || line.startsWith('-')) {
323
+ lineType = 'removed';
324
+ } else if (line.startsWith('<span class="hljs-addition">+') || line.startsWith('+')) {
325
+ lineType = 'added';
326
+ } else if (line.startsWith('@@') || line.includes('hljs-meta')) {
327
+ lineType = 'hunk';
328
+ } else if (line.startsWith('diff --git') || line.startsWith('index ') || line.startsWith('---') || line.startsWith('+++')) {
329
+ lineType = 'header';
330
+ }
331
+
332
+ const currentLineNumber = (lineType === 'context' || lineType === 'removed' || lineType === 'added') ? lineNumber++ : '';
333
+
334
+ return `<div class="diff-line diff-line-${lineType}">
335
+ <span class="diff-line-number">${currentLineNumber}</span>
336
+ <span class="diff-line-content">${displayLine}</span>
337
+ </div>`;
338
+ }).join('');
339
+
340
+ return `<div class="raw-diff-container">${linesHtml}</div>`;
341
+ }
342
+
343
+ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStatus = null, workingDir) {
344
+ const breadcrumbs = generateBreadcrumbs(filePath);
345
+ let displayContent;
346
+ let viewToggle = '';
347
+
348
+ // Check if file has git changes
349
+ const absolutePath = path.resolve(path.join(workingDir, filePath));
350
+ const hasGitChanges = gitStatus && gitStatus[absolutePath];
351
+
352
+ if (ext === 'md') {
353
+ if (viewMode === 'raw') {
354
+ const highlighted = hljs.highlight(content, { language: 'markdown' }).value;
355
+
356
+ // Add line numbers for raw markdown view
357
+ const lines = highlighted.split('\n');
358
+ const numberedLines = lines.map((line, index) => {
359
+ const lineNum = index + 1;
360
+ 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>`;
361
+ }).join('');
362
+
363
+ displayContent = `<pre><code class="hljs with-line-numbers">${numberedLines}</code></pre>`;
364
+ } else {
365
+ displayContent = `<div class="markdown">${marked.parse(content)}</div>`;
366
+ }
367
+
368
+ const currentParams = new URLSearchParams({ path: filePath });
369
+ const rawUrl = `/?${currentParams.toString()}&view=raw`;
370
+ const renderedUrl = `/?${currentParams.toString()}&view=rendered`;
371
+ const diffUrl = `/?${currentParams.toString()}&view=diff`;
372
+
373
+ viewToggle = `
374
+ <div class="view-toggle">
375
+ <a href="${renderedUrl}" class="view-btn ${viewMode === 'rendered' ? 'active' : ''}">
376
+ ${octicons.eye.toSVG({ class: 'view-icon' })} View
377
+ </a>
378
+ <a href="${rawUrl}" class="view-btn ${viewMode === 'raw' ? 'active' : ''}">
379
+ ${octicons['file-code'].toSVG({ class: 'view-icon' })} Raw
380
+ </a>
381
+ ${hasGitChanges ? `
382
+ <a href="${diffUrl}" class="view-btn ${viewMode === 'diff' ? 'active' : ''}">
383
+ ${octicons.diff.toSVG({ class: 'view-icon' })} Diff
384
+ </a>
385
+ ` : ''}
386
+ </div>
387
+ `;
388
+ } else {
389
+ const language = getLanguageFromExtension(ext);
390
+ const highlighted = language ?
391
+ hljs.highlight(content, { language }).value :
392
+ hljs.highlightAuto(content).value;
393
+
394
+ // Add line numbers with clickable links
395
+ const lines = highlighted.split('\n');
396
+ const numberedLines = lines.map((line, index) => {
397
+ const lineNum = index + 1;
398
+ 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>`;
399
+ }).join('');
400
+
401
+ displayContent = `<pre><code class="hljs with-line-numbers">${numberedLines}</code></pre>`;
402
+
403
+ // Add view toggle for non-markdown files with git changes
404
+ if (hasGitChanges) {
405
+ const currentParams = new URLSearchParams({ path: filePath });
406
+ const viewUrl = `/?${currentParams.toString()}&view=rendered`;
407
+ const diffUrl = `/?${currentParams.toString()}&view=diff`;
408
+
409
+ viewToggle = `
410
+ <div class="view-toggle">
411
+ <a href="${viewUrl}" class="view-btn ${viewMode === 'rendered' ? 'active' : ''}">
412
+ ${octicons.eye.toSVG({ class: 'view-icon' })} View
413
+ </a>
414
+ <a href="${diffUrl}" class="view-btn ${viewMode === 'diff' ? 'active' : ''}">
415
+ ${octicons.diff.toSVG({ class: 'view-icon' })} Diff
416
+ </a>
417
+ </div>
418
+ `;
419
+ }
420
+ }
421
+
422
+ return `
423
+ <!DOCTYPE html>
424
+ <html data-theme="dark">
425
+ <head>
426
+ <title>gh-here: ${path.basename(filePath)}</title>
427
+ <link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
428
+ <link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
429
+ <script src="/static/app.js"></script>
430
+ <script src="https://unpkg.com/monaco-editor@0.45.0/min/vs/loader.js"></script>
431
+ </head>
432
+ <body>
433
+ <header>
434
+ <div class="header-content">
435
+ <div class="header-left">
436
+ <h1 class="header-path">
437
+ ${breadcrumbs}
438
+ ${hasGitChanges ? (hasGitChanges.status === '??' ? `<span class="git-status git-status-untracked" title="Untracked file">${getGitStatusIcon('??')}</span>` : `<span class="git-status git-status-${hasGitChanges.status.replace(' ', '')}" title="Git Status: ${getGitStatusDescription(hasGitChanges.status)}">${getGitStatusIcon(hasGitChanges.status)}</span>`) : ''}
439
+ </h1>
440
+ </div>
441
+ <div class="header-right">
442
+ <div id="filename-input-container" class="filename-input-container" style="display: none;">
443
+ <input type="text" id="filename-input" class="filename-input" placeholder="Name your file...">
444
+ </div>
445
+ <button id="edit-btn" class="edit-btn" aria-label="Edit file">
446
+ ${octicons.pencil.toSVG({ class: 'edit-icon' })}
447
+ </button>
448
+ ${viewToggle}
449
+ <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
450
+ ${octicons.moon.toSVG({ class: 'theme-icon' })}
451
+ </button>
452
+ </div>
453
+ </div>
454
+ </header>
455
+ <main>
456
+ <div class="file-content">
457
+ ${displayContent}
458
+ </div>
459
+ <div id="editor-container" class="editor-container" style="display: none;">
460
+ <div class="editor-header">
461
+ <div class="editor-title">Edit ${path.basename(filePath)}</div>
462
+ <div class="editor-actions">
463
+ <button id="word-wrap-btn" class="btn btn-secondary" title="Toggle word wrap (Alt+Z)">↩ Wrap</button>
464
+ <button id="cancel-btn" class="btn btn-secondary">Cancel</button>
465
+ <button id="save-btn" class="btn btn-primary">Save</button>
466
+ </div>
467
+ </div>
468
+ <div class="monaco-editor-container">
469
+ <div id="file-editor" class="monaco-editor"></div>
470
+ </div>
471
+ </div>
472
+ </main>
473
+ </body>
474
+ </html>
475
+ `;
476
+ }
477
+
478
+ function renderNewFile(currentPath) {
479
+ const breadcrumbs = generateBreadcrumbs(currentPath);
480
+
481
+ return `
482
+ <!DOCTYPE html>
483
+ <html data-theme="dark">
484
+ <head>
485
+ <title>gh-here: Create new file</title>
486
+ <link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
487
+ <link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
488
+ <script src="/static/app.js"></script>
489
+ <script src="https://unpkg.com/monaco-editor@0.45.0/min/vs/loader.js"></script>
490
+ </head>
491
+ <body>
492
+ <header>
493
+ <div class="header-content">
494
+ <div class="header-left">
495
+ <h1 class="header-path">${breadcrumbs}</h1>
496
+ </div>
497
+ <div class="header-right">
498
+ <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
499
+ ${octicons.moon.toSVG({ class: 'theme-icon' })}
500
+ </button>
501
+ </div>
502
+ </div>
503
+ </header>
504
+ <main>
505
+ <div class="new-file-container">
506
+ <div class="new-file-header">
507
+ <div class="filename-section">
508
+ <span class="filename-label">Name your file...</span>
509
+ <input type="text" id="new-filename-input" class="new-filename-input" placeholder="README.md" autofocus>
510
+ </div>
511
+ <div class="new-file-actions">
512
+ <button id="cancel-new-file" class="btn btn-secondary">Cancel</button>
513
+ <button id="create-new-file" class="btn btn-primary">Create file</button>
514
+ </div>
515
+ </div>
516
+ <div class="new-file-editor">
517
+ <div class="monaco-editor-container">
518
+ <div id="new-file-content" class="monaco-editor"></div>
519
+ </div>
520
+ </div>
521
+ </div>
522
+ </main>
523
+ </body>
524
+ </html>
525
+ `;
526
+ }
527
+
528
+ function generateBreadcrumbs(currentPath, gitBranch = null) {
529
+ // At root, show gh-here branding with git branch if available
530
+ if (!currentPath || currentPath === '.') {
531
+ const gitBranchDisplay = gitBranch ? `<span class="git-branch">${octicons['git-branch'].toSVG({ class: 'octicon-branch' })} ${gitBranch}</span>` : '';
532
+ return `${octicons.home.toSVG({ class: 'octicon-home' })} gh-here ${gitBranchDisplay}`;
533
+ }
534
+
535
+ // In subdirectories, show clickable path
536
+ const parts = currentPath.split('/').filter(p => p && p !== '.');
537
+ let breadcrumbs = `
538
+ <div class="breadcrumb-item">
539
+ <a href="/">${octicons.home.toSVG({ class: 'octicon-home' })}</a>
540
+ </div>
541
+ `;
542
+ let buildPath = '';
543
+
544
+ parts.forEach((part, index) => {
545
+ buildPath += (buildPath ? '/' : '') + part;
546
+ breadcrumbs += `
547
+ <span class="breadcrumb-separator">/</span>
548
+ <div class="breadcrumb-item">
549
+ <a href="/?path=${encodeURIComponent(buildPath)}">
550
+ <span>${part}</span>
551
+ </a>
552
+ </div>
553
+ `;
554
+ });
555
+
556
+ return breadcrumbs;
557
+ }
558
+
559
+ module.exports = {
560
+ renderDirectory,
561
+ renderFileDiff,
562
+ renderFile,
563
+ renderNewFile,
564
+ generateBreadcrumbs,
565
+ findReadmeFile,
566
+ generateReadmePreview,
567
+ generateLanguageStats,
568
+ renderRawDiff
569
+ };