@herb-tools/linter 0.7.5 → 0.8.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.
- package/README.md +253 -13
- package/dist/herb-lint.js +26023 -3424
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +5759 -1583
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5727 -1584
- package/dist/index.js.map +1 -1
- package/dist/loader.cjs +17010 -0
- package/dist/loader.cjs.map +1 -0
- package/dist/loader.js +16879 -0
- package/dist/loader.js.map +1 -0
- package/dist/package.json +13 -5
- package/dist/src/cli/argument-parser.js +38 -33
- package/dist/src/cli/argument-parser.js.map +1 -1
- package/dist/src/cli/file-processor.js +124 -23
- package/dist/src/cli/file-processor.js.map +1 -1
- package/dist/src/cli/formatters/detailed-formatter.js +18 -3
- package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
- package/dist/src/cli/formatters/github-actions-formatter.js +15 -1
- package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -1
- package/dist/src/cli/formatters/json-formatter.js +3 -0
- package/dist/src/cli/formatters/json-formatter.js.map +1 -1
- package/dist/src/cli/formatters/simple-formatter.js +20 -7
- package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
- package/dist/src/cli/output-manager.js +22 -3
- package/dist/src/cli/output-manager.js.map +1 -1
- package/dist/src/cli/summary-reporter.js +26 -3
- package/dist/src/cli/summary-reporter.js.map +1 -1
- package/dist/src/cli.js +107 -42
- package/dist/src/cli.js.map +1 -1
- package/dist/src/custom-rule-loader.js +139 -0
- package/dist/src/custom-rule-loader.js.map +1 -0
- package/dist/src/herb-disable-comment-utils.js +129 -0
- package/dist/src/herb-disable-comment-utils.js.map +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/linter.js +369 -34
- package/dist/src/linter.js.map +1 -1
- package/dist/src/loader.js +17 -0
- package/dist/src/loader.js.map +1 -0
- package/dist/src/rules/erb-comment-syntax.js +31 -2
- package/dist/src/rules/erb-comment-syntax.js.map +1 -1
- package/dist/src/rules/erb-no-case-node-children.js +52 -0
- package/dist/src/rules/erb-no-case-node-children.js.map +1 -0
- package/dist/src/rules/erb-no-empty-tags.js +7 -1
- package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
- package/dist/src/rules/erb-no-extra-newline.js +65 -0
- package/dist/src/rules/erb-no-extra-newline.js.map +1 -0
- package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js +95 -0
- package/dist/src/rules/erb-no-extra-whitespace-inside-tags.js.map +1 -0
- package/dist/src/rules/erb-no-output-control-flow.js +7 -1
- package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
- package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js +7 -1
- package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -1
- package/dist/src/rules/erb-prefer-image-tag-helper.js +7 -1
- package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
- package/dist/src/rules/erb-require-trailing-newline.js +35 -0
- package/dist/src/rules/erb-require-trailing-newline.js.map +1 -0
- package/dist/src/rules/erb-require-whitespace-inside-tags.js +69 -11
- package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
- package/dist/src/rules/erb-right-trim.js +26 -9
- package/dist/src/rules/erb-right-trim.js.map +1 -1
- package/dist/src/rules/herb-disable-comment-base.js +51 -0
- package/dist/src/rules/herb-disable-comment-base.js.map +1 -0
- package/dist/src/rules/herb-disable-comment-malformed.js +51 -0
- package/dist/src/rules/herb-disable-comment-malformed.js.map +1 -0
- package/dist/src/rules/herb-disable-comment-missing-rules.js +29 -0
- package/dist/src/rules/herb-disable-comment-missing-rules.js.map +1 -0
- package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js +32 -0
- package/dist/src/rules/herb-disable-comment-no-duplicate-rules.js.map +1 -0
- package/dist/src/rules/herb-disable-comment-no-redundant-all.js +31 -0
- package/dist/src/rules/herb-disable-comment-no-redundant-all.js.map +1 -0
- package/dist/src/rules/herb-disable-comment-unnecessary.js +65 -0
- package/dist/src/rules/herb-disable-comment-unnecessary.js.map +1 -0
- package/dist/src/rules/herb-disable-comment-valid-rule-name.js +44 -0
- package/dist/src/rules/herb-disable-comment-valid-rule-name.js.map +1 -0
- package/dist/src/rules/html-anchor-require-href.js +7 -1
- package/dist/src/rules/html-anchor-require-href.js.map +1 -1
- package/dist/src/rules/html-aria-attribute-must-be-valid.js +7 -1
- package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-aria-label-is-well-formatted.js +9 -3
- package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -1
- package/dist/src/rules/html-aria-level-must-be-valid.js +6 -0
- package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-aria-role-heading-requires-level.js +7 -1
- package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
- package/dist/src/rules/html-aria-role-must-be-valid.js +7 -1
- package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-attribute-double-quotes.js +29 -2
- package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
- package/dist/src/rules/html-attribute-equals-spacing.js +18 -2
- package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -1
- package/dist/src/rules/html-attribute-values-require-quotes.js +39 -3
- package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
- package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js +7 -1
- package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -1
- package/dist/src/rules/html-body-only-elements.js +46 -0
- package/dist/src/rules/html-body-only-elements.js.map +1 -0
- package/dist/src/rules/html-boolean-attributes-no-value.js +18 -1
- package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
- package/dist/src/rules/html-head-only-elements.js +51 -0
- package/dist/src/rules/html-head-only-elements.js.map +1 -0
- package/dist/src/rules/html-iframe-has-title.js +8 -2
- package/dist/src/rules/html-iframe-has-title.js.map +1 -1
- package/dist/src/rules/html-img-require-alt.js +7 -1
- package/dist/src/rules/html-img-require-alt.js.map +1 -1
- package/dist/src/rules/html-input-require-autocomplete.js +70 -0
- package/dist/src/rules/html-input-require-autocomplete.js.map +1 -0
- package/dist/src/rules/html-navigation-has-label.js +7 -1
- package/dist/src/rules/html-navigation-has-label.js.map +1 -1
- package/dist/src/rules/html-no-aria-hidden-on-focusable.js +7 -1
- package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -1
- package/dist/src/rules/html-no-block-inside-inline.js +7 -1
- package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-attributes.js +7 -1
- package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-ids.js +9 -3
- package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-meta-names.js +136 -0
- package/dist/src/rules/html-no-duplicate-meta-names.js.map +1 -0
- package/dist/src/rules/html-no-empty-attributes.js +45 -7
- package/dist/src/rules/html-no-empty-attributes.js.map +1 -1
- package/dist/src/rules/html-no-empty-headings.js +7 -6
- package/dist/src/rules/html-no-empty-headings.js.map +1 -1
- package/dist/src/rules/html-no-nested-links.js +7 -1
- package/dist/src/rules/html-no-nested-links.js.map +1 -1
- package/dist/src/rules/html-no-positive-tab-index.js +7 -1
- package/dist/src/rules/html-no-positive-tab-index.js.map +1 -1
- package/dist/src/rules/html-no-self-closing.js +48 -3
- package/dist/src/rules/html-no-self-closing.js.map +1 -1
- package/dist/src/rules/html-no-space-in-tag.js +173 -0
- package/dist/src/rules/html-no-space-in-tag.js.map +1 -0
- package/dist/src/rules/html-no-title-attribute.js +7 -1
- package/dist/src/rules/html-no-title-attribute.js.map +1 -1
- package/dist/src/rules/html-no-underscores-in-attribute-names.js +7 -1
- package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +1 -1
- package/dist/src/rules/html-tag-name-lowercase.js +23 -5
- package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
- package/dist/src/rules/index.js +19 -3
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/parser-no-errors.js +6 -0
- package/dist/src/rules/parser-no-errors.js.map +1 -1
- package/dist/src/rules/rule-utils.js +211 -31
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +22 -2
- package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
- package/dist/src/{default-rules.js → rules.js} +44 -16
- package/dist/src/rules.js.map +1 -0
- package/dist/src/types.js +34 -1
- package/dist/src/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/cli/argument-parser.d.ts +8 -2
- package/dist/types/cli/file-processor.d.ts +15 -0
- package/dist/types/cli/formatters/json-formatter.d.ts +6 -0
- package/dist/types/cli/formatters/simple-formatter.d.ts +1 -0
- package/dist/types/cli/summary-reporter.d.ts +6 -0
- package/dist/types/cli.d.ts +9 -4
- package/dist/types/custom-rule-loader.d.ts +62 -0
- package/dist/types/herb-disable-comment-utils.d.ts +69 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/linter.d.ts +99 -3
- package/dist/types/loader.d.ts +20 -0
- package/dist/types/rules/erb-comment-syntax.d.ts +12 -5
- package/dist/types/rules/erb-no-case-node-children.d.ts +8 -0
- package/dist/types/rules/erb-no-empty-tags.d.ts +3 -2
- package/dist/types/rules/erb-no-extra-newline.d.ts +14 -0
- package/dist/types/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
- package/dist/types/rules/erb-no-output-control-flow.d.ts +3 -2
- package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
- package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +3 -2
- package/dist/types/rules/erb-require-trailing-newline.d.ts +9 -0
- package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
- package/dist/types/rules/erb-right-trim.d.ts +12 -5
- package/dist/types/rules/herb-disable-comment-base.d.ts +37 -0
- package/dist/types/rules/herb-disable-comment-malformed.d.ts +8 -0
- package/dist/types/rules/herb-disable-comment-missing-rules.d.ts +8 -0
- package/dist/types/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
- package/dist/types/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
- package/dist/types/rules/herb-disable-comment-unnecessary.d.ts +8 -0
- package/dist/types/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
- package/dist/types/rules/html-anchor-require-href.d.ts +3 -2
- package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
- package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +3 -2
- package/dist/types/rules/html-aria-level-must-be-valid.d.ts +3 -2
- package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +3 -2
- package/dist/types/rules/html-aria-role-must-be-valid.d.ts +3 -2
- package/dist/types/rules/html-attribute-double-quotes.d.ts +13 -5
- package/dist/types/rules/html-attribute-equals-spacing.d.ts +12 -5
- package/dist/types/rules/html-attribute-values-require-quotes.d.ts +13 -5
- package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
- package/dist/types/rules/html-body-only-elements.d.ts +9 -0
- package/dist/types/rules/html-boolean-attributes-no-value.d.ts +12 -5
- package/dist/types/rules/html-head-only-elements.d.ts +9 -0
- package/dist/types/rules/html-iframe-has-title.d.ts +3 -2
- package/dist/types/rules/html-img-require-alt.d.ts +3 -2
- package/dist/types/rules/html-input-require-autocomplete.d.ts +8 -0
- package/dist/types/rules/html-navigation-has-label.d.ts +3 -2
- package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
- package/dist/types/rules/html-no-block-inside-inline.d.ts +3 -2
- package/dist/types/rules/html-no-duplicate-attributes.d.ts +3 -2
- package/dist/types/rules/html-no-duplicate-ids.d.ts +3 -2
- package/dist/types/rules/html-no-duplicate-meta-names.d.ts +9 -0
- package/dist/types/rules/html-no-empty-attributes.d.ts +3 -2
- package/dist/types/rules/html-no-empty-headings.d.ts +3 -2
- package/dist/types/rules/html-no-nested-links.d.ts +3 -2
- package/dist/types/rules/html-no-positive-tab-index.d.ts +3 -2
- package/dist/types/rules/html-no-self-closing.d.ts +14 -5
- package/dist/types/rules/html-no-space-in-tag.d.ts +16 -0
- package/dist/types/rules/html-no-title-attribute.d.ts +3 -2
- package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
- package/dist/types/rules/html-tag-name-lowercase.d.ts +16 -6
- package/dist/types/rules/index.d.ts +19 -3
- package/dist/types/rules/parser-no-errors.d.ts +2 -1
- package/dist/types/rules/rule-utils.d.ts +72 -25
- package/dist/types/rules/svg-tag-name-capitalization.d.ts +13 -4
- package/dist/types/rules.d.ts +2 -0
- package/dist/types/src/cli/argument-parser.d.ts +8 -2
- package/dist/types/src/cli/file-processor.d.ts +15 -0
- package/dist/types/src/cli/formatters/json-formatter.d.ts +6 -0
- package/dist/types/src/cli/formatters/simple-formatter.d.ts +1 -0
- package/dist/types/src/cli/summary-reporter.d.ts +6 -0
- package/dist/types/src/cli.d.ts +9 -4
- package/dist/types/src/custom-rule-loader.d.ts +62 -0
- package/dist/types/src/herb-disable-comment-utils.d.ts +69 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/linter.d.ts +99 -3
- package/dist/types/src/loader.d.ts +20 -0
- package/dist/types/src/rules/erb-comment-syntax.d.ts +12 -5
- package/dist/types/src/rules/erb-no-case-node-children.d.ts +8 -0
- package/dist/types/src/rules/erb-no-empty-tags.d.ts +3 -2
- package/dist/types/src/rules/erb-no-extra-newline.d.ts +14 -0
- package/dist/types/src/rules/erb-no-extra-whitespace-inside-tags.d.ts +18 -0
- package/dist/types/src/rules/erb-no-output-control-flow.d.ts +3 -2
- package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +3 -2
- package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +3 -2
- package/dist/types/src/rules/erb-require-trailing-newline.d.ts +9 -0
- package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +16 -5
- package/dist/types/src/rules/erb-right-trim.d.ts +12 -5
- package/dist/types/src/rules/herb-disable-comment-base.d.ts +37 -0
- package/dist/types/src/rules/herb-disable-comment-malformed.d.ts +8 -0
- package/dist/types/src/rules/herb-disable-comment-missing-rules.d.ts +8 -0
- package/dist/types/src/rules/herb-disable-comment-no-duplicate-rules.d.ts +8 -0
- package/dist/types/src/rules/herb-disable-comment-no-redundant-all.d.ts +8 -0
- package/dist/types/src/rules/herb-disable-comment-unnecessary.d.ts +8 -0
- package/dist/types/src/rules/herb-disable-comment-valid-rule-name.d.ts +8 -0
- package/dist/types/src/rules/html-anchor-require-href.d.ts +3 -2
- package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +3 -2
- package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +3 -2
- package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +3 -2
- package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +3 -2
- package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +3 -2
- package/dist/types/src/rules/html-attribute-double-quotes.d.ts +13 -5
- package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +12 -5
- package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +13 -5
- package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +3 -2
- package/dist/types/src/rules/html-body-only-elements.d.ts +9 -0
- package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +12 -5
- package/dist/types/src/rules/html-head-only-elements.d.ts +9 -0
- package/dist/types/src/rules/html-iframe-has-title.d.ts +3 -2
- package/dist/types/src/rules/html-img-require-alt.d.ts +3 -2
- package/dist/types/src/rules/html-input-require-autocomplete.d.ts +8 -0
- package/dist/types/src/rules/html-navigation-has-label.d.ts +3 -2
- package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +3 -2
- package/dist/types/src/rules/html-no-block-inside-inline.d.ts +3 -2
- package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +3 -2
- package/dist/types/src/rules/html-no-duplicate-ids.d.ts +3 -2
- package/dist/types/src/rules/html-no-duplicate-meta-names.d.ts +9 -0
- package/dist/types/src/rules/html-no-empty-attributes.d.ts +3 -2
- package/dist/types/src/rules/html-no-empty-headings.d.ts +3 -2
- package/dist/types/src/rules/html-no-nested-links.d.ts +3 -2
- package/dist/types/src/rules/html-no-positive-tab-index.d.ts +3 -2
- package/dist/types/src/rules/html-no-self-closing.d.ts +14 -5
- package/dist/types/src/rules/html-no-space-in-tag.d.ts +16 -0
- package/dist/types/src/rules/html-no-title-attribute.d.ts +3 -2
- package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +3 -2
- package/dist/types/src/rules/html-tag-name-lowercase.d.ts +16 -6
- package/dist/types/src/rules/index.d.ts +19 -3
- package/dist/types/src/rules/parser-no-errors.d.ts +2 -1
- package/dist/types/src/rules/rule-utils.d.ts +72 -25
- package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +13 -4
- package/dist/types/src/rules.d.ts +2 -0
- package/dist/types/src/types.d.ts +102 -11
- package/dist/types/types.d.ts +102 -11
- package/docs/rules/README.md +16 -2
- package/docs/rules/erb-no-case-node-children.md +50 -0
- package/docs/rules/erb-no-extra-newline.md +74 -0
- package/docs/rules/erb-no-extra-whitespace-inside-tags.md +39 -0
- package/docs/rules/{erb-requires-trailing-newline.md → erb-require-trailing-newline.md} +1 -1
- package/docs/rules/erb-right-trim.md +5 -10
- package/docs/rules/herb-disable-comment-malformed.md +45 -0
- package/docs/rules/herb-disable-comment-missing-rules.md +60 -0
- package/docs/rules/herb-disable-comment-no-duplicate-rules.md +49 -0
- package/docs/rules/herb-disable-comment-no-redundant-all.md +53 -0
- package/docs/rules/herb-disable-comment-unnecessary.md +44 -0
- package/docs/rules/herb-disable-comment-valid-rule-name.md +41 -0
- package/docs/rules/html-aria-attribute-must-be-valid.md +2 -5
- package/docs/rules/html-aria-label-is-well-formatted.md +1 -1
- package/docs/rules/html-attribute-double-quotes.md +2 -2
- package/docs/rules/html-attribute-equals-spacing.md +2 -2
- package/docs/rules/html-attribute-values-require-quotes.md +3 -3
- package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +2 -2
- package/docs/rules/html-body-only-elements.md +99 -0
- package/docs/rules/html-boolean-attributes-no-value.md +2 -2
- package/docs/rules/html-head-only-elements.md +81 -0
- package/docs/rules/html-input-require-autocomplete.md +64 -0
- package/docs/rules/html-no-aria-hidden-on-focusable.md +2 -2
- package/docs/rules/html-no-duplicate-attributes.md +2 -2
- package/docs/rules/html-no-duplicate-meta-names.md +64 -0
- package/docs/rules/html-no-empty-attributes.md +3 -3
- package/docs/rules/html-no-empty-headings.md +4 -26
- package/docs/rules/html-no-positive-tab-index.md +1 -2
- package/docs/rules/html-no-self-closing.md +17 -2
- package/docs/rules/html-no-space-in-tag.md +66 -0
- package/docs/rules/html-no-title-attribute.md +2 -2
- package/docs/rules/html-no-underscores-in-attribute-names.md +2 -2
- package/docs/rules/html-tag-name-lowercase.md +2 -2
- package/package.json +13 -5
- package/src/cli/argument-parser.ts +46 -37
- package/src/cli/file-processor.ts +159 -28
- package/src/cli/formatters/detailed-formatter.ts +21 -3
- package/src/cli/formatters/github-actions-formatter.ts +17 -1
- package/src/cli/formatters/json-formatter.ts +9 -0
- package/src/cli/formatters/simple-formatter.ts +24 -8
- package/src/cli/output-manager.ts +23 -3
- package/src/cli/summary-reporter.ts +40 -3
- package/src/cli.ts +134 -51
- package/src/custom-rule-loader.ts +189 -0
- package/src/herb-disable-comment-utils.ts +175 -0
- package/src/index.ts +2 -0
- package/src/linter.ts +501 -36
- package/src/loader.ts +30 -0
- package/src/rules/erb-comment-syntax.ts +53 -10
- package/src/rules/erb-no-case-node-children.ts +68 -0
- package/src/rules/erb-no-empty-tags.ts +9 -3
- package/src/rules/erb-no-extra-newline.ts +91 -0
- package/src/rules/erb-no-extra-whitespace-inside-tags.ts +147 -0
- package/src/rules/erb-no-output-control-flow.ts +9 -3
- package/src/rules/erb-no-silent-tag-in-attribute-name.ts +9 -3
- package/src/rules/erb-prefer-image-tag-helper.ts +9 -3
- package/src/rules/erb-require-trailing-newline.ts +47 -0
- package/src/rules/erb-require-whitespace-inside-tags.ts +94 -16
- package/src/rules/erb-right-trim.ts +45 -22
- package/src/rules/herb-disable-comment-base.ts +76 -0
- package/src/rules/herb-disable-comment-malformed.ts +66 -0
- package/src/rules/herb-disable-comment-missing-rules.ts +41 -0
- package/src/rules/herb-disable-comment-no-duplicate-rules.ts +46 -0
- package/src/rules/herb-disable-comment-no-redundant-all.ts +40 -0
- package/src/rules/herb-disable-comment-unnecessary.ts +103 -0
- package/src/rules/herb-disable-comment-valid-rule-name.ts +62 -0
- package/src/rules/html-anchor-require-href.ts +9 -3
- package/src/rules/html-aria-attribute-must-be-valid.ts +9 -3
- package/src/rules/html-aria-label-is-well-formatted.ts +9 -5
- package/src/rules/html-aria-level-must-be-valid.ts +9 -2
- package/src/rules/html-aria-role-heading-requires-level.ts +9 -3
- package/src/rules/html-aria-role-must-be-valid.ts +9 -3
- package/src/rules/html-attribute-double-quotes.ts +42 -8
- package/src/rules/html-attribute-equals-spacing.ts +31 -7
- package/src/rules/html-attribute-values-require-quotes.ts +56 -10
- package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +9 -3
- package/src/rules/html-body-only-elements.ts +60 -0
- package/src/rules/html-boolean-attributes-no-value.ts +31 -6
- package/src/rules/html-head-only-elements.ts +65 -0
- package/src/rules/html-iframe-has-title.ts +9 -4
- package/src/rules/html-img-require-alt.ts +10 -4
- package/src/rules/html-input-require-autocomplete.ts +85 -0
- package/src/rules/html-navigation-has-label.ts +9 -3
- package/src/rules/html-no-aria-hidden-on-focusable.ts +9 -3
- package/src/rules/html-no-block-inside-inline.ts +9 -3
- package/src/rules/html-no-duplicate-attributes.ts +9 -3
- package/src/rules/html-no-duplicate-ids.ts +11 -7
- package/src/rules/html-no-duplicate-meta-names.ts +188 -0
- package/src/rules/html-no-empty-attributes.ts +58 -10
- package/src/rules/html-no-empty-headings.ts +10 -8
- package/src/rules/html-no-nested-links.ts +10 -4
- package/src/rules/html-no-positive-tab-index.ts +9 -3
- package/src/rules/html-no-self-closing.ts +69 -9
- package/src/rules/html-no-space-in-tag.ts +221 -0
- package/src/rules/html-no-title-attribute.ts +9 -3
- package/src/rules/html-no-underscores-in-attribute-names.ts +12 -4
- package/src/rules/html-tag-name-lowercase.ts +41 -10
- package/src/rules/index.ts +23 -3
- package/src/rules/parser-no-errors.ts +8 -1
- package/src/rules/rule-utils.ts +248 -42
- package/src/rules/svg-tag-name-capitalization.ts +39 -6
- package/src/{default-rules.ts → rules.ts} +51 -15
- package/src/types.ts +133 -15
- package/dist/src/default-rules.js.map +0 -1
- package/dist/src/rules/erb-requires-trailing-newline.js +0 -22
- package/dist/src/rules/erb-requires-trailing-newline.js.map +0 -1
- package/dist/types/default-rules.d.ts +0 -2
- package/dist/types/rules/erb-requires-trailing-newline.d.ts +0 -6
- package/dist/types/src/default-rules.d.ts +0 -2
- package/dist/types/src/rules/erb-requires-trailing-newline.d.ts +0 -6
- package/src/rules/erb-requires-trailing-newline.ts +0 -29
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ParserRule } from "../types.js"
|
|
2
2
|
import { AttributeVisitorMixin, StaticAttributeStaticValueParams, DynamicAttributeStaticValueParams } from "./rule-utils.js"
|
|
3
3
|
import { IdentityPrinter } from "@herb-tools/printer"
|
|
4
|
+
import { Visitor, isERBOutputNode } from "@herb-tools/core"
|
|
4
5
|
|
|
5
|
-
import type {
|
|
6
|
-
import type { ParseResult, HTMLAttributeNode } from "@herb-tools/core"
|
|
6
|
+
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
|
|
7
|
+
import type { ParseResult, HTMLAttributeNode, ERBContentNode, LiteralNode, Node } from "@herb-tools/core"
|
|
7
8
|
|
|
8
|
-
// Attributes that must not have empty values
|
|
9
9
|
const RESTRICTED_ATTRIBUTES = new Set([
|
|
10
10
|
'id',
|
|
11
11
|
'class',
|
|
@@ -18,19 +18,15 @@ const RESTRICTED_ATTRIBUTES = new Set([
|
|
|
18
18
|
'role'
|
|
19
19
|
])
|
|
20
20
|
|
|
21
|
-
// Check if attribute name matches any restricted patterns
|
|
22
21
|
function isRestrictedAttribute(attributeName: string): boolean {
|
|
23
|
-
// Check direct matches
|
|
24
22
|
if (RESTRICTED_ATTRIBUTES.has(attributeName)) {
|
|
25
23
|
return true
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
// Check for data-* attributes
|
|
29
26
|
if (attributeName.startsWith('data-')) {
|
|
30
27
|
return true
|
|
31
28
|
}
|
|
32
29
|
|
|
33
|
-
// Check for aria-* attributes
|
|
34
30
|
if (attributeName.startsWith('aria-')) {
|
|
35
31
|
return true
|
|
36
32
|
}
|
|
@@ -42,6 +38,50 @@ function isDataAttribute(attributeName: string): boolean {
|
|
|
42
38
|
return attributeName.startsWith('data-')
|
|
43
39
|
}
|
|
44
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Visitor that checks if a node tree contains any output content.
|
|
43
|
+
* Output content includes:
|
|
44
|
+
* - Non-whitespace literal text (LiteralNode)
|
|
45
|
+
* - ERB output tags (<%= %>, <%== %>)
|
|
46
|
+
*/
|
|
47
|
+
class ContainsOutputContentVisitor extends Visitor {
|
|
48
|
+
public hasOutputContent: boolean = false
|
|
49
|
+
|
|
50
|
+
visitLiteralNode(node: LiteralNode): void {
|
|
51
|
+
if (this.hasOutputContent) return
|
|
52
|
+
|
|
53
|
+
if (node.content && node.content.trim() !== "") {
|
|
54
|
+
this.hasOutputContent = true
|
|
55
|
+
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.visitChildNodes(node)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
visitERBContentNode(node: ERBContentNode): void {
|
|
63
|
+
if (this.hasOutputContent) return
|
|
64
|
+
|
|
65
|
+
if (isERBOutputNode(node)) {
|
|
66
|
+
this.hasOutputContent = true
|
|
67
|
+
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.visitChildNodes(node)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
function containsOutputContent(node: Node): boolean {
|
|
77
|
+
const visitor = new ContainsOutputContentVisitor()
|
|
78
|
+
|
|
79
|
+
visitor.visit(node)
|
|
80
|
+
|
|
81
|
+
return visitor.hasOutputContent
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
45
85
|
class NoEmptyAttributesVisitor extends AttributeVisitorMixin {
|
|
46
86
|
protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams): void {
|
|
47
87
|
this.checkEmptyAttribute(attributeName, attributeValue, attributeNode)
|
|
@@ -56,6 +96,9 @@ class NoEmptyAttributesVisitor extends AttributeVisitorMixin {
|
|
|
56
96
|
if (!isRestrictedAttribute(attributeName)) return
|
|
57
97
|
if (attributeValue.trim() !== "") return
|
|
58
98
|
|
|
99
|
+
if (!attributeNode?.value) return
|
|
100
|
+
if (containsOutputContent(attributeNode.value)) return
|
|
101
|
+
|
|
59
102
|
const hasExplicitValue = attributeNode.value !== null
|
|
60
103
|
|
|
61
104
|
if (isDataAttribute(attributeName)) {
|
|
@@ -63,7 +106,6 @@ class NoEmptyAttributesVisitor extends AttributeVisitorMixin {
|
|
|
63
106
|
this.addOffense(
|
|
64
107
|
`Data attribute \`${attributeName}\` should not have an empty value. Either provide a meaningful value or use \`${attributeName}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`,
|
|
65
108
|
attributeNode.location,
|
|
66
|
-
"warning"
|
|
67
109
|
)
|
|
68
110
|
}
|
|
69
111
|
|
|
@@ -73,7 +115,6 @@ class NoEmptyAttributesVisitor extends AttributeVisitorMixin {
|
|
|
73
115
|
this.addOffense(
|
|
74
116
|
`Attribute \`${attributeName}\` must not be empty. Either provide a meaningful value or remove the attribute entirely.`,
|
|
75
117
|
attributeNode.location,
|
|
76
|
-
"warning"
|
|
77
118
|
)
|
|
78
119
|
}
|
|
79
120
|
}
|
|
@@ -81,7 +122,14 @@ class NoEmptyAttributesVisitor extends AttributeVisitorMixin {
|
|
|
81
122
|
export class HTMLNoEmptyAttributesRule extends ParserRule {
|
|
82
123
|
name = "html-no-empty-attributes"
|
|
83
124
|
|
|
84
|
-
|
|
125
|
+
get defaultConfig(): FullRuleConfig {
|
|
126
|
+
return {
|
|
127
|
+
enabled: true,
|
|
128
|
+
severity: "warning"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
|
|
85
133
|
const visitor = new NoEmptyAttributesVisitor(this.name, context)
|
|
86
134
|
|
|
87
135
|
visitor.visit(result.value)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { BaseRuleVisitor, getTagName, getAttributes, findAttributeByName, getAttributeValue, HEADING_TAGS } from "./rule-utils.js"
|
|
2
2
|
|
|
3
3
|
import { ParserRule } from "../types.js"
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
|
|
5
6
|
import type { HTMLElementNode, HTMLOpenTagNode, ParseResult, LiteralNode, HTMLTextNode } from "@herb-tools/core"
|
|
6
7
|
|
|
7
8
|
class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
@@ -38,7 +39,6 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
38
39
|
this.addOffense(
|
|
39
40
|
`Heading element ${elementDescription} must not be empty. Provide accessible text content for screen readers and SEO.`,
|
|
40
41
|
node.location,
|
|
41
|
-
"error"
|
|
42
42
|
)
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -49,7 +49,6 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
49
49
|
return true
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// Check if all content is just whitespace or inaccessible
|
|
53
52
|
let hasAccessibleContent = false
|
|
54
53
|
|
|
55
54
|
for (const child of node.body) {
|
|
@@ -70,13 +69,11 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
70
69
|
} else if (child.type === "AST_HTML_ELEMENT_NODE") {
|
|
71
70
|
const elementNode = child as HTMLElementNode
|
|
72
71
|
|
|
73
|
-
// Check if this element is accessible (not aria-hidden="true")
|
|
74
72
|
if (this.isElementAccessible(elementNode)) {
|
|
75
73
|
hasAccessibleContent = true
|
|
76
74
|
break
|
|
77
75
|
}
|
|
78
76
|
} else {
|
|
79
|
-
// If there's any non-literal/non-text/non-element content (like ERB), consider it accessible
|
|
80
77
|
hasAccessibleContent = true
|
|
81
78
|
break
|
|
82
79
|
}
|
|
@@ -98,7 +95,6 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
98
95
|
}
|
|
99
96
|
|
|
100
97
|
private isElementAccessible(node: HTMLElementNode): boolean {
|
|
101
|
-
// Check if the element has aria-hidden="true"
|
|
102
98
|
if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
|
|
103
99
|
return true
|
|
104
100
|
}
|
|
@@ -115,7 +111,6 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
115
111
|
}
|
|
116
112
|
}
|
|
117
113
|
|
|
118
|
-
// Recursively check if the element has any accessible content
|
|
119
114
|
if (!node.body || node.body.length === 0) {
|
|
120
115
|
return false
|
|
121
116
|
}
|
|
@@ -149,7 +144,14 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
149
144
|
export class HTMLNoEmptyHeadingsRule extends ParserRule {
|
|
150
145
|
name = "html-no-empty-headings"
|
|
151
146
|
|
|
152
|
-
|
|
147
|
+
get defaultConfig(): FullRuleConfig {
|
|
148
|
+
return {
|
|
149
|
+
enabled: true,
|
|
150
|
+
severity: "error"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
|
|
153
155
|
const visitor = new NoEmptyHeadingsVisitor(this.name, context)
|
|
154
156
|
visitor.visit(result.value)
|
|
155
157
|
return visitor.offenses
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseRuleVisitor, getTagName } from "./rule-utils.js"
|
|
2
|
-
|
|
3
2
|
import { ParserRule } from "../types.js"
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
|
|
5
5
|
import type { HTMLOpenTagNode, HTMLElementNode, ParseResult } from "@herb-tools/core"
|
|
6
6
|
|
|
7
7
|
class NestedLinkVisitor extends BaseRuleVisitor {
|
|
@@ -12,7 +12,6 @@ class NestedLinkVisitor extends BaseRuleVisitor {
|
|
|
12
12
|
this.addOffense(
|
|
13
13
|
"Nested `<a>` elements are not allowed. Links cannot contain other links.",
|
|
14
14
|
openTag.tag_name!.location,
|
|
15
|
-
"error"
|
|
16
15
|
)
|
|
17
16
|
|
|
18
17
|
return true
|
|
@@ -58,7 +57,14 @@ class NestedLinkVisitor extends BaseRuleVisitor {
|
|
|
58
57
|
export class HTMLNoNestedLinksRule extends ParserRule {
|
|
59
58
|
name = "html-no-nested-links"
|
|
60
59
|
|
|
61
|
-
|
|
60
|
+
get defaultConfig(): FullRuleConfig {
|
|
61
|
+
return {
|
|
62
|
+
enabled: true,
|
|
63
|
+
severity: "error"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
|
|
62
68
|
const visitor = new NestedLinkVisitor(this.name, context)
|
|
63
69
|
visitor.visit(result.value)
|
|
64
70
|
return visitor.offenses
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ParserRule } from "../types.js"
|
|
2
2
|
import { AttributeVisitorMixin, StaticAttributeStaticValueParams } from "./rule-utils.js"
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
|
|
5
5
|
import type { ParseResult } from "@herb-tools/core"
|
|
6
6
|
|
|
7
7
|
class NoPositiveTabIndexVisitor extends AttributeVisitorMixin {
|
|
@@ -14,7 +14,6 @@ class NoPositiveTabIndexVisitor extends AttributeVisitorMixin {
|
|
|
14
14
|
this.addOffense(
|
|
15
15
|
`Do not use positive \`tabindex\` values as they are error prone and can severely disrupt navigation experience for keyboard users. Use \`tabindex="0"\` to make an element focusable or \`tabindex="-1"\` to remove it from the tab sequence.`,
|
|
16
16
|
attributeNode.location,
|
|
17
|
-
"error"
|
|
18
17
|
)
|
|
19
18
|
}
|
|
20
19
|
}
|
|
@@ -23,7 +22,14 @@ class NoPositiveTabIndexVisitor extends AttributeVisitorMixin {
|
|
|
23
22
|
export class HTMLNoPositiveTabIndexRule extends ParserRule {
|
|
24
23
|
name = "html-no-positive-tab-index"
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
get defaultConfig(): FullRuleConfig {
|
|
26
|
+
return {
|
|
27
|
+
enabled: true,
|
|
28
|
+
severity: "error"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
|
|
27
33
|
const visitor = new NoPositiveTabIndexVisitor(this.name, context)
|
|
28
34
|
|
|
29
35
|
visitor.visit(result.value)
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import { ParserRule } from "../types.js"
|
|
2
|
-
import {
|
|
3
|
-
import { getTagName } from "@herb-tools/core"
|
|
1
|
+
import { ParserRule, BaseAutofixContext, Mutable } from "../types.js"
|
|
2
|
+
import { isVoidElement, findParent, BaseRuleVisitor } from "./rule-utils.js"
|
|
3
|
+
import { getTagName, isWhitespaceNode, Location, HTMLCloseTagNode } from "@herb-tools/core"
|
|
4
4
|
|
|
5
|
-
import type { LintContext, LintOffense } from "../types.js"
|
|
6
|
-
import type { HTMLOpenTagNode, HTMLElementNode, ParseResult } from "@herb-tools/core"
|
|
5
|
+
import type { UnboundLintOffense, LintContext, LintOffense, FullRuleConfig } from "../types.js"
|
|
6
|
+
import type { Node, HTMLOpenTagNode, HTMLElementNode, SerializedToken, ParseResult } from "@herb-tools/core"
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
interface NoSelfClosingAutofixContext extends BaseAutofixContext {
|
|
9
|
+
node: Mutable<HTMLOpenTagNode>
|
|
10
|
+
tagName: string
|
|
11
|
+
isVoid: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class NoSelfClosingVisitor extends BaseRuleVisitor<NoSelfClosingAutofixContext> {
|
|
9
15
|
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
10
16
|
if (getTagName(node) === "svg") {
|
|
11
17
|
this.visit(node.open_tag)
|
|
@@ -22,20 +28,74 @@ class NoSelfClosingVisitor extends BaseRuleVisitor {
|
|
|
22
28
|
this.addOffense(
|
|
23
29
|
`Use \`${instead}\` instead of self-closing \`<${tagName} />\` for HTML compatibility.`,
|
|
24
30
|
node.location,
|
|
25
|
-
|
|
31
|
+
{
|
|
32
|
+
node,
|
|
33
|
+
tagName,
|
|
34
|
+
isVoid: isVoidElement(tagName)
|
|
35
|
+
}
|
|
26
36
|
)
|
|
27
37
|
}
|
|
28
38
|
}
|
|
29
39
|
}
|
|
30
40
|
|
|
31
|
-
export class HTMLNoSelfClosingRule extends ParserRule {
|
|
41
|
+
export class HTMLNoSelfClosingRule extends ParserRule<NoSelfClosingAutofixContext> {
|
|
42
|
+
static autocorrectable = true
|
|
32
43
|
name = "html-no-self-closing"
|
|
33
44
|
|
|
34
|
-
|
|
45
|
+
get defaultConfig(): FullRuleConfig {
|
|
46
|
+
return {
|
|
47
|
+
enabled: true,
|
|
48
|
+
severity: "error",
|
|
49
|
+
exclude: ["**/views/**/*_mailer/**/*"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<NoSelfClosingAutofixContext>[] {
|
|
35
54
|
const visitor = new NoSelfClosingVisitor(this.name, context)
|
|
36
55
|
|
|
37
56
|
visitor.visit(result.value)
|
|
38
57
|
|
|
39
58
|
return visitor.offenses
|
|
40
59
|
}
|
|
60
|
+
|
|
61
|
+
autofix(offense: LintOffense<NoSelfClosingAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
|
|
62
|
+
if (!offense.autofixContext) return null
|
|
63
|
+
|
|
64
|
+
const { node, tagName, isVoid } = offense.autofixContext
|
|
65
|
+
const { tag_closing } = node
|
|
66
|
+
|
|
67
|
+
if (!tag_closing) return null
|
|
68
|
+
|
|
69
|
+
tag_closing.value = ">"
|
|
70
|
+
|
|
71
|
+
if (node.children && Array.isArray(node.children)) {
|
|
72
|
+
const children = node.children as Node[]
|
|
73
|
+
|
|
74
|
+
if (children.length > 0 && isWhitespaceNode(children[children.length - 1])) {
|
|
75
|
+
node.children = children.slice(0, -1)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!isVoid) {
|
|
80
|
+
const parent = findParent(result.value, node as any as Node) as Mutable<HTMLElementNode> | null
|
|
81
|
+
|
|
82
|
+
if (parent && parent.type === "AST_HTML_ELEMENT_NODE") {
|
|
83
|
+
const tag_opening: SerializedToken = { type: "TOKEN_HTML_TAG_START_CLOSE", value: "</", location: Location.zero, range: [0, 0] }
|
|
84
|
+
const tag_name: SerializedToken = { type: "TOKEN_IDENTIFIER", value: tagName, location: Location.zero, range: [0, 0] }
|
|
85
|
+
const tag_closing: SerializedToken = { type: "TOKEN_HTML_TAG_END", value: ">", location: Location.zero, range: [0, 0] }
|
|
86
|
+
|
|
87
|
+
parent.close_tag = HTMLCloseTagNode.from({
|
|
88
|
+
type: "AST_HTML_CLOSE_TAG_NODE",
|
|
89
|
+
tag_opening,
|
|
90
|
+
tag_name,
|
|
91
|
+
tag_closing,
|
|
92
|
+
children: [],
|
|
93
|
+
errors: [],
|
|
94
|
+
location: Location.zero,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result
|
|
100
|
+
}
|
|
41
101
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { Token, Location, WhitespaceNode } from "@herb-tools/core"
|
|
2
|
+
import { ParserRule, BaseAutofixContext } from "../types.js"
|
|
3
|
+
|
|
4
|
+
import { findParent, BaseRuleVisitor } from "./rule-utils.js"
|
|
5
|
+
import { filterWhitespaceNodes, isWhitespaceNode, isHTMLOpenTagNode } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
import type { ParseResult, Node, HTMLCloseTagNode, HTMLOpenTagNode } from "@herb-tools/core"
|
|
8
|
+
import type { UnboundLintOffense, LintOffense, LintContext, Mutable, FullRuleConfig } from "../types.js"
|
|
9
|
+
|
|
10
|
+
const MESSAGES = {
|
|
11
|
+
EXTRA_SPACE_NO_SPACE: "Extra space detected where there should be no space.",
|
|
12
|
+
EXTRA_SPACE_SINGLE_SPACE: "Extra space detected where there should be a single space.",
|
|
13
|
+
EXTRA_SPACE_SINGLE_BREAK: "Extra space detected where there should be a single space or a single line break.",
|
|
14
|
+
NO_SPACE_SINGLE_SPACE: "No space detected where there should be a single space.",
|
|
15
|
+
} as const
|
|
16
|
+
|
|
17
|
+
interface HTMLNoSpaceInTagAutofixContext extends BaseAutofixContext {
|
|
18
|
+
node: WhitespaceNode | HTMLOpenTagNode
|
|
19
|
+
message: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class HTMLNoSpaceInTagVisitor extends BaseRuleVisitor<HTMLNoSpaceInTagAutofixContext> {
|
|
23
|
+
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
24
|
+
if (node.isSingleLine) {
|
|
25
|
+
this.checkSingleLineTag(node)
|
|
26
|
+
} else {
|
|
27
|
+
this.checkMultilineTag(node)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
visitHTMLCloseTagNode(node: HTMLCloseTagNode): void {
|
|
32
|
+
this.reportAllWhitespace(node.children, MESSAGES.EXTRA_SPACE_NO_SPACE)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private checkSingleLineTag(node: HTMLOpenTagNode): void {
|
|
36
|
+
const { children, tag_closing } = node
|
|
37
|
+
const isSelfClosing = tag_closing ? this.isSelfClosing(tag_closing) : false
|
|
38
|
+
|
|
39
|
+
this.checkWhitespaceInSingleLineTag(children, isSelfClosing)
|
|
40
|
+
this.checkMissingSpaceBeforeSelfClosing(node, children, isSelfClosing)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private checkWhitespaceInSingleLineTag(children: Node[], isSelfClosing: boolean): void {
|
|
44
|
+
const whitespaceNodes = filterWhitespaceNodes(children)
|
|
45
|
+
|
|
46
|
+
whitespaceNodes.forEach((whitespace) => {
|
|
47
|
+
const content = this.getWhitespaceContent(whitespace)
|
|
48
|
+
if (!content) return
|
|
49
|
+
|
|
50
|
+
const isLastChild = children[children.length - 1] === whitespace
|
|
51
|
+
if (isLastChild) {
|
|
52
|
+
this.checkTrailingWhitespace(whitespace, content, isSelfClosing)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (content.length > 1) {
|
|
57
|
+
this.addOffense(MESSAGES.EXTRA_SPACE_SINGLE_SPACE, whitespace.location, { node: whitespace, message: MESSAGES.EXTRA_SPACE_SINGLE_SPACE })
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private checkTrailingWhitespace(whitespace: WhitespaceNode, content: string, isSelfClosing: boolean): void {
|
|
63
|
+
if (isSelfClosing && content === ' ') return
|
|
64
|
+
|
|
65
|
+
this.addOffense(MESSAGES.EXTRA_SPACE_NO_SPACE, whitespace.location, { node: whitespace, message: MESSAGES.EXTRA_SPACE_NO_SPACE })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private checkMissingSpaceBeforeSelfClosing(node: HTMLOpenTagNode, children: Node[], isSelfClosing: boolean): void {
|
|
69
|
+
if (!isSelfClosing) return
|
|
70
|
+
|
|
71
|
+
const lastChild = children[children.length - 1]
|
|
72
|
+
if (lastChild && isWhitespaceNode(lastChild)) return
|
|
73
|
+
|
|
74
|
+
const lastNonWhitespace = children.filter(child => !isWhitespaceNode(child)).pop()
|
|
75
|
+
const locationToReport = lastNonWhitespace?.location ?? node.tag_name?.location ?? node.location
|
|
76
|
+
|
|
77
|
+
this.addOffense(MESSAGES.NO_SPACE_SINGLE_SPACE, locationToReport, { node, message: MESSAGES.NO_SPACE_SINGLE_SPACE })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private checkMultilineTag(node: HTMLOpenTagNode): void {
|
|
81
|
+
const whitespaceNodes = filterWhitespaceNodes(node.children)
|
|
82
|
+
let previousWhitespace: WhitespaceNode | null = null
|
|
83
|
+
|
|
84
|
+
whitespaceNodes.forEach((whitespace, index) => {
|
|
85
|
+
const content = this.getWhitespaceContent(whitespace)
|
|
86
|
+
if (!content) return
|
|
87
|
+
|
|
88
|
+
if (this.hasConsecutiveNewlines(content, previousWhitespace)) {
|
|
89
|
+
this.addOffense(MESSAGES.EXTRA_SPACE_SINGLE_BREAK, whitespace.location, { node: whitespace, message: MESSAGES.EXTRA_SPACE_SINGLE_BREAK })
|
|
90
|
+
previousWhitespace = whitespace
|
|
91
|
+
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (this.isNonNewlineWhitespace(content)) {
|
|
96
|
+
this.checkIndentation(whitespace, index, whitespaceNodes.length, node)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
previousWhitespace = whitespace
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private hasConsecutiveNewlines(content: string, previousWhitespace: WhitespaceNode | null): boolean {
|
|
104
|
+
if (content === "\n") return previousWhitespace?.value?.value === "\n"
|
|
105
|
+
if (!content.includes("\n")) return false
|
|
106
|
+
|
|
107
|
+
const newlines = content.match(/\n/g)
|
|
108
|
+
|
|
109
|
+
return (newlines?.length ?? 0) > 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private isNonNewlineWhitespace(content: string): boolean {
|
|
113
|
+
return !content.includes("\n")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private checkIndentation(whitespace: WhitespaceNode, index: number, totalWhitespaceNodes: number, node: HTMLOpenTagNode): void {
|
|
117
|
+
const isLastWhitespace = index === totalWhitespaceNodes - 1
|
|
118
|
+
const expectedIndent = isLastWhitespace ? node.location.start.column : node.location.start.column + 2
|
|
119
|
+
|
|
120
|
+
if (whitespace.location.end.column === expectedIndent) return
|
|
121
|
+
|
|
122
|
+
this.addOffense(MESSAGES.EXTRA_SPACE_NO_SPACE, whitespace.location, { node: whitespace, message: MESSAGES.EXTRA_SPACE_NO_SPACE })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private isSelfClosing(tag_closing: Token): boolean {
|
|
126
|
+
return tag_closing?.value?.includes('/') ?? false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private getWhitespaceContent(whitespace: WhitespaceNode): string | null {
|
|
130
|
+
return whitespace.value?.value ?? null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private reportAllWhitespace(nodes: Node[] | WhitespaceNode[], message: string): void {
|
|
134
|
+
const whitespaceNodes = Array.isArray(nodes) && nodes.length > 0 && !isWhitespaceNode(nodes[0])
|
|
135
|
+
? filterWhitespaceNodes(nodes)
|
|
136
|
+
: nodes as WhitespaceNode[]
|
|
137
|
+
|
|
138
|
+
whitespaceNodes.forEach(whitespace => {
|
|
139
|
+
this.addOffense(message, whitespace.location, { node: whitespace, message })
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class HTMLNoSpaceInTagRule extends ParserRule<HTMLNoSpaceInTagAutofixContext> {
|
|
145
|
+
// TODO: enable and fix autofix
|
|
146
|
+
static autocorrectable = false
|
|
147
|
+
name = "html-no-space-in-tag"
|
|
148
|
+
|
|
149
|
+
get defaultConfig(): FullRuleConfig {
|
|
150
|
+
return {
|
|
151
|
+
enabled: false,
|
|
152
|
+
severity: "error"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<HTMLNoSpaceInTagAutofixContext>[] {
|
|
157
|
+
const visitor = new HTMLNoSpaceInTagVisitor(this.name, context)
|
|
158
|
+
|
|
159
|
+
visitor.visit(result.value)
|
|
160
|
+
|
|
161
|
+
return visitor.offenses
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
autofix(offense: LintOffense<HTMLNoSpaceInTagAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
|
|
165
|
+
if (!offense.autofixContext) return null
|
|
166
|
+
|
|
167
|
+
const { node, message } = offense.autofixContext
|
|
168
|
+
if (!node) return null
|
|
169
|
+
|
|
170
|
+
if (isHTMLOpenTagNode(node)) {
|
|
171
|
+
const token = Token.from({ type: "TOKEN_WHITESPACE", value: " ", range: [0, 0], location: Location.zero })
|
|
172
|
+
const whitespace = new WhitespaceNode({ type: "AST_WHITESPACE_NODE", value: token, location: Location.zero, errors: [] })
|
|
173
|
+
|
|
174
|
+
node.children.push(whitespace)
|
|
175
|
+
|
|
176
|
+
return result
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!isWhitespaceNode(node)) return null
|
|
180
|
+
|
|
181
|
+
const whitespaceNode = node as Mutable<WhitespaceNode>
|
|
182
|
+
if (!whitespaceNode.value) return null
|
|
183
|
+
|
|
184
|
+
switch (message) {
|
|
185
|
+
case MESSAGES.EXTRA_SPACE_NO_SPACE: {
|
|
186
|
+
let selfClosing = false
|
|
187
|
+
let beginningOfLine = false
|
|
188
|
+
|
|
189
|
+
const parent = findParent(result.value, node)
|
|
190
|
+
|
|
191
|
+
if (parent && isHTMLOpenTagNode(parent)) {
|
|
192
|
+
selfClosing = parent.tag_closing?.value === "/>"
|
|
193
|
+
beginningOfLine = node.location.start.column === 0
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
whitespaceNode.value.value = selfClosing && !beginningOfLine ? " " : ""
|
|
197
|
+
|
|
198
|
+
return result
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
case MESSAGES.EXTRA_SPACE_SINGLE_BREAK: {
|
|
202
|
+
if (whitespaceNode.value.value.includes("\n")) {
|
|
203
|
+
whitespaceNode.value.value = ""
|
|
204
|
+
} else {
|
|
205
|
+
whitespaceNode.value.value = " "
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return result
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case MESSAGES.EXTRA_SPACE_SINGLE_SPACE:
|
|
212
|
+
case MESSAGES.NO_SPACE_SINGLE_SPACE: {
|
|
213
|
+
whitespaceNode.value.value = " "
|
|
214
|
+
|
|
215
|
+
return result
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
default: return null
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ParserRule } from "../types.js"
|
|
2
2
|
import { BaseRuleVisitor, getTagName, hasAttribute } from "./rule-utils.js"
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
|
|
5
5
|
import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
|
|
6
6
|
|
|
7
7
|
class NoTitleAttributeVisitor extends BaseRuleVisitor {
|
|
@@ -23,7 +23,6 @@ class NoTitleAttributeVisitor extends BaseRuleVisitor {
|
|
|
23
23
|
this.addOffense(
|
|
24
24
|
"The `title` attribute should never be used as it is inaccessible for several groups of users. Use `aria-label` or `aria-describedby` instead. Exceptions are provided for `<iframe>` and `<link>` elements.",
|
|
25
25
|
node.tag_name!.location,
|
|
26
|
-
"error"
|
|
27
26
|
)
|
|
28
27
|
}
|
|
29
28
|
}
|
|
@@ -32,7 +31,14 @@ class NoTitleAttributeVisitor extends BaseRuleVisitor {
|
|
|
32
31
|
export class HTMLNoTitleAttributeRule extends ParserRule {
|
|
33
32
|
name = "html-no-title-attribute"
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
get defaultConfig(): FullRuleConfig {
|
|
35
|
+
return {
|
|
36
|
+
enabled: false,
|
|
37
|
+
severity: "error"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
|
|
36
42
|
const visitor = new NoTitleAttributeVisitor(this.name, context)
|
|
37
43
|
|
|
38
44
|
visitor.visit(result.value)
|
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
DynamicAttributeStaticValueParams,
|
|
7
7
|
DynamicAttributeDynamicValueParams
|
|
8
8
|
} from "./rule-utils.js"
|
|
9
|
+
|
|
9
10
|
import { getStaticContentFromNodes } from "@herb-tools/core"
|
|
10
11
|
import { IdentityPrinter } from "@herb-tools/printer"
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
|
|
12
14
|
import type { ParseResult, HTMLAttributeNode } from "@herb-tools/core"
|
|
13
15
|
|
|
14
16
|
class HTMLNoUnderscoresInAttributeNamesVisitor extends AttributeVisitorMixin {
|
|
@@ -38,8 +40,7 @@ class HTMLNoUnderscoresInAttributeNamesVisitor extends AttributeVisitorMixin {
|
|
|
38
40
|
if (attributeName.includes("_")) {
|
|
39
41
|
this.addOffense(
|
|
40
42
|
`Attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not contain underscores. Use hyphens (-) instead.`,
|
|
41
|
-
attributeNode.
|
|
42
|
-
"warning"
|
|
43
|
+
attributeNode.name?.location ?? attributeNode.location,
|
|
43
44
|
)
|
|
44
45
|
}
|
|
45
46
|
}
|
|
@@ -48,7 +49,14 @@ class HTMLNoUnderscoresInAttributeNamesVisitor extends AttributeVisitorMixin {
|
|
|
48
49
|
export class HTMLNoUnderscoresInAttributeNamesRule extends ParserRule {
|
|
49
50
|
name = "html-no-underscores-in-attribute-names"
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
get defaultConfig(): FullRuleConfig {
|
|
53
|
+
return {
|
|
54
|
+
enabled: true,
|
|
55
|
+
severity: "warning"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
|
|
52
60
|
const visitor = new HTMLNoUnderscoresInAttributeNamesVisitor(this.name, context)
|
|
53
61
|
|
|
54
62
|
visitor.visit(result.value)
|