@herb-tools/linter 0.7.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (394) hide show
  1. package/README.md +253 -13
  2. package/dist/herb-lint.js +26023 -3424
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +5759 -1583
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +5727 -1584
  7. package/dist/index.js.map +1 -1
  8. package/dist/loader.cjs +17010 -0
  9. package/dist/loader.cjs.map +1 -0
  10. package/dist/loader.js +16879 -0
  11. package/dist/loader.js.map +1 -0
  12. package/dist/package.json +13 -5
  13. package/dist/src/cli/argument-parser.js +38 -33
  14. package/dist/src/cli/argument-parser.js.map +1 -1
  15. package/dist/src/cli/file-processor.js +124 -23
  16. package/dist/src/cli/file-processor.js.map +1 -1
  17. package/dist/src/cli/formatters/detailed-formatter.js +18 -3
  18. package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
  19. package/dist/src/cli/formatters/github-actions-formatter.js +15 -1
  20. package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -1
  21. package/dist/src/cli/formatters/json-formatter.js +3 -0
  22. package/dist/src/cli/formatters/json-formatter.js.map +1 -1
  23. package/dist/src/cli/formatters/simple-formatter.js +20 -7
  24. package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
  25. package/dist/src/cli/output-manager.js +22 -3
  26. package/dist/src/cli/output-manager.js.map +1 -1
  27. package/dist/src/cli/summary-reporter.js +26 -3
  28. package/dist/src/cli/summary-reporter.js.map +1 -1
  29. package/dist/src/cli.js +107 -42
  30. package/dist/src/cli.js.map +1 -1
  31. package/dist/src/custom-rule-loader.js +139 -0
  32. package/dist/src/custom-rule-loader.js.map +1 -0
  33. package/dist/src/herb-disable-comment-utils.js +129 -0
  34. package/dist/src/herb-disable-comment-utils.js.map +1 -0
  35. package/dist/src/index.js +1 -0
  36. package/dist/src/index.js.map +1 -1
  37. package/dist/src/linter.js +369 -34
  38. package/dist/src/linter.js.map +1 -1
  39. package/dist/src/loader.js +17 -0
  40. package/dist/src/loader.js.map +1 -0
  41. package/dist/src/rules/erb-comment-syntax.js +31 -2
  42. package/dist/src/rules/erb-comment-syntax.js.map +1 -1
  43. package/dist/src/rules/erb-no-case-node-children.js +52 -0
  44. package/dist/src/rules/erb-no-case-node-children.js.map +1 -0
  45. package/dist/src/rules/erb-no-empty-tags.js +7 -1
  46. package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
  47. package/dist/src/rules/erb-no-extra-newline.js +65 -0
  48. package/dist/src/rules/erb-no-extra-newline.js.map +1 -0
  49. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js +95 -0
  50. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js.map +1 -0
  51. package/dist/src/rules/erb-no-output-control-flow.js +7 -1
  52. package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
  53. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js +7 -1
  54. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -1
  55. package/dist/src/rules/erb-prefer-image-tag-helper.js +7 -1
  56. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
  57. package/dist/src/rules/erb-require-trailing-newline.js +35 -0
  58. package/dist/src/rules/erb-require-trailing-newline.js.map +1 -0
  59. package/dist/src/rules/erb-require-whitespace-inside-tags.js +69 -11
  60. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  61. package/dist/src/rules/erb-right-trim.js +26 -9
  62. package/dist/src/rules/erb-right-trim.js.map +1 -1
  63. package/dist/src/rules/herb-disable-comment-base.js +51 -0
  64. package/dist/src/rules/herb-disable-comment-base.js.map +1 -0
  65. package/dist/src/rules/herb-disable-comment-malformed.js +51 -0
  66. package/dist/src/rules/herb-disable-comment-malformed.js.map +1 -0
  67. package/dist/src/rules/herb-disable-comment-missing-rules.js +29 -0
  68. package/dist/src/rules/herb-disable-comment-missing-rules.js.map +1 -0
  69. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js +32 -0
  70. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js.map +1 -0
  71. package/dist/src/rules/herb-disable-comment-no-redundant-all.js +31 -0
  72. package/dist/src/rules/herb-disable-comment-no-redundant-all.js.map +1 -0
  73. package/dist/src/rules/herb-disable-comment-unnecessary.js +65 -0
  74. package/dist/src/rules/herb-disable-comment-unnecessary.js.map +1 -0
  75. package/dist/src/rules/herb-disable-comment-valid-rule-name.js +44 -0
  76. package/dist/src/rules/herb-disable-comment-valid-rule-name.js.map +1 -0
  77. package/dist/src/rules/html-anchor-require-href.js +7 -1
  78. package/dist/src/rules/html-anchor-require-href.js.map +1 -1
  79. package/dist/src/rules/html-aria-attribute-must-be-valid.js +7 -1
  80. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
  81. package/dist/src/rules/html-aria-label-is-well-formatted.js +9 -3
  82. package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -1
  83. package/dist/src/rules/html-aria-level-must-be-valid.js +6 -0
  84. package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
  85. package/dist/src/rules/html-aria-role-heading-requires-level.js +7 -1
  86. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
  87. package/dist/src/rules/html-aria-role-must-be-valid.js +7 -1
  88. package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
  89. package/dist/src/rules/html-attribute-double-quotes.js +29 -2
  90. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
  91. package/dist/src/rules/html-attribute-equals-spacing.js +18 -2
  92. package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -1
  93. package/dist/src/rules/html-attribute-values-require-quotes.js +39 -3
  94. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
  95. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js +7 -1
  96. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -1
  97. package/dist/src/rules/html-body-only-elements.js +46 -0
  98. package/dist/src/rules/html-body-only-elements.js.map +1 -0
  99. package/dist/src/rules/html-boolean-attributes-no-value.js +18 -1
  100. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  101. package/dist/src/rules/html-head-only-elements.js +51 -0
  102. package/dist/src/rules/html-head-only-elements.js.map +1 -0
  103. package/dist/src/rules/html-iframe-has-title.js +8 -2
  104. package/dist/src/rules/html-iframe-has-title.js.map +1 -1
  105. package/dist/src/rules/html-img-require-alt.js +7 -1
  106. package/dist/src/rules/html-img-require-alt.js.map +1 -1
  107. package/dist/src/rules/html-input-require-autocomplete.js +70 -0
  108. package/dist/src/rules/html-input-require-autocomplete.js.map +1 -0
  109. package/dist/src/rules/html-navigation-has-label.js +7 -1
  110. package/dist/src/rules/html-navigation-has-label.js.map +1 -1
  111. package/dist/src/rules/html-no-aria-hidden-on-focusable.js +7 -1
  112. package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -1
  113. package/dist/src/rules/html-no-block-inside-inline.js +7 -1
  114. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
  115. package/dist/src/rules/html-no-duplicate-attributes.js +7 -1
  116. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
  117. package/dist/src/rules/html-no-duplicate-ids.js +9 -3
  118. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
  119. package/dist/src/rules/html-no-duplicate-meta-names.js +136 -0
  120. package/dist/src/rules/html-no-duplicate-meta-names.js.map +1 -0
  121. package/dist/src/rules/html-no-empty-attributes.js +45 -7
  122. package/dist/src/rules/html-no-empty-attributes.js.map +1 -1
  123. package/dist/src/rules/html-no-empty-headings.js +7 -6
  124. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  125. package/dist/src/rules/html-no-nested-links.js +7 -1
  126. package/dist/src/rules/html-no-nested-links.js.map +1 -1
  127. package/dist/src/rules/html-no-positive-tab-index.js +7 -1
  128. package/dist/src/rules/html-no-positive-tab-index.js.map +1 -1
  129. package/dist/src/rules/html-no-self-closing.js +48 -3
  130. package/dist/src/rules/html-no-self-closing.js.map +1 -1
  131. package/dist/src/rules/html-no-space-in-tag.js +173 -0
  132. package/dist/src/rules/html-no-space-in-tag.js.map +1 -0
  133. package/dist/src/rules/html-no-title-attribute.js +7 -1
  134. package/dist/src/rules/html-no-title-attribute.js.map +1 -1
  135. package/dist/src/rules/html-no-underscores-in-attribute-names.js +7 -1
  136. package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +1 -1
  137. package/dist/src/rules/html-tag-name-lowercase.js +23 -5
  138. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  139. package/dist/src/rules/index.js +19 -3
  140. package/dist/src/rules/index.js.map +1 -1
  141. package/dist/src/rules/parser-no-errors.js +6 -0
  142. package/dist/src/rules/parser-no-errors.js.map +1 -1
  143. package/dist/src/rules/rule-utils.js +211 -31
  144. package/dist/src/rules/rule-utils.js.map +1 -1
  145. package/dist/src/rules/svg-tag-name-capitalization.js +22 -2
  146. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
  147. package/dist/src/{default-rules.js → rules.js} +44 -16
  148. package/dist/src/rules.js.map +1 -0
  149. package/dist/src/types.js +34 -1
  150. package/dist/src/types.js.map +1 -1
  151. package/dist/tsconfig.tsbuildinfo +1 -1
  152. package/dist/types/cli/argument-parser.d.ts +8 -2
  153. package/dist/types/cli/file-processor.d.ts +15 -0
  154. package/dist/types/cli/formatters/json-formatter.d.ts +6 -0
  155. package/dist/types/cli/formatters/simple-formatter.d.ts +1 -0
  156. package/dist/types/cli/summary-reporter.d.ts +6 -0
  157. package/dist/types/cli.d.ts +9 -4
  158. package/dist/types/custom-rule-loader.d.ts +62 -0
  159. package/dist/types/herb-disable-comment-utils.d.ts +69 -0
  160. package/dist/types/index.d.ts +1 -0
  161. package/dist/types/linter.d.ts +99 -3
  162. package/dist/types/loader.d.ts +20 -0
  163. package/dist/types/rules/erb-comment-syntax.d.ts +12 -5
  164. package/dist/types/rules/erb-no-case-node-children.d.ts +8 -0
  165. package/dist/types/rules/erb-no-empty-tags.d.ts +3 -2
  166. package/dist/types/rules/erb-no-extra-newline.d.ts +14 -0
  167. package/dist/types/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
  168. package/dist/types/rules/erb-no-output-control-flow.d.ts +3 -2
  169. package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
  170. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +3 -2
  171. package/dist/types/rules/erb-require-trailing-newline.d.ts +9 -0
  172. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
  173. package/dist/types/rules/erb-right-trim.d.ts +12 -5
  174. package/dist/types/rules/herb-disable-comment-base.d.ts +37 -0
  175. package/dist/types/rules/herb-disable-comment-malformed.d.ts +8 -0
  176. package/dist/types/rules/herb-disable-comment-missing-rules.d.ts +8 -0
  177. package/dist/types/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
  178. package/dist/types/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
  179. package/dist/types/rules/herb-disable-comment-unnecessary.d.ts +8 -0
  180. package/dist/types/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
  181. package/dist/types/rules/html-anchor-require-href.d.ts +3 -2
  182. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
  183. package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +3 -2
  184. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +3 -2
  185. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +3 -2
  186. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +3 -2
  187. package/dist/types/rules/html-attribute-double-quotes.d.ts +13 -5
  188. package/dist/types/rules/html-attribute-equals-spacing.d.ts +12 -5
  189. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +13 -5
  190. package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
  191. package/dist/types/rules/html-body-only-elements.d.ts +9 -0
  192. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +12 -5
  193. package/dist/types/rules/html-head-only-elements.d.ts +9 -0
  194. package/dist/types/rules/html-iframe-has-title.d.ts +3 -2
  195. package/dist/types/rules/html-img-require-alt.d.ts +3 -2
  196. package/dist/types/rules/html-input-require-autocomplete.d.ts +8 -0
  197. package/dist/types/rules/html-navigation-has-label.d.ts +3 -2
  198. package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
  199. package/dist/types/rules/html-no-block-inside-inline.d.ts +3 -2
  200. package/dist/types/rules/html-no-duplicate-attributes.d.ts +3 -2
  201. package/dist/types/rules/html-no-duplicate-ids.d.ts +3 -2
  202. package/dist/types/rules/html-no-duplicate-meta-names.d.ts +9 -0
  203. package/dist/types/rules/html-no-empty-attributes.d.ts +3 -2
  204. package/dist/types/rules/html-no-empty-headings.d.ts +3 -2
  205. package/dist/types/rules/html-no-nested-links.d.ts +3 -2
  206. package/dist/types/rules/html-no-positive-tab-index.d.ts +3 -2
  207. package/dist/types/rules/html-no-self-closing.d.ts +14 -5
  208. package/dist/types/rules/html-no-space-in-tag.d.ts +16 -0
  209. package/dist/types/rules/html-no-title-attribute.d.ts +3 -2
  210. package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
  211. package/dist/types/rules/html-tag-name-lowercase.d.ts +16 -6
  212. package/dist/types/rules/index.d.ts +19 -3
  213. package/dist/types/rules/parser-no-errors.d.ts +2 -1
  214. package/dist/types/rules/rule-utils.d.ts +72 -25
  215. package/dist/types/rules/svg-tag-name-capitalization.d.ts +13 -4
  216. package/dist/types/rules.d.ts +2 -0
  217. package/dist/types/src/cli/argument-parser.d.ts +8 -2
  218. package/dist/types/src/cli/file-processor.d.ts +15 -0
  219. package/dist/types/src/cli/formatters/json-formatter.d.ts +6 -0
  220. package/dist/types/src/cli/formatters/simple-formatter.d.ts +1 -0
  221. package/dist/types/src/cli/summary-reporter.d.ts +6 -0
  222. package/dist/types/src/cli.d.ts +9 -4
  223. package/dist/types/src/custom-rule-loader.d.ts +62 -0
  224. package/dist/types/src/herb-disable-comment-utils.d.ts +69 -0
  225. package/dist/types/src/index.d.ts +1 -0
  226. package/dist/types/src/linter.d.ts +99 -3
  227. package/dist/types/src/loader.d.ts +20 -0
  228. package/dist/types/src/rules/erb-comment-syntax.d.ts +12 -5
  229. package/dist/types/src/rules/erb-no-case-node-children.d.ts +8 -0
  230. package/dist/types/src/rules/erb-no-empty-tags.d.ts +3 -2
  231. package/dist/types/src/rules/erb-no-extra-newline.d.ts +14 -0
  232. package/dist/types/src/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
  233. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +3 -2
  234. package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
  235. package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +3 -2
  236. package/dist/types/src/rules/erb-require-trailing-newline.d.ts +9 -0
  237. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
  238. package/dist/types/src/rules/erb-right-trim.d.ts +12 -5
  239. package/dist/types/src/rules/herb-disable-comment-base.d.ts +37 -0
  240. package/dist/types/src/rules/herb-disable-comment-malformed.d.ts +8 -0
  241. package/dist/types/src/rules/herb-disable-comment-missing-rules.d.ts +8 -0
  242. package/dist/types/src/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
  243. package/dist/types/src/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
  244. package/dist/types/src/rules/herb-disable-comment-unnecessary.d.ts +8 -0
  245. package/dist/types/src/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
  246. package/dist/types/src/rules/html-anchor-require-href.d.ts +3 -2
  247. package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
  248. package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +3 -2
  249. package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +3 -2
  250. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +3 -2
  251. package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +3 -2
  252. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +13 -5
  253. package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +12 -5
  254. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +13 -5
  255. package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
  256. package/dist/types/src/rules/html-body-only-elements.d.ts +9 -0
  257. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +12 -5
  258. package/dist/types/src/rules/html-head-only-elements.d.ts +9 -0
  259. package/dist/types/src/rules/html-iframe-has-title.d.ts +3 -2
  260. package/dist/types/src/rules/html-img-require-alt.d.ts +3 -2
  261. package/dist/types/src/rules/html-input-require-autocomplete.d.ts +8 -0
  262. package/dist/types/src/rules/html-navigation-has-label.d.ts +3 -2
  263. package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
  264. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +3 -2
  265. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +3 -2
  266. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +3 -2
  267. package/dist/types/src/rules/html-no-duplicate-meta-names.d.ts +9 -0
  268. package/dist/types/src/rules/html-no-empty-attributes.d.ts +3 -2
  269. package/dist/types/src/rules/html-no-empty-headings.d.ts +3 -2
  270. package/dist/types/src/rules/html-no-nested-links.d.ts +3 -2
  271. package/dist/types/src/rules/html-no-positive-tab-index.d.ts +3 -2
  272. package/dist/types/src/rules/html-no-self-closing.d.ts +14 -5
  273. package/dist/types/src/rules/html-no-space-in-tag.d.ts +16 -0
  274. package/dist/types/src/rules/html-no-title-attribute.d.ts +3 -2
  275. package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
  276. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +16 -6
  277. package/dist/types/src/rules/index.d.ts +19 -3
  278. package/dist/types/src/rules/parser-no-errors.d.ts +2 -1
  279. package/dist/types/src/rules/rule-utils.d.ts +72 -25
  280. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +13 -4
  281. package/dist/types/src/rules.d.ts +2 -0
  282. package/dist/types/src/types.d.ts +102 -11
  283. package/dist/types/types.d.ts +102 -11
  284. package/docs/rules/README.md +16 -2
  285. package/docs/rules/erb-no-case-node-children.md +50 -0
  286. package/docs/rules/erb-no-extra-newline.md +74 -0
  287. package/docs/rules/erb-no-extra-whitespace-inside-tags.md +39 -0
  288. package/docs/rules/{erb-requires-trailing-newline.md → erb-require-trailing-newline.md} +1 -1
  289. package/docs/rules/erb-right-trim.md +5 -10
  290. package/docs/rules/herb-disable-comment-malformed.md +45 -0
  291. package/docs/rules/herb-disable-comment-missing-rules.md +60 -0
  292. package/docs/rules/herb-disable-comment-no-duplicate-rules.md +49 -0
  293. package/docs/rules/herb-disable-comment-no-redundant-all.md +53 -0
  294. package/docs/rules/herb-disable-comment-unnecessary.md +44 -0
  295. package/docs/rules/herb-disable-comment-valid-rule-name.md +41 -0
  296. package/docs/rules/html-aria-attribute-must-be-valid.md +2 -5
  297. package/docs/rules/html-aria-label-is-well-formatted.md +1 -1
  298. package/docs/rules/html-attribute-double-quotes.md +2 -2
  299. package/docs/rules/html-attribute-equals-spacing.md +2 -2
  300. package/docs/rules/html-attribute-values-require-quotes.md +3 -3
  301. package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +2 -2
  302. package/docs/rules/html-body-only-elements.md +99 -0
  303. package/docs/rules/html-boolean-attributes-no-value.md +2 -2
  304. package/docs/rules/html-head-only-elements.md +81 -0
  305. package/docs/rules/html-input-require-autocomplete.md +64 -0
  306. package/docs/rules/html-no-aria-hidden-on-focusable.md +2 -2
  307. package/docs/rules/html-no-duplicate-attributes.md +2 -2
  308. package/docs/rules/html-no-duplicate-meta-names.md +64 -0
  309. package/docs/rules/html-no-empty-attributes.md +3 -3
  310. package/docs/rules/html-no-empty-headings.md +4 -26
  311. package/docs/rules/html-no-positive-tab-index.md +1 -2
  312. package/docs/rules/html-no-self-closing.md +17 -2
  313. package/docs/rules/html-no-space-in-tag.md +66 -0
  314. package/docs/rules/html-no-title-attribute.md +2 -2
  315. package/docs/rules/html-no-underscores-in-attribute-names.md +2 -2
  316. package/docs/rules/html-tag-name-lowercase.md +2 -2
  317. package/package.json +13 -5
  318. package/src/cli/argument-parser.ts +46 -37
  319. package/src/cli/file-processor.ts +159 -28
  320. package/src/cli/formatters/detailed-formatter.ts +21 -3
  321. package/src/cli/formatters/github-actions-formatter.ts +17 -1
  322. package/src/cli/formatters/json-formatter.ts +9 -0
  323. package/src/cli/formatters/simple-formatter.ts +24 -8
  324. package/src/cli/output-manager.ts +23 -3
  325. package/src/cli/summary-reporter.ts +40 -3
  326. package/src/cli.ts +134 -51
  327. package/src/custom-rule-loader.ts +189 -0
  328. package/src/herb-disable-comment-utils.ts +175 -0
  329. package/src/index.ts +2 -0
  330. package/src/linter.ts +501 -36
  331. package/src/loader.ts +30 -0
  332. package/src/rules/erb-comment-syntax.ts +53 -10
  333. package/src/rules/erb-no-case-node-children.ts +68 -0
  334. package/src/rules/erb-no-empty-tags.ts +9 -3
  335. package/src/rules/erb-no-extra-newline.ts +91 -0
  336. package/src/rules/erb-no-extra-whitespace-inside-tags.ts +147 -0
  337. package/src/rules/erb-no-output-control-flow.ts +9 -3
  338. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +9 -3
  339. package/src/rules/erb-prefer-image-tag-helper.ts +9 -3
  340. package/src/rules/erb-require-trailing-newline.ts +47 -0
  341. package/src/rules/erb-require-whitespace-inside-tags.ts +94 -16
  342. package/src/rules/erb-right-trim.ts +45 -22
  343. package/src/rules/herb-disable-comment-base.ts +76 -0
  344. package/src/rules/herb-disable-comment-malformed.ts +66 -0
  345. package/src/rules/herb-disable-comment-missing-rules.ts +41 -0
  346. package/src/rules/herb-disable-comment-no-duplicate-rules.ts +46 -0
  347. package/src/rules/herb-disable-comment-no-redundant-all.ts +40 -0
  348. package/src/rules/herb-disable-comment-unnecessary.ts +103 -0
  349. package/src/rules/herb-disable-comment-valid-rule-name.ts +62 -0
  350. package/src/rules/html-anchor-require-href.ts +9 -3
  351. package/src/rules/html-aria-attribute-must-be-valid.ts +9 -3
  352. package/src/rules/html-aria-label-is-well-formatted.ts +9 -5
  353. package/src/rules/html-aria-level-must-be-valid.ts +9 -2
  354. package/src/rules/html-aria-role-heading-requires-level.ts +9 -3
  355. package/src/rules/html-aria-role-must-be-valid.ts +9 -3
  356. package/src/rules/html-attribute-double-quotes.ts +42 -8
  357. package/src/rules/html-attribute-equals-spacing.ts +31 -7
  358. package/src/rules/html-attribute-values-require-quotes.ts +56 -10
  359. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +9 -3
  360. package/src/rules/html-body-only-elements.ts +60 -0
  361. package/src/rules/html-boolean-attributes-no-value.ts +31 -6
  362. package/src/rules/html-head-only-elements.ts +65 -0
  363. package/src/rules/html-iframe-has-title.ts +9 -4
  364. package/src/rules/html-img-require-alt.ts +10 -4
  365. package/src/rules/html-input-require-autocomplete.ts +85 -0
  366. package/src/rules/html-navigation-has-label.ts +9 -3
  367. package/src/rules/html-no-aria-hidden-on-focusable.ts +9 -3
  368. package/src/rules/html-no-block-inside-inline.ts +9 -3
  369. package/src/rules/html-no-duplicate-attributes.ts +9 -3
  370. package/src/rules/html-no-duplicate-ids.ts +11 -7
  371. package/src/rules/html-no-duplicate-meta-names.ts +188 -0
  372. package/src/rules/html-no-empty-attributes.ts +58 -10
  373. package/src/rules/html-no-empty-headings.ts +10 -8
  374. package/src/rules/html-no-nested-links.ts +10 -4
  375. package/src/rules/html-no-positive-tab-index.ts +9 -3
  376. package/src/rules/html-no-self-closing.ts +69 -9
  377. package/src/rules/html-no-space-in-tag.ts +221 -0
  378. package/src/rules/html-no-title-attribute.ts +9 -3
  379. package/src/rules/html-no-underscores-in-attribute-names.ts +12 -4
  380. package/src/rules/html-tag-name-lowercase.ts +41 -10
  381. package/src/rules/index.ts +23 -3
  382. package/src/rules/parser-no-errors.ts +8 -1
  383. package/src/rules/rule-utils.ts +248 -42
  384. package/src/rules/svg-tag-name-capitalization.ts +39 -6
  385. package/src/{default-rules.ts → rules.ts} +51 -15
  386. package/src/types.ts +133 -15
  387. package/dist/src/default-rules.js.map +0 -1
  388. package/dist/src/rules/erb-requires-trailing-newline.js +0 -22
  389. package/dist/src/rules/erb-requires-trailing-newline.js.map +0 -1
  390. package/dist/types/default-rules.d.ts +0 -2
  391. package/dist/types/rules/erb-requires-trailing-newline.d.ts +0 -6
  392. package/dist/types/src/default-rules.d.ts +0 -2
  393. package/dist/types/src/rules/erb-requires-trailing-newline.d.ts +0 -6
  394. package/src/rules/erb-requires-trailing-newline.ts +0 -29
@@ -1,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
+ }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js"
2
2
  import { BaseRuleVisitor, getTagName, hasAttribute, getAttributeValue, findAttributeByName, getAttributes } 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 NavigationHasLabelVisitor extends BaseRuleVisitor {
@@ -32,7 +32,6 @@ class NavigationHasLabelVisitor extends BaseRuleVisitor {
32
32
  this.addOffense(
33
33
  message,
34
34
  node.tag_name!.location,
35
- "error"
36
35
  )
37
36
  }
38
37
  }
@@ -54,7 +53,14 @@ class NavigationHasLabelVisitor extends BaseRuleVisitor {
54
53
  export class HTMLNavigationHasLabelRule extends ParserRule {
55
54
  name = "html-navigation-has-label"
56
55
 
57
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
56
+ get defaultConfig(): FullRuleConfig {
57
+ return {
58
+ enabled: false,
59
+ severity: "error"
60
+ }
61
+ }
62
+
63
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
58
64
  const visitor = new NavigationHasLabelVisitor(this.name, context)
59
65
 
60
66
  visitor.visit(result.value)
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js"
2
2
  import { BaseRuleVisitor, getTagName, hasAttribute, getAttributeValue, findAttributeByName, getAttributes } 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
  const INTERACTIVE_ELEMENTS = new Set([
@@ -21,7 +21,6 @@ class NoAriaHiddenOnFocusableVisitor extends BaseRuleVisitor {
21
21
  this.addOffense(
22
22
  `Elements that are focusable should not have \`aria-hidden="true"\` because it will cause confusion for assistive technology users.`,
23
23
  node.tag_name!.location,
24
- "error"
25
24
  )
26
25
  }
27
26
  }
@@ -80,7 +79,14 @@ class NoAriaHiddenOnFocusableVisitor extends BaseRuleVisitor {
80
79
  export class HTMLNoAriaHiddenOnFocusableRule extends ParserRule {
81
80
  name = "html-no-aria-hidden-on-focusable"
82
81
 
83
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
82
+ get defaultConfig(): FullRuleConfig {
83
+ return {
84
+ enabled: true,
85
+ severity: "error"
86
+ }
87
+ }
88
+
89
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
84
90
  const visitor = new NoAriaHiddenOnFocusableVisitor(this.name, context)
85
91
 
86
92
  visitor.visit(result.value)
@@ -1,7 +1,7 @@
1
1
  import { BaseRuleVisitor, isInlineElement, isBlockElement } 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, HTMLElementNode, ParseResult } from "@herb-tools/core"
6
6
 
7
7
  class BlockInsideInlineVisitor extends BaseRuleVisitor {
@@ -26,7 +26,6 @@ class BlockInsideInlineVisitor extends BaseRuleVisitor {
26
26
  this.addOffense(
27
27
  `${elementType} element \`<${tagName}>\` cannot be placed inside inline element \`<${parentInline}>\`.`,
28
28
  openTag.tag_name!.location,
29
- "error"
30
29
  )
31
30
  }
32
31
 
@@ -77,7 +76,14 @@ class BlockInsideInlineVisitor extends BaseRuleVisitor {
77
76
  export class HTMLNoBlockInsideInlineRule extends ParserRule {
78
77
  name = "html-no-block-inside-inline"
79
78
 
80
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
79
+ get defaultConfig(): FullRuleConfig {
80
+ return {
81
+ enabled: false,
82
+ severity: "error"
83
+ }
84
+ }
85
+
86
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
81
87
  const visitor = new BlockInsideInlineVisitor(this.name, context)
82
88
  visitor.visit(result.value)
83
89
  return visitor.offenses
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js"
2
2
  import { 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 { HTMLOpenTagNode, HTMLAttributeNode, ParseResult } from "@herb-tools/core"
6
6
 
7
7
  class NoDuplicateAttributesVisitor extends AttributeVisitorMixin {
@@ -39,7 +39,6 @@ class NoDuplicateAttributesVisitor extends AttributeVisitorMixin {
39
39
  this.addOffense(
40
40
  `Duplicate attribute \`${attributeName}\` found on tag. Remove the duplicate occurrence.`,
41
41
  attributeNode.name!.location,
42
- "error"
43
42
  )
44
43
  }
45
44
  }
@@ -50,7 +49,14 @@ class NoDuplicateAttributesVisitor extends AttributeVisitorMixin {
50
49
  export class HTMLNoDuplicateAttributesRule extends ParserRule {
51
50
  name = "html-no-duplicate-attributes"
52
51
 
53
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
52
+ get defaultConfig(): FullRuleConfig {
53
+ return {
54
+ enabled: true,
55
+ severity: "error"
56
+ }
57
+ }
58
+
59
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
54
60
  const visitor = new NoDuplicateAttributesVisitor(this.name, context)
55
61
 
56
62
  visitor.visit(result.value)
@@ -1,4 +1,4 @@
1
- import { ParserRule } from "../types"
1
+ import { ParserRule, BaseAutofixContext } from "../types"
2
2
  import { ControlFlowTrackingVisitor, ControlFlowType } from "./rule-utils"
3
3
  import { LiteralNode } from "@herb-tools/core"
4
4
  import { Printer, IdentityPrinter } from "@herb-tools/printer"
@@ -6,7 +6,7 @@ import { Printer, IdentityPrinter } from "@herb-tools/printer"
6
6
  import { hasERBOutput, getValidatableStaticContent, isEffectivelyStatic, isNode, getStaticAttributeName, isERBOutputNode } from "@herb-tools/core"
7
7
 
8
8
  import type { ParseResult, HTMLAttributeNode, ERBContentNode } from "@herb-tools/core"
9
- import type { LintOffense, LintContext } from "../types"
9
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types"
10
10
 
11
11
  interface ControlFlowState {
12
12
  previousBranchIds: Set<string>
@@ -29,7 +29,7 @@ class OutputPrinter extends Printer {
29
29
  }
30
30
  }
31
31
 
32
- class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor<ControlFlowState, BranchState> {
32
+ class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor<BaseAutofixContext, ControlFlowState, BranchState> {
33
33
  private documentIds: Set<string> = new Set<string>()
34
34
  private currentBranchIds: Set<string> = new Set<string>()
35
35
  private controlFlowIds: Set<string> = new Set<string>()
@@ -180,7 +180,6 @@ class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor<ControlFlowState,
180
180
  this.addOffense(
181
181
  `Duplicate ID \`${identifier}\` found. IDs must be unique within a document.`,
182
182
  location,
183
- "error"
184
183
  )
185
184
  }
186
185
 
@@ -188,7 +187,6 @@ class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor<ControlFlowState,
188
187
  this.addOffense(
189
188
  `Duplicate ID \`${identifier}\` found within the same loop iteration. IDs must be unique within the same loop iteration.`,
190
189
  location,
191
- "error"
192
190
  )
193
191
  }
194
192
 
@@ -196,7 +194,6 @@ class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor<ControlFlowState,
196
194
  this.addOffense(
197
195
  `Duplicate ID \`${identifier}\` found within the same control flow branch. IDs must be unique within the same control flow branch.`,
198
196
  location,
199
- "error"
200
197
  )
201
198
  }
202
199
  }
@@ -204,7 +201,14 @@ class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor<ControlFlowState,
204
201
  export class HTMLNoDuplicateIdsRule extends ParserRule {
205
202
  name = "html-no-duplicate-ids"
206
203
 
207
- check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
204
+ get defaultConfig(): FullRuleConfig {
205
+ return {
206
+ enabled: true,
207
+ severity: "error"
208
+ }
209
+ }
210
+
211
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
208
212
  const visitor = new NoDuplicateIdsVisitor(this.name, context)
209
213
 
210
214
  visitor.visit(result.value)
@@ -0,0 +1,188 @@
1
+ import { isHTMLElementNode } from "@herb-tools/core"
2
+ import { getTagName, getAttributeName, getAttributeValue, forEachAttribute } from "./rule-utils"
3
+
4
+ import { ControlFlowTrackingVisitor, ControlFlowType } from "./rule-utils"
5
+ import { ParserRule, BaseAutofixContext } from "../types"
6
+
7
+ import type { ParseResult, HTMLElementNode, HTMLAttributeNode } from "@herb-tools/core"
8
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types"
9
+
10
+ interface MetaTag {
11
+ node: HTMLElementNode
12
+ nameValue?: string
13
+ httpEquivValue?: string
14
+ }
15
+
16
+ interface ControlFlowState {
17
+ previousBranchMetas: MetaTag[]
18
+ previousControlFlowMetas: MetaTag[]
19
+ }
20
+
21
+ interface BranchState {
22
+ previousBranchMetas: MetaTag[]
23
+ }
24
+
25
+ class HTMLNoDuplicateMetaNamesVisitor extends ControlFlowTrackingVisitor<BaseAutofixContext, ControlFlowState, BranchState> {
26
+ private elementStack: string[] = []
27
+ private documentMetas: MetaTag[] = []
28
+ private currentBranchMetas: MetaTag[] = []
29
+ private controlFlowMetas: MetaTag[] = []
30
+
31
+ visitHTMLElementNode(node: HTMLElementNode): void {
32
+ const tagName = getTagName(node)?.toLowerCase()
33
+ if (!tagName) return
34
+
35
+ if (tagName === "head") {
36
+ this.documentMetas = []
37
+ this.currentBranchMetas = []
38
+ this.controlFlowMetas = []
39
+ } else if (tagName === "meta" && this.insideHead) {
40
+ this.collectAndCheckMetaTag(node)
41
+ }
42
+
43
+ this.elementStack.push(tagName)
44
+ this.visitChildNodes(node)
45
+ this.elementStack.pop()
46
+ }
47
+
48
+ protected onEnterControlFlow(_controlFlowType: ControlFlowType, wasAlreadyInControlFlow: boolean): ControlFlowState {
49
+ const stateToRestore: ControlFlowState = {
50
+ previousBranchMetas: this.currentBranchMetas,
51
+ previousControlFlowMetas: this.controlFlowMetas
52
+ }
53
+
54
+ this.currentBranchMetas = []
55
+
56
+ if (!wasAlreadyInControlFlow) {
57
+ this.controlFlowMetas = []
58
+ }
59
+
60
+ return stateToRestore
61
+ }
62
+
63
+ protected onExitControlFlow(controlFlowType: ControlFlowType, wasAlreadyInControlFlow: boolean, stateToRestore: ControlFlowState): void {
64
+ if (controlFlowType === ControlFlowType.CONDITIONAL && !wasAlreadyInControlFlow) {
65
+ this.controlFlowMetas.forEach(meta => this.documentMetas.push(meta))
66
+ }
67
+
68
+ this.currentBranchMetas = stateToRestore.previousBranchMetas
69
+ this.controlFlowMetas = stateToRestore.previousControlFlowMetas
70
+ }
71
+
72
+ protected onEnterBranch(): BranchState {
73
+ const stateToRestore: BranchState = {
74
+ previousBranchMetas: this.currentBranchMetas
75
+ }
76
+
77
+ if (this.isInControlFlow) {
78
+ this.currentBranchMetas = []
79
+ }
80
+
81
+ return stateToRestore
82
+ }
83
+
84
+ protected onExitBranch(_stateToRestore: BranchState): void {}
85
+
86
+ private get insideHead(): boolean {
87
+ return this.elementStack.includes("head")
88
+ }
89
+
90
+ private collectAndCheckMetaTag(node: HTMLElementNode): void {
91
+ const metaTag: MetaTag = { node }
92
+ this.extractAttributes(node, metaTag)
93
+
94
+ if (!metaTag.nameValue && !metaTag.httpEquivValue) return
95
+
96
+ if (this.isInControlFlow) {
97
+ this.handleControlFlowMeta(metaTag)
98
+ } else {
99
+ this.handleGlobalMeta(metaTag)
100
+ }
101
+
102
+ this.currentBranchMetas.push(metaTag)
103
+ }
104
+
105
+ private extractAttributes(node: HTMLElementNode, metaTag: MetaTag): void {
106
+ if (isHTMLElementNode(node) && node.open_tag) {
107
+ forEachAttribute(node.open_tag as any, (attributeNode: HTMLAttributeNode) => {
108
+ const name = getAttributeName(attributeNode)
109
+ const value = getAttributeValue(attributeNode)?.trim()
110
+
111
+ if (name === "name" && value) {
112
+ metaTag.nameValue = value
113
+ } else if (name === "http-equiv" && value) {
114
+ metaTag.httpEquivValue = value
115
+ }
116
+ })
117
+ }
118
+ }
119
+
120
+ private handleControlFlowMeta(metaTag: MetaTag): void {
121
+ if (this.currentControlFlowType === ControlFlowType.LOOP) {
122
+ this.checkAgainstMetaList(metaTag, this.currentBranchMetas, "within the same loop iteration")
123
+ } else {
124
+ this.checkAgainstMetaList(metaTag, this.currentBranchMetas, "within the same control flow branch")
125
+ this.checkAgainstMetaList(metaTag, this.documentMetas, "")
126
+
127
+ this.controlFlowMetas.push(metaTag)
128
+ }
129
+ }
130
+
131
+ private handleGlobalMeta(metaTag: MetaTag): void {
132
+ this.checkAgainstMetaList(metaTag, this.documentMetas, "")
133
+ this.documentMetas.push(metaTag)
134
+ }
135
+
136
+ private checkAgainstMetaList(metaTag: MetaTag, existingMetas: MetaTag[], context: string): void {
137
+ for (const existing of existingMetas) {
138
+ if (this.areMetaTagsDuplicate(metaTag, existing)) {
139
+ const attributeDescription = metaTag.nameValue
140
+ ? `\`name="${metaTag.nameValue}"\``
141
+ : `\`http-equiv="${metaTag.httpEquivValue}"\``
142
+
143
+ const attributeType = metaTag.nameValue ? "Meta names" : "`http-equiv` values"
144
+
145
+ const contextMsg = context ? ` ${context}` : ""
146
+
147
+ this.addOffense(
148
+ `Duplicate \`<meta>\` tag with ${attributeDescription}${contextMsg}. ${attributeType} should be unique within the \`<head>\` section.`,
149
+ metaTag.node.location,
150
+ )
151
+
152
+ return
153
+ }
154
+ }
155
+ }
156
+
157
+ private areMetaTagsDuplicate(meta1: MetaTag, meta2: MetaTag): boolean {
158
+ if (meta1.nameValue && meta2.nameValue) {
159
+ return meta1.nameValue.toLowerCase() === meta2.nameValue.toLowerCase()
160
+ }
161
+
162
+ if (meta1.httpEquivValue && meta2.httpEquivValue) {
163
+ return meta1.httpEquivValue.toLowerCase() === meta2.httpEquivValue.toLowerCase()
164
+ }
165
+
166
+ return false
167
+ }
168
+ }
169
+
170
+ export class HTMLNoDuplicateMetaNamesRule extends ParserRule {
171
+ static autocorrectable = false
172
+ name = "html-no-duplicate-meta-names"
173
+
174
+ get defaultConfig(): FullRuleConfig {
175
+ return {
176
+ enabled: true,
177
+ severity: "error"
178
+ }
179
+ }
180
+
181
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
182
+ const visitor = new HTMLNoDuplicateMetaNamesVisitor(this.name, context)
183
+
184
+ visitor.visit(result.value)
185
+
186
+ return visitor.offenses
187
+ }
188
+ }