@html-eslint/eslint-plugin 0.38.1 → 0.39.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.
@@ -46,6 +46,7 @@ const noNestedInteractive = require("./no-nested-interactive");
46
46
  const maxElementDepth = require("./max-element-depth");
47
47
  const requireExplicitSize = require("./require-explicit-size");
48
48
  const useBaseLine = require("./use-baseline");
49
+ const noDuplicateClass = require("./no-duplicate-class");
49
50
  // import new rule here ↑
50
51
  // DO NOT REMOVE THIS COMMENT
51
52
 
@@ -98,6 +99,7 @@ module.exports = {
98
99
  "max-element-depth": maxElementDepth,
99
100
  "require-explicit-size": requireExplicitSize,
100
101
  "use-baseline": useBaseLine,
102
+ "no-duplicate-class": noDuplicateClass,
101
103
  // export new rule here ↑
102
104
  // DO NOT REMOVE THIS COMMENT
103
105
  };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @typedef { import("@html-eslint/types").Tag } Tag
3
+ * @typedef { import("@html-eslint/types").StyleTag } StyleTag
4
+ * @typedef { import("@html-eslint/types").ScriptTag } ScriptTag
5
+ * @typedef { import("@html-eslint/types").AttributeValue } AttributeValue
6
+ * @typedef { import("../types").RuleModule<[]> } RuleModule
7
+ * @typedef {Object} ClassInfo
8
+ * @property {string} name
9
+ * @property {import("@html-eslint/types").AnyNode['loc']} loc
10
+ * @property {import("@html-eslint/types").AnyNode['range']} range
11
+ */
12
+
13
+ const { NodeTypes } = require("es-html-parser");
14
+ const { RULE_CATEGORY } = require("../constants");
15
+ const { createVisitors } = require("./utils/visitors");
16
+
17
+ const MESSAGE_IDS = {
18
+ DUPLICATE_CLASS: "duplicateClass",
19
+ };
20
+
21
+ /**
22
+ * @type {RuleModule}
23
+ */
24
+ module.exports = {
25
+ meta: {
26
+ type: "code",
27
+ docs: {
28
+ description: "Disallow to use duplicate class",
29
+ category: RULE_CATEGORY.BEST_PRACTICE,
30
+ recommended: false,
31
+ },
32
+ fixable: "code",
33
+ schema: [],
34
+ messages: {
35
+ [MESSAGE_IDS.DUPLICATE_CLASS]: "The class '{{class}}' is duplicated.",
36
+ },
37
+ },
38
+
39
+ create(context) {
40
+ /**
41
+ * @param {AttributeValue} value
42
+ * @returns {{value: string, pos: number}[]}
43
+ */
44
+ function splitClassAndSpaces(value) {
45
+ /**
46
+ * @type {{value: string, pos: number}[]}
47
+ */
48
+ const result = [];
49
+ const regex = /(\s+|\S+)/g;
50
+ /**
51
+ * @type {RegExpExecArray | null}
52
+ */
53
+ let match = null;
54
+
55
+ while ((match = regex.exec(value.value)) !== null) {
56
+ result.push({
57
+ value: match[0],
58
+ pos: match.index,
59
+ });
60
+ }
61
+
62
+ return result;
63
+ }
64
+
65
+ return createVisitors(context, {
66
+ Attribute(node) {
67
+ if (node.key.value.toLowerCase() !== "class") {
68
+ return;
69
+ }
70
+ const attributeValue = node.value;
71
+ if (
72
+ !attributeValue ||
73
+ !attributeValue.value ||
74
+ attributeValue.parts.some((part) => part.type === NodeTypes.Template)
75
+ ) {
76
+ return;
77
+ }
78
+ const classesAndSpaces = splitClassAndSpaces(attributeValue);
79
+ const classSet = new Set();
80
+ classesAndSpaces.forEach(({ value, pos }, index) => {
81
+ const className = value.trim();
82
+
83
+ if (className.length && classSet.has(className)) {
84
+ context.report({
85
+ loc: {
86
+ start: {
87
+ line: attributeValue.loc.start.line,
88
+ column: attributeValue.loc.start.column + pos,
89
+ },
90
+ end: {
91
+ line: attributeValue.loc.start.line,
92
+ column:
93
+ attributeValue.loc.start.column + pos + className.length,
94
+ },
95
+ },
96
+ data: {
97
+ class: className,
98
+ },
99
+ messageId: MESSAGE_IDS.DUPLICATE_CLASS,
100
+ fix(fixer) {
101
+ if (!node.value) {
102
+ return null;
103
+ }
104
+ const before = classesAndSpaces[index - 1];
105
+ const after = classesAndSpaces[index + 1];
106
+ const hasSpacesBefore =
107
+ !!before && before.value.trim().length === 0;
108
+ const hasSpacesAfter =
109
+ !!after && after.value.trim().length === 0;
110
+ const hasClassBefore = !!classesAndSpaces[index - 2];
111
+ const hasClassAfter = !!classesAndSpaces[index + 2];
112
+
113
+ const startRange = hasSpacesBefore
114
+ ? attributeValue.range[0] + before.pos
115
+ : attributeValue.range[0] + pos;
116
+
117
+ const endRange = hasSpacesAfter
118
+ ? attributeValue.range[0] +
119
+ pos +
120
+ value.length +
121
+ after.value.length
122
+ : attributeValue.range[0] + pos + value.length;
123
+
124
+ return fixer.replaceTextRange(
125
+ [startRange, endRange],
126
+ hasClassBefore && hasClassAfter ? " " : ""
127
+ );
128
+ },
129
+ });
130
+ } else {
131
+ classSet.add(className);
132
+ }
133
+ });
134
+ },
135
+ });
136
+ },
137
+ };
@@ -178,6 +178,65 @@ module.exports = {
178
178
  return isSupported(globalAttrStatus);
179
179
  }
180
180
 
181
+ /**
182
+ *
183
+ * @param {string} element
184
+ * @param {string} key
185
+ * @param {string} value
186
+ * @returns {string | null}
187
+ */
188
+ function getElementAttributeSpecificStatusKey(element, key, value) {
189
+ const elementName = element.toLowerCase();
190
+ const attributeKey = key.toLowerCase();
191
+ const attributeValue = value.toLowerCase();
192
+
193
+ // <input type="...">
194
+ if (elementName === "input" && attributeKey === "type") {
195
+ return `input.type_${attributeValue}`;
196
+ }
197
+
198
+ // <a href="sms:0000..">
199
+ if (
200
+ elementName === "a" &&
201
+ attributeKey === "href" &&
202
+ attributeValue.trim().startsWith("sms:")
203
+ ) {
204
+ return "a.href.href_sms";
205
+ }
206
+
207
+ // <td rowspan="0"> <th rowspan="0">
208
+ if (
209
+ (elementName === "td" || elementName === "th") &&
210
+ attributeKey === "rowspan" &&
211
+ attributeValue === "0"
212
+ ) {
213
+ return `${elementName}.rowspan.rowspan_zero`;
214
+ }
215
+ return null;
216
+ }
217
+
218
+ /**
219
+ * @param {string} element
220
+ * @param {string} key
221
+ * @param {string} value
222
+ * @returns {boolean}
223
+ */
224
+ function isSupportedElementSpecificAttributeKeyValue(element, key, value) {
225
+ const statusKey = getElementAttributeSpecificStatusKey(
226
+ element,
227
+ key,
228
+ value
229
+ );
230
+ if (!statusKey) {
231
+ return true;
232
+ }
233
+ const elementStatus = elements.get(statusKey);
234
+ if (!elementStatus) {
235
+ return true;
236
+ }
237
+ return isSupported(elementStatus);
238
+ }
239
+
181
240
  /**
182
241
  * @param {Tag | ScriptTag | StyleTag} node
183
242
  * @param {string} elementName
@@ -224,6 +283,11 @@ module.exports = {
224
283
  elementName,
225
284
  attribute.key.value,
226
285
  attribute.value.value
286
+ ) ||
287
+ !isSupportedElementSpecificAttributeKeyValue(
288
+ elementName,
289
+ attribute.key.value,
290
+ attribute.value.value
227
291
  )
228
292
  ) {
229
293
  context.report({
@@ -11,7 +11,10 @@ const elements = new Map([
11
11
  ["a.attributionsrc", "0:"],
12
12
  ["a.download", "10:2019"],
13
13
  ["a.href", "10:2015"],
14
+ ["a.href.href_sms", "0:"],
15
+ ["a.href.href_top", "10:2015"],
14
16
  ["a.hreflang", "10:2015"],
17
+ ["a.implicit_noopener", "10:2021"],
15
18
  ["a.ping", "0:"],
16
19
  ["a.referrerpolicy", "10:2020"],
17
20
  ["a.referrerpolicy.no-referrer-when-downgrade", "0:"],
@@ -22,6 +25,7 @@ const elements = new Map([
22
25
  ["a.rel.noreferrer", "10:2015"],
23
26
  ["a.target", "10:2015"],
24
27
  ["a.target.unfencedTop", "0:"],
28
+ ["a.text_fragments", "5:2024"],
25
29
  ["a.type", "10:2015"],
26
30
  ["abbr", "10:2015"],
27
31
  ["address", "10:2015"],
@@ -31,6 +35,7 @@ const elements = new Map([
31
35
  ["area.coords", "10:2015"],
32
36
  ["area.download", "10:2017"],
33
37
  ["area.href", "10:2015"],
38
+ ["area.implicit_noopener", "10:2021"],
34
39
  ["area.ping", "0:"],
35
40
  ["area.referrerpolicy", "10:2020"],
36
41
  ["area.referrerpolicy.no-referrer-when-downgrade", "0:"],
@@ -56,6 +61,8 @@ const elements = new Map([
56
61
  ["b", "10:2015"],
57
62
  ["base", "10:2015"],
58
63
  ["base.href", "10:2015"],
64
+ ["base.href.forbid_data_javascript_urls", "5:2024"],
65
+ ["base.href.relative_url", "10:2015"],
59
66
  ["base.target", "10:2015"],
60
67
  ["bdi", "10:2020"],
61
68
  ["bdo", "10:2015"],
@@ -75,6 +82,7 @@ const elements = new Map([
75
82
  ["button.formtarget", "10:2015"],
76
83
  ["button.name", "10:2015"],
77
84
  ["button.popovertarget", "5:2024"],
85
+ ["button.popovertarget.implicit_anchor_reference", "0:"],
78
86
  ["button.popovertargetaction", "5:2024"],
79
87
  ["button.type", "10:2015"],
80
88
  ["button.value", "10:2015"],
@@ -203,6 +211,7 @@ const elements = new Map([
203
211
  ["iframe.width", "10:2015"],
204
212
  ["img", "10:2015"],
205
213
  ["img.alt", "10:2015"],
214
+ ["img.aspect_ratio_computed_from_attributes", "10:2021"],
206
215
  ["img.attributionsrc", "0:"],
207
216
  ["img.crossorigin", "10:2015"],
208
217
  ["img.decoding", "10:2020"],
@@ -244,12 +253,38 @@ const elements = new Map([
244
253
  ["input.pattern", "10:2015"],
245
254
  ["input.placeholder", "10:2015"],
246
255
  ["input.popovertarget", "5:2024"],
256
+ ["input.popovertarget.implicit_anchor_reference", "0:"],
247
257
  ["input.popovertargetaction", "5:2024"],
248
258
  ["input.readonly", "10:2015"],
249
259
  ["input.required", "10:2015"],
250
260
  ["input.size", "10:2015"],
251
261
  ["input.src", "10:2015"],
252
262
  ["input.step", "10:2015"],
263
+ ["input.type_button", "10:2015"],
264
+ ["input.type_checkbox", "10:2015"],
265
+ ["input.type_color", "0:"],
266
+ ["input.type_date", "10:2021"],
267
+ ["input.type_datetime-local", "10:2021"],
268
+ ["input.type_email", "10:2015"],
269
+ ["input.type_file", "10:2015"],
270
+ ["input.type_hidden", "10:2015"],
271
+ ["input.type_image", "10:2015"],
272
+ ["input.type_month", "0:"],
273
+ ["input.type_number", "10:2015"],
274
+ ["input.type_password", "10:2015"],
275
+ ["input.type_password.insecure_login_handling", "0:"],
276
+ ["input.type_radio", "10:2015"],
277
+ ["input.type_range", "10:2017"],
278
+ ["input.type_range.tick_marks", "5:2023"],
279
+ ["input.type_range.vertical_orientation", "5:2024"],
280
+ ["input.type_reset", "10:2015"],
281
+ ["input.type_search", "10:2015"],
282
+ ["input.type_submit", "10:2015"],
283
+ ["input.type_tel", "10:2015"],
284
+ ["input.type_text", "10:2015"],
285
+ ["input.type_time", "10:2021"],
286
+ ["input.type_url", "10:2015"],
287
+ ["input.type_week", "0:"],
253
288
  ["ins", "10:2015"],
254
289
  ["ins.cite", "10:2015"],
255
290
  ["ins.datetime", "10:2015"],
@@ -276,6 +311,7 @@ const elements = new Map([
276
311
  ["link.referrerpolicy.origin-when-cross-origin", "0:"],
277
312
  ["link.referrerpolicy.unsafe-url", "0:"],
278
313
  ["link.rel", "10:2015"],
314
+ ["link.rel.alternate_stylesheet", "0:"],
279
315
  ["link.rel.dns-prefetch", "5:2024"],
280
316
  ["link.rel.expect", "0:"],
281
317
  ["link.rel.manifest", "0:"],
@@ -372,13 +408,17 @@ const elements = new Map([
372
408
  ["script.type.module", "10:2018"],
373
409
  ["script.type.speculationrules", "0:"],
374
410
  ["script.type.speculationrules.eagerness", "0:"],
411
+ ["script.type.speculationrules.expects_no_vary_search", "0:"],
375
412
  ["script.type.speculationrules.prefetch", "0:"],
376
413
  ["script.type.speculationrules.prerender", "0:"],
414
+ ["script.type.speculationrules.referrer_policy", "0:"],
415
+ ["script.type.speculationrules.relative_to", "0:"],
377
416
  ["script.type.speculationrules.requires", "0:"],
378
417
  [
379
418
  "script.type.speculationrules.requires.anonymous-client-ip-when-cross-origin",
380
419
  "0:",
381
420
  ],
421
+ ["script.type.speculationrules.source_optional", "0:"],
382
422
  ["script.type.speculationrules.urls", "0:"],
383
423
  ["script.type.speculationrules.where", "0:"],
384
424
  ["search", "5:2023"],
@@ -386,6 +426,7 @@ const elements = new Map([
386
426
  ["select", "10:2015"],
387
427
  ["select.disabled", "10:2015"],
388
428
  ["select.form", "10:2015"],
429
+ ["select.hr_in_select", "0:"],
389
430
  ["select.multiple", "10:2015"],
390
431
  ["select.name", "10:2015"],
391
432
  ["select.required", "10:2015"],
@@ -409,6 +450,7 @@ const elements = new Map([
409
450
  ["style.media", "10:2015"],
410
451
  ["sub", "10:2015"],
411
452
  ["summary", "10:2020"],
453
+ ["summary.display_list_item", "0:"],
412
454
  ["sup", "10:2015"],
413
455
  ["table", "10:2015"],
414
456
  ["tbody", "10:2015"],
@@ -416,6 +458,7 @@ const elements = new Map([
416
458
  ["td.colspan", "10:2015"],
417
459
  ["td.headers", "10:2015"],
418
460
  ["td.rowspan", "10:2015"],
461
+ ["td.rowspan.rowspan_zero", "0:"],
419
462
  ["template", "10:2015"],
420
463
  ["template.shadowrootclonable", "0:"],
421
464
  ["template.shadowrootdelegatesfocus", "0:"],
@@ -431,6 +474,7 @@ const elements = new Map([
431
474
  ["textarea.minlength", "10:2018"],
432
475
  ["textarea.name", "10:2015"],
433
476
  ["textarea.placeholder", "10:2015"],
477
+ ["textarea.placeholder.line_breaks", "10:2022"],
434
478
  ["textarea.readonly", "10:2015"],
435
479
  ["textarea.required", "10:2015"],
436
480
  ["textarea.rows", "10:2015"],
@@ -443,6 +487,7 @@ const elements = new Map([
443
487
  ["th.colspan", "10:2015"],
444
488
  ["th.headers", "10:2015"],
445
489
  ["th.rowspan", "10:2015"],
490
+ ["th.rowspan.rowspan_zero", "0:"],
446
491
  ["th.scope", "10:2015"],
447
492
  ["thead", "10:2015"],
448
493
  ["time", "10:2017"],
@@ -459,6 +504,7 @@ const elements = new Map([
459
504
  ["ul", "10:2015"],
460
505
  ["var", "10:2015"],
461
506
  ["video", "10:2015"],
507
+ ["video.aspect_ratio_computed_from_attributes", "10:2020"],
462
508
  ["video.autoplay", "10:2016"],
463
509
  ["video.controls", "10:2015"],
464
510
  ["video.controlslist", "0:"],
@@ -484,10 +530,12 @@ const globalAttributes = new Map([
484
530
  ["enterkeyhint", "10:2021"],
485
531
  ["exportparts", "10:2020"],
486
532
  ["inert", "5:2023"],
533
+ ["inert.ignores_find_in_page", "0:"],
487
534
  ["inputmode", "10:2021"],
488
535
  ["is", "0:"],
489
536
  ["lang", "10:2015"],
490
537
  ["nonce", "10:2022"],
538
+ ["nonce.nonce_hiding", "10:2022"],
491
539
  ["part", "10:2020"],
492
540
  ["popover", "5:2024"],
493
541
  ["popover.hint", "0:"],
@@ -496,6 +544,7 @@ const globalAttributes = new Map([
496
544
  ["style", "10:2015"],
497
545
  ["tabindex", "10:2015"],
498
546
  ["title", "10:2015"],
547
+ ["title.multi-line_titles", "10:2018"],
499
548
  ["translate", "5:2023"],
500
549
  ["virtualkeyboardpolicy", "0:"],
501
550
  ["writingsuggestions", "0:"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@html-eslint/eslint-plugin",
3
- "version": "0.38.1",
3
+ "version": "0.39.0",
4
4
  "description": "ESLint plugin for html",
5
5
  "author": "yeonjuan",
6
6
  "homepage": "https://github.com/yeonjuan/html-eslint#readme",
@@ -37,12 +37,12 @@
37
37
  "accessibility"
38
38
  ],
39
39
  "dependencies": {
40
- "@html-eslint/template-parser": "^0.38.0",
41
- "@html-eslint/template-syntax-parser": "^0.38.0"
40
+ "@html-eslint/template-parser": "^0.39.0",
41
+ "@html-eslint/template-syntax-parser": "^0.39.0"
42
42
  },
43
43
  "devDependencies": {
44
- "@html-eslint/parser": "^0.38.0",
45
- "@html-eslint/types": "^0.38.0",
44
+ "@html-eslint/parser": "^0.39.0",
45
+ "@html-eslint/types": "^0.39.0",
46
46
  "@types/eslint": "^9.6.1",
47
47
  "@types/estree": "^0.0.47",
48
48
  "es-html-parser": "0.1.1",
@@ -50,5 +50,5 @@
50
50
  "espree": "^10.3.0",
51
51
  "typescript": "^5.7.2"
52
52
  },
53
- "gitHead": "501ee2415f685bada6b4a967e0e7e72f605b403c"
53
+ "gitHead": "baf5c0359f6c4c45d70fe67eba88ec408a141036"
54
54
  }
package/types/index.d.ts CHANGED
@@ -65,6 +65,7 @@ type AllRules = {
65
65
  "max-element-depth": import("./types").RuleModule<[import("./rules/max-element-depth").Option]>;
66
66
  "require-explicit-size": import("./types").RuleModule<[import("./rules/require-explicit-size").Option]>;
67
67
  "use-baseline": import("./types").RuleModule<[import("./rules/use-baseline").Option]>;
68
+ "no-duplicate-class": import("./types").RuleModule<[]>;
68
69
  };
69
70
  type RecommendedConfig = {
70
71
  rules: {
@@ -47,6 +47,7 @@ declare const _exports: {
47
47
  "max-element-depth": import("../types").RuleModule<[maxElementDepth.Option]>;
48
48
  "require-explicit-size": import("../types").RuleModule<[requireExplicitSize.Option]>;
49
49
  "use-baseline": import("../types").RuleModule<[useBaseLine.Option]>;
50
+ "no-duplicate-class": import("../types").RuleModule<[]>;
50
51
  };
51
52
  export = _exports;
52
53
  import requireImgAlt = require("./require-img-alt");
@@ -0,0 +1,16 @@
1
+ declare namespace _exports {
2
+ export { Tag, StyleTag, ScriptTag, AttributeValue, RuleModule, ClassInfo };
3
+ }
4
+ declare const _exports: RuleModule;
5
+ export = _exports;
6
+ type Tag = import("@html-eslint/types").Tag;
7
+ type StyleTag = import("@html-eslint/types").StyleTag;
8
+ type ScriptTag = import("@html-eslint/types").ScriptTag;
9
+ type AttributeValue = import("@html-eslint/types").AttributeValue;
10
+ type RuleModule = import("../types").RuleModule<[]>;
11
+ type ClassInfo = {
12
+ name: string;
13
+ loc: import("@html-eslint/types").AnyNode["loc"];
14
+ range: import("@html-eslint/types").AnyNode["range"];
15
+ };
16
+ //# sourceMappingURL=no-duplicate-class.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-duplicate-class.d.ts","sourceRoot":"","sources":["../../lib/rules/no-duplicate-class.js"],"names":[],"mappings":";;;wBAqBU,UAAU;;WApBN,OAAO,oBAAoB,EAAE,GAAG;gBAChC,OAAO,oBAAoB,EAAE,QAAQ;iBACrC,OAAO,oBAAoB,EAAE,SAAS;sBACtC,OAAO,oBAAoB,EAAE,cAAc;kBAC3C,OAAO,UAAU,EAAE,UAAU,CAAC,EAAE,CAAC;;UAEjC,MAAM;SACN,OAAO,oBAAoB,EAAE,OAAO,CAAC,KAAK,CAAC;WAC3C,OAAO,oBAAoB,EAAE,OAAO,CAAC,OAAO,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../../../lib/rules/utils/baseline.js"],"names":[],"mappings":"AAOA,2CAodG;AACH,mDAyBG;AArfH;;GAEG;AACH,4BAAsB,EAAE,CAAC;AACzB,2BAAqB,CAAC,CAAC;AACvB,6BAAuB,CAAC,CAAC"}
1
+ {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../../../lib/rules/utils/baseline.js"],"names":[],"mappings":"AAOA,2CAkgBG;AACH,mDA4BG;AAtiBH;;GAEG;AACH,4BAAsB,EAAE,CAAC;AACzB,2BAAqB,CAAC,CAAC;AACvB,6BAAuB,CAAC,CAAC"}