@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
|
/**
|
|
@@ -11,237 +13,236 @@
|
|
|
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 = 'area-alt-decorative';
|
|
15
17
|
|
|
16
18
|
const meta = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
19
|
+
title: '<area> with alt="" must be decorative (manual review)',
|
|
20
|
+
description:
|
|
21
|
+
'Flags <area> elements with empty alt for human review that they are decorative/non-informative.',
|
|
22
|
+
i18n: {
|
|
23
|
+
titleKey: 'area_altDecorative_title',
|
|
24
|
+
descriptionKey: 'area_altDecorative_description'
|
|
25
|
+
},
|
|
26
|
+
helpUrl: null,
|
|
27
|
+
tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'imagemap', 'manual', 'atomic'],
|
|
28
|
+
wcagSc: ['1.1.1'],
|
|
29
|
+
normativeMappings: [
|
|
30
|
+
{
|
|
31
|
+
standard: 'WCAG',
|
|
32
|
+
version: '2.2',
|
|
33
|
+
requirement: '1.1.1',
|
|
34
|
+
title: 'Non-text Content',
|
|
35
|
+
conformanceLevel: 'A'
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
defaultSeverity: 'minor',
|
|
39
|
+
category: 'perceivable',
|
|
40
|
+
type: 'manual',
|
|
41
|
+
defaultConfidence: 'medium',
|
|
42
|
+
coverage: {
|
|
43
|
+
facetsBySc: {
|
|
44
|
+
'1.1.1': ['text-alternative-quality']
|
|
37
45
|
}
|
|
46
|
+
}
|
|
38
47
|
};
|
|
39
48
|
|
|
40
49
|
function runInPage(ctx) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|
|
53
66
|
};
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return res;
|
|
68
|
+
const __accEligCache = new WeakMap();
|
|
69
|
+
function accEligibleCached(node) {
|
|
70
|
+
if (!isAccTreeEligible) return { eligible: true, reasons: [] };
|
|
71
|
+
if (!node || typeof node !== 'object') return { eligible: true, reasons: [] };
|
|
72
|
+
const cached = __accEligCache.get(node);
|
|
73
|
+
if (cached) return cached;
|
|
74
|
+
let res;
|
|
75
|
+
try {
|
|
76
|
+
res = isAccTreeEligible(node, ctx);
|
|
77
|
+
} catch {
|
|
78
|
+
res = { eligible: true, reasons: [] };
|
|
67
79
|
}
|
|
80
|
+
res = res && typeof res === 'object' ? res : { eligible: !!res, reasons: [] };
|
|
81
|
+
__accEligCache.set(node, res);
|
|
82
|
+
return res;
|
|
83
|
+
}
|
|
68
84
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
: null;
|
|
85
|
+
const getEligibilityInfo =
|
|
86
|
+
helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
|
|
72
87
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
: null;
|
|
88
|
+
const isAccTreeEligible =
|
|
89
|
+
helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
76
90
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return '';
|
|
86
|
-
}
|
|
91
|
+
// --- image-map semantics (rule-local; match automatic <area> applicability) ---
|
|
92
|
+
function normUsemap(val) {
|
|
93
|
+
try {
|
|
94
|
+
const t = String(val || '').trim();
|
|
95
|
+
if (!t) return '';
|
|
96
|
+
return t[0] === '#' ? t.slice(1).trim().toLowerCase() : t.toLowerCase();
|
|
97
|
+
} catch {
|
|
98
|
+
return '';
|
|
87
99
|
}
|
|
100
|
+
}
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
102
|
+
function getMapName(mapEl) {
|
|
103
|
+
try {
|
|
104
|
+
if (!mapEl || !mapEl.getAttribute) return '';
|
|
105
|
+
const n = String(mapEl.getAttribute('name') || mapEl.getAttribute('id') || '').trim();
|
|
106
|
+
return n ? n.toLowerCase() : '';
|
|
107
|
+
} catch {
|
|
108
|
+
return '';
|
|
97
109
|
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const getFocusableInfo =
|
|
113
|
+
helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
|
|
114
|
+
|
|
115
|
+
function isRolePresentationExcluded(el) {
|
|
116
|
+
const role = (() => {
|
|
117
|
+
try {
|
|
118
|
+
return String(el.getAttribute('role') || '')
|
|
119
|
+
.trim()
|
|
120
|
+
.toLowerCase();
|
|
121
|
+
} catch {
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
})();
|
|
125
|
+
if (role !== 'presentation' && role !== 'none') return false;
|
|
98
126
|
|
|
99
|
-
|
|
127
|
+
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
128
|
+
let focusable;
|
|
129
|
+
if (getFocusableInfo) {
|
|
130
|
+
const fi = (() => {
|
|
100
131
|
try {
|
|
101
|
-
|
|
102
|
-
const map = areaEl.closest('map');
|
|
103
|
-
if (!map) return null;
|
|
104
|
-
const mapName = getMapName(map);
|
|
105
|
-
if (!mapName) return null;
|
|
106
|
-
|
|
107
|
-
const imgs = Array.from(document.querySelectorAll('img[usemap]'));
|
|
108
|
-
for (const img of imgs) {
|
|
109
|
-
const u = normUsemap(img.getAttribute('usemap'));
|
|
110
|
-
if (u && u === mapName) return img; // deterministic: first match in doc order
|
|
111
|
-
}
|
|
132
|
+
return getFocusableInfo(el, ctx);
|
|
112
133
|
} catch {
|
|
134
|
+
return null;
|
|
113
135
|
}
|
|
114
|
-
|
|
136
|
+
})();
|
|
137
|
+
focusable = !!(fi && fi.focusable);
|
|
138
|
+
} else {
|
|
139
|
+
const tabindex = el.getAttribute('tabindex');
|
|
140
|
+
focusable =
|
|
141
|
+
tabindex != null &&
|
|
142
|
+
String(tabindex).trim() !== '' &&
|
|
143
|
+
!Number.isNaN(Number(String(tabindex).trim()));
|
|
115
144
|
}
|
|
145
|
+
return !focusable;
|
|
146
|
+
}
|
|
116
147
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const role = (() => {
|
|
123
|
-
try {
|
|
124
|
-
return String(el.getAttribute('role') || '').trim().toLowerCase();
|
|
125
|
-
} catch {
|
|
126
|
-
return '';
|
|
127
|
-
}
|
|
128
|
-
})();
|
|
129
|
-
if (role !== 'presentation' && role !== 'none') return false;
|
|
130
|
-
|
|
131
|
-
// Exclude only when NOT focusable (mirrors img-alt-present policy)
|
|
132
|
-
let focusable = false;
|
|
133
|
-
if (getFocusableInfo) {
|
|
134
|
-
const fi = (() => {
|
|
135
|
-
try {
|
|
136
|
-
return getFocusableInfo(el, ctx);
|
|
137
|
-
} catch {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
})();
|
|
141
|
-
focusable = !!(fi && fi.focusable);
|
|
142
|
-
} else {
|
|
143
|
-
const tabindex = el.getAttribute('tabindex');
|
|
144
|
-
focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
|
|
145
|
-
}
|
|
146
|
-
return !focusable;
|
|
148
|
+
const els = (() => {
|
|
149
|
+
try {
|
|
150
|
+
return Array.from((queryAllSmart ? queryAllSmart('area') : queryAll('area')) || []);
|
|
151
|
+
} catch {
|
|
152
|
+
return queryAll('area');
|
|
147
153
|
}
|
|
154
|
+
})();
|
|
155
|
+
|
|
156
|
+
const __usemapIndex = new Map(); // mapName -> img (first in document order)
|
|
157
|
+
try {
|
|
158
|
+
const imgs = Array.from(document.querySelectorAll('img[usemap]'));
|
|
159
|
+
for (const img of imgs) {
|
|
160
|
+
const u = normUsemap(img.getAttribute('usemap'));
|
|
161
|
+
if (!u) continue;
|
|
162
|
+
if (!__usemapIndex.has(u)) __usemapIndex.set(u, img); // keep first match only
|
|
163
|
+
}
|
|
164
|
+
} catch {}
|
|
148
165
|
|
|
166
|
+
if (!els.length) {
|
|
167
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
168
|
+
}
|
|
149
169
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return Array.from((queryAllSmart ? queryAllSmart("area") : queryAll("area")) || []);
|
|
153
|
-
} catch {
|
|
154
|
-
return queryAll("area");
|
|
155
|
-
}
|
|
156
|
-
})();
|
|
170
|
+
const occurrences = [];
|
|
171
|
+
let applicableCount = 0;
|
|
157
172
|
|
|
158
|
-
|
|
173
|
+
for (const el of els) {
|
|
174
|
+
if (!el || !el.getAttribute) continue;
|
|
175
|
+
|
|
176
|
+
// Must belong to a *used* image map (referenced by an <img usemap>). If unused, not applicable.
|
|
177
|
+
let img;
|
|
159
178
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (!u) continue;
|
|
164
|
-
if (!__usemapIndex.has(u)) __usemapIndex.set(u, img); // keep first match only
|
|
165
|
-
}
|
|
179
|
+
const map = el.closest && el.closest('map');
|
|
180
|
+
const mapName = map ? getMapName(map) : '';
|
|
181
|
+
img = mapName ? __usemapIndex.get(mapName) || null : null;
|
|
166
182
|
} catch {
|
|
183
|
+
img = null;
|
|
167
184
|
}
|
|
185
|
+
if (!img) continue;
|
|
168
186
|
|
|
169
|
-
|
|
170
|
-
|
|
187
|
+
// The referencing <img> must be eligible in the accessibility tree.
|
|
188
|
+
if (isAccTreeEligible) {
|
|
189
|
+
const imgElig = accEligibleCached(img);
|
|
190
|
+
if (imgElig && imgElig.eligible === false) continue;
|
|
171
191
|
}
|
|
172
192
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
for (const el of els) {
|
|
177
|
-
if (!el || !el.getAttribute) continue;
|
|
178
|
-
|
|
179
|
-
// Must belong to a *used* image map (referenced by an <img usemap>). If unused, not applicable.
|
|
180
|
-
let img = null;
|
|
193
|
+
if (isAccTreeEligible) {
|
|
194
|
+
const elig = (() => {
|
|
181
195
|
try {
|
|
182
|
-
|
|
183
|
-
const mapName = map ? getMapName(map) : '';
|
|
184
|
-
img = mapName ? (__usemapIndex.get(mapName) || null) : null;
|
|
196
|
+
return isAccTreeEligible(el, ctx);
|
|
185
197
|
} catch {
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
if (!img) continue;
|
|
189
|
-
|
|
190
|
-
// The referencing <img> must be eligible in the accessibility tree.
|
|
191
|
-
if (isAccTreeEligible) {
|
|
192
|
-
const imgElig = accEligibleCached(img);
|
|
193
|
-
if (imgElig && imgElig.eligible === false) continue;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (isAccTreeEligible) {
|
|
197
|
-
const elig = (() => {
|
|
198
|
-
try {
|
|
199
|
-
return isAccTreeEligible(el, ctx);
|
|
200
|
-
} catch {
|
|
201
|
-
return {eligible: true, reasons: []};
|
|
202
|
-
}
|
|
203
|
-
})();
|
|
204
|
-
if (elig && elig.eligible === false) continue;
|
|
198
|
+
return { eligible: true, reasons: [] };
|
|
205
199
|
}
|
|
200
|
+
})();
|
|
201
|
+
if (elig && elig.eligible === false) continue;
|
|
202
|
+
}
|
|
206
203
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// Rule-specific applicability (only elements that already have a text alternative mechanism)
|
|
210
|
-
let alt = null;
|
|
211
|
-
try { alt = el.getAttribute('alt'); } catch { alt = null; }
|
|
212
|
-
if (alt === null) continue;
|
|
213
|
-
if (String(alt).trim() !== '') continue; // only alt=""
|
|
214
|
-
|
|
215
|
-
applicableCount += 1;
|
|
216
|
-
|
|
217
|
-
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, {targetSet: 'acc'}) : null;
|
|
218
|
-
|
|
219
|
-
const baseOccurrence = {
|
|
220
|
-
summary: 'Review whether <area> is decorative (alt="").',
|
|
221
|
-
hint: 'Confirm the area does not convey information or function. If it is interactive or meaningful, provide meaningful alt text.',
|
|
222
|
-
i18n: {
|
|
223
|
-
summaryKey: 'area_altDecorative_summary_cantTell',
|
|
224
|
-
hintKey: 'area_altDecorative_hint_cantTell',
|
|
225
|
-
params: {element: (el.tagName || '').toLowerCase()}
|
|
226
|
-
},
|
|
227
|
-
data: {
|
|
228
|
-
visibilityFilter: eligInfo || {targetSet: 'acc', accEligible: null, reasons: []},
|
|
229
|
-
details: null
|
|
230
|
-
}
|
|
231
|
-
};
|
|
204
|
+
if (isRolePresentationExcluded(el)) continue;
|
|
232
205
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
206
|
+
// Rule-specific applicability (only elements that already have a text alternative mechanism)
|
|
207
|
+
let alt;
|
|
208
|
+
try {
|
|
209
|
+
alt = el.getAttribute('alt');
|
|
210
|
+
} catch {
|
|
211
|
+
alt = null;
|
|
238
212
|
}
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
213
|
+
if (alt === null) continue;
|
|
214
|
+
if (String(alt).trim() !== '') continue; // only alt=""
|
|
215
|
+
|
|
216
|
+
applicableCount += 1;
|
|
217
|
+
|
|
218
|
+
const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
|
|
219
|
+
|
|
220
|
+
const baseOccurrence = {
|
|
221
|
+
summary: 'Review whether <area> is decorative (alt="").',
|
|
222
|
+
hint: 'Confirm the area does not convey information or function. If it is interactive or meaningful, provide meaningful alt text.',
|
|
223
|
+
i18n: {
|
|
224
|
+
summaryKey: 'area_altDecorative_summary_cantTell',
|
|
225
|
+
hintKey: 'area_altDecorative_hint_cantTell',
|
|
226
|
+
params: { element: (el.tagName || '').toLowerCase() }
|
|
227
|
+
},
|
|
228
|
+
data: {
|
|
229
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
|
|
230
|
+
details: null
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (helpers && typeof helpers.reportOccurrence === 'function') {
|
|
235
|
+
occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
|
|
236
|
+
} else {
|
|
237
|
+
occurrences.push({ selector: '', html: '', ...baseOccurrence });
|
|
242
238
|
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (applicableCount === 0) {
|
|
242
|
+
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
243
|
+
}
|
|
243
244
|
|
|
244
|
-
|
|
245
|
+
return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
module.exports = {id, meta, runInPage};
|
|
248
|
+
module.exports = { id, meta, runInPage };
|