@html-eslint/eslint-plugin 0.14.1 → 0.15.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/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,20 @@
|
|
|
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_SELF_CLOSE: "missingBeforeSelfClose",
|
|
12
|
+
EXTRA_BEFORE_SELF_CLOSE: "unexpectedBeforeSelfClose",
|
|
7
13
|
};
|
|
8
14
|
|
|
15
|
+
/**
|
|
16
|
+
* @type {Rule}
|
|
17
|
+
*/
|
|
9
18
|
module.exports = {
|
|
10
19
|
meta: {
|
|
11
20
|
type: "code",
|
|
@@ -17,14 +26,30 @@ module.exports = {
|
|
|
17
26
|
},
|
|
18
27
|
|
|
19
28
|
fixable: true,
|
|
20
|
-
schema: [
|
|
29
|
+
schema: [
|
|
30
|
+
{
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
enforceBeforeSelfClose: {
|
|
34
|
+
type: "boolean",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
21
39
|
messages: {
|
|
22
40
|
[MESSAGE_IDS.EXTRA_BETWEEN]: "Unexpected space between attributes",
|
|
23
41
|
[MESSAGE_IDS.EXTRA_AFTER]: "Unexpected space after attribute",
|
|
24
42
|
[MESSAGE_IDS.EXTRA_BEFORE]: "Unexpected space before attribute",
|
|
43
|
+
[MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE]:
|
|
44
|
+
"Missing space before self closing",
|
|
45
|
+
[MESSAGE_IDS.EXTRA_BEFORE_SELF_CLOSE]:
|
|
46
|
+
"Unexpected extra spaces before self closing",
|
|
25
47
|
},
|
|
26
48
|
},
|
|
27
49
|
create(context) {
|
|
50
|
+
const enforceBeforeSelfClose = !!(context.options[0] || {})
|
|
51
|
+
.enforceBeforeSelfClose;
|
|
52
|
+
|
|
28
53
|
function checkExtraSpacesBetweenAttrs(attrs) {
|
|
29
54
|
attrs.forEach((current, index, attrs) => {
|
|
30
55
|
if (index >= attrs.length - 1) {
|
|
@@ -38,10 +63,7 @@ module.exports = {
|
|
|
38
63
|
const spacesBetween = after.loc.start.column - current.loc.end.column;
|
|
39
64
|
if (spacesBetween > 1) {
|
|
40
65
|
context.report({
|
|
41
|
-
loc:
|
|
42
|
-
start: current.loc.end,
|
|
43
|
-
end: after.loc.start,
|
|
44
|
-
},
|
|
66
|
+
loc: NodeUtils.getLocBetween(current, after),
|
|
45
67
|
messageId: MESSAGE_IDS.EXTRA_BETWEEN,
|
|
46
68
|
fix(fixer) {
|
|
47
69
|
return fixer.removeRange([current.range[1] + 1, after.range[0]]);
|
|
@@ -56,26 +78,31 @@ module.exports = {
|
|
|
56
78
|
// skip the attribute on the different line with the start tag
|
|
57
79
|
return;
|
|
58
80
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
spacesBetween--;
|
|
62
|
-
}
|
|
81
|
+
const limit = isSelfClosed && enforceBeforeSelfClose ? 1 : 0;
|
|
82
|
+
const spacesBetween = openEnd.loc.start.column - lastAttr.loc.end.column;
|
|
63
83
|
|
|
64
|
-
if (spacesBetween >
|
|
84
|
+
if (spacesBetween > limit) {
|
|
65
85
|
context.report({
|
|
66
|
-
loc:
|
|
67
|
-
start: lastAttr.loc.end,
|
|
68
|
-
end: openEnd.loc.end,
|
|
69
|
-
},
|
|
86
|
+
loc: NodeUtils.getLocBetween(lastAttr, openEnd),
|
|
70
87
|
messageId: MESSAGE_IDS.EXTRA_AFTER,
|
|
71
88
|
fix(fixer) {
|
|
72
89
|
return fixer.removeRange([
|
|
73
90
|
lastAttr.range[1],
|
|
74
|
-
lastAttr.range[1] + spacesBetween -
|
|
91
|
+
lastAttr.range[1] + spacesBetween - limit,
|
|
75
92
|
]);
|
|
76
93
|
},
|
|
77
94
|
});
|
|
78
95
|
}
|
|
96
|
+
|
|
97
|
+
if (isSelfClosed && enforceBeforeSelfClose && spacesBetween < 1) {
|
|
98
|
+
context.report({
|
|
99
|
+
loc: NodeUtils.getLocBetween(lastAttr, openEnd),
|
|
100
|
+
messageId: MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE,
|
|
101
|
+
fix(fixer) {
|
|
102
|
+
return fixer.insertTextAfter(lastAttr, " ");
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
79
106
|
}
|
|
80
107
|
|
|
81
108
|
function checkExtraSpaceBefore(node, firstAttr) {
|
|
@@ -87,10 +114,8 @@ module.exports = {
|
|
|
87
114
|
const spacesBetween = firstAttr.loc.start.column - node.loc.end.column;
|
|
88
115
|
if (spacesBetween >= 2) {
|
|
89
116
|
context.report({
|
|
90
|
-
loc:
|
|
91
|
-
|
|
92
|
-
end: firstAttr.loc.start,
|
|
93
|
-
},
|
|
117
|
+
loc: NodeUtils.getLocBetween(node, firstAttr),
|
|
118
|
+
|
|
94
119
|
messageId: MESSAGE_IDS.EXTRA_BEFORE,
|
|
95
120
|
fix(fixer) {
|
|
96
121
|
return fixer.removeRange([
|
|
@@ -102,23 +127,65 @@ module.exports = {
|
|
|
102
127
|
}
|
|
103
128
|
}
|
|
104
129
|
|
|
130
|
+
function checkSpaceBeforeSelfClosing(beforeSelfClosing, openEnd) {
|
|
131
|
+
if (beforeSelfClosing.loc.start.line !== openEnd.loc.start.line) {
|
|
132
|
+
// skip the attribute on the different line with the start tag
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const spacesBetween =
|
|
136
|
+
openEnd.loc.start.column - beforeSelfClosing.loc.end.column;
|
|
137
|
+
const locBetween = NodeUtils.getLocBetween(beforeSelfClosing, openEnd);
|
|
138
|
+
|
|
139
|
+
if (spacesBetween > 1) {
|
|
140
|
+
context.report({
|
|
141
|
+
loc: locBetween,
|
|
142
|
+
messageId: MESSAGE_IDS.EXTRA_BEFORE_SELF_CLOSE,
|
|
143
|
+
fix(fixer) {
|
|
144
|
+
return fixer.removeRange([
|
|
145
|
+
beforeSelfClosing.range[1] + 1,
|
|
146
|
+
openEnd.range[0],
|
|
147
|
+
]);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
} else if (spacesBetween < 1) {
|
|
151
|
+
context.report({
|
|
152
|
+
loc: locBetween,
|
|
153
|
+
messageId: MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE,
|
|
154
|
+
fix(fixer) {
|
|
155
|
+
return fixer.insertTextAfter(beforeSelfClosing, " ");
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
105
161
|
return {
|
|
106
162
|
[["Tag", "StyleTag", "ScriptTag"].join(",")](node) {
|
|
107
|
-
if (!node.attributes
|
|
163
|
+
if (!node.attributes) {
|
|
108
164
|
return;
|
|
109
165
|
}
|
|
110
166
|
|
|
111
|
-
|
|
167
|
+
if (node.attributes.length) {
|
|
168
|
+
checkExtraSpaceBefore(node.openStart, node.attributes[0]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const isSelfClosing = node.openEnd.value === "/>";
|
|
112
172
|
|
|
113
173
|
if (node.openEnd && node.attributes && node.attributes.length > 0) {
|
|
114
|
-
const selfClosing = node.openEnd.value === "/>";
|
|
115
174
|
checkExtraSpaceAfter(
|
|
116
175
|
node.openEnd,
|
|
117
176
|
node.attributes[node.attributes.length - 1],
|
|
118
|
-
|
|
177
|
+
isSelfClosing
|
|
119
178
|
);
|
|
120
179
|
}
|
|
180
|
+
|
|
121
181
|
checkExtraSpacesBetweenAttrs(node.attributes);
|
|
182
|
+
if (
|
|
183
|
+
node.attributes.length === 0 &&
|
|
184
|
+
isSelfClosing &&
|
|
185
|
+
enforceBeforeSelfClose
|
|
186
|
+
) {
|
|
187
|
+
checkSpaceBeforeSelfClosing(node.openStart, node.openEnd);
|
|
188
|
+
}
|
|
122
189
|
},
|
|
123
190
|
};
|
|
124
191
|
},
|
|
@@ -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.15.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.15.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": "9795f56dcf8662ba882555a35c9d3114927f2075"
|
|
50
50
|
}
|