pour-engine 1.1.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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/config/project.config.js +11 -0
  4. package/engine/COVERAGE.md +115 -0
  5. package/engine/README.md +115 -0
  6. package/engine/index.js +258 -0
  7. package/engine/lib/accessible-name.js +126 -0
  8. package/engine/lib/contrast.js +628 -0
  9. package/engine/lib/dom.js +116 -0
  10. package/engine/lib/roles.js +125 -0
  11. package/engine/rules/best-practice/accesskeys.js +29 -0
  12. package/engine/rules/best-practice/aria-label-misuse.js +22 -0
  13. package/engine/rules/best-practice/button-type.js +15 -0
  14. package/engine/rules/best-practice/empty-heading.js +19 -0
  15. package/engine/rules/best-practice/fieldset-legend.js +35 -0
  16. package/engine/rules/best-practice/heading-order.js +30 -0
  17. package/engine/rules/best-practice/landmark-one-main.js +22 -0
  18. package/engine/rules/best-practice/landmark-top-level.js +46 -0
  19. package/engine/rules/best-practice/landmark-unique.js +84 -0
  20. package/engine/rules/best-practice/multiple-labels.js +23 -0
  21. package/engine/rules/best-practice/new-window-link.js +21 -0
  22. package/engine/rules/best-practice/no-autofocus.js +16 -0
  23. package/engine/rules/best-practice/page-heading-one.js +17 -0
  24. package/engine/rules/best-practice/positive-tabindex.js +17 -0
  25. package/engine/rules/best-practice/redundant-alt-phrase.js +22 -0
  26. package/engine/rules/best-practice/redundant-aria-label.js +18 -0
  27. package/engine/rules/best-practice/redundant-aria.js +32 -0
  28. package/engine/rules/best-practice/redundant-image-alt.js +20 -0
  29. package/engine/rules/best-practice/redundant-role.js +62 -0
  30. package/engine/rules/best-practice/redundant-tabindex.js +15 -0
  31. package/engine/rules/best-practice/region.js +73 -0
  32. package/engine/rules/index.js +179 -0
  33. package/engine/rules/wcag/1.1.1-area-alt.js +18 -0
  34. package/engine/rules/wcag/1.1.1-image-alt.js +48 -0
  35. package/engine/rules/wcag/1.1.1-input-image-alt.js +17 -0
  36. package/engine/rules/wcag/1.1.1-object-alt.js +17 -0
  37. package/engine/rules/wcag/1.1.1-svg-img-alt.js +34 -0
  38. package/engine/rules/wcag/1.2.2-media-captions.js +19 -0
  39. package/engine/rules/wcag/1.3.1-aria-required-children.js +66 -0
  40. package/engine/rules/wcag/1.3.1-aria-required-parent.js +49 -0
  41. package/engine/rules/wcag/1.3.1-definition-list.js +21 -0
  42. package/engine/rules/wcag/1.3.1-dlitem-parent.js +17 -0
  43. package/engine/rules/wcag/1.3.1-form-label.js +81 -0
  44. package/engine/rules/wcag/1.3.1-list-structure.js +59 -0
  45. package/engine/rules/wcag/1.3.1-listitem-parent.js +33 -0
  46. package/engine/rules/wcag/1.3.1-p-as-heading.js +25 -0
  47. package/engine/rules/wcag/1.3.1-table-headers.js +61 -0
  48. package/engine/rules/wcag/1.3.5-autocomplete-valid.js +38 -0
  49. package/engine/rules/wcag/1.4.1-link-in-text-block.js +74 -0
  50. package/engine/rules/wcag/1.4.10-reflow.js +25 -0
  51. package/engine/rules/wcag/1.4.11-non-text-contrast.js +109 -0
  52. package/engine/rules/wcag/1.4.12-text-spacing.js +71 -0
  53. package/engine/rules/wcag/1.4.2-audio-control.js +40 -0
  54. package/engine/rules/wcag/1.4.3-color-contrast.js +462 -0
  55. package/engine/rules/wcag/1.4.3-control-contrast.js +72 -0
  56. package/engine/rules/wcag/1.4.4-meta-viewport.js +22 -0
  57. package/engine/rules/wcag/1.4.6-contrast-enhanced.js +11 -0
  58. package/engine/rules/wcag/2.1.1-scrollable-region-focusable.js +48 -0
  59. package/engine/rules/wcag/2.2.1-meta-refresh.js +20 -0
  60. package/engine/rules/wcag/2.2.2-pause-stop-hide.js +25 -0
  61. package/engine/rules/wcag/2.4.1-bypass-blocks.js +36 -0
  62. package/engine/rules/wcag/2.4.11-focus-not-obscured.js +80 -0
  63. package/engine/rules/wcag/2.4.2-document-title.js +18 -0
  64. package/engine/rules/wcag/2.4.4-link-name.js +19 -0
  65. package/engine/rules/wcag/2.4.7-focus-visible.js +46 -0
  66. package/engine/rules/wcag/2.5.3-label-in-name.js +88 -0
  67. package/engine/rules/wcag/2.5.5-target-size-enhanced.js +14 -0
  68. package/engine/rules/wcag/2.5.8-target-size.js +159 -0
  69. package/engine/rules/wcag/3.1.1-html-lang.js +31 -0
  70. package/engine/rules/wcag/3.1.2-valid-lang-parts.js +20 -0
  71. package/engine/rules/wcag/3.3.8-auth-field-obstruction.js +37 -0
  72. package/engine/rules/wcag/4.1.2-aria-allowed-attr.js +44 -0
  73. package/engine/rules/wcag/4.1.2-aria-attr-valid.js +50 -0
  74. package/engine/rules/wcag/4.1.2-aria-field-name.js +54 -0
  75. package/engine/rules/wcag/4.1.2-aria-hidden-focus.js +48 -0
  76. package/engine/rules/wcag/4.1.2-aria-valid-refs.js +61 -0
  77. package/engine/rules/wcag/4.1.2-button-name.js +17 -0
  78. package/engine/rules/wcag/4.1.2-dialog-name.js +35 -0
  79. package/engine/rules/wcag/4.1.2-frame-title.js +17 -0
  80. package/engine/rules/wcag/4.1.2-label-for-valid.js +25 -0
  81. package/engine/rules/wcag/4.1.2-nested-interactive.js +30 -0
  82. package/engine/rules/wcag/4.1.2-role-required-aria.js +36 -0
  83. package/engine/rules/wcag/4.1.2-valid-role.js +52 -0
  84. package/engine/wcag22.js +116 -0
  85. package/engine/wcag30-draft.js +149 -0
  86. package/package.json +34 -0
@@ -0,0 +1,126 @@
1
+ // Simplified accessible-name computation, following the priority order of
2
+ // the ARIA accname spec: aria-labelledby → aria-label → native labelling
3
+ // (alt, <label>, text content, value) → title/placeholder fallbacks.
4
+ //
5
+ // Deliberately simpler than the full spec (no recursion into referenced
6
+ // hidden subtrees, no CSS content). Good enough for name-presence rules;
7
+ // grow it as rules need more.
8
+
9
+ export function accessibleName(element) {
10
+ const labelledby = element.getAttribute('aria-labelledby');
11
+ if (labelledby) {
12
+ const text = labelledby
13
+ .split(/\s+/)
14
+ .map((id) => element.getRootNode().getElementById?.(id)?.textContent ?? '')
15
+ .join(' ')
16
+ .trim();
17
+ if (text) return text;
18
+ }
19
+
20
+ const ariaLabel = element.getAttribute('aria-label')?.trim();
21
+ if (ariaLabel) return ariaLabel;
22
+
23
+ const tag = element.tagName.toLowerCase();
24
+
25
+ if (tag === 'img' || tag === 'area') {
26
+ const alt = element.getAttribute('alt')?.trim();
27
+ if (alt) return alt;
28
+ }
29
+
30
+ if (tag === 'input' || tag === 'select' || tag === 'textarea') {
31
+ if (element.labels?.length) {
32
+ const text = [...element.labels].map((label) => label.textContent).join(' ').trim();
33
+ if (text) return text;
34
+ }
35
+ if (element.type === 'submit' || element.type === 'reset' || element.type === 'button') {
36
+ // The value PROPERTY, not the attribute: scripts set `el.value`
37
+ // without reflecting it, and the property is what names the button.
38
+ const value = (element.value ?? element.getAttribute('value') ?? '').trim();
39
+ if (value) return value;
40
+ }
41
+ if (element.type === 'image') {
42
+ const alt = element.getAttribute('alt')?.trim();
43
+ if (alt) return alt;
44
+ }
45
+ // HTML-AAM: value-less submit/reset buttons get a UA-default name —
46
+ // browsers announce "Submit"/"Reset button" without any author text.
47
+ if (element.type === 'submit') return 'Submit';
48
+ if (element.type === 'reset') return 'Reset';
49
+ }
50
+
51
+ // Buttons, links, headings etc. — name from contents, including image
52
+ // alts, but EXCLUDING hidden content: text inside aria-hidden or
53
+ // display:none/visibility:hidden subtrees does not name an element
54
+ // (per the accname spec).
55
+ const fromContents = visibleContentText(element).replace(/\s+/g, ' ').trim();
56
+ if (fromContents) return fromContents;
57
+
58
+ return (element.getAttribute('title') ?? element.getAttribute('placeholder') ?? '').trim();
59
+ }
60
+
61
+ function visibleContentText(element) {
62
+ // A custom element with a shadow root renders its shadow content — name
63
+ // from contents follows the FLAT tree (slots inside pull light DOM back).
64
+ const nodes = element.shadowRoot ? element.shadowRoot.childNodes : element.childNodes;
65
+ return generatedContent(element, '::before') + textFromNodes(nodes) + generatedContent(element, '::after');
66
+ }
67
+
68
+ /** CSS generated content contributes to name-from-contents per the accname
69
+ * spec — icon-font prefixes and content:"…" labels are real names. Only
70
+ * plain strings count: counters, url(), attr() are not resolvable here. */
71
+ function generatedContent(element, pseudo) {
72
+ const content = getComputedStyle(element, pseudo).content;
73
+ if (!content || content === 'none' || content === 'normal') return '';
74
+ const match = content.match(/^"((?:[^"\\]|\\.)*)"$/);
75
+ return match ? match[1].replace(/\\(.)/g, '$1') : '';
76
+ }
77
+
78
+ function textFromNodes(nodes) {
79
+ let text = '';
80
+ for (const node of nodes) {
81
+ if (node.nodeType === 3 /* TEXT_NODE */) {
82
+ text += node.textContent;
83
+ continue;
84
+ }
85
+ if (node.nodeType !== 1 /* ELEMENT_NODE */) continue;
86
+ const tag = node.tagName.toLowerCase();
87
+ // Never-rendered content can't name anything. Checked by tag, not
88
+ // computed style: noscript contents are parsed as raw TEXT when JS is
89
+ // on, and sites can override the UA's display:none (seen in the wild
90
+ // with lazy-load <noscript> fallbacks leaking "<img …>" into names).
91
+ if (tag === 'script' || tag === 'style' || tag === 'noscript' || tag === 'template') continue;
92
+ if (node.getAttribute('aria-hidden') === 'true' || node.hasAttribute('hidden')) continue;
93
+ const style = getComputedStyle(node);
94
+ if (style.display === 'none' || style.visibility === 'hidden') continue;
95
+ // A slot renders its assigned light-DOM nodes (fallback children when
96
+ // nothing is slotted) — a shadow <button><slot></slot></button> is named
97
+ // by the text the page slots in.
98
+ if (tag === 'slot') {
99
+ const assigned = node.assignedNodes?.() ?? [];
100
+ text += textFromNodes(assigned.length ? assigned : node.childNodes);
101
+ continue;
102
+ }
103
+ if (tag === 'img' || tag === 'area') {
104
+ text += ` ${node.getAttribute('alt') ?? ''} `;
105
+ continue;
106
+ }
107
+ const ariaLabel = node.getAttribute('aria-label')?.trim();
108
+ if (ariaLabel) {
109
+ text += ` ${ariaLabel} `;
110
+ continue;
111
+ }
112
+ // Flat-tree descent: shadow content renders in place of a host's
113
+ // light children (archive.org-style nested web components).
114
+ const fromSubtree = textFromNodes(node.shadowRoot ? node.shadowRoot.childNodes : node.childNodes);
115
+ if (fromSubtree.trim()) {
116
+ text += fromSubtree;
117
+ continue;
118
+ }
119
+ // Accname's tooltip fallback applies per traversed element: a
120
+ // text-less <div title="upvote"> inside a link names it "upvote"
121
+ // (browsers and screen readers both honour this).
122
+ const title = node.getAttribute('title')?.trim();
123
+ if (title) text += ` ${title} `;
124
+ }
125
+ return text;
126
+ }