@herb-tools/linter 0.8.9 → 0.9.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 (573) hide show
  1. package/README.md +5 -5
  2. package/dist/{src/cli → cli}/argument-parser.js +15 -2
  3. package/dist/cli/argument-parser.js.map +1 -0
  4. package/dist/{src/cli → cli}/file-processor.js +155 -9
  5. package/dist/cli/file-processor.js.map +1 -0
  6. package/dist/cli/file-url.js +6 -0
  7. package/dist/cli/file-url.js.map +1 -0
  8. package/dist/cli/formatters/base-formatter.js.map +1 -0
  9. package/dist/{src/cli → cli}/formatters/detailed-formatter.js +16 -19
  10. package/dist/cli/formatters/detailed-formatter.js.map +1 -0
  11. package/dist/cli/formatters/github-actions-formatter.js.map +1 -0
  12. package/dist/cli/formatters/index.js.map +1 -0
  13. package/dist/cli/formatters/json-formatter.js.map +1 -0
  14. package/dist/cli/formatters/simple-formatter.js +54 -0
  15. package/dist/cli/formatters/simple-formatter.js.map +1 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/cli/lint-worker.js +143 -0
  18. package/dist/cli/lint-worker.js.map +1 -0
  19. package/dist/cli/output-manager.js.map +1 -0
  20. package/dist/{src/cli → cli}/summary-reporter.js +13 -16
  21. package/dist/cli/summary-reporter.js.map +1 -0
  22. package/dist/{src/cli.js → cli.js} +5 -3
  23. package/dist/cli.js.map +1 -0
  24. package/dist/{src/custom-rule-loader.js → custom-rule-loader.js} +20 -4
  25. package/dist/custom-rule-loader.js.map +1 -0
  26. package/dist/herb-disable-comment-utils.js.map +1 -0
  27. package/dist/herb-lint.js +60648 -17513
  28. package/dist/herb-lint.js.map +1 -1
  29. package/dist/index.cjs +2621 -934
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +2554 -873
  32. package/dist/index.js.map +1 -1
  33. package/dist/lint-worker.js +71462 -0
  34. package/dist/lint-worker.js.map +1 -0
  35. package/dist/linter-ignore.js.map +1 -0
  36. package/dist/{src/linter.js → linter.js} +89 -74
  37. package/dist/linter.js.map +1 -0
  38. package/dist/loader.cjs +31206 -7834
  39. package/dist/loader.cjs.map +1 -1
  40. package/dist/loader.js +31168 -7802
  41. package/dist/loader.js.map +1 -1
  42. package/dist/parse-cache.js +30 -0
  43. package/dist/parse-cache.js.map +1 -0
  44. package/dist/rules/actionview-no-silent-helper.js +45 -0
  45. package/dist/rules/actionview-no-silent-helper.js.map +1 -0
  46. package/dist/{src/rules → rules}/erb-comment-syntax.js +2 -2
  47. package/dist/rules/erb-comment-syntax.js.map +1 -0
  48. package/dist/{src/rules → rules}/erb-no-case-node-children.js +2 -2
  49. package/dist/rules/erb-no-case-node-children.js.map +1 -0
  50. package/dist/rules/erb-no-conditional-html-element.js +38 -0
  51. package/dist/rules/erb-no-conditional-html-element.js.map +1 -0
  52. package/dist/rules/erb-no-conditional-open-tag.js +24 -0
  53. package/dist/rules/erb-no-conditional-open-tag.js.map +1 -0
  54. package/dist/rules/erb-no-duplicate-branch-elements.js +245 -0
  55. package/dist/rules/erb-no-duplicate-branch-elements.js.map +1 -0
  56. package/dist/{src/rules → rules}/erb-no-empty-tags.js +2 -2
  57. package/dist/rules/erb-no-empty-tags.js.map +1 -0
  58. package/dist/{src/rules → rules}/erb-no-extra-newline.js +4 -21
  59. package/dist/rules/erb-no-extra-newline.js.map +1 -0
  60. package/dist/{src/rules → rules}/erb-no-extra-whitespace-inside-tags.js +39 -13
  61. package/dist/rules/erb-no-extra-whitespace-inside-tags.js.map +1 -0
  62. package/dist/rules/erb-no-inline-case-conditions.js +40 -0
  63. package/dist/rules/erb-no-inline-case-conditions.js.map +1 -0
  64. package/dist/rules/erb-no-instance-variables-in-partials.js +67 -0
  65. package/dist/rules/erb-no-instance-variables-in-partials.js.map +1 -0
  66. package/dist/rules/erb-no-interpolated-class-names.js +47 -0
  67. package/dist/rules/erb-no-interpolated-class-names.js.map +1 -0
  68. package/dist/rules/erb-no-javascript-tag-helper.js +34 -0
  69. package/dist/rules/erb-no-javascript-tag-helper.js.map +1 -0
  70. package/dist/{src/rules → rules}/erb-no-output-control-flow.js +9 -12
  71. package/dist/rules/erb-no-output-control-flow.js.map +1 -0
  72. package/dist/rules/erb-no-output-in-attribute-name.js +30 -0
  73. package/dist/rules/erb-no-output-in-attribute-name.js.map +1 -0
  74. package/dist/rules/erb-no-output-in-attribute-position.js +30 -0
  75. package/dist/rules/erb-no-output-in-attribute-position.js.map +1 -0
  76. package/dist/rules/erb-no-raw-output-in-attribute-value.js +35 -0
  77. package/dist/rules/erb-no-raw-output-in-attribute-value.js.map +1 -0
  78. package/dist/{src/rules → rules}/erb-no-silent-tag-in-attribute-name.js +2 -2
  79. package/dist/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -0
  80. package/dist/rules/erb-no-statement-in-script.js +58 -0
  81. package/dist/rules/erb-no-statement-in-script.js.map +1 -0
  82. package/dist/rules/erb-no-then-in-control-flow.js +45 -0
  83. package/dist/rules/erb-no-then-in-control-flow.js.map +1 -0
  84. package/dist/rules/erb-no-trailing-whitespace.js +138 -0
  85. package/dist/rules/erb-no-trailing-whitespace.js.map +1 -0
  86. package/dist/rules/erb-no-unsafe-js-attribute.js +36 -0
  87. package/dist/rules/erb-no-unsafe-js-attribute.js.map +1 -0
  88. package/dist/rules/erb-no-unsafe-raw.js +63 -0
  89. package/dist/rules/erb-no-unsafe-raw.js.map +1 -0
  90. package/dist/rules/erb-no-unsafe-script-interpolation.js +54 -0
  91. package/dist/rules/erb-no-unsafe-script-interpolation.js.map +1 -0
  92. package/dist/{src/rules → rules}/erb-prefer-image-tag-helper.js +5 -4
  93. package/dist/rules/erb-prefer-image-tag-helper.js.map +1 -0
  94. package/dist/{src/rules → rules}/erb-require-trailing-newline.js +2 -2
  95. package/dist/rules/erb-require-trailing-newline.js.map +1 -0
  96. package/dist/{src/rules → rules}/erb-require-whitespace-inside-tags.js +39 -15
  97. package/dist/rules/erb-require-whitespace-inside-tags.js.map +1 -0
  98. package/dist/{src/rules → rules}/erb-right-trim.js +2 -2
  99. package/dist/rules/erb-right-trim.js.map +1 -0
  100. package/dist/{src/rules → rules}/erb-strict-locals-comment-syntax.js +5 -5
  101. package/dist/rules/erb-strict-locals-comment-syntax.js.map +1 -0
  102. package/dist/{src/rules → rules}/erb-strict-locals-required.js +2 -2
  103. package/dist/rules/erb-strict-locals-required.js.map +1 -0
  104. package/dist/rules/file-utils.js.map +1 -0
  105. package/dist/rules/herb-disable-comment-base.js.map +1 -0
  106. package/dist/{src/rules → rules}/herb-disable-comment-malformed.js +2 -2
  107. package/dist/rules/herb-disable-comment-malformed.js.map +1 -0
  108. package/dist/{src/rules → rules}/herb-disable-comment-missing-rules.js +2 -2
  109. package/dist/rules/herb-disable-comment-missing-rules.js.map +1 -0
  110. package/dist/{src/rules → rules}/herb-disable-comment-no-duplicate-rules.js +2 -2
  111. package/dist/rules/herb-disable-comment-no-duplicate-rules.js.map +1 -0
  112. package/dist/{src/rules → rules}/herb-disable-comment-no-redundant-all.js +2 -2
  113. package/dist/rules/herb-disable-comment-no-redundant-all.js.map +1 -0
  114. package/dist/{src/rules → rules}/herb-disable-comment-unnecessary.js +2 -2
  115. package/dist/rules/herb-disable-comment-unnecessary.js.map +1 -0
  116. package/dist/{src/rules → rules}/herb-disable-comment-valid-rule-name.js +2 -2
  117. package/dist/rules/herb-disable-comment-valid-rule-name.js.map +1 -0
  118. package/dist/rules/html-allowed-script-type.js +57 -0
  119. package/dist/rules/html-allowed-script-type.js.map +1 -0
  120. package/dist/rules/html-anchor-require-href.js +68 -0
  121. package/dist/rules/html-anchor-require-href.js.map +1 -0
  122. package/dist/{src/rules → rules}/html-aria-attribute-must-be-valid.js +3 -3
  123. package/dist/rules/html-aria-attribute-must-be-valid.js.map +1 -0
  124. package/dist/{src/rules → rules}/html-aria-label-is-well-formatted.js +3 -3
  125. package/dist/rules/html-aria-label-is-well-formatted.js.map +1 -0
  126. package/dist/{src/rules → rules}/html-aria-level-must-be-valid.js +3 -3
  127. package/dist/rules/html-aria-level-must-be-valid.js.map +1 -0
  128. package/dist/{src/rules → rules}/html-aria-role-heading-requires-level.js +5 -4
  129. package/dist/rules/html-aria-role-heading-requires-level.js.map +1 -0
  130. package/dist/{src/rules → rules}/html-aria-role-must-be-valid.js +3 -3
  131. package/dist/rules/html-aria-role-must-be-valid.js.map +1 -0
  132. package/dist/{src/rules → rules}/html-attribute-double-quotes.js +4 -4
  133. package/dist/rules/html-attribute-double-quotes.js.map +1 -0
  134. package/dist/{src/rules → rules}/html-attribute-equals-spacing.js +2 -2
  135. package/dist/rules/html-attribute-equals-spacing.js.map +1 -0
  136. package/dist/{src/rules → rules}/html-attribute-values-require-quotes.js +2 -2
  137. package/dist/rules/html-attribute-values-require-quotes.js.map +1 -0
  138. package/dist/{src/rules → rules}/html-avoid-both-disabled-and-aria-disabled.js +9 -9
  139. package/dist/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -0
  140. package/dist/{src/rules → rules}/html-body-only-elements.js +5 -4
  141. package/dist/rules/html-body-only-elements.js.map +1 -0
  142. package/dist/{src/rules → rules}/html-boolean-attributes-no-value.js +4 -3
  143. package/dist/rules/html-boolean-attributes-no-value.js.map +1 -0
  144. package/dist/rules/html-details-has-summary.js +52 -0
  145. package/dist/rules/html-details-has-summary.js.map +1 -0
  146. package/dist/{src/rules → rules}/html-head-only-elements.js +6 -5
  147. package/dist/rules/html-head-only-elements.js.map +1 -0
  148. package/dist/{src/rules → rules}/html-iframe-has-title.js +8 -11
  149. package/dist/rules/html-iframe-has-title.js.map +1 -0
  150. package/dist/{src/rules → rules}/html-img-require-alt.js +11 -5
  151. package/dist/rules/html-img-require-alt.js.map +1 -0
  152. package/dist/{src/rules → rules}/html-input-require-autocomplete.js +7 -10
  153. package/dist/rules/html-input-require-autocomplete.js.map +1 -0
  154. package/dist/{src/rules → rules}/html-navigation-has-label.js +6 -5
  155. package/dist/rules/html-navigation-has-label.js.map +1 -0
  156. package/dist/rules/html-no-abstract-roles.js +29 -0
  157. package/dist/rules/html-no-abstract-roles.js.map +1 -0
  158. package/dist/rules/html-no-aria-hidden-on-body.js +42 -0
  159. package/dist/rules/html-no-aria-hidden-on-body.js.map +1 -0
  160. package/dist/{src/rules → rules}/html-no-aria-hidden-on-focusable.js +6 -5
  161. package/dist/rules/html-no-aria-hidden-on-focusable.js.map +1 -0
  162. package/dist/{src/rules → rules}/html-no-block-inside-inline.js +6 -9
  163. package/dist/rules/html-no-block-inside-inline.js.map +1 -0
  164. package/dist/{src/rules → rules}/html-no-duplicate-attributes.js +4 -3
  165. package/dist/rules/html-no-duplicate-attributes.js.map +1 -0
  166. package/dist/{src/rules → rules}/html-no-duplicate-ids.js +14 -11
  167. package/dist/rules/html-no-duplicate-ids.js.map +1 -0
  168. package/dist/{src/rules → rules}/html-no-duplicate-meta-names.js +22 -20
  169. package/dist/rules/html-no-duplicate-meta-names.js.map +1 -0
  170. package/dist/{src/rules → rules}/html-no-empty-attributes.js +2 -2
  171. package/dist/rules/html-no-empty-attributes.js.map +1 -0
  172. package/dist/rules/html-no-empty-headings.js +98 -0
  173. package/dist/rules/html-no-empty-headings.js.map +1 -0
  174. package/dist/{src/rules → rules}/html-no-nested-links.js +23 -15
  175. package/dist/rules/html-no-nested-links.js.map +1 -0
  176. package/dist/{src/rules → rules}/html-no-positive-tab-index.js +3 -3
  177. package/dist/rules/html-no-positive-tab-index.js.map +1 -0
  178. package/dist/{src/rules → rules}/html-no-self-closing.js +4 -4
  179. package/dist/rules/html-no-self-closing.js.map +1 -0
  180. package/dist/{src/rules → rules}/html-no-space-in-tag.js +4 -6
  181. package/dist/rules/html-no-space-in-tag.js.map +1 -0
  182. package/dist/{src/rules → rules}/html-no-title-attribute.js +6 -5
  183. package/dist/rules/html-no-title-attribute.js.map +1 -0
  184. package/dist/{src/rules → rules}/html-no-underscores-in-attribute-names.js +2 -2
  185. package/dist/rules/html-no-underscores-in-attribute-names.js.map +1 -0
  186. package/dist/rules/html-require-closing-tags.js +29 -0
  187. package/dist/rules/html-require-closing-tags.js.map +1 -0
  188. package/dist/{src/rules → rules}/html-tag-name-lowercase.js +13 -9
  189. package/dist/rules/html-tag-name-lowercase.js.map +1 -0
  190. package/dist/{src/rules → rules}/index.js +19 -0
  191. package/dist/rules/index.js.map +1 -0
  192. package/dist/{src/rules → rules}/parser-no-errors.js +3 -3
  193. package/dist/rules/parser-no-errors.js.map +1 -0
  194. package/dist/{src/rules → rules}/rule-utils.js +141 -219
  195. package/dist/rules/rule-utils.js.map +1 -0
  196. package/dist/rules/string-utils.js.map +1 -0
  197. package/dist/{src/rules → rules}/svg-tag-name-capitalization.js +7 -6
  198. package/dist/rules/svg-tag-name-capitalization.js.map +1 -0
  199. package/dist/rules/turbo-permanent-require-id.js +34 -0
  200. package/dist/rules/turbo-permanent-require-id.js.map +1 -0
  201. package/dist/{src/rules.js → rules.js} +56 -10
  202. package/dist/rules.js.map +1 -0
  203. package/dist/types/cli/argument-parser.d.ts +1 -0
  204. package/dist/types/cli/file-processor.d.ts +13 -0
  205. package/dist/types/cli/file-url.d.ts +1 -0
  206. package/dist/types/cli/index.d.ts +1 -0
  207. package/dist/types/cli/lint-worker.d.ts +34 -0
  208. package/dist/types/custom-rule-loader.d.ts +4 -0
  209. package/dist/types/index.d.ts +1 -0
  210. package/dist/types/linter.d.ts +13 -6
  211. package/dist/types/parse-cache.d.ts +9 -0
  212. package/dist/types/{src/rules/html-aria-level-must-be-valid.d.ts → rules/actionview-no-silent-helper.d.ts} +4 -3
  213. package/dist/types/rules/erb-comment-syntax.d.ts +1 -1
  214. package/dist/types/rules/erb-no-case-node-children.d.ts +1 -1
  215. package/dist/types/{src/rules/herb-disable-comment-malformed.d.ts → rules/erb-no-conditional-html-element.d.ts} +3 -3
  216. package/dist/types/{src/rules/erb-prefer-image-tag-helper.d.ts → rules/erb-no-conditional-open-tag.d.ts} +3 -3
  217. package/dist/types/rules/erb-no-duplicate-branch-elements.d.ts +17 -0
  218. package/dist/types/rules/erb-no-empty-tags.d.ts +1 -1
  219. package/dist/types/rules/erb-no-extra-newline.d.ts +1 -1
  220. package/dist/types/rules/erb-no-extra-whitespace-inside-tags.d.ts +1 -1
  221. package/dist/types/{src/rules/html-no-duplicate-attributes.d.ts → rules/erb-no-inline-case-conditions.d.ts} +4 -3
  222. package/dist/types/rules/erb-no-instance-variables-in-partials.d.ts +10 -0
  223. package/dist/types/{src/rules/html-no-aria-hidden-on-focusable.d.ts → rules/erb-no-interpolated-class-names.d.ts} +2 -2
  224. package/dist/types/{src/rules/html-aria-attribute-must-be-valid.d.ts → rules/erb-no-javascript-tag-helper.d.ts} +2 -2
  225. package/dist/types/rules/erb-no-output-control-flow.d.ts +1 -1
  226. package/dist/types/{src/rules/erb-no-silent-tag-in-attribute-name.d.ts → rules/erb-no-output-in-attribute-name.d.ts} +2 -2
  227. package/dist/types/{src/rules/herb-disable-comment-missing-rules.d.ts → rules/erb-no-output-in-attribute-position.d.ts} +2 -2
  228. package/dist/types/{src/rules/erb-no-empty-tags.d.ts → rules/erb-no-raw-output-in-attribute-value.d.ts} +2 -2
  229. package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +1 -1
  230. package/dist/types/{src/rules/html-navigation-has-label.d.ts → rules/erb-no-statement-in-script.d.ts} +2 -2
  231. package/dist/types/rules/erb-no-then-in-control-flow.d.ts +9 -0
  232. package/dist/types/rules/erb-no-trailing-whitespace.d.ts +19 -0
  233. package/dist/types/{src/rules/html-no-positive-tab-index.d.ts → rules/erb-no-unsafe-js-attribute.d.ts} +2 -2
  234. package/dist/types/{src/rules/erb-no-case-node-children.d.ts → rules/erb-no-unsafe-raw.d.ts} +2 -2
  235. package/dist/types/rules/erb-no-unsafe-script-interpolation.d.ts +8 -0
  236. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +1 -1
  237. package/dist/types/rules/erb-require-trailing-newline.d.ts +1 -1
  238. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +1 -1
  239. package/dist/types/rules/erb-right-trim.d.ts +1 -1
  240. package/dist/types/rules/erb-strict-locals-comment-syntax.d.ts +1 -1
  241. package/dist/types/rules/erb-strict-locals-required.d.ts +1 -1
  242. package/dist/types/rules/herb-disable-comment-malformed.d.ts +1 -1
  243. package/dist/types/rules/herb-disable-comment-missing-rules.d.ts +1 -1
  244. package/dist/types/rules/herb-disable-comment-no-duplicate-rules.d.ts +1 -1
  245. package/dist/types/rules/herb-disable-comment-no-redundant-all.d.ts +1 -1
  246. package/dist/types/rules/herb-disable-comment-unnecessary.d.ts +1 -1
  247. package/dist/types/rules/herb-disable-comment-valid-rule-name.d.ts +1 -1
  248. package/dist/types/{src/rules/html-anchor-require-href.d.ts → rules/html-allowed-script-type.d.ts} +2 -2
  249. package/dist/types/rules/html-anchor-require-href.d.ts +3 -2
  250. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +1 -1
  251. package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +1 -1
  252. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +1 -1
  253. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +1 -1
  254. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +1 -1
  255. package/dist/types/rules/html-attribute-double-quotes.d.ts +1 -1
  256. package/dist/types/rules/html-attribute-equals-spacing.d.ts +1 -1
  257. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +1 -1
  258. package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +1 -1
  259. package/dist/types/rules/html-body-only-elements.d.ts +1 -1
  260. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +1 -1
  261. package/dist/types/{src/rules/html-no-empty-attributes.d.ts → rules/html-details-has-summary.d.ts} +4 -3
  262. package/dist/types/rules/html-head-only-elements.d.ts +1 -1
  263. package/dist/types/rules/html-iframe-has-title.d.ts +1 -1
  264. package/dist/types/rules/html-img-require-alt.d.ts +1 -1
  265. package/dist/types/rules/html-input-require-autocomplete.d.ts +1 -1
  266. package/dist/types/rules/html-navigation-has-label.d.ts +1 -1
  267. package/dist/types/{src/rules/html-no-empty-headings.d.ts → rules/html-no-abstract-roles.d.ts} +2 -2
  268. package/dist/types/{src/rules/erb-no-output-control-flow.d.ts → rules/html-no-aria-hidden-on-body.d.ts} +3 -3
  269. package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +1 -1
  270. package/dist/types/rules/html-no-block-inside-inline.d.ts +1 -1
  271. package/dist/types/rules/html-no-duplicate-attributes.d.ts +1 -1
  272. package/dist/types/rules/html-no-duplicate-ids.d.ts +1 -1
  273. package/dist/types/rules/html-no-duplicate-meta-names.d.ts +1 -1
  274. package/dist/types/rules/html-no-empty-attributes.d.ts +1 -1
  275. package/dist/types/rules/html-no-empty-headings.d.ts +1 -1
  276. package/dist/types/rules/html-no-nested-links.d.ts +1 -1
  277. package/dist/types/rules/html-no-positive-tab-index.d.ts +1 -1
  278. package/dist/types/rules/html-no-self-closing.d.ts +1 -1
  279. package/dist/types/rules/html-no-space-in-tag.d.ts +1 -1
  280. package/dist/types/rules/html-no-title-attribute.d.ts +1 -1
  281. package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +1 -1
  282. package/dist/types/{src/rules/html-body-only-elements.d.ts → rules/html-require-closing-tags.d.ts} +4 -3
  283. package/dist/types/rules/html-tag-name-lowercase.d.ts +1 -1
  284. package/dist/types/rules/index.d.ts +19 -0
  285. package/dist/types/rules/parser-no-errors.d.ts +1 -1
  286. package/dist/types/rules/rule-utils.d.ts +35 -88
  287. package/dist/types/rules/svg-tag-name-capitalization.d.ts +1 -1
  288. package/dist/types/{src/rules/html-aria-role-must-be-valid.d.ts → rules/turbo-permanent-require-id.d.ts} +2 -2
  289. package/dist/types/types.d.ts +25 -7
  290. package/dist/types/urls.d.ts +1 -0
  291. package/dist/{src/types.js → types.js} +53 -0
  292. package/dist/types.js.map +1 -0
  293. package/dist/urls.js +5 -0
  294. package/dist/urls.js.map +1 -0
  295. package/docs/rules/README.md +23 -2
  296. package/docs/rules/actionview-no-silent-helper.md +57 -0
  297. package/docs/rules/erb-no-conditional-html-element.md +90 -0
  298. package/docs/rules/erb-no-conditional-open-tag.md +130 -0
  299. package/docs/rules/erb-no-duplicate-branch-elements.md +98 -0
  300. package/docs/rules/erb-no-inline-case-conditions.md +85 -0
  301. package/docs/rules/erb-no-instance-variables-in-partials.md +43 -0
  302. package/docs/rules/erb-no-interpolated-class-names.md +57 -0
  303. package/docs/rules/erb-no-javascript-tag-helper.md +33 -0
  304. package/docs/rules/erb-no-output-in-attribute-name.md +38 -0
  305. package/docs/rules/erb-no-output-in-attribute-position.md +60 -0
  306. package/docs/rules/erb-no-raw-output-in-attribute-value.md +37 -0
  307. package/docs/rules/erb-no-statement-in-script.md +68 -0
  308. package/docs/rules/erb-no-then-in-control-flow.md +86 -0
  309. package/docs/rules/erb-no-trailing-whitespace.md +69 -0
  310. package/docs/rules/erb-no-unsafe-js-attribute.md +41 -0
  311. package/docs/rules/erb-no-unsafe-raw.md +47 -0
  312. package/docs/rules/erb-no-unsafe-script-interpolation.md +73 -0
  313. package/docs/rules/html-allowed-script-type.md +59 -0
  314. package/docs/rules/html-anchor-require-href.md +19 -6
  315. package/docs/rules/html-details-has-summary.md +46 -0
  316. package/docs/rules/html-img-require-alt.md +5 -3
  317. package/docs/rules/html-no-abstract-roles.md +74 -0
  318. package/docs/rules/html-no-aria-hidden-on-body.md +44 -0
  319. package/docs/rules/html-require-closing-tags.md +142 -0
  320. package/docs/rules/parser-no-errors.md +4 -17
  321. package/docs/rules/turbo-permanent-require-id.md +41 -0
  322. package/package.json +12 -11
  323. package/src/cli/argument-parser.ts +20 -2
  324. package/src/cli/file-processor.ts +189 -10
  325. package/src/cli/file-url.ts +6 -0
  326. package/src/cli/formatters/detailed-formatter.ts +19 -21
  327. package/src/cli/formatters/simple-formatter.ts +23 -13
  328. package/src/cli/index.ts +2 -0
  329. package/src/cli/lint-worker.ts +208 -0
  330. package/src/cli/summary-reporter.ts +14 -15
  331. package/src/cli.ts +5 -3
  332. package/src/custom-rule-loader.ts +20 -5
  333. package/src/herb-disable-comment-utils.ts +0 -3
  334. package/src/index.ts +1 -0
  335. package/src/linter.ts +98 -79
  336. package/src/parse-cache.ts +39 -0
  337. package/src/rules/actionview-no-silent-helper.ts +58 -0
  338. package/src/rules/erb-comment-syntax.ts +2 -2
  339. package/src/rules/erb-no-case-node-children.ts +2 -2
  340. package/src/rules/erb-no-conditional-html-element.ts +53 -0
  341. package/src/rules/erb-no-conditional-open-tag.ts +37 -0
  342. package/src/rules/erb-no-duplicate-branch-elements.ts +320 -0
  343. package/src/rules/erb-no-empty-tags.ts +2 -2
  344. package/src/rules/erb-no-extra-newline.ts +5 -25
  345. package/src/rules/erb-no-extra-whitespace-inside-tags.ts +45 -15
  346. package/src/rules/erb-no-inline-case-conditions.ts +54 -0
  347. package/src/rules/erb-no-instance-variables-in-partials.ts +101 -0
  348. package/src/rules/erb-no-interpolated-class-names.ts +65 -0
  349. package/src/rules/erb-no-javascript-tag-helper.ts +47 -0
  350. package/src/rules/erb-no-output-control-flow.ts +10 -10
  351. package/src/rules/erb-no-output-in-attribute-name.ts +39 -0
  352. package/src/rules/erb-no-output-in-attribute-position.ts +39 -0
  353. package/src/rules/erb-no-raw-output-in-attribute-value.ts +47 -0
  354. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +2 -2
  355. package/src/rules/erb-no-statement-in-script.ts +82 -0
  356. package/src/rules/erb-no-then-in-control-flow.ts +62 -0
  357. package/src/rules/erb-no-trailing-whitespace.ts +187 -0
  358. package/src/rules/erb-no-unsafe-js-attribute.ts +47 -0
  359. package/src/rules/erb-no-unsafe-raw.ts +83 -0
  360. package/src/rules/erb-no-unsafe-script-interpolation.ts +76 -0
  361. package/src/rules/erb-prefer-image-tag-helper.ts +5 -4
  362. package/src/rules/erb-require-trailing-newline.ts +2 -2
  363. package/src/rules/erb-require-whitespace-inside-tags.ts +42 -18
  364. package/src/rules/erb-right-trim.ts +2 -2
  365. package/src/rules/erb-strict-locals-comment-syntax.ts +5 -5
  366. package/src/rules/erb-strict-locals-required.ts +2 -2
  367. package/src/rules/herb-disable-comment-malformed.ts +2 -2
  368. package/src/rules/herb-disable-comment-missing-rules.ts +2 -2
  369. package/src/rules/herb-disable-comment-no-duplicate-rules.ts +2 -2
  370. package/src/rules/herb-disable-comment-no-redundant-all.ts +2 -2
  371. package/src/rules/herb-disable-comment-unnecessary.ts +2 -2
  372. package/src/rules/herb-disable-comment-valid-rule-name.ts +2 -2
  373. package/src/rules/html-allowed-script-type.ts +84 -0
  374. package/src/rules/html-anchor-require-href.ts +73 -11
  375. package/src/rules/html-aria-attribute-must-be-valid.ts +3 -3
  376. package/src/rules/html-aria-label-is-well-formatted.ts +3 -3
  377. package/src/rules/html-aria-level-must-be-valid.ts +3 -3
  378. package/src/rules/html-aria-role-heading-requires-level.ts +5 -4
  379. package/src/rules/html-aria-role-must-be-valid.ts +3 -3
  380. package/src/rules/html-attribute-double-quotes.ts +4 -4
  381. package/src/rules/html-attribute-equals-spacing.ts +2 -2
  382. package/src/rules/html-attribute-values-require-quotes.ts +2 -2
  383. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +10 -11
  384. package/src/rules/html-body-only-elements.ts +5 -4
  385. package/src/rules/html-boolean-attributes-no-value.ts +4 -3
  386. package/src/rules/html-details-has-summary.ts +69 -0
  387. package/src/rules/html-head-only-elements.ts +6 -5
  388. package/src/rules/html-iframe-has-title.ts +8 -11
  389. package/src/rules/html-img-require-alt.ts +16 -5
  390. package/src/rules/html-input-require-autocomplete.ts +7 -10
  391. package/src/rules/html-navigation-has-label.ts +6 -5
  392. package/src/rules/html-no-abstract-roles.ts +40 -0
  393. package/src/rules/html-no-aria-hidden-on-body.ts +58 -0
  394. package/src/rules/html-no-aria-hidden-on-focusable.ts +6 -5
  395. package/src/rules/html-no-block-inside-inline.ts +7 -13
  396. package/src/rules/html-no-duplicate-attributes.ts +4 -3
  397. package/src/rules/html-no-duplicate-ids.ts +16 -13
  398. package/src/rules/html-no-duplicate-meta-names.ts +20 -19
  399. package/src/rules/html-no-empty-attributes.ts +2 -2
  400. package/src/rules/html-no-empty-headings.ts +44 -58
  401. package/src/rules/html-no-nested-links.ts +25 -16
  402. package/src/rules/html-no-positive-tab-index.ts +3 -3
  403. package/src/rules/html-no-self-closing.ts +5 -5
  404. package/src/rules/html-no-space-in-tag.ts +5 -8
  405. package/src/rules/html-no-title-attribute.ts +6 -5
  406. package/src/rules/html-no-underscores-in-attribute-names.ts +2 -2
  407. package/src/rules/html-require-closing-tags.ts +41 -0
  408. package/src/rules/html-tag-name-lowercase.ts +14 -9
  409. package/src/rules/index.ts +19 -0
  410. package/src/rules/parser-no-errors.ts +3 -3
  411. package/src/rules/rule-utils.ts +162 -279
  412. package/src/rules/svg-tag-name-capitalization.ts +10 -10
  413. package/src/rules/turbo-permanent-require-id.ts +49 -0
  414. package/src/rules.ts +60 -10
  415. package/src/types.ts +76 -7
  416. package/src/urls.ts +5 -0
  417. package/dist/package.json +0 -65
  418. package/dist/src/cli/argument-parser.js.map +0 -1
  419. package/dist/src/cli/file-processor.js.map +0 -1
  420. package/dist/src/cli/formatters/base-formatter.js.map +0 -1
  421. package/dist/src/cli/formatters/detailed-formatter.js.map +0 -1
  422. package/dist/src/cli/formatters/github-actions-formatter.js.map +0 -1
  423. package/dist/src/cli/formatters/index.js.map +0 -1
  424. package/dist/src/cli/formatters/json-formatter.js.map +0 -1
  425. package/dist/src/cli/formatters/simple-formatter.js +0 -44
  426. package/dist/src/cli/formatters/simple-formatter.js.map +0 -1
  427. package/dist/src/cli/index.js.map +0 -1
  428. package/dist/src/cli/output-manager.js.map +0 -1
  429. package/dist/src/cli/summary-reporter.js.map +0 -1
  430. package/dist/src/cli.js.map +0 -1
  431. package/dist/src/custom-rule-loader.js.map +0 -1
  432. package/dist/src/herb-disable-comment-utils.js.map +0 -1
  433. package/dist/src/herb-lint.js +0 -5
  434. package/dist/src/herb-lint.js.map +0 -1
  435. package/dist/src/index.js +0 -5
  436. package/dist/src/index.js.map +0 -1
  437. package/dist/src/linter-ignore.js.map +0 -1
  438. package/dist/src/linter.js.map +0 -1
  439. package/dist/src/loader.js +0 -17
  440. package/dist/src/loader.js.map +0 -1
  441. package/dist/src/rules/erb-comment-syntax.js.map +0 -1
  442. package/dist/src/rules/erb-no-case-node-children.js.map +0 -1
  443. package/dist/src/rules/erb-no-empty-tags.js.map +0 -1
  444. package/dist/src/rules/erb-no-extra-newline.js.map +0 -1
  445. package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js.map +0 -1
  446. package/dist/src/rules/erb-no-output-control-flow.js.map +0 -1
  447. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +0 -1
  448. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +0 -1
  449. package/dist/src/rules/erb-require-trailing-newline.js.map +0 -1
  450. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +0 -1
  451. package/dist/src/rules/erb-right-trim.js.map +0 -1
  452. package/dist/src/rules/erb-strict-locals-comment-syntax.js.map +0 -1
  453. package/dist/src/rules/erb-strict-locals-required.js.map +0 -1
  454. package/dist/src/rules/file-utils.js.map +0 -1
  455. package/dist/src/rules/herb-disable-comment-base.js.map +0 -1
  456. package/dist/src/rules/herb-disable-comment-malformed.js.map +0 -1
  457. package/dist/src/rules/herb-disable-comment-missing-rules.js.map +0 -1
  458. package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js.map +0 -1
  459. package/dist/src/rules/herb-disable-comment-no-redundant-all.js.map +0 -1
  460. package/dist/src/rules/herb-disable-comment-unnecessary.js.map +0 -1
  461. package/dist/src/rules/herb-disable-comment-valid-rule-name.js.map +0 -1
  462. package/dist/src/rules/html-anchor-require-href.js +0 -32
  463. package/dist/src/rules/html-anchor-require-href.js.map +0 -1
  464. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +0 -1
  465. package/dist/src/rules/html-aria-label-is-well-formatted.js.map +0 -1
  466. package/dist/src/rules/html-aria-level-must-be-valid.js.map +0 -1
  467. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +0 -1
  468. package/dist/src/rules/html-aria-role-must-be-valid.js.map +0 -1
  469. package/dist/src/rules/html-attribute-double-quotes.js.map +0 -1
  470. package/dist/src/rules/html-attribute-equals-spacing.js.map +0 -1
  471. package/dist/src/rules/html-attribute-values-require-quotes.js.map +0 -1
  472. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +0 -1
  473. package/dist/src/rules/html-body-only-elements.js.map +0 -1
  474. package/dist/src/rules/html-boolean-attributes-no-value.js.map +0 -1
  475. package/dist/src/rules/html-head-only-elements.js.map +0 -1
  476. package/dist/src/rules/html-iframe-has-title.js.map +0 -1
  477. package/dist/src/rules/html-img-require-alt.js.map +0 -1
  478. package/dist/src/rules/html-input-require-autocomplete.js.map +0 -1
  479. package/dist/src/rules/html-navigation-has-label.js.map +0 -1
  480. package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +0 -1
  481. package/dist/src/rules/html-no-block-inside-inline.js.map +0 -1
  482. package/dist/src/rules/html-no-duplicate-attributes.js.map +0 -1
  483. package/dist/src/rules/html-no-duplicate-ids.js.map +0 -1
  484. package/dist/src/rules/html-no-duplicate-meta-names.js.map +0 -1
  485. package/dist/src/rules/html-no-empty-attributes.js.map +0 -1
  486. package/dist/src/rules/html-no-empty-headings.js +0 -115
  487. package/dist/src/rules/html-no-empty-headings.js.map +0 -1
  488. package/dist/src/rules/html-no-nested-links.js.map +0 -1
  489. package/dist/src/rules/html-no-positive-tab-index.js.map +0 -1
  490. package/dist/src/rules/html-no-self-closing.js.map +0 -1
  491. package/dist/src/rules/html-no-space-in-tag.js.map +0 -1
  492. package/dist/src/rules/html-no-title-attribute.js.map +0 -1
  493. package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +0 -1
  494. package/dist/src/rules/html-tag-name-lowercase.js.map +0 -1
  495. package/dist/src/rules/index.js.map +0 -1
  496. package/dist/src/rules/parser-no-errors.js.map +0 -1
  497. package/dist/src/rules/rule-utils.js.map +0 -1
  498. package/dist/src/rules/string-utils.js.map +0 -1
  499. package/dist/src/rules/svg-tag-name-capitalization.js.map +0 -1
  500. package/dist/src/rules.js.map +0 -1
  501. package/dist/src/types.js.map +0 -1
  502. package/dist/tsconfig.tsbuildinfo +0 -1
  503. package/dist/types/src/cli/argument-parser.d.ts +0 -25
  504. package/dist/types/src/cli/file-processor.d.ts +0 -43
  505. package/dist/types/src/cli/formatters/base-formatter.d.ts +0 -6
  506. package/dist/types/src/cli/formatters/detailed-formatter.d.ts +0 -13
  507. package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +0 -17
  508. package/dist/types/src/cli/formatters/index.d.ts +0 -5
  509. package/dist/types/src/cli/formatters/json-formatter.d.ts +0 -48
  510. package/dist/types/src/cli/formatters/simple-formatter.d.ts +0 -8
  511. package/dist/types/src/cli/index.d.ts +0 -5
  512. package/dist/types/src/cli/output-manager.d.ts +0 -32
  513. package/dist/types/src/cli/summary-reporter.d.ts +0 -28
  514. package/dist/types/src/cli.d.ts +0 -28
  515. package/dist/types/src/custom-rule-loader.d.ts +0 -62
  516. package/dist/types/src/herb-disable-comment-utils.d.ts +0 -69
  517. package/dist/types/src/herb-lint.d.ts +0 -2
  518. package/dist/types/src/index.d.ts +0 -4
  519. package/dist/types/src/linter-ignore.d.ts +0 -12
  520. package/dist/types/src/linter.d.ts +0 -133
  521. package/dist/types/src/loader.d.ts +0 -20
  522. package/dist/types/src/rules/erb-comment-syntax.d.ts +0 -14
  523. package/dist/types/src/rules/erb-no-extra-newline.d.ts +0 -14
  524. package/dist/types/src/rules/erb-no-extra-whitespace-inside-tags.d.ts +0 -18
  525. package/dist/types/src/rules/erb-require-trailing-newline.d.ts +0 -9
  526. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +0 -18
  527. package/dist/types/src/rules/erb-right-trim.d.ts +0 -14
  528. package/dist/types/src/rules/erb-strict-locals-comment-syntax.d.ts +0 -9
  529. package/dist/types/src/rules/erb-strict-locals-required.d.ts +0 -9
  530. package/dist/types/src/rules/file-utils.d.ts +0 -13
  531. package/dist/types/src/rules/herb-disable-comment-base.d.ts +0 -37
  532. package/dist/types/src/rules/herb-disable-comment-no-duplicate-rules.d.ts +0 -8
  533. package/dist/types/src/rules/herb-disable-comment-no-redundant-all.d.ts +0 -8
  534. package/dist/types/src/rules/herb-disable-comment-unnecessary.d.ts +0 -8
  535. package/dist/types/src/rules/herb-disable-comment-valid-rule-name.d.ts +0 -8
  536. package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +0 -8
  537. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +0 -8
  538. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +0 -15
  539. package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +0 -14
  540. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +0 -15
  541. package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +0 -8
  542. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +0 -14
  543. package/dist/types/src/rules/html-head-only-elements.d.ts +0 -9
  544. package/dist/types/src/rules/html-iframe-has-title.d.ts +0 -8
  545. package/dist/types/src/rules/html-img-require-alt.d.ts +0 -8
  546. package/dist/types/src/rules/html-input-require-autocomplete.d.ts +0 -8
  547. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +0 -8
  548. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +0 -8
  549. package/dist/types/src/rules/html-no-duplicate-meta-names.d.ts +0 -9
  550. package/dist/types/src/rules/html-no-nested-links.d.ts +0 -8
  551. package/dist/types/src/rules/html-no-self-closing.d.ts +0 -16
  552. package/dist/types/src/rules/html-no-space-in-tag.d.ts +0 -16
  553. package/dist/types/src/rules/html-no-title-attribute.d.ts +0 -8
  554. package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +0 -8
  555. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +0 -18
  556. package/dist/types/src/rules/index.d.ts +0 -54
  557. package/dist/types/src/rules/parser-no-errors.d.ts +0 -9
  558. package/dist/types/src/rules/rule-utils.d.ts +0 -351
  559. package/dist/types/src/rules/string-utils.d.ts +0 -15
  560. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +0 -16
  561. package/dist/types/src/rules.d.ts +0 -2
  562. package/dist/types/src/types.d.ts +0 -190
  563. /package/dist/{src/cli → cli}/formatters/base-formatter.js +0 -0
  564. /package/dist/{src/cli → cli}/formatters/github-actions-formatter.js +0 -0
  565. /package/dist/{src/cli → cli}/formatters/index.js +0 -0
  566. /package/dist/{src/cli → cli}/formatters/json-formatter.js +0 -0
  567. /package/dist/{src/cli → cli}/index.js +0 -0
  568. /package/dist/{src/cli → cli}/output-manager.js +0 -0
  569. /package/dist/{src/herb-disable-comment-utils.js → herb-disable-comment-utils.js} +0 -0
  570. /package/dist/{src/linter-ignore.js → linter-ignore.js} +0 -0
  571. /package/dist/{src/rules → rules}/file-utils.js +0 -0
  572. /package/dist/{src/rules → rules}/herb-disable-comment-base.js +0 -0
  573. /package/dist/{src/rules → rules}/string-utils.js +0 -0
@@ -0,0 +1,142 @@
1
+ # Linter Rule: Require explicit closing tags
2
+
3
+ **Rule:** `html-require-closing-tags`
4
+
5
+ ## Description
6
+
7
+ Disallow the omission of optional closing tags for HTML elements where the closing tag is technically optional according to the HTML specification. This rule flags elements that have an `HTMLOmittedCloseTagNode` as their close tag.
8
+
9
+ ## Rationale
10
+
11
+ While HTML allows certain closing tags to be omitted (implicitly closed by sibling elements or parent closing), explicit closing tags improve code readability and maintainability. They make the document structure clear at a glance and reduce ambiguity about where elements end.
12
+
13
+ Explicit closing tags also:
14
+
15
+ - Make templates easier to understand for developers unfamiliar with HTML's implicit closing rules
16
+ - Reduce potential for subtle bugs when refactoring or moving code
17
+ - Improve consistency across the codebase
18
+ - Make diffs cleaner when adding content to elements
19
+
20
+ ## Elements with Optional Closing Tags
21
+
22
+ This rule would flag elements that have omitted closing tags:
23
+
24
+ - `<li>` - list items
25
+ - `<dt>`, `<dd>` - definition list terms and descriptions
26
+ - `<p>` - paragraphs
27
+ - `<option>`, `<optgroup>` - select options and groups
28
+ - `<thead>`, `<tbody>`, `<tfoot>` - table sections
29
+ - `<tr>` - table rows
30
+ - `<td>`, `<th>` - table cells
31
+ - `<colgroup>` - table column groups
32
+ - `<rt>`, `<rp>` - ruby annotations
33
+
34
+ ## Examples
35
+
36
+ ### ✅ Good
37
+
38
+ Explicit closing tags:
39
+
40
+ ```erb
41
+ <ul>
42
+ <li>Item 1</li>
43
+ <li>Item 2</li>
44
+ <li>Item 3</li>
45
+ </ul>
46
+ ```
47
+
48
+ ```erb
49
+ <dl>
50
+ <dt>Term 1</dt>
51
+ <dd>Definition 1</dd>
52
+ <dt>Term 2</dt>
53
+ <dd>Definition 2</dd>
54
+ </dl>
55
+ ```
56
+
57
+ ```erb
58
+ <table>
59
+ <thead>
60
+ <tr>
61
+ <th>Header 1</th>
62
+ <th>Header 2</th>
63
+ </tr>
64
+ </thead>
65
+ <tbody>
66
+ <tr>
67
+ <td>Cell 1</td>
68
+ <td>Cell 2</td>
69
+ </tr>
70
+ </tbody>
71
+ </table>
72
+ ```
73
+
74
+ ```erb
75
+ <select>
76
+ <option>Option 1</option>
77
+ <option>Option 2</option>
78
+ <option>Option 3</option>
79
+ </select>
80
+ ```
81
+
82
+ ```erb
83
+ <div>
84
+ <p>Paragraph 1</p>
85
+ <p>Paragraph 2</p>
86
+ </div>
87
+ ```
88
+
89
+ ### 🚫 Bad
90
+
91
+ Omitted closing tags (implicitly closed):
92
+
93
+ ```erb
94
+ <ul>
95
+ <li>Item 1
96
+ <li>Item 2
97
+ <li>Item 3
98
+ </ul>
99
+ ```
100
+
101
+ ```erb
102
+ <dl>
103
+ <dt>Term 1
104
+ <dd>Definition 1
105
+ <dt>Term 2
106
+ <dd>Definition 2
107
+ </dl>
108
+ ```
109
+
110
+ ```erb
111
+ <table>
112
+ <thead>
113
+ <tr>
114
+ <th>Header 1
115
+ <th>Header 2
116
+ <tbody>
117
+ <tr>
118
+ <td>Cell 1
119
+ <td>Cell 2
120
+ </table>
121
+ ```
122
+
123
+ ```erb
124
+ <select>
125
+ <option>Option 1
126
+ <option>Option 2
127
+ <option>Option 3
128
+ </select>
129
+ ```
130
+
131
+ ```erb
132
+ <div>
133
+ <p>Paragraph 1
134
+ <p>Paragraph 2
135
+ </div>
136
+ ```
137
+
138
+ ## References
139
+
140
+ - [HTML Spec: Optional Tags](https://html.spec.whatwg.org/multipage/syntax.html#optional-tags)
141
+ - [HTML Spec: Tag Omission](https://html.spec.whatwg.org/multipage/dom.html#concept-element-tag-omission)
142
+ - [CSS-Tricks: Fighting the Space Between Inline Block Elements](https://css-tricks.com/fighting-the-space-between-inline-block-elements/)
@@ -39,15 +39,16 @@ By surfacing parser errors through the linter, developers can catch these critic
39
39
  ### 🚫 Bad
40
40
 
41
41
  ```html
42
- <!-- Mismatched closing tag -->
43
42
  <h2>Welcome to our site</h3>
43
+ ```
44
44
 
45
- <!-- Unclosed element -->
45
+ ```html
46
46
  <div>
47
47
  <p>This paragraph is never closed
48
48
  </div>
49
+ ```
49
50
 
50
- <!-- Missing opening tag -->
51
+ ```html
51
52
  Some content
52
53
  </div>
53
54
  ```
@@ -63,20 +64,6 @@ Some content
63
64
  <img src="image.jpg" alt="Description"></img>
64
65
  ```
65
66
 
66
- ## Error Types
67
-
68
- This rule reports various parser error types:
69
-
70
- - **`UNCLOSED_ELEMENT_ERROR`**: Elements that are opened but never closed
71
- - **`MISSING_CLOSING_TAG_ERROR`**: Opening tags without matching closing tags
72
- - **`MISSING_OPENING_TAG_ERROR`**: Closing tags without matching opening tags
73
- - **`TAG_NAMES_MISMATCH_ERROR`**: Opening and closing tags with different names
74
- - **`QUOTES_MISMATCH_ERROR`**: Mismatched quotation marks in attributes
75
- - **`VOID_ELEMENT_CLOSING_TAG_ERROR`**: Void elements (like `<img>`) with closing tags
76
- - **`RUBY_PARSE_ERROR`**: Invalid Ruby syntax within ERB tags
77
- - **`UNEXPECTED_TOKEN_ERROR`**: Unexpected tokens during parsing
78
- - **`UNEXPECTED_ERROR`**: Other unexpected parsing issues
79
-
80
67
  ## References
81
68
 
82
69
  * [HTML Living Standard - Parsing](https://html.spec.whatwg.org/multipage/parsing.html)
@@ -0,0 +1,41 @@
1
+ # Linter Rule: Require `id` attribute on elements with `data-turbo-permanent`
2
+
3
+ **Rule:** `turbo-permanent-require-id`
4
+
5
+ ## Description
6
+
7
+ Ensure that all HTML elements with the `data-turbo-permanent` attribute also have an `id` attribute. Without an `id`, Turbo can't track the element across page changes and the permanent behavior won't work as expected.
8
+
9
+ ## Rationale
10
+
11
+ Turbo's `data-turbo-permanent` attribute marks an element to be persisted across page navigations. Turbo uses the element's `id` to match it between the current page and the new page. If no `id` is present, Turbo has no way to identify and preserve the element, so the `data-turbo-permanent` attribute has no effect.
12
+
13
+ ## Examples
14
+
15
+ ### ✅ Good
16
+
17
+ ```erb
18
+ <div id="player" data-turbo-permanent>
19
+ <!-- This element will persist across page navigations -->
20
+ </div>
21
+
22
+ <audio id="background-music" data-turbo-permanent>
23
+ <source src="/music.mp3" type="audio/mpeg">
24
+ </audio>
25
+ ```
26
+
27
+ ### 🚫 Bad
28
+
29
+ ```erb
30
+ <div data-turbo-permanent>
31
+ <!-- Missing id: Turbo can't track this element -->
32
+ </div>
33
+
34
+ <audio data-turbo-permanent>
35
+ <source src="/music.mp3" type="audio/mpeg">
36
+ </audio>
37
+ ```
38
+
39
+ ## References
40
+
41
+ - [Turbo Handbook: Persisting Elements Across Page Loads](https://turbo.hotwired.dev/handbook/building#persisting-elements-across-page-loads)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herb-tools/linter",
3
- "version": "0.8.9",
3
+ "version": "0.9.0",
4
4
  "description": "HTML+ERB linter for validating HTML structure and enforcing best practices",
5
5
  "license": "MIT",
6
6
  "homepage": "https://herb-tools.dev",
@@ -22,7 +22,7 @@
22
22
  "watch": "tsc -b -w",
23
23
  "test": "vitest run",
24
24
  "test:watch": "vitest --watch",
25
- "prepublishOnly": "yarn clean && yarn build && yarn test"
25
+ "prepublishOnly": "yarn clean && yarn build"
26
26
  },
27
27
  "exports": {
28
28
  "./package.json": "./package.json",
@@ -33,9 +33,10 @@
33
33
  "default": "./dist/index.js"
34
34
  },
35
35
  "./cli": {
36
- "types": "./dist/types/src/cli.d.ts",
37
- "require": "./dist/src/cli.js",
38
- "default": "./dist/src/cli.js"
36
+ "types": "./dist/types/cli.d.ts",
37
+ "import": "./dist/cli.js",
38
+ "require": "./dist/cli.js",
39
+ "default": "./dist/cli.js"
39
40
  },
40
41
  "./loader": {
41
42
  "types": "./dist/types/loader.d.ts",
@@ -45,12 +46,12 @@
45
46
  }
46
47
  },
47
48
  "dependencies": {
48
- "@herb-tools/config": "0.8.9",
49
- "@herb-tools/core": "0.8.9",
50
- "@herb-tools/highlighter": "0.8.9",
51
- "@herb-tools/node-wasm": "0.8.9",
52
- "@herb-tools/printer": "0.8.9",
53
- "@herb-tools/rewriter": "0.8.9",
49
+ "@herb-tools/config": "0.9.0",
50
+ "@herb-tools/core": "0.9.0",
51
+ "@herb-tools/highlighter": "0.9.0",
52
+ "@herb-tools/node-wasm": "0.9.0",
53
+ "@herb-tools/printer": "0.9.0",
54
+ "@herb-tools/rewriter": "0.9.0",
54
55
  "picomatch": "^4.0.2",
55
56
  "tinyglobby": "^0.2.15"
56
57
  },
@@ -1,5 +1,6 @@
1
1
  import dedent from "dedent"
2
2
 
3
+ import { availableParallelism } from "node:os"
3
4
  import { parseArgs } from "util"
4
5
  import { Herb } from "@herb-tools/node-wasm"
5
6
 
@@ -27,6 +28,7 @@ export interface ParsedArguments {
27
28
  init: boolean
28
29
  loadCustomRules: boolean
29
30
  failLevel?: DiagnosticSeverity
31
+ jobs: number
30
32
  }
31
33
 
32
34
  export class ArgumentParser {
@@ -53,6 +55,8 @@ export class ArgumentParser {
53
55
  --github enable GitHub Actions annotations (combines with --format)
54
56
  --no-github disable GitHub Actions annotations (even in GitHub Actions environment)
55
57
  --no-custom-rules disable loading custom rules from project (custom rules are loaded by default from .herb/rules/**/*.{mjs,js})
58
+ -j, --jobs <n> number of parallel workers for linting files [default: auto]
59
+ use "auto" to detect based on available CPU cores
56
60
  --theme syntax highlighting theme (${THEME_NAMES.join("|")}) or path to custom theme file [default: ${DEFAULT_THEME}]
57
61
  --no-color disable colored output
58
62
  --no-timing hide timing information
@@ -83,7 +87,8 @@ export class ArgumentParser {
83
87
  "no-timing": { type: "boolean" },
84
88
  "no-wrap-lines": { type: "boolean" },
85
89
  "truncate-lines": { type: "boolean" },
86
- "no-custom-rules": { type: "boolean" }
90
+ "no-custom-rules": { type: "boolean" },
91
+ jobs: { type: "string", short: "j" }
87
92
  },
88
93
  allowPositionals: true
89
94
  })
@@ -163,7 +168,20 @@ export class ArgumentParser {
163
168
  }
164
169
  }
165
170
 
166
- return { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, loadCustomRules, failLevel }
171
+ let jobs = availableParallelism()
172
+
173
+ if (values.jobs && values.jobs !== "auto") {
174
+ const parsed = parseInt(values.jobs, 10)
175
+
176
+ if (isNaN(parsed) || parsed < 1) {
177
+ console.error(`Error: Invalid --jobs value "${values.jobs}". Must be a positive integer or "auto".`)
178
+ process.exit(1)
179
+ }
180
+
181
+ jobs = parsed
182
+ }
183
+
184
+ return { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, loadCustomRules, failLevel, jobs }
167
185
  }
168
186
 
169
187
  private getFilePatterns(positionals: string[]): string[] {
@@ -3,13 +3,17 @@ import { Linter } from "../linter.js"
3
3
  import { loadCustomRules } from "../loader.js"
4
4
  import { Config } from "@herb-tools/config"
5
5
 
6
- import { readFileSync, writeFileSync } from "fs"
7
- import { resolve } from "path"
6
+ import { Worker } from "node:worker_threads"
7
+ import { readFileSync, writeFileSync } from "node:fs"
8
+ import { resolve, dirname, join } from "node:path"
9
+ import { fileURLToPath } from "node:url"
10
+ import { availableParallelism } from "node:os"
8
11
  import { colorize } from "@herb-tools/highlighter"
9
12
 
10
13
  import type { Diagnostic } from "@herb-tools/core"
11
14
  import type { FormatOption } from "./argument-parser.js"
12
15
  import type { HerbConfigOptions } from "@herb-tools/config"
16
+ import type { WorkerInput, WorkerResult } from "./lint-worker.js"
13
17
 
14
18
  export interface ProcessedFile {
15
19
  filename: string
@@ -20,6 +24,7 @@ export interface ProcessedFile {
20
24
 
21
25
  export interface ProcessingContext {
22
26
  projectPath?: string
27
+ configPath?: string
23
28
  pattern?: string
24
29
  fix?: boolean
25
30
  fixUnsafe?: boolean
@@ -27,6 +32,7 @@ export interface ProcessingContext {
27
32
  linterConfig?: HerbConfigOptions['linter']
28
33
  config?: Config
29
34
  loadCustomRules?: boolean
35
+ jobs?: number
30
36
  }
31
37
 
32
38
  export interface ProcessingResult {
@@ -44,6 +50,13 @@ export interface ProcessingResult {
44
50
  context?: ProcessingContext
45
51
  }
46
52
 
53
+ /**
54
+ * Minimum number of files required to use parallel processing.
55
+ * Below this threshold, sequential processing is faster due to
56
+ * worker thread startup overhead (loading WASM, config, etc.).
57
+ */
58
+ const PARALLEL_FILE_THRESHOLD = 10
59
+
47
60
  export class FileProcessor {
48
61
  private linter: Linter | null = null
49
62
  private customRulesLoaded: boolean = false
@@ -51,18 +64,27 @@ export class FileProcessor {
51
64
  private isRuleAutocorrectable(ruleName: string): boolean {
52
65
  if (!this.linter) return false
53
66
 
54
- const RuleClass = (this.linter as any).rules.find((rule: any) => {
55
- const instance = new rule()
56
-
57
- return instance.name === ruleName
58
- })
67
+ const ruleClass = (this.linter as any).rules.find(
68
+ (rule: any) => rule.ruleName === ruleName
69
+ )
59
70
 
60
- if (!RuleClass) return false
71
+ if (!ruleClass) return false
61
72
 
62
- return RuleClass.autocorrectable === true
73
+ return ruleClass.autocorrectable === true
63
74
  }
64
75
 
65
76
  async processFiles(files: string[], formatOption: FormatOption = 'detailed', context?: ProcessingContext): Promise<ProcessingResult> {
77
+ const jobs = context?.jobs ?? 1
78
+ const shouldParallelize = jobs > 1 && files.length >= PARALLEL_FILE_THRESHOLD
79
+
80
+ if (shouldParallelize) {
81
+ return this.processFilesInParallel(files, jobs, formatOption, context)
82
+ }
83
+
84
+ return this.processFilesSequentially(files, formatOption, context)
85
+ }
86
+
87
+ private async processFilesSequentially(files: string[], formatOption: FormatOption = 'detailed', context?: ProcessingContext): Promise<ProcessingResult> {
66
88
  let totalErrors = 0
67
89
  let totalWarnings = 0
68
90
  let totalInfo = 0
@@ -147,7 +169,7 @@ export class FileProcessor {
147
169
  filesFixed++
148
170
 
149
171
  if (formatOption !== 'json') {
150
- console.log(`${colorize("✓", "brightGreen")} ${colorize(filename, "cyan")} - ${colorize(`Fixed ${autofixResult.fixed.length} offense(s)`, "green")}`)
172
+ console.log(`${colorize("✓", "brightGreen")} ${colorize(filename, "cyan")} - ${colorize(`Fixed ${autofixResult.fixed.length} ${autofixResult.fixed.length === 1 ? "offense" : "offenses"}`, "green")}`)
151
173
  }
152
174
  }
153
175
 
@@ -225,4 +247,161 @@ export class FileProcessor {
225
247
 
226
248
  return result
227
249
  }
250
+
251
+ private async processFilesInParallel(files: string[], jobs: number, formatOption: FormatOption, context?: ProcessingContext): Promise<ProcessingResult> {
252
+ const workerCount = Math.min(jobs, files.length)
253
+ const chunks = this.splitIntoChunks(files, workerCount)
254
+ const workerPath = this.resolveWorkerPath()
255
+
256
+ const workerPromises = chunks.map(chunk => this.runWorker(workerPath, chunk, context))
257
+ const workerResults = await Promise.all(workerPromises)
258
+
259
+ for (const result of workerResults) {
260
+ if (result.error) {
261
+ throw new Error(`Worker error: ${result.error}`)
262
+ }
263
+ }
264
+
265
+ return this.aggregateWorkerResults(workerResults, formatOption, context)
266
+ }
267
+
268
+ private resolveWorkerPath(): string {
269
+ try {
270
+ const currentDir = dirname(fileURLToPath(import.meta.url))
271
+
272
+ return join(currentDir, "lint-worker.js")
273
+ } catch {
274
+ return join(__dirname, "lint-worker.js")
275
+ }
276
+ }
277
+
278
+ private splitIntoChunks(files: string[], chunkCount: number): string[][] {
279
+ const chunks: string[][] = Array.from({ length: chunkCount }, () => [])
280
+
281
+ for (let i = 0; i < files.length; i++) {
282
+ chunks[i % chunkCount].push(files[i])
283
+ }
284
+
285
+ return chunks.filter(chunk => chunk.length > 0)
286
+ }
287
+
288
+ private runWorker(workerPath: string, files: string[], context?: ProcessingContext): Promise<WorkerResult> {
289
+ return new Promise((resolve, reject) => {
290
+ const workerData: WorkerInput = {
291
+ files,
292
+ projectPath: context?.projectPath || process.cwd(),
293
+ configPath: context?.configPath,
294
+ fix: context?.fix || false,
295
+ fixUnsafe: context?.fixUnsafe || false,
296
+ ignoreDisableComments: context?.ignoreDisableComments || false,
297
+ loadCustomRules: context?.loadCustomRules || false,
298
+ }
299
+
300
+ const worker = new Worker(workerPath, { workerData })
301
+
302
+ worker.on("message", (result: WorkerResult) => {
303
+ resolve(result)
304
+ })
305
+
306
+ worker.on("error", (error) => {
307
+ reject(error)
308
+ })
309
+
310
+ worker.on("exit", (code) => {
311
+ if (code !== 0) {
312
+ reject(new Error(`Worker exited with code ${code}`))
313
+ }
314
+ })
315
+ })
316
+ }
317
+
318
+ private aggregateWorkerResults(results: WorkerResult[], formatOption: FormatOption, context?: ProcessingContext): ProcessingResult {
319
+ let totalErrors = 0
320
+ let totalWarnings = 0
321
+ let totalInfo = 0
322
+ let totalHints = 0
323
+ let totalIgnored = 0
324
+ let totalWouldBeIgnored = 0
325
+ let filesWithOffenses = 0
326
+ let filesFixed = 0
327
+ let ruleCount = 0
328
+
329
+ const allOffenses: ProcessedFile[] = []
330
+ const ruleOffenses = new Map<string, { count: number, files: Set<string> }>()
331
+
332
+ for (const result of results) {
333
+ totalErrors += result.totalErrors
334
+ totalWarnings += result.totalWarnings
335
+ totalInfo += result.totalInfo
336
+ totalHints += result.totalHints
337
+ totalIgnored += result.totalIgnored
338
+ totalWouldBeIgnored += result.totalWouldBeIgnored
339
+ filesWithOffenses += result.filesWithOffenses
340
+ filesFixed += result.filesFixed
341
+
342
+ if (result.ruleCount > 0) {
343
+ ruleCount = result.ruleCount
344
+ }
345
+
346
+ for (const offense of result.offenses) {
347
+ allOffenses.push({
348
+ filename: offense.filename,
349
+ offense: offense.offense,
350
+ content: offense.content,
351
+ autocorrectable: offense.autocorrectable
352
+ })
353
+ }
354
+
355
+ for (const [rule, data] of result.ruleOffenses) {
356
+ const existing = ruleOffenses.get(rule) || { count: 0, files: new Set<string>() }
357
+ existing.count += data.count
358
+
359
+ for (const file of data.files) {
360
+ existing.files.add(file)
361
+ }
362
+
363
+ ruleOffenses.set(rule, existing)
364
+ }
365
+
366
+ if (formatOption !== 'json') {
367
+ for (const fixMessage of result.fixMessages) {
368
+ const [filename, countStr] = fixMessage.split("\t")
369
+ const count = parseInt(countStr, 10)
370
+ console.log(`${colorize("\u2713", "brightGreen")} ${colorize(filename, "cyan")} - ${colorize(`Fixed ${count} ${count === 1 ? "offense" : "offenses"}`, "green")}`)
371
+ }
372
+ }
373
+ }
374
+
375
+ const processingResult: ProcessingResult = {
376
+ totalErrors,
377
+ totalWarnings,
378
+ totalInfo,
379
+ totalHints,
380
+ totalIgnored,
381
+ filesWithOffenses,
382
+ filesFixed,
383
+ ruleCount,
384
+ allOffenses,
385
+ ruleOffenses,
386
+ context
387
+ }
388
+
389
+ if (totalWouldBeIgnored > 0) {
390
+ processingResult.totalWouldBeIgnored = totalWouldBeIgnored
391
+ }
392
+
393
+ return processingResult
394
+ }
395
+
396
+ /**
397
+ * Returns the default number of parallel jobs based on available CPU cores.
398
+ * Returns 1 if parallelism detection fails.
399
+ */
400
+ static defaultJobs(): number {
401
+ try {
402
+ return availableParallelism()
403
+ } catch {
404
+ return 1
405
+ }
406
+ }
228
407
  }
@@ -0,0 +1,6 @@
1
+ import { resolve } from "node:path"
2
+
3
+ export function fileUrl(filePath: string): string {
4
+ const absolutePath = resolve(filePath)
5
+ return `file://${absolutePath}`
6
+ }
@@ -2,6 +2,8 @@ import { colorize, Highlighter, type ThemeInput, DEFAULT_THEME } from "@herb-too
2
2
 
3
3
  import { BaseFormatter } from "./base-formatter.js"
4
4
  import { LineWrapper } from "@herb-tools/highlighter"
5
+ import { ruleDocumentationUrl } from "../../urls.js"
6
+ import { fileUrl } from "../file-url.js"
5
7
 
6
8
  import type { Diagnostic } from "@herb-tools/core"
7
9
  import type { ProcessedFile } from "../file-processor.js"
@@ -27,24 +29,24 @@ export class DetailedFormatter extends BaseFormatter {
27
29
  await this.highlighter.initialize()
28
30
  }
29
31
 
32
+ const correctableTag = colorize(colorize("[Correctable]", "green"), "bold")
33
+ const autocorrectableSet = new Set(
34
+ allOffenses.filter(item => item.autocorrectable).map(item => item.offense)
35
+ )
36
+
30
37
  if (isSingleFile) {
31
38
  const { filename, content } = allOffenses[0]
32
- const diagnostics = allOffenses.map(item => {
33
- if (item.autocorrectable && item.offense.code) {
34
- return {
35
- ...item.offense,
36
- message: `${item.offense.message} ${colorize(colorize("[Correctable]", "green"), "bold")}`
37
- }
38
- }
39
- return item.offense
40
- })
39
+ const diagnostics = allOffenses.map(item => item.offense)
41
40
 
42
41
  const highlighted = this.highlighter.highlight(filename, content, {
43
42
  diagnostics: diagnostics,
44
43
  splitDiagnostics: true,
45
44
  contextLines: 2,
46
45
  wrapLines: this.wrapLines,
47
- truncateLines: this.truncateLines
46
+ truncateLines: this.truncateLines,
47
+ codeUrlBuilder: ruleDocumentationUrl,
48
+ fileUrlBuilder: (path) => fileUrl(path),
49
+ suffixBuilder: (diagnostic) => autocorrectableSet.has(diagnostic) ? correctableTag : undefined,
48
50
  })
49
51
 
50
52
  console.log(`\n${highlighted}`)
@@ -54,19 +56,15 @@ export class DetailedFormatter extends BaseFormatter {
54
56
  for (let i = 0; i < allOffenses.length; i++) {
55
57
  const { filename, offense, content, autocorrectable } = allOffenses[i]
56
58
 
57
- let modifiedOffense = offense
58
-
59
- if (autocorrectable && offense.code) {
60
- modifiedOffense = {
61
- ...offense,
62
- message: `${offense.message} ${colorize(colorize("[Correctable]", "green"), "bold")}`
63
- }
64
- }
65
-
66
- const formatted = this.highlighter.highlightDiagnostic(filename, modifiedOffense, content, {
59
+ const codeUrl = offense.code ? ruleDocumentationUrl(offense.code) : undefined
60
+ const suffix = autocorrectable ? correctableTag : undefined
61
+ const formatted = this.highlighter.highlightDiagnostic(filename, offense, content, {
67
62
  contextLines: 2,
68
63
  wrapLines: this.wrapLines,
69
- truncateLines: this.truncateLines
64
+ truncateLines: this.truncateLines,
65
+ codeUrl,
66
+ fileUrl: fileUrl(filename),
67
+ suffix,
70
68
  })
71
69
  console.log(`\n${formatted}`)
72
70