@html-eslint/eslint-plugin 0.25.0 → 0.27.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 (90) hide show
  1. package/lib/configs/recommended.js +6 -1
  2. package/lib/rules/element-newline.js +240 -77
  3. package/lib/rules/index.js +2 -0
  4. package/lib/rules/no-extra-spacing-attrs.js +152 -88
  5. package/lib/rules/no-extra-spacing-text.js +117 -0
  6. package/lib/types.d.ts +8 -0
  7. package/package.json +3 -3
  8. package/types/configs/recommended.d.ts +3 -1
  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/element-newline.d.ts +18 -0
  18. package/types/rules/element-newline.d.ts.map +1 -0
  19. package/types/rules/id-naming-convention.d.ts +7 -0
  20. package/types/rules/indent.d.ts +15 -0
  21. package/types/rules/indent.d.ts.map +1 -0
  22. package/types/rules/index.d.ts +1 -0
  23. package/types/rules/lowercase.d.ts +7 -0
  24. package/types/rules/no-abstract-roles.d.ts +7 -0
  25. package/types/rules/no-abstract-roles.d.ts.map +1 -0
  26. package/types/rules/no-accesskey-attrs.d.ts +7 -0
  27. package/types/rules/no-accesskey-attrs.d.ts.map +1 -0
  28. package/types/rules/no-aria-hidden-body.d.ts +4 -0
  29. package/types/rules/no-aria-hidden-body.d.ts.map +1 -0
  30. package/types/rules/no-duplicate-attrs.d.ts +7 -0
  31. package/types/rules/no-duplicate-attrs.d.ts.map +1 -0
  32. package/types/rules/no-duplicate-id.d.ts +7 -0
  33. package/types/rules/no-duplicate-id.d.ts.map +1 -0
  34. package/types/rules/no-extra-spacing-attrs.d.ts +15 -0
  35. package/types/rules/no-extra-spacing-attrs.d.ts.map +1 -0
  36. package/types/rules/no-extra-spacing-text.d.ts +8 -0
  37. package/types/rules/no-extra-spacing-text.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-doctype.d.ts +4 -0
  68. package/types/rules/require-doctype.d.ts.map +1 -0
  69. package/types/rules/require-frame-title.d.ts +4 -0
  70. package/types/rules/require-frame-title.d.ts.map +1 -0
  71. package/types/rules/require-img-alt.d.ts +5 -0
  72. package/types/rules/require-img-alt.d.ts.map +1 -0
  73. package/types/rules/require-lang.d.ts +4 -0
  74. package/types/rules/require-lang.d.ts.map +1 -0
  75. package/types/rules/require-li-container.d.ts +4 -0
  76. package/types/rules/require-meta-charset.d.ts +5 -0
  77. package/types/rules/require-meta-description.d.ts +5 -0
  78. package/types/rules/require-meta-viewport.d.ts +5 -0
  79. package/types/rules/require-open-graph-protocol.d.ts +5 -0
  80. package/types/rules/require-title.d.ts +6 -0
  81. package/types/rules/sort-attrs.d.ts +7 -0
  82. package/types/rules/sort-attrs.d.ts.map +1 -0
  83. package/types/rules/utils/array.d.ts +17 -0
  84. package/types/rules/utils/array.d.ts.map +1 -0
  85. package/types/rules/utils/naming.d.ts +25 -0
  86. package/types/rules/utils/naming.d.ts.map +1 -0
  87. package/types/rules/utils/node.d.ts +37 -0
  88. package/types/rules/utils/node.d.ts.map +1 -0
  89. package/types/constants/svg-camelcase-attributes.d.ts +0 -3
  90. package/types/constants/svg-camelcase-attributes.d.ts.map +0 -1
@@ -7,7 +7,12 @@ module.exports = {
7
7
  "@html-eslint/no-multiple-h1": "error",
8
8
  "@html-eslint/no-extra-spacing-attrs": "error",
9
9
  "@html-eslint/attrs-newline": "error",
10
- "@html-eslint/element-newline": "error",
10
+ "@html-eslint/element-newline": [
11
+ "error",
12
+ {
13
+ inline: [`$inline`],
14
+ },
15
+ ],
11
16
  "@html-eslint/no-duplicate-id": "error",
12
17
  "@html-eslint/indent": "error",
13
18
  "@html-eslint/require-li-container": "error",
@@ -3,13 +3,66 @@
3
3
  * @typedef { import("../types").ProgramNode } ProgramNode
4
4
  * @typedef { import("../types").TagNode } TagNode
5
5
  * @typedef { import("../types").BaseNode } BaseNode
6
+ * @typedef { import("../types").CommentNode } CommentNode
7
+ * @typedef { import("../types").DoctypeNode } DoctypeNode
8
+ * @typedef { import("../types").ScriptTagNode } ScriptTagNode
9
+ * @typedef { import("../types").StyleTagNode } StyleTagNode
10
+ * @typedef { import("../types").TextNode } TextNode
11
+ * @typedef { CommentNode | DoctypeNode | ScriptTagNode | StyleTagNode | TagNode | TextNode } NewlineNode
12
+ * @typedef {{
13
+ * childFirst: NewlineNode | null;
14
+ * childLast: NewlineNode | null;
15
+ * shouldBeNewline: boolean;
16
+ * }} NodeMeta
6
17
  */
7
18
 
8
19
  const { RULE_CATEGORY } = require("../constants");
9
20
 
10
21
  const MESSAGE_IDS = {
11
22
  EXPECT_NEW_LINE_AFTER: "expectAfter",
23
+ EXPECT_NEW_LINE_AFTER_OPEN: "expectAfterOpen",
12
24
  EXPECT_NEW_LINE_BEFORE: "expectBefore",
25
+ EXPECT_NEW_LINE_BEFORE_CLOSE: "expectBeforeClose",
26
+ };
27
+
28
+ /**
29
+ * @type {Object.<string, Array<string>>}
30
+ */
31
+ const PRESETS = {
32
+ // From https://developer.mozilla.org/en-US/docs/Web/HTML/Element#inline_text_semantics
33
+ $inline: `
34
+ a
35
+ abbr
36
+ b
37
+ bdi
38
+ bdo
39
+ br
40
+ cite
41
+ code
42
+ data
43
+ dfn
44
+ em
45
+ i
46
+ kbd
47
+ mark
48
+ q
49
+ rp
50
+ rt
51
+ ruby
52
+ s
53
+ samp
54
+ small
55
+ span
56
+ strong
57
+ sub
58
+ sup
59
+ time
60
+ u
61
+ var
62
+ wbr
63
+ `
64
+ .trim()
65
+ .split(`\n`),
13
66
  };
14
67
 
15
68
  /**
@@ -30,6 +83,13 @@ module.exports = {
30
83
  {
31
84
  type: "object",
32
85
  properties: {
86
+ inline: {
87
+ type: "array",
88
+ items: {
89
+ type: "string",
90
+ },
91
+ },
92
+
33
93
  skip: {
34
94
  type: "array",
35
95
  items: {
@@ -41,111 +101,214 @@ module.exports = {
41
101
  ],
42
102
  messages: {
43
103
  [MESSAGE_IDS.EXPECT_NEW_LINE_AFTER]:
44
- "There should be a linebreak after {{tag}}.",
104
+ "There should be a linebreak after {{tag}} element.",
105
+ [MESSAGE_IDS.EXPECT_NEW_LINE_AFTER_OPEN]:
106
+ "There should be a linebreak after {{tag}} open.",
45
107
  [MESSAGE_IDS.EXPECT_NEW_LINE_BEFORE]:
46
- "There should be a linebreak before {{tag}}.",
108
+ "There should be a linebreak before {{tag}} element.",
109
+ [MESSAGE_IDS.EXPECT_NEW_LINE_BEFORE_CLOSE]:
110
+ "There should be a linebreak before {{tag}} close.",
47
111
  },
48
112
  },
49
113
 
50
114
  create(context) {
51
- const option = context.options[0] || { skip: [] };
52
- const skipTags = option.skip;
53
- let skipTagCount = 0;
115
+ const option = context.options[0] || {};
116
+ const skipTags = option.skip || [];
117
+ const inlineTags = optionsOrPresets(option.inline || []);
118
+
54
119
  /**
55
- * @param {import("../types").ChildType<TagNode | ProgramNode>[]} siblings
120
+ * @param {Array<NewlineNode>} siblings
121
+ * @returns {NodeMeta} meta
56
122
  */
57
123
  function checkSiblings(siblings) {
58
- siblings
59
- .filter((node) => node.type !== "Text")
60
- .forEach((current, index, arr) => {
61
- const after = arr[index + 1];
62
- if (after) {
63
- if (isOnTheSameLine(current, after)) {
124
+ /**
125
+ * @type {NodeMeta}
126
+ */
127
+ const meta = {
128
+ childFirst: null,
129
+ childLast: null,
130
+ shouldBeNewline: false,
131
+ };
132
+
133
+ const nodesWithContent = [];
134
+ for (
135
+ let length = siblings.length, index = 0;
136
+ index < length;
137
+ index += 1
138
+ ) {
139
+ const node = siblings[index];
140
+
141
+ if (isEmptyText(node) === false) {
142
+ nodesWithContent.push(node);
143
+ }
144
+ }
145
+
146
+ for (
147
+ let length = nodesWithContent.length, index = 0;
148
+ index < length;
149
+ index += 1
150
+ ) {
151
+ const node = nodesWithContent[index];
152
+ const nodeNext = nodesWithContent[index + 1];
153
+
154
+ if (meta.childFirst === null) {
155
+ meta.childFirst = node;
156
+ }
157
+
158
+ meta.childLast = node;
159
+
160
+ const nodeShouldBeNewline = shouldBeNewline(node);
161
+
162
+ if (node.type === `Tag` && skipTags.includes(node.name) === false) {
163
+ const nodeMeta = checkSiblings(node.children);
164
+ const nodeChildShouldBeNewline = nodeMeta.shouldBeNewline;
165
+
166
+ if (nodeShouldBeNewline || nodeChildShouldBeNewline) {
167
+ meta.shouldBeNewline = true;
168
+ }
169
+
170
+ if (
171
+ nodeShouldBeNewline &&
172
+ nodeChildShouldBeNewline &&
173
+ nodeMeta.childFirst &&
174
+ nodeMeta.childLast
175
+ ) {
176
+ if (
177
+ node.openEnd.loc.end.line === nodeMeta.childFirst.loc.start.line
178
+ ) {
179
+ if (isNotNewlineStart(nodeMeta.childFirst)) {
180
+ context.report({
181
+ node: node,
182
+ messageId: MESSAGE_IDS.EXPECT_NEW_LINE_AFTER_OPEN,
183
+ data: { tag: label(node) },
184
+ fix(fixer) {
185
+ return fixer.insertTextAfter(node.openEnd, `\n`);
186
+ },
187
+ });
188
+ }
189
+ }
190
+
191
+ if (nodeMeta.childLast.loc.end.line === node.close.loc.start.line) {
192
+ if (isNotNewlineEnd(nodeMeta.childLast)) {
193
+ context.report({
194
+ node: node,
195
+ messageId: MESSAGE_IDS.EXPECT_NEW_LINE_BEFORE_CLOSE,
196
+ data: { tag: label(node, { isClose: true }) },
197
+ fix(fixer) {
198
+ return fixer.insertTextBefore(node.close, `\n`);
199
+ },
200
+ });
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ if (nodeNext && node.loc.end.line === nodeNext.loc.start.line) {
207
+ if (nodeShouldBeNewline) {
208
+ if (isNotNewlineStart(nodeNext)) {
64
209
  context.report({
65
- node: current,
210
+ node: nodeNext,
66
211
  messageId: MESSAGE_IDS.EXPECT_NEW_LINE_AFTER,
67
- // @ts-ignore
68
- data: { tag: `<${current.name}>` },
212
+ data: { tag: label(node) },
69
213
  fix(fixer) {
70
- return fixer.insertTextAfter(current, "\n");
214
+ return fixer.insertTextAfter(node, `\n`);
71
215
  },
72
216
  });
73
217
  }
218
+ } else if (shouldBeNewline(nodeNext)) {
219
+ if (isNotNewlineEnd(node)) {
220
+ context.report({
221
+ node: nodeNext,
222
+ messageId: MESSAGE_IDS.EXPECT_NEW_LINE_BEFORE,
223
+ data: { tag: label(nodeNext) },
224
+ fix(fixer) {
225
+ return fixer.insertTextBefore(nodeNext, `\n`);
226
+ },
227
+ });
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ return meta;
234
+ }
235
+
236
+ /**
237
+ * @param {NewlineNode} node
238
+ */
239
+ function isEmptyText(node) {
240
+ return node.type === `Text` && node.value.trim().length === 0;
241
+ }
242
+
243
+ /**
244
+ * @param {NewlineNode} node
245
+ */
246
+ function isNotNewlineEnd(node) {
247
+ return node.type !== `Text` || /\n\s*$/.test(node.value) === false;
248
+ }
249
+
250
+ /**
251
+ * @param {NewlineNode} node
252
+ */
253
+ function isNotNewlineStart(node) {
254
+ return node.type !== `Text` || /^\n/.test(node.value) === false;
255
+ }
256
+
257
+ /**
258
+ * @param {NewlineNode} node
259
+ * @param {{ isClose?: boolean }} options
260
+ */
261
+ function label(node, options = {}) {
262
+ const isClose = options.isClose || false;
263
+
264
+ switch (node.type) {
265
+ case `Tag`:
266
+ if (isClose) {
267
+ return `</${node.name}>`;
74
268
  }
75
- });
269
+ return `<${node.name}>`;
270
+ default:
271
+ return `<${node.type}>`;
272
+ }
76
273
  }
77
274
 
78
275
  /**
79
- * @param {TagNode} node
80
- * @param {import("../types").ChildType<TagNode>[]} children
276
+ * @param {Array<string>} options
81
277
  */
82
- function checkChild(node, children) {
83
- const targetChildren = children.filter((n) => n.type !== "Text");
84
- const first = targetChildren[0];
85
- const last = targetChildren[targetChildren.length - 1];
86
- if (first) {
87
- if (isOnTheSameLine(node.openEnd, first)) {
88
- context.report({
89
- node: node.openEnd,
90
- messageId: MESSAGE_IDS.EXPECT_NEW_LINE_AFTER,
91
- data: { tag: `<${node.name}>` },
92
- fix(fixer) {
93
- return fixer.insertTextAfter(node.openEnd, "\n");
94
- },
95
- });
278
+ function optionsOrPresets(options) {
279
+ const result = [];
280
+ for (const option of options) {
281
+ if (option in PRESETS) {
282
+ const preset = PRESETS[option];
283
+ result.push(...preset);
284
+ } else {
285
+ result.push(option);
96
286
  }
97
287
  }
288
+ return result;
289
+ }
98
290
 
99
- if (last) {
100
- if (node.close && isOnTheSameLine(node.close, last)) {
101
- context.report({
102
- node: node.close,
103
- messageId: MESSAGE_IDS.EXPECT_NEW_LINE_BEFORE,
104
- data: { tag: `</${node.name}>` },
105
- fix(fixer) {
106
- return fixer.insertTextBefore(node.close, "\n");
107
- },
108
- });
109
- }
291
+ /**
292
+ * @param {NewlineNode} node
293
+ */
294
+ function shouldBeNewline(node) {
295
+ switch (node.type) {
296
+ case `Comment`:
297
+ return /[\n\r]+/.test(node.value.value.trim());
298
+ case `Tag`:
299
+ return inlineTags.includes(node.name.toLowerCase()) === false;
300
+ case `Text`:
301
+ return /[\n\r]+/.test(node.value.trim());
302
+ default:
303
+ return true;
110
304
  }
111
305
  }
306
+
112
307
  return {
113
308
  Program(node) {
309
+ // @ts-ignore
114
310
  checkSiblings(node.body);
115
311
  },
116
- Tag(node) {
117
- if (skipTagCount > 0) {
118
- return;
119
- }
120
- if (skipTags.includes(node.name)) {
121
- skipTagCount++;
122
- return;
123
- }
124
- checkSiblings(node.children);
125
- checkChild(node, node.children);
126
- },
127
- /**
128
- * @param {TagNode} node
129
- * @returns
130
- */
131
- "Tag:exit"(node) {
132
- if (skipTags.includes(node.name)) {
133
- skipTagCount--;
134
- return;
135
- }
136
- },
137
312
  };
138
313
  },
139
314
  };
140
-
141
- /**
142
- * @param {BaseNode} nodeBefore
143
- * @param {BaseNode} nodeAfter
144
- * @returns
145
- */
146
- function isOnTheSameLine(nodeBefore, nodeAfter) {
147
- if (nodeBefore && nodeAfter) {
148
- return nodeBefore.loc.end.line === nodeAfter.loc.start.line;
149
- }
150
- return false;
151
- }
@@ -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 noExtraSpacingText = require("./no-extra-spacing-text");
9
10
  const attrsNewline = require("./attrs-newline");
10
11
  const elementNewLine = require("./element-newline");
11
12
  const noSkipHeadingLevels = require("./no-skip-heading-levels");
@@ -46,6 +47,7 @@ module.exports = {
46
47
  "no-inline-styles": noInlineStyles,
47
48
  "no-multiple-h1": noMultipleH1,
48
49
  "no-extra-spacing-attrs": noExtraSpacingAttrs,
50
+ "no-extra-spacing-text": noExtraSpacingText,
49
51
  "attrs-newline": attrsNewline,
50
52
  "element-newline": elementNewLine,
51
53
  "no-skip-heading-levels": noSkipHeadingLevels,