@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.
Files changed (36) hide show
  1. package/README.md +1 -3
  2. package/dist/herb-lint.js +142 -17
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +136 -10
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +136 -11
  7. package/dist/index.js.map +1 -1
  8. package/dist/package.json +4 -4
  9. package/dist/src/default-rules.js +2 -0
  10. package/dist/src/default-rules.js.map +1 -1
  11. package/dist/src/rules/erb-require-whitespace-inside-tags.js +18 -2
  12. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  13. package/dist/src/rules/html-tag-name-lowercase.js +17 -8
  14. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  15. package/dist/src/rules/index.js +1 -0
  16. package/dist/src/rules/index.js.map +1 -1
  17. package/dist/src/rules/rule-utils.js +43 -0
  18. package/dist/src/rules/rule-utils.js.map +1 -1
  19. package/dist/src/rules/svg-tag-name-capitalization.js +57 -0
  20. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -0
  21. package/dist/tsconfig.tsbuildinfo +1 -1
  22. package/dist/types/rules/index.d.ts +1 -0
  23. package/dist/types/rules/rule-utils.d.ts +9 -0
  24. package/dist/types/rules/svg-tag-name-capitalization.d.ts +6 -0
  25. package/dist/types/src/rules/index.d.ts +1 -0
  26. package/dist/types/src/rules/rule-utils.d.ts +9 -0
  27. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +6 -0
  28. package/docs/rules/README.md +1 -0
  29. package/docs/rules/svg-tag-name-capitalization.md +57 -0
  30. package/package.json +4 -4
  31. package/src/default-rules.ts +2 -0
  32. package/src/rules/erb-require-whitespace-inside-tags.ts +33 -2
  33. package/src/rules/html-tag-name-lowercase.ts +24 -9
  34. package/src/rules/index.ts +1 -0
  35. package/src/rules/rule-utils.ts +47 -0
  36. 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
- this.checkOpenTagWhitespace(openTag, value)
28
- this.checkCloseTagWhitespace(closeTag, value)
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
- visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
8
- this.checkTagName(node)
9
- this.visitChildNodes(node)
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
- if (tagName !== tagName.toLowerCase()) {
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 \`${tagName.toLowerCase()}\` instead.`,
49
+ `${type} tag name \`${tagName}\` should be lowercase. Use \`${lowercaseTagName}\` instead.`,
35
50
  node.tag_name!.location,
36
51
  "error"
37
52
  )
@@ -13,3 +13,4 @@ export * from "./html-no-duplicate-ids.js"
13
13
  export * from "./html-no-empty-headings.js"
14
14
  export * from "./html-no-nested-links.js"
15
15
  export * from "./html-tag-name-lowercase.js"
16
+ export * from "./svg-tag-name-capitalization.js"
@@ -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
+ }