@html-eslint/eslint-plugin 0.14.0 → 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.
@@ -77,7 +77,7 @@ module.exports = {
77
77
  }
78
78
  }
79
79
  return {
80
- [["Tag", "StyleTag", "ScriptTag", "Program"].join(",")](node) {
80
+ [["Tag", "Program"].join(",")](node) {
81
81
  const children = node.type === "Program" ? node.body : node.children;
82
82
  checkSiblings(children);
83
83
  checkChild(node, children);
@@ -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
- let spacesBetween = openEnd.loc.end.column - lastAttr.loc.end.column;
60
- if (isSelfClosed) {
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 > 1) {
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 - 1,
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
- start: node.loc.start,
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 || node.attributes.length <= 0) {
163
+ if (!node.attributes) {
108
164
  return;
109
165
  }
110
166
 
111
- checkExtraSpaceBefore(node.openStart, node.attributes[0]);
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
- selfClosing
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
- 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.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.14.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.7",
46
+ "es-html-parser": "^0.0.8",
47
47
  "typescript": "^4.4.4"
48
48
  },
49
- "gitHead": "3b5398af6d9432407296dd3b20806c6142ac3d1a"
49
+ "gitHead": "9795f56dcf8662ba882555a35c9d3114927f2075"
50
50
  }