@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
package/src/cli.ts CHANGED
@@ -1,14 +1,41 @@
1
1
  import { glob } from "glob"
2
2
  import { Herb } from "@herb-tools/node-wasm"
3
- import { ArgumentParser } from "./cli/argument-parser.js"
3
+ import { ArgumentParser, type FormatOption } from "./cli/argument-parser.js"
4
4
  import { FileProcessor } from "./cli/file-processor.js"
5
- import { SimpleFormatter, DetailedFormatter } from "./cli/formatters/index.js"
6
- import { SummaryReporter } from "./cli/summary-reporter.js"
5
+ import { OutputManager } from "./cli/output-manager.js"
7
6
 
8
7
  export class CLI {
9
8
  private argumentParser = new ArgumentParser()
10
9
  private fileProcessor = new FileProcessor()
11
- private summaryReporter = new SummaryReporter()
10
+ private outputManager = new OutputManager()
11
+
12
+ private exitWithError(message: string, formatOption: FormatOption, exitCode: number = 1) {
13
+ this.outputManager.outputError(message, {
14
+ formatOption,
15
+ theme: 'auto',
16
+ wrapLines: false,
17
+ truncateLines: false,
18
+ showTiming: false,
19
+ startTime: 0,
20
+ startDate: new Date()
21
+ })
22
+ process.exit(exitCode)
23
+ }
24
+
25
+ private exitWithInfo(message: string, formatOption: FormatOption, exitCode: number = 0, timingData?: { startTime: number, startDate: Date, showTiming: boolean }) {
26
+ const outputOptions = {
27
+ formatOption,
28
+ theme: 'auto' as const,
29
+ wrapLines: false,
30
+ truncateLines: false,
31
+ showTiming: timingData?.showTiming ?? false,
32
+ startTime: timingData?.startTime ?? Date.now(),
33
+ startDate: timingData?.startDate ?? new Date()
34
+ }
35
+
36
+ this.outputManager.outputInfo(message, outputOptions)
37
+ process.exit(exitCode)
38
+ }
12
39
 
13
40
  async run() {
14
41
  const startTime = Date.now()
@@ -16,45 +43,35 @@ export class CLI {
16
43
 
17
44
  const { pattern, formatOption, showTiming, theme, wrapLines, truncateLines } = this.argumentParser.parse(process.argv)
18
45
 
46
+ const outputOptions = {
47
+ formatOption,
48
+ theme,
49
+ wrapLines,
50
+ truncateLines,
51
+ showTiming,
52
+ startTime,
53
+ startDate
54
+ }
55
+
19
56
  try {
20
57
  await Herb.load()
21
58
 
22
59
  const files = await glob(pattern)
23
60
 
24
61
  if (files.length === 0) {
25
- console.log(`No files found matching pattern: ${pattern}`)
26
- process.exit(0)
62
+ this.exitWithInfo(`No files found matching pattern: ${pattern}`, formatOption, 0, { startTime, startDate, showTiming })
27
63
  }
28
64
 
29
- const results = await this.fileProcessor.processFiles(files)
30
- const { totalErrors, totalWarnings, filesWithIssues, ruleCount, allDiagnostics, ruleViolations } = results
31
-
32
- const formatter = formatOption === 'simple'
33
- ? new SimpleFormatter()
34
- : new DetailedFormatter(theme, wrapLines, truncateLines)
35
-
36
- await formatter.format(allDiagnostics, files.length === 1)
37
-
38
- this.summaryReporter.displayMostViolatedRules(ruleViolations)
39
- this.summaryReporter.displaySummary({
40
- files,
41
- totalErrors,
42
- totalWarnings,
43
- filesWithViolations: filesWithIssues,
44
- ruleCount,
45
- startTime,
46
- startDate,
47
- showTiming,
48
- ruleViolations
49
- })
50
-
51
- if (totalErrors > 0) {
65
+ const results = await this.fileProcessor.processFiles(files, formatOption)
66
+
67
+ await this.outputManager.outputResults({ ...results, files }, outputOptions)
68
+
69
+ if (results.totalErrors > 0) {
52
70
  process.exit(1)
53
71
  }
54
72
 
55
73
  } catch (error) {
56
- console.error(`Error:`, error)
57
- process.exit(1)
74
+ this.exitWithError(`Error: ${error}`, formatOption)
58
75
  }
59
76
  }
60
77
  }
@@ -2,46 +2,68 @@ import type { RuleClass } from "./types.js"
2
2
 
3
3
  import { ERBNoEmptyTagsRule } from "./rules/erb-no-empty-tags.js"
4
4
  import { ERBNoOutputControlFlowRule } from "./rules/erb-no-output-control-flow.js"
5
+ import { ERBNoSilentTagInAttributeNameRule } from "./rules/erb-no-silent-tag-in-attribute-name.js"
5
6
  import { ERBPreferImageTagHelperRule } from "./rules/erb-prefer-image-tag-helper.js"
6
7
  import { ERBRequiresTrailingNewlineRule } from "./rules/erb-requires-trailing-newline.js"
7
8
  import { ERBRequireWhitespaceRule } from "./rules/erb-require-whitespace-inside-tags.js"
8
9
  import { HTMLAnchorRequireHrefRule } from "./rules/html-anchor-require-href.js"
9
10
  import { HTMLAriaAttributeMustBeValid } from "./rules/html-aria-attribute-must-be-valid.js"
11
+ import { HTMLAriaLabelIsWellFormattedRule } from "./rules/html-aria-label-is-well-formatted.js"
10
12
  import { HTMLAriaLevelMustBeValidRule } from "./rules/html-aria-level-must-be-valid.js"
11
13
  import { HTMLAriaRoleHeadingRequiresLevelRule } from "./rules/html-aria-role-heading-requires-level.js"
12
14
  import { HTMLAriaRoleMustBeValidRule } from "./rules/html-aria-role-must-be-valid.js"
13
15
  import { HTMLAttributeDoubleQuotesRule } from "./rules/html-attribute-double-quotes.js"
16
+ import { HTMLAttributeEqualsSpacingRule } from "./rules/html-attribute-equals-spacing.js"
14
17
  import { HTMLAttributeValuesRequireQuotesRule } from "./rules/html-attribute-values-require-quotes.js"
18
+ import { HTMLAvoidBothDisabledAndAriaDisabledRule } from "./rules/html-avoid-both-disabled-and-aria-disabled.js"
15
19
  import { HTMLBooleanAttributesNoValueRule } from "./rules/html-boolean-attributes-no-value.js"
20
+ import { HTMLIframeHasTitleRule } from "./rules/html-iframe-has-title.js"
16
21
  import { HTMLImgRequireAltRule } from "./rules/html-img-require-alt.js"
22
+ import { HTMLNavigationHasLabelRule } from "./rules/html-navigation-has-label.js"
23
+ import { HTMLNoAriaHiddenOnFocusableRule } from "./rules/html-no-aria-hidden-on-focusable.js"
17
24
  // import { HTMLNoBlockInsideInlineRule } from "./rules/html-no-block-inside-inline.js"
18
25
  import { HTMLNoDuplicateAttributesRule } from "./rules/html-no-duplicate-attributes.js"
19
26
  import { HTMLNoDuplicateIdsRule } from "./rules/html-no-duplicate-ids.js"
20
27
  import { HTMLNoEmptyHeadingsRule } from "./rules/html-no-empty-headings.js"
21
28
  import { HTMLNoNestedLinksRule } from "./rules/html-no-nested-links.js"
29
+ import { HTMLNoPositiveTabIndexRule } from "./rules/html-no-positive-tab-index.js"
30
+ import { HTMLNoSelfClosingRule } from "./rules/html-no-self-closing.js"
31
+ import { HTMLNoTitleAttributeRule } from "./rules/html-no-title-attribute.js"
22
32
  import { HTMLTagNameLowercaseRule } from "./rules/html-tag-name-lowercase.js"
33
+ import { ParserNoErrorsRule } from "./rules/parser-no-errors.js"
23
34
  import { SVGTagNameCapitalizationRule } from "./rules/svg-tag-name-capitalization.js"
24
35
 
25
36
  export const defaultRules: RuleClass[] = [
26
37
  ERBNoEmptyTagsRule,
27
38
  ERBNoOutputControlFlowRule,
39
+ ERBNoSilentTagInAttributeNameRule,
28
40
  ERBPreferImageTagHelperRule,
29
41
  ERBRequiresTrailingNewlineRule,
30
42
  ERBRequireWhitespaceRule,
31
43
  HTMLAnchorRequireHrefRule,
32
44
  HTMLAriaAttributeMustBeValid,
45
+ HTMLAriaLabelIsWellFormattedRule,
33
46
  HTMLAriaLevelMustBeValidRule,
34
47
  HTMLAriaRoleHeadingRequiresLevelRule,
35
48
  HTMLAriaRoleMustBeValidRule,
36
49
  HTMLAttributeDoubleQuotesRule,
50
+ HTMLAttributeEqualsSpacingRule,
37
51
  HTMLAttributeValuesRequireQuotesRule,
52
+ HTMLAvoidBothDisabledAndAriaDisabledRule,
38
53
  HTMLBooleanAttributesNoValueRule,
54
+ HTMLIframeHasTitleRule,
39
55
  HTMLImgRequireAltRule,
56
+ HTMLNavigationHasLabelRule,
57
+ HTMLNoAriaHiddenOnFocusableRule,
40
58
  // HTMLNoBlockInsideInlineRule,
41
59
  HTMLNoDuplicateAttributesRule,
42
60
  HTMLNoDuplicateIdsRule,
43
61
  HTMLNoEmptyHeadingsRule,
44
62
  HTMLNoNestedLinksRule,
63
+ HTMLNoPositiveTabIndexRule,
64
+ HTMLNoSelfClosingRule,
65
+ HTMLNoTitleAttributeRule,
45
66
  HTMLTagNameLowercaseRule,
67
+ ParserNoErrorsRule,
46
68
  SVGTagNameCapitalizationRule,
47
69
  ]
package/src/linter.ts CHANGED
@@ -53,20 +53,46 @@ export class Linter {
53
53
  lint(source: string, context?: Partial<LintContext>): LintResult {
54
54
  this.offenses = []
55
55
 
56
- const parseResult = this.herb.parse(source)
56
+ const parseResult = this.herb.parse(source, { track_whitespace: true })
57
57
  const lexResult = this.herb.lex(source)
58
58
 
59
59
  for (const RuleClass of this.rules) {
60
60
  const rule = new RuleClass()
61
61
 
62
+ let isEnabled = true
62
63
  let ruleOffenses: LintOffense[]
63
64
 
64
65
  if (this.isLexerRule(rule)) {
65
- ruleOffenses = (rule as LexerRule).check(lexResult, context)
66
+ if (rule.isEnabled) {
67
+ isEnabled = rule.isEnabled(lexResult, context)
68
+ }
69
+
70
+ if (isEnabled) {
71
+ ruleOffenses = (rule as LexerRule).check(lexResult, context)
72
+ } else {
73
+ ruleOffenses = []
74
+ }
75
+
66
76
  } else if (this.isSourceRule(rule)) {
67
- ruleOffenses = (rule as SourceRule).check(source, context)
77
+ if (rule.isEnabled) {
78
+ isEnabled = rule.isEnabled(source, context)
79
+ }
80
+
81
+ if (isEnabled) {
82
+ ruleOffenses = (rule as SourceRule).check(source, context)
83
+ } else {
84
+ ruleOffenses = []
85
+ }
68
86
  } else {
69
- ruleOffenses = (rule as ParserRule).check(parseResult.value, context)
87
+ if (rule.isEnabled) {
88
+ isEnabled = rule.isEnabled(parseResult, context)
89
+ }
90
+
91
+ if (isEnabled) {
92
+ ruleOffenses = (rule as ParserRule).check(parseResult, context)
93
+ } else {
94
+ ruleOffenses = []
95
+ }
70
96
  }
71
97
 
72
98
  this.offenses.push(...ruleOffenses)
@@ -2,7 +2,7 @@ import { BaseRuleVisitor } from "./rule-utils.js"
2
2
 
3
3
  import { ParserRule } from "../types.js"
4
4
  import type { LintOffense, LintContext } from "../types.js"
5
- import type { Node, ERBContentNode } from "@herb-tools/core"
5
+ import type { ParseResult, ERBContentNode } from "@herb-tools/core"
6
6
 
7
7
  class ERBNoEmptyTagsVisitor extends BaseRuleVisitor {
8
8
  visitERBContentNode(node: ERBContentNode): void {
@@ -25,10 +25,10 @@ class ERBNoEmptyTagsVisitor extends BaseRuleVisitor {
25
25
  export class ERBNoEmptyTagsRule extends ParserRule {
26
26
  name = "erb-no-empty-tags"
27
27
 
28
- check(node: Node, context?: Partial<LintContext>): LintOffense[] {
28
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
29
29
  const visitor = new ERBNoEmptyTagsVisitor(this.name, context)
30
30
 
31
- visitor.visit(node)
31
+ visitor.visit(result.value)
32
32
 
33
33
  return visitor.offenses
34
34
  }
@@ -1,6 +1,6 @@
1
1
  import { BaseRuleVisitor } from "./rule-utils.js"
2
2
 
3
- import type { Node, ERBIfNode, ERBUnlessNode, ERBElseNode, ERBEndNode } from "@herb-tools/core"
3
+ import type { ParseResult, ERBIfNode, ERBUnlessNode, ERBElseNode, ERBEndNode } from "@herb-tools/core"
4
4
  import { ParserRule } from "../types.js"
5
5
  import type { LintOffense, LintContext } from "../types.js"
6
6
 
@@ -53,10 +53,10 @@ class ERBNoOutputControlFlowRuleVisitor extends BaseRuleVisitor {
53
53
  export class ERBNoOutputControlFlowRule extends ParserRule {
54
54
  name = "erb-no-output-control-flow"
55
55
 
56
- check(node: Node, context?: Partial<LintContext>): LintOffense[] {
56
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
57
57
  const visitor = new ERBNoOutputControlFlowRuleVisitor(this.name, context)
58
58
 
59
- visitor.visit(node)
59
+ visitor.visit(result.value)
60
60
 
61
61
  return visitor.offenses
62
62
  }
@@ -0,0 +1,40 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { BaseRuleVisitor } from "./rule-utils.js"
3
+ import { filterERBContentNodes } from "@herb-tools/core"
4
+
5
+ import type { LintOffense, LintContext } from "../types.js"
6
+ import type { ParseResult, HTMLAttributeNameNode, ERBContentNode } from "@herb-tools/core"
7
+
8
+ class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
9
+ visitHTMLAttributeNameNode(node: HTMLAttributeNameNode): void {
10
+ const erbNodes = filterERBContentNodes(node.children)
11
+ const silentNodes = erbNodes.filter(this.isSilentERBTag)
12
+
13
+ for (const node of silentNodes) {
14
+ this.addOffense(
15
+ `Remove silent ERB tag from HTML attribute name. Silent ERB tags (\`${node.tag_opening?.value}\`) do not output content and should not be used in attribute names.`,
16
+ node.location,
17
+ "error"
18
+ )
19
+ }
20
+ }
21
+
22
+ // TODO: might be worth to extract
23
+ private isSilentERBTag(node: ERBContentNode): boolean {
24
+ const silentTags = ["<%", "<%-", "<%#"]
25
+
26
+ return silentTags.includes(node.tag_opening?.value || "")
27
+ }
28
+ }
29
+
30
+ export class ERBNoSilentTagInAttributeNameRule extends ParserRule {
31
+ name = "erb-no-silent-tag-in-attribute-name"
32
+
33
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
34
+ const visitor = new ERBNoSilentTagInAttributeNameVisitor(this.name, context)
35
+
36
+ visitor.visit(result.value)
37
+
38
+ return visitor.offenses
39
+ }
40
+ }
@@ -2,7 +2,7 @@ import { BaseRuleVisitor, getTagName, findAttributeByName, getAttributes } from
2
2
 
3
3
  import { ParserRule } from "../types.js"
4
4
  import type { LintOffense, LintContext } from "../types.js"
5
- import type { HTMLOpenTagNode, HTMLSelfCloseTagNode, HTMLAttributeValueNode, ERBContentNode, LiteralNode, Node } from "@herb-tools/core"
5
+ import type { HTMLOpenTagNode, HTMLAttributeValueNode, ERBContentNode, LiteralNode, ParseResult } from "@herb-tools/core"
6
6
 
7
7
  class ERBPreferImageTagHelperVisitor extends BaseRuleVisitor {
8
8
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
@@ -10,12 +10,7 @@ class ERBPreferImageTagHelperVisitor extends BaseRuleVisitor {
10
10
  super.visitHTMLOpenTagNode(node)
11
11
  }
12
12
 
13
- visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
14
- this.checkImgTag(node)
15
- super.visitHTMLSelfCloseTagNode(node)
16
- }
17
-
18
- private checkImgTag(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): void {
13
+ private checkImgTag(node: HTMLOpenTagNode): void {
19
14
  const tagName = getTagName(node)
20
15
 
21
16
  if (tagName !== "img") {
@@ -116,9 +111,9 @@ class ERBPreferImageTagHelperVisitor extends BaseRuleVisitor {
116
111
  export class ERBPreferImageTagHelperRule extends ParserRule {
117
112
  name = "erb-prefer-image-tag-helper"
118
113
 
119
- check(node: Node, context?: Partial<LintContext>): LintOffense[] {
114
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
120
115
  const visitor = new ERBPreferImageTagHelperVisitor(this.name, context)
121
- visitor.visit(node)
116
+ visitor.visit(result.value)
122
117
  return visitor.offenses
123
118
  }
124
119
  }
@@ -1,4 +1,4 @@
1
- import type { Node, Token } from "@herb-tools/core"
1
+ import type { ParseResult, Token, Node } from "@herb-tools/core"
2
2
  import { isERBNode } from "@herb-tools/core";
3
3
  import { ParserRule } from "../types.js"
4
4
  import type { LintOffense, LintContext } from "../types.js"
@@ -85,9 +85,9 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
85
85
  export class ERBRequireWhitespaceRule extends ParserRule {
86
86
  name = "erb-require-whitespace-inside-tags"
87
87
 
88
- check(node: Node, context?: Partial<LintContext>): LintOffense[] {
88
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
89
89
  const visitor = new RequireWhitespaceInsideTags(this.name, context)
90
- visitor.visit(node)
90
+ visitor.visit(result.value)
91
91
  return visitor.offenses
92
92
  }
93
93
  }
@@ -21,7 +21,9 @@ export class ERBRequiresTrailingNewlineRule extends SourceRule {
21
21
 
22
22
  check(source: string, context?: Partial<LintContext>): LintOffense[] {
23
23
  const visitor = new ERBRequiresTrailingNewlineVisitor(this.name, context)
24
+
24
25
  visitor.visit(source)
26
+
25
27
  return visitor.offenses
26
28
  }
27
29
  }
@@ -2,7 +2,7 @@ import { BaseRuleVisitor, getTagName, hasAttribute } from "./rule-utils.js"
2
2
 
3
3
  import { ParserRule } from "../types.js"
4
4
  import type { LintOffense, LintContext } from "../types.js"
5
- import type { HTMLOpenTagNode, Node } from "@herb-tools/core"
5
+ import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
6
6
 
7
7
  class AnchorRechireHrefVisitor extends BaseRuleVisitor {
8
8
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
@@ -30,10 +30,10 @@ class AnchorRechireHrefVisitor extends BaseRuleVisitor {
30
30
  export class HTMLAnchorRequireHrefRule extends ParserRule {
31
31
  name = "html-anchor-require-href"
32
32
 
33
- check(node: Node, context?: Partial<LintContext>): LintOffense[] {
33
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
34
34
  const visitor = new AnchorRechireHrefVisitor(this.name, context)
35
35
 
36
- visitor.visit(node)
36
+ visitor.visit(result.value)
37
37
 
38
38
  return visitor.offenses
39
39
  }
@@ -1,42 +1,38 @@
1
- import {
2
- ARIA_ATTRIBUTES,
3
- AttributeVisitorMixin,
4
- } from "./rule-utils.js";
5
- import { ParserRule } from "../types.js";
6
- import type { LintOffense, LintContext } from "../types.js";
7
- import type {
8
- HTMLAttributeNode,
9
- HTMLOpenTagNode,
10
- HTMLSelfCloseTagNode,
11
- Node,
12
- } from "@herb-tools/core";
1
+ import { ParserRule } from "../types.js"
2
+ import { ARIA_ATTRIBUTES, AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams } from "./rule-utils.js"
3
+
4
+ import type { LintOffense, LintContext } from "../types.js"
5
+ import type { ParseResult, HTMLAttributeNode } from "@herb-tools/core"
13
6
 
14
7
  class AriaAttributeMustBeValid extends AttributeVisitorMixin {
15
- checkAttribute(
16
- attributeName: string,
17
- _attributeValue: string | null,
18
- attributeNode: HTMLAttributeNode,
19
- _parentNode: HTMLOpenTagNode | HTMLSelfCloseTagNode,
20
- ): void {
21
- if (!attributeName.startsWith("aria-")) return;
22
-
23
- if (!ARIA_ATTRIBUTES.has(attributeName)){
24
- this.offenses.push({
25
- message: `The attribute \`${attributeName}\` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.`,
26
- severity: "error",
27
- location: attributeNode.location,
28
- rule: this.ruleName,
29
- });
30
- }
8
+ protected checkStaticAttributeStaticValue({ attributeName, attributeNode }: StaticAttributeStaticValueParams) {
9
+ this.check(attributeName, attributeNode)
10
+ }
11
+
12
+ protected checkStaticAttributeDynamicValue({ attributeName, attributeNode }: StaticAttributeDynamicValueParams) {
13
+ this.check(attributeName, attributeNode)
14
+ }
15
+
16
+ private check(attributeName: string, attributeNode: HTMLAttributeNode) {
17
+ if (!attributeName.startsWith("aria-")) return
18
+ if (ARIA_ATTRIBUTES.has(attributeName)) return
19
+
20
+ this.addOffense(
21
+ `The attribute \`${attributeName}\` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.`,
22
+ attributeNode.location,
23
+ "error"
24
+ )
31
25
  }
32
26
  }
33
27
 
34
28
  export class HTMLAriaAttributeMustBeValid extends ParserRule {
35
- name = "html-aria-attribute-must-be-valid";
29
+ name = "html-aria-attribute-must-be-valid"
30
+
31
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
32
+ const visitor = new AriaAttributeMustBeValid(this.name, context)
33
+
34
+ visitor.visit(result.value)
36
35
 
37
- check(node: Node, context?: Partial<LintContext>): LintOffense[] {
38
- const visitor = new AriaAttributeMustBeValid(this.name, context);
39
- visitor.visit(node);
40
- return visitor.offenses;
36
+ return visitor.offenses
41
37
  }
42
38
  }
@@ -0,0 +1,59 @@
1
+ import { ParserRule } from "../types.js"
2
+ import { AttributeVisitorMixin, StaticAttributeStaticValueParams } from "./rule-utils.js"
3
+
4
+ import type { LintOffense, LintContext } from "../types.js"
5
+ import type { ParseResult } from "@herb-tools/core"
6
+
7
+ class AriaLabelIsWellFormattedVisitor extends AttributeVisitorMixin {
8
+ protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams): void {
9
+ if (attributeName !== "aria-label") return
10
+
11
+ if (attributeValue.match(/[\r\n]+/) || attributeValue.match(/&#10;|&#13;|&#x0A;|&#x0D;/i)) {
12
+ this.addOffense(
13
+ "The `aria-label` attribute value text should not contain line breaks. Use concise, single-line descriptions.",
14
+ attributeNode.location,
15
+ "error"
16
+ )
17
+
18
+ return
19
+ }
20
+
21
+ if (this.looksLikeId(attributeValue)) {
22
+ this.addOffense(
23
+ "The `aria-label` attribute value should not be formatted like an ID. Use natural, sentence-case text instead.",
24
+ attributeNode.location,
25
+ "error"
26
+ )
27
+
28
+ return
29
+ }
30
+
31
+ if (attributeValue.match(/^[a-z]/)) {
32
+ this.addOffense(
33
+ "The `aria-label` attribute value text should be formatted like visual text. Use sentence case (capitalize the first letter).",
34
+ attributeNode.location,
35
+ "error"
36
+ )
37
+ }
38
+ }
39
+
40
+ private looksLikeId(text: string): boolean {
41
+ return (
42
+ text.includes('_') ||
43
+ text.includes('-') ||
44
+ /^[a-z]+([A-Z][a-z]*)*$/.test(text)
45
+ ) && !text.includes(' ')
46
+ }
47
+ }
48
+
49
+ export class HTMLAriaLabelIsWellFormattedRule extends ParserRule {
50
+ name = "html-aria-label-is-well-formatted"
51
+
52
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
53
+ const visitor = new AriaLabelIsWellFormattedVisitor(this.name, context)
54
+
55
+ visitor.visit(result.value)
56
+
57
+ return visitor.offenses
58
+ }
59
+ }
@@ -1,15 +1,48 @@
1
- import { AttributeVisitorMixin } from "./rule-utils.js"
2
1
  import { ParserRule } from "../types.js"
2
+ import { AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams } from "./rule-utils.js"
3
+ import { getValidatableStaticContent, hasERBOutput, filterLiteralNodes, filterERBContentNodes, isERBOutputNode } from "@herb-tools/core"
3
4
 
4
5
  import type { LintOffense, LintContext } from "../types.js"
5
- import type { Node, HTMLAttributeNode, HTMLOpenTagNode, HTMLSelfCloseTagNode } from "@herb-tools/core"
6
+ import type { ParseResult, HTMLAttributeNode } from "@herb-tools/core"
6
7
 
7
8
  class HTMLAriaLevelMustBeValidVisitor extends AttributeVisitorMixin {
8
- protected checkAttribute(attributeName: string, attributeValue: string | null, attributeNode: HTMLAttributeNode, _parentNode: HTMLOpenTagNode | HTMLSelfCloseTagNode): void {
9
+ protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams) {
9
10
  if (attributeName !== "aria-level") return
10
- if (attributeValue !== null && attributeValue.includes("<%")) return
11
11
 
12
- if (attributeValue === null || attributeValue === "") {
12
+ this.validateAriaLevel(attributeValue, attributeNode)
13
+ }
14
+
15
+ protected checkStaticAttributeDynamicValue({ attributeName, valueNodes, attributeNode }: StaticAttributeDynamicValueParams) {
16
+ if (attributeName !== "aria-level") return
17
+
18
+ const validatableContent = getValidatableStaticContent(valueNodes)
19
+
20
+ if (validatableContent !== null) {
21
+ this.validateAriaLevel(validatableContent, attributeNode)
22
+ return
23
+ }
24
+
25
+ if (!hasERBOutput(valueNodes)) return
26
+
27
+ const literalNodes = filterLiteralNodes(valueNodes)
28
+ const erbOutputNodes = filterERBContentNodes(valueNodes).filter(isERBOutputNode)
29
+
30
+ if (literalNodes.length > 0 && erbOutputNodes.length > 0) {
31
+ const staticPart = literalNodes.map(node => node.content).join("")
32
+
33
+ // TODO: this can be cleaned up using @herb-tools/printer
34
+ const erbPart = erbOutputNodes[0]
35
+ const erbText = `${erbPart.tag_opening?.value || ""}${erbPart.content?.value || ""}${erbPart.tag_closing?.value || ""}`
36
+
37
+ this.addOffense(
38
+ `The \`aria-level\` attribute must be an integer between 1 and 6, got \`${staticPart}\` and the ERB expression \`${erbText}\`.`,
39
+ attributeNode.location,
40
+ )
41
+ }
42
+ }
43
+
44
+ private validateAriaLevel(attributeValue: string, attributeNode: HTMLAttributeNode): void {
45
+ if (!attributeValue || attributeValue === "") {
13
46
  this.addOffense(
14
47
  `The \`aria-level\` attribute must be an integer between 1 and 6, got an empty value.`,
15
48
  attributeNode.location,
@@ -32,10 +65,10 @@ class HTMLAriaLevelMustBeValidVisitor extends AttributeVisitorMixin {
32
65
  export class HTMLAriaLevelMustBeValidRule extends ParserRule {
33
66
  name = "html-aria-level-must-be-valid"
34
67
 
35
- check(node: Node, context?: Partial<LintContext>): LintOffense[] {
68
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
36
69
  const visitor = new HTMLAriaLevelMustBeValidVisitor(this.name, context)
37
70
 
38
- visitor.visit(node)
71
+ visitor.visit(result.value)
39
72
 
40
73
  return visitor.offenses
41
74
  }
@@ -1,45 +1,33 @@
1
- import { AttributeVisitorMixin, getAttributeName, getAttributes } from "./rule-utils.js"
2
-
3
1
  import { ParserRule } from "../types.js"
2
+ import { AttributeVisitorMixin, getAttributeName, getAttributes, StaticAttributeStaticValueParams } from "./rule-utils.js"
3
+
4
4
  import type { LintOffense, LintContext } from "../types.js"
5
- import type { Node, HTMLAttributeNode, HTMLOpenTagNode, HTMLSelfCloseTagNode } from "@herb-tools/core"
5
+ import type { ParseResult } from "@herb-tools/core"
6
6
 
7
7
  class AriaRoleHeadingRequiresLevel extends AttributeVisitorMixin {
8
+ protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode, parentNode }: StaticAttributeStaticValueParams): void {
9
+ if (!(attributeName === "role" && attributeValue === "heading")) return
10
+
11
+ const ariaLevelAttributes = getAttributes(parentNode).find(attribute => getAttributeName(attribute) === "aria-level")
8
12
 
9
- // We want to check 2 attributes here:
10
- // 1. role="heading"
11
- // 2. aria-level (which must be present if role="heading")
12
- checkAttribute(
13
- attributeName: string,
14
- attributeValue: string | null,
15
- attributeNode: HTMLAttributeNode,
16
- parentNode: HTMLOpenTagNode | HTMLSelfCloseTagNode
17
- ): void {
18
-
19
- if (!(attributeName === "role" && attributeValue === "heading")) {
20
- return
21
- }
22
-
23
- const allAttributes = getAttributes(parentNode)
24
-
25
- // If we have a role="heading", we must check for aria-level
26
- const ariaLevelAttr = allAttributes.find(attr => getAttributeName(attr) === "aria-level")
27
- if (!ariaLevelAttr) {
28
- this.addOffense(
29
- `Element with \`role="heading"\` must have an \`aria-level\` attribute.`,
30
- attributeNode.location,
31
- "error"
32
- )
33
- }
13
+ if (ariaLevelAttributes) return
14
+
15
+ this.addOffense(
16
+ `Element with \`role="heading"\` must have an \`aria-level\` attribute.`,
17
+ attributeNode.location,
18
+ "error"
19
+ )
34
20
  }
35
21
  }
36
22
 
37
23
  export class HTMLAriaRoleHeadingRequiresLevelRule extends ParserRule {
38
24
  name = "html-aria-role-heading-requires-level"
39
25
 
40
- check(node: Node, context?: Partial<LintContext>): LintOffense[] {
26
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
41
27
  const visitor = new AriaRoleHeadingRequiresLevel(this.name, context)
42
- visitor.visit(node)
28
+
29
+ visitor.visit(result.value)
30
+
43
31
  return visitor.offenses
44
32
  }
45
33
  }