codesummary 1.1.1 → 1.2.1

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