@html-eslint/eslint-plugin 0.12.0 → 0.13.2
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/indent.js +37 -14
- package/lib/rules/index.js +2 -0
- package/lib/rules/no-extra-spacing-attrs.js +11 -4
- package/lib/rules/no-restricted-attrs.js +140 -0
- package/lib/rules/quotes.js +4 -0
- package/lib/rules/require-closing-tags.js +12 -2
- package/lib/rules/require-meta-charset.js +2 -2
- package/lib/types.d.ts +1 -0
- package/package.json +3 -3
package/lib/rules/indent.js
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
* @typedef {Object} MessageId
|
|
13
13
|
* @property {"wrongIndent"} WRONG_INDENT
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
15
|
const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
|
|
17
16
|
const { NodeUtils } = require("./utils");
|
|
18
17
|
|
|
@@ -68,18 +67,33 @@ module.exports = {
|
|
|
68
67
|
},
|
|
69
68
|
create(context) {
|
|
70
69
|
const sourceCode = context.getSourceCode();
|
|
70
|
+
|
|
71
71
|
const indentLevel = new IndentLevel();
|
|
72
72
|
const { indentType, indentSize } = getIndentTypeAndSize(context.options);
|
|
73
73
|
const indentUnit =
|
|
74
74
|
indentType === INDENT_TYPES.SPACE ? " ".repeat(indentSize) : "\t";
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} str
|
|
78
|
+
*/
|
|
79
|
+
function countIndentSize(str) {
|
|
80
|
+
return str.length - str.replace(/^[\s\t]+/, "").length;
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
/**
|
|
77
84
|
* @param {BaseNode} node
|
|
78
85
|
*/
|
|
79
86
|
function getLineCodeBefore(node) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
const lines = sourceCode.getLines();
|
|
88
|
+
const line = lines[node.loc.start.line - 1];
|
|
89
|
+
let end = node.loc.start.column;
|
|
90
|
+
// @ts-ignore
|
|
91
|
+
if (typeof node.textLine === "string") {
|
|
92
|
+
// @ts-ignore
|
|
93
|
+
end += countIndentSize(node.textLine);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return line.slice(0, end);
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
/**
|
|
@@ -120,10 +134,13 @@ module.exports = {
|
|
|
120
134
|
actual,
|
|
121
135
|
},
|
|
122
136
|
fix(fixer) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
137
|
+
const start = node.range[0] - node.loc.start.column;
|
|
138
|
+
let end = node.range[0];
|
|
139
|
+
// @ts-ignore
|
|
140
|
+
if (node.textLine) {
|
|
141
|
+
end += codeBefore.length;
|
|
142
|
+
}
|
|
143
|
+
return fixer.replaceTextRange([start, end], expectedIndent);
|
|
127
144
|
},
|
|
128
145
|
});
|
|
129
146
|
}
|
|
@@ -131,14 +148,19 @@ module.exports = {
|
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
/**
|
|
151
|
+
* @param {AnyNode} startTag
|
|
134
152
|
* @param {AttrNode[]} attrs
|
|
135
153
|
*/
|
|
136
|
-
function checkAttrsIndent(attrs) {
|
|
137
|
-
attrs.forEach((attr) =>
|
|
154
|
+
function checkAttrsIndent(startTag, attrs) {
|
|
155
|
+
attrs.forEach((attr) => {
|
|
156
|
+
if (attr.loc.start.line !== startTag.loc.start.line) {
|
|
157
|
+
checkIndent(attr);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
138
160
|
}
|
|
139
161
|
|
|
140
162
|
/**
|
|
141
|
-
* @param {
|
|
163
|
+
* @param {BaseNode} startTag
|
|
142
164
|
*/
|
|
143
165
|
function checkEndOfStartTag(startTag) {
|
|
144
166
|
const start = startTag.range[1] - 1;
|
|
@@ -146,6 +168,7 @@ module.exports = {
|
|
|
146
168
|
const line = startTag.loc.end.line;
|
|
147
169
|
const endCol = startTag.loc.end.column;
|
|
148
170
|
const startCol = startTag.loc.end.column - 1;
|
|
171
|
+
|
|
149
172
|
checkIndent({
|
|
150
173
|
range: [start, end],
|
|
151
174
|
start,
|
|
@@ -178,8 +201,8 @@ module.exports = {
|
|
|
178
201
|
|
|
179
202
|
indentLevel.up();
|
|
180
203
|
|
|
181
|
-
if (Array.isArray(node.attrs)) {
|
|
182
|
-
checkAttrsIndent(node.attrs);
|
|
204
|
+
if (node.startTag && Array.isArray(node.attrs)) {
|
|
205
|
+
checkAttrsIndent(node.startTag, node.attrs);
|
|
183
206
|
}
|
|
184
207
|
|
|
185
208
|
(node.childNodes || []).forEach((current) => {
|
|
@@ -201,7 +224,7 @@ module.exports = {
|
|
|
201
224
|
}
|
|
202
225
|
node.lineNodes.forEach((lineNode) => {
|
|
203
226
|
if (lineNode.textLine.trim().length) {
|
|
204
|
-
checkIndent(lineNode
|
|
227
|
+
checkIndent(lineNode);
|
|
205
228
|
}
|
|
206
229
|
});
|
|
207
230
|
if (!node.startTag) {
|
package/lib/rules/index.js
CHANGED
|
@@ -27,6 +27,7 @@ const requireButtonType = require("./require-button-type");
|
|
|
27
27
|
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
|
+
const noRestrictedAttrs = require("./no-restricted-attrs");
|
|
30
31
|
|
|
31
32
|
module.exports = {
|
|
32
33
|
"require-lang": requireLang,
|
|
@@ -58,4 +59,5 @@ module.exports = {
|
|
|
58
59
|
"no-aria-hidden-body": noAriaHiddenBody,
|
|
59
60
|
"no-multiple-empty-lines": noMultipleEmptyLines,
|
|
60
61
|
"no-accesskey-attrs": noAccesskeyAttrs,
|
|
62
|
+
"no-restricted-attrs": noRestrictedAttrs,
|
|
61
63
|
};
|
|
@@ -66,13 +66,18 @@ module.exports = {
|
|
|
66
66
|
/**
|
|
67
67
|
* @param {TagNode} startTag
|
|
68
68
|
* @param {AttrNode} lastAttr
|
|
69
|
+
* @param {boolean} isSelfClosed
|
|
69
70
|
*/
|
|
70
|
-
function checkExtraSpaceAfter(startTag, lastAttr) {
|
|
71
|
+
function checkExtraSpaceAfter(startTag, lastAttr, isSelfClosed) {
|
|
71
72
|
if (startTag.loc.end.line !== lastAttr.loc.end.line) {
|
|
72
73
|
// skip the attribute on the different line with the start tag
|
|
73
74
|
return;
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
+
let spacesBetween = startTag.loc.end.column - lastAttr.loc.end.column;
|
|
77
|
+
if (isSelfClosed) {
|
|
78
|
+
spacesBetween--;
|
|
79
|
+
}
|
|
80
|
+
|
|
76
81
|
if (spacesBetween > 1) {
|
|
77
82
|
context.report({
|
|
78
83
|
loc: {
|
|
@@ -83,7 +88,7 @@ module.exports = {
|
|
|
83
88
|
fix(fixer) {
|
|
84
89
|
return fixer.removeRange([
|
|
85
90
|
lastAttr.range[1],
|
|
86
|
-
|
|
91
|
+
lastAttr.range[1] + spacesBetween - 1,
|
|
87
92
|
]);
|
|
88
93
|
},
|
|
89
94
|
});
|
|
@@ -125,9 +130,11 @@ module.exports = {
|
|
|
125
130
|
checkExtraSpaceBefore(node, node.attrs[0]);
|
|
126
131
|
}
|
|
127
132
|
if (node.startTag && node.attrs && node.attrs.length > 0) {
|
|
133
|
+
const isSelfClosed = !node.endTag;
|
|
128
134
|
checkExtraSpaceAfter(
|
|
129
135
|
node.startTag,
|
|
130
|
-
node.attrs[node.attrs.length - 1]
|
|
136
|
+
node.attrs[node.attrs.length - 1],
|
|
137
|
+
isSelfClosed
|
|
131
138
|
);
|
|
132
139
|
}
|
|
133
140
|
checkExtraSpacesBetweenAttrs(node.attrs || []);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../types").Rule} Rule
|
|
3
|
+
* @typedef {import("../types").AnyNode} AnyNode
|
|
4
|
+
* @typedef {{tagPatterns: string[], attrPatterns: string[], message?: string}[]} Options
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { RULE_CATEGORY } = require("../constants");
|
|
8
|
+
|
|
9
|
+
const MESSAGE_IDS = {
|
|
10
|
+
RESTRICTED: "restricted",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @type {Rule}
|
|
15
|
+
*/
|
|
16
|
+
module.exports = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: "code",
|
|
19
|
+
|
|
20
|
+
docs: {
|
|
21
|
+
description: "Disallow specified attributes",
|
|
22
|
+
category: RULE_CATEGORY.BEST_PRACTICE,
|
|
23
|
+
recommended: false,
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
fixable: null,
|
|
27
|
+
schema: {
|
|
28
|
+
type: "array",
|
|
29
|
+
|
|
30
|
+
items: {
|
|
31
|
+
type: "object",
|
|
32
|
+
required: ["tagPatterns", "attrPatterns"],
|
|
33
|
+
properties: {
|
|
34
|
+
tagPatterns: {
|
|
35
|
+
type: "array",
|
|
36
|
+
items: {
|
|
37
|
+
type: "string",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
attrPatterns: {
|
|
41
|
+
type: "array",
|
|
42
|
+
items: {
|
|
43
|
+
type: "string",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
message: {
|
|
47
|
+
type: "string",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
messages: {
|
|
53
|
+
[MESSAGE_IDS.RESTRICTED]: "'{{attr}}' is restricted from being used.",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
create(context) {
|
|
58
|
+
/**
|
|
59
|
+
* @type {Options}
|
|
60
|
+
*/
|
|
61
|
+
const options = context.options;
|
|
62
|
+
const checkers = options.map((option) => new PatternChecker(option));
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"*"(node) {
|
|
66
|
+
const tagName = node.tagName;
|
|
67
|
+
const startTag = node.startTag;
|
|
68
|
+
if (!tagName || !startTag) return;
|
|
69
|
+
if (!node.attrs.length) return;
|
|
70
|
+
|
|
71
|
+
node.attrs.forEach((attr) => {
|
|
72
|
+
if (!attr.name) return;
|
|
73
|
+
|
|
74
|
+
const matched = checkers.find((checker) =>
|
|
75
|
+
checker.test(node.tagName, attr.name)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (!matched) return;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @type {{node: AnyNode, message: string, messageId?: string}}
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
const result = {
|
|
85
|
+
node: startTag,
|
|
86
|
+
message: "",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const customMessage = matched.getMessage();
|
|
90
|
+
|
|
91
|
+
if (customMessage) {
|
|
92
|
+
result.message = customMessage;
|
|
93
|
+
} else {
|
|
94
|
+
result.messageId = MESSAGE_IDS.RESTRICTED;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
context.report({
|
|
98
|
+
...result,
|
|
99
|
+
data: { attr: attr.name },
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
class PatternChecker {
|
|
108
|
+
/**
|
|
109
|
+
* @param {Options[number]} option
|
|
110
|
+
*/
|
|
111
|
+
constructor(option) {
|
|
112
|
+
this.option = option;
|
|
113
|
+
this.tagRegExps = option.tagPatterns.map(
|
|
114
|
+
(pattern) => new RegExp(pattern, "u")
|
|
115
|
+
);
|
|
116
|
+
this.attrRegExps = option.attrPatterns.map(
|
|
117
|
+
(pattern) => new RegExp(pattern, "u")
|
|
118
|
+
);
|
|
119
|
+
this.message = option.message;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {string} tagName
|
|
124
|
+
* @param {string} attrName
|
|
125
|
+
* @returns {boolean}
|
|
126
|
+
*/
|
|
127
|
+
test(tagName, attrName) {
|
|
128
|
+
const result =
|
|
129
|
+
this.tagRegExps.some((exp) => exp.test(tagName)) &&
|
|
130
|
+
this.attrRegExps.some((exp) => exp.test(attrName));
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
getMessage() {
|
|
138
|
+
return this.message || "";
|
|
139
|
+
}
|
|
140
|
+
}
|
package/lib/rules/quotes.js
CHANGED
|
@@ -96,6 +96,10 @@ module.exports = {
|
|
|
96
96
|
* @param {AttrNode} attr
|
|
97
97
|
*/
|
|
98
98
|
function checkQuotes(attr) {
|
|
99
|
+
if (attr.value.includes(expectedQuote)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
99
103
|
const [opening, closing] = getQuotes(attr);
|
|
100
104
|
if (QUOTES_CODES.includes(opening)) {
|
|
101
105
|
if (opening === closing && opening !== expectedQuote) {
|
|
@@ -57,6 +57,16 @@ module.exports = {
|
|
|
57
57
|
|
|
58
58
|
function checkClosingTag(node) {
|
|
59
59
|
if (node.startTag && !node.endTag) {
|
|
60
|
+
if (
|
|
61
|
+
node.namespaceURI === "http://www.w3.org/2000/svg" ||
|
|
62
|
+
node.namespaceURI === "http://www.w3.org/1998/Math/MathML"
|
|
63
|
+
) {
|
|
64
|
+
const code = getCodeIn(node.startTag.range);
|
|
65
|
+
const hasSelfClose = code.endsWith("/>");
|
|
66
|
+
if (hasSelfClose) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
60
70
|
context.report({
|
|
61
71
|
node: node.startTag,
|
|
62
72
|
data: {
|
|
@@ -67,7 +77,7 @@ module.exports = {
|
|
|
67
77
|
}
|
|
68
78
|
}
|
|
69
79
|
|
|
70
|
-
function
|
|
80
|
+
function checkVoidElement(node) {
|
|
71
81
|
const startTag = node.startTag;
|
|
72
82
|
const code = getCodeIn(startTag.range);
|
|
73
83
|
const hasSelfClose = code.endsWith("/>");
|
|
@@ -108,7 +118,7 @@ module.exports = {
|
|
|
108
118
|
"*"(node) {
|
|
109
119
|
if (node.startTag) {
|
|
110
120
|
if (VOID_ELEMENTS_SET.has(node.tagName)) {
|
|
111
|
-
|
|
121
|
+
checkVoidElement(node);
|
|
112
122
|
} else {
|
|
113
123
|
checkClosingTag(node);
|
|
114
124
|
}
|
|
@@ -16,7 +16,7 @@ module.exports = {
|
|
|
16
16
|
type: "code",
|
|
17
17
|
|
|
18
18
|
docs: {
|
|
19
|
-
description: 'Enforce to use `<meta
|
|
19
|
+
description: 'Enforce to use `<meta charset="...">` in `<head>`',
|
|
20
20
|
category: RULE_CATEGORY.BEST_PRACTICE,
|
|
21
21
|
recommended: false,
|
|
22
22
|
},
|
|
@@ -24,7 +24,7 @@ module.exports = {
|
|
|
24
24
|
fixable: null,
|
|
25
25
|
schema: [],
|
|
26
26
|
messages: {
|
|
27
|
-
[MESSAGE_IDS.MISSING]: 'Missing `<meta
|
|
27
|
+
[MESSAGE_IDS.MISSING]: 'Missing `<meta charset="...">`.',
|
|
28
28
|
[MESSAGE_IDS.EMPTY]: "Unexpected empty charset.",
|
|
29
29
|
},
|
|
30
30
|
},
|
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.13.2",
|
|
4
4
|
"description": "ESLint plugin for html",
|
|
5
5
|
"author": "yeonjuan",
|
|
6
6
|
"homepage": "https://github.com/yeonjuan/html-eslint#readme",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"accessibility"
|
|
41
41
|
],
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@html-eslint/parser": "^0.
|
|
43
|
+
"@html-eslint/parser": "^0.13.2",
|
|
44
44
|
"@types/eslint": "^7.2.10",
|
|
45
45
|
"@types/estree": "^0.0.47",
|
|
46
46
|
"typescript": "^4.4.4"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "3f1599284d5725db38e0984ecdcd283a76a530fa"
|
|
49
49
|
}
|