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.
- package/README.md +179 -0
- package/bin/vg-coder.js +11 -0
- package/package.json +64 -0
- package/src/detectors/project-detector.js +333 -0
- package/src/exporter/html-exporter.js +1026 -0
- package/src/ignore/ignore-manager.js +298 -0
- package/src/index.js +282 -0
- package/src/scanner/file-scanner.js +592 -0
- package/src/tokenizer/token-manager.js +389 -0
- package/src/utils/helpers.js +128 -0
- package/test-project/package.json +21 -0
- package/test-project/src/controllers/userController.js +129 -0
- package/test-project/src/index.js +46 -0
- package/test-project/src/middleware/auth.js +142 -0
- package/test-project/styles/main.css +287 -0
- package/vg-coder-cli-1.0.0.tgz +0 -0
|
@@ -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
|
+
'&': '&',
|
|
919
|
+
'<': '<',
|
|
920
|
+
'>': '>',
|
|
921
|
+
'"': '"',
|
|
922
|
+
"'": '''
|
|
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
|
+
'&': '&',
|
|
1017
|
+
'<': '<',
|
|
1018
|
+
'>': '>',
|
|
1019
|
+
'"': '"',
|
|
1020
|
+
"'": '''
|
|
1021
|
+
};
|
|
1022
|
+
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
module.exports = HtmlExporter;
|