@herb-tools/linter 0.7.4 → 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 (395) hide show
  1. package/README.md +253 -13
  2. package/dist/herb-lint.js +26087 -3414
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +5783 -1568
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +5749 -1569
  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 +42 -35
  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 +109 -43
  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 +48 -0
  42. package/dist/src/rules/erb-comment-syntax.js.map +1 -0
  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 +70 -20
  60. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  61. package/dist/src/rules/erb-right-trim.js +45 -0
  62. package/dist/src/rules/erb-right-trim.js.map +1 -0
  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 +20 -2
  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} +46 -14
  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 +14 -0
  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 +14 -0
  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 +20 -2
  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 +14 -0
  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 +14 -0
  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 +20 -2
  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 +19 -3
  285. package/docs/rules/erb-comment-syntax.md +44 -0
  286. package/docs/rules/erb-no-case-node-children.md +50 -0
  287. package/docs/rules/erb-no-extra-newline.md +74 -0
  288. package/docs/rules/erb-no-extra-whitespace-inside-tags.md +39 -0
  289. package/docs/rules/{erb-requires-trailing-newline.md → erb-require-trailing-newline.md} +1 -1
  290. package/docs/rules/erb-right-trim.md +52 -0
  291. package/docs/rules/herb-disable-comment-malformed.md +45 -0
  292. package/docs/rules/herb-disable-comment-missing-rules.md +60 -0
  293. package/docs/rules/herb-disable-comment-no-duplicate-rules.md +49 -0
  294. package/docs/rules/herb-disable-comment-no-redundant-all.md +53 -0
  295. package/docs/rules/herb-disable-comment-unnecessary.md +44 -0
  296. package/docs/rules/herb-disable-comment-valid-rule-name.md +41 -0
  297. package/docs/rules/html-aria-attribute-must-be-valid.md +2 -5
  298. package/docs/rules/html-aria-label-is-well-formatted.md +1 -1
  299. package/docs/rules/html-attribute-double-quotes.md +2 -2
  300. package/docs/rules/html-attribute-equals-spacing.md +2 -2
  301. package/docs/rules/html-attribute-values-require-quotes.md +3 -3
  302. package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +2 -2
  303. package/docs/rules/html-body-only-elements.md +99 -0
  304. package/docs/rules/html-boolean-attributes-no-value.md +2 -2
  305. package/docs/rules/html-head-only-elements.md +81 -0
  306. package/docs/rules/html-input-require-autocomplete.md +64 -0
  307. package/docs/rules/html-no-aria-hidden-on-focusable.md +2 -2
  308. package/docs/rules/html-no-duplicate-attributes.md +2 -2
  309. package/docs/rules/html-no-duplicate-meta-names.md +64 -0
  310. package/docs/rules/html-no-empty-attributes.md +3 -3
  311. package/docs/rules/html-no-empty-headings.md +4 -26
  312. package/docs/rules/html-no-positive-tab-index.md +1 -2
  313. package/docs/rules/html-no-self-closing.md +17 -2
  314. package/docs/rules/html-no-space-in-tag.md +66 -0
  315. package/docs/rules/html-no-title-attribute.md +2 -2
  316. package/docs/rules/html-no-underscores-in-attribute-names.md +2 -2
  317. package/docs/rules/html-tag-name-lowercase.md +2 -2
  318. package/package.json +13 -5
  319. package/src/cli/argument-parser.ts +50 -39
  320. package/src/cli/file-processor.ts +159 -28
  321. package/src/cli/formatters/detailed-formatter.ts +21 -3
  322. package/src/cli/formatters/github-actions-formatter.ts +17 -1
  323. package/src/cli/formatters/json-formatter.ts +9 -0
  324. package/src/cli/formatters/simple-formatter.ts +24 -8
  325. package/src/cli/output-manager.ts +23 -3
  326. package/src/cli/summary-reporter.ts +40 -3
  327. package/src/cli.ts +137 -52
  328. package/src/custom-rule-loader.ts +189 -0
  329. package/src/herb-disable-comment-utils.ts +175 -0
  330. package/src/index.ts +2 -0
  331. package/src/linter.ts +501 -36
  332. package/src/loader.ts +30 -0
  333. package/src/rules/erb-comment-syntax.ts +73 -0
  334. package/src/rules/erb-no-case-node-children.ts +68 -0
  335. package/src/rules/erb-no-empty-tags.ts +9 -3
  336. package/src/rules/erb-no-extra-newline.ts +91 -0
  337. package/src/rules/erb-no-extra-whitespace-inside-tags.ts +147 -0
  338. package/src/rules/erb-no-output-control-flow.ts +9 -3
  339. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +9 -3
  340. package/src/rules/erb-prefer-image-tag-helper.ts +9 -3
  341. package/src/rules/erb-require-trailing-newline.ts +47 -0
  342. package/src/rules/erb-require-whitespace-inside-tags.ts +96 -26
  343. package/src/rules/erb-right-trim.ts +67 -0
  344. package/src/rules/herb-disable-comment-base.ts +76 -0
  345. package/src/rules/herb-disable-comment-malformed.ts +66 -0
  346. package/src/rules/herb-disable-comment-missing-rules.ts +41 -0
  347. package/src/rules/herb-disable-comment-no-duplicate-rules.ts +46 -0
  348. package/src/rules/herb-disable-comment-no-redundant-all.ts +40 -0
  349. package/src/rules/herb-disable-comment-unnecessary.ts +103 -0
  350. package/src/rules/herb-disable-comment-valid-rule-name.ts +62 -0
  351. package/src/rules/html-anchor-require-href.ts +9 -3
  352. package/src/rules/html-aria-attribute-must-be-valid.ts +9 -3
  353. package/src/rules/html-aria-label-is-well-formatted.ts +9 -5
  354. package/src/rules/html-aria-level-must-be-valid.ts +9 -2
  355. package/src/rules/html-aria-role-heading-requires-level.ts +9 -3
  356. package/src/rules/html-aria-role-must-be-valid.ts +9 -3
  357. package/src/rules/html-attribute-double-quotes.ts +42 -8
  358. package/src/rules/html-attribute-equals-spacing.ts +31 -7
  359. package/src/rules/html-attribute-values-require-quotes.ts +56 -10
  360. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +9 -3
  361. package/src/rules/html-body-only-elements.ts +60 -0
  362. package/src/rules/html-boolean-attributes-no-value.ts +31 -6
  363. package/src/rules/html-head-only-elements.ts +65 -0
  364. package/src/rules/html-iframe-has-title.ts +9 -4
  365. package/src/rules/html-img-require-alt.ts +10 -4
  366. package/src/rules/html-input-require-autocomplete.ts +85 -0
  367. package/src/rules/html-navigation-has-label.ts +9 -3
  368. package/src/rules/html-no-aria-hidden-on-focusable.ts +9 -3
  369. package/src/rules/html-no-block-inside-inline.ts +9 -3
  370. package/src/rules/html-no-duplicate-attributes.ts +9 -3
  371. package/src/rules/html-no-duplicate-ids.ts +11 -7
  372. package/src/rules/html-no-duplicate-meta-names.ts +188 -0
  373. package/src/rules/html-no-empty-attributes.ts +58 -10
  374. package/src/rules/html-no-empty-headings.ts +10 -8
  375. package/src/rules/html-no-nested-links.ts +10 -4
  376. package/src/rules/html-no-positive-tab-index.ts +9 -3
  377. package/src/rules/html-no-self-closing.ts +69 -9
  378. package/src/rules/html-no-space-in-tag.ts +221 -0
  379. package/src/rules/html-no-title-attribute.ts +9 -3
  380. package/src/rules/html-no-underscores-in-attribute-names.ts +12 -4
  381. package/src/rules/html-tag-name-lowercase.ts +41 -10
  382. package/src/rules/index.ts +24 -2
  383. package/src/rules/parser-no-errors.ts +8 -1
  384. package/src/rules/rule-utils.ts +250 -44
  385. package/src/rules/svg-tag-name-capitalization.ts +39 -6
  386. package/src/{default-rules.ts → rules.ts} +53 -13
  387. package/src/types.ts +133 -15
  388. package/dist/src/default-rules.js.map +0 -1
  389. package/dist/src/rules/erb-requires-trailing-newline.js +0 -22
  390. package/dist/src/rules/erb-requires-trailing-newline.js.map +0 -1
  391. package/dist/types/default-rules.d.ts +0 -2
  392. package/dist/types/rules/erb-requires-trailing-newline.d.ts +0 -6
  393. package/dist/types/src/default-rules.d.ts +0 -2
  394. package/dist/types/src/rules/erb-requires-trailing-newline.d.ts +0 -6
  395. package/src/rules/erb-requires-trailing-newline.ts +0 -29
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js"
2
2
  import { AttributeVisitorMixin, VALID_ARIA_ROLES, StaticAttributeStaticValueParams } from "./rule-utils.js"
3
3
 
4
- import type { LintOffense, LintContext } from "../types.js"
4
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5
5
  import type { ParseResult } from "@herb-tools/core"
6
6
 
7
7
  class AriaRoleMustBeValid extends AttributeVisitorMixin {
@@ -13,7 +13,6 @@ class AriaRoleMustBeValid extends AttributeVisitorMixin {
13
13
  this.addOffense(
14
14
  `The \`role\` attribute must be a valid ARIA role. Role \`${attributeValue}\` is not recognized.`,
15
15
  attributeNode.location,
16
- "error"
17
16
  )
18
17
  }
19
18
  }
@@ -21,7 +20,14 @@ class AriaRoleMustBeValid extends AttributeVisitorMixin {
21
20
  export class HTMLAriaRoleMustBeValidRule extends ParserRule {
22
21
  name = "html-aria-role-must-be-valid"
23
22
 
24
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
23
+ get defaultConfig(): FullRuleConfig {
24
+ return {
25
+ enabled: true,
26
+ severity: "error"
27
+ }
28
+ }
29
+
30
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
25
31
  const visitor = new AriaRoleMustBeValid(this.name, context)
26
32
 
27
33
  visitor.visit(result.value)
@@ -1,11 +1,16 @@
1
- import { ParserRule } from "../types.js"
1
+ import { ParserRule, BaseAutofixContext, Mutable } from "../types.js"
2
2
  import { AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams, getAttributeValueQuoteType, hasAttributeValue } from "./rule-utils.js"
3
3
  import { filterLiteralNodes } from "@herb-tools/core"
4
4
 
5
- import type { LintOffense, LintContext } from "../types.js"
6
- import type { ParseResult } from "@herb-tools/core"
5
+ import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type { ParseResult, HTMLAttributeNode } from "@herb-tools/core"
7
7
 
8
- class AttributeDoubleQuotesVisitor extends AttributeVisitorMixin {
8
+ interface AttributeDoubleQuotesAutofixContext extends BaseAutofixContext {
9
+ node: Mutable<HTMLAttributeNode>
10
+ valueContent: string
11
+ }
12
+
13
+ class AttributeDoubleQuotesVisitor extends AttributeVisitorMixin<AttributeDoubleQuotesAutofixContext> {
9
14
  protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams) {
10
15
  if (!hasAttributeValue(attributeNode)) return
11
16
  if (getAttributeValueQuoteType(attributeNode) !== "single") return
@@ -14,7 +19,10 @@ class AttributeDoubleQuotesVisitor extends AttributeVisitorMixin {
14
19
  this.addOffense(
15
20
  `Attribute \`${attributeName}\` uses single quotes. Prefer double quotes for HTML attribute values: \`${attributeName}="${attributeValue}"\`.`,
16
21
  attributeNode.value!.location,
17
- "warning"
22
+ {
23
+ node: attributeNode,
24
+ valueContent: attributeValue
25
+ }
18
26
  )
19
27
  }
20
28
 
@@ -26,19 +34,45 @@ class AttributeDoubleQuotesVisitor extends AttributeVisitorMixin {
26
34
  this.addOffense(
27
35
  `Attribute \`${attributeName}\` uses single quotes. Prefer double quotes for HTML attribute values: \`${attributeName}="${combinedValue}"\`.`,
28
36
  attributeNode.value!.location,
29
- "warning"
37
+ {
38
+ node: attributeNode,
39
+ valueContent: combinedValue || ""
40
+ }
30
41
  )
31
42
  }
32
43
  }
33
44
 
34
- export class HTMLAttributeDoubleQuotesRule extends ParserRule {
45
+ export class HTMLAttributeDoubleQuotesRule extends ParserRule<AttributeDoubleQuotesAutofixContext> {
46
+ static autocorrectable = true
35
47
  name = "html-attribute-double-quotes"
36
48
 
37
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
49
+ get defaultConfig(): FullRuleConfig {
50
+ return {
51
+ enabled: true,
52
+ severity: "warning"
53
+ }
54
+ }
55
+
56
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<AttributeDoubleQuotesAutofixContext>[] {
38
57
  const visitor = new AttributeDoubleQuotesVisitor(this.name, context)
39
58
 
40
59
  visitor.visit(result.value)
41
60
 
42
61
  return visitor.offenses
43
62
  }
63
+
64
+ autofix(offense: LintOffense<AttributeDoubleQuotesAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
65
+ if (!offense.autofixContext) return null
66
+
67
+ const { node: { value } } = offense.autofixContext
68
+
69
+ if (!value) return null
70
+ if (!value.open_quote) return null
71
+ if (!value.close_quote) return null
72
+
73
+ value.open_quote.value = '"'
74
+ value.close_quote.value = '"'
75
+
76
+ return result
77
+ }
44
78
  }
@@ -1,10 +1,14 @@
1
1
  import { BaseRuleVisitor } from "./rule-utils.js"
2
- import { ParserRule } from "../types.js"
2
+ import { ParserRule, BaseAutofixContext, Mutable } from "../types.js"
3
3
 
4
- import type { LintOffense, LintContext } from "../types.js"
4
+ import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
5
5
  import type { ParseResult, HTMLAttributeNode } from "@herb-tools/core"
6
6
 
7
- class HTMLAttributeEqualsSpacingVisitor extends BaseRuleVisitor {
7
+ interface AttributeEqualsSpacingAutofixContext extends BaseAutofixContext {
8
+ node: Mutable<HTMLAttributeNode>
9
+ }
10
+
11
+ class HTMLAttributeEqualsSpacingVisitor extends BaseRuleVisitor<AttributeEqualsSpacingAutofixContext> {
8
12
  visitHTMLAttributeNode(attribute: HTMLAttributeNode): void {
9
13
  if (!attribute.equals || !attribute.name || !attribute.value) {
10
14
  return
@@ -14,7 +18,7 @@ class HTMLAttributeEqualsSpacingVisitor extends BaseRuleVisitor {
14
18
  this.addOffense(
15
19
  "Remove whitespace before `=` in HTML attribute",
16
20
  attribute.equals.location,
17
- "error"
21
+ { node: attribute }
18
22
  )
19
23
  }
20
24
 
@@ -22,20 +26,40 @@ class HTMLAttributeEqualsSpacingVisitor extends BaseRuleVisitor {
22
26
  this.addOffense(
23
27
  "Remove whitespace after `=` in HTML attribute",
24
28
  attribute.equals.location,
25
- "error"
29
+ { node: attribute }
26
30
  )
27
31
  }
28
32
  }
29
33
  }
30
34
 
31
- export class HTMLAttributeEqualsSpacingRule extends ParserRule {
35
+ export class HTMLAttributeEqualsSpacingRule extends ParserRule<AttributeEqualsSpacingAutofixContext> {
36
+ static autocorrectable = true
32
37
  name = "html-attribute-equals-spacing"
33
38
 
34
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
39
+ get defaultConfig(): FullRuleConfig {
40
+ return {
41
+ enabled: true,
42
+ severity: "error"
43
+ }
44
+ }
45
+
46
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<AttributeEqualsSpacingAutofixContext>[] {
35
47
  const visitor = new HTMLAttributeEqualsSpacingVisitor(this.name, context)
36
48
 
37
49
  visitor.visit(result.value)
38
50
 
39
51
  return visitor.offenses
40
52
  }
53
+
54
+ autofix(offense: LintOffense<AttributeEqualsSpacingAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
55
+ if (!offense.autofixContext) return null
56
+
57
+ const { node: { equals } } = offense.autofixContext
58
+
59
+ if (!equals) return null
60
+
61
+ equals.value = "="
62
+
63
+ return result
64
+ }
41
65
  }
@@ -1,10 +1,16 @@
1
- import { ParserRule } from "../types.js"
1
+ import { Token, Location } from "@herb-tools/core"
2
+ import { ParserRule, BaseAutofixContext, Mutable } from "../types.js"
2
3
  import { AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams } from "./rule-utils.js"
3
4
 
4
- import type { LintOffense, LintContext } from "../types.js"
5
- import type { HTMLAttributeNode, HTMLAttributeValueNode, ParseResult } from "@herb-tools/core"
5
+ import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type { HTMLAttributeNode, ParseResult, } from "@herb-tools/core"
6
7
 
7
- class AttributeValuesRequireQuotesVisitor extends AttributeVisitorMixin {
8
+ interface AttributeValuesRequireQuotesAutofixContext extends BaseAutofixContext {
9
+ node: Mutable<HTMLAttributeNode>
10
+ unquotedValue: string
11
+ }
12
+
13
+ class AttributeValuesRequireQuotesVisitor extends AttributeVisitorMixin<AttributeValuesRequireQuotesAutofixContext> {
8
14
  protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams): void {
9
15
  if (this.hasAttributeValue(attributeNode)) return
10
16
  if (this.isQuoted(attributeNode)) return
@@ -12,7 +18,10 @@ class AttributeValuesRequireQuotesVisitor extends AttributeVisitorMixin {
12
18
  this.addOffense(
13
19
  `Attribute value should be quoted: \`${attributeName}="${attributeValue}"\`. Always wrap attribute values in quotes.`,
14
20
  attributeNode.value!.location,
15
- "error"
21
+ {
22
+ node: attributeNode,
23
+ unquotedValue: attributeValue
24
+ }
16
25
  )
17
26
  }
18
27
 
@@ -23,7 +32,10 @@ class AttributeValuesRequireQuotesVisitor extends AttributeVisitorMixin {
23
32
  this.addOffense(
24
33
  `Attribute value should be quoted: \`${attributeName}="${combinedValue}"\`. Always wrap attribute values in quotes.`,
25
34
  attributeNode.value!.location,
26
- "error"
35
+ {
36
+ node: attributeNode,
37
+ unquotedValue: combinedValue || ""
38
+ }
27
39
  )
28
40
  }
29
41
 
@@ -32,20 +44,54 @@ class AttributeValuesRequireQuotesVisitor extends AttributeVisitorMixin {
32
44
  }
33
45
 
34
46
  private isQuoted(attributeNode: HTMLAttributeNode): boolean {
35
- const valueNode = attributeNode.value as HTMLAttributeValueNode
47
+ const valueNode = attributeNode.value
36
48
 
37
- return valueNode.quoted
49
+ return valueNode ? valueNode.quoted : false
38
50
  }
39
51
  }
40
52
 
41
- export class HTMLAttributeValuesRequireQuotesRule extends ParserRule {
53
+ export class HTMLAttributeValuesRequireQuotesRule extends ParserRule<AttributeValuesRequireQuotesAutofixContext> {
54
+ static autocorrectable = true
42
55
  name = "html-attribute-values-require-quotes"
43
56
 
44
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
57
+ get defaultConfig(): FullRuleConfig {
58
+ return {
59
+ enabled: true,
60
+ severity: "error"
61
+ }
62
+ }
63
+
64
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<AttributeValuesRequireQuotesAutofixContext>[] {
45
65
  const visitor = new AttributeValuesRequireQuotesVisitor(this.name, context)
46
66
 
47
67
  visitor.visit(result.value)
48
68
 
49
69
  return visitor.offenses
50
70
  }
71
+
72
+ autofix(offense: LintOffense<AttributeValuesRequireQuotesAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
73
+ if (!offense.autofixContext) return null
74
+
75
+ const { node: { value } } = offense.autofixContext
76
+
77
+ if (!value) return null
78
+
79
+ const quote = Token.from({ type: "TOKEN_QUOTE", value: '"', location: Location.zero, range: [0, 0] })
80
+
81
+ if (value.open_quote) {
82
+ value.open_quote.value = '"'
83
+ } else {
84
+ value.open_quote = quote
85
+ }
86
+
87
+ if (value.close_quote) {
88
+ value.close_quote.value = '"'
89
+ } else {
90
+ value.close_quote = quote
91
+ }
92
+
93
+ value.quoted = true
94
+
95
+ return result
96
+ }
51
97
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js"
2
2
  import { BaseRuleVisitor, getTagName, hasAttribute, getAttributes, findAttributeByName } from "./rule-utils.js"
3
3
 
4
- import type { LintOffense, LintContext } from "../types.js"
4
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5
5
  import type { HTMLOpenTagNode, HTMLAttributeValueNode, ParseResult, Node } from "@herb-tools/core"
6
6
 
7
7
  const ELEMENTS_WITH_NATIVE_DISABLED_ATTRIBUTE_SUPPORT = new Set([
@@ -32,7 +32,6 @@ class AvoidBothDisabledAndAriaDisabledVisitor extends BaseRuleVisitor {
32
32
  this.addOffense(
33
33
  "aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.",
34
34
  node.tag_name!.location,
35
- "error"
36
35
  )
37
36
  }
38
37
  }
@@ -56,7 +55,14 @@ class AvoidBothDisabledAndAriaDisabledVisitor extends BaseRuleVisitor {
56
55
  export class HTMLAvoidBothDisabledAndAriaDisabledRule extends ParserRule {
57
56
  name = "html-avoid-both-disabled-and-aria-disabled"
58
57
 
59
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
58
+ get defaultConfig(): FullRuleConfig {
59
+ return {
60
+ enabled: true,
61
+ severity: "error"
62
+ }
63
+ }
64
+
65
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
60
66
  const visitor = new AvoidBothDisabledAndAriaDisabledVisitor(this.name, context)
61
67
 
62
68
  visitor.visit(result.value)
@@ -0,0 +1,60 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { BaseRuleVisitor, getTagName, isBodyOnlyTag } from "./rule-utils.js"
3
+
4
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5
+ import type { HTMLElementNode, ParseResult } from "@herb-tools/core"
6
+
7
+ class HTMLBodyOnlyElementsVisitor extends BaseRuleVisitor {
8
+ private elementStack: string[] = []
9
+
10
+ visitHTMLElementNode(node: HTMLElementNode): void {
11
+ const tagName = getTagName(node.open_tag)?.toLowerCase()
12
+ if (!tagName) return
13
+
14
+ this.checkBodyOnlyElement(node, tagName)
15
+
16
+ this.elementStack.push(tagName)
17
+ this.visitChildNodes(node)
18
+ this.elementStack.pop()
19
+ }
20
+
21
+ private checkBodyOnlyElement(node: HTMLElementNode, tagName: string): void {
22
+ if (this.insideBody) return
23
+ if (!this.insideHead) return
24
+ if (!isBodyOnlyTag(tagName)) return
25
+
26
+ this.addOffense(
27
+ `Element \`<${tagName}>\` must be placed inside the \`<body>\` tag.`,
28
+ node.location,
29
+ )
30
+ }
31
+
32
+ private get insideBody(): boolean {
33
+ return this.elementStack.includes("body")
34
+ }
35
+
36
+ private get insideHead(): boolean {
37
+ return this.elementStack.includes("head")
38
+ }
39
+ }
40
+
41
+ export class HTMLBodyOnlyElementsRule extends ParserRule {
42
+ static autocorrectable = false
43
+ name = "html-body-only-elements"
44
+
45
+ get defaultConfig(): FullRuleConfig {
46
+ return {
47
+ enabled: true,
48
+ severity: "error",
49
+ exclude: ["**/*.xml", "**/*.xml.erb"]
50
+ }
51
+ }
52
+
53
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
54
+ const visitor = new HTMLBodyOnlyElementsVisitor(this.name, context)
55
+
56
+ visitor.visit(result.value)
57
+
58
+ return visitor.offenses
59
+ }
60
+ }
@@ -1,11 +1,15 @@
1
- import { ParserRule } from "../types.js"
1
+ import { ParserRule, BaseAutofixContext, Mutable } from "../types.js"
2
2
  import { AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams, isBooleanAttribute, hasAttributeValue } from "./rule-utils.js"
3
3
  import { IdentityPrinter } from "@herb-tools/printer"
4
4
 
5
- import type { LintOffense, LintContext } from "../types.js"
5
+ import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
6
6
  import type { ParseResult, HTMLAttributeNode } from "@herb-tools/core"
7
7
 
8
- class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
8
+ interface BooleanAttributeAutofixContext extends BaseAutofixContext {
9
+ node: Mutable<HTMLAttributeNode>
10
+ }
11
+
12
+ class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin<BooleanAttributeAutofixContext> {
9
13
  protected checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }: StaticAttributeStaticValueParams) {
10
14
  this.checkAttribute(originalAttributeName, attributeNode)
11
15
  }
@@ -21,19 +25,40 @@ class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
21
25
  this.addOffense(
22
26
  `Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`,
23
27
  attributeNode.value!.location,
24
- "error"
28
+ {
29
+ node: attributeNode
30
+ }
25
31
  )
26
32
  }
27
33
  }
28
34
 
29
- export class HTMLBooleanAttributesNoValueRule extends ParserRule {
35
+ export class HTMLBooleanAttributesNoValueRule extends ParserRule<BooleanAttributeAutofixContext> {
36
+ static autocorrectable = true
30
37
  name = "html-boolean-attributes-no-value"
31
38
 
32
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
39
+ get defaultConfig(): FullRuleConfig {
40
+ return {
41
+ enabled: true,
42
+ severity: "error"
43
+ }
44
+ }
45
+
46
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<BooleanAttributeAutofixContext>[] {
33
47
  const visitor = new BooleanAttributesNoValueVisitor(this.name, context)
34
48
 
35
49
  visitor.visit(result.value)
36
50
 
37
51
  return visitor.offenses
38
52
  }
53
+
54
+ autofix(offense: LintOffense<BooleanAttributeAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
55
+ if (!offense.autofixContext) return null
56
+
57
+ const { node } = offense.autofixContext
58
+
59
+ node.equals = null
60
+ node.value = null
61
+
62
+ return result
63
+ }
39
64
  }
@@ -0,0 +1,65 @@
1
+ import { ParserRule } from "../types"
2
+ import { BaseRuleVisitor, getTagName, isHeadOnlyTag } from "./rule-utils"
3
+
4
+ import type { ParseResult, HTMLElementNode } from "@herb-tools/core"
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types"
6
+
7
+ class HeadOnlyElementsVisitor extends BaseRuleVisitor {
8
+ private elementStack: string[] = []
9
+
10
+ visitHTMLElementNode(node: HTMLElementNode): void {
11
+ const tagName = getTagName(node)?.toLowerCase()
12
+ if (!tagName) return
13
+
14
+ this.checkHeadOnlyElement(node, tagName)
15
+
16
+ this.elementStack.push(tagName)
17
+ this.visitChildNodes(node)
18
+ this.elementStack.pop()
19
+ }
20
+
21
+ private checkHeadOnlyElement(node: HTMLElementNode, tagName: string): void {
22
+ if (this.insideHead) return
23
+ if (!this.insideBody) return
24
+ if (!isHeadOnlyTag(tagName)) return
25
+ if (tagName === "title" && this.insideSVG) return
26
+
27
+ this.addOffense(
28
+ `Element \`<${tagName}>\` must be placed inside the \`<head>\` tag.`,
29
+ node.location,
30
+ )
31
+ }
32
+
33
+ private get insideHead(): boolean {
34
+ return this.elementStack.includes("head")
35
+ }
36
+
37
+ private get insideBody(): boolean {
38
+ return this.elementStack.includes("body")
39
+ }
40
+
41
+ private get insideSVG(): boolean {
42
+ return this.elementStack.includes("svg")
43
+ }
44
+ }
45
+
46
+ export class HTMLHeadOnlyElementsRule extends ParserRule {
47
+ static autocorrectable = false
48
+ name = "html-head-only-elements"
49
+
50
+ get defaultConfig(): FullRuleConfig {
51
+ return {
52
+ enabled: true,
53
+ severity: "error",
54
+ exclude: ["**/*.xml", "**/*.xml.erb"]
55
+ }
56
+ }
57
+
58
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
59
+ const visitor = new HeadOnlyElementsVisitor(this.name, context)
60
+
61
+ visitor.visit(result.value)
62
+
63
+ return visitor.offenses
64
+ }
65
+ }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js"
2
2
  import { BaseRuleVisitor, getTagName, getAttribute, getAttributeValue } from "./rule-utils.js"
3
3
 
4
- import type { LintOffense, LintContext } from "../types.js"
4
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5
5
  import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
6
6
 
7
7
  class IframeHasTitleVisitor extends BaseRuleVisitor {
@@ -31,7 +31,6 @@ class IframeHasTitleVisitor extends BaseRuleVisitor {
31
31
  this.addOffense(
32
32
  "`<iframe>` elements must have a `title` attribute that describes the content of the frame for screen reader users.",
33
33
  node.location,
34
- "error"
35
34
  )
36
35
 
37
36
  return
@@ -43,7 +42,6 @@ class IframeHasTitleVisitor extends BaseRuleVisitor {
43
42
  this.addOffense(
44
43
  "`<iframe>` elements must have a `title` attribute that describes the content of the frame for screen reader users.",
45
44
  node.location,
46
- "error"
47
45
  )
48
46
  }
49
47
  }
@@ -52,7 +50,14 @@ class IframeHasTitleVisitor extends BaseRuleVisitor {
52
50
  export class HTMLIframeHasTitleRule extends ParserRule {
53
51
  name = "html-iframe-has-title"
54
52
 
55
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
53
+ get defaultConfig(): FullRuleConfig {
54
+ return {
55
+ enabled: true,
56
+ severity: "error"
57
+ }
58
+ }
59
+
60
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
56
61
  const visitor = new IframeHasTitleVisitor(this.name, context)
57
62
 
58
63
  visitor.visit(result.value)
@@ -1,7 +1,7 @@
1
1
  import { BaseRuleVisitor, getTagName, hasAttribute } from "./rule-utils.js"
2
2
 
3
3
  import { ParserRule } from "../types.js"
4
- import type { LintOffense, LintContext } from "../types.js"
4
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5
5
  import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
6
6
 
7
7
  class ImgRequireAltVisitor extends BaseRuleVisitor {
@@ -20,8 +20,7 @@ class ImgRequireAltVisitor extends BaseRuleVisitor {
20
20
  if (!hasAttribute(node, "alt")) {
21
21
  this.addOffense(
22
22
  'Missing required `alt` attribute on `<img>` tag. Add `alt=""` for decorative images or `alt="description"` for informative images.',
23
- node.tag_name!.location,
24
- "error"
23
+ node.tag_name!.location
25
24
  )
26
25
  }
27
26
  }
@@ -30,7 +29,14 @@ class ImgRequireAltVisitor extends BaseRuleVisitor {
30
29
  export class HTMLImgRequireAltRule extends ParserRule {
31
30
  name = "html-img-require-alt"
32
31
 
33
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
32
+ get defaultConfig(): FullRuleConfig {
33
+ return {
34
+ enabled: true,
35
+ severity: "error"
36
+ }
37
+ }
38
+
39
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
34
40
  const visitor = new ImgRequireAltVisitor(this.name, context)
35
41
  visitor.visit(result.value)
36
42
  return visitor.offenses
@@ -0,0 +1,85 @@
1
+ import { getTagName } from "@herb-tools/core"
2
+ import { BaseRuleVisitor, getAttribute, getAttributeValue, getStaticAttributeValueContent } from "./rule-utils.js"
3
+ import { ParserRule } from "../types.js"
4
+
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type { ParseResult, HTMLOpenTagNode } from "@herb-tools/core"
7
+
8
+ class HTMLInputRequireAutocompleteVisitor extends BaseRuleVisitor {
9
+ readonly HTML_INPUT_TYPES_REQUIRING_AUTOCOMPLETE = new Set([
10
+ "color",
11
+ "date",
12
+ "datetime-local",
13
+ "email",
14
+ "month",
15
+ "number",
16
+ "password",
17
+ "range",
18
+ "search",
19
+ "tel",
20
+ "text",
21
+ "time",
22
+ "url",
23
+ "week",
24
+ ])
25
+
26
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
27
+ this.checkInputTag(node)
28
+ }
29
+
30
+ private checkInputTag(node: HTMLOpenTagNode): void {
31
+ if (!this.isInputTag(node) || this.hasAutocomplete(node)) return
32
+
33
+ const typeAttribute = getAttribute(node, "type");
34
+ if (!typeAttribute) return
35
+
36
+ const typeValue = getStaticAttributeValueContent(typeAttribute)
37
+ if (!typeValue) return
38
+
39
+ if (!this.HTML_INPUT_TYPES_REQUIRING_AUTOCOMPLETE.has(typeValue)) return
40
+
41
+ this.addOffense(
42
+ "Add an `autocomplete` attribute to improve form accessibility. Use a specific value (e.g., `autocomplete=\"email\"`), `autocomplete=\"on\"` for defaults, or `autocomplete=\"off\"` to disable.",
43
+ node.location
44
+ )
45
+ }
46
+
47
+ private hasAutocomplete(node: HTMLOpenTagNode) {
48
+ const autocompleteAttribute = getAttribute(node, "autocomplete");
49
+ if (!autocompleteAttribute) return false
50
+
51
+ const autocompleteValue = getAttributeValue(autocompleteAttribute)
52
+ if (!autocompleteValue) return false
53
+
54
+ return true
55
+ }
56
+
57
+ private isInputTag(node: HTMLOpenTagNode) {
58
+ const tagName = getTagName(node);
59
+
60
+ if (tagName === "input") {
61
+ return true
62
+ } else {
63
+ return false
64
+ }
65
+ }
66
+ }
67
+
68
+ export class HTMLInputRequireAutocompleteRule extends ParserRule {
69
+ name = "html-input-require-autocomplete"
70
+
71
+ get defaultConfig(): FullRuleConfig {
72
+ return {
73
+ enabled: true,
74
+ severity: "error"
75
+ }
76
+ }
77
+
78
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
79
+ const visitor = new HTMLInputRequireAutocompleteVisitor(this.name, context)
80
+
81
+ visitor.visit(result.value)
82
+
83
+ return visitor.offenses
84
+ }
85
+ }