@herb-tools/linter 0.6.1 → 0.7.1
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 +60 -16
- package/dist/herb-lint.js +364 -181
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +321 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +270 -89
- package/dist/index.js.map +1 -1
- package/dist/package.json +11 -5
- package/dist/src/cli/argument-parser.js +11 -6
- package/dist/src/cli/argument-parser.js.map +1 -1
- package/dist/src/cli/file-processor.js +5 -6
- package/dist/src/cli/file-processor.js.map +1 -1
- package/dist/src/cli/formatters/detailed-formatter.js +3 -5
- package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
- package/dist/src/cli/formatters/github-actions-formatter.js +55 -11
- package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -1
- package/dist/src/cli/index.js +1 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/cli/output-manager.js +23 -5
- package/dist/src/cli/output-manager.js.map +1 -1
- package/dist/src/cli/summary-reporter.js +2 -11
- package/dist/src/cli/summary-reporter.js.map +1 -1
- package/dist/src/cli.js +88 -4
- package/dist/src/cli.js.map +1 -1
- package/dist/src/default-rules.js +8 -4
- package/dist/src/default-rules.js.map +1 -1
- package/dist/src/linter.js.map +1 -1
- package/dist/src/rules/erb-prefer-image-tag-helper.js +1 -1
- package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
- package/dist/src/rules/html-boolean-attributes-no-value.js +8 -8
- package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
- package/dist/src/rules/html-no-empty-attributes.js +56 -0
- package/dist/src/rules/html-no-empty-attributes.js.map +1 -0
- package/dist/src/rules/html-no-positive-tab-index.js +1 -1
- package/dist/src/rules/html-no-positive-tab-index.js.map +1 -1
- package/dist/src/rules/html-no-underscores-in-attribute-names.js +36 -0
- package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +1 -0
- package/dist/src/rules/index.js +3 -0
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/rule-utils.js +11 -7
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +2 -2
- package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/cli/argument-parser.d.ts +2 -1
- package/dist/types/cli/file-processor.d.ts +6 -1
- package/dist/types/cli/formatters/github-actions-formatter.d.ts +6 -1
- package/dist/types/cli/index.d.ts +1 -0
- package/dist/types/cli/output-manager.d.ts +1 -0
- package/dist/types/cli.d.ts +20 -5
- package/dist/types/linter.d.ts +7 -7
- package/dist/types/rules/html-no-empty-attributes.d.ts +7 -0
- package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +7 -0
- package/dist/types/rules/index.d.ts +3 -0
- package/dist/types/rules/rule-utils.d.ts +7 -5
- package/dist/types/src/cli/argument-parser.d.ts +2 -1
- package/dist/types/src/cli/file-processor.d.ts +6 -1
- package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +6 -1
- package/dist/types/src/cli/index.d.ts +1 -0
- package/dist/types/src/cli/output-manager.d.ts +1 -0
- package/dist/types/src/cli.d.ts +20 -5
- package/dist/types/src/linter.d.ts +7 -7
- package/dist/types/src/rules/html-no-empty-attributes.d.ts +7 -0
- package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +7 -0
- package/dist/types/src/rules/index.d.ts +3 -0
- package/dist/types/src/rules/rule-utils.d.ts +7 -5
- package/docs/rules/README.md +2 -0
- package/docs/rules/html-img-require-alt.md +0 -2
- package/docs/rules/html-no-empty-attributes.md +77 -0
- package/docs/rules/html-no-underscores-in-attribute-names.md +45 -0
- package/package.json +11 -5
- package/src/cli/argument-parser.ts +15 -7
- package/src/cli/file-processor.ts +11 -7
- package/src/cli/formatters/detailed-formatter.ts +5 -7
- package/src/cli/formatters/github-actions-formatter.ts +64 -11
- package/src/cli/index.ts +2 -0
- package/src/cli/output-manager.ts +27 -5
- package/src/cli/summary-reporter.ts +3 -11
- package/src/cli.ts +125 -20
- package/src/default-rules.ts +8 -4
- package/src/linter.ts +6 -6
- package/src/rules/erb-no-silent-tag-in-attribute-name.ts +1 -1
- package/src/rules/erb-prefer-image-tag-helper.ts +2 -2
- package/src/rules/erb-require-whitespace-inside-tags.ts +2 -2
- package/src/rules/html-attribute-double-quotes.ts +1 -1
- package/src/rules/html-boolean-attributes-no-value.ts +9 -11
- package/src/rules/html-no-empty-attributes.ts +75 -0
- package/src/rules/html-no-positive-tab-index.ts +1 -1
- package/src/rules/html-no-underscores-in-attribute-names.ts +58 -0
- package/src/rules/html-tag-name-lowercase.ts +1 -1
- package/src/rules/index.ts +3 -0
- package/src/rules/rule-utils.ts +15 -11
- package/src/rules/svg-tag-name-capitalization.ts +2 -2
package/dist/index.cjs
CHANGED
|
@@ -18,11 +18,11 @@ class SourceRule {
|
|
|
18
18
|
static type = "source";
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
exports.ControlFlowType = void 0;
|
|
22
22
|
(function (ControlFlowType) {
|
|
23
23
|
ControlFlowType[ControlFlowType["CONDITIONAL"] = 0] = "CONDITIONAL";
|
|
24
24
|
ControlFlowType[ControlFlowType["LOOP"] = 1] = "LOOP";
|
|
25
|
-
})(ControlFlowType || (ControlFlowType = {}));
|
|
25
|
+
})(exports.ControlFlowType || (exports.ControlFlowType = {}));
|
|
26
26
|
/**
|
|
27
27
|
* Base visitor class that provides common functionality for rule visitors
|
|
28
28
|
*/
|
|
@@ -89,28 +89,28 @@ class ControlFlowTrackingVisitor extends BaseRuleVisitor {
|
|
|
89
89
|
this.onExitBranch(stateToRestore);
|
|
90
90
|
}
|
|
91
91
|
visitERBIfNode(node) {
|
|
92
|
-
this.handleControlFlowNode(node, ControlFlowType.CONDITIONAL, () => super.visitERBIfNode(node));
|
|
92
|
+
this.handleControlFlowNode(node, exports.ControlFlowType.CONDITIONAL, () => super.visitERBIfNode(node));
|
|
93
93
|
}
|
|
94
94
|
visitERBUnlessNode(node) {
|
|
95
|
-
this.handleControlFlowNode(node, ControlFlowType.CONDITIONAL, () => super.visitERBUnlessNode(node));
|
|
95
|
+
this.handleControlFlowNode(node, exports.ControlFlowType.CONDITIONAL, () => super.visitERBUnlessNode(node));
|
|
96
96
|
}
|
|
97
97
|
visitERBCaseNode(node) {
|
|
98
|
-
this.handleControlFlowNode(node, ControlFlowType.CONDITIONAL, () => super.visitERBCaseNode(node));
|
|
98
|
+
this.handleControlFlowNode(node, exports.ControlFlowType.CONDITIONAL, () => super.visitERBCaseNode(node));
|
|
99
99
|
}
|
|
100
100
|
visitERBCaseMatchNode(node) {
|
|
101
|
-
this.handleControlFlowNode(node, ControlFlowType.CONDITIONAL, () => super.visitERBCaseMatchNode(node));
|
|
101
|
+
this.handleControlFlowNode(node, exports.ControlFlowType.CONDITIONAL, () => super.visitERBCaseMatchNode(node));
|
|
102
102
|
}
|
|
103
103
|
visitERBWhileNode(node) {
|
|
104
|
-
this.handleControlFlowNode(node, ControlFlowType.LOOP, () => super.visitERBWhileNode(node));
|
|
104
|
+
this.handleControlFlowNode(node, exports.ControlFlowType.LOOP, () => super.visitERBWhileNode(node));
|
|
105
105
|
}
|
|
106
106
|
visitERBForNode(node) {
|
|
107
|
-
this.handleControlFlowNode(node, ControlFlowType.LOOP, () => super.visitERBForNode(node));
|
|
107
|
+
this.handleControlFlowNode(node, exports.ControlFlowType.LOOP, () => super.visitERBForNode(node));
|
|
108
108
|
}
|
|
109
109
|
visitERBUntilNode(node) {
|
|
110
|
-
this.handleControlFlowNode(node, ControlFlowType.LOOP, () => super.visitERBUntilNode(node));
|
|
110
|
+
this.handleControlFlowNode(node, exports.ControlFlowType.LOOP, () => super.visitERBUntilNode(node));
|
|
111
111
|
}
|
|
112
112
|
visitERBBlockNode(node) {
|
|
113
|
-
this.handleControlFlowNode(node, ControlFlowType.CONDITIONAL, () => super.visitERBBlockNode(node));
|
|
113
|
+
this.handleControlFlowNode(node, exports.ControlFlowType.CONDITIONAL, () => super.visitERBBlockNode(node));
|
|
114
114
|
}
|
|
115
115
|
visitERBElseNode(node) {
|
|
116
116
|
this.startNewBranch(() => super.visitERBElseNode(node));
|
|
@@ -135,10 +135,12 @@ function getTagName(node) {
|
|
|
135
135
|
* Gets the attribute name from an HTMLAttributeNode (lowercased)
|
|
136
136
|
* Returns null if the attribute name contains dynamic content (ERB)
|
|
137
137
|
*/
|
|
138
|
-
function getAttributeName(attributeNode) {
|
|
138
|
+
function getAttributeName(attributeNode, lowercase = true) {
|
|
139
139
|
if (attributeNode.name?.type === "AST_HTML_ATTRIBUTE_NAME_NODE") {
|
|
140
140
|
const nameNode = attributeNode.name;
|
|
141
141
|
const staticName = core.getStaticAttributeName(nameNode);
|
|
142
|
+
if (!lowercase)
|
|
143
|
+
return staticName;
|
|
142
144
|
return staticName ? staticName.toLowerCase() : null;
|
|
143
145
|
}
|
|
144
146
|
return null;
|
|
@@ -173,6 +175,15 @@ function hasStaticAttributeValue(attributeNode) {
|
|
|
173
175
|
return false;
|
|
174
176
|
return valueNode.children.every(child => child.type === "AST_LITERAL_NODE");
|
|
175
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Checks if an attribute value contains dynamic content (ERB)
|
|
180
|
+
*/
|
|
181
|
+
function hasDynamicAttributeValue(attributeNode) {
|
|
182
|
+
const valueNode = attributeNode.value;
|
|
183
|
+
if (!valueNode?.children)
|
|
184
|
+
return false;
|
|
185
|
+
return valueNode.children.some(child => child.type === "AST_ERB_CONTENT_NODE");
|
|
186
|
+
}
|
|
176
187
|
/**
|
|
177
188
|
* Gets the static string value of an attribute (returns null if it contains ERB)
|
|
178
189
|
*/
|
|
@@ -193,6 +204,21 @@ function getAttributeValueNodes(attributeNode) {
|
|
|
193
204
|
const valueNode = attributeNode.value;
|
|
194
205
|
return valueNode?.children || [];
|
|
195
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Checks if an attribute value contains any static content (for validation purposes)
|
|
209
|
+
*/
|
|
210
|
+
function hasStaticAttributeValueContent(attributeNode) {
|
|
211
|
+
const valueNodes = getAttributeValueNodes(attributeNode);
|
|
212
|
+
return core.hasStaticContent(valueNodes);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Gets the static content of an attribute value (all literal parts combined)
|
|
216
|
+
* Returns the concatenated literal content, or null if no literal nodes exist
|
|
217
|
+
*/
|
|
218
|
+
function getStaticAttributeValueContent(attributeNode) {
|
|
219
|
+
const valueNodes = getAttributeValueNodes(attributeNode);
|
|
220
|
+
return core.getStaticContentFromNodes(valueNodes);
|
|
221
|
+
}
|
|
196
222
|
/**
|
|
197
223
|
* Gets the attribute value content from an HTMLAttributeValueNode
|
|
198
224
|
*/
|
|
@@ -467,6 +493,7 @@ class AttributeVisitorMixin extends BaseRuleVisitor {
|
|
|
467
493
|
checkAttributesOnNode(node) {
|
|
468
494
|
forEachAttribute(node, (attributeNode) => {
|
|
469
495
|
const staticAttributeName = getAttributeName(attributeNode);
|
|
496
|
+
const originalAttributeName = getAttributeName(attributeNode, false) || "";
|
|
470
497
|
const isDynamicName = hasDynamicAttributeName(attributeNode);
|
|
471
498
|
const staticAttributeValue = getStaticAttributeValue(attributeNode);
|
|
472
499
|
const valueNodes = getAttributeValueNodes(attributeNode);
|
|
@@ -477,16 +504,17 @@ class AttributeVisitorMixin extends BaseRuleVisitor {
|
|
|
477
504
|
attributeName: staticAttributeName,
|
|
478
505
|
attributeValue: staticAttributeValue,
|
|
479
506
|
attributeNode,
|
|
507
|
+
originalAttributeName,
|
|
480
508
|
parentNode: node
|
|
481
509
|
});
|
|
482
510
|
}
|
|
483
511
|
else if (staticAttributeName && isEffectivelyStaticValue && !hasOutputERB) {
|
|
484
512
|
const validatableContent = core.getValidatableStaticContent(valueNodes) || "";
|
|
485
|
-
this.checkStaticAttributeStaticValue({ attributeName: staticAttributeName, attributeValue: validatableContent, attributeNode, parentNode: node });
|
|
513
|
+
this.checkStaticAttributeStaticValue({ attributeName: staticAttributeName, attributeValue: validatableContent, attributeNode, originalAttributeName, parentNode: node });
|
|
486
514
|
}
|
|
487
515
|
else if (staticAttributeName && hasOutputERB) {
|
|
488
516
|
const combinedValue = getAttributeValue(attributeNode);
|
|
489
|
-
this.checkStaticAttributeDynamicValue({ attributeName: staticAttributeName, valueNodes, attributeNode, parentNode: node, combinedValue });
|
|
517
|
+
this.checkStaticAttributeDynamicValue({ attributeName: staticAttributeName, valueNodes, attributeNode, parentNode: node, originalAttributeName, combinedValue });
|
|
490
518
|
}
|
|
491
519
|
else if (isDynamicName && staticAttributeValue !== null) {
|
|
492
520
|
const nameNode = attributeNode.name;
|
|
@@ -506,28 +534,38 @@ class AttributeVisitorMixin extends BaseRuleVisitor {
|
|
|
506
534
|
/**
|
|
507
535
|
* Static attribute name with static value: class="container"
|
|
508
536
|
*/
|
|
509
|
-
checkStaticAttributeStaticValue(
|
|
537
|
+
checkStaticAttributeStaticValue(_params) {
|
|
510
538
|
// Default implementation does nothing
|
|
511
539
|
}
|
|
512
540
|
/**
|
|
513
541
|
* Static attribute name with dynamic value: class="<%= css_class %>"
|
|
514
542
|
*/
|
|
515
|
-
checkStaticAttributeDynamicValue(
|
|
543
|
+
checkStaticAttributeDynamicValue(_params) {
|
|
516
544
|
// Default implementation does nothing
|
|
517
545
|
}
|
|
518
546
|
/**
|
|
519
547
|
* Dynamic attribute name with static value: data-<%= key %>="foo"
|
|
520
548
|
*/
|
|
521
|
-
checkDynamicAttributeStaticValue(
|
|
549
|
+
checkDynamicAttributeStaticValue(_params) {
|
|
522
550
|
// Default implementation does nothing
|
|
523
551
|
}
|
|
524
552
|
/**
|
|
525
553
|
* Dynamic attribute name with dynamic value: data-<%= key %>="<%= value %>"
|
|
526
554
|
*/
|
|
527
|
-
checkDynamicAttributeDynamicValue(
|
|
555
|
+
checkDynamicAttributeDynamicValue(_params) {
|
|
528
556
|
// Default implementation does nothing
|
|
529
557
|
}
|
|
530
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* Checks if an attribute value is quoted
|
|
561
|
+
*/
|
|
562
|
+
function isAttributeValueQuoted(attributeNode) {
|
|
563
|
+
if (attributeNode.value?.type === "AST_HTML_ATTRIBUTE_VALUE_NODE") {
|
|
564
|
+
const valueNode = attributeNode.value;
|
|
565
|
+
return !!valueNode.quoted;
|
|
566
|
+
}
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
531
569
|
/**
|
|
532
570
|
* Iterates over all attributes of a tag node, calling the callback for each attribute
|
|
533
571
|
*/
|
|
@@ -539,6 +577,60 @@ function forEachAttribute(node, callback) {
|
|
|
539
577
|
}
|
|
540
578
|
}
|
|
541
579
|
}
|
|
580
|
+
/**
|
|
581
|
+
* Base lexer visitor class that provides common functionality for lexer-based rule visitors
|
|
582
|
+
*/
|
|
583
|
+
class BaseLexerRuleVisitor {
|
|
584
|
+
offenses = [];
|
|
585
|
+
ruleName;
|
|
586
|
+
context;
|
|
587
|
+
constructor(ruleName, context) {
|
|
588
|
+
this.ruleName = ruleName;
|
|
589
|
+
this.context = { ...DEFAULT_LINT_CONTEXT, ...context };
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Helper method to create a lint offense for lexer rules
|
|
593
|
+
*/
|
|
594
|
+
createOffense(message, location, severity = "error") {
|
|
595
|
+
return {
|
|
596
|
+
rule: this.ruleName,
|
|
597
|
+
code: this.ruleName,
|
|
598
|
+
source: "Herb Linter",
|
|
599
|
+
message,
|
|
600
|
+
location,
|
|
601
|
+
severity,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Helper method to add an offense to the offenses array
|
|
606
|
+
*/
|
|
607
|
+
addOffense(message, location, severity = "error") {
|
|
608
|
+
this.offenses.push(this.createOffense(message, location, severity));
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Main entry point for lexer rule visitors
|
|
612
|
+
* @param lexResult - The lexer result containing tokens and source
|
|
613
|
+
*/
|
|
614
|
+
visit(lexResult) {
|
|
615
|
+
this.visitTokens(lexResult.value.tokens);
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Visit all tokens
|
|
619
|
+
* Override this method to implement token-level checks
|
|
620
|
+
*/
|
|
621
|
+
visitTokens(tokens) {
|
|
622
|
+
for (const token of tokens) {
|
|
623
|
+
this.visitToken(token);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Visit individual tokens
|
|
628
|
+
* Override this method to implement per-token checks
|
|
629
|
+
*/
|
|
630
|
+
visitToken(_token) {
|
|
631
|
+
// Default implementation does nothing
|
|
632
|
+
}
|
|
633
|
+
}
|
|
542
634
|
/**
|
|
543
635
|
* Base source visitor class that provides common functionality for source-based rule visitors
|
|
544
636
|
*/
|
|
@@ -802,6 +894,8 @@ class Printer extends core.Visitor {
|
|
|
802
894
|
* @throws {Error} When node has parse errors and ignoreErrors is false
|
|
803
895
|
*/
|
|
804
896
|
print(input, options = DEFAULT_PRINT_OPTIONS) {
|
|
897
|
+
if (!input)
|
|
898
|
+
return "";
|
|
805
899
|
if (core.isToken(input)) {
|
|
806
900
|
return input.value;
|
|
807
901
|
}
|
|
@@ -1404,7 +1498,7 @@ class ERBPreferImageTagHelperVisitor extends BaseRuleVisitor {
|
|
|
1404
1498
|
try {
|
|
1405
1499
|
return ERBToRubyStringPrinter.print(node, { ignoreErrors: false });
|
|
1406
1500
|
}
|
|
1407
|
-
catch
|
|
1501
|
+
catch {
|
|
1408
1502
|
return "expression";
|
|
1409
1503
|
}
|
|
1410
1504
|
}
|
|
@@ -1788,19 +1882,18 @@ class HTMLAvoidBothDisabledAndAriaDisabledRule extends ParserRule {
|
|
|
1788
1882
|
}
|
|
1789
1883
|
|
|
1790
1884
|
class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
|
|
1791
|
-
checkStaticAttributeStaticValue({
|
|
1792
|
-
|
|
1793
|
-
return;
|
|
1794
|
-
if (!hasAttributeValue(attributeNode))
|
|
1795
|
-
return;
|
|
1796
|
-
this.addOffense(`Boolean attribute \`${attributeName}\` should not have a value. Use \`${attributeName}\` instead of \`${attributeName}="${attributeName}"\`.`, attributeNode.value.location, "error");
|
|
1885
|
+
checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
|
|
1886
|
+
this.checkAttribute(originalAttributeName, attributeNode);
|
|
1797
1887
|
}
|
|
1798
|
-
checkStaticAttributeDynamicValue({
|
|
1888
|
+
checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
|
|
1889
|
+
this.checkAttribute(originalAttributeName, attributeNode);
|
|
1890
|
+
}
|
|
1891
|
+
checkAttribute(attributeName, attributeNode) {
|
|
1799
1892
|
if (!isBooleanAttribute(attributeName))
|
|
1800
1893
|
return;
|
|
1801
1894
|
if (!hasAttributeValue(attributeNode))
|
|
1802
1895
|
return;
|
|
1803
|
-
this.addOffense(`Boolean attribute \`${
|
|
1896
|
+
this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, "error");
|
|
1804
1897
|
}
|
|
1805
1898
|
}
|
|
1806
1899
|
class HTMLBooleanAttributesNoValueRule extends ParserRule {
|
|
@@ -1873,47 +1966,6 @@ class HTMLImgRequireAltRule extends ParserRule {
|
|
|
1873
1966
|
}
|
|
1874
1967
|
}
|
|
1875
1968
|
|
|
1876
|
-
class NavigationHasLabelVisitor extends BaseRuleVisitor {
|
|
1877
|
-
visitHTMLOpenTagNode(node) {
|
|
1878
|
-
this.checkNavigationElement(node);
|
|
1879
|
-
super.visitHTMLOpenTagNode(node);
|
|
1880
|
-
}
|
|
1881
|
-
checkNavigationElement(node) {
|
|
1882
|
-
const tagName = getTagName(node);
|
|
1883
|
-
const isNavElement = tagName === "nav";
|
|
1884
|
-
const hasNavigationRole = this.hasRoleNavigation(node);
|
|
1885
|
-
if (!isNavElement && !hasNavigationRole) {
|
|
1886
|
-
return;
|
|
1887
|
-
}
|
|
1888
|
-
const hasAriaLabel = hasAttribute(node, "aria-label");
|
|
1889
|
-
const hasAriaLabelledby = hasAttribute(node, "aria-labelledby");
|
|
1890
|
-
if (!hasAriaLabel && !hasAriaLabelledby) {
|
|
1891
|
-
let message = `The navigation landmark should have a unique accessible name via \`aria-label\` or \`aria-labelledby\`. Remember that the name does not need to include "navigation" or "nav" since it will already be announced.`;
|
|
1892
|
-
if (hasNavigationRole && !isNavElement) {
|
|
1893
|
-
message += ` Additionally, you can safely drop the \`role="navigation"\` and replace it with the native HTML \`<nav>\` element.`;
|
|
1894
|
-
}
|
|
1895
|
-
this.addOffense(message, node.tag_name.location, "error");
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
hasRoleNavigation(node) {
|
|
1899
|
-
const attributes = getAttributes(node);
|
|
1900
|
-
const roleAttribute = findAttributeByName(attributes, "role");
|
|
1901
|
-
if (!roleAttribute) {
|
|
1902
|
-
return false;
|
|
1903
|
-
}
|
|
1904
|
-
const roleValue = getAttributeValue(roleAttribute);
|
|
1905
|
-
return roleValue === "navigation";
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
class HTMLNavigationHasLabelRule extends ParserRule {
|
|
1909
|
-
name = "html-navigation-has-label";
|
|
1910
|
-
check(result, context) {
|
|
1911
|
-
const visitor = new NavigationHasLabelVisitor(this.name, context);
|
|
1912
|
-
visitor.visit(result.value);
|
|
1913
|
-
return visitor.offenses;
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
1969
|
const INTERACTIVE_ELEMENTS = new Set([
|
|
1918
1970
|
"button", "summary", "input", "select", "textarea", "a"
|
|
1919
1971
|
]);
|
|
@@ -2047,7 +2099,7 @@ class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor {
|
|
|
2047
2099
|
return stateToRestore;
|
|
2048
2100
|
}
|
|
2049
2101
|
onExitControlFlow(controlFlowType, wasAlreadyInControlFlow, stateToRestore) {
|
|
2050
|
-
if (controlFlowType === ControlFlowType.CONDITIONAL && !wasAlreadyInControlFlow) {
|
|
2102
|
+
if (controlFlowType === exports.ControlFlowType.CONDITIONAL && !wasAlreadyInControlFlow) {
|
|
2051
2103
|
this.controlFlowIds.forEach(id => this.documentIds.add(id));
|
|
2052
2104
|
}
|
|
2053
2105
|
this.currentBranchIds = stateToRestore.previousBranchIds;
|
|
@@ -2080,7 +2132,7 @@ class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor {
|
|
|
2080
2132
|
}
|
|
2081
2133
|
extractIdValue(attributeNode) {
|
|
2082
2134
|
const valueNodes = attributeNode.value?.children || [];
|
|
2083
|
-
if (core.hasERBOutput(valueNodes) && this.isInControlFlow && this.currentControlFlowType === ControlFlowType.LOOP) {
|
|
2135
|
+
if (core.hasERBOutput(valueNodes) && this.isInControlFlow && this.currentControlFlowType === exports.ControlFlowType.LOOP) {
|
|
2084
2136
|
return null;
|
|
2085
2137
|
}
|
|
2086
2138
|
const identifier = core.isEffectivelyStatic(valueNodes) ? core.getValidatableStaticContent(valueNodes) : OutputPrinter.print(valueNodes);
|
|
@@ -2103,7 +2155,7 @@ class NoDuplicateIdsVisitor extends ControlFlowTrackingVisitor {
|
|
|
2103
2155
|
}
|
|
2104
2156
|
}
|
|
2105
2157
|
handleControlFlowId(identifier, attributeNode) {
|
|
2106
|
-
if (this.currentControlFlowType === ControlFlowType.LOOP) {
|
|
2158
|
+
if (this.currentControlFlowType === exports.ControlFlowType.LOOP) {
|
|
2107
2159
|
this.handleLoopId(identifier, attributeNode);
|
|
2108
2160
|
}
|
|
2109
2161
|
else {
|
|
@@ -2164,6 +2216,60 @@ class HTMLNoDuplicateIdsRule extends ParserRule {
|
|
|
2164
2216
|
}
|
|
2165
2217
|
}
|
|
2166
2218
|
|
|
2219
|
+
// Attributes that must not have empty values
|
|
2220
|
+
const RESTRICTED_ATTRIBUTES = new Set([
|
|
2221
|
+
'id',
|
|
2222
|
+
'class',
|
|
2223
|
+
'name',
|
|
2224
|
+
'for',
|
|
2225
|
+
'src',
|
|
2226
|
+
'href',
|
|
2227
|
+
'title',
|
|
2228
|
+
'data',
|
|
2229
|
+
'role'
|
|
2230
|
+
]);
|
|
2231
|
+
// Check if attribute name matches any restricted patterns
|
|
2232
|
+
function isRestrictedAttribute(attributeName) {
|
|
2233
|
+
// Check direct matches
|
|
2234
|
+
if (RESTRICTED_ATTRIBUTES.has(attributeName)) {
|
|
2235
|
+
return true;
|
|
2236
|
+
}
|
|
2237
|
+
// Check for data-* attributes
|
|
2238
|
+
if (attributeName.startsWith('data-')) {
|
|
2239
|
+
return true;
|
|
2240
|
+
}
|
|
2241
|
+
// Check for aria-* attributes
|
|
2242
|
+
if (attributeName.startsWith('aria-')) {
|
|
2243
|
+
return true;
|
|
2244
|
+
}
|
|
2245
|
+
return false;
|
|
2246
|
+
}
|
|
2247
|
+
class NoEmptyAttributesVisitor extends AttributeVisitorMixin {
|
|
2248
|
+
checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }) {
|
|
2249
|
+
if (!isRestrictedAttribute(attributeName))
|
|
2250
|
+
return;
|
|
2251
|
+
if (attributeValue.trim() !== "")
|
|
2252
|
+
return;
|
|
2253
|
+
this.addOffense(`Attribute \`${attributeName}\` must not be empty. Either provide a meaningful value or remove the attribute entirely.`, attributeNode.name.location, "warning");
|
|
2254
|
+
}
|
|
2255
|
+
checkDynamicAttributeStaticValue({ combinedName, attributeValue, attributeNode }) {
|
|
2256
|
+
const name = (combinedName || "").toLowerCase();
|
|
2257
|
+
if (!isRestrictedAttribute(name))
|
|
2258
|
+
return;
|
|
2259
|
+
if (attributeValue.trim() !== "")
|
|
2260
|
+
return;
|
|
2261
|
+
this.addOffense(`Attribute \`${combinedName}\` must not be empty. Either provide a meaningful value or remove the attribute entirely.`, attributeNode.name.location, "warning");
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
class HTMLNoEmptyAttributesRule extends ParserRule {
|
|
2265
|
+
name = "html-no-empty-attributes";
|
|
2266
|
+
check(result, context) {
|
|
2267
|
+
const visitor = new NoEmptyAttributesVisitor(this.name, context);
|
|
2268
|
+
visitor.visit(result.value);
|
|
2269
|
+
return visitor.offenses;
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2167
2273
|
class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
2168
2274
|
visitHTMLElementNode(node) {
|
|
2169
2275
|
this.checkHeadingElement(node);
|
|
@@ -2340,7 +2446,7 @@ class NoPositiveTabIndexVisitor extends AttributeVisitorMixin {
|
|
|
2340
2446
|
return;
|
|
2341
2447
|
const tabIndexValue = parseInt(attributeValue, 10);
|
|
2342
2448
|
if (!isNaN(tabIndexValue) && tabIndexValue > 0) {
|
|
2343
|
-
this.addOffense(`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
|
|
2449
|
+
this.addOffense(`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.`, attributeNode.location, "error");
|
|
2344
2450
|
}
|
|
2345
2451
|
}
|
|
2346
2452
|
}
|
|
@@ -2379,31 +2485,6 @@ class HTMLNoSelfClosingRule extends ParserRule {
|
|
|
2379
2485
|
}
|
|
2380
2486
|
}
|
|
2381
2487
|
|
|
2382
|
-
class NoTitleAttributeVisitor extends BaseRuleVisitor {
|
|
2383
|
-
ALLOWED_ELEMENTS_WITH_TITLE = new Set(["iframe", "link"]);
|
|
2384
|
-
visitHTMLOpenTagNode(node) {
|
|
2385
|
-
this.checkTitleAttribute(node);
|
|
2386
|
-
super.visitHTMLOpenTagNode(node);
|
|
2387
|
-
}
|
|
2388
|
-
checkTitleAttribute(node) {
|
|
2389
|
-
const tagName = getTagName(node);
|
|
2390
|
-
if (!tagName || this.ALLOWED_ELEMENTS_WITH_TITLE.has(tagName)) {
|
|
2391
|
-
return;
|
|
2392
|
-
}
|
|
2393
|
-
if (hasAttribute(node, "title")) {
|
|
2394
|
-
this.addOffense("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.", node.tag_name.location, "error");
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
class HTMLNoTitleAttributeRule extends ParserRule {
|
|
2399
|
-
name = "html-no-title-attribute";
|
|
2400
|
-
check(result, context) {
|
|
2401
|
-
const visitor = new NoTitleAttributeVisitor(this.name, context);
|
|
2402
|
-
visitor.visit(result.value);
|
|
2403
|
-
return visitor.offenses;
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
2488
|
class XMLDeclarationChecker extends BaseRuleVisitor {
|
|
2408
2489
|
hasXMLDeclaration = false;
|
|
2409
2490
|
visitXMLDeclarationNode(_node) {
|
|
@@ -2510,9 +2591,9 @@ class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor {
|
|
|
2510
2591
|
const correctCamelCase = SVG_LOWERCASE_TO_CAMELCASE.get(lowercaseTagName);
|
|
2511
2592
|
if (correctCamelCase && tagName !== correctCamelCase) {
|
|
2512
2593
|
let type = node.type;
|
|
2513
|
-
if (node.type
|
|
2594
|
+
if (node.type === "AST_HTML_OPEN_TAG_NODE")
|
|
2514
2595
|
type = "Opening";
|
|
2515
|
-
if (node.type
|
|
2596
|
+
if (node.type === "AST_HTML_CLOSE_TAG_NODE")
|
|
2516
2597
|
type = "Closing";
|
|
2517
2598
|
this.addOffense(`${type} SVG tag name \`${tagName}\` should use proper capitalization. Use \`${correctCamelCase}\` instead.`, node.tag_name.location, "error");
|
|
2518
2599
|
}
|
|
@@ -2527,6 +2608,38 @@ class SVGTagNameCapitalizationRule extends ParserRule {
|
|
|
2527
2608
|
}
|
|
2528
2609
|
}
|
|
2529
2610
|
|
|
2611
|
+
class HTMLNoUnderscoresInAttributeNamesVisitor extends AttributeVisitorMixin {
|
|
2612
|
+
checkStaticAttributeStaticValue({ attributeName, attributeNode }) {
|
|
2613
|
+
this.check(attributeName, attributeNode);
|
|
2614
|
+
}
|
|
2615
|
+
checkStaticAttributeDynamicValue({ attributeName, attributeNode }) {
|
|
2616
|
+
this.check(attributeName, attributeNode);
|
|
2617
|
+
}
|
|
2618
|
+
checkDynamicAttributeStaticValue({ nameNodes, attributeNode }) {
|
|
2619
|
+
const attributeName = core.getStaticContentFromNodes(nameNodes);
|
|
2620
|
+
this.check(attributeName, attributeNode);
|
|
2621
|
+
}
|
|
2622
|
+
checkDynamicAttributeDynamicValue({ nameNodes, attributeNode }) {
|
|
2623
|
+
const attributeName = core.getStaticContentFromNodes(nameNodes);
|
|
2624
|
+
this.check(attributeName, attributeNode);
|
|
2625
|
+
}
|
|
2626
|
+
check(attributeName, attributeNode) {
|
|
2627
|
+
if (!attributeName)
|
|
2628
|
+
return;
|
|
2629
|
+
if (attributeName.includes("_")) {
|
|
2630
|
+
this.addOffense(`Attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not contain underscores. Use hyphens (-) instead.`, attributeNode.value.location, "warning");
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
class HTMLNoUnderscoresInAttributeNamesRule extends ParserRule {
|
|
2635
|
+
name = "html-no-underscores-in-attribute-names";
|
|
2636
|
+
check(result, context) {
|
|
2637
|
+
const visitor = new HTMLNoUnderscoresInAttributeNamesVisitor(this.name, context);
|
|
2638
|
+
visitor.visit(result.value);
|
|
2639
|
+
return visitor.offenses;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2530
2643
|
const defaultRules = [
|
|
2531
2644
|
ERBNoEmptyTagsRule,
|
|
2532
2645
|
ERBNoOutputControlFlowRule,
|
|
@@ -2547,19 +2660,21 @@ const defaultRules = [
|
|
|
2547
2660
|
HTMLBooleanAttributesNoValueRule,
|
|
2548
2661
|
HTMLIframeHasTitleRule,
|
|
2549
2662
|
HTMLImgRequireAltRule,
|
|
2550
|
-
HTMLNavigationHasLabelRule,
|
|
2663
|
+
// HTMLNavigationHasLabelRule,
|
|
2551
2664
|
HTMLNoAriaHiddenOnFocusableRule,
|
|
2552
2665
|
// HTMLNoBlockInsideInlineRule,
|
|
2553
2666
|
HTMLNoDuplicateAttributesRule,
|
|
2554
2667
|
HTMLNoDuplicateIdsRule,
|
|
2668
|
+
HTMLNoEmptyAttributesRule,
|
|
2555
2669
|
HTMLNoEmptyHeadingsRule,
|
|
2556
2670
|
HTMLNoNestedLinksRule,
|
|
2557
2671
|
HTMLNoPositiveTabIndexRule,
|
|
2558
2672
|
HTMLNoSelfClosingRule,
|
|
2559
|
-
HTMLNoTitleAttributeRule,
|
|
2673
|
+
// HTMLNoTitleAttributeRule,
|
|
2560
2674
|
HTMLTagNameLowercaseRule,
|
|
2561
2675
|
ParserNoErrorsRule,
|
|
2562
2676
|
SVGTagNameCapitalizationRule,
|
|
2677
|
+
HTMLNoUnderscoresInAttributeNamesRule,
|
|
2563
2678
|
];
|
|
2564
2679
|
|
|
2565
2680
|
class Linter {
|
|
@@ -2656,6 +2771,47 @@ class Linter {
|
|
|
2656
2771
|
}
|
|
2657
2772
|
}
|
|
2658
2773
|
|
|
2774
|
+
class NavigationHasLabelVisitor extends BaseRuleVisitor {
|
|
2775
|
+
visitHTMLOpenTagNode(node) {
|
|
2776
|
+
this.checkNavigationElement(node);
|
|
2777
|
+
super.visitHTMLOpenTagNode(node);
|
|
2778
|
+
}
|
|
2779
|
+
checkNavigationElement(node) {
|
|
2780
|
+
const tagName = getTagName(node);
|
|
2781
|
+
const isNavElement = tagName === "nav";
|
|
2782
|
+
const hasNavigationRole = this.hasRoleNavigation(node);
|
|
2783
|
+
if (!isNavElement && !hasNavigationRole) {
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
const hasAriaLabel = hasAttribute(node, "aria-label");
|
|
2787
|
+
const hasAriaLabelledby = hasAttribute(node, "aria-labelledby");
|
|
2788
|
+
if (!hasAriaLabel && !hasAriaLabelledby) {
|
|
2789
|
+
let message = `The navigation landmark should have a unique accessible name via \`aria-label\` or \`aria-labelledby\`. Remember that the name does not need to include "navigation" or "nav" since it will already be announced.`;
|
|
2790
|
+
if (hasNavigationRole && !isNavElement) {
|
|
2791
|
+
message += ` Additionally, you can safely drop the \`role="navigation"\` and replace it with the native HTML \`<nav>\` element.`;
|
|
2792
|
+
}
|
|
2793
|
+
this.addOffense(message, node.tag_name.location, "error");
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
hasRoleNavigation(node) {
|
|
2797
|
+
const attributes = getAttributes(node);
|
|
2798
|
+
const roleAttribute = findAttributeByName(attributes, "role");
|
|
2799
|
+
if (!roleAttribute) {
|
|
2800
|
+
return false;
|
|
2801
|
+
}
|
|
2802
|
+
const roleValue = getAttributeValue(roleAttribute);
|
|
2803
|
+
return roleValue === "navigation";
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
class HTMLNavigationHasLabelRule extends ParserRule {
|
|
2807
|
+
name = "html-navigation-has-label";
|
|
2808
|
+
check(result, context) {
|
|
2809
|
+
const visitor = new NavigationHasLabelVisitor(this.name, context);
|
|
2810
|
+
visitor.visit(result.value);
|
|
2811
|
+
return visitor.offenses;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2659
2815
|
class BlockInsideInlineVisitor extends BaseRuleVisitor {
|
|
2660
2816
|
inlineStack = [];
|
|
2661
2817
|
isValidHTMLOpenTag(node) {
|
|
@@ -2714,12 +2870,44 @@ class HTMLNoBlockInsideInlineRule extends ParserRule {
|
|
|
2714
2870
|
}
|
|
2715
2871
|
}
|
|
2716
2872
|
|
|
2873
|
+
class NoTitleAttributeVisitor extends BaseRuleVisitor {
|
|
2874
|
+
ALLOWED_ELEMENTS_WITH_TITLE = new Set(["iframe", "link"]);
|
|
2875
|
+
visitHTMLOpenTagNode(node) {
|
|
2876
|
+
this.checkTitleAttribute(node);
|
|
2877
|
+
super.visitHTMLOpenTagNode(node);
|
|
2878
|
+
}
|
|
2879
|
+
checkTitleAttribute(node) {
|
|
2880
|
+
const tagName = getTagName(node);
|
|
2881
|
+
if (!tagName || this.ALLOWED_ELEMENTS_WITH_TITLE.has(tagName)) {
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
if (hasAttribute(node, "title")) {
|
|
2885
|
+
this.addOffense("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.", node.tag_name.location, "error");
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
class HTMLNoTitleAttributeRule extends ParserRule {
|
|
2890
|
+
name = "html-no-title-attribute";
|
|
2891
|
+
check(result, context) {
|
|
2892
|
+
const visitor = new NoTitleAttributeVisitor(this.name, context);
|
|
2893
|
+
visitor.visit(result.value);
|
|
2894
|
+
return visitor.offenses;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
exports.ARIA_ATTRIBUTES = ARIA_ATTRIBUTES;
|
|
2899
|
+
exports.AttributeVisitorMixin = AttributeVisitorMixin;
|
|
2900
|
+
exports.BaseLexerRuleVisitor = BaseLexerRuleVisitor;
|
|
2901
|
+
exports.BaseRuleVisitor = BaseRuleVisitor;
|
|
2902
|
+
exports.BaseSourceRuleVisitor = BaseSourceRuleVisitor;
|
|
2903
|
+
exports.ControlFlowTrackingVisitor = ControlFlowTrackingVisitor;
|
|
2717
2904
|
exports.DEFAULT_LINT_CONTEXT = DEFAULT_LINT_CONTEXT;
|
|
2718
2905
|
exports.ERBNoEmptyTagsRule = ERBNoEmptyTagsRule;
|
|
2719
2906
|
exports.ERBNoOutputControlFlowRule = ERBNoOutputControlFlowRule;
|
|
2720
2907
|
exports.ERBNoSilentTagInAttributeNameRule = ERBNoSilentTagInAttributeNameRule;
|
|
2721
2908
|
exports.ERBPreferImageTagHelperRule = ERBPreferImageTagHelperRule;
|
|
2722
2909
|
exports.ERBRequiresTrailingNewlineRule = ERBRequiresTrailingNewlineRule;
|
|
2910
|
+
exports.HEADING_TAGS = HEADING_TAGS;
|
|
2723
2911
|
exports.HTMLAnchorRequireHrefRule = HTMLAnchorRequireHrefRule;
|
|
2724
2912
|
exports.HTMLAriaLabelIsWellFormattedRule = HTMLAriaLabelIsWellFormattedRule;
|
|
2725
2913
|
exports.HTMLAriaLevelMustBeValidRule = HTMLAriaLevelMustBeValidRule;
|
|
@@ -2737,15 +2925,48 @@ exports.HTMLNoAriaHiddenOnFocusableRule = HTMLNoAriaHiddenOnFocusableRule;
|
|
|
2737
2925
|
exports.HTMLNoBlockInsideInlineRule = HTMLNoBlockInsideInlineRule;
|
|
2738
2926
|
exports.HTMLNoDuplicateAttributesRule = HTMLNoDuplicateAttributesRule;
|
|
2739
2927
|
exports.HTMLNoDuplicateIdsRule = HTMLNoDuplicateIdsRule;
|
|
2928
|
+
exports.HTMLNoEmptyAttributesRule = HTMLNoEmptyAttributesRule;
|
|
2740
2929
|
exports.HTMLNoEmptyHeadingsRule = HTMLNoEmptyHeadingsRule;
|
|
2741
2930
|
exports.HTMLNoNestedLinksRule = HTMLNoNestedLinksRule;
|
|
2742
2931
|
exports.HTMLNoPositiveTabIndexRule = HTMLNoPositiveTabIndexRule;
|
|
2743
2932
|
exports.HTMLNoSelfClosingRule = HTMLNoSelfClosingRule;
|
|
2744
2933
|
exports.HTMLNoTitleAttributeRule = HTMLNoTitleAttributeRule;
|
|
2934
|
+
exports.HTMLNoUnderscoresInAttributeNamesRule = HTMLNoUnderscoresInAttributeNamesRule;
|
|
2745
2935
|
exports.HTMLTagNameLowercaseRule = HTMLTagNameLowercaseRule;
|
|
2936
|
+
exports.HTML_BLOCK_ELEMENTS = HTML_BLOCK_ELEMENTS;
|
|
2937
|
+
exports.HTML_BOOLEAN_ATTRIBUTES = HTML_BOOLEAN_ATTRIBUTES;
|
|
2938
|
+
exports.HTML_INLINE_ELEMENTS = HTML_INLINE_ELEMENTS;
|
|
2939
|
+
exports.HTML_VOID_ELEMENTS = HTML_VOID_ELEMENTS;
|
|
2746
2940
|
exports.LexerRule = LexerRule;
|
|
2747
2941
|
exports.Linter = Linter;
|
|
2748
2942
|
exports.ParserRule = ParserRule;
|
|
2749
2943
|
exports.SVGTagNameCapitalizationRule = SVGTagNameCapitalizationRule;
|
|
2944
|
+
exports.SVG_CAMEL_CASE_ELEMENTS = SVG_CAMEL_CASE_ELEMENTS;
|
|
2945
|
+
exports.SVG_LOWERCASE_TO_CAMELCASE = SVG_LOWERCASE_TO_CAMELCASE;
|
|
2750
2946
|
exports.SourceRule = SourceRule;
|
|
2947
|
+
exports.VALID_ARIA_ROLES = VALID_ARIA_ROLES;
|
|
2948
|
+
exports.createEndOfFileLocation = createEndOfFileLocation;
|
|
2949
|
+
exports.findAttributeByName = findAttributeByName;
|
|
2950
|
+
exports.forEachAttribute = forEachAttribute;
|
|
2951
|
+
exports.getAttribute = getAttribute;
|
|
2952
|
+
exports.getAttributeName = getAttributeName;
|
|
2953
|
+
exports.getAttributeValue = getAttributeValue;
|
|
2954
|
+
exports.getAttributeValueNodes = getAttributeValueNodes;
|
|
2955
|
+
exports.getAttributeValueQuoteType = getAttributeValueQuoteType;
|
|
2956
|
+
exports.getAttributes = getAttributes;
|
|
2957
|
+
exports.getCombinedAttributeNameString = getCombinedAttributeNameString;
|
|
2958
|
+
exports.getStaticAttributeValue = getStaticAttributeValue;
|
|
2959
|
+
exports.getStaticAttributeValueContent = getStaticAttributeValueContent;
|
|
2960
|
+
exports.getTagName = getTagName;
|
|
2961
|
+
exports.hasAttribute = hasAttribute;
|
|
2962
|
+
exports.hasAttributeValue = hasAttributeValue;
|
|
2963
|
+
exports.hasDynamicAttributeName = hasDynamicAttributeName;
|
|
2964
|
+
exports.hasDynamicAttributeValue = hasDynamicAttributeValue;
|
|
2965
|
+
exports.hasStaticAttributeValue = hasStaticAttributeValue;
|
|
2966
|
+
exports.hasStaticAttributeValueContent = hasStaticAttributeValueContent;
|
|
2967
|
+
exports.isAttributeValueQuoted = isAttributeValueQuoted;
|
|
2968
|
+
exports.isBlockElement = isBlockElement;
|
|
2969
|
+
exports.isBooleanAttribute = isBooleanAttribute;
|
|
2970
|
+
exports.isInlineElement = isInlineElement;
|
|
2971
|
+
exports.isVoidElement = isVoidElement;
|
|
2751
2972
|
//# sourceMappingURL=index.cjs.map
|