@diegovelasquezweb/a11y-engine 0.11.40 → 0.11.42
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/assets/.DS_Store
ADDED
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export default {"patterns":[{"id":"placeholder-only-label","title":"Input uses placeholder as its only label","severity":"Critical","wcag":"WCAG 1.3.1 / 4.1.2 A","wcag_criterion":"1.3.1","wcag_level":"A","type":"structural","fix_description":"Placeholder text is not a label — it disappears on input and is not reliably announced by screen readers. Fix using one of these approaches:\n\n**Option A — Visually hidden label (preferred when no visible label exists):**\n```jsx\n<label htmlFor=\"field-id\" className=\"sr-only\">Field name</label>\n<input id=\"field-id\" placeholder=\"...\" />\n```\n\n**Option B — aria-label (acceptable for icon-only inputs like search):**\n```jsx\n<input aria-label=\"Search\" placeholder=\"Search\" />\n```\n\n**Option C — Visible label already exists nearby (wire it up):**\n```jsx\n<label htmlFor=\"field-id\">Field name</label>\n<input id=\"field-id\" placeholder=\"...\" />\n```\n\nNever rely on `placeholder` alone as the accessible name. Always verify the label is announced correctly with a screen reader.","requires_manual_verification":true,"regex":"\\bplaceholder=[\"']","globs":["**/*.tsx","**/*.jsx","**/*.html","**/*.vue","**/*.svelte","**/*.astro"],"context_reject_regex":"aria-label|aria-labelledby|for=|htmlFor=|<label","context_window":6},{"id":"mouseover-without-focus","title":"Hover handler has no keyboard focus equivalent","severity":"Serious","wcag":"WCAG 2.1.1 A","wcag_criterion":"2.1.1","wcag_level":"A","type":"structural","fix_description":"Add `onFocus`/`onBlur` (or `onFocusIn`/`onFocusOut`) handlers alongside every `onMouseOver`/`onMouseEnter` handler so keyboard users receive the same interaction.","requires_manual_verification":true,"regex":"onMouseOver=|onMouseEnter=","globs":["**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte","**/*.html"],"context_reject_regex":"onFocus[^I]|onFocusIn","context_window":10},{"id":"new-window-no-warning","title":"Link opens in new tab without warning","severity":"Serious","wcag":"WCAG 3.2.2 A","wcag_criterion":"3.2.2","wcag_level":"A","type":"structural","fix_description":"Add visible text such as `(opens in new tab)` or an `aria-label` containing that warning. Always pair `target=\"_blank\"` with `rel=\"noopener noreferrer\"`.","requires_manual_verification":true,"regex":"target=[\"']_blank[\"']","globs":["**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte","**/*.html","**/*.astro"],"context_reject_regex":"new.tab|new.window|opens.in|sr-only","context_window":5},{"id":"spa-route-title","title":"SPA navigation does not update document.title","severity":"Serious","wcag":"WCAG 2.4.2 A","wcag_criterion":"2.4.2","wcag_level":"A","type":"structural","fix_description":"Set `document.title` after every router navigation call to reflect the new route's page title. Screen reader users rely on the title to know when the page has changed.","requires_manual_verification":true,"regex":"router\\.push\\(|router\\.replace\\(|navigate\\(|useNavigate\\(","globs":["**/*.tsx","**/*.jsx","**/*.ts","**/*.js","**/*.vue"],"context_reject_regex":"document\\.title","context_window":20},{"id":"focus-outline-suppressed","title":"Focus outline suppressed without replacement","severity":"Serious","wcag":"WCAG 2.4.7 AA","wcag_criterion":"2.4.7","wcag_level":"AA","type":"style","fix_description":"Replace `outline: none` / `outline: 0` with a `:focus-visible` rule that renders a clearly visible custom focus indicator (e.g., a 2px solid high-contrast outline). Never suppress focus outlines globally.","requires_manual_verification":true,"regex":"outline:\\s*none|outline:\\s*0|focus:outline-none","globs":["**/*.css","**/*.scss","**/*.sass","**/*.tsx","**/*.jsx"],"context_reject_regex":":focus-visible","context_window":5},{"id":"orientation-lock","title":"Screen orientation locked programmatically","severity":"Moderate","wcag":"WCAG 1.3.4 AA","wcag_criterion":"1.3.4","wcag_level":"AA","type":"structural","fix_description":"Remove the programmatic orientation lock. If a specific orientation is essential for the content, provide an accessible alternative layout for the other orientation.","requires_manual_verification":false,"regex":"screen\\.orientation\\.lock\\(|lockOrientation\\(","globs":["**/*.ts","**/*.js","**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte"],"context_reject_regex":null,"context_window":0},{"id":"character-key-shortcut","title":"Single-character accesskey shortcut with no override mechanism","severity":"Moderate","wcag":"WCAG 2.1.4 A","wcag_criterion":"2.1.4","wcag_level":"A","type":"structural","fix_description":"Remove the `accesskey` attribute or provide a user-facing mechanism to remap or disable it. Single-character shortcuts conflict with screen reader and speech-input keystroke commands.","requires_manual_verification":false,"regex":"\\baccesskey=","globs":["**/*.html","**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte","**/*.astro"],"context_reject_regex":null,"context_window":0}]};
|
|
1
|
+
export default {"patterns":[{"id":"placeholder-only-label","title":"Input uses placeholder as its only label","severity":"Critical","wcag":"WCAG 1.3.1 / 4.1.2 A","wcag_criterion":"1.3.1","wcag_level":"A","type":"structural","fix_description":"Placeholder text is not a label — it disappears on input and is not reliably announced by screen readers. Fix using one of these approaches:\n\n**Option A — Visually hidden label (preferred when no visible label exists):**\n```jsx\n<label htmlFor=\"field-id\" className=\"sr-only\">Field name</label>\n<input id=\"field-id\" placeholder=\"...\" />\n```\n\n**Option B — aria-label (acceptable for icon-only inputs like search):**\n```jsx\n<input aria-label=\"Search\" placeholder=\"Search\" />\n```\n\n**Option C — Visible label already exists nearby (wire it up):**\n```jsx\n<label htmlFor=\"field-id\">Field name</label>\n<input id=\"field-id\" placeholder=\"...\" />\n```\n\nNever rely on `placeholder` alone as the accessible name. Always verify the label is announced correctly with a screen reader.","requires_manual_verification":true,"regex":"\\bplaceholder=[\"']","globs":["**/*.tsx","**/*.jsx","**/*.html","**/*.vue","**/*.svelte","**/*.astro"],"context_reject_regex":"aria-label|aria-labelledby|for=|htmlFor=|<label","context_window":6},{"id":"mouseover-without-focus","title":"Hover handler has no keyboard focus equivalent","severity":"Serious","wcag":"WCAG 2.1.1 A","wcag_criterion":"2.1.1","wcag_level":"A","type":"structural","fix_description":"Add `onFocus`/`onBlur` (or `onFocusIn`/`onFocusOut`) handlers alongside every `onMouseOver`/`onMouseEnter` handler so keyboard users receive the same interaction.","requires_manual_verification":true,"regex":"onMouseOver=|onMouseEnter=","globs":["**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte","**/*.html"],"context_reject_regex":"onFocus[^I]|onFocusIn","context_window":10},{"id":"new-window-no-warning","title":"Link opens in new tab without warning","severity":"Serious","wcag":"WCAG 3.2.2 A","wcag_criterion":"3.2.2","wcag_level":"A","type":"structural","fix_description":"Add visible text such as `(opens in new tab)` or an `aria-label` containing that warning. Always pair `target=\"_blank\"` with `rel=\"noopener noreferrer\"`.","requires_manual_verification":true,"regex":"target=[\"']_blank[\"']","globs":["**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte","**/*.html","**/*.astro"],"context_reject_regex":"new.tab|new.window|opens.in|sr-only","context_window":5},{"id":"spa-route-title","title":"SPA navigation does not update document.title","severity":"Serious","wcag":"WCAG 2.4.2 A","wcag_criterion":"2.4.2","wcag_level":"A","type":"structural","fix_description":"Set `document.title` after every router navigation call to reflect the new route's page title. Screen reader users rely on the title to know when the page has changed.","requires_manual_verification":true,"regex":"router\\.push\\(|router\\.replace\\(|navigate\\(|useNavigate\\(","globs":["**/*.tsx","**/*.jsx","**/*.ts","**/*.js","**/*.vue"],"context_reject_regex":"document\\.title","context_window":20},{"id":"focus-outline-suppressed","title":"Focus outline suppressed without replacement","severity":"Serious","wcag":"WCAG 2.4.7 AA","wcag_criterion":"2.4.7","wcag_level":"AA","type":"style","fix_description":"Replace `outline: none` / `outline: 0` with a `:focus-visible` rule that renders a clearly visible custom focus indicator (e.g., a 2px solid high-contrast outline). Never suppress focus outlines globally.","requires_manual_verification":true,"regex":"outline:\\s*none|outline:\\s*0|focus:outline-none","globs":["**/*.css","**/*.scss","**/*.sass","**/*.tsx","**/*.jsx","**/*.html","**/*.vue","**/*.svelte","**/*.astro"],"context_reject_regex":":focus-visible","context_window":5},{"id":"orientation-lock","title":"Screen orientation locked programmatically","severity":"Moderate","wcag":"WCAG 1.3.4 AA","wcag_criterion":"1.3.4","wcag_level":"AA","type":"structural","fix_description":"Remove the programmatic orientation lock. If a specific orientation is essential for the content, provide an accessible alternative layout for the other orientation.","requires_manual_verification":false,"regex":"screen\\.orientation\\.lock\\(|lockOrientation\\(","globs":["**/*.ts","**/*.js","**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte"],"context_reject_regex":null,"context_window":0},{"id":"character-key-shortcut","title":"Single-character accesskey shortcut with no override mechanism","severity":"Moderate","wcag":"WCAG 2.1.4 A","wcag_criterion":"2.1.4","wcag_level":"A","type":"structural","fix_description":"Remove the `accesskey` attribute or provide a user-facing mechanism to remap or disable it. Single-character shortcuts conflict with screen reader and speech-input keystroke commands.","requires_manual_verification":false,"regex":"\\baccesskey=","globs":["**/*.html","**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte","**/*.astro"],"context_reject_regex":null,"context_window":0}]};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegovelasquezweb/a11y-engine",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.42",
|
|
4
4
|
"description": "WCAG 2.2 accessibility audit engine — scanner, analyzer, and report builders",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,9 +32,6 @@
|
|
|
32
32
|
"CHANGELOG.md",
|
|
33
33
|
"LICENSE"
|
|
34
34
|
],
|
|
35
|
-
"scripts": {
|
|
36
|
-
"test": "vitest run"
|
|
37
|
-
},
|
|
38
35
|
"dependencies": {
|
|
39
36
|
"@axe-core/playwright": "^4.11.1",
|
|
40
37
|
"axe-core": "^4.11.1",
|
|
@@ -44,5 +41,7 @@
|
|
|
44
41
|
"devDependencies": {
|
|
45
42
|
"vitest": "^4.0.18"
|
|
46
43
|
},
|
|
47
|
-
"
|
|
48
|
-
|
|
44
|
+
"scripts": {
|
|
45
|
+
"test": "vitest run"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
File without changes
|
|
@@ -154,14 +154,43 @@ function buildPatternAiInput({ finding, candidate, projectHints }) {
|
|
|
154
154
|
// Extract the exact line(s) containing the pattern match so Claude has an
|
|
155
155
|
// unambiguous search anchor instead of inferring it from the full file.
|
|
156
156
|
const fileLines = candidate.content.split("\n");
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
157
|
+
const originalLine = typeof finding.line === "number" ? finding.line : null;
|
|
158
|
+
const rawMatch = typeof finding.match === "string" ? finding.match.trim() : "";
|
|
159
|
+
|
|
160
|
+
// rawMatch is often just the regex prefix (e.g. 'placeholder="') and is not unique
|
|
161
|
+
// when multiple elements match the same pattern in the same file.
|
|
162
|
+
// Use finding.context to build a more specific anchor so sequential fixes on the
|
|
163
|
+
// same file don't end up targeting an already-patched sibling element.
|
|
164
|
+
const contextLines = typeof finding.context === "string"
|
|
165
|
+
? finding.context.split("\n").map((l) => l.trim()).filter(Boolean)
|
|
166
|
+
: [];
|
|
167
|
+
const matchPrefix = rawMatch.slice(0, 30);
|
|
168
|
+
const contextAnchor = contextLines.reduce((best, line) => {
|
|
169
|
+
if (!matchPrefix || !line.includes(matchPrefix)) return best;
|
|
170
|
+
return !best || line.length > best.length ? line : best;
|
|
171
|
+
}, null);
|
|
172
|
+
const anchor = contextAnchor || matchPrefix;
|
|
173
|
+
|
|
174
|
+
// The file may have been modified by a previous sequential fix (lines shifted).
|
|
175
|
+
// Search for the actual current line containing the anchor instead of
|
|
176
|
+
// relying solely on the original line number from the scan.
|
|
177
|
+
let effectiveLineIndex = originalLine !== null ? originalLine - 1 : -1;
|
|
178
|
+
if (anchor && effectiveLineIndex >= 0) {
|
|
179
|
+
const lineAtOriginal = (fileLines[effectiveLineIndex] || "").trim();
|
|
180
|
+
if (!lineAtOriginal.includes(anchor)) {
|
|
181
|
+
const found = fileLines.findIndex((l) => l.trim().includes(anchor));
|
|
182
|
+
if (found !== -1) effectiveLineIndex = found;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const lineNumber = effectiveLineIndex >= 0 ? effectiveLineIndex + 1 : originalLine;
|
|
187
|
+
const matchLine = effectiveLineIndex >= 0 && effectiveLineIndex < fileLines.length
|
|
188
|
+
? fileLines[effectiveLineIndex]
|
|
160
189
|
: "";
|
|
161
190
|
|
|
162
191
|
// Widen context: include ±4 lines around the match line for multi-line elements.
|
|
163
|
-
const contextStart =
|
|
164
|
-
const contextEnd =
|
|
192
|
+
const contextStart = effectiveLineIndex >= 0 ? Math.max(0, effectiveLineIndex - 4) : 0;
|
|
193
|
+
const contextEnd = effectiveLineIndex >= 0 ? Math.min(fileLines.length, effectiveLineIndex + 5) : 0;
|
|
165
194
|
const surroundingLines = contextStart < contextEnd
|
|
166
195
|
? fileLines.slice(contextStart, contextEnd).join("\n")
|
|
167
196
|
: (finding.context || "");
|
|
File without changes
|
package/src/reports/html.mjs
CHANGED
|
File without changes
|