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.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/config/project.config.js +11 -0
- package/engine/COVERAGE.md +115 -0
- package/engine/README.md +115 -0
- package/engine/index.js +258 -0
- package/engine/lib/accessible-name.js +126 -0
- package/engine/lib/contrast.js +628 -0
- package/engine/lib/dom.js +116 -0
- package/engine/lib/roles.js +125 -0
- package/engine/rules/best-practice/accesskeys.js +29 -0
- package/engine/rules/best-practice/aria-label-misuse.js +22 -0
- package/engine/rules/best-practice/button-type.js +15 -0
- package/engine/rules/best-practice/empty-heading.js +19 -0
- package/engine/rules/best-practice/fieldset-legend.js +35 -0
- package/engine/rules/best-practice/heading-order.js +30 -0
- package/engine/rules/best-practice/landmark-one-main.js +22 -0
- package/engine/rules/best-practice/landmark-top-level.js +46 -0
- package/engine/rules/best-practice/landmark-unique.js +84 -0
- package/engine/rules/best-practice/multiple-labels.js +23 -0
- package/engine/rules/best-practice/new-window-link.js +21 -0
- package/engine/rules/best-practice/no-autofocus.js +16 -0
- package/engine/rules/best-practice/page-heading-one.js +17 -0
- package/engine/rules/best-practice/positive-tabindex.js +17 -0
- package/engine/rules/best-practice/redundant-alt-phrase.js +22 -0
- package/engine/rules/best-practice/redundant-aria-label.js +18 -0
- package/engine/rules/best-practice/redundant-aria.js +32 -0
- package/engine/rules/best-practice/redundant-image-alt.js +20 -0
- package/engine/rules/best-practice/redundant-role.js +62 -0
- package/engine/rules/best-practice/redundant-tabindex.js +15 -0
- package/engine/rules/best-practice/region.js +73 -0
- package/engine/rules/index.js +179 -0
- package/engine/rules/wcag/1.1.1-area-alt.js +18 -0
- package/engine/rules/wcag/1.1.1-image-alt.js +48 -0
- package/engine/rules/wcag/1.1.1-input-image-alt.js +17 -0
- package/engine/rules/wcag/1.1.1-object-alt.js +17 -0
- package/engine/rules/wcag/1.1.1-svg-img-alt.js +34 -0
- package/engine/rules/wcag/1.2.2-media-captions.js +19 -0
- package/engine/rules/wcag/1.3.1-aria-required-children.js +66 -0
- package/engine/rules/wcag/1.3.1-aria-required-parent.js +49 -0
- package/engine/rules/wcag/1.3.1-definition-list.js +21 -0
- package/engine/rules/wcag/1.3.1-dlitem-parent.js +17 -0
- package/engine/rules/wcag/1.3.1-form-label.js +81 -0
- package/engine/rules/wcag/1.3.1-list-structure.js +59 -0
- package/engine/rules/wcag/1.3.1-listitem-parent.js +33 -0
- package/engine/rules/wcag/1.3.1-p-as-heading.js +25 -0
- package/engine/rules/wcag/1.3.1-table-headers.js +61 -0
- package/engine/rules/wcag/1.3.5-autocomplete-valid.js +38 -0
- package/engine/rules/wcag/1.4.1-link-in-text-block.js +74 -0
- package/engine/rules/wcag/1.4.10-reflow.js +25 -0
- package/engine/rules/wcag/1.4.11-non-text-contrast.js +109 -0
- package/engine/rules/wcag/1.4.12-text-spacing.js +71 -0
- package/engine/rules/wcag/1.4.2-audio-control.js +40 -0
- package/engine/rules/wcag/1.4.3-color-contrast.js +462 -0
- package/engine/rules/wcag/1.4.3-control-contrast.js +72 -0
- package/engine/rules/wcag/1.4.4-meta-viewport.js +22 -0
- package/engine/rules/wcag/1.4.6-contrast-enhanced.js +11 -0
- package/engine/rules/wcag/2.1.1-scrollable-region-focusable.js +48 -0
- package/engine/rules/wcag/2.2.1-meta-refresh.js +20 -0
- package/engine/rules/wcag/2.2.2-pause-stop-hide.js +25 -0
- package/engine/rules/wcag/2.4.1-bypass-blocks.js +36 -0
- package/engine/rules/wcag/2.4.11-focus-not-obscured.js +80 -0
- package/engine/rules/wcag/2.4.2-document-title.js +18 -0
- package/engine/rules/wcag/2.4.4-link-name.js +19 -0
- package/engine/rules/wcag/2.4.7-focus-visible.js +46 -0
- package/engine/rules/wcag/2.5.3-label-in-name.js +88 -0
- package/engine/rules/wcag/2.5.5-target-size-enhanced.js +14 -0
- package/engine/rules/wcag/2.5.8-target-size.js +159 -0
- package/engine/rules/wcag/3.1.1-html-lang.js +31 -0
- package/engine/rules/wcag/3.1.2-valid-lang-parts.js +20 -0
- package/engine/rules/wcag/3.3.8-auth-field-obstruction.js +37 -0
- package/engine/rules/wcag/4.1.2-aria-allowed-attr.js +44 -0
- package/engine/rules/wcag/4.1.2-aria-attr-valid.js +50 -0
- package/engine/rules/wcag/4.1.2-aria-field-name.js +54 -0
- package/engine/rules/wcag/4.1.2-aria-hidden-focus.js +48 -0
- package/engine/rules/wcag/4.1.2-aria-valid-refs.js +61 -0
- package/engine/rules/wcag/4.1.2-button-name.js +17 -0
- package/engine/rules/wcag/4.1.2-dialog-name.js +35 -0
- package/engine/rules/wcag/4.1.2-frame-title.js +17 -0
- package/engine/rules/wcag/4.1.2-label-for-valid.js +25 -0
- package/engine/rules/wcag/4.1.2-nested-interactive.js +30 -0
- package/engine/rules/wcag/4.1.2-role-required-aria.js +36 -0
- package/engine/rules/wcag/4.1.2-valid-role.js +52 -0
- package/engine/wcag22.js +116 -0
- package/engine/wcag30-draft.js +149 -0
- 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
|
+
}
|