@herb-tools/linter 0.4.0 → 0.4.2
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 +2 -4
- package/dist/herb-lint.js +292 -107
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +351 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +348 -78
- package/dist/index.js.map +1 -1
- package/dist/package.json +4 -4
- package/dist/src/default-rules.js +10 -2
- package/dist/src/default-rules.js.map +1 -1
- package/dist/src/rules/erb-require-whitespace-inside-tags.js +18 -2
- package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
- package/dist/src/rules/html-aria-attribute-must-be-valid.js +24 -0
- package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -0
- package/dist/src/rules/html-aria-role-must-be-valid.js +21 -0
- package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -0
- package/dist/src/rules/html-no-duplicate-ids.js +25 -0
- package/dist/src/rules/html-no-duplicate-ids.js.map +1 -0
- package/dist/src/rules/html-tag-name-lowercase.js +17 -8
- package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
- package/dist/src/rules/index.js +4 -0
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/rule-utils.js +126 -8
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +57 -0
- package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +6 -0
- package/dist/types/rules/html-aria-role-must-be-valid.d.ts +6 -0
- package/dist/types/rules/html-no-duplicate-ids.d.ts +6 -0
- package/dist/types/rules/index.d.ts +4 -0
- package/dist/types/rules/rule-utils.d.ts +11 -0
- package/dist/types/rules/svg-tag-name-capitalization.d.ts +6 -0
- package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +6 -0
- package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +6 -0
- package/dist/types/src/rules/html-no-duplicate-ids.d.ts +6 -0
- package/dist/types/src/rules/index.d.ts +4 -0
- package/dist/types/src/rules/rule-utils.d.ts +11 -0
- package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +6 -0
- package/docs/rules/README.md +5 -0
- package/docs/rules/html-aria-attribute-must-be-valid.md +45 -0
- package/docs/rules/html-aria-role-must-be-valid.md +45 -0
- package/docs/rules/html-no-duplicate-ids.md +49 -0
- package/docs/rules/svg-tag-name-capitalization.md +57 -0
- package/package.json +4 -4
- package/src/default-rules.ts +10 -2
- package/src/rules/erb-require-whitespace-inside-tags.ts +33 -2
- package/src/rules/html-aria-attribute-must-be-valid.ts +42 -0
- package/src/rules/html-aria-role-must-be-valid.ts +30 -0
- package/src/rules/html-no-duplicate-ids.ts +39 -0
- package/src/rules/html-tag-name-lowercase.ts +24 -9
- package/src/rules/index.ts +4 -0
- package/src/rules/rule-utils.ts +145 -17
- package/src/rules/svg-tag-name-capitalization.ts +73 -0
|
@@ -56,6 +56,17 @@ export declare const HTML_INLINE_ELEMENTS: Set<string>;
|
|
|
56
56
|
export declare const HTML_BLOCK_ELEMENTS: Set<string>;
|
|
57
57
|
export declare const HTML_BOOLEAN_ATTRIBUTES: Set<string>;
|
|
58
58
|
export declare const HEADING_TAGS: Set<string>;
|
|
59
|
+
/**
|
|
60
|
+
* SVG elements that use camelCase naming
|
|
61
|
+
*/
|
|
62
|
+
export declare const SVG_CAMEL_CASE_ELEMENTS: Set<string>;
|
|
63
|
+
/**
|
|
64
|
+
* Mapping from lowercase SVG element names to their correct camelCase versions
|
|
65
|
+
* Generated dynamically from SVG_CAMEL_CASE_ELEMENTS
|
|
66
|
+
*/
|
|
67
|
+
export declare const SVG_LOWERCASE_TO_CAMELCASE: Map<string, string>;
|
|
68
|
+
export declare const VALID_ARIA_ROLES: Set<string>;
|
|
69
|
+
export declare const ARIA_ATTRIBUTES: Set<string>;
|
|
59
70
|
/**
|
|
60
71
|
* Checks if an element is inline
|
|
61
72
|
*/
|
package/docs/rules/README.md
CHANGED
|
@@ -7,15 +7,20 @@ This page contains documentation for all Herb Linter rules.
|
|
|
7
7
|
- [`erb-no-empty-tags`](./erb-no-empty-tags.md) - Disallow empty ERB tags
|
|
8
8
|
- [`erb-no-output-control-flow`](./erb-no-output-control-flow.md) - Prevents outputting control flow blocks
|
|
9
9
|
- [`erb-require-whitespace-inside-tags`](./erb-require-whitespace-inside-tags.md) - Requires whitespace around erb tags
|
|
10
|
+
- [`html-anchor-require-href`](./html-anchor-require-href.md) - Requires an href attribute on anchor tags
|
|
11
|
+
- [`html-aria-attribute-must-be-valid`](./html-aria-attribute-must-be-valid.md) - Disallow invalid or unknown `aria-*` attributes.
|
|
10
12
|
- [`html-aria-role-heading-requires-level`](./html-aria-role-heading-requires-level.md) - Requires `aria-level` when supplying a `role`
|
|
13
|
+
- [`html-aria-role-must-be-valid`](./html-aria-role-must-be-valid.md) - The `role` attribute must have a valid WAI-ARIA Role.
|
|
11
14
|
- [`html-attribute-double-quotes`](./html-attribute-double-quotes.md) - Enforces double quotes for attribute values
|
|
12
15
|
- [`html-attribute-values-require-quotes`](./html-attribute-values-require-quotes.md) - Requires quotes around attribute values
|
|
13
16
|
- [`html-boolean-attributes-no-value`](./html-boolean-attributes-no-value.md) - Prevents values on boolean attributes
|
|
14
17
|
- [`html-img-require-alt`](./html-img-require-alt.md) - Requires alt attributes on img tags
|
|
15
18
|
- [`html-no-block-inside-inline`](./html-no-block-inside-inline.md) - Prevents block-level elements inside inline elements
|
|
19
|
+
- [`html-no-duplicate-ids`](./html-no-duplicate-ids.md) - Prevents duplicate IDs within a document
|
|
16
20
|
- [`html-no-duplicate-attributes`](./html-no-duplicate-attributes.md) - Prevents duplicate attributes on HTML elements
|
|
17
21
|
- [`html-no-nested-links`](./html-no-nested-links.md) - Prevents nested anchor tags
|
|
18
22
|
- [`html-tag-name-lowercase`](./html-tag-name-lowercase.md) - Enforces lowercase tag names in HTML
|
|
23
|
+
- [`svg-tag-name-capitalization`](./svg-tag-name-capitalization.md) - Enforces proper camelCase capitalization for SVG elements
|
|
19
24
|
|
|
20
25
|
## Contributing
|
|
21
26
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Linter Rule: Disallow invalid or unknown `aria-*` attributes.
|
|
2
|
+
|
|
3
|
+
**Rule:** `html-aria-attribute-must-be-valid`
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
Disallow unknown or invalid `aria-*` attributes. Only attributes defined in the WAI-ARIA specification should be used. This rule helps catch typos (e.g. `aria-lable`), misuse, or outdated attribute names that won't be interpreted by assistive technologies.
|
|
8
|
+
|
|
9
|
+
## Rationale
|
|
10
|
+
|
|
11
|
+
ARIA attributes are powerful accessibility tools, but **only if used correctly**. Mistyped or unsupported attributes:
|
|
12
|
+
|
|
13
|
+
- Are silently ignored by browsers and screen readers
|
|
14
|
+
- Fail to communicate intent
|
|
15
|
+
- Give a false sense of accessibility
|
|
16
|
+
|
|
17
|
+
Validating against a known list ensures you're using correct and effective ARIA patterns.
|
|
18
|
+
|
|
19
|
+
## Examples
|
|
20
|
+
|
|
21
|
+
### ✅ Good
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<div role="button" aria-pressed="false">Toggle</div>
|
|
25
|
+
<input type="text" aria-label="Search" />
|
|
26
|
+
<span role="heading" aria-level="2">Title</span>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 🚫 Bad
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<!-- typo -->
|
|
33
|
+
<div role="button" aria-presed="false">Toggle</div>
|
|
34
|
+
|
|
35
|
+
<!-- typo -->
|
|
36
|
+
<input type="text" aria-lable="Search" />
|
|
37
|
+
|
|
38
|
+
<!-- invalid -->
|
|
39
|
+
<span aria-size="large" role="heading" aria-level="2">Title</span>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## References
|
|
43
|
+
|
|
44
|
+
- [ARIA states and properties (attributes)](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes)
|
|
45
|
+
- [NPM Package: `aria-attributes`](https://github.com/wooorm/aria-attributes)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Linter Rule: Disallow invalid values for the `role` attribute
|
|
2
|
+
|
|
3
|
+
**Rule:** `html-aria-role-must-be-valid`
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
Disallow invalid or unknown values for the `role` attribute. The `role` attribute must match one of the recognized ARIA role values as defined by the [WAI-ARIA specification](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles).
|
|
8
|
+
|
|
9
|
+
## Rationale
|
|
10
|
+
|
|
11
|
+
ARIA `role` attributes are used to define the purpose of an element to assistive technologies. Using invalid, misspelled, or non-standard roles results in:
|
|
12
|
+
|
|
13
|
+
* Screen readers ignoring the role
|
|
14
|
+
* Broken accessibility semantics
|
|
15
|
+
* False sense of correctness
|
|
16
|
+
|
|
17
|
+
Validating against the official list of ARIA roles prevents silent accessibility failures.
|
|
18
|
+
|
|
19
|
+
## Examples
|
|
20
|
+
|
|
21
|
+
### ✅ Good
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<div role="button">Click me</div>
|
|
25
|
+
<nav role="navigation">...</nav>
|
|
26
|
+
<section role="region">...</section>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 🚫 Bad
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<!-- typo -->
|
|
33
|
+
<div role="buton">Click me</div>
|
|
34
|
+
|
|
35
|
+
<!-- not a valid role -->
|
|
36
|
+
<nav role="nav">...</nav>
|
|
37
|
+
|
|
38
|
+
<!-- not in the ARIA spec -->
|
|
39
|
+
<section role="header">...</section>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## References
|
|
43
|
+
|
|
44
|
+
* [ARIA 1.2 Specification - Roles](https://www.w3.org/TR/wai-aria/#roles)
|
|
45
|
+
* [MDN: ARIA roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Linter Rule: Disallow duplicate IDs in the same document
|
|
2
|
+
|
|
3
|
+
**Rule:** `html-no-duplicate-ids`
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
Ensure that `id` attribute is unique within a document.
|
|
8
|
+
|
|
9
|
+
## Rationale
|
|
10
|
+
|
|
11
|
+
Duplicate IDs in an HTML document can lead to unexpected behavior, especially when using JavaScript or CSS that relies on unique identifiers. Browsers may not handle duplicate IDs consistently, which can cause issues with element selection, styling, and event handling.
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
### ✅ Good
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<div id="header">Header</div>
|
|
19
|
+
<div id="main-content">Main Content</div>
|
|
20
|
+
<div id="footer">Footer</div>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```erb
|
|
24
|
+
<div id="<%= dom_id("header") %>">Header</div>
|
|
25
|
+
<div id="<%= dom_id("main_content") %>">Main Content</div>
|
|
26
|
+
<div id="<%= dom_id("footer") %>">Footer</div>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 🚫 Bad
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<div id="header">Header</div>
|
|
33
|
+
|
|
34
|
+
<div id="header">Duplicate Header</div>
|
|
35
|
+
|
|
36
|
+
<div id="footer">Footer</div>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```erb
|
|
40
|
+
<div id="<%= dom_id("header") %>">Header</div>
|
|
41
|
+
|
|
42
|
+
<div id="<%= dom_id("header") %>">Duplicate Header</div>
|
|
43
|
+
|
|
44
|
+
<div id="<%= dom_id("footer") %>">Footer</div>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## References
|
|
48
|
+
* [W3 org - The id attribute](https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#the-id-attribute)
|
|
49
|
+
* [Rails `ActionView::RecordIdentifier#dom_id`](https://api.rubyonrails.org/classes/ActionView/RecordIdentifier.html#method-i-dom_id)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Linter Rule: SVG tag name capitalization
|
|
2
|
+
|
|
3
|
+
**Rule:** `svg-tag-name-capitalization`
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
Enforces proper camelCase capitalization for SVG element names within SVG contexts.
|
|
8
|
+
|
|
9
|
+
## Rationale
|
|
10
|
+
|
|
11
|
+
SVG elements use camelCase naming conventions (e.g., `linearGradient`, `clipPath`, `feGaussianBlur`) rather than the lowercase conventions used in HTML. This rule ensures that SVG elements within `<svg>` tags use the correct capitalization for proper rendering and standards compliance.
|
|
12
|
+
|
|
13
|
+
This rule only applies to elements within SVG contexts and does not check the `<svg>` tag itself (that's handled by the `html-tag-name-lowercase` rule).
|
|
14
|
+
|
|
15
|
+
## Examples
|
|
16
|
+
|
|
17
|
+
### ✅ Good
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<svg>
|
|
21
|
+
<linearGradient id="grad1">
|
|
22
|
+
<stop offset="0%" stop-color="rgb(255,255,0)" />
|
|
23
|
+
</linearGradient>
|
|
24
|
+
</svg>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<svg>
|
|
29
|
+
<clipPath id="clip">
|
|
30
|
+
<rect width="100" height="100" />
|
|
31
|
+
</clipPath>
|
|
32
|
+
<feGaussianBlur stdDeviation="5" />
|
|
33
|
+
</svg>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 🚫 Bad
|
|
37
|
+
|
|
38
|
+
```html
|
|
39
|
+
<svg>
|
|
40
|
+
<lineargradient id="grad1">
|
|
41
|
+
<stop offset="0%" stop-color="rgb(255,255,0)" />
|
|
42
|
+
</lineargradient>
|
|
43
|
+
</svg>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<svg>
|
|
48
|
+
<CLIPPATH id="clip">
|
|
49
|
+
<rect width="100" height="100" />
|
|
50
|
+
</CLIPPATH>
|
|
51
|
+
</svg>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## References
|
|
55
|
+
|
|
56
|
+
* [SVG Element Reference](https://developer.mozilla.org/en-US/docs/Web/SVG/Element)
|
|
57
|
+
* [SVG Naming Conventions](https://www.w3.org/TR/SVG2/)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herb-tools/linter",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "HTML+ERB linter for validating HTML structure and enforcing best practices",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://herb-tools.dev",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@herb-tools/core": "0.4.
|
|
37
|
-
"@herb-tools/highlighter": "0.4.
|
|
38
|
-
"@herb-tools/node-wasm": "0.4.
|
|
36
|
+
"@herb-tools/core": "0.4.2",
|
|
37
|
+
"@herb-tools/highlighter": "0.4.2",
|
|
38
|
+
"@herb-tools/node-wasm": "0.4.2",
|
|
39
39
|
"glob": "^11.0.3"
|
|
40
40
|
},
|
|
41
41
|
"files": [
|
package/src/default-rules.ts
CHANGED
|
@@ -4,30 +4,38 @@ import { ERBNoEmptyTagsRule } from "./rules/erb-no-empty-tags.js"
|
|
|
4
4
|
import { ERBNoOutputControlFlowRule } from "./rules/erb-no-output-control-flow.js"
|
|
5
5
|
import { ERBRequireWhitespaceRule } from "./rules/erb-require-whitespace-inside-tags.js"
|
|
6
6
|
import { HTMLAnchorRequireHrefRule } from "./rules/html-anchor-require-href.js"
|
|
7
|
+
import { HTMLAriaAttributeMustBeValid } from "./rules/html-aria-attribute-must-be-valid.js"
|
|
7
8
|
import { HTMLAriaRoleHeadingRequiresLevelRule } from "./rules/html-aria-role-heading-requires-level.js"
|
|
9
|
+
import { HTMLAriaRoleMustBeValidRule } from "./rules/html-aria-role-must-be-valid.js"
|
|
8
10
|
import { HTMLAttributeDoubleQuotesRule } from "./rules/html-attribute-double-quotes.js"
|
|
9
11
|
import { HTMLAttributeValuesRequireQuotesRule } from "./rules/html-attribute-values-require-quotes.js"
|
|
10
12
|
import { HTMLBooleanAttributesNoValueRule } from "./rules/html-boolean-attributes-no-value.js"
|
|
11
13
|
import { HTMLImgRequireAltRule } from "./rules/html-img-require-alt.js"
|
|
12
|
-
import { HTMLNoBlockInsideInlineRule } from "./rules/html-no-block-inside-inline.js"
|
|
14
|
+
// import { HTMLNoBlockInsideInlineRule } from "./rules/html-no-block-inside-inline.js"
|
|
13
15
|
import { HTMLNoDuplicateAttributesRule } from "./rules/html-no-duplicate-attributes.js"
|
|
16
|
+
import { HTMLNoDuplicateIdsRule } from "./rules/html-no-duplicate-ids.js"
|
|
14
17
|
import { HTMLNoEmptyHeadingsRule } from "./rules/html-no-empty-headings.js"
|
|
15
18
|
import { HTMLNoNestedLinksRule } from "./rules/html-no-nested-links.js"
|
|
16
19
|
import { HTMLTagNameLowercaseRule } from "./rules/html-tag-name-lowercase.js"
|
|
20
|
+
import { SVGTagNameCapitalizationRule } from "./rules/svg-tag-name-capitalization.js"
|
|
17
21
|
|
|
18
22
|
export const defaultRules: RuleClass[] = [
|
|
19
23
|
ERBNoEmptyTagsRule,
|
|
20
24
|
ERBNoOutputControlFlowRule,
|
|
21
25
|
ERBRequireWhitespaceRule,
|
|
22
26
|
HTMLAnchorRequireHrefRule,
|
|
27
|
+
HTMLAriaAttributeMustBeValid,
|
|
23
28
|
HTMLAriaRoleHeadingRequiresLevelRule,
|
|
29
|
+
HTMLAriaRoleMustBeValidRule,
|
|
24
30
|
HTMLAttributeDoubleQuotesRule,
|
|
25
31
|
HTMLAttributeValuesRequireQuotesRule,
|
|
26
32
|
HTMLBooleanAttributesNoValueRule,
|
|
27
33
|
HTMLImgRequireAltRule,
|
|
28
|
-
HTMLNoBlockInsideInlineRule,
|
|
34
|
+
// HTMLNoBlockInsideInlineRule,
|
|
29
35
|
HTMLNoDuplicateAttributesRule,
|
|
36
|
+
HTMLNoDuplicateIdsRule,
|
|
30
37
|
HTMLNoEmptyHeadingsRule,
|
|
31
38
|
HTMLNoNestedLinksRule,
|
|
32
39
|
HTMLTagNameLowercaseRule,
|
|
40
|
+
SVGTagNameCapitalizationRule,
|
|
33
41
|
]
|
|
@@ -24,14 +24,43 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
|
|
|
24
24
|
|
|
25
25
|
const value = content.value
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
if (openTag.value === "<%#") {
|
|
28
|
+
this.checkCommentTagWhitespace(openTag, closeTag, value)
|
|
29
|
+
} else {
|
|
30
|
+
this.checkOpenTagWhitespace(openTag, value)
|
|
31
|
+
this.checkCloseTagWhitespace(closeTag, value)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private checkCommentTagWhitespace(openTag: Token, closeTag: Token, content: string): void {
|
|
36
|
+
if (!content.startsWith(" ") && !content.startsWith("\n") && !content.startsWith("=")) {
|
|
37
|
+
this.addOffense(
|
|
38
|
+
`Add whitespace after \`${openTag.value}\`.`,
|
|
39
|
+
openTag.location,
|
|
40
|
+
"error"
|
|
41
|
+
)
|
|
42
|
+
} else if (content.startsWith("=") && content.length > 1 && !content[1].match(/\s/)) {
|
|
43
|
+
this.addOffense(
|
|
44
|
+
`Add whitespace after \`<%#=\`.`,
|
|
45
|
+
openTag.location,
|
|
46
|
+
"error"
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!content.endsWith(" ") && !content.endsWith("\n")) {
|
|
51
|
+
this.addOffense(
|
|
52
|
+
`Add whitespace before \`${closeTag.value}\`.`,
|
|
53
|
+
closeTag.location,
|
|
54
|
+
"error"
|
|
55
|
+
)
|
|
56
|
+
}
|
|
29
57
|
}
|
|
30
58
|
|
|
31
59
|
private checkOpenTagWhitespace(openTag: Token, content:string):void {
|
|
32
60
|
if (content.startsWith(" ") || content.startsWith("\n")) {
|
|
33
61
|
return
|
|
34
62
|
}
|
|
63
|
+
|
|
35
64
|
this.addOffense(
|
|
36
65
|
`Add whitespace after \`${openTag.value}\`.`,
|
|
37
66
|
openTag.location,
|
|
@@ -43,6 +72,7 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
|
|
|
43
72
|
if (content.endsWith(" ") || content.endsWith("\n")) {
|
|
44
73
|
return
|
|
45
74
|
}
|
|
75
|
+
|
|
46
76
|
this.addOffense(
|
|
47
77
|
`Add whitespace before \`${closeTag.value}\`.`,
|
|
48
78
|
closeTag.location,
|
|
@@ -53,6 +83,7 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
|
|
|
53
83
|
|
|
54
84
|
export class ERBRequireWhitespaceRule implements Rule {
|
|
55
85
|
name = "erb-require-whitespace-inside-tags"
|
|
86
|
+
|
|
56
87
|
check(node: Node): LintOffense[] {
|
|
57
88
|
const visitor = new RequireWhitespaceInsideTags(this.name)
|
|
58
89
|
visitor.visit(node)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ARIA_ATTRIBUTES,
|
|
3
|
+
AttributeVisitorMixin,
|
|
4
|
+
} from "./rule-utils.js";
|
|
5
|
+
|
|
6
|
+
import type { LintOffense, Rule } from "../types.js";
|
|
7
|
+
import type {
|
|
8
|
+
HTMLAttributeNode,
|
|
9
|
+
HTMLOpenTagNode,
|
|
10
|
+
HTMLSelfCloseTagNode,
|
|
11
|
+
Node,
|
|
12
|
+
} from "@herb-tools/core";
|
|
13
|
+
|
|
14
|
+
class AriaAttributeMustBeValid extends AttributeVisitorMixin {
|
|
15
|
+
checkAttribute(
|
|
16
|
+
attributeName: string,
|
|
17
|
+
_attributeValue: string | null,
|
|
18
|
+
attributeNode: HTMLAttributeNode,
|
|
19
|
+
_parentNode: HTMLOpenTagNode | HTMLSelfCloseTagNode,
|
|
20
|
+
): void {
|
|
21
|
+
if (!attributeName.startsWith("aria-")) return;
|
|
22
|
+
|
|
23
|
+
if (!ARIA_ATTRIBUTES.has(attributeName)){
|
|
24
|
+
this.offenses.push({
|
|
25
|
+
message: `The attribute \`${attributeName}\` is not a valid ARIA attribute. ARIA attributes must match the WAI-ARIA specification.`,
|
|
26
|
+
severity: "error",
|
|
27
|
+
location: attributeNode.location,
|
|
28
|
+
rule: this.ruleName,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class HTMLAriaAttributeMustBeValid implements Rule {
|
|
35
|
+
name = "html-aria-attribute-must-be-valid";
|
|
36
|
+
|
|
37
|
+
check(node: Node): LintOffense[] {
|
|
38
|
+
const visitor = new AriaAttributeMustBeValid(this.name);
|
|
39
|
+
visitor.visit(node);
|
|
40
|
+
return visitor.offenses;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AttributeVisitorMixin, VALID_ARIA_ROLES } from "./rule-utils.js"
|
|
2
|
+
|
|
3
|
+
import type { Rule, LintOffense } from "../types.js"
|
|
4
|
+
import type { Node, HTMLAttributeNode } from "@herb-tools/core"
|
|
5
|
+
|
|
6
|
+
class AriaRoleMustBeValid extends AttributeVisitorMixin {
|
|
7
|
+
checkAttribute(attributeName: string, attributeValue: string | null, attributeNode: HTMLAttributeNode,): void {
|
|
8
|
+
if (attributeName !== "role") return
|
|
9
|
+
if (attributeValue === null) return
|
|
10
|
+
if (VALID_ARIA_ROLES.has(attributeValue)) return
|
|
11
|
+
|
|
12
|
+
this.addOffense(
|
|
13
|
+
`The \`role\` attribute must be a valid ARIA role. Role \`${attributeValue}\` is not recognized.`,
|
|
14
|
+
attributeNode.location,
|
|
15
|
+
"error"
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class HTMLAriaRoleMustBeValidRule implements Rule {
|
|
21
|
+
name = "html-aria-role-must-be-valid"
|
|
22
|
+
|
|
23
|
+
check(node: Node): LintOffense[] {
|
|
24
|
+
const visitor = new AriaRoleMustBeValid(this.name)
|
|
25
|
+
|
|
26
|
+
visitor.visit(node)
|
|
27
|
+
|
|
28
|
+
return visitor.offenses
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { AttributeVisitorMixin } from "./rule-utils"
|
|
2
|
+
|
|
3
|
+
import type { Node } from "@herb-tools/core"
|
|
4
|
+
import type { LintOffense, Rule } from "../types"
|
|
5
|
+
|
|
6
|
+
class NoDuplicateIdsVisitor extends AttributeVisitorMixin {
|
|
7
|
+
private documentIds: Set<string> = new Set<string>()
|
|
8
|
+
|
|
9
|
+
protected checkAttribute(attributeName: string, attributeValue: string | null, attributeNode: Node): void {
|
|
10
|
+
if (attributeName.toLowerCase() !== "id") return
|
|
11
|
+
if (!attributeValue) return
|
|
12
|
+
|
|
13
|
+
const id = attributeValue.trim()
|
|
14
|
+
|
|
15
|
+
if (this.documentIds.has(id)) {
|
|
16
|
+
this.addOffense(
|
|
17
|
+
`Duplicate ID \`${id}\` found. IDs must be unique within a document.`,
|
|
18
|
+
attributeNode.location,
|
|
19
|
+
"error"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.documentIds.add(id)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class HTMLNoDuplicateIdsRule implements Rule {
|
|
30
|
+
name = "html-no-duplicate-ids"
|
|
31
|
+
|
|
32
|
+
check(node: Node): LintOffense[] {
|
|
33
|
+
const visitor = new NoDuplicateIdsVisitor(this.name)
|
|
34
|
+
|
|
35
|
+
visitor.visit(node)
|
|
36
|
+
|
|
37
|
+
return visitor.offenses
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
import { BaseRuleVisitor } from "./rule-utils.js"
|
|
2
2
|
|
|
3
3
|
import type { Rule, LintOffense } from "../types.js"
|
|
4
|
-
import type { HTMLOpenTagNode, HTMLCloseTagNode, HTMLSelfCloseTagNode, Node } from "@herb-tools/core"
|
|
4
|
+
import type { HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, HTMLSelfCloseTagNode, Node } from "@herb-tools/core"
|
|
5
5
|
|
|
6
6
|
class TagNameLowercaseVisitor extends BaseRuleVisitor {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
8
|
+
const tagName = node.tag_name?.value
|
|
9
|
+
|
|
10
|
+
if (node.open_tag) {
|
|
11
|
+
this.checkTagName(node.open_tag as HTMLOpenTagNode)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (tagName && ["svg"].includes(tagName.toLowerCase())) {
|
|
15
|
+
if (node.close_tag) {
|
|
16
|
+
this.checkTagName(node.close_tag as HTMLCloseTagNode)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return
|
|
20
|
+
}
|
|
11
21
|
|
|
12
|
-
visitHTMLCloseTagNode(node: HTMLCloseTagNode): void {
|
|
13
|
-
this.checkTagName(node)
|
|
14
22
|
this.visitChildNodes(node)
|
|
23
|
+
|
|
24
|
+
if (node.close_tag) {
|
|
25
|
+
this.checkTagName(node.close_tag as HTMLCloseTagNode)
|
|
26
|
+
}
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
|
|
@@ -21,9 +33,12 @@ class TagNameLowercaseVisitor extends BaseRuleVisitor {
|
|
|
21
33
|
|
|
22
34
|
private checkTagName(node: HTMLOpenTagNode | HTMLCloseTagNode | HTMLSelfCloseTagNode): void {
|
|
23
35
|
const tagName = node.tag_name?.value
|
|
36
|
+
|
|
24
37
|
if (!tagName) return
|
|
25
38
|
|
|
26
|
-
|
|
39
|
+
const lowercaseTagName = tagName.toLowerCase()
|
|
40
|
+
|
|
41
|
+
if (tagName !== lowercaseTagName) {
|
|
27
42
|
let type: string = node.type
|
|
28
43
|
|
|
29
44
|
if (node.type == "AST_HTML_OPEN_TAG_NODE") type = "Opening"
|
|
@@ -31,7 +46,7 @@ class TagNameLowercaseVisitor extends BaseRuleVisitor {
|
|
|
31
46
|
if (node.type == "AST_HTML_SELF_CLOSE_TAG_NODE") type = "Self-closing"
|
|
32
47
|
|
|
33
48
|
this.addOffense(
|
|
34
|
-
`${type} tag name \`${tagName}\` should be lowercase. Use \`${
|
|
49
|
+
`${type} tag name \`${tagName}\` should be lowercase. Use \`${lowercaseTagName}\` instead.`,
|
|
35
50
|
node.tag_name!.location,
|
|
36
51
|
"error"
|
|
37
52
|
)
|
package/src/rules/index.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
export * from "./erb-no-empty-tags.js"
|
|
2
2
|
export * from "./erb-no-output-control-flow.js"
|
|
3
3
|
export * from "./html-anchor-require-href.js"
|
|
4
|
+
export * from "./html-aria-role-heading-requires-level.js"
|
|
5
|
+
export * from "./html-aria-role-must-be-valid.js"
|
|
4
6
|
export * from "./html-attribute-double-quotes.js"
|
|
5
7
|
export * from "./html-attribute-values-require-quotes.js"
|
|
6
8
|
export * from "./html-boolean-attributes-no-value.js"
|
|
7
9
|
export * from "./html-img-require-alt.js"
|
|
8
10
|
export * from "./html-no-block-inside-inline.js"
|
|
9
11
|
export * from "./html-no-duplicate-attributes.js"
|
|
12
|
+
export * from "./html-no-duplicate-ids.js"
|
|
10
13
|
export * from "./html-no-empty-headings.js"
|
|
11
14
|
export * from "./html-no-nested-links.js"
|
|
12
15
|
export * from "./html-tag-name-lowercase.js"
|
|
16
|
+
export * from "./svg-tag-name-capitalization.js"
|