pulse-js-framework 1.7.9 → 1.7.10
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.
- package/cli/lint.js +442 -3
- package/compiler/lexer.js +6 -0
- package/compiler/parser.js +144 -1
- package/compiler/transformer/imports.js +15 -0
- package/compiler/transformer/index.js +46 -0
- package/compiler/transformer/view.js +180 -5
- package/package.json +9 -2
- package/runtime/a11y.js +1005 -0
- package/runtime/devtools/a11y-audit.js +442 -0
- package/runtime/devtools/diagnostics.js +403 -0
- package/runtime/devtools/index.js +53 -0
- package/runtime/devtools/time-travel.js +189 -0
- package/runtime/devtools.js +138 -497
- package/runtime/dom-binding.js +7 -4
- package/runtime/dom-element.js +192 -1
- package/runtime/dom.js +8 -2
- package/runtime/index.js +2 -0
- package/runtime/native.js +2 -2
- package/runtime/security.js +461 -0
- package/runtime/utils.js +37 -16
- package/types/a11y.d.ts +336 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse DevTools - Accessibility Audit Module
|
|
3
|
+
* @module pulse-js-framework/runtime/devtools/a11y-audit
|
|
4
|
+
*
|
|
5
|
+
* Real-time accessibility validation with visual highlighting and reports.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { validateA11y, highlightA11yIssues } from '../a11y.js';
|
|
9
|
+
import { createLogger } from '../logger.js';
|
|
10
|
+
import { config as diagnosticsConfig } from './diagnostics.js';
|
|
11
|
+
|
|
12
|
+
const log = createLogger('DevTools:A11y');
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// A11Y AUDIT CONFIGURATION
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A11y audit configuration
|
|
20
|
+
*/
|
|
21
|
+
export const a11yAuditConfig = {
|
|
22
|
+
enabled: false,
|
|
23
|
+
autoAudit: false,
|
|
24
|
+
auditInterval: 5000,
|
|
25
|
+
highlightIssues: true,
|
|
26
|
+
logToConsole: true,
|
|
27
|
+
breakOnError: false,
|
|
28
|
+
watchMutations: false
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Current a11y audit state
|
|
33
|
+
*/
|
|
34
|
+
let a11yAuditState = {
|
|
35
|
+
issues: [],
|
|
36
|
+
lastAuditTime: null,
|
|
37
|
+
auditCount: 0,
|
|
38
|
+
highlightCleanup: null,
|
|
39
|
+
mutationObserver: null,
|
|
40
|
+
intervalId: null,
|
|
41
|
+
mutationTimeout: null
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// A11Y AUDIT API
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} A11yAuditResult
|
|
50
|
+
* @property {Array} issues - List of accessibility issues found
|
|
51
|
+
* @property {number} errorCount - Number of errors
|
|
52
|
+
* @property {number} warningCount - Number of warnings
|
|
53
|
+
* @property {number} auditTime - Time taken for audit in ms
|
|
54
|
+
* @property {string} timestamp - ISO timestamp of audit
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run an accessibility audit on the document or specific element
|
|
59
|
+
* @param {Element} [root=document.body] - Root element to audit
|
|
60
|
+
* @param {Object} [options] - Audit options
|
|
61
|
+
* @returns {A11yAuditResult} Audit result
|
|
62
|
+
*/
|
|
63
|
+
export function runA11yAudit(root, options = {}) {
|
|
64
|
+
if (typeof document === 'undefined') {
|
|
65
|
+
return { issues: [], errorCount: 0, warningCount: 0, auditTime: 0, timestamp: new Date().toISOString() };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const startTime = performance.now();
|
|
69
|
+
const targetRoot = root || document.body;
|
|
70
|
+
|
|
71
|
+
// Run validation with options
|
|
72
|
+
const issues = validateA11y(targetRoot, options);
|
|
73
|
+
|
|
74
|
+
const auditTime = performance.now() - startTime;
|
|
75
|
+
a11yAuditState.lastAuditTime = Date.now();
|
|
76
|
+
a11yAuditState.auditCount++;
|
|
77
|
+
a11yAuditState.issues = issues;
|
|
78
|
+
|
|
79
|
+
// Count by severity
|
|
80
|
+
const errorCount = issues.filter(i => i.severity === 'error').length;
|
|
81
|
+
const warningCount = issues.filter(i => i.severity === 'warning').length;
|
|
82
|
+
|
|
83
|
+
const result = {
|
|
84
|
+
issues,
|
|
85
|
+
errorCount,
|
|
86
|
+
warningCount,
|
|
87
|
+
auditTime,
|
|
88
|
+
timestamp: new Date().toISOString()
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Log to console if enabled
|
|
92
|
+
if (a11yAuditConfig.logToConsole && diagnosticsConfig.enabled) {
|
|
93
|
+
logA11yAuditResult(result);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Highlight issues if enabled
|
|
97
|
+
if (a11yAuditConfig.highlightIssues && diagnosticsConfig.enabled) {
|
|
98
|
+
if (a11yAuditState.highlightCleanup) {
|
|
99
|
+
a11yAuditState.highlightCleanup();
|
|
100
|
+
}
|
|
101
|
+
a11yAuditState.highlightCleanup = highlightA11yIssues(issues);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Break on error if configured
|
|
105
|
+
if (a11yAuditConfig.breakOnError && errorCount > 0) {
|
|
106
|
+
log.error('Breaking due to accessibility errors');
|
|
107
|
+
// eslint-disable-next-line no-debugger
|
|
108
|
+
debugger;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Log audit result to console with formatting
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
function logA11yAuditResult(result) {
|
|
119
|
+
const { issues, errorCount, warningCount, auditTime } = result;
|
|
120
|
+
|
|
121
|
+
console.group(`%c[A11y Audit] ${errorCount} errors, ${warningCount} warnings (${auditTime.toFixed(1)}ms)`,
|
|
122
|
+
errorCount > 0 ? 'color: red; font-weight: bold' : 'color: green; font-weight: bold');
|
|
123
|
+
|
|
124
|
+
if (issues.length === 0) {
|
|
125
|
+
console.log('%c✓ No accessibility issues found', 'color: green');
|
|
126
|
+
} else {
|
|
127
|
+
// Group by severity
|
|
128
|
+
const errors = issues.filter(i => i.severity === 'error');
|
|
129
|
+
const warnings = issues.filter(i => i.severity === 'warning');
|
|
130
|
+
|
|
131
|
+
if (errors.length > 0) {
|
|
132
|
+
console.group('%cErrors', 'color: red; font-weight: bold');
|
|
133
|
+
for (const issue of errors) {
|
|
134
|
+
console.error(`${issue.rule}: ${issue.message}`, issue.element || '');
|
|
135
|
+
}
|
|
136
|
+
console.groupEnd();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (warnings.length > 0) {
|
|
140
|
+
console.group('%cWarnings', 'color: orange');
|
|
141
|
+
for (const issue of warnings) {
|
|
142
|
+
console.warn(`${issue.rule}: ${issue.message}`, issue.element || '');
|
|
143
|
+
}
|
|
144
|
+
console.groupEnd();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.groupEnd();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get current accessibility issues from last audit
|
|
153
|
+
* @returns {Array} List of a11y issues
|
|
154
|
+
*/
|
|
155
|
+
export function getA11yIssues() {
|
|
156
|
+
return [...a11yAuditState.issues];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get a11y audit statistics
|
|
161
|
+
* @returns {Object} Audit statistics
|
|
162
|
+
*/
|
|
163
|
+
export function getA11yStats() {
|
|
164
|
+
const issues = a11yAuditState.issues;
|
|
165
|
+
const byRule = {};
|
|
166
|
+
|
|
167
|
+
for (const issue of issues) {
|
|
168
|
+
byRule[issue.rule] = (byRule[issue.rule] || 0) + 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
totalIssues: issues.length,
|
|
173
|
+
errorCount: issues.filter(i => i.severity === 'error').length,
|
|
174
|
+
warningCount: issues.filter(i => i.severity === 'warning').length,
|
|
175
|
+
issuesByRule: byRule,
|
|
176
|
+
auditCount: a11yAuditState.auditCount,
|
|
177
|
+
lastAuditTime: a11yAuditState.lastAuditTime,
|
|
178
|
+
isWatching: a11yAuditState.mutationObserver !== null,
|
|
179
|
+
isAutoAuditing: a11yAuditState.intervalId !== null
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Enable accessibility audit mode
|
|
185
|
+
* @param {Object} [options] - Configuration options
|
|
186
|
+
*/
|
|
187
|
+
export function enableA11yAudit(options = {}) {
|
|
188
|
+
Object.assign(a11yAuditConfig, options, { enabled: true });
|
|
189
|
+
|
|
190
|
+
// Run initial audit
|
|
191
|
+
runA11yAudit();
|
|
192
|
+
|
|
193
|
+
// Setup auto-audit if enabled
|
|
194
|
+
if (a11yAuditConfig.autoAudit && typeof window !== 'undefined') {
|
|
195
|
+
a11yAuditState.intervalId = setInterval(() => {
|
|
196
|
+
runA11yAudit();
|
|
197
|
+
}, a11yAuditConfig.auditInterval);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Setup mutation observer if enabled
|
|
201
|
+
if (a11yAuditConfig.watchMutations && typeof MutationObserver !== 'undefined') {
|
|
202
|
+
a11yAuditState.mutationObserver = new MutationObserver(() => {
|
|
203
|
+
// Debounce audit on mutations
|
|
204
|
+
clearTimeout(a11yAuditState.mutationTimeout);
|
|
205
|
+
a11yAuditState.mutationTimeout = setTimeout(() => {
|
|
206
|
+
runA11yAudit();
|
|
207
|
+
}, 250);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
a11yAuditState.mutationObserver.observe(document.body, {
|
|
211
|
+
childList: true,
|
|
212
|
+
subtree: true,
|
|
213
|
+
attributes: true,
|
|
214
|
+
attributeFilter: ['role', 'aria-label', 'aria-hidden', 'aria-describedby', 'alt', 'tabindex']
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (diagnosticsConfig.enabled) {
|
|
219
|
+
log.info('A11y Audit enabled', a11yAuditConfig);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Disable accessibility audit mode
|
|
225
|
+
*/
|
|
226
|
+
export function disableA11yAudit() {
|
|
227
|
+
a11yAuditConfig.enabled = false;
|
|
228
|
+
|
|
229
|
+
// Clear auto-audit interval
|
|
230
|
+
if (a11yAuditState.intervalId) {
|
|
231
|
+
clearInterval(a11yAuditState.intervalId);
|
|
232
|
+
a11yAuditState.intervalId = null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Disconnect mutation observer
|
|
236
|
+
if (a11yAuditState.mutationObserver) {
|
|
237
|
+
a11yAuditState.mutationObserver.disconnect();
|
|
238
|
+
a11yAuditState.mutationObserver = null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Clear highlights
|
|
242
|
+
if (a11yAuditState.highlightCleanup) {
|
|
243
|
+
a11yAuditState.highlightCleanup();
|
|
244
|
+
a11yAuditState.highlightCleanup = null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (diagnosticsConfig.enabled) {
|
|
248
|
+
log.info('A11y Audit disabled');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Toggle issue highlighting
|
|
254
|
+
* @param {boolean} [show] - Show or hide highlights (toggles if not specified)
|
|
255
|
+
*/
|
|
256
|
+
export function toggleA11yHighlights(show) {
|
|
257
|
+
const shouldShow = show !== undefined ? show : !a11yAuditState.highlightCleanup;
|
|
258
|
+
|
|
259
|
+
if (shouldShow) {
|
|
260
|
+
if (a11yAuditState.highlightCleanup) {
|
|
261
|
+
a11yAuditState.highlightCleanup();
|
|
262
|
+
}
|
|
263
|
+
a11yAuditState.highlightCleanup = highlightA11yIssues(a11yAuditState.issues);
|
|
264
|
+
} else {
|
|
265
|
+
if (a11yAuditState.highlightCleanup) {
|
|
266
|
+
a11yAuditState.highlightCleanup();
|
|
267
|
+
a11yAuditState.highlightCleanup = null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// =============================================================================
|
|
273
|
+
// EXPORT REPORTS
|
|
274
|
+
// =============================================================================
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get a CSS selector for an element
|
|
278
|
+
* @private
|
|
279
|
+
*/
|
|
280
|
+
function getElementSelector(element) {
|
|
281
|
+
if (!element) return 'unknown';
|
|
282
|
+
|
|
283
|
+
const parts = [];
|
|
284
|
+
let el = element;
|
|
285
|
+
|
|
286
|
+
while (el && el !== document.body) {
|
|
287
|
+
let selector = el.tagName?.toLowerCase() || '';
|
|
288
|
+
|
|
289
|
+
if (el.id) {
|
|
290
|
+
selector += `#${el.id}`;
|
|
291
|
+
parts.unshift(selector);
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (el.className && typeof el.className === 'string') {
|
|
296
|
+
const classes = el.className.trim().split(/\s+/).slice(0, 2).join('.');
|
|
297
|
+
if (classes) selector += `.${classes}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
parts.unshift(selector);
|
|
301
|
+
el = el.parentElement;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return parts.join(' > ');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Export report as CSV
|
|
309
|
+
* @private
|
|
310
|
+
*/
|
|
311
|
+
function exportA11yReportAsCsv(report) {
|
|
312
|
+
const headers = ['severity', 'rule', 'message', 'element', 'selector'];
|
|
313
|
+
const rows = report.issues.map(i =>
|
|
314
|
+
[i.severity, i.rule, `"${i.message.replace(/"/g, '""')}"`, i.element, `"${i.selector}"`].join(',')
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
return [headers.join(','), ...rows].join('\n');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Export report as HTML
|
|
322
|
+
* @private
|
|
323
|
+
*/
|
|
324
|
+
function exportA11yReportAsHtml(report) {
|
|
325
|
+
const errorCount = report.stats.errorCount;
|
|
326
|
+
const warningCount = report.stats.warningCount;
|
|
327
|
+
|
|
328
|
+
return `<!DOCTYPE html>
|
|
329
|
+
<html lang="en">
|
|
330
|
+
<head>
|
|
331
|
+
<meta charset="UTF-8">
|
|
332
|
+
<title>Accessibility Audit Report</title>
|
|
333
|
+
<style>
|
|
334
|
+
body { font-family: system-ui, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; }
|
|
335
|
+
h1 { color: #333; }
|
|
336
|
+
.summary { display: flex; gap: 20px; margin: 20px 0; }
|
|
337
|
+
.stat { padding: 15px 25px; border-radius: 8px; }
|
|
338
|
+
.errors { background: #fee; color: #c00; }
|
|
339
|
+
.warnings { background: #ffd; color: #a50; }
|
|
340
|
+
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
|
341
|
+
th, td { text-align: left; padding: 10px; border-bottom: 1px solid #ddd; }
|
|
342
|
+
th { background: #f5f5f5; }
|
|
343
|
+
.error { color: #c00; }
|
|
344
|
+
.warning { color: #a50; }
|
|
345
|
+
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
|
|
346
|
+
</style>
|
|
347
|
+
</head>
|
|
348
|
+
<body>
|
|
349
|
+
<h1>Accessibility Audit Report</h1>
|
|
350
|
+
<p>Generated: ${report.timestamp}</p>
|
|
351
|
+
<p>URL: <code>${report.url}</code></p>
|
|
352
|
+
|
|
353
|
+
<div class="summary">
|
|
354
|
+
<div class="stat errors"><strong>${errorCount}</strong> Errors</div>
|
|
355
|
+
<div class="stat warnings"><strong>${warningCount}</strong> Warnings</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
${report.issues.length > 0 ? `
|
|
359
|
+
<table>
|
|
360
|
+
<thead>
|
|
361
|
+
<tr>
|
|
362
|
+
<th>Severity</th>
|
|
363
|
+
<th>Rule</th>
|
|
364
|
+
<th>Message</th>
|
|
365
|
+
<th>Element</th>
|
|
366
|
+
</tr>
|
|
367
|
+
</thead>
|
|
368
|
+
<tbody>
|
|
369
|
+
${report.issues.map(i => `
|
|
370
|
+
<tr>
|
|
371
|
+
<td class="${i.severity}">${i.severity}</td>
|
|
372
|
+
<td><code>${i.rule}</code></td>
|
|
373
|
+
<td>${i.message}</td>
|
|
374
|
+
<td><code>${i.selector}</code></td>
|
|
375
|
+
</tr>
|
|
376
|
+
`).join('')}
|
|
377
|
+
</tbody>
|
|
378
|
+
</table>
|
|
379
|
+
` : '<p style="color: green;">✓ No accessibility issues found!</p>'}
|
|
380
|
+
</body>
|
|
381
|
+
</html>`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Export a11y audit report
|
|
386
|
+
* @param {string} [format='json'] - Export format ('json', 'csv', 'html')
|
|
387
|
+
* @returns {string} Formatted report
|
|
388
|
+
*/
|
|
389
|
+
export function exportA11yReport(format = 'json') {
|
|
390
|
+
const stats = getA11yStats();
|
|
391
|
+
const issues = getA11yIssues();
|
|
392
|
+
|
|
393
|
+
const report = {
|
|
394
|
+
timestamp: new Date().toISOString(),
|
|
395
|
+
url: typeof location !== 'undefined' ? location.href : 'unknown',
|
|
396
|
+
stats,
|
|
397
|
+
issues: issues.map(i => ({
|
|
398
|
+
rule: i.rule,
|
|
399
|
+
severity: i.severity,
|
|
400
|
+
message: i.message,
|
|
401
|
+
element: i.element?.tagName?.toLowerCase() || 'unknown',
|
|
402
|
+
selector: i.element ? getElementSelector(i.element) : 'unknown'
|
|
403
|
+
}))
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
switch (format) {
|
|
407
|
+
case 'csv':
|
|
408
|
+
return exportA11yReportAsCsv(report);
|
|
409
|
+
case 'html':
|
|
410
|
+
return exportA11yReportAsHtml(report);
|
|
411
|
+
default:
|
|
412
|
+
return JSON.stringify(report, null, 2);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Reset a11y audit state
|
|
418
|
+
*/
|
|
419
|
+
export function resetA11yAudit() {
|
|
420
|
+
disableA11yAudit();
|
|
421
|
+
a11yAuditState = {
|
|
422
|
+
issues: [],
|
|
423
|
+
lastAuditTime: null,
|
|
424
|
+
auditCount: 0,
|
|
425
|
+
highlightCleanup: null,
|
|
426
|
+
mutationObserver: null,
|
|
427
|
+
intervalId: null,
|
|
428
|
+
mutationTimeout: null
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export default {
|
|
433
|
+
a11yAuditConfig,
|
|
434
|
+
runA11yAudit,
|
|
435
|
+
getA11yIssues,
|
|
436
|
+
getA11yStats,
|
|
437
|
+
enableA11yAudit,
|
|
438
|
+
disableA11yAudit,
|
|
439
|
+
toggleA11yHighlights,
|
|
440
|
+
exportA11yReport,
|
|
441
|
+
resetA11yAudit
|
|
442
|
+
};
|