@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
@@ -1,24 +1,24 @@
1
1
  import {
2
2
  Visitor,
3
3
  Location,
4
- getStaticAttributeName,
5
- hasDynamicAttributeName as hasNodeDynamicAttributeName,
6
- getCombinedAttributeName,
4
+ Position,
7
5
  hasERBOutput,
8
- getStaticContentFromNodes,
9
- hasStaticContent,
10
6
  isEffectivelyStatic,
11
- getValidatableStaticContent
7
+ getValidatableStaticContent,
8
+ getAttributeName,
9
+ getStaticAttributeValue,
10
+ hasDynamicAttributeNameOnAttribute as hasDynamicAttributeName,
11
+ getCombinedAttributeNameString,
12
+ getAttributeValueNodes,
13
+ getAttributeValue,
14
+ forEachAttribute,
12
15
  } from "@herb-tools/core"
13
16
 
14
17
  import type {
15
- ERBContentNode,
16
18
  HTMLAttributeNameNode,
17
19
  HTMLAttributeNode,
18
- HTMLAttributeValueNode,
19
20
  HTMLElementNode,
20
21
  HTMLOpenTagNode,
21
- LiteralNode,
22
22
  LexResult,
23
23
  Token,
24
24
  Node
@@ -27,7 +27,7 @@ import type {
27
27
  import { DEFAULT_LINT_CONTEXT } from "../types.js"
28
28
 
29
29
  import type * as Nodes from "@herb-tools/core"
30
- import type { UnboundLintOffense, LintContext, BaseAutofixContext } from "../types.js"
30
+ import type { UnboundLintOffense, LintContext, LintSeverity, BaseAutofixContext } from "../types.js"
31
31
 
32
32
  export enum ControlFlowType {
33
33
  CONDITIONAL,
@@ -53,7 +53,7 @@ export abstract class BaseRuleVisitor<TAutofixContext extends BaseAutofixContext
53
53
  * Helper method to create an unbound lint offense (without severity).
54
54
  * The Linter will bind severity based on the rule's config.
55
55
  */
56
- protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext): UnboundLintOffense<TAutofixContext> {
56
+ protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity): UnboundLintOffense<TAutofixContext> {
57
57
  return {
58
58
  rule: this.ruleName,
59
59
  code: this.ruleName,
@@ -61,14 +61,15 @@ export abstract class BaseRuleVisitor<TAutofixContext extends BaseAutofixContext
61
61
  message,
62
62
  location,
63
63
  autofixContext,
64
+ severity,
64
65
  }
65
66
  }
66
67
 
67
68
  /**
68
69
  * Helper method to add an offense to the offenses array
69
70
  */
70
- protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext): void {
71
- this.offenses.push(this.createOffense(message, location, autofixContext))
71
+ protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity): void {
72
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity))
72
73
  }
73
74
  }
74
75
 
@@ -162,236 +163,6 @@ export abstract class ControlFlowTrackingVisitor<TAutofixContext extends BaseAut
162
163
  protected abstract onExitBranch(stateToRestore: TBranchState): void
163
164
  }
164
165
 
165
- /**
166
- * Gets attributes from an HTMLOpenTagNode
167
- */
168
- export function getAttributes(node: HTMLOpenTagNode): HTMLAttributeNode[] {
169
- return node.children.filter(node => node.type === "AST_HTML_ATTRIBUTE_NODE") as HTMLAttributeNode[]
170
- }
171
-
172
- /**
173
- * Gets the tag name from an HTML tag node (lowercased)
174
- */
175
- export function getTagName(node: HTMLElementNode | HTMLOpenTagNode | null | undefined): string | null {
176
- if (!node) return null
177
-
178
- return node.tag_name?.value.toLowerCase() || null
179
- }
180
-
181
- /**
182
- * Gets the attribute name from an HTMLAttributeNode (lowercased)
183
- * Returns null if the attribute name contains dynamic content (ERB)
184
- */
185
- export function getAttributeName(attributeNode: HTMLAttributeNode, lowercase = true): string | null {
186
- if (attributeNode.name?.type === "AST_HTML_ATTRIBUTE_NAME_NODE") {
187
- const nameNode = attributeNode.name as HTMLAttributeNameNode
188
- const staticName = getStaticAttributeName(nameNode)
189
-
190
- if (!lowercase) return staticName
191
-
192
- return staticName ? staticName.toLowerCase() : null
193
- }
194
-
195
- return null
196
- }
197
-
198
- /**
199
- * Checks if an attribute has a dynamic (ERB-containing) name
200
- */
201
- export function hasDynamicAttributeName(attributeNode: HTMLAttributeNode): boolean {
202
- if (attributeNode.name?.type === "AST_HTML_ATTRIBUTE_NAME_NODE") {
203
- const nameNode = attributeNode.name as HTMLAttributeNameNode
204
- return hasNodeDynamicAttributeName(nameNode)
205
- }
206
-
207
- return false
208
- }
209
-
210
- /**
211
- * Gets the combined string representation of an attribute name (for debugging)
212
- * This includes both static content and ERB syntax
213
- */
214
- export function getCombinedAttributeNameString(attributeNode: HTMLAttributeNode): string {
215
- if (attributeNode.name?.type === "AST_HTML_ATTRIBUTE_NAME_NODE") {
216
- const nameNode = attributeNode.name as HTMLAttributeNameNode
217
-
218
- return getCombinedAttributeName(nameNode)
219
- }
220
-
221
- return ""
222
- }
223
-
224
- /**
225
- * Checks if an attribute value contains only static content (no ERB)
226
- */
227
- export function hasStaticAttributeValue(attributeNode: HTMLAttributeNode): boolean {
228
- const valueNode = attributeNode.value as HTMLAttributeValueNode | null
229
-
230
- if (!valueNode?.children) return false
231
-
232
- return valueNode.children.every(child => child.type === "AST_LITERAL_NODE")
233
- }
234
-
235
- /**
236
- * Checks if an attribute value contains dynamic content (ERB)
237
- */
238
- export function hasDynamicAttributeValue(attributeNode: HTMLAttributeNode): boolean {
239
- const valueNode = attributeNode.value as HTMLAttributeValueNode | null
240
-
241
- if (!valueNode?.children) return false
242
-
243
- return valueNode.children.some(child => child.type === "AST_ERB_CONTENT_NODE")
244
- }
245
-
246
- /**
247
- * Gets the static string value of an attribute (returns null if it contains ERB)
248
- */
249
- export function getStaticAttributeValue(attributeNode: HTMLAttributeNode): string | null {
250
- if (!hasStaticAttributeValue(attributeNode)) return null
251
-
252
- const valueNode = attributeNode.value as HTMLAttributeValueNode
253
-
254
- const result = valueNode.children
255
- ?.filter(child => child.type === "AST_LITERAL_NODE")
256
- .map(child => (child as LiteralNode).content)
257
- .join("") || ""
258
-
259
- return result
260
- }
261
-
262
- /**
263
- * Gets the value nodes array for dynamic inspection
264
- */
265
- export function getAttributeValueNodes(attributeNode: HTMLAttributeNode): Node[] {
266
- const valueNode = attributeNode.value as HTMLAttributeValueNode | null
267
-
268
- return valueNode?.children || []
269
- }
270
-
271
- /**
272
- * Checks if an attribute value contains any static content (for validation purposes)
273
- */
274
- export function hasStaticAttributeValueContent(attributeNode: HTMLAttributeNode): boolean {
275
- const valueNodes = getAttributeValueNodes(attributeNode)
276
-
277
- return hasStaticContent(valueNodes)
278
- }
279
-
280
- /**
281
- * Gets the static content of an attribute value (all literal parts combined)
282
- * Returns the concatenated literal content, or null if no literal nodes exist
283
- */
284
- export function getStaticAttributeValueContent(attributeNode: HTMLAttributeNode): string | null {
285
- const valueNodes = getAttributeValueNodes(attributeNode)
286
-
287
- return getStaticContentFromNodes(valueNodes)
288
- }
289
-
290
- /**
291
- * Gets the attribute value content from an HTMLAttributeValueNode
292
- */
293
- export function getAttributeValue(attributeNode: HTMLAttributeNode): string | null {
294
- const valueNode: HTMLAttributeValueNode | null = attributeNode.value as HTMLAttributeValueNode
295
-
296
- if (valueNode === null) return null
297
-
298
- if (valueNode.type !== "AST_HTML_ATTRIBUTE_VALUE_NODE" || !valueNode.children?.length) {
299
- return null
300
- }
301
-
302
- let result = ""
303
-
304
- for (const child of valueNode.children) {
305
- switch (child.type) {
306
- case "AST_ERB_CONTENT_NODE": {
307
- const erbNode = child as ERBContentNode
308
-
309
- if (erbNode.content) {
310
- result += `${erbNode.tag_opening?.value}${erbNode.content.value}${erbNode.tag_closing?.value}`
311
- }
312
-
313
- break
314
- }
315
-
316
- case "AST_LITERAL_NODE": {
317
- result += (child as LiteralNode).content
318
- break
319
- }
320
- }
321
- }
322
-
323
- return result
324
- }
325
-
326
- /**
327
- * Checks if an attribute has a value
328
- */
329
- export function hasAttributeValue(attributeNode: HTMLAttributeNode): boolean {
330
- return attributeNode.value?.type === "AST_HTML_ATTRIBUTE_VALUE_NODE"
331
- }
332
-
333
- /**
334
- * Gets the quote type used for an attribute value
335
- */
336
- export function getAttributeValueQuoteType(nodeOrAttribute: HTMLAttributeNode | HTMLAttributeValueNode): "single" | "double" | "none" | null {
337
- let valueNode: HTMLAttributeValueNode | undefined
338
-
339
- if (nodeOrAttribute.type === "AST_HTML_ATTRIBUTE_NODE") {
340
- const attributeNode = nodeOrAttribute as HTMLAttributeNode
341
-
342
- if (attributeNode.value?.type === "AST_HTML_ATTRIBUTE_VALUE_NODE") {
343
- valueNode = attributeNode.value as HTMLAttributeValueNode
344
- }
345
- } else if (nodeOrAttribute.type === "AST_HTML_ATTRIBUTE_VALUE_NODE") {
346
- valueNode = nodeOrAttribute as HTMLAttributeValueNode
347
- }
348
-
349
- if (valueNode) {
350
- if (valueNode.quoted && valueNode.open_quote) {
351
- return valueNode.open_quote.value === '"' ? "double" : "single"
352
- }
353
-
354
- return "none"
355
- }
356
-
357
- return null
358
- }
359
-
360
- /**
361
- * Finds an attribute by name in a list of attributes
362
- */
363
- export function findAttributeByName(attributes: Node[], attributeName: string): HTMLAttributeNode | null {
364
- for (const child of attributes) {
365
- if (child.type === "AST_HTML_ATTRIBUTE_NODE") {
366
- const attributeNode = child as HTMLAttributeNode
367
- const name = getAttributeName(attributeNode)
368
-
369
- if (name === attributeName.toLowerCase()) {
370
- return attributeNode
371
- }
372
- }
373
- }
374
-
375
- return null
376
- }
377
-
378
- /**
379
- * Checks if a tag has a specific attribute
380
- */
381
- export function hasAttribute(node: HTMLOpenTagNode | null | undefined, attributeName: string): boolean {
382
- if (!node) return false
383
-
384
- return getAttribute(node, attributeName) !== null
385
- }
386
-
387
- /**
388
- * Checks if a tag has a specific attribute
389
- */
390
- export function getAttribute(node: HTMLOpenTagNode, attributeName: string): HTMLAttributeNode | null {
391
- const attributes = getAttributes(node)
392
-
393
- return findAttributeByName(attributes, attributeName)
394
- }
395
166
 
396
167
  /**
397
168
  * Common HTML element categorization
@@ -485,6 +256,26 @@ export const VALID_ARIA_ROLES = new Set([
485
256
  "log", "marquee"
486
257
  ]);
487
258
 
259
+ /**
260
+ * Abstract ARIA roles used to support the WAI-ARIA Roles Model.
261
+ * Authors MUST NOT use abstract roles in content.
262
+ * @see https://www.w3.org/TR/wai-aria-1.0/roles#abstract_roles
263
+ */
264
+ export const ABSTRACT_ARIA_ROLES = new Set([
265
+ "command",
266
+ "composite",
267
+ "input",
268
+ "landmark",
269
+ "range",
270
+ "roletype",
271
+ "section",
272
+ "sectionhead",
273
+ "select",
274
+ "structure",
275
+ "widget",
276
+ "window"
277
+ ]);
278
+
488
279
  /**
489
280
  * Parameter types for AttributeVisitorMixin methods
490
281
  */
@@ -638,7 +429,7 @@ export abstract class AttributeVisitorMixin<TAutofixContext extends BaseAutofixC
638
429
  private checkAttributesOnNode(node: HTMLOpenTagNode): void {
639
430
  forEachAttribute(node, (attributeNode) => {
640
431
  const staticAttributeName = getAttributeName(attributeNode)
641
- const originalAttributeName = getAttributeName(attributeNode, false) || ""
432
+ const originalAttributeName = getAttributeName(attributeNode, false) || ""
642
433
  const isDynamicName = hasDynamicAttributeName(attributeNode)
643
434
  const staticAttributeValue = getStaticAttributeValue(attributeNode)
644
435
  const valueNodes = getAttributeValueNodes(attributeNode)
@@ -707,35 +498,6 @@ export abstract class AttributeVisitorMixin<TAutofixContext extends BaseAutofixC
707
498
  }
708
499
  }
709
500
 
710
- /**
711
- * Checks if an attribute value is quoted
712
- */
713
- export function isAttributeValueQuoted(attributeNode: HTMLAttributeNode): boolean {
714
- if (attributeNode.value?.type === "AST_HTML_ATTRIBUTE_VALUE_NODE") {
715
- const valueNode = attributeNode.value as HTMLAttributeValueNode
716
-
717
- return !!valueNode.quoted
718
- }
719
-
720
- return false
721
- }
722
-
723
- /**
724
- * Iterates over all attributes of a tag node, calling the callback for each attribute
725
- */
726
- export function forEachAttribute(
727
- node: HTMLOpenTagNode,
728
- callback: (attributeNode: HTMLAttributeNode) => void
729
- ): void {
730
- const attributes = getAttributes(node)
731
-
732
- for (const child of attributes) {
733
- if (child.type === "AST_HTML_ATTRIBUTE_NODE") {
734
- callback(child as HTMLAttributeNode)
735
- }
736
- }
737
- }
738
-
739
501
  /**
740
502
  * Base lexer visitor class that provides common functionality for lexer-based rule visitors
741
503
  */
@@ -753,7 +515,7 @@ export abstract class BaseLexerRuleVisitor<TAutofixContext extends BaseAutofixCo
753
515
  * Helper method to create an unbound lint offense (without severity).
754
516
  * The Linter will bind severity based on the rule's config.
755
517
  */
756
- protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext): UnboundLintOffense<TAutofixContext> {
518
+ protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity): UnboundLintOffense<TAutofixContext> {
757
519
  return {
758
520
  rule: this.ruleName,
759
521
  code: this.ruleName,
@@ -761,14 +523,15 @@ export abstract class BaseLexerRuleVisitor<TAutofixContext extends BaseAutofixCo
761
523
  message,
762
524
  location,
763
525
  autofixContext,
526
+ severity,
764
527
  }
765
528
  }
766
529
 
767
530
  /**
768
531
  * Helper method to add an offense to the offenses array
769
532
  */
770
- protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext): void {
771
- this.offenses.push(this.createOffense(message, location, autofixContext))
533
+ protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity): void {
534
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity))
772
535
  }
773
536
 
774
537
  /**
@@ -815,7 +578,7 @@ export abstract class BaseSourceRuleVisitor<TAutofixContext extends BaseAutofixC
815
578
  * Helper method to create an unbound lint offense (without severity).
816
579
  * The Linter will bind severity based on the rule's config.
817
580
  */
818
- protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext): UnboundLintOffense<TAutofixContext> {
581
+ protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity): UnboundLintOffense<TAutofixContext> {
819
582
  return {
820
583
  rule: this.ruleName,
821
584
  code: this.ruleName,
@@ -823,14 +586,15 @@ export abstract class BaseSourceRuleVisitor<TAutofixContext extends BaseAutofixC
823
586
  message,
824
587
  location,
825
588
  autofixContext,
589
+ severity,
826
590
  }
827
591
  }
828
592
 
829
593
  /**
830
594
  * Helper method to add an offense to the offenses array
831
595
  */
832
- protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext): void {
833
- this.offenses.push(this.createOffense(message, location, autofixContext))
596
+ protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity): void {
597
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity))
834
598
  }
835
599
 
836
600
  /**
@@ -1065,3 +829,122 @@ export function isHeadTag(tagName: string): boolean {
1065
829
  (isHeadOnlyTag(tag) || isHeadAndBodyTag(tag))
1066
830
  )
1067
831
  }
832
+
833
+ /**
834
+ * Converts a character offset in a source string to a Position (line, column).
835
+ * Lines are 1-based, columns are 0-based.
836
+ */
837
+ export function positionFromOffset(source: string, offset: number): Position {
838
+ let line = 1
839
+ let column = 0
840
+ let currentOffset = 0
841
+
842
+ for (let i = 0; i < source.length && currentOffset < offset; i++) {
843
+ const char = source[i]
844
+ currentOffset++
845
+ if (char === "\n") {
846
+ line++
847
+ column = 0
848
+ } else {
849
+ column++
850
+ }
851
+ }
852
+
853
+ return new Position(line, column)
854
+ }
855
+
856
+ /**
857
+ * Creates a Location from a source string, a start offset, and a length.
858
+ */
859
+ export function locationFromOffset(source: string, startOffset: number, length: number): Location {
860
+ const start = positionFromOffset(source, startOffset)
861
+ const end = positionFromOffset(source, startOffset + length)
862
+ return Location.from(start.line, start.column, end.line, end.column)
863
+ }
864
+
865
+ /**
866
+ * Checks if a position (line, column) is within a node's location range.
867
+ * @param node - The node to check
868
+ * @param line - Line number (1-based)
869
+ * @param column - Column number (0-based)
870
+ * @returns true if the position is within the node's location
871
+ */
872
+ function isPositionInNode(node: Node, line: number, column: number): boolean {
873
+ if (!node.location) return false
874
+
875
+ const { start, end } = node.location
876
+
877
+ if (line < start.line) return false
878
+ if (line === start.line && column < start.column) return false
879
+
880
+ if (line > end.line) return false
881
+ if (line === end.line && column >= end.column) return false
882
+
883
+ return true
884
+ }
885
+
886
+ /**
887
+ * Finds a node in the AST that contains a specific position.
888
+ * Returns the deepest (most specific) node that matches the position and optional predicate.
889
+ *
890
+ * @param root - The root node to search from
891
+ * @param line - Line number (1-based)
892
+ * @param column - Column number (0-based)
893
+ * @param predicate - Optional predicate function to filter nodes
894
+ * @returns The matching node or null if not found
895
+ */
896
+ export function findNodeAtPosition(root: Node, line: number, column: number, predicate?: (node: Node) => boolean): Node | null {
897
+ let bestMatch: Node | null = null
898
+ const visited = new Set<Node>()
899
+
900
+ function search(node: Node): void {
901
+ if (!node || visited.has(node)) return
902
+ visited.add(node)
903
+
904
+ if (isPositionInNode(node, line, column)) {
905
+ if (!predicate || predicate(node)) {
906
+ if (!bestMatch || isMoreSpecific(node, bestMatch)) {
907
+ bestMatch = node
908
+ }
909
+ }
910
+ }
911
+
912
+ const nodeAny = node as any
913
+
914
+ if (typeof nodeAny.compactChildNodes === 'function') {
915
+ for (const child of nodeAny.compactChildNodes()) {
916
+ search(child)
917
+ }
918
+ } else {
919
+ if (nodeAny.children && Array.isArray(nodeAny.children)) {
920
+ for (const child of nodeAny.children) {
921
+ if (child) search(child)
922
+ }
923
+ }
924
+
925
+ if (nodeAny.body && Array.isArray(nodeAny.body)) {
926
+ for (const child of nodeAny.body) {
927
+ if (child) search(child)
928
+ }
929
+ }
930
+ }
931
+ }
932
+
933
+ function isMoreSpecific(nodeA: Node, nodeB: Node): boolean {
934
+ if (!nodeA.location || !nodeB.location) return false
935
+
936
+ const aStart = nodeA.location.start
937
+ const aEnd = nodeA.location.end
938
+ const bStart = nodeB.location.start
939
+ const bEnd = nodeB.location.end
940
+
941
+ const startsAtOrAfter = aStart.line > bStart.line || (aStart.line === bStart.line && aStart.column >= bStart.column)
942
+ const endsAtOrBefore = aEnd.line < bEnd.line || (aEnd.line === bEnd.line && aEnd.column <= bEnd.column)
943
+
944
+ return startsAtOrAfter && endsAtOrBefore
945
+ }
946
+
947
+ search(root)
948
+
949
+ return bestMatch
950
+ }
@@ -1,5 +1,6 @@
1
1
  import { ParserRule } from "../types.js"
2
2
  import { BaseRuleVisitor, SVG_CAMEL_CASE_ELEMENTS, SVG_LOWERCASE_TO_CAMELCASE } from "./rule-utils.js"
3
+ import { isHTMLOpenTagNode, isHTMLCloseTagNode } from "@herb-tools/core"
3
4
 
4
5
  import type { UnboundLintOffense, LintOffense, LintContext, BaseAutofixContext, Mutable, FullRuleConfig } from "../types.js"
5
6
  import type { HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, ParseResult } from "@herb-tools/core"
@@ -25,12 +26,12 @@ class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor<SVGTagNameCapitali
25
26
  }
26
27
 
27
28
  if (this.insideSVG) {
28
- if (node.open_tag) {
29
- this.checkTagName(node.open_tag as HTMLOpenTagNode)
29
+ if (isHTMLOpenTagNode(node.open_tag)) {
30
+ this.checkTagName(node.open_tag)
30
31
  }
31
32
 
32
- if (node.close_tag) {
33
- this.checkTagName(node.close_tag as HTMLCloseTagNode)
33
+ if (node.close_tag && isHTMLCloseTagNode(node.close_tag)) {
34
+ this.checkTagName(node.close_tag)
34
35
  }
35
36
  }
36
37
 
@@ -40,7 +41,6 @@ class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor<SVGTagNameCapitali
40
41
 
41
42
  private checkTagName(node: HTMLOpenTagNode | HTMLCloseTagNode): void {
42
43
  const tagName = node.tag_name?.value
43
-
44
44
  if (!tagName) return
45
45
 
46
46
  if (SVG_CAMEL_CASE_ELEMENTS.has(tagName)) return
@@ -51,8 +51,8 @@ class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor<SVGTagNameCapitali
51
51
  if (correctCamelCase && tagName !== correctCamelCase) {
52
52
  let type: string = node.type
53
53
 
54
- if (node.type === "AST_HTML_OPEN_TAG_NODE") type = "Opening"
55
- if (node.type === "AST_HTML_CLOSE_TAG_NODE") type = "Closing"
54
+ if (isHTMLOpenTagNode(node)) type = "Opening"
55
+ if (isHTMLCloseTagNode(node)) type = "Closing"
56
56
 
57
57
  this.addOffense(
58
58
  `${type} SVG tag name \`${tagName}\` should use proper capitalization. Use \`${correctCamelCase}\` instead.`,
@@ -69,7 +69,7 @@ class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor<SVGTagNameCapitali
69
69
 
70
70
  export class SVGTagNameCapitalizationRule extends ParserRule<SVGTagNameCapitalizationAutofixContext> {
71
71
  static autocorrectable = true
72
- name = "svg-tag-name-capitalization"
72
+ static ruleName = "svg-tag-name-capitalization"
73
73
 
74
74
  get defaultConfig(): FullRuleConfig {
75
75
  return {
@@ -79,7 +79,7 @@ export class SVGTagNameCapitalizationRule extends ParserRule<SVGTagNameCapitaliz
79
79
  }
80
80
 
81
81
  check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<SVGTagNameCapitalizationAutofixContext>[] {
82
- const visitor = new SVGTagNameCapitalizationVisitor(this.name, context)
82
+ const visitor = new SVGTagNameCapitalizationVisitor(this.ruleName, context)
83
83
 
84
84
  visitor.visit(result.value)
85
85
 
@@ -89,7 +89,7 @@ export class SVGTagNameCapitalizationRule extends ParserRule<SVGTagNameCapitaliz
89
89
  autofix(offense: LintOffense<SVGTagNameCapitalizationAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
90
90
  if (!offense.autofixContext) return null
91
91
 
92
- const { node: { tag_name }, correctCamelCase } = offense.autofixContext
92
+ const { node: { tag_name }, correctCamelCase } = offense.autofixContext
93
93
 
94
94
  if (!tag_name) return null
95
95
 
@@ -0,0 +1,49 @@
1
+ import { BaseRuleVisitor } from "./rule-utils.js"
2
+ import { getAttribute } from "@herb-tools/core"
3
+
4
+ import { ParserRule } from "../types.js"
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
7
+
8
+ class TurboPermanentRequireIdVisitor extends BaseRuleVisitor {
9
+ visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
10
+ this.checkTurboPermanent(node)
11
+ super.visitHTMLOpenTagNode(node)
12
+ }
13
+
14
+ private checkTurboPermanent(node: HTMLOpenTagNode): void {
15
+ const turboPermanentAttribute = getAttribute(node, "data-turbo-permanent")
16
+
17
+ if (!turboPermanentAttribute) {
18
+ return
19
+ }
20
+
21
+ const idAttribute = getAttribute(node, "id")
22
+
23
+ if (!idAttribute) {
24
+ this.addOffense(
25
+ "Elements with `data-turbo-permanent` must 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.",
26
+ turboPermanentAttribute.location,
27
+ )
28
+ }
29
+ }
30
+ }
31
+
32
+ export class TurboPermanentRequireIdRule extends ParserRule {
33
+ static ruleName = "turbo-permanent-require-id"
34
+
35
+ get defaultConfig(): FullRuleConfig {
36
+ return {
37
+ enabled: true,
38
+ severity: "error"
39
+ }
40
+ }
41
+
42
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
43
+ const visitor = new TurboPermanentRequireIdVisitor(this.ruleName, context)
44
+
45
+ visitor.visit(result.value)
46
+
47
+ return visitor.offenses
48
+ }
49
+ }