@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.
- package/CHANGELOG.md +46 -2
- package/README.md +109 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +26 -0
- package/docs/CI_INTEGRATIONS.md +7 -7
- package/docs/ENGINE_OPTIONS.md +1 -1
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +1 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/REPORT.md +1 -1
- package/package.json +50 -16
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +4 -6
- package/src/checks/automatic/aria-allowed-attr.js +15 -51
- package/src/checks/automatic/aria-allowed-role.js +2 -0
- package/src/checks/automatic/aria-braille-equivalent.js +2 -0
- package/src/checks/automatic/aria-conditional-attr.js +8 -7
- package/src/checks/automatic/aria-deprecated-role.js +4 -3
- package/src/checks/automatic/aria-hidden-body.js +6 -4
- package/src/checks/automatic/aria-hidden-focus.js +12 -13
- package/src/checks/automatic/aria-prohibited-attr.js +98 -105
- package/src/checks/automatic/aria-prohibited-children.js +56 -87
- package/src/checks/automatic/aria-required-attr.js +6 -7
- package/src/checks/automatic/aria-required-children.js +7 -10
- package/src/checks/automatic/aria-required-parent.js +20 -25
- package/src/checks/automatic/aria-role-name-present.js +2 -0
- package/src/checks/automatic/aria-roles-valid.js +2 -0
- package/src/checks/automatic/aria-valid-attr-value.js +18 -15
- package/src/checks/automatic/aria-valid-attr.js +2 -0
- package/src/checks/automatic/autocomplete-valid.js +2 -0
- package/src/checks/automatic/avoid-inline-spacing.js +3 -2
- package/src/checks/automatic/binary-control-name-present.js +2 -0
- package/src/checks/automatic/button-name-present.js +7 -7
- package/src/checks/automatic/bypass-blocks-present.js +9 -7
- package/src/checks/automatic/canvas-text-alternative-present.js +2 -0
- package/src/checks/automatic/combobox-name-present.js +2 -0
- package/src/checks/automatic/contrast-computable.js +2 -0
- package/src/checks/automatic/contrast-enhanced.js +2 -0
- package/src/checks/automatic/contrast-minimum.js +2 -0
- package/src/checks/automatic/css-orientation-lock.js +21 -27
- package/src/checks/automatic/definition-list-children-valid.js +6 -6
- package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
- package/src/checks/automatic/dialog-name-present.js +10 -10
- package/src/checks/automatic/dlitem-parent-valid.js +2 -0
- package/src/checks/automatic/duplicate-id-aria.js +4 -3
- package/src/checks/automatic/embed-text-alternative-present.js +2 -0
- package/src/checks/automatic/form-control-programmatic-label-present.js +2 -0
- package/src/checks/automatic/form-control-single-label.js +6 -7
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
- package/src/checks/automatic/iframe-focusable-content.js +246 -18
- package/src/checks/automatic/iframe-name-present.js +2 -0
- package/src/checks/automatic/iframe-title-unique.js +3 -1
- package/src/checks/automatic/img-alt-present.js +7 -9
- package/src/checks/automatic/input-image-alt-present.js +4 -6
- package/src/checks/automatic/label-in-name.js +15 -19
- package/src/checks/automatic/language-page-present.js +2 -0
- package/src/checks/automatic/link-in-text-block.js +2 -0
- package/src/checks/automatic/link-name-present.js +2 -0
- package/src/checks/automatic/list-children-valid.js +14 -24
- package/src/checks/automatic/listbox-name-present.js +2 -0
- package/src/checks/automatic/listitem-parent-valid.js +30 -7
- package/src/checks/automatic/menuitem-name-present.js +2 -0
- package/src/checks/automatic/meta-refresh-no-exceptions.js +4 -4
- package/src/checks/automatic/meta-refresh-timing-absent.js +2 -0
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +2 -0
- package/src/checks/automatic/meter-name-present.js +4 -3
- package/src/checks/automatic/nested-interactive-controls-absent.js +27 -5
- package/src/checks/automatic/object-text-alternative-present.js +2 -0
- package/src/checks/automatic/option-name-present.js +2 -0
- package/src/checks/automatic/page-title-present.js +2 -0
- package/src/checks/automatic/progressbar-name-present.js +8 -10
- package/src/checks/automatic/role-img-alt-present.js +4 -4
- package/src/checks/automatic/searchbox-name-present.js +2 -0
- package/src/checks/automatic/server-side-image-map-absent.js +4 -3
- package/src/checks/automatic/slider-name-present.js +2 -0
- package/src/checks/automatic/spinbutton-name-present.js +2 -0
- package/src/checks/automatic/summary-name-present.js +2 -0
- package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
- package/src/checks/automatic/svg-text-alternative-present.js +17 -5
- package/src/checks/automatic/tab-name-present.js +2 -0
- package/src/checks/automatic/table-headers-attr-valid.js +3 -2
- package/src/checks/automatic/table-th-has-data-cells.js +2 -0
- package/src/checks/automatic/target-size-minimum.js +5 -0
- package/src/checks/automatic/td-has-header.js +24 -1
- package/src/checks/automatic/textbox-name-present.js +2 -0
- package/src/checks/automatic/tooltip-name-present.js +2 -0
- package/src/checks/automatic/treeitem-name-present.js +2 -0
- package/src/checks/automatic/valid-lang.js +2 -0
- package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
- package/src/checks/manual/accesskeys-manual.js +3 -1
- package/src/checks/manual/area-alt-decorative-manual.js +2 -0
- package/src/checks/manual/area-alt-quality-manual.js +2 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
- package/src/checks/manual/aria-text-manual.js +6 -5
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/css-hidden-focus.js +184 -9
- package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
- package/src/checks/manual/empty-heading-manual.js +17 -17
- package/src/checks/manual/empty-table-header-manual.js +52 -25
- package/src/checks/manual/focus-order-semantics-manual.js +16 -4
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
- package/src/checks/manual/heading-order-manual.js +28 -1
- package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
- package/src/checks/manual/image-redundant-alt-manual.js +21 -1
- package/src/checks/manual/img-alt-decorative-manual.js +2 -0
- package/src/checks/manual/img-alt-quality-manual.js +2 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +2 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
- package/src/checks/manual/label-title-only-manual.js +29 -22
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +54 -47
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +42 -26
- package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
- package/src/checks/manual/landmark-one-main-manual.js +12 -23
- package/src/checks/manual/landmark-unique-manual.js +37 -52
- package/src/checks/manual/link-name-quality-manual.js +2 -0
- package/src/checks/manual/media-transcript-present-manual.js +2 -0
- package/src/checks/manual/meta-viewport-large-manual.js +3 -1
- package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
- package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
- package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/p-as-heading-manual.js +2 -0
- package/src/checks/manual/page-has-heading-one-manual.js +12 -11
- package/src/checks/manual/page-title-patterns-manual.js +2 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
- package/src/checks/manual/region-manual.js +27 -36
- package/src/checks/manual/scope-attr-valid-manual.js +3 -1
- package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
- package/src/checks/manual/skip-link-manual.js +7 -6
- package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/tabindex-manual.js +3 -1
- package/src/checks/manual/table-duplicate-name-manual.js +5 -4
- package/src/checks/manual/table-fake-caption-manual.js +24 -3
- package/src/checks/manual/video-caption-manual.js +2 -0
- package/src/checks/manual-review.js +2 -0
- package/src/core.js +6772 -2158
- package/src/index.js +2 -0
- package/src/report.js +51 -9
- package/src/sarif.js +18 -3
- package/surea11y.browser.js +2731 -999
- package/bin/core.js +0 -473
- package/docs/CLI.md +0 -128
- package/src/catalogs/composites.wcag.js +0 -454
- package/src/checks/rules-and-tags.full.csv +0 -19
- package/src/checks/rules-and-tags.full.json +0 -259
- package/src/core/aria-helpers.js +0 -1211
- package/src/core/contrast-helpers.js +0 -1302
- package/src/core/dom-helpers.js +0 -4493
- package/src/core/dom-runner.js +0 -787
- package/src/core/frame-messaging.js +0 -261
- package/src/core/frame-scan.js +0 -190
- package/src/core/rollup-composites.js +0 -127
- package/src/core/rule-meta.js +0 -176
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -1228
- package/src/i18n/fr.js +0 -1185
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -59
- package/src/policy/schemas/engine-options.schema.json +0 -103
- 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 (
|
|
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
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
|
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
|
|
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
|
|
148
|
-
//
|
|
149
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
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
|
|
345
|
-
//
|
|
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 (
|
|
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,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 (
|
|
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"
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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,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 (
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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';
|