@surea11y/core 1.3.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 (163) hide show
  1. package/CHANGELOG.md +46 -2
  2. package/README.md +109 -35
  3. package/bin/surea11y-core.js +20 -0
  4. package/docs/API_STABILITY.md +26 -0
  5. package/docs/CI_INTEGRATIONS.md +7 -7
  6. package/docs/ENGINE_OPTIONS.md +1 -1
  7. package/docs/I18N.md +12 -9
  8. package/docs/INTEGRATION.md +1 -1
  9. package/docs/LIMITATIONS.md +1 -1
  10. package/docs/REPORT.md +1 -1
  11. package/package.json +50 -16
  12. package/src/baseline.js +0 -0
  13. package/src/checks/automatic/area-alt-present.js +4 -6
  14. package/src/checks/automatic/aria-allowed-attr.js +15 -51
  15. package/src/checks/automatic/aria-allowed-role.js +2 -0
  16. package/src/checks/automatic/aria-braille-equivalent.js +2 -0
  17. package/src/checks/automatic/aria-conditional-attr.js +8 -7
  18. package/src/checks/automatic/aria-deprecated-role.js +4 -3
  19. package/src/checks/automatic/aria-hidden-body.js +6 -4
  20. package/src/checks/automatic/aria-hidden-focus.js +12 -13
  21. package/src/checks/automatic/aria-prohibited-attr.js +98 -105
  22. package/src/checks/automatic/aria-prohibited-children.js +56 -87
  23. package/src/checks/automatic/aria-required-attr.js +6 -7
  24. package/src/checks/automatic/aria-required-children.js +7 -10
  25. package/src/checks/automatic/aria-required-parent.js +20 -25
  26. package/src/checks/automatic/aria-role-name-present.js +2 -0
  27. package/src/checks/automatic/aria-roles-valid.js +2 -0
  28. package/src/checks/automatic/aria-valid-attr-value.js +18 -15
  29. package/src/checks/automatic/aria-valid-attr.js +2 -0
  30. package/src/checks/automatic/autocomplete-valid.js +2 -0
  31. package/src/checks/automatic/avoid-inline-spacing.js +3 -2
  32. package/src/checks/automatic/binary-control-name-present.js +2 -0
  33. package/src/checks/automatic/button-name-present.js +7 -7
  34. package/src/checks/automatic/bypass-blocks-present.js +9 -7
  35. package/src/checks/automatic/canvas-text-alternative-present.js +2 -0
  36. package/src/checks/automatic/combobox-name-present.js +2 -0
  37. package/src/checks/automatic/contrast-computable.js +2 -0
  38. package/src/checks/automatic/contrast-enhanced.js +2 -0
  39. package/src/checks/automatic/contrast-minimum.js +2 -0
  40. package/src/checks/automatic/css-orientation-lock.js +21 -27
  41. package/src/checks/automatic/definition-list-children-valid.js +6 -6
  42. package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
  43. package/src/checks/automatic/dialog-name-present.js +10 -10
  44. package/src/checks/automatic/dlitem-parent-valid.js +2 -0
  45. package/src/checks/automatic/duplicate-id-aria.js +4 -3
  46. package/src/checks/automatic/embed-text-alternative-present.js +2 -0
  47. package/src/checks/automatic/form-control-programmatic-label-present.js +2 -0
  48. package/src/checks/automatic/form-control-single-label.js +6 -7
  49. package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
  50. package/src/checks/automatic/iframe-focusable-content.js +246 -18
  51. package/src/checks/automatic/iframe-name-present.js +2 -0
  52. package/src/checks/automatic/iframe-title-unique.js +3 -1
  53. package/src/checks/automatic/img-alt-present.js +7 -9
  54. package/src/checks/automatic/input-image-alt-present.js +4 -6
  55. package/src/checks/automatic/label-in-name.js +15 -19
  56. package/src/checks/automatic/language-page-present.js +2 -0
  57. package/src/checks/automatic/link-in-text-block.js +2 -0
  58. package/src/checks/automatic/link-name-present.js +2 -0
  59. package/src/checks/automatic/list-children-valid.js +14 -24
  60. package/src/checks/automatic/listbox-name-present.js +2 -0
  61. package/src/checks/automatic/listitem-parent-valid.js +30 -7
  62. package/src/checks/automatic/menuitem-name-present.js +2 -0
  63. package/src/checks/automatic/meta-refresh-no-exceptions.js +4 -4
  64. package/src/checks/automatic/meta-refresh-timing-absent.js +2 -0
  65. package/src/checks/automatic/meta-viewport-zoom-enabled.js +2 -0
  66. package/src/checks/automatic/meter-name-present.js +4 -3
  67. package/src/checks/automatic/nested-interactive-controls-absent.js +27 -5
  68. package/src/checks/automatic/object-text-alternative-present.js +2 -0
  69. package/src/checks/automatic/option-name-present.js +2 -0
  70. package/src/checks/automatic/page-title-present.js +2 -0
  71. package/src/checks/automatic/progressbar-name-present.js +8 -10
  72. package/src/checks/automatic/role-img-alt-present.js +4 -4
  73. package/src/checks/automatic/searchbox-name-present.js +2 -0
  74. package/src/checks/automatic/server-side-image-map-absent.js +4 -3
  75. package/src/checks/automatic/slider-name-present.js +2 -0
  76. package/src/checks/automatic/spinbutton-name-present.js +2 -0
  77. package/src/checks/automatic/summary-name-present.js +2 -0
  78. package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
  79. package/src/checks/automatic/svg-text-alternative-present.js +17 -5
  80. package/src/checks/automatic/tab-name-present.js +2 -0
  81. package/src/checks/automatic/table-headers-attr-valid.js +3 -2
  82. package/src/checks/automatic/table-th-has-data-cells.js +2 -0
  83. package/src/checks/automatic/target-size-minimum.js +5 -0
  84. package/src/checks/automatic/td-has-header.js +24 -1
  85. package/src/checks/automatic/textbox-name-present.js +2 -0
  86. package/src/checks/automatic/tooltip-name-present.js +2 -0
  87. package/src/checks/automatic/treeitem-name-present.js +2 -0
  88. package/src/checks/automatic/valid-lang.js +2 -0
  89. package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
  90. package/src/checks/manual/accesskeys-manual.js +3 -1
  91. package/src/checks/manual/area-alt-decorative-manual.js +2 -0
  92. package/src/checks/manual/area-alt-quality-manual.js +2 -0
  93. package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
  94. package/src/checks/manual/aria-text-manual.js +6 -5
  95. package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
  96. package/src/checks/manual/css-hidden-focus.js +184 -9
  97. package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
  98. package/src/checks/manual/empty-heading-manual.js +17 -17
  99. package/src/checks/manual/empty-table-header-manual.js +52 -25
  100. package/src/checks/manual/focus-order-semantics-manual.js +16 -4
  101. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
  102. package/src/checks/manual/heading-order-manual.js +28 -1
  103. package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
  104. package/src/checks/manual/image-redundant-alt-manual.js +21 -1
  105. package/src/checks/manual/img-alt-decorative-manual.js +2 -0
  106. package/src/checks/manual/img-alt-quality-manual.js +2 -0
  107. package/src/checks/manual/input-image-alt-decorative-manual.js +2 -0
  108. package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
  109. package/src/checks/manual/label-title-only-manual.js +29 -22
  110. package/src/checks/manual/landmark-banner-is-top-level-manual.js +54 -47
  111. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +42 -26
  112. package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
  113. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
  114. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
  115. package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
  116. package/src/checks/manual/landmark-one-main-manual.js +12 -23
  117. package/src/checks/manual/landmark-unique-manual.js +37 -52
  118. package/src/checks/manual/link-name-quality-manual.js +2 -0
  119. package/src/checks/manual/media-transcript-present-manual.js +2 -0
  120. package/src/checks/manual/meta-viewport-large-manual.js +3 -1
  121. package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
  122. package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
  123. package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
  124. package/src/checks/manual/p-as-heading-manual.js +2 -0
  125. package/src/checks/manual/page-has-heading-one-manual.js +12 -11
  126. package/src/checks/manual/page-title-patterns-manual.js +2 -0
  127. package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
  128. package/src/checks/manual/region-manual.js +27 -36
  129. package/src/checks/manual/scope-attr-valid-manual.js +3 -1
  130. package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
  131. package/src/checks/manual/skip-link-manual.js +7 -6
  132. package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
  133. package/src/checks/manual/tabindex-manual.js +3 -1
  134. package/src/checks/manual/table-duplicate-name-manual.js +5 -4
  135. package/src/checks/manual/table-fake-caption-manual.js +24 -3
  136. package/src/checks/manual/video-caption-manual.js +2 -0
  137. package/src/checks/manual-review.js +2 -0
  138. package/src/core.js +6772 -2158
  139. package/src/index.js +2 -0
  140. package/src/report.js +51 -9
  141. package/src/sarif.js +18 -3
  142. package/surea11y.browser.js +2731 -999
  143. package/bin/core.js +0 -473
  144. package/docs/CLI.md +0 -128
  145. package/src/catalogs/composites.wcag.js +0 -454
  146. package/src/checks/rules-and-tags.full.csv +0 -19
  147. package/src/checks/rules-and-tags.full.json +0 -259
  148. package/src/core/aria-helpers.js +0 -1211
  149. package/src/core/contrast-helpers.js +0 -1302
  150. package/src/core/dom-helpers.js +0 -4493
  151. package/src/core/dom-runner.js +0 -787
  152. package/src/core/frame-messaging.js +0 -261
  153. package/src/core/frame-scan.js +0 -190
  154. package/src/core/rollup-composites.js +0 -127
  155. package/src/core/rule-meta.js +0 -176
  156. package/src/coverage/wcag-facets.js +0 -1079
  157. package/src/coverage/wcag-version-map.js +0 -84
  158. package/src/i18n/en.js +0 -1228
  159. package/src/i18n/fr.js +0 -1185
  160. package/src/policy/contracts.js +0 -18
  161. package/src/policy/resolvePolicy.js +0 -59
  162. package/src/policy/schemas/engine-options.schema.json +0 -103
  163. package/src/policy/schemas/policy-contract.schema.json +0 -40
@@ -1,10 +1,12 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
4
6
  * @check region
5
7
  * @atomic true
6
8
  * @summary Page content should be contained within a landmark region
7
- * @standard Best Practices (a widely-used reference engine's classification; no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
8
10
  * @applicability
9
11
  * Applies to any element under <body> that directly carries visible text
10
12
  * (or other own content — see @implementation-notes) and is not itself a
@@ -20,18 +22,15 @@
20
22
  * `type: 'manual'` rule; see landmark-banner-is-top-level's
21
23
  * header comment for the shared rationale/precedent and the landmark-
22
24
  * detection model.
23
- * - Recursive tree walk, not a direct-<body>-children-only scan (that was
24
- * this check's original scope, replaced 2026-08-01). The direct-children
25
- * scope was originally chosen to keep this check quiet, but it turned out
26
- * to be nearly inert on the single most common real-world page shape: a
27
- * modern framework's single root mount div
28
- * (`<body><div id="root">...everything...</div></body>`, confirmed
29
- * present as the ONLY direct <body> child on 37 of ~90 pages in a
30
- * real-world corpus). On that shape, the old scan had at most one
31
- * candidate for the entire page and either missed every real gap inside
32
- * it or collapsed the whole page into one undifferentiated report.
33
- * - Algorithm, using this engine's own (already more spec-correct in
34
- * places — see hasLandmarkScopingAncestor's header comment) helpers:
25
+ * - Recursive tree walk, not a direct-<body>-children-only scan: a
26
+ * direct-children-only scope is nearly inert on the single most common
27
+ * real-world page shape, a modern framework's single root mount div
28
+ * (`<body><div id="root">...everything...</div></body>`) — that shape
29
+ * gives at most one candidate for the entire page and either misses
30
+ * every real gap inside it or collapses the whole page into one
31
+ * undifferentiated report.
32
+ * - Algorithm, using this engine's own helpers (see
33
+ * hasLandmarkScopingAncestor's header comment):
35
34
  * 1. Depth-first walk from <body>'s children.
36
35
  * 2. At each node: if it's ineligible for the accessibility tree
37
36
  * (helpers.isAccTreeEligible), OR is itself a "stopper" (landmark,
@@ -52,7 +51,7 @@
52
51
  * contiguous unplaced content into one occurrence per real gap instead
53
52
  * of reporting every individual text-bearing leaf — this is what keeps
54
53
  * the walk from being noisy on ordinary pages that mix landmarked and
55
- * stray content, not the old direct-children-only restriction.
54
+ * stray content.
56
55
  * - "Stopper" exemptions (button, dialog, <svg>, resolvable skip-links)
57
56
  * are a deliberate scope choice, not an oversight: these
58
57
  * are extremely common real-world patterns (floating action buttons,
@@ -119,9 +118,7 @@ function runInPage(ctx) {
119
118
 
120
119
  // Delegates to the shared helpers.getLandmarkNameInfo (aria-label -> aria-labelledby, via the
121
120
  // target's own accessible name, not raw textContent -> title attribute fallback) rather than a
122
- // local copy -- see that function's header comment in src/core/dom-helpers.js for the real bug
123
- // (missing title fallback) this replaced across all 7 landmark rule files that had their own
124
- // copy of this logic.
121
+ // local copy -- see that function's header comment in src/core/dom-helpers.js.
125
122
  function getAccessibleLandmarkName(el) {
126
123
  try {
127
124
  if (helpers && typeof helpers.getLandmarkNameInfo === 'function') {
@@ -144,10 +141,9 @@ function runInPage(ctx) {
144
141
  // (an ancestor's bare TAG only counts when it carries no role attribute
145
142
  // at all; an explicit role="dialog"-style override no longer suppresses)
146
143
  // rather than a local tag-only copy. See that function's header comment
147
- // in src/core/aria-helpers.js for the full algorithm and the real page
148
- // (handsontable.com's docs-assistant side panel, an
149
- // <aside role="dialog"> containing its own <header>) that surfaced this
150
- // rule's own former tag-only copy as a false negative.
144
+ // in src/core/aria-helpers.js for the full algorithm — e.g. an
145
+ // <aside role="dialog"> containing its own <header>, where the <header>
146
+ // keeps its banner role.
151
147
  function hasSectioningAncestor(el, includeMain) {
152
148
  return helpers && typeof helpers.hasLandmarkScopingAncestor === 'function'
153
149
  ? helpers.hasLandmarkScopingAncestor(el, { includeMain })
@@ -161,11 +157,9 @@ function runInPage(ctx) {
161
157
  if (tag === 'main') return 'main';
162
158
  if (tag === 'nav') return 'navigation';
163
159
  if (tag === 'aside') {
164
- // A named <aside> is never suppressed, even when nested — matches
165
- // landmark-unique's own verified-against-reference-engine precedent
166
- // (that engine's real `aside` implicit-role function keeps
160
+ // A named <aside> is never suppressed, even when nested: keeps
167
161
  // "complementary" when the element has an accessible name, even
168
- // inside sectioning content); propagated here for consistency.
162
+ // inside sectioning content. See landmark-unique-manual.js.
169
163
  if (!hasSectioningAncestor(el, false)) return 'complementary';
170
164
  return getAccessibleLandmarkName(el) ? 'complementary' : '';
171
165
  }
@@ -196,8 +190,7 @@ function runInPage(ctx) {
196
190
 
197
191
  // Roles/attributes that make an element its own self-contained
198
192
  // announced area — not literally a WAI-ARIA landmark, but not "content
199
- // that needs a landmark" either. Same live-region role list a widely-used
200
- // reference engine's own region rule stops recursion at.
193
+ // that needs a landmark" either.
201
194
  const LIVE_REGION_ROLES = new Set(['alert', 'status', 'log', 'marquee', 'timer']);
202
195
 
203
196
  function isAriaLive(el) {
@@ -257,12 +250,11 @@ function runInPage(ctx) {
257
250
  const VISUAL_CONTENT_TAGS = new Set(['img', 'video', 'audio', 'canvas', 'object', 'embed']);
258
251
 
259
252
  // Non-recursive "does THIS element, on its own, carry content" check —
260
- // deliberately mirrors only the direct-content half of getContentNameInfo/
261
- // a widely-used reference engine's own has-content check, not a full
262
- // name-from-content recursion: the whole point is to keep recursing
263
- // through plain wrapper elements (a framework's root mount <div> included)
264
- // until reaching the actual content-bearing node, rather than a coarse
265
- // ancestor swallowing everything beneath it into one report.
253
+ // deliberately mirrors only the direct-content half of getContentNameInfo,
254
+ // not a full name-from-content recursion: the whole point is to keep
255
+ // recursing through plain wrapper elements (a framework's root mount
256
+ // <div> included) until reaching the actual content-bearing node, rather
257
+ // than a coarse ancestor swallowing everything beneath it into one report.
266
258
  function hasOwnContent(el) {
267
259
  const kids = el.childNodes || [];
268
260
  for (let i = 0; i < kids.length; i++) {
@@ -341,9 +333,8 @@ function runInPage(ctx) {
341
333
  // Collapse each candidate leaf upward through parents that have no OTHER
342
334
  // stopper anywhere in their subtree, so contiguous unplaced content
343
335
  // merges into ONE occurrence per real gap instead of one per text node --
344
- // this collapsing, not the old direct-children-only scope, is what keeps
345
- // ordinary pages (landmarked content mixed with a little stray content)
346
- // from producing noisy, one-per-leaf reports.
336
+ // this collapsing is what keeps ordinary pages (landmarked content mixed
337
+ // with a little stray content) from producing noisy, one-per-leaf reports.
347
338
  const collapsed = [];
348
339
  const seen = new Set();
349
340
  for (const leaf of leaves) {
@@ -1,10 +1,12 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
4
6
  * @check scope-attr-valid
5
7
  * @atomic true
6
8
  * @summary The scope attribute must have a valid value
7
- * @standard Best Practices (a widely-used reference engine's classification; no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
8
10
  * @applicability
9
11
  * Applies to elements with a non-empty scope attribute.
10
12
  * @expectation
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -1,20 +1,21 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
4
6
  * @check skip-link
5
7
  * @atomic true
6
8
  * @summary A "skip" link must resolve to a real, usable target
7
- * @standard Best Practices (a widely-used reference engine's classification; no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
8
10
  * @applicability
9
11
  * Applies to <a href="#fragment"> elements whose accessible name
10
12
  * matches a common "skip to ..." / "jump to ..." authoring convention
11
13
  * (case-insensitive "skip" or "jump to" in the name) — the recognizable
12
14
  * pattern for a skip-navigation link, not every same-page anchor link
13
- * on the page. "jump to" added 2026-07-23 after a real page (Wish.com)
14
- * surfaced a skip link reading "Jump to section" with a genuinely
15
- * missing target — invisible to the original "skip"-only pattern,
16
- * while a widely-used reference engine's own (purely positional, not text-based) matching
17
- * caught it. Text-pattern matching itself stays deliberate (see
15
+ * on the page. "jump to" is included alongside "skip" since real skip
16
+ * links use both conventions (e.g. a "Jump to section" link, which a
17
+ * purely positional match would catch but a "skip"-only text pattern
18
+ * would miss). Text-pattern matching itself stays deliberate (see
18
19
  * implementation-notes) — this only widens the known-convention list.
19
20
  * @expectation
20
21
  * The link's fragment resolves to a real element in the document
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -1,10 +1,12 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
4
6
  * @check tabindex
5
7
  * @atomic true
6
8
  * @summary tabindex should not be greater than 0
7
- * @standard Best Practices (a widely-used reference engine's classification; no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
8
10
  * @applicability
9
11
  * Applies to elements with a tabindex attribute whose value parses as
10
12
  * a valid integer.
@@ -1,10 +1,12 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
4
6
  * @check table-duplicate-name
5
7
  * @atomic true
6
8
  * @summary A table's caption must not duplicate its summary attribute
7
- * @standard Best Practices (a widely-used reference engine's classification; no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
9
+ * @standard Best Practices (no formal WCAG Success Criterion — see ROADMAP.md Tier 1b)
8
10
  * @applicability
9
11
  * Applies to <table> elements that have both a <caption> with text
10
12
  * content and a (deprecated but still encountered) summary attribute.
@@ -17,9 +19,8 @@
17
19
  * - Not WCAG-normative — authored as an advisory, cantTell-capped
18
20
  * `type: 'manual'` rule; see landmark-banner-is-top-level's
19
21
  * header comment for the shared rationale/precedent.
20
- * - Narrowly scoped to caption-vs-summary duplication specifically
21
- * (matching a reference engine's table-duplicate-name check), not a general "table
22
- * name quality" check.
22
+ * - Narrowly scoped to caption-vs-summary duplication specifically, not a
23
+ * general "table name quality" check.
23
24
  */
24
25
 
25
26
  const id = 'table-duplicate-name';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -64,6 +66,19 @@ function runInPage(ctx) {
64
66
  return (v == null ? '' : String(v)).trim();
65
67
  }
66
68
 
69
+ const isAccTreeEligible =
70
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
71
+
72
+ function isEligible(node) {
73
+ if (!isAccTreeEligible) return true;
74
+ try {
75
+ const r = isAccTreeEligible(node, ctx);
76
+ return !!(r && r.eligible);
77
+ } catch {
78
+ return true;
79
+ }
80
+ }
81
+
67
82
  const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
68
83
 
69
84
  const occurrences = [];
@@ -75,20 +90,26 @@ function runInPage(ctx) {
75
90
  const hasCaption = !!(table.querySelector && table.querySelector('caption'));
76
91
  if (hasCaption) continue;
77
92
 
78
- const rows = Array.from(table.rows);
93
+ // An aria-hidden row (or cell) isn't part of the AT-perceived table
94
+ // structure at all -- it must not be treated as the table's "first
95
+ // row" (or counted toward a row's cell count) for this positional
96
+ // heuristic.
97
+ const rows = Array.from(table.rows).filter(isEligible);
79
98
  if (rows.length < 2) continue;
80
99
 
81
100
  applicableCount += 1;
82
101
 
83
102
  const firstRow = rows[0];
84
- const firstRowCells = Array.from(firstRow.cells || []);
103
+ const firstRowCells = Array.from(firstRow.cells || []).filter(isEligible);
85
104
  if (firstRowCells.length !== 1) continue;
86
105
 
87
106
  const candidateCell = firstRowCells[0];
88
107
  const candidateText = trim(candidateCell.textContent || '');
89
108
  if (!candidateText) continue;
90
109
 
91
- const hasMultiCellRow = rows.slice(1).some((r) => Array.from(r.cells || []).length > 1);
110
+ const hasMultiCellRow = rows
111
+ .slice(1)
112
+ .some((r) => Array.from(r.cells || []).filter(isEligible).length > 1);
92
113
  if (!hasMultiCellRow) continue;
93
114
 
94
115
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(table) : 'html';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**