@html-eslint/eslint-plugin 0.12.0 → 0.13.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/indent.js
CHANGED
|
@@ -131,10 +131,15 @@ module.exports = {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
+
* @param {AnyNode} startTag
|
|
134
135
|
* @param {AttrNode[]} attrs
|
|
135
136
|
*/
|
|
136
|
-
function checkAttrsIndent(attrs) {
|
|
137
|
-
attrs.forEach((attr) =>
|
|
137
|
+
function checkAttrsIndent(startTag, attrs) {
|
|
138
|
+
attrs.forEach((attr) => {
|
|
139
|
+
if (attr.loc.start.line !== startTag.loc.start.line) {
|
|
140
|
+
checkIndent(attr);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
/**
|
|
@@ -146,6 +151,7 @@ module.exports = {
|
|
|
146
151
|
const line = startTag.loc.end.line;
|
|
147
152
|
const endCol = startTag.loc.end.column;
|
|
148
153
|
const startCol = startTag.loc.end.column - 1;
|
|
154
|
+
|
|
149
155
|
checkIndent({
|
|
150
156
|
range: [start, end],
|
|
151
157
|
start,
|
|
@@ -178,8 +184,8 @@ module.exports = {
|
|
|
178
184
|
|
|
179
185
|
indentLevel.up();
|
|
180
186
|
|
|
181
|
-
if (Array.isArray(node.attrs)) {
|
|
182
|
-
checkAttrsIndent(node.attrs);
|
|
187
|
+
if (node.startTag && Array.isArray(node.attrs)) {
|
|
188
|
+
checkAttrsIndent(node.startTag, node.attrs);
|
|
183
189
|
}
|
|
184
190
|
|
|
185
191
|
(node.childNodes || []).forEach((current) => {
|
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
|
+
}
|
|
@@ -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
|
}
|
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.0",
|
|
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.0",
|
|
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": "d7acca5d9d0517cc44085901e71e57e81704bcb7"
|
|
49
49
|
}
|