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.
@@ -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;