@sun-asterisk/sunlint 1.3.24 → 1.3.26

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,839 @@
1
+ /**
2
+ * HTML Report Generator
3
+ * Generate standalone HTML report with embedded CSS for SunLint results
4
+ */
5
+
6
+ /**
7
+ * Generate HTML report from violations data
8
+ * @param {Array} violations - Array of violation objects
9
+ * @param {Object} metadata - Report metadata
10
+ * @param {Object} metadata.score - Scoring summary
11
+ * @param {Object} metadata.gitInfo - Git information
12
+ * @param {string} metadata.timestamp - Report timestamp
13
+ * @returns {string} Complete HTML report
14
+ */
15
+ function generateHTMLReport(violations, metadata = {}) {
16
+ const {
17
+ score = {},
18
+ gitInfo = {},
19
+ timestamp = new Date().toISOString()
20
+ } = metadata;
21
+
22
+ // Calculate statistics
23
+ const stats = calculateStatistics(violations);
24
+
25
+ // Generate HTML sections
26
+ const html = `<!DOCTYPE html>
27
+ <html lang="en">
28
+ <head>
29
+ <meta charset="UTF-8">
30
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
31
+ <title>SunLint Report - ${gitInfo.repository_name || 'Project'}</title>
32
+ ${embedCSS()}
33
+ </head>
34
+ <body>
35
+ <div class="container">
36
+ ${generateHeader(gitInfo, timestamp)}
37
+ ${generateSummary(stats, score)}
38
+ ${generateByFileSection(violations, stats.fileGroups)}
39
+ ${generateByRuleSection(violations, stats.ruleGroups)}
40
+ ${generateFooter(timestamp)}
41
+ </div>
42
+ ${embedJavaScript()}
43
+ </body>
44
+ </html>`;
45
+
46
+ return html;
47
+ }
48
+
49
+ /**
50
+ * Calculate statistics from violations
51
+ * @param {Array} violations - Violations array
52
+ * @returns {Object} Statistics object
53
+ */
54
+ function calculateStatistics(violations) {
55
+ const totalViolations = violations.length;
56
+ const errorCount = violations.filter(v => v.severity === 'error').length;
57
+ const warningCount = violations.filter(v => v.severity === 'warning').length;
58
+
59
+ // Group by file
60
+ const fileGroups = {};
61
+ for (const v of violations) {
62
+ if (!fileGroups[v.file]) {
63
+ fileGroups[v.file] = [];
64
+ }
65
+ fileGroups[v.file].push(v);
66
+ }
67
+
68
+ // Group by rule
69
+ const ruleGroups = {};
70
+ for (const v of violations) {
71
+ if (!ruleGroups[v.rule]) {
72
+ ruleGroups[v.rule] = [];
73
+ }
74
+ ruleGroups[v.rule].push(v);
75
+ }
76
+
77
+ const filesWithIssues = Object.keys(fileGroups).length;
78
+
79
+ return {
80
+ totalViolations,
81
+ errorCount,
82
+ warningCount,
83
+ filesWithIssues,
84
+ fileGroups,
85
+ ruleGroups
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Generate HTML header
91
+ * @param {Object} gitInfo - Git information
92
+ * @param {string} timestamp - Report timestamp
93
+ * @returns {string} Header HTML
94
+ */
95
+ function generateHeader(gitInfo, timestamp) {
96
+ const repoName = gitInfo.repository_name || 'Project';
97
+ const branch = gitInfo.branch || 'Unknown';
98
+ const commit = gitInfo.commit_hash ? gitInfo.commit_hash.substring(0, 7) : 'N/A';
99
+
100
+ return `
101
+ <header class="header">
102
+ <div class="header-content">
103
+ <h1>
104
+ <span class="logo">🌟</span>
105
+ SunLint Code Quality Report
106
+ </h1>
107
+ <div class="header-meta">
108
+ <div class="meta-item">
109
+ <span class="meta-label">Repository:</span>
110
+ <span class="meta-value">${escapeHTML(repoName)}</span>
111
+ </div>
112
+ <div class="meta-item">
113
+ <span class="meta-label">Branch:</span>
114
+ <span class="meta-value">${escapeHTML(branch)}</span>
115
+ </div>
116
+ <div class="meta-item">
117
+ <span class="meta-label">Commit:</span>
118
+ <span class="meta-value">${escapeHTML(commit)}</span>
119
+ </div>
120
+ <div class="meta-item">
121
+ <span class="meta-label">Generated:</span>
122
+ <span class="meta-value">${new Date(timestamp).toLocaleString()}</span>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </header>`;
127
+ }
128
+
129
+ /**
130
+ * Generate summary section
131
+ * @param {Object} stats - Statistics object
132
+ * @param {Object} score - Score information
133
+ * @returns {string} Summary HTML
134
+ */
135
+ function generateSummary(stats, score) {
136
+ const { totalViolations, errorCount, warningCount, filesWithIssues } = stats;
137
+ const status = errorCount > 0 ? 'failed' : warningCount > 0 ? 'warning' : 'passed';
138
+ const statusIcon = errorCount > 0 ? '❌' : warningCount > 0 ? '⚠️' : '✅';
139
+ const statusText = errorCount > 0 ? 'Failed' : warningCount > 0 ? 'Passed with Warnings' : 'Passed';
140
+
141
+ const scoreValue = score.score !== undefined ? score.score : 'N/A';
142
+ const grade = score.grade || 'N/A';
143
+
144
+ return `
145
+ <section class="summary">
146
+ <div class="summary-header">
147
+ <div class="status-badge status-${status}">
148
+ <span class="status-icon">${statusIcon}</span>
149
+ <span class="status-text">${statusText}</span>
150
+ </div>
151
+ ${scoreValue !== 'N/A' ? `
152
+ <div class="score-display">
153
+ <div class="score-value">${scoreValue}</div>
154
+ <div class="score-grade">${grade}</div>
155
+ </div>
156
+ ` : ''}
157
+ </div>
158
+
159
+ <div class="summary-stats">
160
+ <div class="stat-card">
161
+ <div class="stat-value">${totalViolations}</div>
162
+ <div class="stat-label">Total Violations</div>
163
+ </div>
164
+ <div class="stat-card stat-error">
165
+ <div class="stat-value">${errorCount}</div>
166
+ <div class="stat-label">Errors</div>
167
+ </div>
168
+ <div class="stat-card stat-warning">
169
+ <div class="stat-value">${warningCount}</div>
170
+ <div class="stat-label">Warnings</div>
171
+ </div>
172
+ <div class="stat-card">
173
+ <div class="stat-value">${filesWithIssues}</div>
174
+ <div class="stat-label">Files with Issues</div>
175
+ </div>
176
+ </div>
177
+ </section>`;
178
+ }
179
+
180
+ /**
181
+ * Generate violations by file section
182
+ * @param {Array} violations - Violations array
183
+ * @param {Object} fileGroups - Grouped by file
184
+ * @returns {string} By file HTML
185
+ */
186
+ function generateByFileSection(violations, fileGroups) {
187
+ if (violations.length === 0) {
188
+ return `
189
+ <section class="section">
190
+ <h2>✅ Great Job!</h2>
191
+ <p class="no-violations">No coding standard violations found.</p>
192
+ </section>`;
193
+ }
194
+
195
+ const sortedFiles = Object.entries(fileGroups)
196
+ .sort((a, b) => b[1].length - a[1].length);
197
+
198
+ let html = `
199
+ <section class="section">
200
+ <h2>📁 Violations by File</h2>
201
+ <div class="controls">
202
+ <input type="text" id="fileSearch" class="search-box" placeholder="Search files...">
203
+ <select id="fileFilter" class="filter-select">
204
+ <option value="all">All Severities</option>
205
+ <option value="error">Errors Only</option>
206
+ <option value="warning">Warnings Only</option>
207
+ </select>
208
+ </div>
209
+ <table class="violations-table" id="fileTable">
210
+ <thead>
211
+ <tr>
212
+ <th onclick="sortTable('fileTable', 0)">File <span class="sort-icon">↕</span></th>
213
+ <th onclick="sortTable('fileTable', 1)">Errors <span class="sort-icon">↕</span></th>
214
+ <th onclick="sortTable('fileTable', 2)">Warnings <span class="sort-icon">↕</span></th>
215
+ <th onclick="sortTable('fileTable', 3)">Total <span class="sort-icon">↕</span></th>
216
+ <th>Details</th>
217
+ </tr>
218
+ </thead>
219
+ <tbody>`;
220
+
221
+ for (const [file, fileViolations] of sortedFiles) {
222
+ const fileErrors = fileViolations.filter(v => v.severity === 'error').length;
223
+ const fileWarnings = fileViolations.filter(v => v.severity === 'warning').length;
224
+ const total = fileViolations.length;
225
+ const fileId = `file-${Buffer.from(file).toString('base64').replace(/=/g, '')}`;
226
+
227
+ html += `
228
+ <tr class="file-row" data-errors="${fileErrors}" data-warnings="${fileWarnings}">
229
+ <td class="file-path"><code>${escapeHTML(file)}</code></td>
230
+ <td class="count-cell ${fileErrors > 0 ? 'has-errors' : ''}">${fileErrors}</td>
231
+ <td class="count-cell ${fileWarnings > 0 ? 'has-warnings' : ''}">${fileWarnings}</td>
232
+ <td class="count-cell">${total}</td>
233
+ <td>
234
+ <button class="details-btn" onclick="toggleDetails('${fileId}')">
235
+ Show Details <span class="arrow">▼</span>
236
+ </button>
237
+ </td>
238
+ </tr>
239
+ <tr class="details-row" id="${fileId}" style="display: none;">
240
+ <td colspan="5">
241
+ <div class="details-content">
242
+ ${generateFileDetails(fileViolations)}
243
+ </div>
244
+ </td>
245
+ </tr>`;
246
+ }
247
+
248
+ html += `
249
+ </tbody>
250
+ </table>
251
+ </section>`;
252
+
253
+ return html;
254
+ }
255
+
256
+ /**
257
+ * Generate file details
258
+ * @param {Array} violations - Violations for a file
259
+ * @returns {string} Details HTML
260
+ */
261
+ function generateFileDetails(violations) {
262
+ let html = '<ul class="violation-list">';
263
+
264
+ for (const v of violations) {
265
+ const severityClass = v.severity === 'error' ? 'severity-error' : 'severity-warning';
266
+ const severityIcon = v.severity === 'error' ? '🔴' : '🟡';
267
+
268
+ html += `
269
+ <li class="violation-item">
270
+ <span class="${severityClass}">${severityIcon} ${v.severity.toUpperCase()}</span>
271
+ <span class="violation-line">Line ${v.line}</span>
272
+ <span class="violation-rule">[${escapeHTML(v.rule)}]</span>
273
+ <span class="violation-message">${escapeHTML(v.message)}</span>
274
+ </li>`;
275
+ }
276
+
277
+ html += '</ul>';
278
+ return html;
279
+ }
280
+
281
+ /**
282
+ * Generate violations by rule section
283
+ * @param {Array} violations - Violations array
284
+ * @param {Object} ruleGroups - Grouped by rule
285
+ * @returns {string} By rule HTML
286
+ */
287
+ function generateByRuleSection(violations, ruleGroups) {
288
+ if (violations.length === 0) {
289
+ return '';
290
+ }
291
+
292
+ const sortedRules = Object.entries(ruleGroups)
293
+ .sort((a, b) => b[1].length - a[1].length);
294
+
295
+ let html = `
296
+ <section class="section">
297
+ <h2>📋 Violations by Rule</h2>
298
+ <table class="violations-table" id="ruleTable">
299
+ <thead>
300
+ <tr>
301
+ <th onclick="sortTable('ruleTable', 0)">Rule <span class="sort-icon">↕</span></th>
302
+ <th onclick="sortTable('ruleTable', 1)">Errors <span class="sort-icon">↕</span></th>
303
+ <th onclick="sortTable('ruleTable', 2)">Warnings <span class="sort-icon">↕</span></th>
304
+ <th onclick="sortTable('ruleTable', 3)">Total <span class="sort-icon">↕</span></th>
305
+ <th>Locations</th>
306
+ </tr>
307
+ </thead>
308
+ <tbody>`;
309
+
310
+ for (const [ruleId, ruleViolations] of sortedRules) {
311
+ const ruleErrors = ruleViolations.filter(v => v.severity === 'error').length;
312
+ const ruleWarnings = ruleViolations.filter(v => v.severity === 'warning').length;
313
+ const total = ruleViolations.length;
314
+ const ruleIdSafe = `rule-${Buffer.from(ruleId).toString('base64').replace(/=/g, '')}`;
315
+
316
+ html += `
317
+ <tr class="rule-row">
318
+ <td class="rule-id"><code>${escapeHTML(ruleId)}</code></td>
319
+ <td class="count-cell ${ruleErrors > 0 ? 'has-errors' : ''}">${ruleErrors}</td>
320
+ <td class="count-cell ${ruleWarnings > 0 ? 'has-warnings' : ''}">${ruleWarnings}</td>
321
+ <td class="count-cell">${total}</td>
322
+ <td>
323
+ <button class="details-btn" onclick="toggleDetails('${ruleIdSafe}')">
324
+ Show Locations <span class="arrow">▼</span>
325
+ </button>
326
+ </td>
327
+ </tr>
328
+ <tr class="details-row" id="${ruleIdSafe}" style="display: none;">
329
+ <td colspan="5">
330
+ <div class="details-content">
331
+ ${generateRuleDetails(ruleViolations)}
332
+ </div>
333
+ </td>
334
+ </tr>`;
335
+ }
336
+
337
+ html += `
338
+ </tbody>
339
+ </table>
340
+ </section>`;
341
+
342
+ return html;
343
+ }
344
+
345
+ /**
346
+ * Generate rule details
347
+ * @param {Array} violations - Violations for a rule
348
+ * @returns {string} Details HTML
349
+ */
350
+ function generateRuleDetails(violations) {
351
+ let html = '<ul class="location-list">';
352
+
353
+ for (const v of violations) {
354
+ const severityClass = v.severity === 'error' ? 'severity-error' : 'severity-warning';
355
+ const severityIcon = v.severity === 'error' ? '🔴' : '🟡';
356
+
357
+ html += `
358
+ <li class="location-item">
359
+ <span class="${severityClass}">${severityIcon}</span>
360
+ <code class="location-path">${escapeHTML(v.file)}:${v.line}</code>
361
+ <span class="location-message">${escapeHTML(v.message)}</span>
362
+ </li>`;
363
+ }
364
+
365
+ html += '</ul>';
366
+ return html;
367
+ }
368
+
369
+ /**
370
+ * Generate footer
371
+ * @param {string} timestamp - Report timestamp
372
+ * @returns {string} Footer HTML
373
+ */
374
+ function generateFooter(timestamp) {
375
+ return `
376
+ <footer class="footer">
377
+ <p>
378
+ Generated by <a href="https://github.com/sun-asterisk/engineer-excellence" target="_blank">SunLint</a>
379
+ on ${new Date(timestamp).toLocaleString()}
380
+ </p>
381
+ </footer>`;
382
+ }
383
+
384
+ /**
385
+ * Embed CSS styles
386
+ * @returns {string} Style tag
387
+ */
388
+ function embedCSS() {
389
+ return `<style>
390
+ * { margin: 0; padding: 0; box-sizing: border-box; }
391
+
392
+ body {
393
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
394
+ line-height: 1.6;
395
+ color: #333;
396
+ background: #f5f5f5;
397
+ padding: 20px;
398
+ }
399
+
400
+ .container {
401
+ max-width: 1400px;
402
+ margin: 0 auto;
403
+ background: white;
404
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
405
+ border-radius: 8px;
406
+ overflow: hidden;
407
+ }
408
+
409
+ .header {
410
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
411
+ color: white;
412
+ padding: 30px;
413
+ }
414
+
415
+ .header-content h1 {
416
+ font-size: 28px;
417
+ margin-bottom: 20px;
418
+ display: flex;
419
+ align-items: center;
420
+ gap: 10px;
421
+ }
422
+
423
+ .logo { font-size: 32px; }
424
+
425
+ .header-meta {
426
+ display: grid;
427
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
428
+ gap: 15px;
429
+ margin-top: 20px;
430
+ }
431
+
432
+ .meta-item {
433
+ background: rgba(255,255,255,0.1);
434
+ padding: 10px 15px;
435
+ border-radius: 5px;
436
+ }
437
+
438
+ .meta-label {
439
+ display: block;
440
+ font-size: 12px;
441
+ opacity: 0.8;
442
+ margin-bottom: 5px;
443
+ }
444
+
445
+ .meta-value {
446
+ display: block;
447
+ font-size: 14px;
448
+ font-weight: 600;
449
+ }
450
+
451
+ .summary {
452
+ padding: 30px;
453
+ background: #f9fafb;
454
+ border-bottom: 1px solid #e5e7eb;
455
+ }
456
+
457
+ .summary-header {
458
+ display: flex;
459
+ justify-content: space-between;
460
+ align-items: center;
461
+ margin-bottom: 30px;
462
+ }
463
+
464
+ .status-badge {
465
+ display: inline-flex;
466
+ align-items: center;
467
+ gap: 10px;
468
+ padding: 12px 24px;
469
+ border-radius: 8px;
470
+ font-size: 20px;
471
+ font-weight: 600;
472
+ }
473
+
474
+ .status-passed { background: #d1fae5; color: #065f46; }
475
+ .status-warning { background: #fef3c7; color: #92400e; }
476
+ .status-failed { background: #fee2e2; color: #991b1b; }
477
+
478
+ .score-display {
479
+ text-align: center;
480
+ }
481
+
482
+ .score-value {
483
+ font-size: 48px;
484
+ font-weight: bold;
485
+ color: #667eea;
486
+ }
487
+
488
+ .score-grade {
489
+ font-size: 24px;
490
+ color: #666;
491
+ margin-top: -5px;
492
+ }
493
+
494
+ .summary-stats {
495
+ display: grid;
496
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
497
+ gap: 20px;
498
+ }
499
+
500
+ .stat-card {
501
+ background: white;
502
+ padding: 20px;
503
+ border-radius: 8px;
504
+ text-align: center;
505
+ border: 2px solid #e5e7eb;
506
+ }
507
+
508
+ .stat-card.stat-error { border-color: #fca5a5; }
509
+ .stat-card.stat-warning { border-color: #fcd34d; }
510
+
511
+ .stat-value {
512
+ font-size: 36px;
513
+ font-weight: bold;
514
+ color: #667eea;
515
+ margin-bottom: 5px;
516
+ }
517
+
518
+ .stat-card.stat-error .stat-value { color: #dc2626; }
519
+ .stat-card.stat-warning .stat-value { color: #f59e0b; }
520
+
521
+ .stat-label {
522
+ font-size: 14px;
523
+ color: #666;
524
+ }
525
+
526
+ .section {
527
+ padding: 30px;
528
+ border-bottom: 1px solid #e5e7eb;
529
+ }
530
+
531
+ .section h2 {
532
+ font-size: 22px;
533
+ margin-bottom: 20px;
534
+ color: #1f2937;
535
+ }
536
+
537
+ .controls {
538
+ display: flex;
539
+ gap: 15px;
540
+ margin-bottom: 20px;
541
+ }
542
+
543
+ .search-box, .filter-select {
544
+ padding: 10px 15px;
545
+ border: 2px solid #e5e7eb;
546
+ border-radius: 6px;
547
+ font-size: 14px;
548
+ }
549
+
550
+ .search-box {
551
+ flex: 1;
552
+ max-width: 400px;
553
+ }
554
+
555
+ .filter-select {
556
+ min-width: 150px;
557
+ }
558
+
559
+ .violations-table {
560
+ width: 100%;
561
+ border-collapse: collapse;
562
+ background: white;
563
+ border: 1px solid #e5e7eb;
564
+ border-radius: 8px;
565
+ overflow: hidden;
566
+ }
567
+
568
+ .violations-table th {
569
+ background: #f9fafb;
570
+ padding: 12px;
571
+ text-align: left;
572
+ font-weight: 600;
573
+ border-bottom: 2px solid #e5e7eb;
574
+ cursor: pointer;
575
+ user-select: none;
576
+ }
577
+
578
+ .violations-table th:hover { background: #f3f4f6; }
579
+
580
+ .sort-icon {
581
+ font-size: 10px;
582
+ opacity: 0.5;
583
+ margin-left: 5px;
584
+ }
585
+
586
+ .violations-table td {
587
+ padding: 12px;
588
+ border-bottom: 1px solid #e5e7eb;
589
+ }
590
+
591
+ .file-path, .rule-id {
592
+ font-family: 'Courier New', monospace;
593
+ font-size: 13px;
594
+ }
595
+
596
+ .count-cell {
597
+ text-align: center;
598
+ font-weight: 600;
599
+ }
600
+
601
+ .count-cell.has-errors { color: #dc2626; }
602
+ .count-cell.has-warnings { color: #f59e0b; }
603
+
604
+ .details-btn {
605
+ background: #667eea;
606
+ color: white;
607
+ border: none;
608
+ padding: 6px 12px;
609
+ border-radius: 4px;
610
+ cursor: pointer;
611
+ font-size: 13px;
612
+ display: inline-flex;
613
+ align-items: center;
614
+ gap: 5px;
615
+ }
616
+
617
+ .details-btn:hover { background: #5568d3; }
618
+
619
+ .arrow {
620
+ font-size: 10px;
621
+ transition: transform 0.2s;
622
+ }
623
+
624
+ .details-row td {
625
+ background: #f9fafb;
626
+ padding: 0;
627
+ }
628
+
629
+ .details-content {
630
+ padding: 20px;
631
+ }
632
+
633
+ .violation-list, .location-list {
634
+ list-style: none;
635
+ }
636
+
637
+ .violation-item, .location-item {
638
+ padding: 10px;
639
+ margin-bottom: 8px;
640
+ background: white;
641
+ border-left: 3px solid #e5e7eb;
642
+ border-radius: 4px;
643
+ display: flex;
644
+ gap: 10px;
645
+ align-items: center;
646
+ }
647
+
648
+ .severity-error { color: #dc2626; font-weight: 600; }
649
+ .severity-warning { color: #f59e0b; font-weight: 600; }
650
+
651
+ .violation-line {
652
+ background: #e5e7eb;
653
+ padding: 2px 8px;
654
+ border-radius: 3px;
655
+ font-size: 12px;
656
+ font-family: monospace;
657
+ }
658
+
659
+ .violation-rule {
660
+ color: #667eea;
661
+ font-weight: 600;
662
+ font-size: 12px;
663
+ }
664
+
665
+ .violation-message, .location-message {
666
+ flex: 1;
667
+ color: #666;
668
+ font-size: 14px;
669
+ }
670
+
671
+ .location-path {
672
+ font-family: 'Courier New', monospace;
673
+ background: #f3f4f6;
674
+ padding: 2px 8px;
675
+ border-radius: 3px;
676
+ font-size: 12px;
677
+ }
678
+
679
+ .no-violations {
680
+ text-align: center;
681
+ padding: 40px;
682
+ font-size: 18px;
683
+ color: #10b981;
684
+ }
685
+
686
+ .footer {
687
+ padding: 20px;
688
+ text-align: center;
689
+ color: #666;
690
+ font-size: 14px;
691
+ background: #f9fafb;
692
+ }
693
+
694
+ .footer a {
695
+ color: #667eea;
696
+ text-decoration: none;
697
+ }
698
+
699
+ .footer a:hover { text-decoration: underline; }
700
+
701
+ @media print {
702
+ body { background: white; padding: 0; }
703
+ .container { box-shadow: none; }
704
+ .details-btn { display: none; }
705
+ .details-row { display: table-row !important; }
706
+ }
707
+
708
+ @media (max-width: 768px) {
709
+ .header-meta { grid-template-columns: 1fr; }
710
+ .summary-stats { grid-template-columns: 1fr; }
711
+ .controls { flex-direction: column; }
712
+ .search-box { max-width: 100%; }
713
+ }
714
+ </style>`;
715
+ }
716
+
717
+ /**
718
+ * Embed JavaScript
719
+ * @returns {string} Script tag
720
+ */
721
+ function embedJavaScript() {
722
+ return `<script>
723
+ // Toggle details row
724
+ function toggleDetails(id) {
725
+ const row = document.getElementById(id);
726
+ const btn = event.target.closest('.details-btn');
727
+ const arrow = btn.querySelector('.arrow');
728
+
729
+ if (row.style.display === 'none') {
730
+ row.style.display = 'table-row';
731
+ arrow.style.transform = 'rotate(180deg)';
732
+ btn.innerHTML = 'Hide Details <span class="arrow" style="transform: rotate(180deg);">▼</span>';
733
+ } else {
734
+ row.style.display = 'none';
735
+ arrow.style.transform = 'rotate(0deg)';
736
+ btn.innerHTML = 'Show Details <span class="arrow">▼</span>';
737
+ }
738
+ }
739
+
740
+ // Sort table
741
+ function sortTable(tableId, columnIndex) {
742
+ const table = document.getElementById(tableId);
743
+ const tbody = table.querySelector('tbody');
744
+ const rows = Array.from(tbody.querySelectorAll('tr')).filter(r => !r.classList.contains('details-row'));
745
+
746
+ const isNumeric = columnIndex > 0;
747
+ rows.sort((a, b) => {
748
+ const aVal = a.cells[columnIndex].textContent.trim();
749
+ const bVal = b.cells[columnIndex].textContent.trim();
750
+
751
+ if (isNumeric) {
752
+ return parseInt(bVal) - parseInt(aVal);
753
+ }
754
+ return aVal.localeCompare(bVal);
755
+ });
756
+
757
+ rows.forEach(row => {
758
+ const detailsRow = row.nextElementSibling;
759
+ tbody.appendChild(row);
760
+ if (detailsRow && detailsRow.classList.contains('details-row')) {
761
+ tbody.appendChild(detailsRow);
762
+ }
763
+ });
764
+ }
765
+
766
+ // File search
767
+ const fileSearch = document.getElementById('fileSearch');
768
+ if (fileSearch) {
769
+ fileSearch.addEventListener('input', function() {
770
+ const searchTerm = this.value.toLowerCase();
771
+ const rows = document.querySelectorAll('#fileTable tbody tr.file-row');
772
+
773
+ rows.forEach(row => {
774
+ const file = row.querySelector('.file-path').textContent.toLowerCase();
775
+ const detailsRow = row.nextElementSibling;
776
+
777
+ if (file.includes(searchTerm)) {
778
+ row.style.display = '';
779
+ if (detailsRow && detailsRow.style.display !== 'none') {
780
+ detailsRow.style.display = '';
781
+ }
782
+ } else {
783
+ row.style.display = 'none';
784
+ if (detailsRow) {
785
+ detailsRow.style.display = 'none';
786
+ }
787
+ }
788
+ });
789
+ });
790
+ }
791
+
792
+ // File filter
793
+ const fileFilter = document.getElementById('fileFilter');
794
+ if (fileFilter) {
795
+ fileFilter.addEventListener('change', function() {
796
+ const filter = this.value;
797
+ const rows = document.querySelectorAll('#fileTable tbody tr.file-row');
798
+
799
+ rows.forEach(row => {
800
+ const errors = parseInt(row.dataset.errors);
801
+ const warnings = parseInt(row.dataset.warnings);
802
+ const detailsRow = row.nextElementSibling;
803
+
804
+ let show = true;
805
+ if (filter === 'error' && errors === 0) show = false;
806
+ if (filter === 'warning' && warnings === 0) show = false;
807
+
808
+ if (show) {
809
+ row.style.display = '';
810
+ } else {
811
+ row.style.display = 'none';
812
+ if (detailsRow) {
813
+ detailsRow.style.display = 'none';
814
+ }
815
+ }
816
+ });
817
+ });
818
+ }
819
+ </script>`;
820
+ }
821
+
822
+ /**
823
+ * Escape HTML special characters
824
+ * @param {string} text - Text to escape
825
+ * @returns {string} Escaped text
826
+ */
827
+ function escapeHTML(text) {
828
+ if (!text) return '';
829
+ return String(text)
830
+ .replace(/&/g, '&amp;')
831
+ .replace(/</g, '&lt;')
832
+ .replace(/>/g, '&gt;')
833
+ .replace(/"/g, '&quot;')
834
+ .replace(/'/g, '&#039;');
835
+ }
836
+
837
+ module.exports = {
838
+ generateHTMLReport
839
+ };