@html-eslint/eslint-plugin 0.13.1 → 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,8 +13,7 @@
12
13
  * @typedef {Object} MessageId
13
14
  * @property {"wrongIndent"} WRONG_INDENT
14
15
  */
15
-
16
- const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
16
+ const { RULE_CATEGORY } = require("../constants");
17
17
  const { NodeUtils } = require("./utils");
18
18
 
19
19
  /** @type {MessageId} */
@@ -27,12 +27,7 @@ const INDENT_TYPES = {
27
27
  SPACE: "space",
28
28
  };
29
29
 
30
- const IGNORING_NODES = [
31
- NODE_TYPES.PRE,
32
- NODE_TYPES.SCRIPT,
33
- NODE_TYPES.STYLE,
34
- NODE_TYPES.XMP,
35
- ];
30
+ const IGNORING_NODES = ["pre", "xmp", "script", "style"];
36
31
 
37
32
  /**
38
33
  * @type {Rule}
@@ -68,201 +63,209 @@ module.exports = {
68
63
  },
69
64
  create(context) {
70
65
  const sourceCode = context.getSourceCode();
71
- const indentLevel = new IndentLevel();
72
- const { indentType, indentSize } = getIndentTypeAndSize(context.options);
73
- const indentUnit =
74
- indentType === INDENT_TYPES.SPACE ? " ".repeat(indentSize) : "\t";
66
+ let indentLevel = -1;
67
+ let parentIgnoringChildCount = 0;
68
+
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
- * @param {BaseNode} node
96
+ * @param {string} str
97
+ * @returns {number}
78
98
  */
79
- function getLineCodeBefore(node) {
80
- return sourceCode.text
81
- .slice(node.range[0] - node.loc.start.column, node.range[0])
82
- .replace("\n", "");
99
+ function countLeftPadding(str) {
100
+ return str.length - str.replace(/^[\s\t]+/, "").length;
83
101
  }
84
102
 
85
103
  /**
86
- * @param {BaseNode} node
87
- * @param {BaseNode} [nodeToReport]
104
+ * @param {AnyNode} node
105
+ * @returns {node is LineNode}
88
106
  */
89
- function checkIndent(node, nodeToReport) {
90
- const codeBefore = getLineCodeBefore(node);
91
- if (codeBefore.trim().length === 0) {
92
- const level = indentLevel.get();
93
- const expectedIndent = indentUnit.repeat(level);
94
- if (codeBefore !== expectedIndent) {
95
- const expected = `${
96
- indentType === INDENT_TYPES.SPACE ? level * indentSize : level
97
- } ${indentType}`;
98
- const actualTabs = (codeBefore.match(/\t/g) || []).length;
99
- const actualSpaces = (codeBefore.match(/[^\S\t\n\r]/g) || []).length;
100
-
101
- let actual = "";
107
+ function isLineNode(node) {
108
+ return node.type === "Line";
109
+ }
102
110
 
103
- if (!actualTabs && !actualSpaces) {
104
- actual = "no indent";
105
- }
106
- if (actualTabs) {
107
- actual += `${actualTabs} ${INDENT_TYPES.TAB}`;
108
- }
109
- if (actualSpaces) {
110
- actual += `${actual.length ? ", " : ""}${actualSpaces} ${
111
- INDENT_TYPES.SPACE
112
- }`;
113
- }
111
+ /**
112
+ * @param {AnyNode} node
113
+ * @returns {string}
114
+ */
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;
114
119
 
115
- context.report({
116
- node: nodeToReport || node,
117
- messageId: MESSAGE_ID.WRONG_INDENT,
118
- data: {
119
- expected,
120
- actual,
121
- },
122
- fix(fixer) {
123
- return fixer.replaceTextRange(
124
- [node.range[0] - (node.loc.start.column - 1), node.range[0]],
125
- expectedIndent
126
- );
127
- },
128
- });
129
- }
120
+ if (isLineNode(node)) {
121
+ column += countLeftPadding(node.value);
130
122
  }
123
+
124
+ return line.slice(0, column);
131
125
  }
132
126
 
133
127
  /**
134
- * @param {AnyNode} startTag
135
- * @param {AttrNode[]} attrs
128
+ * @returns {string}
136
129
  */
137
- function checkAttrsIndent(startTag, attrs) {
138
- attrs.forEach((attr) => {
139
- if (attr.loc.start.line !== startTag.loc.start.line) {
140
- checkIndent(attr);
141
- }
142
- });
130
+ function getExpectedIndent() {
131
+ return indentChar.repeat(indentLevel);
143
132
  }
144
133
 
145
134
  /**
146
- * @param {AnyNode} startTag
135
+ * @param {AnyNode} node
136
+ * @param {string} actualIndent
137
+ * @return {BaseNode}
147
138
  */
148
- function checkEndOfStartTag(startTag) {
149
- const start = startTag.range[1] - 1;
150
- const end = startTag.range[1];
151
- const line = startTag.loc.end.line;
152
- const endCol = startTag.loc.end.column;
153
- const startCol = startTag.loc.end.column - 1;
139
+ function getIndentNodeToReport(node, actualIndent) {
140
+ let rangeStart = node.range[0];
154
141
 
155
- checkIndent({
156
- range: [start, end],
157
- start,
158
- end,
142
+ if (node.type !== "Line") {
143
+ rangeStart -= actualIndent.length;
144
+ }
145
+
146
+ return {
147
+ range: [rangeStart, rangeStart + actualIndent.length],
159
148
  loc: {
160
149
  start: {
161
- line,
162
- column: startCol,
150
+ column: 0,
151
+ line: node.loc.start.line,
163
152
  },
164
153
  end: {
165
- line,
166
- column: endCol,
154
+ column: actualIndent.length,
155
+ line: node.loc.start.line,
167
156
  },
168
157
  },
169
- });
158
+ };
170
159
  }
171
- let nodesToIgnoreChildren = [];
172
- return {
173
- /**
174
- * @param {ElementNode} node
175
- */
176
- "*"(node) {
177
- if (IGNORING_NODES.includes(node.type)) {
178
- nodesToIgnoreChildren.push(node);
179
- 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`;
180
174
  }
181
- if (nodesToIgnoreChildren.length) {
182
- return;
175
+ if (actualSpaces) {
176
+ if (actual) {
177
+ actual += ", ";
178
+ }
179
+ actual += `${actualSpaces} space`;
183
180
  }
181
+ }
184
182
 
185
- indentLevel.up();
183
+ if (indentType === "space") {
184
+ expectedIndentSize *= indentSize;
185
+ }
186
186
 
187
- if (node.startTag && Array.isArray(node.attrs)) {
188
- checkAttrsIndent(node.startTag, node.attrs);
189
- }
187
+ return {
188
+ actual,
189
+ expected: `${expectedIndentSize} ${indentType}`,
190
+ };
191
+ }
190
192
 
191
- (node.childNodes || []).forEach((current) => {
192
- if (current.startTag) {
193
- checkIndent(current.startTag);
194
- }
195
- if (current.endTag) {
196
- checkIndent(current.endTag);
197
- }
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
+ },
198
214
  });
215
+ }
216
+ }
199
217
 
200
- if (
201
- (NodeUtils.isTextNode(node) || NodeUtils.isCommentNode(node)) &&
202
- node.lineNodes &&
203
- node.lineNodes.length
204
- ) {
205
- if (!node.startTag) {
206
- indentLevel.down();
207
- }
208
- node.lineNodes.forEach((lineNode) => {
209
- if (lineNode.textLine.trim().length) {
210
- checkIndent(lineNode, node);
211
- }
212
- });
213
- if (!node.startTag) {
214
- indentLevel.up();
215
- }
218
+ return {
219
+ // Tag
220
+ Tag(node) {
221
+ if (IGNORING_NODES.includes(node.name)) {
222
+ parentIgnoringChildCount++;
216
223
  }
224
+ indent();
217
225
  },
218
- "*:exit"(node) {
219
- if (IGNORING_NODES.includes(node.type)) {
220
- nodesToIgnoreChildren.pop();
221
- return;
222
- }
223
- if (nodesToIgnoreChildren.length) {
224
- return;
226
+ OpenTagStart: checkIndent,
227
+ OpenTagEnd: checkIndent,
228
+ CloseTag: checkIndent,
229
+ "Tag:exit"(node) {
230
+ if (IGNORING_NODES.includes(node.name)) {
231
+ parentIgnoringChildCount--;
225
232
  }
226
- indentLevel.down();
233
+ unindent();
234
+ },
227
235
 
228
- if (node.startTag) {
229
- checkEndOfStartTag(node.startTag);
230
- }
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
+ });
231
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,
232
269
  };
233
270
  },
234
271
  };
235
-
236
- function getIndentTypeAndSize(options) {
237
- /**
238
- * @type {IndentType['SPACE'] | IndentType['TAB']}
239
- */
240
- let indentType = INDENT_TYPES.SPACE;
241
- let indentSize = 4;
242
- if (options.length) {
243
- if (options[0] === INDENT_TYPES.TAB) {
244
- indentType = INDENT_TYPES.TAB;
245
- } else {
246
- indentSize = options[0];
247
- }
248
- }
249
- return { indentType, indentSize };
250
- }
251
-
252
- class IndentLevel {
253
- constructor() {
254
- this.level = -1;
255
- }
256
-
257
- up() {
258
- this.level++;
259
- }
260
-
261
- down() {
262
- this.level--;
263
- }
264
-
265
- get() {
266
- return this.level;
267
- }
268
- }
@@ -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"() {