pour-engine 1.44.2 → 1.46.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.
@@ -6,6 +6,6 @@
6
6
  export default {
7
7
  engine: {
8
8
  name: 'pour engine',
9
- version: '1.44.2',
9
+ version: '1.46.0',
10
10
  },
11
11
  };
package/engine/README.md CHANGED
@@ -111,6 +111,23 @@ pour engine therefore reports on **every** in-scope criterion, one of two ways:
111
111
  in-scope criteria (filtered by the requested WCAG version and level)
112
112
  that automation can't judge, for a host to render however it wants.
113
113
 
114
+ ### WCAG 3 Working Draft
115
+
116
+ A check set tag, `wcag3-core` or `wcag3-supplemental`, reports against the
117
+ WCAG 3.0 Working Draft of 10 September 2026 instead (`wcag3.js`). The
118
+ rules are still WCAG 2 rules: each is matched, by reading both documents,
119
+ to the draft requirement its failure also fails, and a rule with no such
120
+ requirement does not run (`UNMAPPED_RULES` gives each reason). Where the
121
+ draft leaves a value to be determined, text contrast among them, the
122
+ rule's WCAG 2 value stands in. `wcag3-core` runs the rules matched to core
123
+ requirements, the ones the draft says must all be met to conform;
124
+ `wcag3-supplemental` adds the rules matched to supplemental requirements.
125
+ `results.standard` is stamped draft and names the set, `manualReview`
126
+ becomes the set's requirements, and each rule result carries `wcag3`.
127
+ The draft's Bronze, Silver and Gold sit above conformance and each needs
128
+ a number of supplemental requirements and assertions it has yet to
129
+ decide, so no check set stands for one. WCAG 2 stays the default.
130
+
114
131
  The checklist accounts for the criteria; it does not mean every page state
115
132
  or possible failure has been tested. A snapshot audit still needs human
116
133
  assessment, including interaction states and any checks reported as skipped.
package/engine/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import config from '../config/project.config.js';
5
5
  import rules from './rules/index.js';
6
6
  import wcagCatalog from './wcag22.js';
7
+ import { ruleMatchesTags, checkSetFromTags, manualReviewChecklist, requirementsForRule, standardFor } from './wcag3.js';
7
8
  import { isVisible, isRendered, cssPath, htmlSnippet, ownText, collectRoots, resetDOMCaches, releaseDOMCaches } from './lib/dom.js';
8
9
  import { accessibleName } from './lib/accessible-name.js';
9
10
  import { resetAuditCaches } from './lib/contrast.js';
@@ -13,12 +14,6 @@ export const version = config.engine.version;
13
14
 
14
15
  const ruleHelpers = { isVisible, isRendered, cssPath, htmlSnippet, ownText, accessibleName };
15
16
 
16
- /** Does this rule match the requested tag selection? Empty/absent = all rules. */
17
- function ruleMatchesTags(rule, tags) {
18
- if (!tags?.length) return true;
19
- return rule.tags.some((tag) => tags.includes(tag));
20
- }
21
-
22
17
  /** Which WCAG version/level was requested, judged from the selected tags. */
23
18
  function scopeFromTags(tags) {
24
19
  if (!tags?.length) return { version: '2.2', level: 'AAA' }; // no filter → everything
@@ -87,7 +82,12 @@ function toResultNode(element, outcome) {
87
82
  * @param {Document|Element} context - what to scan
88
83
  * @param {{ tags?: string[], exclude?: string, signal?: AbortSignal }} options -
89
84
  * rule selection by tag (wcag2a…wcag22aa, best-practice), empty selects
90
- * every rule; exclude is a CSS selector — elements matching it, or inside
85
+ * every rule; a WCAG 3 check set tag (wcag3-core, wcag3-supplemental)
86
+ * selects the rules matched to that set's draft requirements and
87
+ * reframes the report against the WCAG 3.0 Working Draft:
88
+ * results.standard is stamped draft, the manual checklist becomes the
89
+ * set's requirements,
90
+ * and each rule result carries the requirements it maps to (wcag3); exclude is a CSS selector — elements matching it, or inside
91
91
  * a match (including across shadow boundaries), are left out of every
92
92
  * rule; signal aborts the run between rules (throws AbortError)
93
93
  * @param {(progress: object) => void} [onProgress] - called before each rule
@@ -98,15 +98,17 @@ function toResultNode(element, outcome) {
98
98
  */
99
99
  export async function run(context = document, options = {}, onProgress) {
100
100
  const auditStarted = performance.now();
101
+ const checkSet = checkSetFromTags(options.tags);
101
102
  const results = {
102
103
  testEngine: { name, version },
103
104
  timestamp: new Date().toISOString(),
104
105
  url: context.location?.href ?? context.ownerDocument?.location?.href ?? '',
106
+ standard: checkSet ? standardFor(checkSet) : null,
105
107
  violations: [],
106
108
  passes: [],
107
109
  incomplete: [],
108
110
  inapplicable: [],
109
- manualReview: manualReviewCriteria(options.tags),
111
+ manualReview: checkSet ? manualReviewChecklist(checkSet) : manualReviewCriteria(options.tags),
110
112
  ruleTimings: [],
111
113
  };
112
114
 
@@ -251,6 +253,9 @@ export async function run(context = document, options = {}, onProgress) {
251
253
  description: rule.help,
252
254
  helpUrl: rule.helpUrl,
253
255
  };
256
+ // In a WCAG 3 audit, the draft requirements this rule speaks to (empty
257
+ // for a best-practice rule, which maps to none).
258
+ if (checkSet) ruleResult.wcag3 = requirementsForRule(rule.id);
254
259
  if (!elements.length) results.inapplicable.push({ ...ruleResult, nodes: [] });
255
260
  if (buckets.fail.length) results.violations.push({ ...ruleResult, nodes: buckets.fail });
256
261
  if (buckets.incomplete.length) results.incomplete.push({ ...ruleResult, nodes: buckets.incomplete });
@@ -1,5 +1,5 @@
1
1
  // WCAG SC 1.4.1 Use of Color (Level A)
2
- import { parseColor, contrastRatio, composite, effectiveBackground, backgroundImageSource } from '../../lib/contrast.js';
2
+ import { parseColor, contrastRatio, composite, effectiveBackground, backgroundImageSource, asRgb } from '../../lib/contrast.js';
3
3
 
4
4
  // Split a computed list value ("a, b(c, d), e") on the commas OUTSIDE
5
5
  // parentheses: gradients and colour functions carry commas of their own.
@@ -274,6 +274,17 @@ export default {
274
274
  // WCAG technique G183: 3:1 against the surrounding text is a sufficient
275
275
  // colour difference for identifying a link.
276
276
  if (ratio >= 3) return { status: 'pass' };
277
+ // The pair the ratio was taken from, for the highlight card and the
278
+ // checker link: the link's colour and the prose's, as judged (composited
279
+ // where translucent), plus the background where it is a known solid.
280
+ const backdrop = effectiveBackground(parent);
281
+ const data = {
282
+ link: asRgb(linkColor),
283
+ surrounding: asRgb(textColor),
284
+ ...(backdrop && (backdrop.a ?? 1) === 1 ? { background: asRgb(backdrop) } : {}),
285
+ ratio: Number(ratio.toFixed(2)),
286
+ required: 3,
287
+ };
277
288
 
278
289
  // 2026-08-25 overnight audit: text-decoration propagates from an
279
290
  // ancestor to its inline descendants and a descendant's "none" cannot
@@ -371,6 +382,7 @@ export default {
371
382
  ? 'This link has no underline and its colour differs from the surrounding text in hue alone, with no luminance difference: the distinction disappears entirely without colour vision, the exact failure F73 describes.'
372
383
  : `This link has no underline and only ${shown}:1 colour difference from the surrounding text, below the 3:1 WCAG technique G183 asks for when colour is the only thing marking a link.`,
373
384
  fix: 'Underline links inside text (text-decoration: underline), or add a non-colour indicator.',
385
+ data,
374
386
  };
375
387
  },
376
388
  };
@@ -577,7 +577,7 @@ async function sampledVerdict(source, foreground, required, doc, overlays = [],
577
577
  * 1.4.3 Contrast (Minimum), AA — 4.5:1, or 3:1 for large text
578
578
  * 1.4.6 Contrast (Enhanced), AAA — 7:1, or 4.5:1 for large text
579
579
  */
580
- export function createContrastRule({ id, tags, help, helpUrl, thresholds }) {
580
+ export function createContrastRule({ id, name, tags, help, helpUrl, thresholds }) {
581
581
  // When the colour pair already clears the LARGE-scale minimum, the miss
582
582
  // is about scale, not colour: name the boundary (14pt bold / 18pt, bold
583
583
  // meaning BOLD_WEIGHT and up), or
@@ -608,6 +608,9 @@ export function createContrastRule({ id, tags, help, helpUrl, thresholds }) {
608
608
 
609
609
  const rule = {
610
610
  id,
611
+ // The display name both callers pass: dropped here until 2026-09-21, so
612
+ // the UI titled these two rules by their ids.
613
+ name,
611
614
  impact: 'serious',
612
615
  tags,
613
616
  help,
@@ -1,8 +1,11 @@
1
1
  // WCAG SC 2.4.7 Focus Visible (Level AA)
2
- // A stylesheet that strips focus outlines without providing a replacement
3
- // indicator is the classic 2.4.7 failure (F78). Cross-origin sheets are
4
- // unreadable and skipped; a replacement indicator can live in a different
5
- // rule entirely — so this flags for review, it never asserts a fail.
2
+ // A stylesheet or style attribute that strips focus outlines without
3
+ // providing a replacement indicator is the classic 2.4.7 failure (F78).
4
+ // Cross-origin sheets are unreadable and skipped; a replacement indicator can
5
+ // live in a different rule entirely — so this flags for review, it never
6
+ // asserts a fail.
7
+ import { isRendered } from '../../lib/dom.js';
8
+
6
9
  // The focus pseudo-classes themselves, and nothing else: :focus-within is a
7
10
  // PARENT's state and stripping ":focus" out of it would leave "-within".
8
11
  const FOCUS_TOKEN = /:focus(?:-visible)?(?![\w-])/g;
@@ -33,7 +36,8 @@ function focusSuppressors(doc) {
33
36
  // human question.
34
37
  let sheet = null;
35
38
  const inspect = (rule) => {
36
- if (!rule.selectorText || !/:focus/.test(rule.selectorText)) return;
39
+ if (!rule.selectorText) return;
40
+ if (!/:focus/.test(rule.selectorText)) { inspectResting(rule); return; }
37
41
  // `:focus:not(:focus-visible) { outline: none }` is the PUBLISHED pattern
38
42
  // for showing the ring to keyboard users only: it strips the indicator
39
43
  // precisely in the state where the browser has decided not to show focus,
@@ -65,7 +69,38 @@ function focusSuppressors(doc) {
65
69
  provider.order > suspect.order && covered(subject, provider)));
66
70
  if (!restored) suspects.push(suspect.selector);
67
71
  }
72
+ allProviders.push(...sheet.providers);
73
+ };
74
+ // A rule with no focus state in it removes the indicator too: author
75
+ // styles outrank the browser's own :focus-visible ring whatever their
76
+ // specificity, so `* { outline: none }` or `a, button { outline: 0 }`
77
+ // leaves a focused link with no ring (measured in Chromium 2026-09-24).
78
+ // Such a rule matters only where it reaches something Tab lands on, and a
79
+ // box-shadow or border in the same rule is not a replacement: it is there
80
+ // at rest, so it marks nothing when focus arrives.
81
+ const resting = [];
82
+ const allProviders = [];
83
+ const tabbable = (el) => !el.disabled && (el.tabIndex >= 0 || el.isContentEditable) && isRendered(el);
84
+ const reachesTabbable = (part) => {
85
+ if (part.includes('::')) return false; // a pseudo-element's outline is not the control's ring
86
+ try { return [...doc.querySelectorAll(part)].some(tabbable); } catch { return false; }
87
+ };
88
+ const inspectResting = (rule) => {
89
+ const style = rule.style;
90
+ if (!style || !rule.selectorText) return;
91
+ const styles = [style, ...[...(rule.cssRules ?? [])]
92
+ .filter((child) => child.style && !child.selectorText).map((child) => child.style)];
93
+ if (!styles.map(outlineOf).some(removesOutline)) return;
94
+ const parts = subjects(rule.selectorText).filter(reachesTabbable);
95
+ if (parts.length) resting.push({ selector: rule.selectorText, subjects: parts });
68
96
  };
97
+ // A focus rule outranks a resting one only when its specificity is
98
+ // higher, which order in the sheet cannot change. `a:focus-visible` always
99
+ // beats `a`; a bare `:focus-visible` (0,1,0) beats only selectors made of
100
+ // type names and `*`, and ties or loses to `.btn` or `.nav .btn`.
101
+ const typeOnly = (subject) => !/[#.[:]/.test(subject);
102
+ const outranked = (subject) => allProviders.some((provider) => provider.subjects.some((p) =>
103
+ p === subject || ((p === '' || p === '*') && typeOnly(subject))));
69
104
  // Inspect each rule, THEN descend into any children. Testing `rule.cssRules`
70
105
  // first and skipping used to walk straight past every plain style rule:
71
106
  // since CSS Nesting shipped, a CSSStyleRule carries its own cssRules list,
@@ -84,6 +119,14 @@ function focusSuppressors(doc) {
84
119
  try { scan(styleSheet.cssRules); } catch { /* cross-origin sheet */ }
85
120
  settle();
86
121
  }
122
+ for (const suspect of resting) {
123
+ if (!suspect.subjects.every(outranked)) suspects.push(suspect.selector);
124
+ }
125
+ // A style attribute outranks every selector, so no focus rule without
126
+ // !important gives the ring back to `<a style="outline: none">`.
127
+ for (const el of doc.querySelectorAll('[style]')) {
128
+ if (removesOutline(outlineOf(el.style)) && tabbable(el)) suspects.push(`the style attribute of <${el.localName}>`);
129
+ }
87
130
  return suspects;
88
131
  }
89
132
 
@@ -102,7 +145,7 @@ export default {
102
145
  const shown = suspects.slice(0, 3).join(', ');
103
146
  return {
104
147
  status: 'incomplete',
105
- message: `${suspects.length} CSS rule(s) remove the focus outline without setting a replacement in the same rule (e.g. ${shown}) — if no other rule provides a visible indicator, keyboard users lose their place. Tab through the page to check.`,
148
+ message: `${suspects.length} CSS rule(s) or style attribute(s) remove the focus outline with no replacement found (e.g. ${shown}). If nothing else draws a visible indicator, keyboard users lose their place. Tab through the page to check.`,
106
149
  };
107
150
  },
108
151
  };
@@ -0,0 +1,520 @@
1
+ // The WCAG 3.0 Working Draft of 10 September 2026, as a catalog the engine
2
+ // can report against. https://www.w3.org/TR/2026/WD-wcag-3.0-20260910/
3
+ //
4
+ // WCAG 3.0 is unfinished. The W3C marks every section of it Developing or
5
+ // Exploratory and says the requirements will be added, combined, removed
6
+ // and reworded. Everything here is therefore stamped draft, and every
7
+ // surface that shows it must say so. WCAG 2 stays the default everywhere.
8
+ //
9
+ // The draft replaces success criteria and A/AA/AAA with three kinds of
10
+ // provision under each guideline: core requirements, supplemental
11
+ // requirements and assertions (documented statements about how content is
12
+ // made or how an organization works, which no tool can check). To conform
13
+ // (3.2), every core requirement must be met; supplemental requirements and
14
+ // assertions are not needed to conform. The draft's Reporting section (4,
15
+ // Exploratory) puts Bronze, Silver and Gold above conformance, each taking
16
+ // a number of supplemental requirements and assertions still to be
17
+ // decided, so none of them is a set of checks a tool can run. pour offers
18
+ // two check sets instead, and no tier:
19
+ // core: the rules matched to core requirements
20
+ // supplemental: those rules plus the ones matched to supplemental
21
+ // requirements
22
+ //
23
+ // No rule here was written for WCAG 3. The rules test WCAG 2 criteria;
24
+ // RULE_REQUIREMENTS names, rule by rule, the draft requirement that a
25
+ // failure of that rule also fails, judged by reading both documents. A
26
+ // rule with no such requirement (UNMAPPED_RULES, with the reason) does
27
+ // not run in a WCAG 3 audit. Where the draft leaves a value to be
28
+ // determined (text contrast, text spacing), the rule's WCAG 2 value
29
+ // stands in, and the report says the values are WCAG 2's.
30
+
31
+ export const draftInfo = {
32
+ id: 'wcag3-draft',
33
+ name: 'WCAG 3.0',
34
+ status: 'Working Draft',
35
+ dated: '2026-09-10',
36
+ draft: true,
37
+ url: 'https://www.w3.org/TR/2026/WD-wcag-3.0-20260910/',
38
+ note: 'An unfinished standard. Findings come from WCAG 2 rules matched to draft requirements, with WCAG 2 values where the draft has none yet. Not for conformance claims.',
39
+ };
40
+
41
+ // The check sets, cumulative, and the provision types each takes in.
42
+ export const CHECK_SETS = ['core', 'supplemental'];
43
+ const SET_TYPES = {
44
+ core: new Set(['core']),
45
+ supplemental: new Set(['core', 'supplemental']),
46
+ };
47
+ const SET_SCOPES = {
48
+ core: 'core requirements',
49
+ supplemental: 'core and supplemental requirements',
50
+ };
51
+ // Names the first WCAG 3 draft mode gave these sets, read the way they ran
52
+ // then, so a saved setting, a script or a tag list that still uses one
53
+ // selects the same rules. They are never shown.
54
+ const RENAMED_SETS = { bronze: 'core', silver: 'supplemental', gold: 'supplemental' };
55
+
56
+ const TYPE_LABELS = {
57
+ core: 'Core',
58
+ supplemental: 'Supplemental',
59
+ assertion: 'Assertion',
60
+ };
61
+
62
+ // Every requirement and assertion of the draft, in document order, under
63
+ // its guideline. The one recommended practice (2.7.2.7) is left out: the
64
+ // draft says recommended practices are not needed to conform. Guidelines
65
+ // with nothing past Exploratory (2.1.2 Figure captions, 2.12.2 Control
66
+ // text) have no provisions to list.
67
+ const RAW = [
68
+ g('2.1.1', 'Image alternatives'),
69
+ p('2.1.1.1', 'Images detectable', 'core', 'x2-1-1-1-images-detectable'),
70
+ p('2.1.1.2', 'Decorative images hidden', 'core', 'x2-1-1-2-decorative-images-hidden'),
71
+ p('2.1.1.3', 'Image alternatives available', 'core', 'x2-1-1-3-image-alternatives-available'),
72
+ p('2.1.1.4', 'Image alternatives equivalent', 'core', 'x2-1-1-4-image-alternatives-equivalent'),
73
+ g('2.1.3', 'Non-text alternatives'),
74
+ p('2.1.3.1', 'Non-text content not relied on', 'core', 'x2-1-3-1-non-text-content-not-relied-on'),
75
+ g('2.1.4', 'Transcripts'),
76
+ p('2.1.4.1', 'Transcripts findable', 'core', 'x2-1-4-1-transcripts-findable'),
77
+ p('2.1.4.2', 'Dialogue transcripts available (prerecorded)', 'core', 'x2-1-4-2-dialogue-transcripts-available-prerecorded'),
78
+ p('2.1.4.3', 'Dialogue transcripts available (live)', 'core', 'x2-1-4-3-dialogue-transcripts-available-live'),
79
+ p('2.1.4.4', 'Transcripts equivalent (prerecorded)', 'core', 'x2-1-4-4-transcripts-equivalent-prerecorded'),
80
+ p('2.1.4.5', 'Descriptive transcripts available', 'core', 'x2-1-4-5-descriptive-transcripts-available'),
81
+ p('2.1.4.6', 'Speakers identified in transcripts', 'supplemental', 'x2-1-4-6-speakers-identified-in-transcripts'),
82
+ p('2.1.4.7', 'Speaker language identified in transcripts', 'supplemental', 'x2-1-4-7-speaker-language-identified-in-transcripts'),
83
+ p('2.1.4.8', 'Sounds identified in transcripts', 'core', 'x2-1-4-8-sounds-identified-in-transcripts'),
84
+ p('2.1.4.9', 'Visual information identified in transcripts', 'core', 'x2-1-4-9-visual-information-identified-in-transcripts'),
85
+ p('2.1.4.10', 'Transcripts style guide', 'assertion', 'x2-1-4-10-transcripts-style-guide'),
86
+ p('2.1.4.11', 'Transcripts usability testing', 'assertion', 'x2-1-4-11-transcripts-usability-testing'),
87
+ p('2.1.4.12', 'Transcripts reviewed by content authors', 'assertion', 'x2-1-4-12-transcripts-reviewed-by-content-authors'),
88
+ g('2.1.5', 'Captions'),
89
+ p('2.1.5.1', 'Captions adjustable', 'supplemental', 'x2-1-5-1-captions-adjustable'),
90
+ p('2.1.5.2', 'Captions available (prerecorded)', 'core', 'x2-1-5-2-captions-available-prerecorded'),
91
+ p('2.1.5.3', 'Captions equivalent (prerecorded)', 'core', 'x2-1-5-3-captions-equivalent-prerecorded'),
92
+ p('2.1.5.4', 'Captions available (live)', 'core', 'x2-1-5-4-captions-available-live'),
93
+ p('2.1.5.5', 'Caption language adjustable', 'supplemental', 'x2-1-5-5-caption-language-adjustable'),
94
+ p('2.1.5.6', 'Captions controllable', 'supplemental', 'x2-1-5-6-captions-controllable'),
95
+ p('2.1.5.7', 'Captions unobstructed', 'supplemental', 'x2-1-5-7-captions-unobstructed'),
96
+ p('2.1.5.8', 'Captions synchronized', 'core', 'x2-1-5-8-captions-synchronized'),
97
+ p('2.1.5.9', 'Captions centered (immersive)', 'core', 'x2-1-5-9-captions-centered-immersive'),
98
+ p('2.1.5.10', 'Direction indicated (immersive)', 'core', 'x2-1-5-10-direction-indicated-immersive'),
99
+ p('2.1.5.11', 'Speakers identified in captions', 'supplemental', 'x2-1-5-11-speakers-identified-in-captions'),
100
+ p('2.1.5.12', 'Speaker language identified in captions', 'supplemental', 'x2-1-5-12-speaker-language-identified-in-captions'),
101
+ p('2.1.5.13', 'Sounds identified in captions', 'core', 'x2-1-5-13-sounds-identified-in-captions'),
102
+ p('2.1.5.14', 'Captions style guide', 'assertion', 'x2-1-5-14-captions-style-guide'),
103
+ p('2.1.5.15', 'Captions usability testing', 'assertion', 'x2-1-5-15-captions-usability-testing'),
104
+ p('2.1.5.16', 'Captions reviewed by content authors', 'assertion', 'x2-1-5-16-captions-reviewed-by-content-authors'),
105
+ g('2.1.6', 'Audio descriptions'),
106
+ p('2.1.6.1', 'Audio descriptions available (prerecorded)', 'core', 'x2-1-6-1-audio-descriptions-available-prerecorded'),
107
+ p('2.1.6.2', 'Audio descriptions equivalent (prerecorded)', 'core', 'x2-1-6-2-audio-descriptions-equivalent-prerecorded'),
108
+ p('2.1.6.3', 'Audio descriptions synchronized', 'core', 'x2-1-6-3-audio-descriptions-synchronized'),
109
+ p('2.1.6.4', 'Audio descriptions available (live)', 'supplemental', 'x2-1-6-4-audio-descriptions-available-live'),
110
+ p('2.1.6.5', 'Extended audio descriptions available', 'core', 'x2-1-6-5-extended-audio-descriptions-available'),
111
+ p('2.1.6.6', 'Extended audio descriptions equivalent', 'core', 'x2-1-6-6-extended-audio-descriptions-equivalent'),
112
+ p('2.1.6.7', 'Audio description language adjustable', 'supplemental', 'x2-1-6-7-audio-description-language-adjustable'),
113
+ p('2.1.6.8', 'Audio descriptions controllable', 'supplemental', 'x2-1-6-8-audio-descriptions-controllable'),
114
+ p('2.1.6.9', 'Speakers identified in audio descriptions', 'supplemental', 'x2-1-6-9-speakers-identified-in-audio-descriptions'),
115
+ p('2.1.6.10', 'Speaker language identified in audio descriptions', 'supplemental', 'x2-1-6-10-speaker-language-identified-in-audio-descriptions'),
116
+ p('2.1.6.11', 'Sounds identified in audio descriptions', 'core', 'x2-1-6-11-sounds-identified-in-audio-descriptions'),
117
+ p('2.1.6.12', 'Visual information identified in audio descriptions', 'core', 'x2-1-6-12-visual-information-identified-in-audio-descriptions'),
118
+ p('2.1.6.13', 'Audio descriptions style guide', 'assertion', 'x2-1-6-13-audio-descriptions-style-guide'),
119
+ p('2.1.6.14', 'Audio descriptions usability testing', 'assertion', 'x2-1-6-14-audio-descriptions-usability-testing'),
120
+ p('2.1.6.15', 'Audio descriptions reviewed by content authors', 'assertion', 'x2-1-6-15-audio-descriptions-reviewed-by-content-authors'),
121
+ g('2.1.7', 'Sign language'),
122
+ p('2.1.7.1', 'Sign language available (prerecorded)', 'supplemental', 'x2-1-7-1-sign-language-available-prerecorded'),
123
+ p('2.1.7.2', 'Sign language controllable', 'supplemental', 'x2-1-7-2-sign-language-controllable'),
124
+ p('2.1.7.3', 'Sign language policy (live)', 'assertion', 'x2-1-7-3-sign-language-policy-live'),
125
+ g('2.1.8', 'Single sense'),
126
+ p('2.1.8.1', 'Hue not relied on', 'core', 'x2-1-8-1-hue-not-relied-on'),
127
+ p('2.1.8.2', 'Graphical object contrast sufficient', 'core', 'x2-1-8-2-graphical-object-contrast-sufficient'),
128
+ p('2.1.8.3', 'Visual depth not relied on', 'core', 'x2-1-8-3-visual-depth-not-relied-on'),
129
+ p('2.1.8.4', 'Sound not relied on', 'core', 'x2-1-8-4-sound-not-relied-on'),
130
+ p('2.1.8.5', 'Spatial audio not relied on', 'core', 'x2-1-8-5-spatial-audio-not-relied-on'),
131
+ g('2.1.9', 'Accessible media player'),
132
+ p('2.1.9.1', 'Accessible video player selected', 'assertion', 'x2-1-9-1-accessible-video-player-selected'),
133
+ p('2.1.9.2', 'Accessible audio player selected', 'assertion', 'x2-1-9-2-accessible-audio-player-selected'),
134
+ g('2.2.1', 'Text appearance'),
135
+ p('2.2.1.1', 'Blocks of text readable (minimum)', 'core', 'x2-2-1-1-blocks-of-text-readable-minimum'),
136
+ p('2.2.1.2', 'Text style readable (minimum)', 'core', 'x2-2-1-2-text-style-readable-minimum'),
137
+ p('2.2.1.3', 'Text contrast sufficient (minimum)', 'core', 'x2-2-1-3-text-contrast-sufficient-minimum'),
138
+ p('2.2.1.4', 'Blocks of text adjustable', 'core', 'x2-2-1-4-blocks-of-text-adjustable'),
139
+ p('2.2.1.5', 'Text style adjustable', 'core', 'x2-2-1-5-text-style-adjustable'),
140
+ p('2.2.1.6', 'Text size adjustable', 'core', 'x2-2-1-6-text-size-adjustable'),
141
+ p('2.2.1.7', 'Text color adjustable', 'core', 'x2-2-1-7-text-color-adjustable'),
142
+ p('2.2.1.8', 'Blocks of text readable (enhanced)', 'supplemental', 'x2-2-1-8-blocks-of-text-readable-enhanced'),
143
+ p('2.2.1.9', 'Text style readable (enhanced)', 'supplemental', 'x2-2-1-9-text-style-readable-enhanced'),
144
+ p('2.2.1.10', 'Text contrast sufficient (enhanced)', 'supplemental', 'x2-2-1-10-text-contrast-sufficient-enhanced'),
145
+ p('2.2.1.11', 'Text customizations retained', 'supplemental', 'x2-2-1-11-text-customizations-retained'),
146
+ g('2.2.2', 'Text-to-speech'),
147
+ p('2.2.2.1', 'Text detectable', 'core', 'x2-2-2-1-text-detectable'),
148
+ p('2.2.2.2', 'Human language detectable', 'core', 'x2-2-2-2-human-language-detectable'),
149
+ p('2.2.2.3', 'Numerical metadata available', 'core', 'x2-2-2-3-numerical-metadata-available'),
150
+ g('2.2.3', 'Clear language'),
151
+ p('2.2.3.1', 'Abbreviations explained', 'core', 'x2-2-3-1-abbreviations-explained'),
152
+ p('2.2.3.2', 'Non-literal language explained', 'supplemental', 'x2-2-3-2-non-literal-language-explained'),
153
+ p('2.2.3.3', 'Summaries available', 'core', 'x2-2-3-3-summaries-available'),
154
+ p('2.2.3.4', 'Common words used', 'supplemental', 'x2-2-3-4-common-words-used'),
155
+ p('2.2.3.5', 'Diacritics available', 'core', 'x2-2-3-5-diacritics-available'),
156
+ p('2.2.3.6', 'No nested clauses', 'supplemental', 'x2-2-3-6-no-nested-clauses'),
157
+ p('2.2.3.7', 'No unnecessary words', 'supplemental', 'x2-2-3-7-no-unnecessary-words'),
158
+ p('2.2.3.8', 'Clear language review', 'assertion', 'x2-2-3-8-clear-language-review'),
159
+ p('2.2.3.9', 'Visual aids review', 'assertion', 'x2-2-3-9-visual-aids-review'),
160
+ g('2.3.1', 'Keyboard focus appearance'),
161
+ p('2.3.1.1', 'Default focus indicator used', 'supplemental', 'x2-3-1-1-default-focus-indicator-used'),
162
+ p('2.3.1.2', 'Focus indicator contrast sufficient', 'core', 'x2-3-1-2-focus-indicator-contrast-sufficient'),
163
+ p('2.3.1.3', 'Focus indicator size sufficient', 'supplemental', 'x2-3-1-3-focus-indicator-size-sufficient'),
164
+ p('2.3.1.4', 'Focus indicator style guide', 'assertion', 'x2-3-1-4-focus-indicator-style-guide'),
165
+ g('2.3.2', 'Pointer focus appearance'),
166
+ p('2.3.2.1', 'Pointer activation indicated (minimum)', 'core', 'x2-3-2-1-pointer-activation-indicated-minimum'),
167
+ p('2.3.2.2', 'Pointer activation indicated (enhanced)', 'supplemental', 'x2-3-2-2-pointer-activation-indicated-enhanced'),
168
+ p('2.3.2.3', 'Pointer contrast sufficient', 'core', 'x2-3-2-3-pointer-contrast-sufficient'),
169
+ p('2.3.2.4', 'Default pointer used', 'supplemental', 'x2-3-2-4-default-pointer-used'),
170
+ p('2.3.2.5', 'Pointer focus indicated', 'core', 'x2-3-2-5-pointer-focus-indicated'),
171
+ p('2.3.2.6', 'Pointer visible', 'core', 'x2-3-2-6-pointer-visible'),
172
+ p('2.3.2.7', 'Enhanced pointer available', 'supplemental', 'x2-3-2-7-enhanced-pointer-available'),
173
+ g('2.3.3', 'Navigating content'),
174
+ p('2.3.3.1', 'Focus relevant', 'core', 'x2-3-3-1-focus-relevant'),
175
+ p('2.3.3.2', 'Focus retained', 'supplemental', 'x2-3-3-2-focus-retained'),
176
+ p('2.3.3.3', 'Focus order meaningful', 'core', 'x2-3-3-3-focus-order-meaningful'),
177
+ g('2.3.4', 'Expected behavior'),
178
+ p('2.3.4.1', 'Consistent interactions', 'assertion', 'x2-3-4-1-consistent-interactions'),
179
+ p('2.3.4.2', 'Consistent control location', 'supplemental', 'x2-3-4-2-consistent-control-location'),
180
+ p('2.3.4.3', 'Conventional pattern used', 'assertion', 'x2-3-4-3-conventional-pattern-used'),
181
+ g('2.3.5', 'Control information'),
182
+ p('2.3.5.1', 'Interactive element contrast sufficient', 'core', 'x2-3-5-1-interactive-element-contrast-sufficient'),
183
+ p('2.3.5.2', 'Interactive element names available', 'core', 'x2-3-5-2-interactive-element-names-available'),
184
+ p('2.3.5.3', 'Changes to elements notified', 'core', 'x2-3-5-3-changes-to-elements-notified'),
185
+ p('2.3.5.4', 'Input constraints used', 'core', 'x2-3-5-4-input-constraints-used'),
186
+ p('2.3.5.5', 'Label included in programmatic name', 'core', 'x2-3-5-5-label-included-in-programmatic-name'),
187
+ p('2.3.5.6', 'Roles, values, states, properties available', 'core', 'x2-3-5-6-roles-values-states-properties-available'),
188
+ g('2.4.1', 'Keyboard interface input'),
189
+ p('2.4.1.1', 'Keyboard operable', 'core', 'x2-4-1-1-keyboard-operable'),
190
+ p('2.4.1.2', 'Keyboard accessible', 'core', 'x2-4-1-2-keyboard-accessible'),
191
+ p('2.4.1.3', 'Bidirectional navigation', 'core', 'x2-4-1-3-bidirectional-navigation'),
192
+ p('2.4.1.4', 'Custom keys documented', 'supplemental', 'x2-4-1-4-custom-keys-documented'),
193
+ p('2.4.1.5', 'No keyboard conflicts', 'core', 'x2-4-1-5-no-keyboard-conflicts'),
194
+ p('2.4.1.6', 'Focus placed', 'supplemental', 'x2-4-1-6-focus-placed'),
195
+ p('2.4.1.7', 'No keyboard traps', 'core', 'x2-4-1-7-no-keyboard-traps'),
196
+ p('2.4.1.8', 'Focus user-controlled', 'core', 'x2-4-1-8-focus-user-controlled'),
197
+ p('2.4.1.9', 'Focus movement relevant', 'supplemental', 'x2-4-1-9-focus-movement-relevant'),
198
+ g('2.4.2', 'Physical or cognitive effort when using keyboard'),
199
+ p('2.4.2.1', 'Navigation keys described', 'supplemental', 'x2-4-2-1-navigation-keys-described'),
200
+ p('2.4.2.2', 'No repetitive adjacent interactive elements', 'supplemental', 'x2-4-2-2-no-repetitive-adjacent-interactive-elements'),
201
+ p('2.4.2.3', 'Keyboard effort comparable', 'assertion', 'x2-4-2-3-keyboard-effort-comparable'),
202
+ g('2.4.3', 'Pointer input'),
203
+ p('2.4.3.1', 'Pointer activation controllable', 'core', 'x2-4-3-1-pointer-activation-controllable'),
204
+ p('2.4.3.2', 'Simple pointer input available', 'core', 'x2-4-3-2-simple-pointer-input-available'),
205
+ p('2.4.3.3', 'Consistent pointer cancellation', 'supplemental', 'x2-4-3-3-consistent-pointer-cancellation'),
206
+ p('2.4.3.4', 'Pointer pressure not relied on', 'core', 'x2-4-3-4-pointer-pressure-not-relied-on'),
207
+ p('2.4.3.5', 'Pointer speed not relied on', 'core', 'x2-4-3-5-pointer-speed-not-relied-on'),
208
+ g('2.4.4', 'Speech and voice input'),
209
+ p('2.4.4.1', 'Speech not relied on', 'core', 'x2-4-4-1-speech-not-relied-on'),
210
+ p('2.4.4.2', 'Real-time text available', 'core', 'x2-4-4-2-real-time-text-available'),
211
+ p('2.4.4.3', 'Generated speech testing', 'assertion', 'x2-4-4-3-generated-speech-testing'),
212
+ g('2.4.5', 'Input operation'),
213
+ p('2.4.5.1', 'Hover or focus content dismissible', 'core', 'x2-4-5-1-hover-or-focus-content-dismissible'),
214
+ p('2.4.5.2', 'Hover content persistent', 'core', 'x2-4-5-2-hover-content-persistent'),
215
+ p('2.4.5.3', 'Hover or focus content persistent', 'supplemental', 'x2-4-5-3-hover-or-focus-content-persistent'),
216
+ p('2.4.5.4', 'Path-based gesture not relied on', 'core', 'x2-4-5-4-path-based-gesture-not-relied-on'),
217
+ p('2.4.5.5', 'Input method flexible', 'supplemental', 'x2-4-5-5-input-method-flexible'),
218
+ p('2.4.5.6', 'Body movements not relied on', 'core', 'x2-4-5-6-body-movements-not-relied-on'),
219
+ p('2.4.5.7', 'Eye tracking not relied on', 'core', 'x2-4-5-7-eye-tracking-not-relied-on'),
220
+ p('2.4.5.8', 'Pointer accessible', 'core', 'x2-4-5-8-pointer-accessible'),
221
+ g('2.4.6', 'Authentication'),
222
+ p('2.4.6.1', 'Biometrics not relied on', 'core', 'x2-4-6-1-biometrics-not-relied-on'),
223
+ p('2.4.6.2', 'Voice identification not relied on', 'core', 'x2-4-6-2-voice-identification-not-relied-on'),
224
+ g('2.5.1', 'Correct errors'),
225
+ p('2.5.1.1', 'Error notifications available', 'core', 'x2-5-1-1-error-notifications-available'),
226
+ p('2.5.1.2', 'Error suggestions provided', 'supplemental', 'x2-5-1-2-error-suggestions-provided'),
227
+ p('2.5.1.3', 'Errors indicated in multiple ways', 'supplemental', 'x2-5-1-3-errors-indicated-in-multiple-ways'),
228
+ p('2.5.1.4', 'Error messages persistent', 'supplemental', 'x2-5-1-4-error-messages-persistent'),
229
+ p('2.5.1.5', 'Errors associated', 'core', 'x2-5-1-5-errors-associated'),
230
+ p('2.5.1.6', 'Error messages collocated', 'supplemental', 'x2-5-1-6-error-messages-collocated'),
231
+ g('2.5.2', 'Prevent errors'),
232
+ p('2.5.2.1', 'Errors preventable', 'core', 'x2-5-2-1-errors-preventable'),
233
+ p('2.5.2.2', 'Submission status notified', 'supplemental', 'x2-5-2-2-submission-status-notified'),
234
+ p('2.5.2.3', 'Data entry validated', 'supplemental', 'x2-5-2-3-data-entry-validated'),
235
+ p('2.5.2.4', 'Error prevention review', 'assertion', 'x2-5-2-4-error-prevention-review'),
236
+ g('2.6.1', 'Avoid physical harm'),
237
+ p('2.6.1.1', 'No flashing over threshold', 'core', 'x2-6-1-1-no-flashing-over-threshold'),
238
+ p('2.6.1.2', 'No flashing over threshold (no exceptions)', 'supplemental', 'x2-6-1-2-no-flashing-over-threshold-no-exceptions'),
239
+ p('2.6.1.3', 'No visual motion', 'core', 'x2-6-1-3-no-visual-motion'),
240
+ p('2.6.1.4', 'No visual motion (no exceptions)', 'supplemental', 'x2-6-1-4-no-visual-motion-no-exceptions'),
241
+ p('2.6.1.5', 'Trigger warning available', 'core', 'x2-6-1-5-trigger-warning-available'),
242
+ p('2.6.1.6', 'Haptic stimulation adjustable', 'core', 'x2-6-1-6-haptic-stimulation-adjustable'),
243
+ p('2.6.1.7', 'Audio shifting adjustable', 'core', 'x2-6-1-7-audio-shifting-adjustable'),
244
+ p('2.6.1.8', 'Safe content review', 'assertion', 'x2-6-1-8-safe-content-review'),
245
+ g('2.7.1', 'Recognizable layouts'),
246
+ p('2.7.1.1', 'Conventional layout review', 'assertion', 'x2-7-1-1-conventional-layout-review'),
247
+ g('2.7.2', 'User orientation'),
248
+ p('2.7.2.1', 'Page/view title available', 'core', 'x2-7-2-1-page-view-title-available'),
249
+ p('2.7.2.2', 'Location within product review', 'assertion', 'x2-7-2-2-location-within-product-review'),
250
+ p('2.7.2.3', 'All steps listed', 'core', 'x2-7-2-3-all-steps-listed'),
251
+ p('2.7.2.4', 'Current step indicated', 'core', 'x2-7-2-4-current-step-indicated'),
252
+ p('2.7.2.5', 'Page/view change notified', 'core', 'x2-7-2-5-page-view-change-notified'),
253
+ p('2.7.2.6', 'Return to start supported', 'supplemental', 'x2-7-2-6-return-to-start-supported'),
254
+ g('2.7.3', 'Structure'),
255
+ p('2.7.3.1', 'Relationships detectable', 'supplemental', 'x2-7-3-1-relationships-detectable'),
256
+ p('2.7.3.2', 'Blocks of content available (minimum)', 'core', 'x2-7-3-2-blocks-of-content-available-minimum'),
257
+ p('2.7.3.3', 'Sections labeled', 'core', 'x2-7-3-3-sections-labeled'),
258
+ p('2.7.3.4', 'Heading structure available', 'supplemental', 'x2-7-3-4-heading-structure-available'),
259
+ p('2.7.3.5', 'Order detectable', 'core', 'x2-7-3-5-order-detectable'),
260
+ p('2.7.3.6', 'Blocks of content available (enhanced)', 'supplemental', 'x2-7-3-6-blocks-of-content-available-enhanced'),
261
+ p('2.7.3.7', 'Clear structure review', 'assertion', 'x2-7-3-7-clear-structure-review'),
262
+ p('2.7.3.8', 'Key information usability testing', 'assertion', 'x2-7-3-8-key-information-usability-testing'),
263
+ g('2.7.4', 'No obstruction'),
264
+ p('2.7.4.1', 'Overlay content dismissible', 'core', 'x2-7-4-1-overlay-content-dismissible'),
265
+ g('2.8.1', 'Consistency'),
266
+ p('2.8.1.1', 'Consistent structural order', 'supplemental', 'x2-8-1-1-consistent-structural-order'),
267
+ p('2.8.1.2', 'Consistent navigation order', 'supplemental', 'x2-8-1-2-consistent-navigation-order'),
268
+ p('2.8.1.3', 'Consistent navigation labels', 'supplemental', 'x2-8-1-3-consistent-navigation-labels'),
269
+ g('2.9.1', 'Avoid exclusionary cognitive tasks'),
270
+ p('2.9.1.1', 'Automated entry allowed', 'core', 'x2-9-1-1-automated-entry-allowed'),
271
+ p('2.9.1.2', 'Cognitive test alternatives available', 'core', 'x2-9-1-2-cognitive-test-alternatives-available'),
272
+ g('2.9.2', 'Adequate time'),
273
+ p('2.9.2.1', 'Timeout adjustable', 'supplemental', 'x2-9-2-1-timeout-adjustable'),
274
+ p('2.9.2.2', 'No time limits', 'supplemental', 'x2-9-2-2-no-time-limits'),
275
+ p('2.9.2.3', 'No unnecessary time limits', 'assertion', 'x2-9-2-3-no-unnecessary-time-limits'),
276
+ p('2.9.2.4', 'Time limits conveyed', 'supplemental', 'x2-9-2-4-time-limits-conveyed'),
277
+ g('2.9.3', 'Avoid deception'),
278
+ p('2.9.3.1', 'Preselections visible', 'core', 'x2-9-3-1-preselections-visible'),
279
+ p('2.9.3.2', 'Deceptive practices usability testing', 'assertion', 'x2-9-3-2-deceptive-practices-usability-testing'),
280
+ p('2.9.3.3', 'Deceptive messaging expert review', 'assertion', 'x2-9-3-3-deceptive-messaging-expert-review'),
281
+ g('2.9.4', 'Retain information'),
282
+ p('2.9.4.1', 'Going back supported', 'supplemental', 'x2-9-4-1-going-back-supported'),
283
+ p('2.9.4.2', 'No redundant entry', 'supplemental', 'x2-9-4-2-no-redundant-entry'),
284
+ p('2.9.4.3', 'Progress saved', 'supplemental', 'x2-9-4-3-progress-saved'),
285
+ g('2.9.5', 'Complete tasks'),
286
+ p('2.9.5.1', 'Required action available', 'supplemental', 'x2-9-5-1-required-action-available'),
287
+ p('2.9.5.2', 'Information requirements available at start', 'supplemental', 'x2-9-5-2-information-requirements-available-at-start'),
288
+ p('2.9.5.3', 'Process instructions available', 'supplemental', 'x2-9-5-3-process-instructions-available'),
289
+ g('2.9.6', 'Unnecessary steps'),
290
+ p('2.9.6.1', 'Usability testing for unnecessary steps', 'assertion', 'x2-9-6-1-usability-testing-for-unnecessary-steps'),
291
+ g('2.10.1', 'Risk'),
292
+ p('2.10.1.1', 'Consequences of choices explained', 'core', 'x2-10-1-1-consequences-of-choices-explained'),
293
+ p('2.10.1.2', 'Consequences explained before agreement', 'supplemental', 'x2-10-1-2-consequences-explained-before-agreement'),
294
+ p('2.10.1.3', 'Diverse disabilities considered', 'assertion', 'x2-10-1-3-diverse-disabilities-considered'),
295
+ p('2.10.1.4', 'Algorithm inclusivity review', 'assertion', 'x2-10-1-4-algorithm-inclusivity-review'),
296
+ g('2.10.2', 'Algorithms'),
297
+ p('2.10.2.1', 'Inclusive data set', 'assertion', 'x2-10-2-1-inclusive-data-set'),
298
+ p('2.10.2.2', 'No harm from algorithms', 'assertion', 'x2-10-2-2-no-harm-from-algorithms'),
299
+ g('2.11.1', 'Help available'),
300
+ p('2.11.1.1', 'Consistent help available', 'supplemental', 'x2-11-1-1-consistent-help-available'),
301
+ p('2.11.1.2', 'Contextual help available', 'supplemental', 'x2-11-1-2-contextual-help-available'),
302
+ p('2.11.1.3', 'Disabled controls explained', 'supplemental', 'x2-11-1-3-disabled-controls-explained'),
303
+ p('2.11.1.4', 'Sensory characteristics not relied on', 'core', 'x2-11-1-4-sensory-characteristics-not-relied-on'),
304
+ p('2.11.1.5', 'Supported decision-making review', 'assertion', 'x2-11-1-5-supported-decision-making-review'),
305
+ p('2.11.1.6', 'Help usability testing', 'assertion', 'x2-11-1-6-help-usability-testing'),
306
+ g('2.11.2', 'Feedback'),
307
+ p('2.11.2.1', 'Feedback mechanism available', 'supplemental', 'x2-11-2-1-feedback-mechanism-available'),
308
+ g('2.12.1', 'Assistive technology control'),
309
+ p('2.12.1.1', 'Assistive technology supported', 'core', 'x2-12-1-1-assistive-technology-supported'),
310
+ p('2.12.1.2', 'User settings supported', 'core', 'x2-12-1-2-user-settings-supported'),
311
+ p('2.12.1.3', 'Virtual cursor supported', 'core', 'x2-12-1-3-virtual-cursor-supported'),
312
+ p('2.12.1.4', 'Notifications adjustable', 'supplemental', 'x2-12-1-4-notifications-adjustable'),
313
+ g('2.12.3', 'Adjustable viewport'),
314
+ p('2.12.3.1', 'Orientation supported (minimum)', 'core', 'x2-12-3-1-orientation-supported-minimum'),
315
+ p('2.12.3.2', 'Orientation supported (enhanced)', 'supplemental', 'x2-12-3-2-orientation-supported-enhanced'),
316
+ p('2.12.3.3', 'Text reflow supported', 'supplemental', 'x2-12-3-3-text-reflow-supported'),
317
+ p('2.12.3.4', 'Layout reflow supported', 'supplemental', 'x2-12-3-4-layout-reflow-supported'),
318
+ g('2.12.4', 'Media control'),
319
+ p('2.12.4.1', 'Page/view audio adjustable', 'core', 'x2-12-4-1-page-view-audio-adjustable'),
320
+ p('2.12.4.2', 'Media alternatives searchable', 'supplemental', 'x2-12-4-2-media-alternatives-searchable'),
321
+ p('2.12.4.3', 'Media alternatives controllable', 'supplemental', 'x2-12-4-3-media-alternatives-controllable'),
322
+ p('2.12.4.4', 'Media chapters available', 'supplemental', 'x2-12-4-4-media-chapters-available'),
323
+ g('2.12.5', 'Content changes'),
324
+ p('2.12.5.1', 'Change of content notified', 'core', 'x2-12-5-1-change-of-content-notified'),
325
+ p('2.12.5.2', 'Change of focus notified', 'core', 'x2-12-5-2-change-of-focus-notified'),
326
+ p('2.12.5.3', 'Change of user agent notified', 'core', 'x2-12-5-3-change-of-user-agent-notified'),
327
+ ];
328
+
329
+ function g(num, name) { return { guideline: true, num, name }; }
330
+ function p(num, name, type, anchor) { return { num, name, type, anchor }; }
331
+
332
+ export const provisions = (() => {
333
+ const list = [];
334
+ let guideline = null;
335
+ for (const entry of RAW) {
336
+ if (entry.guideline) { guideline = entry; continue; }
337
+ list.push({
338
+ num: entry.num,
339
+ name: entry.name,
340
+ type: entry.type,
341
+ guideline: `${guideline.num} ${guideline.name}`,
342
+ url: `${draftInfo.url}#${entry.anchor}`,
343
+ });
344
+ }
345
+ return list;
346
+ })();
347
+
348
+ const provisionByNum = new Map(provisions.map((entry) => [entry.num, entry]));
349
+
350
+ // Rule id → the draft requirements its failure also fails.
351
+ export const RULE_REQUIREMENTS = {
352
+ 'area-alt': ['2.1.1.3', '2.3.5.2'],
353
+ 'canvas-alt': ['2.1.3.1'],
354
+ 'embed-alt': ['2.1.3.1'],
355
+ 'image-alt': ['2.1.1.3'],
356
+ 'input-image-alt': ['2.1.1.3', '2.3.5.2'],
357
+ 'object-alt': ['2.1.3.1'],
358
+ 'svg-img-alt': ['2.1.1.3'],
359
+ 'audio-transcript': ['2.1.4.2'],
360
+ 'media-captions': ['2.1.5.2'],
361
+ 'video-audio-description': ['2.1.6.1'],
362
+ 'aria-required-children': ['2.3.5.6'],
363
+ 'aria-required-parent': ['2.3.5.6'],
364
+ 'definition-list': ['2.7.3.1'],
365
+ 'dlitem-parent': ['2.7.3.1'],
366
+ 'form-label': ['2.3.5.2'],
367
+ 'list-structure': ['2.7.3.1'],
368
+ 'listitem-parent': ['2.7.3.1'],
369
+ 'p-as-heading': ['2.7.3.4'],
370
+ 'table-headers': ['2.7.3.1'],
371
+ // A lock to the platform's own default orientation meets 2.12.3.1; only
372
+ // the supplemental both-orientations requirement fails for every lock.
373
+ 'orientation-lock': ['2.12.3.2'],
374
+ 'region-purpose': ['2.7.3.3'],
375
+ 'link-in-text-block': ['2.1.8.1'],
376
+ 'audio-control': ['2.12.4.1'],
377
+ 'color-contrast': ['2.2.1.3'],
378
+ 'control-contrast': ['2.2.1.3'],
379
+ 'meta-viewport': ['2.2.1.6'],
380
+ 'color-contrast-enhanced': ['2.2.1.10'],
381
+ 'reflow': ['2.12.3.4'],
382
+ 'non-text-contrast': ['2.3.5.1'],
383
+ 'scrollable-region-focusable': ['2.4.1.2'],
384
+ 'meta-refresh': ['2.9.2.1'],
385
+ 'pause-stop-hide': ['2.6.1.3'],
386
+ 'video-loop-motion': ['2.6.1.3'],
387
+ 'meta-refresh-no-exceptions': ['2.9.2.2'],
388
+ 'reduced-motion': ['2.12.1.2'],
389
+ 'bypass-blocks': ['2.7.3.2'],
390
+ 'document-title': ['2.7.2.1'],
391
+ 'visual-order-divergence': ['2.3.3.3'],
392
+ 'link-name': ['2.3.5.2'],
393
+ 'link-text-generic': ['2.3.5.2'],
394
+ 'heading-label-placeholder': ['2.7.3.3', '2.3.5.2'],
395
+ 'focus-visible': ['2.3.1.2'],
396
+ 'section-heading': ['2.7.3.4'],
397
+ // Path-based gestures (2.4.5.4) and multipoint ones (2.4.3.2 lists them
398
+ // among complex pointer inputs).
399
+ 'pointer-gesture-alternative': ['2.4.5.4', '2.4.3.2'],
400
+ 'drag-alternative': ['2.4.3.2'],
401
+ 'html-lang': ['2.2.2.2'],
402
+ 'valid-lang-parts': ['2.2.2.2'],
403
+ 'error-message-linkage': ['2.5.1.5'],
404
+ 'financial-form-confirmation': ['2.5.2.1'],
405
+ 'redundant-entry': ['2.9.4.2'],
406
+ 'auth-field-obstruction': ['2.9.1.1'],
407
+ 'captcha-alternative': ['2.9.1.2'],
408
+ 'aria-allowed-attr': ['2.3.5.6'],
409
+ 'aria-attr-valid': ['2.3.5.6'],
410
+ 'aria-field-name': ['2.3.5.2'],
411
+ 'aria-hidden-focus': ['2.3.5.6'],
412
+ 'aria-state-unreachable': ['2.3.5.6'],
413
+ 'aria-valid-refs': ['2.3.5.6'],
414
+ 'button-name': ['2.3.5.2'],
415
+ 'composite-widget-name': ['2.3.5.2'],
416
+ 'frame-title': ['2.7.3.3'],
417
+ 'invoker-target': ['2.3.5.6'],
418
+ 'label-for-valid': ['2.3.5.2'],
419
+ 'nested-interactive': ['2.3.5.6'],
420
+ 'role-required-aria': ['2.3.5.6'],
421
+ 'summary-name': ['2.3.5.2'],
422
+ 'valid-role': ['2.3.5.6'],
423
+ };
424
+
425
+ // WCAG rules that stay out of a WCAG 3 audit, and why. Best-practice rules
426
+ // are not listed: they run in either standard when best practices are on.
427
+ export const UNMAPPED_RULES = {
428
+ 'reading-order-divergence': 'The draft has no requirement on reading sequence; focus order (2.3.3.3) is a different outcome.',
429
+ 'autocomplete-valid': 'An invalid token hinders autofill but does not prevent automated entry (2.9.1.1), and the draft has no identify-purpose requirement.',
430
+ 'text-justified': 'The draft asks that justification can be adjusted (2.2.1.4), not that text is unjustified by default.',
431
+ 'text-spacing': 'The rule applies letter and word spacing, which neither adjustable-text requirement (2.2.1.4, 2.2.1.5) lists, so a clipping it finds may not fail the draft.',
432
+ 'aria-label-misuse': 'It concerns labels on plain containers; the draft asks for names on interactive elements (2.3.5.2, 2.3.5.6) and on meaningful blocks only (2.7.3.3).',
433
+ 'link-text-generic-only': 'The draft does not say whether a name may take its purpose from context; the context-aware link-text-generic rule stands for 2.3.5.2.',
434
+ 'focus-not-obscured': 'The draft has no focus-obscured requirement.',
435
+ 'focus-not-obscured-enhanced': 'The draft has no focus-obscured requirement.',
436
+ 'target-size': 'The draft has no target size requirement.',
437
+ 'target-size-enhanced': 'The draft has no target size requirement.',
438
+ 'on-input-navigation': 'The draft\'s change notifications (2.7.2.5, 2.12.5) cover different triggers from a change of context on input.',
439
+ };
440
+
441
+ /** The check set a value names, 'core' or 'supplemental', or null. */
442
+ export function checkSetOf(value) {
443
+ const name = String(value ?? '').toLowerCase();
444
+ return CHECK_SETS.includes(name) ? name : RENAMED_SETS[name] ?? null;
445
+ }
446
+
447
+ /** The WCAG 3 check set a tag list asks for, wcag3-core or
448
+ * wcag3-supplemental, the larger when both are present; null for a
449
+ * WCAG 2 audit. */
450
+ export function checkSetFromTags(tags) {
451
+ let found = null;
452
+ for (const tag of tags ?? []) {
453
+ const set = /^wcag3-/.test(tag) ? checkSetOf(tag.slice(6)) : null;
454
+ if (set && (!found || CHECK_SETS.indexOf(set) > CHECK_SETS.indexOf(found))) found = set;
455
+ }
456
+ return found;
457
+ }
458
+
459
+ /** The draft requirements a rule speaks to, as { num, name, type, url }. */
460
+ export function requirementsForRule(ruleId) {
461
+ return (RULE_REQUIREMENTS[ruleId] ?? []).map((num) => {
462
+ const { name, type, url } = provisionByNum.get(num);
463
+ return { num, name, type: TYPE_LABELS[type], url };
464
+ });
465
+ }
466
+
467
+ /** Does the check set take in any requirement this rule tests? */
468
+ export function ruleInCheckSet(ruleId, set) {
469
+ const types = SET_TYPES[set];
470
+ return Boolean(types) && (RULE_REQUIREMENTS[ruleId] ?? [])
471
+ .some((num) => types.has(provisionByNum.get(num).type));
472
+ }
473
+
474
+ /**
475
+ * Rule selection shared by every runner. A tag list selects the rules that
476
+ * carry any of its tags; a WCAG 3 check set tag also selects the rules
477
+ * matched to its requirements. An empty or absent list selects every rule.
478
+ */
479
+ export function ruleMatchesTags(rule, tags) {
480
+ if (!tags?.length) return true;
481
+ if (rule.tags.some((tag) => tags.includes(tag))) return true;
482
+ const set = checkSetFromTags(tags);
483
+ return Boolean(set) && ruleInCheckSet(rule.id, set);
484
+ }
485
+
486
+ /**
487
+ * The check set's requirements as a human checklist, shaped like the WCAG 2
488
+ * manual-review entries: `level` carries the provision type and `principle`
489
+ * the guideline. Every requirement in the set is listed, since no rule was
490
+ * written for any of them; the ones a rule speaks to are 'partial'.
491
+ * Assertions are in neither set: they are not needed to conform, and only
492
+ * the people who make the content can make them.
493
+ */
494
+ export function manualReviewChecklist(set) {
495
+ const types = SET_TYPES[set];
496
+ if (!types) return [];
497
+ const partly = new Set(Object.values(RULE_REQUIREMENTS).flat());
498
+ return provisions
499
+ .filter((entry) => types.has(entry.type))
500
+ .map(({ num, name, type, guideline, url }) => ({
501
+ num,
502
+ name,
503
+ level: TYPE_LABELS[type],
504
+ principle: guideline,
505
+ url,
506
+ automation: partly.has(num) ? 'partial' : 'manual',
507
+ draft: true,
508
+ }));
509
+ }
510
+
511
+ /** The results header for a WCAG 3 audit: the draft, the check set and
512
+ * what it covers, as "core requirements". */
513
+ export function standardFor(set) {
514
+ return { ...draftInfo, checkSet: set, scope: SET_SCOPES[set] };
515
+ }
516
+
517
+ export default {
518
+ draftInfo, CHECK_SETS, provisions, RULE_REQUIREMENTS, UNMAPPED_RULES,
519
+ checkSetOf, checkSetFromTags, requirementsForRule, ruleInCheckSet, ruleMatchesTags, manualReviewChecklist, standardFor,
520
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pour-engine",
3
- "version": "1.44.2",
3
+ "version": "1.46.0",
4
4
  "description": "Accessibility (WCAG 2.2) audit engine written from the W3C specifications — runs entirely in the browser, no dependencies.",
5
5
  "type": "module",
6
6
  "main": "engine/index.js",