vg-coder-cli 1.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.
@@ -0,0 +1,1026 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const hljs = require('highlight.js');
4
+
5
+ /**
6
+ * HTML Exporter với syntax highlighting và copy functionality
7
+ */
8
+ class HtmlExporter {
9
+ constructor(outputPath, options = {}) {
10
+ this.outputPath = outputPath;
11
+ this.options = {
12
+ theme: options.theme || 'github',
13
+ includeLineNumbers: options.includeLineNumbers !== false,
14
+ includeStats: options.includeStats !== false,
15
+ includeSearch: options.includeSearch !== false,
16
+ title: options.title || 'VG Coder Analysis',
17
+ ...options
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Xuất HTML cho chunks
23
+ */
24
+ async exportChunks(chunks, metadata = {}) {
25
+ // Tạo thư mục output
26
+ await fs.ensureDir(this.outputPath);
27
+ await fs.ensureDir(path.join(this.outputPath, 'chunks'));
28
+
29
+ // Copy static assets
30
+ await this.copyStaticAssets();
31
+
32
+ // Tạo index.html
33
+ await this.createIndexPage(chunks, metadata);
34
+
35
+ // Tạo từng chunk file
36
+ for (let i = 0; i < chunks.length; i++) {
37
+ await this.createChunkPage(chunks[i], i, chunks.length, metadata);
38
+ }
39
+
40
+ // Tạo combined view
41
+ await this.createCombinedPage(chunks, metadata);
42
+
43
+ return {
44
+ indexPath: path.join(this.outputPath, 'index.html'),
45
+ chunksPath: path.join(this.outputPath, 'chunks'),
46
+ combinedPath: path.join(this.outputPath, 'combined.html'),
47
+ totalFiles: chunks.length
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Tạo trang index
53
+ */
54
+ async createIndexPage(chunks, metadata) {
55
+ const totalTokens = chunks.reduce((sum, chunk) => sum + chunk.tokens, 0);
56
+ const avgTokens = Math.round(totalTokens / chunks.length);
57
+
58
+ const html = `
59
+ <!DOCTYPE html>
60
+ <html lang="en">
61
+ <head>
62
+ <meta charset="UTF-8">
63
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
64
+ <title>${this.options.title}</title>
65
+ <link rel="stylesheet" href="assets/styles.css">
66
+ <link rel="stylesheet" href="assets/highlight.css">
67
+ </head>
68
+ <body>
69
+ <div class="container">
70
+ <header class="header">
71
+ <h1>${this.options.title}</h1>
72
+ <p class="subtitle">Generated on ${new Date().toLocaleString()}</p>
73
+ </header>
74
+
75
+ ${this.options.includeStats ? this.generateStatsSection(chunks, metadata, totalTokens, avgTokens) : ''}
76
+
77
+ ${metadata.directoryStructure ? this.generateDirectorySection(metadata.directoryStructure) : '<!-- No directory structure -->'}
78
+
79
+ <section class="chunks-section">
80
+ <h2>Content Chunks</h2>
81
+ <div class="chunks-grid">
82
+ ${chunks.map((chunk, index) => this.generateChunkCard(chunk, index)).join('')}
83
+ </div>
84
+ </section>
85
+
86
+ <section class="actions-section">
87
+ <div class="action-buttons">
88
+ <a href="combined.html" class="btn btn-primary">View All Combined</a>
89
+ <button onclick="downloadAllChunks()" class="btn btn-secondary">Download All</button>
90
+ </div>
91
+ </section>
92
+ </div>
93
+
94
+ <script src="assets/scripts.js"></script>
95
+ </body>
96
+ </html>`;
97
+
98
+ await fs.writeFile(path.join(this.outputPath, 'index.html'), html);
99
+ }
100
+
101
+ /**
102
+ * Tạo trang cho từng chunk
103
+ */
104
+ async createChunkPage(chunk, index, total, metadata) {
105
+ const fileName = `chunk-${index + 1}.html`;
106
+ const prevLink = index > 0 ? `chunk-${index}.html` : null;
107
+ const nextLink = index < total - 1 ? `chunk-${index + 2}.html` : null;
108
+
109
+ const highlightedContent = this.highlightContent(chunk.content);
110
+
111
+ const html = `
112
+ <!DOCTYPE html>
113
+ <html lang="en">
114
+ <head>
115
+ <meta charset="UTF-8">
116
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
117
+ <title>Chunk ${index + 1} - ${this.options.title}</title>
118
+ <link rel="stylesheet" href="../assets/styles.css">
119
+ <link rel="stylesheet" href="../assets/highlight.css">
120
+ </head>
121
+ <body>
122
+ <div class="container">
123
+ <header class="header">
124
+ <div class="nav-header">
125
+ <a href="../index.html" class="back-link">← Back to Index</a>
126
+ <h1>Chunk ${index + 1} of ${total}</h1>
127
+ <div class="chunk-nav">
128
+ ${prevLink ? `<a href="${prevLink}" class="btn btn-sm">← Previous</a>` : ''}
129
+ ${nextLink ? `<a href="${nextLink}" class="btn btn-sm">Next →</a>` : ''}
130
+ </div>
131
+ </div>
132
+ </header>
133
+
134
+ <section class="chunk-info">
135
+ <div class="info-grid">
136
+ <div class="info-item">
137
+ <span class="label">Tokens:</span>
138
+ <span class="value">${chunk.tokens.toLocaleString()}</span>
139
+ </div>
140
+ <div class="info-item">
141
+ <span class="label">Type:</span>
142
+ <span class="value">${chunk.metadata?.type || 'unknown'}</span>
143
+ </div>
144
+ ${chunk.metadata?.filePath ? `
145
+ <div class="info-item">
146
+ <span class="label">File:</span>
147
+ <span class="value">${chunk.metadata.filePath}</span>
148
+ </div>` : ''}
149
+ </div>
150
+ <button onclick="copyToClipboard('chunk-content')" class="btn btn-copy">
151
+ 📋 Copy Content
152
+ </button>
153
+ </section>
154
+
155
+ <section class="content-section">
156
+ <div class="code-container">
157
+ <pre id="chunk-content"><code>${highlightedContent}</code></pre>
158
+ </div>
159
+ </section>
160
+ </div>
161
+
162
+ <script src="../assets/scripts.js"></script>
163
+ </body>
164
+ </html>`;
165
+
166
+ await fs.writeFile(path.join(this.outputPath, 'chunks', fileName), html);
167
+ }
168
+
169
+ /**
170
+ * Tạo trang combined
171
+ */
172
+ async createCombinedPage(chunks, metadata) {
173
+ const combinedContent = chunks.map(chunk => chunk.content).join('\n\n');
174
+ const highlightedContent = this.highlightContent(combinedContent);
175
+ const totalTokens = chunks.reduce((sum, chunk) => sum + chunk.tokens, 0);
176
+
177
+ const html = `
178
+ <!DOCTYPE html>
179
+ <html lang="en">
180
+ <head>
181
+ <meta charset="UTF-8">
182
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
183
+ <title>Combined View - ${this.options.title}</title>
184
+ <link rel="stylesheet" href="assets/styles.css">
185
+ <link rel="stylesheet" href="assets/highlight.css">
186
+ </head>
187
+ <body>
188
+ <div class="container">
189
+ <header class="header">
190
+ <div class="nav-header">
191
+ <a href="index.html" class="back-link">← Back to Index</a>
192
+ <h1>Combined View</h1>
193
+ <button onclick="copyToClipboard('combined-content')" class="btn btn-copy">
194
+ 📋 Copy All Content
195
+ </button>
196
+ </div>
197
+ </header>
198
+
199
+ <section class="combined-info">
200
+ <div class="info-grid">
201
+ <div class="info-item">
202
+ <span class="label">Total Chunks:</span>
203
+ <span class="value">${chunks.length}</span>
204
+ </div>
205
+ <div class="info-item">
206
+ <span class="label">Total Tokens:</span>
207
+ <span class="value">${totalTokens.toLocaleString()}</span>
208
+ </div>
209
+ <div class="info-item">
210
+ <span class="label">Size:</span>
211
+ <span class="value">${this.formatBytes(combinedContent.length)}</span>
212
+ </div>
213
+ </div>
214
+ </section>
215
+
216
+ ${this.options.includeSearch ? this.generateSearchSection() : ''}
217
+
218
+ <section class="content-section">
219
+ <div class="code-container">
220
+ <pre id="combined-content"><code>${highlightedContent}</code></pre>
221
+ </div>
222
+ </section>
223
+ </div>
224
+
225
+ <script src="assets/scripts.js"></script>
226
+ </body>
227
+ </html>`;
228
+
229
+ await fs.writeFile(path.join(this.outputPath, 'combined.html'), html);
230
+ }
231
+
232
+ /**
233
+ * Copy static assets
234
+ */
235
+ async copyStaticAssets() {
236
+ const assetsPath = path.join(this.outputPath, 'assets');
237
+ await fs.ensureDir(assetsPath);
238
+
239
+ // CSS
240
+ await this.createStylesCSS(assetsPath);
241
+ await this.createHighlightCSS(assetsPath);
242
+
243
+ // JavaScript
244
+ await this.createScriptsJS(assetsPath);
245
+ }
246
+
247
+ /**
248
+ * Tạo styles.css
249
+ */
250
+ async createStylesCSS(assetsPath) {
251
+ const css = `
252
+ /* VG Coder Styles */
253
+ * {
254
+ margin: 0;
255
+ padding: 0;
256
+ box-sizing: border-box;
257
+ }
258
+
259
+ body {
260
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
261
+ line-height: 1.6;
262
+ color: #333;
263
+ background-color: #f8f9fa;
264
+ }
265
+
266
+ .container {
267
+ max-width: 1200px;
268
+ margin: 0 auto;
269
+ padding: 20px;
270
+ }
271
+
272
+ .header {
273
+ background: white;
274
+ padding: 30px;
275
+ border-radius: 10px;
276
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
277
+ margin-bottom: 30px;
278
+ }
279
+
280
+ .header h1 {
281
+ color: #2c3e50;
282
+ margin-bottom: 10px;
283
+ }
284
+
285
+ .subtitle {
286
+ color: #7f8c8d;
287
+ font-size: 14px;
288
+ }
289
+
290
+ .nav-header {
291
+ display: flex;
292
+ justify-content: space-between;
293
+ align-items: center;
294
+ flex-wrap: wrap;
295
+ gap: 15px;
296
+ }
297
+
298
+ .back-link {
299
+ color: #3498db;
300
+ text-decoration: none;
301
+ font-weight: 500;
302
+ }
303
+
304
+ .back-link:hover {
305
+ text-decoration: underline;
306
+ }
307
+
308
+ .chunk-nav {
309
+ display: flex;
310
+ gap: 10px;
311
+ }
312
+
313
+ .stats-section {
314
+ background: white;
315
+ padding: 25px;
316
+ border-radius: 10px;
317
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
318
+ margin-bottom: 30px;
319
+ }
320
+
321
+ .stats-grid {
322
+ display: grid;
323
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
324
+ gap: 20px;
325
+ margin-top: 20px;
326
+ }
327
+
328
+ .stat-item {
329
+ text-align: center;
330
+ padding: 15px;
331
+ background: #f8f9fa;
332
+ border-radius: 8px;
333
+ }
334
+
335
+ .stat-value {
336
+ font-size: 24px;
337
+ font-weight: bold;
338
+ color: #2c3e50;
339
+ display: block;
340
+ }
341
+
342
+ .stat-label {
343
+ font-size: 14px;
344
+ color: #7f8c8d;
345
+ margin-top: 5px;
346
+ }
347
+
348
+ .chunks-section {
349
+ background: white;
350
+ padding: 25px;
351
+ border-radius: 10px;
352
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
353
+ margin-bottom: 30px;
354
+ }
355
+
356
+ .chunks-grid {
357
+ display: grid;
358
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
359
+ gap: 20px;
360
+ margin-top: 20px;
361
+ }
362
+
363
+ .chunk-card {
364
+ border: 1px solid #e1e8ed;
365
+ border-radius: 8px;
366
+ padding: 20px;
367
+ background: #f8f9fa;
368
+ transition: transform 0.2s, box-shadow 0.2s;
369
+ }
370
+
371
+ .chunk-card:hover {
372
+ transform: translateY(-2px);
373
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
374
+ }
375
+
376
+ .chunk-title {
377
+ font-weight: bold;
378
+ margin-bottom: 10px;
379
+ color: #2c3e50;
380
+ }
381
+
382
+ .chunk-meta {
383
+ font-size: 14px;
384
+ color: #7f8c8d;
385
+ margin-bottom: 15px;
386
+ }
387
+
388
+ .chunk-actions {
389
+ display: flex;
390
+ gap: 10px;
391
+ }
392
+
393
+ .btn {
394
+ padding: 8px 16px;
395
+ border: none;
396
+ border-radius: 5px;
397
+ text-decoration: none;
398
+ font-size: 14px;
399
+ font-weight: 500;
400
+ cursor: pointer;
401
+ transition: background-color 0.2s;
402
+ display: inline-block;
403
+ text-align: center;
404
+ }
405
+
406
+ .btn-primary {
407
+ background: #3498db;
408
+ color: white;
409
+ }
410
+
411
+ .btn-primary:hover {
412
+ background: #2980b9;
413
+ }
414
+
415
+ .btn-secondary {
416
+ background: #95a5a6;
417
+ color: white;
418
+ }
419
+
420
+ .btn-secondary:hover {
421
+ background: #7f8c8d;
422
+ }
423
+
424
+ .btn-sm {
425
+ padding: 6px 12px;
426
+ font-size: 12px;
427
+ }
428
+
429
+ .btn-copy {
430
+ background: #27ae60;
431
+ color: white;
432
+ }
433
+
434
+ .btn-copy:hover {
435
+ background: #229954;
436
+ }
437
+
438
+ .info-grid {
439
+ display: grid;
440
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
441
+ gap: 15px;
442
+ margin-bottom: 20px;
443
+ }
444
+
445
+ .info-item {
446
+ display: flex;
447
+ justify-content: space-between;
448
+ padding: 10px;
449
+ background: #f8f9fa;
450
+ border-radius: 5px;
451
+ }
452
+
453
+ .label {
454
+ font-weight: 500;
455
+ color: #7f8c8d;
456
+ }
457
+
458
+ .value {
459
+ font-weight: bold;
460
+ color: #2c3e50;
461
+ }
462
+
463
+ .content-section {
464
+ background: white;
465
+ border-radius: 10px;
466
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
467
+ overflow: hidden;
468
+ }
469
+
470
+ .directory-section {
471
+ background: white;
472
+ border-radius: 10px;
473
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
474
+ padding: 20px;
475
+ margin-bottom: 30px;
476
+ }
477
+
478
+ .directory-section h2 {
479
+ margin-bottom: 15px;
480
+ color: #2c3e50;
481
+ border-bottom: 2px solid #3498db;
482
+ padding-bottom: 10px;
483
+ }
484
+
485
+ .directory-tree {
486
+ position: relative;
487
+ background: #f8f9fa;
488
+ border-radius: 8px;
489
+ border: 1px solid #e9ecef;
490
+ max-height: 500px;
491
+ overflow-y: auto;
492
+ }
493
+
494
+ .tree-content {
495
+ margin: 0;
496
+ padding: 20px;
497
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
498
+ font-size: 13px;
499
+ line-height: 1.4;
500
+ color: #2c3e50;
501
+ background: transparent;
502
+ white-space: pre;
503
+ overflow-x: auto;
504
+ }
505
+
506
+ .directory-tree .btn-copy {
507
+ position: absolute;
508
+ top: 10px;
509
+ right: 10px;
510
+ z-index: 10;
511
+ }
512
+
513
+ .code-container {
514
+ position: relative;
515
+ }
516
+
517
+ .code-container pre {
518
+ margin: 0;
519
+ padding: 20px;
520
+ overflow-x: auto;
521
+ background: #f8f9fa;
522
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
523
+ font-size: 14px;
524
+ line-height: 1.5;
525
+ }
526
+
527
+ .search-section {
528
+ background: white;
529
+ padding: 20px;
530
+ border-radius: 10px;
531
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
532
+ margin-bottom: 20px;
533
+ }
534
+
535
+ .search-input {
536
+ width: 100%;
537
+ padding: 10px;
538
+ border: 1px solid #ddd;
539
+ border-radius: 5px;
540
+ font-size: 16px;
541
+ }
542
+
543
+ .actions-section {
544
+ text-align: center;
545
+ margin-top: 30px;
546
+ }
547
+
548
+ .action-buttons {
549
+ display: flex;
550
+ justify-content: center;
551
+ gap: 15px;
552
+ flex-wrap: wrap;
553
+ }
554
+
555
+ .copy-success {
556
+ position: fixed;
557
+ top: 20px;
558
+ right: 20px;
559
+ background: #27ae60;
560
+ color: white;
561
+ padding: 10px 20px;
562
+ border-radius: 5px;
563
+ z-index: 1000;
564
+ animation: slideIn 0.3s ease;
565
+ }
566
+
567
+ @keyframes slideIn {
568
+ from { transform: translateX(100%); }
569
+ to { transform: translateX(0); }
570
+ }
571
+
572
+ @media (max-width: 768px) {
573
+ .container {
574
+ padding: 10px;
575
+ }
576
+
577
+ .nav-header {
578
+ flex-direction: column;
579
+ align-items: flex-start;
580
+ }
581
+
582
+ .chunks-grid {
583
+ grid-template-columns: 1fr;
584
+ }
585
+
586
+ .stats-grid {
587
+ grid-template-columns: repeat(2, 1fr);
588
+ }
589
+
590
+ .action-buttons {
591
+ flex-direction: column;
592
+ align-items: center;
593
+ }
594
+
595
+ .directory-tree {
596
+ max-height: 400px;
597
+ overflow-y: auto;
598
+ }
599
+ }`;
600
+
601
+ await fs.writeFile(path.join(assetsPath, 'styles.css'), css);
602
+ }
603
+
604
+ /**
605
+ * Tạo highlight.css
606
+ */
607
+ async createHighlightCSS(assetsPath) {
608
+ // Sử dụng GitHub theme
609
+ const css = `
610
+ /* GitHub Theme for highlight.js */
611
+ .hljs {
612
+ color: #333;
613
+ background: #f8f8f8;
614
+ }
615
+
616
+ .hljs-comment,
617
+ .hljs-quote {
618
+ color: #998;
619
+ font-style: italic;
620
+ }
621
+
622
+ .hljs-keyword,
623
+ .hljs-selector-tag,
624
+ .hljs-subst {
625
+ color: #333;
626
+ font-weight: bold;
627
+ }
628
+
629
+ .hljs-number,
630
+ .hljs-literal,
631
+ .hljs-variable,
632
+ .hljs-template-variable,
633
+ .hljs-tag .hljs-attr {
634
+ color: #008080;
635
+ }
636
+
637
+ .hljs-string,
638
+ .hljs-doctag {
639
+ color: #d14;
640
+ }
641
+
642
+ .hljs-title,
643
+ .hljs-section,
644
+ .hljs-selector-id {
645
+ color: #900;
646
+ font-weight: bold;
647
+ }
648
+
649
+ .hljs-subst {
650
+ font-weight: normal;
651
+ }
652
+
653
+ .hljs-type,
654
+ .hljs-class .hljs-title {
655
+ color: #458;
656
+ font-weight: bold;
657
+ }
658
+
659
+ .hljs-tag,
660
+ .hljs-name,
661
+ .hljs-attribute {
662
+ color: #000080;
663
+ font-weight: normal;
664
+ }
665
+
666
+ .hljs-regexp,
667
+ .hljs-link {
668
+ color: #009926;
669
+ }
670
+
671
+ .hljs-symbol,
672
+ .hljs-bullet {
673
+ color: #990073;
674
+ }
675
+
676
+ .hljs-built_in,
677
+ .hljs-builtin-name {
678
+ color: #0086b3;
679
+ }
680
+
681
+ .hljs-meta {
682
+ color: #999;
683
+ font-weight: bold;
684
+ }
685
+
686
+ .hljs-deletion {
687
+ background: #fdd;
688
+ }
689
+
690
+ .hljs-addition {
691
+ background: #dfd;
692
+ }
693
+
694
+ .hljs-emphasis {
695
+ font-style: italic;
696
+ }
697
+
698
+ .hljs-strong {
699
+ font-weight: bold;
700
+ }`;
701
+
702
+ await fs.writeFile(path.join(assetsPath, 'highlight.css'), css);
703
+ }
704
+
705
+ /**
706
+ * Tạo scripts.js
707
+ */
708
+ async createScriptsJS(assetsPath) {
709
+ const js = `// VG Coder Scripts
710
+
711
+ // Copy to clipboard functionality
712
+ async function copyToClipboard(elementId) {
713
+ try {
714
+ const element = document.getElementById(elementId);
715
+ const text = element.textContent || element.innerText;
716
+
717
+ if (navigator.clipboard && window.isSecureContext) {
718
+ await navigator.clipboard.writeText(text);
719
+ } else {
720
+ // Fallback for older browsers
721
+ const textArea = document.createElement('textarea');
722
+ textArea.value = text;
723
+ textArea.style.position = 'fixed';
724
+ textArea.style.left = '-999999px';
725
+ textArea.style.top = '-999999px';
726
+ document.body.appendChild(textArea);
727
+ textArea.focus();
728
+ textArea.select();
729
+ document.execCommand('copy');
730
+ textArea.remove();
731
+ }
732
+
733
+ showCopySuccess();
734
+ } catch (err) {
735
+ console.error('Failed to copy: ', err);
736
+ alert('Failed to copy content to clipboard');
737
+ }
738
+ }
739
+
740
+ // Show copy success message
741
+ function showCopySuccess() {
742
+ const message = document.createElement('div');
743
+ message.className = 'copy-success';
744
+ message.textContent = 'Content copied to clipboard!';
745
+ document.body.appendChild(message);
746
+
747
+ setTimeout(function() {
748
+ message.remove();
749
+ }, 3000);
750
+ }
751
+
752
+ // Copy chunk content by index
753
+ async function copyChunkContent(chunkIndex) {
754
+ try {
755
+ // Find the chunk content in the current page
756
+ const chunkElement = document.querySelector(\`[data-chunk-index="\${chunkIndex}"]\`);
757
+ let content = '';
758
+
759
+ if (chunkElement) {
760
+ // Get content from the current page
761
+ const codeElement = chunkElement.querySelector('pre code, pre');
762
+ content = codeElement ? (codeElement.textContent || codeElement.innerText) : '';
763
+ } else {
764
+ // Fallback: try to get content from chunk data if available
765
+ if (window.chunkData && window.chunkData[chunkIndex]) {
766
+ content = window.chunkData[chunkIndex].content;
767
+ } else {
768
+ // Last fallback: show error
769
+ alert(\`Chunk \${chunkIndex + 1} content not found\`);
770
+ return;
771
+ }
772
+ }
773
+
774
+ if (content.trim()) {
775
+ if (navigator.clipboard && window.isSecureContext) {
776
+ await navigator.clipboard.writeText(content);
777
+ } else {
778
+ // Fallback for older browsers
779
+ const textArea = document.createElement('textarea');
780
+ textArea.value = content;
781
+ document.body.appendChild(textArea);
782
+ textArea.select();
783
+ document.execCommand('copy');
784
+ document.body.removeChild(textArea);
785
+ }
786
+ showCopySuccess();
787
+ } else {
788
+ alert('No content found to copy');
789
+ }
790
+ } catch (error) {
791
+ console.error('Copy failed:', error);
792
+ alert('Failed to copy chunk content');
793
+ }
794
+ }
795
+
796
+ // Download all chunks
797
+ function downloadAllChunks() {
798
+ // This would need to be implemented based on specific requirements
799
+ alert('Download functionality would be implemented here');
800
+ }
801
+
802
+ // Copy directory structure
803
+ async function copyDirectoryStructure() {
804
+ try {
805
+ const treeElement = document.querySelector('.tree-content');
806
+ if (!treeElement) {
807
+ alert('Directory structure not found');
808
+ return;
809
+ }
810
+
811
+ const content = treeElement.textContent || treeElement.innerText;
812
+
813
+ if (navigator.clipboard && window.isSecureContext) {
814
+ await navigator.clipboard.writeText(content);
815
+ } else {
816
+ // Fallback for older browsers
817
+ const textArea = document.createElement('textarea');
818
+ textArea.value = content;
819
+ textArea.style.position = 'fixed';
820
+ textArea.style.left = '-999999px';
821
+ textArea.style.top = '-999999px';
822
+ document.body.appendChild(textArea);
823
+ textArea.focus();
824
+ textArea.select();
825
+ document.execCommand('copy');
826
+ document.body.removeChild(textArea);
827
+ }
828
+
829
+ // Show success feedback
830
+ const button = event.target;
831
+ const originalText = button.textContent;
832
+ button.textContent = '✅ Copied!';
833
+ button.style.background = '#27ae60';
834
+
835
+ setTimeout(() => {
836
+ button.textContent = originalText;
837
+ button.style.background = '';
838
+ }, 2000);
839
+
840
+ } catch (err) {
841
+ console.error('Failed to copy directory structure:', err);
842
+ alert('Failed to copy directory structure');
843
+ }
844
+ }
845
+
846
+ // Search functionality
847
+ function initializeSearch() {
848
+ const searchInput = document.getElementById('search-input');
849
+ const content = document.getElementById('combined-content') || document.getElementById('chunk-content');
850
+
851
+ if (!searchInput || !content) return;
852
+
853
+ let originalContent = content.innerHTML;
854
+
855
+ searchInput.addEventListener('input', function() {
856
+ const query = this.value.trim();
857
+
858
+ if (!query) {
859
+ content.innerHTML = originalContent;
860
+ return;
861
+ }
862
+
863
+ const regex = new RegExp('(' + escapeRegex(query) + ')', 'gi');
864
+ const highlighted = originalContent.replace(regex, '<mark>$1</mark>');
865
+ content.innerHTML = highlighted;
866
+ });
867
+ }
868
+
869
+ // Escape regex special characters
870
+ function escapeRegex(string) {
871
+ var specialChars = ['\\\\', '.', '*', '+', '?', '^', '$', '{', '}', '(', ')', '|', '[', ']'];
872
+ var result = string;
873
+ for (var i = 0; i < specialChars.length; i++) {
874
+ result = result.split(specialChars[i]).join('\\\\' + specialChars[i]);
875
+ }
876
+ return result;
877
+ }
878
+
879
+ // Initialize when DOM is loaded
880
+ document.addEventListener('DOMContentLoaded', function() {
881
+ initializeSearch();
882
+
883
+ // Add keyboard shortcuts
884
+ document.addEventListener('keydown', function(e) {
885
+ // Ctrl+C or Cmd+C to copy current content
886
+ if ((e.ctrlKey || e.metaKey) && e.key === 'c' && e.target.tagName !== 'INPUT') {
887
+ const content = document.getElementById('combined-content') || document.getElementById('chunk-content');
888
+ if (content) {
889
+ copyToClipboard(content.id);
890
+ e.preventDefault();
891
+ }
892
+ }
893
+ });
894
+ });`;
895
+
896
+ await fs.writeFile(path.join(assetsPath, 'scripts.js'), js);
897
+ }
898
+
899
+ /**
900
+ * Highlight content
901
+ */
902
+ highlightContent(content) {
903
+ try {
904
+ // Auto-detect language or use plain text
905
+ const result = hljs.highlightAuto(content);
906
+ return result.value;
907
+ } catch (error) {
908
+ // Fallback to plain text with HTML escaping
909
+ return this.escapeHtml(content);
910
+ }
911
+ }
912
+
913
+ /**
914
+ * Escape HTML
915
+ */
916
+ escapeHtml(text) {
917
+ const map = {
918
+ '&': '&amp;',
919
+ '<': '&lt;',
920
+ '>': '&gt;',
921
+ '"': '&quot;',
922
+ "'": '&#039;'
923
+ };
924
+ return text.replace(/[&<>"']/g, m => map[m]);
925
+ }
926
+
927
+ /**
928
+ * Generate directory structure section
929
+ */
930
+ generateDirectorySection(directoryStructure) {
931
+ return `
932
+ <section class="directory-section">
933
+ <h2>📁 Project Structure</h2>
934
+ <div class="directory-tree">
935
+ <pre class="tree-content">${this.escapeHtml(directoryStructure)}</pre>
936
+ <button onclick="copyDirectoryStructure()" class="btn btn-copy">📋 Copy Structure</button>
937
+ </div>
938
+ </section>`;
939
+ }
940
+
941
+ /**
942
+ * Generate stats section
943
+ */
944
+ generateStatsSection(chunks, metadata, totalTokens, avgTokens) {
945
+ return `
946
+ <section class="stats-section">
947
+ <h2>Statistics</h2>
948
+ <div class="stats-grid">
949
+ <div class="stat-item">
950
+ <span class="stat-value">${chunks.length}</span>
951
+ <span class="stat-label">Total Chunks</span>
952
+ </div>
953
+ <div class="stat-item">
954
+ <span class="stat-value">${totalTokens.toLocaleString()}</span>
955
+ <span class="stat-label">Total Tokens</span>
956
+ </div>
957
+ <div class="stat-item">
958
+ <span class="stat-value">${avgTokens.toLocaleString()}</span>
959
+ <span class="stat-label">Avg Tokens/Chunk</span>
960
+ </div>
961
+ <div class="stat-item">
962
+ <span class="stat-value">${metadata.projectType || 'Unknown'}</span>
963
+ <span class="stat-label">Project Type</span>
964
+ </div>
965
+ </div>
966
+ </section>`;
967
+ }
968
+
969
+ /**
970
+ * Generate search section
971
+ */
972
+ generateSearchSection() {
973
+ return `
974
+ <section class="search-section">
975
+ <input type="text" id="search-input" class="search-input" placeholder="Search in content...">
976
+ </section>`;
977
+ }
978
+
979
+ /**
980
+ * Generate chunk card
981
+ */
982
+ generateChunkCard(chunk, index) {
983
+ return `
984
+ <div class="chunk-card" data-chunk-index="${index}">
985
+ <div class="chunk-title">Chunk ${index + 1}</div>
986
+ <div class="chunk-meta">
987
+ ${chunk.tokens.toLocaleString()} tokens • ${chunk.metadata?.type || 'unknown'}
988
+ ${chunk.metadata?.filePath ? `<br>File: ${chunk.metadata.filePath}` : ''}
989
+ </div>
990
+ <div class="chunk-actions">
991
+ <a href="chunks/chunk-${index + 1}.html" class="btn btn-primary">View</a>
992
+ <button onclick="copyChunkContent(${index})" class="btn btn-copy">Copy</button>
993
+ </div>
994
+ <div class="chunk-content" style="display: none;">
995
+ <pre><code>${this.escapeHtml(chunk.content)}</code></pre>
996
+ </div>
997
+ </div>`;
998
+ }
999
+
1000
+ /**
1001
+ * Format bytes
1002
+ */
1003
+ formatBytes(bytes) {
1004
+ if (bytes === 0) return '0 Bytes';
1005
+ const k = 1024;
1006
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
1007
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1008
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1009
+ }
1010
+
1011
+ /**
1012
+ * Escape HTML characters
1013
+ */
1014
+ escapeHtml(text) {
1015
+ const map = {
1016
+ '&': '&amp;',
1017
+ '<': '&lt;',
1018
+ '>': '&gt;',
1019
+ '"': '&quot;',
1020
+ "'": '&#039;'
1021
+ };
1022
+ return text.replace(/[&<>"']/g, function(m) { return map[m]; });
1023
+ }
1024
+ }
1025
+
1026
+ module.exports = HtmlExporter;