semantica11y 1.0.0 โ 1.1.1
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 +48 -8
- package/package.json +4 -11
- package/src/analyzer.js +6 -6
- package/src/config.js +49 -0
- package/src/engine/index.js +13 -5
- package/src/engine/rules/README.md +10 -2
- package/src/engine/rules/aria-expanded.js +1 -0
- package/src/engine/rules/image-alt.js +18 -0
- package/src/engine/rules/missing-role-action.js +1 -1
- package/src/index.js +2 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<img src="
|
|
1
|
+
<img src="https://media.licdn.com/dms/image/v2/D5622AQGesCZdmU6big/feedshare-shrink_800/B56Z5fRmg6KcAc-/0/1779714910497?e=2147483647&v=beta&t=WrvuX6KsIjAy6aVgzPf3C9K27W5CL3CiR4ngtqHKvJc" alt="Semantica11y logo" width="500" height="350">
|
|
2
2
|
|
|
3
3
|
# Semantica11y
|
|
4
4
|
|
|
@@ -53,6 +53,51 @@ const results = await analyzer.analyzeHTML(htmlString, 'https://example.com');
|
|
|
53
53
|
console.log(analyzer.formatResults(results));
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
### Configuration (1.1.1)
|
|
57
|
+
|
|
58
|
+
Configure shared defaults once before creating analyzers:
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
import { Analyzer, configure, resetConfig } from 'semantica11y';
|
|
62
|
+
|
|
63
|
+
configure({
|
|
64
|
+
severities: ['error'], // Report errors only
|
|
65
|
+
enabledRules: ['image-alt', 'missing-form-labels'],
|
|
66
|
+
experimental: false,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const analyzer = new Analyzer();
|
|
70
|
+
const results = await analyzer.analyzeHTML(htmlString);
|
|
71
|
+
|
|
72
|
+
// Instance options override shared defaults.
|
|
73
|
+
const disclosures = new Analyzer({
|
|
74
|
+
enabledRules: ['aria-expanded'],
|
|
75
|
+
severities: ['warning'],
|
|
76
|
+
experimental: true,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
resetConfig(); // Restore defaults for future instances
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Option | Default | Behavior |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `severities` | `['error', 'warning', 'suggestion']` | Include only these finding severities; `[]` returns no findings. |
|
|
85
|
+
| `enabledRules` | `null` | Run all eligible rules, or only the listed rule IDs; `[]` runs none. |
|
|
86
|
+
| `experimental` | `false` | Opt into experimental rules, currently `aria-expanded`. |
|
|
87
|
+
| `includeWarnings` | `true` | Set to `false` to exclude warnings while retaining selected errors and suggestions. |
|
|
88
|
+
|
|
89
|
+
Rule selection controls which checks execute. Severity filtering applies to the
|
|
90
|
+
findings because a single rule can produce multiple severities. Summary counts
|
|
91
|
+
and reports reflect the filtered findings. Experimental rules require
|
|
92
|
+
`experimental: true` even when listed in `enabledRules`; rules with
|
|
93
|
+
`enabled: false` remain disabled. Unknown rule IDs match no rules.
|
|
94
|
+
|
|
95
|
+
Shared settings are copied when an instance is created; later configuration
|
|
96
|
+
changes do not affect existing instances. Custom rules continue to be appended
|
|
97
|
+
using `rules` and follow the same filters; mark a custom rule with
|
|
98
|
+
`experimental: true` to require opt-in. Direct engine users can pass options as
|
|
99
|
+
`new RuleEngine(customRules, options)`; engines also inherit shared defaults.
|
|
100
|
+
|
|
56
101
|
### Reports
|
|
57
102
|
|
|
58
103
|
```javascript
|
|
@@ -87,9 +132,9 @@ const analyzer = new Analyzer({ rules: customRules });
|
|
|
87
132
|
|
|
88
133
|
## ๐ Default Rules
|
|
89
134
|
|
|
90
|
-
Semantica11y ships with 11
|
|
135
|
+
Semantica11y ships with 11 built-in rules (10 enabled by default and one experimental rule) that check semantic HTML, ARIA usage, headings, landmarks, forms, images, disclosure controls, modal dialogs, and native label conflicts.
|
|
91
136
|
|
|
92
|
-
For the full rule-by-rule reference, see [src/engine/rules/README.md](
|
|
137
|
+
For the full rule-by-rule reference, see [src/engine/rules/README.md](https://github.com/Steady5063/Semantica11y/tree/main/src/engine/rules).
|
|
93
138
|
|
|
94
139
|
## ๐งช Testing
|
|
95
140
|
|
|
@@ -111,11 +156,6 @@ Run the Playwright example against `https://example.com`:
|
|
|
111
156
|
node examples/basic.js
|
|
112
157
|
```
|
|
113
158
|
|
|
114
|
-
Analyze a different page:
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
node examples/basic.js https://www.statefarm.com
|
|
118
|
-
```
|
|
119
159
|
|
|
120
160
|
## ๐ฆ Build Package
|
|
121
161
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "semantica11y",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A JavaScript engine to check webpages for ARIA and non-semantic HTML elements with suggestions for improvements",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -18,12 +18,7 @@
|
|
|
18
18
|
"./reporter": "./src/engine/reporter/index.js"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"
|
|
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"
|
|
21
|
+
"test": "node --test test/**/*.test.js"
|
|
27
22
|
},
|
|
28
23
|
"keywords": [
|
|
29
24
|
"accessibility",
|
|
@@ -37,9 +32,7 @@
|
|
|
37
32
|
"author": "",
|
|
38
33
|
"license": "MIT",
|
|
39
34
|
"dependencies": {
|
|
40
|
-
"jsdom": "^24.0.0"
|
|
41
|
-
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"playwright": "^1.60.0"
|
|
35
|
+
"jsdom": "^24.0.0",
|
|
36
|
+
"semantica11y": "^1.0.0"
|
|
44
37
|
}
|
|
45
38
|
}
|
package/src/analyzer.js
CHANGED
|
@@ -10,15 +10,15 @@ export class Analyzer {
|
|
|
10
10
|
/**
|
|
11
11
|
* Creates a new Analyzer instance
|
|
12
12
|
* @param {Object} options - Configuration options
|
|
13
|
-
* @param {Array} options.rules - Custom rules to
|
|
13
|
+
* @param {Array} options.rules - Custom rules to append to defaults
|
|
14
|
+
* @param {string[]} options.severities - Issue severities to include
|
|
15
|
+
* @param {string[]|null} options.enabledRules - Rule IDs to run (null runs all eligible rules)
|
|
16
|
+
* @param {boolean} options.experimental - Enable experimental rules (default: false)
|
|
14
17
|
* @param {boolean} options.includeWarnings - Include warning-level issues (default: true)
|
|
15
18
|
*/
|
|
16
19
|
constructor(options = {}) {
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
...options,
|
|
20
|
-
};
|
|
21
|
-
this.ruleEngine = new RuleEngine(options.rules);
|
|
20
|
+
this.ruleEngine = new RuleEngine(options.rules, options);
|
|
21
|
+
this.options = this.ruleEngine.options;
|
|
22
22
|
this.results = null;
|
|
23
23
|
}
|
|
24
24
|
|
package/src/config.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Shared defaults for newly created analyzers and rule engines. */
|
|
2
|
+
const DEFAULT_CONFIG = {
|
|
3
|
+
severities: ['error', 'warning', 'suggestion'],
|
|
4
|
+
enabledRules: null,
|
|
5
|
+
experimental: false,
|
|
6
|
+
includeWarnings: true,
|
|
7
|
+
};
|
|
8
|
+
let globalConfig = { ...DEFAULT_CONFIG };
|
|
9
|
+
|
|
10
|
+
function copyConfig(config) {
|
|
11
|
+
return {
|
|
12
|
+
...config,
|
|
13
|
+
severities: [...config.severities],
|
|
14
|
+
enabledRules: config.enabledRules === null ? null : [...config.enabledRules],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveConfig(options = {}) {
|
|
19
|
+
const config = { ...globalConfig, ...options };
|
|
20
|
+
if (!Array.isArray(config.severities) ||
|
|
21
|
+
config.severities.some((value) => !DEFAULT_CONFIG.severities.includes(value))) {
|
|
22
|
+
throw new TypeError('severities must be an array of error, warning, or suggestion');
|
|
23
|
+
}
|
|
24
|
+
if (config.enabledRules !== null &&
|
|
25
|
+
(!Array.isArray(config.enabledRules) ||
|
|
26
|
+
config.enabledRules.some((id) => typeof id !== 'string' || !id))) {
|
|
27
|
+
throw new TypeError('enabledRules must be null or an array of rule IDs');
|
|
28
|
+
}
|
|
29
|
+
for (const key of ['experimental', 'includeWarnings']) {
|
|
30
|
+
if (typeof config[key] !== 'boolean') {
|
|
31
|
+
throw new TypeError(`${key} must be a boolean`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return copyConfig(config);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Update shared defaults. Instance options take precedence. */
|
|
38
|
+
export function configure(options = {}) {
|
|
39
|
+
const config = resolveConfig(options);
|
|
40
|
+
globalConfig = Object.fromEntries(
|
|
41
|
+
Object.keys(DEFAULT_CONFIG).map((key) => [key, config[key]])
|
|
42
|
+
);
|
|
43
|
+
return copyConfig(globalConfig);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Restore the built-in defaults for future instances. */
|
|
47
|
+
export function resetConfig() {
|
|
48
|
+
globalConfig = copyConfig(DEFAULT_CONFIG);
|
|
49
|
+
}
|
package/src/engine/index.js
CHANGED
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { DEFAULT_RULES } from './definitions.js';
|
|
6
|
+
import { resolveConfig } from '../config.js';
|
|
6
7
|
|
|
7
8
|
export class RuleEngine {
|
|
8
9
|
/**
|
|
9
10
|
* Creates a new RuleEngine instance
|
|
10
|
-
* @param {Array} customRules - Optional custom rules to
|
|
11
|
+
* @param {Array} customRules - Optional custom rules to append to defaults
|
|
12
|
+
* @param {Object} options - Shared configuration overrides
|
|
11
13
|
*/
|
|
12
|
-
constructor(customRules = []) {
|
|
14
|
+
constructor(customRules = [], options = {}) {
|
|
15
|
+
this.options = resolveConfig(options);
|
|
13
16
|
this.rules = [...DEFAULT_RULES, ...customRules];
|
|
14
17
|
}
|
|
15
18
|
|
|
@@ -27,8 +30,7 @@ export class RuleEngine {
|
|
|
27
30
|
* @param {Object} results - Results object to populate
|
|
28
31
|
*/
|
|
29
32
|
async analyze(document, results) {
|
|
30
|
-
for (const rule of this.
|
|
31
|
-
if (!rule.enabled) continue;
|
|
33
|
+
for (const rule of this.getActiveRules()) {
|
|
32
34
|
|
|
33
35
|
try {
|
|
34
36
|
const issues = rule.check(document);
|
|
@@ -49,6 +51,8 @@ export class RuleEngine {
|
|
|
49
51
|
*/
|
|
50
52
|
addIssuesToResults(results, issues) {
|
|
51
53
|
issues.forEach((issue) => {
|
|
54
|
+
if (!this.options.severities.includes(issue.severity)) return;
|
|
55
|
+
if (!this.options.includeWarnings && issue.severity === 'warning') return;
|
|
52
56
|
results.issues.push(issue);
|
|
53
57
|
|
|
54
58
|
// Update summary counts
|
|
@@ -67,7 +71,11 @@ export class RuleEngine {
|
|
|
67
71
|
* @returns {Array} Active rules
|
|
68
72
|
*/
|
|
69
73
|
getActiveRules() {
|
|
70
|
-
return this.rules.filter((
|
|
74
|
+
return this.rules.filter((rule) =>
|
|
75
|
+
rule.enabled &&
|
|
76
|
+
(!rule.experimental || this.options.experimental) &&
|
|
77
|
+
(this.options.enabledRules === null || this.options.enabledRules.includes(rule.id))
|
|
78
|
+
);
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
/**
|
|
@@ -84,6 +84,8 @@ Exports: `ariaExpandedRule`
|
|
|
84
84
|
Rule id: `aria-expanded`
|
|
85
85
|
Severity: `warning`
|
|
86
86
|
|
|
87
|
+
Experimental: disabled by default. Enable with `experimental: true`.
|
|
88
|
+
|
|
87
89
|
Checks elements with `aria-expanded="true"` or `aria-expanded="false"`.
|
|
88
90
|
|
|
89
91
|
Individual checks:
|
|
@@ -180,13 +182,19 @@ Checks `img` elements for `alt` usage and conflicting ARIA labels.
|
|
|
180
182
|
|
|
181
183
|
Individual checks:
|
|
182
184
|
- Missing `alt`: reports an `error` when an image has no `alt` attribute.
|
|
185
|
+
- `aria-hidden="true"` or `role="presentation"` without `alt=""`: reports a
|
|
186
|
+
`warning` recommending native decorative markup. Missing `alt` still reports
|
|
187
|
+
an `error`; these attributes do not exempt an image from this semantic check.
|
|
188
|
+
With meaningful alt text, review whether the image is decorative or informative.
|
|
189
|
+
Using both attributes produces one combined warning.
|
|
183
190
|
- `aria-label` without `alt`: reports a `warning`.
|
|
184
191
|
- `aria-label` with `alt=""`: reports a `warning`.
|
|
185
192
|
- `aria-label` overriding `alt`: reports a `warning` when normalized
|
|
186
193
|
`aria-label` text differs from normalized `alt` text.
|
|
187
194
|
|
|
188
|
-
|
|
189
|
-
`aria-label`
|
|
195
|
+
The new decorative-image checks pass with `alt=""` (including bare `alt`).
|
|
196
|
+
Existing `aria-label` conflict checks still apply. Otherwise, images pass with
|
|
197
|
+
`alt` and no conflicting `aria-label`, or matching normalized label text.
|
|
190
198
|
|
|
191
199
|
## `native-label.js`
|
|
192
200
|
|
|
@@ -32,6 +32,24 @@ export const imageAltRule = {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const decorativeAttributes = [];
|
|
36
|
+
if (image.getAttribute('aria-hidden')?.trim().toLowerCase() === 'true') {
|
|
37
|
+
decorativeAttributes.push('aria-hidden="true"');
|
|
38
|
+
}
|
|
39
|
+
if (image.getAttribute('role')?.trim().toLowerCase() === 'presentation') {
|
|
40
|
+
decorativeAttributes.push('role="presentation"');
|
|
41
|
+
}
|
|
42
|
+
if (decorativeAttributes.length && (!hasAltAttribute || altText !== '')) {
|
|
43
|
+
issues.push({
|
|
44
|
+
severity: 'warning',
|
|
45
|
+
rule: 'image-alt',
|
|
46
|
+
element: getElementSignature(image),
|
|
47
|
+
message: `Image uses ${decorativeAttributes.join(' and ')} without alt=""`,
|
|
48
|
+
suggestion: 'Use alt="" for decorative images. If the image conveys information, keep meaningful alt text and remove the attributes that hide its semantics',
|
|
49
|
+
line: getLineNumber(image),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
if (!hasAriaLabel) {
|
|
36
54
|
return;
|
|
37
55
|
}
|
|
@@ -35,7 +35,7 @@ function createIssue(element, message) {
|
|
|
35
35
|
rule: 'missing-role-action',
|
|
36
36
|
element: getElementSignature(element),
|
|
37
37
|
message,
|
|
38
|
-
suggestion: 'Use a native action element such as <button> or <a
|
|
38
|
+
suggestion: 'Use a native action element such as <button> or <a>',
|
|
39
39
|
line: getLineNumber(element),
|
|
40
40
|
};
|
|
41
41
|
}
|