@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.
Files changed (54) hide show
  1. package/README.md +2 -4
  2. package/dist/herb-lint.js +292 -107
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +351 -77
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +348 -78
  7. package/dist/index.js.map +1 -1
  8. package/dist/package.json +4 -4
  9. package/dist/src/default-rules.js +10 -2
  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-aria-attribute-must-be-valid.js +24 -0
  14. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -0
  15. package/dist/src/rules/html-aria-role-must-be-valid.js +21 -0
  16. package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -0
  17. package/dist/src/rules/html-no-duplicate-ids.js +25 -0
  18. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -0
  19. package/dist/src/rules/html-tag-name-lowercase.js +17 -8
  20. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  21. package/dist/src/rules/index.js +4 -0
  22. package/dist/src/rules/index.js.map +1 -1
  23. package/dist/src/rules/rule-utils.js +126 -8
  24. package/dist/src/rules/rule-utils.js.map +1 -1
  25. package/dist/src/rules/svg-tag-name-capitalization.js +57 -0
  26. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -0
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +6 -0
  29. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +6 -0
  30. package/dist/types/rules/html-no-duplicate-ids.d.ts +6 -0
  31. package/dist/types/rules/index.d.ts +4 -0
  32. package/dist/types/rules/rule-utils.d.ts +11 -0
  33. package/dist/types/rules/svg-tag-name-capitalization.d.ts +6 -0
  34. package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +6 -0
  35. package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +6 -0
  36. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +6 -0
  37. package/dist/types/src/rules/index.d.ts +4 -0
  38. package/dist/types/src/rules/rule-utils.d.ts +11 -0
  39. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +6 -0
  40. package/docs/rules/README.md +5 -0
  41. package/docs/rules/html-aria-attribute-must-be-valid.md +45 -0
  42. package/docs/rules/html-aria-role-must-be-valid.md +45 -0
  43. package/docs/rules/html-no-duplicate-ids.md +49 -0
  44. package/docs/rules/svg-tag-name-capitalization.md +57 -0
  45. package/package.json +4 -4
  46. package/src/default-rules.ts +10 -2
  47. package/src/rules/erb-require-whitespace-inside-tags.ts +33 -2
  48. package/src/rules/html-aria-attribute-must-be-valid.ts +42 -0
  49. package/src/rules/html-aria-role-must-be-valid.ts +30 -0
  50. package/src/rules/html-no-duplicate-ids.ts +39 -0
  51. package/src/rules/html-tag-name-lowercase.ts +24 -9
  52. package/src/rules/index.ts +4 -0
  53. package/src/rules/rule-utils.ts +145 -17
  54. package/src/rules/svg-tag-name-capitalization.ts +73 -0
@@ -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
- if (attributeNode.value?.type === "AST_HTML_ATTRIBUTE_VALUE_NODE") {
87
- const valueNode = attributeNode.value as HTMLAttributeValueNode
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
- if (valueNode.children && valueNode.children.length > 0) {
90
- return valueNode.children
91
- .filter(child => child.type === "AST_LITERAL_NODE")
92
- .map(child => (child as LiteralNode).content)
93
- .join("")
107
+ case "AST_LITERAL_NODE": {
108
+ result += (child as LiteralNode).content
109
+ break
110
+ }
94
111
  }
95
112
  }
96
113
 
97
- return null
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
+ }