semantica11y 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.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/Semantically-logo.png +0 -0
- package/package.json +45 -0
- package/src/analyzer.js +84 -0
- package/src/engine/aria-structure-role-mappings.js +131 -0
- package/src/engine/definitions.js +37 -0
- package/src/engine/index.js +83 -0
- package/src/engine/reporter/index.js +127 -0
- package/src/engine/rules/README.md +232 -0
- package/src/engine/rules/aria-actions.js +109 -0
- package/src/engine/rules/aria-expanded.js +32 -0
- package/src/engine/rules/aria-landmarks.js +36 -0
- package/src/engine/rules/aria-modal.js +48 -0
- package/src/engine/rules/aria-structure.js +37 -0
- package/src/engine/rules/heading-hierarchy.js +45 -0
- package/src/engine/rules/image-alt.js +77 -0
- package/src/engine/rules/missing-form-labels.js +49 -0
- package/src/engine/rules/missing-key-landmark.js +63 -0
- package/src/engine/rules/missing-role-action.js +77 -0
- package/src/engine/rules/native-label.js +113 -0
- package/src/engine/semantic-role-mappings.js +51 -0
- package/src/engine/utils.js +27 -0
- package/src/index.js +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Semantica11y Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<img src="./Semantically-logo.png" alt="Semantica11y logo" width="500" height="350">
|
|
2
|
+
|
|
3
|
+
# Semantica11y
|
|
4
|
+
|
|
5
|
+
A JavaScript analysis engine for checking webpages (HTML) for non-semantic HTML elements with ARIA using intelligent suggestions for semantic improvements.
|
|
6
|
+
|
|
7
|
+
## 🎯 Purpose
|
|
8
|
+
|
|
9
|
+
Using semantic and native HTML elements is the foundation to building an accessible webpage. Semantica11y is here to help ensure that when you build your web applications, it is build semantically first, for better accessibility and long term code sustainability!
|
|
10
|
+
|
|
11
|
+
## 📦 Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install semantica11y
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 🚀 Quick Start
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
import { Analyzer } from 'semantica11y';
|
|
21
|
+
|
|
22
|
+
const analyzer = new Analyzer();
|
|
23
|
+
|
|
24
|
+
const html = `
|
|
25
|
+
<html>
|
|
26
|
+
<body>
|
|
27
|
+
<div role="banner">Header</div>
|
|
28
|
+
<img src="logo.png" />
|
|
29
|
+
<form>
|
|
30
|
+
<input type="text" id="name" />
|
|
31
|
+
</form>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const results = await analyzer.analyzeHTML(html);
|
|
37
|
+
console.log(analyzer.formatResults(results));
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 📖 Usage
|
|
41
|
+
|
|
42
|
+
### Basic Analysis
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
import { Analyzer } from 'semantica11y';
|
|
46
|
+
|
|
47
|
+
const analyzer = new Analyzer();
|
|
48
|
+
|
|
49
|
+
// Analyze HTML string
|
|
50
|
+
const results = await analyzer.analyzeHTML(htmlString, 'https://example.com');
|
|
51
|
+
|
|
52
|
+
// Get formatted output
|
|
53
|
+
console.log(analyzer.formatResults(results));
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Reports
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
import { Analyzer, exportTextReport, formatConsoleReport } from 'semantica11y';
|
|
60
|
+
|
|
61
|
+
const analyzer = new Analyzer();
|
|
62
|
+
const results = await analyzer.analyzeHTML(html);
|
|
63
|
+
|
|
64
|
+
console.log(formatConsoleReport(results));
|
|
65
|
+
await exportTextReport(results, './semantica11y-report.txt');
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Custom Rules
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
const customRules = [
|
|
72
|
+
{
|
|
73
|
+
id: 'custom-rule',
|
|
74
|
+
name: 'My Custom Rule',
|
|
75
|
+
enabled: true,
|
|
76
|
+
description: 'Custom accessibility check',
|
|
77
|
+
check(document) {
|
|
78
|
+
const issues = [];
|
|
79
|
+
// Your custom check logic
|
|
80
|
+
return issues;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const analyzer = new Analyzer({ rules: customRules });
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 📋 Default Rules
|
|
89
|
+
|
|
90
|
+
Semantica11y ships with 11 default rules that check semantic HTML, ARIA usage, headings, landmarks, forms, images, disclosure controls, modal dialogs, and native label conflicts.
|
|
91
|
+
|
|
92
|
+
For the full rule-by-rule reference, see [src/engine/rules/README.md](./src/engine/rules/README.md).
|
|
93
|
+
|
|
94
|
+
## 🧪 Testing
|
|
95
|
+
|
|
96
|
+
Run the test suite:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm test
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Run tests in watch mode:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm run test:watch
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Run the Playwright example against `https://example.com`:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
node examples/basic.js
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Analyze a different page:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
node examples/basic.js https://www.statefarm.com
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## 📦 Build Package
|
|
121
|
+
|
|
122
|
+
Create a clean package directory:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm run build
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Create a tarball from the build output:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npm pack ./dist
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 🏗️ Project Structure
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
semantica11y/
|
|
138
|
+
├── src/
|
|
139
|
+
│ ├── index.js # Main export
|
|
140
|
+
│ ├── analyzer.js # Core analyzer class
|
|
141
|
+
│ └── engine/
|
|
142
|
+
│ ├── index.js # RuleEngine class
|
|
143
|
+
│ ├── definitions.js # Default rule registry
|
|
144
|
+
│ ├── reporter/ # Report formatting and exporting
|
|
145
|
+
│ ├── rules/ # Individual rule definitions
|
|
146
|
+
│ ├── semantic-role-mappings.js
|
|
147
|
+
│ └── utils.js
|
|
148
|
+
├── examples/
|
|
149
|
+
│ └── basic.js # Usage example
|
|
150
|
+
├── test/
|
|
151
|
+
│ ├── analyzer.test.js # Analyzer tests
|
|
152
|
+
│ └── rules.test.js # RuleEngine tests
|
|
153
|
+
├── package.json
|
|
154
|
+
└── README.md
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 🤝 Contributing
|
|
158
|
+
|
|
159
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
160
|
+
|
|
161
|
+
## 📄 License
|
|
162
|
+
|
|
163
|
+
MIT
|
|
164
|
+
|
|
165
|
+
## 🔗 Resources
|
|
166
|
+
|
|
167
|
+
- [WCAG Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
|
|
168
|
+
- [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
|
|
169
|
+
- [Semantic HTML](https://developer.mozilla.org/en-US/docs/Glossary/Semantic_HTML)
|
|
170
|
+
- [Web Accessibility](https://www.w3.org/WAI/)
|
|
171
|
+
|
|
172
|
+
## 📞 Support
|
|
173
|
+
|
|
174
|
+
For issues, questions, or suggestions, please create an issue in the repository.
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "semantica11y",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A JavaScript engine to check webpages for ARIA and non-semantic HTML elements with suggestions for improvements",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"Semantically-logo.png"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./src/index.js",
|
|
15
|
+
"./analyzer": "./src/analyzer.js",
|
|
16
|
+
"./rules": "./src/engine/index.js",
|
|
17
|
+
"./engine": "./src/engine/index.js",
|
|
18
|
+
"./reporter": "./src/engine/reporter/index.js"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "node scripts/build-package.js",
|
|
22
|
+
"pack:dist": "npm run build && npm pack ./dist",
|
|
23
|
+
"test": "node --test test/**/*.test.js",
|
|
24
|
+
"test:watch": "node --test test/**/*.test.js --watch",
|
|
25
|
+
"lint": "echo 'Add eslint configuration'",
|
|
26
|
+
"example": "node examples/basic.js"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"accessibility",
|
|
30
|
+
"a11y",
|
|
31
|
+
"ARIA",
|
|
32
|
+
"semantic",
|
|
33
|
+
"HTML",
|
|
34
|
+
"web-standards",
|
|
35
|
+
"analyzer"
|
|
36
|
+
],
|
|
37
|
+
"author": "",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"jsdom": "^24.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"playwright": "^1.60.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/analyzer.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Analyzer - Core analysis engine for semantic and ARIA compliance
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { JSDOM } from 'jsdom';
|
|
6
|
+
import { RuleEngine } from './engine/index.js';
|
|
7
|
+
import { formatConsoleReport } from './engine/reporter/index.js';
|
|
8
|
+
|
|
9
|
+
export class Analyzer {
|
|
10
|
+
/**
|
|
11
|
+
* Creates a new Analyzer instance
|
|
12
|
+
* @param {Object} options - Configuration options
|
|
13
|
+
* @param {Array} options.rules - Custom rules to apply (uses defaults if not provided)
|
|
14
|
+
* @param {boolean} options.includeWarnings - Include warning-level issues (default: true)
|
|
15
|
+
*/
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.options = {
|
|
18
|
+
includeWarnings: true,
|
|
19
|
+
...options,
|
|
20
|
+
};
|
|
21
|
+
this.ruleEngine = new RuleEngine(options.rules);
|
|
22
|
+
this.results = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Analyze HTML content for semantic and ARIA issues
|
|
27
|
+
* @param {string} html - HTML content to analyze
|
|
28
|
+
* @param {string} url - Optional URL for context
|
|
29
|
+
* @returns {Promise<Object>} Analysis results with issues and suggestions
|
|
30
|
+
*/
|
|
31
|
+
async analyzeHTML(html, url = '') {
|
|
32
|
+
try {
|
|
33
|
+
const dom = new JSDOM(html, url ? { url } : undefined);
|
|
34
|
+
const document = dom.window.document;
|
|
35
|
+
|
|
36
|
+
this.results = {
|
|
37
|
+
url,
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
summary: {
|
|
40
|
+
total: 0,
|
|
41
|
+
errors: 0,
|
|
42
|
+
warnings: 0,
|
|
43
|
+
suggestions: 0,
|
|
44
|
+
},
|
|
45
|
+
issues: [],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Analyze the document with all rules
|
|
49
|
+
await this.ruleEngine.analyze(document, this.results);
|
|
50
|
+
|
|
51
|
+
return this.results;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(`Failed to analyze HTML: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Analyze HTML from a file
|
|
59
|
+
* @param {string} filePath - Path to HTML file
|
|
60
|
+
* @returns {Promise<Object>} Analysis results
|
|
61
|
+
*/
|
|
62
|
+
async analyzeFile(filePath) {
|
|
63
|
+
const fs = await import('fs').then((m) => m.promises);
|
|
64
|
+
const html = await fs.readFile(filePath, 'utf-8');
|
|
65
|
+
return this.analyzeHTML(html, `file://${filePath}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get results from the last analysis
|
|
70
|
+
* @returns {Object|null} Last analysis results or null if no analysis performed
|
|
71
|
+
*/
|
|
72
|
+
getResults() {
|
|
73
|
+
return this.results;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format results for display
|
|
78
|
+
* @param {Object} results - Analysis results
|
|
79
|
+
* @returns {string} Formatted output
|
|
80
|
+
*/
|
|
81
|
+
formatResults(results = this.results) {
|
|
82
|
+
return formatConsoleReport(results);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
function tagName(element) {
|
|
2
|
+
return element.tagName.toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export const ARIA_STRUCTURE_ROLE_MAPPINGS = [
|
|
6
|
+
{
|
|
7
|
+
role: 'article',
|
|
8
|
+
semanticElement: '<article>',
|
|
9
|
+
isSemanticMatch: (element) => tagName(element) === 'article',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
role: 'blockquote',
|
|
13
|
+
semanticElement: '<blockquote>',
|
|
14
|
+
isSemanticMatch: (element) => tagName(element) === 'blockquote',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
role: 'caption',
|
|
18
|
+
semanticElement: '<caption>',
|
|
19
|
+
isSemanticMatch: (element) => tagName(element) === 'caption',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
role: 'cell',
|
|
23
|
+
semanticElement: '<td>',
|
|
24
|
+
isSemanticMatch: (element) => tagName(element) === 'td',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
role: 'code',
|
|
28
|
+
semanticElement: '<code>',
|
|
29
|
+
isSemanticMatch: (element) => tagName(element) === 'code',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
role: 'columnheader',
|
|
33
|
+
semanticElement: '<th scope="col">',
|
|
34
|
+
isSemanticMatch: (element) => tagName(element) === 'th',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
role: 'definition',
|
|
38
|
+
semanticElement: '<dfn>',
|
|
39
|
+
isSemanticMatch: (element) => tagName(element) === 'dfn',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
role: 'deletion',
|
|
43
|
+
semanticElement: '<del>',
|
|
44
|
+
isSemanticMatch: (element) => tagName(element) === 'del',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
role: 'emphasis',
|
|
48
|
+
semanticElement: '<em>',
|
|
49
|
+
isSemanticMatch: (element) => tagName(element) === 'em',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
role: 'figure',
|
|
53
|
+
semanticElement: '<figure>',
|
|
54
|
+
isSemanticMatch: (element) => tagName(element) === 'figure',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
role: 'generic',
|
|
58
|
+
semanticElement: 'a more specific semantic element or remove role="generic"',
|
|
59
|
+
isSemanticMatch: () => false,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
role: 'heading',
|
|
63
|
+
semanticElement: '<h1> through <h6>',
|
|
64
|
+
isSemanticMatch: (element) => /^h[1-6]$/i.test(element.tagName),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
role: 'list',
|
|
68
|
+
semanticElement: '<ul> or <ol>',
|
|
69
|
+
isSemanticMatch: (element) => ['ul', 'ol'].includes(tagName(element)),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
role: 'listitem',
|
|
73
|
+
semanticElement: '<li>',
|
|
74
|
+
isSemanticMatch: (element) => tagName(element) === 'li',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
role: 'paragraph',
|
|
78
|
+
semanticElement: '<p>',
|
|
79
|
+
isSemanticMatch: (element) => tagName(element) === 'p',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
role: 'row',
|
|
83
|
+
semanticElement: '<tr>',
|
|
84
|
+
isSemanticMatch: (element) => tagName(element) === 'tr',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
role: 'rowgroup',
|
|
88
|
+
semanticElement: '<thead>, <tbody>, or <tfoot>',
|
|
89
|
+
isSemanticMatch: (element) => ['thead', 'tbody', 'tfoot'].includes(tagName(element)),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
role: 'rowheader',
|
|
93
|
+
semanticElement: '<th scope="row">',
|
|
94
|
+
isSemanticMatch: (element) => tagName(element) === 'th',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
role: 'separator',
|
|
98
|
+
semanticElement: '<hr>',
|
|
99
|
+
isSemanticMatch: (element) => tagName(element) === 'hr',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
role: 'strong',
|
|
103
|
+
semanticElement: '<strong>',
|
|
104
|
+
isSemanticMatch: (element) => tagName(element) === 'strong',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
role: 'subscript',
|
|
108
|
+
semanticElement: '<sub>',
|
|
109
|
+
isSemanticMatch: (element) => tagName(element) === 'sub',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
role: 'superscript',
|
|
113
|
+
semanticElement: '<sup>',
|
|
114
|
+
isSemanticMatch: (element) => tagName(element) === 'sup',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
role: 'table',
|
|
118
|
+
semanticElement: '<table>',
|
|
119
|
+
isSemanticMatch: (element) => tagName(element) === 'table',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
role: 'term',
|
|
123
|
+
semanticElement: '<dt>',
|
|
124
|
+
isSemanticMatch: (element) => tagName(element) === 'dt',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
role: 'time',
|
|
128
|
+
semanticElement: '<time>',
|
|
129
|
+
isSemanticMatch: (element) => tagName(element) === 'time',
|
|
130
|
+
},
|
|
131
|
+
];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Definitions - Semantic and ARIA compliance rules
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ariaActionsRule } from './rules/aria-actions.js';
|
|
6
|
+
import { ariaExpandedRule } from './rules/aria-expanded.js';
|
|
7
|
+
import { ariaModalRule } from './rules/aria-modal.js';
|
|
8
|
+
import { ariaStructureRule } from './rules/aria-structure.js';
|
|
9
|
+
import { headingHierarchyRule } from './rules/heading-hierarchy.js';
|
|
10
|
+
import { imageAltRule } from './rules/image-alt.js';
|
|
11
|
+
import { missingFormLabelsRule } from './rules/missing-form-labels.js';
|
|
12
|
+
import { missingKeyLandmarkRule } from './rules/missing-key-landmark.js';
|
|
13
|
+
import { missingRoleActionRule } from './rules/missing-role-action.js';
|
|
14
|
+
import { nativeLabelRule } from './rules/native-label.js';
|
|
15
|
+
import { ariaLandmarksRule } from './rules/aria-landmarks.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default set of rules for semantic HTML and ARIA analysis
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_RULES = [
|
|
21
|
+
ariaLandmarksRule,
|
|
22
|
+
missingFormLabelsRule,
|
|
23
|
+
headingHierarchyRule,
|
|
24
|
+
ariaActionsRule,
|
|
25
|
+
ariaExpandedRule,
|
|
26
|
+
ariaModalRule,
|
|
27
|
+
ariaStructureRule,
|
|
28
|
+
missingRoleActionRule,
|
|
29
|
+
imageAltRule,
|
|
30
|
+
nativeLabelRule,
|
|
31
|
+
missingKeyLandmarkRule,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Placeholder for custom rules users might define
|
|
36
|
+
*/
|
|
37
|
+
export const CUSTOM_RULES = [];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Engine - Core logic for semantic and ARIA analysis rules
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { DEFAULT_RULES } from './definitions.js';
|
|
6
|
+
|
|
7
|
+
export class RuleEngine {
|
|
8
|
+
/**
|
|
9
|
+
* Creates a new RuleEngine instance
|
|
10
|
+
* @param {Array} customRules - Optional custom rules to extend/override defaults
|
|
11
|
+
*/
|
|
12
|
+
constructor(customRules = []) {
|
|
13
|
+
this.rules = [...DEFAULT_RULES, ...customRules];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add a custom rule
|
|
18
|
+
* @param {Object} rule - Rule definition
|
|
19
|
+
*/
|
|
20
|
+
addRule(rule) {
|
|
21
|
+
this.rules.push(rule);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Analyze a document against all rules
|
|
26
|
+
* @param {Document} document - DOM document to analyze
|
|
27
|
+
* @param {Object} results - Results object to populate
|
|
28
|
+
*/
|
|
29
|
+
async analyze(document, results) {
|
|
30
|
+
for (const rule of this.rules) {
|
|
31
|
+
if (!rule.enabled) continue;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const issues = rule.check(document);
|
|
35
|
+
this.addIssuesToResults(results, issues);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(`Error running rule "${rule.id}":`, error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Update summary
|
|
42
|
+
results.summary.total = results.issues.length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Add issues from a rule to results
|
|
47
|
+
* @param {Object} results - Results object
|
|
48
|
+
* @param {Array} issues - Issues array from rule
|
|
49
|
+
*/
|
|
50
|
+
addIssuesToResults(results, issues) {
|
|
51
|
+
issues.forEach((issue) => {
|
|
52
|
+
results.issues.push(issue);
|
|
53
|
+
|
|
54
|
+
// Update summary counts
|
|
55
|
+
if (issue.severity === 'error') {
|
|
56
|
+
results.summary.errors++;
|
|
57
|
+
} else if (issue.severity === 'warning') {
|
|
58
|
+
results.summary.warnings++;
|
|
59
|
+
} else if (issue.severity === 'suggestion') {
|
|
60
|
+
results.summary.suggestions++;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get all active rules
|
|
67
|
+
* @returns {Array} Active rules
|
|
68
|
+
*/
|
|
69
|
+
getActiveRules() {
|
|
70
|
+
return this.rules.filter((r) => r.enabled);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get rule by ID
|
|
75
|
+
* @param {string} id - Rule ID
|
|
76
|
+
* @returns {Object|undefined} Rule definition or undefined
|
|
77
|
+
*/
|
|
78
|
+
getRule(id) {
|
|
79
|
+
return this.rules.find((r) => r.id === id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { DEFAULT_RULES };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { writeFile } from 'fs/promises';
|
|
2
|
+
|
|
3
|
+
const COLORS = {
|
|
4
|
+
reset: '\x1b[0m',
|
|
5
|
+
red: '\x1b[31m',
|
|
6
|
+
yellow: '\x1b[33m',
|
|
7
|
+
cyan: '\x1b[36m',
|
|
8
|
+
bold: '\x1b[1m',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const ISSUE_SECTIONS = [
|
|
12
|
+
{
|
|
13
|
+
severity: 'error',
|
|
14
|
+
title: 'Errors',
|
|
15
|
+
color: COLORS.red,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
severity: 'warning',
|
|
19
|
+
title: 'Warnings',
|
|
20
|
+
color: COLORS.yellow,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
severity: 'suggestion',
|
|
24
|
+
title: 'Suggestions',
|
|
25
|
+
color: COLORS.cyan,
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function colorize(text, color, useColors) {
|
|
30
|
+
return useColors ? `${color}${text}${COLORS.reset}` : text;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function groupIssues(issues) {
|
|
34
|
+
const groups = new Map();
|
|
35
|
+
|
|
36
|
+
issues.forEach((issue) => {
|
|
37
|
+
const key = JSON.stringify({
|
|
38
|
+
rule: issue.rule,
|
|
39
|
+
message: issue.message,
|
|
40
|
+
suggestion: issue.suggestion || '',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!groups.has(key)) {
|
|
44
|
+
groups.set(key, {
|
|
45
|
+
rule: issue.rule,
|
|
46
|
+
message: issue.message,
|
|
47
|
+
suggestion: issue.suggestion,
|
|
48
|
+
elements: [],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
groups.get(key).elements.push(issue.element);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return Array.from(groups.values());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatIssueGroup(group, index) {
|
|
59
|
+
const instanceLabel = group.elements.length === 1 ? 'instance' : 'instances';
|
|
60
|
+
let output = `\n${index + 1}. Rule: ${group.rule} (${group.elements.length} ${instanceLabel})\n`;
|
|
61
|
+
output += ` Message: ${group.message}\n`;
|
|
62
|
+
|
|
63
|
+
if (group.suggestion) {
|
|
64
|
+
output += ` Suggestion: ${group.suggestion}\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
output += ' Elements:\n';
|
|
68
|
+
group.elements.forEach((element) => {
|
|
69
|
+
output += ` - ${element}\n`;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return output;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatConsoleReport(results, options = {}) {
|
|
76
|
+
if (!results) return '';
|
|
77
|
+
|
|
78
|
+
const { colors = true } = options;
|
|
79
|
+
let output = `\nSemantica11y Analysis Report\n`;
|
|
80
|
+
output += `URL: ${results.url}\n`;
|
|
81
|
+
output += `Time: ${results.timestamp}\n`;
|
|
82
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
83
|
+
output += `Total Findings: ${results.summary.total}\n`;
|
|
84
|
+
output += ` Errors: ${results.summary.errors}\n`;
|
|
85
|
+
output += ` Warnings: ${results.summary.warnings}\n`;
|
|
86
|
+
output += ` Suggestions: ${results.summary.suggestions}\n`;
|
|
87
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
88
|
+
|
|
89
|
+
if (results.issues.length > 0) {
|
|
90
|
+
output += `\n${colorize('Findings By Category', COLORS.bold, colors)}\n`;
|
|
91
|
+
|
|
92
|
+
ISSUE_SECTIONS.forEach(({ severity, title, color }) => {
|
|
93
|
+
const issues = results.issues.filter((issue) => issue.severity === severity);
|
|
94
|
+
|
|
95
|
+
output += `\n${colorize(`${title} (${issues.length})`, color, colors)}\n`;
|
|
96
|
+
output += `${'─'.repeat(28)}\n`;
|
|
97
|
+
|
|
98
|
+
if (issues.length === 0) {
|
|
99
|
+
output += `No ${title.toLowerCase()} found.\n`;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
groupIssues(issues).forEach((group, index) => {
|
|
104
|
+
output += formatIssueGroup(group, index);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
output += `\nNo issues found!\n`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return output;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function printConsoleReport(results, options = {}) {
|
|
115
|
+
const report = formatConsoleReport(results, options);
|
|
116
|
+
console.log(report);
|
|
117
|
+
return report;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function exportTextReport(results, filePath, options = {}) {
|
|
121
|
+
const { colors = false } = options;
|
|
122
|
+
const report = formatConsoleReport(results, { colors });
|
|
123
|
+
|
|
124
|
+
await writeFile(filePath, report, 'utf8');
|
|
125
|
+
|
|
126
|
+
return filePath;
|
|
127
|
+
}
|