@herb-tools/linter 0.5.0 → 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/dist/herb-lint.js +5131 -1647
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +662 -145
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +654 -147
- package/dist/index.js.map +1 -1
- package/dist/package.json +4 -4
- package/dist/src/cli/argument-parser.js +0 -4
- package/dist/src/cli/argument-parser.js.map +1 -1
- package/dist/src/default-rules.js +20 -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-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 +0 -4
- package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
- package/dist/src/rules/html-aria-attribute-must-be-valid.js +11 -10
- 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 +26 -4
- 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 -13
- 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 +3 -3
- package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-attribute-double-quotes.js +14 -4
- 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 +19 -8
- 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 +9 -2
- 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 +0 -4
- 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-duplicate-attributes.js +22 -25
- package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-ids.js +2 -2
- package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
- package/dist/src/rules/html-no-empty-headings.js +0 -21
- package/dist/src/rules/html-no-empty-headings.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 +35 -23
- 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/rule-utils.js +176 -22
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +0 -8
- 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/index.d.ts +4 -0
- package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
- package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +7 -0
- package/dist/types/rules/html-attribute-equals-spacing.d.ts +7 -0
- package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
- package/dist/types/rules/html-iframe-has-title.d.ts +7 -0
- 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-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 +2 -1
- package/dist/types/rules/index.d.ts +10 -0
- package/dist/types/rules/rule-utils.d.ts +107 -13
- package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
- package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +7 -0
- package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +7 -0
- package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
- package/dist/types/src/rules/html-iframe-has-title.d.ts +7 -0
- 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-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 +2 -1
- package/dist/types/src/rules/index.d.ts +10 -0
- package/dist/types/src/rules/rule-utils.d.ts +107 -13
- package/dist/types/src/types.d.ts +24 -0
- package/dist/types/types.d.ts +24 -0
- package/docs/rules/README.md +12 -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/package.json +4 -4
- package/src/cli/argument-parser.ts +0 -5
- package/src/default-rules.ts +20 -0
- package/src/linter.ts +30 -4
- package/src/rules/erb-no-silent-tag-in-attribute-name.ts +40 -0
- package/src/rules/erb-prefer-image-tag-helper.ts +2 -7
- package/src/rules/html-aria-attribute-must-be-valid.ts +28 -32
- package/src/rules/html-aria-label-is-well-formatted.ts +59 -0
- package/src/rules/html-aria-level-must-be-valid.ts +38 -5
- package/src/rules/html-aria-role-heading-requires-level.ts +16 -28
- package/src/rules/html-aria-role-must-be-valid.ts +5 -5
- package/src/rules/html-attribute-double-quotes.ts +21 -6
- package/src/rules/html-attribute-equals-spacing.ts +41 -0
- package/src/rules/html-attribute-values-require-quotes.ts +29 -9
- package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +66 -0
- package/src/rules/html-boolean-attributes-no-value.ts +17 -4
- package/src/rules/html-iframe-has-title.ts +62 -0
- package/src/rules/html-img-require-alt.ts +2 -7
- 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-duplicate-attributes.ts +28 -28
- package/src/rules/html-no-duplicate-ids.ts +4 -3
- package/src/rules/html-no-empty-headings.ts +2 -31
- 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 +42 -29
- package/src/rules/index.ts +10 -0
- package/src/rules/rule-utils.ts +260 -39
- package/src/rules/svg-tag-name-capitalization.ts +2 -9
- package/src/types.ts +27 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { AttributeVisitorMixin } from "./rule-utils"
|
|
2
1
|
import { ParserRule } from "../types"
|
|
3
|
-
import
|
|
2
|
+
import { AttributeVisitorMixin, StaticAttributeStaticValueParams } from "./rule-utils"
|
|
3
|
+
|
|
4
|
+
import type { ParseResult } from "@herb-tools/core"
|
|
4
5
|
import type { LintOffense, LintContext } from "../types"
|
|
5
6
|
|
|
6
7
|
class NoDuplicateIdsVisitor extends AttributeVisitorMixin {
|
|
7
8
|
private documentIds: Set<string> = new Set<string>()
|
|
8
9
|
|
|
9
|
-
protected
|
|
10
|
+
protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams): void {
|
|
10
11
|
if (attributeName.toLowerCase() !== "id") return
|
|
11
12
|
if (!attributeValue) return
|
|
12
13
|
|
|
@@ -2,7 +2,7 @@ import { BaseRuleVisitor, getTagName, getAttributes, findAttributeByName, getAtt
|
|
|
2
2
|
|
|
3
3
|
import { ParserRule } from "../types.js"
|
|
4
4
|
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
-
import type { HTMLElementNode, HTMLOpenTagNode,
|
|
5
|
+
import type { HTMLElementNode, HTMLOpenTagNode, ParseResult, LiteralNode, HTMLTextNode } from "@herb-tools/core"
|
|
6
6
|
|
|
7
7
|
class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
8
8
|
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
@@ -10,10 +10,6 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
10
10
|
super.visitHTMLElementNode(node)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
|
|
14
|
-
this.checkSelfClosingHeading(node)
|
|
15
|
-
super.visitHTMLSelfCloseTagNode(node)
|
|
16
|
-
}
|
|
17
13
|
|
|
18
14
|
private checkHeadingElement(node: HTMLElementNode): void {
|
|
19
15
|
if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
|
|
@@ -47,31 +43,6 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
47
43
|
}
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
private checkSelfClosingHeading(node: HTMLSelfCloseTagNode): void {
|
|
51
|
-
const tagName = getTagName(node)
|
|
52
|
-
if (!tagName) {
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Check if it's a standard heading tag (h1-h6) or has role="heading"
|
|
57
|
-
const isStandardHeading = HEADING_TAGS.has(tagName)
|
|
58
|
-
const isAriaHeading = this.hasHeadingRole(node)
|
|
59
|
-
|
|
60
|
-
if (!isStandardHeading && !isAriaHeading) {
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Self-closing headings are always empty
|
|
65
|
-
const elementDescription = isStandardHeading
|
|
66
|
-
? `\`<${tagName}>\``
|
|
67
|
-
: `\`<${tagName} role="heading">\``
|
|
68
|
-
|
|
69
|
-
this.addOffense(
|
|
70
|
-
`Heading element ${elementDescription} must not be empty. Provide accessible text content for screen readers and SEO.`,
|
|
71
|
-
node.tag_name!.location,
|
|
72
|
-
"error"
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
46
|
|
|
76
47
|
private isEmptyHeading(node: HTMLElementNode): boolean {
|
|
77
48
|
if (!node.body || node.body.length === 0) {
|
|
@@ -114,7 +85,7 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
114
85
|
return !hasAccessibleContent
|
|
115
86
|
}
|
|
116
87
|
|
|
117
|
-
private hasHeadingRole(node: HTMLOpenTagNode
|
|
88
|
+
private hasHeadingRole(node: HTMLOpenTagNode): boolean {
|
|
118
89
|
const attributes = getAttributes(node)
|
|
119
90
|
const roleAttribute = findAttributeByName(attributes, "role")
|
|
120
91
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
2
|
+
import { AttributeVisitorMixin, StaticAttributeStaticValueParams } from "./rule-utils.js"
|
|
3
|
+
|
|
4
|
+
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
+
import type { ParseResult } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
class NoPositiveTabIndexVisitor extends AttributeVisitorMixin {
|
|
8
|
+
protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams): void {
|
|
9
|
+
if (attributeName !== "tabindex") return
|
|
10
|
+
|
|
11
|
+
const tabIndexValue = parseInt(attributeValue, 10)
|
|
12
|
+
|
|
13
|
+
if (!isNaN(tabIndexValue) && tabIndexValue > 0) {
|
|
14
|
+
this.addOffense(
|
|
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
|
+
attributeNode.location,
|
|
17
|
+
"error"
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class HTMLNoPositiveTabIndexRule extends ParserRule {
|
|
24
|
+
name = "html-no-positive-tab-index"
|
|
25
|
+
|
|
26
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
27
|
+
const visitor = new NoPositiveTabIndexVisitor(this.name, context)
|
|
28
|
+
|
|
29
|
+
visitor.visit(result.value)
|
|
30
|
+
|
|
31
|
+
return visitor.offenses
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
2
|
+
import { BaseRuleVisitor, getTagName, isVoidElement } from "./rule-utils.js"
|
|
3
|
+
|
|
4
|
+
import type { LintContext, LintOffense } from "../types.js"
|
|
5
|
+
import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
class NoSelfClosingVisitor extends BaseRuleVisitor {
|
|
8
|
+
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
9
|
+
if (node.tag_closing?.value === "/>") {
|
|
10
|
+
const tagName = getTagName(node)
|
|
11
|
+
|
|
12
|
+
const shouldBeVoid = tagName ? isVoidElement(tagName) : false
|
|
13
|
+
const instead = shouldBeVoid ? `Use \`<${tagName}>\` instead.` : `Use \`<${tagName}></${tagName}>\` instead.`
|
|
14
|
+
|
|
15
|
+
this.addOffense(
|
|
16
|
+
`Self-closing syntax \`<${tagName} />\` is not allowed in HTML. ${instead}`,
|
|
17
|
+
node.location,
|
|
18
|
+
"error"
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
super.visitHTMLOpenTagNode(node)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class HTMLNoSelfClosingRule extends ParserRule {
|
|
27
|
+
name = "html-no-self-closing"
|
|
28
|
+
|
|
29
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
30
|
+
const visitor = new NoSelfClosingVisitor(this.name, context)
|
|
31
|
+
|
|
32
|
+
visitor.visit(result.value)
|
|
33
|
+
|
|
34
|
+
return visitor.offenses
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
2
|
+
import { BaseRuleVisitor, getTagName, hasAttribute } 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 NoTitleAttributeVisitor extends BaseRuleVisitor {
|
|
8
|
+
ALLOWED_ELEMENTS_WITH_TITLE = new Set(["iframe", "link"])
|
|
9
|
+
|
|
10
|
+
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
11
|
+
this.checkTitleAttribute(node)
|
|
12
|
+
super.visitHTMLOpenTagNode(node)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private checkTitleAttribute(node: HTMLOpenTagNode): void {
|
|
16
|
+
const tagName = getTagName(node)
|
|
17
|
+
|
|
18
|
+
if (!tagName || this.ALLOWED_ELEMENTS_WITH_TITLE.has(tagName)) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (hasAttribute(node, "title")) {
|
|
23
|
+
this.addOffense(
|
|
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
|
+
node.tag_name!.location,
|
|
26
|
+
"error"
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class HTMLNoTitleAttributeRule extends ParserRule {
|
|
33
|
+
name = "html-no-title-attribute"
|
|
34
|
+
|
|
35
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
36
|
+
const visitor = new NoTitleAttributeVisitor(this.name, context)
|
|
37
|
+
|
|
38
|
+
visitor.visit(result.value)
|
|
39
|
+
|
|
40
|
+
return visitor.offenses
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,53 +1,56 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
1
2
|
import { BaseRuleVisitor } from "./rule-utils.js"
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import { isNode, getTagName, HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, ParseResult, XMLDeclarationNode, Node } from "@herb-tools/core"
|
|
5
|
+
|
|
4
6
|
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
-
import type { HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, HTMLSelfCloseTagNode, ParseResult } from "@herb-tools/core"
|
|
6
7
|
|
|
7
|
-
class
|
|
8
|
-
|
|
9
|
-
const tagName = node.tag_name?.value
|
|
8
|
+
class XMLDeclarationChecker extends BaseRuleVisitor {
|
|
9
|
+
hasXMLDeclaration: boolean = false
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
visitXMLDeclarationNode(_node: XMLDeclarationNode): void {
|
|
12
|
+
this.hasXMLDeclaration = true
|
|
13
|
+
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
visitChildNodes(node: Node): void {
|
|
16
|
+
if (this.hasXMLDeclaration) return
|
|
17
|
+
super.visitChildNodes(node)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
class TagNameLowercaseVisitor extends BaseRuleVisitor {
|
|
22
|
+
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
23
|
+
if (getTagName(node).toLowerCase() === "svg") {
|
|
24
|
+
this.checkTagName(node.open_tag)
|
|
25
|
+
this.checkTagName(node.close_tag)
|
|
26
|
+
} else {
|
|
27
|
+
super.visitHTMLElementNode(node)
|
|
21
28
|
}
|
|
29
|
+
}
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (node.close_tag) {
|
|
26
|
-
this.checkTagName(node.close_tag as HTMLCloseTagNode)
|
|
27
|
-
}
|
|
31
|
+
visitHTMLOpenTagNode(node: HTMLOpenTagNode) {
|
|
32
|
+
this.checkTagName(node)
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
visitHTMLCloseTagNode(node: HTMLCloseTagNode) {
|
|
31
36
|
this.checkTagName(node)
|
|
32
|
-
this.visitChildNodes(node)
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
private checkTagName(node: HTMLOpenTagNode | HTMLCloseTagNode |
|
|
36
|
-
|
|
39
|
+
private checkTagName(node: HTMLOpenTagNode | HTMLCloseTagNode | null): void {
|
|
40
|
+
if (!node) return
|
|
41
|
+
|
|
42
|
+
const tagName = getTagName(node)
|
|
37
43
|
|
|
38
44
|
if (!tagName) return
|
|
39
45
|
|
|
40
46
|
const lowercaseTagName = tagName.toLowerCase()
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (node.type == "AST_HTML_OPEN_TAG_NODE") type = "Opening"
|
|
46
|
-
if (node.type == "AST_HTML_CLOSE_TAG_NODE") type = "Closing"
|
|
47
|
-
if (node.type == "AST_HTML_SELF_CLOSE_TAG_NODE") type = "Self-closing"
|
|
48
|
+
const type = isNode(node, HTMLOpenTagNode) ? "Opening" : "Closing"
|
|
49
|
+
const open = isNode(node, HTMLOpenTagNode) ? "<" : "</"
|
|
48
50
|
|
|
51
|
+
if (tagName !== lowercaseTagName) {
|
|
49
52
|
this.addOffense(
|
|
50
|
-
`${type} tag name \`${tagName}
|
|
53
|
+
`${type} tag name \`${open}${tagName}>\` should be lowercase. Use \`${open}${lowercaseTagName}>\` instead.`,
|
|
51
54
|
node.tag_name!.location,
|
|
52
55
|
"error"
|
|
53
56
|
)
|
|
@@ -58,6 +61,16 @@ class TagNameLowercaseVisitor extends BaseRuleVisitor {
|
|
|
58
61
|
export class HTMLTagNameLowercaseRule extends ParserRule {
|
|
59
62
|
name = "html-tag-name-lowercase"
|
|
60
63
|
|
|
64
|
+
isEnabled(result: ParseResult, context?: Partial<LintContext>): boolean {
|
|
65
|
+
if (context?.fileName?.endsWith(".xml") || context?.fileName?.endsWith(".xml.erb")) {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const checker = new XMLDeclarationChecker(this.name)
|
|
70
|
+
checker.visit(result.value)
|
|
71
|
+
return !checker.hasXMLDeclaration
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
62
75
|
const visitor = new TagNameLowercaseVisitor(this.name, context)
|
|
63
76
|
visitor.visit(result.value)
|
package/src/rules/index.ts
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
export * from "./erb-no-empty-tags.js"
|
|
2
2
|
export * from "./erb-no-output-control-flow.js"
|
|
3
|
+
export * from "./erb-no-silent-tag-in-attribute-name.js"
|
|
3
4
|
export * from "./erb-prefer-image-tag-helper.js"
|
|
4
5
|
export * from "./erb-requires-trailing-newline.js"
|
|
5
6
|
export * from "./html-anchor-require-href.js"
|
|
7
|
+
export * from "./html-aria-label-is-well-formatted.js"
|
|
6
8
|
export * from "./html-aria-level-must-be-valid.js"
|
|
7
9
|
export * from "./html-aria-role-heading-requires-level.js"
|
|
8
10
|
export * from "./html-aria-role-must-be-valid.js"
|
|
9
11
|
export * from "./html-attribute-double-quotes.js"
|
|
12
|
+
export * from "./html-attribute-equals-spacing.js"
|
|
10
13
|
export * from "./html-attribute-values-require-quotes.js"
|
|
14
|
+
export * from "./html-avoid-both-disabled-and-aria-disabled.js"
|
|
11
15
|
export * from "./html-boolean-attributes-no-value.js"
|
|
16
|
+
export * from "./html-iframe-has-title.js"
|
|
12
17
|
export * from "./html-img-require-alt.js"
|
|
18
|
+
export * from "./html-navigation-has-label.js"
|
|
19
|
+
export * from "./html-no-aria-hidden-on-focusable.js"
|
|
13
20
|
export * from "./html-no-block-inside-inline.js"
|
|
14
21
|
export * from "./html-no-duplicate-attributes.js"
|
|
15
22
|
export * from "./html-no-duplicate-ids.js"
|
|
16
23
|
export * from "./html-no-empty-headings.js"
|
|
17
24
|
export * from "./html-no-nested-links.js"
|
|
25
|
+
export * from "./html-no-positive-tab-index.js"
|
|
26
|
+
export * from "./html-no-self-closing.js"
|
|
27
|
+
export * from "./html-no-title-attribute.js"
|
|
18
28
|
export * from "./html-tag-name-lowercase.js"
|
|
19
29
|
export * from "./svg-tag-name-capitalization.js"
|