@herb-tools/linter 0.4.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 +34 -0
- package/bin/herb-lint +3 -0
- package/dist/herb-lint.js +16505 -0
- package/dist/herb-lint.js.map +1 -0
- package/dist/index.cjs +834 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +820 -0
- package/dist/index.js.map +1 -0
- package/dist/package.json +49 -0
- package/dist/src/cli/argument-parser.js +96 -0
- package/dist/src/cli/argument-parser.js.map +1 -0
- package/dist/src/cli/file-processor.js +58 -0
- package/dist/src/cli/file-processor.js.map +1 -0
- package/dist/src/cli/formatters/base-formatter.js +3 -0
- package/dist/src/cli/formatters/base-formatter.js.map +1 -0
- package/dist/src/cli/formatters/detailed-formatter.js +62 -0
- package/dist/src/cli/formatters/detailed-formatter.js.map +1 -0
- package/dist/src/cli/formatters/index.js +4 -0
- package/dist/src/cli/formatters/index.js.map +1 -0
- package/dist/src/cli/formatters/simple-formatter.js +31 -0
- package/dist/src/cli/formatters/simple-formatter.js.map +1 -0
- package/dist/src/cli/index.js +5 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/cli/summary-reporter.js +96 -0
- package/dist/src/cli/summary-reporter.js.map +1 -0
- package/dist/src/cli.js +50 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/default-rules.js +31 -0
- package/dist/src/default-rules.js.map +1 -0
- package/dist/src/herb-lint.js +5 -0
- package/dist/src/herb-lint.js.map +1 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/linter.js +39 -0
- package/dist/src/linter.js.map +1 -0
- package/dist/src/rules/erb-no-empty-tags.js +23 -0
- package/dist/src/rules/erb-no-empty-tags.js.map +1 -0
- package/dist/src/rules/erb-no-output-control-flow.js +47 -0
- package/dist/src/rules/erb-no-output-control-flow.js.map +1 -0
- package/dist/src/rules/erb-require-whitespace-inside-tags.js +43 -0
- package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -0
- package/dist/src/rules/html-anchor-require-href.js +25 -0
- package/dist/src/rules/html-anchor-require-href.js.map +1 -0
- package/dist/src/rules/html-aria-role-heading-requires-level.js +26 -0
- package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -0
- package/dist/src/rules/html-attribute-double-quotes.js +21 -0
- package/dist/src/rules/html-attribute-double-quotes.js.map +1 -0
- package/dist/src/rules/html-attribute-values-require-quotes.js +22 -0
- package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -0
- package/dist/src/rules/html-boolean-attributes-no-value.js +19 -0
- package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -0
- package/dist/src/rules/html-img-require-alt.js +29 -0
- package/dist/src/rules/html-img-require-alt.js.map +1 -0
- package/dist/src/rules/html-no-block-inside-inline.js +59 -0
- package/dist/src/rules/html-no-block-inside-inline.js.map +1 -0
- package/dist/src/rules/html-no-duplicate-attributes.js +43 -0
- package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -0
- package/dist/src/rules/html-no-empty-headings.js +148 -0
- package/dist/src/rules/html-no-empty-headings.js.map +1 -0
- package/dist/src/rules/html-no-nested-links.js +45 -0
- package/dist/src/rules/html-no-nested-links.js.map +1 -0
- package/dist/src/rules/html-tag-name-lowercase.js +39 -0
- package/dist/src/rules/html-tag-name-lowercase.js.map +1 -0
- package/dist/src/rules/index.js +13 -0
- package/dist/src/rules/index.js.map +1 -0
- package/dist/src/rules/rule-utils.js +198 -0
- package/dist/src/rules/rule-utils.js.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/cli/argument-parser.d.ts +14 -0
- package/dist/types/cli/file-processor.d.ts +21 -0
- package/dist/types/cli/formatters/base-formatter.d.ts +6 -0
- package/dist/types/cli/formatters/detailed-formatter.d.ts +13 -0
- package/dist/types/cli/formatters/index.d.ts +3 -0
- package/dist/types/cli/formatters/simple-formatter.d.ts +7 -0
- package/dist/types/cli/summary-reporter.d.ts +22 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/default-rules.d.ts +2 -0
- package/dist/types/herb-lint.d.ts +2 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/linter.d.ts +18 -0
- package/dist/types/rules/erb-no-empty-tags.d.ts +6 -0
- package/dist/types/rules/erb-no-output-control-flow.d.ts +6 -0
- package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +6 -0
- package/dist/types/rules/html-anchor-require-href.d.ts +6 -0
- package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +6 -0
- package/dist/types/rules/html-attribute-double-quotes.d.ts +6 -0
- package/dist/types/rules/html-attribute-values-require-quotes.d.ts +6 -0
- package/dist/types/rules/html-boolean-attributes-no-value.d.ts +6 -0
- package/dist/types/rules/html-img-require-alt.d.ts +6 -0
- package/dist/types/rules/html-no-block-inside-inline.d.ts +6 -0
- package/dist/types/rules/html-no-duplicate-attributes.d.ts +6 -0
- package/dist/types/rules/html-no-empty-headings.d.ts +6 -0
- package/dist/types/rules/html-no-nested-links.d.ts +6 -0
- package/dist/types/rules/html-tag-name-lowercase.d.ts +6 -0
- package/dist/types/rules/index.d.ts +12 -0
- package/dist/types/rules/rule-utils.d.ts +89 -0
- package/dist/types/src/cli/argument-parser.d.ts +14 -0
- package/dist/types/src/cli/file-processor.d.ts +21 -0
- package/dist/types/src/cli/formatters/base-formatter.d.ts +6 -0
- package/dist/types/src/cli/formatters/detailed-formatter.d.ts +13 -0
- package/dist/types/src/cli/formatters/index.d.ts +3 -0
- package/dist/types/src/cli/formatters/simple-formatter.d.ts +7 -0
- package/dist/types/src/cli/index.d.ts +4 -0
- package/dist/types/src/cli/summary-reporter.d.ts +22 -0
- package/dist/types/src/cli.d.ts +6 -0
- package/dist/types/src/default-rules.d.ts +2 -0
- package/dist/types/src/herb-lint.d.ts +2 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/linter.d.ts +18 -0
- package/dist/types/src/rules/erb-no-empty-tags.d.ts +6 -0
- package/dist/types/src/rules/erb-no-output-control-flow.d.ts +6 -0
- package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +6 -0
- package/dist/types/src/rules/html-anchor-require-href.d.ts +6 -0
- package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +6 -0
- package/dist/types/src/rules/html-attribute-double-quotes.d.ts +6 -0
- package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +6 -0
- package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +6 -0
- package/dist/types/src/rules/html-img-require-alt.d.ts +6 -0
- package/dist/types/src/rules/html-no-block-inside-inline.d.ts +6 -0
- package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +6 -0
- package/dist/types/src/rules/html-no-empty-headings.d.ts +6 -0
- package/dist/types/src/rules/html-no-nested-links.d.ts +6 -0
- package/dist/types/src/rules/html-tag-name-lowercase.d.ts +6 -0
- package/dist/types/src/rules/index.d.ts +12 -0
- package/dist/types/src/rules/rule-utils.d.ts +89 -0
- package/dist/types/src/types.d.ts +26 -0
- package/dist/types/types.d.ts +26 -0
- package/docs/rules/README.md +39 -0
- package/docs/rules/erb-no-empty-tags.md +38 -0
- package/docs/rules/erb-no-output-control-flow.md +45 -0
- package/docs/rules/erb-require-whitespace-inside-tags.md +43 -0
- package/docs/rules/html-anchor-require-href.md +32 -0
- package/docs/rules/html-aria-role-heading-requires-level.md +34 -0
- package/docs/rules/html-attribute-double-quotes.md +43 -0
- package/docs/rules/html-attribute-values-require-quotes.md +43 -0
- package/docs/rules/html-boolean-attributes-no-value.md +39 -0
- package/docs/rules/html-img-require-alt.md +44 -0
- package/docs/rules/html-no-block-inside-inline.md +66 -0
- package/docs/rules/html-no-duplicate-attributes.md +35 -0
- package/docs/rules/html-no-empty-headings.md +78 -0
- package/docs/rules/html-no-nested-links.md +44 -0
- package/docs/rules/html-tag-name-lowercase.md +44 -0
- package/package.json +49 -0
- package/src/cli/argument-parser.ts +125 -0
- package/src/cli/file-processor.ts +86 -0
- package/src/cli/formatters/base-formatter.ts +11 -0
- package/src/cli/formatters/detailed-formatter.ts +74 -0
- package/src/cli/formatters/index.ts +3 -0
- package/src/cli/formatters/simple-formatter.ts +40 -0
- package/src/cli/index.ts +4 -0
- package/src/cli/summary-reporter.ts +127 -0
- package/src/cli.ts +60 -0
- package/src/default-rules.ts +33 -0
- package/src/herb-lint.ts +6 -0
- package/src/index.ts +3 -0
- package/src/linter.ts +50 -0
- package/src/rules/erb-no-empty-tags.ts +34 -0
- package/src/rules/erb-no-output-control-flow.ts +61 -0
- package/src/rules/erb-require-whitespace-inside-tags.ts +61 -0
- package/src/rules/html-anchor-require-href.ts +39 -0
- package/src/rules/html-aria-role-heading-requires-level.ts +44 -0
- package/src/rules/html-attribute-double-quotes.ts +28 -0
- package/src/rules/html-attribute-values-require-quotes.ts +30 -0
- package/src/rules/html-boolean-attributes-no-value.ts +27 -0
- package/src/rules/html-img-require-alt.ts +42 -0
- package/src/rules/html-no-block-inside-inline.ts +84 -0
- package/src/rules/html-no-duplicate-attributes.ts +59 -0
- package/src/rules/html-no-empty-headings.ts +185 -0
- package/src/rules/html-no-nested-links.ts +65 -0
- package/src/rules/html-tag-name-lowercase.ts +50 -0
- package/src/rules/index.ts +12 -0
- package/src/rules/rule-utils.ts +257 -0
- package/src/types.ts +32 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defaultRules } from "./default-rules.js";
|
|
2
|
+
export class Linter {
|
|
3
|
+
rules;
|
|
4
|
+
offenses;
|
|
5
|
+
/**
|
|
6
|
+
* Creates a new Linter instance.
|
|
7
|
+
* @param rules - Array of rule classes (not instances) to use. If not provided, uses default rules.
|
|
8
|
+
*/
|
|
9
|
+
constructor(rules) {
|
|
10
|
+
this.rules = rules !== undefined ? rules : this.getDefaultRules();
|
|
11
|
+
this.offenses = [];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Returns the default set of rule classes used by the linter.
|
|
15
|
+
* @returns Array of rule classes
|
|
16
|
+
*/
|
|
17
|
+
getDefaultRules() {
|
|
18
|
+
return defaultRules;
|
|
19
|
+
}
|
|
20
|
+
getRuleCount() {
|
|
21
|
+
return this.rules.length;
|
|
22
|
+
}
|
|
23
|
+
lint(document) {
|
|
24
|
+
this.offenses = [];
|
|
25
|
+
for (const Rule of this.rules) {
|
|
26
|
+
const rule = new Rule();
|
|
27
|
+
const ruleOffenses = rule.check(document);
|
|
28
|
+
this.offenses.push(...ruleOffenses);
|
|
29
|
+
}
|
|
30
|
+
const errors = this.offenses.filter(offense => offense.severity === "error").length;
|
|
31
|
+
const warnings = this.offenses.filter(offense => offense.severity === "warning").length;
|
|
32
|
+
return {
|
|
33
|
+
offenses: this.offenses,
|
|
34
|
+
errors,
|
|
35
|
+
warnings
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=linter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linter.js","sourceRoot":"","sources":["../../src/linter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAKjD,MAAM,OAAO,MAAM;IACT,KAAK,CAAa;IAClB,QAAQ,CAAe;IAE/B;;;OAGG;IACH,YAAY,KAAmB;QAC7B,IAAI,CAAC,KAAK,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,EAAE,CAAA;QACjE,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAA;IACpB,CAAC;IAED;;;OAGG;IACK,eAAe;QACrB,OAAO,YAAY,CAAA;IACrB,CAAC;IAED,YAAY;QACV,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAA;IAC1B,CAAC;IAED,IAAI,CAAC,QAAsB;QACzB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAA;QAElB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAA;YACvB,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;YAEzC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,CAAA;QACrC,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM,CAAA;QACnF,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,MAAM,CAAA;QAEvF,OAAO;YACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,MAAM;YACN,QAAQ;SACT,CAAA;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BaseRuleVisitor } from "./rule-utils.js";
|
|
2
|
+
class ERBNoEmptyTagsVisitor extends BaseRuleVisitor {
|
|
3
|
+
visitERBContentNode(node) {
|
|
4
|
+
this.visitChildNodes(node);
|
|
5
|
+
const { content, tag_closing } = node;
|
|
6
|
+
if (!content)
|
|
7
|
+
return;
|
|
8
|
+
if (tag_closing?.value === "")
|
|
9
|
+
return;
|
|
10
|
+
if (content.value.trim().length > 0)
|
|
11
|
+
return;
|
|
12
|
+
this.addOffense("ERB tag should not be empty. Remove empty ERB tags or add content.", node.location, "error");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class ERBNoEmptyTagsRule {
|
|
16
|
+
name = "erb-no-empty-tags";
|
|
17
|
+
check(node) {
|
|
18
|
+
const visitor = new ERBNoEmptyTagsVisitor(this.name);
|
|
19
|
+
visitor.visit(node);
|
|
20
|
+
return visitor.offenses;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=erb-no-empty-tags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erb-no-empty-tags.js","sourceRoot":"","sources":["../../../src/rules/erb-no-empty-tags.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAKjD,MAAM,qBAAsB,SAAQ,eAAe;IACjD,mBAAmB,CAAC,IAAoB;QACtC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;QAE1B,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI,CAAA;QAErC,IAAI,CAAC,OAAO;YAAE,OAAM;QACpB,IAAI,WAAW,EAAE,KAAK,KAAK,EAAE;YAAE,OAAM;QACrC,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;YAAE,OAAM;QAE3C,IAAI,CAAC,UAAU,CACb,oEAAoE,EACpE,IAAI,CAAC,QAAQ,EACb,OAAO,CACR,CAAA;IACH,CAAC;CACF;AAED,MAAM,OAAO,kBAAkB;IAC7B,IAAI,GAAG,mBAAmB,CAAA;IAE1B,KAAK,CAAC,IAAU;QACd,MAAM,OAAO,GAAG,IAAI,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAEpD,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAEnB,OAAO,OAAO,CAAC,QAAQ,CAAA;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BaseRuleVisitor } from "./rule-utils.js";
|
|
2
|
+
class ERBNoOutputControlFlowRuleVisitor extends BaseRuleVisitor {
|
|
3
|
+
visitERBIfNode(node) {
|
|
4
|
+
this.checkOutputControlFlow(node);
|
|
5
|
+
this.visitChildNodes(node);
|
|
6
|
+
}
|
|
7
|
+
visitERBUnlessNode(node) {
|
|
8
|
+
this.checkOutputControlFlow(node);
|
|
9
|
+
this.visitChildNodes(node);
|
|
10
|
+
}
|
|
11
|
+
visitERBElseNode(node) {
|
|
12
|
+
this.checkOutputControlFlow(node);
|
|
13
|
+
this.visitChildNodes(node);
|
|
14
|
+
}
|
|
15
|
+
visitERBEndNode(node) {
|
|
16
|
+
this.checkOutputControlFlow(node);
|
|
17
|
+
this.visitChildNodes(node);
|
|
18
|
+
}
|
|
19
|
+
checkOutputControlFlow(controlBlock) {
|
|
20
|
+
const openTag = controlBlock.tag_opening;
|
|
21
|
+
if (!openTag) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (openTag.value === "<%=") {
|
|
25
|
+
let controlBlockType = controlBlock.type;
|
|
26
|
+
if (controlBlock.type === "AST_ERB_IF_NODE")
|
|
27
|
+
controlBlockType = "if";
|
|
28
|
+
if (controlBlock.type === "AST_ERB_ELSE_NODE")
|
|
29
|
+
controlBlockType = "else";
|
|
30
|
+
if (controlBlock.type === "AST_ERB_END_NODE")
|
|
31
|
+
controlBlockType = "end";
|
|
32
|
+
if (controlBlock.type === "AST_ERB_UNLESS_NODE")
|
|
33
|
+
controlBlockType = "unless";
|
|
34
|
+
this.addOffense(`Control flow statements like \`${controlBlockType}\` should not be used with output tags. Use \`<% ${controlBlockType} ... %>\` instead.`, openTag.location, "error");
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class ERBNoOutputControlFlowRule {
|
|
40
|
+
name = "erb-no-output-control-flow";
|
|
41
|
+
check(node) {
|
|
42
|
+
const visitor = new ERBNoOutputControlFlowRuleVisitor(this.name);
|
|
43
|
+
visitor.visit(node);
|
|
44
|
+
return visitor.offenses;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=erb-no-output-control-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erb-no-output-control-flow.js","sourceRoot":"","sources":["../../../src/rules/erb-no-output-control-flow.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAKjD,MAAM,iCAAkC,SAAQ,eAAe;IAC7D,cAAc,CAAC,IAAe;QAC5B,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAA;QACjC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;IAC5B,CAAC;IAED,kBAAkB,CAAC,IAAmB;QACpC,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAA;QACjC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;IAC5B,CAAC;IAED,gBAAgB,CAAC,IAAiB;QAChC,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAA;QACjC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;IAC5B,CAAC;IAED,eAAe,CAAC,IAAgB;QAC9B,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAA;QACjC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;IAC5B,CAAC;IAEO,sBAAsB,CAAC,YAAkE;QAC/F,MAAM,OAAO,GAAG,YAAY,CAAC,WAAW,CAAC;QACzC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAM;QACR,CAAC;QAED,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,EAAC,CAAC;YAC3B,IAAI,gBAAgB,GAAW,YAAY,CAAC,IAAI,CAAA;YAEhD,IAAI,YAAY,CAAC,IAAI,KAAK,iBAAiB;gBAAE,gBAAgB,GAAG,IAAI,CAAA;YACpE,IAAI,YAAY,CAAC,IAAI,KAAK,mBAAmB;gBAAE,gBAAgB,GAAG,MAAM,CAAA;YACxE,IAAI,YAAY,CAAC,IAAI,KAAK,kBAAkB;gBAAE,gBAAgB,GAAG,KAAK,CAAA;YACtE,IAAI,YAAY,CAAC,IAAI,KAAK,qBAAqB;gBAAE,gBAAgB,GAAG,QAAQ,CAAA;YAE5E,IAAI,CAAC,UAAU,CACb,kCAAkC,gBAAgB,oDAAoD,gBAAgB,oBAAoB,EAC1I,OAAO,CAAC,QAAQ,EAChB,OAAO,CACR,CAAA;QACH,CAAC;QAED,OAAM;IACR,CAAC;CACF;AAED,MAAM,OAAO,0BAA0B;IACrC,IAAI,GAAG,4BAA4B,CAAA;IACnC,KAAK,CAAC,IAAU;QACd,MAAM,OAAO,GAAG,IAAI,iCAAiC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAEhE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAEnB,OAAO,OAAO,CAAC,QAAQ,CAAA;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isERBNode } from "@herb-tools/core";
|
|
2
|
+
import { BaseRuleVisitor } from "./rule-utils.js";
|
|
3
|
+
class RequireWhitespaceInsideTags extends BaseRuleVisitor {
|
|
4
|
+
visitChildNodes(node) {
|
|
5
|
+
this.checkWhitespace(node);
|
|
6
|
+
super.visitChildNodes(node);
|
|
7
|
+
}
|
|
8
|
+
checkWhitespace(node) {
|
|
9
|
+
if (!isERBNode(node)) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const openTag = node.tag_opening;
|
|
13
|
+
const closeTag = node.tag_closing;
|
|
14
|
+
const content = node.content;
|
|
15
|
+
if (!openTag || !closeTag || !content) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const value = content.value;
|
|
19
|
+
this.checkOpenTagWhitespace(openTag, value);
|
|
20
|
+
this.checkCloseTagWhitespace(closeTag, value);
|
|
21
|
+
}
|
|
22
|
+
checkOpenTagWhitespace(openTag, content) {
|
|
23
|
+
if (content.startsWith(" ") || content.startsWith("\n")) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.addOffense(`Add whitespace after \`${openTag.value}\`.`, openTag.location, "error");
|
|
27
|
+
}
|
|
28
|
+
checkCloseTagWhitespace(closeTag, content) {
|
|
29
|
+
if (content.endsWith(" ") || content.endsWith("\n")) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.addOffense(`Add whitespace before \`${closeTag.value}\`.`, closeTag.location, "error");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class ERBRequireWhitespaceRule {
|
|
36
|
+
name = "erb-require-whitespace-inside-tags";
|
|
37
|
+
check(node) {
|
|
38
|
+
const visitor = new RequireWhitespaceInsideTags(this.name);
|
|
39
|
+
visitor.visit(node);
|
|
40
|
+
return visitor.offenses;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=erb-require-whitespace-inside-tags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erb-require-whitespace-inside-tags.js","sourceRoot":"","sources":["../../../src/rules/erb-require-whitespace-inside-tags.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEjD,MAAM,2BAA4B,SAAQ,eAAe;IAEvD,eAAe,CAAC,IAAU;QACxB,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;QAC1B,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;IAC7B,CAAC;IAEO,eAAe,CAAC,IAAU;QAChC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;YACrB,OAAM;QACR,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAA;QAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAA;QACjC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;QAE5B,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ,IAAI,CAAC,OAAO,EAAE,CAAC;YACtC,OAAM;QACR,CAAC;QAED,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAA;QAE3B,IAAI,CAAC,sBAAsB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;QAC3C,IAAI,CAAC,uBAAuB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;IAC/C,CAAC;IAEO,sBAAsB,CAAC,OAAc,EAAE,OAAc;QAC3D,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACxD,OAAM;QACR,CAAC;QACD,IAAI,CAAC,UAAU,CACb,0BAA0B,OAAO,CAAC,KAAK,KAAK,EAC5C,OAAO,CAAC,QAAQ,EAChB,OAAO,CACR,CAAA;IACH,CAAC;IAEO,uBAAuB,CAAC,QAAe,EAAE,OAAc;QAC7D,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACpD,OAAM;QACR,CAAC;QACD,IAAI,CAAC,UAAU,CACb,2BAA2B,QAAQ,CAAC,KAAK,KAAK,EAC9C,QAAQ,CAAC,QAAQ,EACjB,OAAO,CACR,CAAA;IACH,CAAC;CACF;AAED,MAAM,OAAO,wBAAwB;IACnC,IAAI,GAAG,oCAAoC,CAAA;IAC3C,KAAK,CAAC,IAAU;QACd,MAAM,OAAO,GAAG,IAAI,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC1D,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACnB,OAAO,OAAO,CAAC,QAAQ,CAAA;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BaseRuleVisitor, getTagName, hasAttribute } from "./rule-utils.js";
|
|
2
|
+
class AnchorRechireHrefVisitor extends BaseRuleVisitor {
|
|
3
|
+
visitHTMLOpenTagNode(node) {
|
|
4
|
+
this.checkATag(node);
|
|
5
|
+
super.visitHTMLOpenTagNode(node);
|
|
6
|
+
}
|
|
7
|
+
checkATag(node) {
|
|
8
|
+
const tagName = getTagName(node);
|
|
9
|
+
if (tagName !== "a") {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (!hasAttribute(node, "href")) {
|
|
13
|
+
this.addOffense("Add an `href` attribute to `<a>` to ensure it is focusable and accessible.", node.tag_name.location, "error");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class HTMLAnchorRequireHrefRule {
|
|
18
|
+
name = "html-anchor-require-href";
|
|
19
|
+
check(node) {
|
|
20
|
+
const visitor = new AnchorRechireHrefVisitor(this.name);
|
|
21
|
+
visitor.visit(node);
|
|
22
|
+
return visitor.offenses;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=html-anchor-require-href.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-anchor-require-href.js","sourceRoot":"","sources":["../../../src/rules/html-anchor-require-href.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAK3E,MAAM,wBAAyB,SAAQ,eAAe;IACpD,oBAAoB,CAAC,IAAqB;QACxC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;QACpB,KAAK,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAA;IAClC,CAAC;IAEO,SAAS,CAAC,IAAqB;QACrC,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;QAEhC,IAAI,OAAO,KAAK,GAAG,EAAE,CAAC;YACpB,OAAM;QACR,CAAC;QAED,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;YAChC,IAAI,CAAC,UAAU,CACb,4EAA4E,EAC5E,IAAI,CAAC,QAAS,CAAC,QAAQ,EACvB,OAAO,CACR,CAAA;QACH,CAAC;IACH,CAAC;CACF;AAED,MAAM,OAAO,yBAAyB;IACpC,IAAI,GAAG,0BAA0B,CAAA;IAEjC,KAAK,CAAC,IAAU;QACd,MAAM,OAAO,GAAG,IAAI,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAEvD,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAEnB,OAAO,OAAO,CAAC,QAAQ,CAAA;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AttributeVisitorMixin, getAttributeName, getAttributes } from "./rule-utils.js";
|
|
2
|
+
class AriaRoleHeadingRequiresLevel extends AttributeVisitorMixin {
|
|
3
|
+
// We want to check 2 attributes here:
|
|
4
|
+
// 1. role="heading"
|
|
5
|
+
// 2. aria-level (which must be present if role="heading")
|
|
6
|
+
checkAttribute(attributeName, attributeValue, attributeNode, parentNode) {
|
|
7
|
+
if (!(attributeName === "role" && attributeValue === "heading")) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const allAttributes = getAttributes(parentNode);
|
|
11
|
+
// If we have a role="heading", we must check for aria-level
|
|
12
|
+
const ariaLevelAttr = allAttributes.find(attr => getAttributeName(attr) === "aria-level");
|
|
13
|
+
if (!ariaLevelAttr) {
|
|
14
|
+
this.addOffense(`Element with \`role="heading"\` must have an \`aria-level\` attribute.`, attributeNode.location, "error");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class HTMLAriaRoleHeadingRequiresLevelRule {
|
|
19
|
+
name = "html-aria-role-heading-requires-level";
|
|
20
|
+
check(node) {
|
|
21
|
+
const visitor = new AriaRoleHeadingRequiresLevel(this.name);
|
|
22
|
+
visitor.visit(node);
|
|
23
|
+
return visitor.offenses;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=html-aria-role-heading-requires-level.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-aria-role-heading-requires-level.js","sourceRoot":"","sources":["../../../src/rules/html-aria-role-heading-requires-level.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAKxF,MAAM,4BAA6B,SAAQ,qBAAqB;IAE9D,sCAAsC;IACtC,oBAAoB;IACpB,0DAA0D;IAC1D,cAAc,CACZ,aAAqB,EACrB,cAA6B,EAC7B,aAAgC,EAChC,UAAkD;QAGlD,IAAI,CAAC,CAAC,aAAa,KAAK,MAAM,IAAI,cAAc,KAAK,SAAS,CAAC,EAAE,CAAC;YAChE,OAAM;QACR,CAAC;QAED,MAAM,aAAa,GAAG,aAAa,CAAC,UAAU,CAAC,CAAA;QAE/C,4DAA4D;QAC5D,MAAM,aAAa,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,YAAY,CAAC,CAAA;QACzF,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,IAAI,CAAC,UAAU,CACb,wEAAwE,EACxE,aAAa,CAAC,QAAQ,EACtB,OAAO,CACR,CAAA;QACH,CAAC;IACH,CAAC;CACF;AAED,MAAM,OAAO,oCAAoC;IAC/C,IAAI,GAAG,uCAAuC,CAAA;IAE9C,KAAK,CAAC,IAAU;QACd,MAAM,OAAO,GAAG,IAAI,4BAA4B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC3D,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACnB,OAAO,OAAO,CAAC,QAAQ,CAAA;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AttributeVisitorMixin, getAttributeValueQuoteType, hasAttributeValue } from "./rule-utils.js";
|
|
2
|
+
class AttributeDoubleQuotesVisitor extends AttributeVisitorMixin {
|
|
3
|
+
checkAttribute(attributeName, attributeValue, attributeNode) {
|
|
4
|
+
if (!hasAttributeValue(attributeNode))
|
|
5
|
+
return;
|
|
6
|
+
if (getAttributeValueQuoteType(attributeNode) !== "single")
|
|
7
|
+
return;
|
|
8
|
+
if (attributeValue?.includes('"'))
|
|
9
|
+
return; // Single quotes acceptable when value contains double quotes
|
|
10
|
+
this.addOffense(`Attribute \`${attributeName}\` uses single quotes. Prefer double quotes for HTML attribute values: \`${attributeName}="value"\`.`, attributeNode.value.location, "warning");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class HTMLAttributeDoubleQuotesRule {
|
|
14
|
+
name = "html-attribute-double-quotes";
|
|
15
|
+
check(node) {
|
|
16
|
+
const visitor = new AttributeDoubleQuotesVisitor(this.name);
|
|
17
|
+
visitor.visit(node);
|
|
18
|
+
return visitor.offenses;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=html-attribute-double-quotes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-attribute-double-quotes.js","sourceRoot":"","sources":["../../../src/rules/html-attribute-double-quotes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAKtG,MAAM,4BAA6B,SAAQ,qBAAqB;IACpD,cAAc,CAAC,aAAqB,EAAE,cAA6B,EAAE,aAAgC;QAC7G,IAAI,CAAC,iBAAiB,CAAC,aAAa,CAAC;YAAE,OAAM;QAC7C,IAAI,0BAA0B,CAAC,aAAa,CAAC,KAAK,QAAQ;YAAE,OAAM;QAClE,IAAI,cAAc,EAAE,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAM,CAAC,6DAA6D;QAEvG,IAAI,CAAC,UAAU,CACb,eAAe,aAAa,4EAA4E,aAAa,aAAa,EAClI,aAAa,CAAC,KAAM,CAAC,QAAQ,EAC7B,SAAS,CACV,CAAA;IACH,CAAC;CACF;AAED,MAAM,OAAO,6BAA6B;IACxC,IAAI,GAAG,8BAA8B,CAAA;IAErC,KAAK,CAAC,IAAU;QACd,MAAM,OAAO,GAAG,IAAI,4BAA4B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC3D,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACnB,OAAO,OAAO,CAAC,QAAQ,CAAA;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AttributeVisitorMixin } from "./rule-utils.js";
|
|
2
|
+
class AttributeValuesRequireQuotesVisitor extends AttributeVisitorMixin {
|
|
3
|
+
checkAttribute(attributeName, _attributeValue, attributeNode) {
|
|
4
|
+
if (attributeNode.value?.type !== "AST_HTML_ATTRIBUTE_VALUE_NODE")
|
|
5
|
+
return;
|
|
6
|
+
const valueNode = attributeNode.value;
|
|
7
|
+
if (valueNode.quoted)
|
|
8
|
+
return;
|
|
9
|
+
this.addOffense(
|
|
10
|
+
// TODO: print actual attribute value in message
|
|
11
|
+
`Attribute value should be quoted: \`${attributeName}="value"\`. Always wrap attribute values in quotes.`, valueNode.location, "error");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class HTMLAttributeValuesRequireQuotesRule {
|
|
15
|
+
name = "html-attribute-values-require-quotes";
|
|
16
|
+
check(node) {
|
|
17
|
+
const visitor = new AttributeValuesRequireQuotesVisitor(this.name);
|
|
18
|
+
visitor.visit(node);
|
|
19
|
+
return visitor.offenses;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=html-attribute-values-require-quotes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-attribute-values-require-quotes.js","sourceRoot":"","sources":["../../../src/rules/html-attribute-values-require-quotes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AAKvD,MAAM,mCAAoC,SAAQ,qBAAqB;IAC3D,cAAc,CAAC,aAAqB,EAAE,eAA8B,EAAE,aAAgC;QAC9G,IAAI,aAAa,CAAC,KAAK,EAAE,IAAI,KAAK,+BAA+B;YAAE,OAAM;QAEzE,MAAM,SAAS,GAAG,aAAa,CAAC,KAA+B,CAAA;QAC/D,IAAI,SAAS,CAAC,MAAM;YAAE,OAAM;QAE5B,IAAI,CAAC,UAAU;QACb,gDAAgD;QAChD,uCAAuC,aAAa,qDAAqD,EACzG,SAAS,CAAC,QAAQ,EAClB,OAAO,CACR,CAAA;IACH,CAAC;CACF;AAED,MAAM,OAAO,oCAAoC;IAC/C,IAAI,GAAG,sCAAsC,CAAA;IAE7C,KAAK,CAAC,IAAU;QACd,MAAM,OAAO,GAAG,IAAI,mCAAmC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAClE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACnB,OAAO,OAAO,CAAC,QAAQ,CAAA;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AttributeVisitorMixin, isBooleanAttribute, hasAttributeValue } from "./rule-utils.js";
|
|
2
|
+
class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
|
|
3
|
+
checkAttribute(attributeName, _attributeValue, attributeNode) {
|
|
4
|
+
if (!isBooleanAttribute(attributeName))
|
|
5
|
+
return;
|
|
6
|
+
if (!hasAttributeValue(attributeNode))
|
|
7
|
+
return;
|
|
8
|
+
this.addOffense(`Boolean attribute \`${attributeName}\` should not have a value. Use \`${attributeName}\` instead of \`${attributeName}="${attributeName}"\`.`, attributeNode.value.location, "error");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class HTMLBooleanAttributesNoValueRule {
|
|
12
|
+
name = "html-boolean-attributes-no-value";
|
|
13
|
+
check(node) {
|
|
14
|
+
const visitor = new BooleanAttributesNoValueVisitor(this.name);
|
|
15
|
+
visitor.visit(node);
|
|
16
|
+
return visitor.offenses;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=html-boolean-attributes-no-value.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-boolean-attributes-no-value.js","sourceRoot":"","sources":["../../../src/rules/html-boolean-attributes-no-value.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAK9F,MAAM,+BAAgC,SAAQ,qBAAqB;IACvD,cAAc,CAAC,aAAqB,EAAE,eAA8B,EAAE,aAAgC;QAC9G,IAAI,CAAC,kBAAkB,CAAC,aAAa,CAAC;YAAE,OAAM;QAC9C,IAAI,CAAC,iBAAiB,CAAC,aAAa,CAAC;YAAE,OAAM;QAE7C,IAAI,CAAC,UAAU,CACb,uBAAuB,aAAa,qCAAqC,aAAa,mBAAmB,aAAa,KAAK,aAAa,MAAM,EAC9I,aAAa,CAAC,KAAM,CAAC,QAAQ,EAC7B,OAAO,CACR,CAAA;IACH,CAAC;CACF;AAED,MAAM,OAAO,gCAAgC;IAC3C,IAAI,GAAG,kCAAkC,CAAA;IAEzC,KAAK,CAAC,IAAU;QACd,MAAM,OAAO,GAAG,IAAI,+BAA+B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9D,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACnB,OAAO,OAAO,CAAC,QAAQ,CAAA;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseRuleVisitor, getTagName, hasAttribute } from "./rule-utils.js";
|
|
2
|
+
class ImgRequireAltVisitor extends BaseRuleVisitor {
|
|
3
|
+
visitHTMLOpenTagNode(node) {
|
|
4
|
+
this.checkImgTag(node);
|
|
5
|
+
super.visitHTMLOpenTagNode(node);
|
|
6
|
+
}
|
|
7
|
+
visitHTMLSelfCloseTagNode(node) {
|
|
8
|
+
this.checkImgTag(node);
|
|
9
|
+
super.visitHTMLSelfCloseTagNode(node);
|
|
10
|
+
}
|
|
11
|
+
checkImgTag(node) {
|
|
12
|
+
const tagName = getTagName(node);
|
|
13
|
+
if (tagName !== "img") {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (!hasAttribute(node, "alt")) {
|
|
17
|
+
this.addOffense('Missing required `alt` attribute on `<img>` tag. Add `alt=""` for decorative images or `alt="description"` for informative images.', node.tag_name.location, "error");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class HTMLImgRequireAltRule {
|
|
22
|
+
name = "html-img-require-alt";
|
|
23
|
+
check(node) {
|
|
24
|
+
const visitor = new ImgRequireAltVisitor(this.name);
|
|
25
|
+
visitor.visit(node);
|
|
26
|
+
return visitor.offenses;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=html-img-require-alt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-img-require-alt.js","sourceRoot":"","sources":["../../../src/rules/html-img-require-alt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAK3E,MAAM,oBAAqB,SAAQ,eAAe;IAChD,oBAAoB,CAAC,IAAqB;QACxC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;QACtB,KAAK,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAA;IAClC,CAAC;IAED,yBAAyB,CAAC,IAA0B;QAClD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;QACtB,KAAK,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAA;IACvC,CAAC;IAEO,WAAW,CAAC,IAA4C;QAC9D,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;QAEhC,IAAI,OAAO,KAAK,KAAK,EAAE,CAAC;YACtB,OAAM;QACR,CAAC;QAED,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC,UAAU,CACb,oIAAoI,EACpI,IAAI,CAAC,QAAS,CAAC,QAAQ,EACvB,OAAO,CACR,CAAA;QACH,CAAC;IACH,CAAC;CACF;AAED,MAAM,OAAO,qBAAqB;IAChC,IAAI,GAAG,sBAAsB,CAAA;IAE7B,KAAK,CAAC,IAAU;QACd,MAAM,OAAO,GAAG,IAAI,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACnD,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACnB,OAAO,OAAO,CAAC,QAAQ,CAAA;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { BaseRuleVisitor, isInlineElement, isBlockElement } from "./rule-utils.js";
|
|
2
|
+
class BlockInsideInlineVisitor extends BaseRuleVisitor {
|
|
3
|
+
inlineStack = [];
|
|
4
|
+
isValidHTMLOpenTag(node) {
|
|
5
|
+
return !!(node.open_tag && node.open_tag.type === "AST_HTML_OPEN_TAG_NODE");
|
|
6
|
+
}
|
|
7
|
+
getElementType(tagName) {
|
|
8
|
+
const isInline = isInlineElement(tagName);
|
|
9
|
+
const isBlock = isBlockElement(tagName);
|
|
10
|
+
const isUnknown = !isInline && !isBlock;
|
|
11
|
+
return { isInline, isBlock, isUnknown };
|
|
12
|
+
}
|
|
13
|
+
addViolationMessage(tagName, isBlock, openTag) {
|
|
14
|
+
const parentInline = this.inlineStack[this.inlineStack.length - 1];
|
|
15
|
+
const elementType = isBlock ? "Block-level" : "Unknown";
|
|
16
|
+
this.addOffense(`${elementType} element \`<${tagName}>\` cannot be placed inside inline element \`<${parentInline}>\`.`, openTag.tag_name.location, "error");
|
|
17
|
+
}
|
|
18
|
+
visitInlineElement(node, tagName) {
|
|
19
|
+
this.inlineStack.push(tagName);
|
|
20
|
+
super.visitHTMLElementNode(node);
|
|
21
|
+
this.inlineStack.pop();
|
|
22
|
+
}
|
|
23
|
+
visitBlockElement(node) {
|
|
24
|
+
const savedStack = [...this.inlineStack];
|
|
25
|
+
this.inlineStack = [];
|
|
26
|
+
super.visitHTMLElementNode(node);
|
|
27
|
+
this.inlineStack = savedStack;
|
|
28
|
+
}
|
|
29
|
+
visitHTMLElementNode(node) {
|
|
30
|
+
if (!this.isValidHTMLOpenTag(node)) {
|
|
31
|
+
super.visitHTMLElementNode(node);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const openTag = node.open_tag;
|
|
35
|
+
const tagName = openTag.tag_name?.value.toLowerCase();
|
|
36
|
+
if (!tagName) {
|
|
37
|
+
super.visitHTMLElementNode(node);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const { isInline, isBlock, isUnknown } = this.getElementType(tagName);
|
|
41
|
+
if ((isBlock || isUnknown) && this.inlineStack.length > 0) {
|
|
42
|
+
this.addViolationMessage(tagName, isBlock, openTag);
|
|
43
|
+
}
|
|
44
|
+
if (isInline) {
|
|
45
|
+
this.visitInlineElement(node, tagName);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.visitBlockElement(node);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export class HTMLNoBlockInsideInlineRule {
|
|
52
|
+
name = "html-no-block-inside-inline";
|
|
53
|
+
check(node) {
|
|
54
|
+
const visitor = new BlockInsideInlineVisitor(this.name);
|
|
55
|
+
visitor.visit(node);
|
|
56
|
+
return visitor.offenses;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=html-no-block-inside-inline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-no-block-inside-inline.js","sourceRoot":"","sources":["../../../src/rules/html-no-block-inside-inline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BaseRuleVisitor, forEachAttribute } from "./rule-utils.js";
|
|
2
|
+
class NoDuplicateAttributesVisitor extends BaseRuleVisitor {
|
|
3
|
+
visitHTMLOpenTagNode(node) {
|
|
4
|
+
this.checkDuplicateAttributes(node);
|
|
5
|
+
super.visitHTMLOpenTagNode(node);
|
|
6
|
+
}
|
|
7
|
+
visitHTMLSelfCloseTagNode(node) {
|
|
8
|
+
this.checkDuplicateAttributes(node);
|
|
9
|
+
super.visitHTMLSelfCloseTagNode(node);
|
|
10
|
+
}
|
|
11
|
+
checkDuplicateAttributes(node) {
|
|
12
|
+
const attributeNames = new Map();
|
|
13
|
+
forEachAttribute(node, (attributeNode) => {
|
|
14
|
+
if (attributeNode.name?.type !== "AST_HTML_ATTRIBUTE_NAME_NODE")
|
|
15
|
+
return;
|
|
16
|
+
const nameNode = attributeNode.name;
|
|
17
|
+
if (!nameNode.name)
|
|
18
|
+
return;
|
|
19
|
+
const attributeName = nameNode.name.value.toLowerCase(); // HTML attributes are case-insensitive
|
|
20
|
+
if (!attributeNames.has(attributeName)) {
|
|
21
|
+
attributeNames.set(attributeName, []);
|
|
22
|
+
}
|
|
23
|
+
attributeNames.get(attributeName).push(nameNode);
|
|
24
|
+
});
|
|
25
|
+
for (const [attributeName, nameNodes] of attributeNames) {
|
|
26
|
+
if (nameNodes.length > 1) {
|
|
27
|
+
for (let i = 1; i < nameNodes.length; i++) {
|
|
28
|
+
const nameNode = nameNodes[i];
|
|
29
|
+
this.addOffense(`Duplicate attribute \`${attributeName}\` found on tag. Remove the duplicate occurrence.`, nameNode.location, "error");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class HTMLNoDuplicateAttributesRule {
|
|
36
|
+
name = "html-no-duplicate-attributes";
|
|
37
|
+
check(node) {
|
|
38
|
+
const visitor = new NoDuplicateAttributesVisitor(this.name);
|
|
39
|
+
visitor.visit(node);
|
|
40
|
+
return visitor.offenses;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=html-no-duplicate-attributes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-no-duplicate-attributes.js","sourceRoot":"","sources":["../../../src/rules/html-no-duplicate-attributes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAKnE,MAAM,4BAA6B,SAAQ,eAAe;IACxD,oBAAoB,CAAC,IAAqB;QACxC,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,CAAA;QACnC,KAAK,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAA;IAClC,CAAC;IAED,yBAAyB,CAAC,IAA0B;QAClD,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,CAAA;QACnC,KAAK,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAA;IACvC,CAAC;IAEO,wBAAwB,CAAC,IAA4C;QAC3E,MAAM,cAAc,GAAG,IAAI,GAAG,EAAmC,CAAA;QAEjE,gBAAgB,CAAC,IAAI,EAAE,CAAC,aAAa,EAAE,EAAE;YACvC,IAAI,aAAa,CAAC,IAAI,EAAE,IAAI,KAAK,8BAA8B;gBAAE,OAAM;YAEvE,MAAM,QAAQ,GAAG,aAAa,CAAC,IAA6B,CAAA;YAC5D,IAAI,CAAC,QAAQ,CAAC,IAAI;gBAAE,OAAM;YAE1B,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAA,CAAC,uCAAuC;YAE/F,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;gBACvC,cAAc,CAAC,GAAG,CAAC,aAAa,EAAE,EAAE,CAAC,CAAA;YACvC,CAAC;YAED,cAAc,CAAC,GAAG,CAAC,aAAa,CAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACnD,CAAC,CAAC,CAAA;QAEF,KAAK,MAAM,CAAC,aAAa,EAAE,SAAS,CAAC,IAAI,cAAc,EAAE,CAAC;YACxD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;oBAC1C,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,CAAA;oBAE7B,IAAI,CAAC,UAAU,CACb,yBAAyB,aAAa,mDAAmD,EACzF,QAAQ,CAAC,QAAQ,EACjB,OAAO,CACR,CAAA;gBACH,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;CACF;AAED,MAAM,OAAO,6BAA6B;IACxC,IAAI,GAAG,8BAA8B,CAAA;IAErC,KAAK,CAAC,IAAU;QACd,MAAM,OAAO,GAAG,IAAI,4BAA4B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC3D,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACnB,OAAO,OAAO,CAAC,QAAQ,CAAA;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { BaseRuleVisitor, getTagName, getAttributes, findAttributeByName, getAttributeValue, HEADING_TAGS } from "./rule-utils.js";
|
|
2
|
+
class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
3
|
+
visitHTMLElementNode(node) {
|
|
4
|
+
this.checkHeadingElement(node);
|
|
5
|
+
super.visitHTMLElementNode(node);
|
|
6
|
+
}
|
|
7
|
+
visitHTMLSelfCloseTagNode(node) {
|
|
8
|
+
this.checkSelfClosingHeading(node);
|
|
9
|
+
super.visitHTMLSelfCloseTagNode(node);
|
|
10
|
+
}
|
|
11
|
+
checkHeadingElement(node) {
|
|
12
|
+
if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const openTag = node.open_tag;
|
|
16
|
+
const tagName = getTagName(openTag);
|
|
17
|
+
if (!tagName) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const isStandardHeading = HEADING_TAGS.has(tagName);
|
|
21
|
+
const isAriaHeading = this.hasHeadingRole(openTag);
|
|
22
|
+
if (!isStandardHeading && !isAriaHeading) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (this.isEmptyHeading(node)) {
|
|
26
|
+
const elementDescription = isStandardHeading
|
|
27
|
+
? `\`<${tagName}>\``
|
|
28
|
+
: `\`<${tagName} role="heading">\``;
|
|
29
|
+
this.addOffense(`Heading element ${elementDescription} must not be empty. Provide accessible text content for screen readers and SEO.`, node.location, "error");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
checkSelfClosingHeading(node) {
|
|
33
|
+
const tagName = getTagName(node);
|
|
34
|
+
if (!tagName) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Check if it's a standard heading tag (h1-h6) or has role="heading"
|
|
38
|
+
const isStandardHeading = HEADING_TAGS.has(tagName);
|
|
39
|
+
const isAriaHeading = this.hasHeadingRole(node);
|
|
40
|
+
if (!isStandardHeading && !isAriaHeading) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Self-closing headings are always empty
|
|
44
|
+
const elementDescription = isStandardHeading
|
|
45
|
+
? `\`<${tagName}>\``
|
|
46
|
+
: `\`<${tagName} role="heading">\``;
|
|
47
|
+
this.addOffense(`Heading element ${elementDescription} must not be empty. Provide accessible text content for screen readers and SEO.`, node.tag_name.location, "error");
|
|
48
|
+
}
|
|
49
|
+
isEmptyHeading(node) {
|
|
50
|
+
if (!node.body || node.body.length === 0) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
// Check if all content is just whitespace or inaccessible
|
|
54
|
+
let hasAccessibleContent = false;
|
|
55
|
+
for (const child of node.body) {
|
|
56
|
+
if (child.type === "AST_LITERAL_NODE") {
|
|
57
|
+
const literalNode = child;
|
|
58
|
+
if (literalNode.content.trim().length > 0) {
|
|
59
|
+
hasAccessibleContent = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (child.type === "AST_HTML_TEXT_NODE") {
|
|
64
|
+
const textNode = child;
|
|
65
|
+
if (textNode.content.trim().length > 0) {
|
|
66
|
+
hasAccessibleContent = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (child.type === "AST_HTML_ELEMENT_NODE") {
|
|
71
|
+
const elementNode = child;
|
|
72
|
+
// Check if this element is accessible (not aria-hidden="true")
|
|
73
|
+
if (this.isElementAccessible(elementNode)) {
|
|
74
|
+
hasAccessibleContent = true;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// If there's any non-literal/non-text/non-element content (like ERB), consider it accessible
|
|
80
|
+
hasAccessibleContent = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return !hasAccessibleContent;
|
|
85
|
+
}
|
|
86
|
+
hasHeadingRole(node) {
|
|
87
|
+
const attributes = getAttributes(node);
|
|
88
|
+
const roleAttribute = findAttributeByName(attributes, "role");
|
|
89
|
+
if (!roleAttribute) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const roleValue = getAttributeValue(roleAttribute);
|
|
93
|
+
return roleValue === "heading";
|
|
94
|
+
}
|
|
95
|
+
isElementAccessible(node) {
|
|
96
|
+
// Check if the element has aria-hidden="true"
|
|
97
|
+
if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
const openTag = node.open_tag;
|
|
101
|
+
const attributes = getAttributes(openTag);
|
|
102
|
+
const ariaHiddenAttribute = findAttributeByName(attributes, "aria-hidden");
|
|
103
|
+
if (ariaHiddenAttribute) {
|
|
104
|
+
const ariaHiddenValue = getAttributeValue(ariaHiddenAttribute);
|
|
105
|
+
if (ariaHiddenValue === "true") {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Recursively check if the element has any accessible content
|
|
110
|
+
if (!node.body || node.body.length === 0) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
for (const child of node.body) {
|
|
114
|
+
if (child.type === "AST_LITERAL_NODE") {
|
|
115
|
+
const literalNode = child;
|
|
116
|
+
if (literalNode.content.trim().length > 0) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (child.type === "AST_HTML_TEXT_NODE") {
|
|
121
|
+
const textNode = child;
|
|
122
|
+
if (textNode.content.trim().length > 0) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (child.type === "AST_HTML_ELEMENT_NODE") {
|
|
127
|
+
const elementNode = child;
|
|
128
|
+
if (this.isElementAccessible(elementNode)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// If there's any non-literal/non-text/non-element content (like ERB), consider it accessible
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export class HTMLNoEmptyHeadingsRule {
|
|
141
|
+
name = "html-no-empty-headings";
|
|
142
|
+
check(node) {
|
|
143
|
+
const visitor = new NoEmptyHeadingsVisitor(this.name);
|
|
144
|
+
visitor.visit(node);
|
|
145
|
+
return visitor.offenses;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=html-no-empty-headings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-no-empty-headings.js","sourceRoot":"","sources":["../../../src/rules/html-no-empty-headings.ts"],"names":[],"mappings":"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"}
|