@herb-tools/linter 0.4.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 (175) hide show
  1. package/README.md +34 -0
  2. package/bin/herb-lint +3 -0
  3. package/dist/herb-lint.js +16505 -0
  4. package/dist/herb-lint.js.map +1 -0
  5. package/dist/index.cjs +834 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.js +820 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/package.json +49 -0
  10. package/dist/src/cli/argument-parser.js +96 -0
  11. package/dist/src/cli/argument-parser.js.map +1 -0
  12. package/dist/src/cli/file-processor.js +58 -0
  13. package/dist/src/cli/file-processor.js.map +1 -0
  14. package/dist/src/cli/formatters/base-formatter.js +3 -0
  15. package/dist/src/cli/formatters/base-formatter.js.map +1 -0
  16. package/dist/src/cli/formatters/detailed-formatter.js +62 -0
  17. package/dist/src/cli/formatters/detailed-formatter.js.map +1 -0
  18. package/dist/src/cli/formatters/index.js +4 -0
  19. package/dist/src/cli/formatters/index.js.map +1 -0
  20. package/dist/src/cli/formatters/simple-formatter.js +31 -0
  21. package/dist/src/cli/formatters/simple-formatter.js.map +1 -0
  22. package/dist/src/cli/index.js +5 -0
  23. package/dist/src/cli/index.js.map +1 -0
  24. package/dist/src/cli/summary-reporter.js +96 -0
  25. package/dist/src/cli/summary-reporter.js.map +1 -0
  26. package/dist/src/cli.js +50 -0
  27. package/dist/src/cli.js.map +1 -0
  28. package/dist/src/default-rules.js +31 -0
  29. package/dist/src/default-rules.js.map +1 -0
  30. package/dist/src/herb-lint.js +5 -0
  31. package/dist/src/herb-lint.js.map +1 -0
  32. package/dist/src/index.js +4 -0
  33. package/dist/src/index.js.map +1 -0
  34. package/dist/src/linter.js +39 -0
  35. package/dist/src/linter.js.map +1 -0
  36. package/dist/src/rules/erb-no-empty-tags.js +23 -0
  37. package/dist/src/rules/erb-no-empty-tags.js.map +1 -0
  38. package/dist/src/rules/erb-no-output-control-flow.js +47 -0
  39. package/dist/src/rules/erb-no-output-control-flow.js.map +1 -0
  40. package/dist/src/rules/erb-require-whitespace-inside-tags.js +43 -0
  41. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -0
  42. package/dist/src/rules/html-anchor-require-href.js +25 -0
  43. package/dist/src/rules/html-anchor-require-href.js.map +1 -0
  44. package/dist/src/rules/html-aria-role-heading-requires-level.js +26 -0
  45. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -0
  46. package/dist/src/rules/html-attribute-double-quotes.js +21 -0
  47. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -0
  48. package/dist/src/rules/html-attribute-values-require-quotes.js +22 -0
  49. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -0
  50. package/dist/src/rules/html-boolean-attributes-no-value.js +19 -0
  51. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -0
  52. package/dist/src/rules/html-img-require-alt.js +29 -0
  53. package/dist/src/rules/html-img-require-alt.js.map +1 -0
  54. package/dist/src/rules/html-no-block-inside-inline.js +59 -0
  55. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -0
  56. package/dist/src/rules/html-no-duplicate-attributes.js +43 -0
  57. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -0
  58. package/dist/src/rules/html-no-empty-headings.js +148 -0
  59. package/dist/src/rules/html-no-empty-headings.js.map +1 -0
  60. package/dist/src/rules/html-no-nested-links.js +45 -0
  61. package/dist/src/rules/html-no-nested-links.js.map +1 -0
  62. package/dist/src/rules/html-tag-name-lowercase.js +39 -0
  63. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -0
  64. package/dist/src/rules/index.js +13 -0
  65. package/dist/src/rules/index.js.map +1 -0
  66. package/dist/src/rules/rule-utils.js +198 -0
  67. package/dist/src/rules/rule-utils.js.map +1 -0
  68. package/dist/src/types.js +2 -0
  69. package/dist/src/types.js.map +1 -0
  70. package/dist/tsconfig.tsbuildinfo +1 -0
  71. package/dist/types/cli/argument-parser.d.ts +14 -0
  72. package/dist/types/cli/file-processor.d.ts +21 -0
  73. package/dist/types/cli/formatters/base-formatter.d.ts +6 -0
  74. package/dist/types/cli/formatters/detailed-formatter.d.ts +13 -0
  75. package/dist/types/cli/formatters/index.d.ts +3 -0
  76. package/dist/types/cli/formatters/simple-formatter.d.ts +7 -0
  77. package/dist/types/cli/summary-reporter.d.ts +22 -0
  78. package/dist/types/cli.d.ts +6 -0
  79. package/dist/types/default-rules.d.ts +2 -0
  80. package/dist/types/herb-lint.d.ts +2 -0
  81. package/dist/types/index.d.ts +3 -0
  82. package/dist/types/linter.d.ts +18 -0
  83. package/dist/types/rules/erb-no-empty-tags.d.ts +6 -0
  84. package/dist/types/rules/erb-no-output-control-flow.d.ts +6 -0
  85. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +6 -0
  86. package/dist/types/rules/html-anchor-require-href.d.ts +6 -0
  87. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +6 -0
  88. package/dist/types/rules/html-attribute-double-quotes.d.ts +6 -0
  89. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +6 -0
  90. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +6 -0
  91. package/dist/types/rules/html-img-require-alt.d.ts +6 -0
  92. package/dist/types/rules/html-no-block-inside-inline.d.ts +6 -0
  93. package/dist/types/rules/html-no-duplicate-attributes.d.ts +6 -0
  94. package/dist/types/rules/html-no-empty-headings.d.ts +6 -0
  95. package/dist/types/rules/html-no-nested-links.d.ts +6 -0
  96. package/dist/types/rules/html-tag-name-lowercase.d.ts +6 -0
  97. package/dist/types/rules/index.d.ts +12 -0
  98. package/dist/types/rules/rule-utils.d.ts +89 -0
  99. package/dist/types/src/cli/argument-parser.d.ts +14 -0
  100. package/dist/types/src/cli/file-processor.d.ts +21 -0
  101. package/dist/types/src/cli/formatters/base-formatter.d.ts +6 -0
  102. package/dist/types/src/cli/formatters/detailed-formatter.d.ts +13 -0
  103. package/dist/types/src/cli/formatters/index.d.ts +3 -0
  104. package/dist/types/src/cli/formatters/simple-formatter.d.ts +7 -0
  105. package/dist/types/src/cli/index.d.ts +4 -0
  106. package/dist/types/src/cli/summary-reporter.d.ts +22 -0
  107. package/dist/types/src/cli.d.ts +6 -0
  108. package/dist/types/src/default-rules.d.ts +2 -0
  109. package/dist/types/src/herb-lint.d.ts +2 -0
  110. package/dist/types/src/index.d.ts +3 -0
  111. package/dist/types/src/linter.d.ts +18 -0
  112. package/dist/types/src/rules/erb-no-empty-tags.d.ts +6 -0
  113. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +6 -0
  114. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +6 -0
  115. package/dist/types/src/rules/html-anchor-require-href.d.ts +6 -0
  116. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +6 -0
  117. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +6 -0
  118. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +6 -0
  119. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +6 -0
  120. package/dist/types/src/rules/html-img-require-alt.d.ts +6 -0
  121. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +6 -0
  122. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +6 -0
  123. package/dist/types/src/rules/html-no-empty-headings.d.ts +6 -0
  124. package/dist/types/src/rules/html-no-nested-links.d.ts +6 -0
  125. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +6 -0
  126. package/dist/types/src/rules/index.d.ts +12 -0
  127. package/dist/types/src/rules/rule-utils.d.ts +89 -0
  128. package/dist/types/src/types.d.ts +26 -0
  129. package/dist/types/types.d.ts +26 -0
  130. package/docs/rules/README.md +39 -0
  131. package/docs/rules/erb-no-empty-tags.md +38 -0
  132. package/docs/rules/erb-no-output-control-flow.md +45 -0
  133. package/docs/rules/erb-require-whitespace-inside-tags.md +43 -0
  134. package/docs/rules/html-anchor-require-href.md +32 -0
  135. package/docs/rules/html-aria-role-heading-requires-level.md +34 -0
  136. package/docs/rules/html-attribute-double-quotes.md +43 -0
  137. package/docs/rules/html-attribute-values-require-quotes.md +43 -0
  138. package/docs/rules/html-boolean-attributes-no-value.md +39 -0
  139. package/docs/rules/html-img-require-alt.md +44 -0
  140. package/docs/rules/html-no-block-inside-inline.md +66 -0
  141. package/docs/rules/html-no-duplicate-attributes.md +35 -0
  142. package/docs/rules/html-no-empty-headings.md +78 -0
  143. package/docs/rules/html-no-nested-links.md +44 -0
  144. package/docs/rules/html-tag-name-lowercase.md +44 -0
  145. package/package.json +49 -0
  146. package/src/cli/argument-parser.ts +125 -0
  147. package/src/cli/file-processor.ts +86 -0
  148. package/src/cli/formatters/base-formatter.ts +11 -0
  149. package/src/cli/formatters/detailed-formatter.ts +74 -0
  150. package/src/cli/formatters/index.ts +3 -0
  151. package/src/cli/formatters/simple-formatter.ts +40 -0
  152. package/src/cli/index.ts +4 -0
  153. package/src/cli/summary-reporter.ts +127 -0
  154. package/src/cli.ts +60 -0
  155. package/src/default-rules.ts +33 -0
  156. package/src/herb-lint.ts +6 -0
  157. package/src/index.ts +3 -0
  158. package/src/linter.ts +50 -0
  159. package/src/rules/erb-no-empty-tags.ts +34 -0
  160. package/src/rules/erb-no-output-control-flow.ts +61 -0
  161. package/src/rules/erb-require-whitespace-inside-tags.ts +61 -0
  162. package/src/rules/html-anchor-require-href.ts +39 -0
  163. package/src/rules/html-aria-role-heading-requires-level.ts +44 -0
  164. package/src/rules/html-attribute-double-quotes.ts +28 -0
  165. package/src/rules/html-attribute-values-require-quotes.ts +30 -0
  166. package/src/rules/html-boolean-attributes-no-value.ts +27 -0
  167. package/src/rules/html-img-require-alt.ts +42 -0
  168. package/src/rules/html-no-block-inside-inline.ts +84 -0
  169. package/src/rules/html-no-duplicate-attributes.ts +59 -0
  170. package/src/rules/html-no-empty-headings.ts +185 -0
  171. package/src/rules/html-no-nested-links.ts +65 -0
  172. package/src/rules/html-tag-name-lowercase.ts +50 -0
  173. package/src/rules/index.ts +12 -0
  174. package/src/rules/rule-utils.ts +257 -0
  175. package/src/types.ts +32 -0
@@ -0,0 +1,43 @@
1
+ # Linter Rule: Enforce whitespace around ERB tag contents
2
+
3
+ **Rule:** `erb-require-whitespace-inside-tags`
4
+
5
+ ## Description
6
+
7
+ Require a single space before and after Ruby code inside ERB tags (`<% ... %>` and `<%= ... %>`). This improves readability and keeps ERB code visually consistent with Ruby style guides.
8
+
9
+ ## Rationale
10
+
11
+ Without spacing, ERB tags can become hard to read and visually cramped:
12
+
13
+ * difficult to scan: `<%=user.name%>`
14
+ * harder to read: `<%if admin%><%end%>`
15
+
16
+ By enforcing consistent spacing around Ruby expressions, templates become easier to read, review, and maintain. It also aligns with standard Ruby formatting conventions, where spaces are used around control keywords and operators.
17
+
18
+ ## Examples
19
+
20
+ ### ✅ Good
21
+
22
+ ```erb
23
+ <%= user.name %>
24
+
25
+ <% if admin %>
26
+ Hello, admin.
27
+ <% end %>
28
+ ```
29
+
30
+ ### 🚫 Bad
31
+
32
+ ```erb
33
+ <%=user.name %>
34
+
35
+ <%if admin %>
36
+
37
+ Hello, admin.
38
+ <% end%>
39
+ ```
40
+
41
+ ## References
42
+
43
+ \-
@@ -0,0 +1,32 @@
1
+ # Linter Rule: Require `href` attribute on `<a>` tags
2
+
3
+ **Rule:** `html-anchor-require-href`
4
+
5
+ ## Description
6
+
7
+ Disallow the use of anchor tags without anhref attribute in HTML templates. Use if you want to perform an action without having the user navigated to a new URL.
8
+
9
+ ## Rationale
10
+
11
+ Anchor tags without href are unfocusable if user is using keyboard navigation, or is unseen by screen readers.
12
+
13
+ ## Examples
14
+
15
+ ### ✅ Good
16
+
17
+ ```erb
18
+ <a href="https://alink.com">I'm a real link</a>
19
+ ```
20
+
21
+ ### 🚫 Bad
22
+
23
+ ```erb
24
+ <a data-action="click->doSomething">I'm a fake link</a>
25
+ ```
26
+
27
+ ## References
28
+
29
+ * https://marcysutton.com/links-vs-buttons-in-modern-web-applications
30
+ * https://a11y-101.com/design/button-vs-link
31
+ * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role
32
+ * https://www.scottohara.me/blog/2021/05/28/disabled-links.html#w3c/html-aria#305
@@ -0,0 +1,34 @@
1
+ # Linter Rule: ARIA role with heading requires level
2
+
3
+ **Rule:** `html-aria-role-heading-requires-level`
4
+
5
+ ## Description
6
+
7
+ Ensure that any element with `role="heading"` also has a valid `aria-level` attribute. The `aria-level` defines the heading level (1–6) and is required for assistive technologies to properly interpret the document structure.
8
+
9
+ ## Rationale
10
+
11
+ In HTML, semantic heading elements like `<h1>` through `<h6>` implicitly define their level. When using `role="heading"` on non-semantic elements (e.g., `<div>`, `<span>`), the level must be explicitly declared using `aria-level`, otherwise screen readers and accessibility tools may not understand the document hierarchy.
12
+
13
+ ## Examples
14
+
15
+ ### ✅ Good
16
+
17
+ ```html
18
+ <div role="heading" aria-level="2">Section Title</div>
19
+
20
+ <span role="heading" aria-level="1">Main Title</span>
21
+ ```
22
+
23
+ ### 🚫 Bad
24
+
25
+ ```html
26
+ <div role="heading">Section Title</div>
27
+
28
+ <span role="heading">Main Title</span>
29
+ ```
30
+
31
+ ## References
32
+
33
+ * [ARIA: `heading` role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/heading_role)
34
+ * [ARIA: `aria-level` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-level)
@@ -0,0 +1,43 @@
1
+ # Linter Rule: Prefer double quotes for HTML Attribute values
2
+
3
+ **Rule:** `html-attribute-double-quotes`
4
+
5
+ ## Description
6
+
7
+ Prefer using double quotes (`"`) around HTML attribute values instead of single quotes (`'`).
8
+
9
+ **Exception:** Single quotes are allowed when the attribute value contains double quotes, as this avoids the need for escaping.
10
+
11
+ ## Rationale
12
+
13
+ Double quotes are the most widely used and expected style for HTML attributes. Consistent use of double quotes improves readability, reduces visual noise when mixing with embedded Ruby (which often uses single quotes), and avoids escaping conflicts when embedding attribute values that contain single quotes.
14
+
15
+ ## Examples
16
+
17
+ ### ✅ Good
18
+
19
+ ```html
20
+ <input type="text">
21
+
22
+ <a href="/profile">Profile</a>
23
+
24
+ <div data-action="click->dropdown#toggle"></div>
25
+
26
+ <!-- Exception: Single quotes allowed when value contains double quotes -->
27
+ <div id='"hello"' title='Say "Hello" to the world'></div>
28
+ ```
29
+
30
+
31
+ ### 🚫 Bad
32
+
33
+ ```html
34
+ <input type='text'>
35
+
36
+ <a href='/profile'>Profile</a>
37
+
38
+ <div data-action='click->dropdown#toggle'></div>
39
+ ```
40
+
41
+ ## References
42
+
43
+ * [HTML Living Standard - Attributes](https://html.spec.whatwg.org/multipage/syntax.html#attributes-2)
@@ -0,0 +1,43 @@
1
+ # Linter Rule: Always quote attribute values
2
+
3
+ **Rule:** `html-attribute-values-require-quotes`
4
+
5
+ ## Description
6
+
7
+ Always wrap HTML attribute values in quotes, even when they are technically optional according to the HTML specification.
8
+
9
+ ## Rationale
10
+
11
+ While some attribute values can be written without quotes if they don't contain spaces or special characters, omitting quotes makes the code harder to read, more error-prone, and inconsistent. Always quoting attribute values ensures:
12
+
13
+ - consistent appearance across all attributes,
14
+ - fewer surprises when attribute values contain special characters,
15
+ - easier editing and maintenance.
16
+
17
+ Additionally, always quoting is the common convention in most HTML formatters, linters, and developer tools.
18
+
19
+ ## Examples
20
+
21
+ ### ✅ Good
22
+
23
+ ```html
24
+ <div id="hello"></div>
25
+
26
+ <input type="text">
27
+
28
+ <a href="/profile">Profile</a>
29
+ ```
30
+
31
+ ### 🚫 Bad
32
+
33
+ ```html
34
+ <div id=hello></div>
35
+
36
+ <input type=text>
37
+
38
+ <a href=/profile></a>
39
+ ```
40
+
41
+ ## References
42
+
43
+ * [HTML Living Standard - Attributes](https://html.spec.whatwg.org/multipage/syntax.html#attributes-2)
@@ -0,0 +1,39 @@
1
+ # Linter Rule: Omit values for boolean attributes
2
+
3
+ **Rule:** `html-boolean-attributes-no-value`
4
+
5
+ ## Description
6
+
7
+ Omit attribute values for boolean HTML attributes. For boolean attributes, their presence alone represents `true`, and their absence represents `false`. There is no need to assign a value or use quotes.
8
+
9
+ ## Rationale
10
+
11
+ Using the canonical form for boolean attributes improves readability, keeps HTML concise, and avoids unnecessary characters. This also matches HTML specifications and the output of many HTML formatters.
12
+
13
+ For example, instead of writing `disabled="disabled"` or `disabled="true"`, simply write `disabled`.
14
+
15
+ ## Examples
16
+
17
+ ### ✅ Good
18
+
19
+ ```html
20
+ <input type="checkbox" checked>
21
+
22
+ <button disabled>Submit</button>
23
+
24
+ <select multiple>
25
+ ```
26
+
27
+ ### 🚫 Bad
28
+
29
+ ```html
30
+ <input type="checkbox" checked="checked">
31
+
32
+ <button disabled="true">Submit</button>
33
+
34
+ <select multiple="multiple">
35
+ ```
36
+
37
+ ## References
38
+
39
+ * [HTML Living Standard - Boolean Attributes](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes)
@@ -0,0 +1,44 @@
1
+ # Linter Rule: Require `alt` attribute on `<img>` tags
2
+
3
+ **Rule:** `html-img-require-alt`
4
+
5
+ ## Description
6
+
7
+ Enforce that all `<img>` elements include an `alt` attribute.
8
+
9
+ ## Rationale
10
+
11
+ The `alt` attribute provides alternative text for images, which is essential for accessibility (screen readers, assistive technologies), SEO, and proper fallback behavior when images fail to load. Even if the image is purely decorative, an empty `alt=""` should be provided to indicate that the image should be ignored by assistive technologies.
12
+
13
+ Omitting the `alt` attribute entirely leads to poor accessibility and can negatively affect user experience.
14
+
15
+ ## Examples
16
+
17
+ ### ✅ Good
18
+
19
+ ```erb
20
+ <img src="/logo.png" alt="Company logo">
21
+
22
+ <img src="/avatar.jpg" alt="<%= user.name %>'s profile picture">
23
+
24
+ <img src="/divider.png" alt="">
25
+
26
+ <%= image_tag image_path("logo.png"), alt: "Company logo" %>
27
+ ```
28
+
29
+ ### 🚫 Bad
30
+
31
+ ```erb
32
+ <img src="/logo.png">
33
+
34
+ <img src="/avatar.jpg" alt> <!-- TODO -->
35
+
36
+ <img src="/divider.png" alt=> <!-- TODO -->
37
+
38
+ <%= image_tag image_path("logo.png") %> <!-- TODO -->
39
+ ```
40
+
41
+ ## References
42
+
43
+ * [W3C: Alternative Text](https://www.w3.org/WAI/tutorials/images/)
44
+ * [WCAG 2.1: Non-text Content](https://www.w3.org/WAI/WCAG22/quickref/?versions=2.1#non-text-content)
@@ -0,0 +1,66 @@
1
+ # Linter Rule: No block elements inside inline elements
2
+
3
+ **Rule:** `html-no-block-inside-inline`
4
+
5
+ ## Description
6
+
7
+ Prevent block-level elements from being placed inside inline elements.
8
+
9
+ ## Rationale
10
+
11
+ Placing block-level elements (like `<div>`, `<p>`, `<section>`) inside inline elements (like `<span>`, `<a>`, `<strong>`) violates HTML content model rules and may lead to unpredictable rendering behavior across browsers.
12
+
13
+ This practice can cause:
14
+ - Invalid HTML that fails validation
15
+ - Inconsistent rendering across different browsers
16
+ - Layout issues and unexpected visual results
17
+ - Accessibility problems with screen readers
18
+
19
+ ## Examples
20
+
21
+
22
+ ### ✅ Good
23
+
24
+ ```erb
25
+ <span>
26
+ Hello <strong>World</strong>
27
+ </span>
28
+
29
+ <div>
30
+ <p>Paragraph inside div (valid)</p>
31
+ </div>
32
+
33
+ <a href="#">
34
+ <img src="icon.png" alt="Icon">
35
+ <span>Link text</span>
36
+ </a>
37
+ ```
38
+
39
+ ### 🚫 Bad
40
+
41
+ ```erb
42
+ <span>
43
+ <div>Invalid block inside span</div>
44
+ </span>
45
+
46
+ <span>
47
+ <p>Paragraph inside span (invalid)</p>
48
+ </span>
49
+
50
+ <a href="#">
51
+ <div class="card">
52
+ <h2>Card title</h2>
53
+ <p>Card content</p>
54
+ </div>
55
+ </a>
56
+
57
+ <strong>
58
+ <section>Section inside strong</section>
59
+ </strong>
60
+ ```
61
+
62
+ ## References
63
+
64
+ * [HTML Living Standard - Content models](https://html.spec.whatwg.org/multipage/dom.html#content-models)
65
+ * [MDN - Block-level elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements)
66
+ * [MDN - Inline elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements)
@@ -0,0 +1,35 @@
1
+ # Linter Rule: Disallow duplicate attributes on the same tag
2
+
3
+ **Rule:** `html-no-duplicate-attributes`
4
+
5
+ ## Description
6
+
7
+ Disallow having multiple attributes with the same name on a single HTML tag.
8
+
9
+ ## Rationale
10
+
11
+ Duplicate attributes on an HTML element are invalid and may lead to undefined or unexpected behavior across browsers. When duplicate attributes exist, the browser typically uses the last occurrence, but this behavior is not guaranteed to be consistent across all engines or future specifications.
12
+
13
+ Catching duplicates early helps prevent subtle bugs, improves code correctness, and avoids accidental overwrites of attribute values.
14
+
15
+ ## Examples
16
+
17
+ ### ✅ Good
18
+
19
+ ```erb
20
+ <input type="text" name="username" id="user-id">
21
+
22
+ <button type="submit" disabled>Submit</button>
23
+ ```
24
+
25
+ ### 🚫 Bad
26
+
27
+ ```erb
28
+ <input type="text" type="password" name="username">
29
+
30
+ <button type="submit" type="button" disabled>Submit</button>
31
+ ```
32
+
33
+ ## References
34
+
35
+ * [HTML Living Standard - Attributes](https://html.spec.whatwg.org/multipage/syntax.html#attributes-2)
@@ -0,0 +1,78 @@
1
+ # Linter Rule: Disallow empty headings
2
+
3
+ **Rule:** `html-no-empty-headings`
4
+
5
+ ## Description
6
+
7
+ Disallow headings (`h1`, `h2`, etc.) with no accessible text content.
8
+
9
+ ## Rationale
10
+
11
+ Headings relay the structure of a webpage and provide a meaningful, hierarchical order of its content. If headings are empty or its text contents are inaccessible, this could confuse users or prevent them accessing sections of interest.
12
+
13
+ ## Examples
14
+
15
+ ### ✅ Good
16
+
17
+ ```erb
18
+ <h*>Heading Content</h*>
19
+ ```
20
+
21
+ ```erb
22
+ <h*><span>Text</span><h*>
23
+ ```
24
+
25
+ ```erb
26
+ <div role="heading" aria-level="1">Heading Content</div>
27
+ ```
28
+
29
+ ```erb
30
+ <h* aria-hidden="true">Heading Content</h*>
31
+ ```
32
+
33
+ ```erb
34
+ <h* hidden>Heading Content</h*>
35
+ ```
36
+
37
+ ### 🚫 Bad
38
+
39
+ ```erb
40
+ <h1></h1>
41
+ ```
42
+
43
+ ```erb
44
+ <h2></h2>
45
+ ```
46
+
47
+ ```erb
48
+ <h3></h3>
49
+ ```
50
+
51
+ ```erb
52
+ <h4></h4>
53
+ ```
54
+
55
+ ```erb
56
+ <h5></h5>
57
+ ```
58
+
59
+ ```erb
60
+ <h6></h6>
61
+ ```
62
+
63
+ ```erb
64
+ <div role="heading" aria-level="1"></div>
65
+ ```
66
+
67
+ ```erb
68
+ <h1><span aria-hidden="true">Inaccessible text</span></h1>
69
+ ```
70
+
71
+ ## References
72
+
73
+ - [`<h1>`–`<h6>`: The HTML Section Heading elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/Heading_Elements)
74
+ - [ARIA: `heading` role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/heading_role)
75
+ - [ARIA: `aria-hidden` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-hidden)
76
+ - [ARIA: `aria-level` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-level)
77
+
78
+ Inspired by [ember-template-lint](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-empty-headings.md)
@@ -0,0 +1,44 @@
1
+ # Linter Rule: Disallow nested links
2
+
3
+ **Rule:** `html-no-nested-links`
4
+
5
+ ## Description
6
+
7
+ Disallow placing one `<a>` element inside another `<a>` element. Links must not contain other links as descendants.
8
+
9
+ ## Rationale
10
+
11
+ The HTML specification forbids nesting one anchor (`<a>`) inside another. Nested links result in invalid HTML, unpredictable click behavior, and inconsistent rendering across browsers.
12
+
13
+ Browsers may attempt error recovery when encountering nested links, but behavior varies and cannot be relied upon. This rule ensures strictly valid document structure and avoids subtle user interaction issues.
14
+
15
+ ## Examples
16
+
17
+ ### ✅ Good
18
+
19
+ ```erb
20
+ <a href="/products">View products</a>
21
+ <a href="/about">About us</a>
22
+
23
+ <%= link_to "View products", products_path %>
24
+ <%= link_to about_path do %>
25
+ About us
26
+ <% end %>
27
+ ```
28
+
29
+ ### 🚫 Bad
30
+
31
+ ```erb
32
+ <a href="/products">
33
+ View <a href="/special-offer">special offer</a>
34
+ </a>
35
+
36
+ <%= link_to "Products", products_path do %>
37
+ <%= link_to "Special offer", offer_path %> <!-- TODO -->
38
+ <% end %>
39
+ ```
40
+
41
+ ## References
42
+
43
+ * [HTML Living Standard - The a element](https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element)
44
+ * [Rails `link_to` helper](https://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to)
@@ -0,0 +1,44 @@
1
+ # Linter Rule: Enforce lowercase tag names
2
+
3
+ **Rule:** `html-tag-name-lowercase`
4
+
5
+ ## Description
6
+
7
+ Enforce that all HTML tag names are written in lowercase.
8
+
9
+ ## Rationale
10
+
11
+ HTML is case-insensitive for tag names, but lowercase is the widely accepted convention for writing HTML. Consistent lowercase tag names improve readability, maintain consistency across codebases, and align with the output of most HTML formatters and validators.
12
+
13
+ Writing tags in uppercase or mixed case can lead to inconsistent code and unnecessary diffs during reviews and merges.
14
+
15
+ ## Examples
16
+
17
+
18
+ ### ✅ Good
19
+
20
+ ```erb
21
+ <div class="container"></div>
22
+
23
+ <input type="text" name="username" />
24
+
25
+ <span>Label</span>
26
+
27
+ <%= content_tag(:div, "Hello world!") %>
28
+ ```
29
+
30
+ ### 🚫 Bad
31
+
32
+ ```erb
33
+ <DIV class="container"></DIV>
34
+
35
+ <Input type="text" name="username" />
36
+
37
+ <Span>Label</Span>
38
+
39
+ <%= content_tag(:DiV, "Hello world!") %> <!-- TODO -->
40
+ ```
41
+
42
+ ## References
43
+
44
+ * [HTML Living Standard - Tag Syntax](https://html.spec.whatwg.org/multipage/syntax.html#syntax-tags)
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@herb-tools/linter",
3
+ "version": "0.4.0",
4
+ "description": "HTML+ERB linter for validating HTML structure and enforcing best practices",
5
+ "license": "MIT",
6
+ "homepage": "https://herb-tools.dev",
7
+ "bugs": "https://github.com/marcoroth/herb/issues/new?title=Package%20%60@herb-tools/linter%60:%20",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/marcoroth/herb.git",
11
+ "directory": "javascript/packages/linter"
12
+ },
13
+ "main": "./dist/index.cjs",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/types/index.d.ts",
16
+ "bin": {
17
+ "herb-lint": "./bin/herb-lint"
18
+ },
19
+ "scripts": {
20
+ "clean": "rimraf dist",
21
+ "build": "yarn clean && tsc -b && rollup -c",
22
+ "watch": "tsc -b -w",
23
+ "test": "vitest run",
24
+ "prepublishOnly": "yarn clean && yarn build && yarn test"
25
+ },
26
+ "exports": {
27
+ "./package.json": "./package.json",
28
+ ".": {
29
+ "types": "./dist/types/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "require": "./dist/index.cjs",
32
+ "default": "./dist/index.js"
33
+ }
34
+ },
35
+ "dependencies": {
36
+ "@herb-tools/core": "0.4.0",
37
+ "@herb-tools/highlighter": "0.4.0",
38
+ "@herb-tools/node-wasm": "0.4.0",
39
+ "glob": "^11.0.3"
40
+ },
41
+ "files": [
42
+ "package.json",
43
+ "README.md",
44
+ "docs/",
45
+ "src/",
46
+ "bin/",
47
+ "dist/"
48
+ ]
49
+ }