@herb-tools/linter 0.4.0

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 (175) hide show
  1. package/README.md +34 -0
  2. package/bin/herb-lint +3 -0
  3. package/dist/herb-lint.js +16505 -0
  4. package/dist/herb-lint.js.map +1 -0
  5. package/dist/index.cjs +834 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.js +820 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/package.json +49 -0
  10. package/dist/src/cli/argument-parser.js +96 -0
  11. package/dist/src/cli/argument-parser.js.map +1 -0
  12. package/dist/src/cli/file-processor.js +58 -0
  13. package/dist/src/cli/file-processor.js.map +1 -0
  14. package/dist/src/cli/formatters/base-formatter.js +3 -0
  15. package/dist/src/cli/formatters/base-formatter.js.map +1 -0
  16. package/dist/src/cli/formatters/detailed-formatter.js +62 -0
  17. package/dist/src/cli/formatters/detailed-formatter.js.map +1 -0
  18. package/dist/src/cli/formatters/index.js +4 -0
  19. package/dist/src/cli/formatters/index.js.map +1 -0
  20. package/dist/src/cli/formatters/simple-formatter.js +31 -0
  21. package/dist/src/cli/formatters/simple-formatter.js.map +1 -0
  22. package/dist/src/cli/index.js +5 -0
  23. package/dist/src/cli/index.js.map +1 -0
  24. package/dist/src/cli/summary-reporter.js +96 -0
  25. package/dist/src/cli/summary-reporter.js.map +1 -0
  26. package/dist/src/cli.js +50 -0
  27. package/dist/src/cli.js.map +1 -0
  28. package/dist/src/default-rules.js +31 -0
  29. package/dist/src/default-rules.js.map +1 -0
  30. package/dist/src/herb-lint.js +5 -0
  31. package/dist/src/herb-lint.js.map +1 -0
  32. package/dist/src/index.js +4 -0
  33. package/dist/src/index.js.map +1 -0
  34. package/dist/src/linter.js +39 -0
  35. package/dist/src/linter.js.map +1 -0
  36. package/dist/src/rules/erb-no-empty-tags.js +23 -0
  37. package/dist/src/rules/erb-no-empty-tags.js.map +1 -0
  38. package/dist/src/rules/erb-no-output-control-flow.js +47 -0
  39. package/dist/src/rules/erb-no-output-control-flow.js.map +1 -0
  40. package/dist/src/rules/erb-require-whitespace-inside-tags.js +43 -0
  41. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -0
  42. package/dist/src/rules/html-anchor-require-href.js +25 -0
  43. package/dist/src/rules/html-anchor-require-href.js.map +1 -0
  44. package/dist/src/rules/html-aria-role-heading-requires-level.js +26 -0
  45. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -0
  46. package/dist/src/rules/html-attribute-double-quotes.js +21 -0
  47. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -0
  48. package/dist/src/rules/html-attribute-values-require-quotes.js +22 -0
  49. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -0
  50. package/dist/src/rules/html-boolean-attributes-no-value.js +19 -0
  51. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -0
  52. package/dist/src/rules/html-img-require-alt.js +29 -0
  53. package/dist/src/rules/html-img-require-alt.js.map +1 -0
  54. package/dist/src/rules/html-no-block-inside-inline.js +59 -0
  55. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -0
  56. package/dist/src/rules/html-no-duplicate-attributes.js +43 -0
  57. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -0
  58. package/dist/src/rules/html-no-empty-headings.js +148 -0
  59. package/dist/src/rules/html-no-empty-headings.js.map +1 -0
  60. package/dist/src/rules/html-no-nested-links.js +45 -0
  61. package/dist/src/rules/html-no-nested-links.js.map +1 -0
  62. package/dist/src/rules/html-tag-name-lowercase.js +39 -0
  63. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -0
  64. package/dist/src/rules/index.js +13 -0
  65. package/dist/src/rules/index.js.map +1 -0
  66. package/dist/src/rules/rule-utils.js +198 -0
  67. package/dist/src/rules/rule-utils.js.map +1 -0
  68. package/dist/src/types.js +2 -0
  69. package/dist/src/types.js.map +1 -0
  70. package/dist/tsconfig.tsbuildinfo +1 -0
  71. package/dist/types/cli/argument-parser.d.ts +14 -0
  72. package/dist/types/cli/file-processor.d.ts +21 -0
  73. package/dist/types/cli/formatters/base-formatter.d.ts +6 -0
  74. package/dist/types/cli/formatters/detailed-formatter.d.ts +13 -0
  75. package/dist/types/cli/formatters/index.d.ts +3 -0
  76. package/dist/types/cli/formatters/simple-formatter.d.ts +7 -0
  77. package/dist/types/cli/summary-reporter.d.ts +22 -0
  78. package/dist/types/cli.d.ts +6 -0
  79. package/dist/types/default-rules.d.ts +2 -0
  80. package/dist/types/herb-lint.d.ts +2 -0
  81. package/dist/types/index.d.ts +3 -0
  82. package/dist/types/linter.d.ts +18 -0
  83. package/dist/types/rules/erb-no-empty-tags.d.ts +6 -0
  84. package/dist/types/rules/erb-no-output-control-flow.d.ts +6 -0
  85. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +6 -0
  86. package/dist/types/rules/html-anchor-require-href.d.ts +6 -0
  87. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +6 -0
  88. package/dist/types/rules/html-attribute-double-quotes.d.ts +6 -0
  89. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +6 -0
  90. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +6 -0
  91. package/dist/types/rules/html-img-require-alt.d.ts +6 -0
  92. package/dist/types/rules/html-no-block-inside-inline.d.ts +6 -0
  93. package/dist/types/rules/html-no-duplicate-attributes.d.ts +6 -0
  94. package/dist/types/rules/html-no-empty-headings.d.ts +6 -0
  95. package/dist/types/rules/html-no-nested-links.d.ts +6 -0
  96. package/dist/types/rules/html-tag-name-lowercase.d.ts +6 -0
  97. package/dist/types/rules/index.d.ts +12 -0
  98. package/dist/types/rules/rule-utils.d.ts +89 -0
  99. package/dist/types/src/cli/argument-parser.d.ts +14 -0
  100. package/dist/types/src/cli/file-processor.d.ts +21 -0
  101. package/dist/types/src/cli/formatters/base-formatter.d.ts +6 -0
  102. package/dist/types/src/cli/formatters/detailed-formatter.d.ts +13 -0
  103. package/dist/types/src/cli/formatters/index.d.ts +3 -0
  104. package/dist/types/src/cli/formatters/simple-formatter.d.ts +7 -0
  105. package/dist/types/src/cli/index.d.ts +4 -0
  106. package/dist/types/src/cli/summary-reporter.d.ts +22 -0
  107. package/dist/types/src/cli.d.ts +6 -0
  108. package/dist/types/src/default-rules.d.ts +2 -0
  109. package/dist/types/src/herb-lint.d.ts +2 -0
  110. package/dist/types/src/index.d.ts +3 -0
  111. package/dist/types/src/linter.d.ts +18 -0
  112. package/dist/types/src/rules/erb-no-empty-tags.d.ts +6 -0
  113. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +6 -0
  114. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +6 -0
  115. package/dist/types/src/rules/html-anchor-require-href.d.ts +6 -0
  116. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +6 -0
  117. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +6 -0
  118. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +6 -0
  119. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +6 -0
  120. package/dist/types/src/rules/html-img-require-alt.d.ts +6 -0
  121. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +6 -0
  122. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +6 -0
  123. package/dist/types/src/rules/html-no-empty-headings.d.ts +6 -0
  124. package/dist/types/src/rules/html-no-nested-links.d.ts +6 -0
  125. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +6 -0
  126. package/dist/types/src/rules/index.d.ts +12 -0
  127. package/dist/types/src/rules/rule-utils.d.ts +89 -0
  128. package/dist/types/src/types.d.ts +26 -0
  129. package/dist/types/types.d.ts +26 -0
  130. package/docs/rules/README.md +39 -0
  131. package/docs/rules/erb-no-empty-tags.md +38 -0
  132. package/docs/rules/erb-no-output-control-flow.md +45 -0
  133. package/docs/rules/erb-require-whitespace-inside-tags.md +43 -0
  134. package/docs/rules/html-anchor-require-href.md +32 -0
  135. package/docs/rules/html-aria-role-heading-requires-level.md +34 -0
  136. package/docs/rules/html-attribute-double-quotes.md +43 -0
  137. package/docs/rules/html-attribute-values-require-quotes.md +43 -0
  138. package/docs/rules/html-boolean-attributes-no-value.md +39 -0
  139. package/docs/rules/html-img-require-alt.md +44 -0
  140. package/docs/rules/html-no-block-inside-inline.md +66 -0
  141. package/docs/rules/html-no-duplicate-attributes.md +35 -0
  142. package/docs/rules/html-no-empty-headings.md +78 -0
  143. package/docs/rules/html-no-nested-links.md +44 -0
  144. package/docs/rules/html-tag-name-lowercase.md +44 -0
  145. package/package.json +49 -0
  146. package/src/cli/argument-parser.ts +125 -0
  147. package/src/cli/file-processor.ts +86 -0
  148. package/src/cli/formatters/base-formatter.ts +11 -0
  149. package/src/cli/formatters/detailed-formatter.ts +74 -0
  150. package/src/cli/formatters/index.ts +3 -0
  151. package/src/cli/formatters/simple-formatter.ts +40 -0
  152. package/src/cli/index.ts +4 -0
  153. package/src/cli/summary-reporter.ts +127 -0
  154. package/src/cli.ts +60 -0
  155. package/src/default-rules.ts +33 -0
  156. package/src/herb-lint.ts +6 -0
  157. package/src/index.ts +3 -0
  158. package/src/linter.ts +50 -0
  159. package/src/rules/erb-no-empty-tags.ts +34 -0
  160. package/src/rules/erb-no-output-control-flow.ts +61 -0
  161. package/src/rules/erb-require-whitespace-inside-tags.ts +61 -0
  162. package/src/rules/html-anchor-require-href.ts +39 -0
  163. package/src/rules/html-aria-role-heading-requires-level.ts +44 -0
  164. package/src/rules/html-attribute-double-quotes.ts +28 -0
  165. package/src/rules/html-attribute-values-require-quotes.ts +30 -0
  166. package/src/rules/html-boolean-attributes-no-value.ts +27 -0
  167. package/src/rules/html-img-require-alt.ts +42 -0
  168. package/src/rules/html-no-block-inside-inline.ts +84 -0
  169. package/src/rules/html-no-duplicate-attributes.ts +59 -0
  170. package/src/rules/html-no-empty-headings.ts +185 -0
  171. package/src/rules/html-no-nested-links.ts +65 -0
  172. package/src/rules/html-tag-name-lowercase.ts +50 -0
  173. package/src/rules/index.ts +12 -0
  174. package/src/rules/rule-utils.ts +257 -0
  175. package/src/types.ts +32 -0
@@ -0,0 +1,65 @@
1
+ import { BaseRuleVisitor, getTagName } from "./rule-utils.js"
2
+
3
+ import type { Rule, LintOffense } from "../types.js"
4
+ import type { HTMLOpenTagNode, HTMLElementNode, Node } from "@herb-tools/core"
5
+
6
+ class NestedLinkVisitor extends BaseRuleVisitor {
7
+ private linkStack: HTMLOpenTagNode[] = []
8
+
9
+ private checkNestedLink(openTag: HTMLOpenTagNode): boolean {
10
+ if (this.linkStack.length > 0) {
11
+ this.addOffense(
12
+ "Nested `<a>` elements are not allowed. Links cannot contain other links.",
13
+ openTag.tag_name!.location,
14
+ "error"
15
+ )
16
+
17
+ return true
18
+ }
19
+
20
+ return false
21
+ }
22
+
23
+ visitHTMLElementNode(node: HTMLElementNode): void {
24
+ if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
25
+ super.visitHTMLElementNode(node)
26
+ return
27
+ }
28
+
29
+ const openTag = node.open_tag as HTMLOpenTagNode
30
+ const tagName = getTagName(openTag)
31
+
32
+ if (tagName !== "a") {
33
+ super.visitHTMLElementNode(node)
34
+ return
35
+ }
36
+
37
+ // If we're already inside a link, this is a nested link
38
+ this.checkNestedLink(openTag)
39
+
40
+ this.linkStack.push(openTag)
41
+ super.visitHTMLElementNode(node)
42
+ this.linkStack.pop()
43
+ }
44
+
45
+ // Handle self-closing <a> tags (though they're not valid HTML, they might exist)
46
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
47
+ const tagName = getTagName(node)
48
+
49
+ if (tagName === "a" && node.is_void) {
50
+ this.checkNestedLink(node)
51
+ }
52
+
53
+ super.visitHTMLOpenTagNode(node)
54
+ }
55
+ }
56
+
57
+ export class HTMLNoNestedLinksRule implements Rule {
58
+ name = "html-no-nested-links"
59
+
60
+ check(node: Node): LintOffense[] {
61
+ const visitor = new NestedLinkVisitor(this.name)
62
+ visitor.visit(node)
63
+ return visitor.offenses
64
+ }
65
+ }
@@ -0,0 +1,50 @@
1
+ import { BaseRuleVisitor } from "./rule-utils.js"
2
+
3
+ import type { Rule, LintOffense } from "../types.js"
4
+ import type { HTMLOpenTagNode, HTMLCloseTagNode, HTMLSelfCloseTagNode, Node } from "@herb-tools/core"
5
+
6
+ class TagNameLowercaseVisitor extends BaseRuleVisitor {
7
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
8
+ this.checkTagName(node)
9
+ this.visitChildNodes(node)
10
+ }
11
+
12
+ visitHTMLCloseTagNode(node: HTMLCloseTagNode): void {
13
+ this.checkTagName(node)
14
+ this.visitChildNodes(node)
15
+ }
16
+
17
+ visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
18
+ this.checkTagName(node)
19
+ this.visitChildNodes(node)
20
+ }
21
+
22
+ private checkTagName(node: HTMLOpenTagNode | HTMLCloseTagNode | HTMLSelfCloseTagNode): void {
23
+ const tagName = node.tag_name?.value
24
+ if (!tagName) return
25
+
26
+ if (tagName !== tagName.toLowerCase()) {
27
+ let type: string = node.type
28
+
29
+ if (node.type == "AST_HTML_OPEN_TAG_NODE") type = "Opening"
30
+ if (node.type == "AST_HTML_CLOSE_TAG_NODE") type = "Closing"
31
+ if (node.type == "AST_HTML_SELF_CLOSE_TAG_NODE") type = "Self-closing"
32
+
33
+ this.addOffense(
34
+ `${type} tag name \`${tagName}\` should be lowercase. Use \`${tagName.toLowerCase()}\` instead.`,
35
+ node.tag_name!.location,
36
+ "error"
37
+ )
38
+ }
39
+ }
40
+ }
41
+
42
+ export class HTMLTagNameLowercaseRule implements Rule {
43
+ name = "html-tag-name-lowercase"
44
+
45
+ check(node: Node): LintOffense[] {
46
+ const visitor = new TagNameLowercaseVisitor(this.name)
47
+ visitor.visit(node)
48
+ return visitor.offenses
49
+ }
50
+ }
@@ -0,0 +1,12 @@
1
+ export * from "./erb-no-empty-tags.js"
2
+ export * from "./erb-no-output-control-flow.js"
3
+ export * from "./html-anchor-require-href.js"
4
+ export * from "./html-attribute-double-quotes.js"
5
+ export * from "./html-attribute-values-require-quotes.js"
6
+ export * from "./html-boolean-attributes-no-value.js"
7
+ export * from "./html-img-require-alt.js"
8
+ export * from "./html-no-block-inside-inline.js"
9
+ export * from "./html-no-duplicate-attributes.js"
10
+ export * from "./html-no-empty-headings.js"
11
+ export * from "./html-no-nested-links.js"
12
+ export * from "./html-tag-name-lowercase.js"
@@ -0,0 +1,257 @@
1
+ import {
2
+ Visitor
3
+ } from "@herb-tools/core"
4
+
5
+ import type {
6
+ ERBNode,
7
+ HTMLAttributeNameNode,
8
+ HTMLAttributeNode,
9
+ HTMLAttributeValueNode,
10
+ HTMLOpenTagNode,
11
+ HTMLSelfCloseTagNode,
12
+ LiteralNode,
13
+ Location,
14
+ Node
15
+ } from "@herb-tools/core"
16
+ import type { LintOffense, LintSeverity, } from "../types.js"
17
+
18
+ /**
19
+ * Base visitor class that provides common functionality for rule visitors
20
+ */
21
+ export abstract class BaseRuleVisitor extends Visitor {
22
+ public readonly offenses: LintOffense[] = []
23
+ protected ruleName: string
24
+
25
+ constructor(ruleName: string) {
26
+ super()
27
+
28
+ this.ruleName = ruleName
29
+ }
30
+
31
+ /**
32
+ * Helper method to create a lint offense
33
+ */
34
+ protected createOffense(message: string, location: Location, severity: LintSeverity = "error"): LintOffense {
35
+ return {
36
+ rule: this.ruleName,
37
+ code: this.ruleName,
38
+ source: "Herb Linter",
39
+ message,
40
+ location,
41
+ severity,
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Helper method to add an offense to the offenses array
47
+ */
48
+ protected addOffense(message: string, location: Location, severity: LintSeverity = "error"): void {
49
+ this.offenses.push(this.createOffense(message, location, severity))
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Gets attributes from either an HTMLOpenTagNode or HTMLSelfCloseTagNode
55
+ */
56
+ export function getAttributes(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): any[] {
57
+ return node.type === "AST_HTML_SELF_CLOSE_TAG_NODE"
58
+ ? (node as HTMLSelfCloseTagNode).attributes
59
+ : (node as HTMLOpenTagNode).children
60
+ }
61
+
62
+ /**
63
+ * Gets the tag name from an HTML tag node (lowercased)
64
+ */
65
+ export function getTagName(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): string | null {
66
+ return node.tag_name?.value.toLowerCase() || null
67
+ }
68
+
69
+ /**
70
+ * Gets the attribute name from an HTMLAttributeNode (lowercased)
71
+ */
72
+ export function getAttributeName(attributeNode: HTMLAttributeNode): string | null {
73
+ if (attributeNode.name?.type === "AST_HTML_ATTRIBUTE_NAME_NODE") {
74
+ const nameNode = attributeNode.name as HTMLAttributeNameNode
75
+
76
+ return nameNode.name?.value.toLowerCase() || null
77
+ }
78
+
79
+ return null
80
+ }
81
+
82
+ /**
83
+ * Gets the attribute value content from an HTMLAttributeValueNode
84
+ */
85
+ export function getAttributeValue(attributeNode: HTMLAttributeNode): string | null {
86
+ if (attributeNode.value?.type === "AST_HTML_ATTRIBUTE_VALUE_NODE") {
87
+ const valueNode = attributeNode.value as HTMLAttributeValueNode
88
+
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("")
94
+ }
95
+ }
96
+
97
+ return null
98
+ }
99
+
100
+ /**
101
+ * Checks if an attribute has a value
102
+ */
103
+ export function hasAttributeValue(attributeNode: HTMLAttributeNode): boolean {
104
+ return attributeNode.value?.type === "AST_HTML_ATTRIBUTE_VALUE_NODE"
105
+ }
106
+
107
+ /**
108
+ * Gets the quote type used for an attribute value
109
+ */
110
+ export function getAttributeValueQuoteType(attributeNode: HTMLAttributeNode): "single" | "double" | "none" | null {
111
+ if (attributeNode.value?.type === "AST_HTML_ATTRIBUTE_VALUE_NODE") {
112
+ const valueNode = attributeNode.value as HTMLAttributeValueNode
113
+ if (valueNode.quoted && valueNode.open_quote) {
114
+ return valueNode.open_quote.value === '"' ? "double" : "single"
115
+ }
116
+
117
+ return "none"
118
+ }
119
+
120
+ return null
121
+ }
122
+
123
+ /**
124
+ * Finds an attribute by name in a list of attributes
125
+ */
126
+ export function findAttributeByName(attributes: any[], attributeName: string): HTMLAttributeNode | null {
127
+ for (const child of attributes) {
128
+ if (child.type === "AST_HTML_ATTRIBUTE_NODE") {
129
+ const attributeNode = child as HTMLAttributeNode
130
+ const name = getAttributeName(attributeNode)
131
+ if (name === attributeName.toLowerCase()) {
132
+ return attributeNode
133
+ }
134
+ }
135
+ }
136
+ return null
137
+ }
138
+
139
+ /**
140
+ * Checks if a tag has a specific attribute
141
+ */
142
+ export function hasAttribute(node: HTMLOpenTagNode | HTMLSelfCloseTagNode, attributeName: string): boolean {
143
+ const attributes = getAttributes(node)
144
+ return findAttributeByName(attributes, attributeName) !== null
145
+ }
146
+
147
+ /**
148
+ * Common HTML element categorization
149
+ */
150
+ export const HTML_INLINE_ELEMENTS = new Set([
151
+ "a", "abbr", "acronym", "b", "bdo", "big", "br", "button", "cite", "code",
152
+ "dfn", "em", "i", "img", "input", "kbd", "label", "map", "object", "output",
153
+ "q", "samp", "script", "select", "small", "span", "strong", "sub", "sup",
154
+ "textarea", "time", "tt", "var"
155
+ ])
156
+
157
+ export const HTML_BLOCK_ELEMENTS = new Set([
158
+ "address", "article", "aside", "blockquote", "canvas", "dd", "div", "dl",
159
+ "dt", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2",
160
+ "h3", "h4", "h5", "h6", "header", "hr", "li", "main", "nav", "noscript",
161
+ "ol", "p", "pre", "section", "table", "tfoot", "ul", "video"
162
+ ])
163
+
164
+ export const HTML_BOOLEAN_ATTRIBUTES = new Set([
165
+ "autofocus", "autoplay", "checked", "controls", "defer", "disabled", "hidden",
166
+ "loop", "multiple", "muted", "readonly", "required", "reversed", "selected",
167
+ "open", "default", "formnovalidate", "novalidate", "itemscope", "scoped",
168
+ "seamless", "allowfullscreen", "async", "compact", "declare", "nohref",
169
+ "noresize", "noshade", "nowrap", "sortable", "truespeed", "typemustmatch"
170
+ ])
171
+
172
+ export const HEADING_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"])
173
+
174
+ /**
175
+ * Checks if an element is inline
176
+ */
177
+ export function isInlineElement(tagName: string): boolean {
178
+ return HTML_INLINE_ELEMENTS.has(tagName.toLowerCase())
179
+ }
180
+
181
+ /**
182
+ * Checks if an element is block-level
183
+ */
184
+ export function isBlockElement(tagName: string): boolean {
185
+ return HTML_BLOCK_ELEMENTS.has(tagName.toLowerCase())
186
+ }
187
+
188
+ /**
189
+ * Checks if an attribute is a boolean attribute
190
+ */
191
+ export function isBooleanAttribute(attributeName: string): boolean {
192
+ return HTML_BOOLEAN_ATTRIBUTES.has(attributeName.toLowerCase())
193
+ }
194
+
195
+ /**
196
+ * Abstract base class for rules that need to check individual attributes on HTML tags
197
+ * Eliminates duplication of visitHTMLOpenTagNode/visitHTMLSelfCloseTagNode patterns
198
+ * and attribute iteration logic. Provides simplified interface with extracted attribute info.
199
+ */
200
+ export abstract class AttributeVisitorMixin extends BaseRuleVisitor {
201
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
202
+ this.checkAttributesOnNode(node)
203
+ super.visitHTMLOpenTagNode(node)
204
+ }
205
+
206
+ visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
207
+ this.checkAttributesOnNode(node)
208
+ super.visitHTMLSelfCloseTagNode(node)
209
+ }
210
+
211
+ private checkAttributesOnNode(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): void {
212
+ forEachAttribute(node, (attributeNode) => {
213
+ const attributeName = getAttributeName(attributeNode)
214
+ const attributeValue = getAttributeValue(attributeNode)
215
+
216
+ if (attributeName) {
217
+ this.checkAttribute(attributeName, attributeValue, attributeNode, node)
218
+ }
219
+ })
220
+ }
221
+
222
+ protected abstract checkAttribute(
223
+ attributeName: string,
224
+ attributeValue: string | null,
225
+ attributeNode: HTMLAttributeNode,
226
+ parentNode: HTMLOpenTagNode | HTMLSelfCloseTagNode
227
+ ): void
228
+ }
229
+
230
+ /**
231
+ * Checks if an attribute value is quoted
232
+ */
233
+ export function isAttributeValueQuoted(attributeNode: HTMLAttributeNode): boolean {
234
+ if (attributeNode.value?.type === "AST_HTML_ATTRIBUTE_VALUE_NODE") {
235
+ const valueNode = attributeNode.value as HTMLAttributeValueNode
236
+
237
+ return !!valueNode.quoted
238
+ }
239
+
240
+ return false
241
+ }
242
+
243
+ /**
244
+ * Iterates over all attributes of a tag node, calling the callback for each attribute
245
+ */
246
+ export function forEachAttribute(
247
+ node: HTMLOpenTagNode | HTMLSelfCloseTagNode,
248
+ callback: (attributeNode: HTMLAttributeNode) => void
249
+ ): void {
250
+ const attributes = getAttributes(node)
251
+
252
+ for (const child of attributes) {
253
+ if (child.type === "AST_HTML_ATTRIBUTE_NODE") {
254
+ callback(child as HTMLAttributeNode)
255
+ }
256
+ }
257
+ }
package/src/types.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { Node, Diagnostic } from "@herb-tools/core"
2
+ import type { defaultRules } from "./default-rules.js"
3
+
4
+ export type LintSeverity = "error" | "warning"
5
+
6
+ /**
7
+ * Automatically inferred union type of all available linter rule names.
8
+ * This type extracts the 'name' property from each rule class instance.
9
+ */
10
+ export type LinterRule = InstanceType<typeof defaultRules[number]>['name']
11
+
12
+ export interface LintOffense extends Diagnostic {
13
+ rule: LinterRule
14
+ severity: LintSeverity
15
+ }
16
+
17
+ export interface LintResult {
18
+ offenses: LintOffense[]
19
+ errors: number
20
+ warnings: number
21
+ }
22
+
23
+ export interface Rule {
24
+ name: string
25
+ check(node: Node): LintOffense[]
26
+ }
27
+
28
+ /**
29
+ * Type representing a rule class constructor.
30
+ * The Linter accepts rule classes rather than instances for better performance and memory usage.
31
+ */
32
+ export type RuleClass = new () => Rule