wcag-scanner 1.2.65 → 1.2.67
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/README.md +107 -83
- package/dist/index.d.ts +5 -24
- package/dist/index.js +11 -126
- package/dist/middleware/express.js +42 -18
- package/dist/react/WcagDevOverlay.d.ts +2 -0
- package/dist/react/WcagDevOverlay.js +282 -154
- package/dist/react/browserScanner.d.ts +11 -1
- package/dist/react/browserScanner.js +107 -22
- package/dist/react/gemini.d.ts +12 -0
- package/dist/react/gemini.js +60 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +10 -1
- package/dist/rules/backgroundImages.d.ts +8 -0
- package/dist/rules/backgroundImages.js +116 -0
- package/dist/rules/contrast.js +52 -23
- package/dist/rules/images.js +0 -91
- package/dist/rules/presets.d.ts +8 -0
- package/dist/rules/presets.js +19 -0
- package/dist/scanner.js +14 -12
- package/dist/types/index.d.ts +3 -0
- package/package.json +16 -15
- package/dist/ai/suggestions.d.ts +0 -11
- package/dist/ai/suggestions.js +0 -103
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -160
- package/dist/wasm/index.d.ts +0 -19
- package/dist/wasm/index.js +0 -53
package/README.md
CHANGED
|
@@ -22,13 +22,13 @@ WCAG Scanner is a powerful accessibility testing tool that helps developers iden
|
|
|
22
22
|
|
|
23
23
|
## ✨ Features
|
|
24
24
|
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
25
|
+
- **WCAG 2.1 Compliance Scanning**: Checks against A, AA, and AAA conformance levels
|
|
26
|
+
- **Fast and Full Presets**: Default fast scans plus optional heavier rules like `backgroundImages`
|
|
27
|
+
- **React Dev Overlay**: Live in-browser inspector with element highlighting, pinning, and impact filtering
|
|
28
|
+
- **AI Fix Suggestions**: Paste your Gemini API key in the overlay settings to get instant fix suggestions per violation
|
|
29
|
+
- **Programmatic API**: Scan HTML strings or local files from Node.js
|
|
30
|
+
- **Express Middleware**: Auto-scan responses in your Express app
|
|
31
|
+
- **Multiple Report Formats**: JSON, HTML, and console output
|
|
32
32
|
|
|
33
33
|
## 📦 Installation
|
|
34
34
|
|
|
@@ -43,105 +43,129 @@ yarn add wcag-scanner
|
|
|
43
43
|
pnpm add wcag-scanner
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
> **React users:** React and React DOM are peer dependencies
|
|
46
|
+
> **React users:** React and React DOM are peer dependencies already in your project — no extra install needed.
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
## 🖥️ React Dev Overlay
|
|
49
49
|
|
|
50
|
-
Add one line to your
|
|
50
|
+
The easiest way to use wcag-scanner in a React app. Add **one line** to your entry file and a live accessibility inspector appears in the corner of your browser during development.
|
|
51
51
|
|
|
52
52
|
```ts
|
|
53
|
+
// main.ts / main.jsx / index.tsx — works with any file type
|
|
53
54
|
import { initWcagOverlay } from 'wcag-scanner/react';
|
|
54
|
-
|
|
55
|
+
|
|
56
|
+
initWcagOverlay(); // auto-disabled in production
|
|
55
57
|
```
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
**Options:**
|
|
60
|
+
```ts
|
|
61
|
+
initWcagOverlay({
|
|
62
|
+
level: 'AA', // 'A' | 'AA' | 'AAA' — default: 'AA'
|
|
63
|
+
preset: 'fast', // 'fast' | 'full' — default: 'fast'
|
|
64
|
+
position: 'bottom-right', // 'bottom-right' | 'bottom-left'
|
|
65
|
+
debounce: 750, // ms to wait after DOM change before rescanning
|
|
66
|
+
rules: ['images', 'backgroundImages', 'contrast'], // explicit rules override preset
|
|
67
|
+
});
|
|
68
|
+
```
|
|
58
69
|
|
|
59
|
-
|
|
60
|
-
### CLI Usage Example:
|
|
70
|
+
`preset: 'full'` includes the heavier optional checks such as `backgroundImages`. Use `rules` when you want an exact rule list.
|
|
61
71
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
**Preset Contents**
|
|
73
|
+
|
|
74
|
+
| Preset | Rules included |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `fast` | `images`, `contrast`, `forms`, `aria`, `structure`, `keyboard` |
|
|
77
|
+
| `full` | `images`, `contrast`, `forms`, `aria`, `structure`, `keyboard`, `backgroundImages` |
|
|
78
|
+
|
|
79
|
+
You can also import these programmatically:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { RULE_PRESETS, resolveRuleNames } from 'wcag-scanner';
|
|
83
|
+
|
|
84
|
+
console.log(RULE_PRESETS.fast);
|
|
85
|
+
console.log(resolveRuleNames({ preset: 'full' }));
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Features:**
|
|
89
|
+
- Hover over a violation to highlight the element on the page
|
|
90
|
+
- Click to pin the highlight; click again to unpin
|
|
91
|
+
- Expand any violation card for the HTML snippet, element path, WCAG criteria, and fix hint
|
|
92
|
+
- Filter by impact level (critical / serious / moderate / minor)
|
|
93
|
+
- Drag the panel anywhere on screen
|
|
94
|
+
- Keyboard shortcut `Alt+Shift+W` to toggle open/close
|
|
95
|
+
- **⚙ Settings** — paste a free Google Gemini API key to get AI-powered fix suggestions per violation
|
|
96
|
+
|
|
97
|
+
> The overlay never runs in production (`NODE_ENV=production`) and is never included in your production bundle.
|
|
98
|
+
|
|
99
|
+
## 🔧 Programmatic API
|
|
65
100
|
|
|
66
|
-
|
|
67
|
-
|
|
101
|
+
Scan HTML strings or local files from Node.js scripts, CI pipelines, or build tools.
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import { scanHtml, scanFile, formatReport, saveReport } from 'wcag-scanner';
|
|
105
|
+
|
|
106
|
+
// Scan an HTML string
|
|
107
|
+
const results = await scanHtml('<img src="logo.png">', { level: 'AA', preset: 'fast' });
|
|
108
|
+
console.log(`${results.violations.length} violations found`);
|
|
109
|
+
|
|
110
|
+
// Scan a local HTML file
|
|
111
|
+
const results = await scanFile('./public/index.html', { level: 'AA', preset: 'full' });
|
|
112
|
+
|
|
113
|
+
// Run an exact subset of rules
|
|
114
|
+
const targeted = await scanHtml('<div style="background-image:url(hero.jpg)"></div>', {
|
|
115
|
+
rules: ['images', 'backgroundImages'],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Or resolve a built-in preset yourself
|
|
119
|
+
// import { RULE_PRESETS } from 'wcag-scanner';
|
|
120
|
+
// const results = await scanHtml(html, { rules: RULE_PRESETS.full });
|
|
121
|
+
|
|
122
|
+
// Generate and save a report
|
|
123
|
+
const html = formatReport(results, 'html'); // 'html' | 'json' | 'console'
|
|
124
|
+
saveReport(html, 'accessibility-report.html');
|
|
68
125
|
```
|
|
69
126
|
|
|
70
|
-
|
|
127
|
+
## 🌐 Express Middleware
|
|
71
128
|
|
|
72
|
-
|
|
129
|
+
Automatically scan every HTML response in your Express app and inject a violation badge.
|
|
130
|
+
|
|
131
|
+
```js
|
|
73
132
|
import express from 'express';
|
|
74
133
|
import { middleware } from 'wcag-scanner';
|
|
75
134
|
|
|
76
135
|
const app = express();
|
|
77
136
|
|
|
78
|
-
// Add the WCAG scanner middleware
|
|
79
137
|
app.use(middleware.express.createMiddleware({
|
|
80
|
-
enabled:
|
|
81
|
-
level:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
138
|
+
enabled: true,
|
|
139
|
+
level: 'AA',
|
|
140
|
+
preset: 'fast',
|
|
141
|
+
headerName: 'X-WCAG-Violations', // violation count added to response headers
|
|
142
|
+
inlineReport: true, // inject a small widget into the HTML response
|
|
143
|
+
onViolation: (results, req) => {
|
|
144
|
+
console.log(`${results.violations.length} issues on ${req.path}`);
|
|
145
|
+
},
|
|
87
146
|
}));
|
|
88
147
|
|
|
89
|
-
//
|
|
148
|
+
// Switch to preset: 'full' if you also want heavier checks like backgroundImages.
|
|
149
|
+
|
|
90
150
|
app.get('/', (req, res) => {
|
|
91
|
-
res.send(
|
|
92
|
-
<!DOCTYPE html>
|
|
93
|
-
<html>
|
|
94
|
-
<head>
|
|
95
|
-
<title>Test Page</title>
|
|
96
|
-
</head>
|
|
97
|
-
<body>
|
|
98
|
-
<h1>Hello World</h1>
|
|
99
|
-
<img src="logo.png"> <!-- Missing alt text will trigger violation -->
|
|
100
|
-
</body>
|
|
101
|
-
</html>
|
|
102
|
-
`);
|
|
151
|
+
res.send(`<!DOCTYPE html><html><body><h1>Hello</h1></body></html>`);
|
|
103
152
|
});
|
|
104
153
|
|
|
105
|
-
app.listen(3000
|
|
106
|
-
console.log('Server running on http://localhost:3000');
|
|
107
|
-
});
|
|
154
|
+
app.listen(3000);
|
|
108
155
|
```
|
|
109
156
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
fs.writeFileSync('accessibility-report.html', htmlReport);
|
|
126
|
-
} catch (error) {
|
|
127
|
-
console.error('Error scanning website:', error);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function checkHtmlString() {
|
|
132
|
-
const html = `
|
|
133
|
-
<!DOCTYPE html>
|
|
134
|
-
<html>
|
|
135
|
-
<head>
|
|
136
|
-
<title>Test</title>
|
|
137
|
-
</head>
|
|
138
|
-
<body>
|
|
139
|
-
<img src="logo.png"> <!-- Missing alt text -->
|
|
140
|
-
</body>
|
|
141
|
-
</html>
|
|
142
|
-
`;
|
|
143
|
-
|
|
144
|
-
const results = await scanHtml(html);
|
|
145
|
-
console.log(formatReport(results, 'console'));
|
|
146
|
-
}
|
|
147
|
-
```
|
|
157
|
+
## 📊 Profile Summary
|
|
158
|
+
|
|
159
|
+
Current local synthetic benchmark baseline from the repo profiling scripts:
|
|
160
|
+
|
|
161
|
+
| Rule | Command | Approx. duration |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| `images` | `npm run profile:images` | `128ms` |
|
|
164
|
+
| `forms` | `npm run profile:forms` | `484ms` |
|
|
165
|
+
| `aria` | `npm run profile:aria` | `398ms` |
|
|
166
|
+
| `contrast` | `npm run profile:contrast` | `1836ms` |
|
|
167
|
+
|
|
168
|
+
Notes:
|
|
169
|
+
- These are synthetic local benchmarks, not production browser traces.
|
|
170
|
+
- `contrast` is currently the main runtime hotspot.
|
|
171
|
+
- `backgroundImages` is intentionally excluded from the default `fast` preset because it is a heavier optional check.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,39 +2,21 @@ import { WCAGScanner } from './scanner';
|
|
|
2
2
|
import { ScannerOptions, ScanResults } from './types';
|
|
3
3
|
import { ReporterFormat } from './reporters';
|
|
4
4
|
import middleware from './middleware';
|
|
5
|
+
export { FAST_RULES, FULL_RULES, RULE_PRESETS, resolveRuleNames } from './rules/presets';
|
|
5
6
|
/**
|
|
6
|
-
* Scan HTML string for WCAG violations
|
|
7
|
-
* @param html HTML content to scan
|
|
8
|
-
* @param options Scanner options
|
|
9
|
-
* @returns Promise<ScanResults> Scan results
|
|
7
|
+
* Scan an HTML string for WCAG violations.
|
|
10
8
|
*/
|
|
11
9
|
export declare function scanHtml(html: string, options?: ScannerOptions): Promise<ScanResults>;
|
|
12
10
|
/**
|
|
13
|
-
* Scan HTML file for WCAG violations
|
|
14
|
-
* @param filePath Path to HTML file
|
|
15
|
-
* @param options Scanner options
|
|
16
|
-
* @returns Promise<ScanResults> Scan results
|
|
11
|
+
* Scan a local HTML file for WCAG violations.
|
|
17
12
|
*/
|
|
18
13
|
export declare function scanFile(filePath: string, options?: ScannerOptions): Promise<ScanResults>;
|
|
19
14
|
/**
|
|
20
|
-
*
|
|
21
|
-
* @param url URL to scan
|
|
22
|
-
* @param options Scanner options
|
|
23
|
-
* @returns Promise<ScanResults> Scan results
|
|
24
|
-
*/
|
|
25
|
-
export declare function scanUrl(url: string, options?: ScannerOptions): Promise<ScanResults>;
|
|
26
|
-
/**
|
|
27
|
-
* Generate a report from scan results
|
|
28
|
-
* @param results Scan results
|
|
29
|
-
* @param format Report format
|
|
30
|
-
* @param options Scanner options
|
|
31
|
-
* @returns Report string
|
|
15
|
+
* Generate a report from scan results.
|
|
32
16
|
*/
|
|
33
17
|
export declare function formatReport(results: ScanResults, format?: ReporterFormat, options?: ScannerOptions): string;
|
|
34
18
|
/**
|
|
35
|
-
* Save report to a file
|
|
36
|
-
* @param report Report string
|
|
37
|
-
* @param filePath Output file path
|
|
19
|
+
* Save a report string to a file.
|
|
38
20
|
*/
|
|
39
21
|
export declare function saveReport(report: string, filePath: string): void;
|
|
40
22
|
export { WCAGScanner };
|
|
@@ -44,7 +26,6 @@ export { middleware };
|
|
|
44
26
|
declare const _default: {
|
|
45
27
|
scanHtml: typeof scanHtml;
|
|
46
28
|
scanFile: typeof scanFile;
|
|
47
|
-
scanUrl: typeof scanUrl;
|
|
48
29
|
formatReport: typeof formatReport;
|
|
49
30
|
saveReport: typeof saveReport;
|
|
50
31
|
middleware: {
|
package/dist/index.js
CHANGED
|
@@ -10,28 +10,6 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
|
|
|
10
10
|
if (k2 === undefined) k2 = k;
|
|
11
11
|
o[k2] = m[k];
|
|
12
12
|
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
13
|
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
36
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
37
15
|
};
|
|
@@ -39,10 +17,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
39
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
18
|
};
|
|
41
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
-
exports.middleware = exports.WCAGScanner = void 0;
|
|
20
|
+
exports.middleware = exports.WCAGScanner = exports.resolveRuleNames = exports.RULE_PRESETS = exports.FULL_RULES = exports.FAST_RULES = void 0;
|
|
43
21
|
exports.scanHtml = scanHtml;
|
|
44
22
|
exports.scanFile = scanFile;
|
|
45
|
-
exports.scanUrl = scanUrl;
|
|
46
23
|
exports.formatReport = formatReport;
|
|
47
24
|
exports.saveReport = saveReport;
|
|
48
25
|
const scanner_1 = require("./scanner");
|
|
@@ -52,13 +29,13 @@ const middleware_1 = __importDefault(require("./middleware"));
|
|
|
52
29
|
exports.middleware = middleware_1.default;
|
|
53
30
|
const fs_1 = __importDefault(require("fs"));
|
|
54
31
|
const path_1 = __importDefault(require("path"));
|
|
55
|
-
|
|
56
|
-
|
|
32
|
+
var presets_1 = require("./rules/presets");
|
|
33
|
+
Object.defineProperty(exports, "FAST_RULES", { enumerable: true, get: function () { return presets_1.FAST_RULES; } });
|
|
34
|
+
Object.defineProperty(exports, "FULL_RULES", { enumerable: true, get: function () { return presets_1.FULL_RULES; } });
|
|
35
|
+
Object.defineProperty(exports, "RULE_PRESETS", { enumerable: true, get: function () { return presets_1.RULE_PRESETS; } });
|
|
36
|
+
Object.defineProperty(exports, "resolveRuleNames", { enumerable: true, get: function () { return presets_1.resolveRuleNames; } });
|
|
57
37
|
/**
|
|
58
|
-
* Scan HTML string for WCAG violations
|
|
59
|
-
* @param html HTML content to scan
|
|
60
|
-
* @param options Scanner options
|
|
61
|
-
* @returns Promise<ScanResults> Scan results
|
|
38
|
+
* Scan an HTML string for WCAG violations.
|
|
62
39
|
*/
|
|
63
40
|
async function scanHtml(html, options = {}) {
|
|
64
41
|
const scanner = new scanner_1.WCAGScanner(options);
|
|
@@ -66,24 +43,7 @@ async function scanHtml(html, options = {}) {
|
|
|
66
43
|
return scanner.scan();
|
|
67
44
|
}
|
|
68
45
|
/**
|
|
69
|
-
*
|
|
70
|
-
*/
|
|
71
|
-
async function getWasmScraper() {
|
|
72
|
-
try {
|
|
73
|
-
const { default: wasmModule } = await Promise.resolve().then(() => __importStar(require('./wasm')));
|
|
74
|
-
await wasmModule.initialize();
|
|
75
|
-
return wasmModule;
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
console.error('Failed to load WASM scraper, falling back to HTTP requests:', error);
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Scan HTML file for WCAG violations
|
|
84
|
-
* @param filePath Path to HTML file
|
|
85
|
-
* @param options Scanner options
|
|
86
|
-
* @returns Promise<ScanResults> Scan results
|
|
46
|
+
* Scan a local HTML file for WCAG violations.
|
|
87
47
|
*/
|
|
88
48
|
async function scanFile(filePath, options = {}) {
|
|
89
49
|
const html = fs_1.default.readFileSync(path_1.default.resolve(filePath), 'utf8');
|
|
@@ -93,91 +53,16 @@ async function scanFile(filePath, options = {}) {
|
|
|
93
53
|
return scanner.scan();
|
|
94
54
|
}
|
|
95
55
|
/**
|
|
96
|
-
*
|
|
97
|
-
* @param url URL to scan
|
|
98
|
-
* @param options Scanner options
|
|
99
|
-
* @returns Promise<ScanResults> Scan results
|
|
100
|
-
*/
|
|
101
|
-
async function scanUrl(url, options = {}) {
|
|
102
|
-
console.log(`Scanning URL: ${url}`);
|
|
103
|
-
// Create temp directory for saving content
|
|
104
|
-
const tempDir = path_1.default.join(os_1.default.tmpdir(), 'wcag-scanner-temp');
|
|
105
|
-
if (!fs_1.default.existsSync(tempDir)) {
|
|
106
|
-
fs_1.default.mkdirSync(tempDir, { recursive: true });
|
|
107
|
-
}
|
|
108
|
-
// Generate unique file name
|
|
109
|
-
const fileId = crypto_1.default.createHash('md5').update(url + Date.now().toString()).digest('hex').substring(0, 10);
|
|
110
|
-
const tempFile = path_1.default.join(tempDir, `${fileId}.html`);
|
|
111
|
-
try {
|
|
112
|
-
// Get the WASM scraper
|
|
113
|
-
const wasmScraper = await getWasmScraper();
|
|
114
|
-
let html;
|
|
115
|
-
if (wasmScraper) {
|
|
116
|
-
// Use WASM scraper
|
|
117
|
-
html = await wasmScraper.scrapeUrl(url);
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
// Fallback to simple HTTP request
|
|
121
|
-
const response = await fetch(url, {
|
|
122
|
-
headers: {
|
|
123
|
-
'User-Agent': 'WCAG-Scanner/1.0-js'
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
if (!response.ok) {
|
|
127
|
-
throw new Error(`HTTP error: ${response.status}`);
|
|
128
|
-
}
|
|
129
|
-
html = await response.text();
|
|
130
|
-
}
|
|
131
|
-
if (!html || html.trim().length === 0) {
|
|
132
|
-
throw new Error('Scraper returned empty HTML content');
|
|
133
|
-
}
|
|
134
|
-
console.log(`Successfully scraped ${html.length} bytes of HTML content`);
|
|
135
|
-
// Save content to temp file
|
|
136
|
-
fs_1.default.writeFileSync(tempFile, html);
|
|
137
|
-
console.log(`Saved scraped content to ${tempFile}`);
|
|
138
|
-
// If verbose logging is enabled, show a sample
|
|
139
|
-
if (options.verbose) {
|
|
140
|
-
console.log('First 200 characters of HTML:');
|
|
141
|
-
console.log(html.substring(0, 200) + '...');
|
|
142
|
-
}
|
|
143
|
-
// Run the scanner on the HTML content
|
|
144
|
-
const scanner = new scanner_1.WCAGScanner({
|
|
145
|
-
...options,
|
|
146
|
-
baseUrl: url
|
|
147
|
-
});
|
|
148
|
-
await scanner.loadHTML(html, url);
|
|
149
|
-
return scanner.scan();
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
console.error('Error scanning URL:', error);
|
|
153
|
-
throw error;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Generate a report from scan results
|
|
158
|
-
* @param results Scan results
|
|
159
|
-
* @param format Report format
|
|
160
|
-
* @param options Scanner options
|
|
161
|
-
* @returns Report string
|
|
56
|
+
* Generate a report from scan results.
|
|
162
57
|
*/
|
|
163
58
|
function formatReport(results, format = 'json', options = {}) {
|
|
164
59
|
return (0, reporters_1.generateReport)(results, format, options);
|
|
165
60
|
}
|
|
166
61
|
/**
|
|
167
|
-
* Save report to a file
|
|
168
|
-
* @param report Report string
|
|
169
|
-
* @param filePath Output file path
|
|
62
|
+
* Save a report string to a file.
|
|
170
63
|
*/
|
|
171
64
|
function saveReport(report, filePath) {
|
|
172
65
|
fs_1.default.writeFileSync(filePath, report);
|
|
173
66
|
}
|
|
174
67
|
__exportStar(require("./types"), exports);
|
|
175
|
-
|
|
176
|
-
exports.default = {
|
|
177
|
-
scanHtml,
|
|
178
|
-
scanFile,
|
|
179
|
-
scanUrl,
|
|
180
|
-
formatReport,
|
|
181
|
-
saveReport,
|
|
182
|
-
middleware: middleware_1.default
|
|
183
|
-
};
|
|
68
|
+
exports.default = { scanHtml, scanFile, formatReport, saveReport, middleware: middleware_1.default };
|
|
@@ -11,13 +11,13 @@ function createMiddleware(options = {}) {
|
|
|
11
11
|
const defaultOptions = {
|
|
12
12
|
enabled: process.env.NODE_ENV !== 'production', // Disable in production by default
|
|
13
13
|
level: 'AA',
|
|
14
|
-
headerName:
|
|
14
|
+
headerName: undefined,
|
|
15
15
|
inlineReport: false,
|
|
16
16
|
...options
|
|
17
17
|
};
|
|
18
18
|
return async function wcagScannerMiddleware(req, res, next) {
|
|
19
19
|
// Skip if disabled or non-HTML request
|
|
20
|
-
if (!defaultOptions.enabled || !shouldProcessRequest(req)) {
|
|
20
|
+
if (!defaultOptions.enabled || !shouldProcessRequest(req) || !needsScanning(defaultOptions)) {
|
|
21
21
|
return next();
|
|
22
22
|
}
|
|
23
23
|
// Store original send method
|
|
@@ -25,32 +25,47 @@ function createMiddleware(options = {}) {
|
|
|
25
25
|
// Override send method to intercept HTML responses
|
|
26
26
|
res.send = function (body) {
|
|
27
27
|
// Only process HTML responses
|
|
28
|
-
if (typeof body === 'string' && isHtmlResponse(res)) {
|
|
28
|
+
if (typeof body === 'string' && isHtmlResponse(res) && looksLikeHtmlDocument(body)) {
|
|
29
29
|
try {
|
|
30
30
|
// Create scanner
|
|
31
31
|
const scanner = new index_1.WCAGScanner(defaultOptions);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
const response = this;
|
|
33
|
+
// If we do not need to mutate the response or set headers, scan after sending.
|
|
34
|
+
if (!requiresBlockingScan(defaultOptions)) {
|
|
35
|
+
void scanner.loadHTML(body)
|
|
36
|
+
.then(() => scanner.scan())
|
|
37
|
+
.then((results) => {
|
|
38
|
+
if (defaultOptions.onViolation && results.violations.length > 0) {
|
|
39
|
+
defaultOptions.onViolation(results, req, res);
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
.catch((error) => {
|
|
43
|
+
console.error('Error in WCAG scanner middleware:', error);
|
|
44
|
+
});
|
|
45
|
+
return originalSend.call(response, body);
|
|
46
|
+
}
|
|
47
|
+
// Run scan before sending when we need to add headers or inline report.
|
|
48
|
+
void scanner.loadHTML(body)
|
|
49
|
+
.then(() => scanner.scan())
|
|
50
|
+
.then((results) => {
|
|
51
|
+
let finalBody = body;
|
|
52
|
+
if (defaultOptions.headerName) {
|
|
53
|
+
res.setHeader(defaultOptions.headerName, results.violations.length.toString());
|
|
54
|
+
}
|
|
39
55
|
if (defaultOptions.onViolation && results.violations.length > 0) {
|
|
40
56
|
defaultOptions.onViolation(results, req, res);
|
|
41
57
|
}
|
|
42
|
-
// Add inline report if enabled
|
|
43
58
|
if (defaultOptions.inlineReport && results.violations.length > 0) {
|
|
44
|
-
|
|
59
|
+
finalBody = insertInlineReport(body, results);
|
|
45
60
|
}
|
|
46
|
-
|
|
47
|
-
originalSend.call(
|
|
48
|
-
})
|
|
61
|
+
res.send = originalSend;
|
|
62
|
+
originalSend.call(response, finalBody);
|
|
63
|
+
})
|
|
64
|
+
.catch((error) => {
|
|
49
65
|
console.error('Error in WCAG scanner middleware:', error);
|
|
50
|
-
|
|
51
|
-
originalSend.call(
|
|
66
|
+
res.send = originalSend;
|
|
67
|
+
originalSend.call(response, body);
|
|
52
68
|
});
|
|
53
|
-
// Return a dummy response to prevent Express from sending twice
|
|
54
69
|
return res;
|
|
55
70
|
}
|
|
56
71
|
catch (error) {
|
|
@@ -93,6 +108,15 @@ function isHtmlResponse(res) {
|
|
|
93
108
|
const contentType = res.get('Content-Type') || '';
|
|
94
109
|
return contentType.includes('html');
|
|
95
110
|
}
|
|
111
|
+
function needsScanning(options) {
|
|
112
|
+
return Boolean(options.inlineReport || options.headerName || options.onViolation);
|
|
113
|
+
}
|
|
114
|
+
function requiresBlockingScan(options) {
|
|
115
|
+
return Boolean(options.inlineReport || options.headerName);
|
|
116
|
+
}
|
|
117
|
+
function looksLikeHtmlDocument(body) {
|
|
118
|
+
return /<(html|body|main|div|section|article|img|svg|form|a|button)\b/i.test(body);
|
|
119
|
+
}
|
|
96
120
|
/**
|
|
97
121
|
* Insert inline accessibility report into HTML
|
|
98
122
|
* @param html Original HTML
|