@herb-tools/linter 0.4.3 → 0.5.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 (181) hide show
  1. package/README.md +216 -19
  2. package/dist/herb-lint.js +420 -205
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +61 -43
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +61 -43
  7. package/dist/index.js.map +1 -1
  8. package/dist/package.json +4 -4
  9. package/dist/src/cli/argument-parser.js +28 -18
  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 +2 -0
  30. package/dist/src/default-rules.js.map +1 -1
  31. package/dist/src/linter.js +1 -1
  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-prefer-image-tag-helper.js +2 -2
  38. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
  39. package/dist/src/rules/erb-require-whitespace-inside-tags.js +2 -2
  40. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  41. package/dist/src/rules/erb-requires-trailing-newline.js.map +1 -1
  42. package/dist/src/rules/html-anchor-require-href.js +2 -2
  43. package/dist/src/rules/html-anchor-require-href.js.map +1 -1
  44. package/dist/src/rules/html-aria-attribute-must-be-valid.js +2 -2
  45. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
  46. package/dist/src/rules/html-aria-level-must-be-valid.js +2 -2
  47. package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
  48. package/dist/src/rules/html-aria-role-heading-requires-level.js +2 -2
  49. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
  50. package/dist/src/rules/html-aria-role-must-be-valid.js +2 -2
  51. package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
  52. package/dist/src/rules/html-attribute-double-quotes.js +2 -2
  53. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
  54. package/dist/src/rules/html-attribute-values-require-quotes.js +2 -2
  55. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
  56. package/dist/src/rules/html-boolean-attributes-no-value.js +2 -2
  57. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  58. package/dist/src/rules/html-img-require-alt.js +2 -2
  59. package/dist/src/rules/html-img-require-alt.js.map +1 -1
  60. package/dist/src/rules/html-no-block-inside-inline.js +4 -4
  61. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
  62. package/dist/src/rules/html-no-duplicate-attributes.js +2 -2
  63. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
  64. package/dist/src/rules/html-no-duplicate-ids.js +2 -2
  65. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
  66. package/dist/src/rules/html-no-empty-headings.js +2 -2
  67. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  68. package/dist/src/rules/html-no-nested-links.js +2 -2
  69. package/dist/src/rules/html-no-nested-links.js.map +1 -1
  70. package/dist/src/rules/html-tag-name-lowercase.js +2 -2
  71. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  72. package/dist/src/rules/parser-no-errors.js +18 -0
  73. package/dist/src/rules/parser-no-errors.js.map +1 -0
  74. package/dist/src/rules/svg-tag-name-capitalization.js +2 -2
  75. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
  76. package/dist/tsconfig.tsbuildinfo +1 -1
  77. package/dist/types/cli/argument-parser.d.ts +2 -1
  78. package/dist/types/cli/file-processor.d.ts +6 -5
  79. package/dist/types/cli/formatters/base-formatter.d.ts +2 -2
  80. package/dist/types/cli/formatters/detailed-formatter.d.ts +2 -2
  81. package/dist/types/cli/formatters/github-actions-formatter.d.ts +12 -0
  82. package/dist/types/cli/formatters/index.d.ts +2 -0
  83. package/dist/types/cli/formatters/json-formatter.d.ts +42 -0
  84. package/dist/types/cli/formatters/simple-formatter.d.ts +2 -2
  85. package/dist/types/cli/output-manager.d.ts +31 -0
  86. package/dist/types/cli/summary-reporter.d.ts +3 -3
  87. package/dist/types/cli.d.ts +3 -1
  88. package/dist/types/rules/erb-no-empty-tags.d.ts +2 -2
  89. package/dist/types/rules/erb-no-output-control-flow.d.ts +2 -2
  90. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +2 -2
  91. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
  92. package/dist/types/rules/html-anchor-require-href.d.ts +2 -2
  93. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
  94. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +2 -2
  95. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +2 -2
  96. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +2 -2
  97. package/dist/types/rules/html-attribute-double-quotes.d.ts +2 -2
  98. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +2 -2
  99. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +2 -2
  100. package/dist/types/rules/html-img-require-alt.d.ts +2 -2
  101. package/dist/types/rules/html-no-block-inside-inline.d.ts +2 -2
  102. package/dist/types/rules/html-no-duplicate-attributes.d.ts +2 -2
  103. package/dist/types/rules/html-no-duplicate-ids.d.ts +2 -2
  104. package/dist/types/rules/html-no-empty-headings.d.ts +2 -2
  105. package/dist/types/rules/html-no-nested-links.d.ts +2 -2
  106. package/dist/types/rules/html-tag-name-lowercase.d.ts +2 -2
  107. package/dist/types/rules/parser-no-errors.d.ts +8 -0
  108. package/dist/types/rules/svg-tag-name-capitalization.d.ts +2 -2
  109. package/dist/types/src/cli/argument-parser.d.ts +2 -1
  110. package/dist/types/src/cli/file-processor.d.ts +6 -5
  111. package/dist/types/src/cli/formatters/base-formatter.d.ts +2 -2
  112. package/dist/types/src/cli/formatters/detailed-formatter.d.ts +2 -2
  113. package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +12 -0
  114. package/dist/types/src/cli/formatters/index.d.ts +2 -0
  115. package/dist/types/src/cli/formatters/json-formatter.d.ts +42 -0
  116. package/dist/types/src/cli/formatters/simple-formatter.d.ts +2 -2
  117. package/dist/types/src/cli/output-manager.d.ts +31 -0
  118. package/dist/types/src/cli/summary-reporter.d.ts +3 -3
  119. package/dist/types/src/cli.d.ts +3 -1
  120. package/dist/types/src/rules/erb-no-empty-tags.d.ts +2 -2
  121. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +2 -2
  122. package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +2 -2
  123. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
  124. package/dist/types/src/rules/html-anchor-require-href.d.ts +2 -2
  125. package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
  126. package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +2 -2
  127. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +2 -2
  128. package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +2 -2
  129. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +2 -2
  130. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +2 -2
  131. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +2 -2
  132. package/dist/types/src/rules/html-img-require-alt.d.ts +2 -2
  133. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +2 -2
  134. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +2 -2
  135. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +2 -2
  136. package/dist/types/src/rules/html-no-empty-headings.d.ts +2 -2
  137. package/dist/types/src/rules/html-no-nested-links.d.ts +2 -2
  138. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +2 -2
  139. package/dist/types/src/rules/parser-no-errors.d.ts +8 -0
  140. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +2 -2
  141. package/dist/types/src/types.d.ts +3 -3
  142. package/dist/types/types.d.ts +3 -3
  143. package/docs/rules/README.md +1 -0
  144. package/docs/rules/parser-no-errors.md +84 -0
  145. package/package.json +4 -4
  146. package/src/cli/argument-parser.ts +33 -19
  147. package/src/cli/file-processor.ts +25 -17
  148. package/src/cli/formatters/base-formatter.ts +2 -2
  149. package/src/cli/formatters/detailed-formatter.ts +9 -9
  150. package/src/cli/formatters/github-actions-formatter.ts +70 -0
  151. package/src/cli/formatters/index.ts +2 -0
  152. package/src/cli/formatters/json-formatter.ts +107 -0
  153. package/src/cli/formatters/simple-formatter.ts +15 -15
  154. package/src/cli/output-manager.ts +143 -0
  155. package/src/cli/summary-reporter.ts +24 -24
  156. package/src/cli.ts +48 -31
  157. package/src/default-rules.ts +2 -0
  158. package/src/linter.ts +1 -1
  159. package/src/rules/erb-no-empty-tags.ts +3 -3
  160. package/src/rules/erb-no-output-control-flow.ts +3 -3
  161. package/src/rules/erb-prefer-image-tag-helper.ts +3 -3
  162. package/src/rules/erb-require-whitespace-inside-tags.ts +3 -3
  163. package/src/rules/erb-requires-trailing-newline.ts +2 -0
  164. package/src/rules/html-anchor-require-href.ts +3 -3
  165. package/src/rules/html-aria-attribute-must-be-valid.ts +3 -3
  166. package/src/rules/html-aria-level-must-be-valid.ts +3 -3
  167. package/src/rules/html-aria-role-heading-requires-level.ts +3 -3
  168. package/src/rules/html-aria-role-must-be-valid.ts +3 -3
  169. package/src/rules/html-attribute-double-quotes.ts +3 -3
  170. package/src/rules/html-attribute-values-require-quotes.ts +3 -3
  171. package/src/rules/html-boolean-attributes-no-value.ts +3 -3
  172. package/src/rules/html-img-require-alt.ts +3 -3
  173. package/src/rules/html-no-block-inside-inline.ts +5 -5
  174. package/src/rules/html-no-duplicate-attributes.ts +3 -3
  175. package/src/rules/html-no-duplicate-ids.ts +3 -3
  176. package/src/rules/html-no-empty-headings.ts +3 -3
  177. package/src/rules/html-no-nested-links.ts +3 -3
  178. package/src/rules/html-tag-name-lowercase.ts +3 -3
  179. package/src/rules/parser-no-errors.ts +25 -0
  180. package/src/rules/svg-tag-name-capitalization.ts +3 -3
  181. package/src/types.ts +3 -3
@@ -1,3 +1,5 @@
1
1
  export { BaseFormatter } from "./base-formatter.js";
2
2
  export { SimpleFormatter } from "./simple-formatter.js";
3
3
  export { DetailedFormatter } from "./detailed-formatter.js";
4
+ export { JSONFormatter, type JSONOutput } from "./json-formatter.js";
5
+ export { GitHubActionsFormatter } from "./github-actions-formatter.js";
@@ -0,0 +1,42 @@
1
+ import { BaseFormatter } from "./base-formatter.js";
2
+ import type { Diagnostic, SerializedDiagnostic } from "@herb-tools/core";
3
+ import type { ProcessedFile } from "../file-processor.js";
4
+ interface JSONOffense extends SerializedDiagnostic {
5
+ filename: string;
6
+ }
7
+ interface JSONSummary {
8
+ filesChecked: number;
9
+ filesWithOffenses: number;
10
+ totalErrors: number;
11
+ totalWarnings: number;
12
+ totalOffenses: number;
13
+ ruleCount: number;
14
+ }
15
+ interface JSONTiming {
16
+ startTime: string;
17
+ duration: number;
18
+ }
19
+ export interface JSONOutput {
20
+ offenses: JSONOffense[];
21
+ summary: JSONSummary | null;
22
+ timing: JSONTiming | null;
23
+ completed: boolean;
24
+ clean: boolean | null;
25
+ message: string | null;
26
+ }
27
+ interface JSONFormatOptions {
28
+ files: string[];
29
+ totalErrors: number;
30
+ totalWarnings: number;
31
+ filesWithOffenses: number;
32
+ ruleCount: number;
33
+ startTime: number;
34
+ startDate: Date;
35
+ showTiming: boolean;
36
+ }
37
+ export declare class JSONFormatter extends BaseFormatter {
38
+ format(allOffenses: ProcessedFile[]): Promise<void>;
39
+ formatWithSummary(allOffenses: ProcessedFile[], options: JSONFormatOptions): Promise<void>;
40
+ formatFile(_filename: string, _offenses: Diagnostic[]): void;
41
+ }
42
+ export {};
@@ -2,6 +2,6 @@ import { BaseFormatter } from "./base-formatter.js";
2
2
  import type { Diagnostic } from "@herb-tools/core";
3
3
  import type { ProcessedFile } from "../file-processor.js";
4
4
  export declare class SimpleFormatter extends BaseFormatter {
5
- format(allDiagnostics: ProcessedFile[]): Promise<void>;
6
- formatFile(filename: string, diagnostics: Diagnostic[]): void;
5
+ format(allOffenses: ProcessedFile[]): Promise<void>;
6
+ formatFile(filename: string, offenses: Diagnostic[]): void;
7
7
  }
@@ -0,0 +1,31 @@
1
+ import type { ThemeInput } from "@herb-tools/highlighter";
2
+ import type { FormatOption } from "./argument-parser.js";
3
+ import type { ProcessingResult } from "./file-processor.js";
4
+ interface OutputOptions {
5
+ formatOption: FormatOption;
6
+ theme: ThemeInput;
7
+ wrapLines: boolean;
8
+ truncateLines: boolean;
9
+ showTiming: boolean;
10
+ startTime: number;
11
+ startDate: Date;
12
+ }
13
+ interface LintResults extends ProcessingResult {
14
+ files: string[];
15
+ }
16
+ export declare class OutputManager {
17
+ private summaryReporter;
18
+ /**
19
+ * Output successful lint results
20
+ */
21
+ outputResults(results: LintResults, options: OutputOptions): Promise<void>;
22
+ /**
23
+ * Output informational message (like "no files found")
24
+ */
25
+ outputInfo(message: string, options: OutputOptions): void;
26
+ /**
27
+ * Output error message
28
+ */
29
+ outputError(message: string, options: OutputOptions): void;
30
+ }
31
+ export {};
@@ -2,12 +2,12 @@ export interface SummaryData {
2
2
  files: string[];
3
3
  totalErrors: number;
4
4
  totalWarnings: number;
5
- filesWithViolations: number;
5
+ filesWithOffenses: number;
6
6
  ruleCount: number;
7
7
  startTime: number;
8
8
  startDate: Date;
9
9
  showTiming: boolean;
10
- ruleViolations: Map<string, {
10
+ ruleOffenses: Map<string, {
11
11
  count: number;
12
12
  files: Set<string>;
13
13
  }>;
@@ -15,7 +15,7 @@ export interface SummaryData {
15
15
  export declare class SummaryReporter {
16
16
  private pluralize;
17
17
  displaySummary(data: SummaryData): void;
18
- displayMostViolatedRules(ruleViolations: Map<string, {
18
+ displayMostViolatedRules(ruleOffenses: Map<string, {
19
19
  count: number;
20
20
  files: Set<string>;
21
21
  }>, limit?: number): void;
@@ -1,6 +1,8 @@
1
1
  export declare class CLI {
2
2
  private argumentParser;
3
3
  private fileProcessor;
4
- private summaryReporter;
4
+ private outputManager;
5
+ private exitWithError;
6
+ private exitWithInfo;
5
7
  run(): Promise<void>;
6
8
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class ERBNoEmptyTagsRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
- import type { Node } from "@herb-tools/core";
1
+ import type { ParseResult } from "@herb-tools/core";
2
2
  import { ParserRule } from "../types.js";
3
3
  import type { LintOffense, LintContext } from "../types.js";
4
4
  export declare class ERBNoOutputControlFlowRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class ERBPreferImageTagHelperRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
- import type { Node } from "@herb-tools/core";
1
+ import type { ParseResult } from "@herb-tools/core";
2
2
  import { ParserRule } from "../types.js";
3
3
  import type { LintOffense, LintContext } from "../types.js";
4
4
  export declare class ERBRequireWhitespaceRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLAnchorRequireHrefRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLAriaAttributeMustBeValid extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLAriaLevelMustBeValidRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLAriaRoleHeadingRequiresLevelRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLAriaRoleMustBeValidRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLAttributeDoubleQuotesRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLAttributeValuesRequireQuotesRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLBooleanAttributesNoValueRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLImgRequireAltRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLNoBlockInsideInlineRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLNoDuplicateAttributesRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types";
2
- import type { Node } from "@herb-tools/core";
2
+ import type { ParseResult } from "@herb-tools/core";
3
3
  import type { LintOffense, LintContext } from "../types";
4
4
  export declare class HTMLNoDuplicateIdsRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLNoEmptyHeadingsRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLNoNestedLinksRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class HTMLTagNameLowercaseRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -0,0 +1,8 @@
1
+ import { ParserRule } from "../types.js";
2
+ import type { LintOffense } from "../types.js";
3
+ import type { ParseResult } from "@herb-tools/core";
4
+ export declare class ParserNoErrorsRule extends ParserRule {
5
+ name: string;
6
+ check(result: ParseResult): LintOffense[];
7
+ private herbErrorToLintOffense;
8
+ }
@@ -1,7 +1,7 @@
1
1
  import { ParserRule } from "../types.js";
2
2
  import type { LintOffense, LintContext } from "../types.js";
3
- import type { Node } from "@herb-tools/core";
3
+ import type { ParseResult } from "@herb-tools/core";
4
4
  export declare class SVGTagNameCapitalizationRule extends ParserRule {
5
5
  name: string;
6
- check(node: Node, context?: Partial<LintContext>): LintOffense[];
6
+ check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
7
7
  }
@@ -1,6 +1,6 @@
1
- import { Node, Diagnostic, LexResult } from "@herb-tools/core";
1
+ import { Diagnostic, LexResult, ParseResult } from "@herb-tools/core";
2
2
  import type { defaultRules } from "./default-rules.js";
3
- export type LintSeverity = "error" | "warning";
3
+ export type LintSeverity = "error" | "warning" | "info" | "hint";
4
4
  /**
5
5
  * Automatically inferred union type of all available linter rule names.
6
6
  * This type extracts the 'name' property from each rule class instance.
@@ -18,7 +18,7 @@ export interface LintResult {
18
18
  export declare abstract class ParserRule {
19
19
  static type: "parser";
20
20
  abstract name: string;
21
- abstract check(node: Node, context?: Partial<LintContext>): LintOffense[];
21
+ abstract check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
22
22
  }
23
23
  export declare abstract class LexerRule {
24
24
  static type: "lexer";
@@ -1,6 +1,6 @@
1
- import { Node, Diagnostic, LexResult } from "@herb-tools/core";
1
+ import { Diagnostic, LexResult, ParseResult } from "@herb-tools/core";
2
2
  import type { defaultRules } from "./default-rules.js";
3
- export type LintSeverity = "error" | "warning";
3
+ export type LintSeverity = "error" | "warning" | "info" | "hint";
4
4
  /**
5
5
  * Automatically inferred union type of all available linter rule names.
6
6
  * This type extracts the 'name' property from each rule class instance.
@@ -18,7 +18,7 @@ export interface LintResult {
18
18
  export declare abstract class ParserRule {
19
19
  static type: "parser";
20
20
  abstract name: string;
21
- abstract check(node: Node, context?: Partial<LintContext>): LintOffense[];
21
+ abstract check(result: ParseResult, context?: Partial<LintContext>): LintOffense[];
22
22
  }
23
23
  export declare abstract class LexerRule {
24
24
  static type: "lexer";
@@ -23,6 +23,7 @@ This page contains documentation for all Herb Linter rules.
23
23
  - [`html-no-duplicate-ids`](./html-no-duplicate-ids.md) - Prevents duplicate IDs within a document
24
24
  - [`html-no-nested-links`](./html-no-nested-links.md) - Prevents nested anchor tags
25
25
  - [`html-tag-name-lowercase`](./html-tag-name-lowercase.md) - Enforces lowercase tag names in HTML
26
+ - [`parser-no-errors`](./parser-no-errors.md) - Disallow parser errors in HTML+ERB documents
26
27
  - [`svg-tag-name-capitalization`](./svg-tag-name-capitalization.md) - Enforces proper camelCase capitalization for SVG elements
27
28
 
28
29
  ## Contributing
@@ -0,0 +1,84 @@
1
+ # Linter Rule: Disallow parser errors in HTML+ERB documents
2
+
3
+ **Rule:** `parser-no-errors`
4
+
5
+ ## Description
6
+
7
+ Report parser errors as linting offenses. This rule surfaces syntax errors, malformed HTML, and other parsing issues that prevent the document from being correctly parsed.
8
+
9
+ ## Rationale
10
+
11
+ Parser errors indicate fundamental structural problems in HTML+ERB documents that can lead to unexpected rendering behavior, accessibility issues, and maintenance difficulties. These errors should be fixed before addressing other linting concerns as they represent invalid markup that browsers may interpret inconsistently.
12
+
13
+ By surfacing parser errors through the linter, developers can catch these critical issues when running lint checks directly, without needing to switch to the language server or other tools.
14
+
15
+ ## Examples
16
+
17
+ ### ✅ Good
18
+
19
+ ```html
20
+ <h2>Welcome to our site</h2>
21
+ <p>This is a paragraph with proper structure.</p>
22
+
23
+ <div class="container">
24
+ <img src="image.jpg" alt="Description">
25
+ </div>
26
+ ```
27
+
28
+ ```erb
29
+ <h2><%= @page.title %></h2>
30
+ <p><%= @page.description %></p>
31
+
32
+ <% if user_signed_in? %>
33
+ <div class="user-section">
34
+ <%= current_user.name %>
35
+ </div>
36
+ <% end %>
37
+ ```
38
+
39
+ ### 🚫 Bad
40
+
41
+ ```html
42
+ <!-- Mismatched closing tag -->
43
+ <h2>Welcome to our site</h3>
44
+
45
+ <!-- Unclosed element -->
46
+ <div>
47
+ <p>This paragraph is never closed
48
+ </div>
49
+
50
+ <!-- Missing opening tag -->
51
+ Some content
52
+ </div>
53
+ ```
54
+
55
+ ```erb
56
+ <!-- Invalid Ruby syntax in ERB -->
57
+ <%= 1 + %>
58
+
59
+ <!-- Mismatched quotes -->
60
+ <div class="container'>Content</div>
61
+
62
+ <!-- Void element with closing tag -->
63
+ <img src="image.jpg" alt="Description"></img>
64
+ ```
65
+
66
+ ## Error Types
67
+
68
+ This rule reports various parser error types:
69
+
70
+ - **`UNCLOSED_ELEMENT_ERROR`**: Elements that are opened but never closed
71
+ - **`MISSING_CLOSING_TAG_ERROR`**: Opening tags without matching closing tags
72
+ - **`MISSING_OPENING_TAG_ERROR`**: Closing tags without matching opening tags
73
+ - **`TAG_NAMES_MISMATCH_ERROR`**: Opening and closing tags with different names
74
+ - **`QUOTES_MISMATCH_ERROR`**: Mismatched quotation marks in attributes
75
+ - **`VOID_ELEMENT_CLOSING_TAG_ERROR`**: Void elements (like `<img>`) with closing tags
76
+ - **`RUBY_PARSE_ERROR`**: Invalid Ruby syntax within ERB tags
77
+ - **`UNEXPECTED_TOKEN_ERROR`**: Unexpected tokens during parsing
78
+ - **`UNEXPECTED_ERROR`**: Other unexpected parsing issues
79
+
80
+ ## References
81
+
82
+ * [HTML Living Standard - Parsing](https://html.spec.whatwg.org/multipage/parsing.html)
83
+ * [W3C HTML Validator](https://validator.w3.org/)
84
+ * [ERB Template Guide](https://guides.rubyonrails.org/layouts_and_rendering.html)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herb-tools/linter",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "HTML+ERB linter for validating HTML structure and enforcing best practices",
5
5
  "license": "MIT",
6
6
  "homepage": "https://herb-tools.dev",
@@ -33,9 +33,9 @@
33
33
  }
34
34
  },
35
35
  "dependencies": {
36
- "@herb-tools/core": "0.4.3",
37
- "@herb-tools/highlighter": "0.4.3",
38
- "@herb-tools/node-wasm": "0.4.3",
36
+ "@herb-tools/core": "0.5.0",
37
+ "@herb-tools/highlighter": "0.5.0",
38
+ "@herb-tools/node-wasm": "0.5.0",
39
39
  "glob": "^11.0.3"
40
40
  },
41
41
  "files": [
@@ -11,9 +11,11 @@ import type { ThemeInput } from "@herb-tools/highlighter"
11
11
 
12
12
  import { name, version } from "../../package.json"
13
13
 
14
+ export type FormatOption = "simple" | "detailed" | "json" | "github"
15
+
14
16
  export interface ParsedArguments {
15
17
  pattern: string
16
- formatOption: 'simple' | 'detailed'
18
+ formatOption: FormatOption
17
19
  showTiming: boolean
18
20
  theme: ThemeInput
19
21
  wrapLines: boolean
@@ -32,9 +34,11 @@ export class ArgumentParser {
32
34
  Options:
33
35
  -h, --help show help
34
36
  -v, --version show version
35
- --format output format (simple|detailed) [default: detailed]
37
+ --format output format (simple|detailed|json|github) [default: detailed]
36
38
  --simple use simple output format (shortcut for --format simple)
37
- --theme syntax highlighting theme (${THEME_NAMES.join('|')}) or path to custom theme file [default: ${DEFAULT_THEME}]
39
+ --json use JSON output format (shortcut for --format json)
40
+ --github use GitHub Actions output format (shortcut for --format github)
41
+ --theme syntax highlighting theme (${THEME_NAMES.join("|")}) or path to custom theme file [default: ${DEFAULT_THEME}]
38
42
  --no-color disable colored output
39
43
  --no-timing hide timing information
40
44
  --no-wrap-lines disable line wrapping
@@ -45,15 +49,17 @@ export class ArgumentParser {
45
49
  const { values, positionals } = parseArgs({
46
50
  args: argv.slice(2),
47
51
  options: {
48
- help: { type: 'boolean', short: 'h' },
49
- version: { type: 'boolean', short: 'v' },
50
- format: { type: 'string' },
51
- simple: { type: 'boolean' },
52
- theme: { type: 'string' },
53
- 'no-color': { type: 'boolean' },
54
- 'no-timing': { type: 'boolean' },
55
- 'no-wrap-lines': { type: 'boolean' },
56
- 'truncate-lines': { type: 'boolean' }
52
+ help: { type: "boolean", short: "h" },
53
+ version: { type: "boolean", short: "v" },
54
+ format: { type: "string" },
55
+ simple: { type: "boolean" },
56
+ json: { type: "boolean" },
57
+ github: { type: "boolean" },
58
+ theme: { type: "string" },
59
+ "no-color": { type: "boolean" },
60
+ "no-timing": { type: "boolean" },
61
+ "no-wrap-lines": { type: "boolean" },
62
+ "truncate-lines": { type: "boolean" }
57
63
  },
58
64
  allowPositionals: true
59
65
  })
@@ -69,8 +75,8 @@ export class ArgumentParser {
69
75
  process.exit(0)
70
76
  }
71
77
 
72
- let formatOption: 'simple' | 'detailed' = 'detailed'
73
- if (values.format && (values.format === "detailed" || values.format === "simple")) {
78
+ let formatOption: FormatOption = "detailed"
79
+ if (values.format && (values.format === "detailed" || values.format === "simple" || values.format === "json" || values.format === "github")) {
74
80
  formatOption = values.format
75
81
  }
76
82
 
@@ -78,21 +84,29 @@ export class ArgumentParser {
78
84
  formatOption = "simple"
79
85
  }
80
86
 
81
- if (values['no-color']) {
87
+ if (values.json) {
88
+ formatOption = "json"
89
+ }
90
+
91
+ if (values.github) {
92
+ formatOption = "github"
93
+ }
94
+
95
+ if (values["no-color"]) {
82
96
  process.env.NO_COLOR = "1"
83
97
  }
84
98
 
85
- const showTiming = !values['no-timing']
99
+ const showTiming = !values["no-timing"]
86
100
 
87
- let wrapLines = !values['no-wrap-lines']
101
+ let wrapLines = !values["no-wrap-lines"]
88
102
  let truncateLines = false
89
103
 
90
- if (values['truncate-lines']) {
104
+ if (values["truncate-lines"]) {
91
105
  truncateLines = true
92
106
  wrapLines = false
93
107
  }
94
108
 
95
- if (!values['no-wrap-lines'] && values['truncate-lines']) {
109
+ if (!values["no-wrap-lines"] && values["truncate-lines"]) {
96
110
  console.error("Error: Line wrapping and --truncate-lines cannot be used together. Use --no-wrap-lines with --truncate-lines.")
97
111
  process.exit(1)
98
112
  }