@html-eslint/eslint-plugin 0.24.1 → 0.25.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 +163 -0
- package/lib/rules/index.js +2 -0
- package/lib/rules/require-closing-tags.js +45 -16
- package/package.json +3 -3
- package/types/configs/recommended.d.ts +18 -0
- package/types/configs/recommended.d.ts.map +1 -0
- package/types/rules/attrs-newline.d.ts +11 -0
- package/types/rules/attrs-newline.d.ts.map +1 -0
- package/types/rules/index.d.ts +42 -0
- package/types/rules/require-closing-tags.d.ts.map +1 -0
|
@@ -6,6 +6,7 @@ module.exports = {
|
|
|
6
6
|
"@html-eslint/require-title": "error",
|
|
7
7
|
"@html-eslint/no-multiple-h1": "error",
|
|
8
8
|
"@html-eslint/no-extra-spacing-attrs": "error",
|
|
9
|
+
"@html-eslint/attrs-newline": "error",
|
|
9
10
|
"@html-eslint/element-newline": "error",
|
|
10
11
|
"@html-eslint/no-duplicate-id": "error",
|
|
11
12
|
"@html-eslint/indent": "error",
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef { import("../types").RuleFixer } RuleFixer
|
|
3
|
+
* @typedef { import("../types").RuleModule } RuleModule
|
|
4
|
+
* @typedef { import("../types").TagNode } TagNode
|
|
5
|
+
* @typedef {Object} MessageId
|
|
6
|
+
* @property {"closeStyleWrong"} CLOSE_STYLE_WRONG
|
|
7
|
+
* @property {"newlineMissing"} NEWLINE_MISSING
|
|
8
|
+
* @property {"newlineUnexpected"} NEWLINE_UNEXPECTED
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { RULE_CATEGORY } = require("../constants");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @type {MessageId}
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MESSAGE_ID = {
|
|
18
|
+
CLOSE_STYLE_WRONG: "closeStyleWrong",
|
|
19
|
+
NEWLINE_MISSING: "newlineMissing",
|
|
20
|
+
NEWLINE_UNEXPECTED: "newlineUnexpected",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @type {RuleModule}
|
|
25
|
+
*/
|
|
26
|
+
module.exports = {
|
|
27
|
+
meta: {
|
|
28
|
+
type: "code",
|
|
29
|
+
|
|
30
|
+
docs: {
|
|
31
|
+
description: "Enforce newline between attributes",
|
|
32
|
+
category: RULE_CATEGORY.STYLE,
|
|
33
|
+
recommended: true,
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
fixable: true,
|
|
37
|
+
schema: [
|
|
38
|
+
{
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
closeStyle: {
|
|
42
|
+
enum: ["newline", "sameline"],
|
|
43
|
+
},
|
|
44
|
+
ifAttrsMoreThan: {
|
|
45
|
+
type: "integer",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
messages: {
|
|
51
|
+
[MESSAGE_ID.CLOSE_STYLE_WRONG]:
|
|
52
|
+
"Closing bracket was on {{actual}}; expected {{expected}}",
|
|
53
|
+
[MESSAGE_ID.NEWLINE_MISSING]: "Newline expected before {{attrName}}",
|
|
54
|
+
[MESSAGE_ID.NEWLINE_UNEXPECTED]:
|
|
55
|
+
"Newlines not expected between attributes, since this tag has fewer than {{attrMin}} attributes",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
create(context) {
|
|
60
|
+
const options = context.options[0] || {};
|
|
61
|
+
const attrMin = isNaN(options.ifAttrsMoreThan)
|
|
62
|
+
? 2
|
|
63
|
+
: options.ifAttrsMoreThan;
|
|
64
|
+
const closeStyle = options.closeStyle || "newline";
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
/**
|
|
68
|
+
* @param {TagNode} node
|
|
69
|
+
*/
|
|
70
|
+
Tag(node) {
|
|
71
|
+
const shouldBeMultiline = node.attributes.length > attrMin;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* This doesn't do any indentation, so the result will look silly. Indentation should be covered by the `indent` rule
|
|
75
|
+
* @param {RuleFixer} fixer
|
|
76
|
+
*/
|
|
77
|
+
function fix(fixer) {
|
|
78
|
+
const spacer = shouldBeMultiline ? "\n" : " ";
|
|
79
|
+
let expected = node.openStart.value;
|
|
80
|
+
for (const attr of node.attributes) {
|
|
81
|
+
expected += `${spacer}${attr.key.value}`;
|
|
82
|
+
if (attr.startWrapper && attr.value && attr.endWrapper) {
|
|
83
|
+
expected += `=${attr.startWrapper.value}${attr.value.value}${attr.endWrapper.value}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (shouldBeMultiline && closeStyle === "newline") {
|
|
87
|
+
expected += "\n";
|
|
88
|
+
} else if (node.selfClosing) {
|
|
89
|
+
expected += " ";
|
|
90
|
+
}
|
|
91
|
+
expected += node.openEnd.value;
|
|
92
|
+
|
|
93
|
+
return fixer.replaceTextRange(
|
|
94
|
+
[node.openStart.range[0], node.openEnd.range[1]],
|
|
95
|
+
expected
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (shouldBeMultiline) {
|
|
100
|
+
let index = 0;
|
|
101
|
+
for (const attr of node.attributes) {
|
|
102
|
+
const attrPrevious = node.attributes[index - 1];
|
|
103
|
+
const relativeToNode = attrPrevious || node.openStart;
|
|
104
|
+
if (attr.loc.start.line === relativeToNode.loc.end.line) {
|
|
105
|
+
return context.report({
|
|
106
|
+
node,
|
|
107
|
+
data: {
|
|
108
|
+
attrName: attr.key.value,
|
|
109
|
+
},
|
|
110
|
+
fix,
|
|
111
|
+
messageId: MESSAGE_ID.NEWLINE_MISSING,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
index += 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const attrLast = node.attributes[node.attributes.length - 1];
|
|
118
|
+
const closeStyleActual =
|
|
119
|
+
node.openEnd.loc.start.line === attrLast.loc.end.line
|
|
120
|
+
? "sameline"
|
|
121
|
+
: "newline";
|
|
122
|
+
if (closeStyle !== closeStyleActual) {
|
|
123
|
+
return context.report({
|
|
124
|
+
node,
|
|
125
|
+
data: {
|
|
126
|
+
actual: closeStyleActual,
|
|
127
|
+
expected: closeStyle,
|
|
128
|
+
},
|
|
129
|
+
fix,
|
|
130
|
+
messageId: MESSAGE_ID.CLOSE_STYLE_WRONG,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
let expectedLastLineNum = node.openStart.loc.start.line;
|
|
135
|
+
for (const attr of node.attributes) {
|
|
136
|
+
if (shouldBeMultiline) {
|
|
137
|
+
expectedLastLineNum += 1;
|
|
138
|
+
}
|
|
139
|
+
if (attr.value) {
|
|
140
|
+
const valueLineSpan =
|
|
141
|
+
attr.value.loc.end.line - attr.value.loc.start.line;
|
|
142
|
+
expectedLastLineNum += valueLineSpan;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (shouldBeMultiline && closeStyle === "newline") {
|
|
146
|
+
expectedLastLineNum += 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (node.openEnd.loc.end.line !== expectedLastLineNum) {
|
|
150
|
+
return context.report({
|
|
151
|
+
node,
|
|
152
|
+
data: {
|
|
153
|
+
attrMin,
|
|
154
|
+
},
|
|
155
|
+
fix,
|
|
156
|
+
messageId: MESSAGE_ID.NEWLINE_UNEXPECTED,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
};
|
package/lib/rules/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const noDuplicateId = require("./no-duplicate-id");
|
|
|
6
6
|
const noInlineStyles = require("./no-inline-styles");
|
|
7
7
|
const noMultipleH1 = require("./no-multiple-h1");
|
|
8
8
|
const noExtraSpacingAttrs = require("./no-extra-spacing-attrs");
|
|
9
|
+
const attrsNewline = require("./attrs-newline");
|
|
9
10
|
const elementNewLine = require("./element-newline");
|
|
10
11
|
const noSkipHeadingLevels = require("./no-skip-heading-levels");
|
|
11
12
|
const indent = require("./indent");
|
|
@@ -45,6 +46,7 @@ module.exports = {
|
|
|
45
46
|
"no-inline-styles": noInlineStyles,
|
|
46
47
|
"no-multiple-h1": noMultipleH1,
|
|
47
48
|
"no-extra-spacing-attrs": noExtraSpacingAttrs,
|
|
49
|
+
"attrs-newline": attrsNewline,
|
|
48
50
|
"element-newline": elementNewLine,
|
|
49
51
|
"no-skip-heading-levels": noSkipHeadingLevels,
|
|
50
52
|
"require-li-container": requireLiContainer,
|
|
@@ -34,8 +34,11 @@ module.exports = {
|
|
|
34
34
|
selfClosing: {
|
|
35
35
|
enum: ["always", "never"],
|
|
36
36
|
},
|
|
37
|
-
|
|
38
|
-
type: "
|
|
37
|
+
selfClosingCustomPatterns: {
|
|
38
|
+
type: "array",
|
|
39
|
+
items: {
|
|
40
|
+
type: "string",
|
|
41
|
+
},
|
|
39
42
|
},
|
|
40
43
|
},
|
|
41
44
|
additionalProperties: false,
|
|
@@ -49,14 +52,21 @@ module.exports = {
|
|
|
49
52
|
},
|
|
50
53
|
|
|
51
54
|
create(context) {
|
|
52
|
-
|
|
55
|
+
/** @type {string[]} */
|
|
56
|
+
const foreignContext = [];
|
|
57
|
+
const shouldSelfCloseVoid =
|
|
53
58
|
context.options && context.options.length
|
|
54
59
|
? context.options[0].selfClosing === "always"
|
|
55
60
|
: false;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
/** @type {string[]} */
|
|
62
|
+
const selfClosingCustomPatternsOption =
|
|
63
|
+
(context.options &&
|
|
64
|
+
context.options.length &&
|
|
65
|
+
context.options[0].selfClosingCustomPatterns) ||
|
|
66
|
+
[];
|
|
67
|
+
const selfClosingCustomPatterns = selfClosingCustomPatternsOption.map(
|
|
68
|
+
(i) => new RegExp(i)
|
|
69
|
+
);
|
|
60
70
|
|
|
61
71
|
/**
|
|
62
72
|
* @param {TagNode} node
|
|
@@ -91,7 +101,10 @@ module.exports = {
|
|
|
91
101
|
if (!fixable) {
|
|
92
102
|
return null;
|
|
93
103
|
}
|
|
94
|
-
|
|
104
|
+
const fixes = [];
|
|
105
|
+
fixes.push(fixer.replaceText(node.openEnd, " />"));
|
|
106
|
+
if (node.close) fixes.push(fixer.remove(node.close));
|
|
107
|
+
return fixes;
|
|
95
108
|
},
|
|
96
109
|
});
|
|
97
110
|
}
|
|
@@ -115,17 +128,33 @@ module.exports = {
|
|
|
115
128
|
return {
|
|
116
129
|
Tag(node) {
|
|
117
130
|
const isVoidElement = VOID_ELEMENTS_SET.has(node.name);
|
|
118
|
-
|
|
119
|
-
node.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
const isSelfClosingCustomElement = !!selfClosingCustomPatterns.some(
|
|
132
|
+
(i) => node.name.match(i)
|
|
133
|
+
);
|
|
134
|
+
const isForeign = foreignContext.length > 0;
|
|
135
|
+
const shouldSelfCloseCustom =
|
|
136
|
+
isSelfClosingCustomElement && !node.children.length;
|
|
137
|
+
const shouldSelfCloseForeign = node.selfClosing;
|
|
138
|
+
const shouldSelfClose =
|
|
139
|
+
(isVoidElement && shouldSelfCloseVoid) ||
|
|
140
|
+
(isSelfClosingCustomElement && shouldSelfCloseCustom) ||
|
|
141
|
+
(isForeign && shouldSelfCloseForeign);
|
|
142
|
+
const canSelfClose =
|
|
143
|
+
isVoidElement || isSelfClosingCustomElement || isForeign;
|
|
144
|
+
if (node.selfClosing || canSelfClose) {
|
|
145
|
+
checkVoidElement(node, shouldSelfClose, canSelfClose);
|
|
126
146
|
} else if (node.openEnd.value !== "/>") {
|
|
127
147
|
checkClosingTag(node);
|
|
128
148
|
}
|
|
149
|
+
if (["svg", "math"].includes(node.name)) foreignContext.push(node.name);
|
|
150
|
+
},
|
|
151
|
+
/**
|
|
152
|
+
* @param {TagNode} node
|
|
153
|
+
*/
|
|
154
|
+
"Tag:exit"(node) {
|
|
155
|
+
if (node.name === foreignContext[foreignContext.length - 1]) {
|
|
156
|
+
foreignContext.pop();
|
|
157
|
+
}
|
|
129
158
|
},
|
|
130
159
|
};
|
|
131
160
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@html-eslint/eslint-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "ESLint plugin for html",
|
|
5
5
|
"author": "yeonjuan",
|
|
6
6
|
"homepage": "https://github.com/yeonjuan/html-eslint#readme",
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
"accessibility"
|
|
46
46
|
],
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@html-eslint/parser": "^0.
|
|
48
|
+
"@html-eslint/parser": "^0.25.0",
|
|
49
49
|
"@types/eslint": "^8.56.2",
|
|
50
50
|
"@types/estree": "^0.0.47",
|
|
51
51
|
"es-html-parser": "^0.0.8",
|
|
52
52
|
"typescript": "^4.4.4"
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "315631a66e9c626655c243ccb6a46319736383cf"
|
|
55
55
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const rules: {
|
|
2
|
+
"@html-eslint/require-lang": string;
|
|
3
|
+
"@html-eslint/require-img-alt": string;
|
|
4
|
+
"@html-eslint/require-doctype": string;
|
|
5
|
+
"@html-eslint/require-title": string;
|
|
6
|
+
"@html-eslint/no-multiple-h1": string;
|
|
7
|
+
"@html-eslint/no-extra-spacing-attrs": string;
|
|
8
|
+
"@html-eslint/attrs-newline": string;
|
|
9
|
+
"@html-eslint/element-newline": string;
|
|
10
|
+
"@html-eslint/no-duplicate-id": string;
|
|
11
|
+
"@html-eslint/indent": string;
|
|
12
|
+
"@html-eslint/require-li-container": string;
|
|
13
|
+
"@html-eslint/quotes": string;
|
|
14
|
+
"@html-eslint/no-obsolete-tags": string;
|
|
15
|
+
"@html-eslint/require-closing-tags": string;
|
|
16
|
+
"@html-eslint/no-duplicate-attrs": string;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=recommended.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recommended.d.ts","sourceRoot":"","sources":["../../lib/configs/recommended.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare const _exports: RuleModule;
|
|
2
|
+
export = _exports;
|
|
3
|
+
export type RuleFixer = import("../types").RuleFixer;
|
|
4
|
+
export type RuleModule = import("../types").RuleModule;
|
|
5
|
+
export type TagNode = import("../types").TagNode;
|
|
6
|
+
export type MessageId = {
|
|
7
|
+
CLOSE_STYLE_WRONG: "closeStyleWrong";
|
|
8
|
+
NEWLINE_MISSING: "newlineMissing";
|
|
9
|
+
NEWLINE_UNEXPECTED: "newlineUnexpected";
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=attrs-newline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attrs-newline.d.ts","sourceRoot":"","sources":["../../lib/rules/attrs-newline.js"],"names":[],"mappings":"wBAuBU,UAAU;;wBAtBN,OAAO,UAAU,EAAE,SAAS;yBAC5B,OAAO,UAAU,EAAE,UAAU;sBAC7B,OAAO,UAAU,EAAE,OAAO;;uBAE1B,iBAAiB;qBACjB,gBAAgB;wBAChB,mBAAmB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
declare const _exports: {
|
|
2
|
+
"require-lang": import("../types").RuleModule;
|
|
3
|
+
"require-img-alt": import("../types").RuleModule;
|
|
4
|
+
"require-doctype": import("../types").RuleModule;
|
|
5
|
+
"require-title": import("../types").RuleModule;
|
|
6
|
+
"no-duplicate-id": import("../types").RuleModule;
|
|
7
|
+
"no-inline-styles": import("../types").RuleModule;
|
|
8
|
+
"no-multiple-h1": import("../types").RuleModule;
|
|
9
|
+
"no-extra-spacing-attrs": import("../types").RuleModule;
|
|
10
|
+
"attrs-newline": import("../types").RuleModule;
|
|
11
|
+
"element-newline": import("../types").RuleModule;
|
|
12
|
+
"no-skip-heading-levels": import("../types").RuleModule;
|
|
13
|
+
"require-li-container": import("../types").RuleModule;
|
|
14
|
+
indent: import("../types").RuleModule;
|
|
15
|
+
quotes: import("../types").RuleModule;
|
|
16
|
+
"id-naming-convention": import("../types").RuleModule;
|
|
17
|
+
"no-obsolete-tags": import("../types").RuleModule;
|
|
18
|
+
"require-attrs": import("../types").RuleModule;
|
|
19
|
+
"require-closing-tags": import("../types").RuleModule;
|
|
20
|
+
"require-meta-description": import("../types").RuleModule;
|
|
21
|
+
"require-frame-title": import("../types").RuleModule;
|
|
22
|
+
"no-non-scalable-viewport": import("../types").RuleModule;
|
|
23
|
+
"no-positive-tabindex": import("../types").RuleModule;
|
|
24
|
+
"require-meta-viewport": import("../types").RuleModule;
|
|
25
|
+
"require-meta-charset": import("../types").RuleModule;
|
|
26
|
+
"no-target-blank": import("../types").RuleModule;
|
|
27
|
+
"no-duplicate-attrs": import("../types").RuleModule;
|
|
28
|
+
"no-abstract-roles": import("../types").RuleModule;
|
|
29
|
+
"require-button-type": import("../types").RuleModule;
|
|
30
|
+
"no-aria-hidden-body": import("../types").RuleModule;
|
|
31
|
+
"no-multiple-empty-lines": import("../types").RuleModule;
|
|
32
|
+
"no-accesskey-attrs": import("../types").RuleModule;
|
|
33
|
+
"no-restricted-attrs": import("../types").RuleModule;
|
|
34
|
+
"no-trailing-spaces": import("../types").RuleModule;
|
|
35
|
+
"no-restricted-attr-values": import("../types").RuleModule;
|
|
36
|
+
"no-script-style-type": import("../types").RuleModule;
|
|
37
|
+
lowercase: import("../types").RuleModule;
|
|
38
|
+
"require-open-graph-protocol": import("../types").RuleModule;
|
|
39
|
+
"sort-attrs": import("../types").RuleModule;
|
|
40
|
+
};
|
|
41
|
+
export = _exports;
|
|
42
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-closing-tags.d.ts","sourceRoot":"","sources":["../../lib/rules/require-closing-tags.js"],"names":[],"mappings":"wBAgBU,UAAU;;yBAfN,OAAO,UAAU,EAAE,UAAU;sBAC7B,OAAO,UAAU,EAAE,OAAO"}
|