@herb-tools/linter 0.5.0 → 0.6.1

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 (139) hide show
  1. package/dist/herb-lint.js +6627 -1937
  2. package/dist/herb-lint.js.map +1 -1
  3. package/dist/index.cjs +1574 -210
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +1566 -212
  6. package/dist/index.js.map +1 -1
  7. package/dist/package.json +5 -4
  8. package/dist/src/cli/argument-parser.js +0 -4
  9. package/dist/src/cli/argument-parser.js.map +1 -1
  10. package/dist/src/default-rules.js +20 -0
  11. package/dist/src/default-rules.js.map +1 -1
  12. package/dist/src/linter.js +29 -4
  13. package/dist/src/linter.js.map +1 -1
  14. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js +26 -0
  15. package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -0
  16. package/dist/src/rules/erb-prefer-image-tag-helper.js +50 -64
  17. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
  18. package/dist/src/rules/html-aria-attribute-must-be-valid.js +11 -10
  19. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
  20. package/dist/src/rules/html-aria-label-is-well-formatted.js +33 -0
  21. package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -0
  22. package/dist/src/rules/html-aria-level-must-be-valid.js +26 -4
  23. package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
  24. package/dist/src/rules/html-aria-role-heading-requires-level.js +7 -13
  25. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
  26. package/dist/src/rules/html-aria-role-must-be-valid.js +3 -3
  27. package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
  28. package/dist/src/rules/html-attribute-double-quotes.js +14 -4
  29. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
  30. package/dist/src/rules/html-attribute-equals-spacing.js +24 -0
  31. package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -0
  32. package/dist/src/rules/html-attribute-values-require-quotes.js +19 -8
  33. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
  34. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js +47 -0
  35. package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -0
  36. package/dist/src/rules/html-boolean-attributes-no-value.js +9 -2
  37. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  38. package/dist/src/rules/html-iframe-has-title.js +39 -0
  39. package/dist/src/rules/html-iframe-has-title.js.map +1 -0
  40. package/dist/src/rules/html-img-require-alt.js +0 -4
  41. package/dist/src/rules/html-img-require-alt.js.map +1 -1
  42. package/dist/src/rules/html-navigation-has-label.js +43 -0
  43. package/dist/src/rules/html-navigation-has-label.js.map +1 -0
  44. package/dist/src/rules/html-no-aria-hidden-on-focusable.js +67 -0
  45. package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -0
  46. package/dist/src/rules/html-no-duplicate-attributes.js +22 -25
  47. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
  48. package/dist/src/rules/html-no-duplicate-ids.js +134 -9
  49. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
  50. package/dist/src/rules/html-no-empty-headings.js +0 -21
  51. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  52. package/dist/src/rules/html-no-positive-tab-index.js +21 -0
  53. package/dist/src/rules/html-no-positive-tab-index.js.map +1 -0
  54. package/dist/src/rules/html-no-self-closing.js +29 -0
  55. package/dist/src/rules/html-no-self-closing.js.map +1 -0
  56. package/dist/src/rules/html-no-title-attribute.js +27 -0
  57. package/dist/src/rules/html-no-title-attribute.js.map +1 -0
  58. package/dist/src/rules/html-tag-name-lowercase.js +35 -23
  59. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  60. package/dist/src/rules/index.js +10 -0
  61. package/dist/src/rules/index.js.map +1 -1
  62. package/dist/src/rules/rule-utils.js +245 -22
  63. package/dist/src/rules/rule-utils.js.map +1 -1
  64. package/dist/src/rules/svg-tag-name-capitalization.js +0 -8
  65. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
  66. package/dist/src/types.js.map +1 -1
  67. package/dist/tsconfig.tsbuildinfo +1 -1
  68. package/dist/types/cli/index.d.ts +4 -0
  69. package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
  70. package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +7 -0
  71. package/dist/types/rules/html-attribute-equals-spacing.d.ts +7 -0
  72. package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
  73. package/dist/types/rules/html-iframe-has-title.d.ts +7 -0
  74. package/dist/types/rules/html-navigation-has-label.d.ts +7 -0
  75. package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
  76. package/dist/types/rules/html-no-positive-tab-index.d.ts +7 -0
  77. package/dist/types/rules/html-no-self-closing.d.ts +7 -0
  78. package/dist/types/rules/html-no-title-attribute.d.ts +7 -0
  79. package/dist/types/rules/html-tag-name-lowercase.d.ts +2 -1
  80. package/dist/types/rules/index.d.ts +10 -0
  81. package/dist/types/rules/rule-utils.d.ts +146 -13
  82. package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
  83. package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +7 -0
  84. package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +7 -0
  85. package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
  86. package/dist/types/src/rules/html-iframe-has-title.d.ts +7 -0
  87. package/dist/types/src/rules/html-navigation-has-label.d.ts +7 -0
  88. package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
  89. package/dist/types/src/rules/html-no-positive-tab-index.d.ts +7 -0
  90. package/dist/types/src/rules/html-no-self-closing.d.ts +7 -0
  91. package/dist/types/src/rules/html-no-title-attribute.d.ts +7 -0
  92. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +2 -1
  93. package/dist/types/src/rules/index.d.ts +10 -0
  94. package/dist/types/src/rules/rule-utils.d.ts +146 -13
  95. package/dist/types/src/types.d.ts +24 -0
  96. package/dist/types/types.d.ts +24 -0
  97. package/docs/rules/README.md +12 -2
  98. package/docs/rules/erb-no-silent-tag-in-attribute-name.md +34 -0
  99. package/docs/rules/html-aria-label-is-well-formatted.md +49 -0
  100. package/docs/rules/html-attribute-equals-spacing.md +35 -0
  101. package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +48 -0
  102. package/docs/rules/html-iframe-has-title.md +43 -0
  103. package/docs/rules/html-navigation-has-label.md +61 -0
  104. package/docs/rules/html-no-aria-hidden-on-focusable.md +54 -0
  105. package/docs/rules/html-no-positive-tab-index.md +55 -0
  106. package/docs/rules/html-no-self-closing.md +65 -0
  107. package/docs/rules/html-no-title-attribute.md +69 -0
  108. package/docs/rules/html-tag-name-lowercase.md +16 -3
  109. package/package.json +5 -4
  110. package/src/cli/argument-parser.ts +0 -5
  111. package/src/default-rules.ts +20 -0
  112. package/src/linter.ts +30 -4
  113. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +40 -0
  114. package/src/rules/erb-prefer-image-tag-helper.ts +53 -76
  115. package/src/rules/html-aria-attribute-must-be-valid.ts +28 -32
  116. package/src/rules/html-aria-label-is-well-formatted.ts +59 -0
  117. package/src/rules/html-aria-level-must-be-valid.ts +38 -5
  118. package/src/rules/html-aria-role-heading-requires-level.ts +16 -28
  119. package/src/rules/html-aria-role-must-be-valid.ts +5 -5
  120. package/src/rules/html-attribute-double-quotes.ts +21 -6
  121. package/src/rules/html-attribute-equals-spacing.ts +41 -0
  122. package/src/rules/html-attribute-values-require-quotes.ts +29 -9
  123. package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +66 -0
  124. package/src/rules/html-boolean-attributes-no-value.ts +17 -4
  125. package/src/rules/html-iframe-has-title.ts +62 -0
  126. package/src/rules/html-img-require-alt.ts +2 -7
  127. package/src/rules/html-navigation-has-label.ts +64 -0
  128. package/src/rules/html-no-aria-hidden-on-focusable.ts +90 -0
  129. package/src/rules/html-no-duplicate-attributes.ts +28 -28
  130. package/src/rules/html-no-duplicate-ids.ts +189 -14
  131. package/src/rules/html-no-empty-headings.ts +2 -31
  132. package/src/rules/html-no-positive-tab-index.ts +33 -0
  133. package/src/rules/html-no-self-closing.ts +41 -0
  134. package/src/rules/html-no-title-attribute.ts +42 -0
  135. package/src/rules/html-tag-name-lowercase.ts +42 -29
  136. package/src/rules/index.ts +10 -0
  137. package/src/rules/rule-utils.ts +357 -39
  138. package/src/rules/svg-tag-name-capitalization.ts +2 -9
  139. package/src/types.ts +27 -0
@@ -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";
@@ -1,6 +1,11 @@
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
+ import type * as Nodes from "@herb-tools/core";
3
4
  import type { LintOffense, LintSeverity, LintContext } from "../types.js";
5
+ export declare enum ControlFlowType {
6
+ CONDITIONAL = 0,
7
+ LOOP = 1
8
+ }
4
9
  /**
5
10
  * Base visitor class that provides common functionality for rule visitors
6
11
  */
@@ -19,17 +24,86 @@ export declare abstract class BaseRuleVisitor extends Visitor {
19
24
  protected addOffense(message: string, location: Location, severity?: LintSeverity): void;
20
25
  }
21
26
  /**
22
- * Gets attributes from either an HTMLOpenTagNode or HTMLSelfCloseTagNode
27
+ * Mixin that adds control flow tracking capabilities to rule visitors
28
+ * This allows rules to track state across different control flow structures
29
+ * like if/else branches, loops, etc.
30
+ *
31
+ * @template TControlFlowState - Type for state passed between onEnterControlFlow and onExitControlFlow
32
+ * @template TBranchState - Type for state passed between onEnterBranch and onExitBranch
23
33
  */
24
- export declare function getAttributes(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): any[];
34
+ export declare abstract class ControlFlowTrackingVisitor<TControlFlowState = any, TBranchState = any> extends BaseRuleVisitor {
35
+ protected isInControlFlow: boolean;
36
+ protected currentControlFlowType: ControlFlowType | null;
37
+ /**
38
+ * Handle visiting a control flow node with proper scope management
39
+ */
40
+ protected handleControlFlowNode(node: Node, controlFlowType: ControlFlowType, visitChildren: () => void): void;
41
+ /**
42
+ * Handle visiting a branch node (like else, when) with proper scope management
43
+ */
44
+ protected startNewBranch(visitChildren: () => void): void;
45
+ visitERBIfNode(node: Nodes.ERBIfNode): void;
46
+ visitERBUnlessNode(node: Nodes.ERBUnlessNode): void;
47
+ visitERBCaseNode(node: Nodes.ERBCaseNode): void;
48
+ visitERBCaseMatchNode(node: Nodes.ERBCaseMatchNode): void;
49
+ visitERBWhileNode(node: Nodes.ERBWhileNode): void;
50
+ visitERBForNode(node: Nodes.ERBForNode): void;
51
+ visitERBUntilNode(node: Nodes.ERBUntilNode): void;
52
+ visitERBBlockNode(node: Nodes.ERBBlockNode): void;
53
+ visitERBElseNode(node: Nodes.ERBElseNode): void;
54
+ visitERBWhenNode(node: Nodes.ERBWhenNode): void;
55
+ protected abstract onEnterControlFlow(controlFlowType: ControlFlowType, wasAlreadyInControlFlow: boolean): TControlFlowState;
56
+ protected abstract onExitControlFlow(controlFlowType: ControlFlowType, wasAlreadyInControlFlow: boolean, stateToRestore: TControlFlowState): void;
57
+ protected abstract onEnterBranch(): TBranchState;
58
+ protected abstract onExitBranch(stateToRestore: TBranchState): void;
59
+ }
60
+ /**
61
+ * Gets attributes from an HTMLOpenTagNode
62
+ */
63
+ export declare function getAttributes(node: HTMLOpenTagNode): HTMLAttributeNode[];
25
64
  /**
26
65
  * Gets the tag name from an HTML tag node (lowercased)
27
66
  */
28
- export declare function getTagName(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): string | null;
67
+ export declare function getTagName(node: HTMLOpenTagNode): string | null;
29
68
  /**
30
69
  * Gets the attribute name from an HTMLAttributeNode (lowercased)
70
+ * Returns null if the attribute name contains dynamic content (ERB)
31
71
  */
32
72
  export declare function getAttributeName(attributeNode: HTMLAttributeNode): string | null;
73
+ /**
74
+ * Checks if an attribute has a dynamic (ERB-containing) name
75
+ */
76
+ export declare function hasDynamicAttributeName(attributeNode: HTMLAttributeNode): boolean;
77
+ /**
78
+ * Gets the combined string representation of an attribute name (for debugging)
79
+ * This includes both static content and ERB syntax
80
+ */
81
+ export declare function getCombinedAttributeNameString(attributeNode: HTMLAttributeNode): string;
82
+ /**
83
+ * Checks if an attribute value contains only static content (no ERB)
84
+ */
85
+ export declare function hasStaticAttributeValue(attributeNode: HTMLAttributeNode): boolean;
86
+ /**
87
+ * Checks if an attribute value contains dynamic content (ERB)
88
+ */
89
+ export declare function hasDynamicAttributeValue(attributeNode: HTMLAttributeNode): boolean;
90
+ /**
91
+ * Gets the static string value of an attribute (returns null if it contains ERB)
92
+ */
93
+ export declare function getStaticAttributeValue(attributeNode: HTMLAttributeNode): string | null;
94
+ /**
95
+ * Gets the value nodes array for dynamic inspection
96
+ */
97
+ export declare function getAttributeValueNodes(attributeNode: HTMLAttributeNode): Node[];
98
+ /**
99
+ * Checks if an attribute value contains any static content (for validation purposes)
100
+ */
101
+ export declare function hasStaticAttributeValueContent(attributeNode: HTMLAttributeNode): boolean;
102
+ /**
103
+ * Gets the static content of an attribute value (all literal parts combined)
104
+ * Returns the concatenated literal content, or null if no literal nodes exist
105
+ */
106
+ export declare function getStaticAttributeValueContent(attributeNode: HTMLAttributeNode): string | null;
33
107
  /**
34
108
  * Gets the attribute value content from an HTMLAttributeValueNode
35
109
  */
@@ -41,20 +115,25 @@ export declare function hasAttributeValue(attributeNode: HTMLAttributeNode): boo
41
115
  /**
42
116
  * Gets the quote type used for an attribute value
43
117
  */
44
- export declare function getAttributeValueQuoteType(attributeNode: HTMLAttributeNode): "single" | "double" | "none" | null;
118
+ export declare function getAttributeValueQuoteType(nodeOrAttribute: HTMLAttributeNode | HTMLAttributeValueNode): "single" | "double" | "none" | null;
45
119
  /**
46
120
  * Finds an attribute by name in a list of attributes
47
121
  */
48
- export declare function findAttributeByName(attributes: any[], attributeName: string): HTMLAttributeNode | null;
122
+ export declare function findAttributeByName(attributes: Node[], attributeName: string): HTMLAttributeNode | null;
123
+ /**
124
+ * Checks if a tag has a specific attribute
125
+ */
126
+ export declare function hasAttribute(node: HTMLOpenTagNode, attributeName: string): boolean;
49
127
  /**
50
128
  * Checks if a tag has a specific attribute
51
129
  */
52
- export declare function hasAttribute(node: HTMLOpenTagNode | HTMLSelfCloseTagNode, attributeName: string): boolean;
130
+ export declare function getAttribute(node: HTMLOpenTagNode, attributeName: string): HTMLAttributeNode | null;
53
131
  /**
54
132
  * Common HTML element categorization
55
133
  */
56
134
  export declare const HTML_INLINE_ELEMENTS: Set<string>;
57
135
  export declare const HTML_BLOCK_ELEMENTS: Set<string>;
136
+ export declare const HTML_VOID_ELEMENTS: Set<string>;
58
137
  export declare const HTML_BOOLEAN_ATTRIBUTES: Set<string>;
59
138
  export declare const HEADING_TAGS: Set<string>;
60
139
  /**
@@ -67,6 +146,37 @@ export declare const SVG_CAMEL_CASE_ELEMENTS: Set<string>;
67
146
  */
68
147
  export declare const SVG_LOWERCASE_TO_CAMELCASE: Map<string, string>;
69
148
  export declare const VALID_ARIA_ROLES: Set<string>;
149
+ /**
150
+ * Parameter types for AttributeVisitorMixin methods
151
+ */
152
+ export interface StaticAttributeStaticValueParams {
153
+ attributeName: string;
154
+ attributeValue: string;
155
+ attributeNode: HTMLAttributeNode;
156
+ parentNode: HTMLOpenTagNode;
157
+ }
158
+ export interface StaticAttributeDynamicValueParams {
159
+ attributeName: string;
160
+ valueNodes: Node[];
161
+ attributeNode: HTMLAttributeNode;
162
+ parentNode: HTMLOpenTagNode;
163
+ combinedValue?: string | null;
164
+ }
165
+ export interface DynamicAttributeStaticValueParams {
166
+ nameNodes: Node[];
167
+ attributeValue: string;
168
+ attributeNode: HTMLAttributeNode;
169
+ parentNode: HTMLOpenTagNode;
170
+ combinedName?: string;
171
+ }
172
+ export interface DynamicAttributeDynamicValueParams {
173
+ nameNodes: Node[];
174
+ valueNodes: Node[];
175
+ attributeNode: HTMLAttributeNode;
176
+ parentNode: HTMLOpenTagNode;
177
+ combinedName?: string;
178
+ combinedValue?: string | null;
179
+ }
70
180
  export declare const ARIA_ATTRIBUTES: Set<string>;
71
181
  /**
72
182
  * Helper function to create a location at the end of the source with a 1-character range
@@ -80,21 +190,44 @@ export declare function isInlineElement(tagName: string): boolean;
80
190
  * Checks if an element is block-level
81
191
  */
82
192
  export declare function isBlockElement(tagName: string): boolean;
193
+ /**
194
+ * Checks if an element is a void element
195
+ */
196
+ export declare function isVoidElement(tagName: string): boolean;
83
197
  /**
84
198
  * Checks if an attribute is a boolean attribute
85
199
  */
86
200
  export declare function isBooleanAttribute(attributeName: string): boolean;
87
201
  /**
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.
202
+ * Attribute visitor that provides granular processing based on both
203
+ * attribute name type (static/dynamic) and value type (static/dynamic)
204
+ *
205
+ * This gives you 4 distinct methods to override:
206
+ * - checkStaticAttributeStaticValue() - name="class" value="foo"
207
+ * - checkStaticAttributeDynamicValue() - name="class" value="<%= css_class %>"
208
+ * - checkDynamicAttributeStaticValue() - name="data-<%= key %>" value="foo"
209
+ * - checkDynamicAttributeDynamicValue() - name="data-<%= key %>" value="<%= value %>"
91
210
  */
92
211
  export declare abstract class AttributeVisitorMixin extends BaseRuleVisitor {
93
212
  constructor(ruleName: string, context?: Partial<LintContext>);
94
213
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void;
95
- visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void;
96
214
  private checkAttributesOnNode;
97
- protected abstract checkAttribute(attributeName: string, attributeValue: string | null, attributeNode: HTMLAttributeNode, parentNode: HTMLOpenTagNode | HTMLSelfCloseTagNode): void;
215
+ /**
216
+ * Static attribute name with static value: class="container"
217
+ */
218
+ protected checkStaticAttributeStaticValue(params: StaticAttributeStaticValueParams): void;
219
+ /**
220
+ * Static attribute name with dynamic value: class="<%= css_class %>"
221
+ */
222
+ protected checkStaticAttributeDynamicValue(params: StaticAttributeDynamicValueParams): void;
223
+ /**
224
+ * Dynamic attribute name with static value: data-<%= key %>="foo"
225
+ */
226
+ protected checkDynamicAttributeStaticValue(params: DynamicAttributeStaticValueParams): void;
227
+ /**
228
+ * Dynamic attribute name with dynamic value: data-<%= key %>="<%= value %>"
229
+ */
230
+ protected checkDynamicAttributeDynamicValue(params: DynamicAttributeDynamicValueParams): void;
98
231
  }
99
232
  /**
100
233
  * Checks if an attribute value is quoted
@@ -103,7 +236,7 @@ export declare function isAttributeValueQuoted(attributeNode: HTMLAttributeNode)
103
236
  /**
104
237
  * Iterates over all attributes of a tag node, calling the callback for each attribute
105
238
  */
106
- export declare function forEachAttribute(node: HTMLOpenTagNode | HTMLSelfCloseTagNode, callback: (attributeNode: HTMLAttributeNode) => void): void;
239
+ export declare function forEachAttribute(node: HTMLOpenTagNode, callback: (attributeNode: HTMLAttributeNode) => void): void;
107
240
  /**
108
241
  * Base lexer visitor class that provides common functionality for lexer-based rule visitors
109
242
  */
@@ -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 ERBNoSilentTagInAttributeNameRule 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 HTMLAriaLabelIsWellFormattedRule 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 HTMLAttributeEqualsSpacingRule 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 HTMLAvoidBothDisabledAndAriaDisabledRule 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 HTMLIframeHasTitleRule 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 HTMLNavigationHasLabelRule 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 HTMLNoAriaHiddenOnFocusableRule 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 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 { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLTagNameLowercaseRule extends ParserRule {
5
5
  name: string;
6
+ isEnabled(result: ParseResult, context?: Partial<LintContext>): boolean;
6
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";
@@ -1,6 +1,11 @@
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
+ import type * as Nodes from "@herb-tools/core";
3
4
  import type { LintOffense, LintSeverity, LintContext } from "../types.js";
5
+ export declare enum ControlFlowType {
6
+ CONDITIONAL = 0,
7
+ LOOP = 1
8
+ }
4
9
  /**
5
10
  * Base visitor class that provides common functionality for rule visitors
6
11
  */
@@ -19,17 +24,86 @@ export declare abstract class BaseRuleVisitor extends Visitor {
19
24
  protected addOffense(message: string, location: Location, severity?: LintSeverity): void;
20
25
  }
21
26
  /**
22
- * Gets attributes from either an HTMLOpenTagNode or HTMLSelfCloseTagNode
27
+ * Mixin that adds control flow tracking capabilities to rule visitors
28
+ * This allows rules to track state across different control flow structures
29
+ * like if/else branches, loops, etc.
30
+ *
31
+ * @template TControlFlowState - Type for state passed between onEnterControlFlow and onExitControlFlow
32
+ * @template TBranchState - Type for state passed between onEnterBranch and onExitBranch
23
33
  */
24
- export declare function getAttributes(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): any[];
34
+ export declare abstract class ControlFlowTrackingVisitor<TControlFlowState = any, TBranchState = any> extends BaseRuleVisitor {
35
+ protected isInControlFlow: boolean;
36
+ protected currentControlFlowType: ControlFlowType | null;
37
+ /**
38
+ * Handle visiting a control flow node with proper scope management
39
+ */
40
+ protected handleControlFlowNode(node: Node, controlFlowType: ControlFlowType, visitChildren: () => void): void;
41
+ /**
42
+ * Handle visiting a branch node (like else, when) with proper scope management
43
+ */
44
+ protected startNewBranch(visitChildren: () => void): void;
45
+ visitERBIfNode(node: Nodes.ERBIfNode): void;
46
+ visitERBUnlessNode(node: Nodes.ERBUnlessNode): void;
47
+ visitERBCaseNode(node: Nodes.ERBCaseNode): void;
48
+ visitERBCaseMatchNode(node: Nodes.ERBCaseMatchNode): void;
49
+ visitERBWhileNode(node: Nodes.ERBWhileNode): void;
50
+ visitERBForNode(node: Nodes.ERBForNode): void;
51
+ visitERBUntilNode(node: Nodes.ERBUntilNode): void;
52
+ visitERBBlockNode(node: Nodes.ERBBlockNode): void;
53
+ visitERBElseNode(node: Nodes.ERBElseNode): void;
54
+ visitERBWhenNode(node: Nodes.ERBWhenNode): void;
55
+ protected abstract onEnterControlFlow(controlFlowType: ControlFlowType, wasAlreadyInControlFlow: boolean): TControlFlowState;
56
+ protected abstract onExitControlFlow(controlFlowType: ControlFlowType, wasAlreadyInControlFlow: boolean, stateToRestore: TControlFlowState): void;
57
+ protected abstract onEnterBranch(): TBranchState;
58
+ protected abstract onExitBranch(stateToRestore: TBranchState): void;
59
+ }
60
+ /**
61
+ * Gets attributes from an HTMLOpenTagNode
62
+ */
63
+ export declare function getAttributes(node: HTMLOpenTagNode): HTMLAttributeNode[];
25
64
  /**
26
65
  * Gets the tag name from an HTML tag node (lowercased)
27
66
  */
28
- export declare function getTagName(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): string | null;
67
+ export declare function getTagName(node: HTMLOpenTagNode): string | null;
29
68
  /**
30
69
  * Gets the attribute name from an HTMLAttributeNode (lowercased)
70
+ * Returns null if the attribute name contains dynamic content (ERB)
31
71
  */
32
72
  export declare function getAttributeName(attributeNode: HTMLAttributeNode): string | null;
73
+ /**
74
+ * Checks if an attribute has a dynamic (ERB-containing) name
75
+ */
76
+ export declare function hasDynamicAttributeName(attributeNode: HTMLAttributeNode): boolean;
77
+ /**
78
+ * Gets the combined string representation of an attribute name (for debugging)
79
+ * This includes both static content and ERB syntax
80
+ */
81
+ export declare function getCombinedAttributeNameString(attributeNode: HTMLAttributeNode): string;
82
+ /**
83
+ * Checks if an attribute value contains only static content (no ERB)
84
+ */
85
+ export declare function hasStaticAttributeValue(attributeNode: HTMLAttributeNode): boolean;
86
+ /**
87
+ * Checks if an attribute value contains dynamic content (ERB)
88
+ */
89
+ export declare function hasDynamicAttributeValue(attributeNode: HTMLAttributeNode): boolean;
90
+ /**
91
+ * Gets the static string value of an attribute (returns null if it contains ERB)
92
+ */
93
+ export declare function getStaticAttributeValue(attributeNode: HTMLAttributeNode): string | null;
94
+ /**
95
+ * Gets the value nodes array for dynamic inspection
96
+ */
97
+ export declare function getAttributeValueNodes(attributeNode: HTMLAttributeNode): Node[];
98
+ /**
99
+ * Checks if an attribute value contains any static content (for validation purposes)
100
+ */
101
+ export declare function hasStaticAttributeValueContent(attributeNode: HTMLAttributeNode): boolean;
102
+ /**
103
+ * Gets the static content of an attribute value (all literal parts combined)
104
+ * Returns the concatenated literal content, or null if no literal nodes exist
105
+ */
106
+ export declare function getStaticAttributeValueContent(attributeNode: HTMLAttributeNode): string | null;
33
107
  /**
34
108
  * Gets the attribute value content from an HTMLAttributeValueNode
35
109
  */
@@ -41,20 +115,25 @@ export declare function hasAttributeValue(attributeNode: HTMLAttributeNode): boo
41
115
  /**
42
116
  * Gets the quote type used for an attribute value
43
117
  */
44
- export declare function getAttributeValueQuoteType(attributeNode: HTMLAttributeNode): "single" | "double" | "none" | null;
118
+ export declare function getAttributeValueQuoteType(nodeOrAttribute: HTMLAttributeNode | HTMLAttributeValueNode): "single" | "double" | "none" | null;
45
119
  /**
46
120
  * Finds an attribute by name in a list of attributes
47
121
  */
48
- export declare function findAttributeByName(attributes: any[], attributeName: string): HTMLAttributeNode | null;
122
+ export declare function findAttributeByName(attributes: Node[], attributeName: string): HTMLAttributeNode | null;
123
+ /**
124
+ * Checks if a tag has a specific attribute
125
+ */
126
+ export declare function hasAttribute(node: HTMLOpenTagNode, attributeName: string): boolean;
49
127
  /**
50
128
  * Checks if a tag has a specific attribute
51
129
  */
52
- export declare function hasAttribute(node: HTMLOpenTagNode | HTMLSelfCloseTagNode, attributeName: string): boolean;
130
+ export declare function getAttribute(node: HTMLOpenTagNode, attributeName: string): HTMLAttributeNode | null;
53
131
  /**
54
132
  * Common HTML element categorization
55
133
  */
56
134
  export declare const HTML_INLINE_ELEMENTS: Set<string>;
57
135
  export declare const HTML_BLOCK_ELEMENTS: Set<string>;
136
+ export declare const HTML_VOID_ELEMENTS: Set<string>;
58
137
  export declare const HTML_BOOLEAN_ATTRIBUTES: Set<string>;
59
138
  export declare const HEADING_TAGS: Set<string>;
60
139
  /**
@@ -67,6 +146,37 @@ export declare const SVG_CAMEL_CASE_ELEMENTS: Set<string>;
67
146
  */
68
147
  export declare const SVG_LOWERCASE_TO_CAMELCASE: Map<string, string>;
69
148
  export declare const VALID_ARIA_ROLES: Set<string>;
149
+ /**
150
+ * Parameter types for AttributeVisitorMixin methods
151
+ */
152
+ export interface StaticAttributeStaticValueParams {
153
+ attributeName: string;
154
+ attributeValue: string;
155
+ attributeNode: HTMLAttributeNode;
156
+ parentNode: HTMLOpenTagNode;
157
+ }
158
+ export interface StaticAttributeDynamicValueParams {
159
+ attributeName: string;
160
+ valueNodes: Node[];
161
+ attributeNode: HTMLAttributeNode;
162
+ parentNode: HTMLOpenTagNode;
163
+ combinedValue?: string | null;
164
+ }
165
+ export interface DynamicAttributeStaticValueParams {
166
+ nameNodes: Node[];
167
+ attributeValue: string;
168
+ attributeNode: HTMLAttributeNode;
169
+ parentNode: HTMLOpenTagNode;
170
+ combinedName?: string;
171
+ }
172
+ export interface DynamicAttributeDynamicValueParams {
173
+ nameNodes: Node[];
174
+ valueNodes: Node[];
175
+ attributeNode: HTMLAttributeNode;
176
+ parentNode: HTMLOpenTagNode;
177
+ combinedName?: string;
178
+ combinedValue?: string | null;
179
+ }
70
180
  export declare const ARIA_ATTRIBUTES: Set<string>;
71
181
  /**
72
182
  * Helper function to create a location at the end of the source with a 1-character range
@@ -80,21 +190,44 @@ export declare function isInlineElement(tagName: string): boolean;
80
190
  * Checks if an element is block-level
81
191
  */
82
192
  export declare function isBlockElement(tagName: string): boolean;
193
+ /**
194
+ * Checks if an element is a void element
195
+ */
196
+ export declare function isVoidElement(tagName: string): boolean;
83
197
  /**
84
198
  * Checks if an attribute is a boolean attribute
85
199
  */
86
200
  export declare function isBooleanAttribute(attributeName: string): boolean;
87
201
  /**
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.
202
+ * Attribute visitor that provides granular processing based on both
203
+ * attribute name type (static/dynamic) and value type (static/dynamic)
204
+ *
205
+ * This gives you 4 distinct methods to override:
206
+ * - checkStaticAttributeStaticValue() - name="class" value="foo"
207
+ * - checkStaticAttributeDynamicValue() - name="class" value="<%= css_class %>"
208
+ * - checkDynamicAttributeStaticValue() - name="data-<%= key %>" value="foo"
209
+ * - checkDynamicAttributeDynamicValue() - name="data-<%= key %>" value="<%= value %>"
91
210
  */
92
211
  export declare abstract class AttributeVisitorMixin extends BaseRuleVisitor {
93
212
  constructor(ruleName: string, context?: Partial<LintContext>);
94
213
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void;
95
- visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void;
96
214
  private checkAttributesOnNode;
97
- protected abstract checkAttribute(attributeName: string, attributeValue: string | null, attributeNode: HTMLAttributeNode, parentNode: HTMLOpenTagNode | HTMLSelfCloseTagNode): void;
215
+ /**
216
+ * Static attribute name with static value: class="container"
217
+ */
218
+ protected checkStaticAttributeStaticValue(params: StaticAttributeStaticValueParams): void;
219
+ /**
220
+ * Static attribute name with dynamic value: class="<%= css_class %>"
221
+ */
222
+ protected checkStaticAttributeDynamicValue(params: StaticAttributeDynamicValueParams): void;
223
+ /**
224
+ * Dynamic attribute name with static value: data-<%= key %>="foo"
225
+ */
226
+ protected checkDynamicAttributeStaticValue(params: DynamicAttributeStaticValueParams): void;
227
+ /**
228
+ * Dynamic attribute name with dynamic value: data-<%= key %>="<%= value %>"
229
+ */
230
+ protected checkDynamicAttributeDynamicValue(params: DynamicAttributeDynamicValueParams): void;
98
231
  }
99
232
  /**
100
233
  * Checks if an attribute value is quoted
@@ -103,7 +236,7 @@ export declare function isAttributeValueQuoted(attributeNode: HTMLAttributeNode)
103
236
  /**
104
237
  * Iterates over all attributes of a tag node, calling the callback for each attribute
105
238
  */
106
- export declare function forEachAttribute(node: HTMLOpenTagNode | HTMLSelfCloseTagNode, callback: (attributeNode: HTMLAttributeNode) => void): void;
239
+ export declare function forEachAttribute(node: HTMLOpenTagNode, callback: (attributeNode: HTMLAttributeNode) => void): void;
107
240
  /**
108
241
  * Base lexer visitor class that provides common functionality for lexer-based rule visitors
109
242
  */
@@ -19,11 +19,27 @@ export declare abstract class ParserRule {
19
19
  static type: "parser";
20
20
  abstract name: string;
21
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";