@html-eslint/eslint-plugin 0.36.0 → 0.38.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/lib/configs/recommended.js +1 -0
- package/lib/rules/attrs-newline.js +28 -59
- package/lib/rules/indent/indent.js +6 -2
- package/lib/rules/index.js +2 -0
- package/lib/rules/no-duplicate-attrs.js +23 -0
- package/lib/rules/require-button-type.js +35 -1
- package/lib/rules/use-baseline.js +269 -0
- package/lib/rules/utils/baseline.js +509 -0
- package/lib/types/rule.ts +2 -2
- package/package.json +6 -6
- package/types/configs/recommended.d.ts +1 -0
- package/types/index.d.ts +2 -0
- package/types/rules/attrs-newline.d.ts +0 -1
- package/types/rules/attrs-newline.d.ts.map +1 -1
- package/types/rules/indent/indent.d.ts +3 -1
- package/types/rules/indent/indent.d.ts.map +1 -1
- package/types/rules/index.d.ts +2 -0
- package/types/rules/no-duplicate-attrs.d.ts +3 -1
- package/types/rules/no-duplicate-attrs.d.ts.map +1 -1
- package/types/rules/require-button-type.d.ts +3 -1
- package/types/rules/require-button-type.d.ts.map +1 -1
- package/types/rules/use-baseline.d.ts +14 -0
- package/types/rules/use-baseline.d.ts.map +1 -0
- package/types/rules/utils/baseline.d.ts +9 -0
- package/types/rules/utils/baseline.d.ts.map +1 -0
- package/types/types/rule.d.ts +1 -3
- package/types/types/rule.d.ts.map +1 -1
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* @typedef {Object} MessageId
|
|
5
5
|
* @property {"closeStyleWrong"} CLOSE_STYLE_WRONG
|
|
6
6
|
* @property {"newlineMissing"} NEWLINE_MISSING
|
|
7
|
-
* @property {"newlineUnexpected"} NEWLINE_UNEXPECTED
|
|
8
7
|
*
|
|
9
8
|
* @typedef {Object} Option
|
|
10
9
|
* @property {"sameline" | "newline"} [option.closeStyle]
|
|
@@ -22,7 +21,6 @@ const { createVisitors } = require("./utils/visitors");
|
|
|
22
21
|
const MESSAGE_ID = {
|
|
23
22
|
CLOSE_STYLE_WRONG: "closeStyleWrong",
|
|
24
23
|
NEWLINE_MISSING: "newlineMissing",
|
|
25
|
-
NEWLINE_UNEXPECTED: "newlineUnexpected",
|
|
26
24
|
};
|
|
27
25
|
|
|
28
26
|
/**
|
|
@@ -56,8 +54,6 @@ module.exports = {
|
|
|
56
54
|
[MESSAGE_ID.CLOSE_STYLE_WRONG]:
|
|
57
55
|
"Closing bracket was on {{actual}}; expected {{expected}}",
|
|
58
56
|
[MESSAGE_ID.NEWLINE_MISSING]: "Newline expected before {{attrName}}",
|
|
59
|
-
[MESSAGE_ID.NEWLINE_UNEXPECTED]:
|
|
60
|
-
"Newlines not expected between attributes, since this tag has fewer than {{attrMin}} attributes",
|
|
61
57
|
},
|
|
62
58
|
},
|
|
63
59
|
|
|
@@ -70,21 +66,22 @@ module.exports = {
|
|
|
70
66
|
return createVisitors(context, {
|
|
71
67
|
Tag(node) {
|
|
72
68
|
const shouldBeMultiline = node.attributes.length > attrMin;
|
|
69
|
+
if (!shouldBeMultiline) return;
|
|
73
70
|
|
|
74
71
|
/**
|
|
75
72
|
* This doesn't do any indentation, so the result will look silly. Indentation should be covered by the `indent` rule
|
|
76
73
|
* @param {RuleFixer} fixer
|
|
77
74
|
*/
|
|
78
75
|
function fix(fixer) {
|
|
79
|
-
const spacer = shouldBeMultiline ? "\n" : " ";
|
|
80
76
|
let expected = node.openStart.value;
|
|
81
77
|
for (const attr of node.attributes) {
|
|
82
|
-
expected +=
|
|
78
|
+
expected += `\n${attr.key.value}`;
|
|
83
79
|
if (attr.startWrapper && attr.value && attr.endWrapper) {
|
|
84
80
|
expected += `=${attr.startWrapper.value}${attr.value.value}${attr.endWrapper.value}`;
|
|
85
81
|
}
|
|
86
82
|
}
|
|
87
|
-
|
|
83
|
+
|
|
84
|
+
if (closeStyle === "newline") {
|
|
88
85
|
expected += "\n";
|
|
89
86
|
} else if (node.selfClosing) {
|
|
90
87
|
expected += " ";
|
|
@@ -97,66 +94,38 @@ module.exports = {
|
|
|
97
94
|
);
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (attr.loc.start.line === relativeToNode.loc.end.line) {
|
|
106
|
-
return context.report({
|
|
107
|
-
node,
|
|
108
|
-
data: {
|
|
109
|
-
attrName: attr.key.value,
|
|
110
|
-
},
|
|
111
|
-
fix,
|
|
112
|
-
messageId: MESSAGE_ID.NEWLINE_MISSING,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
index += 1;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const attrLast = node.attributes[node.attributes.length - 1];
|
|
119
|
-
const closeStyleActual =
|
|
120
|
-
node.openEnd.loc.start.line === attrLast.loc.end.line
|
|
121
|
-
? "sameline"
|
|
122
|
-
: "newline";
|
|
123
|
-
if (closeStyle !== closeStyleActual) {
|
|
97
|
+
let index = 0;
|
|
98
|
+
for (const attr of node.attributes) {
|
|
99
|
+
const attrPrevious = node.attributes[index - 1];
|
|
100
|
+
const relativeToNode = attrPrevious || node.openStart;
|
|
101
|
+
if (attr.loc.start.line === relativeToNode.loc.end.line) {
|
|
124
102
|
return context.report({
|
|
125
103
|
node,
|
|
126
104
|
data: {
|
|
127
|
-
|
|
128
|
-
expected: closeStyle,
|
|
105
|
+
attrName: attr.key.value,
|
|
129
106
|
},
|
|
130
107
|
fix,
|
|
131
|
-
messageId: MESSAGE_ID.
|
|
108
|
+
messageId: MESSAGE_ID.NEWLINE_MISSING,
|
|
132
109
|
});
|
|
133
110
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
for (const attr of node.attributes) {
|
|
137
|
-
if (shouldBeMultiline) {
|
|
138
|
-
expectedLastLineNum += 1;
|
|
139
|
-
}
|
|
140
|
-
if (attr.value) {
|
|
141
|
-
const valueLineSpan =
|
|
142
|
-
attr.value.loc.end.line - attr.value.loc.start.line;
|
|
143
|
-
expectedLastLineNum += valueLineSpan;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (shouldBeMultiline && closeStyle === "newline") {
|
|
147
|
-
expectedLastLineNum += 1;
|
|
148
|
-
}
|
|
111
|
+
index += 1;
|
|
112
|
+
}
|
|
149
113
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
114
|
+
const attrLast = node.attributes[node.attributes.length - 1];
|
|
115
|
+
const closeStyleActual =
|
|
116
|
+
node.openEnd.loc.start.line === attrLast.loc.end.line
|
|
117
|
+
? "sameline"
|
|
118
|
+
: "newline";
|
|
119
|
+
if (closeStyle !== closeStyleActual) {
|
|
120
|
+
return context.report({
|
|
121
|
+
node,
|
|
122
|
+
data: {
|
|
123
|
+
actual: closeStyleActual,
|
|
124
|
+
expected: closeStyle,
|
|
125
|
+
},
|
|
126
|
+
fix,
|
|
127
|
+
messageId: MESSAGE_ID.CLOSE_STYLE_WRONG,
|
|
128
|
+
});
|
|
160
129
|
}
|
|
161
130
|
},
|
|
162
131
|
});
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* @typedef { import("@html-eslint/types").TemplateLiteral } TemplateLiteral
|
|
13
13
|
* @typedef { import("@html-eslint/types").OpenTemplate } OpenTemplate
|
|
14
14
|
* @typedef { import("@html-eslint/types").CloseTemplate } CloseTemplate
|
|
15
|
+
* @typedef { import("@html-eslint/types").ScriptTag } ScriptTag
|
|
16
|
+
* @typedef { import("@html-eslint/types").StyleTag } StyleTag
|
|
15
17
|
*
|
|
16
18
|
* @typedef {AnyNode | Line} AnyNodeOrLine
|
|
17
19
|
* @typedef {Object} IndentType
|
|
@@ -40,6 +42,8 @@ const {
|
|
|
40
42
|
isLine,
|
|
41
43
|
isTag,
|
|
42
44
|
hasTemplate,
|
|
45
|
+
isScript,
|
|
46
|
+
isStyle,
|
|
43
47
|
} = require("../utils/node");
|
|
44
48
|
const {
|
|
45
49
|
shouldCheckTaggedTemplateExpression,
|
|
@@ -121,7 +125,7 @@ module.exports = {
|
|
|
121
125
|
const { indentType, indentSize, indentChar } = getIndentOptionInfo(context);
|
|
122
126
|
|
|
123
127
|
/**
|
|
124
|
-
* @param {Tag} node
|
|
128
|
+
* @param {Tag | ScriptTag | StyleTag} node
|
|
125
129
|
* @return {number}
|
|
126
130
|
*/
|
|
127
131
|
function getTagIncreasingLevel(node) {
|
|
@@ -150,7 +154,7 @@ module.exports = {
|
|
|
150
154
|
if (isLine(node)) {
|
|
151
155
|
return 1;
|
|
152
156
|
}
|
|
153
|
-
if (isTag(node)) {
|
|
157
|
+
if (isTag(node) || isScript(node) || isStyle(node)) {
|
|
154
158
|
return getTagIncreasingLevel(node);
|
|
155
159
|
}
|
|
156
160
|
const type = node.type;
|
package/lib/rules/index.js
CHANGED
|
@@ -45,6 +45,7 @@ const noInvalidRole = require("./no-invalid-role");
|
|
|
45
45
|
const noNestedInteractive = require("./no-nested-interactive");
|
|
46
46
|
const maxElementDepth = require("./max-element-depth");
|
|
47
47
|
const requireExplicitSize = require("./require-explicit-size");
|
|
48
|
+
const useBaseLine = require("./use-baseline");
|
|
48
49
|
// import new rule here ↑
|
|
49
50
|
// DO NOT REMOVE THIS COMMENT
|
|
50
51
|
|
|
@@ -96,6 +97,7 @@ module.exports = {
|
|
|
96
97
|
"require-input-label": requireInputLabel,
|
|
97
98
|
"max-element-depth": maxElementDepth,
|
|
98
99
|
"require-explicit-size": requireExplicitSize,
|
|
100
|
+
"use-baseline": useBaseLine,
|
|
99
101
|
// export new rule here ↑
|
|
100
102
|
// DO NOT REMOVE THIS COMMENT
|
|
101
103
|
};
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* @typedef { import("@html-eslint/types").StyleTag } StyleTag
|
|
4
4
|
* @typedef { import("@html-eslint/types").ScriptTag } ScriptTag
|
|
5
5
|
* @typedef { import("../types").RuleModule<[]> } RuleModule
|
|
6
|
+
* @typedef { import("@html-eslint/types").Attribute } Attribute
|
|
7
|
+
* @typedef { import("../types").SuggestionReportDescriptor } SuggestionReportDescriptor
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
const { RULE_CATEGORY } = require("../constants");
|
|
@@ -10,6 +12,7 @@ const { createVisitors } = require("./utils/visitors");
|
|
|
10
12
|
|
|
11
13
|
const MESSAGE_IDS = {
|
|
12
14
|
DUPLICATE_ATTRS: "duplicateAttrs",
|
|
15
|
+
REMOVE_ATTR: "removeAttr",
|
|
13
16
|
};
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -26,14 +29,33 @@ module.exports = {
|
|
|
26
29
|
},
|
|
27
30
|
|
|
28
31
|
fixable: null,
|
|
32
|
+
hasSuggestions: true,
|
|
29
33
|
schema: [],
|
|
30
34
|
messages: {
|
|
31
35
|
[MESSAGE_IDS.DUPLICATE_ATTRS]:
|
|
32
36
|
"The attribute '{{attrName}}' is duplicated.",
|
|
37
|
+
[MESSAGE_IDS.REMOVE_ATTR]:
|
|
38
|
+
"Remove this duplicate '{{attrName}}' attribute.",
|
|
33
39
|
},
|
|
34
40
|
},
|
|
35
41
|
|
|
36
42
|
create(context) {
|
|
43
|
+
/**
|
|
44
|
+
* @param {Attribute} node
|
|
45
|
+
* @returns {SuggestionReportDescriptor[]}
|
|
46
|
+
*/
|
|
47
|
+
function getSuggestions(node) {
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
messageId: MESSAGE_IDS.REMOVE_ATTR,
|
|
51
|
+
fix: (fixer) => fixer.removeRange(node.range),
|
|
52
|
+
data: {
|
|
53
|
+
attrName: node.key.value,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
/**
|
|
38
60
|
* @param {Tag | StyleTag | ScriptTag} node
|
|
39
61
|
*/
|
|
@@ -48,6 +70,7 @@ module.exports = {
|
|
|
48
70
|
attrName: attr.key.value,
|
|
49
71
|
},
|
|
50
72
|
messageId: MESSAGE_IDS.DUPLICATE_ATTRS,
|
|
73
|
+
suggest: getSuggestions(attr),
|
|
51
74
|
});
|
|
52
75
|
} else {
|
|
53
76
|
attrsSet.add(attr.key.value.toLowerCase());
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef { import("../types").RuleModule<[]> } RuleModule
|
|
3
|
+
* @typedef { import("@html-eslint/types").AttributeValue } AttributeValue
|
|
4
|
+
* @typedef { import("../types").SuggestionReportDescriptor } SuggestionReportDescriptor
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
const { RULE_CATEGORY } = require("../constants");
|
|
@@ -9,6 +11,9 @@ const { createVisitors } = require("./utils/visitors");
|
|
|
9
11
|
const MESSAGE_IDS = {
|
|
10
12
|
MISSING: "missing",
|
|
11
13
|
INVALID: "invalid",
|
|
14
|
+
REPLACE_TO_SUBMIT: "replaceToSubmit",
|
|
15
|
+
REPLACE_TO_BUTTON: "replaceToButton",
|
|
16
|
+
REPLACE_TO_RESET: "replaceToReset",
|
|
12
17
|
};
|
|
13
18
|
|
|
14
19
|
const VALID_BUTTON_TYPES_SET = new Set(["submit", "button", "reset"]);
|
|
@@ -26,16 +31,41 @@ module.exports = {
|
|
|
26
31
|
recommended: false,
|
|
27
32
|
},
|
|
28
33
|
|
|
29
|
-
fixable:
|
|
34
|
+
fixable: true,
|
|
35
|
+
hasSuggestions: true,
|
|
30
36
|
schema: [],
|
|
31
37
|
messages: {
|
|
32
38
|
[MESSAGE_IDS.MISSING]: "Missing a type attribute for button",
|
|
33
39
|
[MESSAGE_IDS.INVALID]:
|
|
34
40
|
'"{{type}}" is an invalid value for button type attribute.',
|
|
41
|
+
[MESSAGE_IDS.REPLACE_TO_BUTTON]: "Replace the type with 'button'",
|
|
42
|
+
[MESSAGE_IDS.REPLACE_TO_SUBMIT]: "Replace the type with 'submit'",
|
|
43
|
+
[MESSAGE_IDS.REPLACE_TO_RESET]: "Replace the type with 'reset'",
|
|
35
44
|
},
|
|
36
45
|
},
|
|
37
46
|
|
|
38
47
|
create(context) {
|
|
48
|
+
/**
|
|
49
|
+
* @param {AttributeValue} node
|
|
50
|
+
* @returns {SuggestionReportDescriptor[]}
|
|
51
|
+
*/
|
|
52
|
+
function getSuggestions(node) {
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
messageId: MESSAGE_IDS.REPLACE_TO_SUBMIT,
|
|
56
|
+
fix: (fixer) => fixer.replaceTextRange(node.range, "submit"),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
messageId: MESSAGE_IDS.REPLACE_TO_BUTTON,
|
|
60
|
+
fix: (fixer) => fixer.replaceTextRange(node.range, "button"),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
messageId: MESSAGE_IDS.REPLACE_TO_RESET,
|
|
64
|
+
fix: (fixer) => fixer.replaceTextRange(node.range, "reset"),
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
39
69
|
return createVisitors(context, {
|
|
40
70
|
Tag(node) {
|
|
41
71
|
if (node.name !== "button") {
|
|
@@ -46,6 +76,9 @@ module.exports = {
|
|
|
46
76
|
context.report({
|
|
47
77
|
node: node.openStart,
|
|
48
78
|
messageId: MESSAGE_IDS.MISSING,
|
|
79
|
+
fix(fixer) {
|
|
80
|
+
return fixer.insertTextAfter(node.openStart, ' type="submit"');
|
|
81
|
+
},
|
|
49
82
|
});
|
|
50
83
|
} else if (
|
|
51
84
|
!VALID_BUTTON_TYPES_SET.has(typeAttr.value.value) &&
|
|
@@ -57,6 +90,7 @@ module.exports = {
|
|
|
57
90
|
data: {
|
|
58
91
|
type: typeAttr.value.value,
|
|
59
92
|
},
|
|
93
|
+
suggest: getSuggestions(typeAttr.value),
|
|
60
94
|
});
|
|
61
95
|
}
|
|
62
96
|
},
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} Option
|
|
3
|
+
* @property {"widely" | "newly" | number} Option.available
|
|
4
|
+
* @typedef { import("../types").RuleModule<[Option]> } RuleModule
|
|
5
|
+
* @typedef {import("@html-eslint/types").Attribute} Attribute
|
|
6
|
+
* @typedef {import("@html-eslint/types").Tag} Tag
|
|
7
|
+
* @typedef {import("@html-eslint/types").ScriptTag} ScriptTag
|
|
8
|
+
* @typedef {import("@html-eslint/types").StyleTag} StyleTag
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { RULE_CATEGORY } = require("../constants");
|
|
12
|
+
const {
|
|
13
|
+
elements,
|
|
14
|
+
globalAttributes,
|
|
15
|
+
BASELINE_HIGH,
|
|
16
|
+
BASELINE_LOW,
|
|
17
|
+
} = require("./utils/baseline");
|
|
18
|
+
const { createVisitors } = require("./utils/visitors");
|
|
19
|
+
|
|
20
|
+
const MESSAGE_IDS = {
|
|
21
|
+
NOT_BASELINE_ELEMENT: "notBaselineElement",
|
|
22
|
+
NOT_BASELINE_ELEMENT_ATTRIBUTE: "notBaselineElementAttribute",
|
|
23
|
+
NOT_BASELINE_GLOBAL_ATTRIBUTE: "notBaselineGlobalAttribute",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @type {RuleModule}
|
|
28
|
+
*/
|
|
29
|
+
module.exports = {
|
|
30
|
+
meta: {
|
|
31
|
+
type: "code",
|
|
32
|
+
docs: {
|
|
33
|
+
description: "Enforce the use of baseline features.",
|
|
34
|
+
recommended: true,
|
|
35
|
+
category: RULE_CATEGORY.BEST_PRACTICE,
|
|
36
|
+
},
|
|
37
|
+
fixable: null,
|
|
38
|
+
schema: [
|
|
39
|
+
{
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
available: {
|
|
43
|
+
anyOf: [
|
|
44
|
+
{
|
|
45
|
+
enum: ["widely", "newly"],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
// baseline year
|
|
49
|
+
type: "integer",
|
|
50
|
+
minimum: 2000,
|
|
51
|
+
maximum: new Date().getFullYear(),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
|
|
60
|
+
messages: {
|
|
61
|
+
[MESSAGE_IDS.NOT_BASELINE_ELEMENT]:
|
|
62
|
+
"Element '{{element}}' is not a {{availability}} available baseline feature.",
|
|
63
|
+
[MESSAGE_IDS.NOT_BASELINE_ELEMENT_ATTRIBUTE]:
|
|
64
|
+
"Attribute '{{attr}}' on '{{element}}' is not a {{availability}} available baseline feature.",
|
|
65
|
+
[MESSAGE_IDS.NOT_BASELINE_GLOBAL_ATTRIBUTE]:
|
|
66
|
+
"Attribute '{{attr}}' is not a {{availability}} available baseline feature.",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
create(context) {
|
|
71
|
+
const options = context.options[0] || { available: "widely" };
|
|
72
|
+
const available = options.available;
|
|
73
|
+
|
|
74
|
+
const baseYear = typeof available === "number" ? available : null;
|
|
75
|
+
const baseStatus = available === "widely" ? BASELINE_HIGH : BASELINE_LOW;
|
|
76
|
+
const availability = String(available);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} element
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
function isCustomElement(element) {
|
|
83
|
+
return element.includes("-");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} encoded
|
|
88
|
+
* @returns {[number, number]}
|
|
89
|
+
*/
|
|
90
|
+
function decodeStatus(encoded) {
|
|
91
|
+
const [status, year = NaN] = encoded
|
|
92
|
+
.split(":")
|
|
93
|
+
.map((part) => Number(part));
|
|
94
|
+
return [status, year];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} encoded
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
function isSupported(encoded) {
|
|
102
|
+
const [status, year = NaN] = decodeStatus(encoded);
|
|
103
|
+
if (baseYear) {
|
|
104
|
+
return year <= baseYear;
|
|
105
|
+
}
|
|
106
|
+
return status >= baseStatus;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} element
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
function isSupportedElement(element) {
|
|
114
|
+
const elementEncoded = elements.get(element);
|
|
115
|
+
if (!elementEncoded) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
return isSupported(elementEncoded);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {string[]} parts
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
function toStatusKey(...parts) {
|
|
126
|
+
return parts.map((part) => part.toLowerCase().trim()).join(".");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} element
|
|
131
|
+
* @param {string} key
|
|
132
|
+
* @returns {boolean}
|
|
133
|
+
*/
|
|
134
|
+
function isSupportedElementAttributeKey(element, key) {
|
|
135
|
+
const elementStatus = elements.get(toStatusKey(element, key));
|
|
136
|
+
if (!elementStatus) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return isSupported(elementStatus);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} key
|
|
144
|
+
* @returns {boolean}
|
|
145
|
+
*/
|
|
146
|
+
function isSupportedGlobalAttributeKey(key) {
|
|
147
|
+
const globalAttrStatus = globalAttributes.get(toStatusKey(key));
|
|
148
|
+
if (!globalAttrStatus) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
return isSupported(globalAttrStatus);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {string} element
|
|
156
|
+
* @param {string} key
|
|
157
|
+
* @param {string} value
|
|
158
|
+
* @returns {boolean}
|
|
159
|
+
*/
|
|
160
|
+
function isSupportedElementAttributeKeyValue(element, key, value) {
|
|
161
|
+
const elementStatus = elements.get(toStatusKey(element, key, value));
|
|
162
|
+
if (!elementStatus) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
return isSupported(elementStatus);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {string} key
|
|
170
|
+
* @param {string} value
|
|
171
|
+
* @returns {boolean}
|
|
172
|
+
*/
|
|
173
|
+
function isSupportedGlobalAttributeKeyValue(key, value) {
|
|
174
|
+
const globalAttrStatus = globalAttributes.get(toStatusKey(key, value));
|
|
175
|
+
if (!globalAttrStatus) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return isSupported(globalAttrStatus);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @param {Tag | ScriptTag | StyleTag} node
|
|
183
|
+
* @param {string} elementName
|
|
184
|
+
* @param {Attribute[]} attributes
|
|
185
|
+
*/
|
|
186
|
+
function check(node, elementName, attributes) {
|
|
187
|
+
if (isCustomElement(elementName)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!isSupportedElement(elementName)) {
|
|
192
|
+
context.report({
|
|
193
|
+
node: node.openStart,
|
|
194
|
+
messageId: MESSAGE_IDS.NOT_BASELINE_ELEMENT,
|
|
195
|
+
data: {
|
|
196
|
+
element: `<${elementName}>`,
|
|
197
|
+
availability,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
attributes.forEach((attribute) => {
|
|
202
|
+
if (!isSupportedElementAttributeKey(elementName, attribute.key.value)) {
|
|
203
|
+
context.report({
|
|
204
|
+
node: attribute.key,
|
|
205
|
+
messageId: MESSAGE_IDS.NOT_BASELINE_ELEMENT_ATTRIBUTE,
|
|
206
|
+
data: {
|
|
207
|
+
element: `<${elementName}>`,
|
|
208
|
+
attr: attribute.key.value,
|
|
209
|
+
availability,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
} else if (!isSupportedGlobalAttributeKey(attribute.key.value)) {
|
|
213
|
+
context.report({
|
|
214
|
+
node: attribute.key,
|
|
215
|
+
messageId: MESSAGE_IDS.NOT_BASELINE_GLOBAL_ATTRIBUTE,
|
|
216
|
+
data: {
|
|
217
|
+
attr: attribute.key.value,
|
|
218
|
+
availability,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
} else if (attribute.value) {
|
|
222
|
+
if (
|
|
223
|
+
!isSupportedElementAttributeKeyValue(
|
|
224
|
+
elementName,
|
|
225
|
+
attribute.key.value,
|
|
226
|
+
attribute.value.value
|
|
227
|
+
)
|
|
228
|
+
) {
|
|
229
|
+
context.report({
|
|
230
|
+
node: attribute.key,
|
|
231
|
+
messageId: MESSAGE_IDS.NOT_BASELINE_ELEMENT_ATTRIBUTE,
|
|
232
|
+
data: {
|
|
233
|
+
element: `<${elementName}>`,
|
|
234
|
+
attr: `${attribute.key.value}="${attribute.value.value}"`,
|
|
235
|
+
availability,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
} else if (
|
|
239
|
+
!isSupportedGlobalAttributeKeyValue(
|
|
240
|
+
attribute.key.value,
|
|
241
|
+
attribute.value.value
|
|
242
|
+
)
|
|
243
|
+
) {
|
|
244
|
+
context.report({
|
|
245
|
+
node: attribute,
|
|
246
|
+
messageId: MESSAGE_IDS.NOT_BASELINE_GLOBAL_ATTRIBUTE,
|
|
247
|
+
data: {
|
|
248
|
+
attr: `${attribute.key.value}="${attribute.value.value}"`,
|
|
249
|
+
availability,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return createVisitors(context, {
|
|
258
|
+
ScriptTag(node) {
|
|
259
|
+
check(node, "script", node.attributes);
|
|
260
|
+
},
|
|
261
|
+
StyleTag(node) {
|
|
262
|
+
check(node, "style", node.attributes);
|
|
263
|
+
},
|
|
264
|
+
Tag(node) {
|
|
265
|
+
check(node, node.name, node.attributes);
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
};
|