gh-here 1.0.2 → 1.0.5
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 +16 -10
- package/README.md +6 -3
- package/bin/gh-here.js +49 -1258
- package/lib/file-utils.js +264 -0
- package/lib/git.js +207 -0
- package/lib/gitignore.js +91 -0
- package/lib/renderers.js +569 -0
- package/lib/server.js +391 -0
- package/package.json +1 -1
- package/public/app.js +692 -129
- package/public/styles.css +414 -44
- package/tests/draftManager.test.js +241 -0
- package/tests/httpService.test.js +268 -0
- package/tests/languageDetection.test.js +145 -0
- package/tests/pathUtils.test.js +136 -0
package/lib/renderers.js
ADDED
|
@@ -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, '&')
|
|
309
|
+
.replace(/</g, '<')
|
|
310
|
+
.replace(/>/g, '>');
|
|
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
|
+
};
|