codesummary 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/LICENSE +674 -0
- package/README.md +370 -0
- package/RELEASE.md +412 -0
- package/bin/codesummary.js +13 -0
- package/features.md +502 -0
- package/package.json +84 -0
- package/src/cli.js +392 -0
- package/src/configManager.js +427 -0
- package/src/errorHandler.js +343 -0
- package/src/index.js +26 -0
- package/src/pdfGenerator.js +427 -0
- package/src/scanner.js +330 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import PDFDocument from 'pdfkit';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ErrorHandler from './errorHandler.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* PDF Generator for CodeSummary
|
|
9
|
+
* Creates styled PDF documents with project code content
|
|
10
|
+
*/
|
|
11
|
+
export class PDFGenerator {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.doc = null;
|
|
15
|
+
this.pageHeight = 842; // A4 height (595 x 842 points)
|
|
16
|
+
this.pageWidth = 595; // A4 width
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Calculate available content width
|
|
21
|
+
* @returns {number} Available width for content
|
|
22
|
+
*/
|
|
23
|
+
getContentWidth() {
|
|
24
|
+
return this.pageWidth - (this.config.styles.layout.marginLeft + this.config.styles.layout.marginRight);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate PDF from scanned files
|
|
29
|
+
* @param {object} filesByExtension - Files grouped by extension
|
|
30
|
+
* @param {Array} selectedExtensions - Extensions selected by user
|
|
31
|
+
* @param {string} outputPath - Output file path
|
|
32
|
+
* @param {string} projectName - Name of the project
|
|
33
|
+
* @returns {Promise<object>} Object with outputPath and pageCount
|
|
34
|
+
*/
|
|
35
|
+
async generatePDF(filesByExtension, selectedExtensions, outputPath, projectName) {
|
|
36
|
+
console.log(chalk.gray('Generating PDF...'));
|
|
37
|
+
|
|
38
|
+
// Initialize PDF document with A4 size and optimized margins
|
|
39
|
+
this.doc = new PDFDocument({
|
|
40
|
+
size: 'A4',
|
|
41
|
+
margins: {
|
|
42
|
+
top: this.config.styles.layout.marginTop,
|
|
43
|
+
bottom: this.config.styles.layout.marginTop,
|
|
44
|
+
left: this.config.styles.layout.marginLeft,
|
|
45
|
+
right: this.config.styles.layout.marginRight
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
// Setup output stream with error handling for files in use
|
|
51
|
+
let finalOutputPath = outputPath;
|
|
52
|
+
let outputStream;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
outputStream = fs.createWriteStream(outputPath);
|
|
56
|
+
this.doc.pipe(outputStream);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error.code === 'EBUSY' || error.code === 'EACCES') {
|
|
59
|
+
// Generate timestamped filename
|
|
60
|
+
const timestamp = new Date().toISOString()
|
|
61
|
+
.replace(/[:.]/g, '')
|
|
62
|
+
.replace('T', '_')
|
|
63
|
+
.substring(0, 15);
|
|
64
|
+
|
|
65
|
+
const dir = path.dirname(outputPath);
|
|
66
|
+
const baseName = path.basename(outputPath, '.pdf');
|
|
67
|
+
finalOutputPath = path.join(dir, `${baseName}_${timestamp}.pdf`);
|
|
68
|
+
|
|
69
|
+
console.log(chalk.yellow(`WARNING: Original file is in use. Creating: ${path.basename(finalOutputPath)}`));
|
|
70
|
+
outputStream = fs.createWriteStream(finalOutputPath);
|
|
71
|
+
this.doc.pipe(outputStream);
|
|
72
|
+
} else {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// PDFKit handles positioning automatically
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Generate the three main sections
|
|
81
|
+
await this.generateTitleSection(selectedExtensions, projectName);
|
|
82
|
+
await this.generateFileStructureSection(filesByExtension, selectedExtensions);
|
|
83
|
+
await this.generateFileContentSection(filesByExtension, selectedExtensions);
|
|
84
|
+
|
|
85
|
+
// No page numbers - content only
|
|
86
|
+
|
|
87
|
+
// Finalize document
|
|
88
|
+
this.doc.end();
|
|
89
|
+
|
|
90
|
+
// Wait for file write to complete
|
|
91
|
+
await new Promise((resolve, reject) => {
|
|
92
|
+
outputStream.on('finish', resolve);
|
|
93
|
+
outputStream.on('error', reject);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log(chalk.green('SUCCESS: PDF generation completed'));
|
|
97
|
+
return { outputPath: finalOutputPath, pageCount: 'N/A' };
|
|
98
|
+
|
|
99
|
+
} catch (error) {
|
|
100
|
+
ErrorHandler.handlePDFError(error, finalOutputPath || outputPath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate the title/overview section
|
|
106
|
+
* @param {Array} selectedExtensions - Selected file extensions
|
|
107
|
+
* @param {string} projectName - Project name
|
|
108
|
+
*/
|
|
109
|
+
async generateTitleSection(selectedExtensions, projectName) {
|
|
110
|
+
// Title
|
|
111
|
+
this.doc
|
|
112
|
+
.fontSize(20)
|
|
113
|
+
.fillColor(this.config.styles.colors.title)
|
|
114
|
+
.font('Helvetica-Bold')
|
|
115
|
+
.text(this.config.settings.documentTitle, {
|
|
116
|
+
align: 'center',
|
|
117
|
+
width: this.getContentWidth()
|
|
118
|
+
})
|
|
119
|
+
.moveDown(1);
|
|
120
|
+
|
|
121
|
+
// Subtitle
|
|
122
|
+
this.doc
|
|
123
|
+
.fontSize(14)
|
|
124
|
+
.fillColor(this.config.styles.colors.section)
|
|
125
|
+
.font('Helvetica')
|
|
126
|
+
.text(`Project: ${projectName}`, {
|
|
127
|
+
align: 'center',
|
|
128
|
+
width: this.getContentWidth()
|
|
129
|
+
})
|
|
130
|
+
.moveDown(1);
|
|
131
|
+
|
|
132
|
+
// Generation timestamp
|
|
133
|
+
const timestamp = new Date().toLocaleString();
|
|
134
|
+
this.doc
|
|
135
|
+
.fontSize(10)
|
|
136
|
+
.fillColor(this.config.styles.colors.footer)
|
|
137
|
+
.font('Helvetica')
|
|
138
|
+
.text(`Generated on: ${timestamp}`, {
|
|
139
|
+
width: this.getContentWidth()
|
|
140
|
+
})
|
|
141
|
+
.moveDown(2);
|
|
142
|
+
|
|
143
|
+
// Included file types section
|
|
144
|
+
this.doc
|
|
145
|
+
.fontSize(16)
|
|
146
|
+
.fillColor(this.config.styles.colors.section)
|
|
147
|
+
.font('Helvetica-Bold')
|
|
148
|
+
.text('Included File Types', {
|
|
149
|
+
width: this.getContentWidth()
|
|
150
|
+
})
|
|
151
|
+
.moveDown(0.5);
|
|
152
|
+
|
|
153
|
+
selectedExtensions.forEach(ext => {
|
|
154
|
+
const description = this.getExtensionDescription(ext);
|
|
155
|
+
this.doc
|
|
156
|
+
.fontSize(11)
|
|
157
|
+
.fillColor(this.config.styles.colors.text)
|
|
158
|
+
.font('Helvetica')
|
|
159
|
+
.text(`- ${ext} -> ${description}`, {
|
|
160
|
+
width: this.getContentWidth()
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
this.doc.moveDown(2);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generate the file structure section
|
|
169
|
+
* @param {object} filesByExtension - Files grouped by extension
|
|
170
|
+
* @param {Array} selectedExtensions - Selected extensions
|
|
171
|
+
*/
|
|
172
|
+
async generateFileStructureSection(filesByExtension, selectedExtensions) {
|
|
173
|
+
// Section header
|
|
174
|
+
this.doc
|
|
175
|
+
.fontSize(16)
|
|
176
|
+
.fillColor(this.config.styles.colors.section)
|
|
177
|
+
.font('Helvetica-Bold')
|
|
178
|
+
.text('Project File Structure', {
|
|
179
|
+
width: this.getContentWidth()
|
|
180
|
+
})
|
|
181
|
+
.moveDown(1);
|
|
182
|
+
|
|
183
|
+
// Collect all files from selected extensions
|
|
184
|
+
const allFiles = [];
|
|
185
|
+
selectedExtensions.forEach(ext => {
|
|
186
|
+
if (filesByExtension[ext]) {
|
|
187
|
+
allFiles.push(...filesByExtension[ext]);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Sort files by path
|
|
192
|
+
allFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
193
|
+
|
|
194
|
+
// Add file list as continuous text
|
|
195
|
+
const fileList = allFiles.map(file => file.relativePath).join('\n');
|
|
196
|
+
|
|
197
|
+
this.doc
|
|
198
|
+
.fontSize(10)
|
|
199
|
+
.fillColor(this.config.styles.colors.text)
|
|
200
|
+
.font('Courier')
|
|
201
|
+
.text(fileList, {
|
|
202
|
+
width: this.getContentWidth()
|
|
203
|
+
})
|
|
204
|
+
.moveDown(2);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Generate the file content section
|
|
209
|
+
* @param {object} filesByExtension - Files grouped by extension
|
|
210
|
+
* @param {Array} selectedExtensions - Selected extensions
|
|
211
|
+
*/
|
|
212
|
+
async generateFileContentSection(filesByExtension, selectedExtensions) {
|
|
213
|
+
// Section header
|
|
214
|
+
this.doc
|
|
215
|
+
.fontSize(16)
|
|
216
|
+
.fillColor(this.config.styles.colors.section)
|
|
217
|
+
.font('Helvetica-Bold')
|
|
218
|
+
.text('Project File Content', {
|
|
219
|
+
width: this.getContentWidth()
|
|
220
|
+
})
|
|
221
|
+
.moveDown(2);
|
|
222
|
+
|
|
223
|
+
for (const extension of selectedExtensions) {
|
|
224
|
+
if (!filesByExtension[extension]) continue;
|
|
225
|
+
|
|
226
|
+
const files = filesByExtension[extension];
|
|
227
|
+
|
|
228
|
+
for (const file of files) {
|
|
229
|
+
await this.addFileContent(file);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Add content of a single file to the PDF
|
|
236
|
+
* @param {object} file - File object with path and metadata
|
|
237
|
+
*/
|
|
238
|
+
async addFileContent(file) {
|
|
239
|
+
// Add file header
|
|
240
|
+
this.addFileHeader(file.relativePath);
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
// Read and validate file content
|
|
244
|
+
const contentBuffer = await fs.readFile(file.absolutePath);
|
|
245
|
+
|
|
246
|
+
// Validate content before processing
|
|
247
|
+
if (!ErrorHandler.validateFileContent(file.relativePath, contentBuffer)) {
|
|
248
|
+
this.addErrorContent('File appears to contain binary data');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const content = contentBuffer.toString('utf8');
|
|
253
|
+
|
|
254
|
+
// Include all content without truncation
|
|
255
|
+
const lines = content.split('\n');
|
|
256
|
+
if (lines.length > 1000) {
|
|
257
|
+
console.log(chalk.gray(`Processing large file with ${lines.length} lines: ${file.relativePath}`));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await this.addCodeContent(content);
|
|
261
|
+
|
|
262
|
+
} catch (error) {
|
|
263
|
+
// Handle unreadable files with appropriate error messages
|
|
264
|
+
let errorMessage = 'Could not read file';
|
|
265
|
+
|
|
266
|
+
if (error.code === 'EACCES') {
|
|
267
|
+
errorMessage = 'Permission denied';
|
|
268
|
+
} else if (error.code === 'ENOENT') {
|
|
269
|
+
errorMessage = 'File not found';
|
|
270
|
+
} else if (error.code === 'EISDIR') {
|
|
271
|
+
errorMessage = 'Path is a directory';
|
|
272
|
+
} else if (error.message) {
|
|
273
|
+
errorMessage = error.message;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.addErrorContent(errorMessage);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Add file header
|
|
282
|
+
* @param {string} filePath - Relative file path
|
|
283
|
+
*/
|
|
284
|
+
addFileHeader(filePath) {
|
|
285
|
+
this.doc
|
|
286
|
+
.moveDown(1)
|
|
287
|
+
.fontSize(12)
|
|
288
|
+
.fillColor(this.config.styles.colors.section)
|
|
289
|
+
.font('Helvetica-Bold')
|
|
290
|
+
.text(`File: ${filePath}`, {
|
|
291
|
+
width: this.getContentWidth()
|
|
292
|
+
})
|
|
293
|
+
.moveDown(0.5);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Add code content with proper formatting
|
|
298
|
+
* @param {string} content - File content
|
|
299
|
+
*/
|
|
300
|
+
async addCodeContent(content) {
|
|
301
|
+
// Clean and prepare content
|
|
302
|
+
const cleanContent = this.prepareContent(content);
|
|
303
|
+
|
|
304
|
+
this.doc
|
|
305
|
+
.fontSize(9)
|
|
306
|
+
.fillColor(this.config.styles.colors.text)
|
|
307
|
+
.font('Courier')
|
|
308
|
+
.text(cleanContent, {
|
|
309
|
+
width: this.getContentWidth(),
|
|
310
|
+
align: 'left'
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Add some spacing after code block
|
|
314
|
+
this.doc.moveDown(1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Add error content for unreadable files
|
|
319
|
+
* @param {string} errorMessage - Error message to display
|
|
320
|
+
*/
|
|
321
|
+
addErrorContent(errorMessage) {
|
|
322
|
+
this.doc
|
|
323
|
+
.fontSize(10)
|
|
324
|
+
.fillColor(this.config.styles.colors.error)
|
|
325
|
+
.font('Helvetica-Oblique')
|
|
326
|
+
.text(`ERROR: ${errorMessage}`, {
|
|
327
|
+
width: this.getContentWidth()
|
|
328
|
+
})
|
|
329
|
+
.moveDown(0.5);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Prepare content for PDF rendering
|
|
334
|
+
* @param {string} content - Raw file content
|
|
335
|
+
* @returns {string} Cleaned content
|
|
336
|
+
*/
|
|
337
|
+
prepareContent(content) {
|
|
338
|
+
return content
|
|
339
|
+
.replace(/\r\n/g, '\n') // Normalize line endings
|
|
340
|
+
.replace(/\r/g, '\n') // Handle old Mac line endings
|
|
341
|
+
.replace(/\t/g, ' ') // Replace tabs with 4 spaces for better alignment
|
|
342
|
+
.replace(/[^\x20-\x7E\n\u00A0-\uFFFF]/g, ''); // Remove control chars, keep Unicode text
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// All text methods removed - using direct PDFKit calls for simplicity
|
|
346
|
+
|
|
347
|
+
// Page numbering removed to prevent duplicate pages
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get description for file extension
|
|
351
|
+
* @param {string} extension - File extension
|
|
352
|
+
* @returns {string} Description
|
|
353
|
+
*/
|
|
354
|
+
getExtensionDescription(extension) {
|
|
355
|
+
const descriptions = {
|
|
356
|
+
'.js': 'JavaScript',
|
|
357
|
+
'.ts': 'TypeScript',
|
|
358
|
+
'.jsx': 'React JSX',
|
|
359
|
+
'.tsx': 'TypeScript JSX',
|
|
360
|
+
'.json': 'JSON',
|
|
361
|
+
'.xml': 'XML',
|
|
362
|
+
'.html': 'HTML',
|
|
363
|
+
'.css': 'CSS',
|
|
364
|
+
'.scss': 'SCSS',
|
|
365
|
+
'.md': 'Markdown',
|
|
366
|
+
'.txt': 'Text',
|
|
367
|
+
'.py': 'Python',
|
|
368
|
+
'.java': 'Java',
|
|
369
|
+
'.cs': 'C#',
|
|
370
|
+
'.cpp': 'C++',
|
|
371
|
+
'.c': 'C',
|
|
372
|
+
'.h': 'Header',
|
|
373
|
+
'.yaml': 'YAML',
|
|
374
|
+
'.yml': 'YAML',
|
|
375
|
+
'.sh': 'Shell Script',
|
|
376
|
+
'.bat': 'Batch File'
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
return descriptions[extension] || 'Unknown';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Generate output file path with timestamp fallback if file is in use
|
|
384
|
+
* @param {string} projectName - Project name
|
|
385
|
+
* @param {string} outputDir - Output directory
|
|
386
|
+
* @returns {string} Complete output file path
|
|
387
|
+
*/
|
|
388
|
+
static generateOutputPath(projectName, outputDir) {
|
|
389
|
+
const baseFileName = `${projectName.toUpperCase()}_code.pdf`;
|
|
390
|
+
const basePath = path.join(outputDir, baseFileName);
|
|
391
|
+
|
|
392
|
+
// Check if file exists and is potentially in use
|
|
393
|
+
if (fs.existsSync(basePath)) {
|
|
394
|
+
try {
|
|
395
|
+
// Try to open the file to check if it's in use
|
|
396
|
+
const fd = fs.openSync(basePath, 'r+');
|
|
397
|
+
fs.closeSync(fd);
|
|
398
|
+
// File is not in use, can overwrite
|
|
399
|
+
return basePath;
|
|
400
|
+
} catch (error) {
|
|
401
|
+
// File is in use, generate timestamped name
|
|
402
|
+
if (error.code === 'EBUSY' || error.code === 'EACCES') {
|
|
403
|
+
const timestamp = new Date().toISOString()
|
|
404
|
+
.replace(/[:.]/g, '')
|
|
405
|
+
.replace('T', '_')
|
|
406
|
+
.substring(0, 15); // YYYYMMDD_HHMMSS
|
|
407
|
+
|
|
408
|
+
const timestampedFileName = `${projectName.toUpperCase()}_code_${timestamp}.pdf`;
|
|
409
|
+
console.log(chalk.yellow(`WARNING: Original file is in use. Creating: ${timestampedFileName}`));
|
|
410
|
+
return path.join(outputDir, timestampedFileName);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return basePath;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Ensure output directory exists
|
|
420
|
+
* @param {string} outputDir - Output directory path
|
|
421
|
+
*/
|
|
422
|
+
static async ensureOutputDirectory(outputDir) {
|
|
423
|
+
await fs.ensureDir(outputDir);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export default PDFGenerator;
|