gh-here 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.env +0 -0
  2. package/.playwright-mcp/fixed-alignment.png +0 -0
  3. package/.playwright-mcp/fixed-layout.png +0 -0
  4. package/.playwright-mcp/gh-here-home-header-table.png +0 -0
  5. package/.playwright-mcp/gh-here-home.png +0 -0
  6. package/.playwright-mcp/line-selection-multiline.png +0 -0
  7. package/.playwright-mcp/line-selection-test-after.png +0 -0
  8. package/.playwright-mcp/line-selection-test-before.png +0 -0
  9. package/.playwright-mcp/page-2026-01-03T17-58-21-336Z.png +0 -0
  10. package/lib/constants.js +25 -15
  11. package/lib/content-search.js +212 -0
  12. package/lib/error-handler.js +39 -28
  13. package/lib/file-utils.js +438 -287
  14. package/lib/git.js +10 -54
  15. package/lib/gitignore.js +70 -41
  16. package/lib/renderers.js +15 -19
  17. package/lib/server.js +70 -193
  18. package/lib/symbol-parser.js +600 -0
  19. package/package.json +1 -1
  20. package/public/app.js +207 -73
  21. package/public/js/constants.js +50 -34
  22. package/public/js/content-search-handler.js +551 -0
  23. package/public/js/file-viewer.js +437 -0
  24. package/public/js/focus-mode.js +280 -0
  25. package/public/js/inline-search.js +659 -0
  26. package/public/js/modal-manager.js +14 -28
  27. package/public/js/navigation.js +5 -0
  28. package/public/js/symbol-outline.js +454 -0
  29. package/public/js/utils.js +152 -94
  30. package/public/styles.css +2049 -296
  31. package/.claude/settings.local.json +0 -30
  32. package/SAMPLE.md +0 -287
  33. package/lib/validation.js +0 -77
  34. package/public/app.js.backup +0 -1902
  35. package/public/js/draft-manager.js +0 -36
  36. package/public/js/editor-manager.js +0 -159
  37. package/test.js +0 -138
  38. package/tests/draftManager.test.js +0 -241
  39. package/tests/fileTypeDetection.test.js +0 -111
  40. package/tests/httpService.test.js +0 -268
  41. package/tests/languageDetection.test.js +0 -145
  42. package/tests/pathUtils.test.js +0 -136
package/lib/file-utils.js CHANGED
@@ -1,332 +1,483 @@
1
+ /**
2
+ * File utilities module
3
+ * Handles file icons, language detection, formatting, and type classification
4
+ *
5
+ * @module file-utils
6
+ */
7
+
1
8
  const path = require('path');
2
9
  const octicons = require('@primer/octicons');
3
10
 
11
+ // ============================================================================
12
+ // Constants
13
+ // ============================================================================
14
+
15
+ const BYTE_UNITS = ['B', 'KB', 'MB', 'GB'];
16
+ const BYTE_BASE = 1024;
17
+
18
+ // File type categories
19
+ const IMAGE_EXTENSIONS = new Set([
20
+ 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'tiff', 'ico'
21
+ ]);
22
+
23
+ const BINARY_EXTENSIONS = new Set([
24
+ // Archives
25
+ 'zip', 'tar', 'gz', 'rar', '7z',
26
+ // Executables
27
+ 'exe', 'bin', 'app', 'deb', 'rpm',
28
+ // Documents
29
+ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
30
+ // Media
31
+ 'mp4', 'mov', 'avi', 'mkv', 'mp3', 'wav', 'flac',
32
+ // Compiled
33
+ 'class', 'so', 'dll', 'dylib',
34
+ // Images (also binary)
35
+ ...IMAGE_EXTENSIONS
36
+ ]);
37
+
38
+ // Language mapping for Monaco/editor support
39
+ const LANGUAGE_MAP = {
40
+ // JavaScript family
41
+ 'js': 'javascript',
42
+ 'mjs': 'javascript',
43
+ 'jsx': 'javascript',
44
+ 'ts': 'typescript',
45
+ 'tsx': 'typescript',
46
+
47
+ // Web
48
+ 'html': 'html',
49
+ 'htm': 'html',
50
+ 'css': 'css',
51
+ 'scss': 'scss',
52
+ 'sass': 'sass',
53
+ 'less': 'less',
54
+
55
+ // Data / config
56
+ 'json': 'json',
57
+ 'xml': 'xml',
58
+ 'yaml': 'yaml',
59
+ 'yml': 'yaml',
60
+
61
+ // Shell & scripts
62
+ 'sh': 'bash',
63
+ 'bash': 'bash',
64
+ 'zsh': 'bash',
65
+ 'fish': 'bash',
66
+ 'ps1': 'powershell',
67
+
68
+ // Compiled / systems
69
+ 'c': 'c',
70
+ 'h': 'c',
71
+ 'cpp': 'cpp',
72
+ 'cc': 'cpp',
73
+ 'cxx': 'cpp',
74
+ 'hpp': 'cpp',
75
+ 'rs': 'rust',
76
+ 'go': 'go',
77
+ 'java': 'java',
78
+ 'kt': 'kotlin',
79
+ 'swift': 'swift',
80
+
81
+ // Scripting
82
+ 'py': 'python',
83
+ 'php': 'php',
84
+ 'rb': 'ruby',
85
+ 'dart': 'dart',
86
+ 'r': 'r',
87
+ 'sql': 'sql',
88
+ 'scala': 'scala',
89
+ 'clj': 'clojure',
90
+ 'lua': 'lua',
91
+ 'pl': 'perl',
92
+ 'groovy': 'groovy',
93
+
94
+ // Markup / frameworks
95
+ 'md': 'markdown',
96
+ 'markdown': 'markdown',
97
+ 'vue': 'vue',
98
+ 'svelte': 'svelte',
99
+
100
+ // Misc text
101
+ 'txt': 'plaintext',
102
+ 'log': 'plaintext',
103
+
104
+ // Special filename-style extensions
105
+ 'dockerfile': 'dockerfile'
106
+ };
107
+
108
+ // Language colors for stats
109
+ const LANGUAGE_COLORS = {
110
+ javascript: '#f1e05a',
111
+ typescript: '#2b7489',
112
+ python: '#3572A5',
113
+ java: '#b07219',
114
+ html: '#e34c26',
115
+ css: '#563d7c',
116
+ json: '#292929',
117
+ markdown: '#083fa1',
118
+ go: '#00ADD8',
119
+ rust: '#dea584',
120
+ php: '#4F5D95',
121
+ ruby: '#701516',
122
+ other: '#cccccc'
123
+ };
124
+
125
+ // ============================================================================
126
+ // File Icon Configuration
127
+ // ============================================================================
128
+
4
129
  /**
5
- * File utilities module
6
- * Handles file icons, language detection, and formatting
130
+ * Icon configuration for special files by exact name match
131
+ */
132
+ const SPECIAL_FILE_ICONS = {
133
+ // Package managers
134
+ 'package.json': { icon: 'file-code', color: 'text-green' },
135
+ 'composer.json': { icon: 'file-code', color: 'text-green' },
136
+
137
+ // TypeScript config
138
+ 'tsconfig.json': { icon: 'file-code', color: 'text-blue' },
139
+ 'jsconfig.json': { icon: 'file-code', color: 'text-blue' },
140
+
141
+ // Linters & formatters
142
+ '.eslintrc': { icon: 'gear', color: 'text-purple' },
143
+ '.eslintrc.json': { icon: 'gear', color: 'text-purple' },
144
+ '.eslintrc.js': { icon: 'gear', color: 'text-purple' },
145
+ '.eslintrc.yml': { icon: 'gear', color: 'text-purple' },
146
+ '.prettierrc': { icon: 'gear', color: 'text-blue' },
147
+ 'prettier.config.js': { icon: 'gear', color: 'text-blue' },
148
+ '.prettierrc.json': { icon: 'gear', color: 'text-blue' },
149
+
150
+ // Build tools
151
+ 'webpack.config.js': { icon: 'gear', color: 'text-orange' },
152
+ 'vite.config.js': { icon: 'gear', color: 'text-orange' },
153
+ 'rollup.config.js': { icon: 'gear', color: 'text-orange' },
154
+ 'next.config.js': { icon: 'gear', color: 'text-orange' },
155
+ 'nuxt.config.js': { icon: 'gear', color: 'text-orange' },
156
+ 'svelte.config.js': { icon: 'gear', color: 'text-orange' },
157
+ 'tailwind.config.js': { icon: 'gear', color: 'text-purple' },
158
+ 'postcss.config.js': { icon: 'gear', color: 'text-purple' },
159
+ 'babel.config.js': { icon: 'gear', color: 'text-purple' },
160
+ '.babelrc': { icon: 'gear', color: 'text-purple' },
161
+
162
+ // Docker
163
+ 'dockerfile': { icon: 'container', color: 'text-blue' },
164
+ 'dockerfile.dev': { icon: 'container', color: 'text-blue' },
165
+ '.dockerignore': { icon: 'container', color: 'text-blue' },
166
+ 'docker-compose.yml': { icon: 'container', color: 'text-blue' },
167
+ 'docker-compose.yaml': { icon: 'container', color: 'text-blue' },
168
+
169
+ // Git
170
+ '.gitignore': { icon: 'git-branch', color: 'text-orange' },
171
+ '.gitattributes': { icon: 'git-branch', color: 'text-orange' },
172
+ '.gitmodules': { icon: 'git-branch', color: 'text-orange' },
173
+
174
+ // Documentation
175
+ 'readme.md': { icon: 'book', color: 'text-blue' },
176
+ 'readme.txt': { icon: 'book', color: 'text-blue' },
177
+ 'changelog.md': { icon: 'book', color: 'text-blue' },
178
+ 'history.md': { icon: 'book', color: 'text-blue' },
179
+ 'license': { icon: 'law', color: 'text-yellow' },
180
+ 'license.txt': { icon: 'law', color: 'text-yellow' },
181
+ 'license.md': { icon: 'law', color: 'text-yellow' },
182
+
183
+ // Build
184
+ 'makefile': { icon: 'tools', color: 'text-gray' },
185
+ 'makefile.am': { icon: 'tools', color: 'text-gray' },
186
+ 'cmakelists.txt': { icon: 'tools', color: 'text-gray' },
187
+
188
+ // Locks
189
+ 'yarn.lock': { icon: 'lock', color: 'text-yellow' },
190
+ 'package-lock.json': { icon: 'lock', color: 'text-yellow' },
191
+ 'pipfile.lock': { icon: 'lock', color: 'text-yellow' },
192
+
193
+ // CI/CD
194
+ '.travis.yml': { icon: 'gear', color: 'text-green' },
195
+ '.circleci': { icon: 'gear', color: 'text-green' },
196
+
197
+ // Environment
198
+ '.env': { icon: 'key', color: 'text-yellow' },
199
+ '.env.local': { icon: 'key', color: 'text-yellow' }
200
+ };
201
+
202
+ /**
203
+ * Icon configuration for files by extension
204
+ */
205
+ const EXTENSION_ICONS = {
206
+ // JavaScript family
207
+ '.js': { icon: 'file-code', color: 'text-yellow' },
208
+ '.mjs': { icon: 'file-code', color: 'text-yellow' },
209
+ '.jsx': { icon: 'file-code', color: 'text-blue' },
210
+ '.ts': { icon: 'file-code', color: 'text-blue' },
211
+ '.tsx': { icon: 'file-code', color: 'text-blue' },
212
+
213
+ // Frameworks
214
+ '.vue': { icon: 'file-code', color: 'text-green' },
215
+ '.svelte': { icon: 'file-code', color: 'text-orange' },
216
+
217
+ // Languages
218
+ '.py': { icon: 'file-code', color: 'text-blue' },
219
+ '.pyx': { icon: 'file-code', color: 'text-blue' },
220
+ '.pyi': { icon: 'file-code', color: 'text-blue' },
221
+ '.java': { icon: 'file-code', color: 'text-red' },
222
+ '.class': { icon: 'file-code', color: 'text-red' },
223
+ '.c': { icon: 'file-code', color: 'text-blue' },
224
+ '.h': { icon: 'file-code', color: 'text-blue' },
225
+ '.cpp': { icon: 'file-code', color: 'text-blue' },
226
+ '.cxx': { icon: 'file-code', color: 'text-blue' },
227
+ '.cc': { icon: 'file-code', color: 'text-blue' },
228
+ '.hpp': { icon: 'file-code', color: 'text-blue' },
229
+ '.cs': { icon: 'file-code', color: 'text-purple' },
230
+ '.go': { icon: 'file-code', color: 'text-blue' },
231
+ '.rs': { icon: 'file-code', color: 'text-orange' },
232
+ '.php': { icon: 'file-code', color: 'text-purple' },
233
+ '.rb': { icon: 'file-code', color: 'text-red' },
234
+ '.swift': { icon: 'file-code', color: 'text-orange' },
235
+ '.kt': { icon: 'file-code', color: 'text-purple' },
236
+ '.kts': { icon: 'file-code', color: 'text-purple' },
237
+ '.dart': { icon: 'file-code', color: 'text-blue' },
238
+ '.scala': { icon: 'file-code', color: 'text-red' },
239
+ '.clj': { icon: 'file-code', color: 'text-green' },
240
+ '.cljs': { icon: 'file-code', color: 'text-green' },
241
+ '.hs': { icon: 'file-code', color: 'text-purple' },
242
+ '.elm': { icon: 'file-code', color: 'text-blue' },
243
+ '.r': { icon: 'file-code', color: 'text-blue' },
244
+
245
+ // Web
246
+ '.html': { icon: 'file-code', color: 'text-orange' },
247
+ '.css': { icon: 'paintbrush', color: 'text-purple' },
248
+ '.scss': { icon: 'paintbrush', color: 'text-purple' },
249
+ '.sass': { icon: 'paintbrush', color: 'text-purple' },
250
+ '.less': { icon: 'paintbrush', color: 'text-purple' },
251
+
252
+ // Data
253
+ '.json': { icon: 'file-code', color: 'text-yellow' },
254
+ '.xml': { icon: 'file-code', color: 'text-orange' },
255
+ '.yml': { icon: 'file-code', color: 'text-purple' },
256
+ '.yaml': { icon: 'file-code', color: 'text-purple' },
257
+
258
+ // Documentation
259
+ '.md': { icon: 'book', color: 'text-blue' },
260
+ '.markdown': { icon: 'book', color: 'text-blue' },
261
+ '.txt': { icon: 'file', color: 'text-gray' },
262
+
263
+ // Media
264
+ '.png': { icon: 'file-media', color: 'text-purple' },
265
+ '.jpg': { icon: 'file-media', color: 'text-purple' },
266
+ '.jpeg': { icon: 'file-media', color: 'text-purple' },
267
+ '.gif': { icon: 'file-media', color: 'text-purple' },
268
+ '.svg': { icon: 'file-media', color: 'text-purple' },
269
+ '.webp': { icon: 'file-media', color: 'text-purple' },
270
+ '.mp4': { icon: 'device-camera-video', color: 'text-red' },
271
+ '.mov': { icon: 'device-camera-video', color: 'text-red' },
272
+ '.avi': { icon: 'device-camera-video', color: 'text-red' },
273
+ '.mkv': { icon: 'device-camera-video', color: 'text-red' },
274
+ '.mp3': { icon: 'unmute', color: 'text-purple' },
275
+ '.wav': { icon: 'unmute', color: 'text-purple' },
276
+ '.flac': { icon: 'unmute', color: 'text-purple' },
277
+
278
+ // Archives
279
+ '.zip': { icon: 'file-zip', color: 'text-yellow' },
280
+ '.tar': { icon: 'file-zip', color: 'text-yellow' },
281
+ '.gz': { icon: 'file-zip', color: 'text-yellow' },
282
+ '.rar': { icon: 'file-zip', color: 'text-yellow' },
283
+ '.7z': { icon: 'file-zip', color: 'text-yellow' },
284
+
285
+ // Shell
286
+ '.sh': { icon: 'terminal', color: 'text-green' },
287
+ '.bash': { icon: 'terminal', color: 'text-green' },
288
+ '.zsh': { icon: 'terminal', color: 'text-green' },
289
+ '.fish': { icon: 'terminal', color: 'text-green' },
290
+
291
+ // Other
292
+ '.sql': { icon: 'file-code', color: 'text-orange' },
293
+ '.pdf': { icon: 'file-binary', color: 'text-red' }
294
+ };
295
+
296
+ // ============================================================================
297
+ // Utilities
298
+ // ============================================================================
299
+
300
+ /**
301
+ * Extract extension from file path or extension string
302
+ * @param {string} filePathOrExt - File path or extension
303
+ * @returns {string} Normalized extension (lowercase, no dot)
304
+ */
305
+ function getExtension(filePathOrExt) {
306
+ // If it's already just an extension (no dots, slashes), return as-is
307
+ if (!filePathOrExt.includes('.') &&
308
+ !filePathOrExt.includes('/') &&
309
+ !filePathOrExt.includes('\\')) {
310
+ return filePathOrExt.toLowerCase();
311
+ }
312
+
313
+ // Extract extension from file path
314
+ return path.extname(filePathOrExt).toLowerCase().slice(1);
315
+ }
316
+
317
+ /**
318
+ * Get icon configuration for a special file by name
319
+ * @param {string} filename - Lowercase filename
320
+ * @returns {Object|null} Icon config or null
321
+ */
322
+ function getSpecialFileIcon(filename) {
323
+ // Check exact matches
324
+ if (SPECIAL_FILE_ICONS[filename]) {
325
+ return SPECIAL_FILE_ICONS[filename];
326
+ }
327
+
328
+ // Check prefix matches (e.g., .env.*, .github/*, README*)
329
+ if (filename.startsWith('.env.')) {
330
+ return { icon: 'key', color: 'text-yellow' };
331
+ }
332
+
333
+ if (filename.startsWith('.github')) {
334
+ return { icon: 'gear', color: 'text-green' };
335
+ }
336
+
337
+ if (filename.startsWith('readme')) {
338
+ return { icon: 'book', color: 'text-blue' };
339
+ }
340
+
341
+ // Check suffix matches (e.g., *.lock)
342
+ if (filename.endsWith('.lock')) {
343
+ return { icon: 'lock', color: 'text-yellow' };
344
+ }
345
+
346
+ return null;
347
+ }
348
+
349
+ /**
350
+ * Get icon configuration for a file by extension
351
+ * @param {string} ext - File extension (with or without dot)
352
+ * @returns {Object|null} Icon config or null
353
+ */
354
+ function getExtensionIcon(ext) {
355
+ const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`;
356
+ return EXTENSION_ICONS[normalizedExt] || null;
357
+ }
358
+
359
+ /**
360
+ * Render icon SVG with fallback
361
+ * @param {string} iconName - Octicon name
362
+ * @param {string} colorClass - CSS color class
363
+ * @returns {string} SVG string
7
364
  */
365
+ function renderIcon(iconName, colorClass) {
366
+ const icon = octicons[iconName];
367
+ const fallback = octicons.file;
368
+
369
+ const iconToUse = icon || fallback;
370
+ const classes = `octicon-file ${colorClass}`;
371
+
372
+ // Render SVG with proper classes
373
+ return iconToUse.toSVG({ class: classes });
374
+ }
375
+
376
+ // ============================================================================
377
+ // Public API
378
+ // ============================================================================
8
379
 
380
+ /**
381
+ * Get file icon SVG for a given filename
382
+ * @param {string} filename - File name
383
+ * @returns {string} SVG icon string
384
+ */
9
385
  function getFileIcon(filename) {
386
+ if (!filename) {
387
+ return renderIcon('file', 'text-gray');
388
+ }
389
+
390
+ const normalizedName = filename.toLowerCase();
10
391
  const ext = path.extname(filename).toLowerCase();
11
- const name = filename.toLowerCase();
12
392
 
13
393
  try {
14
- // Configuration files
15
- if (name === 'package.json' || name === 'composer.json') {
16
- return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
17
- }
18
- if (name === 'tsconfig.json' || name === 'jsconfig.json') {
19
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
20
- }
21
- if (name === '.eslintrc' || name === '.eslintrc.json' || name === '.eslintrc.js' || name === '.eslintrc.yml') {
22
- return octicons.gear?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
23
- }
24
- if (name === '.prettierrc' || name === 'prettier.config.js' || name === '.prettierrc.json') {
25
- return octicons.gear?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
26
- }
27
- if (name === 'webpack.config.js' || name === 'vite.config.js' || name === 'rollup.config.js' || name === 'next.config.js' || name === 'nuxt.config.js' || name === 'svelte.config.js') {
28
- return octicons.gear?.toSVG({ class: 'octicon-file text-orange' }) || octicons.file.toSVG({ class: 'octicon-file text-orange' });
29
- }
30
- if (name === 'tailwind.config.js' || name === 'postcss.config.js' || name === 'babel.config.js' || name === '.babelrc') {
31
- return octicons.gear?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
32
- }
33
-
34
- // Docker files
35
- if (name === 'dockerfile' || name === 'dockerfile.dev' || name === '.dockerignore') {
36
- return octicons.container?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
37
- }
38
- if (name === 'docker-compose.yml' || name === 'docker-compose.yaml') {
39
- return octicons.container?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
40
- }
41
-
42
- // Git files
43
- if (name === '.gitignore' || name === '.gitattributes' || name === '.gitmodules') {
44
- return octicons['git-branch']?.toSVG({ class: 'octicon-file text-orange' }) || octicons.file.toSVG({ class: 'octicon-file text-orange' });
45
- }
46
-
47
- // Documentation
48
- if (name.startsWith('readme') || name === 'changelog.md' || name === 'history.md') {
49
- return octicons.book.toSVG({ class: 'octicon-file text-blue' });
50
- }
51
- if (name === 'license' || name === 'license.txt' || name === 'license.md') {
52
- return octicons.law?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
53
- }
54
-
55
- // Build files
56
- if (name === 'makefile' || name === 'makefile.am' || name === 'cmakelists.txt') {
57
- return octicons.tools?.toSVG({ class: 'octicon-file text-gray' }) || octicons.file.toSVG({ class: 'octicon-file text-gray' });
58
- }
59
- if (name.endsWith('.lock') || name === 'yarn.lock' || name === 'package-lock.json' || name === 'pipfile.lock') {
60
- return octicons.lock?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
61
- }
62
-
63
- // CI/CD files
64
- if (name === '.travis.yml' || name === '.circleci' || name.startsWith('.github')) {
65
- return octicons.gear?.toSVG({ class: 'octicon-file text-green' }) || octicons.file.toSVG({ class: 'octicon-file text-green' });
394
+ // Check special files first (exact name matches, prefixes, suffixes)
395
+ const specialIcon = getSpecialFileIcon(normalizedName);
396
+ if (specialIcon) {
397
+ return renderIcon(specialIcon.icon, specialIcon.color);
66
398
  }
67
399
 
68
- // Environment files
69
- if (name === '.env' || name === '.env.local' || name.startsWith('.env.')) {
70
- return octicons.key?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
400
+ // Check extension-based icons
401
+ const extIcon = getExtensionIcon(ext);
402
+ if (extIcon) {
403
+ return renderIcon(extIcon.icon, extIcon.color);
71
404
  }
72
405
 
73
- // Extension-based icons
74
- switch (ext) {
75
- case '.js':
76
- case '.mjs':
77
- return octicons['file-code'].toSVG({ class: 'octicon-file text-yellow' });
78
- case '.jsx':
79
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
80
- case '.ts':
81
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
82
- case '.tsx':
83
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
84
- case '.vue':
85
- return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
86
- case '.svelte':
87
- return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
88
- case '.py':
89
- case '.pyx':
90
- case '.pyi':
91
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
92
- case '.java':
93
- case '.class':
94
- return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
95
- case '.c':
96
- case '.h':
97
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
98
- case '.cpp':
99
- case '.cxx':
100
- case '.cc':
101
- case '.hpp':
102
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
103
- case '.cs':
104
- return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
105
- case '.go':
106
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
107
- case '.rs':
108
- return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
109
- case '.php':
110
- return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
111
- case '.rb':
112
- return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
113
- case '.swift':
114
- return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
115
- case '.kt':
116
- case '.kts':
117
- return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
118
- case '.dart':
119
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
120
- case '.scala':
121
- return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
122
- case '.clj':
123
- case '.cljs':
124
- return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
125
- case '.hs':
126
- return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
127
- case '.elm':
128
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
129
- case '.r':
130
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
131
- case '.html':
132
- return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
133
- case '.css':
134
- case '.scss':
135
- case '.sass':
136
- case '.less':
137
- return octicons.paintbrush?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
138
- case '.json':
139
- return octicons['file-code'].toSVG({ class: 'octicon-file text-yellow' });
140
- case '.xml':
141
- return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
142
- case '.yml':
143
- case '.yaml':
144
- return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
145
- case '.md':
146
- case '.markdown':
147
- return octicons.book.toSVG({ class: 'octicon-file text-blue' });
148
- case '.txt':
149
- return octicons['file-text']?.toSVG({ class: 'octicon-file text-gray' }) || octicons.file.toSVG({ class: 'octicon-file text-gray' });
150
- case '.pdf':
151
- return octicons['file-binary']?.toSVG({ class: 'octicon-file text-red' }) || octicons.file.toSVG({ class: 'octicon-file text-red' });
152
- case '.png':
153
- case '.jpg':
154
- case '.jpeg':
155
- case '.gif':
156
- case '.svg':
157
- case '.webp':
158
- return octicons['file-media']?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
159
- case '.mp4':
160
- case '.mov':
161
- case '.avi':
162
- case '.mkv':
163
- return octicons['device-camera-video']?.toSVG({ class: 'octicon-file text-red' }) || octicons.file.toSVG({ class: 'octicon-file text-red' });
164
- case '.mp3':
165
- case '.wav':
166
- case '.flac':
167
- return octicons.unmute?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
168
- case '.zip':
169
- case '.tar':
170
- case '.gz':
171
- case '.rar':
172
- case '.7z':
173
- return octicons['file-zip']?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
174
- case '.sh':
175
- case '.bash':
176
- case '.zsh':
177
- case '.fish':
178
- return octicons.terminal?.toSVG({ class: 'octicon-file text-green' }) || octicons.file.toSVG({ class: 'octicon-file text-green' });
179
- case '.sql':
180
- return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
181
- default:
182
- return octicons.file.toSVG({ class: 'octicon-file text-gray' });
183
- }
406
+ // Default fallback
407
+ return renderIcon('file', 'text-gray');
184
408
  } catch (error) {
185
- return octicons.file.toSVG({ class: 'octicon-file text-gray' });
409
+ console.warn('Error generating file icon:', error);
410
+ return renderIcon('file', 'text-gray');
186
411
  }
187
412
  }
188
413
 
414
+ /**
415
+ * Get language identifier from file extension
416
+ * @param {string} ext - File extension (with or without dot)
417
+ * @returns {string|undefined} Language identifier or undefined
418
+ */
189
419
  function getLanguageFromExtension(ext) {
190
420
  if (!ext) return undefined;
191
- const normalized = String(ext).toLowerCase();
192
- const langMap = {
193
- // JavaScript family
194
- 'js': 'javascript',
195
- 'mjs': 'javascript',
196
- 'jsx': 'javascript',
197
- 'ts': 'typescript',
198
- 'tsx': 'typescript',
199
-
200
- // Web
201
- 'html': 'html',
202
- 'htm': 'html',
203
- 'css': 'css',
204
- 'scss': 'scss',
205
- 'sass': 'sass',
206
- 'less': 'less',
207
-
208
- // Data / config
209
- 'json': 'json',
210
- 'xml': 'xml',
211
- 'yaml': 'yaml',
212
- 'yml': 'yaml',
213
-
214
- // Shell & scripts
215
- 'sh': 'bash',
216
- 'bash': 'bash',
217
- 'zsh': 'bash',
218
- 'fish': 'bash',
219
- 'ps1': 'powershell',
220
-
221
- // Compiled / systems
222
- 'c': 'c',
223
- 'h': 'c',
224
- 'cpp': 'cpp',
225
- 'cc': 'cpp',
226
- 'cxx': 'cpp',
227
- 'hpp': 'cpp',
228
- 'rs': 'rust',
229
- 'go': 'go',
230
- 'java': 'java',
231
- 'kt': 'kotlin',
232
- 'swift': 'swift',
233
-
234
- // Scripting
235
- 'py': 'python',
236
- 'php': 'php',
237
- 'rb': 'ruby',
238
- 'dart': 'dart',
239
- 'r': 'r',
240
- 'sql': 'sql',
241
- 'scala': 'scala',
242
- 'clj': 'clojure',
243
- 'lua': 'lua',
244
- 'pl': 'perl',
245
- 'groovy': 'groovy',
246
-
247
- // Markup / frameworks
248
- 'md': 'markdown',
249
- 'markdown': 'markdown',
250
- 'vue': 'vue',
251
- 'svelte': 'svelte',
252
-
253
- // Misc text
254
- 'txt': 'plaintext',
255
- 'log': 'plaintext',
256
-
257
- // Special filename-style extensions
258
- 'dockerfile': 'dockerfile'
259
- };
260
- return langMap[normalized];
421
+
422
+ const normalized = getExtension(ext);
423
+ return LANGUAGE_MAP[normalized];
261
424
  }
262
425
 
426
+ /**
427
+ * Get color for a programming language (for stats display)
428
+ * @param {string} language - Language identifier
429
+ * @returns {string} Hex color code
430
+ */
263
431
  function getLanguageColor(language) {
264
- const colors = {
265
- javascript: '#f1e05a',
266
- typescript: '#2b7489',
267
- python: '#3572A5',
268
- java: '#b07219',
269
- html: '#e34c26',
270
- css: '#563d7c',
271
- json: '#292929',
272
- markdown: '#083fa1',
273
- go: '#00ADD8',
274
- rust: '#dea584',
275
- php: '#4F5D95',
276
- ruby: '#701516',
277
- other: '#cccccc'
278
- };
279
- return colors[language] || colors.other;
432
+ return LANGUAGE_COLORS[language] || LANGUAGE_COLORS.other;
280
433
  }
281
434
 
435
+ /**
436
+ * Format bytes to human-readable string
437
+ * @param {number} bytes - Number of bytes
438
+ * @returns {string} Formatted string (e.g., "1.5 MB")
439
+ */
282
440
  function formatBytes(bytes) {
283
441
  if (bytes === 0) return '0 B';
284
- const k = 1024;
285
- const sizes = ['B', 'KB', 'MB', 'GB'];
286
- const i = Math.floor(Math.log(bytes) / Math.log(k));
287
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
442
+
443
+ const i = Math.floor(Math.log(bytes) / Math.log(BYTE_BASE));
444
+ const size = parseFloat((bytes / Math.pow(BYTE_BASE, i)).toFixed(2));
445
+
446
+ return `${size} ${BYTE_UNITS[i]}`;
288
447
  }
289
448
 
290
449
  /**
291
- * File type detection functions
450
+ * Check if file is an image based on extension
451
+ * @param {string} filePathOrExt - File path or extension
452
+ * @returns {boolean} True if image file
292
453
  */
293
454
  function isImageFile(filePathOrExt) {
294
455
  const ext = getExtension(filePathOrExt);
295
- const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'tiff', 'ico'];
296
- return imageExtensions.includes(ext);
456
+ return IMAGE_EXTENSIONS.has(ext);
297
457
  }
298
458
 
459
+ /**
460
+ * Check if file is binary based on extension
461
+ * @param {string} filePathOrExt - File path or extension
462
+ * @returns {boolean} True if binary file
463
+ */
299
464
  function isBinaryFile(filePathOrExt) {
300
465
  const ext = getExtension(filePathOrExt);
301
- const binaryExtensions = [
302
- // Images (handled separately)
303
- 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'tiff', 'ico',
304
- // Archives
305
- 'zip', 'tar', 'gz', 'rar', '7z',
306
- // Executables
307
- 'exe', 'bin', 'app', 'deb', 'rpm',
308
- // Documents
309
- 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
310
- // Media
311
- 'mp4', 'mov', 'avi', 'mkv', 'mp3', 'wav', 'flac',
312
- // Other
313
- 'class', 'so', 'dll', 'dylib'
314
- ];
315
- return binaryExtensions.includes(ext);
466
+ return BINARY_EXTENSIONS.has(ext);
316
467
  }
317
468
 
469
+ /**
470
+ * Check if file is text-based (not binary)
471
+ * @param {string} filePathOrExt - File path or extension
472
+ * @returns {boolean} True if text file
473
+ */
318
474
  function isTextFile(filePathOrExt) {
319
475
  return !isBinaryFile(filePathOrExt);
320
476
  }
321
477
 
322
- function getExtension(filePathOrExt) {
323
- // If it's already just an extension (no dots), return as-is
324
- if (!filePathOrExt.includes('.') && !filePathOrExt.includes('/') && !filePathOrExt.includes('\\')) {
325
- return filePathOrExt.toLowerCase();
326
- }
327
- // Extract extension from file path
328
- return path.extname(filePathOrExt).toLowerCase().slice(1);
329
- }
478
+ // ============================================================================
479
+ // Exports
480
+ // ============================================================================
330
481
 
331
482
  module.exports = {
332
483
  getFileIcon,
@@ -336,4 +487,4 @@ module.exports = {
336
487
  isImageFile,
337
488
  isBinaryFile,
338
489
  isTextFile
339
- };
490
+ };