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 CHANGED
@@ -1,4 +1,4 @@
1
- <img src="./Semantically-logo.png" alt="Semantica11y logo" width="500" height="350">
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 default rules that check semantic HTML, ARIA usage, headings, landmarks, forms, images, disclosure controls, modal dialogs, and native label conflicts.
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](./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.0.0",
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
- "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"
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 apply (uses defaults if not provided)
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.options = {
18
- includeWarnings: true,
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
+ }
@@ -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 extend/override defaults
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.rules) {
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((r) => r.enabled);
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
- Passes when an image has `alt` and no conflicting `aria-label`, or when
189
- `aria-label` exactly matches the `alt` text.
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
 
@@ -6,6 +6,7 @@ export const ariaExpandedRule = {
6
6
  id: 'aria-expanded',
7
7
  name: 'ARIA expanded disclosure',
8
8
  enabled: true,
9
+ experimental: true,
9
10
  description: 'Detects aria-expanded usage that could use native disclosure elements',
10
11
  check(document) {
11
12
  const issues = [];
@@ -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>, or add an appropriate action role',
38
+ suggestion: 'Use a native action element such as <button> or <a>',
39
39
  line: getLineNumber(element),
40
40
  };
41
41
  }
package/src/index.js CHANGED
@@ -12,3 +12,5 @@ export {
12
12
  formatConsoleReport,
13
13
  printConsoleReport,
14
14
  } from './engine/reporter/index.js';
15
+
16
+ export { configure, resetConfig } from './config.js';