@html-eslint/eslint-plugin 0.41.0 → 0.43.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 (195) hide show
  1. package/lib/configs/recommended.js +1 -0
  2. package/lib/data/entities.json +2299 -0
  3. package/lib/languages/html-language.js +3 -3
  4. package/lib/languages/html-source-code.js +9 -11
  5. package/lib/languages/html-traversal-step.js +4 -3
  6. package/lib/rules/attrs-newline.js +3 -4
  7. package/lib/rules/element-newline.js +4 -14
  8. package/lib/rules/id-naming-convention.js +3 -6
  9. package/lib/rules/indent/indent-level.js +1 -1
  10. package/lib/rules/indent/indent.js +18 -23
  11. package/lib/rules/index.js +8 -0
  12. package/lib/rules/lowercase.js +11 -10
  13. package/lib/rules/max-element-depth.js +4 -6
  14. package/lib/rules/no-abstract-roles.js +3 -5
  15. package/lib/rules/no-accesskey-attrs.js +3 -5
  16. package/lib/rules/no-aria-hidden-body.js +2 -2
  17. package/lib/rules/no-aria-hidden-on-focusable.js +118 -0
  18. package/lib/rules/no-duplicate-attrs.js +3 -7
  19. package/lib/rules/no-duplicate-class.js +3 -6
  20. package/lib/rules/no-duplicate-id.js +6 -9
  21. package/lib/rules/no-duplicate-in-head.js +188 -0
  22. package/lib/rules/no-empty-headings.js +121 -0
  23. package/lib/rules/no-extra-spacing-attrs.js +4 -14
  24. package/lib/rules/no-extra-spacing-text.js +4 -10
  25. package/lib/rules/no-heading-inside-button.js +2 -2
  26. package/lib/rules/no-inline-styles.js +2 -2
  27. package/lib/rules/no-invalid-entity.js +107 -0
  28. package/lib/rules/no-invalid-role.js +2 -2
  29. package/lib/rules/no-multiple-empty-lines.js +10 -11
  30. package/lib/rules/no-multiple-h1.js +3 -3
  31. package/lib/rules/no-nested-interactive.js +3 -3
  32. package/lib/rules/no-non-scalable-viewport.js +2 -2
  33. package/lib/rules/no-obsolete-tags.js +2 -2
  34. package/lib/rules/no-positive-tabindex.js +3 -5
  35. package/lib/rules/no-restricted-attr-values.js +4 -6
  36. package/lib/rules/no-restricted-attrs.js +4 -6
  37. package/lib/rules/no-script-style-type.js +3 -5
  38. package/lib/rules/no-skip-heading-levels.js +3 -3
  39. package/lib/rules/no-target-blank.js +2 -2
  40. package/lib/rules/no-trailing-spaces.js +10 -9
  41. package/lib/rules/prefer-https.js +3 -6
  42. package/lib/rules/quotes.js +25 -10
  43. package/lib/rules/require-attrs.js +3 -8
  44. package/lib/rules/require-button-type.js +3 -4
  45. package/lib/rules/require-closing-tags.js +3 -3
  46. package/lib/rules/require-doctype.js +2 -2
  47. package/lib/rules/require-explicit-size.js +2 -5
  48. package/lib/rules/require-form-method.js +2 -2
  49. package/lib/rules/require-frame-title.js +2 -2
  50. package/lib/rules/require-img-alt.js +40 -17
  51. package/lib/rules/require-input-label.js +3 -3
  52. package/lib/rules/require-lang.js +2 -2
  53. package/lib/rules/require-li-container.js +2 -2
  54. package/lib/rules/require-meta-charset.js +3 -4
  55. package/lib/rules/require-meta-description.js +3 -4
  56. package/lib/rules/require-meta-viewport.js +3 -4
  57. package/lib/rules/require-open-graph-protocol.js +3 -6
  58. package/lib/rules/require-title.js +3 -5
  59. package/lib/rules/sort-attrs.js +4 -5
  60. package/lib/rules/use-baseline.js +3 -6
  61. package/lib/rules/utils/baseline.js +7 -4
  62. package/lib/rules/utils/node.js +11 -26
  63. package/lib/rules/utils/settings.js +4 -7
  64. package/lib/rules/utils/source-code.js +2 -2
  65. package/lib/rules/utils/template-literal.js +43 -0
  66. package/lib/rules/utils/visitors.js +6 -7
  67. package/package.json +6 -6
  68. package/types/configs/recommended.d.ts +1 -0
  69. package/types/index.d.ts +2 -0
  70. package/types/index.d.ts.map +1 -1
  71. package/types/languages/html-language.d.ts +0 -5
  72. package/types/languages/html-language.d.ts.map +1 -1
  73. package/types/languages/html-source-code.d.ts +13 -14
  74. package/types/languages/html-source-code.d.ts.map +1 -1
  75. package/types/languages/html-traversal-step.d.ts +5 -5
  76. package/types/languages/html-traversal-step.d.ts.map +1 -1
  77. package/types/rules/attrs-newline.d.ts +3 -4
  78. package/types/rules/attrs-newline.d.ts.map +1 -1
  79. package/types/rules/element-newline.d.ts +5 -13
  80. package/types/rules/element-newline.d.ts.map +1 -1
  81. package/types/rules/id-naming-convention.d.ts +3 -6
  82. package/types/rules/id-naming-convention.d.ts.map +1 -1
  83. package/types/rules/indent/indent-level.d.ts +3 -3
  84. package/types/rules/indent/indent-level.d.ts.map +1 -1
  85. package/types/rules/indent/indent.d.ts +5 -18
  86. package/types/rules/indent/indent.d.ts.map +1 -1
  87. package/types/rules/lowercase.d.ts +2 -8
  88. package/types/rules/lowercase.d.ts.map +1 -1
  89. package/types/rules/max-element-depth.d.ts +3 -6
  90. package/types/rules/max-element-depth.d.ts.map +1 -1
  91. package/types/rules/no-abstract-roles.d.ts +2 -8
  92. package/types/rules/no-abstract-roles.d.ts.map +1 -1
  93. package/types/rules/no-accesskey-attrs.d.ts +2 -8
  94. package/types/rules/no-accesskey-attrs.d.ts.map +1 -1
  95. package/types/rules/no-aria-hidden-body.d.ts +2 -5
  96. package/types/rules/no-aria-hidden-body.d.ts.map +1 -1
  97. package/types/rules/no-aria-hidden-on-focusable.d.ts +4 -0
  98. package/types/rules/no-aria-hidden-on-focusable.d.ts.map +1 -0
  99. package/types/rules/no-duplicate-attrs.d.ts +2 -10
  100. package/types/rules/no-duplicate-attrs.d.ts.map +1 -1
  101. package/types/rules/no-duplicate-class.d.ts +3 -7
  102. package/types/rules/no-duplicate-class.d.ts.map +1 -1
  103. package/types/rules/no-duplicate-id.d.ts +2 -9
  104. package/types/rules/no-duplicate-id.d.ts.map +1 -1
  105. package/types/rules/no-duplicate-in-head.d.ts +4 -0
  106. package/types/rules/no-duplicate-in-head.d.ts.map +1 -0
  107. package/types/rules/no-empty-headings.d.ts +4 -0
  108. package/types/rules/no-empty-headings.d.ts.map +1 -0
  109. package/types/rules/no-extra-spacing-attrs.d.ts +3 -14
  110. package/types/rules/no-extra-spacing-attrs.d.ts.map +1 -1
  111. package/types/rules/no-extra-spacing-text.d.ts +3 -9
  112. package/types/rules/no-extra-spacing-text.d.ts.map +1 -1
  113. package/types/rules/no-heading-inside-button.d.ts +2 -5
  114. package/types/rules/no-heading-inside-button.d.ts.map +1 -1
  115. package/types/rules/no-inline-styles.d.ts +2 -5
  116. package/types/rules/no-inline-styles.d.ts.map +1 -1
  117. package/types/rules/no-invalid-entity.d.ts +11 -0
  118. package/types/rules/no-invalid-entity.d.ts.map +1 -0
  119. package/types/rules/no-invalid-role.d.ts +2 -5
  120. package/types/rules/no-invalid-role.d.ts.map +1 -1
  121. package/types/rules/no-multiple-empty-lines.d.ts +3 -7
  122. package/types/rules/no-multiple-empty-lines.d.ts.map +1 -1
  123. package/types/rules/no-multiple-h1.d.ts +2 -6
  124. package/types/rules/no-multiple-h1.d.ts.map +1 -1
  125. package/types/rules/no-nested-interactive.d.ts +2 -6
  126. package/types/rules/no-nested-interactive.d.ts.map +1 -1
  127. package/types/rules/no-non-scalable-viewport.d.ts +2 -5
  128. package/types/rules/no-non-scalable-viewport.d.ts.map +1 -1
  129. package/types/rules/no-obsolete-tags.d.ts +2 -5
  130. package/types/rules/no-obsolete-tags.d.ts.map +1 -1
  131. package/types/rules/no-positive-tabindex.d.ts +2 -8
  132. package/types/rules/no-positive-tabindex.d.ts.map +1 -1
  133. package/types/rules/no-restricted-attr-values.d.ts +3 -7
  134. package/types/rules/no-restricted-attr-values.d.ts.map +1 -1
  135. package/types/rules/no-restricted-attrs.d.ts +3 -7
  136. package/types/rules/no-restricted-attrs.d.ts.map +1 -1
  137. package/types/rules/no-script-style-type.d.ts +2 -8
  138. package/types/rules/no-script-style-type.d.ts.map +1 -1
  139. package/types/rules/no-skip-heading-levels.d.ts +2 -6
  140. package/types/rules/no-skip-heading-levels.d.ts.map +1 -1
  141. package/types/rules/no-target-blank.d.ts +2 -5
  142. package/types/rules/no-target-blank.d.ts.map +1 -1
  143. package/types/rules/no-trailing-spaces.d.ts +2 -7
  144. package/types/rules/no-trailing-spaces.d.ts.map +1 -1
  145. package/types/rules/prefer-https.d.ts +2 -9
  146. package/types/rules/prefer-https.d.ts.map +1 -1
  147. package/types/rules/quotes.d.ts +7 -9
  148. package/types/rules/quotes.d.ts.map +1 -1
  149. package/types/rules/require-attrs.d.ts +3 -8
  150. package/types/rules/require-attrs.d.ts.map +1 -1
  151. package/types/rules/require-button-type.d.ts +2 -7
  152. package/types/rules/require-button-type.d.ts.map +1 -1
  153. package/types/rules/require-closing-tags.d.ts +3 -4
  154. package/types/rules/require-closing-tags.d.ts.map +1 -1
  155. package/types/rules/require-doctype.d.ts +2 -5
  156. package/types/rules/require-doctype.d.ts.map +1 -1
  157. package/types/rules/require-explicit-size.d.ts +3 -5
  158. package/types/rules/require-explicit-size.d.ts.map +1 -1
  159. package/types/rules/require-form-method.d.ts +2 -5
  160. package/types/rules/require-form-method.d.ts.map +1 -1
  161. package/types/rules/require-frame-title.d.ts +2 -5
  162. package/types/rules/require-frame-title.d.ts.map +1 -1
  163. package/types/rules/require-img-alt.d.ts +3 -4
  164. package/types/rules/require-img-alt.d.ts.map +1 -1
  165. package/types/rules/require-input-label.d.ts +2 -6
  166. package/types/rules/require-input-label.d.ts.map +1 -1
  167. package/types/rules/require-lang.d.ts +2 -5
  168. package/types/rules/require-lang.d.ts.map +1 -1
  169. package/types/rules/require-li-container.d.ts +2 -5
  170. package/types/rules/require-li-container.d.ts.map +1 -1
  171. package/types/rules/require-meta-charset.d.ts +2 -7
  172. package/types/rules/require-meta-charset.d.ts.map +1 -1
  173. package/types/rules/require-meta-description.d.ts +2 -7
  174. package/types/rules/require-meta-description.d.ts.map +1 -1
  175. package/types/rules/require-meta-viewport.d.ts +2 -7
  176. package/types/rules/require-meta-viewport.d.ts.map +1 -1
  177. package/types/rules/require-open-graph-protocol.d.ts +3 -5
  178. package/types/rules/require-open-graph-protocol.d.ts.map +1 -1
  179. package/types/rules/require-title.d.ts +2 -8
  180. package/types/rules/require-title.d.ts.map +1 -1
  181. package/types/rules/sort-attrs.d.ts +3 -6
  182. package/types/rules/sort-attrs.d.ts.map +1 -1
  183. package/types/rules/use-baseline.d.ts +3 -7
  184. package/types/rules/use-baseline.d.ts.map +1 -1
  185. package/types/rules/utils/baseline.d.ts.map +1 -1
  186. package/types/rules/utils/node.d.ts +25 -29
  187. package/types/rules/utils/node.d.ts.map +1 -1
  188. package/types/rules/utils/settings.d.ts +7 -9
  189. package/types/rules/utils/settings.d.ts.map +1 -1
  190. package/types/rules/utils/source-code.d.ts +4 -4
  191. package/types/rules/utils/source-code.d.ts.map +1 -1
  192. package/types/rules/utils/template-literal.d.ts +18 -0
  193. package/types/rules/utils/template-literal.d.ts.map +1 -0
  194. package/types/rules/utils/visitors.d.ts +4 -4
  195. package/types/rules/utils/visitors.d.ts.map +1 -1
@@ -0,0 +1,188 @@
1
+ /**
2
+ * @import {Tag} from "@html-eslint/types";
3
+ * @import {RuleModule} from "../types";
4
+ */
5
+
6
+ const { parseTemplateLiteral } = require("./utils/template-literal");
7
+ const { RULE_CATEGORY } = require("../constants");
8
+ const { findAttr } = require("./utils/node");
9
+ const {
10
+ shouldCheckTaggedTemplateExpression,
11
+ shouldCheckTemplateLiteral,
12
+ } = require("./utils/settings");
13
+ const { getSourceCode } = require("./utils/source-code");
14
+ const { getRuleUrl } = require("./utils/rule");
15
+
16
+ const MESSAGE_IDS = {
17
+ DUPLICATE_TAG: "duplicateTag",
18
+ };
19
+
20
+ /**
21
+ * Returns a formatted string representing a tag's key detail.
22
+ * E.g., meta[charset=UTF-8], meta[name=viewport], link[rel=canonical]
23
+ * @param {Tag} node
24
+ * @returns {string | null}
25
+ */
26
+ function getTrackingKey(node) {
27
+ const tagName = node.name.toLowerCase();
28
+
29
+ if (["title", "base"].includes(tagName)) {
30
+ return tagName;
31
+ }
32
+
33
+ if (tagName === "meta") {
34
+ const charsetAttr = findAttr(node, "charset");
35
+ if (charsetAttr) {
36
+ return "meta[charset]";
37
+ }
38
+
39
+ const nameAttr = findAttr(node, "name");
40
+ if (nameAttr && nameAttr.value && nameAttr.value.value === "viewport") {
41
+ return "meta[name=viewport]";
42
+ }
43
+ }
44
+
45
+ if (tagName === "link") {
46
+ const relAttr = findAttr(node, "rel");
47
+ const hrefAttr = findAttr(node, "href");
48
+ if (
49
+ relAttr &&
50
+ relAttr.value &&
51
+ relAttr.value.value === "canonical" &&
52
+ hrefAttr
53
+ ) {
54
+ return "link[rel=canonical]";
55
+ }
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * @type {RuleModule<[]>}
63
+ */
64
+ module.exports = {
65
+ meta: {
66
+ type: "code",
67
+ docs: {
68
+ description: "Disallow duplicate tags in `<head>`",
69
+ category: RULE_CATEGORY.BEST_PRACTICE,
70
+ recommended: false,
71
+ url: getRuleUrl("no-duplicate-in-head"),
72
+ },
73
+ fixable: null,
74
+ schema: [],
75
+ messages: {
76
+ [MESSAGE_IDS.DUPLICATE_TAG]: "Duplicate <{{tag}}> tag in <head>.",
77
+ },
78
+ },
79
+
80
+ create(context) {
81
+ const htmlTagsMap = new Map();
82
+ let headCount = 0;
83
+
84
+ /**
85
+ * @param {Map<string, Tag[]>} map
86
+ * @param {{count: number}|null} headCountRef
87
+ */
88
+ function createTagVisitor(map, headCountRef = null) {
89
+ return {
90
+ /**
91
+ * @param {Tag} node
92
+ */
93
+ Tag(node) {
94
+ const tagName = node.name.toLowerCase();
95
+
96
+ if (tagName === "head") {
97
+ if (headCountRef !== null) {
98
+ headCountRef.count++;
99
+ } else {
100
+ headCount++;
101
+ }
102
+ return;
103
+ }
104
+
105
+ const currentHeadCount =
106
+ headCountRef !== null ? headCountRef.count : headCount;
107
+ if (currentHeadCount === 0) return;
108
+
109
+ const trackingKey = getTrackingKey(node);
110
+ if (typeof trackingKey !== "string") return;
111
+
112
+ if (!map.has(trackingKey)) {
113
+ map.set(trackingKey, []);
114
+ }
115
+
116
+ const nodes = map.get(trackingKey);
117
+ if (nodes) {
118
+ nodes.push(node);
119
+ }
120
+ },
121
+
122
+ /**
123
+ * @param {Tag} node
124
+ */
125
+ "Tag:exit"(node) {
126
+ const tagName = node.name.toLowerCase();
127
+ if (tagName === "head") {
128
+ if (headCountRef !== null) {
129
+ headCountRef.count--;
130
+ } else {
131
+ headCount--;
132
+ }
133
+ }
134
+ },
135
+ };
136
+ }
137
+
138
+ /**
139
+ * @param {Map<string, Tag[]>} map
140
+ */
141
+ function report(map) {
142
+ map.forEach((tags, tagKey) => {
143
+ if (Array.isArray(tags) && tags.length > 1) {
144
+ tags.slice(1).forEach((tag) => {
145
+ context.report({
146
+ node: tag,
147
+ data: { tag: tagKey },
148
+ messageId: MESSAGE_IDS.DUPLICATE_TAG,
149
+ });
150
+ });
151
+ }
152
+ });
153
+ }
154
+
155
+ const htmlVisitor = createTagVisitor(htmlTagsMap);
156
+
157
+ return {
158
+ Tag: htmlVisitor.Tag,
159
+ "Tag:exit": htmlVisitor["Tag:exit"],
160
+
161
+ "Document:exit"() {
162
+ report(htmlTagsMap);
163
+ },
164
+
165
+ TaggedTemplateExpression(node) {
166
+ const tagsMap = new Map();
167
+ const headCountRef = { count: 0 };
168
+
169
+ if (shouldCheckTaggedTemplateExpression(node, context)) {
170
+ const visitor = createTagVisitor(tagsMap, headCountRef);
171
+ parseTemplateLiteral(node.quasi, getSourceCode(context), visitor);
172
+ report(tagsMap);
173
+ }
174
+ },
175
+
176
+ TemplateLiteral(node) {
177
+ const tagsMap = new Map();
178
+ const headCountRef = { count: 0 };
179
+
180
+ if (shouldCheckTemplateLiteral(node, context)) {
181
+ const visitor = createTagVisitor(tagsMap, headCountRef);
182
+ parseTemplateLiteral(node, getSourceCode(context), visitor);
183
+ report(tagsMap);
184
+ }
185
+ },
186
+ };
187
+ },
188
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @import {Tag, Text} from "@html-eslint/types";
3
+ * @import {RuleModule} from "../types";
4
+ */
5
+
6
+ const { RULE_CATEGORY } = require("../constants");
7
+ const { findAttr, isTag, isText } = require("./utils/node");
8
+ const { createVisitors } = require("./utils/visitors");
9
+ const { getRuleUrl } = require("./utils/rule");
10
+
11
+ const MESSAGE_IDS = {
12
+ EMPTY_HEADING: "emptyHeading",
13
+ INACCESSIBLE_HEADING: "inaccessibleHeading",
14
+ };
15
+
16
+ const HEADING_NAMES = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
17
+
18
+ /**
19
+ * @param {Tag} node
20
+ */
21
+ function isAriaHidden(node) {
22
+ const ariaHiddenAttr = findAttr(node, "aria-hidden");
23
+ return (
24
+ ariaHiddenAttr &&
25
+ ariaHiddenAttr.value &&
26
+ ariaHiddenAttr.value.value === "true"
27
+ );
28
+ }
29
+
30
+ /**
31
+ * @param {Tag} node
32
+ * @returns {boolean}
33
+ */
34
+ function isRoleHeading(node) {
35
+ const roleAttr = findAttr(node, "role");
36
+ return !!roleAttr && !!roleAttr.value && roleAttr.value.value === "heading";
37
+ }
38
+
39
+ /**
40
+ * @param {Text | Tag} node
41
+ * @returns {string}
42
+ */
43
+ function getAllText(node) {
44
+ if (!isTag(node) || !node.children.length) return "";
45
+ let text = "";
46
+ for (const child of node.children) {
47
+ if (isText(child)) {
48
+ text += child.value.trim();
49
+ } else if (isTag(child)) {
50
+ text += getAllText(child);
51
+ }
52
+ }
53
+ return text;
54
+ }
55
+
56
+ /**
57
+ * @param {Text | Tag} node
58
+ * @returns {string}
59
+ */
60
+ function getAccessibleText(node) {
61
+ if (!isTag(node) || !node.children.length) return "";
62
+ let text = "";
63
+ for (const child of node.children) {
64
+ if (isText(child)) {
65
+ text += child.value.trim();
66
+ } else if (isTag(child) && !isAriaHidden(child)) {
67
+ text += getAccessibleText(child);
68
+ }
69
+ }
70
+ return text;
71
+ }
72
+
73
+ /**
74
+ * @type {RuleModule<[]>}
75
+ */
76
+ module.exports = {
77
+ meta: {
78
+ type: "code",
79
+ docs: {
80
+ description: "Disallow empty or inaccessible headings.",
81
+ category: RULE_CATEGORY.ACCESSIBILITY,
82
+ recommended: false,
83
+ url: getRuleUrl("no-empty-headings"),
84
+ },
85
+ fixable: null,
86
+ schema: [],
87
+ messages: {
88
+ [MESSAGE_IDS.EMPTY_HEADING]: "Headings must not be empty.",
89
+ [MESSAGE_IDS.INACCESSIBLE_HEADING]:
90
+ "Heading text is inaccessible to assistive technology.",
91
+ },
92
+ },
93
+ create(context) {
94
+ return createVisitors(context, {
95
+ Tag(node) {
96
+ const tagName = node.name.toLowerCase();
97
+ const isHeadingTag = HEADING_NAMES.has(tagName);
98
+ const isRoleHeadingEl = isRoleHeading(node);
99
+ if (!isHeadingTag && !isRoleHeadingEl) return;
100
+
101
+ // Gather all text (including aria-hidden)
102
+ const allText = getAllText(node);
103
+ if (!allText) {
104
+ context.report({
105
+ node,
106
+ messageId: MESSAGE_IDS.EMPTY_HEADING,
107
+ });
108
+ return;
109
+ }
110
+ // Gather accessible text (not aria-hidden)
111
+ const accessibleText = getAccessibleText(node);
112
+ if (!accessibleText) {
113
+ context.report({
114
+ node,
115
+ messageId: MESSAGE_IDS.INACCESSIBLE_HEADING,
116
+ });
117
+ }
118
+ },
119
+ });
120
+ },
121
+ };
@@ -1,22 +1,11 @@
1
1
  /**
2
- * @typedef { import("@html-eslint/types").Attribute } Attribute
3
- * @typedef { import("@html-eslint/types").OpenTagEnd } OpenTagEnd
4
- * @typedef { import("@html-eslint/types").OpenScriptTagEnd } OpenScriptTagEnd
5
- * @typedef { import("@html-eslint/types").OpenStyleTagEnd } OpenStyleTagEnd
6
- * @typedef { import("@html-eslint/types").OpenScriptTagStart } OpenScriptTagStart
7
- * @typedef { import("@html-eslint/types").OpenTagStart } OpenTagStart
8
- * @typedef { import("@html-eslint/types").OpenStyleTagStart } OpenStyleTagStart
9
- * @typedef { import("@html-eslint/types").Tag } Tag
10
- * @typedef { import("@html-eslint/types").StyleTag } StyleTag
11
- * @typedef { import("@html-eslint/types").ScriptTag } ScriptTag
12
- * @typedef { import("@html-eslint/types").AnyNode } AnyNode
13
- *
2
+ * @import {Attribute, OpenScriptTagStart, OpenTagStart, OpenStyleTagStart, Tag, StyleTag, ScriptTag, AnyNode} from "@html-eslint/types";
3
+ * @import {RuleModule} from "../types";
14
4
  * @typedef {Object} Option
15
5
  * @property {boolean} [Option.disallowInAssignment]
16
6
  * @property {boolean} [Option.disallowMissing]
17
7
  * @property {boolean} [Option.disallowTabs]
18
8
  * @property {boolean} [Option.enforceBeforeSelfClose]
19
- * @typedef { import("../types").RuleModule<[Option]> } RuleModule
20
9
  */
21
10
 
22
11
  const { RULE_CATEGORY } = require("../constants");
@@ -40,7 +29,7 @@ const MESSAGE_IDS = {
40
29
  };
41
30
 
42
31
  /**
43
- * @type {RuleModule}
32
+ * @type {RuleModule<[Option]>}
44
33
  */
45
34
  module.exports = {
46
35
  meta: {
@@ -71,6 +60,7 @@ module.exports = {
71
60
  type: "boolean",
72
61
  },
73
62
  },
63
+ additionalProperties: false,
74
64
  },
75
65
  ],
76
66
  messages: {
@@ -1,14 +1,8 @@
1
1
  /**
2
- * @typedef { import("@html-eslint/types").CommentContent } CommentContent
3
- * @typedef { import("@html-eslint/types").Tag } Tag
4
- * @typedef { import("@html-eslint/types").Comment } Comment
5
- * @typedef { import("@html-eslint/types").Text } Text
6
- * @typedef { import("../types").Line } Line
7
- * @typedef { import("eslint").AST.Range } Range
8
- *
2
+ * @import {CommentContent, Tag, Comment, Text} from "@html-eslint/types";
3
+ * @import {RuleModule} from "../types";
9
4
  * @typedef {Object} Option
10
5
  * @property {string[]} [Option.skip]
11
- * @typedef { import("../types").RuleModule<[Option]> } RuleModule
12
6
  */
13
7
 
14
8
  const { RULE_CATEGORY } = require("../constants");
@@ -22,7 +16,7 @@ const MESSAGE_IDS = {
22
16
  };
23
17
 
24
18
  /**
25
- * @type {RuleModule}
19
+ * @type {RuleModule<[Option]>}
26
20
  */
27
21
  module.exports = {
28
22
  meta: {
@@ -30,7 +24,7 @@ module.exports = {
30
24
 
31
25
  docs: {
32
26
  description: "Disallow unnecessary consecutive spaces",
33
- category: RULE_CATEGORY.BEST_PRACTICE,
27
+ category: RULE_CATEGORY.STYLE,
34
28
  recommended: false,
35
29
  url: getRuleUrl("no-extra-spacing-text"),
36
30
  },
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @typedef { import("../types").RuleModule<[]> } RuleModule
2
+ * @import {RuleModule} from "../types";
3
3
  */
4
4
 
5
5
  const { RULE_CATEGORY } = require("../constants");
@@ -14,7 +14,7 @@ const MESSAGE_IDS = {
14
14
  const HEADING_NAMES = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
15
15
 
16
16
  /**
17
- * @type {RuleModule}
17
+ * @type {RuleModule<[]>}
18
18
  */
19
19
  module.exports = {
20
20
  meta: {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @typedef { import("../types").RuleModule<[]> } RuleModule
2
+ * @import {RuleModule} from "../types";
3
3
  */
4
4
 
5
5
  const { RULE_CATEGORY } = require("../constants");
@@ -12,7 +12,7 @@ const MESSAGE_IDS = {
12
12
  };
13
13
 
14
14
  /**
15
- * @type {RuleModule}
15
+ * @type {RuleModule<[]>}
16
16
  */
17
17
  module.exports = {
18
18
  meta: {
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @import {RuleModule} from "../types";
3
+ * @import {Text} from "@html-eslint/types";
4
+ */
5
+
6
+ // Define the type for entities.json
7
+ /**
8
+ * @typedef {Object} EntityData
9
+ * @property {number[]} codepoints
10
+ * @property {string} characters
11
+ */
12
+
13
+ /** @type {{ [key: string]: EntityData }} */
14
+ const entities = require("../data/entities.json");
15
+
16
+ const { RULE_CATEGORY } = require("../constants");
17
+ const { createVisitors } = require("./utils/visitors");
18
+ const { getRuleUrl } = require("./utils/rule");
19
+
20
+ const MESSAGE_IDS = {
21
+ INVALID_ENTITY: "invalidEntity",
22
+ };
23
+
24
+ /**
25
+ * @type {RuleModule<[]>}
26
+ */
27
+ module.exports = {
28
+ meta: {
29
+ type: "code",
30
+ docs: {
31
+ description: "Disallows the use of invalid HTML entities",
32
+ category: RULE_CATEGORY.BEST_PRACTICE,
33
+ recommended: false,
34
+ url: getRuleUrl("no-invalid-entity"),
35
+ },
36
+ fixable: null,
37
+ hasSuggestions: false,
38
+ schema: [],
39
+ messages: {
40
+ [MESSAGE_IDS.INVALID_ENTITY]: "Invalid HTML entity '{{entity}}' used.",
41
+ },
42
+ },
43
+
44
+ create(context) {
45
+ /**
46
+ * @param {Text} node
47
+ */
48
+ function check(node) {
49
+ const text = node.value;
50
+
51
+ // Regular expression to match named and numeric entities
52
+ const entityRegex = /&([a-zA-Z]+|#[0-9]+|#x[0-9a-fA-F]+|[#][^;]+);/g;
53
+ let match;
54
+
55
+ while ((match = entityRegex.exec(text)) !== null) {
56
+ const entity = match[0];
57
+ const entityName = match[1];
58
+
59
+ // Check named entities
60
+ if (!entityName.startsWith("#")) {
61
+ const fullEntity = `&${entityName};`;
62
+ if (!Object.prototype.hasOwnProperty.call(entities, fullEntity)) {
63
+ context.report({
64
+ node,
65
+ messageId: MESSAGE_IDS.INVALID_ENTITY,
66
+ data: { entity },
67
+ });
68
+ }
69
+ }
70
+ // Check numeric entities
71
+ else {
72
+ const isHex = entityName[1] === "x";
73
+ const numStr = isHex ? entityName.slice(2) : entityName.slice(1);
74
+ const num = isHex ? parseInt(numStr, 16) : parseInt(numStr, 10);
75
+
76
+ // If the number is not a valid integer, report an error
77
+ if (isNaN(num)) {
78
+ context.report({
79
+ node,
80
+ messageId: MESSAGE_IDS.INVALID_ENTITY,
81
+ data: { entity },
82
+ });
83
+ continue;
84
+ }
85
+
86
+ // Check if the numeric entity is valid (exists in entities.json or within valid Unicode range)
87
+ const entityKey = Object.keys(entities).find((key) => {
88
+ const codepoints = entities[key].codepoints;
89
+ return codepoints.length === 1 && codepoints[0] === num;
90
+ });
91
+
92
+ if (!entityKey && (num < 0 || num > 0x10ffff)) {
93
+ context.report({
94
+ node,
95
+ messageId: MESSAGE_IDS.INVALID_ENTITY,
96
+ data: { entity },
97
+ });
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ return createVisitors(context, {
104
+ Text: check,
105
+ });
106
+ },
107
+ };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @typedef { import("../types").RuleModule<[]> } RuleModule
2
+ * @import {RuleModule} from "../types";
3
3
  */
4
4
 
5
5
  const { NODE_TYPES } = require("@html-eslint/parser");
@@ -227,7 +227,7 @@ const ELEMENTS_DISALLOWING_PRESENTATION_OR_NONE_ROLE = new Set([
227
227
  ]);
228
228
 
229
229
  /**
230
- * @type {RuleModule}
230
+ * @type {RuleModule<[]>}
231
231
  */
232
232
  module.exports = {
233
233
  meta: {
@@ -1,15 +1,12 @@
1
1
  /**
2
- * @typedef { import("@html-eslint/types").AnyToken } AnyToken
3
- * @typedef { import("@html-eslint/types").Document } Document
4
- * @typedef { import("@html-eslint/types").CommentContent } CommentContent
5
- * @typedef { import("@html-eslint/types").Text } Text
2
+ * @import {CommentContent, Text} from "@html-eslint/types";
3
+ * @import {RuleModule} from "../types";
6
4
  *
7
5
  * @typedef {Object} Option
8
6
  * @property {number} Option.max
9
- * @typedef { import("../types").RuleModule<[Option]> } RuleModule
10
7
  */
11
8
 
12
- const { parse } = require("@html-eslint/template-parser");
9
+ const { parseTemplateLiteral } = require("./utils/template-literal");
13
10
  const { RULE_CATEGORY } = require("../constants");
14
11
  const {
15
12
  shouldCheckTaggedTemplateExpression,
@@ -28,7 +25,7 @@ const MESSAGE_IDS = {
28
25
  };
29
26
 
30
27
  /**
31
- * @type {RuleModule}
28
+ * @type {RuleModule<[Option]>}
32
29
  */
33
30
  module.exports = {
34
31
  meta: {
@@ -120,10 +117,9 @@ module.exports = {
120
117
  },
121
118
  TaggedTemplateExpression(node) {
122
119
  if (shouldCheckTaggedTemplateExpression(node, context)) {
123
- const { html, tokens } = parse(
120
+ const { html, tokens } = parseTemplateLiteral(
124
121
  node.quasi,
125
- getSourceCode(context),
126
- {}
122
+ getSourceCode(context)
127
123
  );
128
124
  const lines = codeToLines(html);
129
125
  check(
@@ -136,7 +132,10 @@ module.exports = {
136
132
  },
137
133
  TemplateLiteral(node) {
138
134
  if (shouldCheckTemplateLiteral(node, context)) {
139
- const { html, tokens } = parse(node, getSourceCode(context), {});
135
+ const { html, tokens } = parseTemplateLiteral(
136
+ node,
137
+ getSourceCode(context)
138
+ );
140
139
  const lines = codeToLines(html);
141
140
  check(
142
141
  lines,
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @typedef { import("../types").RuleModule<[]> } RuleModule
3
- * @typedef { import("@html-eslint/types").Tag } Tag
2
+ * @import {Tag} from "@html-eslint/types";
3
+ * @import {RuleModule} from "../types";
4
4
  */
5
5
 
6
6
  const { RULE_CATEGORY } = require("../constants");
@@ -11,7 +11,7 @@ const MESSAGE_IDS = {
11
11
  };
12
12
 
13
13
  /**
14
- * @type {RuleModule}
14
+ * @type {RuleModule<[]>}
15
15
  */
16
16
  module.exports = {
17
17
  meta: {
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @typedef { import("../types").RuleModule<[]> } RuleModule
3
- * @typedef { import("@html-eslint/types").Tag } Tag
2
+ * @import {Tag} from "@html-eslint/types";
3
+ * @import {RuleModule} from "../types";
4
4
  */
5
5
 
6
6
  const { RULE_CATEGORY } = require("../constants");
@@ -56,7 +56,7 @@ function isInteractive(tag) {
56
56
  }
57
57
 
58
58
  /**
59
- * @type {RuleModule}
59
+ * @type {RuleModule<[]>}
60
60
  */
61
61
  module.exports = {
62
62
  meta: {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @typedef { import("../types").RuleModule<[]> } RuleModule
2
+ * @import {RuleModule} from "../types";
3
3
  */
4
4
 
5
5
  const { RULE_CATEGORY } = require("../constants");
@@ -11,7 +11,7 @@ const MESSAGE_IDS = {
11
11
  };
12
12
 
13
13
  /**
14
- * @type {RuleModule}
14
+ * @type {RuleModule<[]>}
15
15
  */
16
16
  module.exports = {
17
17
  meta: {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @typedef { import("../types").RuleModule<[]> } RuleModule
2
+ * @import {RuleModule} from "../types";
3
3
  */
4
4
 
5
5
  const { RULE_CATEGORY, OBSOLETE_TAGS } = require("../constants");
@@ -13,7 +13,7 @@ const MESSAGE_IDS = {
13
13
  };
14
14
 
15
15
  /**
16
- * @type {RuleModule}
16
+ * @type {RuleModule<[]>}
17
17
  */
18
18
  module.exports = {
19
19
  meta: {
@@ -1,8 +1,6 @@
1
1
  /**
2
- * @typedef { import("../types").RuleModule<[]> } RuleModule
3
- * @typedef { import("@html-eslint/types").Tag } Tag
4
- * @typedef { import("@html-eslint/types").StyleTag } StyleTag
5
- * @typedef { import("@html-eslint/types").ScriptTag } ScriptTag
2
+ * @import {Tag, StyleTag,ScriptTag } from "@html-eslint/types";
3
+ * @import {RuleModule} from "../types";
6
4
  */
7
5
 
8
6
  const { RULE_CATEGORY } = require("../constants");
@@ -15,7 +13,7 @@ const MESSAGE_IDS = {
15
13
  };
16
14
 
17
15
  /**
18
- * @type {RuleModule}
16
+ * @type {RuleModule<[]>}
19
17
  */
20
18
  module.exports = {
21
19
  meta: {