@herb-tools/linter 0.4.0 → 0.4.2
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/README.md +2 -4
- package/dist/herb-lint.js +292 -107
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +351 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +348 -78
- package/dist/index.js.map +1 -1
- package/dist/package.json +4 -4
- package/dist/src/default-rules.js +10 -2
- package/dist/src/default-rules.js.map +1 -1
- package/dist/src/rules/erb-require-whitespace-inside-tags.js +18 -2
- package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
- package/dist/src/rules/html-aria-attribute-must-be-valid.js +24 -0
- package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -0
- package/dist/src/rules/html-aria-role-must-be-valid.js +21 -0
- package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -0
- package/dist/src/rules/html-no-duplicate-ids.js +25 -0
- package/dist/src/rules/html-no-duplicate-ids.js.map +1 -0
- package/dist/src/rules/html-tag-name-lowercase.js +17 -8
- package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
- package/dist/src/rules/index.js +4 -0
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/rule-utils.js +126 -8
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +57 -0
- package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +6 -0
- package/dist/types/rules/html-aria-role-must-be-valid.d.ts +6 -0
- package/dist/types/rules/html-no-duplicate-ids.d.ts +6 -0
- package/dist/types/rules/index.d.ts +4 -0
- package/dist/types/rules/rule-utils.d.ts +11 -0
- package/dist/types/rules/svg-tag-name-capitalization.d.ts +6 -0
- package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +6 -0
- package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +6 -0
- package/dist/types/src/rules/html-no-duplicate-ids.d.ts +6 -0
- package/dist/types/src/rules/index.d.ts +4 -0
- package/dist/types/src/rules/rule-utils.d.ts +11 -0
- package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +6 -0
- package/docs/rules/README.md +5 -0
- package/docs/rules/html-aria-attribute-must-be-valid.md +45 -0
- package/docs/rules/html-aria-role-must-be-valid.md +45 -0
- package/docs/rules/html-no-duplicate-ids.md +49 -0
- package/docs/rules/svg-tag-name-capitalization.md +57 -0
- package/package.json +4 -4
- package/src/default-rules.ts +10 -2
- package/src/rules/erb-require-whitespace-inside-tags.ts +33 -2
- package/src/rules/html-aria-attribute-must-be-valid.ts +42 -0
- package/src/rules/html-aria-role-must-be-valid.ts +30 -0
- package/src/rules/html-no-duplicate-ids.ts +39 -0
- package/src/rules/html-tag-name-lowercase.ts +24 -9
- package/src/rules/index.ts +4 -0
- package/src/rules/rule-utils.ts +145 -17
- package/src/rules/svg-tag-name-capitalization.ts +73 -0
package/src/rules/rule-utils.ts
CHANGED
|
@@ -2,16 +2,15 @@ import {
|
|
|
2
2
|
Visitor
|
|
3
3
|
} from "@herb-tools/core"
|
|
4
4
|
|
|
5
|
-
import type {
|
|
6
|
-
ERBNode,
|
|
5
|
+
import type {
|
|
6
|
+
ERBNode,
|
|
7
7
|
HTMLAttributeNameNode,
|
|
8
|
-
HTMLAttributeNode,
|
|
9
|
-
HTMLAttributeValueNode,
|
|
10
|
-
HTMLOpenTagNode,
|
|
11
|
-
HTMLSelfCloseTagNode,
|
|
12
|
-
LiteralNode,
|
|
13
|
-
Location
|
|
14
|
-
Node
|
|
8
|
+
HTMLAttributeNode,
|
|
9
|
+
HTMLAttributeValueNode,
|
|
10
|
+
HTMLOpenTagNode,
|
|
11
|
+
HTMLSelfCloseTagNode,
|
|
12
|
+
LiteralNode,
|
|
13
|
+
Location
|
|
15
14
|
} from "@herb-tools/core"
|
|
16
15
|
import type { LintOffense, LintSeverity, } from "../types.js"
|
|
17
16
|
|
|
@@ -83,18 +82,36 @@ export function getAttributeName(attributeNode: HTMLAttributeNode): string | nul
|
|
|
83
82
|
* Gets the attribute value content from an HTMLAttributeValueNode
|
|
84
83
|
*/
|
|
85
84
|
export function getAttributeValue(attributeNode: HTMLAttributeNode): string | null {
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
const valueNode: HTMLAttributeValueNode | null = attributeNode.value as HTMLAttributeValueNode
|
|
86
|
+
|
|
87
|
+
if (valueNode === null) return null
|
|
88
|
+
|
|
89
|
+
if (valueNode.type !== "AST_HTML_ATTRIBUTE_VALUE_NODE" || !valueNode.children?.length) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let result = ""
|
|
94
|
+
|
|
95
|
+
for (const child of valueNode.children) {
|
|
96
|
+
switch (child.type) {
|
|
97
|
+
case "AST_ERB_CONTENT_NODE": {
|
|
98
|
+
const erbNode = child as ERBNode
|
|
99
|
+
|
|
100
|
+
if (erbNode.content) {
|
|
101
|
+
result += `${erbNode.tag_opening?.value}${erbNode.content.value}${erbNode.tag_closing?.value}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
break
|
|
105
|
+
}
|
|
88
106
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
.join("")
|
|
107
|
+
case "AST_LITERAL_NODE": {
|
|
108
|
+
result += (child as LiteralNode).content
|
|
109
|
+
break
|
|
110
|
+
}
|
|
94
111
|
}
|
|
95
112
|
}
|
|
96
113
|
|
|
97
|
-
return
|
|
114
|
+
return result
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
/**
|
|
@@ -171,6 +188,117 @@ export const HTML_BOOLEAN_ATTRIBUTES = new Set([
|
|
|
171
188
|
|
|
172
189
|
export const HEADING_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"])
|
|
173
190
|
|
|
191
|
+
/**
|
|
192
|
+
* SVG elements that use camelCase naming
|
|
193
|
+
*/
|
|
194
|
+
export const SVG_CAMEL_CASE_ELEMENTS = new Set([
|
|
195
|
+
"animateMotion",
|
|
196
|
+
"animateTransform",
|
|
197
|
+
"clipPath",
|
|
198
|
+
"feBlend",
|
|
199
|
+
"feColorMatrix",
|
|
200
|
+
"feComponentTransfer",
|
|
201
|
+
"feComposite",
|
|
202
|
+
"feConvolveMatrix",
|
|
203
|
+
"feDiffuseLighting",
|
|
204
|
+
"feDisplacementMap",
|
|
205
|
+
"feDistantLight",
|
|
206
|
+
"feDropShadow",
|
|
207
|
+
"feFlood",
|
|
208
|
+
"feFuncA",
|
|
209
|
+
"feFuncB",
|
|
210
|
+
"feFuncG",
|
|
211
|
+
"feFuncR",
|
|
212
|
+
"feGaussianBlur",
|
|
213
|
+
"feImage",
|
|
214
|
+
"feMerge",
|
|
215
|
+
"feMergeNode",
|
|
216
|
+
"feMorphology",
|
|
217
|
+
"feOffset",
|
|
218
|
+
"fePointLight",
|
|
219
|
+
"feSpecularLighting",
|
|
220
|
+
"feSpotLight",
|
|
221
|
+
"feTile",
|
|
222
|
+
"feTurbulence",
|
|
223
|
+
"foreignObject",
|
|
224
|
+
"glyphRef",
|
|
225
|
+
"linearGradient",
|
|
226
|
+
"radialGradient",
|
|
227
|
+
"textPath"
|
|
228
|
+
])
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Mapping from lowercase SVG element names to their correct camelCase versions
|
|
232
|
+
* Generated dynamically from SVG_CAMEL_CASE_ELEMENTS
|
|
233
|
+
*/
|
|
234
|
+
export const SVG_LOWERCASE_TO_CAMELCASE = new Map(
|
|
235
|
+
Array.from(SVG_CAMEL_CASE_ELEMENTS).map(element => [element.toLowerCase(), element])
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
export const VALID_ARIA_ROLES = new Set([
|
|
239
|
+
"banner", "complementary", "contentinfo", "form", "main", "navigation", "region", "search",
|
|
240
|
+
"article", "cell", "columnheader", "definition", "directory", "document", "feed", "figure",
|
|
241
|
+
"group", "heading", "img", "list", "listitem", "math", "none", "note", "presentation",
|
|
242
|
+
"row", "rowgroup", "rowheader", "separator", "table", "term", "tooltip",
|
|
243
|
+
"alert", "alertdialog", "button", "checkbox", "combobox", "dialog", "grid", "gridcell", "link",
|
|
244
|
+
"listbox", "menu", "menubar", "menuitem", "menuitemcheckbox", "menuitemradio", "option",
|
|
245
|
+
"progressbar", "radio", "radiogroup", "scrollbar", "searchbox", "slider", "spinbutton",
|
|
246
|
+
"status", "switch", "tab", "tablist", "tabpanel", "textbox", "timer", "toolbar", "tree",
|
|
247
|
+
"treegrid", "treeitem",
|
|
248
|
+
"log", "marquee"
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
export const ARIA_ATTRIBUTES = new Set([
|
|
252
|
+
'aria-activedescendant',
|
|
253
|
+
'aria-atomic',
|
|
254
|
+
'aria-autocomplete',
|
|
255
|
+
'aria-busy',
|
|
256
|
+
'aria-checked',
|
|
257
|
+
'aria-colcount',
|
|
258
|
+
'aria-colindex',
|
|
259
|
+
'aria-colspan',
|
|
260
|
+
'aria-controls',
|
|
261
|
+
'aria-current',
|
|
262
|
+
'aria-describedby',
|
|
263
|
+
'aria-details',
|
|
264
|
+
'aria-disabled',
|
|
265
|
+
'aria-dropeffect',
|
|
266
|
+
'aria-errormessage',
|
|
267
|
+
'aria-expanded',
|
|
268
|
+
'aria-flowto',
|
|
269
|
+
'aria-grabbed',
|
|
270
|
+
'aria-haspopup',
|
|
271
|
+
'aria-hidden',
|
|
272
|
+
'aria-invalid',
|
|
273
|
+
'aria-keyshortcuts',
|
|
274
|
+
'aria-label',
|
|
275
|
+
'aria-labelledby',
|
|
276
|
+
'aria-level',
|
|
277
|
+
'aria-live',
|
|
278
|
+
'aria-modal',
|
|
279
|
+
'aria-multiline',
|
|
280
|
+
'aria-multiselectable',
|
|
281
|
+
'aria-orientation',
|
|
282
|
+
'aria-owns',
|
|
283
|
+
'aria-placeholder',
|
|
284
|
+
'aria-posinset',
|
|
285
|
+
'aria-pressed',
|
|
286
|
+
'aria-readonly',
|
|
287
|
+
'aria-relevant',
|
|
288
|
+
'aria-required',
|
|
289
|
+
'aria-roledescription',
|
|
290
|
+
'aria-rowcount',
|
|
291
|
+
'aria-rowindex',
|
|
292
|
+
'aria-rowspan',
|
|
293
|
+
'aria-selected',
|
|
294
|
+
'aria-setsize',
|
|
295
|
+
'aria-sort',
|
|
296
|
+
'aria-valuemax',
|
|
297
|
+
'aria-valuemin',
|
|
298
|
+
'aria-valuenow',
|
|
299
|
+
'aria-valuetext',
|
|
300
|
+
])
|
|
301
|
+
|
|
174
302
|
/**
|
|
175
303
|
* Checks if an element is inline
|
|
176
304
|
*/
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { BaseRuleVisitor, SVG_CAMEL_CASE_ELEMENTS, SVG_LOWERCASE_TO_CAMELCASE } from "./rule-utils.js"
|
|
2
|
+
|
|
3
|
+
import type { Rule, LintOffense } from "../types.js"
|
|
4
|
+
import type { HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, HTMLSelfCloseTagNode, Node } from "@herb-tools/core"
|
|
5
|
+
|
|
6
|
+
class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor {
|
|
7
|
+
private insideSVG = false
|
|
8
|
+
|
|
9
|
+
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
10
|
+
const tagName = node.tag_name?.value
|
|
11
|
+
|
|
12
|
+
if (tagName && ["svg"].includes(tagName.toLowerCase())) {
|
|
13
|
+
const wasInsideSVG = this.insideSVG
|
|
14
|
+
this.insideSVG = true
|
|
15
|
+
this.visitChildNodes(node)
|
|
16
|
+
this.insideSVG = wasInsideSVG
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (this.insideSVG) {
|
|
21
|
+
if (node.open_tag) {
|
|
22
|
+
this.checkTagName(node.open_tag as HTMLOpenTagNode)
|
|
23
|
+
}
|
|
24
|
+
if (node.close_tag) {
|
|
25
|
+
this.checkTagName(node.close_tag as HTMLCloseTagNode)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.visitChildNodes(node)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
|
|
33
|
+
if (this.insideSVG) {
|
|
34
|
+
this.checkTagName(node)
|
|
35
|
+
}
|
|
36
|
+
this.visitChildNodes(node)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private checkTagName(node: HTMLOpenTagNode | HTMLCloseTagNode | HTMLSelfCloseTagNode): void {
|
|
40
|
+
const tagName = node.tag_name?.value
|
|
41
|
+
|
|
42
|
+
if (!tagName) return
|
|
43
|
+
|
|
44
|
+
if (SVG_CAMEL_CASE_ELEMENTS.has(tagName)) return
|
|
45
|
+
|
|
46
|
+
const lowercaseTagName = tagName.toLowerCase()
|
|
47
|
+
const correctCamelCase = SVG_LOWERCASE_TO_CAMELCASE.get(lowercaseTagName)
|
|
48
|
+
|
|
49
|
+
if (correctCamelCase && tagName !== correctCamelCase) {
|
|
50
|
+
let type: string = node.type
|
|
51
|
+
|
|
52
|
+
if (node.type == "AST_HTML_OPEN_TAG_NODE") type = "Opening"
|
|
53
|
+
if (node.type == "AST_HTML_CLOSE_TAG_NODE") type = "Closing"
|
|
54
|
+
if (node.type == "AST_HTML_SELF_CLOSE_TAG_NODE") type = "Self-closing"
|
|
55
|
+
|
|
56
|
+
this.addOffense(
|
|
57
|
+
`${type} SVG tag name \`${tagName}\` should use proper capitalization. Use \`${correctCamelCase}\` instead.`,
|
|
58
|
+
node.tag_name!.location,
|
|
59
|
+
"error"
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class SVGTagNameCapitalizationRule implements Rule {
|
|
66
|
+
name = "svg-tag-name-capitalization"
|
|
67
|
+
|
|
68
|
+
check(node: Node): LintOffense[] {
|
|
69
|
+
const visitor = new SVGTagNameCapitalizationVisitor(this.name)
|
|
70
|
+
visitor.visit(node)
|
|
71
|
+
return visitor.offenses
|
|
72
|
+
}
|
|
73
|
+
}
|