@herb-tools/linter 0.4.3 → 0.6.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 (254) hide show
  1. package/README.md +216 -19
  2. package/dist/herb-lint.js +5559 -1860
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +722 -187
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +714 -189
  7. package/dist/index.js.map +1 -1
  8. package/dist/package.json +4 -4
  9. package/dist/src/cli/argument-parser.js +28 -22
  10. package/dist/src/cli/argument-parser.js.map +1 -1
  11. package/dist/src/cli/file-processor.js +19 -13
  12. package/dist/src/cli/file-processor.js.map +1 -1
  13. package/dist/src/cli/formatters/detailed-formatter.js +9 -9
  14. package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
  15. package/dist/src/cli/formatters/github-actions-formatter.js +50 -0
  16. package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -0
  17. package/dist/src/cli/formatters/index.js +2 -0
  18. package/dist/src/cli/formatters/index.js.map +1 -1
  19. package/dist/src/cli/formatters/json-formatter.js +58 -0
  20. package/dist/src/cli/formatters/json-formatter.js.map +1 -0
  21. package/dist/src/cli/formatters/simple-formatter.js +15 -15
  22. package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
  23. package/dist/src/cli/output-manager.js +120 -0
  24. package/dist/src/cli/output-manager.js.map +1 -0
  25. package/dist/src/cli/summary-reporter.js +22 -22
  26. package/dist/src/cli/summary-reporter.js.map +1 -1
  27. package/dist/src/cli.js +41 -26
  28. package/dist/src/cli.js.map +1 -1
  29. package/dist/src/default-rules.js +22 -0
  30. package/dist/src/default-rules.js.map +1 -1
  31. package/dist/src/linter.js +29 -4
  32. package/dist/src/linter.js.map +1 -1
  33. package/dist/src/rules/erb-no-empty-tags.js +2 -2
  34. package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
  35. package/dist/src/rules/erb-no-output-control-flow.js +2 -2
  36. package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
  37. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js +26 -0
  38. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -0
  39. package/dist/src/rules/erb-prefer-image-tag-helper.js +2 -6
  40. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
  41. package/dist/src/rules/erb-require-whitespace-inside-tags.js +2 -2
  42. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  43. package/dist/src/rules/erb-requires-trailing-newline.js.map +1 -1
  44. package/dist/src/rules/html-anchor-require-href.js +2 -2
  45. package/dist/src/rules/html-anchor-require-href.js.map +1 -1
  46. package/dist/src/rules/html-aria-attribute-must-be-valid.js +13 -12
  47. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
  48. package/dist/src/rules/html-aria-label-is-well-formatted.js +33 -0
  49. package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -0
  50. package/dist/src/rules/html-aria-level-must-be-valid.js +28 -6
  51. package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
  52. package/dist/src/rules/html-aria-role-heading-requires-level.js +9 -15
  53. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
  54. package/dist/src/rules/html-aria-role-must-be-valid.js +5 -5
  55. package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
  56. package/dist/src/rules/html-attribute-double-quotes.js +16 -6
  57. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
  58. package/dist/src/rules/html-attribute-equals-spacing.js +24 -0
  59. package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -0
  60. package/dist/src/rules/html-attribute-values-require-quotes.js +21 -10
  61. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
  62. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js +47 -0
  63. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -0
  64. package/dist/src/rules/html-boolean-attributes-no-value.js +11 -4
  65. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  66. package/dist/src/rules/html-iframe-has-title.js +39 -0
  67. package/dist/src/rules/html-iframe-has-title.js.map +1 -0
  68. package/dist/src/rules/html-img-require-alt.js +2 -6
  69. package/dist/src/rules/html-img-require-alt.js.map +1 -1
  70. package/dist/src/rules/html-navigation-has-label.js +43 -0
  71. package/dist/src/rules/html-navigation-has-label.js.map +1 -0
  72. package/dist/src/rules/html-no-aria-hidden-on-focusable.js +67 -0
  73. package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -0
  74. package/dist/src/rules/html-no-block-inside-inline.js +4 -4
  75. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
  76. package/dist/src/rules/html-no-duplicate-attributes.js +24 -27
  77. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
  78. package/dist/src/rules/html-no-duplicate-ids.js +4 -4
  79. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
  80. package/dist/src/rules/html-no-empty-headings.js +2 -23
  81. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  82. package/dist/src/rules/html-no-nested-links.js +2 -2
  83. package/dist/src/rules/html-no-nested-links.js.map +1 -1
  84. package/dist/src/rules/html-no-positive-tab-index.js +21 -0
  85. package/dist/src/rules/html-no-positive-tab-index.js.map +1 -0
  86. package/dist/src/rules/html-no-self-closing.js +22 -0
  87. package/dist/src/rules/html-no-self-closing.js.map +1 -0
  88. package/dist/src/rules/html-no-title-attribute.js +27 -0
  89. package/dist/src/rules/html-no-title-attribute.js.map +1 -0
  90. package/dist/src/rules/html-tag-name-lowercase.js +37 -25
  91. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  92. package/dist/src/rules/index.js +10 -0
  93. package/dist/src/rules/index.js.map +1 -1
  94. package/dist/src/rules/parser-no-errors.js +18 -0
  95. package/dist/src/rules/parser-no-errors.js.map +1 -0
  96. package/dist/src/rules/rule-utils.js +176 -22
  97. package/dist/src/rules/rule-utils.js.map +1 -1
  98. package/dist/src/rules/svg-tag-name-capitalization.js +2 -10
  99. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
  100. package/dist/src/types.js.map +1 -1
  101. package/dist/tsconfig.tsbuildinfo +1 -1
  102. package/dist/types/cli/argument-parser.d.ts +2 -1
  103. package/dist/types/cli/file-processor.d.ts +6 -5
  104. package/dist/types/cli/formatters/base-formatter.d.ts +2 -2
  105. package/dist/types/cli/formatters/detailed-formatter.d.ts +2 -2
  106. package/dist/types/cli/formatters/github-actions-formatter.d.ts +12 -0
  107. package/dist/types/cli/formatters/index.d.ts +2 -0
  108. package/dist/types/cli/formatters/json-formatter.d.ts +42 -0
  109. package/dist/types/cli/formatters/simple-formatter.d.ts +2 -2
  110. package/dist/types/cli/index.d.ts +4 -0
  111. package/dist/types/cli/output-manager.d.ts +31 -0
  112. package/dist/types/cli/summary-reporter.d.ts +3 -3
  113. package/dist/types/cli.d.ts +3 -1
  114. package/dist/types/rules/erb-no-empty-tags.d.ts +2 -2
  115. package/dist/types/rules/erb-no-output-control-flow.d.ts +2 -2
  116. package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
  117. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +2 -2
  118. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
  119. package/dist/types/rules/html-anchor-require-href.d.ts +2 -2
  120. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
  121. package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +7 -0
  122. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +2 -2
  123. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +2 -2
  124. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +2 -2
  125. package/dist/types/rules/html-attribute-double-quotes.d.ts +2 -2
  126. package/dist/types/rules/html-attribute-equals-spacing.d.ts +7 -0
  127. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +2 -2
  128. package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
  129. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +2 -2
  130. package/dist/types/rules/html-iframe-has-title.d.ts +7 -0
  131. package/dist/types/rules/html-img-require-alt.d.ts +2 -2
  132. package/dist/types/rules/html-navigation-has-label.d.ts +7 -0
  133. package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
  134. package/dist/types/rules/html-no-block-inside-inline.d.ts +2 -2
  135. package/dist/types/rules/html-no-duplicate-attributes.d.ts +2 -2
  136. package/dist/types/rules/html-no-duplicate-ids.d.ts +2 -2
  137. package/dist/types/rules/html-no-empty-headings.d.ts +2 -2
  138. package/dist/types/rules/html-no-nested-links.d.ts +2 -2
  139. package/dist/types/rules/html-no-positive-tab-index.d.ts +7 -0
  140. package/dist/types/rules/html-no-self-closing.d.ts +7 -0
  141. package/dist/types/rules/html-no-title-attribute.d.ts +7 -0
  142. package/dist/types/rules/html-tag-name-lowercase.d.ts +3 -2
  143. package/dist/types/rules/index.d.ts +10 -0
  144. package/dist/types/rules/parser-no-errors.d.ts +8 -0
  145. package/dist/types/rules/rule-utils.d.ts +107 -13
  146. package/dist/types/rules/svg-tag-name-capitalization.d.ts +2 -2
  147. package/dist/types/src/cli/argument-parser.d.ts +2 -1
  148. package/dist/types/src/cli/file-processor.d.ts +6 -5
  149. package/dist/types/src/cli/formatters/base-formatter.d.ts +2 -2
  150. package/dist/types/src/cli/formatters/detailed-formatter.d.ts +2 -2
  151. package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +12 -0
  152. package/dist/types/src/cli/formatters/index.d.ts +2 -0
  153. package/dist/types/src/cli/formatters/json-formatter.d.ts +42 -0
  154. package/dist/types/src/cli/formatters/simple-formatter.d.ts +2 -2
  155. package/dist/types/src/cli/output-manager.d.ts +31 -0
  156. package/dist/types/src/cli/summary-reporter.d.ts +3 -3
  157. package/dist/types/src/cli.d.ts +3 -1
  158. package/dist/types/src/rules/erb-no-empty-tags.d.ts +2 -2
  159. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +2 -2
  160. package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
  161. package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +2 -2
  162. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
  163. package/dist/types/src/rules/html-anchor-require-href.d.ts +2 -2
  164. package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
  165. package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +7 -0
  166. package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +2 -2
  167. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +2 -2
  168. package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +2 -2
  169. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +2 -2
  170. package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +7 -0
  171. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +2 -2
  172. package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
  173. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +2 -2
  174. package/dist/types/src/rules/html-iframe-has-title.d.ts +7 -0
  175. package/dist/types/src/rules/html-img-require-alt.d.ts +2 -2
  176. package/dist/types/src/rules/html-navigation-has-label.d.ts +7 -0
  177. package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
  178. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +2 -2
  179. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +2 -2
  180. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +2 -2
  181. package/dist/types/src/rules/html-no-empty-headings.d.ts +2 -2
  182. package/dist/types/src/rules/html-no-nested-links.d.ts +2 -2
  183. package/dist/types/src/rules/html-no-positive-tab-index.d.ts +7 -0
  184. package/dist/types/src/rules/html-no-self-closing.d.ts +7 -0
  185. package/dist/types/src/rules/html-no-title-attribute.d.ts +7 -0
  186. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +3 -2
  187. package/dist/types/src/rules/index.d.ts +10 -0
  188. package/dist/types/src/rules/parser-no-errors.d.ts +8 -0
  189. package/dist/types/src/rules/rule-utils.d.ts +107 -13
  190. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +2 -2
  191. package/dist/types/src/types.d.ts +27 -3
  192. package/dist/types/types.d.ts +27 -3
  193. package/docs/rules/README.md +13 -2
  194. package/docs/rules/erb-no-silent-tag-in-attribute-name.md +34 -0
  195. package/docs/rules/html-aria-label-is-well-formatted.md +49 -0
  196. package/docs/rules/html-attribute-equals-spacing.md +35 -0
  197. package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +48 -0
  198. package/docs/rules/html-iframe-has-title.md +43 -0
  199. package/docs/rules/html-navigation-has-label.md +61 -0
  200. package/docs/rules/html-no-aria-hidden-on-focusable.md +54 -0
  201. package/docs/rules/html-no-positive-tab-index.md +55 -0
  202. package/docs/rules/html-no-self-closing.md +65 -0
  203. package/docs/rules/html-no-title-attribute.md +69 -0
  204. package/docs/rules/html-tag-name-lowercase.md +16 -3
  205. package/docs/rules/parser-no-errors.md +84 -0
  206. package/package.json +4 -4
  207. package/src/cli/argument-parser.ts +33 -24
  208. package/src/cli/file-processor.ts +25 -17
  209. package/src/cli/formatters/base-formatter.ts +2 -2
  210. package/src/cli/formatters/detailed-formatter.ts +9 -9
  211. package/src/cli/formatters/github-actions-formatter.ts +70 -0
  212. package/src/cli/formatters/index.ts +2 -0
  213. package/src/cli/formatters/json-formatter.ts +107 -0
  214. package/src/cli/formatters/simple-formatter.ts +15 -15
  215. package/src/cli/output-manager.ts +143 -0
  216. package/src/cli/summary-reporter.ts +24 -24
  217. package/src/cli.ts +48 -31
  218. package/src/default-rules.ts +22 -0
  219. package/src/linter.ts +30 -4
  220. package/src/rules/erb-no-empty-tags.ts +3 -3
  221. package/src/rules/erb-no-output-control-flow.ts +3 -3
  222. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +40 -0
  223. package/src/rules/erb-prefer-image-tag-helper.ts +4 -9
  224. package/src/rules/erb-require-whitespace-inside-tags.ts +3 -3
  225. package/src/rules/erb-requires-trailing-newline.ts +2 -0
  226. package/src/rules/html-anchor-require-href.ts +3 -3
  227. package/src/rules/html-aria-attribute-must-be-valid.ts +29 -33
  228. package/src/rules/html-aria-label-is-well-formatted.ts +59 -0
  229. package/src/rules/html-aria-level-must-be-valid.ts +40 -7
  230. package/src/rules/html-aria-role-heading-requires-level.ts +18 -30
  231. package/src/rules/html-aria-role-must-be-valid.ts +7 -7
  232. package/src/rules/html-attribute-double-quotes.ts +23 -8
  233. package/src/rules/html-attribute-equals-spacing.ts +41 -0
  234. package/src/rules/html-attribute-values-require-quotes.ts +32 -12
  235. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +66 -0
  236. package/src/rules/html-boolean-attributes-no-value.ts +19 -6
  237. package/src/rules/html-iframe-has-title.ts +62 -0
  238. package/src/rules/html-img-require-alt.ts +4 -9
  239. package/src/rules/html-navigation-has-label.ts +64 -0
  240. package/src/rules/html-no-aria-hidden-on-focusable.ts +90 -0
  241. package/src/rules/html-no-block-inside-inline.ts +5 -5
  242. package/src/rules/html-no-duplicate-attributes.ts +30 -30
  243. package/src/rules/html-no-duplicate-ids.ts +6 -5
  244. package/src/rules/html-no-empty-headings.ts +4 -33
  245. package/src/rules/html-no-nested-links.ts +3 -3
  246. package/src/rules/html-no-positive-tab-index.ts +33 -0
  247. package/src/rules/html-no-self-closing.ts +36 -0
  248. package/src/rules/html-no-title-attribute.ts +42 -0
  249. package/src/rules/html-tag-name-lowercase.ts +44 -31
  250. package/src/rules/index.ts +10 -0
  251. package/src/rules/parser-no-errors.ts +25 -0
  252. package/src/rules/rule-utils.ts +260 -39
  253. package/src/rules/svg-tag-name-capitalization.ts +4 -11
  254. package/src/types.ts +30 -3
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLNoEmptyHeadingsRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLNoNestedLinksRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -0,0 +1,7 @@
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
3
+ import type { ParseResult } from "@herb-tools/core";
4
+ export declare class HTMLNoPositiveTabIndexRule extends ParserRule {
5
+ name: string;
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
+ }
@@ -0,0 +1,7 @@
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintContext, LintOffense } from "../types.js";
3
+ import type { ParseResult } from "@herb-tools/core";
4
+ export declare class HTMLNoSelfClosingRule extends ParserRule {
5
+ name: string;
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
+ }
@@ -0,0 +1,7 @@
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense, LintContext } from "../types.js";
3
+ import type { ParseResult } from "@herb-tools/core";
4
+ export declare class HTMLNoTitleAttributeRule extends ParserRule {
5
+ name: string;
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
+ }
@@ -1,7 +1,8 @@
1
1
  import { ParserRule } from "../types.js";
2
+ import { ParseResult } from "@herb-tools/core";
2
3
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
4
4
  export declare class HTMLTagNameLowercaseRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ isEnabled(result: ParseResult, context?: Partial<LintContext>): boolean;
7
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
8
  }
@@ -1,19 +1,29 @@
1
1
  export * from "./erb-no-empty-tags.js";
2
2
  export * from "./erb-no-output-control-flow.js";
3
+ export * from "./erb-no-silent-tag-in-attribute-name.js";
3
4
  export * from "./erb-prefer-image-tag-helper.js";
4
5
  export * from "./erb-requires-trailing-newline.js";
5
6
  export * from "./html-anchor-require-href.js";
7
+ export * from "./html-aria-label-is-well-formatted.js";
6
8
  export * from "./html-aria-level-must-be-valid.js";
7
9
  export * from "./html-aria-role-heading-requires-level.js";
8
10
  export * from "./html-aria-role-must-be-valid.js";
9
11
  export * from "./html-attribute-double-quotes.js";
12
+ export * from "./html-attribute-equals-spacing.js";
10
13
  export * from "./html-attribute-values-require-quotes.js";
14
+ export * from "./html-avoid-both-disabled-and-aria-disabled.js";
11
15
  export * from "./html-boolean-attributes-no-value.js";
16
+ export * from "./html-iframe-has-title.js";
12
17
  export * from "./html-img-require-alt.js";
18
+ export * from "./html-navigation-has-label.js";
19
+ export * from "./html-no-aria-hidden-on-focusable.js";
13
20
  export * from "./html-no-block-inside-inline.js";
14
21
  export * from "./html-no-duplicate-attributes.js";
15
22
  export * from "./html-no-duplicate-ids.js";
16
23
  export * from "./html-no-empty-headings.js";
17
24
  export * from "./html-no-nested-links.js";
25
+ export * from "./html-no-positive-tab-index.js";
26
+ export * from "./html-no-self-closing.js";
27
+ export * from "./html-no-title-attribute.js";
18
28
  export * from "./html-tag-name-lowercase.js";
19
29
  export * from "./svg-tag-name-capitalization.js";
@@ -0,0 +1,8 @@
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense } from "../types.js";
3
+ import type { ParseResult } from "@herb-tools/core";
4
+ export declare class ParserNoErrorsRule extends ParserRule {
5
+ name: string;
6
+ check(result: ParseResult): LintOffense[];
7
+ private herbErrorToLintOffense;
8
+ }
@@ -1,5 +1,5 @@
1
1
  import { Visitor, Location } from "@herb-tools/core";
2
- import type { HTMLAttributeNode, HTMLOpenTagNode, HTMLSelfCloseTagNode, LexResult, Token } from "@herb-tools/core";
2
+ import type { HTMLAttributeNode, HTMLAttributeValueNode, HTMLOpenTagNode, LexResult, Token, Node } from "@herb-tools/core";
3
3
  import type { LintOffense, LintSeverity, LintContext } from "../types.js";
4
4
  /**
5
5
  * Base visitor class that provides common functionality for rule visitors
@@ -19,17 +19,52 @@ export declare abstract class BaseRuleVisitor extends Visitor {
19
19
  protected addOffense(message: string, location: Location, severity?: LintSeverity): void;
20
20
  }
21
21
  /**
22
- * Gets attributes from either an HTMLOpenTagNode or HTMLSelfCloseTagNode
22
+ * Gets attributes from an HTMLOpenTagNode
23
23
  */
24
- export declare function getAttributes(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): any[];
24
+ export declare function getAttributes(node: HTMLOpenTagNode): HTMLAttributeNode[];
25
25
  /**
26
26
  * Gets the tag name from an HTML tag node (lowercased)
27
27
  */
28
- export declare function getTagName(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): string | null;
28
+ export declare function getTagName(node: HTMLOpenTagNode): string | null;
29
29
  /**
30
30
  * Gets the attribute name from an HTMLAttributeNode (lowercased)
31
+ * Returns null if the attribute name contains dynamic content (ERB)
31
32
  */
32
33
  export declare function getAttributeName(attributeNode: HTMLAttributeNode): string | null;
34
+ /**
35
+ * Checks if an attribute has a dynamic (ERB-containing) name
36
+ */
37
+ export declare function hasDynamicAttributeName(attributeNode: HTMLAttributeNode): boolean;
38
+ /**
39
+ * Gets the combined string representation of an attribute name (for debugging)
40
+ * This includes both static content and ERB syntax
41
+ */
42
+ export declare function getCombinedAttributeNameString(attributeNode: HTMLAttributeNode): string;
43
+ /**
44
+ * Checks if an attribute value contains only static content (no ERB)
45
+ */
46
+ export declare function hasStaticAttributeValue(attributeNode: HTMLAttributeNode): boolean;
47
+ /**
48
+ * Checks if an attribute value contains dynamic content (ERB)
49
+ */
50
+ export declare function hasDynamicAttributeValue(attributeNode: HTMLAttributeNode): boolean;
51
+ /**
52
+ * Gets the static string value of an attribute (returns null if it contains ERB)
53
+ */
54
+ export declare function getStaticAttributeValue(attributeNode: HTMLAttributeNode): string | null;
55
+ /**
56
+ * Gets the value nodes array for dynamic inspection
57
+ */
58
+ export declare function getAttributeValueNodes(attributeNode: HTMLAttributeNode): Node[];
59
+ /**
60
+ * Checks if an attribute value contains any static content (for validation purposes)
61
+ */
62
+ export declare function hasStaticAttributeValueContent(attributeNode: HTMLAttributeNode): boolean;
63
+ /**
64
+ * Gets the static content of an attribute value (all literal parts combined)
65
+ * Returns the concatenated literal content, or null if no literal nodes exist
66
+ */
67
+ export declare function getStaticAttributeValueContent(attributeNode: HTMLAttributeNode): string | null;
33
68
  /**
34
69
  * Gets the attribute value content from an HTMLAttributeValueNode
35
70
  */
@@ -41,20 +76,25 @@ export declare function hasAttributeValue(attributeNode: HTMLAttributeNode): boo
41
76
  /**
42
77
  * Gets the quote type used for an attribute value
43
78
  */
44
- export declare function getAttributeValueQuoteType(attributeNode: HTMLAttributeNode): "single" | "double" | "none" | null;
79
+ export declare function getAttributeValueQuoteType(nodeOrAttribute: HTMLAttributeNode | HTMLAttributeValueNode): "single" | "double" | "none" | null;
45
80
  /**
46
81
  * Finds an attribute by name in a list of attributes
47
82
  */
48
- export declare function findAttributeByName(attributes: any[], attributeName: string): HTMLAttributeNode | null;
83
+ export declare function findAttributeByName(attributes: Node[], attributeName: string): HTMLAttributeNode | null;
49
84
  /**
50
85
  * Checks if a tag has a specific attribute
51
86
  */
52
- export declare function hasAttribute(node: HTMLOpenTagNode | HTMLSelfCloseTagNode, attributeName: string): boolean;
87
+ export declare function hasAttribute(node: HTMLOpenTagNode, attributeName: string): boolean;
88
+ /**
89
+ * Checks if a tag has a specific attribute
90
+ */
91
+ export declare function getAttribute(node: HTMLOpenTagNode, attributeName: string): HTMLAttributeNode | null;
53
92
  /**
54
93
  * Common HTML element categorization
55
94
  */
56
95
  export declare const HTML_INLINE_ELEMENTS: Set<string>;
57
96
  export declare const HTML_BLOCK_ELEMENTS: Set<string>;
97
+ export declare const HTML_VOID_ELEMENTS: Set<string>;
58
98
  export declare const HTML_BOOLEAN_ATTRIBUTES: Set<string>;
59
99
  export declare const HEADING_TAGS: Set<string>;
60
100
  /**
@@ -67,6 +107,37 @@ export declare const SVG_CAMEL_CASE_ELEMENTS: Set<string>;
67
107
  */
68
108
  export declare const SVG_LOWERCASE_TO_CAMELCASE: Map<string, string>;
69
109
  export declare const VALID_ARIA_ROLES: Set<string>;
110
+ /**
111
+ * Parameter types for AttributeVisitorMixin methods
112
+ */
113
+ export interface StaticAttributeStaticValueParams {
114
+ attributeName: string;
115
+ attributeValue: string;
116
+ attributeNode: HTMLAttributeNode;
117
+ parentNode: HTMLOpenTagNode;
118
+ }
119
+ export interface StaticAttributeDynamicValueParams {
120
+ attributeName: string;
121
+ valueNodes: Node[];
122
+ attributeNode: HTMLAttributeNode;
123
+ parentNode: HTMLOpenTagNode;
124
+ combinedValue?: string | null;
125
+ }
126
+ export interface DynamicAttributeStaticValueParams {
127
+ nameNodes: Node[];
128
+ attributeValue: string;
129
+ attributeNode: HTMLAttributeNode;
130
+ parentNode: HTMLOpenTagNode;
131
+ combinedName?: string;
132
+ }
133
+ export interface DynamicAttributeDynamicValueParams {
134
+ nameNodes: Node[];
135
+ valueNodes: Node[];
136
+ attributeNode: HTMLAttributeNode;
137
+ parentNode: HTMLOpenTagNode;
138
+ combinedName?: string;
139
+ combinedValue?: string | null;
140
+ }
70
141
  export declare const ARIA_ATTRIBUTES: Set<string>;
71
142
  /**
72
143
  * Helper function to create a location at the end of the source with a 1-character range
@@ -80,21 +151,44 @@ export declare function isInlineElement(tagName: string): boolean;
80
151
  * Checks if an element is block-level
81
152
  */
82
153
  export declare function isBlockElement(tagName: string): boolean;
154
+ /**
155
+ * Checks if an element is a void element
156
+ */
157
+ export declare function isVoidElement(tagName: string): boolean;
83
158
  /**
84
159
  * Checks if an attribute is a boolean attribute
85
160
  */
86
161
  export declare function isBooleanAttribute(attributeName: string): boolean;
87
162
  /**
88
- * Abstract base class for rules that need to check individual attributes on HTML tags
89
- * Eliminates duplication of visitHTMLOpenTagNode/visitHTMLSelfCloseTagNode patterns
90
- * and attribute iteration logic. Provides simplified interface with extracted attribute info.
163
+ * Attribute visitor that provides granular processing based on both
164
+ * attribute name type (static/dynamic) and value type (static/dynamic)
165
+ *
166
+ * This gives you 4 distinct methods to override:
167
+ * - checkStaticAttributeStaticValue() - name="class" value="foo"
168
+ * - checkStaticAttributeDynamicValue() - name="class" value="<%= css_class %>"
169
+ * - checkDynamicAttributeStaticValue() - name="data-<%= key %>" value="foo"
170
+ * - checkDynamicAttributeDynamicValue() - name="data-<%= key %>" value="<%= value %>"
91
171
  */
92
172
  export declare abstract class AttributeVisitorMixin extends BaseRuleVisitor {
93
173
  constructor(ruleName: string, context?: Partial<LintContext>);
94
174
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void;
95
- visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void;
96
175
  private checkAttributesOnNode;
97
- protected abstract checkAttribute(attributeName: string, attributeValue: string | null, attributeNode: HTMLAttributeNode, parentNode: HTMLOpenTagNode | HTMLSelfCloseTagNode): void;
176
+ /**
177
+ * Static attribute name with static value: class="container"
178
+ */
179
+ protected checkStaticAttributeStaticValue(params: StaticAttributeStaticValueParams): void;
180
+ /**
181
+ * Static attribute name with dynamic value: class="<%= css_class %>"
182
+ */
183
+ protected checkStaticAttributeDynamicValue(params: StaticAttributeDynamicValueParams): void;
184
+ /**
185
+ * Dynamic attribute name with static value: data-<%= key %>="foo"
186
+ */
187
+ protected checkDynamicAttributeStaticValue(params: DynamicAttributeStaticValueParams): void;
188
+ /**
189
+ * Dynamic attribute name with dynamic value: data-<%= key %>="<%= value %>"
190
+ */
191
+ protected checkDynamicAttributeDynamicValue(params: DynamicAttributeDynamicValueParams): void;
98
192
  }
99
193
  /**
100
194
  * Checks if an attribute value is quoted
@@ -103,7 +197,7 @@ export declare function isAttributeValueQuoted(attributeNode: HTMLAttributeNode)
103
197
  /**
104
198
  * Iterates over all attributes of a tag node, calling the callback for each attribute
105
199
  */
106
- export declare function forEachAttribute(node: HTMLOpenTagNode | HTMLSelfCloseTagNode, callback: (attributeNode: HTMLAttributeNode) => void): void;
200
+ export declare function forEachAttribute(node: HTMLOpenTagNode, callback: (attributeNode: HTMLAttributeNode) => void): void;
107
201
  /**
108
202
  * Base lexer visitor class that provides common functionality for lexer-based rule visitors
109
203
  */
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class SVGTagNameCapitalizationRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,6 +1,6 @@
1
- import { Node, Diagnostic, LexResult } from "@herb-tools/core";
1
+ import { Diagnostic, LexResult, ParseResult } from "@herb-tools/core";
2
2
  import type { defaultRules } from "./default-rules.js";
3
- export type LintSeverity = "error" | "warning";
3
+ export type LintSeverity = "error" | "warning" | "info" | "hint";
4
4
  /**
5
5
  * Automatically inferred union type of all available linter rule names.
6
6
  * This type extracts the 'name' property from each rule class instance.
@@ -18,12 +18,28 @@ export interface LintResult {
18
18
  export declare abstract class ParserRule {
19
19
  static type: "parser";
20
20
  abstract name: string;
21
- abstract check(node: Node, context?: Partial<LintContext>): LintOffense[];
21
+ abstract check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
22
+ /**
23
+ * Optional method to determine if this rule should run.
24
+ * If not implemented, rule is always enabled.
25
+ * @param result - The parse result to analyze
26
+ * @param context - Optional context for linting
27
+ * @returns true if rule should run, false to skip
28
+ */
29
+ isEnabled?(result: ParseResult, context?: Partial<LintContext>): boolean;
22
30
  }
23
31
  export declare abstract class LexerRule {
24
32
  static type: "lexer";
25
33
  abstract name: string;
26
34
  abstract check(lexResult: LexResult, context?: Partial<LintContext>): LintOffense[];
35
+ /**
36
+ * Optional method to determine if this rule should run.
37
+ * If not implemented, rule is always enabled.
38
+ * @param lexResult - The lex result to analyze
39
+ * @param context - Optional context for linting
40
+ * @returns true if rule should run, false to skip
41
+ */
42
+ isEnabled?(lexResult: LexResult, context?: Partial<LintContext>): boolean;
27
43
  }
28
44
  export interface LexerRuleConstructor {
29
45
  type: "lexer";
@@ -44,6 +60,14 @@ export declare abstract class SourceRule {
44
60
  static type: "source";
45
61
  abstract name: string;
46
62
  abstract check(source: string, context?: Partial<LintContext>): LintOffense[];
63
+ /**
64
+ * Optional method to determine if this rule should run.
65
+ * If not implemented, rule is always enabled.
66
+ * @param source - The source code to analyze
67
+ * @param context - Optional context for linting
68
+ * @returns true if rule should run, false to skip
69
+ */
70
+ isEnabled?(source: string, context?: Partial<LintContext>): boolean;
47
71
  }
48
72
  export interface SourceRuleConstructor {
49
73
  type: "source";
@@ -1,6 +1,6 @@
1
- import { Node, Diagnostic, LexResult } from "@herb-tools/core";
1
+ import { Diagnostic, LexResult, ParseResult } from "@herb-tools/core";
2
2
  import type { defaultRules } from "./default-rules.js";
3
- export type LintSeverity = "error" | "warning";
3
+ export type LintSeverity = "error" | "warning" | "info" | "hint";
4
4
  /**
5
5
  * Automatically inferred union type of all available linter rule names.
6
6
  * This type extracts the 'name' property from each rule class instance.
@@ -18,12 +18,28 @@ export interface LintResult {
18
18
  export declare abstract class ParserRule {
19
19
  static type: "parser";
20
20
  abstract name: string;
21
- abstract check(node: Node, context?: Partial<LintContext>): LintOffense[];
21
+ abstract check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
22
+ /**
23
+ * Optional method to determine if this rule should run.
24
+ * If not implemented, rule is always enabled.
25
+ * @param result - The parse result to analyze
26
+ * @param context - Optional context for linting
27
+ * @returns true if rule should run, false to skip
28
+ */
29
+ isEnabled?(result: ParseResult, context?: Partial<LintContext>): boolean;
22
30
  }
23
31
  export declare abstract class LexerRule {
24
32
  static type: "lexer";
25
33
  abstract name: string;
26
34
  abstract check(lexResult: LexResult, context?: Partial<LintContext>): LintOffense[];
35
+ /**
36
+ * Optional method to determine if this rule should run.
37
+ * If not implemented, rule is always enabled.
38
+ * @param lexResult - The lex result to analyze
39
+ * @param context - Optional context for linting
40
+ * @returns true if rule should run, false to skip
41
+ */
42
+ isEnabled?(lexResult: LexResult, context?: Partial<LintContext>): boolean;
27
43
  }
28
44
  export interface LexerRuleConstructor {
29
45
  type: "lexer";
@@ -44,6 +60,14 @@ export declare abstract class SourceRule {
44
60
  static type: "source";
45
61
  abstract name: string;
46
62
  abstract check(source: string, context?: Partial<LintContext>): LintOffense[];
63
+ /**
64
+ * Optional method to determine if this rule should run.
65
+ * If not implemented, rule is always enabled.
66
+ * @param source - The source code to analyze
67
+ * @param context - Optional context for linting
68
+ * @returns true if rule should run, false to skip
69
+ */
70
+ isEnabled?(source: string, context?: Partial<LintContext>): boolean;
47
71
  }
48
72
  export interface SourceRuleConstructor {
49
73
  type: "source";
@@ -6,23 +6,34 @@ This page contains documentation for all Herb Linter rules.
6
6
 
7
7
  - [`erb-no-empty-tags`](./erb-no-empty-tags.md) - Disallow empty ERB tags
8
8
  - [`erb-no-output-control-flow`](./erb-no-output-control-flow.md) - Prevents outputting control flow blocks
9
+ - [`erb-no-silent-tag-in-attribute-name`](./erb-no-silent-tag-in-attribute-name.md) - Disallow ERB silent tags in HTML attribute names
9
10
  - [`erb-prefer-image-tag-helper`](./erb-prefer-image-tag-helper.md) - Prefer `image_tag` helper over `<img>` with ERB expressions
10
- - [`erb-require-whitespace-inside-tags`](./erb-require-whitespace-inside-tags.md) - Requires whitespace around erb tags
11
+ - [`erb-require-whitespace-inside-tags`](./erb-require-whitespace-inside-tags.md) - Requires whitespace around ERB tags
11
12
  - [`erb-requires-trailing-newline`](./erb-requires-trailing-newline.md) - Enforces that all HTML+ERB template files end with exactly one trailing newline character.
12
13
  - [`html-anchor-require-href`](./html-anchor-require-href.md) - Requires an href attribute on anchor tags
13
14
  - [`html-aria-attribute-must-be-valid`](./html-aria-attribute-must-be-valid.md) - Disallow invalid or unknown `aria-*` attributes.
15
+ - [`html-aria-label-is-well-formatted`](./html-aria-label-is-well-formatted.md) - `aria-label` must be well-formatted
14
16
  - [`html-aria-level-must-be-valid`](./html-aria-level-must-be-valid.md) - `aria-level` must be between 1 and 6
15
17
  - [`html-aria-role-heading-requires-level`](./html-aria-role-heading-requires-level.md) - Requires `aria-level` when supplying a `role`
16
18
  - [`html-aria-role-must-be-valid`](./html-aria-role-must-be-valid.md) - The `role` attribute must have a valid WAI-ARIA Role.
17
19
  - [`html-attribute-double-quotes`](./html-attribute-double-quotes.md) - Enforces double quotes for attribute values
20
+ - [`html-attribute-equals-spacing`](./html-attribute-equals-spacing.md) - No whitespace around `=` in HTML attributes
18
21
  - [`html-attribute-values-require-quotes`](./html-attribute-values-require-quotes.md) - Requires quotes around attribute values
22
+ - [`html-avoid-both-disabled-and-aria-disabled`](./html-avoid-both-disabled-and-aria-disabled.md) - Avoid using both `disabled` and `aria-disabled` attributes
19
23
  - [`html-boolean-attributes-no-value`](./html-boolean-attributes-no-value.md) - Prevents values on boolean attributes
20
- - [`html-img-require-alt`](./html-img-require-alt.md) - Requires alt attributes on img tags
24
+ - [`html-iframe-has-title`](./html-iframe-has-title.md) - `iframe` elements must have a `title` attribute
25
+ - [`html-img-require-alt`](./html-img-require-alt.md) - Requires `alt` attributes on `<img>` tags
26
+ - [`html-navigation-has-label`](./html-navigation-has-label.md) - Navigation landmarks must have accessible labels
27
+ - [`html-no-aria-hidden-on-focusable`](./html-no-aria-hidden-on-focusable.md) - Focusable elements should not have `aria-hidden="true"`
21
28
  - [`html-no-block-inside-inline`](./html-no-block-inside-inline.md) - Prevents block-level elements inside inline elements
22
29
  - [`html-no-duplicate-attributes`](./html-no-duplicate-attributes.md) - Prevents duplicate attributes on HTML elements
23
30
  - [`html-no-duplicate-ids`](./html-no-duplicate-ids.md) - Prevents duplicate IDs within a document
24
31
  - [`html-no-nested-links`](./html-no-nested-links.md) - Prevents nested anchor tags
32
+ - [`html-no-positive-tab-index`](./html-no-positive-tab-index.md) - Avoid positive `tabindex` values
33
+ - [`html-no-self-closing`](./html-no-self-closing.md.md) - Disallow self closing tags
34
+ - [`html-no-title-attribute`](./html-no-title-attribute.md) - Avoid using the `title` attribute
25
35
  - [`html-tag-name-lowercase`](./html-tag-name-lowercase.md) - Enforces lowercase tag names in HTML
36
+ - [`parser-no-errors`](./parser-no-errors.md) - Disallow parser errors in HTML+ERB documents
26
37
  - [`svg-tag-name-capitalization`](./svg-tag-name-capitalization.md) - Enforces proper camelCase capitalization for SVG elements
27
38
 
28
39
  ## Contributing
@@ -0,0 +1,34 @@
1
+ # Linter Rule: Disallow ERB silent tags in HTML attribute names
2
+
3
+ **Rule:** `erb-no-silent-tag-in-attribute-name`
4
+
5
+ ## Description
6
+
7
+ Disallow the use of ERB silent tags (`<% ... %>`) inside HTML attribute names. These tags introduce Ruby logic that does not output anything, and placing them in attribute names leads to malformed HTML or completely unpredictable rendering.
8
+
9
+ ## Rationale
10
+
11
+ HTML attribute names, ideally, must be valid, statically defined strings. Placing ERB silent tags (`<% ... %>`) that don't output anything inside attribute names might result in invalid HTML and is confusing. These cases are rarely intentional and almost always indicate a mistake or misuse of ERB templating.
12
+
13
+ ## Examples
14
+
15
+ ### ✅ Good
16
+
17
+ ```erb
18
+ <div data-<%= key %>-target="value"></div>
19
+ <div <%= data_attributes_for(user) %>></div>
20
+ ```
21
+
22
+ ### 🚫 Bad
23
+
24
+ ```erb
25
+ <div data-<% key %>-id="value"></div>
26
+
27
+ <div data-<%# key %>-id="thing"></div>
28
+
29
+ <div data-<%- key -%>-id="thing"></div>
30
+ ```
31
+
32
+ ## References
33
+
34
+ \-
@@ -0,0 +1,49 @@
1
+ # Linter Rule: `aria-label` must be well-formatted
2
+
3
+ **Rule:** `html-aria-label-is-well-formatted`
4
+
5
+ ## Description
6
+
7
+ Ensure that the value of the `aria-label` attribute is formatted like natural, visual text. The text should use sentence case (capitalize the first letter), avoid line breaks, and not look like an ID or code identifier.
8
+
9
+ ## Rationale
10
+
11
+ The `aria-label` attribute provides an accessible name for elements that will be read aloud by screen readers. The text should be formatted like natural language that users would expect to hear, not like technical identifiers. Using proper sentence case and avoiding formatting that looks like code (snake_case, kebab-case, camelCase) ensures a better user experience for assistive technology users.
12
+
13
+ ## Examples
14
+
15
+ ### ✅ Good
16
+
17
+ ```erb
18
+ <button aria-label="Close dialog">X</button>
19
+ <input aria-label="Search products" type="search">
20
+ <button aria-label="Page 2 of 10">2</button>
21
+ ```
22
+
23
+ ### 🚫 Bad
24
+
25
+ ```erb
26
+ <!-- Starts with lowercase -->
27
+ <button aria-label="close dialog">X</button>
28
+
29
+ <!-- Contains line breaks -->
30
+ <button aria-label="Close
31
+ dialog">X</button>
32
+
33
+ <!-- Looks like an ID (snake_case) -->
34
+ <button aria-label="close_dialog">X</button>
35
+
36
+ <!-- Looks like an ID (kebab-case) -->
37
+ <button aria-label="close-dialog">X</button>
38
+
39
+ <!-- Looks like an ID (camelCase) -->
40
+ <button aria-label="closeDialog">X</button>
41
+
42
+ <!-- HTML entity line breaks -->
43
+ <button aria-label="Close&#10;dialog">X</button>
44
+ ```
45
+
46
+ ## References
47
+
48
+ - [ARIA: `aria-label` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label)
49
+ - [erblint-github: GitHub::Accessibility::AriaLabelIsWellFormatted](https://github.com/github/erblint-github/blob/main/docs/rules/accessibility/aria-label-is-well-formatted.md)
@@ -0,0 +1,35 @@
1
+ # Linter Rule: No whitespace around `=` in HTML attributes
2
+
3
+ **Rule:** `html-attribute-equals-spacing`
4
+
5
+ ## Description
6
+
7
+ Disallow whitespace before or after the equals sign (`=`) for attribute values in HTML. Attributes must follow the canonical format (`name="value"`) with no spaces between the attribute name, the equals sign, or the opening quote.
8
+
9
+ ## Rationale
10
+
11
+ Extra whitespace around the `=` in HTML attribute assignments is unnecessary, inconsistent, and not idiomatic. While browsers are usually forgiving, it can lead to confusing diffs, poor formatting, or inconsistent parsing in tools.
12
+
13
+ ## Examples
14
+
15
+ ### ✅ Good
16
+
17
+ ```erb
18
+ <div class="container"></div>
19
+ <img src="/logo.png" alt="Logo">
20
+ <input type="text" value="<%= @value %>">
21
+ ```
22
+
23
+ ### 🚫 Bad
24
+
25
+ ```erb
26
+ <div class ="container"></div>
27
+
28
+ <img src= "/logo.png" alt="Logo">
29
+
30
+ <input type = "text">
31
+ ```
32
+
33
+ ## References
34
+
35
+ \-
@@ -0,0 +1,48 @@
1
+ # Linter Rule: Avoid using both `disabled` and `aria-disabled` attributes
2
+
3
+ **Rule:** `html-avoid-both-disabled-and-aria-disabled`
4
+
5
+ ## Description
6
+
7
+ Prevent using both the native `disabled` attribute and the `aria-disabled` attribute on the same HTML element. Elements should use either the native `disabled` attribute or `aria-disabled`, but not both.
8
+
9
+ ## Rationale
10
+
11
+ Using both `disabled` and `aria-disabled` on the same element creates redundancy and potential confusion for assistive technologies. The native `disabled` attribute provides both visual and functional disabling, while `aria-disabled` only provides semantic information without preventing interaction. Having both can lead to inconsistent behavior and unclear expectations for users.
12
+
13
+ Elements that support the native `disabled` attribute include: `button`, `fieldset`, `input`, `optgroup`, `option`, `select`, and `textarea`.
14
+
15
+ ## Examples
16
+
17
+ ### ✅ Good
18
+
19
+ ```erb
20
+ <!-- Use only the native disabled attribute -->
21
+ <button disabled>Submit</button>
22
+ <input type="text" disabled>
23
+
24
+ <!-- Use only aria-disabled for custom elements -->
25
+ <div role="button" aria-disabled="true">Custom Button</div>
26
+
27
+ <!-- Use only aria-disabled -->
28
+ <button aria-disabled="true">Submit</button>
29
+ ```
30
+
31
+ ### 🚫 Bad
32
+
33
+ ```erb
34
+ <!-- Both disabled and aria-disabled -->
35
+ <button disabled aria-disabled="true">Submit</button>
36
+
37
+ <input type="text" disabled aria-disabled="true">
38
+
39
+ <select disabled aria-disabled="true">
40
+ <option>Option 1</option>
41
+ </select>
42
+ ```
43
+
44
+ ## References
45
+
46
+ - [HTML: `disabled` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled)
47
+ - [ARIA: `aria-disabled` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-disabled)
48
+ - [erblint-github: GitHub::Accessibility::AvoidBothDisabledAndAriaDisabled](https://github.com/github/erblint-github/blob/main/docs/rules/accessibility/avoid-both-disabled-and-aria-disabled.md)