pour-engine 1.44.1 → 1.45.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.1',
9
+ version: '1.45.0',
10
10
  },
11
11
  };
package/engine/README.md CHANGED
@@ -111,6 +111,21 @@ 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 tier tag, `wcag3-bronze`, `wcag3-silver` or `wcag3-gold`, reports against
117
+ the 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. `results.standard` is stamped draft,
123
+ `manualReview` becomes the tier's provisions, and each rule result carries
124
+ `wcag3`. The draft's tiers build on conformance and leave their counts to
125
+ be decided, so pour reads them as: Bronze, the core requirements and the
126
+ content assertions; Silver, plus every supplemental requirement; Gold,
127
+ plus the assertions about the organization. WCAG 2 stays the default.
128
+
114
129
  The checklist accounts for the criteria; it does not mean every page state
115
130
  or possible failure has been tested. A snapshot audit still needs human
116
131
  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, tierFromTags, 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,11 @@ 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 tier tag (wcag3-bronze, -silver, -gold) selects
86
+ * the rules mapped to that tier's draft requirements and reframes the
87
+ * report against the WCAG 3.0 Working Draft: results.standard is
88
+ * stamped draft, the manual checklist becomes the tier's provisions,
89
+ * and each rule result carries the requirements it maps to (wcag3); exclude is a CSS selector — elements matching it, or inside
91
90
  * a match (including across shadow boundaries), are left out of every
92
91
  * rule; signal aborts the run between rules (throws AbortError)
93
92
  * @param {(progress: object) => void} [onProgress] - called before each rule
@@ -98,15 +97,17 @@ function toResultNode(element, outcome) {
98
97
  */
99
98
  export async function run(context = document, options = {}, onProgress) {
100
99
  const auditStarted = performance.now();
100
+ const tier = tierFromTags(options.tags);
101
101
  const results = {
102
102
  testEngine: { name, version },
103
103
  timestamp: new Date().toISOString(),
104
104
  url: context.location?.href ?? context.ownerDocument?.location?.href ?? '',
105
+ standard: tier ? standardFor(tier) : null,
105
106
  violations: [],
106
107
  passes: [],
107
108
  incomplete: [],
108
109
  inapplicable: [],
109
- manualReview: manualReviewCriteria(options.tags),
110
+ manualReview: tier ? manualReviewChecklist(tier) : manualReviewCriteria(options.tags),
110
111
  ruleTimings: [],
111
112
  };
112
113
 
@@ -251,6 +252,9 @@ export async function run(context = document, options = {}, onProgress) {
251
252
  description: rule.help,
252
253
  helpUrl: rule.helpUrl,
253
254
  };
255
+ // In a WCAG 3 audit, the draft requirements this rule speaks to (empty
256
+ // for a best-practice rule, which maps to none).
257
+ if (tier) ruleResult.wcag3 = requirementsForRule(rule.id);
254
258
  if (!elements.length) results.inapplicable.push({ ...ruleResult, nodes: [] });
255
259
  if (buckets.fail.length) results.violations.push({ ...ruleResult, nodes: buckets.fail });
256
260
  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,
@@ -10,6 +10,40 @@
10
10
  // Keep this in step with 3.1.2-valid-lang-parts.js.
11
11
  const LANG_PATTERN = /^([a-zA-Z]{2,3}(-[a-zA-Z0-9]{1,8})*|[xXiI](-[a-zA-Z0-9]{1,8})+)$/;
12
12
 
13
+ /** Elements that hold no words and draw nothing. */
14
+ const WORDLESS = 'script, style, template, link, meta, base';
15
+
16
+ /**
17
+ * Does this document hold nothing a language could apply to? 3.1.1 asks
18
+ * that "the default human language of each Web page can be programmatically
19
+ * determined", and a document with no words has no human language to
20
+ * determine: the placeholder about:blank frame a page parks for later, a
21
+ * frame a script has not written into yet. Its html element has no lang
22
+ * because it has no anything, and the frame's real fault, where it has one,
23
+ * is its missing title (4.1.2, frame-title). 3.1.2 draws the same line:
24
+ * a lang can only fail through words it governs.
25
+ *
26
+ * Blank is kept narrow so a doubtful document keeps its failure: no title
27
+ * text (a title is spoken), no text in the body, and no element in the
28
+ * body beyond scripts and metadata. Any other element counts as
29
+ * content, since an image, a control, a nested frame or a custom element
30
+ * can all carry words this check does not look for. An application shell
31
+ * waiting on its script (<div id="root"></div>) is therefore not blank.
32
+ */
33
+ function blankDocument(element) {
34
+ const doc = element.ownerDocument;
35
+ if (doc.title?.trim()) return false;
36
+ const body = doc.body;
37
+ if (!body) return true;
38
+ // Direct children only: once every element child is wordless, the one
39
+ // place left for words is a text node of the body itself, and a
40
+ // script's source is not words.
41
+ return [...body.childNodes].every((node) => {
42
+ if (node.nodeType === 3) return !node.data.trim();
43
+ return node.nodeType !== 1 || node.matches(WORDLESS);
44
+ });
45
+ }
46
+
13
47
  export default {
14
48
  id: 'html-lang',
15
49
  name: 'Page language',
@@ -20,6 +54,7 @@ export default {
20
54
  selector: 'html',
21
55
  visibleOnly: false,
22
56
  evaluate(element) {
57
+ if (blankDocument(element)) return { status: 'pass' };
23
58
  const lang = element.getAttribute('lang')?.trim();
24
59
  if (!element.hasAttribute('lang')) {
25
60
  return {
@@ -46,6 +46,27 @@ const HANDLED_ELSEWHERE = new Set(['label', 'labelledby']);
46
46
  // Those are aria-state-unreachable's, which imports this.
47
47
  export const NATIVE_STATE = { readonly: 'readonly', required: 'required', disabled: 'disabled', checked: 'checked' };
48
48
 
49
+ // What the author most likely meant, for the attributes that turn up most
50
+ // on real pages, so the fix names the repair and not just the fault. Each
51
+ // says where ARIA 1.2 puts the attribute (its "Used in Roles" list) and what
52
+ // serves the same purpose on this role. An attribute with no entry gets the
53
+ // general advice. Added 2026-09-21 after inclusion.hsbc.com marked the
54
+ // chosen language with aria-selected on a link, where "move it to the
55
+ // control that toggles this one" pointed nowhere: the repair there is
56
+ // aria-current.
57
+ const HINT = {
58
+ selected: (role) => (role === 'cell'
59
+ ? 'A table whose cells can be selected is a grid: aria-selected needs role="grid" on the table, which makes its cells gridcells.'
60
+ : `aria-selected belongs to option, tab, row and gridcell. To mark the current item in a set of ${role === 'link' ? 'links' : 'items'}, such as the current page, language or step, use aria-current, which is global and allowed on this role.`),
61
+ expanded: (role) => (role === 'textbox' || role === 'searchbox'
62
+ ? 'A text field that opens a list of suggestions is a combobox: aria-expanded needs role="combobox", which ARIA in HTML allows on a text input, with aria-controls pointing at the list.'
63
+ : 'aria-expanded belongs on the control that opens and closes the content, a button in most cases, and not on the content or its container. Put it on that control; if this element is itself what the user presses, it should be a button.'),
64
+ checked: (role) => (role === 'button'
65
+ ? 'A button that stays on or off takes aria-pressed, not aria-checked.'
66
+ : 'aria-checked belongs to checkbox, radio, switch, option and the checkable menu items. For something the user ticks, use a native checkbox or radio input.'),
67
+ pressed: () => 'aria-pressed belongs to button alone. If this element switches something on and off, make it a button element.',
68
+ };
69
+
49
70
  /** The element's role and the non-global aria- attributes (names without
50
71
  * the prefix) that role does not support, or null when there are none or
51
72
  * the role is not modelled. Shared with aria-state-unreachable. */
@@ -66,6 +87,15 @@ export function unsupportedAria(element) {
66
87
  return names.length ? { role, names } : null;
67
88
  }
68
89
 
90
+ /** The repair for each unsupported attribute: its own hint where there is
91
+ * one, the general advice for the rest. */
92
+ function repair(attrs, role) {
93
+ const hinted = attrs.filter((name) => HINT[name]).map((name) => HINT[name](role));
94
+ const rest = attrs.filter((name) => !HINT[name]).map((name) => `aria-${name}`);
95
+ if (rest.length) hinted.push(`Move ${rest.join('/')} to the element whose role supports it.`);
96
+ return hinted.join(' ');
97
+ }
98
+
69
99
  export default {
70
100
  id: 'aria-allowed-attr',
71
101
  name: 'Allowed ARIA attributes',
@@ -86,7 +116,7 @@ export default {
86
116
  return {
87
117
  status: 'fail',
88
118
  message: `${names.join(', ')} is not supported on role "${role}" (ARIA 1.2 §8.6), so user agents drop it and assistive technology never sees it. Nothing is announced wrongly, but nothing is announced at all. If the element really has that state, it is invisible to a screen reader; if it does not, the attribute is stray and no one is affected, but ARIA 1.2 still says authors MUST NOT write it.`,
89
- fix: `Move ${names.join('/')} to the element whose role supports it, usually the control that toggles this one, or remove it if the element has no such state. Adding a role to this element to make the attribute legal is rarely right: the host language restricts which roles each element may take.`,
119
+ fix: `${repair(disallowed, role)} If the element has no such state, remove the attribute. Adding a role to this element to make the attribute legal is rarely right: the host language restricts which roles each element may take.`,
90
120
  };
91
121
  },
92
122
  };
@@ -0,0 +1,509 @@
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 (all must be met to
11
+ // conform), supplemental requirements (beyond conformance) and assertions
12
+ // (documented statements about an organization's practice, which no tool
13
+ // can check). Its Reporting section (4.1.4, Exploratory) lays out tiers
14
+ // that build on conformance: Bronze and Silver add a to-be-decided number
15
+ // of supplemental requirements and content assertions, Gold adds the
16
+ // assertions about the organization. Until the draft fixes those numbers,
17
+ // pour reads the tiers this way, and says so:
18
+ // Bronze: the core requirements, plus the content assertions to make
19
+ // Silver: Bronze plus every supplemental requirement
20
+ // Gold: Silver plus the assertions about the organization
21
+ //
22
+ // The draft tags each assertion Content or Organization in principle but
23
+ // not yet one by one; an assertion about a style guide, policy or process
24
+ // the organization keeps is read as Organization, any other as Content.
25
+ //
26
+ // No rule here was written for WCAG 3. The rules test WCAG 2 criteria;
27
+ // RULE_REQUIREMENTS names, rule by rule, the draft requirement that a
28
+ // failure of that rule also fails, judged by reading both documents. A
29
+ // rule with no such requirement (UNMAPPED_RULES, with the reason) does
30
+ // not run in a WCAG 3 audit. Where the draft leaves a value to be
31
+ // determined (text contrast, text spacing), the rule's WCAG 2 value
32
+ // stands in, and the report says the values are WCAG 2's.
33
+
34
+ export const draftInfo = {
35
+ id: 'wcag3-draft',
36
+ name: 'WCAG 3.0',
37
+ status: 'Working Draft',
38
+ dated: '2026-09-10',
39
+ draft: true,
40
+ url: 'https://www.w3.org/TR/2026/WD-wcag-3.0-20260910/',
41
+ 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.',
42
+ };
43
+
44
+ export const TIERS = ['bronze', 'silver', 'gold'];
45
+ export const TIER_LABELS = { bronze: 'Bronze', silver: 'Silver', gold: 'Gold' };
46
+
47
+ // Which provision types each tier takes in. Cumulative, as in the draft.
48
+ const TIER_TYPES = {
49
+ bronze: new Set(['core', 'content']),
50
+ silver: new Set(['core', 'content', 'supplemental']),
51
+ gold: new Set(['core', 'content', 'supplemental', 'organization']),
52
+ };
53
+
54
+ const TYPE_LABELS = {
55
+ core: 'Core',
56
+ supplemental: 'Supplemental',
57
+ content: 'Assertion',
58
+ organization: 'Assertion',
59
+ };
60
+
61
+ // Every requirement and assertion of the draft, in document order, under
62
+ // its guideline. The one recommended practice (2.7.2.7) is left out: the
63
+ // draft says recommended practices are not needed to conform or to reach
64
+ // any tier. Guidelines with nothing past Exploratory (2.1.2 Figure
65
+ // captions, 2.12.2 Control text) have no provisions to list.
66
+ const RAW = [
67
+ g('2.1.1', 'Image alternatives'),
68
+ p('2.1.1.1', 'Images detectable', 'core', 'x2-1-1-1-images-detectable'),
69
+ p('2.1.1.2', 'Decorative images hidden', 'core', 'x2-1-1-2-decorative-images-hidden'),
70
+ p('2.1.1.3', 'Image alternatives available', 'core', 'x2-1-1-3-image-alternatives-available'),
71
+ p('2.1.1.4', 'Image alternatives equivalent', 'core', 'x2-1-1-4-image-alternatives-equivalent'),
72
+ g('2.1.3', 'Non-text alternatives'),
73
+ p('2.1.3.1', 'Non-text content not relied on', 'core', 'x2-1-3-1-non-text-content-not-relied-on'),
74
+ g('2.1.4', 'Transcripts'),
75
+ p('2.1.4.1', 'Transcripts findable', 'core', 'x2-1-4-1-transcripts-findable'),
76
+ p('2.1.4.2', 'Dialogue transcripts available (prerecorded)', 'core', 'x2-1-4-2-dialogue-transcripts-available-prerecorded'),
77
+ p('2.1.4.3', 'Dialogue transcripts available (live)', 'core', 'x2-1-4-3-dialogue-transcripts-available-live'),
78
+ p('2.1.4.4', 'Transcripts equivalent (prerecorded)', 'core', 'x2-1-4-4-transcripts-equivalent-prerecorded'),
79
+ p('2.1.4.5', 'Descriptive transcripts available', 'core', 'x2-1-4-5-descriptive-transcripts-available'),
80
+ p('2.1.4.6', 'Speakers identified in transcripts', 'supplemental', 'x2-1-4-6-speakers-identified-in-transcripts'),
81
+ p('2.1.4.7', 'Speaker language identified in transcripts', 'supplemental', 'x2-1-4-7-speaker-language-identified-in-transcripts'),
82
+ p('2.1.4.8', 'Sounds identified in transcripts', 'core', 'x2-1-4-8-sounds-identified-in-transcripts'),
83
+ p('2.1.4.9', 'Visual information identified in transcripts', 'core', 'x2-1-4-9-visual-information-identified-in-transcripts'),
84
+ p('2.1.4.10', 'Transcripts style guide', 'organization', 'x2-1-4-10-transcripts-style-guide'),
85
+ p('2.1.4.11', 'Transcripts usability testing', 'content', 'x2-1-4-11-transcripts-usability-testing'),
86
+ p('2.1.4.12', 'Transcripts reviewed by content authors', 'content', 'x2-1-4-12-transcripts-reviewed-by-content-authors'),
87
+ g('2.1.5', 'Captions'),
88
+ p('2.1.5.1', 'Captions adjustable', 'supplemental', 'x2-1-5-1-captions-adjustable'),
89
+ p('2.1.5.2', 'Captions available (prerecorded)', 'core', 'x2-1-5-2-captions-available-prerecorded'),
90
+ p('2.1.5.3', 'Captions equivalent (prerecorded)', 'core', 'x2-1-5-3-captions-equivalent-prerecorded'),
91
+ p('2.1.5.4', 'Captions available (live)', 'core', 'x2-1-5-4-captions-available-live'),
92
+ p('2.1.5.5', 'Caption language adjustable', 'supplemental', 'x2-1-5-5-caption-language-adjustable'),
93
+ p('2.1.5.6', 'Captions controllable', 'supplemental', 'x2-1-5-6-captions-controllable'),
94
+ p('2.1.5.7', 'Captions unobstructed', 'supplemental', 'x2-1-5-7-captions-unobstructed'),
95
+ p('2.1.5.8', 'Captions synchronized', 'core', 'x2-1-5-8-captions-synchronized'),
96
+ p('2.1.5.9', 'Captions centered (immersive)', 'core', 'x2-1-5-9-captions-centered-immersive'),
97
+ p('2.1.5.10', 'Direction indicated (immersive)', 'core', 'x2-1-5-10-direction-indicated-immersive'),
98
+ p('2.1.5.11', 'Speakers identified in captions', 'supplemental', 'x2-1-5-11-speakers-identified-in-captions'),
99
+ p('2.1.5.12', 'Speaker language identified in captions', 'supplemental', 'x2-1-5-12-speaker-language-identified-in-captions'),
100
+ p('2.1.5.13', 'Sounds identified in captions', 'core', 'x2-1-5-13-sounds-identified-in-captions'),
101
+ p('2.1.5.14', 'Captions style guide', 'organization', 'x2-1-5-14-captions-style-guide'),
102
+ p('2.1.5.15', 'Captions usability testing', 'content', 'x2-1-5-15-captions-usability-testing'),
103
+ p('2.1.5.16', 'Captions reviewed by content authors', 'content', 'x2-1-5-16-captions-reviewed-by-content-authors'),
104
+ g('2.1.6', 'Audio descriptions'),
105
+ p('2.1.6.1', 'Audio descriptions available (prerecorded)', 'core', 'x2-1-6-1-audio-descriptions-available-prerecorded'),
106
+ p('2.1.6.2', 'Audio descriptions equivalent (prerecorded)', 'core', 'x2-1-6-2-audio-descriptions-equivalent-prerecorded'),
107
+ p('2.1.6.3', 'Audio descriptions synchronized', 'core', 'x2-1-6-3-audio-descriptions-synchronized'),
108
+ p('2.1.6.4', 'Audio descriptions available (live)', 'supplemental', 'x2-1-6-4-audio-descriptions-available-live'),
109
+ p('2.1.6.5', 'Extended audio descriptions available', 'core', 'x2-1-6-5-extended-audio-descriptions-available'),
110
+ p('2.1.6.6', 'Extended audio descriptions equivalent', 'core', 'x2-1-6-6-extended-audio-descriptions-equivalent'),
111
+ p('2.1.6.7', 'Audio description language adjustable', 'supplemental', 'x2-1-6-7-audio-description-language-adjustable'),
112
+ p('2.1.6.8', 'Audio descriptions controllable', 'supplemental', 'x2-1-6-8-audio-descriptions-controllable'),
113
+ p('2.1.6.9', 'Speakers identified in audio descriptions', 'supplemental', 'x2-1-6-9-speakers-identified-in-audio-descriptions'),
114
+ p('2.1.6.10', 'Speaker language identified in audio descriptions', 'supplemental', 'x2-1-6-10-speaker-language-identified-in-audio-descriptions'),
115
+ p('2.1.6.11', 'Sounds identified in audio descriptions', 'core', 'x2-1-6-11-sounds-identified-in-audio-descriptions'),
116
+ p('2.1.6.12', 'Visual information identified in audio descriptions', 'core', 'x2-1-6-12-visual-information-identified-in-audio-descriptions'),
117
+ p('2.1.6.13', 'Audio descriptions style guide', 'organization', 'x2-1-6-13-audio-descriptions-style-guide'),
118
+ p('2.1.6.14', 'Audio descriptions usability testing', 'content', 'x2-1-6-14-audio-descriptions-usability-testing'),
119
+ p('2.1.6.15', 'Audio descriptions reviewed by content authors', 'content', 'x2-1-6-15-audio-descriptions-reviewed-by-content-authors'),
120
+ g('2.1.7', 'Sign language'),
121
+ p('2.1.7.1', 'Sign language available (prerecorded)', 'supplemental', 'x2-1-7-1-sign-language-available-prerecorded'),
122
+ p('2.1.7.2', 'Sign language controllable', 'supplemental', 'x2-1-7-2-sign-language-controllable'),
123
+ p('2.1.7.3', 'Sign language policy (live)', 'organization', 'x2-1-7-3-sign-language-policy-live'),
124
+ g('2.1.8', 'Single sense'),
125
+ p('2.1.8.1', 'Hue not relied on', 'core', 'x2-1-8-1-hue-not-relied-on'),
126
+ p('2.1.8.2', 'Graphical object contrast sufficient', 'core', 'x2-1-8-2-graphical-object-contrast-sufficient'),
127
+ p('2.1.8.3', 'Visual depth not relied on', 'core', 'x2-1-8-3-visual-depth-not-relied-on'),
128
+ p('2.1.8.4', 'Sound not relied on', 'core', 'x2-1-8-4-sound-not-relied-on'),
129
+ p('2.1.8.5', 'Spatial audio not relied on', 'core', 'x2-1-8-5-spatial-audio-not-relied-on'),
130
+ g('2.1.9', 'Accessible media player'),
131
+ p('2.1.9.1', 'Accessible video player selected', 'content', 'x2-1-9-1-accessible-video-player-selected'),
132
+ p('2.1.9.2', 'Accessible audio player selected', 'content', 'x2-1-9-2-accessible-audio-player-selected'),
133
+ g('2.2.1', 'Text appearance'),
134
+ p('2.2.1.1', 'Blocks of text readable (minimum)', 'core', 'x2-2-1-1-blocks-of-text-readable-minimum'),
135
+ p('2.2.1.2', 'Text style readable (minimum)', 'core', 'x2-2-1-2-text-style-readable-minimum'),
136
+ p('2.2.1.3', 'Text contrast sufficient (minimum)', 'core', 'x2-2-1-3-text-contrast-sufficient-minimum'),
137
+ p('2.2.1.4', 'Blocks of text adjustable', 'core', 'x2-2-1-4-blocks-of-text-adjustable'),
138
+ p('2.2.1.5', 'Text style adjustable', 'core', 'x2-2-1-5-text-style-adjustable'),
139
+ p('2.2.1.6', 'Text size adjustable', 'core', 'x2-2-1-6-text-size-adjustable'),
140
+ p('2.2.1.7', 'Text color adjustable', 'core', 'x2-2-1-7-text-color-adjustable'),
141
+ p('2.2.1.8', 'Blocks of text readable (enhanced)', 'supplemental', 'x2-2-1-8-blocks-of-text-readable-enhanced'),
142
+ p('2.2.1.9', 'Text style readable (enhanced)', 'supplemental', 'x2-2-1-9-text-style-readable-enhanced'),
143
+ p('2.2.1.10', 'Text contrast sufficient (enhanced)', 'supplemental', 'x2-2-1-10-text-contrast-sufficient-enhanced'),
144
+ p('2.2.1.11', 'Text customizations retained', 'supplemental', 'x2-2-1-11-text-customizations-retained'),
145
+ g('2.2.2', 'Text-to-speech'),
146
+ p('2.2.2.1', 'Text detectable', 'core', 'x2-2-2-1-text-detectable'),
147
+ p('2.2.2.2', 'Human language detectable', 'core', 'x2-2-2-2-human-language-detectable'),
148
+ p('2.2.2.3', 'Numerical metadata available', 'core', 'x2-2-2-3-numerical-metadata-available'),
149
+ g('2.2.3', 'Clear language'),
150
+ p('2.2.3.1', 'Abbreviations explained', 'core', 'x2-2-3-1-abbreviations-explained'),
151
+ p('2.2.3.2', 'Non-literal language explained', 'supplemental', 'x2-2-3-2-non-literal-language-explained'),
152
+ p('2.2.3.3', 'Summaries available', 'core', 'x2-2-3-3-summaries-available'),
153
+ p('2.2.3.4', 'Common words used', 'supplemental', 'x2-2-3-4-common-words-used'),
154
+ p('2.2.3.5', 'Diacritics available', 'core', 'x2-2-3-5-diacritics-available'),
155
+ p('2.2.3.6', 'No nested clauses', 'supplemental', 'x2-2-3-6-no-nested-clauses'),
156
+ p('2.2.3.7', 'No unnecessary words', 'supplemental', 'x2-2-3-7-no-unnecessary-words'),
157
+ p('2.2.3.8', 'Clear language review', 'organization', 'x2-2-3-8-clear-language-review'),
158
+ p('2.2.3.9', 'Visual aids review', 'organization', 'x2-2-3-9-visual-aids-review'),
159
+ g('2.3.1', 'Keyboard focus appearance'),
160
+ p('2.3.1.1', 'Default focus indicator used', 'supplemental', 'x2-3-1-1-default-focus-indicator-used'),
161
+ p('2.3.1.2', 'Focus indicator contrast sufficient', 'core', 'x2-3-1-2-focus-indicator-contrast-sufficient'),
162
+ p('2.3.1.3', 'Focus indicator size sufficient', 'supplemental', 'x2-3-1-3-focus-indicator-size-sufficient'),
163
+ p('2.3.1.4', 'Focus indicator style guide', 'organization', 'x2-3-1-4-focus-indicator-style-guide'),
164
+ g('2.3.2', 'Pointer focus appearance'),
165
+ p('2.3.2.1', 'Pointer activation indicated (minimum)', 'core', 'x2-3-2-1-pointer-activation-indicated-minimum'),
166
+ p('2.3.2.2', 'Pointer activation indicated (enhanced)', 'supplemental', 'x2-3-2-2-pointer-activation-indicated-enhanced'),
167
+ p('2.3.2.3', 'Pointer contrast sufficient', 'core', 'x2-3-2-3-pointer-contrast-sufficient'),
168
+ p('2.3.2.4', 'Default pointer used', 'supplemental', 'x2-3-2-4-default-pointer-used'),
169
+ p('2.3.2.5', 'Pointer focus indicated', 'core', 'x2-3-2-5-pointer-focus-indicated'),
170
+ p('2.3.2.6', 'Pointer visible', 'core', 'x2-3-2-6-pointer-visible'),
171
+ p('2.3.2.7', 'Enhanced pointer available', 'supplemental', 'x2-3-2-7-enhanced-pointer-available'),
172
+ g('2.3.3', 'Navigating content'),
173
+ p('2.3.3.1', 'Focus relevant', 'core', 'x2-3-3-1-focus-relevant'),
174
+ p('2.3.3.2', 'Focus retained', 'supplemental', 'x2-3-3-2-focus-retained'),
175
+ p('2.3.3.3', 'Focus order meaningful', 'core', 'x2-3-3-3-focus-order-meaningful'),
176
+ g('2.3.4', 'Expected behavior'),
177
+ p('2.3.4.1', 'Consistent interactions', 'content', 'x2-3-4-1-consistent-interactions'),
178
+ p('2.3.4.2', 'Consistent control location', 'supplemental', 'x2-3-4-2-consistent-control-location'),
179
+ p('2.3.4.3', 'Conventional pattern used', 'content', 'x2-3-4-3-conventional-pattern-used'),
180
+ g('2.3.5', 'Control information'),
181
+ p('2.3.5.1', 'Interactive element contrast sufficient', 'core', 'x2-3-5-1-interactive-element-contrast-sufficient'),
182
+ p('2.3.5.2', 'Interactive element names available', 'core', 'x2-3-5-2-interactive-element-names-available'),
183
+ p('2.3.5.3', 'Changes to elements notified', 'core', 'x2-3-5-3-changes-to-elements-notified'),
184
+ p('2.3.5.4', 'Input constraints used', 'core', 'x2-3-5-4-input-constraints-used'),
185
+ p('2.3.5.5', 'Label included in programmatic name', 'core', 'x2-3-5-5-label-included-in-programmatic-name'),
186
+ p('2.3.5.6', 'Roles, values, states, properties available', 'core', 'x2-3-5-6-roles-values-states-properties-available'),
187
+ g('2.4.1', 'Keyboard interface input'),
188
+ p('2.4.1.1', 'Keyboard operable', 'core', 'x2-4-1-1-keyboard-operable'),
189
+ p('2.4.1.2', 'Keyboard accessible', 'core', 'x2-4-1-2-keyboard-accessible'),
190
+ p('2.4.1.3', 'Bidirectional navigation', 'core', 'x2-4-1-3-bidirectional-navigation'),
191
+ p('2.4.1.4', 'Custom keys documented', 'supplemental', 'x2-4-1-4-custom-keys-documented'),
192
+ p('2.4.1.5', 'No keyboard conflicts', 'core', 'x2-4-1-5-no-keyboard-conflicts'),
193
+ p('2.4.1.6', 'Focus placed', 'supplemental', 'x2-4-1-6-focus-placed'),
194
+ p('2.4.1.7', 'No keyboard traps', 'core', 'x2-4-1-7-no-keyboard-traps'),
195
+ p('2.4.1.8', 'Focus user-controlled', 'core', 'x2-4-1-8-focus-user-controlled'),
196
+ p('2.4.1.9', 'Focus movement relevant', 'supplemental', 'x2-4-1-9-focus-movement-relevant'),
197
+ g('2.4.2', 'Physical or cognitive effort when using keyboard'),
198
+ p('2.4.2.1', 'Navigation keys described', 'supplemental', 'x2-4-2-1-navigation-keys-described'),
199
+ p('2.4.2.2', 'No repetitive adjacent interactive elements', 'supplemental', 'x2-4-2-2-no-repetitive-adjacent-interactive-elements'),
200
+ p('2.4.2.3', 'Keyboard effort comparable', 'content', 'x2-4-2-3-keyboard-effort-comparable'),
201
+ g('2.4.3', 'Pointer input'),
202
+ p('2.4.3.1', 'Pointer activation controllable', 'core', 'x2-4-3-1-pointer-activation-controllable'),
203
+ p('2.4.3.2', 'Simple pointer input available', 'core', 'x2-4-3-2-simple-pointer-input-available'),
204
+ p('2.4.3.3', 'Consistent pointer cancellation', 'supplemental', 'x2-4-3-3-consistent-pointer-cancellation'),
205
+ p('2.4.3.4', 'Pointer pressure not relied on', 'core', 'x2-4-3-4-pointer-pressure-not-relied-on'),
206
+ p('2.4.3.5', 'Pointer speed not relied on', 'core', 'x2-4-3-5-pointer-speed-not-relied-on'),
207
+ g('2.4.4', 'Speech and voice input'),
208
+ p('2.4.4.1', 'Speech not relied on', 'core', 'x2-4-4-1-speech-not-relied-on'),
209
+ p('2.4.4.2', 'Real-time text available', 'core', 'x2-4-4-2-real-time-text-available'),
210
+ p('2.4.4.3', 'Generated speech testing', 'content', 'x2-4-4-3-generated-speech-testing'),
211
+ g('2.4.5', 'Input operation'),
212
+ p('2.4.5.1', 'Hover or focus content dismissible', 'core', 'x2-4-5-1-hover-or-focus-content-dismissible'),
213
+ p('2.4.5.2', 'Hover content persistent', 'core', 'x2-4-5-2-hover-content-persistent'),
214
+ p('2.4.5.3', 'Hover or focus content persistent', 'supplemental', 'x2-4-5-3-hover-or-focus-content-persistent'),
215
+ p('2.4.5.4', 'Path-based gesture not relied on', 'core', 'x2-4-5-4-path-based-gesture-not-relied-on'),
216
+ p('2.4.5.5', 'Input method flexible', 'supplemental', 'x2-4-5-5-input-method-flexible'),
217
+ p('2.4.5.6', 'Body movements not relied on', 'core', 'x2-4-5-6-body-movements-not-relied-on'),
218
+ p('2.4.5.7', 'Eye tracking not relied on', 'core', 'x2-4-5-7-eye-tracking-not-relied-on'),
219
+ p('2.4.5.8', 'Pointer accessible', 'core', 'x2-4-5-8-pointer-accessible'),
220
+ g('2.4.6', 'Authentication'),
221
+ p('2.4.6.1', 'Biometrics not relied on', 'core', 'x2-4-6-1-biometrics-not-relied-on'),
222
+ p('2.4.6.2', 'Voice identification not relied on', 'core', 'x2-4-6-2-voice-identification-not-relied-on'),
223
+ g('2.5.1', 'Correct errors'),
224
+ p('2.5.1.1', 'Error notifications available', 'core', 'x2-5-1-1-error-notifications-available'),
225
+ p('2.5.1.2', 'Error suggestions provided', 'supplemental', 'x2-5-1-2-error-suggestions-provided'),
226
+ p('2.5.1.3', 'Errors indicated in multiple ways', 'supplemental', 'x2-5-1-3-errors-indicated-in-multiple-ways'),
227
+ p('2.5.1.4', 'Error messages persistent', 'supplemental', 'x2-5-1-4-error-messages-persistent'),
228
+ p('2.5.1.5', 'Errors associated', 'core', 'x2-5-1-5-errors-associated'),
229
+ p('2.5.1.6', 'Error messages collocated', 'supplemental', 'x2-5-1-6-error-messages-collocated'),
230
+ g('2.5.2', 'Prevent errors'),
231
+ p('2.5.2.1', 'Errors preventable', 'core', 'x2-5-2-1-errors-preventable'),
232
+ p('2.5.2.2', 'Submission status notified', 'supplemental', 'x2-5-2-2-submission-status-notified'),
233
+ p('2.5.2.3', 'Data entry validated', 'supplemental', 'x2-5-2-3-data-entry-validated'),
234
+ p('2.5.2.4', 'Error prevention review', 'content', 'x2-5-2-4-error-prevention-review'),
235
+ g('2.6.1', 'Avoid physical harm'),
236
+ p('2.6.1.1', 'No flashing over threshold', 'core', 'x2-6-1-1-no-flashing-over-threshold'),
237
+ p('2.6.1.2', 'No flashing over threshold (no exceptions)', 'supplemental', 'x2-6-1-2-no-flashing-over-threshold-no-exceptions'),
238
+ p('2.6.1.3', 'No visual motion', 'core', 'x2-6-1-3-no-visual-motion'),
239
+ p('2.6.1.4', 'No visual motion (no exceptions)', 'supplemental', 'x2-6-1-4-no-visual-motion-no-exceptions'),
240
+ p('2.6.1.5', 'Trigger warning available', 'core', 'x2-6-1-5-trigger-warning-available'),
241
+ p('2.6.1.6', 'Haptic stimulation adjustable', 'core', 'x2-6-1-6-haptic-stimulation-adjustable'),
242
+ p('2.6.1.7', 'Audio shifting adjustable', 'core', 'x2-6-1-7-audio-shifting-adjustable'),
243
+ p('2.6.1.8', 'Safe content review', 'content', 'x2-6-1-8-safe-content-review'),
244
+ g('2.7.1', 'Recognizable layouts'),
245
+ p('2.7.1.1', 'Conventional layout review', 'content', 'x2-7-1-1-conventional-layout-review'),
246
+ g('2.7.2', 'User orientation'),
247
+ p('2.7.2.1', 'Page/view title available', 'core', 'x2-7-2-1-page-view-title-available'),
248
+ p('2.7.2.2', 'Location within product review', 'content', 'x2-7-2-2-location-within-product-review'),
249
+ p('2.7.2.3', 'All steps listed', 'core', 'x2-7-2-3-all-steps-listed'),
250
+ p('2.7.2.4', 'Current step indicated', 'core', 'x2-7-2-4-current-step-indicated'),
251
+ p('2.7.2.5', 'Page/view change notified', 'core', 'x2-7-2-5-page-view-change-notified'),
252
+ p('2.7.2.6', 'Return to start supported', 'supplemental', 'x2-7-2-6-return-to-start-supported'),
253
+ g('2.7.3', 'Structure'),
254
+ p('2.7.3.1', 'Relationships detectable', 'supplemental', 'x2-7-3-1-relationships-detectable'),
255
+ p('2.7.3.2', 'Blocks of content available (minimum)', 'core', 'x2-7-3-2-blocks-of-content-available-minimum'),
256
+ p('2.7.3.3', 'Sections labeled', 'core', 'x2-7-3-3-sections-labeled'),
257
+ p('2.7.3.4', 'Heading structure available', 'supplemental', 'x2-7-3-4-heading-structure-available'),
258
+ p('2.7.3.5', 'Order detectable', 'core', 'x2-7-3-5-order-detectable'),
259
+ p('2.7.3.6', 'Blocks of content available (enhanced)', 'supplemental', 'x2-7-3-6-blocks-of-content-available-enhanced'),
260
+ p('2.7.3.7', 'Clear structure review', 'organization', 'x2-7-3-7-clear-structure-review'),
261
+ p('2.7.3.8', 'Key information usability testing', 'content', 'x2-7-3-8-key-information-usability-testing'),
262
+ g('2.7.4', 'No obstruction'),
263
+ p('2.7.4.1', 'Overlay content dismissible', 'core', 'x2-7-4-1-overlay-content-dismissible'),
264
+ g('2.8.1', 'Consistency'),
265
+ p('2.8.1.1', 'Consistent structural order', 'supplemental', 'x2-8-1-1-consistent-structural-order'),
266
+ p('2.8.1.2', 'Consistent navigation order', 'supplemental', 'x2-8-1-2-consistent-navigation-order'),
267
+ p('2.8.1.3', 'Consistent navigation labels', 'supplemental', 'x2-8-1-3-consistent-navigation-labels'),
268
+ g('2.9.1', 'Avoid exclusionary cognitive tasks'),
269
+ p('2.9.1.1', 'Automated entry allowed', 'core', 'x2-9-1-1-automated-entry-allowed'),
270
+ p('2.9.1.2', 'Cognitive test alternatives available', 'core', 'x2-9-1-2-cognitive-test-alternatives-available'),
271
+ g('2.9.2', 'Adequate time'),
272
+ p('2.9.2.1', 'Timeout adjustable', 'supplemental', 'x2-9-2-1-timeout-adjustable'),
273
+ p('2.9.2.2', 'No time limits', 'supplemental', 'x2-9-2-2-no-time-limits'),
274
+ p('2.9.2.3', 'No unnecessary time limits', 'content', 'x2-9-2-3-no-unnecessary-time-limits'),
275
+ p('2.9.2.4', 'Time limits conveyed', 'supplemental', 'x2-9-2-4-time-limits-conveyed'),
276
+ g('2.9.3', 'Avoid deception'),
277
+ p('2.9.3.1', 'Preselections visible', 'core', 'x2-9-3-1-preselections-visible'),
278
+ p('2.9.3.2', 'Deceptive practices usability testing', 'content', 'x2-9-3-2-deceptive-practices-usability-testing'),
279
+ p('2.9.3.3', 'Deceptive messaging expert review', 'content', 'x2-9-3-3-deceptive-messaging-expert-review'),
280
+ g('2.9.4', 'Retain information'),
281
+ p('2.9.4.1', 'Going back supported', 'supplemental', 'x2-9-4-1-going-back-supported'),
282
+ p('2.9.4.2', 'No redundant entry', 'supplemental', 'x2-9-4-2-no-redundant-entry'),
283
+ p('2.9.4.3', 'Progress saved', 'supplemental', 'x2-9-4-3-progress-saved'),
284
+ g('2.9.5', 'Complete tasks'),
285
+ p('2.9.5.1', 'Required action available', 'supplemental', 'x2-9-5-1-required-action-available'),
286
+ p('2.9.5.2', 'Information requirements available at start', 'supplemental', 'x2-9-5-2-information-requirements-available-at-start'),
287
+ p('2.9.5.3', 'Process instructions available', 'supplemental', 'x2-9-5-3-process-instructions-available'),
288
+ g('2.9.6', 'Unnecessary steps'),
289
+ p('2.9.6.1', 'Usability testing for unnecessary steps', 'content', 'x2-9-6-1-usability-testing-for-unnecessary-steps'),
290
+ g('2.10.1', 'Risk'),
291
+ p('2.10.1.1', 'Consequences of choices explained', 'core', 'x2-10-1-1-consequences-of-choices-explained'),
292
+ p('2.10.1.2', 'Consequences explained before agreement', 'supplemental', 'x2-10-1-2-consequences-explained-before-agreement'),
293
+ p('2.10.1.3', 'Diverse disabilities considered', 'content', 'x2-10-1-3-diverse-disabilities-considered'),
294
+ p('2.10.1.4', 'Algorithm inclusivity review', 'organization', 'x2-10-1-4-algorithm-inclusivity-review'),
295
+ g('2.10.2', 'Algorithms'),
296
+ p('2.10.2.1', 'Inclusive data set', 'content', 'x2-10-2-1-inclusive-data-set'),
297
+ p('2.10.2.2', 'No harm from algorithms', 'content', 'x2-10-2-2-no-harm-from-algorithms'),
298
+ g('2.11.1', 'Help available'),
299
+ p('2.11.1.1', 'Consistent help available', 'supplemental', 'x2-11-1-1-consistent-help-available'),
300
+ p('2.11.1.2', 'Contextual help available', 'supplemental', 'x2-11-1-2-contextual-help-available'),
301
+ p('2.11.1.3', 'Disabled controls explained', 'supplemental', 'x2-11-1-3-disabled-controls-explained'),
302
+ p('2.11.1.4', 'Sensory characteristics not relied on', 'core', 'x2-11-1-4-sensory-characteristics-not-relied-on'),
303
+ p('2.11.1.5', 'Supported decision-making review', 'content', 'x2-11-1-5-supported-decision-making-review'),
304
+ p('2.11.1.6', 'Help usability testing', 'content', 'x2-11-1-6-help-usability-testing'),
305
+ g('2.11.2', 'Feedback'),
306
+ p('2.11.2.1', 'Feedback mechanism available', 'supplemental', 'x2-11-2-1-feedback-mechanism-available'),
307
+ g('2.12.1', 'Assistive technology control'),
308
+ p('2.12.1.1', 'Assistive technology supported', 'core', 'x2-12-1-1-assistive-technology-supported'),
309
+ p('2.12.1.2', 'User settings supported', 'core', 'x2-12-1-2-user-settings-supported'),
310
+ p('2.12.1.3', 'Virtual cursor supported', 'core', 'x2-12-1-3-virtual-cursor-supported'),
311
+ p('2.12.1.4', 'Notifications adjustable', 'supplemental', 'x2-12-1-4-notifications-adjustable'),
312
+ g('2.12.3', 'Adjustable viewport'),
313
+ p('2.12.3.1', 'Orientation supported (minimum)', 'core', 'x2-12-3-1-orientation-supported-minimum'),
314
+ p('2.12.3.2', 'Orientation supported (enhanced)', 'supplemental', 'x2-12-3-2-orientation-supported-enhanced'),
315
+ p('2.12.3.3', 'Text reflow supported', 'supplemental', 'x2-12-3-3-text-reflow-supported'),
316
+ p('2.12.3.4', 'Layout reflow supported', 'supplemental', 'x2-12-3-4-layout-reflow-supported'),
317
+ g('2.12.4', 'Media control'),
318
+ p('2.12.4.1', 'Page/view audio adjustable', 'core', 'x2-12-4-1-page-view-audio-adjustable'),
319
+ p('2.12.4.2', 'Media alternatives searchable', 'supplemental', 'x2-12-4-2-media-alternatives-searchable'),
320
+ p('2.12.4.3', 'Media alternatives controllable', 'supplemental', 'x2-12-4-3-media-alternatives-controllable'),
321
+ p('2.12.4.4', 'Media chapters available', 'supplemental', 'x2-12-4-4-media-chapters-available'),
322
+ g('2.12.5', 'Content changes'),
323
+ p('2.12.5.1', 'Change of content notified', 'core', 'x2-12-5-1-change-of-content-notified'),
324
+ p('2.12.5.2', 'Change of focus notified', 'core', 'x2-12-5-2-change-of-focus-notified'),
325
+ p('2.12.5.3', 'Change of user agent notified', 'core', 'x2-12-5-3-change-of-user-agent-notified'),
326
+ ];
327
+
328
+ function g(num, name) { return { guideline: true, num, name }; }
329
+ function p(num, name, type, anchor) { return { num, name, type, anchor }; }
330
+
331
+ export const provisions = (() => {
332
+ const list = [];
333
+ let guideline = null;
334
+ for (const entry of RAW) {
335
+ if (entry.guideline) { guideline = entry; continue; }
336
+ list.push({
337
+ num: entry.num,
338
+ name: entry.name,
339
+ type: entry.type,
340
+ guideline: `${guideline.num} ${guideline.name}`,
341
+ url: `${draftInfo.url}#${entry.anchor}`,
342
+ });
343
+ }
344
+ return list;
345
+ })();
346
+
347
+ const provisionByNum = new Map(provisions.map((entry) => [entry.num, entry]));
348
+
349
+ // Rule id → the draft requirements its failure also fails.
350
+ export const RULE_REQUIREMENTS = {
351
+ 'area-alt': ['2.1.1.3', '2.3.5.2'],
352
+ 'canvas-alt': ['2.1.3.1'],
353
+ 'embed-alt': ['2.1.3.1'],
354
+ 'image-alt': ['2.1.1.3'],
355
+ 'input-image-alt': ['2.1.1.3', '2.3.5.2'],
356
+ 'object-alt': ['2.1.3.1'],
357
+ 'svg-img-alt': ['2.1.1.3'],
358
+ 'audio-transcript': ['2.1.4.2'],
359
+ 'media-captions': ['2.1.5.2'],
360
+ 'video-audio-description': ['2.1.6.1'],
361
+ 'aria-required-children': ['2.3.5.6'],
362
+ 'aria-required-parent': ['2.3.5.6'],
363
+ 'definition-list': ['2.7.3.1'],
364
+ 'dlitem-parent': ['2.7.3.1'],
365
+ 'form-label': ['2.3.5.2'],
366
+ 'list-structure': ['2.7.3.1'],
367
+ 'listitem-parent': ['2.7.3.1'],
368
+ 'p-as-heading': ['2.7.3.4'],
369
+ 'table-headers': ['2.7.3.1'],
370
+ // A lock to the platform's own default orientation meets 2.12.3.1; only
371
+ // the supplemental both-orientations requirement fails for every lock.
372
+ 'orientation-lock': ['2.12.3.2'],
373
+ 'region-purpose': ['2.7.3.3'],
374
+ 'link-in-text-block': ['2.1.8.1'],
375
+ 'audio-control': ['2.12.4.1'],
376
+ 'color-contrast': ['2.2.1.3'],
377
+ 'control-contrast': ['2.2.1.3'],
378
+ 'meta-viewport': ['2.2.1.6'],
379
+ 'color-contrast-enhanced': ['2.2.1.10'],
380
+ 'reflow': ['2.12.3.4'],
381
+ 'non-text-contrast': ['2.3.5.1'],
382
+ 'scrollable-region-focusable': ['2.4.1.2'],
383
+ 'meta-refresh': ['2.9.2.1'],
384
+ 'pause-stop-hide': ['2.6.1.3'],
385
+ 'video-loop-motion': ['2.6.1.3'],
386
+ 'meta-refresh-no-exceptions': ['2.9.2.2'],
387
+ 'reduced-motion': ['2.12.1.2'],
388
+ 'bypass-blocks': ['2.7.3.2'],
389
+ 'document-title': ['2.7.2.1'],
390
+ 'visual-order-divergence': ['2.3.3.3'],
391
+ 'link-name': ['2.3.5.2'],
392
+ 'link-text-generic': ['2.3.5.2'],
393
+ 'heading-label-placeholder': ['2.7.3.3', '2.3.5.2'],
394
+ 'focus-visible': ['2.3.1.2'],
395
+ 'section-heading': ['2.7.3.4'],
396
+ // Path-based gestures (2.4.5.4) and multipoint ones (2.4.3.2 lists them
397
+ // among complex pointer inputs).
398
+ 'pointer-gesture-alternative': ['2.4.5.4', '2.4.3.2'],
399
+ 'drag-alternative': ['2.4.3.2'],
400
+ 'html-lang': ['2.2.2.2'],
401
+ 'valid-lang-parts': ['2.2.2.2'],
402
+ 'error-message-linkage': ['2.5.1.5'],
403
+ 'financial-form-confirmation': ['2.5.2.1'],
404
+ 'redundant-entry': ['2.9.4.2'],
405
+ 'auth-field-obstruction': ['2.9.1.1'],
406
+ 'captcha-alternative': ['2.9.1.2'],
407
+ 'aria-allowed-attr': ['2.3.5.6'],
408
+ 'aria-attr-valid': ['2.3.5.6'],
409
+ 'aria-field-name': ['2.3.5.2'],
410
+ 'aria-hidden-focus': ['2.3.5.6'],
411
+ 'aria-state-unreachable': ['2.3.5.6'],
412
+ 'aria-valid-refs': ['2.3.5.6'],
413
+ 'button-name': ['2.3.5.2'],
414
+ 'composite-widget-name': ['2.3.5.2'],
415
+ 'frame-title': ['2.7.3.3'],
416
+ 'invoker-target': ['2.3.5.6'],
417
+ 'label-for-valid': ['2.3.5.2'],
418
+ 'nested-interactive': ['2.3.5.6'],
419
+ 'role-required-aria': ['2.3.5.6'],
420
+ 'summary-name': ['2.3.5.2'],
421
+ 'valid-role': ['2.3.5.6'],
422
+ };
423
+
424
+ // WCAG rules that stay out of a WCAG 3 audit, and why. Best-practice rules
425
+ // are not listed: they run in either standard when best practices are on.
426
+ export const UNMAPPED_RULES = {
427
+ 'reading-order-divergence': 'The draft has no requirement on reading sequence; focus order (2.3.3.3) is a different outcome.',
428
+ '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.',
429
+ 'text-justified': 'The draft asks that justification can be adjusted (2.2.1.4), not that text is unjustified by default.',
430
+ '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.',
431
+ '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).',
432
+ '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.',
433
+ 'focus-not-obscured': 'The draft has no focus-obscured requirement.',
434
+ 'focus-not-obscured-enhanced': 'The draft has no focus-obscured requirement.',
435
+ 'target-size': 'The draft has no target size requirement.',
436
+ 'target-size-enhanced': 'The draft has no target size requirement.',
437
+ '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.',
438
+ };
439
+
440
+ /** The WCAG 3 tier a tag list asks for: wcag3-bronze, -silver or -gold,
441
+ * the highest when several are present; null for a WCAG 2 audit. */
442
+ export function tierFromTags(tags) {
443
+ let found = null;
444
+ for (const tag of tags ?? []) {
445
+ const tier = /^wcag3-(bronze|silver|gold)$/.exec(tag)?.[1];
446
+ if (tier && (!found || TIERS.indexOf(tier) > TIERS.indexOf(found))) found = tier;
447
+ }
448
+ return found;
449
+ }
450
+
451
+ /** The draft requirements a rule speaks to, as { num, name, type, url }. */
452
+ export function requirementsForRule(ruleId) {
453
+ return (RULE_REQUIREMENTS[ruleId] ?? []).map((num) => {
454
+ const { name, type, url } = provisionByNum.get(num);
455
+ return { num, name, type: TYPE_LABELS[type], url };
456
+ });
457
+ }
458
+
459
+ /** Does the tier take in any requirement this rule tests? */
460
+ export function ruleInTier(ruleId, tier) {
461
+ const types = TIER_TYPES[tier];
462
+ return Boolean(types) && (RULE_REQUIREMENTS[ruleId] ?? [])
463
+ .some((num) => types.has(provisionByNum.get(num).type));
464
+ }
465
+
466
+ /**
467
+ * Rule selection shared by every runner. A tag list selects the rules that
468
+ * carry any of its tags; a WCAG 3 tier tag also selects the rules its
469
+ * requirements map to. An empty or absent list selects every rule.
470
+ */
471
+ export function ruleMatchesTags(rule, tags) {
472
+ if (!tags?.length) return true;
473
+ if (rule.tags.some((tag) => tags.includes(tag))) return true;
474
+ const tier = tierFromTags(tags);
475
+ return Boolean(tier) && ruleInTier(rule.id, tier);
476
+ }
477
+
478
+ /**
479
+ * The tier as a human checklist, shaped like the WCAG 2 manual-review
480
+ * entries: `level` carries the provision type and `principle` the
481
+ * guideline. Every provision in the tier is listed, since no rule was
482
+ * written for any of them; the ones a rule speaks to are 'partial'.
483
+ */
484
+ export function manualReviewChecklist(tier) {
485
+ const types = TIER_TYPES[tier];
486
+ if (!types) return [];
487
+ const partly = new Set(Object.values(RULE_REQUIREMENTS).flat());
488
+ return provisions
489
+ .filter((entry) => types.has(entry.type))
490
+ .map(({ num, name, type, guideline, url }) => ({
491
+ num,
492
+ name,
493
+ level: TYPE_LABELS[type],
494
+ principle: guideline,
495
+ url,
496
+ automation: partly.has(num) ? 'partial' : 'manual',
497
+ draft: true,
498
+ }));
499
+ }
500
+
501
+ /** The results header for a WCAG 3 audit. */
502
+ export function standardFor(tier) {
503
+ return { ...draftInfo, tier, tierLabel: TIER_LABELS[tier] };
504
+ }
505
+
506
+ export default {
507
+ draftInfo, TIERS, TIER_LABELS, provisions, RULE_REQUIREMENTS, UNMAPPED_RULES,
508
+ tierFromTags, requirementsForRule, ruleInTier, ruleMatchesTags, manualReviewChecklist, standardFor,
509
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pour-engine",
3
- "version": "1.44.1",
3
+ "version": "1.45.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",