renderpilot 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.
Files changed (34) hide show
  1. package/README.md +121 -0
  2. package/package.json +65 -0
  3. package/src/cli/commands/scan.js +83 -0
  4. package/src/cli/index.js +49 -0
  5. package/src/cli/options.js +18 -0
  6. package/src/config/ConfigLoader.js +102 -0
  7. package/src/config/DefaultConfig.js +33 -0
  8. package/src/index.js +34 -0
  9. package/src/parser/ASTUtils.js +241 -0
  10. package/src/parser/BabelParser.js +66 -0
  11. package/src/parser/ComponentDetector.js +53 -0
  12. package/src/reporters/HtmlReporter.js +42 -0
  13. package/src/reporters/JsonReporter.js +52 -0
  14. package/src/reporters/TerminalReporter.js +91 -0
  15. package/src/reporters/templates/report.html.js +655 -0
  16. package/src/rules/base/AbstractRule.js +136 -0
  17. package/src/rules/engine/RuleEngine.js +67 -0
  18. package/src/rules/implementations/R001_MissingKeyProp.js +97 -0
  19. package/src/rules/implementations/R002_InfiniteUseEffect.js +86 -0
  20. package/src/rules/implementations/R003_IncorrectDepsArray.js +112 -0
  21. package/src/rules/implementations/R004_LargeComponent.js +47 -0
  22. package/src/rules/implementations/R005_InlineCallback.js +58 -0
  23. package/src/rules/implementations/R006_HeavyRenderCalc.js +86 -0
  24. package/src/rules/implementations/R007_MemoCandidate.js +87 -0
  25. package/src/rules/implementations/R008_NestedComponent.js +65 -0
  26. package/src/rules/implementations/R009_UnusedState.js +62 -0
  27. package/src/rules/index.js +37 -0
  28. package/src/rules/registry/RuleRegistry.js +67 -0
  29. package/src/scanner/FileScanner.js +42 -0
  30. package/src/scanner/ProjectScanner.js +116 -0
  31. package/src/score/CategoryScorer.js +53 -0
  32. package/src/score/ScoreCalculator.js +97 -0
  33. package/src/score/ScoreWeights.js +33 -0
  34. package/src/types/index.js +176 -0
@@ -0,0 +1,655 @@
1
+ /**
2
+ * @file reporters/templates/report.html.js
3
+ * @description
4
+ * Standalone HTML template for the RenderPilot report.
5
+ * Uses vanilla JS and CSS to ensure zero runtime dependencies.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ /**
11
+ * Returns the full HTML string for the report.
12
+ * @param {string} jsonData - JSON string of the report data
13
+ * @returns {string}
14
+ */
15
+ function getHtmlTemplate(jsonData) {
16
+ return `<!DOCTYPE html>
17
+ <html lang="en">
18
+ <head>
19
+ <meta charset="UTF-8">
20
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
21
+ <title>RenderPilot Analysis Report</title>
22
+ <style>
23
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
24
+
25
+ :root {
26
+ --bg: #fafafa;
27
+ --surface: #ffffff;
28
+ --border: #eaeaea;
29
+ --text: #11181c;
30
+ --text-muted: #687076;
31
+ --primary: #0070f3;
32
+ --critical: #e5484d;
33
+ --warning: #f7ce00;
34
+ --warning-text: #705e00;
35
+ --info: #0090ff;
36
+ --success: #30a46c;
37
+ --code-bg: #f1f3f5;
38
+ }
39
+
40
+ @media (prefers-color-scheme: dark) {
41
+ :root {
42
+ --bg: #111111;
43
+ --surface: #1a1a1a;
44
+ --border: #333333;
45
+ --text: #ededed;
46
+ --text-muted: #a1a1a1;
47
+ --code-bg: #222222;
48
+ --warning-text: #f7ce00;
49
+ }
50
+ }
51
+
52
+ body {
53
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
54
+ background-color: var(--bg);
55
+ color: var(--text);
56
+ margin: 0;
57
+ padding: 0;
58
+ line-height: 1.6;
59
+ -webkit-font-smoothing: antialiased;
60
+ }
61
+
62
+ .app-container {
63
+ max-width: 1200px;
64
+ margin: 0 auto;
65
+ padding: 3rem 2rem;
66
+ }
67
+
68
+ header {
69
+ margin-bottom: 2rem;
70
+ border-bottom: 1px solid var(--border);
71
+ padding-bottom: 1rem;
72
+ }
73
+
74
+ .header-content {
75
+ display: flex;
76
+ justify-content: space-between;
77
+ align-items: flex-end;
78
+ }
79
+
80
+ h1 {
81
+ margin: 0;
82
+ font-size: 2rem;
83
+ font-weight: 700;
84
+ letter-spacing: -0.02em;
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 0.5rem;
88
+ }
89
+
90
+ .meta {
91
+ color: var(--text-muted);
92
+ font-size: 0.875rem;
93
+ margin-top: 0.5rem;
94
+ }
95
+
96
+ /* Top Stats Banner */
97
+ .top-banner {
98
+ background: var(--surface);
99
+ border: 1px solid var(--border);
100
+ border-radius: 12px;
101
+ padding: 2.5rem;
102
+ margin-bottom: 3rem;
103
+ display: flex;
104
+ flex-direction: row;
105
+ align-items: center;
106
+ gap: 4rem;
107
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
108
+ }
109
+
110
+ @media (max-width: 900px) {
111
+ .top-banner {
112
+ flex-direction: column;
113
+ gap: 2rem;
114
+ }
115
+ }
116
+
117
+ .score-section {
118
+ display: flex;
119
+ flex-direction: column;
120
+ align-items: center;
121
+ min-width: 250px;
122
+ }
123
+
124
+ .score-circle {
125
+ width: 140px;
126
+ height: 140px;
127
+ border-radius: 50%;
128
+ display: flex;
129
+ flex-direction: column;
130
+ align-items: center;
131
+ justify-content: center;
132
+ border: 8px solid var(--border);
133
+ font-size: 3.5rem;
134
+ font-weight: 700;
135
+ letter-spacing: -0.04em;
136
+ margin-bottom: 1rem;
137
+ }
138
+
139
+ .score-title {
140
+ text-align: center;
141
+ font-weight: 600;
142
+ font-size: 1.25rem;
143
+ margin-bottom: 0.5rem;
144
+ }
145
+
146
+ .top-rec {
147
+ font-size: 0.875rem;
148
+ color: var(--text-muted);
149
+ text-align: center;
150
+ padding: 0.5rem 1rem;
151
+ background: var(--code-bg);
152
+ border-radius: 8px;
153
+ }
154
+
155
+ .stats-section {
156
+ flex: 1;
157
+ display: grid;
158
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
159
+ gap: 2rem;
160
+ }
161
+
162
+ .stats-group h3 {
163
+ margin-top: 0;
164
+ font-size: 0.9rem;
165
+ text-transform: uppercase;
166
+ letter-spacing: 0.05em;
167
+ color: var(--text-muted);
168
+ margin-bottom: 1rem;
169
+ }
170
+
171
+ .stats-list {
172
+ list-style: none;
173
+ padding: 0;
174
+ margin: 0;
175
+ }
176
+
177
+ .stats-list li {
178
+ display: flex;
179
+ justify-content: space-between;
180
+ padding: 0.5rem 0;
181
+ border-bottom: 1px solid var(--border);
182
+ font-size: 0.95rem;
183
+ }
184
+
185
+ .stats-list li:last-child {
186
+ border-bottom: none;
187
+ }
188
+
189
+ .stat-label {
190
+ color: var(--text-muted);
191
+ font-weight: 500;
192
+ }
193
+
194
+ .stat-value {
195
+ font-weight: 600;
196
+ font-variant-numeric: tabular-nums;
197
+ }
198
+
199
+ /* Files Panel: Accordion Files */
200
+ .files-panel h2 {
201
+ margin-top: 0;
202
+ font-size: 1.5rem;
203
+ font-weight: 600;
204
+ letter-spacing: -0.02em;
205
+ margin-bottom: 1.5rem;
206
+ }
207
+
208
+ .file-accordion {
209
+ background: var(--surface);
210
+ border: 1px solid var(--border);
211
+ border-radius: 12px;
212
+ margin-bottom: 1rem;
213
+ overflow: hidden;
214
+ box-shadow: 0 1px 3px rgba(0,0,0,0.02);
215
+ transition: border-color 0.2s;
216
+ }
217
+
218
+ .file-accordion.all-fixed {
219
+ opacity: 0.6;
220
+ border-color: var(--success);
221
+ }
222
+
223
+ .file-header {
224
+ background: var(--surface);
225
+ padding: 1.25rem 1.5rem;
226
+ cursor: pointer;
227
+ display: flex;
228
+ align-items: center;
229
+ justify-content: space-between;
230
+ transition: background 0.2s;
231
+ }
232
+
233
+ .file-header:hover {
234
+ background: var(--code-bg);
235
+ }
236
+
237
+ .file-header-left {
238
+ display: flex;
239
+ align-items: center;
240
+ gap: 0.75rem;
241
+ font-family: 'JetBrains Mono', monospace;
242
+ font-size: 0.9rem;
243
+ font-weight: 500;
244
+ }
245
+
246
+ .file-header-right {
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 1rem;
250
+ }
251
+
252
+ .file-stats {
253
+ font-size: 0.8rem;
254
+ color: var(--text-muted);
255
+ display: flex;
256
+ gap: 0.5rem;
257
+ }
258
+
259
+ .file-stats span {
260
+ background: var(--code-bg);
261
+ padding: 0.2rem 0.6rem;
262
+ border-radius: 99px;
263
+ border: 1px solid var(--border);
264
+ }
265
+
266
+ .chevron {
267
+ transition: transform 0.3s ease;
268
+ color: var(--text-muted);
269
+ }
270
+
271
+ .file-accordion.open .chevron {
272
+ transform: rotate(180deg);
273
+ }
274
+
275
+ .file-content {
276
+ display: none;
277
+ border-top: 1px solid var(--border);
278
+ background: var(--bg);
279
+ }
280
+
281
+ .file-accordion.open .file-content {
282
+ display: block;
283
+ }
284
+
285
+ /* Violations */
286
+ .violation-item {
287
+ padding: 1.5rem;
288
+ border-bottom: 1px solid var(--border);
289
+ transition: opacity 0.3s ease;
290
+ position: relative;
291
+ }
292
+
293
+ .violation-item.fixed {
294
+ opacity: 0.4;
295
+ }
296
+
297
+ .violation-item.fixed::after {
298
+ content: 'FIXED';
299
+ position: absolute;
300
+ top: 1.5rem;
301
+ right: 1.5rem;
302
+ background: var(--success);
303
+ color: white;
304
+ padding: 0.25rem 0.75rem;
305
+ border-radius: 99px;
306
+ font-size: 0.75rem;
307
+ font-weight: 700;
308
+ letter-spacing: 0.05em;
309
+ }
310
+
311
+ .violation-item:last-child {
312
+ border-bottom: none;
313
+ }
314
+
315
+ .v-header {
316
+ display: flex;
317
+ align-items: center;
318
+ gap: 0.75rem;
319
+ margin-bottom: 1rem;
320
+ }
321
+
322
+ .badge {
323
+ padding: 0.2rem 0.6rem;
324
+ border-radius: 9999px;
325
+ font-size: 0.7rem;
326
+ font-weight: 700;
327
+ text-transform: uppercase;
328
+ letter-spacing: 0.05em;
329
+ }
330
+
331
+ .badge.critical { background: rgba(229, 72, 77, 0.1); color: var(--critical); border: 1px solid rgba(229, 72, 77, 0.2); }
332
+ .badge.warning { background: rgba(247, 206, 0, 0.1); color: var(--warning-text); border: 1px solid rgba(247, 206, 0, 0.2); }
333
+ .badge.info { background: rgba(0, 144, 255, 0.1); color: var(--info); border: 1px solid rgba(0, 144, 255, 0.2); }
334
+
335
+ .v-title {
336
+ font-weight: 600;
337
+ font-size: 0.95rem;
338
+ }
339
+
340
+ .v-line {
341
+ color: var(--text-muted);
342
+ font-family: 'JetBrains Mono', monospace;
343
+ font-size: 0.85rem;
344
+ background: var(--surface);
345
+ border: 1px solid var(--border);
346
+ padding: 0.2rem 0.6rem;
347
+ border-radius: 6px;
348
+ }
349
+
350
+ .v-message {
351
+ font-size: 0.95rem;
352
+ margin-bottom: 1rem;
353
+ color: var(--text);
354
+ }
355
+
356
+ .code-block-wrapper {
357
+ position: relative;
358
+ margin-bottom: 1rem;
359
+ }
360
+
361
+ pre {
362
+ background: var(--code-bg);
363
+ padding: 1rem;
364
+ border-radius: 8px;
365
+ overflow-x: auto;
366
+ font-family: 'JetBrains Mono', monospace;
367
+ font-size: 0.875rem;
368
+ margin: 0;
369
+ border: 1px solid var(--border);
370
+ color: var(--text);
371
+ }
372
+
373
+ .v-suggestion {
374
+ display: flex;
375
+ gap: 0.75rem;
376
+ padding: 1rem;
377
+ background: rgba(48, 164, 108, 0.05);
378
+ border-left: 4px solid var(--success);
379
+ border-radius: 0 8px 8px 0;
380
+ font-size: 0.9rem;
381
+ color: var(--text);
382
+ margin-bottom: 1.25rem;
383
+ }
384
+
385
+ .action-row {
386
+ display: flex;
387
+ justify-content: flex-start;
388
+ }
389
+
390
+ .btn-fix {
391
+ display: flex;
392
+ align-items: center;
393
+ gap: 0.5rem;
394
+ background: var(--surface);
395
+ border: 1px solid var(--border);
396
+ color: var(--text);
397
+ padding: 0.5rem 1rem;
398
+ border-radius: 6px;
399
+ font-size: 0.875rem;
400
+ font-weight: 500;
401
+ cursor: pointer;
402
+ transition: all 0.2s;
403
+ }
404
+
405
+ .btn-fix:hover {
406
+ background: var(--code-bg);
407
+ border-color: var(--success);
408
+ color: var(--success);
409
+ }
410
+
411
+ .violation-item.fixed .btn-fix {
412
+ background: var(--success);
413
+ color: white;
414
+ border-color: var(--success);
415
+ }
416
+ </style>
417
+ </head>
418
+ <body>
419
+ <div class="app-container">
420
+ <header>
421
+ <div class="header-content">
422
+ <div>
423
+ <h1>✈ RenderPilot Report</h1>
424
+ <div class="meta" id="scan-meta">Loading scan data...</div>
425
+ </div>
426
+ </div>
427
+ </header>
428
+
429
+ <div class="top-banner">
430
+ <div class="score-section">
431
+ <div class="score-circle" id="overall-score">--</div>
432
+ <div class="score-title" id="score-label">--</div>
433
+ <div class="top-rec" id="top-rec"></div>
434
+ </div>
435
+
436
+ <div class="stats-section">
437
+ <div class="stats-group">
438
+ <h3>Issues Summary</h3>
439
+ <ul class="stats-list" id="stats-list">
440
+ <!-- Stats injected here -->
441
+ </ul>
442
+ </div>
443
+
444
+ <div class="stats-group">
445
+ <h3>Category Grades</h3>
446
+ <ul class="stats-list" id="categories-list">
447
+ <!-- Categories injected here -->
448
+ </ul>
449
+ </div>
450
+ </div>
451
+ </div>
452
+
453
+ <main class="files-panel">
454
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
455
+ <h2 style="margin: 0;">Detected Violations</h2>
456
+ <button onclick="toggleAllFiles()" style="background: var(--surface); border: 1px solid var(--border); padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; color: var(--text);">Expand / Collapse All</button>
457
+ </div>
458
+
459
+ <div id="violations-container">
460
+ <!-- Violations injected here -->
461
+ </div>
462
+ </main>
463
+ </div>
464
+
465
+ <script>
466
+ const reportData = ${jsonData};
467
+
468
+ // Meta Data
469
+ document.getElementById('scan-meta').innerText =
470
+ \`Scanned \${reportData.scan.totalFiles} files in \${reportData.scan.durationMs}ms — \${new Date(reportData.scan.completedAt).toLocaleString()}\`;
471
+
472
+ // Overall Score
473
+ const scoreCircle = document.getElementById('overall-score');
474
+ scoreCircle.innerText = reportData.score.overall;
475
+
476
+ let color = 'var(--success)';
477
+ if (reportData.score.grade === 'C' || reportData.score.grade === 'D') color = 'var(--warning-text)';
478
+ if (reportData.score.grade === 'F') color = 'var(--critical)';
479
+ scoreCircle.style.borderColor = color;
480
+ scoreCircle.style.color = color;
481
+
482
+ document.getElementById('score-label').innerText = \`Grade \${reportData.score.grade} • \${reportData.score.label}\`;
483
+ document.getElementById('top-rec').innerText = reportData.score.topRecommendation;
484
+
485
+ // Summary Stats
486
+ const statsList = document.getElementById('stats-list');
487
+ statsList.innerHTML = \`
488
+ <li>
489
+ <span class="stat-label">Critical Issues</span>
490
+ <span class="stat-value" style="color: var(--critical);">\${reportData.scan.violationsBySeverity.critical || 0}</span>
491
+ </li>
492
+ <li>
493
+ <span class="stat-label">Warnings</span>
494
+ <span class="stat-value" style="color: var(--warning-text);">\${reportData.scan.violationsBySeverity.warning || 0}</span>
495
+ </li>
496
+ <li>
497
+ <span class="stat-label">Info / Tips</span>
498
+ <span class="stat-value" style="color: var(--info);">\${reportData.scan.violationsBySeverity.info || 0}</span>
499
+ </li>
500
+ <li>
501
+ <span class="stat-label">Total Violations</span>
502
+ <span class="stat-value">\${reportData.scan.totalViolations}</span>
503
+ </li>
504
+ \`;
505
+
506
+ // Category Grades
507
+ const catList = document.getElementById('categories-list');
508
+ catList.innerHTML = reportData.score.categories.map(c => {
509
+ let catColor = 'var(--text)';
510
+ if (c.grade === 'A' || c.grade === 'B') catColor = 'var(--success)';
511
+ if (c.grade === 'C' || c.grade === 'D') catColor = 'var(--warning-text)';
512
+ if (c.grade === 'F') catColor = 'var(--critical)';
513
+
514
+ return \`
515
+ <li>
516
+ <span class="stat-label">\${c.category.charAt(0).toUpperCase() + c.category.slice(1)}</span>
517
+ <span class="stat-value" style="color: \${catColor};">\${c.score} (\${c.grade})</span>
518
+ </li>
519
+ \`}).join('');
520
+
521
+ // Populate Files
522
+ const container = document.getElementById('violations-container');
523
+ const fileGroups = {};
524
+
525
+ reportData.scan.allViolations.forEach((v, index) => {
526
+ // Attach a unique ID to each violation for the checkbox logic
527
+ v.uniqueId = 'v_' + index;
528
+ if (!fileGroups[v.filePath]) fileGroups[v.filePath] = [];
529
+ fileGroups[v.filePath].push(v);
530
+ });
531
+
532
+ if (Object.keys(fileGroups).length === 0) {
533
+ container.innerHTML = \`
534
+ <div class="file-accordion" style="padding: 4rem 2rem; text-align: center; color: var(--text-muted);">
535
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-bottom: 1rem; color: var(--success);">
536
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
537
+ <polyline points="22 4 12 14.01 9 11.01"></polyline>
538
+ </svg>
539
+ <h3 style="margin:0; color: var(--text);">No violations found!</h3>
540
+ <p>Your codebase is perfectly optimized according to RenderPilot.</p>
541
+ </div>\`;
542
+ } else {
543
+ let html = '';
544
+ let fileIndex = 0;
545
+
546
+ for (const [filePath, violations] of Object.entries(fileGroups)) {
547
+ // Extract a clean relative path
548
+ const pathParts = filePath.split(/[\\\\/]/);
549
+ const relPath = pathParts.slice(Math.max(pathParts.length - 3, 0)).join('/');
550
+
551
+ let vHtml = '';
552
+ let criticalCount = 0;
553
+ let warningCount = 0;
554
+
555
+ violations.forEach(v => {
556
+ if (v.severity === 'critical') criticalCount++;
557
+ if (v.severity === 'warning') warningCount++;
558
+
559
+ vHtml += \`
560
+ <div class="violation-item" id="\${v.uniqueId}">
561
+ <div class="v-header">
562
+ <span class="badge \${v.severity}">\${v.severity}</span>
563
+ <span class="v-title">\${v.ruleName}</span>
564
+ <span class="v-line">Line \${v.line}</span>
565
+ </div>
566
+ <div class="v-message">\${v.message}</div>
567
+ \${v.codeSnippet ? \`<div class="code-block-wrapper"><pre><code>\${v.codeSnippet.replace(/</g, '&lt;')}</code></pre></div>\` : ''}
568
+ <div class="v-suggestion">
569
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
570
+ <span>\${v.suggestion}</span>
571
+ </div>
572
+ <div class="action-row">
573
+ <button class="btn-fix" onclick="toggleFix('\${v.uniqueId}', '\${filePath}')">
574
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>
575
+ <span class="btn-text">Mark as Fixed</span>
576
+ </button>
577
+ </div>
578
+ </div>
579
+ \`;
580
+ });
581
+
582
+ // NOTE: We omit 'open' class so it is closed by default
583
+ html += \`
584
+ <div class="file-accordion" id="file_\${fileIndex}" data-filepath="\${filePath}">
585
+ <div class="file-header" onclick="toggleAccordion('file_\${fileIndex}')">
586
+ <div class="file-header-left">
587
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: var(--primary);"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
588
+ \${relPath}
589
+ </div>
590
+ <div class="file-header-right">
591
+ <div class="file-stats">
592
+ \${criticalCount > 0 ? \`<span style="color: var(--critical); border-color: rgba(229, 72, 77, 0.3);">\${criticalCount} Crit</span>\` : ''}
593
+ \${warningCount > 0 ? \`<span style="color: var(--warning-text); border-color: rgba(247, 206, 0, 0.3);">\${warningCount} Warn</span>\` : ''}
594
+ <span>\${violations.length} Total</span>
595
+ </div>
596
+ <svg class="chevron" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>
597
+ </div>
598
+ </div>
599
+ <div class="file-content">
600
+ \${vHtml}
601
+ </div>
602
+ </div>
603
+ \`;
604
+
605
+ fileIndex++;
606
+ }
607
+ container.innerHTML = html;
608
+ }
609
+
610
+ // Interactivity Functions
611
+ function toggleAccordion(id) {
612
+ const el = document.getElementById(id);
613
+ if (el.classList.contains('open')) {
614
+ el.classList.remove('open');
615
+ } else {
616
+ el.classList.add('open');
617
+ }
618
+ }
619
+
620
+ let allOpen = false;
621
+ function toggleAllFiles() {
622
+ const accordions = document.querySelectorAll('.file-accordion');
623
+ allOpen = !allOpen;
624
+ accordions.forEach(acc => {
625
+ if (allOpen) acc.classList.add('open');
626
+ else acc.classList.remove('open');
627
+ });
628
+ }
629
+
630
+ function toggleFix(violationId, filePath) {
631
+ const vEl = document.getElementById(violationId);
632
+ const isFixed = vEl.classList.toggle('fixed');
633
+
634
+ const btnText = vEl.querySelector('.btn-text');
635
+ btnText.innerText = isFixed ? 'Unmark Fixed' : 'Mark as Fixed';
636
+
637
+ // Check if all violations in this file are fixed
638
+ const fileAccordion = vEl.closest('.file-accordion');
639
+ const allViolations = fileAccordion.querySelectorAll('.violation-item');
640
+ const fixedViolations = fileAccordion.querySelectorAll('.violation-item.fixed');
641
+
642
+ if (allViolations.length === fixedViolations.length) {
643
+ fileAccordion.classList.add('all-fixed');
644
+ // Auto close it when done
645
+ fileAccordion.classList.remove('open');
646
+ } else {
647
+ fileAccordion.classList.remove('all-fixed');
648
+ }
649
+ }
650
+ </script>
651
+ </body>
652
+ </html>`;
653
+ }
654
+
655
+ module.exports = { getHtmlTemplate };