@html-eslint/eslint-plugin 0.14.1 → 0.16.0-alpha.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 +14 -6
- package/lib/rules/index.js +2 -0
- package/lib/rules/no-extra-spacing-attrs.js +111 -29
- package/lib/rules/no-trailing-spaces.js +75 -0
- package/lib/rules/require-closing-tags.js +23 -13
- package/lib/rules/utils/node-utils.js +14 -0
- package/lib/types.d.ts +2 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
An ESLint plugin which provides lint rules for HTML.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
- [
|
|
9
|
-
- [
|
|
10
|
-
|
|
5
|
+
1. [Getting Started](https://yeonjuan.github.io/html-eslint/docs/getting-started)
|
|
6
|
+
- [Installation](https://yeonjuan.github.io/html-eslint/docs/getting-started#installation)
|
|
7
|
+
- [Configuration](https://yeonjuan.github.io/html-eslint/docs/getting-started#configuration)
|
|
8
|
+
- [Editor Configuration](https://yeonjuan.github.io/html-eslint/docs/getting-started#editor-configuration)
|
|
9
|
+
- [VSCode](https://yeonjuan.github.io/html-eslint/docs/getting-started#vscode)
|
|
10
|
+
1. [Recommended Configs](https://yeonjuan.github.io/html-eslint/docs/getting-started#recommended-configs)
|
|
11
|
+
1. [Rules](https://yeonjuan.github.io/html-eslint/docs/rules)
|
|
12
|
+
1. [CLI](https://yeonjuan.github.io/html-eslint/docs/cli)
|
|
13
|
+
1. [Playground](https://yeonjuan.github.io/html-eslint/playground)
|
|
14
|
+
1. [License](#License)
|
|
15
|
+
|
|
16
|
+
## License
|
|
17
|
+
|
|
18
|
+
Distributed under the MIT License.
|
package/lib/rules/index.js
CHANGED
|
@@ -28,6 +28,7 @@ const noAriaHiddenBody = require("./no-aria-hidden-body");
|
|
|
28
28
|
const noMultipleEmptyLines = require("./no-multiple-empty-lines");
|
|
29
29
|
const noAccesskeyAttrs = require("./no-accesskey-attrs");
|
|
30
30
|
const noRestrictedAttrs = require("./no-restricted-attrs");
|
|
31
|
+
const noTrailingSpaces = require("./no-trailing-spaces");
|
|
31
32
|
|
|
32
33
|
module.exports = {
|
|
33
34
|
"require-lang": requireLang,
|
|
@@ -60,4 +61,5 @@ module.exports = {
|
|
|
60
61
|
"no-multiple-empty-lines": noMultipleEmptyLines,
|
|
61
62
|
"no-accesskey-attrs": noAccesskeyAttrs,
|
|
62
63
|
"no-restricted-attrs": noRestrictedAttrs,
|
|
64
|
+
"no-trailing-spaces": noTrailingSpaces,
|
|
63
65
|
};
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../types").Rule} Rule
|
|
3
|
+
*/
|
|
1
4
|
const { RULE_CATEGORY } = require("../constants");
|
|
5
|
+
const { NodeUtils } = require("./utils");
|
|
2
6
|
|
|
3
7
|
const MESSAGE_IDS = {
|
|
4
8
|
EXTRA_BETWEEN: "unexpectedBetween",
|
|
5
9
|
EXTRA_AFTER: "unexpectedAfter",
|
|
6
10
|
EXTRA_BEFORE: "unexpectedBefore",
|
|
11
|
+
MISSING_BEFORE: "missingBefore",
|
|
12
|
+
MISSING_BEFORE_SELF_CLOSE: "missingBeforeSelfClose",
|
|
13
|
+
EXTRA_BEFORE_SELF_CLOSE: "unexpectedBeforeSelfClose",
|
|
7
14
|
};
|
|
8
15
|
|
|
16
|
+
/**
|
|
17
|
+
* @type {Rule}
|
|
18
|
+
*/
|
|
9
19
|
module.exports = {
|
|
10
20
|
meta: {
|
|
11
21
|
type: "code",
|
|
@@ -17,14 +27,35 @@ module.exports = {
|
|
|
17
27
|
},
|
|
18
28
|
|
|
19
29
|
fixable: true,
|
|
20
|
-
schema: [
|
|
30
|
+
schema: [
|
|
31
|
+
{
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
disallowMissing: {
|
|
35
|
+
type: "boolean",
|
|
36
|
+
},
|
|
37
|
+
enforceBeforeSelfClose: {
|
|
38
|
+
type: "boolean",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
],
|
|
21
43
|
messages: {
|
|
22
44
|
[MESSAGE_IDS.EXTRA_BETWEEN]: "Unexpected space between attributes",
|
|
23
45
|
[MESSAGE_IDS.EXTRA_AFTER]: "Unexpected space after attribute",
|
|
24
46
|
[MESSAGE_IDS.EXTRA_BEFORE]: "Unexpected space before attribute",
|
|
47
|
+
[MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE]:
|
|
48
|
+
"Missing space before self closing",
|
|
49
|
+
[MESSAGE_IDS.EXTRA_BEFORE_SELF_CLOSE]:
|
|
50
|
+
"Unexpected extra spaces before self closing",
|
|
51
|
+
[MESSAGE_IDS.MISSING_BEFORE]: "Missing space before attribute",
|
|
25
52
|
},
|
|
26
53
|
},
|
|
27
54
|
create(context) {
|
|
55
|
+
const enforceBeforeSelfClose = !!(context.options[0] || {})
|
|
56
|
+
.enforceBeforeSelfClose;
|
|
57
|
+
const disallowMissing = !!(context.options[0] || {}).disallowMissing;
|
|
58
|
+
|
|
28
59
|
function checkExtraSpacesBetweenAttrs(attrs) {
|
|
29
60
|
attrs.forEach((current, index, attrs) => {
|
|
30
61
|
if (index >= attrs.length - 1) {
|
|
@@ -38,15 +69,20 @@ module.exports = {
|
|
|
38
69
|
const spacesBetween = after.loc.start.column - current.loc.end.column;
|
|
39
70
|
if (spacesBetween > 1) {
|
|
40
71
|
context.report({
|
|
41
|
-
loc:
|
|
42
|
-
start: current.loc.end,
|
|
43
|
-
end: after.loc.start,
|
|
44
|
-
},
|
|
72
|
+
loc: NodeUtils.getLocBetween(current, after),
|
|
45
73
|
messageId: MESSAGE_IDS.EXTRA_BETWEEN,
|
|
46
74
|
fix(fixer) {
|
|
47
75
|
return fixer.removeRange([current.range[1] + 1, after.range[0]]);
|
|
48
76
|
},
|
|
49
77
|
});
|
|
78
|
+
} else if (disallowMissing && spacesBetween < 1) {
|
|
79
|
+
context.report({
|
|
80
|
+
loc: after.loc,
|
|
81
|
+
messageId: MESSAGE_IDS.MISSING_BEFORE,
|
|
82
|
+
fix(fixer) {
|
|
83
|
+
return fixer.insertTextAfter(current, " ");
|
|
84
|
+
},
|
|
85
|
+
});
|
|
50
86
|
}
|
|
51
87
|
});
|
|
52
88
|
}
|
|
@@ -56,26 +92,31 @@ module.exports = {
|
|
|
56
92
|
// skip the attribute on the different line with the start tag
|
|
57
93
|
return;
|
|
58
94
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
spacesBetween--;
|
|
62
|
-
}
|
|
95
|
+
const limit = isSelfClosed && enforceBeforeSelfClose ? 1 : 0;
|
|
96
|
+
const spacesBetween = openEnd.loc.start.column - lastAttr.loc.end.column;
|
|
63
97
|
|
|
64
|
-
if (spacesBetween >
|
|
98
|
+
if (spacesBetween > limit) {
|
|
65
99
|
context.report({
|
|
66
|
-
loc:
|
|
67
|
-
start: lastAttr.loc.end,
|
|
68
|
-
end: openEnd.loc.end,
|
|
69
|
-
},
|
|
100
|
+
loc: NodeUtils.getLocBetween(lastAttr, openEnd),
|
|
70
101
|
messageId: MESSAGE_IDS.EXTRA_AFTER,
|
|
71
102
|
fix(fixer) {
|
|
72
103
|
return fixer.removeRange([
|
|
73
104
|
lastAttr.range[1],
|
|
74
|
-
lastAttr.range[1] + spacesBetween -
|
|
105
|
+
lastAttr.range[1] + spacesBetween - limit,
|
|
75
106
|
]);
|
|
76
107
|
},
|
|
77
108
|
});
|
|
78
109
|
}
|
|
110
|
+
|
|
111
|
+
if (isSelfClosed && enforceBeforeSelfClose && spacesBetween < 1) {
|
|
112
|
+
context.report({
|
|
113
|
+
loc: NodeUtils.getLocBetween(lastAttr, openEnd),
|
|
114
|
+
messageId: MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE,
|
|
115
|
+
fix(fixer) {
|
|
116
|
+
return fixer.insertTextAfter(lastAttr, " ");
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
79
120
|
}
|
|
80
121
|
|
|
81
122
|
function checkExtraSpaceBefore(node, firstAttr) {
|
|
@@ -87,10 +128,8 @@ module.exports = {
|
|
|
87
128
|
const spacesBetween = firstAttr.loc.start.column - node.loc.end.column;
|
|
88
129
|
if (spacesBetween >= 2) {
|
|
89
130
|
context.report({
|
|
90
|
-
loc:
|
|
91
|
-
|
|
92
|
-
end: firstAttr.loc.start,
|
|
93
|
-
},
|
|
131
|
+
loc: NodeUtils.getLocBetween(node, firstAttr),
|
|
132
|
+
|
|
94
133
|
messageId: MESSAGE_IDS.EXTRA_BEFORE,
|
|
95
134
|
fix(fixer) {
|
|
96
135
|
return fixer.removeRange([
|
|
@@ -102,23 +141,66 @@ module.exports = {
|
|
|
102
141
|
}
|
|
103
142
|
}
|
|
104
143
|
|
|
144
|
+
function checkSpaceBeforeSelfClosing(beforeSelfClosing, openEnd) {
|
|
145
|
+
if (beforeSelfClosing.loc.start.line !== openEnd.loc.start.line) {
|
|
146
|
+
// skip the attribute on the different line with the start tag
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const spacesBetween =
|
|
150
|
+
openEnd.loc.start.column - beforeSelfClosing.loc.end.column;
|
|
151
|
+
const locBetween = NodeUtils.getLocBetween(beforeSelfClosing, openEnd);
|
|
152
|
+
|
|
153
|
+
if (spacesBetween > 1) {
|
|
154
|
+
context.report({
|
|
155
|
+
loc: locBetween,
|
|
156
|
+
messageId: MESSAGE_IDS.EXTRA_BEFORE_SELF_CLOSE,
|
|
157
|
+
fix(fixer) {
|
|
158
|
+
return fixer.removeRange([
|
|
159
|
+
beforeSelfClosing.range[1] + 1,
|
|
160
|
+
openEnd.range[0],
|
|
161
|
+
]);
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
} else if (spacesBetween < 1) {
|
|
165
|
+
context.report({
|
|
166
|
+
loc: locBetween,
|
|
167
|
+
messageId: MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE,
|
|
168
|
+
fix(fixer) {
|
|
169
|
+
return fixer.insertTextAfter(beforeSelfClosing, " ");
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
105
175
|
return {
|
|
106
176
|
[["Tag", "StyleTag", "ScriptTag"].join(",")](node) {
|
|
107
|
-
if (!node.attributes
|
|
177
|
+
if (!node.attributes) {
|
|
108
178
|
return;
|
|
109
179
|
}
|
|
110
180
|
|
|
111
|
-
|
|
181
|
+
if (node.attributes.length) {
|
|
182
|
+
checkExtraSpaceBefore(node.openStart, node.attributes[0]);
|
|
183
|
+
}
|
|
184
|
+
if (node.openEnd) {
|
|
185
|
+
const isSelfClosing = node.openEnd.value === "/>";
|
|
186
|
+
|
|
187
|
+
if (node.attributes && node.attributes.length > 0) {
|
|
188
|
+
checkExtraSpaceAfter(
|
|
189
|
+
node.openEnd,
|
|
190
|
+
node.attributes[node.attributes.length - 1],
|
|
191
|
+
isSelfClosing
|
|
192
|
+
);
|
|
193
|
+
}
|
|
112
194
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
195
|
+
checkExtraSpacesBetweenAttrs(node.attributes);
|
|
196
|
+
if (
|
|
197
|
+
node.attributes.length === 0 &&
|
|
198
|
+
isSelfClosing &&
|
|
199
|
+
enforceBeforeSelfClose
|
|
200
|
+
) {
|
|
201
|
+
checkSpaceBeforeSelfClosing(node.openStart, node.openEnd);
|
|
202
|
+
}
|
|
120
203
|
}
|
|
121
|
-
checkExtraSpacesBetweenAttrs(node.attributes);
|
|
122
204
|
},
|
|
123
205
|
};
|
|
124
206
|
},
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../types").Rule} Rule
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { RULE_CATEGORY } = require("../constants");
|
|
6
|
+
|
|
7
|
+
const MESSAGE_IDS = {
|
|
8
|
+
TRAILING_SPACE: "trailingSpace",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @type {Rule}
|
|
13
|
+
*/
|
|
14
|
+
module.exports = {
|
|
15
|
+
meta: {
|
|
16
|
+
type: "layout",
|
|
17
|
+
docs: {
|
|
18
|
+
description: "Disallow trailing whitespace at the end of lines",
|
|
19
|
+
recommended: false,
|
|
20
|
+
category: RULE_CATEGORY.STYLE,
|
|
21
|
+
},
|
|
22
|
+
fixable: true,
|
|
23
|
+
schema: [],
|
|
24
|
+
messages: {
|
|
25
|
+
[MESSAGE_IDS.TRAILING_SPACE]: "Trailing spaces not allowed",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
create(context) {
|
|
30
|
+
const sourceCode = context.getSourceCode();
|
|
31
|
+
const lineBreaks = sourceCode.getText().match(/\r\n|[\r\n\u2028\u2029]/gu);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
Program() {
|
|
35
|
+
const lines = sourceCode.lines;
|
|
36
|
+
let rangeIndex = 0;
|
|
37
|
+
|
|
38
|
+
lines.forEach((line, index) => {
|
|
39
|
+
const lineNumber = index + 1;
|
|
40
|
+
const match = line.match(/[ \t\u00a0\u2000-\u200b\u3000]+$/);
|
|
41
|
+
const lineBreakLength =
|
|
42
|
+
lineBreaks && lineBreaks[index] ? lineBreaks[index].length : 1;
|
|
43
|
+
const lineLength = line.length + lineBreakLength;
|
|
44
|
+
|
|
45
|
+
if (match) {
|
|
46
|
+
if (typeof match.index === "number" && match.index > 0) {
|
|
47
|
+
const loc = {
|
|
48
|
+
start: {
|
|
49
|
+
line: lineNumber,
|
|
50
|
+
column: match.index,
|
|
51
|
+
},
|
|
52
|
+
end: {
|
|
53
|
+
line: lineNumber,
|
|
54
|
+
column: lineLength - lineBreakLength,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
context.report({
|
|
59
|
+
messageId: MESSAGE_IDS.TRAILING_SPACE,
|
|
60
|
+
loc,
|
|
61
|
+
fix(fixer) {
|
|
62
|
+
return fixer.removeRange([
|
|
63
|
+
rangeIndex + loc.start.column,
|
|
64
|
+
rangeIndex + loc.end.column,
|
|
65
|
+
]);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
rangeIndex += lineLength;
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -33,6 +33,9 @@ module.exports = {
|
|
|
33
33
|
selfClosing: {
|
|
34
34
|
enum: ["always", "never"],
|
|
35
35
|
},
|
|
36
|
+
allowSelfClosingCustom: {
|
|
37
|
+
type: "boolean",
|
|
38
|
+
},
|
|
36
39
|
},
|
|
37
40
|
additionalProperties: false,
|
|
38
41
|
},
|
|
@@ -46,12 +49,14 @@ module.exports = {
|
|
|
46
49
|
},
|
|
47
50
|
|
|
48
51
|
create(context) {
|
|
49
|
-
let svgStacks = [];
|
|
50
|
-
|
|
51
52
|
const shouldSelfClose =
|
|
52
53
|
context.options && context.options.length
|
|
53
54
|
? context.options[0].selfClosing === "always"
|
|
54
55
|
: false;
|
|
56
|
+
const allowSelfClosingCustom =
|
|
57
|
+
context.options && context.options.length
|
|
58
|
+
? context.options[0].allowSelfClosingCustom === true
|
|
59
|
+
: false;
|
|
55
60
|
|
|
56
61
|
function checkClosingTag(node) {
|
|
57
62
|
if (!node.close) {
|
|
@@ -65,7 +70,7 @@ module.exports = {
|
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
function checkVoidElement(node) {
|
|
73
|
+
function checkVoidElement(node, shouldSelfClose, fixable) {
|
|
69
74
|
const hasSelfClose = node.openEnd.value === "/>";
|
|
70
75
|
if (shouldSelfClose && !hasSelfClose) {
|
|
71
76
|
context.report({
|
|
@@ -75,6 +80,9 @@ module.exports = {
|
|
|
75
80
|
},
|
|
76
81
|
messageId: MESSAGE_IDS.MISSING_SELF,
|
|
77
82
|
fix(fixer) {
|
|
83
|
+
if (!fixable) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
78
86
|
return fixer.replaceText(node.openEnd, " />");
|
|
79
87
|
},
|
|
80
88
|
});
|
|
@@ -87,6 +95,9 @@ module.exports = {
|
|
|
87
95
|
},
|
|
88
96
|
messageId: MESSAGE_IDS.UNEXPECTED,
|
|
89
97
|
fix(fixer) {
|
|
98
|
+
if (!fixable) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
90
101
|
return fixer.replaceText(node.openEnd, ">");
|
|
91
102
|
},
|
|
92
103
|
});
|
|
@@ -95,20 +106,19 @@ module.exports = {
|
|
|
95
106
|
|
|
96
107
|
return {
|
|
97
108
|
Tag(node) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
const isVoidElement = VOID_ELEMENTS_SET.has(node.name);
|
|
110
|
+
if (
|
|
111
|
+
node.selfClosing &&
|
|
112
|
+
allowSelfClosingCustom &&
|
|
113
|
+
node.name.indexOf("-") !== -1
|
|
114
|
+
) {
|
|
115
|
+
checkVoidElement(node, true, false);
|
|
116
|
+
} else if (node.selfClosing || isVoidElement) {
|
|
117
|
+
checkVoidElement(node, shouldSelfClose, isVoidElement);
|
|
103
118
|
} else if (node.openEnd.value !== "/>") {
|
|
104
119
|
checkClosingTag(node);
|
|
105
120
|
}
|
|
106
121
|
},
|
|
107
|
-
"Tag:exit"(node) {
|
|
108
|
-
if (node.name === "svg") {
|
|
109
|
-
svgStacks.push(node);
|
|
110
|
-
}
|
|
111
|
-
},
|
|
112
122
|
};
|
|
113
123
|
},
|
|
114
124
|
};
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* @typedef {import("es-html-parser").AttributeNode} AttributeNode
|
|
6
6
|
* @typedef {import("../../types").LineNode} LineNode
|
|
7
7
|
* @typedef {import("../../types").CommentContentNode} CommentContentNode
|
|
8
|
+
* @typedef {import("../../types").BaseNode} BaseNode
|
|
9
|
+
* @typedef {import("../../types").Location} Location
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
module.exports = {
|
|
@@ -95,4 +97,16 @@ module.exports = {
|
|
|
95
97
|
return lineNode;
|
|
96
98
|
});
|
|
97
99
|
},
|
|
100
|
+
/**
|
|
101
|
+
* Get location between two nodes.
|
|
102
|
+
* @param {BaseNode} before A node placed in before
|
|
103
|
+
* @param {BaseNode} after A node placed in after
|
|
104
|
+
* @returns {Location} location between two nodes.
|
|
105
|
+
*/
|
|
106
|
+
getLocBetween(before, after) {
|
|
107
|
+
return {
|
|
108
|
+
start: before.loc.end,
|
|
109
|
+
end: after.loc.start,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
98
112
|
};
|
package/lib/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@html-eslint/eslint-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0-alpha.0",
|
|
4
4
|
"description": "ESLint plugin for html",
|
|
5
5
|
"author": "yeonjuan",
|
|
6
6
|
"homepage": "https://github.com/yeonjuan/html-eslint#readme",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"accessibility"
|
|
41
41
|
],
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@html-eslint/parser": "^0.
|
|
43
|
+
"@html-eslint/parser": "^0.16.0-alpha.0",
|
|
44
44
|
"@types/eslint": "^7.2.10",
|
|
45
45
|
"@types/estree": "^0.0.47",
|
|
46
|
-
"es-html-parser": "^0.0.
|
|
46
|
+
"es-html-parser": "^0.0.8",
|
|
47
47
|
"typescript": "^4.4.4"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "499a9a47682743426515af6ac26b8f4bab50e052"
|
|
50
50
|
}
|