@html-eslint/eslint-plugin 0.13.2 → 0.14.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.
@@ -1,10 +1,4 @@
1
- /**
2
- * @typedef {import("../types").ElementNode} ElementNode
3
- * @typedef {import("../types").AnyNode} AnyNode
4
- * @typedef {import("../types").Context} Context
5
- */
6
-
7
- const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
1
+ const { RULE_CATEGORY } = require("../constants");
8
2
 
9
3
  const MESSAGE_IDS = {
10
4
  EXPECT_NEW_LINE_AFTER: "expectAfter",
@@ -31,13 +25,10 @@ module.exports = {
31
25
  },
32
26
  },
33
27
 
34
- /**
35
- * @param {Context} context
36
- */
37
28
  create(context) {
38
29
  function checkSiblings(siblings) {
39
30
  siblings
40
- .filter((node) => node.type !== NODE_TYPES.TEXT && node.range[0])
31
+ .filter((node) => node.type !== "Text")
41
32
  .forEach((current, index, arr) => {
42
33
  const after = arr[index + 1];
43
34
  if (after) {
@@ -45,7 +36,7 @@ module.exports = {
45
36
  context.report({
46
37
  node: current,
47
38
  messageId: MESSAGE_IDS.EXPECT_NEW_LINE_AFTER,
48
- data: { tag: `<${current.tagName}>` },
39
+ data: { tag: `<${current.name}>` },
49
40
  fix(fixer) {
50
41
  return fixer.insertTextAfter(current, "\n");
51
42
  },
@@ -55,75 +46,49 @@ module.exports = {
55
46
  });
56
47
  }
57
48
 
58
- /**
59
- *
60
- * @param {ElementNode['childNodes'][number]} node
61
- */
62
- function checkChild(node) {
63
- const children = (node.childNodes || []).filter(
64
- (n) => !!n.range[0] && n.type !== NODE_TYPES.TEXT
65
- );
66
- const first = children[0];
67
- const last = children[children.length - 1];
49
+ function checkChild(node, children) {
50
+ const targetChildren = children.filter((n) => n.type !== "Text");
51
+ const first = targetChildren[0];
52
+ const last = targetChildren[targetChildren.length - 1];
68
53
  if (first) {
69
- if (node.startTag && isOnTheSameLine(node.startTag, first)) {
54
+ if (isOnTheSameLine(node.openEnd, first)) {
70
55
  context.report({
71
- node: node.startTag,
56
+ node: node.openEnd,
72
57
  messageId: MESSAGE_IDS.EXPECT_NEW_LINE_AFTER,
73
- data: { tag: `<${node.tagName}>` },
58
+ data: { tag: `<${node.name}>` },
74
59
  fix(fixer) {
75
- if (node.startTag) {
76
- return fixer.insertTextAfter(node.startTag, "\n");
77
- }
78
- return null;
60
+ return fixer.insertTextAfter(node.openEnd, "\n");
79
61
  },
80
62
  });
81
63
  }
82
64
  }
65
+
83
66
  if (last) {
84
- if (node.endTag && isOnTheSameLine(node.endTag, last)) {
67
+ if (node.close && isOnTheSameLine(node.close, last)) {
85
68
  context.report({
86
- node: node.endTag,
69
+ node: node.close,
87
70
  messageId: MESSAGE_IDS.EXPECT_NEW_LINE_BEFORE,
88
- data: { tag: `</${node.tagName}>` },
71
+ data: { tag: `</${node.name}>` },
89
72
  fix(fixer) {
90
- if (node.endTag) {
91
- return fixer.insertTextBefore(node.endTag, "\n");
92
- }
93
- return null;
73
+ return fixer.insertTextBefore(node.close, "\n");
94
74
  },
95
75
  });
96
76
  }
97
77
  }
98
78
  }
99
79
  return {
100
- /**
101
- * @param {ElementNode} node
102
- */
103
- "*"(node) {
104
- if (node.type !== NODE_TYPES.TEXT) {
105
- checkSiblings(node.childNodes || []);
106
- checkChild(node);
107
- }
80
+ [["Tag", "StyleTag", "ScriptTag", "Program"].join(",")](node) {
81
+ const children = node.type === "Program" ? node.body : node.children;
82
+ checkSiblings(children);
83
+ checkChild(node, children);
108
84
  },
109
85
  };
110
86
  },
111
87
  };
112
88
 
113
- /**
114
- * Checks whether two nodes are on the same line or not.
115
- * @param {AnyNode} nodeBefore A node before
116
- * @param {AnyNode} nodeAfter A node after
117
- * @returns {boolean} `true` if two nodes are on the same line, otherwise `false`.
118
- */
119
89
  function isOnTheSameLine(nodeBefore, nodeAfter) {
120
90
  if (nodeBefore && nodeAfter) {
121
- // @ts-ignore
122
- if (nodeBefore.endTag) {
123
- // @ts-ignore
124
- return nodeBefore.endTag.loc.end.line === nodeAfter.loc.start.line;
125
- }
126
- return nodeBefore.loc.start.line === nodeAfter.loc.start.line;
91
+ return nodeBefore.loc.end.line === nodeAfter.loc.start.line;
127
92
  }
128
93
  return false;
129
94
  }
@@ -58,12 +58,15 @@ module.exports = {
58
58
 
59
59
  return {
60
60
  "*"(node) {
61
+ if (!node.attributes || node.attributes.length <= 0) {
62
+ return;
63
+ }
61
64
  const idAttr = NodeUtils.findAttr(node, "id");
62
- if (idAttr && idAttr.value && !checkNaming(idAttr.value)) {
65
+ if (idAttr && idAttr.value && !checkNaming(idAttr.value.value)) {
63
66
  context.report({
64
67
  node: idAttr,
65
68
  data: {
66
- actual: idAttr.value,
69
+ actual: idAttr.value.value,
67
70
  convention,
68
71
  },
69
72
  messageId: MESSAGE_IDS.WRONG,
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * @typedef {import("../types").Rule} Rule
3
- * @typedef {import("../types").ElementNode} ElementNode
4
- * @typedef {import("../types").AttrNode} AttrNode
5
3
  * @typedef {import("../types").TagNode} TagNode
6
- * @typedef {import("../types").AnyNode} AnyNode
7
4
  * @typedef {import("../types").BaseNode} BaseNode
5
+ * @typedef {import("../types").OpenTagStartNode} OpenTagStartNode
6
+ * @typedef {import("../types").CloseTagNode} CloseTagNode
7
+ * @typedef {import("../types").LineNode} LineNode
8
+ *@typedef {import("../types").AnyNode} AnyNode
8
9
  * @typedef {Object} IndentType
9
10
  * @property {"tab"} TAB
10
11
  * @property {"space"} SPACE
@@ -12,7 +13,7 @@
12
13
  * @typedef {Object} MessageId
13
14
  * @property {"wrongIndent"} WRONG_INDENT
14
15
  */
15
- const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
16
+ const { RULE_CATEGORY } = require("../constants");
16
17
  const { NodeUtils } = require("./utils");
17
18
 
18
19
  /** @type {MessageId} */
@@ -26,12 +27,7 @@ const INDENT_TYPES = {
26
27
  SPACE: "space",
27
28
  };
28
29
 
29
- const IGNORING_NODES = [
30
- NODE_TYPES.PRE,
31
- NODE_TYPES.SCRIPT,
32
- NODE_TYPES.STYLE,
33
- NODE_TYPES.XMP,
34
- ];
30
+ const IGNORING_NODES = ["pre", "xmp", "script", "style"];
35
31
 
36
32
  /**
37
33
  * @type {Rule}
@@ -67,219 +63,209 @@ module.exports = {
67
63
  },
68
64
  create(context) {
69
65
  const sourceCode = context.getSourceCode();
66
+ let indentLevel = -1;
67
+ let parentIgnoringChildCount = 0;
70
68
 
71
- const indentLevel = new IndentLevel();
72
- const { indentType, indentSize } = getIndentTypeAndSize(context.options);
73
- const indentUnit =
74
- indentType === INDENT_TYPES.SPACE ? " ".repeat(indentSize) : "\t";
69
+ const { indentType, indentSize, indentChar } = (function () {
70
+ const options = context.options;
71
+ /**
72
+ * @type {IndentType['SPACE'] | IndentType['TAB']}
73
+ */
74
+ let indentType = INDENT_TYPES.SPACE;
75
+ let indentSize = 4;
76
+ if (options.length) {
77
+ if (options[0] === INDENT_TYPES.TAB) {
78
+ indentType = INDENT_TYPES.TAB;
79
+ } else {
80
+ indentSize = options[0];
81
+ }
82
+ }
83
+ const indentChar =
84
+ indentType === INDENT_TYPES.SPACE ? " ".repeat(indentSize) : "\t";
85
+ return { indentType, indentSize, indentChar };
86
+ })();
87
+
88
+ function indent() {
89
+ indentLevel++;
90
+ }
91
+ function unindent() {
92
+ indentLevel--;
93
+ }
75
94
 
76
95
  /**
77
96
  * @param {string} str
97
+ * @returns {number}
78
98
  */
79
- function countIndentSize(str) {
99
+ function countLeftPadding(str) {
80
100
  return str.length - str.replace(/^[\s\t]+/, "").length;
81
101
  }
82
102
 
83
103
  /**
84
- * @param {BaseNode} node
104
+ * @param {AnyNode} node
105
+ * @returns {node is LineNode}
85
106
  */
86
- function getLineCodeBefore(node) {
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);
107
+ function isLineNode(node) {
108
+ return node.type === "Line";
97
109
  }
98
110
 
99
111
  /**
100
- * @param {BaseNode} node
101
- * @param {BaseNode} [nodeToReport]
112
+ * @param {AnyNode} node
113
+ * @returns {string}
102
114
  */
103
- function checkIndent(node, nodeToReport) {
104
- const codeBefore = getLineCodeBefore(node);
105
- if (codeBefore.trim().length === 0) {
106
- const level = indentLevel.get();
107
- const expectedIndent = indentUnit.repeat(level);
108
- if (codeBefore !== expectedIndent) {
109
- const expected = `${
110
- indentType === INDENT_TYPES.SPACE ? level * indentSize : level
111
- } ${indentType}`;
112
- const actualTabs = (codeBefore.match(/\t/g) || []).length;
113
- const actualSpaces = (codeBefore.match(/[^\S\t\n\r]/g) || []).length;
114
-
115
- let actual = "";
116
-
117
- if (!actualTabs && !actualSpaces) {
118
- actual = "no indent";
119
- }
120
- if (actualTabs) {
121
- actual += `${actualTabs} ${INDENT_TYPES.TAB}`;
122
- }
123
- if (actualSpaces) {
124
- actual += `${actual.length ? ", " : ""}${actualSpaces} ${
125
- INDENT_TYPES.SPACE
126
- }`;
127
- }
115
+ function getActualIndent(node) {
116
+ const lines = sourceCode.getLines();
117
+ const line = lines[node.loc.start.line - 1];
118
+ let column = node.loc.start.column;
128
119
 
129
- context.report({
130
- node: nodeToReport || node,
131
- messageId: MESSAGE_ID.WRONG_INDENT,
132
- data: {
133
- expected,
134
- actual,
135
- },
136
- fix(fixer) {
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);
144
- },
145
- });
146
- }
120
+ if (isLineNode(node)) {
121
+ column += countLeftPadding(node.value);
147
122
  }
123
+
124
+ return line.slice(0, column);
148
125
  }
149
126
 
150
127
  /**
151
- * @param {AnyNode} startTag
152
- * @param {AttrNode[]} attrs
128
+ * @returns {string}
153
129
  */
154
- function checkAttrsIndent(startTag, attrs) {
155
- attrs.forEach((attr) => {
156
- if (attr.loc.start.line !== startTag.loc.start.line) {
157
- checkIndent(attr);
158
- }
159
- });
130
+ function getExpectedIndent() {
131
+ return indentChar.repeat(indentLevel);
160
132
  }
161
133
 
162
134
  /**
163
- * @param {BaseNode} startTag
135
+ * @param {AnyNode} node
136
+ * @param {string} actualIndent
137
+ * @return {BaseNode}
164
138
  */
165
- function checkEndOfStartTag(startTag) {
166
- const start = startTag.range[1] - 1;
167
- const end = startTag.range[1];
168
- const line = startTag.loc.end.line;
169
- const endCol = startTag.loc.end.column;
170
- const startCol = startTag.loc.end.column - 1;
139
+ function getIndentNodeToReport(node, actualIndent) {
140
+ let rangeStart = node.range[0];
141
+
142
+ if (node.type !== "Line") {
143
+ rangeStart -= actualIndent.length;
144
+ }
171
145
 
172
- checkIndent({
173
- range: [start, end],
174
- start,
175
- end,
146
+ return {
147
+ range: [rangeStart, rangeStart + actualIndent.length],
176
148
  loc: {
177
149
  start: {
178
- line,
179
- column: startCol,
150
+ column: 0,
151
+ line: node.loc.start.line,
180
152
  },
181
153
  end: {
182
- line,
183
- column: endCol,
154
+ column: actualIndent.length,
155
+ line: node.loc.start.line,
184
156
  },
185
157
  },
186
- });
158
+ };
187
159
  }
188
- let nodesToIgnoreChildren = [];
189
- return {
190
- /**
191
- * @param {ElementNode} node
192
- */
193
- "*"(node) {
194
- if (IGNORING_NODES.includes(node.type)) {
195
- nodesToIgnoreChildren.push(node);
196
- return;
160
+
161
+ /**
162
+ * @param {string} actualIndent
163
+ * @param {number} expectedIndentSize
164
+ */
165
+ function getMessageData(actualIndent, expectedIndentSize) {
166
+ const actualTabs = (actualIndent.match(/\t/g) || []).length;
167
+ const actualSpaces = (actualIndent.match(/[^\S\t\n\r]/g) || []).length;
168
+ let actual = "";
169
+ if (!actualTabs && !actualSpaces) {
170
+ actual = "no indent";
171
+ } else {
172
+ if (actualTabs) {
173
+ actual += `${actualTabs} tab`;
197
174
  }
198
- if (nodesToIgnoreChildren.length) {
199
- return;
175
+ if (actualSpaces) {
176
+ if (actual) {
177
+ actual += ", ";
178
+ }
179
+ actual += `${actualSpaces} space`;
200
180
  }
181
+ }
201
182
 
202
- indentLevel.up();
183
+ if (indentType === "space") {
184
+ expectedIndentSize *= indentSize;
185
+ }
203
186
 
204
- if (node.startTag && Array.isArray(node.attrs)) {
205
- checkAttrsIndent(node.startTag, node.attrs);
206
- }
187
+ return {
188
+ actual,
189
+ expected: `${expectedIndentSize} ${indentType}`,
190
+ };
191
+ }
207
192
 
208
- (node.childNodes || []).forEach((current) => {
209
- if (current.startTag) {
210
- checkIndent(current.startTag);
211
- }
212
- if (current.endTag) {
213
- checkIndent(current.endTag);
214
- }
193
+ /**
194
+ * @param {AnyNode} node
195
+ */
196
+ function checkIndent(node) {
197
+ if (parentIgnoringChildCount > 0) {
198
+ return;
199
+ }
200
+ const actualIndent = getActualIndent(node);
201
+ const expectedIndent = getExpectedIndent();
202
+ if (actualIndent.trim().length) {
203
+ return;
204
+ }
205
+ if (actualIndent !== expectedIndent) {
206
+ const targetNode = getIndentNodeToReport(node, actualIndent);
207
+ context.report({
208
+ node: targetNode,
209
+ messageId: MESSAGE_ID.WRONG_INDENT,
210
+ data: getMessageData(actualIndent, indentLevel),
211
+ fix(fixer) {
212
+ return fixer.replaceText(targetNode, expectedIndent);
213
+ },
215
214
  });
215
+ }
216
+ }
216
217
 
217
- if (
218
- (NodeUtils.isTextNode(node) || NodeUtils.isCommentNode(node)) &&
219
- node.lineNodes &&
220
- node.lineNodes.length
221
- ) {
222
- if (!node.startTag) {
223
- indentLevel.down();
224
- }
225
- node.lineNodes.forEach((lineNode) => {
226
- if (lineNode.textLine.trim().length) {
227
- checkIndent(lineNode);
228
- }
229
- });
230
- if (!node.startTag) {
231
- indentLevel.up();
232
- }
218
+ return {
219
+ // Tag
220
+ Tag(node) {
221
+ if (IGNORING_NODES.includes(node.name)) {
222
+ parentIgnoringChildCount++;
233
223
  }
224
+ indent();
234
225
  },
235
- "*:exit"(node) {
236
- if (IGNORING_NODES.includes(node.type)) {
237
- nodesToIgnoreChildren.pop();
238
- return;
239
- }
240
- if (nodesToIgnoreChildren.length) {
241
- return;
226
+ OpenTagStart: checkIndent,
227
+ OpenTagEnd: checkIndent,
228
+ CloseTag: checkIndent,
229
+ "Tag:exit"(node) {
230
+ if (IGNORING_NODES.includes(node.name)) {
231
+ parentIgnoringChildCount--;
242
232
  }
243
- indentLevel.down();
233
+ unindent();
234
+ },
244
235
 
245
- if (node.startTag) {
246
- checkEndOfStartTag(node.startTag);
247
- }
236
+ // Attribute
237
+ Attribute: indent,
238
+ AttributeKey: checkIndent,
239
+ AttributeValue: checkIndent,
240
+ "Attribute:exit": unindent,
241
+
242
+ // Text
243
+ Text(node) {
244
+ indent();
245
+ const lineNodes = NodeUtils.splitToLineNodes(node);
246
+ lineNodes.forEach((lineNode) => {
247
+ if (lineNode.value.trim().length) {
248
+ checkIndent(lineNode);
249
+ }
250
+ });
248
251
  },
252
+ "Text:exit": unindent,
253
+
254
+ // Comment
255
+ Comment: indent,
256
+ CommentOpen: checkIndent,
257
+ CommentContent(node) {
258
+ indent();
259
+ const lineNodes = NodeUtils.splitToLineNodes(node);
260
+ lineNodes.forEach((lineNode) => {
261
+ if (lineNode.value.trim().length) {
262
+ checkIndent(lineNode);
263
+ }
264
+ });
265
+ },
266
+ CommentClose: checkIndent,
267
+ "Comment:exit": unindent,
268
+ "CommentContent:exit": unindent,
249
269
  };
250
270
  },
251
271
  };
252
-
253
- function getIndentTypeAndSize(options) {
254
- /**
255
- * @type {IndentType['SPACE'] | IndentType['TAB']}
256
- */
257
- let indentType = INDENT_TYPES.SPACE;
258
- let indentSize = 4;
259
- if (options.length) {
260
- if (options[0] === INDENT_TYPES.TAB) {
261
- indentType = INDENT_TYPES.TAB;
262
- } else {
263
- indentSize = options[0];
264
- }
265
- }
266
- return { indentType, indentSize };
267
- }
268
-
269
- class IndentLevel {
270
- constructor() {
271
- this.level = -1;
272
- }
273
-
274
- up() {
275
- this.level++;
276
- }
277
-
278
- down() {
279
- this.level--;
280
- }
281
-
282
- get() {
283
- return this.level;
284
- }
285
- }
@@ -46,10 +46,10 @@ module.exports = {
46
46
 
47
47
  create(context) {
48
48
  return {
49
- "*"(node) {
49
+ [["Tag", "ScriptTag", "StyleTag"].join(",")](node) {
50
50
  const roleAttr = NodeUtils.findAttr(node, "role");
51
51
  if (roleAttr) {
52
- if (ABSTRACT_ROLE_SET.has(roleAttr.value)) {
52
+ if (roleAttr.value && ABSTRACT_ROLE_SET.has(roleAttr.value.value)) {
53
53
  context.report({
54
54
  messageId: MESSAGE_IDS.UNEXPECTED,
55
55
  node: roleAttr,
@@ -31,7 +31,7 @@ module.exports = {
31
31
 
32
32
  create(context) {
33
33
  return {
34
- "*"(node) {
34
+ [["Tag", "ScriptTag", "StyleTag"].join(",")](node) {
35
35
  const accessKeyAttr = NodeUtils.findAttr(node, "accesskey");
36
36
  if (accessKeyAttr) {
37
37
  context.report({
@@ -32,13 +32,21 @@ module.exports = {
32
32
 
33
33
  create(context) {
34
34
  return {
35
- Body(node) {
35
+ Tag(node) {
36
+ if (node.name !== "body") {
37
+ return;
38
+ }
36
39
  const ariaHiddenAttr = NodeUtils.findAttr(node, "aria-hidden");
37
- if (ariaHiddenAttr && ariaHiddenAttr.value !== "false") {
38
- context.report({
39
- node: ariaHiddenAttr,
40
- messageId: MESSAGE_IDS.UNEXPECTED,
41
- });
40
+ if (ariaHiddenAttr) {
41
+ if (
42
+ (ariaHiddenAttr.value && ariaHiddenAttr.value.value !== "false") ||
43
+ !ariaHiddenAttr.value
44
+ ) {
45
+ context.report({
46
+ node: ariaHiddenAttr,
47
+ messageId: MESSAGE_IDS.UNEXPECTED,
48
+ });
49
+ }
42
50
  }
43
51
  },
44
52
  };
@@ -31,20 +31,20 @@ module.exports = {
31
31
 
32
32
  create(context) {
33
33
  return {
34
- "*"(node) {
35
- if (Array.isArray(node.attrs)) {
34
+ [["Tag", "StyleTag", "ScriptTag"].join(",")](node) {
35
+ if (Array.isArray(node.attributes)) {
36
36
  const attrsSet = new Set();
37
- node.attrs.forEach((attr) => {
38
- if (attrsSet.has(attr.name)) {
37
+ node.attributes.forEach((attr) => {
38
+ if (attr.key && attrsSet.has(attr.key.value)) {
39
39
  context.report({
40
- node: node.startTag,
40
+ node: attr,
41
41
  data: {
42
- attrName: attr.name,
42
+ attrName: attr.key.value,
43
43
  },
44
44
  messageId: MESSAGE_IDS.DUPLICATE_ATTRS,
45
45
  });
46
46
  } else {
47
- attrsSet.add(attr.name);
47
+ attrsSet.add(attr.key.value);
48
48
  }
49
49
  });
50
50
  }
@@ -33,13 +33,16 @@ module.exports = {
33
33
  const IdAttrsMap = new Map();
34
34
  return {
35
35
  "*"(node) {
36
+ if (!node.attributes || node.attributes.length <= 0) {
37
+ return;
38
+ }
36
39
  const idAttr = NodeUtils.findAttr(node, "id");
37
- if (idAttr) {
38
- if (!IdAttrsMap.has(idAttr.value)) {
39
- IdAttrsMap.set(idAttr.value, []);
40
+ if (idAttr && idAttr.value) {
41
+ if (!IdAttrsMap.has(idAttr.value.value)) {
42
+ IdAttrsMap.set(idAttr.value.value, []);
40
43
  }
41
- const nodes = IdAttrsMap.get(idAttr.value);
42
- nodes.push(idAttr);
44
+ const nodes = IdAttrsMap.get(idAttr.value.value);
45
+ nodes.push(idAttr.value);
43
46
  }
44
47
  },
45
48
  "Program:exit"() {