@html-eslint/eslint-plugin 0.24.1 → 0.26.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.
Files changed (91) hide show
  1. package/lib/configs/recommended.js +1 -0
  2. package/lib/rules/attrs-newline.js +163 -0
  3. package/lib/rules/index.js +2 -0
  4. package/lib/rules/no-extra-spacing-attrs.js +121 -87
  5. package/lib/rules/require-closing-tags.js +45 -16
  6. package/package.json +3 -3
  7. package/types/configs/recommended.d.ts +18 -0
  8. package/types/configs/recommended.d.ts.map +1 -0
  9. package/types/constants/index.d.ts +5 -0
  10. package/types/constants/index.d.ts.map +1 -0
  11. package/types/constants/obsolete-tags.d.ts +3 -0
  12. package/types/constants/obsolete-tags.d.ts.map +1 -0
  13. package/types/constants/rule-category.d.ts +5 -0
  14. package/types/constants/rule-category.d.ts.map +1 -0
  15. package/types/constants/void-elements.d.ts +3 -0
  16. package/types/constants/void-elements.d.ts.map +1 -0
  17. package/types/rules/attrs-newline.d.ts +11 -0
  18. package/types/rules/attrs-newline.d.ts.map +1 -0
  19. package/types/rules/element-newline.d.ts +7 -0
  20. package/types/rules/element-newline.d.ts.map +1 -0
  21. package/types/rules/id-naming-convention.d.ts +7 -0
  22. package/types/rules/indent.d.ts +15 -0
  23. package/types/rules/indent.d.ts.map +1 -0
  24. package/types/rules/index.d.ts +42 -0
  25. package/types/rules/lowercase.d.ts +7 -0
  26. package/types/rules/no-abstract-roles.d.ts +7 -0
  27. package/types/rules/no-abstract-roles.d.ts.map +1 -0
  28. package/types/rules/no-accesskey-attrs.d.ts +7 -0
  29. package/types/rules/no-accesskey-attrs.d.ts.map +1 -0
  30. package/types/rules/no-aria-hidden-body.d.ts +4 -0
  31. package/types/rules/no-aria-hidden-body.d.ts.map +1 -0
  32. package/types/rules/no-duplicate-attrs.d.ts +7 -0
  33. package/types/rules/no-duplicate-attrs.d.ts.map +1 -0
  34. package/types/rules/no-duplicate-id.d.ts +7 -0
  35. package/types/rules/no-duplicate-id.d.ts.map +1 -0
  36. package/types/rules/no-extra-spacing-attrs.d.ts +15 -0
  37. package/types/rules/no-extra-spacing-attrs.d.ts.map +1 -0
  38. package/types/rules/no-inline-styles.d.ts +4 -0
  39. package/types/rules/no-inline-styles.d.ts.map +1 -0
  40. package/types/rules/no-multiple-empty-lines.d.ts +5 -0
  41. package/types/rules/no-multiple-empty-lines.d.ts.map +1 -0
  42. package/types/rules/no-multiple-h1.d.ts +5 -0
  43. package/types/rules/no-multiple-h1.d.ts.map +1 -0
  44. package/types/rules/no-non-scalable-viewport.d.ts +4 -0
  45. package/types/rules/no-non-scalable-viewport.d.ts.map +1 -0
  46. package/types/rules/no-obsolete-tags.d.ts +4 -0
  47. package/types/rules/no-obsolete-tags.d.ts.map +1 -0
  48. package/types/rules/no-positive-tabindex.d.ts +7 -0
  49. package/types/rules/no-positive-tabindex.d.ts.map +1 -0
  50. package/types/rules/no-restricted-attr-values.d.ts +13 -0
  51. package/types/rules/no-restricted-attr-values.d.ts.map +1 -0
  52. package/types/rules/no-restricted-attrs.d.ts +13 -0
  53. package/types/rules/no-script-style-type.d.ts +7 -0
  54. package/types/rules/no-script-style-type.d.ts.map +1 -0
  55. package/types/rules/no-skip-heading-levels.d.ts +5 -0
  56. package/types/rules/no-skip-heading-levels.d.ts.map +1 -0
  57. package/types/rules/no-target-blank.d.ts +4 -0
  58. package/types/rules/no-target-blank.d.ts.map +1 -0
  59. package/types/rules/no-trailing-spaces.d.ts +4 -0
  60. package/types/rules/no-trailing-spaces.d.ts.map +1 -0
  61. package/types/rules/quotes.d.ts +9 -0
  62. package/types/rules/quotes.d.ts.map +1 -0
  63. package/types/rules/require-attrs.d.ts +7 -0
  64. package/types/rules/require-button-type.d.ts +4 -0
  65. package/types/rules/require-button-type.d.ts.map +1 -0
  66. package/types/rules/require-closing-tags.d.ts +5 -0
  67. package/types/rules/require-closing-tags.d.ts.map +1 -0
  68. package/types/rules/require-doctype.d.ts +4 -0
  69. package/types/rules/require-doctype.d.ts.map +1 -0
  70. package/types/rules/require-frame-title.d.ts +4 -0
  71. package/types/rules/require-frame-title.d.ts.map +1 -0
  72. package/types/rules/require-img-alt.d.ts +5 -0
  73. package/types/rules/require-img-alt.d.ts.map +1 -0
  74. package/types/rules/require-lang.d.ts +4 -0
  75. package/types/rules/require-lang.d.ts.map +1 -0
  76. package/types/rules/require-li-container.d.ts +4 -0
  77. package/types/rules/require-meta-charset.d.ts +5 -0
  78. package/types/rules/require-meta-description.d.ts +5 -0
  79. package/types/rules/require-meta-viewport.d.ts +5 -0
  80. package/types/rules/require-open-graph-protocol.d.ts +5 -0
  81. package/types/rules/require-title.d.ts +6 -0
  82. package/types/rules/sort-attrs.d.ts +7 -0
  83. package/types/rules/sort-attrs.d.ts.map +1 -0
  84. package/types/rules/utils/array.d.ts +17 -0
  85. package/types/rules/utils/array.d.ts.map +1 -0
  86. package/types/rules/utils/naming.d.ts +25 -0
  87. package/types/rules/utils/naming.d.ts.map +1 -0
  88. package/types/rules/utils/node.d.ts +37 -0
  89. package/types/rules/utils/node.d.ts.map +1 -0
  90. package/types/constants/svg-camelcase-attributes.d.ts +0 -3
  91. package/types/constants/svg-camelcase-attributes.d.ts.map +0 -1
@@ -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
+ };
@@ -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,
@@ -20,9 +20,13 @@ const MESSAGE_IDS = {
20
20
  EXTRA_BETWEEN: "unexpectedBetween",
21
21
  EXTRA_AFTER: "unexpectedAfter",
22
22
  EXTRA_BEFORE: "unexpectedBefore",
23
+ EXTRA_BEFORE_CLOSE: "unexpectedBeforeClose",
23
24
  MISSING_BEFORE: "missingBefore",
24
25
  MISSING_BEFORE_SELF_CLOSE: "missingBeforeSelfClose",
25
26
  EXTRA_BEFORE_SELF_CLOSE: "unexpectedBeforeSelfClose",
27
+ EXTRA_TAB_BEFORE: "unexpectedTabBefore",
28
+ EXTRA_TAB_BEFORE_SELF_CLOSE: "unexpectedTabBeforeSelfClose",
29
+ EXTRA_TAB_BETWEEN: "unexpectedTabBetween",
26
30
  };
27
31
 
28
32
  /**
@@ -46,6 +50,9 @@ module.exports = {
46
50
  disallowMissing: {
47
51
  type: "boolean",
48
52
  },
53
+ disallowTabs: {
54
+ type: "boolean",
55
+ },
49
56
  enforceBeforeSelfClose: {
50
57
  type: "boolean",
51
58
  },
@@ -56,17 +63,27 @@ module.exports = {
56
63
  [MESSAGE_IDS.EXTRA_BETWEEN]: "Unexpected space between attributes",
57
64
  [MESSAGE_IDS.EXTRA_AFTER]: "Unexpected space after attribute",
58
65
  [MESSAGE_IDS.EXTRA_BEFORE]: "Unexpected space before attribute",
66
+ [MESSAGE_IDS.EXTRA_BEFORE_CLOSE]: "Unexpected space before closing",
59
67
  [MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE]:
60
68
  "Missing space before self closing",
61
69
  [MESSAGE_IDS.EXTRA_BEFORE_SELF_CLOSE]:
62
70
  "Unexpected extra spaces before self closing",
63
71
  [MESSAGE_IDS.MISSING_BEFORE]: "Missing space before attribute",
72
+ [MESSAGE_IDS.EXTRA_TAB_BEFORE]:
73
+ "Unexpected tab before attribute; use space instead",
74
+ [MESSAGE_IDS.EXTRA_TAB_BEFORE_SELF_CLOSE]:
75
+ "Unexpected tab before self closing; use space instead",
76
+ [MESSAGE_IDS.EXTRA_TAB_BETWEEN]:
77
+ "Unexpected tab between attributes; use space instead",
64
78
  },
65
79
  },
66
80
  create(context) {
67
81
  const enforceBeforeSelfClose = !!(context.options[0] || {})
68
82
  .enforceBeforeSelfClose;
69
83
  const disallowMissing = !!(context.options[0] || {}).disallowMissing;
84
+ const disallowTabs = !!(context.options[0] || {}).disallowTabs;
85
+
86
+ const sourceCode = context.getSourceCode().text;
70
87
 
71
88
  /**
72
89
  * @param {AttributeNode[]} attrs
@@ -98,48 +115,23 @@ module.exports = {
98
115
  return fixer.insertTextAfter(current, " ");
99
116
  },
100
117
  });
118
+ } else if (disallowTabs) {
119
+ if (sourceCode[current.range[1]] === `\t`) {
120
+ context.report({
121
+ loc: getLocBetween(current, after),
122
+ messageId: MESSAGE_IDS.EXTRA_TAB_BETWEEN,
123
+ fix(fixer) {
124
+ return fixer.replaceTextRange(
125
+ [current.range[1], current.range[1] + 1],
126
+ ` `
127
+ );
128
+ },
129
+ });
130
+ }
101
131
  }
102
132
  });
103
133
  }
104
134
 
105
- /**
106
- * @param {OpenTagEndNode | OpenScriptTagEndNode | OpenStyleTagEndNode} openEnd
107
- * @param {AttributeNode} lastAttr
108
- * @param {boolean} isSelfClosed
109
- * @returns {void}
110
- */
111
- function checkExtraSpaceAfter(openEnd, lastAttr, isSelfClosed) {
112
- if (openEnd.loc.end.line !== lastAttr.loc.end.line) {
113
- // skip the attribute on the different line with the start tag
114
- return;
115
- }
116
- const limit = isSelfClosed && enforceBeforeSelfClose ? 1 : 0;
117
- const spacesBetween = openEnd.loc.start.column - lastAttr.loc.end.column;
118
-
119
- if (spacesBetween > limit) {
120
- context.report({
121
- loc: getLocBetween(lastAttr, openEnd),
122
- messageId: MESSAGE_IDS.EXTRA_AFTER,
123
- fix(fixer) {
124
- return fixer.removeRange([
125
- lastAttr.range[1],
126
- lastAttr.range[1] + spacesBetween - limit,
127
- ]);
128
- },
129
- });
130
- }
131
-
132
- if (isSelfClosed && enforceBeforeSelfClose && spacesBetween < 1) {
133
- context.report({
134
- loc: getLocBetween(lastAttr, openEnd),
135
- messageId: MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE,
136
- fix(fixer) {
137
- return fixer.insertTextAfter(lastAttr, " ");
138
- },
139
- });
140
- }
141
- }
142
-
143
135
  /**
144
136
  * @param {OpenScriptTagStartNode | OpenTagStartNode | OpenStyleTagStartNode} node
145
137
  * @param {AttributeNode} firstAttr
@@ -164,42 +156,19 @@ module.exports = {
164
156
  ]);
165
157
  },
166
158
  });
167
- }
168
- }
169
-
170
- /**
171
- * @param {AnyNode} beforeSelfClosing
172
- * @param {OpenTagEndNode | OpenScriptTagEndNode | OpenStyleTagEndNode} openEnd
173
- * @returns
174
- */
175
- function checkSpaceBeforeSelfClosing(beforeSelfClosing, openEnd) {
176
- if (beforeSelfClosing.loc.start.line !== openEnd.loc.start.line) {
177
- // skip the attribute on the different line with the start tag
178
- return;
179
- }
180
- const spacesBetween =
181
- openEnd.loc.start.column - beforeSelfClosing.loc.end.column;
182
- const locBetween = getLocBetween(beforeSelfClosing, openEnd);
183
-
184
- if (spacesBetween > 1) {
185
- context.report({
186
- loc: locBetween,
187
- messageId: MESSAGE_IDS.EXTRA_BEFORE_SELF_CLOSE,
188
- fix(fixer) {
189
- return fixer.removeRange([
190
- beforeSelfClosing.range[1] + 1,
191
- openEnd.range[0],
192
- ]);
193
- },
194
- });
195
- } else if (spacesBetween < 1) {
196
- context.report({
197
- loc: locBetween,
198
- messageId: MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE,
199
- fix(fixer) {
200
- return fixer.insertTextAfter(beforeSelfClosing, " ");
201
- },
202
- });
159
+ } else if (disallowTabs) {
160
+ if (sourceCode[firstAttr.range[0] - 1] === `\t`) {
161
+ context.report({
162
+ loc: firstAttr.loc,
163
+ messageId: MESSAGE_IDS.EXTRA_TAB_BEFORE,
164
+ fix(fixer) {
165
+ return fixer.replaceTextRange(
166
+ [firstAttr.range[0] - 1, firstAttr.range[0]],
167
+ ` `
168
+ );
169
+ },
170
+ });
171
+ }
203
172
  }
204
173
  }
205
174
 
@@ -216,24 +185,89 @@ module.exports = {
216
185
  if (node.attributes.length) {
217
186
  checkExtraSpaceBefore(node.openStart, node.attributes[0]);
218
187
  }
188
+
219
189
  if (node.openEnd) {
190
+ checkExtraSpacesBetweenAttrs(node.attributes);
191
+
192
+ const lastAttr = node.attributes[node.attributes.length - 1];
193
+ const nodeBeforeEnd =
194
+ node.attributes.length === 0 ? node.openStart : lastAttr;
195
+
196
+ if (nodeBeforeEnd.loc.end.line !== node.openEnd.loc.start.line) {
197
+ return;
198
+ }
199
+
220
200
  const isSelfClosing = node.openEnd.value === "/>";
221
201
 
222
- if (node.attributes && node.attributes.length > 0) {
223
- checkExtraSpaceAfter(
224
- node.openEnd,
225
- node.attributes[node.attributes.length - 1],
226
- isSelfClosing
227
- );
202
+ const spacesBetween =
203
+ node.openEnd.loc.start.column - nodeBeforeEnd.loc.end.column;
204
+ const locBetween = getLocBetween(nodeBeforeEnd, node.openEnd);
205
+
206
+ if (isSelfClosing && enforceBeforeSelfClose) {
207
+ if (spacesBetween < 1) {
208
+ context.report({
209
+ loc: locBetween,
210
+ messageId: MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE,
211
+ fix(fixer) {
212
+ return fixer.insertTextAfter(nodeBeforeEnd, " ");
213
+ },
214
+ });
215
+ } else if (spacesBetween === 1) {
216
+ if (
217
+ disallowTabs &&
218
+ sourceCode[node.openEnd.range[0] - 1] === `\t`
219
+ ) {
220
+ context.report({
221
+ loc: node.openEnd.loc,
222
+ messageId: MESSAGE_IDS.EXTRA_TAB_BEFORE_SELF_CLOSE,
223
+ fix(fixer) {
224
+ return fixer.replaceTextRange(
225
+ [node.openEnd.range[0] - 1, node.openEnd.range[0]],
226
+ ` `
227
+ );
228
+ },
229
+ });
230
+ }
231
+ } else {
232
+ context.report({
233
+ loc: locBetween,
234
+ messageId: MESSAGE_IDS.EXTRA_BEFORE_SELF_CLOSE,
235
+ fix(fixer) {
236
+ return fixer.removeRange([
237
+ nodeBeforeEnd.range[1] + 1,
238
+ node.openEnd.range[0],
239
+ ]);
240
+ },
241
+ });
242
+ }
243
+
244
+ return;
228
245
  }
229
246
 
230
- checkExtraSpacesBetweenAttrs(node.attributes);
231
- if (
232
- node.attributes.length === 0 &&
233
- isSelfClosing &&
234
- enforceBeforeSelfClose
235
- ) {
236
- checkSpaceBeforeSelfClosing(node.openStart, node.openEnd);
247
+ if (spacesBetween > 0) {
248
+ if (node.attributes.length > 0) {
249
+ context.report({
250
+ loc: locBetween,
251
+ messageId: MESSAGE_IDS.EXTRA_AFTER,
252
+ fix(fixer) {
253
+ return fixer.removeRange([
254
+ lastAttr.range[1],
255
+ node.openEnd.range[0],
256
+ ]);
257
+ },
258
+ });
259
+ } else {
260
+ context.report({
261
+ loc: locBetween,
262
+ messageId: MESSAGE_IDS.EXTRA_BEFORE_CLOSE,
263
+ fix(fixer) {
264
+ return fixer.removeRange([
265
+ node.openStart.range[1],
266
+ node.openEnd.range[0],
267
+ ]);
268
+ },
269
+ });
270
+ }
237
271
  }
238
272
  }
239
273
  },
@@ -34,8 +34,11 @@ module.exports = {
34
34
  selfClosing: {
35
35
  enum: ["always", "never"],
36
36
  },
37
- allowSelfClosingCustom: {
38
- type: "boolean",
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
- const shouldSelfClose =
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
- const allowSelfClosingCustom =
57
- context.options && context.options.length
58
- ? context.options[0].allowSelfClosingCustom === true
59
- : false;
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
- return fixer.replaceText(node.openEnd, " />");
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
- if (
119
- node.selfClosing &&
120
- allowSelfClosingCustom &&
121
- node.name.indexOf("-") !== -1
122
- ) {
123
- checkVoidElement(node, true, false);
124
- } else if (node.selfClosing || isVoidElement) {
125
- checkVoidElement(node, shouldSelfClose, isVoidElement);
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.24.1",
3
+ "version": "0.26.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.24.1",
48
+ "@html-eslint/parser": "^0.26.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": "c9095f0738fea1688099937ebc0a93b1a26b7da9"
54
+ "gitHead": "34d55c3b5be5a29cc416063b4b4375cb89b3a519"
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,5 @@
1
+ import RULE_CATEGORY = require("./rule-category");
2
+ import OBSOLETE_TAGS = require("./obsolete-tags");
3
+ import VOID_ELEMENTS = require("./void-elements");
4
+ export { RULE_CATEGORY, OBSOLETE_TAGS, VOID_ELEMENTS };
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/constants/index.js"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ declare const _exports: string[];
2
+ export = _exports;
3
+ //# sourceMappingURL=obsolete-tags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"obsolete-tags.d.ts","sourceRoot":"","sources":["../../lib/constants/obsolete-tags.js"],"names":[],"mappings":"wBACW,MAAM,EAAE"}
@@ -0,0 +1,5 @@
1
+ export const BEST_PRACTICE: string;
2
+ export const SEO: string;
3
+ export const ACCESSIBILITY: string;
4
+ export const STYLE: string;
5
+ //# sourceMappingURL=rule-category.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-category.d.ts","sourceRoot":"","sources":["../../lib/constants/rule-category.js"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ declare const _exports: string[];
2
+ export = _exports;
3
+ //# sourceMappingURL=void-elements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"void-elements.d.ts","sourceRoot":"","sources":["../../lib/constants/void-elements.js"],"names":[],"mappings":"wBAAW,MAAM,EAAE"}
@@ -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,7 @@
1
+ declare const _exports: RuleModule;
2
+ export = _exports;
3
+ export type RuleModule = import("../types").RuleModule;
4
+ export type ProgramNode = import("../types").ProgramNode;
5
+ export type TagNode = import("../types").TagNode;
6
+ export type BaseNode = import("../types").BaseNode;
7
+ //# sourceMappingURL=element-newline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"element-newline.d.ts","sourceRoot":"","sources":["../../lib/rules/element-newline.js"],"names":[],"mappings":"wBAeU,UAAU;;yBAdN,OAAO,UAAU,EAAE,UAAU;0BAC7B,OAAO,UAAU,EAAE,WAAW;sBAC9B,OAAO,UAAU,EAAE,OAAO;uBAC1B,OAAO,UAAU,EAAE,QAAQ"}
@@ -0,0 +1,7 @@
1
+ declare const _exports: RuleModule;
2
+ export = _exports;
3
+ export type RuleModule = import("../types").RuleModule;
4
+ export type TagNode = import("../types").TagNode;
5
+ export type ScriptTagNode = import("../types").ScriptTagNode;
6
+ export type StyleTagNode = import("../types").StyleTagNode;
7
+ //# sourceMappingURL=id-naming-convention.d.ts.map