@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.
- package/CHANGELOG.md +81 -7
- package/LICENSE +373 -21
- package/README.md +175 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +27 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +19 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +63 -18
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +63 -31
- package/src/checks/automatic/aria-allowed-attr.js +204 -80
- package/src/checks/automatic/aria-allowed-role.js +23 -7
- package/src/checks/automatic/aria-braille-equivalent.js +34 -10
- package/src/checks/automatic/aria-conditional-attr.js +32 -14
- package/src/checks/automatic/aria-deprecated-role.js +26 -11
- package/src/checks/automatic/aria-hidden-body.js +48 -23
- package/src/checks/automatic/aria-hidden-focus.js +420 -66
- package/src/checks/automatic/aria-prohibited-attr.js +327 -60
- package/src/checks/automatic/aria-prohibited-children.js +111 -103
- package/src/checks/automatic/aria-required-attr.js +29 -15
- package/src/checks/automatic/aria-required-children.js +44 -24
- package/src/checks/automatic/aria-required-parent.js +64 -35
- package/src/checks/automatic/aria-role-name-present.js +49 -21
- package/src/checks/automatic/aria-roles-valid.js +24 -12
- package/src/checks/automatic/aria-valid-attr-value.js +46 -22
- package/src/checks/automatic/aria-valid-attr.js +19 -5
- package/src/checks/automatic/autocomplete-valid.js +76 -16
- package/src/checks/automatic/avoid-inline-spacing.js +23 -8
- package/src/checks/automatic/binary-control-name-present.js +62 -50
- package/src/checks/automatic/button-name-present.js +54 -24
- package/src/checks/automatic/bypass-blocks-present.js +51 -32
- package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
- package/src/checks/automatic/combobox-name-present.js +40 -45
- package/src/checks/automatic/contrast-computable.js +363 -341
- package/src/checks/automatic/contrast-enhanced.js +489 -466
- package/src/checks/automatic/contrast-minimum.js +488 -465
- package/src/checks/automatic/css-orientation-lock.js +51 -35
- package/src/checks/automatic/definition-list-children-valid.js +46 -25
- package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
- package/src/checks/automatic/dialog-name-present.js +47 -85
- package/src/checks/automatic/dlitem-parent-valid.js +25 -8
- package/src/checks/automatic/duplicate-id-aria.js +28 -9
- package/src/checks/automatic/embed-text-alternative-present.js +88 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
- package/src/checks/automatic/form-control-single-label.js +50 -14
- package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
- package/src/checks/automatic/iframe-focusable-content.js +265 -22
- package/src/checks/automatic/iframe-name-present.js +33 -9
- package/src/checks/automatic/iframe-title-unique.js +32 -9
- package/src/checks/automatic/img-alt-present.js +54 -52
- package/src/checks/automatic/input-image-alt-present.js +141 -112
- package/src/checks/automatic/label-in-name.js +65 -41
- package/src/checks/automatic/language-page-present.js +111 -109
- package/src/checks/automatic/link-in-text-block.js +61 -19
- package/src/checks/automatic/link-name-present.js +47 -14
- package/src/checks/automatic/list-children-valid.js +40 -33
- package/src/checks/automatic/listbox-name-present.js +41 -19
- package/src/checks/automatic/listitem-parent-valid.js +48 -13
- package/src/checks/automatic/menuitem-name-present.js +41 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
- package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
- package/src/checks/automatic/meter-name-present.js +40 -36
- package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
- package/src/checks/automatic/object-text-alternative-present.js +93 -39
- package/src/checks/automatic/option-name-present.js +40 -21
- package/src/checks/automatic/page-title-present.js +19 -6
- package/src/checks/automatic/progressbar-name-present.js +49 -44
- package/src/checks/automatic/role-img-alt-present.js +211 -159
- package/src/checks/automatic/searchbox-name-present.js +41 -19
- package/src/checks/automatic/server-side-image-map-absent.js +27 -11
- package/src/checks/automatic/slider-name-present.js +42 -47
- package/src/checks/automatic/spinbutton-name-present.js +41 -19
- package/src/checks/automatic/summary-name-present.js +39 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
- package/src/checks/automatic/svg-text-alternative-present.js +262 -230
- package/src/checks/automatic/tab-name-present.js +39 -60
- package/src/checks/automatic/table-headers-attr-valid.js +27 -10
- package/src/checks/automatic/table-th-has-data-cells.js +24 -8
- package/src/checks/automatic/target-size-minimum.js +123 -48
- package/src/checks/automatic/td-has-header.js +53 -12
- package/src/checks/automatic/textbox-name-present.js +41 -19
- package/src/checks/automatic/tooltip-name-present.js +39 -18
- package/src/checks/automatic/treeitem-name-present.js +40 -21
- package/src/checks/automatic/valid-lang.js +22 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
- package/src/checks/manual/accesskeys-manual.js +17 -6
- package/src/checks/manual/area-alt-decorative-manual.js +194 -193
- package/src/checks/manual/area-alt-quality-manual.js +184 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
- package/src/checks/manual/aria-text-manual.js +20 -11
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
- package/src/checks/manual/css-hidden-focus.js +375 -169
- package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
- package/src/checks/manual/empty-heading-manual.js +41 -24
- package/src/checks/manual/empty-table-header-manual.js +69 -31
- package/src/checks/manual/focus-order-semantics-manual.js +60 -13
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
- package/src/checks/manual/heading-order-manual.js +50 -8
- package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
- package/src/checks/manual/image-redundant-alt-manual.js +38 -8
- package/src/checks/manual/img-alt-decorative-manual.js +133 -96
- package/src/checks/manual/img-alt-quality-manual.js +178 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
- package/src/checks/manual/label-title-only-manual.js +44 -28
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
- package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
- package/src/checks/manual/landmark-one-main-manual.js +38 -43
- package/src/checks/manual/landmark-unique-manual.js +78 -67
- package/src/checks/manual/link-name-quality-manual.js +45 -12
- package/src/checks/manual/media-transcript-present-manual.js +37 -22
- package/src/checks/manual/meta-viewport-large-manual.js +19 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
- package/src/checks/manual/p-as-heading-manual.js +24 -7
- package/src/checks/manual/page-has-heading-one-manual.js +42 -32
- package/src/checks/manual/page-title-patterns-manual.js +80 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
- package/src/checks/manual/region-manual.js +244 -60
- package/src/checks/manual/scope-attr-valid-manual.js +13 -4
- package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
- package/src/checks/manual/skip-link-manual.js +42 -18
- package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
- package/src/checks/manual/tabindex-manual.js +13 -4
- package/src/checks/manual/table-duplicate-name-manual.js +22 -11
- package/src/checks/manual/table-fake-caption-manual.js +48 -10
- package/src/checks/manual/video-caption-manual.js +17 -4
- package/src/checks/manual-review.js +58 -12
- package/src/core.js +41705 -29650
- package/src/index.js +2 -0
- package/src/report.js +109 -47
- package/src/sarif.js +190 -0
- package/surea11y.browser.js +37774 -0
- package/bin/core.js +0 -348
- package/docs/CLI.md +0 -75
- package/src/catalogs/composites.wcag.js +0 -490
- 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 -970
- package/src/core/contrast-helpers.js +0 -1147
- package/src/core/dom-helpers.js +0 -4235
- package/src/core/dom-runner.js +0 -671
- package/src/core/frame-messaging.js +0 -210
- package/src/core/frame-scan.js +0 -178
- package/src/core/rollup-composites.js +0 -135
- package/src/core/rule-meta.js +0 -159
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -923
- package/src/i18n/fr.js +0 -844
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -55
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -42,7 +44,13 @@ const meta = {
|
|
|
42
44
|
tags: ['wcag2aaa', 'wcag249', 'navigation', 'atomic', 'manual'],
|
|
43
45
|
wcagSc: ['2.4.9'],
|
|
44
46
|
normativeMappings: [
|
|
45
|
-
{
|
|
47
|
+
{
|
|
48
|
+
standard: 'WCAG',
|
|
49
|
+
version: '2.2',
|
|
50
|
+
requirement: '2.4.9',
|
|
51
|
+
title: 'Link Purpose (Link Only)',
|
|
52
|
+
conformanceLevel: 'AAA'
|
|
53
|
+
}
|
|
46
54
|
],
|
|
47
55
|
defaultSeverity: 'minor',
|
|
48
56
|
category: 'operable',
|
|
@@ -52,13 +60,15 @@ const meta = {
|
|
|
52
60
|
};
|
|
53
61
|
|
|
54
62
|
function runInPage(ctx) {
|
|
55
|
-
const {
|
|
63
|
+
const { helpers, rule } = ctx;
|
|
56
64
|
|
|
57
65
|
function normName(s) {
|
|
58
66
|
return (s == null ? '' : String(s)).replace(/\s+/g, ' ').trim().toLowerCase();
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
const nodes = helpers.queryAllSmart
|
|
69
|
+
const nodes = helpers.queryAllSmart
|
|
70
|
+
? helpers.queryAllSmart('a[href]')
|
|
71
|
+
: helpers.queryAll('a[href]');
|
|
62
72
|
|
|
63
73
|
const groups = new Map(); // normName -> [{ el, href }]
|
|
64
74
|
let applicableCount = 0;
|
|
@@ -67,18 +77,24 @@ function runInPage(ctx) {
|
|
|
67
77
|
if (!el || !el.getAttribute) continue;
|
|
68
78
|
|
|
69
79
|
const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
|
|
70
|
-
const eligible =
|
|
80
|
+
const eligible =
|
|
81
|
+
typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
|
|
71
82
|
if (!eligible) continue;
|
|
72
83
|
|
|
73
84
|
const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
|
|
74
|
-
const rawName =
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
const rawName =
|
|
86
|
+
nameInfo && typeof nameInfo.value === 'string' && nameInfo.value.trim()
|
|
87
|
+
? nameInfo.value
|
|
88
|
+
: el.textContent || '';
|
|
77
89
|
const name = normName(rawName);
|
|
78
90
|
if (!name) continue;
|
|
79
91
|
|
|
80
|
-
let href
|
|
81
|
-
try {
|
|
92
|
+
let href;
|
|
93
|
+
try {
|
|
94
|
+
href = String(el.href || '');
|
|
95
|
+
} catch {
|
|
96
|
+
href = '';
|
|
97
|
+
}
|
|
82
98
|
if (!href) continue;
|
|
83
99
|
|
|
84
100
|
applicableCount += 1;
|
|
@@ -95,12 +111,15 @@ function runInPage(ctx) {
|
|
|
95
111
|
|
|
96
112
|
for (const { el, href } of entries) {
|
|
97
113
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
98
|
-
const html = helpers.getOuterHtmlSnippet
|
|
114
|
+
const html = helpers.getOuterHtmlSnippet
|
|
115
|
+
? helpers.getOuterHtmlSnippet(el)
|
|
116
|
+
: el.outerHTML || '';
|
|
99
117
|
|
|
100
118
|
const baseOccurrence = {
|
|
101
119
|
selector: stableSelector,
|
|
102
120
|
html,
|
|
103
|
-
summary:
|
|
121
|
+
summary:
|
|
122
|
+
'This link shares an accessible name with other links on the page that lead to a different destination.',
|
|
104
123
|
hint: 'Ensure links with the same text serve the same purpose, or make the link text distinct enough to describe each destination.',
|
|
105
124
|
i18n: {
|
|
106
125
|
summaryKey: 'identicalLinksSamePurpose_summary_cantTell',
|
|
@@ -130,7 +149,12 @@ function runInPage(ctx) {
|
|
|
130
149
|
}
|
|
131
150
|
|
|
132
151
|
if (occurrences.length) {
|
|
133
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
ruleId: rule.ruleId,
|
|
154
|
+
outcome: 'cantTell',
|
|
155
|
+
severity: rule.defaultSeverity || 'minor',
|
|
156
|
+
occurrences
|
|
157
|
+
};
|
|
134
158
|
}
|
|
135
159
|
|
|
136
160
|
// Manual rules may only emit cantTell/notApplicable (never pass/fail):
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @check image-redundant-alt
|
|
5
7
|
* @atomic true
|
|
6
8
|
* @summary An image's alt text must not duplicate adjacent visible text
|
|
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 <img> elements with non-empty alt text whose immediate
|
|
10
12
|
* parent element also has other visible text content (i.e. text nodes
|
|
@@ -30,7 +32,8 @@ const id = 'image-redundant-alt';
|
|
|
30
32
|
|
|
31
33
|
const meta = {
|
|
32
34
|
title: 'Image alt text must not duplicate adjacent visible text',
|
|
33
|
-
description:
|
|
35
|
+
description:
|
|
36
|
+
'Checks that an <img> alt text is not identical to other visible text already present in its immediate parent element.',
|
|
34
37
|
i18n: {
|
|
35
38
|
titleKey: 'imageRedundantAlt_title',
|
|
36
39
|
descriptionKey: 'imageRedundantAlt_description'
|
|
@@ -47,26 +50,48 @@ const meta = {
|
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
function runInPage(ctx) {
|
|
50
|
-
const {
|
|
53
|
+
const { helpers, rule } = ctx;
|
|
51
54
|
|
|
52
55
|
function normalizeWs(s) {
|
|
53
|
-
return String(s || '')
|
|
56
|
+
return String(s || '')
|
|
57
|
+
.replace(/\s+/g, ' ')
|
|
58
|
+
.trim();
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
function getOwnTextExcludingImg(parent, imgEl) {
|
|
62
|
+
const isAccTreeEligible =
|
|
63
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
64
|
+
|
|
57
65
|
let text = '';
|
|
58
66
|
for (const child of parent.childNodes || []) {
|
|
59
67
|
if (child === imgEl) continue;
|
|
60
68
|
if (child.nodeType === 3) {
|
|
61
69
|
text += ' ' + (child.nodeValue || '');
|
|
62
70
|
} else if (child.nodeType === 1 && child !== imgEl) {
|
|
71
|
+
// An aria-hidden sibling is never actually announced to assistive
|
|
72
|
+
// technology, so its text can't cause the "same words twice"
|
|
73
|
+
// double-announcement this rule exists to catch -- counting it
|
|
74
|
+
// anyway flags a redundancy that doesn't exist in what AT users
|
|
75
|
+
// actually hear.
|
|
76
|
+
if (isAccTreeEligible) {
|
|
77
|
+
const elig = (() => {
|
|
78
|
+
try {
|
|
79
|
+
return isAccTreeEligible(child, ctx);
|
|
80
|
+
} catch {
|
|
81
|
+
return { eligible: true, reasons: [] };
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
if (elig && elig.eligible === false) continue;
|
|
85
|
+
}
|
|
63
86
|
text += ' ' + (child.textContent || '');
|
|
64
87
|
}
|
|
65
88
|
}
|
|
66
89
|
return normalizeWs(text);
|
|
67
90
|
}
|
|
68
91
|
|
|
69
|
-
const nodes = helpers.queryAllSmart
|
|
92
|
+
const nodes = helpers.queryAllSmart
|
|
93
|
+
? helpers.queryAllSmart('img[alt]')
|
|
94
|
+
: helpers.queryAll('img[alt]');
|
|
70
95
|
|
|
71
96
|
const occurrences = [];
|
|
72
97
|
let applicableCount = 0;
|
|
@@ -87,12 +112,12 @@ function runInPage(ctx) {
|
|
|
87
112
|
if (otherText.toLowerCase() !== alt.toLowerCase()) continue;
|
|
88
113
|
|
|
89
114
|
const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
|
|
90
|
-
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) :
|
|
115
|
+
const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
|
|
91
116
|
|
|
92
117
|
occurrences.push({
|
|
93
118
|
selector: stableSelector,
|
|
94
119
|
html,
|
|
95
|
-
summary:
|
|
120
|
+
summary: "This image's alt text duplicates other visible text right next to it.",
|
|
96
121
|
hint: 'Make the alt text empty (alt="") if the image is purely decorative alongside the text, or remove the redundant duplication.',
|
|
97
122
|
i18n: {
|
|
98
123
|
summaryKey: 'imageRedundantAlt_summary_cantTell',
|
|
@@ -109,7 +134,12 @@ function runInPage(ctx) {
|
|
|
109
134
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
110
135
|
}
|
|
111
136
|
if (occurrences.length) {
|
|
112
|
-
return {
|
|
137
|
+
return {
|
|
138
|
+
ruleId: rule.ruleId,
|
|
139
|
+
outcome: 'cantTell',
|
|
140
|
+
severity: rule.defaultSeverity || 'minor',
|
|
141
|
+
occurrences
|
|
142
|
+
};
|
|
113
143
|
}
|
|
114
144
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
115
145
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -11,20 +13,27 @@
|
|
|
11
13
|
* Human review is required to confirm that the provided text alternative is accurate and appropriate.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
const id =
|
|
16
|
+
const id = 'img-alt-decorative';
|
|
15
17
|
|
|
16
18
|
const meta = {
|
|
17
|
-
title:
|
|
18
|
-
description:
|
|
19
|
+
title: '<img> with alt="" must be decorative (manual review)',
|
|
20
|
+
description:
|
|
21
|
+
'Flags <img> elements with empty alt for human review that they are purely decorative.',
|
|
19
22
|
i18n: {
|
|
20
|
-
titleKey:
|
|
21
|
-
descriptionKey:
|
|
23
|
+
titleKey: 'img_altDecorative_title',
|
|
24
|
+
descriptionKey: 'img_altDecorative_description'
|
|
22
25
|
},
|
|
23
26
|
helpUrl: null,
|
|
24
|
-
tags: [
|
|
27
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'manual', 'atomic'],
|
|
25
28
|
wcagSc: ['1.1.1'],
|
|
26
29
|
normativeMappings: [
|
|
27
|
-
{
|
|
30
|
+
{
|
|
31
|
+
standard: 'WCAG',
|
|
32
|
+
version: '2.2',
|
|
33
|
+
requirement: '1.1.1',
|
|
34
|
+
title: 'Non-text Content',
|
|
35
|
+
conformanceLevel: 'A'
|
|
36
|
+
}
|
|
28
37
|
],
|
|
29
38
|
defaultSeverity: 'minor',
|
|
30
39
|
category: 'perceivable',
|
|
@@ -38,111 +47,139 @@ const meta = {
|
|
|
38
47
|
};
|
|
39
48
|
|
|
40
49
|
function runInPage(ctx) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
const { document, root, helpers, rule } = ctx;
|
|
51
|
+
const safeRoot = root || document;
|
|
52
|
+
|
|
53
|
+
const queryAllSmart =
|
|
54
|
+
helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
|
|
55
|
+
const queryAll =
|
|
56
|
+
helpers && typeof helpers.queryAll === 'function'
|
|
57
|
+
? helpers.queryAll
|
|
58
|
+
: (sel) => {
|
|
59
|
+
try {
|
|
60
|
+
return safeRoot && safeRoot.querySelectorAll
|
|
61
|
+
? Array.from(safeRoot.querySelectorAll(sel))
|
|
62
|
+
: [];
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
51
66
|
};
|
|
52
67
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
const isAccTreeEligible =
|
|
69
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
70
|
+
|
|
71
|
+
const getFocusableInfo =
|
|
72
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
73
|
+
|
|
74
|
+
function isRolePresentationExcluded(el) {
|
|
75
|
+
const role = (() => {
|
|
76
|
+
try {
|
|
77
|
+
return String(el.getAttribute('role') || '')
|
|
78
|
+
.trim()
|
|
79
|
+
.toLowerCase();
|
|
80
|
+
} catch {
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
if (role !== 'presentation' && role !== 'none') return false;
|
|
85
|
+
|
|
86
|
+
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
87
|
+
let focusable;
|
|
88
|
+
if (getFocusableInfo) {
|
|
89
|
+
const fi = (() => {
|
|
90
|
+
try {
|
|
91
|
+
return getFocusableInfo(el, ctx);
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
76
94
|
}
|
|
77
|
-
|
|
95
|
+
})();
|
|
96
|
+
focusable = !!(fi && fi.focusable);
|
|
97
|
+
} else {
|
|
98
|
+
const tabindex = el.getAttribute('tabindex');
|
|
99
|
+
focusable =
|
|
100
|
+
tabindex != null &&
|
|
101
|
+
String(tabindex).trim() !== '' &&
|
|
102
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
78
103
|
}
|
|
104
|
+
return !focusable;
|
|
105
|
+
}
|
|
79
106
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const seen = new Set();
|
|
90
|
-
for (const el of els) {
|
|
91
|
-
if (!seen.has(el)) { seen.add(el); uniqueEls.push(el); }
|
|
107
|
+
const els = (() => {
|
|
108
|
+
// Only likely candidates:
|
|
109
|
+
// - alt="" (exact)
|
|
110
|
+
// - alt that starts/ends with space (to catch whitespace-only like " ")
|
|
111
|
+
const sel = 'img[alt=""], img[alt^=" "], img[alt$=" "]';
|
|
112
|
+
try {
|
|
113
|
+
return Array.from((queryAllSmart ? queryAllSmart(sel) : queryAll(sel)) || []);
|
|
114
|
+
} catch {
|
|
115
|
+
return queryAll(sel);
|
|
92
116
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
117
|
+
})();
|
|
118
|
+
const uniqueEls = [];
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
for (const el of els) {
|
|
121
|
+
if (!seen.has(el)) {
|
|
122
|
+
seen.add(el);
|
|
123
|
+
uniqueEls.push(el);
|
|
96
124
|
}
|
|
125
|
+
}
|
|
97
126
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
for (const el of uniqueEls) {
|
|
102
|
-
if (!el || !el.getAttribute) continue;
|
|
127
|
+
if (!els.length) {
|
|
128
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
129
|
+
}
|
|
103
130
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
|
|
107
|
-
})();
|
|
108
|
-
if (elig && elig.eligible === false) continue;
|
|
109
|
-
}
|
|
131
|
+
const occurrences = [];
|
|
132
|
+
let applicableCount = 0;
|
|
110
133
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Rule-specific applicability (only elements that already have a text alternative mechanism)
|
|
114
|
-
const rawAlt = el.getAttribute('alt');
|
|
115
|
-
if (rawAlt == null) continue;
|
|
116
|
-
if (String(rawAlt).trim() !== '') continue;
|
|
117
|
-
|
|
118
|
-
applicableCount += 1;
|
|
119
|
-
|
|
120
|
-
const baseOccurrence = {
|
|
121
|
-
summary: 'Review whether <img> is decorative (alt="").',
|
|
122
|
-
hint: 'Confirm the image is purely decorative. If it conveys information or function, provide meaningful alt text.',
|
|
123
|
-
i18n: {
|
|
124
|
-
summaryKey: 'img_altDecorative_summary_cantTell',
|
|
125
|
-
hintKey: 'img_altDecorative_hint_cantTell',
|
|
126
|
-
params: { element: 'img' }
|
|
127
|
-
},
|
|
128
|
-
data: {
|
|
129
|
-
visibilityFilter: { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
130
|
-
details: null
|
|
131
|
-
}
|
|
132
|
-
};
|
|
134
|
+
for (const el of uniqueEls) {
|
|
135
|
+
if (!el || !el.getAttribute) continue;
|
|
133
136
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
if (isAccTreeEligible) {
|
|
138
|
+
const elig = (() => {
|
|
139
|
+
try {
|
|
140
|
+
return isAccTreeEligible(el, ctx);
|
|
141
|
+
} catch {
|
|
142
|
+
return { eligible: true, reasons: [] };
|
|
138
143
|
}
|
|
144
|
+
})();
|
|
145
|
+
if (elig && elig.eligible === false) continue;
|
|
139
146
|
}
|
|
140
147
|
|
|
141
|
-
if (
|
|
142
|
-
|
|
148
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
149
|
+
|
|
150
|
+
// Rule-specific applicability (only elements that already have a text alternative mechanism)
|
|
151
|
+
const rawAlt = el.getAttribute('alt');
|
|
152
|
+
if (rawAlt == null) continue;
|
|
153
|
+
if (String(rawAlt).trim() !== '') continue;
|
|
154
|
+
|
|
155
|
+
applicableCount += 1;
|
|
156
|
+
|
|
157
|
+
const baseOccurrence = {
|
|
158
|
+
summary: 'Review whether <img> is decorative (alt="").',
|
|
159
|
+
hint: 'Confirm the image is purely decorative. If it conveys information or function, provide meaningful alt text.',
|
|
160
|
+
i18n: {
|
|
161
|
+
summaryKey: 'img_altDecorative_summary_cantTell',
|
|
162
|
+
hintKey: 'img_altDecorative_hint_cantTell',
|
|
163
|
+
params: { element: 'img' }
|
|
164
|
+
},
|
|
165
|
+
data: {
|
|
166
|
+
visibilityFilter: { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
167
|
+
details: null
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
172
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
173
|
+
} else {
|
|
174
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
143
175
|
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (applicableCount === 0) {
|
|
179
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
180
|
+
}
|
|
144
181
|
|
|
145
|
-
|
|
182
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
146
183
|
}
|
|
147
184
|
|
|
148
185
|
module.exports = { id, meta, runInPage };
|