pour-engine 1.44.1 → 1.44.2

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.44.2',
10
10
  },
11
11
  };
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pour-engine",
3
- "version": "1.44.1",
3
+ "version": "1.44.2",
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",