@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
@@ -0,0 +1,43 @@
1
+ # Linter Rule: `iframe` elements must have a `title` attribute
2
+
3
+ **Rule:** `html-iframe-has-title`
4
+
5
+ ## Description
6
+
7
+ Ensure that all `iframe` elements have a meaningful `title` attribute that describes the content of the frame. The title should not be empty or contain only whitespace.
8
+
9
+ ## Rationale
10
+
11
+ The `title` attribute on `iframe` elements provides essential context for screen reader users about what content the frame contains. Without this information, users of assistive technology cannot understand the purpose or content of the embedded frame, creating significant accessibility barriers.
12
+
13
+ ::: tip Note
14
+ `<iframe>`'s with `aria-hidden="true"` are exempt from this requirement as they are hidden from assistive technologies.
15
+ :::
16
+
17
+ ## Examples
18
+
19
+ ### ✅ Good
20
+
21
+ ```erb
22
+ <iframe src="https://youtube.com/embed/123" title="Product demonstration video"></iframe>
23
+ <iframe src="https://example.com" title="Example website content"></iframe>
24
+
25
+ <!-- Hidden from screen readers -->
26
+ <iframe aria-hidden="true"></iframe>
27
+ ```
28
+
29
+ ### 🚫 Bad
30
+
31
+ ```erb
32
+ <iframe src="https://example.com"></iframe>
33
+
34
+ <iframe src="https://example.com" title=""></iframe>
35
+
36
+ <iframe src="https://example.com" title=" "></iframe>
37
+ ```
38
+
39
+ ## References
40
+
41
+ - [HTML: `iframe` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe)
42
+ - [HTML: `title` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/title)
43
+ - [erblint-github: GitHub::Accessibility::IframeHasTitle](https://github.com/github/erblint-github/blob/main/docs/rules/accessibility/iframe-has-title.md)
@@ -0,0 +1,61 @@
1
+ # Linter Rule: Navigation landmarks must have accessible labels
2
+
3
+ **Rule:** `html-navigation-has-label`
4
+
5
+ ## Description
6
+
7
+ Ensure that navigation landmarks have a unique accessible name via `aria-label` or `aria-labelledby` attributes. This applies to both `<nav>` elements and elements with `role="navigation"`.
8
+
9
+ ## Rationale
10
+
11
+ Navigation landmarks help users of assistive technology quickly understand and navigate to different sections of a website. When multiple navigation landmarks exist on a page, each needs a unique accessible name so users can distinguish between them (e.g., "Main navigation", "Footer links", "Breadcrumb navigation").
12
+
13
+ ## Examples
14
+
15
+ ### ✅ Good
16
+
17
+ ```erb
18
+ <nav aria-label="Main navigation">
19
+ <ul>
20
+ <li><a href="/">Home</a></li>
21
+ <li><a href="/about">About</a></li>
22
+ </ul>
23
+ </nav>
24
+
25
+ <nav aria-labelledby="breadcrumb-title">
26
+ <h2 id="breadcrumb-title">Breadcrumb</h2>
27
+ <ol>
28
+ <li><a href="/">Home</a></li>
29
+ <li>Current Page</li>
30
+ </ol>
31
+ </nav>
32
+
33
+ <div role="navigation" aria-label="Footer links">
34
+ <a href="/privacy">Privacy</a>
35
+ <a href="/terms">Terms</a>
36
+ </div>
37
+ ```
38
+
39
+ ### 🚫 Bad
40
+
41
+ ```erb
42
+ <nav>
43
+ <ul>
44
+ <li><a href="/">Home</a></li>
45
+ <li><a href="/about">About</a></li>
46
+ </ul>
47
+ </nav>
48
+
49
+ <div role="navigation">
50
+ <a href="/privacy">Privacy</a>
51
+ <a href="/terms">Terms</a>
52
+ </div>
53
+ ```
54
+
55
+ ## References
56
+
57
+ - [ARIA: `navigation` role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/navigation_role)
58
+ - [HTML: `nav` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nav)
59
+ - [ARIA: `aria-label` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label)
60
+ - [ARIA: `aria-labelledby` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-labelledby)
61
+ - [erblint-github: GitHub::Accessibility::NavigationHasLabel](https://github.com/github/erblint-github/blob/main/docs/rules/accessibility/navigation-has-label.md)
@@ -0,0 +1,54 @@
1
+ # Linter Rule: Focusable elements should not have `aria-hidden="true"`
2
+
3
+ **Rule:** `html-no-aria-hidden-on-focusable`
4
+
5
+ ## Description
6
+
7
+ Prevent using `aria-hidden="true"` on elements that can receive keyboard focus. When an element is focusable but hidden from screen readers, it creates a confusing experience where keyboard users can tab to "invisible" elements.
8
+
9
+ ## Rationale
10
+
11
+ Elements with `aria-hidden="true"` are completely hidden from assistive technologies, but they remain visible and interactive for mouse and keyboard users. If a focusable element is hidden from screen readers, keyboard-only users (especially those using screen readers) will encounter focused elements that provide no accessible information, creating a broken user experience.
12
+
13
+ ## Examples
14
+
15
+ ### ✅ Good
16
+
17
+ ```erb
18
+ <button>Submit</button>
19
+ <a href="/link">Link</a>
20
+ <input type="text">
21
+ <textarea></textarea>
22
+
23
+ <div aria-hidden="true">Decorative content</div>
24
+ <span aria-hidden="true">🎉</span>
25
+
26
+ <button tabindex="-1" aria-hidden="true">Hidden button</button>
27
+ ```
28
+
29
+ ### 🚫 Bad
30
+
31
+ ```erb
32
+ <button aria-hidden="true">Submit</button>
33
+
34
+ <a href="/link" aria-hidden="true">Link</a>
35
+
36
+ <input type="text" aria-hidden="true">
37
+
38
+ <textarea aria-hidden="true"></textarea>
39
+
40
+ <select aria-hidden="true">
41
+ <option>Option</option>
42
+ </select>
43
+
44
+ <div tabindex="0" aria-hidden="true">Focusable div</div>
45
+
46
+ <a href="/link" aria-hidden="true">Hidden link</a>
47
+ ```
48
+
49
+ ## References
50
+
51
+ - [ARIA: `aria-hidden` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-hidden)
52
+ - [HTML: `tabindex` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex)
53
+ - [WebAIM: Keyboard Accessibility](https://webaim.org/techniques/keyboard/)
54
+ - [erblint-github: GitHub::Accessibility::NoAriaHiddenOnFocusable](https://github.com/github/erblint-github/blob/main/docs/rules/accessibility/no-aria-hidden-on-focusable.md)
@@ -0,0 +1,55 @@
1
+ # Linter Rule: Avoid positive `tabindex` values
2
+
3
+ **Rule:** `html-no-positive-tab-index`
4
+
5
+ ## Description
6
+
7
+ Prevent using positive values for the `tabindex` attribute. Only `tabindex="0"` (to make elements focusable) and `tabindex="-1"` (to remove from tab order) should be used.
8
+
9
+ ## Rationale
10
+
11
+ Positive `tabindex` values create a custom tab order that can be confusing and unpredictable for keyboard users. They override the natural document flow and can cause elements to be focused in an unexpected sequence. This breaks the logical reading order and creates usability issues, especially for screen reader users who rely on a predictable navigation pattern.
12
+
13
+ The recommended approach is to structure your HTML in the correct tab order and use `tabindex="0"` only when you need to make non-interactive elements focusable, or `tabindex="-1"` to remove elements from the tab sequence while keeping them programmatically focusable.
14
+
15
+ ## Examples
16
+
17
+ ### ✅ Good
18
+
19
+ ```erb
20
+ <!-- Natural tab order (no tabindex needed) -->
21
+ <button>First</button>
22
+ <button>Second</button>
23
+ <button>Third</button>
24
+
25
+ <!-- Make non-interactive element focusable -->
26
+ <div tabindex="0" role="button">Custom button</div>
27
+
28
+ <!-- Remove from tab order but keep programmatically focusable -->
29
+ <button tabindex="-1">Skip this in tab order</button>
30
+
31
+ <!-- Zero tabindex to ensure focusability -->
32
+ <span tabindex="0" role="button">Focusable span</span>
33
+ ```
34
+
35
+ ### 🚫 Bad
36
+
37
+ ```erb
38
+ <button tabindex="3">Third in tab order</button>
39
+
40
+ <button tabindex="1">First in tab order</button>
41
+
42
+ <button tabindex="2">Second in tab order</button>
43
+
44
+
45
+ <input tabindex="5" type="text">
46
+
47
+ <button tabindex="10">Submit</button>
48
+ ```
49
+
50
+ ## References
51
+
52
+ - [HTML: `tabindex` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex)
53
+ - [WebAIM: Keyboard Accessibility](https://webaim.org/techniques/keyboard/tabindex)
54
+ - [WCAG: Focus Order](https://www.w3.org/WAI/WCAG21/Understanding/focus-order.html)
55
+ - [erblint-github: GitHub::Accessibility::NoPositiveTabIndex](https://github.com/github/erblint-github/blob/main/docs/rules/accessibility/no-positive-tab-index.md)
@@ -0,0 +1,65 @@
1
+ # Linter Rule: Disallow self-closing tag syntax for void elements
2
+
3
+ **Rule:** `html-no-self-closing`
4
+
5
+ ## Description
6
+
7
+ Disallow self-closing syntax (`<tag />`) in HTML for all elements.
8
+
9
+ In HTML5, the trailing slash in a start tag is obsolete and has no effect.
10
+ Non-void elements require explicit end tags, and void elements are
11
+ self-contained without the slash.
12
+
13
+ ## Rationale
14
+
15
+ Self-closing syntax is an XHTML artifact. In HTML:
16
+
17
+ - On **non-void** elements, it’s a parse error and produces invalid markup
18
+ (`<div />` is invalid).
19
+ - On **void elements**, the slash is ignored and unnecessary (`<input />` is
20
+ equivalent to `<input>`).
21
+
22
+ Removing the slash ensures HTML5-compliant, cleaner markup and avoids mixing
23
+ XHTML and HTML styles.
24
+
25
+ ## Examples
26
+
27
+ ### ✅ Good
28
+
29
+ ```html
30
+ <span></span>
31
+ <div></div>
32
+ <section></section>
33
+ <custom-element></custom-element>
34
+
35
+ <img src="/logo.png" alt="Logo">
36
+ <input type="text">
37
+ <br>
38
+ <hr>
39
+ ```
40
+
41
+ ### 🚫 Bad
42
+
43
+ ```html
44
+ <span />
45
+
46
+ <div />
47
+
48
+ <section />
49
+
50
+ <custom-element />
51
+
52
+ <img src="/logo.png" alt="Logo" />
53
+
54
+ <input type="text" />
55
+
56
+ <br />
57
+
58
+ <hr />
59
+ ```
60
+
61
+ ## References
62
+
63
+ - [HTML Living Standard: Void Elements](https://html.spec.whatwg.org/multipage/syntax.html#void-elements)
64
+ - [MDN: Void element](https://developer.mozilla.org/en-US/docs/Glossary/Void_element)
65
+ - [erb_lint: SelfClosingTag](https://github.com/Shopify/erb_lint#selfclosingtag)
@@ -0,0 +1,69 @@
1
+ # Linter Rule: Avoid using the `title` attribute
2
+
3
+ **Rule:** `html-no-title-attribute`
4
+
5
+ ## Description
6
+
7
+ Discourage the use of the `title` attribute on most HTML elements, as it provides poor accessibility and user experience. The `title` attribute is only accessible via mouse hover and is not reliably exposed to screen readers or keyboard users.
8
+
9
+ ## Rationale
10
+
11
+ The `title` attribute has several accessibility problems:
12
+ - It's only visible on mouse hover, making it inaccessible to keyboard and touch users
13
+ - Screen readers don't consistently announce title attributes
14
+ - Mobile devices don't show title tooltips
15
+ - The visual presentation is inconsistent across browsers and operating systems
16
+
17
+ Instead of relying on `title`, use visible text, `aria-label`, `aria-describedby`, or other accessible alternatives.
18
+
19
+ ::: warning Exceptions
20
+ This rule allows `title` on `<iframe>` and `<link>` elements where it serves specific accessibility purposes.
21
+ :::
22
+
23
+ ## Examples
24
+
25
+ ### ✅ Good
26
+
27
+ ```erb
28
+ <!-- Use visible text instead of title -->
29
+ <button>Save document</button>
30
+ <span class="help-text">Click to save your changes</span>
31
+
32
+ <!-- Use aria-label for accessible names -->
33
+ <button aria-label="Close dialog">×</button>
34
+
35
+ <!-- Use aria-describedby for additional context -->
36
+ <input type="password" aria-describedby="pwd-help">
37
+ <div id="pwd-help">Password must be at least 8 characters</div>
38
+
39
+ <!-- Exceptions: title allowed on iframe and links -->
40
+ <iframe src="https://example.com" title="Example website content"></iframe>
41
+ <link href="default.css" rel="stylesheet" title="Default Style">
42
+ ```
43
+
44
+ ### 🚫 Bad
45
+
46
+ ```erb
47
+ <!-- Don't use title for essential information -->
48
+ <button title="Save your changes">Save</button>
49
+
50
+ <div title="This is important information">Content</div>
51
+
52
+ <span title="Required field">*</span>
53
+
54
+ <!-- Don't use title on form elements -->
55
+ <input type="text" title="Enter your name">
56
+
57
+ <select title="Choose your country">
58
+ <option>US</option>
59
+ <option>CA</option>
60
+ </select>
61
+ ```
62
+
63
+ ## References
64
+
65
+ - [HTML: `title` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/title)
66
+ - [WebAIM: Accessible Forms](https://webaim.org/techniques/forms/)
67
+ - [ARIA: `aria-label` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label)
68
+ - [ARIA: `aria-describedby` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-describedby)
69
+ - [erblint-github: GitHub::Accessibility::NoTitleAttribute](https://github.com/github/erblint-github/blob/main/docs/rules/accessibility/no-title-attribute.md)
@@ -12,15 +12,28 @@ HTML is case-insensitive for tag names, but lowercase is the widely accepted con
12
12
 
13
13
  Writing tags in uppercase or mixed case can lead to inconsistent code and unnecessary diffs during reviews and merges.
14
14
 
15
- ## Examples
15
+ ### Notes
16
+
17
+ ::: tip XML Documents
18
+ This rule is automatically disabled for XML documents and XML+ERB templates. XML allows uppercase tag names and follows different naming conventions than HTML.
19
+
20
+ The rule will be disabled when:
21
+ - The document contains an XML declaration (`<?xml version="1.0" ?>`)
22
+ - The file extension is `.xml` or `.xml.erb`
23
+ :::
16
24
 
25
+ ::: tip SVG Elements
26
+ This rule does not apply to child elements within `<svg>` tags, as SVG element names are case-sensitive and may require specific capitalization (e.g., `linearGradient`, `clipPath`). However, the rule still applies to the `<svg>` element itself.
27
+ :::
28
+
29
+ ## Examples
17
30
 
18
31
  ### ✅ Good
19
32
 
20
33
  ```erb
21
34
  <div class="container"></div>
22
35
 
23
- <input type="text" name="username" />
36
+ <input type="text" name="username">
24
37
 
25
38
  <span>Label</span>
26
39
 
@@ -32,7 +45,7 @@ Writing tags in uppercase or mixed case can lead to inconsistent code and unnece
32
45
  ```erb
33
46
  <DIV class="container"></DIV>
34
47
 
35
- <Input type="text" name="username" />
48
+ <Input type="text" name="username">
36
49
 
37
50
  <Span>Label</Span>
38
51
 
@@ -0,0 +1,84 @@
1
+ # Linter Rule: Disallow parser errors in HTML+ERB documents
2
+
3
+ **Rule:** `parser-no-errors`
4
+
5
+ ## Description
6
+
7
+ Report parser errors as linting offenses. This rule surfaces syntax errors, malformed HTML, and other parsing issues that prevent the document from being correctly parsed.
8
+
9
+ ## Rationale
10
+
11
+ Parser errors indicate fundamental structural problems in HTML+ERB documents that can lead to unexpected rendering behavior, accessibility issues, and maintenance difficulties. These errors should be fixed before addressing other linting concerns as they represent invalid markup that browsers may interpret inconsistently.
12
+
13
+ By surfacing parser errors through the linter, developers can catch these critical issues when running lint checks directly, without needing to switch to the language server or other tools.
14
+
15
+ ## Examples
16
+
17
+ ### ✅ Good
18
+
19
+ ```html
20
+ <h2>Welcome to our site</h2>
21
+ <p>This is a paragraph with proper structure.</p>
22
+
23
+ <div class="container">
24
+ <img src="image.jpg" alt="Description">
25
+ </div>
26
+ ```
27
+
28
+ ```erb
29
+ <h2><%= @page.title %></h2>
30
+ <p><%= @page.description %></p>
31
+
32
+ <% if user_signed_in? %>
33
+ <div class="user-section">
34
+ <%= current_user.name %>
35
+ </div>
36
+ <% end %>
37
+ ```
38
+
39
+ ### 🚫 Bad
40
+
41
+ ```html
42
+ <!-- Mismatched closing tag -->
43
+ <h2>Welcome to our site</h3>
44
+
45
+ <!-- Unclosed element -->
46
+ <div>
47
+ <p>This paragraph is never closed
48
+ </div>
49
+
50
+ <!-- Missing opening tag -->
51
+ Some content
52
+ </div>
53
+ ```
54
+
55
+ ```erb
56
+ <!-- Invalid Ruby syntax in ERB -->
57
+ <%= 1 + %>
58
+
59
+ <!-- Mismatched quotes -->
60
+ <div class="container'>Content</div>
61
+
62
+ <!-- Void element with closing tag -->
63
+ <img src="image.jpg" alt="Description"></img>
64
+ ```
65
+
66
+ ## Error Types
67
+
68
+ This rule reports various parser error types:
69
+
70
+ - **`UNCLOSED_ELEMENT_ERROR`**: Elements that are opened but never closed
71
+ - **`MISSING_CLOSING_TAG_ERROR`**: Opening tags without matching closing tags
72
+ - **`MISSING_OPENING_TAG_ERROR`**: Closing tags without matching opening tags
73
+ - **`TAG_NAMES_MISMATCH_ERROR`**: Opening and closing tags with different names
74
+ - **`QUOTES_MISMATCH_ERROR`**: Mismatched quotation marks in attributes
75
+ - **`VOID_ELEMENT_CLOSING_TAG_ERROR`**: Void elements (like `<img>`) with closing tags
76
+ - **`RUBY_PARSE_ERROR`**: Invalid Ruby syntax within ERB tags
77
+ - **`UNEXPECTED_TOKEN_ERROR`**: Unexpected tokens during parsing
78
+ - **`UNEXPECTED_ERROR`**: Other unexpected parsing issues
79
+
80
+ ## References
81
+
82
+ * [HTML Living Standard - Parsing](https://html.spec.whatwg.org/multipage/parsing.html)
83
+ * [W3C HTML Validator](https://validator.w3.org/)
84
+ * [ERB Template Guide](https://guides.rubyonrails.org/layouts_and_rendering.html)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herb-tools/linter",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "description": "HTML+ERB linter for validating HTML structure and enforcing best practices",
5
5
  "license": "MIT",
6
6
  "homepage": "https://herb-tools.dev",
@@ -33,9 +33,9 @@
33
33
  }
34
34
  },
35
35
  "dependencies": {
36
- "@herb-tools/core": "0.4.3",
37
- "@herb-tools/highlighter": "0.4.3",
38
- "@herb-tools/node-wasm": "0.4.3",
36
+ "@herb-tools/core": "0.6.0",
37
+ "@herb-tools/highlighter": "0.6.0",
38
+ "@herb-tools/node-wasm": "0.6.0",
39
39
  "glob": "^11.0.3"
40
40
  },
41
41
  "files": [
@@ -11,9 +11,11 @@ import type { ThemeInput } from "@herb-tools/highlighter"
11
11
 
12
12
  import { name, version } from "../../package.json"
13
13
 
14
+ export type FormatOption = "simple" | "detailed" | "json" | "github"
15
+
14
16
  export interface ParsedArguments {
15
17
  pattern: string
16
- formatOption: 'simple' | 'detailed'
18
+ formatOption: FormatOption
17
19
  showTiming: boolean
18
20
  theme: ThemeInput
19
21
  wrapLines: boolean
@@ -32,9 +34,11 @@ export class ArgumentParser {
32
34
  Options:
33
35
  -h, --help show help
34
36
  -v, --version show version
35
- --format output format (simple|detailed) [default: detailed]
37
+ --format output format (simple|detailed|json|github) [default: detailed]
36
38
  --simple use simple output format (shortcut for --format simple)
37
- --theme syntax highlighting theme (${THEME_NAMES.join('|')}) or path to custom theme file [default: ${DEFAULT_THEME}]
39
+ --json use JSON output format (shortcut for --format json)
40
+ --github use GitHub Actions output format (shortcut for --format github)
41
+ --theme syntax highlighting theme (${THEME_NAMES.join("|")}) or path to custom theme file [default: ${DEFAULT_THEME}]
38
42
  --no-color disable colored output
39
43
  --no-timing hide timing information
40
44
  --no-wrap-lines disable line wrapping
@@ -45,15 +49,17 @@ export class ArgumentParser {
45
49
  const { values, positionals } = parseArgs({
46
50
  args: argv.slice(2),
47
51
  options: {
48
- help: { type: 'boolean', short: 'h' },
49
- version: { type: 'boolean', short: 'v' },
50
- format: { type: 'string' },
51
- simple: { type: 'boolean' },
52
- theme: { type: 'string' },
53
- 'no-color': { type: 'boolean' },
54
- 'no-timing': { type: 'boolean' },
55
- 'no-wrap-lines': { type: 'boolean' },
56
- 'truncate-lines': { type: 'boolean' }
52
+ help: { type: "boolean", short: "h" },
53
+ version: { type: "boolean", short: "v" },
54
+ format: { type: "string" },
55
+ simple: { type: "boolean" },
56
+ json: { type: "boolean" },
57
+ github: { type: "boolean" },
58
+ theme: { type: "string" },
59
+ "no-color": { type: "boolean" },
60
+ "no-timing": { type: "boolean" },
61
+ "no-wrap-lines": { type: "boolean" },
62
+ "truncate-lines": { type: "boolean" }
57
63
  },
58
64
  allowPositionals: true
59
65
  })
@@ -69,8 +75,8 @@ export class ArgumentParser {
69
75
  process.exit(0)
70
76
  }
71
77
 
72
- let formatOption: 'simple' | 'detailed' = 'detailed'
73
- if (values.format && (values.format === "detailed" || values.format === "simple")) {
78
+ let formatOption: FormatOption = "detailed"
79
+ if (values.format && (values.format === "detailed" || values.format === "simple" || values.format === "json" || values.format === "github")) {
74
80
  formatOption = values.format
75
81
  }
76
82
 
@@ -78,21 +84,29 @@ export class ArgumentParser {
78
84
  formatOption = "simple"
79
85
  }
80
86
 
81
- if (values['no-color']) {
87
+ if (values.json) {
88
+ formatOption = "json"
89
+ }
90
+
91
+ if (values.github) {
92
+ formatOption = "github"
93
+ }
94
+
95
+ if (values["no-color"]) {
82
96
  process.env.NO_COLOR = "1"
83
97
  }
84
98
 
85
- const showTiming = !values['no-timing']
99
+ const showTiming = !values["no-timing"]
86
100
 
87
- let wrapLines = !values['no-wrap-lines']
101
+ let wrapLines = !values["no-wrap-lines"]
88
102
  let truncateLines = false
89
103
 
90
- if (values['truncate-lines']) {
104
+ if (values["truncate-lines"]) {
91
105
  truncateLines = true
92
106
  wrapLines = false
93
107
  }
94
108
 
95
- if (!values['no-wrap-lines'] && values['truncate-lines']) {
109
+ if (!values["no-wrap-lines"] && values["truncate-lines"]) {
96
110
  console.error("Error: Line wrapping and --truncate-lines cannot be used together. Use --no-wrap-lines with --truncate-lines.")
97
111
  process.exit(1)
98
112
  }
@@ -100,11 +114,6 @@ export class ArgumentParser {
100
114
  const theme = values.theme || DEFAULT_THEME
101
115
  const pattern = this.getFilePattern(positionals)
102
116
 
103
- if (positionals.length === 0) {
104
- console.error("Please specify input file.")
105
- process.exit(1)
106
- }
107
-
108
117
  return { pattern, formatOption, showTiming, theme, wrapLines, truncateLines }
109
118
  }
110
119