pour-engine 1.4.0 → 1.5.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/config/project.config.js
CHANGED
package/engine/COVERAGE.md
CHANGED
|
@@ -62,12 +62,12 @@ meaning that only a person can settle, even where a rule decides most real pages
|
|
|
62
62
|
| 2.4.1 | [Bypass Blocks](https://www.w3.org/WAI/WCAG22/Understanding/bypass-blocks.html) | A | Partially automated | bypass-blocks |
|
|
63
63
|
| 2.4.2 | [Page Titled](https://www.w3.org/WAI/WCAG22/Understanding/page-titled.html) | A | Partially automated | document-title |
|
|
64
64
|
| 2.4.3 | [Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html) | A | Manual review | — |
|
|
65
|
-
| 2.4.4 | [Link Purpose (In Context)](https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html) | A | Partially automated | area-alt, link-name |
|
|
65
|
+
| 2.4.4 | [Link Purpose (In Context)](https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html) | A | Partially automated | area-alt, link-name, link-text-generic |
|
|
66
66
|
| 2.4.5 | [Multiple Ways](https://www.w3.org/WAI/WCAG22/Understanding/multiple-ways.html) | AA | Manual review | — |
|
|
67
67
|
| 2.4.6 | [Headings and Labels](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html) | AA | Manual review | — |
|
|
68
68
|
| 2.4.7 | [Focus Visible](https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html) | AA | Partially automated | focus-visible |
|
|
69
69
|
| 2.4.8 | [Location](https://www.w3.org/WAI/WCAG22/Understanding/location.html) | AAA | Manual review | — |
|
|
70
|
-
| 2.4.9 | [Link Purpose (Link Only)](https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-link-only.html) | AAA |
|
|
70
|
+
| 2.4.9 | [Link Purpose (Link Only)](https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-link-only.html) | AAA | Partially automated | link-text-generic-only |
|
|
71
71
|
| 2.4.10 | [Section Headings](https://www.w3.org/WAI/WCAG22/Understanding/section-headings.html) | AAA | Manual review | — |
|
|
72
72
|
| 2.4.11 | [Focus Not Obscured (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html) | AA | Partially automated | focus-not-obscured |
|
|
73
73
|
| 2.4.12 | [Focus Not Obscured (Enhanced)](https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-enhanced.html) | AAA | Manual review | — |
|
|
@@ -114,4 +114,4 @@ meaning that only a person can settle, even where a rule decides most real pages
|
|
|
114
114
|
| 4.1.2 | [Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) | A | Partially automated | input-image-alt, form-label, link-name, aria-allowed-attr, aria-attr-valid, aria-field-name, aria-hidden-focus, aria-valid-refs, button-name, dialog-name, frame-title, label-for-valid, nested-interactive, role-required-aria, valid-role |
|
|
115
115
|
| 4.1.3 | [Status Messages](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html) | AA | Manual review | — |
|
|
116
116
|
|
|
117
|
-
**Totals:**
|
|
117
|
+
**Totals:** 30 partially automated · 56 manual — with 74 rules.
|
package/engine/rules/index.js
CHANGED
|
@@ -15,6 +15,8 @@ import imageAlt from './wcag/1.1.1-image-alt.js';
|
|
|
15
15
|
import svgImgAlt from './wcag/1.1.1-svg-img-alt.js';
|
|
16
16
|
import buttonName from './wcag/4.1.2-button-name.js';
|
|
17
17
|
import linkName from './wcag/2.4.4-link-name.js';
|
|
18
|
+
import linkTextGeneric from './wcag/2.4.4-link-text-generic.js';
|
|
19
|
+
import linkTextGenericOnly from './wcag/2.4.9-link-text-generic-only.js';
|
|
18
20
|
// Re-parked by product decision (2026-08-01, David, after a one-day
|
|
19
21
|
// un-parking): 2.5.3 is normative Level A but chiefly serves speech-input
|
|
20
22
|
// users, and the findings volume isn't wanted in results for now. The SC is
|
|
@@ -113,6 +115,8 @@ export default [
|
|
|
113
115
|
svgImgAlt,
|
|
114
116
|
buttonName,
|
|
115
117
|
linkName,
|
|
118
|
+
linkTextGeneric,
|
|
119
|
+
linkTextGenericOnly,
|
|
116
120
|
// labelInName, — re-parked 2026-08-01, see note at the import
|
|
117
121
|
formLabel,
|
|
118
122
|
autocompleteValid,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// WCAG SC 2.4.4 Link Purpose (In Context) (Level A)
|
|
2
|
+
// · SC 2.4.9 Link Purpose (Link Only) (Level AAA)
|
|
3
|
+
//
|
|
4
|
+
// A link named "Read more" carries no purpose in its name. Screen reader
|
|
5
|
+
// users routinely pull up a list of every link on a page and navigate from
|
|
6
|
+
// it, and nine identical "Read more" entries in that list are worthless —
|
|
7
|
+
// the technique WCAG names for this is F63, and G91 is the fix.
|
|
8
|
+
//
|
|
9
|
+
// The two criteria differ in exactly one way, and it decides the verdict:
|
|
10
|
+
//
|
|
11
|
+
// 2.4.4 (A) purpose may come from CONTEXT — the sentence around the
|
|
12
|
+
// link, its list item, its table cell, the heading of the card
|
|
13
|
+
// it sits in. A "Read more" under a headline naming the
|
|
14
|
+
// article is a judgment call about whether that context really
|
|
15
|
+
// reaches the user, so this REVIEWS. It never asserts.
|
|
16
|
+
// 2.4.9 (AAA) purpose must come from the link text ALONE, with the only
|
|
17
|
+
// exception being links whose purpose "would be ambiguous to
|
|
18
|
+
// users in general". A link named nothing but "Read more" is
|
|
19
|
+
// ambiguous to everyone, which is the exception failing to
|
|
20
|
+
// apply rather than saving it, so this FAILS.
|
|
21
|
+
//
|
|
22
|
+
// Judged on the ACCESSIBLE NAME, not the text content: a link written as
|
|
23
|
+
// <a href="…" aria-label="Read more about the budget">Read more</a> is
|
|
24
|
+
// already fixed, and must not be flagged. That also means an aria-label
|
|
25
|
+
// which is itself generic is caught, which is the more common mistake.
|
|
26
|
+
//
|
|
27
|
+
// English only, deliberately. A phrase list cannot be quietly extended to
|
|
28
|
+
// other languages without a speaker of each checking it, and a wrong entry
|
|
29
|
+
// would fail correct links. Non-English pages get no findings here rather
|
|
30
|
+
// than bad ones.
|
|
31
|
+
|
|
32
|
+
const GENERIC = new Set([
|
|
33
|
+
'read more', 'read more about this', 'more', 'much more', 'learn more',
|
|
34
|
+
'click here', 'click', 'click this', 'tap here', 'press here',
|
|
35
|
+
'here', 'this', 'this link', 'link', 'this page', 'go', 'go here',
|
|
36
|
+
'more info', 'more information', 'further information',
|
|
37
|
+
'see more', 'view more', 'show more', 'find out more', 'discover more',
|
|
38
|
+
'continue', 'continue reading', 'keep reading', 'full story', 'full article',
|
|
39
|
+
'details', 'see details', 'view details', 'more details',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/** Lowercased, punctuation and arrows dropped, spaces collapsed, so
|
|
43
|
+
* "Read more →", "Click here!" and "READ MORE" all compare equal. */
|
|
44
|
+
export function normalizeName(name) {
|
|
45
|
+
return name.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').replace(/\s+/g, ' ').trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createLinkPurposeRule({ id, impact, tags, help, helpUrl, verdict }) {
|
|
49
|
+
return {
|
|
50
|
+
id,
|
|
51
|
+
impact,
|
|
52
|
+
tags,
|
|
53
|
+
help,
|
|
54
|
+
helpUrl,
|
|
55
|
+
// role="link" claims the link role explicitly; an <a> without href has no
|
|
56
|
+
// link role at all and is correctly out of scope. Links with NO name are
|
|
57
|
+
// link-name's finding, not this one.
|
|
58
|
+
selector: 'a[href], [role="link"]',
|
|
59
|
+
evaluate(element, { accessibleName }) {
|
|
60
|
+
const name = accessibleName(element);
|
|
61
|
+
if (!name) return { status: 'pass' }; // nameless: link-name reports it
|
|
62
|
+
const normalized = normalizeName(name);
|
|
63
|
+
if (!GENERIC.has(normalized)) return { status: 'pass' };
|
|
64
|
+
return verdict(name);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default createLinkPurposeRule({
|
|
70
|
+
id: 'link-text-generic',
|
|
71
|
+
impact: 'moderate',
|
|
72
|
+
tags: ['wcag2a', 'wcag244'],
|
|
73
|
+
help: 'Link text should describe where the link goes',
|
|
74
|
+
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html',
|
|
75
|
+
verdict: (name) => ({
|
|
76
|
+
status: 'incomplete',
|
|
77
|
+
message: `“${name}” does not say where this link goes. 2.4.4 lets the surrounding text supply that, so this passes only if the context a screen reader reaches — the same sentence, list item, table cell, or the heading of the card it sits in — makes the destination obvious. Check it does, and remember that anyone navigating by a list of the page's links sees this name on its own.`,
|
|
78
|
+
fix: 'Put the destination in the link text itself, e.g. "Read more about the 2026 budget", or extend the name with aria-label while keeping the visible words at the start of it.',
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// WCAG SC 2.4.9 Link Purpose (Link Only) (Level AAA)
|
|
2
|
+
//
|
|
3
|
+
// Same phrase list and same name computation as 2.4.4's rule, at the AAA
|
|
4
|
+
// threshold: the link text ALONE has to identify the purpose, and context
|
|
5
|
+
// is not allowed to make up the difference.
|
|
6
|
+
//
|
|
7
|
+
// This one asserts where the AA rule reviews. The criterion's only escape is
|
|
8
|
+
// links "where the purpose of the link would be ambiguous to users in
|
|
9
|
+
// general", and a link named nothing but "Read more" is ambiguous to
|
|
10
|
+
// everyone — which is the exception failing to apply rather than rescuing
|
|
11
|
+
// it. Nothing about the surrounding page can change that reading, so there
|
|
12
|
+
// is nothing here for a human to settle.
|
|
13
|
+
import { createLinkPurposeRule } from './2.4.4-link-text-generic.js';
|
|
14
|
+
|
|
15
|
+
export default createLinkPurposeRule({
|
|
16
|
+
id: 'link-text-generic-only',
|
|
17
|
+
impact: 'moderate',
|
|
18
|
+
tags: ['wcag2aaa', 'wcag249'],
|
|
19
|
+
help: 'Link text alone must identify where the link goes (AAA)',
|
|
20
|
+
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-link-only.html',
|
|
21
|
+
verdict: (name) => ({
|
|
22
|
+
status: 'fail',
|
|
23
|
+
message: `“${name}” gives no purpose on its own. 2.4.9 asks that the link text alone identify where a link goes, without relying on the sentence or heading around it, so anyone reading the page's links as a list can tell them apart.`,
|
|
24
|
+
fix: 'Name the destination in the link text, e.g. "Read more about the 2026 budget".',
|
|
25
|
+
}),
|
|
26
|
+
});
|
package/engine/wcag22.js
CHANGED
|
@@ -75,7 +75,7 @@ export default [
|
|
|
75
75
|
sc('2.4.6', 'Headings and Labels', 'AA', 'manual'),
|
|
76
76
|
sc('2.4.7', 'Focus Visible', 'AA', 'partial'), // partial: focus-visible flags outline suppression; the indicator itself needs eyes
|
|
77
77
|
sc('2.4.8', 'Location', 'AAA', 'manual'),
|
|
78
|
-
sc('2.4.9', 'Link Purpose (Link Only)', 'AAA', '
|
|
78
|
+
sc('2.4.9', 'Link Purpose (Link Only)', 'AAA', 'partial'), // partial: contentless link names are provable; whether a real name describes its destination needs a reader
|
|
79
79
|
sc('2.4.10', 'Section Headings', 'AAA', 'manual'),
|
|
80
80
|
sc('2.4.11', 'Focus Not Obscured (Minimum)', 'AA', 'partial', '2.2'), // partial: focus-not-obscured catches fully-covered targets at rest
|
|
81
81
|
sc('2.4.12', 'Focus Not Obscured (Enhanced)', 'AAA', 'manual', '2.2'),
|
package/package.json
CHANGED