@surea11y/core 1.2.0 → 1.4.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.
Files changed (168) hide show
  1. package/CHANGELOG.md +81 -7
  2. package/LICENSE +373 -21
  3. package/README.md +175 -35
  4. package/bin/surea11y-core.js +20 -0
  5. package/docs/API_STABILITY.md +27 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/ENGINE_OPTIONS.md +2 -0
  9. package/docs/I18N.md +12 -9
  10. package/docs/INTEGRATION.md +19 -1
  11. package/docs/LIMITATIONS.md +1 -1
  12. package/docs/OUTPUT_SCHEMA.md +1 -1
  13. package/docs/REPORT.md +1 -1
  14. package/docs/RULE_CATALOG.md +1 -1
  15. package/docs/SARIF.md +59 -0
  16. package/package.json +63 -18
  17. package/src/baseline.js +0 -0
  18. package/src/checks/automatic/area-alt-present.js +63 -31
  19. package/src/checks/automatic/aria-allowed-attr.js +204 -80
  20. package/src/checks/automatic/aria-allowed-role.js +23 -7
  21. package/src/checks/automatic/aria-braille-equivalent.js +34 -10
  22. package/src/checks/automatic/aria-conditional-attr.js +32 -14
  23. package/src/checks/automatic/aria-deprecated-role.js +26 -11
  24. package/src/checks/automatic/aria-hidden-body.js +48 -23
  25. package/src/checks/automatic/aria-hidden-focus.js +420 -66
  26. package/src/checks/automatic/aria-prohibited-attr.js +327 -60
  27. package/src/checks/automatic/aria-prohibited-children.js +111 -103
  28. package/src/checks/automatic/aria-required-attr.js +29 -15
  29. package/src/checks/automatic/aria-required-children.js +44 -24
  30. package/src/checks/automatic/aria-required-parent.js +64 -35
  31. package/src/checks/automatic/aria-role-name-present.js +49 -21
  32. package/src/checks/automatic/aria-roles-valid.js +24 -12
  33. package/src/checks/automatic/aria-valid-attr-value.js +46 -22
  34. package/src/checks/automatic/aria-valid-attr.js +19 -5
  35. package/src/checks/automatic/autocomplete-valid.js +76 -16
  36. package/src/checks/automatic/avoid-inline-spacing.js +23 -8
  37. package/src/checks/automatic/binary-control-name-present.js +62 -50
  38. package/src/checks/automatic/button-name-present.js +54 -24
  39. package/src/checks/automatic/bypass-blocks-present.js +51 -32
  40. package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
  41. package/src/checks/automatic/combobox-name-present.js +40 -45
  42. package/src/checks/automatic/contrast-computable.js +363 -341
  43. package/src/checks/automatic/contrast-enhanced.js +489 -466
  44. package/src/checks/automatic/contrast-minimum.js +488 -465
  45. package/src/checks/automatic/css-orientation-lock.js +51 -35
  46. package/src/checks/automatic/definition-list-children-valid.js +46 -25
  47. package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
  48. package/src/checks/automatic/dialog-name-present.js +47 -85
  49. package/src/checks/automatic/dlitem-parent-valid.js +25 -8
  50. package/src/checks/automatic/duplicate-id-aria.js +28 -9
  51. package/src/checks/automatic/embed-text-alternative-present.js +88 -35
  52. package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
  53. package/src/checks/automatic/form-control-single-label.js +50 -14
  54. package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
  55. package/src/checks/automatic/iframe-focusable-content.js +265 -22
  56. package/src/checks/automatic/iframe-name-present.js +33 -9
  57. package/src/checks/automatic/iframe-title-unique.js +32 -9
  58. package/src/checks/automatic/img-alt-present.js +54 -52
  59. package/src/checks/automatic/input-image-alt-present.js +141 -112
  60. package/src/checks/automatic/label-in-name.js +65 -41
  61. package/src/checks/automatic/language-page-present.js +111 -109
  62. package/src/checks/automatic/link-in-text-block.js +61 -19
  63. package/src/checks/automatic/link-name-present.js +47 -14
  64. package/src/checks/automatic/list-children-valid.js +40 -33
  65. package/src/checks/automatic/listbox-name-present.js +41 -19
  66. package/src/checks/automatic/listitem-parent-valid.js +48 -13
  67. package/src/checks/automatic/menuitem-name-present.js +41 -61
  68. package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
  69. package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
  70. package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
  71. package/src/checks/automatic/meter-name-present.js +40 -36
  72. package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
  73. package/src/checks/automatic/object-text-alternative-present.js +93 -39
  74. package/src/checks/automatic/option-name-present.js +40 -21
  75. package/src/checks/automatic/page-title-present.js +19 -6
  76. package/src/checks/automatic/progressbar-name-present.js +49 -44
  77. package/src/checks/automatic/role-img-alt-present.js +211 -159
  78. package/src/checks/automatic/searchbox-name-present.js +41 -19
  79. package/src/checks/automatic/server-side-image-map-absent.js +27 -11
  80. package/src/checks/automatic/slider-name-present.js +42 -47
  81. package/src/checks/automatic/spinbutton-name-present.js +41 -19
  82. package/src/checks/automatic/summary-name-present.js +39 -17
  83. package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
  84. package/src/checks/automatic/svg-text-alternative-present.js +262 -230
  85. package/src/checks/automatic/tab-name-present.js +39 -60
  86. package/src/checks/automatic/table-headers-attr-valid.js +27 -10
  87. package/src/checks/automatic/table-th-has-data-cells.js +24 -8
  88. package/src/checks/automatic/target-size-minimum.js +123 -48
  89. package/src/checks/automatic/td-has-header.js +53 -12
  90. package/src/checks/automatic/textbox-name-present.js +41 -19
  91. package/src/checks/automatic/tooltip-name-present.js +39 -18
  92. package/src/checks/automatic/treeitem-name-present.js +40 -21
  93. package/src/checks/automatic/valid-lang.js +22 -6
  94. package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
  95. package/src/checks/manual/accesskeys-manual.js +17 -6
  96. package/src/checks/manual/area-alt-decorative-manual.js +194 -193
  97. package/src/checks/manual/area-alt-quality-manual.js +184 -141
  98. package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
  99. package/src/checks/manual/aria-text-manual.js +20 -11
  100. package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
  101. package/src/checks/manual/css-hidden-focus.js +375 -169
  102. package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
  103. package/src/checks/manual/empty-heading-manual.js +41 -24
  104. package/src/checks/manual/empty-table-header-manual.js +69 -31
  105. package/src/checks/manual/focus-order-semantics-manual.js +60 -13
  106. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
  107. package/src/checks/manual/heading-order-manual.js +50 -8
  108. package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
  109. package/src/checks/manual/image-redundant-alt-manual.js +38 -8
  110. package/src/checks/manual/img-alt-decorative-manual.js +133 -96
  111. package/src/checks/manual/img-alt-quality-manual.js +178 -127
  112. package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
  113. package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
  114. package/src/checks/manual/label-title-only-manual.js +44 -28
  115. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
  116. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
  117. package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
  118. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
  119. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
  120. package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
  121. package/src/checks/manual/landmark-one-main-manual.js +38 -43
  122. package/src/checks/manual/landmark-unique-manual.js +78 -67
  123. package/src/checks/manual/link-name-quality-manual.js +45 -12
  124. package/src/checks/manual/media-transcript-present-manual.js +37 -22
  125. package/src/checks/manual/meta-viewport-large-manual.js +19 -6
  126. package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
  127. package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
  128. package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
  129. package/src/checks/manual/p-as-heading-manual.js +24 -7
  130. package/src/checks/manual/page-has-heading-one-manual.js +42 -32
  131. package/src/checks/manual/page-title-patterns-manual.js +80 -50
  132. package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
  133. package/src/checks/manual/region-manual.js +244 -60
  134. package/src/checks/manual/scope-attr-valid-manual.js +13 -4
  135. package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
  136. package/src/checks/manual/skip-link-manual.js +42 -18
  137. package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
  138. package/src/checks/manual/tabindex-manual.js +13 -4
  139. package/src/checks/manual/table-duplicate-name-manual.js +22 -11
  140. package/src/checks/manual/table-fake-caption-manual.js +48 -10
  141. package/src/checks/manual/video-caption-manual.js +17 -4
  142. package/src/checks/manual-review.js +58 -12
  143. package/src/core.js +41705 -29650
  144. package/src/index.js +2 -0
  145. package/src/report.js +109 -47
  146. package/src/sarif.js +190 -0
  147. package/surea11y.browser.js +37774 -0
  148. package/bin/core.js +0 -348
  149. package/docs/CLI.md +0 -75
  150. package/src/catalogs/composites.wcag.js +0 -490
  151. package/src/checks/rules-and-tags.full.csv +0 -19
  152. package/src/checks/rules-and-tags.full.json +0 -259
  153. package/src/core/aria-helpers.js +0 -970
  154. package/src/core/contrast-helpers.js +0 -1147
  155. package/src/core/dom-helpers.js +0 -4235
  156. package/src/core/dom-runner.js +0 -671
  157. package/src/core/frame-messaging.js +0 -210
  158. package/src/core/frame-scan.js +0 -178
  159. package/src/core/rollup-composites.js +0 -135
  160. package/src/core/rule-meta.js +0 -159
  161. package/src/coverage/wcag-facets.js +0 -1079
  162. package/src/coverage/wcag-version-map.js +0 -84
  163. package/src/i18n/en.js +0 -923
  164. package/src/i18n/fr.js +0 -844
  165. package/src/policy/contracts.js +0 -18
  166. package/src/policy/resolvePolicy.js +0 -55
  167. package/src/policy/schemas/engine-options.schema.json +0 -103
  168. package/src/policy/schemas/policy-contract.schema.json +0 -40
@@ -14,7 +14,7 @@ surea11y is a **static DOM scan**: it reads the DOM tree and computed styles at
14
14
 
15
15
  - **jsdom (Node, no real browser) has no CSS layout engine.** Rules needing real geometry — most notably `target-size-minimum` (WCAG 2.5.8, needs real `getBoundingClientRect()`) — report `notApplicable` under plain jsdom rather than guess. Run under a real browser (Puppeteer/Playwright — see [`INTEGRATION.md`](./INTEGRATION.md) Pattern 2) to get real findings from these rules.
16
16
  - **`<dialog>` and other elements hidden by the default UA stylesheet** (no `open` attribute, `display: none` by spec), along with any other subtree hidden via `display:none`, `visibility:hidden`, `[hidden]`, or closed `<details>`, are **excluded from rule evaluation by default** — matching the visibility-aware behavior of other established engines. This is a deliberate default (`engineOptions.includeHiddenElements: false`), not an oversight: hidden content isn't reachable by assistive technology or keyboard until it's shown, so flagging a markup defect inside it by default would often be noise. Set `engineOptions.includeHiddenElements: true` to evaluate hidden/collapsed subtrees anyway — e.g. to catch a markup defect (like a broken ARIA ID reference) before a dialog ever opens. See [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md#engineoptions--the-rest) for the option and exactly which hiding mechanisms it covers.
17
- - **Static markup vs. live/post-hydration DOM state.** The rule logic itself is DOM-source-agnostic — it evaluates whatever DOM it's handed, whether that's jsdom-parsed static HTML (Pattern 1) or an already-loaded, already-hydrated real browser tab (Pattern 2, see [`INTEGRATION.md`](./INTEGRATION.md)). But the CLI (`npx @surea11y/core scan <url>`) specifically fetches static HTML only, with no JS execution — see [`CLI.md`](./CLI.md). For a JS-framework-hydrated widget whose server-rendered markup intentionally ships one state before client JS syncs it (e.g. `<input type="checkbox" aria-checked="true">` shipped before client JS sets the native `checked` property to match on hydration — an extremely common, entirely legitimate pattern), a CLI scan only sees the pre-hydration markup. Other engines running inside an actual loaded browser tab see the post-hydration state instead, so the two can disagree on exactly this class of element for reasons that have nothing to do with either engine's rule correctness. `aria-checked-state-mismatch` is deliberately `manual`/`cantTell`-capped for this exact reason rather than a hard `fail`. If you need live-DOM accuracy for hydration-sensitive checks, run the library directly against an already-loaded page via Pattern 2, not the static-fetch CLI.
17
+ - **Static markup vs. live/post-hydration DOM state.** The rule logic itself is DOM-source-agnostic — it evaluates whatever DOM it's handed, whether that's jsdom-parsed static HTML (Pattern 1) or an already-loaded, already-hydrated real browser tab (Pattern 2, see [`INTEGRATION.md`](./INTEGRATION.md)). But the CLI (`npx @surea11y/cli scan <url>`) specifically fetches static HTML only, with no JS execution — see [the CLI docs](https://github.com/SureA11y/cli/blob/main/docs/CLI.md). For a JS-framework-hydrated widget whose server-rendered markup intentionally ships one state before client JS syncs it (e.g. `<input type="checkbox" aria-checked="true">` shipped before client JS sets the native `checked` property to match on hydration — an extremely common, entirely legitimate pattern), a CLI scan only sees the pre-hydration markup. Other engines running inside an actual loaded browser tab see the post-hydration state instead, so the two can disagree on exactly this class of element for reasons that have nothing to do with either engine's rule correctness. `aria-checked-state-mismatch` is deliberately `manual`/`cantTell`-capped for this exact reason rather than a hard `fail`. If you need live-DOM accuracy for hydration-sensitive checks, run the library directly against an already-loaded page via Pattern 2, not the static-fetch CLI.
18
18
 
19
19
  ## Deliberately not attempted — judgment calls, not automatable safely
20
20
 
@@ -56,7 +56,7 @@ This is the exact shape of the object returned by `runDomRulesInPage(...)` / `ru
56
56
 
57
57
  - `topFrame` is exactly the [top-level result](#top-level-result) shape, for the frame the function was called in.
58
58
  - `frames` has one entry per direct child `<iframe>`/`<frame>` in the scanned scope. A reachable child (one that called `a11yCoreEnableFrameResponder()`) contributes its own complete `{ url, topFrame, frames }` — including *its own* nested `frames`, recursively, since a further-nested grandchild is only reachable through its immediate parent. An unreachable child (the common case for most third-party embeds — no cooperating responder, or it timed out) contributes `{ url, error }` instead, and does not abort the rest of the scan.
59
- - This is a **tree, not a flat list** — a deliberate difference from the `surea11y-playwright` binding's `.frames(true)`, which *can* flatten because Playwright's `page.frames()` already gives every frame regardless of nesting depth; a `postMessage` relay has no such global view, so nesting is expressed structurally instead.
59
+ - This is a **tree, not a flat list** — a deliberate difference from the `@surea11y/playwright` binding's `.frames(true)`, which *can* flatten because Playwright's `page.frames()` already gives every frame regardless of nesting depth; a `postMessage` relay has no such global view, so nesting is expressed structurally instead.
60
60
 
61
61
  ## A check result (`checksResults[i]`)
62
62
 
package/docs/REPORT.md CHANGED
@@ -18,7 +18,7 @@ Open `report.html` directly from disk. Works alongside any other output mode —
18
18
  ## Library usage
19
19
 
20
20
  ```js
21
- const { renderHtmlReport } = require('@surea11y/core/src/report');
21
+ const { renderHtmlReport } = require('@surea11y/core/report');
22
22
  const { runDomRulesInPage } = require('@surea11y/core');
23
23
 
24
24
  const result = runDomRulesInPage(url, null, {}, null);
@@ -134,7 +134,7 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
134
134
  | `region` | Page content should be inside a landmark region | — | — | medium | minor |
135
135
  | `scope-attr-valid` | scope attribute must have a valid value | — | — | medium | minor |
136
136
  | `scrollable-region-focusable` | Scrollable regions with no focusable content should be keyboard-focusable | 2.1.1, 2.1.3 | AAA | low | moderate |
137
- | `skip-link` | Skip link must have a resolvable target | — | — | medium | minor |
137
+ | `skip-link` | Skip link must have a resolvable, usable target | — | — | medium | minor |
138
138
  | `svg-text-alternative-quality` | &lt;svg&gt; text alternative must be appropriate (manual review) | 1.1.1 | A | medium | minor |
139
139
  | `tabindex` | tabindex should not be greater than 0 | — | — | medium | minor |
140
140
  | `table-duplicate-name` | Table caption must not duplicate its summary attribute | — | — | medium | minor |
package/docs/SARIF.md ADDED
@@ -0,0 +1,59 @@
1
+ # SARIF report
2
+
3
+ `--sarif <path>` writes a [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) log — the standard format GitHub Code Scanning (and other SARIF-consuming dashboards) expect — instead of, or alongside, `--json`'s raw engine result.
4
+
5
+ ```sh
6
+ surea11y scan ./dist/index.html --sarif results.sarif
7
+ ```
8
+
9
+ See [`CI_INTEGRATIONS.md`](./CI_INTEGRATIONS.md) for a ready-to-paste GitHub Actions workflow that runs a scan and uploads `results.sarif` to the "Security" tab.
10
+
11
+ ## Why a separate format from `--json`
12
+
13
+ `--json`'s raw result (see [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md)) is this engine's own contract, versioned and stable per [`API_STABILITY.md`](./API_STABILITY.md). SARIF is a different, externally-defined contract purpose-built for code-scanning dashboards — a `checksResults[]` entry and a SARIF `result` don't map 1:1, so this is a real conversion, not a re-serialization.
14
+
15
+ ## What becomes a SARIF result
16
+
17
+ Only `fail`/`cantTell` occurrences produce SARIF results — a `pass`/`notApplicable` check has no occurrences to report at all (same "violations only" framing as [`REPORT.md`](./REPORT.md)'s HTML report).
18
+
19
+ | Engine outcome | SARIF `level` | Meaning |
20
+ |---|---|---|
21
+ | `fail` | `error` | Deterministic, high-confidence violation — the CI-gating case. |
22
+ | `cantTell` | `warning` | Needs human review — surfaced, but shouldn't block a build on its own. |
23
+
24
+ Every rule that ran (regardless of whether it produced a result) is listed once in `runs[0].tool.driver.rules`, with `defaultConfiguration.level` set from the rule's `type`: `automatic` (fail-capable) → `error`, `manual` (capped at `cantTell`) → `warning`.
25
+
26
+ ## Field mapping
27
+
28
+ | SARIF field | Source |
29
+ |---|---|
30
+ | `results[].ruleId` | `checksResults[i].ruleId` |
31
+ | `results[].message.text` | `occurrence.summary` + `occurrence.hint` |
32
+ | `results[].locations[].physicalLocation.artifactLocation.uri` | The scanned target — see "Locations" below. |
33
+ | `results[].locations[].logicalLocations[].fullyQualifiedName` | `occurrence.selector`, when present. |
34
+ | `results[].partialFingerprints["surea11y/violation/v1"]` | The same `ruleId + reasonCode + html` identity key used by [`BASELINE.md`](./BASELINE.md) (`computeBaselineKey`) — a stable, content-based fingerprint rather than a position-based one. |
35
+ | `results[].properties.severity` / `.confidence` | `checksResults[i].severity` / `.confidence` — informational, not part of SARIF's own schema. |
36
+ | `tool.driver.rules[].properties.tags` | `accessibility`, `automatic`/`manual`, and a `wcag-<SC>` tag per `meta.normativeMappings[].requirement`. |
37
+
38
+ ## Locations
39
+
40
+ DOM-based scanning has no line/column to report, so `physicalLocation.artifactLocation.uri` is the scanned target itself, not a source-file position:
41
+
42
+ - **Local file scans**: a path relative to the current working directory (forward-slashed). If this matches a real file in your repository, GitHub Code Scanning can render the finding as an inline annotation.
43
+ - **URL scans**: the scanned URL itself. GitHub Code Scanning will still list the finding, but can't attach an inline annotation to a URL that isn't a file in the repository — this is inherent to how SARIF/Code Scanning associate findings with source, not a surea11y limitation. If you need inline annotations, scan the rendered HTML file (e.g. a build output artifact) rather than a live URL.
44
+
45
+ `occurrence.selector` is additionally carried as a `logicalLocations[].fullyQualifiedName`, so a consumer that reads logical locations still gets the "which element" signal even without a usable physical location.
46
+
47
+ ## Combining with `--baseline`
48
+
49
+ A generic SARIF consumer has no "known, don't gate on this" concept of its own — the only faithful way to honor a baseline in SARIF output is to omit already-known `fail` occurrences entirely, rather than downgrade them to `warning`:
50
+
51
+ ```sh
52
+ surea11y scan ./dist/index.html --baseline baseline.json --sarif results.sarif
53
+ ```
54
+
55
+ `cantTell` occurrences are never filtered by a baseline — the baseline mechanism only ever tracks `fail` occurrences (matching `--write-baseline`, see [`BASELINE.md`](./BASELINE.md)).
56
+
57
+ ## Combining with `--html`/`--json`
58
+
59
+ `--sarif`, `--html`, and `--json` are independent output flags — pass any combination in one run; each writes/prints its own report from the same single scan.
package/package.json CHANGED
@@ -1,20 +1,50 @@
1
1
  {
2
2
  "name": "@surea11y/core",
3
- "version": "1.2.0",
4
- "description": "Lightweight DOM rules accessibility core with modular rules.",
3
+ "version": "1.4.0",
4
+ "description": "Deterministic WCAG 2.2 accessibility engine that tells you what it can't tell you. Zero dependencies.",
5
+ "keywords": [
6
+ "accessibility",
7
+ "a11y",
8
+ "wcag",
9
+ "wcag21",
10
+ "wcag22",
11
+ "accessibility-testing",
12
+ "a11y-testing",
13
+ "audit",
14
+ "aria",
15
+ "contrast",
16
+ "sarif",
17
+ "zero-dependencies",
18
+ "jsdom",
19
+ "playwright",
20
+ "puppeteer",
21
+ "selenium",
22
+ "cypress",
23
+ "webdriverio",
24
+ "jest",
25
+ "vitest"
26
+ ],
5
27
  "main": "src/index.js",
6
28
  "bin": {
7
- "surea11y": "bin/core.js"
29
+ "surea11y-core": "bin/surea11y-core.js"
30
+ },
31
+ "exports": {
32
+ ".": "./src/index.js",
33
+ "./baseline": "./src/baseline.js",
34
+ "./report": "./src/report.js",
35
+ "./sarif": "./src/sarif.js",
36
+ "./browser": "./surea11y.browser.js",
37
+ "./package.json": "./package.json"
8
38
  },
9
39
  "author": "Jorge Rumoroso",
10
- "license": "MIT",
40
+ "license": "MPL-2.0",
11
41
  "repository": {
12
42
  "type": "git",
13
- "url": "git+https://github.com/rumoroso/surea11y-core.git"
43
+ "url": "git+https://github.com/SureA11y/core.git"
14
44
  },
15
- "homepage": "https://github.com/rumoroso/surea11y-core#readme",
45
+ "homepage": "https://github.com/SureA11y/core#readme",
16
46
  "bugs": {
17
- "url": "https://github.com/rumoroso/surea11y-core/issues"
47
+ "url": "https://github.com/SureA11y/core/issues"
18
48
  },
19
49
  "engines": {
20
50
  "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
@@ -23,8 +53,14 @@
23
53
  "access": "public"
24
54
  },
25
55
  "files": [
26
- "src",
27
- "bin",
56
+ "src/index.js",
57
+ "src/core.js",
58
+ "src/baseline.js",
59
+ "src/report.js",
60
+ "src/sarif.js",
61
+ "src/checks/**/*.js",
62
+ "bin/surea11y-core.js",
63
+ "surea11y.browser.js",
28
64
  "docs/**/*.md",
29
65
  "README.md",
30
66
  "LICENSE",
@@ -32,13 +68,17 @@
32
68
  "!docs/RULE_TEMPLATE.md",
33
69
  "!docs/RULE_TEST_TEMPLATE.md",
34
70
  "!docs/RULE_TEST_AUTHORING.md",
35
- "!docs/TEST_OUTCOME_STABILITY.md",
36
- "!src/explain"
71
+ "!docs/TEST_OUTCOME_STABILITY.md"
37
72
  ],
38
73
  "scripts": {
39
- "build": "node scripts/build-core.js",
74
+ "lint": "eslint .",
75
+ "lint:fix": "eslint . --fix",
76
+ "format": "prettier --write \"src/**/*.js\" \"bin/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"eslint.config.js\"",
77
+ "format:check": "prettier --check \"src/**/*.js\" \"bin/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"eslint.config.js\"",
78
+ "build": "node scripts/build-core.js && node scripts/build-browser.js",
40
79
  "pretest": "playwright install chromium",
41
- "test": "npm run build && node scripts/run-tests.js",
80
+ "test": "npm run format:check && npm run build && node scripts/run-tests.js",
81
+ "test:coverage": "npm run build && node scripts/run-tests.js --experimental-test-coverage --test-coverage-include=src/**/*.js",
42
82
  "test:contrast-helpers": "node tests/contrast-helpers.test.js",
43
83
  "helpers-perf-bench": "node --expose-gc scripts/dom-helpers-perf-bench.js",
44
84
  "engine-perf-bench": "node --expose-gc scripts/engine-perf-bench.js --profileRules=true --iters=25 --top=15",
@@ -50,12 +90,17 @@
50
90
  "fixtures:index": "node scripts/generate-fixture-index.js",
51
91
  "docs:rule-catalog": "npm run build && node scripts/generate-rule-catalog.js",
52
92
  "validate:automatic-rules": "npm run build && node scripts/validate-all-rules.js src/checks/automatic",
53
- "validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual"
54
- },
55
- "dependencies": {
56
- "jsdom": "^29.1.1"
93
+ "validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual",
94
+ "i18n:new": "node scripts/i18n-scaffold.js",
95
+ "i18n:report": "node scripts/i18n-report.js"
57
96
  },
58
97
  "devDependencies": {
59
- "playwright": "^1.61.1"
98
+ "@eslint/js": "^10.0.1",
99
+ "eslint": "^10.8.0",
100
+ "eslint-config-prettier": "^10.1.8",
101
+ "globals": "^17.8.0",
102
+ "jsdom": "^29.1.1",
103
+ "playwright": "^1.62.1",
104
+ "prettier": "^3.9.6"
60
105
  }
61
106
  }
package/src/baseline.js CHANGED
Binary file
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -20,7 +22,8 @@ const id = 'area-alt-present';
20
22
 
21
23
  const meta = {
22
24
  title: '&lt;area&gt; must have an alt attribute',
23
- description: 'Checks that &lt;area&gt; elements provide an alt attribute to support a text alternative mechanism.',
25
+ description:
26
+ 'Checks that &lt;area&gt; elements provide an alt attribute to support a text alternative mechanism.',
24
27
  i18n: {
25
28
  titleKey: 'area_altPresent_title',
26
29
  descriptionKey: 'area_altPresent_description'
@@ -29,7 +32,13 @@ const meta = {
29
32
  tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'imagemap', 'atomic', 'automatic'],
30
33
  wcagSc: ['1.1.1'],
31
34
  normativeMappings: [
32
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
35
+ {
36
+ standard: 'WCAG',
37
+ version: '2.2',
38
+ requirement: '1.1.1',
39
+ title: 'Non-text Content',
40
+ conformanceLevel: 'A'
41
+ }
33
42
  ],
34
43
  defaultSeverity: 'serious',
35
44
  category: 'perceivable',
@@ -46,25 +55,29 @@ function runInPage(ctx) {
46
55
  const { document, root, helpers, rule } = ctx;
47
56
  const safeRoot = root || document;
48
57
 
49
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
50
- const queryAll = helpers && typeof helpers.queryAll === 'function'
58
+ const queryAllSmart =
59
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
60
+ const queryAll =
61
+ helpers && typeof helpers.queryAll === 'function'
51
62
  ? helpers.queryAll
52
63
  : (sel) => {
53
- try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
54
- catch { return []; }
55
- };
64
+ try {
65
+ return safeRoot && safeRoot.querySelectorAll
66
+ ? Array.from(safeRoot.querySelectorAll(sel))
67
+ : [];
68
+ } catch {
69
+ return [];
70
+ }
71
+ };
56
72
 
57
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
58
- ? helpers.getEligibilityInfo
59
- : null;
73
+ const getEligibilityInfo =
74
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
60
75
 
61
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
62
- ? helpers.isAccTreeEligible
63
- : null;
76
+ const isAccTreeEligible =
77
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
64
78
 
65
- const getAriaNameInfo = helpers && typeof helpers.getAriaNameInfo === 'function'
66
- ? helpers.getAriaNameInfo
67
- : null;
79
+ const getAriaNameInfo =
80
+ helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
68
81
 
69
82
  // --- image-map semantics (rule-local) ---
70
83
 
@@ -72,7 +85,7 @@ function runInPage(ctx) {
72
85
  try {
73
86
  const s = String(val || '').trim();
74
87
  if (!s) return '';
75
- return (s[0] === '#') ? s.slice(1).trim().toLowerCase() : s.toLowerCase();
88
+ return s[0] === '#' ? s.slice(1).trim().toLowerCase() : s.toLowerCase();
76
89
  } catch {
77
90
  return '';
78
91
  }
@@ -92,7 +105,8 @@ function runInPage(ctx) {
92
105
  const __usemapIndex = (() => {
93
106
  const idx = new Map();
94
107
  try {
95
- const imgs = document && document.querySelectorAll ? document.querySelectorAll('img[usemap]') : [];
108
+ const imgs =
109
+ document && document.querySelectorAll ? document.querySelectorAll('img[usemap]') : [];
96
110
  for (const img of imgs) {
97
111
  if (!img || !img.getAttribute) continue;
98
112
  const u = normUsemap(img.getAttribute('usemap'));
@@ -120,8 +134,11 @@ function runInPage(ctx) {
120
134
  }
121
135
 
122
136
  const areas = (() => {
123
- try { return Array.from((queryAllSmart ? queryAllSmart('area') : queryAll('area')) || []); }
124
- catch { return queryAll('area'); }
137
+ try {
138
+ return Array.from((queryAllSmart ? queryAllSmart('area') : queryAll('area')) || []);
139
+ } catch {
140
+ return queryAll('area');
141
+ }
125
142
  })();
126
143
 
127
144
  if (!areas.length) {
@@ -143,7 +160,11 @@ function runInPage(ctx) {
143
160
  // This is the "visibility of map/area doesn't matter; the image does" policy.
144
161
  if (isAccTreeEligible) {
145
162
  const imgElig = (() => {
146
- try { return isAccTreeEligible(img, ctx); } catch { return { eligible: true, reasons: [] }; }
163
+ try {
164
+ return isAccTreeEligible(img, ctx);
165
+ } catch {
166
+ return { eligible: true, reasons: [] };
167
+ }
147
168
  })();
148
169
  if (imgElig && imgElig.eligible === false) continue;
149
170
  }
@@ -151,7 +172,11 @@ function runInPage(ctx) {
151
172
  // 2) The <area> itself must be eligible (aria-hidden/inert exceptions handled by helper).
152
173
  if (isAccTreeEligible) {
153
174
  const elig = (() => {
154
- try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
175
+ try {
176
+ return isAccTreeEligible(el, ctx);
177
+ } catch {
178
+ return { eligible: true, reasons: [] };
179
+ }
155
180
  })();
156
181
  if (elig && elig.eligible === false) continue;
157
182
  }
@@ -166,7 +191,7 @@ function runInPage(ctx) {
166
191
  // text-alternative mechanism for <area> (HTML-AAM accessible name
167
192
  // computation includes ARIA naming before falling back to alt).
168
193
  if (getAriaNameInfo) {
169
- let ariaName = null;
194
+ let ariaName;
170
195
  try {
171
196
  ariaName = getAriaNameInfo(el, ctx);
172
197
  } catch {
@@ -176,13 +201,15 @@ function runInPage(ctx) {
176
201
  }
177
202
 
178
203
  // A non-empty title attribute is HTML-AAM's own next fallback naming
179
- // source once alt is entirely absent -- also accepted by a widely-used
180
- // reference engine's equivalent area-alt rule (non-empty-title, same "any" list as
181
- // non-empty-alt/aria-label/aria-labelledby). See img-alt-present's
182
- // sibling fix (2026-07-23, AliExpress's title-only logo <img>) for
183
- // the real page this was found via -- same gap, same fix, different
184
- // element.
185
- const titleRaw = (() => { try { return el.getAttribute('title'); } catch { return null; } })();
204
+ // source once alt is entirely absent. Same gap img-alt-present handles
205
+ // for <img title="..."> with no alt.
206
+ const titleRaw = (() => {
207
+ try {
208
+ return el.getAttribute('title');
209
+ } catch {
210
+ return null;
211
+ }
212
+ })();
186
213
  if (titleRaw !== null && String(titleRaw).trim()) continue;
187
214
 
188
215
  const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
@@ -219,7 +246,12 @@ function runInPage(ctx) {
219
246
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
220
247
  }
221
248
 
222
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
249
+ return {
250
+ ruleId: rule.ruleId,
251
+ outcome: 'fail',
252
+ severity: rule.defaultSeverity || 'minor',
253
+ occurrences
254
+ };
223
255
  }
224
256
 
225
257
  module.exports = { id, meta, runInPage };