@dallask/a11y-mcp-srv 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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +9 -0
  3. package/README.md +1328 -0
  4. package/bin/server.js +8 -0
  5. package/dist/core/accessibility-runner.d.ts +123 -0
  6. package/dist/core/accessibility-runner.d.ts.map +1 -0
  7. package/dist/core/accessibility-runner.js +465 -0
  8. package/dist/core/accessibility-runner.js.map +1 -0
  9. package/dist/core/basic-auth.d.ts +35 -0
  10. package/dist/core/basic-auth.d.ts.map +1 -0
  11. package/dist/core/basic-auth.js +52 -0
  12. package/dist/core/basic-auth.js.map +1 -0
  13. package/dist/core/config.d.ts +44 -0
  14. package/dist/core/config.d.ts.map +1 -0
  15. package/dist/core/config.js +163 -0
  16. package/dist/core/config.js.map +1 -0
  17. package/dist/core/error-handler.d.ts +66 -0
  18. package/dist/core/error-handler.d.ts.map +1 -0
  19. package/dist/core/error-handler.js +305 -0
  20. package/dist/core/error-handler.js.map +1 -0
  21. package/dist/core/normalize-audit-result.d.ts +18 -0
  22. package/dist/core/normalize-audit-result.d.ts.map +1 -0
  23. package/dist/core/normalize-audit-result.js +118 -0
  24. package/dist/core/normalize-audit-result.js.map +1 -0
  25. package/dist/core/playwright-bootstrap.d.ts +21 -0
  26. package/dist/core/playwright-bootstrap.d.ts.map +1 -0
  27. package/dist/core/playwright-bootstrap.js +144 -0
  28. package/dist/core/playwright-bootstrap.js.map +1 -0
  29. package/dist/core/progress-streamer.d.ts +44 -0
  30. package/dist/core/progress-streamer.d.ts.map +1 -0
  31. package/dist/core/progress-streamer.js +160 -0
  32. package/dist/core/progress-streamer.js.map +1 -0
  33. package/dist/core/result-processor.d.ts +86 -0
  34. package/dist/core/result-processor.d.ts.map +1 -0
  35. package/dist/core/result-processor.js +475 -0
  36. package/dist/core/result-processor.js.map +1 -0
  37. package/dist/core/session-manager.d.ts +73 -0
  38. package/dist/core/session-manager.d.ts.map +1 -0
  39. package/dist/core/session-manager.js +243 -0
  40. package/dist/core/session-manager.js.map +1 -0
  41. package/dist/server.d.ts +10 -0
  42. package/dist/server.d.ts.map +1 -0
  43. package/dist/server.js +1439 -0
  44. package/dist/server.js.map +1 -0
  45. package/dist/tools/aggregate.d.ts +26 -0
  46. package/dist/tools/aggregate.d.ts.map +1 -0
  47. package/dist/tools/aggregate.js +340 -0
  48. package/dist/tools/aggregate.js.map +1 -0
  49. package/dist/tools/analysis.d.ts +68 -0
  50. package/dist/tools/analysis.d.ts.map +1 -0
  51. package/dist/tools/analysis.js +1199 -0
  52. package/dist/tools/analysis.js.map +1 -0
  53. package/dist/tools/audit.d.ts +38 -0
  54. package/dist/tools/audit.d.ts.map +1 -0
  55. package/dist/tools/audit.js +472 -0
  56. package/dist/tools/audit.js.map +1 -0
  57. package/dist/tools/comparison.d.ts +27 -0
  58. package/dist/tools/comparison.d.ts.map +1 -0
  59. package/dist/tools/comparison.js +499 -0
  60. package/dist/tools/comparison.js.map +1 -0
  61. package/dist/tools/export.d.ts +43 -0
  62. package/dist/tools/export.d.ts.map +1 -0
  63. package/dist/tools/export.js +746 -0
  64. package/dist/tools/export.js.map +1 -0
  65. package/dist/tools/filter.d.ts +26 -0
  66. package/dist/tools/filter.d.ts.map +1 -0
  67. package/dist/tools/filter.js +244 -0
  68. package/dist/tools/filter.js.map +1 -0
  69. package/dist/tools/session.d.ts +26 -0
  70. package/dist/tools/session.d.ts.map +1 -0
  71. package/dist/tools/session.js +228 -0
  72. package/dist/tools/session.js.map +1 -0
  73. package/dist/tools/visualize.d.ts +26 -0
  74. package/dist/tools/visualize.d.ts.map +1 -0
  75. package/dist/tools/visualize.js +942 -0
  76. package/dist/tools/visualize.js.map +1 -0
  77. package/dist/types/index.d.ts +792 -0
  78. package/dist/types/index.d.ts.map +1 -0
  79. package/dist/types/index.js +24 -0
  80. package/dist/types/index.js.map +1 -0
  81. package/package.json +69 -0
@@ -0,0 +1,746 @@
1
+ /**
2
+ * Export tools for audit results
3
+ * Implements: export_to_csv, export_to_excel, export_to_json, export_to_html_report
4
+ */
5
+ import { resolveBasicAuth } from '../core/basic-auth.js';
6
+ import { normalizeAuditResult } from '../core/normalize-audit-result.js';
7
+ import { auditUrl } from './audit.js';
8
+ /**
9
+ * Escape CSV field - handles quotes, commas, and newlines
10
+ */
11
+ function escapeCSV(field) {
12
+ if (field === null || field === undefined) {
13
+ return '""';
14
+ }
15
+ const stringField = String(field);
16
+ // If the field contains quotes, commas, or newlines, wrap it in quotes and escape internal quotes
17
+ if (stringField.includes('"') ||
18
+ stringField.includes(',') ||
19
+ stringField.includes('\n')) {
20
+ return `"${stringField.replace(/"/g, '""')}"`;
21
+ }
22
+ return stringField;
23
+ }
24
+ /**
25
+ * Generate metadata section for CSV
26
+ */
27
+ function generateMetadataSection(result) {
28
+ const metadata = result.metadata;
29
+ if (!metadata) {
30
+ return '';
31
+ }
32
+ const orientationInfo = metadata.testEnvironment.orientationAngle !== undefined
33
+ ? `${metadata.testEnvironment.orientationType} (${metadata.testEnvironment.orientationAngle}°)`
34
+ : metadata.testEnvironment.orientationType;
35
+ return [
36
+ 'Test Information',
37
+ `Test Engine,${escapeCSV(`${metadata.testEngine.name} v${metadata.testEngine.version}`)}`,
38
+ `Test Runner,${escapeCSV(metadata.testRunner.name)}`,
39
+ `Test URL,${escapeCSV(metadata.url)}`,
40
+ `Timestamp,${escapeCSV(metadata.timestamp)}`,
41
+ '',
42
+ 'Environment Information',
43
+ `User Agent,${escapeCSV(metadata.testEnvironment.userAgent)}`,
44
+ `Window Size,${escapeCSV(`${metadata.testEnvironment.windowWidth}x${metadata.testEnvironment.windowHeight}`)}`,
45
+ `Orientation,${escapeCSV(orientationInfo || 'N/A')}`,
46
+ '',
47
+ 'Summary',
48
+ `Total Issues,${result.summary.totalIssues}`,
49
+ `Accessibility Score,${result.summary.score}`,
50
+ `WCAG Level A Compliance,${result.summary.wcagCompliance.A}%`,
51
+ `WCAG Level AA Compliance,${result.summary.wcagCompliance.AA}%`,
52
+ `WCAG Level AAA Compliance,${result.summary.wcagCompliance.AAA}%`,
53
+ '',
54
+ 'Test Results',
55
+ '',
56
+ ].join('\n');
57
+ }
58
+ /**
59
+ * export_to_csv - Export audit results to CSV format
60
+ *
61
+ * Exports audit results to CSV format for spreadsheet analysis, including
62
+ * metadata section and detailed violation rows.
63
+ *
64
+ * @param input - Export configuration
65
+ * @returns CSV content as string
66
+ */
67
+ export async function exportToCsv(input) {
68
+ const { results, includeMetadata = true, includeViolations = true, format = 'standard', basicAuthUsername, basicAuthPassword, } = input;
69
+ let auditResult;
70
+ // If input is a URL string, run an audit first (with optional Basic Auth, same as audit_url)
71
+ if (typeof results === 'string') {
72
+ const { urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p } = resolveBasicAuth(results, basicAuthUsername, basicAuthPassword);
73
+ auditResult = await auditUrl({ url: urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p });
74
+ }
75
+ else {
76
+ const normalized = normalizeAuditResult(results);
77
+ auditResult = normalized ?? results;
78
+ }
79
+ const issues = Array.isArray(auditResult?.prioritizedIssues) ? auditResult.prioritizedIssues : [];
80
+ const csvRows = [];
81
+ // Add metadata section if requested
82
+ if (includeMetadata) {
83
+ csvRows.push(generateMetadataSection(auditResult));
84
+ }
85
+ // Add violation rows if requested
86
+ if (includeViolations) {
87
+ // Determine headers based on format
88
+ let headers;
89
+ if (format === 'minimal') {
90
+ headers = ['Rule ID', 'Impact', 'Description', 'Element'];
91
+ }
92
+ else if (format === 'detailed') {
93
+ headers = [
94
+ 'Rule ID',
95
+ 'Category',
96
+ 'Impact',
97
+ 'WCAG Level',
98
+ 'Description',
99
+ 'Element',
100
+ 'XPath',
101
+ 'Class selector',
102
+ 'User Impact',
103
+ 'Fix Explanation',
104
+ 'Current Code',
105
+ 'Suggested Code',
106
+ 'Priority',
107
+ ];
108
+ }
109
+ else {
110
+ // standard format
111
+ headers = [
112
+ 'Rule ID',
113
+ 'Category',
114
+ 'Impact',
115
+ 'WCAG Level',
116
+ 'Description',
117
+ 'Element',
118
+ 'XPath',
119
+ 'Class selector',
120
+ 'User Impact',
121
+ 'Fix Explanation',
122
+ ];
123
+ }
124
+ // Add header row
125
+ csvRows.push(headers.map(escapeCSV).join(','));
126
+ // Add violation rows
127
+ issues.forEach((issue) => {
128
+ const row = [];
129
+ if (format === 'minimal') {
130
+ row.push(issue.ruleId, issue.impact, issue.description, issue.element);
131
+ }
132
+ else if (format === 'detailed') {
133
+ row.push(issue.ruleId, issue.category || 'unknown', issue.impact, issue.wcagLevel, issue.description, issue.element, issue.xpath, issue.classSelector ?? '', issue.userImpact, issue.fix.explanation, issue.fix.current, issue.fix.suggested, String(issue.priority));
134
+ }
135
+ else {
136
+ // standard format
137
+ row.push(issue.ruleId, issue.category || 'unknown', issue.impact, issue.wcagLevel, issue.description, issue.element, issue.xpath, issue.classSelector ?? '', issue.userImpact, issue.fix.explanation);
138
+ }
139
+ csvRows.push(row.map(escapeCSV).join(','));
140
+ });
141
+ }
142
+ const csvContent = csvRows.join('\n');
143
+ return {
144
+ csv: csvContent,
145
+ format,
146
+ totalIssues: auditResult.summary.totalIssues,
147
+ includeMetadata,
148
+ includeViolations,
149
+ };
150
+ }
151
+ /**
152
+ * export_to_excel - Export audit results to Excel/XLSX format
153
+ *
154
+ * Exports audit results to Excel format with formatting. Requires xlsx package.
155
+ *
156
+ * @param input - Export configuration
157
+ * @returns Excel file content (base64 encoded)
158
+ */
159
+ export async function exportToExcel(input) {
160
+ const { results, includeCharts = false, formatting = true, basicAuthUsername, basicAuthPassword, } = input;
161
+ let auditResult;
162
+ // If input is a URL string, run an audit first (with optional Basic Auth, same as audit_url)
163
+ if (typeof results === 'string') {
164
+ const { urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p } = resolveBasicAuth(results, basicAuthUsername, basicAuthPassword);
165
+ auditResult = await auditUrl({ url: urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p });
166
+ }
167
+ else {
168
+ const normalized = normalizeAuditResult(results);
169
+ auditResult = normalized ?? results;
170
+ }
171
+ const issues = Array.isArray(auditResult?.prioritizedIssues) ? auditResult.prioritizedIssues : [];
172
+ // Try to import xlsx, but handle gracefully if not available
173
+ let XLSX;
174
+ try {
175
+ XLSX = await import('xlsx');
176
+ }
177
+ catch (error) {
178
+ throw new Error('xlsx package is required for Excel export. Install it with: npm install xlsx');
179
+ }
180
+ // Create workbook
181
+ const workbook = XLSX.utils.book_new();
182
+ // Create summary sheet
183
+ const summaryData = [
184
+ ['Accessibility Audit Summary'],
185
+ [],
186
+ ['Total Issues', auditResult.summary.totalIssues],
187
+ ['Accessibility Score', auditResult.summary.score],
188
+ ['WCAG Level A Compliance', `${auditResult.summary.wcagCompliance.A}%`],
189
+ ['WCAG Level AA Compliance', `${auditResult.summary.wcagCompliance.AA}%`],
190
+ ['WCAG Level AAA Compliance', `${auditResult.summary.wcagCompliance.AAA}%`],
191
+ [],
192
+ ['Issues by Category'],
193
+ ];
194
+ // Add category breakdown
195
+ Object.entries(auditResult.summary.byCategory).forEach(([category, count]) => {
196
+ summaryData.push([category, count]);
197
+ });
198
+ summaryData.push([]);
199
+ summaryData.push(['Issues by Impact']);
200
+ // Add impact breakdown
201
+ Object.entries(auditResult.summary.byImpact).forEach(([impact, count]) => {
202
+ summaryData.push([impact, count]);
203
+ });
204
+ // Add metadata if available
205
+ if (auditResult.metadata) {
206
+ summaryData.push([]);
207
+ summaryData.push(['Test Information']);
208
+ summaryData.push(['Test Engine', `${auditResult.metadata.testEngine.name} v${auditResult.metadata.testEngine.version}`]);
209
+ summaryData.push(['Test Runner', auditResult.metadata.testRunner.name]);
210
+ summaryData.push(['Test URL', auditResult.metadata.url]);
211
+ summaryData.push(['Timestamp', auditResult.metadata.timestamp]);
212
+ }
213
+ const summarySheet = XLSX.utils.aoa_to_sheet(summaryData);
214
+ if (formatting) {
215
+ // Set column widths
216
+ summarySheet['!cols'] = [{ wch: 30 }, { wch: 20 }];
217
+ }
218
+ XLSX.utils.book_append_sheet(workbook, summarySheet, 'Summary');
219
+ // Create violations sheet (WCAG Level = full label e.g. "WCAG 2.2 AA"; Element = tag name; Class selector after XPath)
220
+ const violationsData = [
221
+ [
222
+ 'Rule ID',
223
+ 'Category',
224
+ 'Impact',
225
+ 'WCAG Level',
226
+ 'Description',
227
+ 'Element',
228
+ 'XPath',
229
+ 'Class selector',
230
+ 'User Impact',
231
+ 'Fix Explanation',
232
+ ],
233
+ ];
234
+ issues.forEach((issue) => {
235
+ violationsData.push([
236
+ issue.ruleId,
237
+ issue.category || 'unknown',
238
+ issue.impact,
239
+ issue.wcagLevel,
240
+ issue.description,
241
+ issue.element,
242
+ issue.xpath,
243
+ issue.classSelector ?? '',
244
+ issue.userImpact,
245
+ issue.fix.explanation,
246
+ ]);
247
+ });
248
+ const violationsSheet = XLSX.utils.aoa_to_sheet(violationsData);
249
+ if (formatting) {
250
+ // Set column widths
251
+ violationsSheet['!cols'] = [
252
+ { wch: 20 }, // Rule ID
253
+ { wch: 15 }, // Category
254
+ { wch: 12 }, // Impact
255
+ { wch: 14 }, // WCAG Level
256
+ { wch: 40 }, // Description
257
+ { wch: 30 }, // Element
258
+ { wch: 50 }, // XPath
259
+ { wch: 28 }, // Class selector
260
+ { wch: 50 }, // User Impact
261
+ { wch: 50 }, // Fix Explanation
262
+ ];
263
+ }
264
+ XLSX.utils.book_append_sheet(workbook, violationsSheet, 'Violations');
265
+ // Generate Excel file as base64
266
+ const excelBuffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
267
+ const base64Content = excelBuffer.toString('base64');
268
+ return {
269
+ excel: base64Content,
270
+ format: 'xlsx',
271
+ totalIssues: auditResult.summary.totalIssues,
272
+ includeCharts,
273
+ formatting,
274
+ };
275
+ }
276
+ /**
277
+ * export_to_json - Export audit results as structured JSON
278
+ *
279
+ * Exports audit results as structured JSON with optional raw results.
280
+ *
281
+ * @param input - Export configuration
282
+ * @returns JSON string
283
+ */
284
+ export async function exportToJson(input) {
285
+ const { results, pretty = true, includeRaw = false, basicAuthUsername, basicAuthPassword, } = input;
286
+ let auditResult;
287
+ // If input is a URL string, run an audit first (with optional Basic Auth, same as audit_url)
288
+ if (typeof results === 'string') {
289
+ const { urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p } = resolveBasicAuth(results, basicAuthUsername, basicAuthPassword);
290
+ auditResult = await auditUrl({ url: urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p });
291
+ }
292
+ else {
293
+ const normalized = normalizeAuditResult(results);
294
+ auditResult = normalized ?? results;
295
+ }
296
+ // Prepare export data
297
+ const exportData = {
298
+ summary: auditResult.summary,
299
+ prioritizedIssues: auditResult.prioritizedIssues,
300
+ quickWins: auditResult.quickWins,
301
+ criticalBlockers: auditResult.criticalBlockers,
302
+ conversationalSummary: auditResult.conversationalSummary,
303
+ metadata: auditResult.metadata,
304
+ };
305
+ // Include raw results if requested
306
+ if (includeRaw && auditResult.rawResults) {
307
+ exportData.rawResults = auditResult.rawResults;
308
+ }
309
+ // Generate JSON
310
+ const jsonContent = pretty
311
+ ? JSON.stringify(exportData, null, 2)
312
+ : JSON.stringify(exportData);
313
+ return {
314
+ json: jsonContent,
315
+ pretty,
316
+ includeRaw,
317
+ totalIssues: auditResult.summary.totalIssues,
318
+ };
319
+ }
320
+ /**
321
+ * Escape HTML special characters
322
+ */
323
+ function escapeHtml(text) {
324
+ return text
325
+ .replace(/&/g, '&')
326
+ .replace(/</g, '&lt;')
327
+ .replace(/>/g, '&gt;')
328
+ .replace(/"/g, '&quot;')
329
+ .replace(/'/g, '&#039;');
330
+ }
331
+ /**
332
+ * Generate HTML report template
333
+ */
334
+ function generateHtmlReport(result, template, includeCharts) {
335
+ const metadata = result.metadata;
336
+ const summary = result.summary;
337
+ const issues = Array.isArray(result?.prioritizedIssues) ? result.prioritizedIssues : [];
338
+ // Generate chart data if requested
339
+ let chartScript = '';
340
+ if (includeCharts) {
341
+ chartScript = `
342
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
343
+ <script>
344
+ document.addEventListener('DOMContentLoaded', function() {
345
+ // Category breakdown chart
346
+ const categoryCtx = document.getElementById('categoryChart');
347
+ if (categoryCtx) {
348
+ new Chart(categoryCtx, {
349
+ type: 'bar',
350
+ data: {
351
+ labels: ${JSON.stringify(Object.keys(summary.byCategory))},
352
+ datasets: [{
353
+ label: 'Issues by Category',
354
+ data: ${JSON.stringify(Object.values(summary.byCategory))},
355
+ backgroundColor: 'rgba(54, 162, 235, 0.5)',
356
+ borderColor: 'rgba(54, 162, 235, 1)',
357
+ borderWidth: 1
358
+ }]
359
+ },
360
+ options: {
361
+ responsive: true,
362
+ scales: {
363
+ y: { beginAtZero: true }
364
+ }
365
+ }
366
+ });
367
+ }
368
+
369
+ // Impact breakdown chart
370
+ const impactCtx = document.getElementById('impactChart');
371
+ if (impactCtx) {
372
+ new Chart(impactCtx, {
373
+ type: 'doughnut',
374
+ data: {
375
+ labels: ${JSON.stringify(Object.keys(summary.byImpact))},
376
+ datasets: [{
377
+ data: ${JSON.stringify(Object.values(summary.byImpact))},
378
+ backgroundColor: [
379
+ 'rgba(255, 99, 132, 0.5)',
380
+ 'rgba(255, 159, 64, 0.5)',
381
+ 'rgba(255, 205, 86, 0.5)',
382
+ 'rgba(75, 192, 192, 0.5)'
383
+ ]
384
+ }]
385
+ },
386
+ options: {
387
+ responsive: true
388
+ }
389
+ });
390
+ }
391
+ });
392
+ </script>
393
+ `;
394
+ }
395
+ // Generate issues table rows
396
+ const issuesRows = issues
397
+ .map((issue) => `
398
+ <tr>
399
+ <td>${escapeHtml(issue.ruleId)}</td>
400
+ <td>${escapeHtml(issue.category || 'unknown')}</td>
401
+ <td><span class="impact-${issue.impact}">${escapeHtml(issue.impact)}</span></td>
402
+ <td>${escapeHtml(issue.wcagLevel)}</td>
403
+ <td>${escapeHtml(issue.description)}</td>
404
+ <td><code>${escapeHtml(issue.element)}</code></td>
405
+ <td>${escapeHtml(issue.userImpact)}</td>
406
+ </tr>
407
+ `)
408
+ .join('');
409
+ // Template-specific content
410
+ let mainContent = '';
411
+ if (template === 'minimal') {
412
+ mainContent = `
413
+ <div class="summary">
414
+ <h2>Summary</h2>
415
+ <p>Total Issues: <strong>${summary.totalIssues}</strong></p>
416
+ <p>Accessibility Score: <strong>${summary.score}/100</strong></p>
417
+ </div>
418
+ <div class="issues">
419
+ <h2>Issues</h2>
420
+ <table>
421
+ <thead>
422
+ <tr>
423
+ <th>Rule ID</th>
424
+ <th>Impact</th>
425
+ <th>Description</th>
426
+ <th>Element</th>
427
+ </tr>
428
+ </thead>
429
+ <tbody>
430
+ ${issues
431
+ .map((issue) => `
432
+ <tr>
433
+ <td>${escapeHtml(issue.ruleId)}</td>
434
+ <td><span class="impact-${issue.impact}">${escapeHtml(issue.impact)}</span></td>
435
+ <td>${escapeHtml(issue.description)}</td>
436
+ <td><code>${escapeHtml(issue.element)}</code></td>
437
+ </tr>
438
+ `)
439
+ .join('')}
440
+ </tbody>
441
+ </table>
442
+ </div>
443
+ `;
444
+ }
445
+ else if (template === 'detailed') {
446
+ mainContent = `
447
+ <div class="summary">
448
+ <h2>Summary</h2>
449
+ <p>Total Issues: <strong>${summary.totalIssues}</strong></p>
450
+ <p>Accessibility Score: <strong>${summary.score}/100</strong></p>
451
+ <p>WCAG Level A Compliance: <strong>${summary.wcagCompliance.A}%</strong></p>
452
+ <p>WCAG Level AA Compliance: <strong>${summary.wcagCompliance.AA}%</strong></p>
453
+ <p>WCAG Level AAA Compliance: <strong>${summary.wcagCompliance.AAA}%</strong></p>
454
+ </div>
455
+ ${includeCharts ? `
456
+ <div class="charts">
457
+ <div style="width: 50%; display: inline-block;">
458
+ <canvas id="categoryChart"></canvas>
459
+ </div>
460
+ <div style="width: 50%; display: inline-block;">
461
+ <canvas id="impactChart"></canvas>
462
+ </div>
463
+ </div>
464
+ ` : ''}
465
+ <div class="issues">
466
+ <h2>Detailed Issues</h2>
467
+ <table>
468
+ <thead>
469
+ <tr>
470
+ <th>Rule ID</th>
471
+ <th>Category</th>
472
+ <th>Impact</th>
473
+ <th>WCAG Level</th>
474
+ <th>Description</th>
475
+ <th>Element</th>
476
+ <th>XPath</th>
477
+ <th>Class selector</th>
478
+ <th>User Impact</th>
479
+ <th>Fix Explanation</th>
480
+ <th>Current Code</th>
481
+ <th>Suggested Code</th>
482
+ </tr>
483
+ </thead>
484
+ <tbody>
485
+ ${issues
486
+ .map((issue) => `
487
+ <tr>
488
+ <td>${escapeHtml(issue.ruleId)}</td>
489
+ <td>${escapeHtml(issue.category || 'unknown')}</td>
490
+ <td><span class="impact-${issue.impact}">${escapeHtml(issue.impact)}</span></td>
491
+ <td>${escapeHtml(issue.wcagLevel)}</td>
492
+ <td>${escapeHtml(issue.description)}</td>
493
+ <td><code>${escapeHtml(issue.element)}</code></td>
494
+ <td><code>${escapeHtml(issue.xpath)}</code></td>
495
+ <td><code>${escapeHtml(issue.classSelector ?? '')}</code></td>
496
+ <td>${escapeHtml(issue.userImpact)}</td>
497
+ <td>${escapeHtml(issue.fix.explanation)}</td>
498
+ <td><pre>${escapeHtml(issue.fix.current)}</pre></td>
499
+ <td><pre>${escapeHtml(issue.fix.suggested)}</pre></td>
500
+ </tr>
501
+ `)
502
+ .join('')}
503
+ </tbody>
504
+ </table>
505
+ </div>
506
+ `;
507
+ }
508
+ else {
509
+ // default template
510
+ mainContent = `
511
+ <div class="summary">
512
+ <h2>Summary</h2>
513
+ <p>Total Issues: <strong>${summary.totalIssues}</strong></p>
514
+ <p>Accessibility Score: <strong>${summary.score}/100</strong></p>
515
+ <p>WCAG Level A Compliance: <strong>${summary.wcagCompliance.A}%</strong></p>
516
+ <p>WCAG Level AA Compliance: <strong>${summary.wcagCompliance.AA}%</strong></p>
517
+ <p>WCAG Level AAA Compliance: <strong>${summary.wcagCompliance.AAA}%</strong></p>
518
+ </div>
519
+ ${includeCharts ? `
520
+ <div class="charts">
521
+ <div style="width: 50%; display: inline-block;">
522
+ <canvas id="categoryChart"></canvas>
523
+ </div>
524
+ <div style="width: 50%; display: inline-block;">
525
+ <canvas id="impactChart"></canvas>
526
+ </div>
527
+ </div>
528
+ ` : ''}
529
+ <div class="issues">
530
+ <h2>Issues</h2>
531
+ <table>
532
+ <thead>
533
+ <tr>
534
+ <th>Rule ID</th>
535
+ <th>Category</th>
536
+ <th>Impact</th>
537
+ <th>WCAG Level</th>
538
+ <th>Description</th>
539
+ <th>Element</th>
540
+ <th>User Impact</th>
541
+ </tr>
542
+ </thead>
543
+ <tbody>
544
+ ${issuesRows}
545
+ </tbody>
546
+ </table>
547
+ </div>
548
+ `;
549
+ }
550
+ return `
551
+ <!DOCTYPE html>
552
+ <html lang="en">
553
+ <head>
554
+ <meta charset="UTF-8">
555
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
556
+ <title>Accessibility Audit Report</title>
557
+ <style>
558
+ body {
559
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
560
+ line-height: 1.6;
561
+ color: #333;
562
+ max-width: 1200px;
563
+ margin: 0 auto;
564
+ padding: 20px;
565
+ background-color: #f5f5f5;
566
+ }
567
+ .header {
568
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
569
+ color: white;
570
+ padding: 30px;
571
+ border-radius: 8px;
572
+ margin-bottom: 30px;
573
+ }
574
+ .header h1 {
575
+ margin: 0;
576
+ font-size: 2em;
577
+ }
578
+ .metadata {
579
+ background: white;
580
+ padding: 20px;
581
+ border-radius: 8px;
582
+ margin-bottom: 20px;
583
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
584
+ }
585
+ .metadata h2 {
586
+ margin-top: 0;
587
+ color: #667eea;
588
+ }
589
+ .metadata p {
590
+ margin: 5px 0;
591
+ }
592
+ .summary {
593
+ background: white;
594
+ padding: 20px;
595
+ border-radius: 8px;
596
+ margin-bottom: 20px;
597
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
598
+ }
599
+ .summary h2 {
600
+ margin-top: 0;
601
+ color: #667eea;
602
+ }
603
+ .summary p {
604
+ margin: 10px 0;
605
+ font-size: 1.1em;
606
+ }
607
+ .charts {
608
+ background: white;
609
+ padding: 20px;
610
+ border-radius: 8px;
611
+ margin-bottom: 20px;
612
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
613
+ }
614
+ .issues {
615
+ background: white;
616
+ padding: 20px;
617
+ border-radius: 8px;
618
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
619
+ }
620
+ .issues h2 {
621
+ margin-top: 0;
622
+ color: #667eea;
623
+ }
624
+ table {
625
+ width: 100%;
626
+ border-collapse: collapse;
627
+ margin-top: 20px;
628
+ }
629
+ th {
630
+ background-color: #667eea;
631
+ color: white;
632
+ padding: 12px;
633
+ text-align: left;
634
+ font-weight: 600;
635
+ }
636
+ td {
637
+ padding: 10px;
638
+ border-bottom: 1px solid #ddd;
639
+ }
640
+ tr:hover {
641
+ background-color: #f9f9f9;
642
+ }
643
+ code {
644
+ background-color: #f4f4f4;
645
+ padding: 2px 6px;
646
+ border-radius: 3px;
647
+ font-family: 'Courier New', monospace;
648
+ font-size: 0.9em;
649
+ }
650
+ pre {
651
+ background-color: #f4f4f4;
652
+ padding: 10px;
653
+ border-radius: 3px;
654
+ overflow-x: auto;
655
+ font-size: 0.85em;
656
+ }
657
+ /* axe: critical, serious, moderate, minor. ACE: violation, potentialviolation, etc. */
658
+ .impact-critical, .impact-violation {
659
+ background-color: #ff4444;
660
+ color: white;
661
+ padding: 4px 8px;
662
+ border-radius: 4px;
663
+ font-weight: bold;
664
+ font-size: 0.85em;
665
+ }
666
+ .impact-serious, .impact-potentialviolation {
667
+ background-color: #ff8800;
668
+ color: white;
669
+ padding: 4px 8px;
670
+ border-radius: 4px;
671
+ font-weight: bold;
672
+ font-size: 0.85em;
673
+ }
674
+ .impact-moderate, .impact-potentialrecommendation, .impact-manual {
675
+ background-color: #ffbb00;
676
+ color: white;
677
+ padding: 4px 8px;
678
+ border-radius: 4px;
679
+ font-weight: bold;
680
+ font-size: 0.85em;
681
+ }
682
+ .impact-minor, .impact-recommendation, .impact-pass {
683
+ background-color: #88cc00;
684
+ color: white;
685
+ padding: 4px 8px;
686
+ border-radius: 4px;
687
+ font-weight: bold;
688
+ font-size: 0.85em;
689
+ }
690
+ [class^="impact-"] {
691
+ padding: 4px 8px;
692
+ border-radius: 4px;
693
+ font-size: 0.85em;
694
+ }
695
+ </style>
696
+ ${chartScript}
697
+ </head>
698
+ <body>
699
+ <div class="header">
700
+ <h1>Accessibility Audit Report</h1>
701
+ </div>
702
+ ${metadata ? `
703
+ <div class="metadata">
704
+ <h2>Test Information</h2>
705
+ <p><strong>Test Engine:</strong> ${metadata.testEngine.name} v${metadata.testEngine.version}</p>
706
+ <p><strong>Test Runner:</strong> ${metadata.testRunner.name}</p>
707
+ <p><strong>Test URL:</strong> <a href="${metadata.url}" target="_blank">${metadata.url}</a></p>
708
+ <p><strong>Timestamp:</strong> ${metadata.timestamp}</p>
709
+ <p><strong>User Agent:</strong> ${metadata.testEnvironment.userAgent}</p>
710
+ <p><strong>Window Size:</strong> ${metadata.testEnvironment.windowWidth}x${metadata.testEnvironment.windowHeight}</p>
711
+ </div>
712
+ ` : ''}
713
+ ${mainContent}
714
+ </body>
715
+ </html>
716
+ `;
717
+ }
718
+ /**
719
+ * export_to_html_report - Generate standalone HTML report
720
+ *
721
+ * Generates a standalone HTML report with styling and optional charts.
722
+ *
723
+ * @param input - Export configuration
724
+ * @returns HTML string with embedded CSS/JS
725
+ */
726
+ export async function exportToHtmlReport(input) {
727
+ const { results, template = 'default', includeCharts = true, basicAuthUsername, basicAuthPassword, } = input;
728
+ let auditResult;
729
+ // If input is a URL string, run an audit first (with optional Basic Auth, same as audit_url)
730
+ if (typeof results === 'string') {
731
+ const { urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p } = resolveBasicAuth(results, basicAuthUsername, basicAuthPassword);
732
+ auditResult = await auditUrl({ url: urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p });
733
+ }
734
+ else {
735
+ const normalized = normalizeAuditResult(results);
736
+ auditResult = normalized ?? results;
737
+ }
738
+ const htmlContent = generateHtmlReport(auditResult, template, includeCharts);
739
+ return {
740
+ html: htmlContent,
741
+ template,
742
+ includeCharts,
743
+ totalIssues: auditResult.summary.totalIssues,
744
+ };
745
+ }
746
+ //# sourceMappingURL=export.js.map