@herb-tools/linter 0.7.4 → 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 +26087 -3414
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +5783 -1568
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5749 -1569
- 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 +42 -35
- 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 +109 -43
- 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 +48 -0
- package/dist/src/rules/erb-comment-syntax.js.map +1 -0
- 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 +70 -20
- package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
- package/dist/src/rules/erb-right-trim.js +45 -0
- package/dist/src/rules/erb-right-trim.js.map +1 -0
- 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 +20 -2
- 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} +46 -14
- 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 +14 -0
- 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 +14 -0
- 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 +20 -2
- 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 +14 -0
- 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 +14 -0
- 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 +20 -2
- 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 +19 -3
- package/docs/rules/erb-comment-syntax.md +44 -0
- 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 +52 -0
- 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 +50 -39
- 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 +137 -52
- 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 +73 -0
- 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 +96 -26
- package/src/rules/erb-right-trim.ts +67 -0
- 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 +24 -2
- package/src/rules/parser-no-errors.ts +8 -1
- package/src/rules/rule-utils.ts +250 -44
- package/src/rules/svg-tag-name-capitalization.ts +39 -6
- package/src/{default-rules.ts → rules.ts} +53 -13
- 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,7 +1,7 @@
|
|
|
1
1
|
import { ParserRule } from "../types.js"
|
|
2
2
|
import { BaseRuleVisitor, getTagName, hasAttribute, getAttributeValue, findAttributeByName, getAttributes } 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 NavigationHasLabelVisitor extends BaseRuleVisitor {
|
|
@@ -32,7 +32,6 @@ class NavigationHasLabelVisitor extends BaseRuleVisitor {
|
|
|
32
32
|
this.addOffense(
|
|
33
33
|
message,
|
|
34
34
|
node.tag_name!.location,
|
|
35
|
-
"error"
|
|
36
35
|
)
|
|
37
36
|
}
|
|
38
37
|
}
|
|
@@ -54,7 +53,14 @@ class NavigationHasLabelVisitor extends BaseRuleVisitor {
|
|
|
54
53
|
export class HTMLNavigationHasLabelRule extends ParserRule {
|
|
55
54
|
name = "html-navigation-has-label"
|
|
56
55
|
|
|
57
|
-
|
|
56
|
+
get defaultConfig(): FullRuleConfig {
|
|
57
|
+
return {
|
|
58
|
+
enabled: false,
|
|
59
|
+
severity: "error"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
|
|
58
64
|
const visitor = new NavigationHasLabelVisitor(this.name, context)
|
|
59
65
|
|
|
60
66
|
visitor.visit(result.value)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ParserRule } from "../types.js"
|
|
2
2
|
import { BaseRuleVisitor, getTagName, hasAttribute, getAttributeValue, findAttributeByName, getAttributes } 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
|
const INTERACTIVE_ELEMENTS = new Set([
|
|
@@ -21,7 +21,6 @@ class NoAriaHiddenOnFocusableVisitor extends BaseRuleVisitor {
|
|
|
21
21
|
this.addOffense(
|
|
22
22
|
`Elements that are focusable should not have \`aria-hidden="true"\` because it will cause confusion for assistive technology users.`,
|
|
23
23
|
node.tag_name!.location,
|
|
24
|
-
"error"
|
|
25
24
|
)
|
|
26
25
|
}
|
|
27
26
|
}
|
|
@@ -80,7 +79,14 @@ class NoAriaHiddenOnFocusableVisitor extends BaseRuleVisitor {
|
|
|
80
79
|
export class HTMLNoAriaHiddenOnFocusableRule extends ParserRule {
|
|
81
80
|
name = "html-no-aria-hidden-on-focusable"
|
|
82
81
|
|
|
83
|
-
|
|
82
|
+
get defaultConfig(): FullRuleConfig {
|
|
83
|
+
return {
|
|
84
|
+
enabled: true,
|
|
85
|
+
severity: "error"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
|
|
84
90
|
const visitor = new NoAriaHiddenOnFocusableVisitor(this.name, context)
|
|
85
91
|
|
|
86
92
|
visitor.visit(result.value)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseRuleVisitor, isInlineElement, isBlockElement } from "./rule-utils.js"
|
|
2
2
|
|
|
3
3
|
import { ParserRule } from "../types.js"
|
|
4
|
-
import type {
|
|
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 BlockInsideInlineVisitor extends BaseRuleVisitor {
|
|
@@ -26,7 +26,6 @@ class BlockInsideInlineVisitor extends BaseRuleVisitor {
|
|
|
26
26
|
this.addOffense(
|
|
27
27
|
`${elementType} element \`<${tagName}>\` cannot be placed inside inline element \`<${parentInline}>\`.`,
|
|
28
28
|
openTag.tag_name!.location,
|
|
29
|
-
"error"
|
|
30
29
|
)
|
|
31
30
|
}
|
|
32
31
|
|
|
@@ -77,7 +76,14 @@ class BlockInsideInlineVisitor extends BaseRuleVisitor {
|
|
|
77
76
|
export class HTMLNoBlockInsideInlineRule extends ParserRule {
|
|
78
77
|
name = "html-no-block-inside-inline"
|
|
79
78
|
|
|
80
|
-
|
|
79
|
+
get defaultConfig(): FullRuleConfig {
|
|
80
|
+
return {
|
|
81
|
+
enabled: false,
|
|
82
|
+
severity: "error"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
|
|
81
87
|
const visitor = new BlockInsideInlineVisitor(this.name, context)
|
|
82
88
|
visitor.visit(result.value)
|
|
83
89
|
return visitor.offenses
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ParserRule } from "../types.js"
|
|
2
2
|
import { AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams } from "./rule-utils.js"
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
|
|
5
5
|
import type { HTMLOpenTagNode, HTMLAttributeNode, ParseResult } from "@herb-tools/core"
|
|
6
6
|
|
|
7
7
|
class NoDuplicateAttributesVisitor extends AttributeVisitorMixin {
|
|
@@ -39,7 +39,6 @@ class NoDuplicateAttributesVisitor extends AttributeVisitorMixin {
|
|
|
39
39
|
this.addOffense(
|
|
40
40
|
`Duplicate attribute \`${attributeName}\` found on tag. Remove the duplicate occurrence.`,
|
|
41
41
|
attributeNode.name!.location,
|
|
42
|
-
"error"
|
|
43
42
|
)
|
|
44
43
|
}
|
|
45
44
|
}
|
|
@@ -50,7 +49,14 @@ class NoDuplicateAttributesVisitor extends AttributeVisitorMixin {
|
|
|
50
49
|
export class HTMLNoDuplicateAttributesRule extends ParserRule {
|
|
51
50
|
name = "html-no-duplicate-attributes"
|
|
52
51
|
|
|
53
|
-
|
|
52
|
+
get defaultConfig(): FullRuleConfig {
|
|
53
|
+
return {
|
|
54
|
+
enabled: true,
|
|
55
|
+
severity: "error"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
|
|
54
60
|
const visitor = new NoDuplicateAttributesVisitor(this.name, context)
|
|
55
61
|
|
|
56
62
|
visitor.visit(result.value)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ParserRule } from "../types"
|
|
1
|
+
import { ParserRule, BaseAutofixContext } from "../types"
|
|
2
2
|
import { ControlFlowTrackingVisitor, ControlFlowType } from "./rule-utils"
|
|
3
3
|
import { LiteralNode } from "@herb-tools/core"
|
|
4
4
|
import { Printer, IdentityPrinter } from "@herb-tools/printer"
|
|
@@ -6,7 +6,7 @@ import { Printer, IdentityPrinter } from "@herb-tools/printer"
|
|
|
6
6
|
import { hasERBOutput, getValidatableStaticContent, isEffectivelyStatic, isNode, getStaticAttributeName, isERBOutputNode } from "@herb-tools/core"
|
|
7
7
|
|
|
8
8
|
import type { ParseResult, HTMLAttributeNode, ERBContentNode } from "@herb-tools/core"
|
|
9
|
-
import type {
|
|
9
|
+
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types"
|
|
10
10
|
|
|
11
11
|
interface ControlFlowState {
|
|
12
12
|
previousBranchIds: Set<string>
|
|
@@ -29,7 +29,7 @@ class OutputPrinter extends Printer {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor<ControlFlowState, BranchState> {
|
|
32
|
+
class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor<BaseAutofixContext, ControlFlowState, BranchState> {
|
|
33
33
|
private documentIds: Set<string> = new Set<string>()
|
|
34
34
|
private currentBranchIds: Set<string> = new Set<string>()
|
|
35
35
|
private controlFlowIds: Set<string> = new Set<string>()
|
|
@@ -180,7 +180,6 @@ class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor<ControlFlowState,
|
|
|
180
180
|
this.addOffense(
|
|
181
181
|
`Duplicate ID \`${identifier}\` found. IDs must be unique within a document.`,
|
|
182
182
|
location,
|
|
183
|
-
"error"
|
|
184
183
|
)
|
|
185
184
|
}
|
|
186
185
|
|
|
@@ -188,7 +187,6 @@ class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor<ControlFlowState,
|
|
|
188
187
|
this.addOffense(
|
|
189
188
|
`Duplicate ID \`${identifier}\` found within the same loop iteration. IDs must be unique within the same loop iteration.`,
|
|
190
189
|
location,
|
|
191
|
-
"error"
|
|
192
190
|
)
|
|
193
191
|
}
|
|
194
192
|
|
|
@@ -196,7 +194,6 @@ class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor<ControlFlowState,
|
|
|
196
194
|
this.addOffense(
|
|
197
195
|
`Duplicate ID \`${identifier}\` found within the same control flow branch. IDs must be unique within the same control flow branch.`,
|
|
198
196
|
location,
|
|
199
|
-
"error"
|
|
200
197
|
)
|
|
201
198
|
}
|
|
202
199
|
}
|
|
@@ -204,7 +201,14 @@ class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor<ControlFlowState,
|
|
|
204
201
|
export class HTMLNoDuplicateIdsRule extends ParserRule {
|
|
205
202
|
name = "html-no-duplicate-ids"
|
|
206
203
|
|
|
207
|
-
|
|
204
|
+
get defaultConfig(): FullRuleConfig {
|
|
205
|
+
return {
|
|
206
|
+
enabled: true,
|
|
207
|
+
severity: "error"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
|
|
208
212
|
const visitor = new NoDuplicateIdsVisitor(this.name, context)
|
|
209
213
|
|
|
210
214
|
visitor.visit(result.value)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { isHTMLElementNode } from "@herb-tools/core"
|
|
2
|
+
import { getTagName, getAttributeName, getAttributeValue, forEachAttribute } from "./rule-utils"
|
|
3
|
+
|
|
4
|
+
import { ControlFlowTrackingVisitor, ControlFlowType } from "./rule-utils"
|
|
5
|
+
import { ParserRule, BaseAutofixContext } from "../types"
|
|
6
|
+
|
|
7
|
+
import type { ParseResult, HTMLElementNode, HTMLAttributeNode } from "@herb-tools/core"
|
|
8
|
+
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types"
|
|
9
|
+
|
|
10
|
+
interface MetaTag {
|
|
11
|
+
node: HTMLElementNode
|
|
12
|
+
nameValue?: string
|
|
13
|
+
httpEquivValue?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ControlFlowState {
|
|
17
|
+
previousBranchMetas: MetaTag[]
|
|
18
|
+
previousControlFlowMetas: MetaTag[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface BranchState {
|
|
22
|
+
previousBranchMetas: MetaTag[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class HTMLNoDuplicateMetaNamesVisitor extends ControlFlowTrackingVisitor<BaseAutofixContext, ControlFlowState, BranchState> {
|
|
26
|
+
private elementStack: string[] = []
|
|
27
|
+
private documentMetas: MetaTag[] = []
|
|
28
|
+
private currentBranchMetas: MetaTag[] = []
|
|
29
|
+
private controlFlowMetas: MetaTag[] = []
|
|
30
|
+
|
|
31
|
+
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
32
|
+
const tagName = getTagName(node)?.toLowerCase()
|
|
33
|
+
if (!tagName) return
|
|
34
|
+
|
|
35
|
+
if (tagName === "head") {
|
|
36
|
+
this.documentMetas = []
|
|
37
|
+
this.currentBranchMetas = []
|
|
38
|
+
this.controlFlowMetas = []
|
|
39
|
+
} else if (tagName === "meta" && this.insideHead) {
|
|
40
|
+
this.collectAndCheckMetaTag(node)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.elementStack.push(tagName)
|
|
44
|
+
this.visitChildNodes(node)
|
|
45
|
+
this.elementStack.pop()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected onEnterControlFlow(_controlFlowType: ControlFlowType, wasAlreadyInControlFlow: boolean): ControlFlowState {
|
|
49
|
+
const stateToRestore: ControlFlowState = {
|
|
50
|
+
previousBranchMetas: this.currentBranchMetas,
|
|
51
|
+
previousControlFlowMetas: this.controlFlowMetas
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.currentBranchMetas = []
|
|
55
|
+
|
|
56
|
+
if (!wasAlreadyInControlFlow) {
|
|
57
|
+
this.controlFlowMetas = []
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return stateToRestore
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
protected onExitControlFlow(controlFlowType: ControlFlowType, wasAlreadyInControlFlow: boolean, stateToRestore: ControlFlowState): void {
|
|
64
|
+
if (controlFlowType === ControlFlowType.CONDITIONAL && !wasAlreadyInControlFlow) {
|
|
65
|
+
this.controlFlowMetas.forEach(meta => this.documentMetas.push(meta))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.currentBranchMetas = stateToRestore.previousBranchMetas
|
|
69
|
+
this.controlFlowMetas = stateToRestore.previousControlFlowMetas
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected onEnterBranch(): BranchState {
|
|
73
|
+
const stateToRestore: BranchState = {
|
|
74
|
+
previousBranchMetas: this.currentBranchMetas
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (this.isInControlFlow) {
|
|
78
|
+
this.currentBranchMetas = []
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return stateToRestore
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
protected onExitBranch(_stateToRestore: BranchState): void {}
|
|
85
|
+
|
|
86
|
+
private get insideHead(): boolean {
|
|
87
|
+
return this.elementStack.includes("head")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private collectAndCheckMetaTag(node: HTMLElementNode): void {
|
|
91
|
+
const metaTag: MetaTag = { node }
|
|
92
|
+
this.extractAttributes(node, metaTag)
|
|
93
|
+
|
|
94
|
+
if (!metaTag.nameValue && !metaTag.httpEquivValue) return
|
|
95
|
+
|
|
96
|
+
if (this.isInControlFlow) {
|
|
97
|
+
this.handleControlFlowMeta(metaTag)
|
|
98
|
+
} else {
|
|
99
|
+
this.handleGlobalMeta(metaTag)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.currentBranchMetas.push(metaTag)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private extractAttributes(node: HTMLElementNode, metaTag: MetaTag): void {
|
|
106
|
+
if (isHTMLElementNode(node) && node.open_tag) {
|
|
107
|
+
forEachAttribute(node.open_tag as any, (attributeNode: HTMLAttributeNode) => {
|
|
108
|
+
const name = getAttributeName(attributeNode)
|
|
109
|
+
const value = getAttributeValue(attributeNode)?.trim()
|
|
110
|
+
|
|
111
|
+
if (name === "name" && value) {
|
|
112
|
+
metaTag.nameValue = value
|
|
113
|
+
} else if (name === "http-equiv" && value) {
|
|
114
|
+
metaTag.httpEquivValue = value
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private handleControlFlowMeta(metaTag: MetaTag): void {
|
|
121
|
+
if (this.currentControlFlowType === ControlFlowType.LOOP) {
|
|
122
|
+
this.checkAgainstMetaList(metaTag, this.currentBranchMetas, "within the same loop iteration")
|
|
123
|
+
} else {
|
|
124
|
+
this.checkAgainstMetaList(metaTag, this.currentBranchMetas, "within the same control flow branch")
|
|
125
|
+
this.checkAgainstMetaList(metaTag, this.documentMetas, "")
|
|
126
|
+
|
|
127
|
+
this.controlFlowMetas.push(metaTag)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private handleGlobalMeta(metaTag: MetaTag): void {
|
|
132
|
+
this.checkAgainstMetaList(metaTag, this.documentMetas, "")
|
|
133
|
+
this.documentMetas.push(metaTag)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private checkAgainstMetaList(metaTag: MetaTag, existingMetas: MetaTag[], context: string): void {
|
|
137
|
+
for (const existing of existingMetas) {
|
|
138
|
+
if (this.areMetaTagsDuplicate(metaTag, existing)) {
|
|
139
|
+
const attributeDescription = metaTag.nameValue
|
|
140
|
+
? `\`name="${metaTag.nameValue}"\``
|
|
141
|
+
: `\`http-equiv="${metaTag.httpEquivValue}"\``
|
|
142
|
+
|
|
143
|
+
const attributeType = metaTag.nameValue ? "Meta names" : "`http-equiv` values"
|
|
144
|
+
|
|
145
|
+
const contextMsg = context ? ` ${context}` : ""
|
|
146
|
+
|
|
147
|
+
this.addOffense(
|
|
148
|
+
`Duplicate \`<meta>\` tag with ${attributeDescription}${contextMsg}. ${attributeType} should be unique within the \`<head>\` section.`,
|
|
149
|
+
metaTag.node.location,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private areMetaTagsDuplicate(meta1: MetaTag, meta2: MetaTag): boolean {
|
|
158
|
+
if (meta1.nameValue && meta2.nameValue) {
|
|
159
|
+
return meta1.nameValue.toLowerCase() === meta2.nameValue.toLowerCase()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (meta1.httpEquivValue && meta2.httpEquivValue) {
|
|
163
|
+
return meta1.httpEquivValue.toLowerCase() === meta2.httpEquivValue.toLowerCase()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export class HTMLNoDuplicateMetaNamesRule extends ParserRule {
|
|
171
|
+
static autocorrectable = false
|
|
172
|
+
name = "html-no-duplicate-meta-names"
|
|
173
|
+
|
|
174
|
+
get defaultConfig(): FullRuleConfig {
|
|
175
|
+
return {
|
|
176
|
+
enabled: true,
|
|
177
|
+
severity: "error"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
|
|
182
|
+
const visitor = new HTMLNoDuplicateMetaNamesVisitor(this.name, context)
|
|
183
|
+
|
|
184
|
+
visitor.visit(result.value)
|
|
185
|
+
|
|
186
|
+
return visitor.offenses
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -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)
|