codesummary 1.0.2 → 1.1.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,427 +1,476 @@
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
-
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
+
427
476
  export default PDFGenerator;