@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
@@ -4,12 +4,18 @@ export interface SummaryData {
4
4
  files: string[]
5
5
  totalErrors: number
6
6
  totalWarnings: number
7
+ totalInfo?: number
8
+ totalHints?: number
9
+ totalIgnored: number
10
+ totalWouldBeIgnored?: number
7
11
  filesWithOffenses: number
8
12
  ruleCount: number
9
13
  startTime: number
10
14
  startDate: Date
11
15
  showTiming: boolean
12
16
  ruleOffenses: Map<string, { count: number, files: Set<string> }>
17
+ autofixableCount: number
18
+ ignoreDisableComments?: boolean
13
19
  }
14
20
 
15
21
  export class SummaryReporter {
@@ -18,7 +24,7 @@ export class SummaryReporter {
18
24
  }
19
25
 
20
26
  displaySummary(data: SummaryData): void {
21
- const { files, totalErrors, totalWarnings, filesWithOffenses, ruleCount, startTime, startDate, showTiming } = data
27
+ const { files, totalErrors, totalWarnings, totalInfo = 0, totalHints = 0, totalIgnored, totalWouldBeIgnored, filesWithOffenses, ruleCount, startTime, startDate, showTiming, autofixableCount, ignoreDisableComments } = data
22
28
 
23
29
  console.log("\n")
24
30
  console.log(` ${colorize("Summary:", "bold")}`)
@@ -62,6 +68,18 @@ export class SummaryReporter {
62
68
  parts.push(colorize(colorize(`${totalWarnings} ${this.pluralize(totalWarnings, "warning")}`, "green"), "bold"))
63
69
  }
64
70
 
71
+ if (totalInfo > 0) {
72
+ parts.push(colorize(colorize(`${totalInfo} info`, "brightBlue"), "bold"))
73
+ }
74
+
75
+ if (totalHints > 0) {
76
+ parts.push(colorize(colorize(`${totalHints} ${this.pluralize(totalHints, "hint")}`, "gray"), "bold"))
77
+ }
78
+
79
+ if (totalIgnored > 0) {
80
+ parts.push(colorize(colorize(`${totalIgnored} ignored`, "gray"), "bold"))
81
+ }
82
+
65
83
  if (parts.length === 0) {
66
84
  offensesSummary = colorize(colorize("0 offenses", "green"), "bold")
67
85
  } else {
@@ -69,17 +87,36 @@ export class SummaryReporter {
69
87
 
70
88
  let detailText = ""
71
89
 
72
- const totalOffenses = totalErrors + totalWarnings
90
+ const totalOffenses = totalErrors + totalWarnings + totalInfo + totalHints
73
91
 
74
92
  if (filesWithOffenses > 0) {
75
93
  detailText = `${totalOffenses} ${this.pluralize(totalOffenses, "offense")} across ${filesWithOffenses} ${this.pluralize(filesWithOffenses, "file")}`
76
94
  }
77
95
 
78
- offensesSummary += ` ${colorize(colorize(`(${detailText})`, "gray"), "dim")}`
96
+ if (detailText) {
97
+ offensesSummary += ` ${colorize(colorize(`(${detailText})`, "gray"), "dim")}`
98
+ }
79
99
  }
80
100
 
81
101
  console.log(` ${colorize(pad("Offenses"), "gray")} ${offensesSummary}`)
82
102
 
103
+ if (ignoreDisableComments && totalWouldBeIgnored && totalWouldBeIgnored > 0) {
104
+ const message = `${colorize(colorize(`${totalWouldBeIgnored} additional ${this.pluralize(totalWouldBeIgnored, "offense")} reported (would have been ignored)`, "cyan"), "bold")}`
105
+ console.log(` ${colorize(pad("Note"), "gray")} ${message}`)
106
+ }
107
+
108
+ const totalOffenses = totalErrors + totalWarnings + totalInfo + totalHints
109
+
110
+ if (autofixableCount > 0 || totalOffenses > 0) {
111
+ let fixableLine = `${colorize(colorize(`${totalOffenses} ${this.pluralize(totalOffenses, "offense")}`, "brightRed"), "bold")}`
112
+
113
+ if (autofixableCount > 0) {
114
+ fixableLine += ` | ${colorize(colorize(`${autofixableCount} autocorrectable using \`--fix\``, "green"), "bold")}`
115
+ }
116
+
117
+ console.log(` ${colorize(pad("Fixable"), "gray")} ${fixableLine}`)
118
+ }
119
+
83
120
  if (showTiming) {
84
121
  const duration = Date.now() - startTime
85
122
  const timeString = startDate.toTimeString().split(' ')[0]
package/src/cli.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import { glob } from "glob"
2
2
  import { Herb } from "@herb-tools/node-wasm"
3
+ import { Config, addHerbExtensionRecommendation, getExtensionsJsonRelativePath } from "@herb-tools/config"
4
+
3
5
  import { existsSync, statSync } from "fs"
4
6
  import { dirname, resolve, relative } from "path"
5
7
 
6
- import { ArgumentParser, type FormatOption } from "./cli/argument-parser.js"
8
+ import { ArgumentParser } from "./cli/argument-parser.js"
7
9
  import { FileProcessor } from "./cli/file-processor.js"
8
10
  import { OutputManager } from "./cli/output-manager.js"
11
+ import { version } from "../package.json"
12
+
13
+ import type { ProcessingContext } from "./cli/file-processor.js"
14
+ import type { FormatOption } from "./cli/argument-parser.js"
9
15
 
10
16
  export * from "./cli/index.js"
11
17
 
@@ -19,44 +25,6 @@ export class CLI {
19
25
  return this.projectPath
20
26
  }
21
27
 
22
- protected findProjectRoot(startPath: string): string {
23
- let currentPath = resolve(startPath)
24
-
25
- if (existsSync(currentPath) && statSync(currentPath).isFile()) {
26
- currentPath = dirname(currentPath)
27
- }
28
-
29
- const projectIndicators = [
30
- 'package.json',
31
- 'Gemfile',
32
- '.git',
33
- 'tsconfig.json',
34
- 'composer.json',
35
- 'pyproject.toml',
36
- 'requirements.txt',
37
- '.herb.yml'
38
- ]
39
-
40
- while (currentPath !== '/') {
41
- for (const indicator of projectIndicators) {
42
- if (existsSync(resolve(currentPath, indicator))) {
43
- return currentPath
44
- }
45
- }
46
-
47
- const parentPath = dirname(currentPath)
48
- if (parentPath === currentPath) {
49
- break
50
- }
51
-
52
- currentPath = parentPath
53
- }
54
-
55
- return existsSync(startPath) && statSync(startPath).isDirectory()
56
- ? startPath
57
- : dirname(startPath)
58
- }
59
-
60
28
  protected exitWithError(message: string, formatOption: FormatOption, exitCode: number = 1) {
61
29
  this.outputManager.outputError(message, {
62
30
  formatOption,
@@ -87,7 +55,8 @@ export class CLI {
87
55
  process.exit(exitCode)
88
56
  }
89
57
 
90
- protected determineProjectPath(pattern: string | undefined): void {
58
+ protected determineProjectPath(patterns: string[]): void {
59
+ const pattern = patterns[0]
91
60
  if (pattern) {
92
61
  const resolvedPattern = resolve(pattern)
93
62
 
@@ -97,15 +66,15 @@ export class CLI {
97
66
  if (stats.isDirectory()) {
98
67
  this.projectPath = resolvedPattern
99
68
  } else {
100
- this.projectPath = this.findProjectRoot(resolvedPattern)
69
+ this.projectPath = dirname(resolvedPattern)
101
70
  }
102
71
  }
103
72
  }
104
73
  }
105
74
 
106
- protected adjustPattern(pattern: string | undefined): string {
75
+ protected adjustPattern(pattern: string | undefined, configGlobPattern: string): string {
107
76
  if (!pattern) {
108
- return '**/*.html.erb'
77
+ return configGlobPattern
109
78
  }
110
79
 
111
80
  const resolvedPattern = resolve(pattern)
@@ -114,7 +83,7 @@ export class CLI {
114
83
  const stats = statSync(resolvedPattern)
115
84
 
116
85
  if (stats.isDirectory()) {
117
- return '**/*.html.erb'
86
+ return configGlobPattern
118
87
  } else if (stats.isFile()) {
119
88
  return relative(this.projectPath, resolvedPattern)
120
89
  }
@@ -123,6 +92,41 @@ export class CLI {
123
92
  return pattern
124
93
  }
125
94
 
95
+ protected async resolvePatternToFiles(pattern: string, config: Config, force: boolean): Promise<{ files: string[], explicitFile: string | undefined }> {
96
+ const resolvedPattern = resolve(pattern)
97
+ const isExplicitFile = existsSync(resolvedPattern) && statSync(resolvedPattern).isFile()
98
+ let explicitFile: string | undefined
99
+
100
+ if (isExplicitFile) {
101
+ explicitFile = pattern
102
+ }
103
+
104
+ const filesConfig = config.getFilesConfigForTool('linter')
105
+ const configGlobPattern = filesConfig.include && filesConfig.include.length > 0
106
+ ? (filesConfig.include.length === 1 ? filesConfig.include[0] : `{${filesConfig.include.join(',')}}`)
107
+ : '**/*.html.erb'
108
+ const adjustedPattern = this.adjustPattern(pattern, configGlobPattern)
109
+
110
+ let files = await glob(adjustedPattern, {
111
+ cwd: this.projectPath,
112
+ ignore: filesConfig.exclude || []
113
+ })
114
+
115
+ if (explicitFile && files.length === 0) {
116
+ if (!force) {
117
+ console.error(`⚠️ File ${explicitFile} is excluded by configuration patterns.`)
118
+ console.error(` Use --force to lint it anyway.\n`)
119
+ process.exit(0)
120
+ } else {
121
+ console.log(`⚠️ Forcing linter on excluded file: ${explicitFile}`)
122
+ console.log()
123
+ files = [adjustedPattern]
124
+ }
125
+ }
126
+
127
+ return { files, explicitFile }
128
+ }
129
+
126
130
  protected async beforeProcess(): Promise<void> {
127
131
  // Hook for subclasses to add custom output before processing
128
132
  }
@@ -137,11 +141,36 @@ export class CLI {
137
141
  const startTime = Date.now()
138
142
  const startDate = new Date()
139
143
 
140
- let { pattern, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions } = this.argumentParser.parse(process.argv)
144
+ let { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, ignoreDisableComments, force, init, loadCustomRules } = this.argumentParser.parse(process.argv)
141
145
 
142
- this.determineProjectPath(pattern)
146
+ this.determineProjectPath(patterns)
143
147
 
144
- pattern = this.adjustPattern(pattern)
148
+ if (init) {
149
+ const configPath = configFile || this.projectPath
150
+
151
+ if (Config.exists(configPath)) {
152
+ const fullPath = configFile || Config.configPathFromProjectPath(this.projectPath)
153
+ console.error(`\n✗ Configuration file already exists at ${fullPath}`)
154
+ console.error(` Use --config-file to specify a different location.\n`)
155
+ process.exit(1)
156
+ }
157
+
158
+ const config = await Config.loadForCLI(configPath, version, true)
159
+ const extensionAdded = addHerbExtensionRecommendation(this.projectPath)
160
+
161
+ console.log(`\n✓ Configuration initialized at ${config.path}`)
162
+
163
+ if (extensionAdded) {
164
+ console.log(`✓ VSCode extension recommended in ${getExtensionsJsonRelativePath()}`)
165
+ }
166
+
167
+ console.log(` Edit this file to customize linter and formatter settings.\n`)
168
+ process.exit(0)
169
+ }
170
+
171
+ const silent = formatOption === 'json'
172
+ const config = await Config.load(configFile || this.projectPath, { version, exitOnError: true, createIfMissing: false, silent })
173
+ const linterConfig = config.options.linter || {}
145
174
 
146
175
  const outputOptions = {
147
176
  formatOption,
@@ -157,15 +186,69 @@ export class CLI {
157
186
  try {
158
187
  await this.beforeProcess()
159
188
 
160
- const files = await glob(pattern, { cwd: this.projectPath })
189
+ if (linterConfig.enabled === false && !force) {
190
+ this.exitWithInfo("Linter is disabled in .herb.yml configuration. Use --force to lint anyway.", formatOption, 0, { startTime, startDate, showTiming })
191
+ }
192
+
193
+ if (force && linterConfig.enabled === false) {
194
+ console.log("⚠️ Forcing linter run (disabled in .herb.yml)")
195
+ console.log()
196
+ }
197
+
198
+ let files: string[]
199
+ let explicitFiles: string[] = []
200
+
201
+ if (patterns.length === 0) {
202
+ files = await config.findFilesForTool('linter', this.projectPath)
203
+ } else {
204
+ const allFiles: string[] = []
205
+
206
+ for (const pattern of patterns) {
207
+ const { files: patternFiles, explicitFile } = await this.resolvePatternToFiles(pattern, config, force)
208
+
209
+ if (patternFiles.length === 0) {
210
+ console.error(`✗ No files found matching pattern: ${pattern}`)
211
+ process.exit(1)
212
+ }
213
+
214
+ allFiles.push(...patternFiles)
215
+ if (explicitFile) {
216
+ explicitFiles.push(explicitFile)
217
+ }
218
+ }
219
+
220
+ files = [...new Set(allFiles)]
221
+ }
161
222
 
162
223
  if (files.length === 0) {
163
- this.exitWithInfo(`No files found matching pattern: ${pattern}`, formatOption, 0, { startTime, startDate, showTiming })
224
+ this.exitWithInfo(`No files found matching patterns: ${patterns.join(', ') || 'from config'}`, formatOption, 0, { startTime, startDate, showTiming })
225
+ }
226
+
227
+ let processingConfig = config
228
+
229
+ if (force && explicitFiles.length > 0) {
230
+ const modifiedConfig = Object.create(Object.getPrototypeOf(config))
231
+ Object.assign(modifiedConfig, config)
232
+
233
+ modifiedConfig.config = {
234
+ ...config.config,
235
+ linter: {
236
+ ...config.config.linter,
237
+ exclude: []
238
+ }
239
+ }
240
+
241
+ processingConfig = modifiedConfig
164
242
  }
165
243
 
166
- const context = {
244
+ const context: ProcessingContext = {
167
245
  projectPath: this.projectPath,
168
- pattern: pattern
246
+ pattern: patterns.join(' '),
247
+ fix,
248
+ ignoreDisableComments,
249
+ linterConfig,
250
+ config: processingConfig,
251
+ loadCustomRules
169
252
  }
170
253
 
171
254
  const results = await this.fileProcessor.processFiles(files, formatOption, context)
@@ -0,0 +1,189 @@
1
+ import { pathToFileURL } from "url"
2
+ import { glob } from "glob"
3
+
4
+ import type { RuleClass } from "./types.js"
5
+
6
+ export interface CustomRuleLoaderOptions {
7
+ /**
8
+ * Base directory to search for custom rules
9
+ * Defaults to current working directory
10
+ */
11
+ baseDir?: string
12
+
13
+ /**
14
+ * Glob patterns to search for custom rule files
15
+ * Defaults to looking in common locations
16
+ */
17
+ patterns?: string[]
18
+
19
+ /**
20
+ * Whether to suppress errors when loading custom rules
21
+ * Defaults to false
22
+ */
23
+ silent?: boolean
24
+ }
25
+
26
+ const DEFAULT_PATTERNS = [
27
+ ".herb/rules/**/*.mjs",
28
+ ]
29
+
30
+ /**
31
+ * Loads custom linter rules from the user's project
32
+ */
33
+ export class CustomRuleLoader {
34
+ private baseDir: string
35
+ private patterns: string[]
36
+ private silent: boolean
37
+
38
+ constructor(options: CustomRuleLoaderOptions = {}) {
39
+ this.baseDir = options.baseDir || process.cwd()
40
+ this.patterns = options.patterns || DEFAULT_PATTERNS
41
+ this.silent = options.silent || false
42
+ }
43
+
44
+ /**
45
+ * Discovers custom rule files in the project
46
+ */
47
+ async discoverRuleFiles(): Promise<string[]> {
48
+ const allFiles: string[] = []
49
+
50
+ for (const pattern of this.patterns) {
51
+ try {
52
+ const files = await glob(pattern, {
53
+ cwd: this.baseDir,
54
+ absolute: true,
55
+ nodir: true
56
+ })
57
+
58
+ allFiles.push(...files)
59
+ } catch (error) {
60
+ if (!this.silent) {
61
+ console.warn(`Warning: Failed to search pattern "${pattern}": ${error}`)
62
+ }
63
+ }
64
+ }
65
+
66
+ return [...new Set(allFiles)]
67
+ }
68
+
69
+ /**
70
+ * Loads a single rule file
71
+ */
72
+ async loadRuleFile(filePath: string): Promise<RuleClass[]> {
73
+ try {
74
+ const fileUrl = pathToFileURL(filePath).href
75
+ const cacheBustedUrl = `${fileUrl}?t=${Date.now()}`
76
+ const module = await import(cacheBustedUrl)
77
+
78
+ if (module.default && this.isValidRuleClass(module.default)) {
79
+ return [module.default]
80
+ }
81
+
82
+ if (!this.silent) {
83
+ console.warn(`Warning: No valid default export found in "${filePath}". Custom rules must use default export.`)
84
+ }
85
+
86
+ return []
87
+ } catch (error) {
88
+ if (!this.silent) {
89
+ console.error(`Error loading rule file "${filePath}": ${error}`)
90
+ }
91
+ return []
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Type guard to check if an export is a valid rule class
97
+ */
98
+ private isValidRuleClass(value: any): value is RuleClass {
99
+ if (typeof value !== 'function') return false
100
+ if (!value.prototype) return false
101
+
102
+ const instance = new value()
103
+
104
+ return typeof instance.check === 'function' && typeof instance.name === 'string'
105
+ }
106
+
107
+ /**
108
+ * Loads all custom rules from the project
109
+ */
110
+ async loadRules(): Promise<RuleClass[]> {
111
+ const ruleFiles = await this.discoverRuleFiles()
112
+
113
+ if (ruleFiles.length === 0) {
114
+ return []
115
+ }
116
+
117
+ const allRules: RuleClass[] = []
118
+
119
+ for (const filePath of ruleFiles) {
120
+ const rules = await this.loadRuleFile(filePath)
121
+ allRules.push(...rules)
122
+ }
123
+
124
+ return allRules
125
+ }
126
+
127
+ /**
128
+ * Loads all custom rules and returns detailed information about each rule
129
+ */
130
+ async loadRulesWithInfo(): Promise<{ rules: RuleClass[], ruleInfo: Array<{ name: string, path: string }>, duplicateWarnings: string[] }> {
131
+ const ruleFiles = await this.discoverRuleFiles()
132
+
133
+ if (ruleFiles.length === 0) {
134
+ return { rules: [], ruleInfo: [], duplicateWarnings: [] }
135
+ }
136
+
137
+ const allRules: RuleClass[] = []
138
+ const ruleInfo: Array<{ name: string, path: string }> = []
139
+ const duplicateWarnings: string[] = []
140
+ const seenNames = new Map<string, string>()
141
+
142
+ for (const filePath of ruleFiles) {
143
+ const rules = await this.loadRuleFile(filePath)
144
+ for (const RuleClass of rules) {
145
+ const instance = new RuleClass()
146
+ const ruleName = instance.name
147
+
148
+ if (seenNames.has(ruleName)) {
149
+ const firstPath = seenNames.get(ruleName)!
150
+ duplicateWarnings.push(
151
+ `Custom rule "${ruleName}" is defined in multiple files: "${firstPath}" and "${filePath}". The later one will be used.`
152
+ )
153
+ } else {
154
+ seenNames.set(ruleName, filePath)
155
+ }
156
+
157
+ allRules.push(RuleClass)
158
+ ruleInfo.push({
159
+ name: ruleName,
160
+ path: filePath
161
+ })
162
+ }
163
+ }
164
+
165
+ return { rules: allRules, ruleInfo, duplicateWarnings }
166
+ }
167
+
168
+ /**
169
+ * Static helper to check if custom rules exist in a project
170
+ */
171
+ static async hasCustomRules(baseDir: string = process.cwd()): Promise<boolean> {
172
+ const loader = new CustomRuleLoader({ baseDir, silent: true })
173
+ const files = await loader.discoverRuleFiles()
174
+ return files.length > 0
175
+ }
176
+
177
+ /**
178
+ * Static helper to load custom rules and merge with default rules
179
+ */
180
+ static async loadAndMergeRules(
181
+ defaultRules: RuleClass[],
182
+ options: CustomRuleLoaderOptions = {}
183
+ ): Promise<RuleClass[]> {
184
+ const loader = new CustomRuleLoader(options)
185
+ const customRules = await loader.loadRules()
186
+
187
+ return [...defaultRules, ...customRules]
188
+ }
189
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Utilities for parsing herb:disable comments
3
+ */
4
+
5
+ /**
6
+ * Information about a single rule name in a herb:disable comment
7
+ */
8
+ export interface HerbDisableRuleName {
9
+ /** The rule name */
10
+ name: string
11
+ /** The starting offset of this rule name within the content/line */
12
+ offset: number
13
+ /** The length of the rule name */
14
+ length: number
15
+ }
16
+
17
+ /**
18
+ * Result of parsing a herb:disable comment
19
+ */
20
+ export interface HerbDisableComment {
21
+ /** The full matched string */
22
+ match: string
23
+ /** Array of rule names specified in the comment */
24
+ ruleNames: string[]
25
+ /** Array of rule name information with positions */
26
+ ruleNameDetails: HerbDisableRuleName[]
27
+ /** The original rules string (e.g., "rule1, rule2") */
28
+ rulesString: string
29
+ }
30
+
31
+ /**
32
+ * Prefix for herb:disable comments
33
+ */
34
+ const HERB_DISABLE_PREFIX = "herb:disable"
35
+
36
+ /**
37
+ * Parse a herb:disable comment from ERB comment content.
38
+ * Use this when you have the content inside <%# ... %> (e.g., from ERBContentNode.content.value)
39
+ *
40
+ * @param content - The content string (without <%# %> delimiters)
41
+ * @returns Parsed comment data or null if not a valid herb:disable comment
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const result = parseHerbDisableContent("herb:disable rule1, rule2")
46
+ * // { match: "herb:disable rule1, rule2", ruleNames: ["rule1", "rule2"], rulesString: "rule1, rule2" }
47
+ * ```
48
+ */
49
+ export function parseHerbDisableContent(content: string): HerbDisableComment | null {
50
+ const trimmed = content.trim()
51
+
52
+ if (!trimmed.startsWith(HERB_DISABLE_PREFIX)) return null
53
+
54
+ const afterPrefix = trimmed.substring(HERB_DISABLE_PREFIX.length).trimStart()
55
+ if (afterPrefix.length === 0) return null
56
+
57
+ const rulesString = afterPrefix.trimEnd()
58
+ const ruleNames = rulesString.split(',').map(name => name.trim())
59
+
60
+ if (ruleNames.some(name => name.length === 0)) return null
61
+ if (ruleNames.length === 0) return null
62
+
63
+ const herbDisablePrefix = content.indexOf(HERB_DISABLE_PREFIX)
64
+ const searchStart = herbDisablePrefix + HERB_DISABLE_PREFIX.length
65
+ const rulesStringOffset = content.indexOf(rulesString, searchStart)
66
+
67
+ const ruleNameDetails: HerbDisableRuleName[] = []
68
+
69
+ let currentOffset = 0
70
+
71
+ for (const ruleName of ruleNames) {
72
+ const ruleOffset = rulesString.indexOf(ruleName, currentOffset)
73
+
74
+ ruleNameDetails.push({
75
+ name: ruleName,
76
+ offset: rulesStringOffset + ruleOffset,
77
+ length: ruleName.length
78
+ })
79
+
80
+ currentOffset = ruleOffset + ruleName.length
81
+ }
82
+
83
+ return {
84
+ match: trimmed,
85
+ ruleNames,
86
+ ruleNameDetails,
87
+ rulesString
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Parse a herb:disable comment from a full source line.
93
+ * Use this when you have a complete line that may contain <%# herb:disable ... %>
94
+ *
95
+ * @param line - The source line that may contain a herb:disable comment
96
+ * @returns Parsed comment data or null if not a valid herb:disable comment
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * const result = parseHerbDisableLine("<div>test</div> <%# herb:disable rule1, rule2 %>")
101
+ * // { match: "<%# herb:disable rule1, rule2 %>", ruleNames: ["rule1", "rule2"], rulesString: "rule1, rule2" }
102
+ * ```
103
+ */
104
+ export function parseHerbDisableLine(line: string): HerbDisableComment | null {
105
+ const startTag = "<%#"
106
+ const endTag = "%>"
107
+
108
+ const startIndex = line.indexOf(startTag)
109
+ if (startIndex === -1) return null
110
+
111
+ const endIndex = line.indexOf(endTag, startIndex)
112
+ if (endIndex === -1) return null
113
+
114
+ const content = line.substring(startIndex + startTag.length, endIndex).trim()
115
+
116
+ if (!content.startsWith(HERB_DISABLE_PREFIX)) return null
117
+
118
+ const afterPrefix = content.substring(HERB_DISABLE_PREFIX.length).trimStart()
119
+ if (afterPrefix.length === 0) return null
120
+
121
+ const rulesString = afterPrefix.trimEnd()
122
+ const ruleNames = rulesString.split(',').map(name => name.trim())
123
+
124
+ if (ruleNames.some(name => name.length === 0)) return null
125
+ if (ruleNames.length === 0) return null
126
+
127
+ const herbDisablePrefix = line.indexOf(HERB_DISABLE_PREFIX)
128
+ const searchStart = herbDisablePrefix + HERB_DISABLE_PREFIX.length
129
+ const rulesStringOffset = line.indexOf(rulesString, searchStart)
130
+
131
+ const ruleNameDetails: HerbDisableRuleName[] = []
132
+
133
+ let currentOffset = 0
134
+
135
+ for (const ruleName of ruleNames) {
136
+ const ruleOffset = rulesString.indexOf(ruleName, currentOffset)
137
+
138
+ ruleNameDetails.push({
139
+ name: ruleName,
140
+ offset: rulesStringOffset + ruleOffset,
141
+ length: ruleName.length
142
+ })
143
+
144
+ currentOffset = ruleOffset + ruleName.length
145
+ }
146
+
147
+ const fullMatch = line.substring(startIndex, endIndex + endTag.length)
148
+
149
+ return {
150
+ match: fullMatch,
151
+ ruleNames,
152
+ ruleNameDetails,
153
+ rulesString
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Check if an ERB comment content contains a herb:disable directive.
159
+ *
160
+ * @param content - The content string (without <%# %> delimiters)
161
+ * @returns true if the content contains a herb:disable directive
162
+ */
163
+ export function isHerbDisableContent(content: string): boolean {
164
+ return parseHerbDisableContent(content) !== null
165
+ }
166
+
167
+ /**
168
+ * Check if a source line contains a herb:disable comment.
169
+ *
170
+ * @param line - The source line
171
+ * @returns true if the line contains a herb:disable comment
172
+ */
173
+ export function isHerbDisableLine(line: string): boolean {
174
+ return parseHerbDisableLine(line) !== null
175
+ }