@herb-tools/linter 0.4.3 → 0.6.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 (254) hide show
  1. package/README.md +216 -19
  2. package/dist/herb-lint.js +5559 -1860
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +722 -187
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +714 -189
  7. package/dist/index.js.map +1 -1
  8. package/dist/package.json +4 -4
  9. package/dist/src/cli/argument-parser.js +28 -22
  10. package/dist/src/cli/argument-parser.js.map +1 -1
  11. package/dist/src/cli/file-processor.js +19 -13
  12. package/dist/src/cli/file-processor.js.map +1 -1
  13. package/dist/src/cli/formatters/detailed-formatter.js +9 -9
  14. package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
  15. package/dist/src/cli/formatters/github-actions-formatter.js +50 -0
  16. package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -0
  17. package/dist/src/cli/formatters/index.js +2 -0
  18. package/dist/src/cli/formatters/index.js.map +1 -1
  19. package/dist/src/cli/formatters/json-formatter.js +58 -0
  20. package/dist/src/cli/formatters/json-formatter.js.map +1 -0
  21. package/dist/src/cli/formatters/simple-formatter.js +15 -15
  22. package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
  23. package/dist/src/cli/output-manager.js +120 -0
  24. package/dist/src/cli/output-manager.js.map +1 -0
  25. package/dist/src/cli/summary-reporter.js +22 -22
  26. package/dist/src/cli/summary-reporter.js.map +1 -1
  27. package/dist/src/cli.js +41 -26
  28. package/dist/src/cli.js.map +1 -1
  29. package/dist/src/default-rules.js +22 -0
  30. package/dist/src/default-rules.js.map +1 -1
  31. package/dist/src/linter.js +29 -4
  32. package/dist/src/linter.js.map +1 -1
  33. package/dist/src/rules/erb-no-empty-tags.js +2 -2
  34. package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
  35. package/dist/src/rules/erb-no-output-control-flow.js +2 -2
  36. package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
  37. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js +26 -0
  38. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -0
  39. package/dist/src/rules/erb-prefer-image-tag-helper.js +2 -6
  40. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
  41. package/dist/src/rules/erb-require-whitespace-inside-tags.js +2 -2
  42. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  43. package/dist/src/rules/erb-requires-trailing-newline.js.map +1 -1
  44. package/dist/src/rules/html-anchor-require-href.js +2 -2
  45. package/dist/src/rules/html-anchor-require-href.js.map +1 -1
  46. package/dist/src/rules/html-aria-attribute-must-be-valid.js +13 -12
  47. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
  48. package/dist/src/rules/html-aria-label-is-well-formatted.js +33 -0
  49. package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -0
  50. package/dist/src/rules/html-aria-level-must-be-valid.js +28 -6
  51. package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
  52. package/dist/src/rules/html-aria-role-heading-requires-level.js +9 -15
  53. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
  54. package/dist/src/rules/html-aria-role-must-be-valid.js +5 -5
  55. package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
  56. package/dist/src/rules/html-attribute-double-quotes.js +16 -6
  57. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
  58. package/dist/src/rules/html-attribute-equals-spacing.js +24 -0
  59. package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -0
  60. package/dist/src/rules/html-attribute-values-require-quotes.js +21 -10
  61. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
  62. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js +47 -0
  63. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -0
  64. package/dist/src/rules/html-boolean-attributes-no-value.js +11 -4
  65. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  66. package/dist/src/rules/html-iframe-has-title.js +39 -0
  67. package/dist/src/rules/html-iframe-has-title.js.map +1 -0
  68. package/dist/src/rules/html-img-require-alt.js +2 -6
  69. package/dist/src/rules/html-img-require-alt.js.map +1 -1
  70. package/dist/src/rules/html-navigation-has-label.js +43 -0
  71. package/dist/src/rules/html-navigation-has-label.js.map +1 -0
  72. package/dist/src/rules/html-no-aria-hidden-on-focusable.js +67 -0
  73. package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -0
  74. package/dist/src/rules/html-no-block-inside-inline.js +4 -4
  75. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
  76. package/dist/src/rules/html-no-duplicate-attributes.js +24 -27
  77. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
  78. package/dist/src/rules/html-no-duplicate-ids.js +4 -4
  79. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
  80. package/dist/src/rules/html-no-empty-headings.js +2 -23
  81. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  82. package/dist/src/rules/html-no-nested-links.js +2 -2
  83. package/dist/src/rules/html-no-nested-links.js.map +1 -1
  84. package/dist/src/rules/html-no-positive-tab-index.js +21 -0
  85. package/dist/src/rules/html-no-positive-tab-index.js.map +1 -0
  86. package/dist/src/rules/html-no-self-closing.js +22 -0
  87. package/dist/src/rules/html-no-self-closing.js.map +1 -0
  88. package/dist/src/rules/html-no-title-attribute.js +27 -0
  89. package/dist/src/rules/html-no-title-attribute.js.map +1 -0
  90. package/dist/src/rules/html-tag-name-lowercase.js +37 -25
  91. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  92. package/dist/src/rules/index.js +10 -0
  93. package/dist/src/rules/index.js.map +1 -1
  94. package/dist/src/rules/parser-no-errors.js +18 -0
  95. package/dist/src/rules/parser-no-errors.js.map +1 -0
  96. package/dist/src/rules/rule-utils.js +176 -22
  97. package/dist/src/rules/rule-utils.js.map +1 -1
  98. package/dist/src/rules/svg-tag-name-capitalization.js +2 -10
  99. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
  100. package/dist/src/types.js.map +1 -1
  101. package/dist/tsconfig.tsbuildinfo +1 -1
  102. package/dist/types/cli/argument-parser.d.ts +2 -1
  103. package/dist/types/cli/file-processor.d.ts +6 -5
  104. package/dist/types/cli/formatters/base-formatter.d.ts +2 -2
  105. package/dist/types/cli/formatters/detailed-formatter.d.ts +2 -2
  106. package/dist/types/cli/formatters/github-actions-formatter.d.ts +12 -0
  107. package/dist/types/cli/formatters/index.d.ts +2 -0
  108. package/dist/types/cli/formatters/json-formatter.d.ts +42 -0
  109. package/dist/types/cli/formatters/simple-formatter.d.ts +2 -2
  110. package/dist/types/cli/index.d.ts +4 -0
  111. package/dist/types/cli/output-manager.d.ts +31 -0
  112. package/dist/types/cli/summary-reporter.d.ts +3 -3
  113. package/dist/types/cli.d.ts +3 -1
  114. package/dist/types/rules/erb-no-empty-tags.d.ts +2 -2
  115. package/dist/types/rules/erb-no-output-control-flow.d.ts +2 -2
  116. package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
  117. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +2 -2
  118. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
  119. package/dist/types/rules/html-anchor-require-href.d.ts +2 -2
  120. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
  121. package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +7 -0
  122. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +2 -2
  123. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +2 -2
  124. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +2 -2
  125. package/dist/types/rules/html-attribute-double-quotes.d.ts +2 -2
  126. package/dist/types/rules/html-attribute-equals-spacing.d.ts +7 -0
  127. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +2 -2
  128. package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
  129. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +2 -2
  130. package/dist/types/rules/html-iframe-has-title.d.ts +7 -0
  131. package/dist/types/rules/html-img-require-alt.d.ts +2 -2
  132. package/dist/types/rules/html-navigation-has-label.d.ts +7 -0
  133. package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
  134. package/dist/types/rules/html-no-block-inside-inline.d.ts +2 -2
  135. package/dist/types/rules/html-no-duplicate-attributes.d.ts +2 -2
  136. package/dist/types/rules/html-no-duplicate-ids.d.ts +2 -2
  137. package/dist/types/rules/html-no-empty-headings.d.ts +2 -2
  138. package/dist/types/rules/html-no-nested-links.d.ts +2 -2
  139. package/dist/types/rules/html-no-positive-tab-index.d.ts +7 -0
  140. package/dist/types/rules/html-no-self-closing.d.ts +7 -0
  141. package/dist/types/rules/html-no-title-attribute.d.ts +7 -0
  142. package/dist/types/rules/html-tag-name-lowercase.d.ts +3 -2
  143. package/dist/types/rules/index.d.ts +10 -0
  144. package/dist/types/rules/parser-no-errors.d.ts +8 -0
  145. package/dist/types/rules/rule-utils.d.ts +107 -13
  146. package/dist/types/rules/svg-tag-name-capitalization.d.ts +2 -2
  147. package/dist/types/src/cli/argument-parser.d.ts +2 -1
  148. package/dist/types/src/cli/file-processor.d.ts +6 -5
  149. package/dist/types/src/cli/formatters/base-formatter.d.ts +2 -2
  150. package/dist/types/src/cli/formatters/detailed-formatter.d.ts +2 -2
  151. package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +12 -0
  152. package/dist/types/src/cli/formatters/index.d.ts +2 -0
  153. package/dist/types/src/cli/formatters/json-formatter.d.ts +42 -0
  154. package/dist/types/src/cli/formatters/simple-formatter.d.ts +2 -2
  155. package/dist/types/src/cli/output-manager.d.ts +31 -0
  156. package/dist/types/src/cli/summary-reporter.d.ts +3 -3
  157. package/dist/types/src/cli.d.ts +3 -1
  158. package/dist/types/src/rules/erb-no-empty-tags.d.ts +2 -2
  159. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +2 -2
  160. package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
  161. package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +2 -2
  162. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
  163. package/dist/types/src/rules/html-anchor-require-href.d.ts +2 -2
  164. package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
  165. package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +7 -0
  166. package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +2 -2
  167. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +2 -2
  168. package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +2 -2
  169. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +2 -2
  170. package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +7 -0
  171. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +2 -2
  172. package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
  173. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +2 -2
  174. package/dist/types/src/rules/html-iframe-has-title.d.ts +7 -0
  175. package/dist/types/src/rules/html-img-require-alt.d.ts +2 -2
  176. package/dist/types/src/rules/html-navigation-has-label.d.ts +7 -0
  177. package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
  178. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +2 -2
  179. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +2 -2
  180. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +2 -2
  181. package/dist/types/src/rules/html-no-empty-headings.d.ts +2 -2
  182. package/dist/types/src/rules/html-no-nested-links.d.ts +2 -2
  183. package/dist/types/src/rules/html-no-positive-tab-index.d.ts +7 -0
  184. package/dist/types/src/rules/html-no-self-closing.d.ts +7 -0
  185. package/dist/types/src/rules/html-no-title-attribute.d.ts +7 -0
  186. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +3 -2
  187. package/dist/types/src/rules/index.d.ts +10 -0
  188. package/dist/types/src/rules/parser-no-errors.d.ts +8 -0
  189. package/dist/types/src/rules/rule-utils.d.ts +107 -13
  190. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +2 -2
  191. package/dist/types/src/types.d.ts +27 -3
  192. package/dist/types/types.d.ts +27 -3
  193. package/docs/rules/README.md +13 -2
  194. package/docs/rules/erb-no-silent-tag-in-attribute-name.md +34 -0
  195. package/docs/rules/html-aria-label-is-well-formatted.md +49 -0
  196. package/docs/rules/html-attribute-equals-spacing.md +35 -0
  197. package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +48 -0
  198. package/docs/rules/html-iframe-has-title.md +43 -0
  199. package/docs/rules/html-navigation-has-label.md +61 -0
  200. package/docs/rules/html-no-aria-hidden-on-focusable.md +54 -0
  201. package/docs/rules/html-no-positive-tab-index.md +55 -0
  202. package/docs/rules/html-no-self-closing.md +65 -0
  203. package/docs/rules/html-no-title-attribute.md +69 -0
  204. package/docs/rules/html-tag-name-lowercase.md +16 -3
  205. package/docs/rules/parser-no-errors.md +84 -0
  206. package/package.json +4 -4
  207. package/src/cli/argument-parser.ts +33 -24
  208. package/src/cli/file-processor.ts +25 -17
  209. package/src/cli/formatters/base-formatter.ts +2 -2
  210. package/src/cli/formatters/detailed-formatter.ts +9 -9
  211. package/src/cli/formatters/github-actions-formatter.ts +70 -0
  212. package/src/cli/formatters/index.ts +2 -0
  213. package/src/cli/formatters/json-formatter.ts +107 -0
  214. package/src/cli/formatters/simple-formatter.ts +15 -15
  215. package/src/cli/output-manager.ts +143 -0
  216. package/src/cli/summary-reporter.ts +24 -24
  217. package/src/cli.ts +48 -31
  218. package/src/default-rules.ts +22 -0
  219. package/src/linter.ts +30 -4
  220. package/src/rules/erb-no-empty-tags.ts +3 -3
  221. package/src/rules/erb-no-output-control-flow.ts +3 -3
  222. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +40 -0
  223. package/src/rules/erb-prefer-image-tag-helper.ts +4 -9
  224. package/src/rules/erb-require-whitespace-inside-tags.ts +3 -3
  225. package/src/rules/erb-requires-trailing-newline.ts +2 -0
  226. package/src/rules/html-anchor-require-href.ts +3 -3
  227. package/src/rules/html-aria-attribute-must-be-valid.ts +29 -33
  228. package/src/rules/html-aria-label-is-well-formatted.ts +59 -0
  229. package/src/rules/html-aria-level-must-be-valid.ts +40 -7
  230. package/src/rules/html-aria-role-heading-requires-level.ts +18 -30
  231. package/src/rules/html-aria-role-must-be-valid.ts +7 -7
  232. package/src/rules/html-attribute-double-quotes.ts +23 -8
  233. package/src/rules/html-attribute-equals-spacing.ts +41 -0
  234. package/src/rules/html-attribute-values-require-quotes.ts +32 -12
  235. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +66 -0
  236. package/src/rules/html-boolean-attributes-no-value.ts +19 -6
  237. package/src/rules/html-iframe-has-title.ts +62 -0
  238. package/src/rules/html-img-require-alt.ts +4 -9
  239. package/src/rules/html-navigation-has-label.ts +64 -0
  240. package/src/rules/html-no-aria-hidden-on-focusable.ts +90 -0
  241. package/src/rules/html-no-block-inside-inline.ts +5 -5
  242. package/src/rules/html-no-duplicate-attributes.ts +30 -30
  243. package/src/rules/html-no-duplicate-ids.ts +6 -5
  244. package/src/rules/html-no-empty-headings.ts +4 -33
  245. package/src/rules/html-no-nested-links.ts +3 -3
  246. package/src/rules/html-no-positive-tab-index.ts +33 -0
  247. package/src/rules/html-no-self-closing.ts +36 -0
  248. package/src/rules/html-no-title-attribute.ts +42 -0
  249. package/src/rules/html-tag-name-lowercase.ts +44 -31
  250. package/src/rules/index.ts +10 -0
  251. package/src/rules/parser-no-errors.ts +25 -0
  252. package/src/rules/rule-utils.ts +260 -39
  253. package/src/rules/svg-tag-name-capitalization.ts +4 -11
  254. package/src/types.ts +30 -3
@@ -1,46 +1,44 @@
1
- import { BaseRuleVisitor, forEachAttribute } from "./rule-utils.js"
2
-
3
1
  import { ParserRule } from "../types.js"
2
+ import { AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams } from "./rule-utils.js"
3
+
4
4
  import type { LintOffense, LintContext } from "../types.js"
5
- import type { HTMLOpenTagNode, HTMLSelfCloseTagNode, HTMLAttributeNameNode, Node } from "@herb-tools/core"
5
+ import type { HTMLOpenTagNode, HTMLAttributeNode, ParseResult } from "@herb-tools/core"
6
+
7
+ class NoDuplicateAttributesVisitor extends AttributeVisitorMixin {
8
+ private attributeNames = new Map<string, HTMLAttributeNode[]>()
6
9
 
7
- class NoDuplicateAttributesVisitor extends BaseRuleVisitor {
8
10
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
9
- this.checkDuplicateAttributes(node)
11
+ this.attributeNames.clear()
10
12
  super.visitHTMLOpenTagNode(node)
13
+ this.reportDuplicates()
11
14
  }
12
15
 
13
- visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
14
- this.checkDuplicateAttributes(node)
15
- super.visitHTMLSelfCloseTagNode(node)
16
- }
17
16
 
18
- private checkDuplicateAttributes(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): void {
19
- const attributeNames = new Map<string, HTMLAttributeNameNode[]>()
20
-
21
- forEachAttribute(node, (attributeNode) => {
22
- if (attributeNode.name?.type !== "AST_HTML_ATTRIBUTE_NAME_NODE") return
23
-
24
- const nameNode = attributeNode.name as HTMLAttributeNameNode
25
- if (!nameNode.name) return
17
+ protected checkStaticAttributeStaticValue({ attributeName, attributeNode }: StaticAttributeStaticValueParams): void {
18
+ this.trackAttributeName(attributeName, attributeNode)
19
+ }
26
20
 
27
- const attributeName = nameNode.name.value.toLowerCase() // HTML attributes are case-insensitive
21
+ protected checkStaticAttributeDynamicValue({ attributeName, attributeNode }: StaticAttributeDynamicValueParams): void {
22
+ this.trackAttributeName(attributeName, attributeNode)
23
+ }
28
24
 
29
- if (!attributeNames.has(attributeName)) {
30
- attributeNames.set(attributeName, [])
31
- }
25
+ private trackAttributeName(attributeName: string, attributeNode: HTMLAttributeNode): void {
26
+ if (!this.attributeNames.has(attributeName)) {
27
+ this.attributeNames.set(attributeName, [])
28
+ }
32
29
 
33
- attributeNames.get(attributeName)!.push(nameNode)
34
- })
30
+ this.attributeNames.get(attributeName)!.push(attributeNode)
31
+ }
35
32
 
36
- for (const [attributeName, nameNodes] of attributeNames) {
37
- if (nameNodes.length > 1) {
38
- for (let i = 1; i < nameNodes.length; i++) {
39
- const nameNode = nameNodes[i]
33
+ private reportDuplicates(): void {
34
+ for (const [attributeName, attributeNodes] of this.attributeNames) {
35
+ if (attributeNodes.length > 1) {
36
+ for (let i = 1; i < attributeNodes.length; i++) {
37
+ const attributeNode = attributeNodes[i]
40
38
 
41
39
  this.addOffense(
42
40
  `Duplicate attribute \`${attributeName}\` found on tag. Remove the duplicate occurrence.`,
43
- nameNode.location,
41
+ attributeNode.name!.location,
44
42
  "error"
45
43
  )
46
44
  }
@@ -52,9 +50,11 @@ class NoDuplicateAttributesVisitor extends BaseRuleVisitor {
52
50
  export class HTMLNoDuplicateAttributesRule extends ParserRule {
53
51
  name = "html-no-duplicate-attributes"
54
52
 
55
- check(node: Node, context?: Partial<LintContext>): LintOffense[] {
53
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
56
54
  const visitor = new NoDuplicateAttributesVisitor(this.name, context)
57
- visitor.visit(node)
55
+
56
+ visitor.visit(result.value)
57
+
58
58
  return visitor.offenses
59
59
  }
60
60
  }
@@ -1,12 +1,13 @@
1
- import { AttributeVisitorMixin } from "./rule-utils"
2
1
  import { ParserRule } from "../types"
3
- import type { Node } from "@herb-tools/core"
2
+ import { AttributeVisitorMixin, StaticAttributeStaticValueParams } from "./rule-utils"
3
+
4
+ import type { ParseResult } from "@herb-tools/core"
4
5
  import type { LintOffense, LintContext } from "../types"
5
6
 
6
7
  class NoDuplicateIdsVisitor extends AttributeVisitorMixin {
7
8
  private documentIds: Set<string> = new Set<string>()
8
9
 
9
- protected checkAttribute(attributeName: string, attributeValue: string | null, attributeNode: Node): void {
10
+ protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams): void {
10
11
  if (attributeName.toLowerCase() !== "id") return
11
12
  if (!attributeValue) return
12
13
 
@@ -29,10 +30,10 @@ class NoDuplicateIdsVisitor extends AttributeVisitorMixin {
29
30
  export class HTMLNoDuplicateIdsRule extends ParserRule {
30
31
  name = "html-no-duplicate-ids"
31
32
 
32
- check(node: Node, context?: Partial<LintContext>): LintOffense[] {
33
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
33
34
  const visitor = new NoDuplicateIdsVisitor(this.name, context)
34
35
 
35
- visitor.visit(node)
36
+ visitor.visit(result.value)
36
37
 
37
38
  return visitor.offenses
38
39
  }
@@ -2,7 +2,7 @@ import { BaseRuleVisitor, getTagName, getAttributes, findAttributeByName, getAtt
2
2
 
3
3
  import { ParserRule } from "../types.js"
4
4
  import type { LintOffense, LintContext } from "../types.js"
5
- import type { HTMLElementNode, HTMLOpenTagNode, HTMLSelfCloseTagNode, Node, LiteralNode, HTMLTextNode } from "@herb-tools/core"
5
+ import type { HTMLElementNode, HTMLOpenTagNode, ParseResult, LiteralNode, HTMLTextNode } from "@herb-tools/core"
6
6
 
7
7
  class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
8
8
  visitHTMLElementNode(node: HTMLElementNode): void {
@@ -10,10 +10,6 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
10
10
  super.visitHTMLElementNode(node)
11
11
  }
12
12
 
13
- visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
14
- this.checkSelfClosingHeading(node)
15
- super.visitHTMLSelfCloseTagNode(node)
16
- }
17
13
 
18
14
  private checkHeadingElement(node: HTMLElementNode): void {
19
15
  if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
@@ -47,31 +43,6 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
47
43
  }
48
44
  }
49
45
 
50
- private checkSelfClosingHeading(node: HTMLSelfCloseTagNode): void {
51
- const tagName = getTagName(node)
52
- if (!tagName) {
53
- return
54
- }
55
-
56
- // Check if it's a standard heading tag (h1-h6) or has role="heading"
57
- const isStandardHeading = HEADING_TAGS.has(tagName)
58
- const isAriaHeading = this.hasHeadingRole(node)
59
-
60
- if (!isStandardHeading && !isAriaHeading) {
61
- return
62
- }
63
-
64
- // Self-closing headings are always empty
65
- const elementDescription = isStandardHeading
66
- ? `\`<${tagName}>\``
67
- : `\`<${tagName} role="heading">\``
68
-
69
- this.addOffense(
70
- `Heading element ${elementDescription} must not be empty. Provide accessible text content for screen readers and SEO.`,
71
- node.tag_name!.location,
72
- "error"
73
- )
74
- }
75
46
 
76
47
  private isEmptyHeading(node: HTMLElementNode): boolean {
77
48
  if (!node.body || node.body.length === 0) {
@@ -114,7 +85,7 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
114
85
  return !hasAccessibleContent
115
86
  }
116
87
 
117
- private hasHeadingRole(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): boolean {
88
+ private hasHeadingRole(node: HTMLOpenTagNode): boolean {
118
89
  const attributes = getAttributes(node)
119
90
  const roleAttribute = findAttributeByName(attributes, "role")
120
91
 
@@ -178,9 +149,9 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
178
149
  export class HTMLNoEmptyHeadingsRule extends ParserRule {
179
150
  name = "html-no-empty-headings"
180
151
 
181
- check(node: Node, context?: Partial<LintContext>): LintOffense[] {
152
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
182
153
  const visitor = new NoEmptyHeadingsVisitor(this.name, context)
183
- visitor.visit(node)
154
+ visitor.visit(result.value)
184
155
  return visitor.offenses
185
156
  }
186
157
  }
@@ -2,7 +2,7 @@ import { BaseRuleVisitor, getTagName } from "./rule-utils.js"
2
2
 
3
3
  import { ParserRule } from "../types.js"
4
4
  import type { LintOffense, LintContext } from "../types.js"
5
- import type { HTMLOpenTagNode, HTMLElementNode, Node } from "@herb-tools/core"
5
+ import type { HTMLOpenTagNode, HTMLElementNode, ParseResult } from "@herb-tools/core"
6
6
 
7
7
  class NestedLinkVisitor extends BaseRuleVisitor {
8
8
  private linkStack: HTMLOpenTagNode[] = []
@@ -58,9 +58,9 @@ class NestedLinkVisitor extends BaseRuleVisitor {
58
58
  export class HTMLNoNestedLinksRule extends ParserRule {
59
59
  name = "html-no-nested-links"
60
60
 
61
- check(node: Node, context?: Partial<LintContext>): LintOffense[] {
61
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
62
62
  const visitor = new NestedLinkVisitor(this.name, context)
63
- visitor.visit(node)
63
+ visitor.visit(result.value)
64
64
  return visitor.offenses
65
65
  }
66
66
  }
@@ -0,0 +1,33 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { AttributeVisitorMixin, StaticAttributeStaticValueParams } from "./rule-utils.js"
3
+
4
+ import type { LintOffense, LintContext } from "../types.js"
5
+ import type { ParseResult } from "@herb-tools/core"
6
+
7
+ class NoPositiveTabIndexVisitor extends AttributeVisitorMixin {
8
+ protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams): void {
9
+ if (attributeName !== "tabindex") return
10
+
11
+ const tabIndexValue = parseInt(attributeValue, 10)
12
+
13
+ if (!isNaN(tabIndexValue) && tabIndexValue > 0) {
14
+ this.addOffense(
15
+ `Do not use positive \`tabindex\` values as they are error prone and can severely disrupt navigation experience for keyboard users. Use \`tabindex="0"\` to make an element focusable or \`tabindex=\"-1\"\` to remove it from the tab sequence.`,
16
+ attributeNode.location,
17
+ "error"
18
+ )
19
+ }
20
+ }
21
+ }
22
+
23
+ export class HTMLNoPositiveTabIndexRule extends ParserRule {
24
+ name = "html-no-positive-tab-index"
25
+
26
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
27
+ const visitor = new NoPositiveTabIndexVisitor(this.name, context)
28
+
29
+ visitor.visit(result.value)
30
+
31
+ return visitor.offenses
32
+ }
33
+ }
@@ -0,0 +1,36 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { BaseRuleVisitor, getTagName, isVoidElement } from "./rule-utils.js"
3
+
4
+ import type { LintContext, LintOffense } from "../types.js"
5
+ import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
6
+
7
+ class NoSelfClosingVisitor extends BaseRuleVisitor {
8
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
9
+ if (node.tag_closing?.value === "/>") {
10
+ const tagName = getTagName(node)
11
+
12
+ const shouldBeVoid = tagName ? isVoidElement(tagName) : false
13
+ const instead = shouldBeVoid ? `Use \`<${tagName}>\` instead.` : `Use \`<${tagName}></${tagName}>\` instead.`
14
+
15
+ this.addOffense(
16
+ `Self-closing syntax \`<${tagName} />\` is not allowed in HTML. ${instead}`,
17
+ node.location,
18
+ "error"
19
+ )
20
+ }
21
+
22
+ super.visitHTMLOpenTagNode(node)
23
+ }
24
+ }
25
+
26
+ export class HTMLNoSelfClosingRule extends ParserRule {
27
+ name = "html-no-self-closing"
28
+
29
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
30
+ const visitor = new NoSelfClosingVisitor(this.name, context)
31
+
32
+ visitor.visit(result.value)
33
+
34
+ return visitor.offenses
35
+ }
36
+ }
@@ -0,0 +1,42 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { BaseRuleVisitor, getTagName, hasAttribute } from "./rule-utils.js"
3
+
4
+ import type { LintOffense, LintContext } from "../types.js"
5
+ import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
6
+
7
+ class NoTitleAttributeVisitor extends BaseRuleVisitor {
8
+ ALLOWED_ELEMENTS_WITH_TITLE = new Set(["iframe", "link"])
9
+
10
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
11
+ this.checkTitleAttribute(node)
12
+ super.visitHTMLOpenTagNode(node)
13
+ }
14
+
15
+ private checkTitleAttribute(node: HTMLOpenTagNode): void {
16
+ const tagName = getTagName(node)
17
+
18
+ if (!tagName || this.ALLOWED_ELEMENTS_WITH_TITLE.has(tagName)) {
19
+ return
20
+ }
21
+
22
+ if (hasAttribute(node, "title")) {
23
+ this.addOffense(
24
+ "The `title` attribute should never be used as it is inaccessible for several groups of users. Use `aria-label` or `aria-describedby` instead. Exceptions are provided for `<iframe>` and `<link>` elements.",
25
+ node.tag_name!.location,
26
+ "error"
27
+ )
28
+ }
29
+ }
30
+ }
31
+
32
+ export class HTMLNoTitleAttributeRule extends ParserRule {
33
+ name = "html-no-title-attribute"
34
+
35
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
36
+ const visitor = new NoTitleAttributeVisitor(this.name, context)
37
+
38
+ visitor.visit(result.value)
39
+
40
+ return visitor.offenses
41
+ }
42
+ }
@@ -1,53 +1,56 @@
1
+ import { ParserRule } from "../types.js"
1
2
  import { BaseRuleVisitor } from "./rule-utils.js"
2
3
 
3
- import { ParserRule } from "../types.js"
4
+ import { isNode, getTagName, HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, ParseResult, XMLDeclarationNode, Node } from "@herb-tools/core"
5
+
4
6
  import type { LintOffense, LintContext } from "../types.js"
5
- import type { HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, HTMLSelfCloseTagNode, Node } from "@herb-tools/core"
6
7
 
7
- class TagNameLowercaseVisitor extends BaseRuleVisitor {
8
- visitHTMLElementNode(node: HTMLElementNode): void {
9
- const tagName = node.tag_name?.value
8
+ class XMLDeclarationChecker extends BaseRuleVisitor {
9
+ hasXMLDeclaration: boolean = false
10
10
 
11
- if (node.open_tag) {
12
- this.checkTagName(node.open_tag as HTMLOpenTagNode)
13
- }
11
+ visitXMLDeclarationNode(_node: XMLDeclarationNode): void {
12
+ this.hasXMLDeclaration = true
13
+ }
14
14
 
15
- if (tagName && ["svg"].includes(tagName.toLowerCase())) {
16
- if (node.close_tag) {
17
- this.checkTagName(node.close_tag as HTMLCloseTagNode)
18
- }
15
+ visitChildNodes(node: Node): void {
16
+ if (this.hasXMLDeclaration) return
17
+ super.visitChildNodes(node)
18
+ }
19
+ }
19
20
 
20
- return
21
+ class TagNameLowercaseVisitor extends BaseRuleVisitor {
22
+ visitHTMLElementNode(node: HTMLElementNode): void {
23
+ if (getTagName(node).toLowerCase() === "svg") {
24
+ this.checkTagName(node.open_tag)
25
+ this.checkTagName(node.close_tag)
26
+ } else {
27
+ super.visitHTMLElementNode(node)
21
28
  }
29
+ }
22
30
 
23
- this.visitChildNodes(node)
24
-
25
- if (node.close_tag) {
26
- this.checkTagName(node.close_tag as HTMLCloseTagNode)
27
- }
31
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode) {
32
+ this.checkTagName(node)
28
33
  }
29
34
 
30
- visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
35
+ visitHTMLCloseTagNode(node: HTMLCloseTagNode) {
31
36
  this.checkTagName(node)
32
- this.visitChildNodes(node)
33
37
  }
34
38
 
35
- private checkTagName(node: HTMLOpenTagNode | HTMLCloseTagNode | HTMLSelfCloseTagNode): void {
36
- const tagName = node.tag_name?.value
39
+ private checkTagName(node: HTMLOpenTagNode | HTMLCloseTagNode | null): void {
40
+ if (!node) return
41
+
42
+ const tagName = getTagName(node)
37
43
 
38
44
  if (!tagName) return
39
45
 
40
46
  const lowercaseTagName = tagName.toLowerCase()
41
47
 
42
- if (tagName !== lowercaseTagName) {
43
- let type: string = node.type
44
-
45
- if (node.type == "AST_HTML_OPEN_TAG_NODE") type = "Opening"
46
- if (node.type == "AST_HTML_CLOSE_TAG_NODE") type = "Closing"
47
- if (node.type == "AST_HTML_SELF_CLOSE_TAG_NODE") type = "Self-closing"
48
+ const type = isNode(node, HTMLOpenTagNode) ? "Opening" : "Closing"
49
+ const open = isNode(node, HTMLOpenTagNode) ? "<" : "</"
48
50
 
51
+ if (tagName !== lowercaseTagName) {
49
52
  this.addOffense(
50
- `${type} tag name \`${tagName}\` should be lowercase. Use \`${lowercaseTagName}\` instead.`,
53
+ `${type} tag name \`${open}${tagName}>\` should be lowercase. Use \`${open}${lowercaseTagName}>\` instead.`,
51
54
  node.tag_name!.location,
52
55
  "error"
53
56
  )
@@ -58,9 +61,19 @@ class TagNameLowercaseVisitor extends BaseRuleVisitor {
58
61
  export class HTMLTagNameLowercaseRule extends ParserRule {
59
62
  name = "html-tag-name-lowercase"
60
63
 
61
- check(node: Node, context?: Partial<LintContext>): LintOffense[] {
64
+ isEnabled(result: ParseResult, context?: Partial<LintContext>): boolean {
65
+ if (context?.fileName?.endsWith(".xml") || context?.fileName?.endsWith(".xml.erb")) {
66
+ return false
67
+ }
68
+
69
+ const checker = new XMLDeclarationChecker(this.name)
70
+ checker.visit(result.value)
71
+ return !checker.hasXMLDeclaration
72
+ }
73
+
74
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
62
75
  const visitor = new TagNameLowercaseVisitor(this.name, context)
63
- visitor.visit(node)
76
+ visitor.visit(result.value)
64
77
  return visitor.offenses
65
78
  }
66
79
  }
@@ -1,19 +1,29 @@
1
1
  export * from "./erb-no-empty-tags.js"
2
2
  export * from "./erb-no-output-control-flow.js"
3
+ export * from "./erb-no-silent-tag-in-attribute-name.js"
3
4
  export * from "./erb-prefer-image-tag-helper.js"
4
5
  export * from "./erb-requires-trailing-newline.js"
5
6
  export * from "./html-anchor-require-href.js"
7
+ export * from "./html-aria-label-is-well-formatted.js"
6
8
  export * from "./html-aria-level-must-be-valid.js"
7
9
  export * from "./html-aria-role-heading-requires-level.js"
8
10
  export * from "./html-aria-role-must-be-valid.js"
9
11
  export * from "./html-attribute-double-quotes.js"
12
+ export * from "./html-attribute-equals-spacing.js"
10
13
  export * from "./html-attribute-values-require-quotes.js"
14
+ export * from "./html-avoid-both-disabled-and-aria-disabled.js"
11
15
  export * from "./html-boolean-attributes-no-value.js"
16
+ export * from "./html-iframe-has-title.js"
12
17
  export * from "./html-img-require-alt.js"
18
+ export * from "./html-navigation-has-label.js"
19
+ export * from "./html-no-aria-hidden-on-focusable.js"
13
20
  export * from "./html-no-block-inside-inline.js"
14
21
  export * from "./html-no-duplicate-attributes.js"
15
22
  export * from "./html-no-duplicate-ids.js"
16
23
  export * from "./html-no-empty-headings.js"
17
24
  export * from "./html-no-nested-links.js"
25
+ export * from "./html-no-positive-tab-index.js"
26
+ export * from "./html-no-self-closing.js"
27
+ export * from "./html-no-title-attribute.js"
18
28
  export * from "./html-tag-name-lowercase.js"
19
29
  export * from "./svg-tag-name-capitalization.js"
@@ -0,0 +1,25 @@
1
+ import { ParserRule } from "../types.js"
2
+
3
+ import type { LintOffense } from "../types.js"
4
+ import type { ParseResult, HerbError } from "@herb-tools/core"
5
+
6
+ export class ParserNoErrorsRule extends ParserRule {
7
+ name = "parser-no-errors"
8
+
9
+ check(result: ParseResult): LintOffense[] {
10
+ return result.recursiveErrors().map(error =>
11
+ this.herbErrorToLintOffense(error)
12
+ )
13
+ }
14
+
15
+ private herbErrorToLintOffense(error: HerbError): LintOffense {
16
+ return {
17
+ message: `${error.message} (\`${error.type}\`)`,
18
+ location: error.location,
19
+ severity: error.severity,
20
+ rule: this.name,
21
+ code: this.name,
22
+ source: "linter"
23
+ }
24
+ }
25
+ }