@herb-tools/linter 0.4.1 → 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 +1 -3
- package/dist/herb-lint.js +142 -17
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +136 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +136 -11
- package/dist/index.js.map +1 -1
- package/dist/package.json +4 -4
- package/dist/src/default-rules.js +2 -0
- 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-tag-name-lowercase.js +17 -8
- package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
- package/dist/src/rules/index.js +1 -0
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/rule-utils.js +43 -0
- 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/index.d.ts +1 -0
- package/dist/types/rules/rule-utils.d.ts +9 -0
- package/dist/types/rules/svg-tag-name-capitalization.d.ts +6 -0
- package/dist/types/src/rules/index.d.ts +1 -0
- package/dist/types/src/rules/rule-utils.d.ts +9 -0
- package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +6 -0
- package/docs/rules/README.md +1 -0
- package/docs/rules/svg-tag-name-capitalization.md +57 -0
- package/package.json +4 -4
- package/src/default-rules.ts +2 -0
- package/src/rules/erb-require-whitespace-inside-tags.ts +33 -2
- package/src/rules/html-tag-name-lowercase.ts +24 -9
- package/src/rules/index.ts +1 -0
- package/src/rules/rule-utils.ts +47 -0
- package/src/rules/svg-tag-name-capitalization.ts +73 -0
|
@@ -24,14 +24,43 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
|
|
|
24
24
|
|
|
25
25
|
const value = content.value
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
if (openTag.value === "<%#") {
|
|
28
|
+
this.checkCommentTagWhitespace(openTag, closeTag, value)
|
|
29
|
+
} else {
|
|
30
|
+
this.checkOpenTagWhitespace(openTag, value)
|
|
31
|
+
this.checkCloseTagWhitespace(closeTag, value)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private checkCommentTagWhitespace(openTag: Token, closeTag: Token, content: string): void {
|
|
36
|
+
if (!content.startsWith(" ") && !content.startsWith("\n") && !content.startsWith("=")) {
|
|
37
|
+
this.addOffense(
|
|
38
|
+
`Add whitespace after \`${openTag.value}\`.`,
|
|
39
|
+
openTag.location,
|
|
40
|
+
"error"
|
|
41
|
+
)
|
|
42
|
+
} else if (content.startsWith("=") && content.length > 1 && !content[1].match(/\s/)) {
|
|
43
|
+
this.addOffense(
|
|
44
|
+
`Add whitespace after \`<%#=\`.`,
|
|
45
|
+
openTag.location,
|
|
46
|
+
"error"
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!content.endsWith(" ") && !content.endsWith("\n")) {
|
|
51
|
+
this.addOffense(
|
|
52
|
+
`Add whitespace before \`${closeTag.value}\`.`,
|
|
53
|
+
closeTag.location,
|
|
54
|
+
"error"
|
|
55
|
+
)
|
|
56
|
+
}
|
|
29
57
|
}
|
|
30
58
|
|
|
31
59
|
private checkOpenTagWhitespace(openTag: Token, content:string):void {
|
|
32
60
|
if (content.startsWith(" ") || content.startsWith("\n")) {
|
|
33
61
|
return
|
|
34
62
|
}
|
|
63
|
+
|
|
35
64
|
this.addOffense(
|
|
36
65
|
`Add whitespace after \`${openTag.value}\`.`,
|
|
37
66
|
openTag.location,
|
|
@@ -43,6 +72,7 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
|
|
|
43
72
|
if (content.endsWith(" ") || content.endsWith("\n")) {
|
|
44
73
|
return
|
|
45
74
|
}
|
|
75
|
+
|
|
46
76
|
this.addOffense(
|
|
47
77
|
`Add whitespace before \`${closeTag.value}\`.`,
|
|
48
78
|
closeTag.location,
|
|
@@ -53,6 +83,7 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
|
|
|
53
83
|
|
|
54
84
|
export class ERBRequireWhitespaceRule implements Rule {
|
|
55
85
|
name = "erb-require-whitespace-inside-tags"
|
|
86
|
+
|
|
56
87
|
check(node: Node): LintOffense[] {
|
|
57
88
|
const visitor = new RequireWhitespaceInsideTags(this.name)
|
|
58
89
|
visitor.visit(node)
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
import { BaseRuleVisitor } from "./rule-utils.js"
|
|
2
2
|
|
|
3
3
|
import type { Rule, LintOffense } from "../types.js"
|
|
4
|
-
import type { HTMLOpenTagNode, HTMLCloseTagNode, HTMLSelfCloseTagNode, Node } from "@herb-tools/core"
|
|
4
|
+
import type { HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, HTMLSelfCloseTagNode, Node } from "@herb-tools/core"
|
|
5
5
|
|
|
6
6
|
class TagNameLowercaseVisitor extends BaseRuleVisitor {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
8
|
+
const tagName = node.tag_name?.value
|
|
9
|
+
|
|
10
|
+
if (node.open_tag) {
|
|
11
|
+
this.checkTagName(node.open_tag as HTMLOpenTagNode)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (tagName && ["svg"].includes(tagName.toLowerCase())) {
|
|
15
|
+
if (node.close_tag) {
|
|
16
|
+
this.checkTagName(node.close_tag as HTMLCloseTagNode)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return
|
|
20
|
+
}
|
|
11
21
|
|
|
12
|
-
visitHTMLCloseTagNode(node: HTMLCloseTagNode): void {
|
|
13
|
-
this.checkTagName(node)
|
|
14
22
|
this.visitChildNodes(node)
|
|
23
|
+
|
|
24
|
+
if (node.close_tag) {
|
|
25
|
+
this.checkTagName(node.close_tag as HTMLCloseTagNode)
|
|
26
|
+
}
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
|
|
@@ -21,9 +33,12 @@ class TagNameLowercaseVisitor extends BaseRuleVisitor {
|
|
|
21
33
|
|
|
22
34
|
private checkTagName(node: HTMLOpenTagNode | HTMLCloseTagNode | HTMLSelfCloseTagNode): void {
|
|
23
35
|
const tagName = node.tag_name?.value
|
|
36
|
+
|
|
24
37
|
if (!tagName) return
|
|
25
38
|
|
|
26
|
-
|
|
39
|
+
const lowercaseTagName = tagName.toLowerCase()
|
|
40
|
+
|
|
41
|
+
if (tagName !== lowercaseTagName) {
|
|
27
42
|
let type: string = node.type
|
|
28
43
|
|
|
29
44
|
if (node.type == "AST_HTML_OPEN_TAG_NODE") type = "Opening"
|
|
@@ -31,7 +46,7 @@ class TagNameLowercaseVisitor extends BaseRuleVisitor {
|
|
|
31
46
|
if (node.type == "AST_HTML_SELF_CLOSE_TAG_NODE") type = "Self-closing"
|
|
32
47
|
|
|
33
48
|
this.addOffense(
|
|
34
|
-
`${type} tag name \`${tagName}\` should be lowercase. Use \`${
|
|
49
|
+
`${type} tag name \`${tagName}\` should be lowercase. Use \`${lowercaseTagName}\` instead.`,
|
|
35
50
|
node.tag_name!.location,
|
|
36
51
|
"error"
|
|
37
52
|
)
|
package/src/rules/index.ts
CHANGED
package/src/rules/rule-utils.ts
CHANGED
|
@@ -188,6 +188,53 @@ export const HTML_BOOLEAN_ATTRIBUTES = new Set([
|
|
|
188
188
|
|
|
189
189
|
export const HEADING_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"])
|
|
190
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
|
+
|
|
191
238
|
export const VALID_ARIA_ROLES = new Set([
|
|
192
239
|
"banner", "complementary", "contentinfo", "form", "main", "navigation", "region", "search",
|
|
193
240
|
"article", "cell", "columnheader", "definition", "directory", "document", "feed", "figure",
|
|
@@ -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
|
+
}
|