@herb-tools/linter 0.7.5 → 0.8.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 (394) hide show
  1. package/README.md +253 -13
  2. package/dist/herb-lint.js +26023 -3424
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +5759 -1583
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +5727 -1584
  7. package/dist/index.js.map +1 -1
  8. package/dist/loader.cjs +17010 -0
  9. package/dist/loader.cjs.map +1 -0
  10. package/dist/loader.js +16879 -0
  11. package/dist/loader.js.map +1 -0
  12. package/dist/package.json +13 -5
  13. package/dist/src/cli/argument-parser.js +38 -33
  14. package/dist/src/cli/argument-parser.js.map +1 -1
  15. package/dist/src/cli/file-processor.js +124 -23
  16. package/dist/src/cli/file-processor.js.map +1 -1
  17. package/dist/src/cli/formatters/detailed-formatter.js +18 -3
  18. package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
  19. package/dist/src/cli/formatters/github-actions-formatter.js +15 -1
  20. package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -1
  21. package/dist/src/cli/formatters/json-formatter.js +3 -0
  22. package/dist/src/cli/formatters/json-formatter.js.map +1 -1
  23. package/dist/src/cli/formatters/simple-formatter.js +20 -7
  24. package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
  25. package/dist/src/cli/output-manager.js +22 -3
  26. package/dist/src/cli/output-manager.js.map +1 -1
  27. package/dist/src/cli/summary-reporter.js +26 -3
  28. package/dist/src/cli/summary-reporter.js.map +1 -1
  29. package/dist/src/cli.js +107 -42
  30. package/dist/src/cli.js.map +1 -1
  31. package/dist/src/custom-rule-loader.js +139 -0
  32. package/dist/src/custom-rule-loader.js.map +1 -0
  33. package/dist/src/herb-disable-comment-utils.js +129 -0
  34. package/dist/src/herb-disable-comment-utils.js.map +1 -0
  35. package/dist/src/index.js +1 -0
  36. package/dist/src/index.js.map +1 -1
  37. package/dist/src/linter.js +369 -34
  38. package/dist/src/linter.js.map +1 -1
  39. package/dist/src/loader.js +17 -0
  40. package/dist/src/loader.js.map +1 -0
  41. package/dist/src/rules/erb-comment-syntax.js +31 -2
  42. package/dist/src/rules/erb-comment-syntax.js.map +1 -1
  43. package/dist/src/rules/erb-no-case-node-children.js +52 -0
  44. package/dist/src/rules/erb-no-case-node-children.js.map +1 -0
  45. package/dist/src/rules/erb-no-empty-tags.js +7 -1
  46. package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
  47. package/dist/src/rules/erb-no-extra-newline.js +65 -0
  48. package/dist/src/rules/erb-no-extra-newline.js.map +1 -0
  49. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js +95 -0
  50. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js.map +1 -0
  51. package/dist/src/rules/erb-no-output-control-flow.js +7 -1
  52. package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
  53. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js +7 -1
  54. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -1
  55. package/dist/src/rules/erb-prefer-image-tag-helper.js +7 -1
  56. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
  57. package/dist/src/rules/erb-require-trailing-newline.js +35 -0
  58. package/dist/src/rules/erb-require-trailing-newline.js.map +1 -0
  59. package/dist/src/rules/erb-require-whitespace-inside-tags.js +69 -11
  60. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  61. package/dist/src/rules/erb-right-trim.js +26 -9
  62. package/dist/src/rules/erb-right-trim.js.map +1 -1
  63. package/dist/src/rules/herb-disable-comment-base.js +51 -0
  64. package/dist/src/rules/herb-disable-comment-base.js.map +1 -0
  65. package/dist/src/rules/herb-disable-comment-malformed.js +51 -0
  66. package/dist/src/rules/herb-disable-comment-malformed.js.map +1 -0
  67. package/dist/src/rules/herb-disable-comment-missing-rules.js +29 -0
  68. package/dist/src/rules/herb-disable-comment-missing-rules.js.map +1 -0
  69. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js +32 -0
  70. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js.map +1 -0
  71. package/dist/src/rules/herb-disable-comment-no-redundant-all.js +31 -0
  72. package/dist/src/rules/herb-disable-comment-no-redundant-all.js.map +1 -0
  73. package/dist/src/rules/herb-disable-comment-unnecessary.js +65 -0
  74. package/dist/src/rules/herb-disable-comment-unnecessary.js.map +1 -0
  75. package/dist/src/rules/herb-disable-comment-valid-rule-name.js +44 -0
  76. package/dist/src/rules/herb-disable-comment-valid-rule-name.js.map +1 -0
  77. package/dist/src/rules/html-anchor-require-href.js +7 -1
  78. package/dist/src/rules/html-anchor-require-href.js.map +1 -1
  79. package/dist/src/rules/html-aria-attribute-must-be-valid.js +7 -1
  80. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
  81. package/dist/src/rules/html-aria-label-is-well-formatted.js +9 -3
  82. package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -1
  83. package/dist/src/rules/html-aria-level-must-be-valid.js +6 -0
  84. package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
  85. package/dist/src/rules/html-aria-role-heading-requires-level.js +7 -1
  86. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
  87. package/dist/src/rules/html-aria-role-must-be-valid.js +7 -1
  88. package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
  89. package/dist/src/rules/html-attribute-double-quotes.js +29 -2
  90. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
  91. package/dist/src/rules/html-attribute-equals-spacing.js +18 -2
  92. package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -1
  93. package/dist/src/rules/html-attribute-values-require-quotes.js +39 -3
  94. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
  95. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js +7 -1
  96. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -1
  97. package/dist/src/rules/html-body-only-elements.js +46 -0
  98. package/dist/src/rules/html-body-only-elements.js.map +1 -0
  99. package/dist/src/rules/html-boolean-attributes-no-value.js +18 -1
  100. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  101. package/dist/src/rules/html-head-only-elements.js +51 -0
  102. package/dist/src/rules/html-head-only-elements.js.map +1 -0
  103. package/dist/src/rules/html-iframe-has-title.js +8 -2
  104. package/dist/src/rules/html-iframe-has-title.js.map +1 -1
  105. package/dist/src/rules/html-img-require-alt.js +7 -1
  106. package/dist/src/rules/html-img-require-alt.js.map +1 -1
  107. package/dist/src/rules/html-input-require-autocomplete.js +70 -0
  108. package/dist/src/rules/html-input-require-autocomplete.js.map +1 -0
  109. package/dist/src/rules/html-navigation-has-label.js +7 -1
  110. package/dist/src/rules/html-navigation-has-label.js.map +1 -1
  111. package/dist/src/rules/html-no-aria-hidden-on-focusable.js +7 -1
  112. package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -1
  113. package/dist/src/rules/html-no-block-inside-inline.js +7 -1
  114. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
  115. package/dist/src/rules/html-no-duplicate-attributes.js +7 -1
  116. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
  117. package/dist/src/rules/html-no-duplicate-ids.js +9 -3
  118. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
  119. package/dist/src/rules/html-no-duplicate-meta-names.js +136 -0
  120. package/dist/src/rules/html-no-duplicate-meta-names.js.map +1 -0
  121. package/dist/src/rules/html-no-empty-attributes.js +45 -7
  122. package/dist/src/rules/html-no-empty-attributes.js.map +1 -1
  123. package/dist/src/rules/html-no-empty-headings.js +7 -6
  124. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  125. package/dist/src/rules/html-no-nested-links.js +7 -1
  126. package/dist/src/rules/html-no-nested-links.js.map +1 -1
  127. package/dist/src/rules/html-no-positive-tab-index.js +7 -1
  128. package/dist/src/rules/html-no-positive-tab-index.js.map +1 -1
  129. package/dist/src/rules/html-no-self-closing.js +48 -3
  130. package/dist/src/rules/html-no-self-closing.js.map +1 -1
  131. package/dist/src/rules/html-no-space-in-tag.js +173 -0
  132. package/dist/src/rules/html-no-space-in-tag.js.map +1 -0
  133. package/dist/src/rules/html-no-title-attribute.js +7 -1
  134. package/dist/src/rules/html-no-title-attribute.js.map +1 -1
  135. package/dist/src/rules/html-no-underscores-in-attribute-names.js +7 -1
  136. package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +1 -1
  137. package/dist/src/rules/html-tag-name-lowercase.js +23 -5
  138. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  139. package/dist/src/rules/index.js +19 -3
  140. package/dist/src/rules/index.js.map +1 -1
  141. package/dist/src/rules/parser-no-errors.js +6 -0
  142. package/dist/src/rules/parser-no-errors.js.map +1 -1
  143. package/dist/src/rules/rule-utils.js +211 -31
  144. package/dist/src/rules/rule-utils.js.map +1 -1
  145. package/dist/src/rules/svg-tag-name-capitalization.js +22 -2
  146. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
  147. package/dist/src/{default-rules.js → rules.js} +44 -16
  148. package/dist/src/rules.js.map +1 -0
  149. package/dist/src/types.js +34 -1
  150. package/dist/src/types.js.map +1 -1
  151. package/dist/tsconfig.tsbuildinfo +1 -1
  152. package/dist/types/cli/argument-parser.d.ts +8 -2
  153. package/dist/types/cli/file-processor.d.ts +15 -0
  154. package/dist/types/cli/formatters/json-formatter.d.ts +6 -0
  155. package/dist/types/cli/formatters/simple-formatter.d.ts +1 -0
  156. package/dist/types/cli/summary-reporter.d.ts +6 -0
  157. package/dist/types/cli.d.ts +9 -4
  158. package/dist/types/custom-rule-loader.d.ts +62 -0
  159. package/dist/types/herb-disable-comment-utils.d.ts +69 -0
  160. package/dist/types/index.d.ts +1 -0
  161. package/dist/types/linter.d.ts +99 -3
  162. package/dist/types/loader.d.ts +20 -0
  163. package/dist/types/rules/erb-comment-syntax.d.ts +12 -5
  164. package/dist/types/rules/erb-no-case-node-children.d.ts +8 -0
  165. package/dist/types/rules/erb-no-empty-tags.d.ts +3 -2
  166. package/dist/types/rules/erb-no-extra-newline.d.ts +14 -0
  167. package/dist/types/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
  168. package/dist/types/rules/erb-no-output-control-flow.d.ts +3 -2
  169. package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
  170. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +3 -2
  171. package/dist/types/rules/erb-require-trailing-newline.d.ts +9 -0
  172. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
  173. package/dist/types/rules/erb-right-trim.d.ts +12 -5
  174. package/dist/types/rules/herb-disable-comment-base.d.ts +37 -0
  175. package/dist/types/rules/herb-disable-comment-malformed.d.ts +8 -0
  176. package/dist/types/rules/herb-disable-comment-missing-rules.d.ts +8 -0
  177. package/dist/types/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
  178. package/dist/types/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
  179. package/dist/types/rules/herb-disable-comment-unnecessary.d.ts +8 -0
  180. package/dist/types/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
  181. package/dist/types/rules/html-anchor-require-href.d.ts +3 -2
  182. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
  183. package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +3 -2
  184. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +3 -2
  185. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +3 -2
  186. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +3 -2
  187. package/dist/types/rules/html-attribute-double-quotes.d.ts +13 -5
  188. package/dist/types/rules/html-attribute-equals-spacing.d.ts +12 -5
  189. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +13 -5
  190. package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
  191. package/dist/types/rules/html-body-only-elements.d.ts +9 -0
  192. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +12 -5
  193. package/dist/types/rules/html-head-only-elements.d.ts +9 -0
  194. package/dist/types/rules/html-iframe-has-title.d.ts +3 -2
  195. package/dist/types/rules/html-img-require-alt.d.ts +3 -2
  196. package/dist/types/rules/html-input-require-autocomplete.d.ts +8 -0
  197. package/dist/types/rules/html-navigation-has-label.d.ts +3 -2
  198. package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
  199. package/dist/types/rules/html-no-block-inside-inline.d.ts +3 -2
  200. package/dist/types/rules/html-no-duplicate-attributes.d.ts +3 -2
  201. package/dist/types/rules/html-no-duplicate-ids.d.ts +3 -2
  202. package/dist/types/rules/html-no-duplicate-meta-names.d.ts +9 -0
  203. package/dist/types/rules/html-no-empty-attributes.d.ts +3 -2
  204. package/dist/types/rules/html-no-empty-headings.d.ts +3 -2
  205. package/dist/types/rules/html-no-nested-links.d.ts +3 -2
  206. package/dist/types/rules/html-no-positive-tab-index.d.ts +3 -2
  207. package/dist/types/rules/html-no-self-closing.d.ts +14 -5
  208. package/dist/types/rules/html-no-space-in-tag.d.ts +16 -0
  209. package/dist/types/rules/html-no-title-attribute.d.ts +3 -2
  210. package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
  211. package/dist/types/rules/html-tag-name-lowercase.d.ts +16 -6
  212. package/dist/types/rules/index.d.ts +19 -3
  213. package/dist/types/rules/parser-no-errors.d.ts +2 -1
  214. package/dist/types/rules/rule-utils.d.ts +72 -25
  215. package/dist/types/rules/svg-tag-name-capitalization.d.ts +13 -4
  216. package/dist/types/rules.d.ts +2 -0
  217. package/dist/types/src/cli/argument-parser.d.ts +8 -2
  218. package/dist/types/src/cli/file-processor.d.ts +15 -0
  219. package/dist/types/src/cli/formatters/json-formatter.d.ts +6 -0
  220. package/dist/types/src/cli/formatters/simple-formatter.d.ts +1 -0
  221. package/dist/types/src/cli/summary-reporter.d.ts +6 -0
  222. package/dist/types/src/cli.d.ts +9 -4
  223. package/dist/types/src/custom-rule-loader.d.ts +62 -0
  224. package/dist/types/src/herb-disable-comment-utils.d.ts +69 -0
  225. package/dist/types/src/index.d.ts +1 -0
  226. package/dist/types/src/linter.d.ts +99 -3
  227. package/dist/types/src/loader.d.ts +20 -0
  228. package/dist/types/src/rules/erb-comment-syntax.d.ts +12 -5
  229. package/dist/types/src/rules/erb-no-case-node-children.d.ts +8 -0
  230. package/dist/types/src/rules/erb-no-empty-tags.d.ts +3 -2
  231. package/dist/types/src/rules/erb-no-extra-newline.d.ts +14 -0
  232. package/dist/types/src/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
  233. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +3 -2
  234. package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
  235. package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +3 -2
  236. package/dist/types/src/rules/erb-require-trailing-newline.d.ts +9 -0
  237. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
  238. package/dist/types/src/rules/erb-right-trim.d.ts +12 -5
  239. package/dist/types/src/rules/herb-disable-comment-base.d.ts +37 -0
  240. package/dist/types/src/rules/herb-disable-comment-malformed.d.ts +8 -0
  241. package/dist/types/src/rules/herb-disable-comment-missing-rules.d.ts +8 -0
  242. package/dist/types/src/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
  243. package/dist/types/src/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
  244. package/dist/types/src/rules/herb-disable-comment-unnecessary.d.ts +8 -0
  245. package/dist/types/src/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
  246. package/dist/types/src/rules/html-anchor-require-href.d.ts +3 -2
  247. package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
  248. package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +3 -2
  249. package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +3 -2
  250. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +3 -2
  251. package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +3 -2
  252. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +13 -5
  253. package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +12 -5
  254. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +13 -5
  255. package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
  256. package/dist/types/src/rules/html-body-only-elements.d.ts +9 -0
  257. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +12 -5
  258. package/dist/types/src/rules/html-head-only-elements.d.ts +9 -0
  259. package/dist/types/src/rules/html-iframe-has-title.d.ts +3 -2
  260. package/dist/types/src/rules/html-img-require-alt.d.ts +3 -2
  261. package/dist/types/src/rules/html-input-require-autocomplete.d.ts +8 -0
  262. package/dist/types/src/rules/html-navigation-has-label.d.ts +3 -2
  263. package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
  264. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +3 -2
  265. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +3 -2
  266. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +3 -2
  267. package/dist/types/src/rules/html-no-duplicate-meta-names.d.ts +9 -0
  268. package/dist/types/src/rules/html-no-empty-attributes.d.ts +3 -2
  269. package/dist/types/src/rules/html-no-empty-headings.d.ts +3 -2
  270. package/dist/types/src/rules/html-no-nested-links.d.ts +3 -2
  271. package/dist/types/src/rules/html-no-positive-tab-index.d.ts +3 -2
  272. package/dist/types/src/rules/html-no-self-closing.d.ts +14 -5
  273. package/dist/types/src/rules/html-no-space-in-tag.d.ts +16 -0
  274. package/dist/types/src/rules/html-no-title-attribute.d.ts +3 -2
  275. package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
  276. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +16 -6
  277. package/dist/types/src/rules/index.d.ts +19 -3
  278. package/dist/types/src/rules/parser-no-errors.d.ts +2 -1
  279. package/dist/types/src/rules/rule-utils.d.ts +72 -25
  280. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +13 -4
  281. package/dist/types/src/rules.d.ts +2 -0
  282. package/dist/types/src/types.d.ts +102 -11
  283. package/dist/types/types.d.ts +102 -11
  284. package/docs/rules/README.md +16 -2
  285. package/docs/rules/erb-no-case-node-children.md +50 -0
  286. package/docs/rules/erb-no-extra-newline.md +74 -0
  287. package/docs/rules/erb-no-extra-whitespace-inside-tags.md +39 -0
  288. package/docs/rules/{erb-requires-trailing-newline.md → erb-require-trailing-newline.md} +1 -1
  289. package/docs/rules/erb-right-trim.md +5 -10
  290. package/docs/rules/herb-disable-comment-malformed.md +45 -0
  291. package/docs/rules/herb-disable-comment-missing-rules.md +60 -0
  292. package/docs/rules/herb-disable-comment-no-duplicate-rules.md +49 -0
  293. package/docs/rules/herb-disable-comment-no-redundant-all.md +53 -0
  294. package/docs/rules/herb-disable-comment-unnecessary.md +44 -0
  295. package/docs/rules/herb-disable-comment-valid-rule-name.md +41 -0
  296. package/docs/rules/html-aria-attribute-must-be-valid.md +2 -5
  297. package/docs/rules/html-aria-label-is-well-formatted.md +1 -1
  298. package/docs/rules/html-attribute-double-quotes.md +2 -2
  299. package/docs/rules/html-attribute-equals-spacing.md +2 -2
  300. package/docs/rules/html-attribute-values-require-quotes.md +3 -3
  301. package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +2 -2
  302. package/docs/rules/html-body-only-elements.md +99 -0
  303. package/docs/rules/html-boolean-attributes-no-value.md +2 -2
  304. package/docs/rules/html-head-only-elements.md +81 -0
  305. package/docs/rules/html-input-require-autocomplete.md +64 -0
  306. package/docs/rules/html-no-aria-hidden-on-focusable.md +2 -2
  307. package/docs/rules/html-no-duplicate-attributes.md +2 -2
  308. package/docs/rules/html-no-duplicate-meta-names.md +64 -0
  309. package/docs/rules/html-no-empty-attributes.md +3 -3
  310. package/docs/rules/html-no-empty-headings.md +4 -26
  311. package/docs/rules/html-no-positive-tab-index.md +1 -2
  312. package/docs/rules/html-no-self-closing.md +17 -2
  313. package/docs/rules/html-no-space-in-tag.md +66 -0
  314. package/docs/rules/html-no-title-attribute.md +2 -2
  315. package/docs/rules/html-no-underscores-in-attribute-names.md +2 -2
  316. package/docs/rules/html-tag-name-lowercase.md +2 -2
  317. package/package.json +13 -5
  318. package/src/cli/argument-parser.ts +46 -37
  319. package/src/cli/file-processor.ts +159 -28
  320. package/src/cli/formatters/detailed-formatter.ts +21 -3
  321. package/src/cli/formatters/github-actions-formatter.ts +17 -1
  322. package/src/cli/formatters/json-formatter.ts +9 -0
  323. package/src/cli/formatters/simple-formatter.ts +24 -8
  324. package/src/cli/output-manager.ts +23 -3
  325. package/src/cli/summary-reporter.ts +40 -3
  326. package/src/cli.ts +134 -51
  327. package/src/custom-rule-loader.ts +189 -0
  328. package/src/herb-disable-comment-utils.ts +175 -0
  329. package/src/index.ts +2 -0
  330. package/src/linter.ts +501 -36
  331. package/src/loader.ts +30 -0
  332. package/src/rules/erb-comment-syntax.ts +53 -10
  333. package/src/rules/erb-no-case-node-children.ts +68 -0
  334. package/src/rules/erb-no-empty-tags.ts +9 -3
  335. package/src/rules/erb-no-extra-newline.ts +91 -0
  336. package/src/rules/erb-no-extra-whitespace-inside-tags.ts +147 -0
  337. package/src/rules/erb-no-output-control-flow.ts +9 -3
  338. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +9 -3
  339. package/src/rules/erb-prefer-image-tag-helper.ts +9 -3
  340. package/src/rules/erb-require-trailing-newline.ts +47 -0
  341. package/src/rules/erb-require-whitespace-inside-tags.ts +94 -16
  342. package/src/rules/erb-right-trim.ts +45 -22
  343. package/src/rules/herb-disable-comment-base.ts +76 -0
  344. package/src/rules/herb-disable-comment-malformed.ts +66 -0
  345. package/src/rules/herb-disable-comment-missing-rules.ts +41 -0
  346. package/src/rules/herb-disable-comment-no-duplicate-rules.ts +46 -0
  347. package/src/rules/herb-disable-comment-no-redundant-all.ts +40 -0
  348. package/src/rules/herb-disable-comment-unnecessary.ts +103 -0
  349. package/src/rules/herb-disable-comment-valid-rule-name.ts +62 -0
  350. package/src/rules/html-anchor-require-href.ts +9 -3
  351. package/src/rules/html-aria-attribute-must-be-valid.ts +9 -3
  352. package/src/rules/html-aria-label-is-well-formatted.ts +9 -5
  353. package/src/rules/html-aria-level-must-be-valid.ts +9 -2
  354. package/src/rules/html-aria-role-heading-requires-level.ts +9 -3
  355. package/src/rules/html-aria-role-must-be-valid.ts +9 -3
  356. package/src/rules/html-attribute-double-quotes.ts +42 -8
  357. package/src/rules/html-attribute-equals-spacing.ts +31 -7
  358. package/src/rules/html-attribute-values-require-quotes.ts +56 -10
  359. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +9 -3
  360. package/src/rules/html-body-only-elements.ts +60 -0
  361. package/src/rules/html-boolean-attributes-no-value.ts +31 -6
  362. package/src/rules/html-head-only-elements.ts +65 -0
  363. package/src/rules/html-iframe-has-title.ts +9 -4
  364. package/src/rules/html-img-require-alt.ts +10 -4
  365. package/src/rules/html-input-require-autocomplete.ts +85 -0
  366. package/src/rules/html-navigation-has-label.ts +9 -3
  367. package/src/rules/html-no-aria-hidden-on-focusable.ts +9 -3
  368. package/src/rules/html-no-block-inside-inline.ts +9 -3
  369. package/src/rules/html-no-duplicate-attributes.ts +9 -3
  370. package/src/rules/html-no-duplicate-ids.ts +11 -7
  371. package/src/rules/html-no-duplicate-meta-names.ts +188 -0
  372. package/src/rules/html-no-empty-attributes.ts +58 -10
  373. package/src/rules/html-no-empty-headings.ts +10 -8
  374. package/src/rules/html-no-nested-links.ts +10 -4
  375. package/src/rules/html-no-positive-tab-index.ts +9 -3
  376. package/src/rules/html-no-self-closing.ts +69 -9
  377. package/src/rules/html-no-space-in-tag.ts +221 -0
  378. package/src/rules/html-no-title-attribute.ts +9 -3
  379. package/src/rules/html-no-underscores-in-attribute-names.ts +12 -4
  380. package/src/rules/html-tag-name-lowercase.ts +41 -10
  381. package/src/rules/index.ts +23 -3
  382. package/src/rules/parser-no-errors.ts +8 -1
  383. package/src/rules/rule-utils.ts +248 -42
  384. package/src/rules/svg-tag-name-capitalization.ts +39 -6
  385. package/src/{default-rules.ts → rules.ts} +51 -15
  386. package/src/types.ts +133 -15
  387. package/dist/src/default-rules.js.map +0 -1
  388. package/dist/src/rules/erb-requires-trailing-newline.js +0 -22
  389. package/dist/src/rules/erb-requires-trailing-newline.js.map +0 -1
  390. package/dist/types/default-rules.d.ts +0 -2
  391. package/dist/types/rules/erb-requires-trailing-newline.d.ts +0 -6
  392. package/dist/types/src/default-rules.d.ts +0 -2
  393. package/dist/types/src/rules/erb-requires-trailing-newline.d.ts +0 -6
  394. package/src/rules/erb-requires-trailing-newline.ts +0 -29
@@ -1,9 +1,15 @@
1
- import { ParserRule } from "../types.js"
1
+ import { ParserRule, BaseAutofixContext, Mutable } from "../types.js"
2
2
  import { BaseRuleVisitor } from "./rule-utils.js"
3
+ import { isNode, getTagName, HTMLOpenTagNode } from "@herb-tools/core"
3
4
 
4
- import { isNode, getTagName, HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, ParseResult, XMLDeclarationNode, Node } from "@herb-tools/core"
5
+ import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type { HTMLElementNode, HTMLCloseTagNode, ParseResult, XMLDeclarationNode, Node } from "@herb-tools/core"
5
7
 
6
- import type { LintOffense, LintContext } from "../types.js"
8
+ interface TagNameAutofixContext extends BaseAutofixContext {
9
+ node: Mutable<HTMLOpenTagNode | HTMLCloseTagNode>
10
+ tagName: string
11
+ correctedTagName: string
12
+ }
7
13
 
8
14
  class XMLDeclarationChecker extends BaseRuleVisitor {
9
15
  hasXMLDeclaration: boolean = false
@@ -18,7 +24,7 @@ class XMLDeclarationChecker extends BaseRuleVisitor {
18
24
  }
19
25
  }
20
26
 
21
- class TagNameLowercaseVisitor extends BaseRuleVisitor {
27
+ class TagNameLowercaseVisitor extends BaseRuleVisitor<TagNameAutofixContext> {
22
28
  visitHTMLElementNode(node: HTMLElementNode): void {
23
29
  if (getTagName(node).toLowerCase() === "svg") {
24
30
  this.checkTagName(node.open_tag)
@@ -52,28 +58,53 @@ class TagNameLowercaseVisitor extends BaseRuleVisitor {
52
58
  this.addOffense(
53
59
  `${type} tag name \`${open}${tagName}>\` should be lowercase. Use \`${open}${lowercaseTagName}>\` instead.`,
54
60
  node.tag_name!.location,
55
- "error"
61
+ {
62
+ node,
63
+ tagName,
64
+ correctedTagName: lowercaseTagName
65
+ }
56
66
  )
57
67
  }
58
68
  }
59
69
  }
60
70
 
61
- export class HTMLTagNameLowercaseRule extends ParserRule {
71
+ export class HTMLTagNameLowercaseRule extends ParserRule<TagNameAutofixContext> {
72
+ static autocorrectable = true
62
73
  name = "html-tag-name-lowercase"
63
74
 
64
- isEnabled(result: ParseResult, context?: Partial<LintContext>): boolean {
65
- if (context?.fileName?.endsWith(".xml") || context?.fileName?.endsWith(".xml.erb")) {
66
- return false
75
+ get defaultConfig(): FullRuleConfig {
76
+ return {
77
+ enabled: true,
78
+ severity: "error",
79
+ exclude: ["**/*.xml", "**/*.xml.erb"]
67
80
  }
81
+ }
68
82
 
83
+ isEnabled(result: ParseResult, _context?: Partial<LintContext>): boolean {
69
84
  const checker = new XMLDeclarationChecker(this.name)
85
+
70
86
  checker.visit(result.value)
87
+
71
88
  return !checker.hasXMLDeclaration
72
89
  }
73
90
 
74
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
91
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<TagNameAutofixContext>[] {
75
92
  const visitor = new TagNameLowercaseVisitor(this.name, context)
93
+
76
94
  visitor.visit(result.value)
95
+
77
96
  return visitor.offenses
78
97
  }
98
+
99
+ autofix(offense: LintOffense<TagNameAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
100
+ if (!offense.autofixContext) return null
101
+
102
+ const { node: { tag_name }, correctedTagName } = offense.autofixContext
103
+
104
+ if (!tag_name) return null
105
+
106
+ tag_name.value = correctedTagName
107
+
108
+ return result
109
+ }
79
110
  }
@@ -1,10 +1,25 @@
1
1
  export * from "./rule-utils.js"
2
+ export * from "./herb-disable-comment-base.js"
3
+
2
4
  export * from "./erb-comment-syntax.js"
5
+ export * from "./erb-no-case-node-children.js"
3
6
  export * from "./erb-no-empty-tags.js"
7
+ export * from "./erb-no-extra-newline.js"
8
+ export * from "./erb-no-extra-whitespace-inside-tags.js"
4
9
  export * from "./erb-no-output-control-flow.js"
5
10
  export * from "./erb-no-silent-tag-in-attribute-name.js"
6
11
  export * from "./erb-prefer-image-tag-helper.js"
7
- export * from "./erb-requires-trailing-newline.js"
12
+ export * from "./erb-require-trailing-newline.js"
13
+ export * from "./erb-require-whitespace-inside-tags.js"
14
+ export * from "./erb-right-trim.js"
15
+
16
+ export * from "./herb-disable-comment-valid-rule-name.js"
17
+ export * from "./herb-disable-comment-no-redundant-all.js"
18
+ export * from "./herb-disable-comment-no-duplicate-rules.js"
19
+ export * from "./herb-disable-comment-missing-rules.js"
20
+ export * from "./herb-disable-comment-malformed.js"
21
+ export * from "./herb-disable-comment-unnecessary.js"
22
+
8
23
  export * from "./html-anchor-require-href.js"
9
24
  export * from "./html-aria-label-is-well-formatted.js"
10
25
  export * from "./html-aria-level-must-be-valid.js"
@@ -14,21 +29,26 @@ export * from "./html-attribute-double-quotes.js"
14
29
  export * from "./html-attribute-equals-spacing.js"
15
30
  export * from "./html-attribute-values-require-quotes.js"
16
31
  export * from "./html-avoid-both-disabled-and-aria-disabled.js"
32
+ export * from "./html-body-only-elements.js"
17
33
  export * from "./html-boolean-attributes-no-value.js"
34
+ export * from "./html-head-only-elements.js"
18
35
  export * from "./html-iframe-has-title.js"
19
36
  export * from "./html-img-require-alt.js"
37
+ export * from "./html-input-require-autocomplete.js"
20
38
  export * from "./html-navigation-has-label.js"
21
39
  export * from "./html-no-aria-hidden-on-focusable.js"
22
40
  export * from "./html-no-block-inside-inline.js"
23
41
  export * from "./html-no-duplicate-attributes.js"
24
42
  export * from "./html-no-duplicate-ids.js"
43
+ export * from "./html-no-duplicate-meta-names.js"
25
44
  export * from "./html-no-empty-attributes.js"
26
45
  export * from "./html-no-empty-headings.js"
27
46
  export * from "./html-no-nested-links.js"
28
47
  export * from "./html-no-positive-tab-index.js"
29
48
  export * from "./html-no-self-closing.js"
49
+ export * from "./html-no-space-in-tag.js"
30
50
  export * from "./html-no-title-attribute.js"
51
+ export * from "./html-no-underscores-in-attribute-names.js"
31
52
  export * from "./html-tag-name-lowercase.js"
53
+
32
54
  export * from "./svg-tag-name-capitalization.js"
33
- export * from "./html-no-underscores-in-attribute-names.js"
34
- export * from "./erb-right-trim.js"
@@ -1,11 +1,18 @@
1
1
  import { ParserRule } from "../types.js"
2
2
 
3
- import type { LintOffense } from "../types.js"
3
+ import type { LintOffense, FullRuleConfig } from "../types.js"
4
4
  import type { ParseResult, HerbError } from "@herb-tools/core"
5
5
 
6
6
  export class ParserNoErrorsRule extends ParserRule {
7
7
  name = "parser-no-errors"
8
8
 
9
+ get defaultConfig(): FullRuleConfig {
10
+ return {
11
+ enabled: true,
12
+ severity: "error"
13
+ }
14
+ }
15
+
9
16
  check(result: ParseResult): LintOffense[] {
10
17
  return result.recursiveErrors().map(error =>
11
18
  this.herbErrorToLintOffense(error)
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  Visitor,
3
- Position,
4
3
  Location,
5
4
  getStaticAttributeName,
6
5
  hasDynamicAttributeName as hasNodeDynamicAttributeName,
@@ -17,6 +16,7 @@ import type {
17
16
  HTMLAttributeNameNode,
18
17
  HTMLAttributeNode,
19
18
  HTMLAttributeValueNode,
19
+ HTMLElementNode,
20
20
  HTMLOpenTagNode,
21
21
  LiteralNode,
22
22
  LexResult,
@@ -27,7 +27,7 @@ import type {
27
27
  import { DEFAULT_LINT_CONTEXT } from "../types.js"
28
28
 
29
29
  import type * as Nodes from "@herb-tools/core"
30
- import type { LintOffense, LintSeverity, LintContext } from "../types.js"
30
+ import type { UnboundLintOffense, LintContext, BaseAutofixContext } from "../types.js"
31
31
 
32
32
  export enum ControlFlowType {
33
33
  CONDITIONAL,
@@ -37,8 +37,8 @@ export enum ControlFlowType {
37
37
  /**
38
38
  * Base visitor class that provides common functionality for rule visitors
39
39
  */
40
- export abstract class BaseRuleVisitor extends Visitor {
41
- public readonly offenses: LintOffense[] = []
40
+ export abstract class BaseRuleVisitor<TAutofixContext extends BaseAutofixContext = BaseAutofixContext> extends Visitor {
41
+ public readonly offenses: UnboundLintOffense<TAutofixContext>[] = []
42
42
  protected ruleName: string
43
43
  protected context: LintContext
44
44
 
@@ -50,24 +50,25 @@ export abstract class BaseRuleVisitor extends Visitor {
50
50
  }
51
51
 
52
52
  /**
53
- * Helper method to create a lint offense
53
+ * Helper method to create an unbound lint offense (without severity).
54
+ * The Linter will bind severity based on the rule's config.
54
55
  */
55
- protected createOffense(message: string, location: Location, severity: LintSeverity = "error"): LintOffense {
56
+ protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext): UnboundLintOffense<TAutofixContext> {
56
57
  return {
57
58
  rule: this.ruleName,
58
59
  code: this.ruleName,
59
60
  source: "Herb Linter",
60
61
  message,
61
62
  location,
62
- severity,
63
+ autofixContext,
63
64
  }
64
65
  }
65
66
 
66
67
  /**
67
68
  * Helper method to add an offense to the offenses array
68
69
  */
69
- protected addOffense(message: string, location: Location, severity: LintSeverity = "error"): void {
70
- this.offenses.push(this.createOffense(message, location, severity))
70
+ protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext): void {
71
+ this.offenses.push(this.createOffense(message, location, autofixContext))
71
72
  }
72
73
  }
73
74
 
@@ -76,17 +77,18 @@ export abstract class BaseRuleVisitor extends Visitor {
76
77
  * This allows rules to track state across different control flow structures
77
78
  * like if/else branches, loops, etc.
78
79
  *
80
+ * @template TAutofixContext - Type for autofix context (node + custom data)
79
81
  * @template TControlFlowState - Type for state passed between onEnterControlFlow and onExitControlFlow
80
82
  * @template TBranchState - Type for state passed between onEnterBranch and onExitBranch
81
83
  */
82
- export abstract class ControlFlowTrackingVisitor<TControlFlowState = any, TBranchState = any> extends BaseRuleVisitor {
84
+ export abstract class ControlFlowTrackingVisitor<TAutofixContext extends BaseAutofixContext = BaseAutofixContext, TControlFlowState = any, TBranchState = any> extends BaseRuleVisitor<TAutofixContext> {
83
85
  protected isInControlFlow: boolean = false
84
86
  protected currentControlFlowType: ControlFlowType | null = null
85
87
 
86
88
  /**
87
89
  * Handle visiting a control flow node with proper scope management
88
90
  */
89
- protected handleControlFlowNode(node: Node, controlFlowType: ControlFlowType, visitChildren: () => void): void {
91
+ protected handleControlFlowNode(_node: Node, controlFlowType: ControlFlowType, visitChildren: () => void): void {
90
92
  const wasInControlFlow = this.isInControlFlow
91
93
  const previousControlFlowType = this.currentControlFlowType
92
94
 
@@ -170,7 +172,9 @@ export function getAttributes(node: HTMLOpenTagNode): HTMLAttributeNode[] {
170
172
  /**
171
173
  * Gets the tag name from an HTML tag node (lowercased)
172
174
  */
173
- export function getTagName(node: HTMLOpenTagNode): string | null {
175
+ export function getTagName(node: HTMLElementNode | HTMLOpenTagNode | null | undefined): string | null {
176
+ if (!node) return null
177
+
174
178
  return node.tag_name?.value.toLowerCase() || null
175
179
  }
176
180
 
@@ -577,10 +581,8 @@ export function createEndOfFileLocation(source: string): Location {
577
581
  const lastColumnNumber = lastLine.length
578
582
 
579
583
  const startColumn = lastColumnNumber > 0 ? lastColumnNumber - 1 : 0
580
- const start = new Position(lastLineNumber, startColumn)
581
- const end = new Position(lastLineNumber, lastColumnNumber)
582
584
 
583
- return new Location(start, end)
585
+ return Location.from(lastLineNumber, startColumn, lastLineNumber, lastColumnNumber)
584
586
  }
585
587
 
586
588
  /**
@@ -621,7 +623,7 @@ export function isBooleanAttribute(attributeName: string): boolean {
621
623
  * - checkDynamicAttributeStaticValue() - name="data-<%= key %>" value="foo"
622
624
  * - checkDynamicAttributeDynamicValue() - name="data-<%= key %>" value="<%= value %>"
623
625
  */
624
- export abstract class AttributeVisitorMixin extends BaseRuleVisitor {
626
+ export abstract class AttributeVisitorMixin<TAutofixContext extends BaseAutofixContext = BaseAutofixContext> extends BaseRuleVisitor<TAutofixContext> {
625
627
  constructor(ruleName: string, context?: Partial<LintContext>) {
626
628
  super(ruleName, context)
627
629
  }
@@ -735,8 +737,8 @@ export function forEachAttribute(
735
737
  /**
736
738
  * Base lexer visitor class that provides common functionality for lexer-based rule visitors
737
739
  */
738
- export abstract class BaseLexerRuleVisitor {
739
- public readonly offenses: LintOffense[] = []
740
+ export abstract class BaseLexerRuleVisitor<TAutofixContext extends BaseAutofixContext = BaseAutofixContext> {
741
+ public readonly offenses: UnboundLintOffense<TAutofixContext>[] = []
740
742
  protected ruleName: string
741
743
  protected context: LintContext
742
744
 
@@ -746,24 +748,25 @@ export abstract class BaseLexerRuleVisitor {
746
748
  }
747
749
 
748
750
  /**
749
- * Helper method to create a lint offense for lexer rules
751
+ * Helper method to create an unbound lint offense (without severity).
752
+ * The Linter will bind severity based on the rule's config.
750
753
  */
751
- protected createOffense(message: string, location: Location, severity: LintSeverity = "error"): LintOffense {
754
+ protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext): UnboundLintOffense<TAutofixContext> {
752
755
  return {
753
756
  rule: this.ruleName,
754
757
  code: this.ruleName,
755
758
  source: "Herb Linter",
756
759
  message,
757
760
  location,
758
- severity,
761
+ autofixContext,
759
762
  }
760
763
  }
761
764
 
762
765
  /**
763
766
  * Helper method to add an offense to the offenses array
764
767
  */
765
- protected addOffense(message: string, location: Location, severity: LintSeverity = "error"): void {
766
- this.offenses.push(this.createOffense(message, location, severity))
768
+ protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext): void {
769
+ this.offenses.push(this.createOffense(message, location, autofixContext))
767
770
  }
768
771
 
769
772
  /**
@@ -791,14 +794,13 @@ export abstract class BaseLexerRuleVisitor {
791
794
  protected visitToken(_token: Token): void {
792
795
  // Default implementation does nothing
793
796
  }
794
-
795
797
  }
796
798
 
797
799
  /**
798
800
  * Base source visitor class that provides common functionality for source-based rule visitors
799
801
  */
800
- export abstract class BaseSourceRuleVisitor {
801
- public readonly offenses: LintOffense[] = []
802
+ export abstract class BaseSourceRuleVisitor<TAutofixContext extends BaseAutofixContext = BaseAutofixContext> {
803
+ public readonly offenses: UnboundLintOffense<TAutofixContext>[] = []
802
804
  protected ruleName: string
803
805
  protected context: LintContext
804
806
 
@@ -808,24 +810,25 @@ export abstract class BaseSourceRuleVisitor {
808
810
  }
809
811
 
810
812
  /**
811
- * Helper method to create a lint offense for source rules
813
+ * Helper method to create an unbound lint offense (without severity).
814
+ * The Linter will bind severity based on the rule's config.
812
815
  */
813
- protected createOffense(message: string, location: Location, severity: LintSeverity = "error"): LintOffense {
816
+ protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext): UnboundLintOffense<TAutofixContext> {
814
817
  return {
815
818
  rule: this.ruleName,
816
819
  code: this.ruleName,
817
820
  source: "Herb Linter",
818
821
  message,
819
822
  location,
820
- severity,
823
+ autofixContext,
821
824
  }
822
825
  }
823
826
 
824
827
  /**
825
828
  * Helper method to add an offense to the offenses array
826
829
  */
827
- protected addOffense(message: string, location: Location, severity: LintSeverity = "error"): void {
828
- this.offenses.push(this.createOffense(message, location, severity))
830
+ protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext): void {
831
+ this.offenses.push(this.createOffense(message, location, autofixContext))
829
832
  }
830
833
 
831
834
  /**
@@ -841,19 +844,222 @@ export abstract class BaseSourceRuleVisitor {
841
844
  * Override this method to implement source-level checks
842
845
  */
843
846
  protected abstract visitSource(source: string): void
847
+ }
844
848
 
845
- /**
846
- * Helper method to create a location for a specific position in the source
847
- */
848
- protected createLocationAt(source: string, position: number): Location {
849
- const beforePosition = source.substring(0, position)
850
- const lines = beforePosition.split('\n')
851
- const line = lines.length
852
- const column = lines[lines.length - 1].length + 1
849
+ /**
850
+ * Autofix utilities for applying string replacements
851
+ */
852
+
853
+ /**
854
+ * Checks if two locations are equal
855
+ * @param a - First location
856
+ * @param b - Second location
857
+ * @returns true if locations are equal
858
+ */
859
+ export function locationsEqual(a: Location, b: Location): boolean {
860
+ return a.start.line === b.start.line &&
861
+ a.start.column === b.start.column &&
862
+ a.end.line === b.end.line &&
863
+ a.end.column === b.end.column
864
+ }
865
+
866
+ /**
867
+ * Finds a node in the AST that has a specific location
868
+ * Uses direct recursive traversal for reliability
869
+ * @param root - The root node to search from
870
+ * @param location - The location to match
871
+ * @param predicate - Optional predicate function to filter nodes (e.g., isERBNode)
872
+ * @returns The matching node or null if not found
873
+ */
874
+ export function findNodeByLocation(root: Node, location: Location, predicate?: (node: Node) => boolean): any {
875
+ const visited = new Set<any>()
876
+
877
+ function search(node: any): any {
878
+ if (!node || visited.has(node)) return null
879
+ visited.add(node)
880
+
881
+ if (node.location && locationsEqual(node.location, location)) {
882
+ if (!predicate || predicate(node)) {
883
+ return node
884
+ }
885
+ }
886
+
887
+ const propsToCheck = ['tag_opening', 'tag_closing', 'tag_name', 'name', 'equals', 'value', 'content']
888
+ for (const prop of propsToCheck) {
889
+ if (node[prop]?.location && locationsEqual(node[prop].location, location)) {
890
+ if (!predicate || predicate(node)) {
891
+ return node
892
+ }
893
+ }
894
+ }
895
+
896
+ if (typeof node.compactChildNodes === 'function') {
897
+ for (const child of node.compactChildNodes()) {
898
+ const found = search(child)
899
+ if (found) return found
900
+ }
901
+ } else {
902
+ if (node.children && Array.isArray(node.children)) {
903
+ for (const child of node.children) {
904
+ const found = search(child)
905
+ if (found) return found
906
+ }
907
+ }
908
+
909
+ if (node.body && Array.isArray(node.body)) {
910
+ for (const child of node.body) {
911
+ const found = search(child)
912
+ if (found) return found
913
+ }
914
+ }
915
+ }
916
+
917
+ return null
918
+ }
919
+
920
+ return search(root)
921
+ }
922
+
923
+ /**
924
+ * AST Navigation Utilities
925
+ * These utilities help navigate the AST tree for complex autofix operations
926
+ */
927
+
928
+ /**
929
+ * Finds the parent node of a given child node in the AST
930
+ * @param root - The root node to search from (typically the document node)
931
+ * @param target - The child node to find the parent of
932
+ * @returns The parent node, or null if not found
933
+ *
934
+ * @example
935
+ * const parent = findParent(result.value, offense.autofixContext.node)
936
+ * if (parent?.type === "AST_HTML_ELEMENT_NODE") {
937
+ * // Modify parent...
938
+ * }
939
+ */
940
+ export function findParent(root: Node, target: Node): Node | null {
941
+ let parentNode: Node | null = null
853
942
 
854
- const start = new Position(line, column)
855
- const end = new Position(line, column)
943
+ const search = (node: Node, _parent: Node | null = null): void => {
944
+ if (parentNode) return
856
945
 
857
- return new Location(start, end)
946
+ const nodeAny = node as any
947
+
948
+ if (nodeAny.children) {
949
+ for (const child of nodeAny.children) {
950
+ if (child === target) {
951
+ parentNode = node
952
+ return
953
+ }
954
+ }
955
+ }
956
+
957
+ const propsToCheck = ['open_tag', 'close_tag', 'body', 'name', 'value']
958
+
959
+ for (const prop of propsToCheck) {
960
+ const value = (node as any)[prop]
961
+ if (value === target) {
962
+ parentNode = node
963
+ return
964
+ }
965
+ if (Array.isArray(value) && value.includes(target)) {
966
+ parentNode = node
967
+ return
968
+ }
969
+ }
970
+
971
+ if (nodeAny.children) {
972
+ for (const child of nodeAny.children) {
973
+ search(child, node)
974
+ if (parentNode) return
975
+ }
976
+ }
977
+
978
+ for (const prop of propsToCheck) {
979
+ const value = (node as any)[prop]
980
+ if (Array.isArray(value)) {
981
+ for (const item of value) {
982
+ if (item && typeof item === 'object' && 'type' in item) {
983
+ search(item, node)
984
+ if (parentNode) return
985
+ }
986
+ }
987
+ } else if (value && typeof value === 'object' && 'type' in value) {
988
+ search(value, node)
989
+ if (parentNode) return
990
+ }
991
+ }
858
992
  }
993
+
994
+ search(root)
995
+
996
+ return parentNode
997
+ }
998
+
999
+ export const DOCUMENT_ONLY_TAG_NAMES = new Set<string>([
1000
+ "html"
1001
+ ])
1002
+
1003
+ export const HTML_ONLY_TAG_NAMES = new Set<string>([
1004
+ "head", "body"
1005
+ ])
1006
+
1007
+ export const HEAD_ONLY_TAG_NAMES = new Set<string>([
1008
+ "base",
1009
+ "title",
1010
+ "style",
1011
+ "meta",
1012
+ "link",
1013
+ ])
1014
+
1015
+ export const HEAD_AND_BODY_TAG_NAMES = new Set<string>([
1016
+ "script",
1017
+ "noscript",
1018
+ "template",
1019
+ ])
1020
+
1021
+ export function isDocumentOnlyTag(tagName: string): boolean {
1022
+ return DOCUMENT_ONLY_TAG_NAMES.has(tagName.toLowerCase())
1023
+ }
1024
+
1025
+ export function isHtmlOnlyTag(tagName: string): boolean {
1026
+ return HTML_ONLY_TAG_NAMES.has(tagName.toLowerCase())
1027
+ }
1028
+
1029
+ export function isHeadOnlyTag(tagName: string): boolean {
1030
+ return HEAD_ONLY_TAG_NAMES.has(tagName.toLowerCase())
1031
+ }
1032
+
1033
+ export function isHeadAndBodyTag(tagName: string): boolean {
1034
+ return HEAD_AND_BODY_TAG_NAMES.has(tagName.toLowerCase())
1035
+ }
1036
+
1037
+ export function isBodyOnlyTag(tagName: string): boolean {
1038
+ const tag = tagName.toLowerCase()
1039
+
1040
+ return (
1041
+ !isDocumentOnlyTag(tag) &&
1042
+ !isHtmlOnlyTag(tag) &&
1043
+ !isHeadOnlyTag(tag) &&
1044
+ !isHeadAndBodyTag(tag)
1045
+ )
1046
+ }
1047
+
1048
+ export function isBodyTag(tagName: string): boolean {
1049
+ const tag = tagName.toLowerCase()
1050
+ return (
1051
+ !isDocumentOnlyTag(tag) &&
1052
+ !isHtmlOnlyTag(tag) &&
1053
+ (isBodyOnlyTag(tag) || isHeadAndBodyTag(tag))
1054
+ )
1055
+ }
1056
+
1057
+ export function isHeadTag(tagName: string): boolean {
1058
+ const tag = tagName.toLowerCase()
1059
+
1060
+ return (
1061
+ !isDocumentOnlyTag(tag) &&
1062
+ !isHtmlOnlyTag(tag) &&
1063
+ (isHeadOnlyTag(tag) || isHeadAndBodyTag(tag))
1064
+ )
859
1065
  }
@@ -1,10 +1,16 @@
1
+ import { ParserRule } from "../types.js"
1
2
  import { BaseRuleVisitor, SVG_CAMEL_CASE_ELEMENTS, SVG_LOWERCASE_TO_CAMELCASE } from "./rule-utils.js"
2
3
 
3
- import { ParserRule } from "../types.js"
4
- import type { LintOffense, LintContext } from "../types.js"
4
+ import type { UnboundLintOffense, LintOffense, LintContext, BaseAutofixContext, Mutable, FullRuleConfig } from "../types.js"
5
5
  import type { HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, ParseResult } from "@herb-tools/core"
6
6
 
7
- class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor {
7
+ interface SVGTagNameCapitalizationAutofixContext extends BaseAutofixContext {
8
+ node: Mutable<HTMLOpenTagNode | HTMLCloseTagNode>
9
+ currentTagName: string
10
+ correctCamelCase: string
11
+ }
12
+
13
+ class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor<SVGTagNameCapitalizationAutofixContext> {
8
14
  private insideSVG = false
9
15
 
10
16
  visitHTMLElementNode(node: HTMLElementNode): void {
@@ -22,6 +28,7 @@ class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor {
22
28
  if (node.open_tag) {
23
29
  this.checkTagName(node.open_tag as HTMLOpenTagNode)
24
30
  }
31
+
25
32
  if (node.close_tag) {
26
33
  this.checkTagName(node.close_tag as HTMLCloseTagNode)
27
34
  }
@@ -50,18 +57,44 @@ class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor {
50
57
  this.addOffense(
51
58
  `${type} SVG tag name \`${tagName}\` should use proper capitalization. Use \`${correctCamelCase}\` instead.`,
52
59
  node.tag_name!.location,
53
- "error"
60
+ {
61
+ node,
62
+ currentTagName: tagName,
63
+ correctCamelCase
64
+ }
54
65
  )
55
66
  }
56
67
  }
57
68
  }
58
69
 
59
- export class SVGTagNameCapitalizationRule extends ParserRule {
70
+ export class SVGTagNameCapitalizationRule extends ParserRule<SVGTagNameCapitalizationAutofixContext> {
71
+ static autocorrectable = true
60
72
  name = "svg-tag-name-capitalization"
61
73
 
62
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
74
+ get defaultConfig(): FullRuleConfig {
75
+ return {
76
+ enabled: true,
77
+ severity: "error"
78
+ }
79
+ }
80
+
81
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<SVGTagNameCapitalizationAutofixContext>[] {
63
82
  const visitor = new SVGTagNameCapitalizationVisitor(this.name, context)
83
+
64
84
  visitor.visit(result.value)
85
+
65
86
  return visitor.offenses
66
87
  }
88
+
89
+ autofix(offense: LintOffense<SVGTagNameCapitalizationAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
90
+ if (!offense.autofixContext) return null
91
+
92
+ const { node: { tag_name }, correctCamelCase } = offense.autofixContext
93
+
94
+ if (!tag_name) return null
95
+
96
+ tag_name.value = correctCamelCase
97
+
98
+ return result
99
+ }
67
100
  }