@herb-tools/linter 0.7.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (394) hide show
  1. package/README.md +253 -13
  2. package/dist/herb-lint.js +26023 -3424
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +5759 -1583
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +5727 -1584
  7. package/dist/index.js.map +1 -1
  8. package/dist/loader.cjs +17010 -0
  9. package/dist/loader.cjs.map +1 -0
  10. package/dist/loader.js +16879 -0
  11. package/dist/loader.js.map +1 -0
  12. package/dist/package.json +13 -5
  13. package/dist/src/cli/argument-parser.js +38 -33
  14. package/dist/src/cli/argument-parser.js.map +1 -1
  15. package/dist/src/cli/file-processor.js +124 -23
  16. package/dist/src/cli/file-processor.js.map +1 -1
  17. package/dist/src/cli/formatters/detailed-formatter.js +18 -3
  18. package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
  19. package/dist/src/cli/formatters/github-actions-formatter.js +15 -1
  20. package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -1
  21. package/dist/src/cli/formatters/json-formatter.js +3 -0
  22. package/dist/src/cli/formatters/json-formatter.js.map +1 -1
  23. package/dist/src/cli/formatters/simple-formatter.js +20 -7
  24. package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
  25. package/dist/src/cli/output-manager.js +22 -3
  26. package/dist/src/cli/output-manager.js.map +1 -1
  27. package/dist/src/cli/summary-reporter.js +26 -3
  28. package/dist/src/cli/summary-reporter.js.map +1 -1
  29. package/dist/src/cli.js +107 -42
  30. package/dist/src/cli.js.map +1 -1
  31. package/dist/src/custom-rule-loader.js +139 -0
  32. package/dist/src/custom-rule-loader.js.map +1 -0
  33. package/dist/src/herb-disable-comment-utils.js +129 -0
  34. package/dist/src/herb-disable-comment-utils.js.map +1 -0
  35. package/dist/src/index.js +1 -0
  36. package/dist/src/index.js.map +1 -1
  37. package/dist/src/linter.js +369 -34
  38. package/dist/src/linter.js.map +1 -1
  39. package/dist/src/loader.js +17 -0
  40. package/dist/src/loader.js.map +1 -0
  41. package/dist/src/rules/erb-comment-syntax.js +31 -2
  42. package/dist/src/rules/erb-comment-syntax.js.map +1 -1
  43. package/dist/src/rules/erb-no-case-node-children.js +52 -0
  44. package/dist/src/rules/erb-no-case-node-children.js.map +1 -0
  45. package/dist/src/rules/erb-no-empty-tags.js +7 -1
  46. package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
  47. package/dist/src/rules/erb-no-extra-newline.js +65 -0
  48. package/dist/src/rules/erb-no-extra-newline.js.map +1 -0
  49. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js +95 -0
  50. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js.map +1 -0
  51. package/dist/src/rules/erb-no-output-control-flow.js +7 -1
  52. package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
  53. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js +7 -1
  54. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -1
  55. package/dist/src/rules/erb-prefer-image-tag-helper.js +7 -1
  56. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
  57. package/dist/src/rules/erb-require-trailing-newline.js +35 -0
  58. package/dist/src/rules/erb-require-trailing-newline.js.map +1 -0
  59. package/dist/src/rules/erb-require-whitespace-inside-tags.js +69 -11
  60. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  61. package/dist/src/rules/erb-right-trim.js +26 -9
  62. package/dist/src/rules/erb-right-trim.js.map +1 -1
  63. package/dist/src/rules/herb-disable-comment-base.js +51 -0
  64. package/dist/src/rules/herb-disable-comment-base.js.map +1 -0
  65. package/dist/src/rules/herb-disable-comment-malformed.js +51 -0
  66. package/dist/src/rules/herb-disable-comment-malformed.js.map +1 -0
  67. package/dist/src/rules/herb-disable-comment-missing-rules.js +29 -0
  68. package/dist/src/rules/herb-disable-comment-missing-rules.js.map +1 -0
  69. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js +32 -0
  70. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js.map +1 -0
  71. package/dist/src/rules/herb-disable-comment-no-redundant-all.js +31 -0
  72. package/dist/src/rules/herb-disable-comment-no-redundant-all.js.map +1 -0
  73. package/dist/src/rules/herb-disable-comment-unnecessary.js +65 -0
  74. package/dist/src/rules/herb-disable-comment-unnecessary.js.map +1 -0
  75. package/dist/src/rules/herb-disable-comment-valid-rule-name.js +44 -0
  76. package/dist/src/rules/herb-disable-comment-valid-rule-name.js.map +1 -0
  77. package/dist/src/rules/html-anchor-require-href.js +7 -1
  78. package/dist/src/rules/html-anchor-require-href.js.map +1 -1
  79. package/dist/src/rules/html-aria-attribute-must-be-valid.js +7 -1
  80. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
  81. package/dist/src/rules/html-aria-label-is-well-formatted.js +9 -3
  82. package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -1
  83. package/dist/src/rules/html-aria-level-must-be-valid.js +6 -0
  84. package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
  85. package/dist/src/rules/html-aria-role-heading-requires-level.js +7 -1
  86. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
  87. package/dist/src/rules/html-aria-role-must-be-valid.js +7 -1
  88. package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
  89. package/dist/src/rules/html-attribute-double-quotes.js +29 -2
  90. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
  91. package/dist/src/rules/html-attribute-equals-spacing.js +18 -2
  92. package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -1
  93. package/dist/src/rules/html-attribute-values-require-quotes.js +39 -3
  94. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
  95. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js +7 -1
  96. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -1
  97. package/dist/src/rules/html-body-only-elements.js +46 -0
  98. package/dist/src/rules/html-body-only-elements.js.map +1 -0
  99. package/dist/src/rules/html-boolean-attributes-no-value.js +18 -1
  100. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  101. package/dist/src/rules/html-head-only-elements.js +51 -0
  102. package/dist/src/rules/html-head-only-elements.js.map +1 -0
  103. package/dist/src/rules/html-iframe-has-title.js +8 -2
  104. package/dist/src/rules/html-iframe-has-title.js.map +1 -1
  105. package/dist/src/rules/html-img-require-alt.js +7 -1
  106. package/dist/src/rules/html-img-require-alt.js.map +1 -1
  107. package/dist/src/rules/html-input-require-autocomplete.js +70 -0
  108. package/dist/src/rules/html-input-require-autocomplete.js.map +1 -0
  109. package/dist/src/rules/html-navigation-has-label.js +7 -1
  110. package/dist/src/rules/html-navigation-has-label.js.map +1 -1
  111. package/dist/src/rules/html-no-aria-hidden-on-focusable.js +7 -1
  112. package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -1
  113. package/dist/src/rules/html-no-block-inside-inline.js +7 -1
  114. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
  115. package/dist/src/rules/html-no-duplicate-attributes.js +7 -1
  116. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
  117. package/dist/src/rules/html-no-duplicate-ids.js +9 -3
  118. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
  119. package/dist/src/rules/html-no-duplicate-meta-names.js +136 -0
  120. package/dist/src/rules/html-no-duplicate-meta-names.js.map +1 -0
  121. package/dist/src/rules/html-no-empty-attributes.js +45 -7
  122. package/dist/src/rules/html-no-empty-attributes.js.map +1 -1
  123. package/dist/src/rules/html-no-empty-headings.js +7 -6
  124. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  125. package/dist/src/rules/html-no-nested-links.js +7 -1
  126. package/dist/src/rules/html-no-nested-links.js.map +1 -1
  127. package/dist/src/rules/html-no-positive-tab-index.js +7 -1
  128. package/dist/src/rules/html-no-positive-tab-index.js.map +1 -1
  129. package/dist/src/rules/html-no-self-closing.js +48 -3
  130. package/dist/src/rules/html-no-self-closing.js.map +1 -1
  131. package/dist/src/rules/html-no-space-in-tag.js +173 -0
  132. package/dist/src/rules/html-no-space-in-tag.js.map +1 -0
  133. package/dist/src/rules/html-no-title-attribute.js +7 -1
  134. package/dist/src/rules/html-no-title-attribute.js.map +1 -1
  135. package/dist/src/rules/html-no-underscores-in-attribute-names.js +7 -1
  136. package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +1 -1
  137. package/dist/src/rules/html-tag-name-lowercase.js +23 -5
  138. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  139. package/dist/src/rules/index.js +19 -3
  140. package/dist/src/rules/index.js.map +1 -1
  141. package/dist/src/rules/parser-no-errors.js +6 -0
  142. package/dist/src/rules/parser-no-errors.js.map +1 -1
  143. package/dist/src/rules/rule-utils.js +211 -31
  144. package/dist/src/rules/rule-utils.js.map +1 -1
  145. package/dist/src/rules/svg-tag-name-capitalization.js +22 -2
  146. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
  147. package/dist/src/{default-rules.js → rules.js} +44 -16
  148. package/dist/src/rules.js.map +1 -0
  149. package/dist/src/types.js +34 -1
  150. package/dist/src/types.js.map +1 -1
  151. package/dist/tsconfig.tsbuildinfo +1 -1
  152. package/dist/types/cli/argument-parser.d.ts +8 -2
  153. package/dist/types/cli/file-processor.d.ts +15 -0
  154. package/dist/types/cli/formatters/json-formatter.d.ts +6 -0
  155. package/dist/types/cli/formatters/simple-formatter.d.ts +1 -0
  156. package/dist/types/cli/summary-reporter.d.ts +6 -0
  157. package/dist/types/cli.d.ts +9 -4
  158. package/dist/types/custom-rule-loader.d.ts +62 -0
  159. package/dist/types/herb-disable-comment-utils.d.ts +69 -0
  160. package/dist/types/index.d.ts +1 -0
  161. package/dist/types/linter.d.ts +99 -3
  162. package/dist/types/loader.d.ts +20 -0
  163. package/dist/types/rules/erb-comment-syntax.d.ts +12 -5
  164. package/dist/types/rules/erb-no-case-node-children.d.ts +8 -0
  165. package/dist/types/rules/erb-no-empty-tags.d.ts +3 -2
  166. package/dist/types/rules/erb-no-extra-newline.d.ts +14 -0
  167. package/dist/types/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
  168. package/dist/types/rules/erb-no-output-control-flow.d.ts +3 -2
  169. package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
  170. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +3 -2
  171. package/dist/types/rules/erb-require-trailing-newline.d.ts +9 -0
  172. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
  173. package/dist/types/rules/erb-right-trim.d.ts +12 -5
  174. package/dist/types/rules/herb-disable-comment-base.d.ts +37 -0
  175. package/dist/types/rules/herb-disable-comment-malformed.d.ts +8 -0
  176. package/dist/types/rules/herb-disable-comment-missing-rules.d.ts +8 -0
  177. package/dist/types/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
  178. package/dist/types/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
  179. package/dist/types/rules/herb-disable-comment-unnecessary.d.ts +8 -0
  180. package/dist/types/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
  181. package/dist/types/rules/html-anchor-require-href.d.ts +3 -2
  182. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
  183. package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +3 -2
  184. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +3 -2
  185. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +3 -2
  186. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +3 -2
  187. package/dist/types/rules/html-attribute-double-quotes.d.ts +13 -5
  188. package/dist/types/rules/html-attribute-equals-spacing.d.ts +12 -5
  189. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +13 -5
  190. package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
  191. package/dist/types/rules/html-body-only-elements.d.ts +9 -0
  192. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +12 -5
  193. package/dist/types/rules/html-head-only-elements.d.ts +9 -0
  194. package/dist/types/rules/html-iframe-has-title.d.ts +3 -2
  195. package/dist/types/rules/html-img-require-alt.d.ts +3 -2
  196. package/dist/types/rules/html-input-require-autocomplete.d.ts +8 -0
  197. package/dist/types/rules/html-navigation-has-label.d.ts +3 -2
  198. package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
  199. package/dist/types/rules/html-no-block-inside-inline.d.ts +3 -2
  200. package/dist/types/rules/html-no-duplicate-attributes.d.ts +3 -2
  201. package/dist/types/rules/html-no-duplicate-ids.d.ts +3 -2
  202. package/dist/types/rules/html-no-duplicate-meta-names.d.ts +9 -0
  203. package/dist/types/rules/html-no-empty-attributes.d.ts +3 -2
  204. package/dist/types/rules/html-no-empty-headings.d.ts +3 -2
  205. package/dist/types/rules/html-no-nested-links.d.ts +3 -2
  206. package/dist/types/rules/html-no-positive-tab-index.d.ts +3 -2
  207. package/dist/types/rules/html-no-self-closing.d.ts +14 -5
  208. package/dist/types/rules/html-no-space-in-tag.d.ts +16 -0
  209. package/dist/types/rules/html-no-title-attribute.d.ts +3 -2
  210. package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
  211. package/dist/types/rules/html-tag-name-lowercase.d.ts +16 -6
  212. package/dist/types/rules/index.d.ts +19 -3
  213. package/dist/types/rules/parser-no-errors.d.ts +2 -1
  214. package/dist/types/rules/rule-utils.d.ts +72 -25
  215. package/dist/types/rules/svg-tag-name-capitalization.d.ts +13 -4
  216. package/dist/types/rules.d.ts +2 -0
  217. package/dist/types/src/cli/argument-parser.d.ts +8 -2
  218. package/dist/types/src/cli/file-processor.d.ts +15 -0
  219. package/dist/types/src/cli/formatters/json-formatter.d.ts +6 -0
  220. package/dist/types/src/cli/formatters/simple-formatter.d.ts +1 -0
  221. package/dist/types/src/cli/summary-reporter.d.ts +6 -0
  222. package/dist/types/src/cli.d.ts +9 -4
  223. package/dist/types/src/custom-rule-loader.d.ts +62 -0
  224. package/dist/types/src/herb-disable-comment-utils.d.ts +69 -0
  225. package/dist/types/src/index.d.ts +1 -0
  226. package/dist/types/src/linter.d.ts +99 -3
  227. package/dist/types/src/loader.d.ts +20 -0
  228. package/dist/types/src/rules/erb-comment-syntax.d.ts +12 -5
  229. package/dist/types/src/rules/erb-no-case-node-children.d.ts +8 -0
  230. package/dist/types/src/rules/erb-no-empty-tags.d.ts +3 -2
  231. package/dist/types/src/rules/erb-no-extra-newline.d.ts +14 -0
  232. package/dist/types/src/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
  233. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +3 -2
  234. package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
  235. package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +3 -2
  236. package/dist/types/src/rules/erb-require-trailing-newline.d.ts +9 -0
  237. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
  238. package/dist/types/src/rules/erb-right-trim.d.ts +12 -5
  239. package/dist/types/src/rules/herb-disable-comment-base.d.ts +37 -0
  240. package/dist/types/src/rules/herb-disable-comment-malformed.d.ts +8 -0
  241. package/dist/types/src/rules/herb-disable-comment-missing-rules.d.ts +8 -0
  242. package/dist/types/src/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
  243. package/dist/types/src/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
  244. package/dist/types/src/rules/herb-disable-comment-unnecessary.d.ts +8 -0
  245. package/dist/types/src/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
  246. package/dist/types/src/rules/html-anchor-require-href.d.ts +3 -2
  247. package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
  248. package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +3 -2
  249. package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +3 -2
  250. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +3 -2
  251. package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +3 -2
  252. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +13 -5
  253. package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +12 -5
  254. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +13 -5
  255. package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
  256. package/dist/types/src/rules/html-body-only-elements.d.ts +9 -0
  257. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +12 -5
  258. package/dist/types/src/rules/html-head-only-elements.d.ts +9 -0
  259. package/dist/types/src/rules/html-iframe-has-title.d.ts +3 -2
  260. package/dist/types/src/rules/html-img-require-alt.d.ts +3 -2
  261. package/dist/types/src/rules/html-input-require-autocomplete.d.ts +8 -0
  262. package/dist/types/src/rules/html-navigation-has-label.d.ts +3 -2
  263. package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
  264. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +3 -2
  265. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +3 -2
  266. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +3 -2
  267. package/dist/types/src/rules/html-no-duplicate-meta-names.d.ts +9 -0
  268. package/dist/types/src/rules/html-no-empty-attributes.d.ts +3 -2
  269. package/dist/types/src/rules/html-no-empty-headings.d.ts +3 -2
  270. package/dist/types/src/rules/html-no-nested-links.d.ts +3 -2
  271. package/dist/types/src/rules/html-no-positive-tab-index.d.ts +3 -2
  272. package/dist/types/src/rules/html-no-self-closing.d.ts +14 -5
  273. package/dist/types/src/rules/html-no-space-in-tag.d.ts +16 -0
  274. package/dist/types/src/rules/html-no-title-attribute.d.ts +3 -2
  275. package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
  276. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +16 -6
  277. package/dist/types/src/rules/index.d.ts +19 -3
  278. package/dist/types/src/rules/parser-no-errors.d.ts +2 -1
  279. package/dist/types/src/rules/rule-utils.d.ts +72 -25
  280. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +13 -4
  281. package/dist/types/src/rules.d.ts +2 -0
  282. package/dist/types/src/types.d.ts +102 -11
  283. package/dist/types/types.d.ts +102 -11
  284. package/docs/rules/README.md +16 -2
  285. package/docs/rules/erb-no-case-node-children.md +50 -0
  286. package/docs/rules/erb-no-extra-newline.md +74 -0
  287. package/docs/rules/erb-no-extra-whitespace-inside-tags.md +39 -0
  288. package/docs/rules/{erb-requires-trailing-newline.md → erb-require-trailing-newline.md} +1 -1
  289. package/docs/rules/erb-right-trim.md +5 -10
  290. package/docs/rules/herb-disable-comment-malformed.md +45 -0
  291. package/docs/rules/herb-disable-comment-missing-rules.md +60 -0
  292. package/docs/rules/herb-disable-comment-no-duplicate-rules.md +49 -0
  293. package/docs/rules/herb-disable-comment-no-redundant-all.md +53 -0
  294. package/docs/rules/herb-disable-comment-unnecessary.md +44 -0
  295. package/docs/rules/herb-disable-comment-valid-rule-name.md +41 -0
  296. package/docs/rules/html-aria-attribute-must-be-valid.md +2 -5
  297. package/docs/rules/html-aria-label-is-well-formatted.md +1 -1
  298. package/docs/rules/html-attribute-double-quotes.md +2 -2
  299. package/docs/rules/html-attribute-equals-spacing.md +2 -2
  300. package/docs/rules/html-attribute-values-require-quotes.md +3 -3
  301. package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +2 -2
  302. package/docs/rules/html-body-only-elements.md +99 -0
  303. package/docs/rules/html-boolean-attributes-no-value.md +2 -2
  304. package/docs/rules/html-head-only-elements.md +81 -0
  305. package/docs/rules/html-input-require-autocomplete.md +64 -0
  306. package/docs/rules/html-no-aria-hidden-on-focusable.md +2 -2
  307. package/docs/rules/html-no-duplicate-attributes.md +2 -2
  308. package/docs/rules/html-no-duplicate-meta-names.md +64 -0
  309. package/docs/rules/html-no-empty-attributes.md +3 -3
  310. package/docs/rules/html-no-empty-headings.md +4 -26
  311. package/docs/rules/html-no-positive-tab-index.md +1 -2
  312. package/docs/rules/html-no-self-closing.md +17 -2
  313. package/docs/rules/html-no-space-in-tag.md +66 -0
  314. package/docs/rules/html-no-title-attribute.md +2 -2
  315. package/docs/rules/html-no-underscores-in-attribute-names.md +2 -2
  316. package/docs/rules/html-tag-name-lowercase.md +2 -2
  317. package/package.json +13 -5
  318. package/src/cli/argument-parser.ts +46 -37
  319. package/src/cli/file-processor.ts +159 -28
  320. package/src/cli/formatters/detailed-formatter.ts +21 -3
  321. package/src/cli/formatters/github-actions-formatter.ts +17 -1
  322. package/src/cli/formatters/json-formatter.ts +9 -0
  323. package/src/cli/formatters/simple-formatter.ts +24 -8
  324. package/src/cli/output-manager.ts +23 -3
  325. package/src/cli/summary-reporter.ts +40 -3
  326. package/src/cli.ts +134 -51
  327. package/src/custom-rule-loader.ts +189 -0
  328. package/src/herb-disable-comment-utils.ts +175 -0
  329. package/src/index.ts +2 -0
  330. package/src/linter.ts +501 -36
  331. package/src/loader.ts +30 -0
  332. package/src/rules/erb-comment-syntax.ts +53 -10
  333. package/src/rules/erb-no-case-node-children.ts +68 -0
  334. package/src/rules/erb-no-empty-tags.ts +9 -3
  335. package/src/rules/erb-no-extra-newline.ts +91 -0
  336. package/src/rules/erb-no-extra-whitespace-inside-tags.ts +147 -0
  337. package/src/rules/erb-no-output-control-flow.ts +9 -3
  338. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +9 -3
  339. package/src/rules/erb-prefer-image-tag-helper.ts +9 -3
  340. package/src/rules/erb-require-trailing-newline.ts +47 -0
  341. package/src/rules/erb-require-whitespace-inside-tags.ts +94 -16
  342. package/src/rules/erb-right-trim.ts +45 -22
  343. package/src/rules/herb-disable-comment-base.ts +76 -0
  344. package/src/rules/herb-disable-comment-malformed.ts +66 -0
  345. package/src/rules/herb-disable-comment-missing-rules.ts +41 -0
  346. package/src/rules/herb-disable-comment-no-duplicate-rules.ts +46 -0
  347. package/src/rules/herb-disable-comment-no-redundant-all.ts +40 -0
  348. package/src/rules/herb-disable-comment-unnecessary.ts +103 -0
  349. package/src/rules/herb-disable-comment-valid-rule-name.ts +62 -0
  350. package/src/rules/html-anchor-require-href.ts +9 -3
  351. package/src/rules/html-aria-attribute-must-be-valid.ts +9 -3
  352. package/src/rules/html-aria-label-is-well-formatted.ts +9 -5
  353. package/src/rules/html-aria-level-must-be-valid.ts +9 -2
  354. package/src/rules/html-aria-role-heading-requires-level.ts +9 -3
  355. package/src/rules/html-aria-role-must-be-valid.ts +9 -3
  356. package/src/rules/html-attribute-double-quotes.ts +42 -8
  357. package/src/rules/html-attribute-equals-spacing.ts +31 -7
  358. package/src/rules/html-attribute-values-require-quotes.ts +56 -10
  359. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +9 -3
  360. package/src/rules/html-body-only-elements.ts +60 -0
  361. package/src/rules/html-boolean-attributes-no-value.ts +31 -6
  362. package/src/rules/html-head-only-elements.ts +65 -0
  363. package/src/rules/html-iframe-has-title.ts +9 -4
  364. package/src/rules/html-img-require-alt.ts +10 -4
  365. package/src/rules/html-input-require-autocomplete.ts +85 -0
  366. package/src/rules/html-navigation-has-label.ts +9 -3
  367. package/src/rules/html-no-aria-hidden-on-focusable.ts +9 -3
  368. package/src/rules/html-no-block-inside-inline.ts +9 -3
  369. package/src/rules/html-no-duplicate-attributes.ts +9 -3
  370. package/src/rules/html-no-duplicate-ids.ts +11 -7
  371. package/src/rules/html-no-duplicate-meta-names.ts +188 -0
  372. package/src/rules/html-no-empty-attributes.ts +58 -10
  373. package/src/rules/html-no-empty-headings.ts +10 -8
  374. package/src/rules/html-no-nested-links.ts +10 -4
  375. package/src/rules/html-no-positive-tab-index.ts +9 -3
  376. package/src/rules/html-no-self-closing.ts +69 -9
  377. package/src/rules/html-no-space-in-tag.ts +221 -0
  378. package/src/rules/html-no-title-attribute.ts +9 -3
  379. package/src/rules/html-no-underscores-in-attribute-names.ts +12 -4
  380. package/src/rules/html-tag-name-lowercase.ts +41 -10
  381. package/src/rules/index.ts +23 -3
  382. package/src/rules/parser-no-errors.ts +8 -1
  383. package/src/rules/rule-utils.ts +248 -42
  384. package/src/rules/svg-tag-name-capitalization.ts +39 -6
  385. package/src/{default-rules.ts → rules.ts} +51 -15
  386. package/src/types.ts +133 -15
  387. package/dist/src/default-rules.js.map +0 -1
  388. package/dist/src/rules/erb-requires-trailing-newline.js +0 -22
  389. package/dist/src/rules/erb-requires-trailing-newline.js.map +0 -1
  390. package/dist/types/default-rules.d.ts +0 -2
  391. package/dist/types/rules/erb-requires-trailing-newline.d.ts +0 -6
  392. package/dist/types/src/default-rules.d.ts +0 -2
  393. package/dist/types/src/rules/erb-requires-trailing-newline.d.ts +0 -6
  394. package/src/rules/erb-requires-trailing-newline.ts +0 -29
@@ -0,0 +1,62 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { HerbDisableCommentParsedVisitor } from "./herb-disable-comment-base.js"
3
+
4
+ import { didyoumean } from "@herb-tools/core"
5
+
6
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
7
+ import type { ERBContentNode, ParseResult } from "@herb-tools/core"
8
+ import type { HerbDisableComment } from "../herb-disable-comment-utils.js"
9
+
10
+ class HerbDisableCommentValidRuleNameVisitor extends HerbDisableCommentParsedVisitor {
11
+ private validRuleNames: Set<string> = new Set()
12
+ private validRuleNamesList: string[] = []
13
+
14
+ constructor(ruleName: string, validRuleNames: string[], context?: Partial<LintContext>) {
15
+ super(ruleName, context)
16
+
17
+ this.validRuleNames = new Set([...validRuleNames, "all"])
18
+ this.validRuleNamesList = Array.from(this.validRuleNames)
19
+ }
20
+
21
+ protected checkParsedHerbDisable(node: ERBContentNode, _content: string, herbDisable: HerbDisableComment): void {
22
+ herbDisable.ruleNameDetails.forEach(ruleDetail => {
23
+ if (this.validRuleNames.has(ruleDetail.name)) return
24
+
25
+ const suggestion = didyoumean(ruleDetail.name, this.validRuleNamesList)
26
+ const message = suggestion
27
+ ? `Unknown rule \`${ruleDetail.name}\`. Did you mean \`${suggestion}\`?`
28
+ : `Unknown rule \`${ruleDetail.name}\`.`
29
+
30
+ const location = this.createRuleNameLocation(node, ruleDetail)
31
+ this.addOffenseWithFallback(message, location, node)
32
+ })
33
+ }
34
+ }
35
+
36
+ export class HerbDisableCommentValidRuleNameRule extends ParserRule {
37
+ name = "herb-disable-comment-valid-rule-name"
38
+
39
+ get defaultConfig(): FullRuleConfig {
40
+ return {
41
+ enabled: true,
42
+ severity: "warning"
43
+ }
44
+ }
45
+
46
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
47
+ const validRuleNames = context?.validRuleNames
48
+
49
+ if (!validRuleNames) return []
50
+ if (validRuleNames.length === 0) return []
51
+
52
+ const visitor = new HerbDisableCommentValidRuleNameVisitor(
53
+ this.name,
54
+ validRuleNames,
55
+ context
56
+ )
57
+
58
+ visitor.visit(result.value)
59
+
60
+ return visitor.offenses
61
+ }
62
+ }
@@ -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 AnchorRechireHrefVisitor extends BaseRuleVisitor {
@@ -21,7 +21,6 @@ class AnchorRechireHrefVisitor extends BaseRuleVisitor {
21
21
  this.addOffense(
22
22
  "Add an `href` attribute to `<a>` to ensure it is focusable and accessible.",
23
23
  node.tag_name!.location,
24
- "error",
25
24
  )
26
25
  }
27
26
  }
@@ -30,7 +29,14 @@ class AnchorRechireHrefVisitor extends BaseRuleVisitor {
30
29
  export class HTMLAnchorRequireHrefRule extends ParserRule {
31
30
  name = "html-anchor-require-href"
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 AnchorRechireHrefVisitor(this.name, context)
35
41
 
36
42
  visitor.visit(result.value)
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js"
2
2
  import { ARIA_ATTRIBUTES, AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams } 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, HTMLAttributeNode } from "@herb-tools/core"
6
6
 
7
7
  class AriaAttributeMustBeValid extends AttributeVisitorMixin {
@@ -20,7 +20,6 @@ class AriaAttributeMustBeValid extends AttributeVisitorMixin {
20
20
  this.addOffense(
21
21
  `The attribute \`${attributeName}\` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.`,
22
22
  attributeNode.location,
23
- "error"
24
23
  )
25
24
  }
26
25
  }
@@ -28,7 +27,14 @@ class AriaAttributeMustBeValid extends AttributeVisitorMixin {
28
27
  export class HTMLAriaAttributeMustBeValid extends ParserRule {
29
28
  name = "html-aria-attribute-must-be-valid"
30
29
 
31
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
30
+ get defaultConfig(): FullRuleConfig {
31
+ return {
32
+ enabled: true,
33
+ severity: "error"
34
+ }
35
+ }
36
+
37
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
32
38
  const visitor = new AriaAttributeMustBeValid(this.name, context)
33
39
 
34
40
  visitor.visit(result.value)
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js"
2
2
  import { AttributeVisitorMixin, 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 AriaLabelIsWellFormattedVisitor extends AttributeVisitorMixin {
@@ -12,7 +12,6 @@ class AriaLabelIsWellFormattedVisitor extends AttributeVisitorMixin {
12
12
  this.addOffense(
13
13
  "The `aria-label` attribute value text should not contain line breaks. Use concise, single-line descriptions.",
14
14
  attributeNode.location,
15
- "error"
16
15
  )
17
16
 
18
17
  return
@@ -22,7 +21,6 @@ class AriaLabelIsWellFormattedVisitor extends AttributeVisitorMixin {
22
21
  this.addOffense(
23
22
  "The `aria-label` attribute value should not be formatted like an ID. Use natural, sentence-case text instead.",
24
23
  attributeNode.location,
25
- "error"
26
24
  )
27
25
 
28
26
  return
@@ -32,7 +30,6 @@ class AriaLabelIsWellFormattedVisitor extends AttributeVisitorMixin {
32
30
  this.addOffense(
33
31
  "The `aria-label` attribute value text should be formatted like visual text. Use sentence case (capitalize the first letter).",
34
32
  attributeNode.location,
35
- "error"
36
33
  )
37
34
  }
38
35
  }
@@ -49,7 +46,14 @@ class AriaLabelIsWellFormattedVisitor extends AttributeVisitorMixin {
49
46
  export class HTMLAriaLabelIsWellFormattedRule extends ParserRule {
50
47
  name = "html-aria-label-is-well-formatted"
51
48
 
52
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
49
+ get defaultConfig(): FullRuleConfig {
50
+ return {
51
+ enabled: true,
52
+ severity: "error"
53
+ }
54
+ }
55
+
56
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
53
57
  const visitor = new AriaLabelIsWellFormattedVisitor(this.name, context)
54
58
 
55
59
  visitor.visit(result.value)
@@ -2,7 +2,7 @@ import { ParserRule } from "../types.js"
2
2
  import { AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams } from "./rule-utils.js"
3
3
  import { getValidatableStaticContent, hasERBOutput, filterLiteralNodes, filterERBContentNodes, isERBOutputNode } from "@herb-tools/core"
4
4
 
5
- import type { LintOffense, LintContext } from "../types.js"
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
6
  import type { ParseResult, HTMLAttributeNode } from "@herb-tools/core"
7
7
 
8
8
  class HTMLAriaLevelMustBeValidVisitor extends AttributeVisitorMixin {
@@ -65,7 +65,14 @@ class HTMLAriaLevelMustBeValidVisitor extends AttributeVisitorMixin {
65
65
  export class HTMLAriaLevelMustBeValidRule extends ParserRule {
66
66
  name = "html-aria-level-must-be-valid"
67
67
 
68
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
68
+ get defaultConfig(): FullRuleConfig {
69
+ return {
70
+ enabled: true,
71
+ severity: "error"
72
+ }
73
+ }
74
+
75
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
69
76
  const visitor = new HTMLAriaLevelMustBeValidVisitor(this.name, context)
70
77
 
71
78
  visitor.visit(result.value)
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js"
2
2
  import { AttributeVisitorMixin, getAttributeName, getAttributes, 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 AriaRoleHeadingRequiresLevel extends AttributeVisitorMixin {
@@ -15,7 +15,6 @@ class AriaRoleHeadingRequiresLevel extends AttributeVisitorMixin {
15
15
  this.addOffense(
16
16
  `Element with \`role="heading"\` must have an \`aria-level\` attribute.`,
17
17
  attributeNode.location,
18
- "error"
19
18
  )
20
19
  }
21
20
  }
@@ -23,7 +22,14 @@ class AriaRoleHeadingRequiresLevel extends AttributeVisitorMixin {
23
22
  export class HTMLAriaRoleHeadingRequiresLevelRule extends ParserRule {
24
23
  name = "html-aria-role-heading-requires-level"
25
24
 
26
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
25
+ get defaultConfig(): FullRuleConfig {
26
+ return {
27
+ enabled: true,
28
+ severity: "error"
29
+ }
30
+ }
31
+
32
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
27
33
  const visitor = new AriaRoleHeadingRequiresLevel(this.name, context)
28
34
 
29
35
  visitor.visit(result.value)
@@ -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
+ }