@html-eslint/eslint-plugin 0.19.1 → 0.21.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 (47) hide show
  1. package/lib/constants/index.js +0 -2
  2. package/lib/rules/element-newline.js +30 -11
  3. package/lib/rules/id-naming-convention.js +17 -11
  4. package/lib/rules/indent.js +7 -10
  5. package/lib/rules/index.js +10 -0
  6. package/lib/rules/lowercase.js +93 -0
  7. package/lib/rules/no-abstract-roles.js +15 -13
  8. package/lib/rules/no-accesskey-attrs.js +5 -6
  9. package/lib/rules/no-aria-hidden-body.js +2 -6
  10. package/lib/rules/no-duplicate-attrs.js +3 -4
  11. package/lib/rules/no-duplicate-id.js +7 -7
  12. package/lib/rules/no-extra-spacing-attrs.js +29 -9
  13. package/lib/rules/no-inline-styles.js +5 -2
  14. package/lib/rules/no-multiple-empty-lines.js +3 -4
  15. package/lib/rules/no-multiple-h1.js +3 -4
  16. package/lib/rules/no-non-scalable-viewport.js +3 -7
  17. package/lib/rules/no-obsolete-tags.js +4 -2
  18. package/lib/rules/no-positive-tabindex.js +5 -6
  19. package/lib/rules/no-restricted-attr-values.js +147 -0
  20. package/lib/rules/no-restricted-attrs.js +13 -2
  21. package/lib/rules/no-script-style-type.js +69 -0
  22. package/lib/rules/no-skip-heading-levels.js +4 -5
  23. package/lib/rules/no-target-blank.js +5 -9
  24. package/lib/rules/no-trailing-spaces.js +0 -4
  25. package/lib/rules/quotes.js +24 -1
  26. package/lib/rules/require-attrs.js +18 -7
  27. package/lib/rules/require-button-type.js +2 -6
  28. package/lib/rules/require-closing-tags.js +8 -5
  29. package/lib/rules/require-doctype.js +4 -5
  30. package/lib/rules/require-frame-title.js +5 -2
  31. package/lib/rules/require-img-alt.js +10 -0
  32. package/lib/rules/require-lang.js +5 -6
  33. package/lib/rules/require-li-container.js +9 -2
  34. package/lib/rules/require-meta-charset.js +19 -13
  35. package/lib/rules/require-meta-description.js +9 -12
  36. package/lib/rules/require-meta-viewport.js +19 -20
  37. package/lib/rules/require-open-graph-protocol.js +135 -0
  38. package/lib/rules/require-title.js +19 -25
  39. package/lib/rules/sort-attrs.js +158 -0
  40. package/lib/rules/utils/array.js +26 -0
  41. package/lib/rules/utils/node.js +70 -0
  42. package/lib/types.d.ts +262 -246
  43. package/package.json +6 -5
  44. package/lib/constants/node-types.js +0 -13
  45. package/lib/rules/utils/index.js +0 -7
  46. package/lib/rules/utils/node-utils.js +0 -112
  47. /package/lib/rules/utils/{naming-utils.js → naming.js} +0 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * @typedef {{attrPatterns: string[], attrValuePatterns: string[], message?: string}[]} Options
3
+ */
4
+
5
+ const { RULE_CATEGORY } = require("../constants");
6
+
7
+ const MESSAGE_IDS = {
8
+ RESTRICTED: "restricted",
9
+ };
10
+
11
+ /**
12
+ * @type {Rule}
13
+ */
14
+ module.exports = {
15
+ meta: {
16
+ type: "code",
17
+
18
+ docs: {
19
+ description: "Disallow specified attributes",
20
+ category: RULE_CATEGORY.BEST_PRACTICE,
21
+ recommended: false,
22
+ },
23
+
24
+ fixable: null,
25
+ schema: {
26
+ type: "array",
27
+
28
+ items: {
29
+ type: "object",
30
+ required: ["attrPatterns", "attrValuePatterns"],
31
+ properties: {
32
+ attrPatterns: {
33
+ type: "array",
34
+ items: {
35
+ type: "string",
36
+ },
37
+ },
38
+ attrValuePatterns: {
39
+ type: "array",
40
+ items: {
41
+ type: "string",
42
+ },
43
+ },
44
+ message: {
45
+ type: "string",
46
+ },
47
+ },
48
+ },
49
+ },
50
+ messages: {
51
+ [MESSAGE_IDS.RESTRICTED]:
52
+ "'{{attrValuePatterns}}' is restricted from being used.",
53
+ },
54
+ },
55
+
56
+ create(context) {
57
+ /**
58
+ * @type {Options}
59
+ */
60
+ const options = context.options;
61
+ const checkers = options.map((option) => new PatternChecker(option));
62
+
63
+ return {
64
+ /**
65
+ * @param {TagNode | StyleTagNode | ScriptTagNode} node
66
+ */
67
+ [["Tag", "StyleTag", "ScriptTag"].join(",")](node) {
68
+ node.attributes.forEach((attr) => {
69
+ if (
70
+ !attr.key ||
71
+ !attr.key.value ||
72
+ !attr.value ||
73
+ typeof attr.value.value !== "string"
74
+ ) {
75
+ return;
76
+ }
77
+
78
+ const matched = checkers.find(
79
+ (checker) =>
80
+ attr.value && checker.test(attr.key.value, attr.value.value)
81
+ );
82
+
83
+ if (!matched) {
84
+ return;
85
+ }
86
+
87
+ /**
88
+ * @type {{node: AttributeNode, message: string, messageId?: string}}
89
+ */
90
+ const result = {
91
+ node: attr,
92
+ message: "",
93
+ };
94
+
95
+ const customMessage = matched.getMessage();
96
+
97
+ if (customMessage) {
98
+ result.message = customMessage;
99
+ } else {
100
+ result.messageId = MESSAGE_IDS.RESTRICTED;
101
+ }
102
+
103
+ context.report({
104
+ ...result,
105
+ data: { attrValuePatterns: attr.value.value },
106
+ });
107
+ });
108
+ },
109
+ };
110
+ },
111
+ };
112
+
113
+ class PatternChecker {
114
+ /**
115
+ * @param {Options[number]} option
116
+ */
117
+ constructor(option) {
118
+ this.option = option;
119
+ this.attrRegExps = option.attrPatterns.map(
120
+ (pattern) => new RegExp(pattern, "u")
121
+ );
122
+
123
+ this.valueRegExps = option.attrValuePatterns.map(
124
+ (pattern) => new RegExp(pattern, "u")
125
+ );
126
+ this.message = option.message;
127
+ }
128
+
129
+ /**
130
+ * @param {string} attrName
131
+ * @param {string} attrValue
132
+ * @returns {boolean}
133
+ */
134
+ test(attrName, attrValue) {
135
+ const result =
136
+ this.attrRegExps.some((exp) => exp.test(attrName)) &&
137
+ this.valueRegExps.some((exp) => exp.test(attrValue));
138
+ return result;
139
+ }
140
+
141
+ /**
142
+ * @returns {string}
143
+ */
144
+ getMessage() {
145
+ return this.message || "";
146
+ }
147
+ }
@@ -1,8 +1,8 @@
1
1
  /**
2
- * @typedef {import("../types").Rule} Rule
3
2
  * @typedef {{tagPatterns: string[], attrPatterns: string[], message?: string}[]} Options
4
3
  */
5
4
 
5
+ const { NODE_TYPES } = require("@html-eslint/parser");
6
6
  const { RULE_CATEGORY } = require("../constants");
7
7
 
8
8
  const MESSAGE_IDS = {
@@ -61,8 +61,16 @@ module.exports = {
61
61
  const checkers = options.map((option) => new PatternChecker(option));
62
62
 
63
63
  return {
64
+ /**
65
+ * @param {TagNode | StyleTagNode | ScriptTagNode} node
66
+ */
64
67
  [["Tag", "StyleTag", "ScriptTag"].join(",")](node) {
65
- const tagName = node.name;
68
+ const tagName =
69
+ node.type === NODE_TYPES.Tag
70
+ ? node.name
71
+ : node.type === NODE_TYPES.ScriptTag
72
+ ? "script"
73
+ : "style";
66
74
  node.attributes.forEach((attr) => {
67
75
  if (!attr.key || !attr.key.value) {
68
76
  return;
@@ -75,6 +83,9 @@ module.exports = {
75
83
  return;
76
84
  }
77
85
 
86
+ /**
87
+ * @type {{node: AttributeNode, message: string, messageId?: string}}
88
+ */
78
89
  const result = {
79
90
  node: attr,
80
91
  message: "",
@@ -0,0 +1,69 @@
1
+ const { RULE_CATEGORY } = require("../constants");
2
+ const { findAttr } = require("./utils/node");
3
+
4
+ const MESSAGE_IDS = {
5
+ UNNECESSARY: "unnecessary",
6
+ };
7
+
8
+ /**
9
+ * @type {Rule}
10
+ */
11
+ module.exports = {
12
+ meta: {
13
+ type: "code",
14
+
15
+ docs: {
16
+ description:
17
+ "Enforce to omit type attributes for style sheets and scripts",
18
+ category: RULE_CATEGORY.BEST_PRACTICE,
19
+ recommended: false,
20
+ },
21
+
22
+ fixable: "code",
23
+ schema: [],
24
+ messages: {
25
+ [MESSAGE_IDS.UNNECESSARY]: "Unnecessary type attributes",
26
+ },
27
+ },
28
+
29
+ create(context) {
30
+ /**
31
+ *
32
+ * @param {ScriptTagNode | TagNode | StyleTagNode} node
33
+ * @param {string} unnecessaryValue
34
+ */
35
+ function check(node, unnecessaryValue) {
36
+ const type = findAttr(node, "type");
37
+ if (
38
+ type &&
39
+ type.value &&
40
+ type.value.value &&
41
+ type.value.value.trim().toLocaleLowerCase() === unnecessaryValue
42
+ ) {
43
+ context.report({
44
+ node: type,
45
+ messageId: MESSAGE_IDS.UNNECESSARY,
46
+ fix(fixer) {
47
+ return fixer.remove(type);
48
+ },
49
+ });
50
+ }
51
+ }
52
+ return {
53
+ ScriptTag(node) {
54
+ check(node, "text/javascript");
55
+ },
56
+ StyleTag(node) {
57
+ check(node, "text/css");
58
+ },
59
+ Tag(node) {
60
+ if (node.name === "link") {
61
+ const rel = findAttr(node, "rel");
62
+ if (rel && rel.value && rel.value.value === "stylesheet") {
63
+ check(node, "text/css");
64
+ }
65
+ }
66
+ },
67
+ };
68
+ },
69
+ };
@@ -1,7 +1,3 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- */
4
-
5
1
  const { RULE_CATEGORY } = require("../constants");
6
2
 
7
3
  const MESSAGE_IDS = {
@@ -30,6 +26,9 @@ module.exports = {
30
26
  },
31
27
 
32
28
  create(context) {
29
+ /**
30
+ * @type {{node: TagNode; level: number}[]}
31
+ */
33
32
  const headings = [];
34
33
 
35
34
  return {
@@ -53,7 +52,7 @@ module.exports = {
53
52
  if (next.level - current.level > 1) {
54
53
  context.report({
55
54
  node: next.node,
56
- data: { expected: current.level + 1 },
55
+ data: { expected: String(current.level + 1) },
57
56
  messageId: MESSAGE_IDS.UNEXPECTED,
58
57
  });
59
58
  }
@@ -1,9 +1,5 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- */
4
-
5
1
  const { RULE_CATEGORY } = require("../constants");
6
- const { NodeUtils } = require("./utils");
2
+ const { findAttr } = require("./utils/node");
7
3
 
8
4
  const MESSAGE_IDS = {
9
5
  MISSING: "missing",
@@ -43,12 +39,12 @@ module.exports = {
43
39
  if (node.name !== "a") {
44
40
  return;
45
41
  }
46
- /* eslint-disable */
47
- const target = NodeUtils.findAttr(node, "target");
42
+
43
+ const target = findAttr(node, "target");
48
44
  if (target && target.value && target.value.value === "_blank") {
49
- const href = NodeUtils.findAttr(node, "href");
45
+ const href = findAttr(node, "href");
50
46
  if (href && href.value && isExternalLink(href.value.value)) {
51
- const rel = NodeUtils.findAttr(node, "rel");
47
+ const rel = findAttr(node, "rel");
52
48
  if (!rel || !rel.value || !rel.value.value.includes("noreferrer")) {
53
49
  context.report({
54
50
  node: target,
@@ -1,7 +1,3 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- */
4
-
5
1
  const { RULE_CATEGORY } = require("../constants");
6
2
 
7
3
  const MESSAGE_IDS = {
@@ -12,6 +12,9 @@ const QUOTES_STYLES = {
12
12
 
13
13
  const QUOTES_CODES = [`"`, `'`];
14
14
 
15
+ /**
16
+ * @type {Rule}
17
+ */
15
18
  module.exports = {
16
19
  meta: {
17
20
  type: "code",
@@ -53,10 +56,20 @@ module.exports = {
53
56
  return sourceCode.text.slice(range[0], range[1]);
54
57
  }
55
58
 
59
+ /**
60
+ *
61
+ * @param {AttributeNode} attr
62
+ * @returns {[string, string]}
63
+ */
56
64
  function getQuotes(attr) {
65
+ // @ts-ignore
57
66
  return [attr.startWrapper.value, attr.endWrapper.value];
58
67
  }
59
68
 
69
+ /**
70
+ * @param {AttributeNode} attr
71
+ * @returns {void}
72
+ */
60
73
  function checkQuotes(attr) {
61
74
  if (!attr.value || attr.value.value.includes(expectedQuote)) {
62
75
  return;
@@ -77,6 +90,12 @@ module.exports = {
77
90
  : `${QUOTES_STYLES.SINGLE}(')`,
78
91
  },
79
92
  fix(fixer) {
93
+ if (
94
+ !attr.startWrapper ||
95
+ !attr.endWrapper ||
96
+ attr.value === undefined
97
+ )
98
+ return null;
80
99
  return fixer.replaceTextRange(
81
100
  [attr.startWrapper.range[0], attr.endWrapper.range[1]],
82
101
  `${expectedQuote}${attr.value.value}${expectedQuote}`
@@ -93,6 +112,7 @@ module.exports = {
93
112
  expected: `${SELECTED_STYLE}(${expectedQuote})`,
94
113
  },
95
114
  fix(fixer) {
115
+ if (attr.value === undefined) return null;
96
116
  const originCode = getCodeIn(attr.value.range);
97
117
  return fixer.replaceTextRange(
98
118
  attr.value.range,
@@ -104,8 +124,11 @@ module.exports = {
104
124
  }
105
125
 
106
126
  return {
127
+ /**
128
+ * @param {TagNode | ScriptTagNode | StyleTagNode} node
129
+ */
107
130
  [["Tag", "ScriptTag", "StyleTag"].join(",")](node) {
108
- node.attributes.forEach(checkQuotes);
131
+ node.attributes.forEach((attr) => checkQuotes(attr));
109
132
  },
110
133
  };
111
134
  },
@@ -1,7 +1,4 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- */
4
-
1
+ const { NODE_TYPES } = require("@html-eslint/parser");
5
2
  const { RULE_CATEGORY } = require("../constants");
6
3
 
7
4
  const MESSAGE_IDS = {
@@ -44,19 +41,29 @@ module.exports = {
44
41
 
45
42
  create(context) {
46
43
  const options = context.options || [];
44
+ /**
45
+ * @type {Map<string, { tag: string, attr: string, value?: string}[]>}
46
+ */
47
47
  const tagOptionsMap = new Map();
48
48
 
49
49
  options.forEach((option) => {
50
50
  const tagName = option.tag.toLowerCase();
51
51
  if (tagOptionsMap.has(tagName)) {
52
- tagOptionsMap.set(tagName, [...tagOptionsMap.get(tagName), option]);
52
+ tagOptionsMap.set(tagName, [
53
+ ...(tagOptionsMap.get(tagName) || []),
54
+ option,
55
+ ]);
53
56
  } else {
54
57
  tagOptionsMap.set(tagName, [option]);
55
58
  }
56
59
  });
57
60
 
61
+ /**
62
+ * @param {StyleTagNode | ScriptTagNode | TagNode} node
63
+ * @param {string} tagName
64
+ */
58
65
  function check(node, tagName) {
59
- const tagOptions = tagOptionsMap.get(tagName);
66
+ const tagOptions = tagOptionsMap.get(tagName) || [];
60
67
  const attributes = node.attributes || [];
61
68
 
62
69
  tagOptions.forEach((option) => {
@@ -90,8 +97,12 @@ module.exports = {
90
97
  }
91
98
 
92
99
  return {
100
+ /**
101
+ * @param {StyleTagNode | ScriptTagNode} node
102
+ * @returns
103
+ */
93
104
  [["StyleTag", "ScriptTag"].join(",")](node) {
94
- const tagName = node.type === "StyleTag" ? "style" : "script";
105
+ const tagName = node.type === NODE_TYPES.StyleTag ? "style" : "script";
95
106
  if (!tagOptionsMap.has(tagName)) {
96
107
  return;
97
108
  }
@@ -1,9 +1,5 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- */
4
-
5
1
  const { RULE_CATEGORY } = require("../constants");
6
- const { NodeUtils } = require("./utils");
2
+ const { findAttr } = require("./utils/node");
7
3
 
8
4
  const MESSAGE_IDS = {
9
5
  MISSING: "missing",
@@ -40,7 +36,7 @@ module.exports = {
40
36
  if (node.name !== "button") {
41
37
  return;
42
38
  }
43
- const typeAttr = NodeUtils.findAttr(node, "type");
39
+ const typeAttr = findAttr(node, "type");
44
40
  if (!typeAttr || !typeAttr.value) {
45
41
  context.report({
46
42
  node: node.openStart,
@@ -1,7 +1,3 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- */
4
-
5
1
  const { RULE_CATEGORY, VOID_ELEMENTS } = require("../constants");
6
2
 
7
3
  const VOID_ELEMENTS_SET = new Set(VOID_ELEMENTS);
@@ -44,7 +40,6 @@ module.exports = {
44
40
  [MESSAGE_IDS.MISSING]: "Missing closing tag for {{tag}}.",
45
41
  [MESSAGE_IDS.MISSING_SELF]: "Missing self closing tag for {{tag}}",
46
42
  [MESSAGE_IDS.UNEXPECTED]: "Unexpected self closing tag for {{tag}}.",
47
- [MESSAGE_IDS.HUCKS]: "HUCKS.",
48
43
  },
49
44
  },
50
45
 
@@ -58,6 +53,9 @@ module.exports = {
58
53
  ? context.options[0].allowSelfClosingCustom === true
59
54
  : false;
60
55
 
56
+ /**
57
+ * @param {TagNode} node
58
+ */
61
59
  function checkClosingTag(node) {
62
60
  if (!node.close) {
63
61
  context.report({
@@ -70,6 +68,11 @@ module.exports = {
70
68
  }
71
69
  }
72
70
 
71
+ /**
72
+ * @param {TagNode} node
73
+ * @param {boolean} shouldSelfClose
74
+ * @param {boolean} fixable
75
+ */
73
76
  function checkVoidElement(node, shouldSelfClose, fixable) {
74
77
  const hasSelfClose = node.openEnd.value === "/>";
75
78
  if (shouldSelfClose && !hasSelfClose) {
@@ -1,7 +1,3 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- */
4
-
5
1
  const { RULE_CATEGORY } = require("../constants");
6
2
 
7
3
  const MESSAGE_IDS = {
@@ -34,7 +30,10 @@ module.exports = {
34
30
  Doctype() {
35
31
  hasDocType = true;
36
32
  },
37
- "Program:exit"(node) {
33
+ Tag(node) {
34
+ if (node.name !== "html") {
35
+ return;
36
+ }
38
37
  if (!hasDocType) {
39
38
  context.report({
40
39
  node,
@@ -1,11 +1,14 @@
1
1
  const { RULE_CATEGORY } = require("../constants");
2
- const { NodeUtils } = require("./utils");
2
+ const { findAttr } = require("./utils/node");
3
3
 
4
4
  const MESSAGE_IDS = {
5
5
  MISSING: "missing",
6
6
  UNEXPECTED: "unexpected",
7
7
  };
8
8
 
9
+ /**
10
+ * @type {Rule}
11
+ */
9
12
  module.exports = {
10
13
  meta: {
11
14
  type: "code",
@@ -30,7 +33,7 @@ module.exports = {
30
33
  if (node.name !== "frame" && node.name !== "iframe") {
31
34
  return;
32
35
  }
33
- const title = NodeUtils.findAttr(node, "title");
36
+ const title = findAttr(node, "title");
34
37
  if (!title) {
35
38
  context.report({
36
39
  node: node.openStart,
@@ -4,6 +4,9 @@ const MESSAGE_IDS = {
4
4
  MISSING_ALT: "missingAlt",
5
5
  };
6
6
 
7
+ /**
8
+ * @type {Rule}
9
+ */
7
10
  module.exports = {
8
11
  meta: {
9
12
  type: "code",
@@ -62,6 +65,12 @@ module.exports = {
62
65
  },
63
66
  };
64
67
 
68
+ /**
69
+ *
70
+ * @param {TagNode} node
71
+ * @param {string[]} substitute
72
+ * @returns
73
+ */
65
74
  function hasAltAttrAndValue(node, substitute = []) {
66
75
  return node.attributes.some((attr) => {
67
76
  if (attr.key && attr.value) {
@@ -70,5 +79,6 @@ function hasAltAttrAndValue(node, substitute = []) {
70
79
  typeof attr.value.value === "string"
71
80
  );
72
81
  }
82
+ return false;
73
83
  });
74
84
  }
@@ -1,15 +1,14 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- */
4
-
5
1
  const { RULE_CATEGORY } = require("../constants");
6
- const { NodeUtils } = require("./utils");
2
+ const { findAttr } = require("./utils/node");
7
3
 
8
4
  const MESSAGE_IDS = {
9
5
  MISSING: "missing",
10
6
  EMPTY: "empty",
11
7
  };
12
8
 
9
+ /**
10
+ * @type {Rule}
11
+ */
13
12
  module.exports = {
14
13
  meta: {
15
14
  type: "code",
@@ -34,7 +33,7 @@ module.exports = {
34
33
  if (node.name !== "html") {
35
34
  return;
36
35
  }
37
- const langAttr = NodeUtils.findAttr(node, "lang");
36
+ const langAttr = findAttr(node, "lang");
38
37
  if (!langAttr) {
39
38
  context.report({
40
39
  node: {
@@ -1,3 +1,4 @@
1
+ const { NODE_TYPES } = require("@html-eslint/parser");
1
2
  const { RULE_CATEGORY } = require("../constants");
2
3
 
3
4
  const MESSAGE_IDS = {
@@ -6,6 +7,9 @@ const MESSAGE_IDS = {
6
7
 
7
8
  const VALID_CONTAINERS = ["ul", "ol", "menu"];
8
9
 
10
+ /**
11
+ * @type {Rule}
12
+ */
9
13
  module.exports = {
10
14
  meta: {
11
15
  type: "code",
@@ -30,12 +34,15 @@ module.exports = {
30
34
  if (node.name !== "li") {
31
35
  return;
32
36
  }
33
- if (!node.parent) {
37
+ if (!node.parent || node.parent.type === NODE_TYPES.Program) {
34
38
  context.report({
35
39
  node,
36
40
  messageId: MESSAGE_IDS.INVALID,
37
41
  });
38
- } else if (!VALID_CONTAINERS.includes(node.parent.name || "")) {
42
+ } else if (
43
+ node.parent.type === NODE_TYPES.Tag &&
44
+ !VALID_CONTAINERS.includes(node.parent.name || "")
45
+ ) {
39
46
  context.report({
40
47
  node,
41
48
  messageId: MESSAGE_IDS.INVALID,
@@ -1,15 +1,25 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- */
4
-
1
+ const { NODE_TYPES } = require("@html-eslint/parser");
5
2
  const { RULE_CATEGORY } = require("../constants");
6
- const { NodeUtils } = require("./utils");
3
+ const { find } = require("./utils/array");
4
+ const { findAttr } = require("./utils/node");
7
5
 
8
6
  const MESSAGE_IDS = {
9
7
  MISSING: "missing",
10
8
  EMPTY: "empty",
11
9
  };
12
10
 
11
+ /**
12
+ * @param {ChildType<TagNode>} node
13
+ * @returns {node is TagNode}
14
+ */
15
+ function isMetaCharset(node) {
16
+ return (
17
+ node.type === NODE_TYPES.Tag &&
18
+ node.name === "meta" &&
19
+ !!findAttr(node, "charset")
20
+ );
21
+ }
22
+
13
23
  /**
14
24
  * @type {Rule}
15
25
  */
@@ -38,13 +48,7 @@ module.exports = {
38
48
  return;
39
49
  }
40
50
 
41
- const metaCharset = node.children.find((child) => {
42
- return (
43
- child.type === "Tag" &&
44
- child.name === "meta" &&
45
- !!NodeUtils.findAttr(child, "charset")
46
- );
47
- });
51
+ const metaCharset = find(node.children, isMetaCharset);
48
52
 
49
53
  if (!metaCharset) {
50
54
  context.report({
@@ -53,7 +57,9 @@ module.exports = {
53
57
  });
54
58
  return;
55
59
  }
56
- const charsetAttr = NodeUtils.findAttr(metaCharset, "charset");
60
+
61
+ const charsetAttr = findAttr(metaCharset, "charset");
62
+
57
63
  if (charsetAttr) {
58
64
  if (!charsetAttr.value || !charsetAttr.value.value.length) {
59
65
  context.report({