@html-eslint/eslint-plugin 0.31.1 → 0.33.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 (135) hide show
  1. package/lib/rules/attrs-newline.js +3 -5
  2. package/lib/rules/element-newline.js +11 -9
  3. package/lib/rules/id-naming-convention.js +5 -5
  4. package/lib/rules/indent/indent.js +121 -69
  5. package/lib/rules/index.js +20 -0
  6. package/lib/rules/lowercase.js +7 -8
  7. package/lib/rules/max-element-depth.js +96 -0
  8. package/lib/rules/no-abstract-roles.js +4 -4
  9. package/lib/rules/no-accesskey-attrs.js +4 -4
  10. package/lib/rules/no-duplicate-attrs.js +4 -4
  11. package/lib/rules/no-duplicate-id.js +7 -7
  12. package/lib/rules/no-extra-spacing-attrs.js +14 -14
  13. package/lib/rules/no-extra-spacing-text.js +9 -10
  14. package/lib/rules/no-heading-inside-button.js +54 -0
  15. package/lib/rules/no-inline-styles.js +0 -1
  16. package/lib/rules/no-invalid-role.js +287 -0
  17. package/lib/rules/no-multiple-empty-lines.js +5 -5
  18. package/lib/rules/no-multiple-h1.js +2 -2
  19. package/lib/rules/no-nested-interactive.js +120 -0
  20. package/lib/rules/no-positive-tabindex.js +4 -4
  21. package/lib/rules/no-restricted-attr-values.js +6 -6
  22. package/lib/rules/no-restricted-attrs.js +6 -6
  23. package/lib/rules/no-script-style-type.js +4 -4
  24. package/lib/rules/no-skip-heading-levels.js +2 -2
  25. package/lib/rules/no-trailing-spaces.js +3 -3
  26. package/lib/rules/prefer-https.js +106 -0
  27. package/lib/rules/quotes.js +8 -8
  28. package/lib/rules/require-attrs.js +14 -6
  29. package/lib/rules/require-closing-tags.js +4 -4
  30. package/lib/rules/require-explicit-size.js +122 -0
  31. package/lib/rules/require-form-method.js +81 -0
  32. package/lib/rules/require-img-alt.js +2 -3
  33. package/lib/rules/require-input-label.js +77 -0
  34. package/lib/rules/require-meta-charset.js +3 -3
  35. package/lib/rules/require-meta-description.js +3 -3
  36. package/lib/rules/require-meta-viewport.js +3 -3
  37. package/lib/rules/require-open-graph-protocol.js +3 -3
  38. package/lib/rules/require-title.js +5 -5
  39. package/lib/rules/sort-attrs.js +12 -12
  40. package/lib/rules/utils/node.js +72 -32
  41. package/lib/rules/utils/settings.js +2 -2
  42. package/lib/rules/utils/visitors.js +1 -1
  43. package/lib/types/ast.d.ts +204 -0
  44. package/lib/types/index.d.ts +3 -0
  45. package/lib/types/rule.d.ts +83 -0
  46. package/lib/types/settings.ts +13 -0
  47. package/package.json +4 -4
  48. package/types/rules/attrs-newline.d.ts +2 -3
  49. package/types/rules/attrs-newline.d.ts.map +1 -1
  50. package/types/rules/element-newline.d.ts +8 -9
  51. package/types/rules/element-newline.d.ts.map +1 -1
  52. package/types/rules/id-naming-convention.d.ts +4 -4
  53. package/types/rules/id-naming-convention.d.ts.map +1 -1
  54. package/types/rules/indent/indent.d.ts +12 -5
  55. package/types/rules/indent/indent.d.ts.map +1 -1
  56. package/types/rules/index.d.ts +8 -0
  57. package/types/rules/lowercase.d.ts +4 -5
  58. package/types/rules/lowercase.d.ts.map +1 -1
  59. package/types/rules/max-element-depth.d.ts +10 -0
  60. package/types/rules/max-element-depth.d.ts.map +1 -0
  61. package/types/rules/no-abstract-roles.d.ts +4 -4
  62. package/types/rules/no-abstract-roles.d.ts.map +1 -1
  63. package/types/rules/no-accesskey-attrs.d.ts +4 -4
  64. package/types/rules/no-accesskey-attrs.d.ts.map +1 -1
  65. package/types/rules/no-duplicate-attrs.d.ts +4 -4
  66. package/types/rules/no-duplicate-attrs.d.ts.map +1 -1
  67. package/types/rules/no-duplicate-id.d.ts +5 -5
  68. package/types/rules/no-duplicate-id.d.ts.map +1 -1
  69. package/types/rules/no-extra-spacing-attrs.d.ts +11 -11
  70. package/types/rules/no-extra-spacing-attrs.d.ts.map +1 -1
  71. package/types/rules/no-extra-spacing-text.d.ts +7 -8
  72. package/types/rules/no-extra-spacing-text.d.ts.map +1 -1
  73. package/types/rules/no-heading-inside-button.d.ts +7 -0
  74. package/types/rules/no-heading-inside-button.d.ts.map +1 -0
  75. package/types/rules/no-inline-styles.d.ts +1 -2
  76. package/types/rules/no-inline-styles.d.ts.map +1 -1
  77. package/types/rules/no-invalid-role.d.ts +7 -0
  78. package/types/rules/no-invalid-role.d.ts.map +1 -0
  79. package/types/rules/no-multiple-empty-lines.d.ts +5 -5
  80. package/types/rules/no-multiple-empty-lines.d.ts.map +1 -1
  81. package/types/rules/no-multiple-h1.d.ts +2 -2
  82. package/types/rules/no-multiple-h1.d.ts.map +1 -1
  83. package/types/rules/no-nested-interactive.d.ts +8 -0
  84. package/types/rules/no-nested-interactive.d.ts.map +1 -0
  85. package/types/rules/no-positive-tabindex.d.ts +4 -4
  86. package/types/rules/no-positive-tabindex.d.ts.map +1 -1
  87. package/types/rules/no-restricted-attr-values.d.ts +5 -5
  88. package/types/rules/no-restricted-attr-values.d.ts.map +1 -1
  89. package/types/rules/no-restricted-attrs.d.ts +5 -5
  90. package/types/rules/no-restricted-attrs.d.ts.map +1 -1
  91. package/types/rules/no-script-style-type.d.ts +4 -4
  92. package/types/rules/no-script-style-type.d.ts.map +1 -1
  93. package/types/rules/no-skip-heading-levels.d.ts +2 -2
  94. package/types/rules/no-skip-heading-levels.d.ts.map +1 -1
  95. package/types/rules/no-trailing-spaces.d.ts +3 -3
  96. package/types/rules/no-trailing-spaces.d.ts.map +1 -1
  97. package/types/rules/prefer-https.d.ts +11 -0
  98. package/types/rules/prefer-https.d.ts.map +1 -0
  99. package/types/rules/quotes.d.ts +6 -6
  100. package/types/rules/quotes.d.ts.map +1 -1
  101. package/types/rules/require-attrs.d.ts +9 -4
  102. package/types/rules/require-attrs.d.ts.map +1 -1
  103. package/types/rules/require-closing-tags.d.ts +2 -2
  104. package/types/rules/require-closing-tags.d.ts.map +1 -1
  105. package/types/rules/require-explicit-size.d.ts +9 -0
  106. package/types/rules/require-explicit-size.d.ts.map +1 -0
  107. package/types/rules/require-form-method.d.ts +7 -0
  108. package/types/rules/require-form-method.d.ts.map +1 -0
  109. package/types/rules/require-img-alt.d.ts +2 -2
  110. package/types/rules/require-img-alt.d.ts.map +1 -1
  111. package/types/rules/require-input-label.d.ts +8 -0
  112. package/types/rules/require-input-label.d.ts.map +1 -0
  113. package/types/rules/require-meta-charset.d.ts +3 -3
  114. package/types/rules/require-meta-charset.d.ts.map +1 -1
  115. package/types/rules/require-meta-description.d.ts +3 -3
  116. package/types/rules/require-meta-description.d.ts.map +1 -1
  117. package/types/rules/require-meta-viewport.d.ts +3 -3
  118. package/types/rules/require-meta-viewport.d.ts.map +1 -1
  119. package/types/rules/require-open-graph-protocol.d.ts +3 -3
  120. package/types/rules/require-open-graph-protocol.d.ts.map +1 -1
  121. package/types/rules/require-title.d.ts +4 -4
  122. package/types/rules/require-title.d.ts.map +1 -1
  123. package/types/rules/sort-attrs.d.ts +4 -4
  124. package/types/rules/sort-attrs.d.ts.map +1 -1
  125. package/types/rules/utils/node.d.ts +56 -37
  126. package/types/rules/utils/node.d.ts.map +1 -1
  127. package/types/rules/utils/settings.d.ts +2 -2
  128. package/types/rules/utils/settings.d.ts.map +1 -1
  129. package/types/rules/utils/source-code.d.ts +1 -1
  130. package/types/rules/utils/source-code.d.ts.map +1 -1
  131. package/types/rules/utils/visitors.d.ts +1 -1
  132. package/types/rules/utils/visitors.d.ts.map +1 -1
  133. package/types/types/settings.d.ts +13 -0
  134. package/types/types/settings.d.ts.map +1 -0
  135. package/lib/types.d.ts +0 -289
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("../types").AttributeNode } AttributeNode
4
- * @typedef { import("../types").OpenTagEndNode } OpenTagEndNode
5
- * @typedef { import("../types").OpenScriptTagEndNode } OpenScriptTagEndNode
6
- * @typedef { import("../types").OpenStyleTagEndNode } OpenStyleTagEndNode
7
- * @typedef { import("../types").OpenScriptTagStartNode } OpenScriptTagStartNode
8
- * @typedef { import("../types").OpenTagStartNode } OpenTagStartNode
9
- * @typedef { import("../types").OpenStyleTagStartNode } OpenStyleTagStartNode
10
- * @typedef { import("../types").TagNode } TagNode
11
- * @typedef { import("../types").StyleTagNode } StyleTagNode
12
- * @typedef { import("../types").ScriptTagNode } ScriptTagNode
3
+ * @typedef { import("../types").Attribute } Attribute
4
+ * @typedef { import("../types").OpenTagEnd } OpenTagEnd
5
+ * @typedef { import("../types").OpenScriptTagEnd } OpenScriptTagEnd
6
+ * @typedef { import("../types").OpenStyleTagEnd } OpenStyleTagEnd
7
+ * @typedef { import("../types").OpenScriptTagStart } OpenScriptTagStart
8
+ * @typedef { import("../types").OpenTagStart } OpenTagStart
9
+ * @typedef { import("../types").OpenStyleTagStart } OpenStyleTagStart
10
+ * @typedef { import("../types").Tag } Tag
11
+ * @typedef { import("../types").StyleTag } StyleTag
12
+ * @typedef { import("../types").ScriptTag } ScriptTag
13
13
  * @typedef { import("../types").AnyNode } AnyNode
14
14
  */
15
15
 
@@ -95,7 +95,7 @@ module.exports = {
95
95
  const sourceCode = context.getSourceCode().text;
96
96
 
97
97
  /**
98
- * @param {AttributeNode[]} attrs
98
+ * @param {Attribute[]} attrs
99
99
  */
100
100
  function checkExtraSpacesBetweenAttrs(attrs) {
101
101
  attrs.forEach((current, index, attrs) => {
@@ -145,8 +145,8 @@ module.exports = {
145
145
  }
146
146
 
147
147
  /**
148
- * @param {OpenScriptTagStartNode | OpenTagStartNode | OpenStyleTagStartNode} node
149
- * @param {AttributeNode} firstAttr
148
+ * @param {OpenScriptTagStart | OpenTagStart | OpenStyleTagStart} node
149
+ * @param {Attribute} firstAttr
150
150
  * @returns
151
151
  */
152
152
  function checkExtraSpaceBefore(node, firstAttr) {
@@ -184,7 +184,7 @@ module.exports = {
184
184
  }
185
185
  }
186
186
  /**
187
- * @param {TagNode | StyleTagNode | ScriptTagNode} node
187
+ * @param {Tag | StyleTag | ScriptTag} node
188
188
  * @returns
189
189
  */
190
190
  function check(node) {
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("es-html-parser").CommentContentNode } CommentContentNode
4
- * @typedef { import("es-html-parser").TagNode } TagNode
5
- * @typedef { import("es-html-parser").CommentNode } CommentNode
6
- * @typedef { import("../types").ContentNode } ContentNode
7
- * @typedef { import("es-html-parser").TextNode } TextNode
8
- * @typedef { import("../types").LineNode } LineNode
9
- * @typedef { import("../types").Range } Range
3
+ * @typedef { import("../types").CommentContent } CommentContent
4
+ * @typedef { import("../types").Tag } Tag
5
+ * @typedef { import("../types").Comment } Comment
6
+ * @typedef { import("../types").Text } Text
7
+ * @typedef { import("../types").Line } Line
8
+ * @typedef { import("eslint").AST.Range } Range
10
9
  */
11
10
 
12
11
  const { RULE_CATEGORY } = require("../constants");
@@ -60,12 +59,12 @@ module.exports = {
60
59
  const skipTags = options.skip || [];
61
60
  const sourceCode = getSourceCode(context);
62
61
  /**
63
- * @type {TagNode[]}
62
+ * @type {Tag[]}
64
63
  */
65
64
  const tagStack = [];
66
65
 
67
66
  /**
68
- * @param {CommentNode | TextNode} node
67
+ * @param {Comment | Text} node
69
68
  * @returns {boolean}
70
69
  */
71
70
  function hasSkipTagOnParent(node) {
@@ -83,7 +82,7 @@ module.exports = {
83
82
  }
84
83
 
85
84
  /**
86
- * @param {CommentContentNode | TextNode} node
85
+ * @param {CommentContent | Text} node
87
86
  */
88
87
  function stripConsecutiveSpaces(node) {
89
88
  const text = node.value;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @typedef { import("../types").RuleModule } RuleModule
3
+ */
4
+
5
+ const { RULE_CATEGORY } = require("../constants");
6
+ const { findParent, isTag } = require("./utils/node");
7
+ const { createVisitors } = require("./utils/visitors");
8
+ const MESSAGE_IDS = {
9
+ UNEXPECTED: "unexpected",
10
+ };
11
+
12
+ const HEADING_NAMES = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
13
+
14
+ /**
15
+ * @type {RuleModule}
16
+ */
17
+ module.exports = {
18
+ meta: {
19
+ type: "code",
20
+
21
+ docs: {
22
+ description: "Disallows the use of heading elements inside <button>.",
23
+ category: RULE_CATEGORY.ACCESSIBILITY,
24
+ recommended: false,
25
+ },
26
+
27
+ fixable: null,
28
+ schema: [],
29
+ messages: {
30
+ [MESSAGE_IDS.UNEXPECTED]: "Unexpected heading inside button",
31
+ },
32
+ },
33
+
34
+ create(context) {
35
+ return createVisitors(context, {
36
+ Tag(node) {
37
+ if (!HEADING_NAMES.has(node.name.toLowerCase())) {
38
+ return;
39
+ }
40
+ const button = findParent(
41
+ node,
42
+ (parent) => isTag(parent) && parent.name.toLowerCase() === "button"
43
+ );
44
+
45
+ if (button) {
46
+ context.report({
47
+ node,
48
+ messageId: MESSAGE_IDS.UNEXPECTED,
49
+ });
50
+ }
51
+ },
52
+ });
53
+ },
54
+ };
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("../types").RuleListener } RuleListener
4
3
  */
5
4
 
6
5
  const { RULE_CATEGORY } = require("../constants");
@@ -0,0 +1,287 @@
1
+ /**
2
+ * @typedef { import("../types").RuleModule } RuleModule
3
+ */
4
+
5
+ const { RULE_CATEGORY } = require("../constants");
6
+ const { findAttr } = require("./utils/node");
7
+ const { createVisitors } = require("./utils/visitors");
8
+ const MESSAGE_IDS = {
9
+ INVALID: "invalid",
10
+ INVALID_PRESENTATION: "invalidPresentation",
11
+ };
12
+
13
+ /**
14
+ * Elements and role attribute constants are taken from ember-template-lint.
15
+ * https://github.com/ember-template-lint/ember-template-lint/blob/master/lib/rules/no-invalid-role.js
16
+ */
17
+
18
+ // https://www.w3.org/TR/wai-aria/#document_structure_roles
19
+ const DOCUMENT_STRUCTURE_ROLES = new Set([
20
+ "application",
21
+ "article",
22
+ "associationlist",
23
+ "associationlistitemkey",
24
+ "associationlistitemvalue",
25
+ "blockquote",
26
+ "caption",
27
+ "cell",
28
+ "code",
29
+ "columnheader",
30
+ "comment",
31
+ "definition",
32
+ "deletion",
33
+ "directory",
34
+ "document",
35
+ "emphasis",
36
+ "feed",
37
+ "figure",
38
+ "generic",
39
+ "group",
40
+ "heading",
41
+ "img",
42
+ "insertion",
43
+ "list",
44
+ "listitem",
45
+ "mark",
46
+ "math",
47
+ "meter",
48
+ "none",
49
+ "note",
50
+ "paragraph",
51
+ "presentation",
52
+ "row",
53
+ "rowgroup",
54
+ "rowheader",
55
+ "separator", // When not focusable
56
+ "strong",
57
+ "subscript",
58
+ "suggestion",
59
+ "superscript",
60
+ "table",
61
+ "term",
62
+ "time",
63
+ "toolbar",
64
+ "tooltip",
65
+ ]);
66
+
67
+ // https://www.w3.org/TR/wai-aria/#widget_roles
68
+ const WIDGET_ROLES = new Set([
69
+ "button",
70
+ "checkbox",
71
+ "gridcell",
72
+ "link",
73
+ "menuitem",
74
+ "menuitemcheckbox",
75
+ "menuitemradio",
76
+ "option",
77
+ "progressbar",
78
+ "radio",
79
+ "scrollbar",
80
+ "searchbox",
81
+ "separator", // When focusable
82
+ "slider",
83
+ "spinbutton",
84
+ "switch",
85
+ "tab",
86
+ "tabpanel",
87
+ "textbox",
88
+ "treeitem",
89
+ ]);
90
+
91
+ const COMPOSITE_WIDGET_ROLES = new Set([
92
+ "combobox",
93
+ "grid",
94
+ "listbox",
95
+ "menu",
96
+ "menubar",
97
+ "radiogroup",
98
+ "tablist",
99
+ "tree",
100
+ "treegrid",
101
+ ]);
102
+
103
+ // https://www.w3.org/TR/wai-aria/#landmark_roles
104
+ const LANDMARK_ROLES = new Set([
105
+ "banner",
106
+ "complementary",
107
+ "contentinfo",
108
+ "form",
109
+ "main",
110
+ "navigation",
111
+ "region",
112
+ "search",
113
+ ]);
114
+
115
+ // https://www.w3.org/TR/wai-aria/#live_region_roles
116
+ const LIVE_REGION_ROLES = new Set([
117
+ "alert",
118
+ "log",
119
+ "marquee",
120
+ "status",
121
+ "timer",
122
+ ]);
123
+
124
+ // https://www.w3.org/TR/wai-aria/#window_roles
125
+ const WINDOW_ROLES = new Set(["alertdialog", "dialog"]);
126
+
127
+ const ALL_ROLES = new Set([
128
+ ...DOCUMENT_STRUCTURE_ROLES,
129
+ ...WIDGET_ROLES,
130
+ ...COMPOSITE_WIDGET_ROLES,
131
+ ...LANDMARK_ROLES,
132
+ ...LIVE_REGION_ROLES,
133
+ ...WINDOW_ROLES,
134
+ ]);
135
+
136
+ const ELEMENTS_DISALLOWING_PRESENTATION_OR_NONE_ROLE = new Set([
137
+ "a",
138
+ "abbr",
139
+ "applet",
140
+ "area",
141
+ "audio",
142
+ "b",
143
+ "bdi",
144
+ "bdo",
145
+ "blockquote",
146
+ "br",
147
+ "button",
148
+ "caption",
149
+ "cite",
150
+ "code",
151
+ "col",
152
+ "colgroup",
153
+ "data",
154
+ "datalist",
155
+ "dd",
156
+ "del",
157
+ "details",
158
+ "dfn",
159
+ "dialog",
160
+ "dir",
161
+ "dl",
162
+ "dt",
163
+ "em",
164
+ "embed",
165
+ "fieldset",
166
+ "figcaption",
167
+ "figure",
168
+ "form",
169
+ "hr",
170
+ "i",
171
+ "iframe",
172
+ "input",
173
+ "ins",
174
+ "kbd",
175
+ "label",
176
+ "legend",
177
+ "main",
178
+ "map",
179
+ "mark",
180
+ "menu",
181
+ "menuitem",
182
+ "meter",
183
+ "noembed",
184
+ "object",
185
+ "ol",
186
+ "optgroup",
187
+ "option",
188
+ "output",
189
+ "p",
190
+ "param",
191
+ "pre",
192
+ "progress",
193
+ "q",
194
+ "rb",
195
+ "rp",
196
+ "rt",
197
+ "rtc",
198
+ "ruby",
199
+ "s",
200
+ "samp",
201
+ "select",
202
+ "small",
203
+ "source",
204
+ "strong",
205
+ "sub",
206
+ "summary",
207
+ "sup",
208
+ "table",
209
+ "tbody",
210
+ "td",
211
+ "textarea",
212
+ "tfoot",
213
+ "th",
214
+ "thead",
215
+ "time",
216
+ "tr",
217
+ "track",
218
+ "tt",
219
+ "u",
220
+ "ul",
221
+ "var",
222
+ "video",
223
+ "wbr",
224
+ ]);
225
+
226
+ /**
227
+ * @type {RuleModule}
228
+ */
229
+ module.exports = {
230
+ meta: {
231
+ type: "code",
232
+
233
+ docs: {
234
+ description: "Disallows use of invalid role.",
235
+ category: RULE_CATEGORY.ACCESSIBILITY,
236
+ recommended: false,
237
+ },
238
+
239
+ fixable: null,
240
+ schema: [],
241
+ messages: {
242
+ [MESSAGE_IDS.INVALID]: "Unexpected use of invalid role '{{role}}'",
243
+ [MESSAGE_IDS.INVALID_PRESENTATION]:
244
+ "Unexpected use of presentation role on <{{element}}>",
245
+ },
246
+ },
247
+
248
+ create(context) {
249
+ return createVisitors(context, {
250
+ Tag(node) {
251
+ const role = findAttr(node, "role");
252
+ if (!role) {
253
+ return;
254
+ }
255
+ const roleValue = (
256
+ (role.value && role.value.value) ||
257
+ ""
258
+ ).toLowerCase();
259
+
260
+ if (
261
+ (roleValue === "presentation" || roleValue === "none") &&
262
+ ELEMENTS_DISALLOWING_PRESENTATION_OR_NONE_ROLE.has(
263
+ node.name.toLowerCase()
264
+ )
265
+ ) {
266
+ context.report({
267
+ node: role,
268
+ messageId: MESSAGE_IDS.INVALID_PRESENTATION,
269
+ data: {
270
+ element: node.name,
271
+ },
272
+ });
273
+ }
274
+
275
+ if (!ALL_ROLES.has(roleValue)) {
276
+ context.report({
277
+ node: role,
278
+ messageId: MESSAGE_IDS.INVALID,
279
+ data: {
280
+ role: roleValue,
281
+ },
282
+ });
283
+ }
284
+ },
285
+ });
286
+ },
287
+ };
@@ -1,9 +1,9 @@
1
1
  /**
2
- * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("es-html-parser").DocumentNode } DocumentNode
4
2
  * @typedef { import("es-html-parser").AnyToken } AnyToken
5
- * @typedef { import("es-html-parser").CommentContentNode } CommentContentNode
6
- * @typedef { import("es-html-parser").TextNode } TextNode
3
+ * @typedef { import("../types").RuleModule } RuleModule
4
+ * @typedef { import("../types").Document } Document
5
+ * @typedef { import("../types").CommentContent } CommentContent
6
+ * @typedef { import("../types").Text } Text
7
7
  */
8
8
 
9
9
  const { parse } = require("@html-eslint/template-parser");
@@ -62,7 +62,7 @@ module.exports = {
62
62
  /**
63
63
  * @param {string[]} lines
64
64
  * @param {number} lineOffset
65
- * @param {((CommentContentNode | TextNode)['templates'][number])[]} tokens
65
+ * @param {((CommentContent | Text)['templates'][number])[]} tokens
66
66
  */
67
67
  function check(lines, lineOffset, tokens) {
68
68
  /** @type {number[]} */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("../types").TagNode } TagNode
3
+ * @typedef { import("../types").Tag } Tag
4
4
  */
5
5
 
6
6
  const { RULE_CATEGORY } = require("../constants");
@@ -31,7 +31,7 @@ module.exports = {
31
31
 
32
32
  create(context) {
33
33
  /**
34
- * @type {TagNode[]}
34
+ * @type {Tag[]}
35
35
  */
36
36
  const h1s = [];
37
37
 
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @typedef { import("../types").RuleModule } RuleModule
3
+ * @typedef { import("../types").Tag } Tag
4
+ */
5
+
6
+ const { RULE_CATEGORY } = require("../constants");
7
+ const { findAttr } = require("./utils/node");
8
+ const { createVisitors } = require("./utils/visitors");
9
+
10
+ const MESSAGE_IDS = {
11
+ UNEXPECTED: "unexpected",
12
+ };
13
+
14
+ /**
15
+ * @see https://html.spec.whatwg.org/multipage/dom.html#interactive-content-2
16
+ * @param {Tag} tag
17
+ * @returns {boolean}
18
+ */
19
+ function isInteractive(tag) {
20
+ const tagName = tag.name.toLowerCase();
21
+
22
+ switch (tagName) {
23
+ // a (if the href attribute is present)
24
+ case "a": {
25
+ return !!findAttr(tag, "href");
26
+ }
27
+ // audio (if the controls attribute is present)
28
+ // video (if the controls attribute is present)
29
+ case "audio":
30
+ case "video": {
31
+ return !!findAttr(tag, "controls");
32
+ }
33
+ // img (if the usemap attribute is present)
34
+ case "img": {
35
+ return !!findAttr(tag, "usemap");
36
+ }
37
+ // input (if the type attribute is not in the Hidden state)
38
+ case "input": {
39
+ const typeAttr = findAttr(tag, "type");
40
+ return !(typeAttr && typeAttr.value && typeAttr.value.value === "hidden");
41
+ }
42
+ case "button":
43
+ case "details":
44
+ case "embed":
45
+ case "iframe":
46
+ case "label":
47
+ case "select":
48
+ case "textarea": {
49
+ return true;
50
+ }
51
+ default: {
52
+ return false;
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * @type {RuleModule}
59
+ */
60
+ module.exports = {
61
+ meta: {
62
+ type: "code",
63
+
64
+ docs: {
65
+ description: "Disallows nested interactive elements",
66
+ category: RULE_CATEGORY.BEST_PRACTICE,
67
+ recommended: false,
68
+ },
69
+
70
+ fixable: null,
71
+ schema: [],
72
+ messages: {
73
+ [MESSAGE_IDS.UNEXPECTED]:
74
+ "Unexpected interactive elements nested (parent interactive: <{{tag}}>)",
75
+ },
76
+ },
77
+
78
+ create(context) {
79
+ /**
80
+ * @type {Tag[]}
81
+ */
82
+ let interactiveStack = [];
83
+
84
+ return createVisitors(context, {
85
+ Document() {
86
+ interactiveStack = [];
87
+ },
88
+ Tag(node) {
89
+ if (!isInteractive(node)) {
90
+ return;
91
+ }
92
+ if (interactiveStack.length) {
93
+ if (interactiveStack.length === 1) {
94
+ const parentLabel = interactiveStack.find(
95
+ (tag) => tag.name.toLowerCase() === "label"
96
+ );
97
+ if (parentLabel && node.name.toLowerCase() !== "label") {
98
+ return;
99
+ }
100
+ }
101
+
102
+ context.report({
103
+ node,
104
+ messageId: MESSAGE_IDS.UNEXPECTED,
105
+ data: {
106
+ tag: interactiveStack[interactiveStack.length - 1].name,
107
+ },
108
+ });
109
+ }
110
+
111
+ interactiveStack.push(node);
112
+ },
113
+ "Tag:exit"(node) {
114
+ if (interactiveStack[interactiveStack.length - 1] === node) {
115
+ interactiveStack.pop();
116
+ }
117
+ },
118
+ });
119
+ },
120
+ };
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("../types").TagNode } TagNode
4
- * @typedef { import("../types").StyleTagNode } StyleTagNode
5
- * @typedef { import("../types").ScriptTagNode } ScriptTagNode
3
+ * @typedef { import("../types").Tag } Tag
4
+ * @typedef { import("../types").StyleTag } StyleTag
5
+ * @typedef { import("../types").ScriptTag } ScriptTag
6
6
  */
7
7
 
8
8
  const { RULE_CATEGORY } = require("../constants");
@@ -35,7 +35,7 @@ module.exports = {
35
35
 
36
36
  create(context) {
37
37
  /**
38
- * @param {TagNode | StyleTagNode | ScriptTagNode} node
38
+ * @param {Tag | StyleTag | ScriptTag} node
39
39
  */
40
40
  function check(node) {
41
41
  const tabIndexAttr = findAttr(node, "tabindex");
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("../types").StyleTagNode } StyleTagNode
4
- * @typedef { import("../types").AttributeNode } AttributeNode
5
- * @typedef { import("../types").TagNode } TagNode
6
- * @typedef { import("../types").ScriptTagNode } ScriptTagNode
3
+ * @typedef { import("../types").StyleTag } StyleTag
4
+ * @typedef { import("../types").Attribute } Attribute
5
+ * @typedef { import("../types").Tag } Tag
6
+ * @typedef { import("../types").ScriptTag } ScriptTag
7
7
  * @typedef {{attrPatterns: string[], attrValuePatterns: string[], message?: string}[]} Options
8
8
  */
9
9
 
@@ -66,7 +66,7 @@ module.exports = {
66
66
  const options = context.options;
67
67
  const checkers = options.map((option) => new PatternChecker(option));
68
68
  /**
69
- * @param {TagNode | StyleTagNode | ScriptTagNode} node
69
+ * @param {Tag | StyleTag | ScriptTag} node
70
70
  */
71
71
  function check(node) {
72
72
  node.attributes.forEach((attr) => {
@@ -89,7 +89,7 @@ module.exports = {
89
89
  }
90
90
 
91
91
  /**
92
- * @type {{node: AttributeNode, message: string, messageId?: string}}
92
+ * @type {{node: Attribute, message: string, messageId?: string}}
93
93
  */
94
94
  const result = {
95
95
  node: attr,
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @typedef { import("../types").RuleModule } RuleModule
3
- * @typedef { import("../types").StyleTagNode } StyleTagNode
4
- * @typedef { import("../types").AttributeNode } AttributeNode
5
- * @typedef { import("../types").TagNode } TagNode
6
- * @typedef { import("../types").ScriptTagNode } ScriptTagNode
3
+ * @typedef { import("../types").StyleTag } StyleTag
4
+ * @typedef { import("../types").Attribute } Attribute
5
+ * @typedef { import("../types").Tag } Tag
6
+ * @typedef { import("../types").ScriptTag } ScriptTag
7
7
  * @typedef {{tagPatterns: string[], attrPatterns: string[], message?: string}[]} Options
8
8
  */
9
9
 
@@ -67,7 +67,7 @@ module.exports = {
67
67
  const checkers = options.map((option) => new PatternChecker(option));
68
68
 
69
69
  /**
70
- * @param {TagNode | StyleTagNode | ScriptTagNode} node
70
+ * @param {Tag | StyleTag | ScriptTag} node
71
71
  */
72
72
  function check(node) {
73
73
  const tagName =
@@ -89,7 +89,7 @@ module.exports = {
89
89
  }
90
90
 
91
91
  /**
92
- * @type {{node: AttributeNode, message: string, messageId?: string}}
92
+ * @type {{node: Attribute, message: string, messageId?: string}}
93
93
  */
94
94
  const result = {
95
95
  node: attr,