@html-eslint/eslint-plugin 0.10.1 → 0.11.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 (37) 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 +19 -7
  8. package/lib/rules/index.js +4 -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 +24 -2
  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-skip-heading-levels.js +7 -0
  22. package/lib/rules/no-target-blank.js +7 -0
  23. package/lib/rules/quotes.js +28 -0
  24. package/lib/rules/require-button-type.js +7 -0
  25. package/lib/rules/require-closing-tags.js +7 -0
  26. package/lib/rules/require-doctype.js +7 -0
  27. package/lib/rules/require-frame-title.js +7 -0
  28. package/lib/rules/require-img-alt.js +13 -0
  29. package/lib/rules/require-lang.js +7 -1
  30. package/lib/rules/require-li-container.js +13 -1
  31. package/lib/rules/require-meta-charset.js +12 -1
  32. package/lib/rules/require-meta-description.js +11 -0
  33. package/lib/rules/require-meta-viewport.js +12 -8
  34. package/lib/rules/require-title.js +9 -3
  35. package/lib/rules/utils/node-utils.js +27 -5
  36. package/lib/types.d.ts +104 -33
  37. package/package.json +8 -4
@@ -1,8 +1,3 @@
1
- /**
2
- * @typedef {import("../types").NodeTypes} NodeTypes
3
- */
4
-
5
- /** @type {NodeTypes} */
6
1
  module.exports = {
7
2
  PROGRAM: "Program",
8
3
  TEXT: "text",
@@ -1,5 +1,5 @@
1
1
  // https://html.spec.whatwg.org/#non-conforming-features
2
- /** @type {string[] */
2
+ /** @type {string[]} */
3
3
  module.exports = [
4
4
  "applet",
5
5
  "acronym",
@@ -1,8 +1,3 @@
1
- /**
2
- * @typedef {import("../types").RuleCategory} RuleCategory
3
- */
4
-
5
- /** @type {RuleCategory} */
6
1
  module.exports = {
7
2
  BEST_PRACTICE: "Best Practice",
8
3
  SEO: "SEO",
@@ -1,4 +1,4 @@
1
- /** @type {string[] */
1
+ /** @type {string[]} */
2
2
  module.exports = [
3
3
  "area",
4
4
  "base",
@@ -1,5 +1,7 @@
1
1
  /**
2
- * @typedef {import("../types").HTMLNode} HTMLNode
2
+ * @typedef {import("../types").ElementNode} ElementNode
3
+ * @typedef {import("../types").AnyNode} AnyNode
4
+ * @typedef {import("../types").Context} Context
3
5
  */
4
6
 
5
7
  const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
@@ -29,9 +31,12 @@ module.exports = {
29
31
  },
30
32
  },
31
33
 
34
+ /**
35
+ * @param {Context} context
36
+ */
32
37
  create(context) {
33
- function checkSiblings(sibilings) {
34
- sibilings
38
+ function checkSiblings(siblings) {
39
+ siblings
35
40
  .filter((node) => node.type !== NODE_TYPES.TEXT && node.range[0])
36
41
  .forEach((current, index, arr) => {
37
42
  const after = arr[index + 1];
@@ -50,6 +55,10 @@ module.exports = {
50
55
  });
51
56
  }
52
57
 
58
+ /**
59
+ *
60
+ * @param {ElementNode['childNodes'][number]} node
61
+ */
53
62
  function checkChild(node) {
54
63
  const children = (node.childNodes || []).filter(
55
64
  (n) => !!n.range[0] && n.type !== NODE_TYPES.TEXT
@@ -57,31 +66,40 @@ module.exports = {
57
66
  const first = children[0];
58
67
  const last = children[children.length - 1];
59
68
  if (first) {
60
- if (isOnTheSameLine(node.startTag, first)) {
69
+ if (node.startTag && isOnTheSameLine(node.startTag, first)) {
61
70
  context.report({
62
71
  node: node.startTag,
63
72
  messageId: MESSAGE_IDS.EXPECT_NEW_LINE_AFTER,
64
73
  data: { tag: `<${node.tagName}>` },
65
74
  fix(fixer) {
66
- return fixer.insertTextAfter(node.startTag, "\n");
75
+ if (node.startTag) {
76
+ return fixer.insertTextAfter(node.startTag, "\n");
77
+ }
78
+ return null;
67
79
  },
68
80
  });
69
81
  }
70
82
  }
71
83
  if (last) {
72
- if (isOnTheSameLine(node.endTag, last)) {
84
+ if (node.endTag && isOnTheSameLine(node.endTag, last)) {
73
85
  context.report({
74
86
  node: node.endTag,
75
87
  messageId: MESSAGE_IDS.EXPECT_NEW_LINE_BEFORE,
76
88
  data: { tag: `</${node.tagName}>` },
77
89
  fix(fixer) {
78
- return fixer.insertTextBefore(node.endTag, "\n");
90
+ if (node.endTag) {
91
+ return fixer.insertTextBefore(node.endTag, "\n");
92
+ }
93
+ return null;
79
94
  },
80
95
  });
81
96
  }
82
97
  }
83
98
  }
84
99
  return {
100
+ /**
101
+ * @param {ElementNode} node
102
+ */
85
103
  "*"(node) {
86
104
  if (node.type !== NODE_TYPES.TEXT) {
87
105
  checkSiblings(node.childNodes || []);
@@ -94,13 +112,15 @@ module.exports = {
94
112
 
95
113
  /**
96
114
  * Checks whether two nodes are on the same line or not.
97
- * @param {HTMLNode} nodeBefore A node before
98
- * @param {HTMLNode} nodeAfter A node after
115
+ * @param {AnyNode} nodeBefore A node before
116
+ * @param {AnyNode} nodeAfter A node after
99
117
  * @returns {boolean} `true` if two nodes are on the same line, otherwise `false`.
100
118
  */
101
119
  function isOnTheSameLine(nodeBefore, nodeAfter) {
102
120
  if (nodeBefore && nodeAfter) {
121
+ // @ts-ignore
103
122
  if (nodeBefore.endTag) {
123
+ // @ts-ignore
104
124
  return nodeBefore.endTag.loc.end.line === nodeAfter.loc.start.line;
105
125
  }
106
126
  return nodeBefore.loc.start.line === nodeAfter.loc.start.line;
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ */
4
+
1
5
  const { RULE_CATEGORY } = require("../constants");
2
6
  const { NamingUtils, NodeUtils } = require("./utils");
3
7
 
@@ -5,7 +9,7 @@ const MESSAGE_IDS = {
5
9
  WRONG: "wrong",
6
10
  };
7
11
 
8
- const CONEVNTIONS = {
12
+ const CONVENTIONS = {
9
13
  CAMEL_CASE: "camelCase",
10
14
  SNAKE_CASE: "snake_case",
11
15
  PASCAL_CASE: "PascalCase",
@@ -13,12 +17,15 @@ const CONEVNTIONS = {
13
17
  };
14
18
 
15
19
  const CONVENTION_CHECKERS = {
16
- [CONEVNTIONS.CAMEL_CASE]: NamingUtils.isCamelCase,
17
- [CONEVNTIONS.SNAKE_CASE]: NamingUtils.isSnakeCase,
18
- [CONEVNTIONS.PASCAL_CASE]: NamingUtils.isPascalCase,
19
- [CONEVNTIONS.KEBAB_CASE]: NamingUtils.isKebabCase,
20
+ [CONVENTIONS.CAMEL_CASE]: NamingUtils.isCamelCase,
21
+ [CONVENTIONS.SNAKE_CASE]: NamingUtils.isSnakeCase,
22
+ [CONVENTIONS.PASCAL_CASE]: NamingUtils.isPascalCase,
23
+ [CONVENTIONS.KEBAB_CASE]: NamingUtils.isKebabCase,
20
24
  };
21
25
 
26
+ /**
27
+ * @type {Rule}
28
+ */
22
29
  module.exports = {
23
30
  meta: {
24
31
  type: "code",
@@ -32,7 +39,7 @@ module.exports = {
32
39
  fixable: null,
33
40
  schema: [
34
41
  {
35
- enum: Object.values(CONEVNTIONS),
42
+ enum: Object.values(CONVENTIONS),
36
43
  },
37
44
  ],
38
45
  messages: {
@@ -45,7 +52,7 @@ module.exports = {
45
52
  const convention =
46
53
  context.options && context.options.length
47
54
  ? context.options[0]
48
- : CONEVNTIONS.SNAKE_CASE;
55
+ : CONVENTIONS.SNAKE_CASE;
49
56
 
50
57
  const checkNaming = CONVENTION_CHECKERS[convention];
51
58
 
@@ -1,7 +1,9 @@
1
- // @ts-check
2
1
  /**
3
- * @typedef {import("../types").HTMLNode} HTMLNode
2
+ * @typedef {import("../types").Rule} Rule
3
+ * @typedef {import("../types").ElementNode} ElementNode
4
4
  * @typedef {import("../types").AttrNode} AttrNode
5
+ * @typedef {import("../types").TagNode} TagNode
6
+ * @typedef {import("../types").AnyNode} AnyNode
5
7
  * @typedef {import("../types").BaseNode} BaseNode
6
8
  * @typedef {Object} IndentType
7
9
  * @property {"tab"} TAB
@@ -12,6 +14,7 @@
12
14
  */
13
15
 
14
16
  const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
17
+ const { NodeUtils } = require("./utils");
15
18
 
16
19
  /** @type {MessageId} */
17
20
  const MESSAGE_ID = {
@@ -31,6 +34,9 @@ const IGNORING_NODES = [
31
34
  NODE_TYPES.XMP,
32
35
  ];
33
36
 
37
+ /**
38
+ * @type {Rule}
39
+ */
34
40
  module.exports = {
35
41
  meta: {
36
42
  type: "code",
@@ -60,7 +66,6 @@ module.exports = {
60
66
  "Expected indentation of {{expected}} but found {{actual}}.",
61
67
  },
62
68
  },
63
-
64
69
  create(context) {
65
70
  const sourceCode = context.getSourceCode();
66
71
  const indentLevel = new IndentLevel();
@@ -79,7 +84,7 @@ module.exports = {
79
84
 
80
85
  /**
81
86
  * @param {BaseNode} node
82
- * @param {HTMLNode} [nodeToReport]
87
+ * @param {BaseNode} [nodeToReport]
83
88
  */
84
89
  function checkIndent(node, nodeToReport) {
85
90
  const codeBefore = getLineCodeBefore(node);
@@ -133,7 +138,7 @@ module.exports = {
133
138
  }
134
139
 
135
140
  /**
136
- * @param {BaseNode} startTag
141
+ * @param {AnyNode} startTag
137
142
  */
138
143
  function checkEndOfStartTag(startTag) {
139
144
  const start = startTag.range[1] - 1;
@@ -160,7 +165,7 @@ module.exports = {
160
165
  let nodesToIgnoreChildren = [];
161
166
  return {
162
167
  /**
163
- * @param {HTMLNode} node
168
+ * @param {ElementNode} node
164
169
  */
165
170
  "*"(node) {
166
171
  if (IGNORING_NODES.includes(node.type)) {
@@ -186,7 +191,11 @@ module.exports = {
186
191
  }
187
192
  });
188
193
 
189
- if (node.lineNodes && node.lineNodes.length) {
194
+ if (
195
+ (NodeUtils.isTextNode(node) || NodeUtils.isCommentNode(node)) &&
196
+ node.lineNodes &&
197
+ node.lineNodes.length
198
+ ) {
190
199
  if (!node.startTag) {
191
200
  indentLevel.down();
192
201
  }
@@ -219,6 +228,9 @@ module.exports = {
219
228
  };
220
229
 
221
230
  function getIndentTypeAndSize(options) {
231
+ /**
232
+ * @type {IndentType['SPACE'] | IndentType['TAB']}
233
+ */
222
234
  let indentType = INDENT_TYPES.SPACE;
223
235
  let indentSize = 4;
224
236
  if (options.length) {
@@ -25,6 +25,8 @@ const noDuplicateAttrs = require("./no-duplicate-attrs");
25
25
  const noAbstractRoles = require("./no-abstract-roles");
26
26
  const requireButtonType = require("./require-button-type");
27
27
  const noAriaHiddenBody = require("./no-aria-hidden-body");
28
+ const noMultipleEmptyLines = require("./no-multiple-empty-lines");
29
+ const noAccesskeyAttrs = require("./no-accesskey-attrs");
28
30
 
29
31
  module.exports = {
30
32
  "require-lang": requireLang,
@@ -54,4 +56,6 @@ module.exports = {
54
56
  "no-abstract-roles": noAbstractRoles,
55
57
  "require-button-type": requireButtonType,
56
58
  "no-aria-hidden-body": noAriaHiddenBody,
59
+ "no-multiple-empty-lines": noMultipleEmptyLines,
60
+ "no-accesskey-attrs": noAccesskeyAttrs,
57
61
  };
@@ -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
 
@@ -20,6 +24,9 @@ const ABSTRACT_ROLE_SET = new Set([
20
24
  "window",
21
25
  ]);
22
26
 
27
+ /**
28
+ * @type {Rule}
29
+ */
23
30
  module.exports = {
24
31
  meta: {
25
32
  type: "code",
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ */
4
+
5
+ const { RULE_CATEGORY } = require("../constants");
6
+ const { NodeUtils } = require("./utils");
7
+
8
+ const MESSAGE_IDS = {
9
+ UNEXPECTED: "unexpected",
10
+ };
11
+
12
+ /**
13
+ * @type {Rule}
14
+ */
15
+ module.exports = {
16
+ meta: {
17
+ type: "code",
18
+
19
+ docs: {
20
+ description: "Disallow to use of accesskey attribute",
21
+ category: RULE_CATEGORY.ACCESSIBILITY,
22
+ recommended: false,
23
+ },
24
+
25
+ fixable: null,
26
+ schema: [],
27
+ messages: {
28
+ [MESSAGE_IDS.UNEXPECTED]: "Unexpected use of accesskey attribute.",
29
+ },
30
+ },
31
+
32
+ create(context) {
33
+ return {
34
+ "*"(node) {
35
+ const accessKeyAttr = NodeUtils.findAttr(node, "accesskey");
36
+ if (accessKeyAttr) {
37
+ context.report({
38
+ node: accessKeyAttr,
39
+ messageId: MESSAGE_IDS.UNEXPECTED,
40
+ });
41
+ }
42
+ },
43
+ };
44
+ },
45
+ };
@@ -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",
@@ -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
  DUPLICATE_ATTRS: "duplicateAttrs",
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
  DUPLICATE_ID: "duplicateId",
6
10
  };
7
11
 
12
+ /**
13
+ * @type {Rule}
14
+ */
8
15
  module.exports = {
9
16
  meta: {
10
17
  type: "code",
@@ -1,3 +1,10 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ * @typedef {import("../types").AttrNode} AttrNode
4
+ * @typedef {import("../types").TagNode} TagNode
5
+ * @typedef {import("../types").ElementNode} ElementNode
6
+ */
7
+
1
8
  const { RULE_CATEGORY } = require("../constants");
2
9
 
3
10
  const MESSAGE_IDS = {
@@ -6,6 +13,9 @@ const MESSAGE_IDS = {
6
13
  EXTRA_BEFORE: "unexpectedBefore",
7
14
  };
8
15
 
16
+ /**
17
+ * @type {Rule}
18
+ */
9
19
  module.exports = {
10
20
  meta: {
11
21
  type: "code",
@@ -25,6 +35,9 @@ module.exports = {
25
35
  },
26
36
  },
27
37
  create(context) {
38
+ /**
39
+ * @param {AttrNode[]} attrs
40
+ */
28
41
  function checkExtraSpacesBetweenAttrs(attrs) {
29
42
  attrs.forEach((current, index, attrs) => {
30
43
  if (index >= attrs.length - 1) {
@@ -50,9 +63,13 @@ module.exports = {
50
63
  }
51
64
  });
52
65
  }
66
+ /**
67
+ * @param {TagNode} startTag
68
+ * @param {AttrNode} lastAttr
69
+ */
53
70
  function checkExtraSpaceAfter(startTag, lastAttr) {
54
71
  if (startTag.loc.end.line !== lastAttr.loc.end.line) {
55
- // skip the attribute on the diffrent line with the start tag
72
+ // skip the attribute on the different line with the start tag
56
73
  return;
57
74
  }
58
75
  const spacesBetween = startTag.loc.end.column - lastAttr.loc.end.column;
@@ -72,9 +89,14 @@ module.exports = {
72
89
  });
73
90
  }
74
91
  }
92
+
93
+ /**
94
+ * @param {ElementNode} node
95
+ * @param {AttrNode} firstAttr
96
+ */
75
97
  function checkExtraSpaceBefore(node, firstAttr) {
76
98
  if (node.loc.start.line !== firstAttr.loc.start.line) {
77
- // skip the attribute on the diffrent line with the start tag
99
+ // skip the attribute on the different line with the start tag
78
100
  return;
79
101
  }
80
102
  const nodeLength = node.tagName.length;
@@ -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
  INLINE_STYLE: "unexpectedInlineStyle",
6
10
  };
7
11
 
12
+ /**
13
+ * @type {Rule}
14
+ */
8
15
  module.exports = {
9
16
  meta: {
10
17
  type: "code",
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ */
4
+
5
+ const { RULE_CATEGORY } = require("../constants");
6
+
7
+ const MESSAGE_IDS = {
8
+ UNEXPECTED: "unexpected",
9
+ };
10
+
11
+ /**
12
+ * @type {Rule}
13
+ */
14
+ module.exports = {
15
+ meta: {
16
+ type: "code",
17
+
18
+ docs: {
19
+ description: "Disallow multiple empty lines",
20
+ category: RULE_CATEGORY.STYLE,
21
+ recommended: false,
22
+ },
23
+
24
+ fixable: "whitespace",
25
+ schema: [
26
+ {
27
+ type: "object",
28
+ properties: {
29
+ max: {
30
+ type: "integer",
31
+ minimum: 0,
32
+ },
33
+ },
34
+ required: ["max"],
35
+ additionalProperties: false,
36
+ },
37
+ ],
38
+ messages: {
39
+ [MESSAGE_IDS.UNEXPECTED]: "More than {{max}} blank lines not allowed.",
40
+ },
41
+ },
42
+
43
+ create(context) {
44
+ const sourceCode = context.getSourceCode();
45
+ const lines = sourceCode.lines;
46
+ const max = context.options.length ? context.options[0].max : 2;
47
+ return {
48
+ "Program:exit"(node) {
49
+ /** @type {number[]} */
50
+ const nonEmptyLineNumbers = [];
51
+
52
+ lines.forEach((line, index) => {
53
+ if (line.trim().length > 0) {
54
+ nonEmptyLineNumbers.push(index + 1);
55
+ }
56
+ });
57
+
58
+ nonEmptyLineNumbers.forEach((current, index, arr) => {
59
+ const before = arr[index - 1];
60
+ if (typeof before === "number") {
61
+ if (current - before - 1 > max) {
62
+ context.report({
63
+ node,
64
+ loc: {
65
+ start: { line: before, column: 0 },
66
+ end: { line: current, column: 0 },
67
+ },
68
+ messageId: MESSAGE_IDS.UNEXPECTED,
69
+ data: {
70
+ max,
71
+ },
72
+ fix(fixer) {
73
+ const start = sourceCode.getIndexFromLoc({
74
+ line: before + 1,
75
+ column: 0,
76
+ });
77
+ const end = sourceCode.getIndexFromLoc({
78
+ line: current - max,
79
+ column: 0,
80
+ });
81
+ return fixer.removeRange([start, end]);
82
+ },
83
+ });
84
+ }
85
+ }
86
+ });
87
+ },
88
+ };
89
+ },
90
+ };
@@ -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
  MULTIPLE_H1: "unexpectedMultiH1",
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
  UNEXPECTED: "unexpected",
6
10
  };
7
11
 
12
+ /**
13
+ * @type {Rule}
14
+ */
8
15
  module.exports = {
9
16
  meta: {
10
17
  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
  const { OBSOLETE_TAGS } = require("../constants");
3
7
 
@@ -7,6 +11,9 @@ const MESSAGE_IDS = {
7
11
  UNEXPECTED: "unexpected",
8
12
  };
9
13
 
14
+ /**
15
+ * @type {Rule}
16
+ */
10
17
  module.exports = {
11
18
  meta: {
12
19
  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
  UNEXPECTED: "unexpected",
6
10
  };
7
11
 
12
+ /**
13
+ * @type {Rule}
14
+ */
8
15
  module.exports = {
9
16
  meta: {
10
17
  type: "code",
@@ -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",
@@ -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,6 +43,11 @@ 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
53
  return attr.name === "alt" && attr.value.trim().length > 0;
@@ -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
  };
package/lib/types.d.ts CHANGED
@@ -1,49 +1,120 @@
1
- export type RuleCategory = {
2
- BEST_PRACTICE: "Best Practice";
3
- SEO: "SEO";
4
- ACCESSIBILITY: "Accessibility";
5
- STYLE: "Style";
6
- };
1
+ import ESTree from "estree";
2
+ import ESLint from "eslint";
3
+
4
+ type Fix = ESLint.Rule.Fix;
5
+ type Token = ESLint.AST.Token;
6
+ export type Range = ESLint.AST.Range;
7
+
8
+ interface RuleListener {
9
+ [key: string]: (node: ElementNode) => void;
10
+ }
11
+
12
+ export interface Rule {
13
+ create(context: Context): RuleListener;
14
+ meta?: ESLint.Rule.RuleMetaData;
15
+ }
16
+
17
+ interface RuleFixer {
18
+ insertTextAfter(nodeOrToken: AnyNode | Token, text: string): Fix;
19
+
20
+ insertTextAfterRange(range: Range, text: string): Fix;
21
+
22
+ insertTextBefore(nodeOrToken: AnyNode | Token, text: string): Fix;
23
+
24
+ insertTextBeforeRange(range: Range, text: string): Fix;
25
+
26
+ remove(nodeOrToken: AnyNode | Token): Fix;
27
+
28
+ removeRange(range: Range): Fix;
29
+
30
+ replaceText(nodeOrToken: AnyNode | Token, text: string): Fix;
31
+
32
+ replaceTextRange(range: Range, text: string): Fix;
33
+ }
34
+
35
+ interface ReportDescriptorOptionsBase {
36
+ data?: { [key: string]: string };
37
+
38
+ fix?:
39
+ | null
40
+ | ((fixer: RuleFixer) => null | Fix | IterableIterator<Fix> | Fix[]);
41
+ }
7
42
 
8
- export type NodeTypes = {
9
- PROGRAM: "Program";
10
- TEXT: "text";
11
- TITLE: "Title";
12
- PRE: "Pre";
13
- MENU: "Menu";
14
- OL: "Ol";
15
- UL: "Ul";
16
- SCRIPT: "Script";
17
- XMP: "Xmp";
18
- META: "Meta";
19
- STYLE: "Style";
43
+ type SuggestionDescriptorMessage = { desc: string } | { messageId: string };
44
+ type SuggestionReportDescriptor = SuggestionDescriptorMessage &
45
+ ReportDescriptorOptionsBase;
46
+
47
+ interface ReportDescriptorOptions extends ReportDescriptorOptionsBase {
48
+ suggest?: SuggestionReportDescriptor[] | null;
49
+ }
50
+
51
+ type ReportDescriptor = ReportDescriptorMessage &
52
+ ReportDescriptorLocation &
53
+ ReportDescriptorOptions;
54
+ type ReportDescriptorMessage = { message: string } | { messageId: string };
55
+ type ReportDescriptorLocation = {
56
+ node?: BaseNode;
57
+ loc?: ESLint.AST.SourceLocation;
58
+ line?: number;
59
+ column?: number;
20
60
  };
21
61
 
22
- interface BaseNode {
62
+ export interface Context extends Omit<ESLint.Rule.RuleContext, "report"> {
63
+ report(descriptor: ReportDescriptor): void;
64
+ }
65
+
66
+ export interface BaseNode {
67
+ parent?: null | AnyNode;
68
+ range: [number, number];
23
69
  start: number;
24
70
  end: number;
25
- range: [number, number];
26
71
  loc: {
27
- end: {
28
- line: number;
29
- column: number;
30
- };
31
- start: {
32
- line: number;
33
- column: number;
34
- };
72
+ start: ESTree.Position;
73
+ end: ESTree.Position;
35
74
  };
75
+ type?: string;
76
+ }
77
+
78
+ export interface TagNode extends BaseNode {
79
+ type: undefined;
36
80
  }
37
81
 
38
- export interface HTMLNode extends BaseNode {
39
- childNodes?: HTMLNode[];
40
- startTag?: BaseNode;
41
- endTag?: BaseNode;
82
+ export interface TextLineNode extends BaseNode {
83
+ textLine: string;
84
+ }
85
+
86
+ export interface TextNode extends BaseNode {
87
+ type: "text";
88
+ value: string;
89
+ lineNodes: TextLineNode[];
90
+ }
91
+
92
+ export interface ElementNode extends BaseNode {
42
93
  type: string;
43
- attrs?: AttrNode[];
94
+ tagName: string;
95
+ attrs: AttrNode[];
96
+ childNodes: ElementNode[];
97
+ startTag?: TagNode;
98
+ endTag?: TagNode;
44
99
  }
45
100
 
46
101
  export interface AttrNode extends BaseNode {
47
102
  name: string;
48
103
  value: string;
49
104
  }
105
+
106
+ export interface CommentNode extends BaseNode {
107
+ type: "comment";
108
+ value: string;
109
+ startTag?: TagNode;
110
+ endTag?: TagNode;
111
+ lineNodes: TextLineNode[];
112
+ }
113
+
114
+ export type AnyNode =
115
+ | AttrNode
116
+ | ElementNode
117
+ | TextNode
118
+ | TextLineNode
119
+ | TagNode
120
+ | CommentNode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@html-eslint/eslint-plugin",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "ESLint plugin for html",
5
5
  "author": "yeonjuan",
6
6
  "homepage": "https://github.com/yeonjuan/html-eslint#readme",
@@ -21,7 +21,8 @@
21
21
  "url": "git+https://github.com/yeonjuan/html-eslint.git"
22
22
  },
23
23
  "scripts": {
24
- "test": "jest --coverage"
24
+ "test": "jest --coverage",
25
+ "check:ts": "tsc"
25
26
  },
26
27
  "bugs": {
27
28
  "url": "https://github.com/yeonjuan/html-eslint/issues"
@@ -39,7 +40,10 @@
39
40
  "accessibility"
40
41
  ],
41
42
  "devDependencies": {
42
- "@html-eslint/parser": "^0.10.1"
43
+ "@html-eslint/parser": "^0.11.0",
44
+ "@types/eslint": "^7.2.10",
45
+ "@types/estree": "^0.0.47",
46
+ "typescript": "^4.2.4"
43
47
  },
44
- "gitHead": "827103223429c0a629d2823512df21fdf511edb5"
48
+ "gitHead": "a3873ecefae3b9306a5e98985cf4ccdbde95299a"
45
49
  }