@html-eslint/eslint-plugin 0.10.0 → 0.13.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 (38) hide show
  1. package/lib/constants/node-types.js +0 -5
  2. package/lib/constants/obsolete-tags.js +1 -1
  3. package/lib/constants/rule-category.js +0 -5
  4. package/lib/constants/void-elements.js +1 -1
  5. package/lib/rules/element-newline.js +29 -9
  6. package/lib/rules/id-naming-convention.js +14 -7
  7. package/lib/rules/indent.js +29 -11
  8. package/lib/rules/index.js +6 -0
  9. package/lib/rules/no-abstract-roles.js +7 -0
  10. package/lib/rules/no-accesskey-attrs.js +45 -0
  11. package/lib/rules/no-aria-hidden-body.js +7 -0
  12. package/lib/rules/no-duplicate-attrs.js +7 -0
  13. package/lib/rules/no-duplicate-id.js +7 -0
  14. package/lib/rules/no-extra-spacing-attrs.js +35 -6
  15. package/lib/rules/no-inline-styles.js +7 -0
  16. package/lib/rules/no-multiple-empty-lines.js +90 -0
  17. package/lib/rules/no-multiple-h1.js +7 -0
  18. package/lib/rules/no-non-scalable-viewport.js +7 -0
  19. package/lib/rules/no-obsolete-tags.js +7 -0
  20. package/lib/rules/no-positive-tabindex.js +7 -0
  21. package/lib/rules/no-restricted-attrs.js +140 -0
  22. package/lib/rules/no-skip-heading-levels.js +7 -0
  23. package/lib/rules/no-target-blank.js +7 -0
  24. package/lib/rules/quotes.js +28 -0
  25. package/lib/rules/require-button-type.js +7 -0
  26. package/lib/rules/require-closing-tags.js +19 -2
  27. package/lib/rules/require-doctype.js +7 -0
  28. package/lib/rules/require-frame-title.js +7 -0
  29. package/lib/rules/require-img-alt.js +14 -1
  30. package/lib/rules/require-lang.js +7 -1
  31. package/lib/rules/require-li-container.js +13 -1
  32. package/lib/rules/require-meta-charset.js +12 -1
  33. package/lib/rules/require-meta-description.js +11 -0
  34. package/lib/rules/require-meta-viewport.js +12 -8
  35. package/lib/rules/require-title.js +9 -3
  36. package/lib/rules/utils/node-utils.js +27 -5
  37. package/lib/types.d.ts +105 -33
  38. package/package.json +8 -4
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ */
4
+
1
5
  const { RULE_CATEGORY } = require("../constants");
2
6
  const { NodeUtils } = require("./utils");
3
7
 
@@ -5,6 +9,9 @@ const MESSAGE_IDS = {
5
9
  UNEXPECTED: "unexpected",
6
10
  };
7
11
 
12
+ /**
13
+ * @type {Rule}
14
+ */
8
15
  module.exports = {
9
16
  meta: {
10
17
  type: "code",
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ * @typedef {import("../types").AnyNode} AnyNode
4
+ * @typedef {{tagPatterns: string[], attrPatterns: string[], message?: string}[]} Options
5
+ */
6
+
7
+ const { RULE_CATEGORY } = require("../constants");
8
+
9
+ const MESSAGE_IDS = {
10
+ RESTRICTED: "restricted",
11
+ };
12
+
13
+ /**
14
+ * @type {Rule}
15
+ */
16
+ module.exports = {
17
+ meta: {
18
+ type: "code",
19
+
20
+ docs: {
21
+ description: "Disallow specified attributes",
22
+ category: RULE_CATEGORY.BEST_PRACTICE,
23
+ recommended: false,
24
+ },
25
+
26
+ fixable: null,
27
+ schema: {
28
+ type: "array",
29
+
30
+ items: {
31
+ type: "object",
32
+ required: ["tagPatterns", "attrPatterns"],
33
+ properties: {
34
+ tagPatterns: {
35
+ type: "array",
36
+ items: {
37
+ type: "string",
38
+ },
39
+ },
40
+ attrPatterns: {
41
+ type: "array",
42
+ items: {
43
+ type: "string",
44
+ },
45
+ },
46
+ message: {
47
+ type: "string",
48
+ },
49
+ },
50
+ },
51
+ },
52
+ messages: {
53
+ [MESSAGE_IDS.RESTRICTED]: "'{{attr}}' is restricted from being used.",
54
+ },
55
+ },
56
+
57
+ create(context) {
58
+ /**
59
+ * @type {Options}
60
+ */
61
+ const options = context.options;
62
+ const checkers = options.map((option) => new PatternChecker(option));
63
+
64
+ return {
65
+ "*"(node) {
66
+ const tagName = node.tagName;
67
+ const startTag = node.startTag;
68
+ if (!tagName || !startTag) return;
69
+ if (!node.attrs.length) return;
70
+
71
+ node.attrs.forEach((attr) => {
72
+ if (!attr.name) return;
73
+
74
+ const matched = checkers.find((checker) =>
75
+ checker.test(node.tagName, attr.name)
76
+ );
77
+
78
+ if (!matched) return;
79
+
80
+ /**
81
+ * @type {{node: AnyNode, message: string, messageId?: string}}
82
+ */
83
+
84
+ const result = {
85
+ node: startTag,
86
+ message: "",
87
+ };
88
+
89
+ const customMessage = matched.getMessage();
90
+
91
+ if (customMessage) {
92
+ result.message = customMessage;
93
+ } else {
94
+ result.messageId = MESSAGE_IDS.RESTRICTED;
95
+ }
96
+
97
+ context.report({
98
+ ...result,
99
+ data: { attr: attr.name },
100
+ });
101
+ });
102
+ },
103
+ };
104
+ },
105
+ };
106
+
107
+ class PatternChecker {
108
+ /**
109
+ * @param {Options[number]} option
110
+ */
111
+ constructor(option) {
112
+ this.option = option;
113
+ this.tagRegExps = option.tagPatterns.map(
114
+ (pattern) => new RegExp(pattern, "u")
115
+ );
116
+ this.attrRegExps = option.attrPatterns.map(
117
+ (pattern) => new RegExp(pattern, "u")
118
+ );
119
+ this.message = option.message;
120
+ }
121
+
122
+ /**
123
+ * @param {string} tagName
124
+ * @param {string} attrName
125
+ * @returns {boolean}
126
+ */
127
+ test(tagName, attrName) {
128
+ const result =
129
+ this.tagRegExps.some((exp) => exp.test(tagName)) &&
130
+ this.attrRegExps.some((exp) => exp.test(attrName));
131
+ return result;
132
+ }
133
+
134
+ /**
135
+ * @returns {string}
136
+ */
137
+ getMessage() {
138
+ return this.message || "";
139
+ }
140
+ }
@@ -1,9 +1,16 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ */
4
+
1
5
  const { RULE_CATEGORY } = require("../constants");
2
6
 
3
7
  const MESSAGE_IDS = {
4
8
  UNEXPECTED: "unexpected",
5
9
  };
6
10
 
11
+ /**
12
+ * @type {Rule}
13
+ */
7
14
  module.exports = {
8
15
  meta: {
9
16
  type: "code",
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ */
4
+
1
5
  const { RULE_CATEGORY } = require("../constants");
2
6
  const { NodeUtils } = require("./utils");
3
7
 
@@ -5,6 +9,9 @@ const MESSAGE_IDS = {
5
9
  MISSING: "missing",
6
10
  };
7
11
 
12
+ /**
13
+ * @type {Rule}
14
+ */
8
15
  module.exports = {
9
16
  meta: {
10
17
  type: "code",
@@ -1,3 +1,9 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ * @typedef {import("../types").Range} Range
4
+ * @typedef {import("../types").AttrNode} AttrNode
5
+ */
6
+
1
7
  const { RULE_CATEGORY } = require("../constants");
2
8
 
3
9
  const MESSAGE_IDS = {
@@ -12,6 +18,9 @@ const QUOTES_STYLES = {
12
18
 
13
19
  const QUOTES_CODES = [`"`, `'`];
14
20
 
21
+ /**
22
+ * @type {Rule}
23
+ */
15
24
  module.exports = {
16
25
  meta: {
17
26
  type: "code",
@@ -45,16 +54,28 @@ module.exports = {
45
54
 
46
55
  const sourceCode = context.getSourceCode();
47
56
 
57
+ /**
58
+ * @param {Range} range
59
+ * @returns {string}
60
+ */
48
61
  function getCodeIn(range) {
49
62
  return sourceCode.text.slice(range[0], range[1]);
50
63
  }
51
64
 
65
+ /**
66
+ * @param {AttrNode} attr
67
+ * @returns {Range}
68
+ */
52
69
  function getValueRange(attr) {
53
70
  const attrCode = getCodeIn(attr.range);
54
71
  const [matched = ""] = attrCode.match(/\S*?\s*=\s*/) || [];
55
72
  return [attr.range[0] + matched.length, attr.range[1]];
56
73
  }
57
74
 
75
+ /**
76
+ * @param {AttrNode} attr
77
+ * @returns {[string, string]}
78
+ */
58
79
  function getQuotes(attr) {
59
80
  const [valueStart, valueEnd] = getValueRange(attr);
60
81
  const opening = getCodeIn([valueStart, valueStart + 1]);
@@ -62,11 +83,18 @@ module.exports = {
62
83
  return [opening, closing];
63
84
  }
64
85
 
86
+ /**
87
+ * @param {AttrNode} attr
88
+ * @returns {boolean}
89
+ */
65
90
  function hasEqualSign(attr) {
66
91
  const keyEnd = attr.range[0] + attr.name.length;
67
92
  return getCodeIn([keyEnd, attr.range[1]]).trimStart().startsWith("=");
68
93
  }
69
94
 
95
+ /**
96
+ * @param {AttrNode} attr
97
+ */
70
98
  function checkQuotes(attr) {
71
99
  const [opening, closing] = getQuotes(attr);
72
100
  if (QUOTES_CODES.includes(opening)) {
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ */
4
+
1
5
  const { RULE_CATEGORY } = require("../constants");
2
6
  const { NodeUtils } = require("./utils");
3
7
 
@@ -8,6 +12,9 @@ const MESSAGE_IDS = {
8
12
 
9
13
  const VALID_BUTTON_TYPES_SET = new Set(["submit", "button", "reset"]);
10
14
 
15
+ /**
16
+ * @type {Rule}
17
+ */
11
18
  module.exports = {
12
19
  meta: {
13
20
  type: "code",
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ */
4
+
1
5
  const { RULE_CATEGORY, VOID_ELEMENTS } = require("../constants");
2
6
 
3
7
  const VOID_ELEMENTS_SET = new Set(VOID_ELEMENTS);
@@ -8,6 +12,9 @@ const MESSAGE_IDS = {
8
12
  UNEXPECTED: "unexpected",
9
13
  };
10
14
 
15
+ /**
16
+ * @type {Rule}
17
+ */
11
18
  module.exports = {
12
19
  meta: {
13
20
  type: "code",
@@ -50,6 +57,16 @@ module.exports = {
50
57
 
51
58
  function checkClosingTag(node) {
52
59
  if (node.startTag && !node.endTag) {
60
+ if (
61
+ node.namespaceURI === "http://www.w3.org/2000/svg" ||
62
+ node.namespaceURI === "http://www.w3.org/1998/Math/MathML"
63
+ ) {
64
+ const code = getCodeIn(node.startTag.range);
65
+ const hasSelfClose = code.endsWith("/>");
66
+ if (hasSelfClose) {
67
+ return;
68
+ }
69
+ }
53
70
  context.report({
54
71
  node: node.startTag,
55
72
  data: {
@@ -60,7 +77,7 @@ module.exports = {
60
77
  }
61
78
  }
62
79
 
63
- function checkSelfClosing(node) {
80
+ function checkVoidElement(node) {
64
81
  const startTag = node.startTag;
65
82
  const code = getCodeIn(startTag.range);
66
83
  const hasSelfClose = code.endsWith("/>");
@@ -101,7 +118,7 @@ module.exports = {
101
118
  "*"(node) {
102
119
  if (node.startTag) {
103
120
  if (VOID_ELEMENTS_SET.has(node.tagName)) {
104
- checkSelfClosing(node);
121
+ checkVoidElement(node);
105
122
  } else {
106
123
  checkClosingTag(node);
107
124
  }
@@ -1,9 +1,16 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ */
4
+
1
5
  const { RULE_CATEGORY } = require("../constants");
2
6
 
3
7
  const MESSAGE_IDS = {
4
8
  MISSING: "missing",
5
9
  };
6
10
 
11
+ /**
12
+ * @type {Rule}
13
+ */
7
14
  module.exports = {
8
15
  meta: {
9
16
  type: "code",
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ */
4
+
1
5
  const { RULE_CATEGORY } = require("../constants");
2
6
  const { NodeUtils } = require("./utils");
3
7
 
@@ -6,6 +10,9 @@ const MESSAGE_IDS = {
6
10
  UNEXPECTED: "unexpected",
7
11
  };
8
12
 
13
+ /**
14
+ * @type {Rule}
15
+ */
9
16
  module.exports = {
10
17
  meta: {
11
18
  type: "code",
@@ -1,9 +1,17 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ * @typedef {import("../types").ElementNode} ElementNode
4
+ */
5
+
1
6
  const { RULE_CATEGORY } = require("../constants");
2
7
 
3
8
  const MESSAGE_IDS = {
4
9
  MISSING_ALT: "missingAlt",
5
10
  };
6
11
 
12
+ /**
13
+ * @type {Rule}
14
+ */
7
15
  module.exports = {
8
16
  meta: {
9
17
  type: "code",
@@ -35,8 +43,13 @@ module.exports = {
35
43
  },
36
44
  };
37
45
 
46
+ /**
47
+ * Checks whether a node has `alt` attribute value or not.
48
+ * @param {ElementNode} node a node to check.
49
+ * @returns {boolean} `true` if a node has `alt` attribute value.
50
+ */
38
51
  function hasAltAttrAndValue(node) {
39
52
  return (node.attrs || []).some((attr) => {
40
- return attr.name === "alt" && attr.value.trim().length > 0;
53
+ return attr.name === "alt" && typeof attr.value === "string";
41
54
  });
42
55
  }
@@ -1,4 +1,7 @@
1
- // @ts-check
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ */
4
+
2
5
  const { RULE_CATEGORY } = require("../constants");
3
6
  const { NodeUtils } = require("./utils");
4
7
 
@@ -7,6 +10,9 @@ const MESSAGE_IDS = {
7
10
  EMPTY: "empty",
8
11
  };
9
12
 
13
+ /**
14
+ * @type {Rule}
15
+ */
10
16
  module.exports = {
11
17
  meta: {
12
18
  type: "code",
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ */
4
+
1
5
  const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
2
6
 
3
7
  const MESSAGE_IDS = {
@@ -6,6 +10,9 @@ const MESSAGE_IDS = {
6
10
 
7
11
  const VALID_CONTAINERS = [NODE_TYPES.UL, NODE_TYPES.OL, NODE_TYPES.MENU];
8
12
 
13
+ /**
14
+ * @type {Rule}
15
+ */
9
16
  module.exports = {
10
17
  meta: {
11
18
  type: "code",
@@ -27,7 +34,12 @@ module.exports = {
27
34
  create(context) {
28
35
  return {
29
36
  Li(node) {
30
- if (!node.parent || !VALID_CONTAINERS.includes(node.parent.type)) {
37
+ if (!node.parent) {
38
+ context.report({
39
+ node,
40
+ messageId: MESSAGE_IDS.INVALID,
41
+ });
42
+ } else if (!VALID_CONTAINERS.includes(node.parent.type || "")) {
31
43
  context.report({
32
44
  node,
33
45
  messageId: MESSAGE_IDS.INVALID,
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @typedef {import("../types").ElementNode} ElementNode
3
+ * @typedef {import("../types").Context} Context
4
+ */
5
+
1
6
  const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
2
7
  const { NodeUtils } = require("./utils");
3
8
 
@@ -24,8 +29,14 @@ module.exports = {
24
29
  },
25
30
  },
26
31
 
32
+ /**
33
+ * @param {Context} context
34
+ */
27
35
  create(context) {
28
36
  return {
37
+ /**
38
+ * @param {ElementNode} node
39
+ */
29
40
  Head(node) {
30
41
  const metaCharset = (node.childNodes || []).find((child) => {
31
42
  return (
@@ -41,7 +52,7 @@ module.exports = {
41
52
  return;
42
53
  }
43
54
  const charsetAttr = NodeUtils.findAttr(metaCharset, "charset");
44
- if (!charsetAttr || !charsetAttr.value.length) {
55
+ if (charsetAttr && !charsetAttr.value.length) {
45
56
  context.report({
46
57
  node: charsetAttr,
47
58
  messageId: MESSAGE_IDS.EMPTY,
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @typedef {import("../types").ElementNode} ElementNode
3
+ * @typedef {import("../types").Context} Context
4
+ */
5
+
1
6
  const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
2
7
  const { NodeUtils } = require("./utils");
3
8
 
@@ -25,8 +30,14 @@ module.exports = {
25
30
  },
26
31
  },
27
32
 
33
+ /**
34
+ * @param {Context} context
35
+ */
28
36
  create(context) {
29
37
  return {
38
+ /**
39
+ * @param {ElementNode} node
40
+ */
30
41
  Head(node) {
31
42
  const metaTags = (node.childNodes || []).filter(
32
43
  (child) => child.type === NODE_TYPES.META
@@ -1,7 +1,6 @@
1
- // @ts-check
2
1
  /**
3
- * @typedef {import("../types").HTMLNode} HTMLNode
4
- * @typedef {import("../types").AttrNode} AttrNode
2
+ * @typedef {import("../types").ElementNode} ElementNode
3
+ * @typedef {import("../types").Rule} Rule
5
4
  */
6
5
 
7
6
  const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
@@ -12,6 +11,9 @@ const MESSAGE_IDS = {
12
11
  EMPTY: "empty",
13
12
  };
14
13
 
14
+ /**
15
+ * @type {Rule}
16
+ */
15
17
  module.exports = {
16
18
  meta: {
17
19
  type: "code",
@@ -33,7 +35,7 @@ module.exports = {
33
35
 
34
36
  create(context) {
35
37
  /**
36
- * @param {HTMLNode} node
38
+ * @param {ElementNode} node
37
39
  * @returns {boolean}
38
40
  */
39
41
  function isMetaViewport(node) {
@@ -44,9 +46,6 @@ module.exports = {
44
46
  return false;
45
47
  }
46
48
  return {
47
- /**
48
- * @param {HTMLNode} node
49
- */
50
49
  Head(node) {
51
50
  const metaViewport = (node.childNodes || []).find(isMetaViewport);
52
51
  if (!metaViewport) {
@@ -57,7 +56,12 @@ module.exports = {
57
56
  return;
58
57
  }
59
58
  const contentAttr = NodeUtils.findAttr(metaViewport, "content");
60
- if (!contentAttr || !contentAttr.value.length) {
59
+ if (!contentAttr) {
60
+ context.report({
61
+ node: metaViewport,
62
+ messageId: MESSAGE_IDS.EMPTY,
63
+ });
64
+ } else if (!contentAttr.value.length) {
61
65
  context.report({
62
66
  node: contentAttr,
63
67
  messageId: MESSAGE_IDS.EMPTY,
@@ -1,10 +1,18 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ */
4
+
1
5
  const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
6
+ const { NodeUtils } = require("./utils");
2
7
 
3
8
  const MESSAGE_IDS = {
4
9
  MISSING_TITLE: "missing",
5
10
  EMPTY_TITLE: "empty",
6
11
  };
7
12
 
13
+ /**
14
+ * @type {Rule}
15
+ */
8
16
  module.exports = {
9
17
  meta: {
10
18
  type: "code",
@@ -23,7 +31,6 @@ module.exports = {
23
31
  [MESSAGE_IDS.EMPTY_TITLE]: "Unexpected empty text in `<title><title/>`",
24
32
  },
25
33
  },
26
-
27
34
  create(context) {
28
35
  return {
29
36
  Head(node) {
@@ -38,8 +45,7 @@ module.exports = {
38
45
  });
39
46
  } else if (
40
47
  !(titleTag.childNodes || []).some(
41
- (node) =>
42
- node.type === NODE_TYPES.TEXT && node.value.trim().length > 0
48
+ (node) => NodeUtils.isTextNode(node) && node.value.trim().length > 0
43
49
  )
44
50
  ) {
45
51
  context.report({
@@ -1,13 +1,17 @@
1
- // @ts-check
2
1
  /**
3
- * @typedef {import("../../types").HTMLNode} HTMLNode
2
+ * @typedef {import("../../types").ElementNode} ElementNode
4
3
  * @typedef {import("../../types").AttrNode} AttrNode
4
+ * @typedef {import("../../types").AnyNode} AnyNode
5
+ * @typedef {import("../../types").TextNode} TextNode
6
+ * @typedef {import("../../types").BaseNode} BaseNode
7
+ * @typedef {import("../../types").TextLineNode} TextLineNode
8
+ * @typedef {import("../../types").CommentNode} CommentNode
5
9
  */
6
10
 
7
11
  module.exports = {
8
12
  /**
9
13
  * Find attribute by name in the given node
10
- * @param {HTMLNode} node node
14
+ * @param {ElementNode} node node
11
15
  * @param {string} name attribute name
12
16
  * @return {AttrNode | void}
13
17
  */
@@ -20,7 +24,7 @@ module.exports = {
20
24
  },
21
25
  /**
22
26
  * Checks a node has attribute with the given name or not.
23
- * @param {HTMLNode} node node
27
+ * @param {ElementNode} node node
24
28
  * @param {string} name attribute name
25
29
  * @return {boolean} `true` if the node has a attribute, otherwise `false`.
26
30
  */
@@ -29,10 +33,28 @@ module.exports = {
29
33
  },
30
34
  /**
31
35
  * Checks whether a node's all tokens are on the same line or not.
32
- * @param {HTMLNode} node A node to check
36
+ * @param {ElementNode} node A node to check
33
37
  * @returns {boolean} `true` if a node's tokens are on the same line, otherwise `false`.
34
38
  */
35
39
  isNodeTokensOnSameLine(node) {
36
40
  return node.loc.start.line === node.loc.end.line;
37
41
  },
42
+
43
+ /**
44
+ * Checks whether a node is a TextNode or not.
45
+ * @param {Object} node A node to check
46
+ * @returns {node is TextNode} `true` if a node is `TextNode`, otherwise `false`.
47
+ */
48
+ isTextNode(node) {
49
+ return !!(node && node.type === "text" && typeof node.value === "string");
50
+ },
51
+
52
+ /**
53
+ * Checks whether a node is a CommentNode or not.
54
+ * @param {Object} node A node to check
55
+ * @returns {node is CommentNode} `true` if a node is `CommentNode`, otherwise `false`.
56
+ */
57
+ isCommentNode(node) {
58
+ return !!(node && node.type === "comment");
59
+ },
38
60
  };