@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 CHANGED
@@ -2,9 +2,17 @@
2
2
 
3
3
  An ESLint plugin which provides lint rules for HTML.
4
4
 
5
- - [Getting Started](https://github.com/yeonjuan/html-eslint#Getting-Started)
6
- - [Installation](https://github.com/yeonjuan/html-eslint#Installation)
7
- - [Configuration](https://github.com/yeonjuan/html-eslint#Configuration)
8
- - [Recommended Configs](https://github.com/yeonjuan/html-eslint#Recommended-Configs)
9
- - [Rules](https://github.com/yeonjuan/html-eslint#Rules)
10
- - [License](https://github.com/yeonjuan/html-eslint#License)
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.
@@ -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
- let spacesBetween = openEnd.loc.end.column - lastAttr.loc.end.column;
60
- if (isSelfClosed) {
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 > 1) {
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 - 1,
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
- start: node.loc.start,
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 || node.attributes.length <= 0) {
177
+ if (!node.attributes) {
108
178
  return;
109
179
  }
110
180
 
111
- checkExtraSpaceBefore(node.openStart, node.attributes[0]);
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
- if (node.openEnd && node.attributes && node.attributes.length > 0) {
114
- const selfClosing = node.openEnd.value === "/>";
115
- checkExtraSpaceAfter(
116
- node.openEnd,
117
- node.attributes[node.attributes.length - 1],
118
- selfClosing
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
- if (node.name === "svg") {
99
- svgStacks.push(node);
100
- }
101
- if (node.selfClosing || VOID_ELEMENTS_SET.has(node.name)) {
102
- checkVoidElement(node);
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
@@ -9,6 +9,8 @@ export type AnyNode = ESHtml.AnyNode | LineNode;
9
9
 
10
10
  export type Range = ESLint.AST.Range;
11
11
 
12
+ export type Location = ESLint.AST.SourceLocation;
13
+
12
14
  export interface BaseNode {
13
15
  range: [number, number];
14
16
  loc: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@html-eslint/eslint-plugin",
3
- "version": "0.14.1",
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.14.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.7",
46
+ "es-html-parser": "^0.0.8",
47
47
  "typescript": "^4.4.4"
48
48
  },
49
- "gitHead": "0fd16ae07553f8d73840ba2e2c07cc488233da05"
49
+ "gitHead": "499a9a47682743426515af6ac26b8f4bab50e052"
50
50
  }