@herb-tools/linter 0.4.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -19
- package/dist/herb-lint.js +5559 -1860
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +722 -187
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +714 -189
- package/dist/index.js.map +1 -1
- package/dist/package.json +4 -4
- package/dist/src/cli/argument-parser.js +28 -22
- package/dist/src/cli/argument-parser.js.map +1 -1
- package/dist/src/cli/file-processor.js +19 -13
- package/dist/src/cli/file-processor.js.map +1 -1
- package/dist/src/cli/formatters/detailed-formatter.js +9 -9
- package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
- package/dist/src/cli/formatters/github-actions-formatter.js +50 -0
- package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -0
- package/dist/src/cli/formatters/index.js +2 -0
- package/dist/src/cli/formatters/index.js.map +1 -1
- package/dist/src/cli/formatters/json-formatter.js +58 -0
- package/dist/src/cli/formatters/json-formatter.js.map +1 -0
- package/dist/src/cli/formatters/simple-formatter.js +15 -15
- package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
- package/dist/src/cli/output-manager.js +120 -0
- package/dist/src/cli/output-manager.js.map +1 -0
- package/dist/src/cli/summary-reporter.js +22 -22
- package/dist/src/cli/summary-reporter.js.map +1 -1
- package/dist/src/cli.js +41 -26
- package/dist/src/cli.js.map +1 -1
- package/dist/src/default-rules.js +22 -0
- package/dist/src/default-rules.js.map +1 -1
- package/dist/src/linter.js +29 -4
- package/dist/src/linter.js.map +1 -1
- package/dist/src/rules/erb-no-empty-tags.js +2 -2
- package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
- package/dist/src/rules/erb-no-output-control-flow.js +2 -2
- 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 +26 -0
- package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -0
- package/dist/src/rules/erb-prefer-image-tag-helper.js +2 -6
- package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
- package/dist/src/rules/erb-require-whitespace-inside-tags.js +2 -2
- package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
- package/dist/src/rules/erb-requires-trailing-newline.js.map +1 -1
- package/dist/src/rules/html-anchor-require-href.js +2 -2
- package/dist/src/rules/html-anchor-require-href.js.map +1 -1
- package/dist/src/rules/html-aria-attribute-must-be-valid.js +13 -12
- 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 +33 -0
- package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -0
- package/dist/src/rules/html-aria-level-must-be-valid.js +28 -6
- 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 +9 -15
- 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 +5 -5
- package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-attribute-double-quotes.js +16 -6
- package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
- package/dist/src/rules/html-attribute-equals-spacing.js +24 -0
- package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -0
- package/dist/src/rules/html-attribute-values-require-quotes.js +21 -10
- 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 +47 -0
- package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -0
- package/dist/src/rules/html-boolean-attributes-no-value.js +11 -4
- package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
- package/dist/src/rules/html-iframe-has-title.js +39 -0
- package/dist/src/rules/html-iframe-has-title.js.map +1 -0
- package/dist/src/rules/html-img-require-alt.js +2 -6
- package/dist/src/rules/html-img-require-alt.js.map +1 -1
- package/dist/src/rules/html-navigation-has-label.js +43 -0
- package/dist/src/rules/html-navigation-has-label.js.map +1 -0
- package/dist/src/rules/html-no-aria-hidden-on-focusable.js +67 -0
- package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -0
- package/dist/src/rules/html-no-block-inside-inline.js +4 -4
- package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-attributes.js +24 -27
- package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-ids.js +4 -4
- package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
- package/dist/src/rules/html-no-empty-headings.js +2 -23
- package/dist/src/rules/html-no-empty-headings.js.map +1 -1
- package/dist/src/rules/html-no-nested-links.js +2 -2
- package/dist/src/rules/html-no-nested-links.js.map +1 -1
- package/dist/src/rules/html-no-positive-tab-index.js +21 -0
- package/dist/src/rules/html-no-positive-tab-index.js.map +1 -0
- package/dist/src/rules/html-no-self-closing.js +22 -0
- package/dist/src/rules/html-no-self-closing.js.map +1 -0
- package/dist/src/rules/html-no-title-attribute.js +27 -0
- package/dist/src/rules/html-no-title-attribute.js.map +1 -0
- package/dist/src/rules/html-tag-name-lowercase.js +37 -25
- package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
- package/dist/src/rules/index.js +10 -0
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/parser-no-errors.js +18 -0
- package/dist/src/rules/parser-no-errors.js.map +1 -0
- package/dist/src/rules/rule-utils.js +176 -22
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +2 -10
- package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/cli/argument-parser.d.ts +2 -1
- package/dist/types/cli/file-processor.d.ts +6 -5
- package/dist/types/cli/formatters/base-formatter.d.ts +2 -2
- package/dist/types/cli/formatters/detailed-formatter.d.ts +2 -2
- package/dist/types/cli/formatters/github-actions-formatter.d.ts +12 -0
- package/dist/types/cli/formatters/index.d.ts +2 -0
- package/dist/types/cli/formatters/json-formatter.d.ts +42 -0
- package/dist/types/cli/formatters/simple-formatter.d.ts +2 -2
- package/dist/types/cli/index.d.ts +4 -0
- package/dist/types/cli/output-manager.d.ts +31 -0
- package/dist/types/cli/summary-reporter.d.ts +3 -3
- package/dist/types/cli.d.ts +3 -1
- package/dist/types/rules/erb-no-empty-tags.d.ts +2 -2
- package/dist/types/rules/erb-no-output-control-flow.d.ts +2 -2
- package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
- package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +2 -2
- package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
- package/dist/types/rules/html-anchor-require-href.d.ts +2 -2
- package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
- package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +7 -0
- package/dist/types/rules/html-aria-level-must-be-valid.d.ts +2 -2
- package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +2 -2
- package/dist/types/rules/html-aria-role-must-be-valid.d.ts +2 -2
- package/dist/types/rules/html-attribute-double-quotes.d.ts +2 -2
- package/dist/types/rules/html-attribute-equals-spacing.d.ts +7 -0
- package/dist/types/rules/html-attribute-values-require-quotes.d.ts +2 -2
- package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
- package/dist/types/rules/html-boolean-attributes-no-value.d.ts +2 -2
- package/dist/types/rules/html-iframe-has-title.d.ts +7 -0
- package/dist/types/rules/html-img-require-alt.d.ts +2 -2
- package/dist/types/rules/html-navigation-has-label.d.ts +7 -0
- package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
- package/dist/types/rules/html-no-block-inside-inline.d.ts +2 -2
- package/dist/types/rules/html-no-duplicate-attributes.d.ts +2 -2
- package/dist/types/rules/html-no-duplicate-ids.d.ts +2 -2
- package/dist/types/rules/html-no-empty-headings.d.ts +2 -2
- package/dist/types/rules/html-no-nested-links.d.ts +2 -2
- package/dist/types/rules/html-no-positive-tab-index.d.ts +7 -0
- package/dist/types/rules/html-no-self-closing.d.ts +7 -0
- package/dist/types/rules/html-no-title-attribute.d.ts +7 -0
- package/dist/types/rules/html-tag-name-lowercase.d.ts +3 -2
- package/dist/types/rules/index.d.ts +10 -0
- package/dist/types/rules/parser-no-errors.d.ts +8 -0
- package/dist/types/rules/rule-utils.d.ts +107 -13
- package/dist/types/rules/svg-tag-name-capitalization.d.ts +2 -2
- package/dist/types/src/cli/argument-parser.d.ts +2 -1
- package/dist/types/src/cli/file-processor.d.ts +6 -5
- package/dist/types/src/cli/formatters/base-formatter.d.ts +2 -2
- package/dist/types/src/cli/formatters/detailed-formatter.d.ts +2 -2
- package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +12 -0
- package/dist/types/src/cli/formatters/index.d.ts +2 -0
- package/dist/types/src/cli/formatters/json-formatter.d.ts +42 -0
- package/dist/types/src/cli/formatters/simple-formatter.d.ts +2 -2
- package/dist/types/src/cli/output-manager.d.ts +31 -0
- package/dist/types/src/cli/summary-reporter.d.ts +3 -3
- package/dist/types/src/cli.d.ts +3 -1
- package/dist/types/src/rules/erb-no-empty-tags.d.ts +2 -2
- package/dist/types/src/rules/erb-no-output-control-flow.d.ts +2 -2
- package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
- package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +2 -2
- package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
- package/dist/types/src/rules/html-anchor-require-href.d.ts +2 -2
- package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
- package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +7 -0
- package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +2 -2
- package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +2 -2
- package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +2 -2
- package/dist/types/src/rules/html-attribute-double-quotes.d.ts +2 -2
- package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +7 -0
- package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +2 -2
- package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
- package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +2 -2
- package/dist/types/src/rules/html-iframe-has-title.d.ts +7 -0
- package/dist/types/src/rules/html-img-require-alt.d.ts +2 -2
- package/dist/types/src/rules/html-navigation-has-label.d.ts +7 -0
- package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
- package/dist/types/src/rules/html-no-block-inside-inline.d.ts +2 -2
- package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +2 -2
- package/dist/types/src/rules/html-no-duplicate-ids.d.ts +2 -2
- package/dist/types/src/rules/html-no-empty-headings.d.ts +2 -2
- package/dist/types/src/rules/html-no-nested-links.d.ts +2 -2
- package/dist/types/src/rules/html-no-positive-tab-index.d.ts +7 -0
- package/dist/types/src/rules/html-no-self-closing.d.ts +7 -0
- package/dist/types/src/rules/html-no-title-attribute.d.ts +7 -0
- package/dist/types/src/rules/html-tag-name-lowercase.d.ts +3 -2
- package/dist/types/src/rules/index.d.ts +10 -0
- package/dist/types/src/rules/parser-no-errors.d.ts +8 -0
- package/dist/types/src/rules/rule-utils.d.ts +107 -13
- package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +2 -2
- package/dist/types/src/types.d.ts +27 -3
- package/dist/types/types.d.ts +27 -3
- package/docs/rules/README.md +13 -2
- package/docs/rules/erb-no-silent-tag-in-attribute-name.md +34 -0
- package/docs/rules/html-aria-label-is-well-formatted.md +49 -0
- package/docs/rules/html-attribute-equals-spacing.md +35 -0
- package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +48 -0
- package/docs/rules/html-iframe-has-title.md +43 -0
- package/docs/rules/html-navigation-has-label.md +61 -0
- package/docs/rules/html-no-aria-hidden-on-focusable.md +54 -0
- package/docs/rules/html-no-positive-tab-index.md +55 -0
- package/docs/rules/html-no-self-closing.md +65 -0
- package/docs/rules/html-no-title-attribute.md +69 -0
- package/docs/rules/html-tag-name-lowercase.md +16 -3
- package/docs/rules/parser-no-errors.md +84 -0
- package/package.json +4 -4
- package/src/cli/argument-parser.ts +33 -24
- package/src/cli/file-processor.ts +25 -17
- package/src/cli/formatters/base-formatter.ts +2 -2
- package/src/cli/formatters/detailed-formatter.ts +9 -9
- package/src/cli/formatters/github-actions-formatter.ts +70 -0
- package/src/cli/formatters/index.ts +2 -0
- package/src/cli/formatters/json-formatter.ts +107 -0
- package/src/cli/formatters/simple-formatter.ts +15 -15
- package/src/cli/output-manager.ts +143 -0
- package/src/cli/summary-reporter.ts +24 -24
- package/src/cli.ts +48 -31
- package/src/default-rules.ts +22 -0
- package/src/linter.ts +30 -4
- package/src/rules/erb-no-empty-tags.ts +3 -3
- package/src/rules/erb-no-output-control-flow.ts +3 -3
- package/src/rules/erb-no-silent-tag-in-attribute-name.ts +40 -0
- package/src/rules/erb-prefer-image-tag-helper.ts +4 -9
- package/src/rules/erb-require-whitespace-inside-tags.ts +3 -3
- package/src/rules/erb-requires-trailing-newline.ts +2 -0
- package/src/rules/html-anchor-require-href.ts +3 -3
- package/src/rules/html-aria-attribute-must-be-valid.ts +29 -33
- package/src/rules/html-aria-label-is-well-formatted.ts +59 -0
- package/src/rules/html-aria-level-must-be-valid.ts +40 -7
- package/src/rules/html-aria-role-heading-requires-level.ts +18 -30
- package/src/rules/html-aria-role-must-be-valid.ts +7 -7
- package/src/rules/html-attribute-double-quotes.ts +23 -8
- package/src/rules/html-attribute-equals-spacing.ts +41 -0
- package/src/rules/html-attribute-values-require-quotes.ts +32 -12
- package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +66 -0
- package/src/rules/html-boolean-attributes-no-value.ts +19 -6
- package/src/rules/html-iframe-has-title.ts +62 -0
- package/src/rules/html-img-require-alt.ts +4 -9
- package/src/rules/html-navigation-has-label.ts +64 -0
- package/src/rules/html-no-aria-hidden-on-focusable.ts +90 -0
- package/src/rules/html-no-block-inside-inline.ts +5 -5
- package/src/rules/html-no-duplicate-attributes.ts +30 -30
- package/src/rules/html-no-duplicate-ids.ts +6 -5
- package/src/rules/html-no-empty-headings.ts +4 -33
- package/src/rules/html-no-nested-links.ts +3 -3
- package/src/rules/html-no-positive-tab-index.ts +33 -0
- package/src/rules/html-no-self-closing.ts +36 -0
- package/src/rules/html-no-title-attribute.ts +42 -0
- package/src/rules/html-tag-name-lowercase.ts +44 -31
- package/src/rules/index.ts +10 -0
- package/src/rules/parser-no-errors.ts +25 -0
- package/src/rules/rule-utils.ts +260 -39
- package/src/rules/svg-tag-name-capitalization.ts +4 -11
- package/src/types.ts +30 -3
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { AttributeVisitorMixin, VALID_ARIA_ROLES } from "./rule-utils.js"
|
|
2
|
-
|
|
3
1
|
import { ParserRule } from "../types.js"
|
|
2
|
+
import { AttributeVisitorMixin, VALID_ARIA_ROLES, StaticAttributeStaticValueParams } from "./rule-utils.js"
|
|
3
|
+
|
|
4
4
|
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
-
import type {
|
|
5
|
+
import type { ParseResult } from "@herb-tools/core"
|
|
6
6
|
|
|
7
7
|
class AriaRoleMustBeValid extends AttributeVisitorMixin {
|
|
8
|
-
|
|
8
|
+
protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams): void {
|
|
9
9
|
if (attributeName !== "role") return
|
|
10
|
-
if (attributeValue
|
|
10
|
+
if (!attributeValue) return
|
|
11
11
|
if (VALID_ARIA_ROLES.has(attributeValue)) return
|
|
12
12
|
|
|
13
13
|
this.addOffense(
|
|
@@ -21,10 +21,10 @@ class AriaRoleMustBeValid extends AttributeVisitorMixin {
|
|
|
21
21
|
export class HTMLAriaRoleMustBeValidRule extends ParserRule {
|
|
22
22
|
name = "html-aria-role-must-be-valid"
|
|
23
23
|
|
|
24
|
-
check(
|
|
24
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
25
25
|
const visitor = new AriaRoleMustBeValid(this.name, context)
|
|
26
26
|
|
|
27
|
-
visitor.visit(
|
|
27
|
+
visitor.visit(result.value)
|
|
28
28
|
|
|
29
29
|
return visitor.offenses
|
|
30
30
|
}
|
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
import { AttributeVisitorMixin, getAttributeValueQuoteType, hasAttributeValue } from "./rule-utils.js"
|
|
2
|
-
|
|
3
1
|
import { ParserRule } from "../types.js"
|
|
2
|
+
import { AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams, getAttributeValueQuoteType, hasAttributeValue } from "./rule-utils.js"
|
|
3
|
+
import { filterLiteralNodes } from "@herb-tools/core"
|
|
4
|
+
|
|
4
5
|
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
-
import type {
|
|
6
|
+
import type { ParseResult } from "@herb-tools/core"
|
|
6
7
|
|
|
7
8
|
class AttributeDoubleQuotesVisitor extends AttributeVisitorMixin {
|
|
8
|
-
protected
|
|
9
|
+
protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams) {
|
|
9
10
|
if (!hasAttributeValue(attributeNode)) return
|
|
10
11
|
if (getAttributeValueQuoteType(attributeNode) !== "single") return
|
|
11
|
-
if (attributeValue?.includes('"')) return
|
|
12
|
+
if (attributeValue?.includes('"')) return
|
|
12
13
|
|
|
13
14
|
this.addOffense(
|
|
14
|
-
`Attribute \`${attributeName}\` uses single quotes. Prefer double quotes for HTML attribute values: \`${attributeName}="
|
|
15
|
+
`Attribute \`${attributeName}\` uses single quotes. Prefer double quotes for HTML attribute values: \`${attributeName}="${attributeValue}"\`.`,
|
|
16
|
+
attributeNode.value!.location,
|
|
17
|
+
"warning"
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected checkStaticAttributeDynamicValue({ attributeName, valueNodes, attributeNode, combinedValue }: StaticAttributeDynamicValueParams) {
|
|
22
|
+
if (!hasAttributeValue(attributeNode)) return
|
|
23
|
+
if (getAttributeValueQuoteType(attributeNode) !== "single") return
|
|
24
|
+
if (filterLiteralNodes(valueNodes).some(node => node.content?.includes('"'))) return
|
|
25
|
+
|
|
26
|
+
this.addOffense(
|
|
27
|
+
`Attribute \`${attributeName}\` uses single quotes. Prefer double quotes for HTML attribute values: \`${attributeName}="${combinedValue}"\`.`,
|
|
15
28
|
attributeNode.value!.location,
|
|
16
29
|
"warning"
|
|
17
30
|
)
|
|
@@ -21,9 +34,11 @@ class AttributeDoubleQuotesVisitor extends AttributeVisitorMixin {
|
|
|
21
34
|
export class HTMLAttributeDoubleQuotesRule extends ParserRule {
|
|
22
35
|
name = "html-attribute-double-quotes"
|
|
23
36
|
|
|
24
|
-
check(
|
|
37
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
25
38
|
const visitor = new AttributeDoubleQuotesVisitor(this.name, context)
|
|
26
|
-
|
|
39
|
+
|
|
40
|
+
visitor.visit(result.value)
|
|
41
|
+
|
|
27
42
|
return visitor.offenses
|
|
28
43
|
}
|
|
29
44
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BaseRuleVisitor } from "./rule-utils.js"
|
|
2
|
+
import { ParserRule } from "../types.js"
|
|
3
|
+
|
|
4
|
+
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
+
import type { ParseResult, HTMLAttributeNode } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
class HTMLAttributeEqualsSpacingVisitor extends BaseRuleVisitor {
|
|
8
|
+
visitHTMLAttributeNode(attribute: HTMLAttributeNode): void {
|
|
9
|
+
if (!attribute.equals || !attribute.name || !attribute.value) {
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (attribute.equals.value.startsWith(" ")) {
|
|
14
|
+
this.addOffense(
|
|
15
|
+
"Remove whitespace before `=` in HTML attribute",
|
|
16
|
+
attribute.equals.location,
|
|
17
|
+
"error"
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (attribute.equals.value.endsWith(" ")) {
|
|
22
|
+
this.addOffense(
|
|
23
|
+
"Remove whitespace after `=` in HTML attribute",
|
|
24
|
+
attribute.equals.location,
|
|
25
|
+
"error"
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class HTMLAttributeEqualsSpacingRule extends ParserRule {
|
|
32
|
+
name = "html-attribute-equals-spacing"
|
|
33
|
+
|
|
34
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
35
|
+
const visitor = new HTMLAttributeEqualsSpacingVisitor(this.name, context)
|
|
36
|
+
|
|
37
|
+
visitor.visit(result.value)
|
|
38
|
+
|
|
39
|
+
return visitor.offenses
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,31 +1,51 @@
|
|
|
1
|
-
import { AttributeVisitorMixin } from "./rule-utils.js"
|
|
2
|
-
|
|
3
1
|
import { ParserRule } from "../types.js"
|
|
2
|
+
import { AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams } from "./rule-utils.js"
|
|
3
|
+
|
|
4
4
|
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
-
import type { HTMLAttributeNode, HTMLAttributeValueNode,
|
|
5
|
+
import type { HTMLAttributeNode, HTMLAttributeValueNode, ParseResult } from "@herb-tools/core"
|
|
6
6
|
|
|
7
7
|
class AttributeValuesRequireQuotesVisitor extends AttributeVisitorMixin {
|
|
8
|
-
protected
|
|
9
|
-
if (attributeNode
|
|
8
|
+
protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams): void {
|
|
9
|
+
if (this.hasAttributeValue(attributeNode)) return
|
|
10
|
+
if (this.isQuoted(attributeNode)) return
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
this.addOffense(
|
|
13
|
+
`Attribute value should be quoted: \`${attributeName}="${attributeValue}"\`. Always wrap attribute values in quotes.`,
|
|
14
|
+
attributeNode.value!.location,
|
|
15
|
+
"error"
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
protected checkStaticAttributeDynamicValue({ attributeName, attributeNode, combinedValue }: StaticAttributeDynamicValueParams): void {
|
|
20
|
+
if (this.hasAttributeValue(attributeNode)) return
|
|
21
|
+
if (this.isQuoted(attributeNode)) return
|
|
13
22
|
|
|
14
23
|
this.addOffense(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
valueNode.location,
|
|
24
|
+
`Attribute value should be quoted: \`${attributeName}="${combinedValue}"\`. Always wrap attribute values in quotes.`,
|
|
25
|
+
attributeNode.value!.location,
|
|
18
26
|
"error"
|
|
19
27
|
)
|
|
20
28
|
}
|
|
29
|
+
|
|
30
|
+
private hasAttributeValue(attributeNode: HTMLAttributeNode): boolean {
|
|
31
|
+
return attributeNode.value?.type !== "AST_HTML_ATTRIBUTE_VALUE_NODE"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private isQuoted(attributeNode: HTMLAttributeNode): boolean {
|
|
35
|
+
const valueNode = attributeNode.value as HTMLAttributeValueNode
|
|
36
|
+
|
|
37
|
+
return valueNode.quoted
|
|
38
|
+
}
|
|
21
39
|
}
|
|
22
40
|
|
|
23
41
|
export class HTMLAttributeValuesRequireQuotesRule extends ParserRule {
|
|
24
42
|
name = "html-attribute-values-require-quotes"
|
|
25
43
|
|
|
26
|
-
check(
|
|
44
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
27
45
|
const visitor = new AttributeValuesRequireQuotesVisitor(this.name, context)
|
|
28
|
-
|
|
46
|
+
|
|
47
|
+
visitor.visit(result.value)
|
|
48
|
+
|
|
29
49
|
return visitor.offenses
|
|
30
50
|
}
|
|
31
51
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
2
|
+
import { BaseRuleVisitor, getTagName, hasAttribute, getAttributes, findAttributeByName } from "./rule-utils.js"
|
|
3
|
+
|
|
4
|
+
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
+
import type { HTMLOpenTagNode, HTMLAttributeValueNode, ParseResult, Node } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
const ELEMENTS_WITH_NATIVE_DISABLED_ATTRIBUTE_SUPPORT = new Set([
|
|
8
|
+
"button", "fieldset", "input", "optgroup", "option", "select", "textarea"
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
class AvoidBothDisabledAndAriaDisabledVisitor extends BaseRuleVisitor {
|
|
12
|
+
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
13
|
+
this.checkElement(node)
|
|
14
|
+
super.visitHTMLOpenTagNode(node)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private checkElement(node: HTMLOpenTagNode): void {
|
|
18
|
+
const tagName = getTagName(node)
|
|
19
|
+
|
|
20
|
+
if (!tagName || !ELEMENTS_WITH_NATIVE_DISABLED_ATTRIBUTE_SUPPORT.has(tagName)) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const hasDisabled = hasAttribute(node, "disabled")
|
|
25
|
+
const hasAriaDisabled = hasAttribute(node, "aria-disabled")
|
|
26
|
+
|
|
27
|
+
if ((hasDisabled && this.hasERBContent(node, "disabled")) || (hasAriaDisabled && this.hasERBContent(node, "aria-disabled"))) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (hasDisabled && hasAriaDisabled) {
|
|
32
|
+
this.addOffense(
|
|
33
|
+
"aria-disabled may be used in place of native HTML disabled to allow tab-focus on an otherwise ignored element. Setting both attributes is contradictory and confusing. Choose either disabled or aria-disabled, not both.",
|
|
34
|
+
node.tag_name!.location,
|
|
35
|
+
"error"
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private hasERBContent(node: HTMLOpenTagNode, attributeName: string): boolean {
|
|
41
|
+
const attributes = getAttributes(node)
|
|
42
|
+
|
|
43
|
+
const attribute = findAttributeByName(attributes, attributeName)
|
|
44
|
+
if (!attribute) return false
|
|
45
|
+
|
|
46
|
+
const valueNode = attribute.value
|
|
47
|
+
if (!valueNode || valueNode.type !== "AST_HTML_ATTRIBUTE_VALUE_NODE") return false
|
|
48
|
+
|
|
49
|
+
const htmlValueNode = valueNode as HTMLAttributeValueNode
|
|
50
|
+
if (!htmlValueNode.children) return false
|
|
51
|
+
|
|
52
|
+
return htmlValueNode.children.some((child: Node) => child.type === "AST_ERB_CONTENT_NODE")
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class HTMLAvoidBothDisabledAndAriaDisabledRule extends ParserRule {
|
|
57
|
+
name = "html-avoid-both-disabled-and-aria-disabled"
|
|
58
|
+
|
|
59
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
60
|
+
const visitor = new AvoidBothDisabledAndAriaDisabledVisitor(this.name, context)
|
|
61
|
+
|
|
62
|
+
visitor.visit(result.value)
|
|
63
|
+
|
|
64
|
+
return visitor.offenses
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { AttributeVisitorMixin, isBooleanAttribute, hasAttributeValue } from "./rule-utils.js"
|
|
2
|
-
|
|
3
1
|
import { ParserRule } from "../types.js"
|
|
2
|
+
import { AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams, isBooleanAttribute, hasAttributeValue } from "./rule-utils.js"
|
|
3
|
+
|
|
4
4
|
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
-
import type {
|
|
5
|
+
import type { ParseResult } from "@herb-tools/core"
|
|
6
6
|
|
|
7
7
|
class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
|
|
8
|
-
protected
|
|
8
|
+
protected checkStaticAttributeStaticValue({ attributeName, attributeNode }: StaticAttributeStaticValueParams) {
|
|
9
9
|
if (!isBooleanAttribute(attributeName)) return
|
|
10
10
|
if (!hasAttributeValue(attributeNode)) return
|
|
11
11
|
|
|
@@ -15,14 +15,27 @@ class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
|
|
|
15
15
|
"error"
|
|
16
16
|
)
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
protected checkStaticAttributeDynamicValue({ attributeName, attributeNode, combinedValue }: StaticAttributeDynamicValueParams) {
|
|
20
|
+
if (!isBooleanAttribute(attributeName)) return
|
|
21
|
+
if (!hasAttributeValue(attributeNode)) return
|
|
22
|
+
|
|
23
|
+
this.addOffense(
|
|
24
|
+
`Boolean attribute \`${attributeName}\` should not have a value. Use \`${attributeName}\` instead of \`${attributeName}="${combinedValue}"\`.`,
|
|
25
|
+
attributeNode.value!.location,
|
|
26
|
+
"error"
|
|
27
|
+
)
|
|
28
|
+
}
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
export class HTMLBooleanAttributesNoValueRule extends ParserRule {
|
|
21
32
|
name = "html-boolean-attributes-no-value"
|
|
22
33
|
|
|
23
|
-
check(
|
|
34
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
24
35
|
const visitor = new BooleanAttributesNoValueVisitor(this.name, context)
|
|
25
|
-
|
|
36
|
+
|
|
37
|
+
visitor.visit(result.value)
|
|
38
|
+
|
|
26
39
|
return visitor.offenses
|
|
27
40
|
}
|
|
28
41
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
2
|
+
import { BaseRuleVisitor, getTagName, getAttribute, getAttributeValue } from "./rule-utils.js"
|
|
3
|
+
|
|
4
|
+
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
+
import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
class IframeHasTitleVisitor extends BaseRuleVisitor {
|
|
8
|
+
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
9
|
+
this.checkIframeElement(node)
|
|
10
|
+
super.visitHTMLOpenTagNode(node)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private checkIframeElement(node: HTMLOpenTagNode): void {
|
|
14
|
+
const tagName = getTagName(node)
|
|
15
|
+
|
|
16
|
+
if (tagName !== "iframe") {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ariaHiddenAttribute = getAttribute(node, "aria-hidden")
|
|
21
|
+
if (ariaHiddenAttribute) {
|
|
22
|
+
const ariaHiddenValue = getAttributeValue(ariaHiddenAttribute)
|
|
23
|
+
if (ariaHiddenValue === "true") {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const attribute = getAttribute(node, "title")
|
|
29
|
+
|
|
30
|
+
if (!attribute) {
|
|
31
|
+
this.addOffense(
|
|
32
|
+
"`<iframe>` elements must have a `title` attribute that describes the content of the frame for screen reader users.",
|
|
33
|
+
node.location,
|
|
34
|
+
"error"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const value = getAttributeValue(attribute)
|
|
41
|
+
|
|
42
|
+
if (!value || value.trim() === "") {
|
|
43
|
+
this.addOffense(
|
|
44
|
+
"`<iframe>` elements must have a `title` attribute that describes the content of the frame for screen reader users.",
|
|
45
|
+
node.location,
|
|
46
|
+
"error"
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class HTMLIframeHasTitleRule extends ParserRule {
|
|
53
|
+
name = "html-iframe-has-title"
|
|
54
|
+
|
|
55
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
56
|
+
const visitor = new IframeHasTitleVisitor(this.name, context)
|
|
57
|
+
|
|
58
|
+
visitor.visit(result.value)
|
|
59
|
+
|
|
60
|
+
return visitor.offenses
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -2,7 +2,7 @@ import { BaseRuleVisitor, getTagName, hasAttribute } from "./rule-utils.js"
|
|
|
2
2
|
|
|
3
3
|
import { ParserRule } from "../types.js"
|
|
4
4
|
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
-
import type { HTMLOpenTagNode,
|
|
5
|
+
import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
|
|
6
6
|
|
|
7
7
|
class ImgRequireAltVisitor extends BaseRuleVisitor {
|
|
8
8
|
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
@@ -10,12 +10,7 @@ class ImgRequireAltVisitor extends BaseRuleVisitor {
|
|
|
10
10
|
super.visitHTMLOpenTagNode(node)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
this.checkImgTag(node)
|
|
15
|
-
super.visitHTMLSelfCloseTagNode(node)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
private checkImgTag(node: HTMLOpenTagNode | HTMLSelfCloseTagNode): void {
|
|
13
|
+
private checkImgTag(node: HTMLOpenTagNode): void {
|
|
19
14
|
const tagName = getTagName(node)
|
|
20
15
|
|
|
21
16
|
if (tagName !== "img") {
|
|
@@ -35,9 +30,9 @@ class ImgRequireAltVisitor extends BaseRuleVisitor {
|
|
|
35
30
|
export class HTMLImgRequireAltRule extends ParserRule {
|
|
36
31
|
name = "html-img-require-alt"
|
|
37
32
|
|
|
38
|
-
check(
|
|
33
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
39
34
|
const visitor = new ImgRequireAltVisitor(this.name, context)
|
|
40
|
-
visitor.visit(
|
|
35
|
+
visitor.visit(result.value)
|
|
41
36
|
return visitor.offenses
|
|
42
37
|
}
|
|
43
38
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
2
|
+
import { BaseRuleVisitor, getTagName, hasAttribute, getAttributeValue, findAttributeByName, getAttributes } from "./rule-utils.js"
|
|
3
|
+
|
|
4
|
+
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
+
import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
class NavigationHasLabelVisitor extends BaseRuleVisitor {
|
|
8
|
+
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
9
|
+
this.checkNavigationElement(node)
|
|
10
|
+
super.visitHTMLOpenTagNode(node)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private checkNavigationElement(node: HTMLOpenTagNode): void {
|
|
14
|
+
const tagName = getTagName(node)
|
|
15
|
+
const isNavElement = tagName === "nav"
|
|
16
|
+
const hasNavigationRole = this.hasRoleNavigation(node)
|
|
17
|
+
|
|
18
|
+
if (!isNavElement && !hasNavigationRole) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const hasAriaLabel = hasAttribute(node, "aria-label")
|
|
23
|
+
const hasAriaLabelledby = hasAttribute(node, "aria-labelledby")
|
|
24
|
+
|
|
25
|
+
if (!hasAriaLabel && !hasAriaLabelledby) {
|
|
26
|
+
let message = `The navigation landmark should have a unique accessible name via \`aria-label\` or \`aria-labelledby\`. Remember that the name does not need to include "navigation" or "nav" since it will already be announced.`
|
|
27
|
+
|
|
28
|
+
if (hasNavigationRole && !isNavElement) {
|
|
29
|
+
message += ` Additionally, you can safely drop the \`role="navigation"\` and replace it with the native HTML \`<nav>\` element.`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.addOffense(
|
|
33
|
+
message,
|
|
34
|
+
node.tag_name!.location,
|
|
35
|
+
"error"
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private hasRoleNavigation(node: HTMLOpenTagNode): boolean {
|
|
41
|
+
const attributes = getAttributes(node)
|
|
42
|
+
const roleAttribute = findAttributeByName(attributes, "role")
|
|
43
|
+
|
|
44
|
+
if (!roleAttribute) {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const roleValue = getAttributeValue(roleAttribute)
|
|
49
|
+
|
|
50
|
+
return roleValue === "navigation"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class HTMLNavigationHasLabelRule extends ParserRule {
|
|
55
|
+
name = "html-navigation-has-label"
|
|
56
|
+
|
|
57
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
58
|
+
const visitor = new NavigationHasLabelVisitor(this.name, context)
|
|
59
|
+
|
|
60
|
+
visitor.visit(result.value)
|
|
61
|
+
|
|
62
|
+
return visitor.offenses
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
2
|
+
import { BaseRuleVisitor, getTagName, hasAttribute, getAttributeValue, findAttributeByName, getAttributes } from "./rule-utils.js"
|
|
3
|
+
|
|
4
|
+
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
+
import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
const INTERACTIVE_ELEMENTS = new Set([
|
|
8
|
+
"button", "summary", "input", "select", "textarea", "a"
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
class NoAriaHiddenOnFocusableVisitor extends BaseRuleVisitor {
|
|
12
|
+
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
13
|
+
this.checkAriaHiddenOnFocusable(node)
|
|
14
|
+
super.visitHTMLOpenTagNode(node)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private checkAriaHiddenOnFocusable(node: HTMLOpenTagNode): void {
|
|
18
|
+
if (!this.hasAriaHiddenTrue(node)) return
|
|
19
|
+
|
|
20
|
+
if (this.isFocusable(node)) {
|
|
21
|
+
this.addOffense(
|
|
22
|
+
`Elements that are focusable should not have \`aria-hidden="true"\` because it will cause confusion for assistive technology users.`,
|
|
23
|
+
node.tag_name!.location,
|
|
24
|
+
"error"
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private hasAriaHiddenTrue(node: HTMLOpenTagNode): boolean {
|
|
30
|
+
const attributes = getAttributes(node)
|
|
31
|
+
const ariaHiddenAttr = findAttributeByName(attributes, "aria-hidden")
|
|
32
|
+
|
|
33
|
+
if (!ariaHiddenAttr) return false
|
|
34
|
+
|
|
35
|
+
const value = getAttributeValue(ariaHiddenAttr)
|
|
36
|
+
|
|
37
|
+
return value === "true"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private isFocusable(node: HTMLOpenTagNode): boolean {
|
|
41
|
+
const tagName = getTagName(node)
|
|
42
|
+
if (!tagName) return false
|
|
43
|
+
|
|
44
|
+
const tabIndexValue = this.getTabIndexValue(node)
|
|
45
|
+
|
|
46
|
+
if (tagName === "a") {
|
|
47
|
+
const hasHref = hasAttribute(node, "href")
|
|
48
|
+
|
|
49
|
+
if (!hasHref) {
|
|
50
|
+
return tabIndexValue !== null && tabIndexValue >= 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return tabIndexValue === null || tabIndexValue >= 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (INTERACTIVE_ELEMENTS.has(tagName)) {
|
|
57
|
+
// Interactive elements are focusable unless tabindex is negative
|
|
58
|
+
return tabIndexValue === null || tabIndexValue >= 0
|
|
59
|
+
} else {
|
|
60
|
+
// Non-interactive elements are focusable only if tabindex >= 0
|
|
61
|
+
return tabIndexValue !== null && tabIndexValue >= 0
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private getTabIndexValue(node: HTMLOpenTagNode): number | null {
|
|
66
|
+
const attributes = getAttributes(node)
|
|
67
|
+
const tabIndexAttribute = findAttributeByName(attributes, "tabindex")
|
|
68
|
+
|
|
69
|
+
if (!tabIndexAttribute) return null
|
|
70
|
+
|
|
71
|
+
const value = getAttributeValue(tabIndexAttribute)
|
|
72
|
+
if (!value) return null
|
|
73
|
+
|
|
74
|
+
const parsed = parseInt(value, 10)
|
|
75
|
+
|
|
76
|
+
return isNaN(parsed) ? null : parsed
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class HTMLNoAriaHiddenOnFocusableRule extends ParserRule {
|
|
81
|
+
name = "html-no-aria-hidden-on-focusable"
|
|
82
|
+
|
|
83
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
84
|
+
const visitor = new NoAriaHiddenOnFocusableVisitor(this.name, context)
|
|
85
|
+
|
|
86
|
+
visitor.visit(result.value)
|
|
87
|
+
|
|
88
|
+
return visitor.offenses
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -2,7 +2,7 @@ import { BaseRuleVisitor, isInlineElement, isBlockElement } from "./rule-utils.j
|
|
|
2
2
|
|
|
3
3
|
import { ParserRule } from "../types.js"
|
|
4
4
|
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
-
import type { HTMLOpenTagNode, HTMLElementNode,
|
|
5
|
+
import type { HTMLOpenTagNode, HTMLElementNode, ParseResult } from "@herb-tools/core"
|
|
6
6
|
|
|
7
7
|
class BlockInsideInlineVisitor extends BaseRuleVisitor {
|
|
8
8
|
private inlineStack: string[] = []
|
|
@@ -19,7 +19,7 @@ class BlockInsideInlineVisitor extends BaseRuleVisitor {
|
|
|
19
19
|
return { isInline, isBlock, isUnknown }
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
private
|
|
22
|
+
private addOffenseMessage(tagName: string, isBlock: boolean, openTag: HTMLOpenTagNode): void {
|
|
23
23
|
const parentInline = this.inlineStack[this.inlineStack.length - 1]
|
|
24
24
|
const elementType = isBlock ? "Block-level" : "Unknown"
|
|
25
25
|
|
|
@@ -62,7 +62,7 @@ class BlockInsideInlineVisitor extends BaseRuleVisitor {
|
|
|
62
62
|
const { isInline, isBlock, isUnknown } = this.getElementType(tagName)
|
|
63
63
|
|
|
64
64
|
if ((isBlock || isUnknown) && this.inlineStack.length > 0) {
|
|
65
|
-
this.
|
|
65
|
+
this.addOffenseMessage(tagName, isBlock, openTag)
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (isInline) {
|
|
@@ -77,9 +77,9 @@ class BlockInsideInlineVisitor extends BaseRuleVisitor {
|
|
|
77
77
|
export class HTMLNoBlockInsideInlineRule extends ParserRule {
|
|
78
78
|
name = "html-no-block-inside-inline"
|
|
79
79
|
|
|
80
|
-
check(
|
|
80
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
81
81
|
const visitor = new BlockInsideInlineVisitor(this.name, context)
|
|
82
|
-
visitor.visit(
|
|
82
|
+
visitor.visit(result.value)
|
|
83
83
|
return visitor.offenses
|
|
84
84
|
}
|
|
85
85
|
}
|