@html-eslint/eslint-plugin 0.26.0 → 0.28.0-alpha.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.
@@ -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,
@@ -21,6 +21,7 @@ const MESSAGE_IDS = {
21
21
  EXTRA_AFTER: "unexpectedAfter",
22
22
  EXTRA_BEFORE: "unexpectedBefore",
23
23
  EXTRA_BEFORE_CLOSE: "unexpectedBeforeClose",
24
+ EXTRA_IN_ASSIGNMENT: "unexpectedInAssignment",
24
25
  MISSING_BEFORE: "missingBefore",
25
26
  MISSING_BEFORE_SELF_CLOSE: "missingBeforeSelfClose",
26
27
  EXTRA_BEFORE_SELF_CLOSE: "unexpectedBeforeSelfClose",
@@ -47,6 +48,9 @@ module.exports = {
47
48
  {
48
49
  type: "object",
49
50
  properties: {
51
+ disallowInAssignment: {
52
+ type: "boolean",
53
+ },
50
54
  disallowMissing: {
51
55
  type: "boolean",
52
56
  },
@@ -64,6 +68,8 @@ module.exports = {
64
68
  [MESSAGE_IDS.EXTRA_AFTER]: "Unexpected space after attribute",
65
69
  [MESSAGE_IDS.EXTRA_BEFORE]: "Unexpected space before attribute",
66
70
  [MESSAGE_IDS.EXTRA_BEFORE_CLOSE]: "Unexpected space before closing",
71
+ [MESSAGE_IDS.EXTRA_IN_ASSIGNMENT]:
72
+ "Unexpected space in attribute assignment",
67
73
  [MESSAGE_IDS.MISSING_BEFORE_SELF_CLOSE]:
68
74
  "Missing space before self closing",
69
75
  [MESSAGE_IDS.EXTRA_BEFORE_SELF_CLOSE]:
@@ -82,6 +88,8 @@ module.exports = {
82
88
  .enforceBeforeSelfClose;
83
89
  const disallowMissing = !!(context.options[0] || {}).disallowMissing;
84
90
  const disallowTabs = !!(context.options[0] || {}).disallowTabs;
91
+ const disallowInAssignment = !!(context.options[0] || [])
92
+ .disallowInAssignment;
85
93
 
86
94
  const sourceCode = context.getSourceCode().text;
87
95
 
@@ -104,7 +112,10 @@ module.exports = {
104
112
  loc: getLocBetween(current, after),
105
113
  messageId: MESSAGE_IDS.EXTRA_BETWEEN,
106
114
  fix(fixer) {
107
- return fixer.removeRange([current.range[1] + 1, after.range[0]]);
115
+ return fixer.replaceTextRange(
116
+ [current.range[1], after.range[0]],
117
+ ` `
118
+ );
108
119
  },
109
120
  });
110
121
  } else if (disallowMissing && spacesBetween < 1) {
@@ -122,7 +133,7 @@ module.exports = {
122
133
  messageId: MESSAGE_IDS.EXTRA_TAB_BETWEEN,
123
134
  fix(fixer) {
124
135
  return fixer.replaceTextRange(
125
- [current.range[1], current.range[1] + 1],
136
+ [current.range[1], after.range[0]],
126
137
  ` `
127
138
  );
128
139
  },
@@ -184,6 +195,25 @@ module.exports = {
184
195
 
185
196
  if (node.attributes.length) {
186
197
  checkExtraSpaceBefore(node.openStart, node.attributes[0]);
198
+
199
+ for (const attr of node.attributes) {
200
+ if (attr.startWrapper && attr.value) {
201
+ if (
202
+ disallowInAssignment &&
203
+ attr.startWrapper.loc.start.column - attr.key.loc.end.column > 1
204
+ ) {
205
+ const start = attr.key.range[1];
206
+ const end = attr.startWrapper.range[0];
207
+ context.report({
208
+ node: attr,
209
+ messageId: MESSAGE_IDS.EXTRA_IN_ASSIGNMENT,
210
+ fix(fixer) {
211
+ return fixer.replaceTextRange([start, end], `=`);
212
+ },
213
+ });
214
+ }
215
+ }
216
+ }
187
217
  }
188
218
 
189
219
  if (node.openEnd) {
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @typedef { import("../types").RuleModule } RuleModule
3
+ * @typedef { import("../types").ProgramNode } ProgramNode
4
+ * @typedef { import("es-html-parser").CommentContentNode } CommentContentNode
5
+ * @typedef { import("../types").ContentNode } ContentNode
6
+ * @typedef { import("../types").TextNode } TextNode
7
+ */
8
+
9
+ const { RULE_CATEGORY } = require("../constants");
10
+
11
+ const MESSAGE_IDS = {
12
+ UNEXPECTED: "unexpected",
13
+ };
14
+
15
+ /**
16
+ * @type {RuleModule}
17
+ */
18
+ module.exports = {
19
+ meta: {
20
+ type: "code",
21
+
22
+ docs: {
23
+ description: "Disallow unnecessary consecutive spaces",
24
+ category: RULE_CATEGORY.BEST_PRACTICE,
25
+ recommended: false,
26
+ },
27
+
28
+ fixable: true,
29
+ schema: [
30
+ {
31
+ type: "object",
32
+ properties: {
33
+ skip: {
34
+ type: "array",
35
+ items: {
36
+ type: "string",
37
+ },
38
+ },
39
+ },
40
+ additionalProperties: false,
41
+ },
42
+ ],
43
+ messages: {
44
+ [MESSAGE_IDS.UNEXPECTED]:
45
+ "Tabs and/or multiple consecutive spaces not allowed here",
46
+ },
47
+ },
48
+
49
+ create(context) {
50
+ const options = context.options[0] || {};
51
+ const skipTags = options.skip || [];
52
+ const sourceCode = context.getSourceCode();
53
+
54
+ /**
55
+ * @param {Array<ContentNode>} siblings
56
+ */
57
+ function checkSiblings(siblings) {
58
+ for (
59
+ let length = siblings.length, index = 0;
60
+ index < length;
61
+ index += 1
62
+ ) {
63
+ const node = siblings[index];
64
+
65
+ if (node.type === `Tag` && skipTags.includes(node.name) === false) {
66
+ checkSiblings(node.children);
67
+ } else if (node.type === `Text`) {
68
+ stripConsecutiveSpaces(node);
69
+ } else if (node.type === `Comment`) {
70
+ stripConsecutiveSpaces(node.value);
71
+ }
72
+ }
73
+ }
74
+
75
+ return {
76
+ Program(node) {
77
+ // @ts-ignore
78
+ checkSiblings(node.body);
79
+ },
80
+ };
81
+
82
+ /**
83
+ * @param {TextNode | CommentContentNode} node
84
+ */
85
+ function stripConsecutiveSpaces(node) {
86
+ const text = node.value;
87
+ const matcher = /(^|[^\n \t])([ \t]+\n|\t[\t ]*|[ \t]{2,})/g;
88
+
89
+ // eslint-disable-next-line no-constant-condition
90
+ while (true) {
91
+ const offender = matcher.exec(text);
92
+ if (offender === null) {
93
+ break;
94
+ }
95
+
96
+ const space = offender[2];
97
+ const indexStart = node.range[0] + matcher.lastIndex - space.length;
98
+ const indexEnd = indexStart + space.length;
99
+
100
+ context.report({
101
+ node: node,
102
+ loc: {
103
+ start: sourceCode.getLocFromIndex(indexStart),
104
+ end: sourceCode.getLocFromIndex(indexEnd),
105
+ },
106
+ messageId: MESSAGE_IDS.UNEXPECTED,
107
+ fix(fixer) {
108
+ return fixer.replaceTextRange(
109
+ [indexStart, indexEnd],
110
+ space.endsWith(`\n`) ? `\n` : ` `
111
+ );
112
+ },
113
+ });
114
+ }
115
+ }
116
+ },
117
+ };
@@ -1,10 +1,16 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
+ * @typedef { import("../types").RuleListener } RuleListener
3
4
  */
4
5
 
5
6
  const { RULE_CATEGORY } = require("../constants");
6
7
  const { findAttr } = require("./utils/node");
7
-
8
+ const { parse } = require("@html-eslint/template-parser");
9
+ const {
10
+ shouldCheckTaggedTemplateExpression,
11
+ shouldCheckTemplateLiteral,
12
+ } = require("./utils/settings");
13
+ const { getSourceCode } = require("./utils/source-code");
8
14
  const MESSAGE_IDS = {
9
15
  INLINE_STYLE: "unexpectedInlineStyle",
10
16
  };
@@ -30,7 +36,10 @@ module.exports = {
30
36
  },
31
37
 
32
38
  create(context) {
33
- return {
39
+ /**
40
+ * @type {RuleListener}
41
+ */
42
+ const visitors = {
34
43
  Tag(node) {
35
44
  const styleAttr = findAttr(node, "style");
36
45
  if (styleAttr) {
@@ -41,5 +50,19 @@ module.exports = {
41
50
  }
42
51
  },
43
52
  };
53
+
54
+ return {
55
+ ...visitors,
56
+ TaggedTemplateExpression(node) {
57
+ if (shouldCheckTaggedTemplateExpression(node, context)) {
58
+ parse(node.quasi, getSourceCode(context), visitors);
59
+ }
60
+ },
61
+ TemplateLiteral(node) {
62
+ if (shouldCheckTemplateLiteral(node, context)) {
63
+ parse(node, getSourceCode(context), visitors);
64
+ }
65
+ },
66
+ };
44
67
  },
45
68
  };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @typedef {import("../../types").MaybeHTMLSettings} MaybeHTMLSettings
3
+ * @typedef {import("../../types").HTMLSettings} HTMLSettings
4
+ * @typedef {import("../../types").Context} Context
5
+ * @typedef {import("estree").TaggedTemplateExpression} TaggedTemplateExpression
6
+ * @typedef {import("estree").TemplateLiteral} TemplateLiteral
7
+ */
8
+
9
+ const { getSourceCode } = require("../utils/source-code");
10
+
11
+ const DEFAULT_SETTINGS = {
12
+ templateLiterals: {
13
+ tags: ["^html$"],
14
+ comments: ["^\\s*html\\s*$"],
15
+ },
16
+ };
17
+
18
+ /**
19
+ * @type {HTMLSettings | null}
20
+ */
21
+ let cachedSettings = null;
22
+
23
+ /**
24
+ * @param {{ html?: MaybeHTMLSettings }} settings
25
+ * @returns {HTMLSettings}
26
+ */
27
+ function getSettings(settings) {
28
+ const tags =
29
+ (settings &&
30
+ settings.html &&
31
+ settings.html.templateLiterals &&
32
+ settings.html.templateLiterals.tags) ||
33
+ DEFAULT_SETTINGS.templateLiterals.tags;
34
+
35
+ const comments =
36
+ (settings &&
37
+ settings.html &&
38
+ settings.html.templateLiterals &&
39
+ settings.html.templateLiterals.comments) ||
40
+ DEFAULT_SETTINGS.templateLiterals.comments;
41
+
42
+ if (cachedSettings) {
43
+ return cachedSettings;
44
+ }
45
+ cachedSettings = {
46
+ templateLiterals: {
47
+ tags: tags.map((tag) => new RegExp(tag, "u")),
48
+ comments: comments.map((comment) => new RegExp(comment, "u")),
49
+ },
50
+ };
51
+ return cachedSettings;
52
+ }
53
+
54
+ /**
55
+ * @param {TaggedTemplateExpression} node
56
+ * @param {Context} context
57
+ * @returns {boolean}
58
+ */
59
+ function shouldCheckTaggedTemplateExpression(node, context) {
60
+ const { templateLiterals } = getSettings(context.settings);
61
+ const tags = templateLiterals.tags;
62
+ const tagNode = node.tag;
63
+ return !!(
64
+ tagNode.type === "Identifier" && tags.some((tag) => tag.test(tagNode.name))
65
+ );
66
+ }
67
+
68
+ /**
69
+ *
70
+ * @param {TemplateLiteral} node
71
+ * @param {Context} context
72
+ * @returns {boolean}
73
+ */
74
+ function shouldCheckTemplateLiteral(node, context) {
75
+ const sourceCode = getSourceCode(context);
76
+ const { templateLiterals } = getSettings(context.settings);
77
+ const comments = sourceCode.getCommentsBefore(node);
78
+ const last = comments[comments.length - 1];
79
+ return !!(
80
+ last &&
81
+ templateLiterals.comments.some((comment) => comment.test(last.value))
82
+ );
83
+ }
84
+
85
+ module.exports = {
86
+ shouldCheckTemplateLiteral,
87
+ shouldCheckTaggedTemplateExpression,
88
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @typedef {import("../../types").Context} Context
3
+ */
4
+
5
+ /**
6
+ * @param {Context} context
7
+ */
8
+ function getSourceCode(context) {
9
+ return context.sourceCode || context.getSourceCode();
10
+ }
11
+
12
+ module.exports = {
13
+ getSourceCode,
14
+ };
package/lib/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import ESTree from "estree";
1
+ import ESTree, { TaggedTemplateExpression, TemplateLiteral } from "estree";
2
2
  import ESLint from "eslint";
3
3
  import * as ESHtml from "es-html-parser";
4
4
 
@@ -200,12 +200,14 @@ interface RuleListener {
200
200
  CommentClose?: (node: CommentCloseNode) => void;
201
201
  CommentContent?: (node: CommentContentNode) => void;
202
202
  Doctype?: (node: DoctypeNode) => void;
203
- DoctypeOpen: (node: DoctypeOpenNode) => void;
203
+ DoctypeOpen?: (node: DoctypeOpenNode) => void;
204
204
  DoctypeClose?: (node: DoctypeCloseNode) => void;
205
205
  DoctypeAttribute?: (node: DoctypeAttributeNode) => void;
206
206
  DoctypeAttributeValue?: (node: DoctypeAttributeValueNode) => void;
207
207
  DoctypeAttributeWrapperStart?: (node: DoctypeAttributeWrapperStart) => void;
208
208
  DoctypeAttributeWrapperEnd?: (node: DoctypeAttributeWrapperEnd) => void;
209
+ TaggedTemplateExpression?: (node: TaggedTemplateExpression) => void;
210
+ TemplateLiteral?: (node: TemplateLiteral) => void;
209
211
  }
210
212
 
211
213
  export interface RuleModule extends ESLint.Rule.RuleModule {
@@ -258,7 +260,7 @@ type ReportDescriptorLocation = {
258
260
  column?: number;
259
261
  };
260
262
 
261
- interface Context extends Omit<ESLint.Rule.RuleContext, "report"> {
263
+ export interface Context extends Omit<ESLint.Rule.RuleContext, "report"> {
262
264
  report(descriptor: ReportDescriptor): void;
263
265
  }
264
266
 
@@ -267,3 +269,25 @@ export type ChildType<T extends BaseNode> = T extends ProgramNode
267
269
  : T extends TagNode
268
270
  ? T["children"][number]
269
271
  : never;
272
+
273
+ export type ContentNode =
274
+ | CommentNode
275
+ | DoctypeNode
276
+ | ScriptTagNode
277
+ | StyleTagNode
278
+ | TagNode
279
+ | TextNode;
280
+
281
+ export type MaybeHTMLSettings = {
282
+ templateLiterals?: {
283
+ tags?: string[];
284
+ comments?: string[];
285
+ };
286
+ };
287
+
288
+ export type HTMLSettings = {
289
+ templateLiterals: {
290
+ tags: RegExp[];
291
+ comments: RegExp[];
292
+ };
293
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@html-eslint/eslint-plugin",
3
- "version": "0.26.0",
3
+ "version": "0.28.0-alpha.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,13 @@
45
45
  "accessibility"
46
46
  ],
47
47
  "devDependencies": {
48
- "@html-eslint/parser": "^0.26.0",
49
- "@types/eslint": "^8.56.2",
48
+ "@html-eslint/parser": "^0.28.0-alpha.0",
49
+ "@html-eslint/template-parser": "^0.28.0-alpha.0",
50
+ "@types/eslint": "^9.6.1",
50
51
  "@types/estree": "^0.0.47",
51
52
  "es-html-parser": "^0.0.8",
53
+ "espree": "^10.3.0",
52
54
  "typescript": "^4.4.4"
53
55
  },
54
- "gitHead": "34d55c3b5be5a29cc416063b4b4375cb89b3a519"
56
+ "gitHead": "4c4e3388a4d8541ade0201fb06690e46098ab9bf"
55
57
  }
@@ -6,7 +6,9 @@ export const rules: {
6
6
  "@html-eslint/no-multiple-h1": string;
7
7
  "@html-eslint/no-extra-spacing-attrs": string;
8
8
  "@html-eslint/attrs-newline": string;
9
- "@html-eslint/element-newline": string;
9
+ "@html-eslint/element-newline": (string | {
10
+ inline: string[];
11
+ })[];
10
12
  "@html-eslint/no-duplicate-id": string;
11
13
  "@html-eslint/indent": string;
12
14
  "@html-eslint/require-li-container": string;
@@ -4,4 +4,15 @@ export type RuleModule = import("../types").RuleModule;
4
4
  export type ProgramNode = import("../types").ProgramNode;
5
5
  export type TagNode = import("../types").TagNode;
6
6
  export type BaseNode = import("../types").BaseNode;
7
+ export type CommentNode = import("../types").CommentNode;
8
+ export type DoctypeNode = import("../types").DoctypeNode;
9
+ export type ScriptTagNode = import("../types").ScriptTagNode;
10
+ export type StyleTagNode = import("../types").StyleTagNode;
11
+ export type TextNode = import("../types").TextNode;
12
+ export type NewlineNode = CommentNode | DoctypeNode | ScriptTagNode | StyleTagNode | TagNode | TextNode;
13
+ export type NodeMeta = {
14
+ childFirst: NewlineNode | null;
15
+ childLast: NewlineNode | null;
16
+ shouldBeNewline: boolean;
17
+ };
7
18
  //# sourceMappingURL=element-newline.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"element-newline.d.ts","sourceRoot":"","sources":["../../lib/rules/element-newline.js"],"names":[],"mappings":"wBAoEU,UAAU;;yBAnEN,OAAO,UAAU,EAAE,UAAU;0BAC7B,OAAO,UAAU,EAAE,WAAW;sBAC9B,OAAO,UAAU,EAAE,OAAO;uBAC1B,OAAO,UAAU,EAAE,QAAQ;0BAC3B,OAAO,UAAU,EAAE,WAAW;0BAC9B,OAAO,UAAU,EAAE,WAAW;4BAC9B,OAAO,UAAU,EAAE,aAAa;2BAChC,OAAO,UAAU,EAAE,YAAY;uBAC/B,OAAO,UAAU,EAAE,QAAQ;0BAC3B,WAAW,GAAG,WAAW,GAAG,aAAa,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ;uBAC9E;IACZ,UAAc,EAAE,WAAW,GAAG,IAAI,CAAC;IACnC,SAAa,EAAE,WAAW,GAAG,IAAI,CAAC;IAClC,eAAmB,EAAE,OAAO,CAAC;CAC1B"}
@@ -7,6 +7,7 @@ declare const _exports: {
7
7
  "no-inline-styles": import("../types").RuleModule;
8
8
  "no-multiple-h1": import("../types").RuleModule;
9
9
  "no-extra-spacing-attrs": import("../types").RuleModule;
10
+ "no-extra-spacing-text": import("../types").RuleModule;
10
11
  "attrs-newline": import("../types").RuleModule;
11
12
  "element-newline": import("../types").RuleModule;
12
13
  "no-skip-heading-levels": import("../types").RuleModule;
@@ -1 +1 @@
1
- {"version":3,"file":"no-extra-spacing-attrs.d.ts","sourceRoot":"","sources":["../../lib/rules/no-extra-spacing-attrs.js"],"names":[],"mappings":"wBAgCU,UAAU;;yBA/BN,OAAO,UAAU,EAAE,UAAU;4BAC7B,OAAO,UAAU,EAAE,aAAa;6BAChC,OAAO,UAAU,EAAE,cAAc;mCACjC,OAAO,UAAU,EAAE,oBAAoB;kCACvC,OAAO,UAAU,EAAE,mBAAmB;qCACtC,OAAO,UAAU,EAAE,sBAAsB;+BACzC,OAAO,UAAU,EAAE,gBAAgB;oCACnC,OAAO,UAAU,EAAE,qBAAqB;sBACxC,OAAO,UAAU,EAAE,OAAO;2BAC1B,OAAO,UAAU,EAAE,YAAY;4BAC/B,OAAO,UAAU,EAAE,aAAa;sBAChC,OAAO,UAAU,EAAE,OAAO"}
1
+ {"version":3,"file":"no-extra-spacing-attrs.d.ts","sourceRoot":"","sources":["../../lib/rules/no-extra-spacing-attrs.js"],"names":[],"mappings":"wBAiCU,UAAU;;yBAhCN,OAAO,UAAU,EAAE,UAAU;4BAC7B,OAAO,UAAU,EAAE,aAAa;6BAChC,OAAO,UAAU,EAAE,cAAc;mCACjC,OAAO,UAAU,EAAE,oBAAoB;kCACvC,OAAO,UAAU,EAAE,mBAAmB;qCACtC,OAAO,UAAU,EAAE,sBAAsB;+BACzC,OAAO,UAAU,EAAE,gBAAgB;oCACnC,OAAO,UAAU,EAAE,qBAAqB;sBACxC,OAAO,UAAU,EAAE,OAAO;2BAC1B,OAAO,UAAU,EAAE,YAAY;4BAC/B,OAAO,UAAU,EAAE,aAAa;sBAChC,OAAO,UAAU,EAAE,OAAO"}
@@ -0,0 +1,8 @@
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 CommentContentNode = import("es-html-parser").CommentContentNode;
6
+ export type ContentNode = import("../types").ContentNode;
7
+ export type TextNode = import("../types").TextNode;
8
+ //# sourceMappingURL=no-extra-spacing-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-extra-spacing-text.d.ts","sourceRoot":"","sources":["../../lib/rules/no-extra-spacing-text.js"],"names":[],"mappings":"wBAeU,UAAU;;yBAdN,OAAO,UAAU,EAAE,UAAU;0BAC7B,OAAO,UAAU,EAAE,WAAW;iCAC9B,OAAO,gBAAgB,EAAE,kBAAkB;0BAC3C,OAAO,UAAU,EAAE,WAAW;uBAC9B,OAAO,UAAU,EAAE,QAAQ"}
@@ -1,4 +1,5 @@
1
1
  declare const _exports: RuleModule;
2
2
  export = _exports;
3
3
  export type RuleModule = import("../types").RuleModule;
4
+ export type RuleListener = import("../types").RuleListener;
4
5
  //# sourceMappingURL=no-inline-styles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"no-inline-styles.d.ts","sourceRoot":"","sources":["../../lib/rules/no-inline-styles.js"],"names":[],"mappings":"wBAYU,UAAU;;yBAXN,OAAO,UAAU,EAAE,UAAU"}
1
+ {"version":3,"file":"no-inline-styles.d.ts","sourceRoot":"","sources":["../../lib/rules/no-inline-styles.js"],"names":[],"mappings":"wBAkBU,UAAU;;yBAjBN,OAAO,UAAU,EAAE,UAAU;2BAC7B,OAAO,UAAU,EAAE,YAAY"}
@@ -13,7 +13,7 @@ export type Location = import("../../types").Location;
13
13
  * @param {string} key
14
14
  * @returns {AttributeNode | undefined}
15
15
  */
16
- declare function findAttr(node: import("../../types").TagNode | import("../../types").ScriptTagNode | import("../../types").StyleTagNode, key: string): import("../../types").AttributeNode | undefined;
16
+ declare function findAttr(node: import("../../types").ScriptTagNode | import("../../types").TagNode | import("../../types").StyleTagNode, key: string): import("../../types").AttributeNode | undefined;
17
17
  /**
18
18
  * Checks whether a node's all tokens are on the same line or not.
19
19
  * @param {AnyNode} node A node to check
@@ -0,0 +1,19 @@
1
+ export type MaybeHTMLSettings = import("../../types").MaybeHTMLSettings;
2
+ export type HTMLSettings = import("../../types").HTMLSettings;
3
+ export type Context = import("../../types").Context;
4
+ export type TaggedTemplateExpression = import("estree").TaggedTemplateExpression;
5
+ export type TemplateLiteral = import("estree").TemplateLiteral;
6
+ /**
7
+ *
8
+ * @param {TemplateLiteral} node
9
+ * @param {Context} context
10
+ * @returns {boolean}
11
+ */
12
+ export function shouldCheckTemplateLiteral(node: TemplateLiteral, context: Context): boolean;
13
+ /**
14
+ * @param {TaggedTemplateExpression} node
15
+ * @param {Context} context
16
+ * @returns {boolean}
17
+ */
18
+ export function shouldCheckTaggedTemplateExpression(node: TaggedTemplateExpression, context: Context): boolean;
19
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../lib/rules/utils/settings.js"],"names":[],"mappings":"gCACa,OAAO,aAAa,EAAE,iBAAiB;2BACvC,OAAO,aAAa,EAAE,YAAY;sBAClC,OAAO,aAAa,EAAE,OAAO;uCAC7B,OAAO,QAAQ,EAAE,wBAAwB;8BACzC,OAAO,QAAQ,EAAE,eAAe;AA8D7C;;;;;GAKG;AACH,iDAJW,eAAe,WACf,OAAO,GACL,OAAO,CAWnB;AA7BD;;;;GAIG;AACH,0DAJW,wBAAwB,WACxB,OAAO,GACL,OAAO,CASnB"}
@@ -0,0 +1,9 @@
1
+ export type Context = import("../../types").Context;
2
+ /**
3
+ * @typedef {import("../../types").Context} Context
4
+ */
5
+ /**
6
+ * @param {Context} context
7
+ */
8
+ export function getSourceCode(context: Context): import("eslint").SourceCode;
9
+ //# sourceMappingURL=source-code.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-code.d.ts","sourceRoot":"","sources":["../../../lib/rules/utils/source-code.js"],"names":[],"mappings":"sBACa,OAAO,aAAa,EAAE,OAAO;AAD1C;;GAEG;AAEH;;GAEG;AACH,uCAFW,OAAO,+BAIjB"}