@herb-tools/linter 0.7.5 → 0.8.1

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 +26041 -3435
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +5437 -1254
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +5405 -1255
  7. package/dist/index.js.map +1 -1
  8. package/dist/loader.cjs +17017 -0
  9. package/dist/loader.cjs.map +1 -0
  10. package/dist/loader.js +16886 -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 +56 -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 +213 -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 +73 -26
  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 +73 -26
  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 +103 -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 +70 -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 +251 -43
  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
 
@@ -374,7 +378,9 @@ export function findAttributeByName(attributes: Node[], attributeName: string):
374
378
  /**
375
379
  * Checks if a tag has a specific attribute
376
380
  */
377
- export function hasAttribute(node: HTMLOpenTagNode, attributeName: string): boolean {
381
+ export function hasAttribute(node: HTMLOpenTagNode | null | undefined, attributeName: string): boolean {
382
+ if (!node) return false
383
+
378
384
  return getAttribute(node, attributeName) !== null
379
385
  }
380
386
 
@@ -577,10 +583,8 @@ export function createEndOfFileLocation(source: string): Location {
577
583
  const lastColumnNumber = lastLine.length
578
584
 
579
585
  const startColumn = lastColumnNumber > 0 ? lastColumnNumber - 1 : 0
580
- const start = new Position(lastLineNumber, startColumn)
581
- const end = new Position(lastLineNumber, lastColumnNumber)
582
586
 
583
- return new Location(start, end)
587
+ return Location.from(lastLineNumber, startColumn, lastLineNumber, lastColumnNumber)
584
588
  }
585
589
 
586
590
  /**
@@ -621,7 +625,7 @@ export function isBooleanAttribute(attributeName: string): boolean {
621
625
  * - checkDynamicAttributeStaticValue() - name="data-<%= key %>" value="foo"
622
626
  * - checkDynamicAttributeDynamicValue() - name="data-<%= key %>" value="<%= value %>"
623
627
  */
624
- export abstract class AttributeVisitorMixin extends BaseRuleVisitor {
628
+ export abstract class AttributeVisitorMixin<TAutofixContext extends BaseAutofixContext = BaseAutofixContext> extends BaseRuleVisitor<TAutofixContext> {
625
629
  constructor(ruleName: string, context?: Partial<LintContext>) {
626
630
  super(ruleName, context)
627
631
  }
@@ -735,8 +739,8 @@ export function forEachAttribute(
735
739
  /**
736
740
  * Base lexer visitor class that provides common functionality for lexer-based rule visitors
737
741
  */
738
- export abstract class BaseLexerRuleVisitor {
739
- public readonly offenses: LintOffense[] = []
742
+ export abstract class BaseLexerRuleVisitor<TAutofixContext extends BaseAutofixContext = BaseAutofixContext> {
743
+ public readonly offenses: UnboundLintOffense<TAutofixContext>[] = []
740
744
  protected ruleName: string
741
745
  protected context: LintContext
742
746
 
@@ -746,24 +750,25 @@ export abstract class BaseLexerRuleVisitor {
746
750
  }
747
751
 
748
752
  /**
749
- * Helper method to create a lint offense for lexer rules
753
+ * Helper method to create an unbound lint offense (without severity).
754
+ * The Linter will bind severity based on the rule's config.
750
755
  */
751
- protected createOffense(message: string, location: Location, severity: LintSeverity = "error"): LintOffense {
756
+ protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext): UnboundLintOffense<TAutofixContext> {
752
757
  return {
753
758
  rule: this.ruleName,
754
759
  code: this.ruleName,
755
760
  source: "Herb Linter",
756
761
  message,
757
762
  location,
758
- severity,
763
+ autofixContext,
759
764
  }
760
765
  }
761
766
 
762
767
  /**
763
768
  * Helper method to add an offense to the offenses array
764
769
  */
765
- protected addOffense(message: string, location: Location, severity: LintSeverity = "error"): void {
766
- this.offenses.push(this.createOffense(message, location, severity))
770
+ protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext): void {
771
+ this.offenses.push(this.createOffense(message, location, autofixContext))
767
772
  }
768
773
 
769
774
  /**
@@ -791,14 +796,13 @@ export abstract class BaseLexerRuleVisitor {
791
796
  protected visitToken(_token: Token): void {
792
797
  // Default implementation does nothing
793
798
  }
794
-
795
799
  }
796
800
 
797
801
  /**
798
802
  * Base source visitor class that provides common functionality for source-based rule visitors
799
803
  */
800
- export abstract class BaseSourceRuleVisitor {
801
- public readonly offenses: LintOffense[] = []
804
+ export abstract class BaseSourceRuleVisitor<TAutofixContext extends BaseAutofixContext = BaseAutofixContext> {
805
+ public readonly offenses: UnboundLintOffense<TAutofixContext>[] = []
802
806
  protected ruleName: string
803
807
  protected context: LintContext
804
808
 
@@ -808,24 +812,25 @@ export abstract class BaseSourceRuleVisitor {
808
812
  }
809
813
 
810
814
  /**
811
- * Helper method to create a lint offense for source rules
815
+ * Helper method to create an unbound lint offense (without severity).
816
+ * The Linter will bind severity based on the rule's config.
812
817
  */
813
- protected createOffense(message: string, location: Location, severity: LintSeverity = "error"): LintOffense {
818
+ protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext): UnboundLintOffense<TAutofixContext> {
814
819
  return {
815
820
  rule: this.ruleName,
816
821
  code: this.ruleName,
817
822
  source: "Herb Linter",
818
823
  message,
819
824
  location,
820
- severity,
825
+ autofixContext,
821
826
  }
822
827
  }
823
828
 
824
829
  /**
825
830
  * Helper method to add an offense to the offenses array
826
831
  */
827
- protected addOffense(message: string, location: Location, severity: LintSeverity = "error"): void {
828
- this.offenses.push(this.createOffense(message, location, severity))
832
+ protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext): void {
833
+ this.offenses.push(this.createOffense(message, location, autofixContext))
829
834
  }
830
835
 
831
836
  /**
@@ -841,19 +846,222 @@ export abstract class BaseSourceRuleVisitor {
841
846
  * Override this method to implement source-level checks
842
847
  */
843
848
  protected abstract visitSource(source: string): void
849
+ }
844
850
 
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
851
+ /**
852
+ * Autofix utilities for applying string replacements
853
+ */
854
+
855
+ /**
856
+ * Checks if two locations are equal
857
+ * @param a - First location
858
+ * @param b - Second location
859
+ * @returns true if locations are equal
860
+ */
861
+ export function locationsEqual(a: Location, b: Location): boolean {
862
+ return a.start.line === b.start.line &&
863
+ a.start.column === b.start.column &&
864
+ a.end.line === b.end.line &&
865
+ a.end.column === b.end.column
866
+ }
867
+
868
+ /**
869
+ * Finds a node in the AST that has a specific location
870
+ * Uses direct recursive traversal for reliability
871
+ * @param root - The root node to search from
872
+ * @param location - The location to match
873
+ * @param predicate - Optional predicate function to filter nodes (e.g., isERBNode)
874
+ * @returns The matching node or null if not found
875
+ */
876
+ export function findNodeByLocation(root: Node, location: Location, predicate?: (node: Node) => boolean): any {
877
+ const visited = new Set<any>()
878
+
879
+ function search(node: any): any {
880
+ if (!node || visited.has(node)) return null
881
+ visited.add(node)
853
882
 
854
- const start = new Position(line, column)
855
- const end = new Position(line, column)
883
+ if (node.location && locationsEqual(node.location, location)) {
884
+ if (!predicate || predicate(node)) {
885
+ return node
886
+ }
887
+ }
888
+
889
+ const propsToCheck = ['tag_opening', 'tag_closing', 'tag_name', 'name', 'equals', 'value', 'content']
890
+ for (const prop of propsToCheck) {
891
+ if (node[prop]?.location && locationsEqual(node[prop].location, location)) {
892
+ if (!predicate || predicate(node)) {
893
+ return node
894
+ }
895
+ }
896
+ }
856
897
 
857
- return new Location(start, end)
898
+ if (typeof node.compactChildNodes === 'function') {
899
+ for (const child of node.compactChildNodes()) {
900
+ const found = search(child)
901
+ if (found) return found
902
+ }
903
+ } else {
904
+ if (node.children && Array.isArray(node.children)) {
905
+ for (const child of node.children) {
906
+ const found = search(child)
907
+ if (found) return found
908
+ }
909
+ }
910
+
911
+ if (node.body && Array.isArray(node.body)) {
912
+ for (const child of node.body) {
913
+ const found = search(child)
914
+ if (found) return found
915
+ }
916
+ }
917
+ }
918
+
919
+ return null
858
920
  }
921
+
922
+ return search(root)
923
+ }
924
+
925
+ /**
926
+ * AST Navigation Utilities
927
+ * These utilities help navigate the AST tree for complex autofix operations
928
+ */
929
+
930
+ /**
931
+ * Finds the parent node of a given child node in the AST
932
+ * @param root - The root node to search from (typically the document node)
933
+ * @param target - The child node to find the parent of
934
+ * @returns The parent node, or null if not found
935
+ *
936
+ * @example
937
+ * const parent = findParent(result.value, offense.autofixContext.node)
938
+ * if (parent?.type === "AST_HTML_ELEMENT_NODE") {
939
+ * // Modify parent...
940
+ * }
941
+ */
942
+ export function findParent(root: Node, target: Node): Node | null {
943
+ let parentNode: Node | null = null
944
+
945
+ const search = (node: Node, _parent: Node | null = null): void => {
946
+ if (parentNode) return
947
+
948
+ const nodeAny = node as any
949
+
950
+ if (nodeAny.children) {
951
+ for (const child of nodeAny.children) {
952
+ if (child === target) {
953
+ parentNode = node
954
+ return
955
+ }
956
+ }
957
+ }
958
+
959
+ const propsToCheck = ['open_tag', 'close_tag', 'body', 'name', 'value']
960
+
961
+ for (const prop of propsToCheck) {
962
+ const value = (node as any)[prop]
963
+ if (value === target) {
964
+ parentNode = node
965
+ return
966
+ }
967
+ if (Array.isArray(value) && value.includes(target)) {
968
+ parentNode = node
969
+ return
970
+ }
971
+ }
972
+
973
+ if (nodeAny.children) {
974
+ for (const child of nodeAny.children) {
975
+ search(child, node)
976
+ if (parentNode) return
977
+ }
978
+ }
979
+
980
+ for (const prop of propsToCheck) {
981
+ const value = (node as any)[prop]
982
+ if (Array.isArray(value)) {
983
+ for (const item of value) {
984
+ if (item && typeof item === 'object' && 'type' in item) {
985
+ search(item, node)
986
+ if (parentNode) return
987
+ }
988
+ }
989
+ } else if (value && typeof value === 'object' && 'type' in value) {
990
+ search(value, node)
991
+ if (parentNode) return
992
+ }
993
+ }
994
+ }
995
+
996
+ search(root)
997
+
998
+ return parentNode
999
+ }
1000
+
1001
+ export const DOCUMENT_ONLY_TAG_NAMES = new Set<string>([
1002
+ "html"
1003
+ ])
1004
+
1005
+ export const HTML_ONLY_TAG_NAMES = new Set<string>([
1006
+ "head", "body"
1007
+ ])
1008
+
1009
+ export const HEAD_ONLY_TAG_NAMES = new Set<string>([
1010
+ "base",
1011
+ "title",
1012
+ "style",
1013
+ "meta",
1014
+ "link",
1015
+ ])
1016
+
1017
+ export const HEAD_AND_BODY_TAG_NAMES = new Set<string>([
1018
+ "script",
1019
+ "noscript",
1020
+ "template",
1021
+ ])
1022
+
1023
+ export function isDocumentOnlyTag(tagName: string): boolean {
1024
+ return DOCUMENT_ONLY_TAG_NAMES.has(tagName.toLowerCase())
1025
+ }
1026
+
1027
+ export function isHtmlOnlyTag(tagName: string): boolean {
1028
+ return HTML_ONLY_TAG_NAMES.has(tagName.toLowerCase())
1029
+ }
1030
+
1031
+ export function isHeadOnlyTag(tagName: string): boolean {
1032
+ return HEAD_ONLY_TAG_NAMES.has(tagName.toLowerCase())
1033
+ }
1034
+
1035
+ export function isHeadAndBodyTag(tagName: string): boolean {
1036
+ return HEAD_AND_BODY_TAG_NAMES.has(tagName.toLowerCase())
1037
+ }
1038
+
1039
+ export function isBodyOnlyTag(tagName: string): boolean {
1040
+ const tag = tagName.toLowerCase()
1041
+
1042
+ return (
1043
+ !isDocumentOnlyTag(tag) &&
1044
+ !isHtmlOnlyTag(tag) &&
1045
+ !isHeadOnlyTag(tag) &&
1046
+ !isHeadAndBodyTag(tag)
1047
+ )
1048
+ }
1049
+
1050
+ export function isBodyTag(tagName: string): boolean {
1051
+ const tag = tagName.toLowerCase()
1052
+ return (
1053
+ !isDocumentOnlyTag(tag) &&
1054
+ !isHtmlOnlyTag(tag) &&
1055
+ (isBodyOnlyTag(tag) || isHeadAndBodyTag(tag))
1056
+ )
1057
+ }
1058
+
1059
+ export function isHeadTag(tagName: string): boolean {
1060
+ const tag = tagName.toLowerCase()
1061
+
1062
+ return (
1063
+ !isDocumentOnlyTag(tag) &&
1064
+ !isHtmlOnlyTag(tag) &&
1065
+ (isHeadOnlyTag(tag) || isHeadAndBodyTag(tag))
1066
+ )
859
1067
  }
@@ -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
  }