@a11yfred/neighbor 0.2.0 → 1.0.1
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/CHANGELOG.md +80 -3
- package/CONTRIBUTING.md +115 -0
- package/README.md +174 -77
- package/RULES-CONTENT.md +296 -0
- package/RULES-CSS.md +61 -0
- package/RULES-MARKUP.md +156 -0
- package/RULES.md +55 -0
- package/lib/content-rules.js +858 -0
- package/lib/helpers-angular.js +146 -146
- package/lib/helpers-jsx.js +193 -193
- package/lib/helpers-vue.js +151 -151
- package/lib/helpers.js +37 -37
- package/lib/rules.js +2413 -2411
- package/lib/ulam-rules.js +301 -301
- package/neighbor-content.mjs +80 -0
- package/neighbor-eslint-angular.mjs +68 -68
- package/neighbor-eslint-vue.mjs +48 -48
- package/neighbor-eslint.mjs +56 -56
- package/neighbor-stylelint.mjs +257 -196
- package/package.json +18 -5
package/lib/helpers-angular.js
CHANGED
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* neighbor/lib/helpers-angular.js
|
|
3
|
-
* Helpers for Angular template AST via @angular-eslint/template-parser.
|
|
4
|
-
*
|
|
5
|
-
* Parser: @angular-eslint/eslint-plugin-template provides its own rule format.
|
|
6
|
-
* Node type for elements: 'Element$1' (from @angular-eslint/template-parser, the class is
|
|
7
|
-
* TmplAstElement). The node has:
|
|
8
|
-
* node.name
|
|
9
|
-
* node.attributes
|
|
10
|
-
* node.inputs
|
|
11
|
-
* node.children
|
|
12
|
-
*
|
|
13
|
-
* We only inspect static (non-bound) attributes for ARIA checks.
|
|
14
|
-
* Bound attributes ([aria-label]="expr") cannot be statically analyzed.
|
|
15
|
-
*
|
|
16
|
-
* Angular ESLint rule visitor uses standard ESLint `create(context)` but the
|
|
17
|
-
* parser emits different node types. The visitor key from @angular-eslint is
|
|
18
|
-
* 'Element$1' (the internal class name exposed via the parser).
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
INTERACTIVE_ELEMENTS,
|
|
23
|
-
INTERACTIVE_ROLES,
|
|
24
|
-
} from './helpers.js'
|
|
25
|
-
|
|
26
|
-
/** Find a static TmplAstTextAttribute by name. */
|
|
27
|
-
export function getAttr(tmplElement, name) {
|
|
28
|
-
return (tmplElement.attributes ?? []).find(a => a.name === name) ?? null
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** TmplAstTextAttribute.value is a plain string. */
|
|
32
|
-
export function getAttrStringValue(attr) {
|
|
33
|
-
if (!attr) return null
|
|
34
|
-
return typeof attr.value === 'string' ? attr.value || null : null
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function getElementName(tmplElement) {
|
|
38
|
-
const raw = tmplElement.name ?? ''
|
|
39
|
-
if (!raw) return null
|
|
40
|
-
// Angular custom component selectors are typically kebab-case (app-button),
|
|
41
|
-
// but guard against PascalCase as well to avoid native element name collisions.
|
|
42
|
-
if (raw[0] !== raw[0].toLowerCase()) return null
|
|
43
|
-
return raw.toLowerCase()
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function hasAttr(tmplElement, name) {
|
|
47
|
-
return getAttr(tmplElement, name) !== null
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function getRoleValue(tmplElement) {
|
|
51
|
-
return getAttrStringValue(getAttr(tmplElement, 'role'))
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function hasAccessibleName(tmplElement) {
|
|
55
|
-
return hasAttr(tmplElement, 'aria-label') || hasAttr(tmplElement, 'aria-labelledby')
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function isInteractiveElement(tmplElement) {
|
|
59
|
-
const el = getElementName(tmplElement)
|
|
60
|
-
if (el && INTERACTIVE_ELEMENTS.has(el)) return true
|
|
61
|
-
const role = getRoleValue(tmplElement)
|
|
62
|
-
return !!(role && INTERACTIVE_ROLES.has(role))
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function hasOnlyHiddenChildren(tmplElement) {
|
|
66
|
-
const children = tmplElement.children ?? []
|
|
67
|
-
if (children.length === 0) return false
|
|
68
|
-
return children.every(child => {
|
|
69
|
-
if (child.constructor?.name === 'TmplAstText') return (child.value ?? '').trim() === ''
|
|
70
|
-
if (child.constructor?.name === 'TmplAstElement') {
|
|
71
|
-
return (child.attributes ?? []).some(a => a.name === 'aria-hidden')
|
|
72
|
-
}
|
|
73
|
-
return false
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const NEW_TAB_PATTERN = /new.tab|new.window|opens in/i
|
|
78
|
-
|
|
79
|
-
function collectAngularText(node) {
|
|
80
|
-
if (!node) return ''
|
|
81
|
-
if (node.constructor?.name === 'TmplAstText') return node.value ?? ''
|
|
82
|
-
if (node.constructor?.name === 'TmplAstElement') return (node.children ?? []).map(collectAngularText).join('')
|
|
83
|
-
return ''
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function hasNewTabWarning(tmplElement) {
|
|
87
|
-
const labelVal = (getAttrStringValue(getAttr(tmplElement, 'aria-label')) ?? '').toLowerCase()
|
|
88
|
-
if (NEW_TAB_PATTERN.test(labelVal)) return true
|
|
89
|
-
const childText = (tmplElement.children ?? []).map(collectAngularText).join('')
|
|
90
|
-
return NEW_TAB_PATTERN.test(childText)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function getParent(tmplElement) {
|
|
94
|
-
// @angular-eslint/template-parser does not set parent on AST nodes
|
|
95
|
-
// ancestor walking is not available without tracking the stack ourselves.
|
|
96
|
-
// Rules needing ancestor traversal are skipped for Angular.
|
|
97
|
-
return null
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function* getAncestors(_tmplElement) { /* not available */ }
|
|
101
|
-
|
|
102
|
-
export function* getChildOpeningElements(tmplElement) {
|
|
103
|
-
for (const child of tmplElement.children ?? []) {
|
|
104
|
-
if (child.constructor?.name === 'TmplAstElement') yield child
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function getClassName(tmplElement) {
|
|
109
|
-
return getAttrStringValue(getAttr(tmplElement, 'class'))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Returns the [innerHTML] bound input node if present, else null.
|
|
114
|
-
* In @angular-eslint/template-parser, property bindings live on node.inputs
|
|
115
|
-
* as TmplAstBoundAttribute nodes with { name: 'innerHTML' }.
|
|
116
|
-
*/
|
|
117
|
-
export function getInnerHtmlAttr(tmplElement) {
|
|
118
|
-
return (tmplElement.inputs ?? []).find(i => i.name === 'innerHTML') ?? null
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function getInnerHtmlAttrName(_tmplElement) {
|
|
122
|
-
return '[innerHTML]'
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export const h = {
|
|
126
|
-
getAttr,
|
|
127
|
-
getAttrStringValue,
|
|
128
|
-
getElementName,
|
|
129
|
-
hasAttr,
|
|
130
|
-
getRoleValue,
|
|
131
|
-
hasAccessibleName,
|
|
132
|
-
isInteractiveElement,
|
|
133
|
-
hasOnlyHiddenChildren,
|
|
134
|
-
hasNewTabWarning,
|
|
135
|
-
getParent,
|
|
136
|
-
getAncestors,
|
|
137
|
-
getChildOpeningElements,
|
|
138
|
-
getClassName,
|
|
139
|
-
getInnerHtmlAttr,
|
|
140
|
-
getInnerHtmlAttrName,
|
|
141
|
-
elementVisitor: 'Element$1',
|
|
142
|
-
elementWithChildrenVisitor: 'Element$1',
|
|
143
|
-
getOpeningElement: (node) => node,
|
|
144
|
-
getChildOpeningElementsFromWrapper: (node) =>
|
|
145
|
-
(node.children ?? []).filter(c => c.constructor?.name === 'TmplAstElement'),
|
|
146
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* neighbor/lib/helpers-angular.js
|
|
3
|
+
* Helpers for Angular template AST via @angular-eslint/template-parser.
|
|
4
|
+
*
|
|
5
|
+
* Parser: @angular-eslint/eslint-plugin-template provides its own rule format.
|
|
6
|
+
* Node type for elements: 'Element$1' (from @angular-eslint/template-parser, the class is
|
|
7
|
+
* TmplAstElement). The node has:
|
|
8
|
+
* node.name - tag name string
|
|
9
|
+
* node.attributes - TmplAstTextAttribute[] (static attrs)
|
|
10
|
+
* node.inputs - TmplAstBoundAttribute[] (property bindings)
|
|
11
|
+
* node.children - TmplAstNode[]
|
|
12
|
+
*
|
|
13
|
+
* We only inspect static (non-bound) attributes for ARIA checks.
|
|
14
|
+
* Bound attributes ([aria-label]="expr") cannot be statically analyzed.
|
|
15
|
+
*
|
|
16
|
+
* Angular ESLint rule visitor uses standard ESLint `create(context)` but the
|
|
17
|
+
* parser emits different node types. The visitor key from @angular-eslint is
|
|
18
|
+
* 'Element$1' (the internal class name exposed via the parser).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
INTERACTIVE_ELEMENTS,
|
|
23
|
+
INTERACTIVE_ROLES,
|
|
24
|
+
} from './helpers.js'
|
|
25
|
+
|
|
26
|
+
/** Find a static TmplAstTextAttribute by name. */
|
|
27
|
+
export function getAttr(tmplElement, name) {
|
|
28
|
+
return (tmplElement.attributes ?? []).find(a => a.name === name) ?? null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** TmplAstTextAttribute.value is a plain string. */
|
|
32
|
+
export function getAttrStringValue(attr) {
|
|
33
|
+
if (!attr) return null
|
|
34
|
+
return typeof attr.value === 'string' ? attr.value || null : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getElementName(tmplElement) {
|
|
38
|
+
const raw = tmplElement.name ?? ''
|
|
39
|
+
if (!raw) return null
|
|
40
|
+
// Angular custom component selectors are typically kebab-case (app-button),
|
|
41
|
+
// but guard against PascalCase as well to avoid native element name collisions.
|
|
42
|
+
if (raw[0] !== raw[0].toLowerCase()) return null
|
|
43
|
+
return raw.toLowerCase()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function hasAttr(tmplElement, name) {
|
|
47
|
+
return getAttr(tmplElement, name) !== null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getRoleValue(tmplElement) {
|
|
51
|
+
return getAttrStringValue(getAttr(tmplElement, 'role'))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function hasAccessibleName(tmplElement) {
|
|
55
|
+
return hasAttr(tmplElement, 'aria-label') || hasAttr(tmplElement, 'aria-labelledby')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isInteractiveElement(tmplElement) {
|
|
59
|
+
const el = getElementName(tmplElement)
|
|
60
|
+
if (el && INTERACTIVE_ELEMENTS.has(el)) return true
|
|
61
|
+
const role = getRoleValue(tmplElement)
|
|
62
|
+
return !!(role && INTERACTIVE_ROLES.has(role))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function hasOnlyHiddenChildren(tmplElement) {
|
|
66
|
+
const children = tmplElement.children ?? []
|
|
67
|
+
if (children.length === 0) return false
|
|
68
|
+
return children.every(child => {
|
|
69
|
+
if (child.constructor?.name === 'TmplAstText') return (child.value ?? '').trim() === ''
|
|
70
|
+
if (child.constructor?.name === 'TmplAstElement') {
|
|
71
|
+
return (child.attributes ?? []).some(a => a.name === 'aria-hidden')
|
|
72
|
+
}
|
|
73
|
+
return false
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const NEW_TAB_PATTERN = /new.tab|new.window|opens in/i
|
|
78
|
+
|
|
79
|
+
function collectAngularText(node) {
|
|
80
|
+
if (!node) return ''
|
|
81
|
+
if (node.constructor?.name === 'TmplAstText') return node.value ?? ''
|
|
82
|
+
if (node.constructor?.name === 'TmplAstElement') return (node.children ?? []).map(collectAngularText).join('')
|
|
83
|
+
return ''
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function hasNewTabWarning(tmplElement) {
|
|
87
|
+
const labelVal = (getAttrStringValue(getAttr(tmplElement, 'aria-label')) ?? '').toLowerCase()
|
|
88
|
+
if (NEW_TAB_PATTERN.test(labelVal)) return true
|
|
89
|
+
const childText = (tmplElement.children ?? []).map(collectAngularText).join('')
|
|
90
|
+
return NEW_TAB_PATTERN.test(childText)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getParent(tmplElement) {
|
|
94
|
+
// @angular-eslint/template-parser does not set parent on AST nodes -
|
|
95
|
+
// ancestor walking is not available without tracking the stack ourselves.
|
|
96
|
+
// Rules needing ancestor traversal are skipped for Angular.
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function* getAncestors(_tmplElement) { /* not available */ }
|
|
101
|
+
|
|
102
|
+
export function* getChildOpeningElements(tmplElement) {
|
|
103
|
+
for (const child of tmplElement.children ?? []) {
|
|
104
|
+
if (child.constructor?.name === 'TmplAstElement') yield child
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getClassName(tmplElement) {
|
|
109
|
+
return getAttrStringValue(getAttr(tmplElement, 'class'))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns the [innerHTML] bound input node if present, else null.
|
|
114
|
+
* In @angular-eslint/template-parser, property bindings live on node.inputs
|
|
115
|
+
* as TmplAstBoundAttribute nodes with { name: 'innerHTML' }.
|
|
116
|
+
*/
|
|
117
|
+
export function getInnerHtmlAttr(tmplElement) {
|
|
118
|
+
return (tmplElement.inputs ?? []).find(i => i.name === 'innerHTML') ?? null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getInnerHtmlAttrName(_tmplElement) {
|
|
122
|
+
return '[innerHTML]'
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const h = {
|
|
126
|
+
getAttr,
|
|
127
|
+
getAttrStringValue,
|
|
128
|
+
getElementName,
|
|
129
|
+
hasAttr,
|
|
130
|
+
getRoleValue,
|
|
131
|
+
hasAccessibleName,
|
|
132
|
+
isInteractiveElement,
|
|
133
|
+
hasOnlyHiddenChildren,
|
|
134
|
+
hasNewTabWarning,
|
|
135
|
+
getParent,
|
|
136
|
+
getAncestors,
|
|
137
|
+
getChildOpeningElements,
|
|
138
|
+
getClassName,
|
|
139
|
+
getInnerHtmlAttr,
|
|
140
|
+
getInnerHtmlAttrName,
|
|
141
|
+
elementVisitor: 'Element$1',
|
|
142
|
+
elementWithChildrenVisitor: 'Element$1',
|
|
143
|
+
getOpeningElement: (node) => node,
|
|
144
|
+
getChildOpeningElementsFromWrapper: (node) =>
|
|
145
|
+
(node.children ?? []).filter(c => c.constructor?.name === 'TmplAstElement'),
|
|
146
|
+
}
|