@html-eslint/eslint-plugin 0.13.1 → 0.14.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.
@@ -1,17 +1,9 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- * @typedef {import("../types").ElementNode} ElementNode
4
- */
5
-
6
1
  const { RULE_CATEGORY } = require("../constants");
7
2
 
8
3
  const MESSAGE_IDS = {
9
4
  MISSING_ALT: "missingAlt",
10
5
  };
11
6
 
12
- /**
13
- * @type {Rule}
14
- */
15
7
  module.exports = {
16
8
  meta: {
17
9
  type: "code",
@@ -31,10 +23,19 @@ module.exports = {
31
23
 
32
24
  create(context) {
33
25
  return {
34
- Img(node) {
26
+ Tag(node) {
27
+ if (node.name !== "img") {
28
+ return;
29
+ }
35
30
  if (!hasAltAttrAndValue(node)) {
36
31
  context.report({
37
- node: node.startTag,
32
+ node: {
33
+ loc: {
34
+ start: node.openStart.loc.start,
35
+ end: node.openEnd.loc.end,
36
+ },
37
+ range: [node.openStart.range[0], node.openEnd.range[1]],
38
+ },
38
39
  messageId: MESSAGE_IDS.MISSING_ALT,
39
40
  });
40
41
  }
@@ -43,13 +44,10 @@ module.exports = {
43
44
  },
44
45
  };
45
46
 
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
- */
51
47
  function hasAltAttrAndValue(node) {
52
- return (node.attrs || []).some((attr) => {
53
- return attr.name === "alt" && typeof attr.value === "string";
48
+ return node.attributes.some((attr) => {
49
+ if (attr.key && attr.value) {
50
+ return attr.key.value === "alt" && typeof attr.value.value === "string";
51
+ }
54
52
  });
55
53
  }
@@ -10,9 +10,6 @@ const MESSAGE_IDS = {
10
10
  EMPTY: "empty",
11
11
  };
12
12
 
13
- /**
14
- * @type {Rule}
15
- */
16
13
  module.exports = {
17
14
  meta: {
18
15
  type: "code",
@@ -33,16 +30,25 @@ module.exports = {
33
30
 
34
31
  create(context) {
35
32
  return {
36
- Html(node) {
33
+ Tag(node) {
34
+ if (node.name !== "html") {
35
+ return;
36
+ }
37
37
  const langAttr = NodeUtils.findAttr(node, "lang");
38
38
  if (!langAttr) {
39
39
  context.report({
40
- node: node.startTag,
40
+ node: {
41
+ loc: {
42
+ start: node.openStart.loc.start,
43
+ end: node.openEnd.loc.end,
44
+ },
45
+ range: [node.openStart.range[0], node.openEnd.range[1]],
46
+ },
41
47
  messageId: MESSAGE_IDS.MISSING,
42
48
  });
43
- } else if (langAttr.value.trim().length === 0) {
49
+ } else if (langAttr.value && langAttr.value.value.trim().length === 0) {
44
50
  context.report({
45
- node: node.startTag,
51
+ node: node.openStart,
46
52
  messageId: MESSAGE_IDS.EMPTY,
47
53
  });
48
54
  }
@@ -1,18 +1,11 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- */
4
-
5
- const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
1
+ const { RULE_CATEGORY } = require("../constants");
6
2
 
7
3
  const MESSAGE_IDS = {
8
4
  INVALID: "invalid",
9
5
  };
10
6
 
11
- const VALID_CONTAINERS = [NODE_TYPES.UL, NODE_TYPES.OL, NODE_TYPES.MENU];
7
+ const VALID_CONTAINERS = ["ul", "ol", "menu"];
12
8
 
13
- /**
14
- * @type {Rule}
15
- */
16
9
  module.exports = {
17
10
  meta: {
18
11
  type: "code",
@@ -33,13 +26,16 @@ module.exports = {
33
26
 
34
27
  create(context) {
35
28
  return {
36
- Li(node) {
29
+ Tag(node) {
30
+ if (node.name !== "li") {
31
+ return;
32
+ }
37
33
  if (!node.parent) {
38
34
  context.report({
39
35
  node,
40
36
  messageId: MESSAGE_IDS.INVALID,
41
37
  });
42
- } else if (!VALID_CONTAINERS.includes(node.parent.type || "")) {
38
+ } else if (!VALID_CONTAINERS.includes(node.parent.name || "")) {
43
39
  context.report({
44
40
  node,
45
41
  messageId: MESSAGE_IDS.INVALID,
@@ -1,9 +1,8 @@
1
1
  /**
2
- * @typedef {import("../types").ElementNode} ElementNode
3
- * @typedef {import("../types").Context} Context
2
+ * @typedef {import("../types").Rule} Rule
4
3
  */
5
4
 
6
- const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
5
+ const { RULE_CATEGORY } = require("../constants");
7
6
  const { NodeUtils } = require("./utils");
8
7
 
9
8
  const MESSAGE_IDS = {
@@ -11,12 +10,15 @@ const MESSAGE_IDS = {
11
10
  EMPTY: "empty",
12
11
  };
13
12
 
13
+ /**
14
+ * @type {Rule}
15
+ */
14
16
  module.exports = {
15
17
  meta: {
16
18
  type: "code",
17
19
 
18
20
  docs: {
19
- description: 'Enforce to use `<meta name="chartset">` in `<head>`',
21
+ description: 'Enforce to use `<meta charset="...">` in `<head>`',
20
22
  category: RULE_CATEGORY.BEST_PRACTICE,
21
23
  recommended: false,
22
24
  },
@@ -24,26 +26,26 @@ module.exports = {
24
26
  fixable: null,
25
27
  schema: [],
26
28
  messages: {
27
- [MESSAGE_IDS.MISSING]: 'Missing `<meta name="description">`.',
29
+ [MESSAGE_IDS.MISSING]: 'Missing `<meta charset="...">`.',
28
30
  [MESSAGE_IDS.EMPTY]: "Unexpected empty charset.",
29
31
  },
30
32
  },
31
33
 
32
- /**
33
- * @param {Context} context
34
- */
35
34
  create(context) {
36
35
  return {
37
- /**
38
- * @param {ElementNode} node
39
- */
40
- Head(node) {
41
- const metaCharset = (node.childNodes || []).find((child) => {
36
+ Tag(node) {
37
+ if (node.name !== "head") {
38
+ return;
39
+ }
40
+
41
+ const metaCharset = node.children.find((child) => {
42
42
  return (
43
- child.type === NODE_TYPES.META &&
43
+ child.type === "Tag" &&
44
+ child.name === "meta" &&
44
45
  !!NodeUtils.findAttr(child, "charset")
45
46
  );
46
47
  });
48
+
47
49
  if (!metaCharset) {
48
50
  context.report({
49
51
  node,
@@ -52,11 +54,13 @@ module.exports = {
52
54
  return;
53
55
  }
54
56
  const charsetAttr = NodeUtils.findAttr(metaCharset, "charset");
55
- if (charsetAttr && !charsetAttr.value.length) {
56
- context.report({
57
- node: charsetAttr,
58
- messageId: MESSAGE_IDS.EMPTY,
59
- });
57
+ if (charsetAttr) {
58
+ if (!charsetAttr.value || !charsetAttr.value.value.length) {
59
+ context.report({
60
+ node: charsetAttr,
61
+ messageId: MESSAGE_IDS.EMPTY,
62
+ });
63
+ }
60
64
  }
61
65
  },
62
66
  };
@@ -1,9 +1,9 @@
1
1
  /**
2
- * @typedef {import("../types").ElementNode} ElementNode
3
- * @typedef {import("../types").Context} Context
2
+ * @typedef {import("../types").Rule} Rule
3
+ * @typedef {import("es-html-parser").TagNode} TagNode
4
4
  */
5
5
 
6
- const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
6
+ const { RULE_CATEGORY } = require("../constants");
7
7
  const { NodeUtils } = require("./utils");
8
8
 
9
9
  const MESSAGE_IDS = {
@@ -11,6 +11,17 @@ const MESSAGE_IDS = {
11
11
  EMPTY: "empty",
12
12
  };
13
13
 
14
+ /**
15
+ * @param {TagNode['children'][number]} node
16
+ * @returns {boolean}
17
+ */
18
+ function isMetaTagNode(node) {
19
+ return node.type === "Tag" && node.name === "meta";
20
+ }
21
+
22
+ /**
23
+ * @type {Rule}
24
+ */
14
25
  module.exports = {
15
26
  meta: {
16
27
  type: "code",
@@ -29,22 +40,21 @@ module.exports = {
29
40
  'Unexpected empty `content` in `<meta name="description">`',
30
41
  },
31
42
  },
32
-
33
- /**
34
- * @param {Context} context
35
- */
36
43
  create(context) {
37
44
  return {
38
- /**
39
- * @param {ElementNode} node
40
- */
41
- Head(node) {
42
- const metaTags = (node.childNodes || []).filter(
43
- (child) => child.type === NODE_TYPES.META
44
- );
45
+ Tag(node) {
46
+ if (node.name !== "head") {
47
+ return;
48
+ }
49
+ const metaTags = node.children.filter(isMetaTagNode);
50
+
45
51
  const descriptionMetaTags = metaTags.filter((meta) => {
46
52
  const nameAttr = NodeUtils.findAttr(meta, "name");
47
- return !!nameAttr && nameAttr.value.toLowerCase() === "description";
53
+ return (
54
+ !!nameAttr &&
55
+ nameAttr.value &&
56
+ nameAttr.value.value.toLowerCase() === "description"
57
+ );
48
58
  });
49
59
 
50
60
  if (descriptionMetaTags.length === 0) {
@@ -55,7 +65,11 @@ module.exports = {
55
65
  } else {
56
66
  descriptionMetaTags.forEach((meta) => {
57
67
  const content = NodeUtils.findAttr(meta, "content");
58
- if (!content || !content.value.trim().length) {
68
+ if (
69
+ !content ||
70
+ !content.value ||
71
+ !content.value.value.trim().length
72
+ ) {
59
73
  context.report({
60
74
  node: content || meta,
61
75
  messageId: MESSAGE_IDS.EMPTY,
@@ -1,9 +1,9 @@
1
1
  /**
2
- * @typedef {import("../types").ElementNode} ElementNode
3
2
  * @typedef {import("../types").Rule} Rule
3
+ * @typedef {import("es-html-parser").TagNode} TagNode
4
4
  */
5
5
 
6
- const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
6
+ const { RULE_CATEGORY } = require("../constants");
7
7
  const { NodeUtils } = require("./utils");
8
8
 
9
9
  const MESSAGE_IDS = {
@@ -11,6 +11,23 @@ const MESSAGE_IDS = {
11
11
  EMPTY: "empty",
12
12
  };
13
13
 
14
+ /**
15
+ * Checks whether a given node is a meta tag with viewport attribute or not.
16
+ * @param {TagNode['children'][number]} node A node to check
17
+ * @returns {node is TagNode} Return true if the given node is a meta tag with viewport attribute, otherwise false.
18
+ */
19
+ function isMetaViewport(node) {
20
+ if (node.type === "Tag" && node.name === "meta") {
21
+ const nameAttribute = NodeUtils.findAttr(node, "name");
22
+ return (
23
+ nameAttribute &&
24
+ nameAttribute.value &&
25
+ nameAttribute.value.value.toLowerCase() === "viewport"
26
+ );
27
+ }
28
+ return false;
29
+ }
30
+
14
31
  /**
15
32
  * @type {Rule}
16
33
  */
@@ -34,20 +51,14 @@ module.exports = {
34
51
  },
35
52
 
36
53
  create(context) {
37
- /**
38
- * @param {ElementNode} node
39
- * @returns {boolean}
40
- */
41
- function isMetaViewport(node) {
42
- if (node.type === NODE_TYPES.META) {
43
- const nameAttr = NodeUtils.findAttr(node, "name");
44
- return !!nameAttr && nameAttr.value.toLowerCase() === "viewport";
45
- }
46
- return false;
47
- }
48
54
  return {
49
- Head(node) {
50
- const metaViewport = (node.childNodes || []).find(isMetaViewport);
55
+ Tag(node) {
56
+ if (node.name !== "head") {
57
+ return;
58
+ }
59
+
60
+ const metaViewport = node.children.find(isMetaViewport);
61
+
51
62
  if (!metaViewport) {
52
63
  context.report({
53
64
  node,
@@ -55,15 +66,17 @@ module.exports = {
55
66
  });
56
67
  return;
57
68
  }
58
- const contentAttr = NodeUtils.findAttr(metaViewport, "content");
59
- if (!contentAttr) {
60
- context.report({
61
- node: metaViewport,
62
- messageId: MESSAGE_IDS.EMPTY,
63
- });
64
- } else if (!contentAttr.value.length) {
69
+
70
+ const contentAttribute = NodeUtils.findAttr(metaViewport, "content");
71
+ const isValueEmpty =
72
+ !contentAttribute.value || !contentAttribute.value.value.length;
73
+
74
+ if (isValueEmpty) {
75
+ const reportTarget = !contentAttribute.value
76
+ ? metaViewport
77
+ : contentAttribute;
65
78
  context.report({
66
- node: contentAttr,
79
+ node: reportTarget,
67
80
  messageId: MESSAGE_IDS.EMPTY,
68
81
  });
69
82
  }
@@ -1,15 +1,33 @@
1
1
  /**
2
2
  * @typedef {import("../types").Rule} Rule
3
+ * @typedef {import("es-html-parser").TagNode} TagNode
4
+ * @typedef {import("es-html-parser").TextNode} TextNode
3
5
  */
4
-
5
- const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
6
- const { NodeUtils } = require("./utils");
6
+ const { RULE_CATEGORY } = require("../constants");
7
7
 
8
8
  const MESSAGE_IDS = {
9
9
  MISSING_TITLE: "missing",
10
10
  EMPTY_TITLE: "empty",
11
11
  };
12
12
 
13
+ /**
14
+ * Checks whether the node is a title TagNode.
15
+ * @param {TagNode['children'][number]} node A node to check
16
+ * @returns {node is TagNode} Returns true if the given node is a title TagNode, otherwise false
17
+ */
18
+ function isTitleTagNode(node) {
19
+ return node.type === "Tag" && node.name === "title";
20
+ }
21
+
22
+ /**
23
+ * Checks whether the node is a TextNode that has value.
24
+ * @param {TagNode['children'][number]} node A node to check
25
+ * @returns {node is TextNode} Returns true if the given node is a TextNode with non-empty value, otherwise false
26
+ */
27
+ function isNonEmptyTextNode(node) {
28
+ return node.type === "Text" && node.value.trim().length > 0;
29
+ }
30
+
13
31
  /**
14
32
  * @type {Rule}
15
33
  */
@@ -33,25 +51,29 @@ module.exports = {
33
51
  },
34
52
  create(context) {
35
53
  return {
36
- Head(node) {
37
- const titleTag = (node.childNodes || []).find(
38
- (node) => node.type === NODE_TYPES.TITLE
39
- );
54
+ Tag(node) {
55
+ if (node.name !== "head") {
56
+ return;
57
+ }
58
+ const titleTag = node.children.find(isTitleTagNode);
40
59
 
41
60
  if (!titleTag) {
42
61
  context.report({
43
62
  node,
44
63
  messageId: MESSAGE_IDS.MISSING_TITLE,
45
64
  });
46
- } else if (
47
- !(titleTag.childNodes || []).some(
48
- (node) => NodeUtils.isTextNode(node) && node.value.trim().length > 0
49
- )
50
- ) {
51
- context.report({
52
- node: titleTag,
53
- messageId: MESSAGE_IDS.EMPTY_TITLE,
54
- });
65
+ return;
66
+ }
67
+
68
+ if (isTitleTagNode(titleTag)) {
69
+ const titleContentText = titleTag.children.find(isNonEmptyTextNode);
70
+
71
+ if (!titleContentText) {
72
+ context.report({
73
+ node: titleTag,
74
+ messageId: MESSAGE_IDS.EMPTY_TITLE,
75
+ });
76
+ }
55
77
  }
56
78
  },
57
79
  };
@@ -1,39 +1,40 @@
1
1
  /**
2
- * @typedef {import("../../types").ElementNode} ElementNode
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
2
+ * @typedef {import("es-html-parser").TagNode} TagNode
3
+ * @typedef {import("es-html-parser").AnyNode} AnyNode
4
+ * @typedef {import("es-html-parser").TextNode} TextNode
5
+ * @typedef {import("es-html-parser").AttributeNode} AttributeNode
6
+ * @typedef {import("../../types").LineNode} LineNode
7
+ * @typedef {import("../../types").CommentContentNode} CommentContentNode
9
8
  */
10
9
 
11
10
  module.exports = {
12
- /**
13
- * Find attribute by name in the given node
14
- * @param {ElementNode} node node
15
- * @param {string} name attribute name
16
- * @return {AttrNode | void}
11
+ /*
12
+ * @param {TagNode} node
13
+ * @param {string} name
14
+ * @returns {AttributeNode | undefined}
17
15
  */
18
16
  findAttr(node, name) {
19
- return node
20
- ? (node.attrs || []).find(
21
- (attr) => attr.name.toLowerCase() === name.toLowerCase()
22
- )
23
- : undefined;
17
+ return node.attributes.find(
18
+ (attr) => attr.key && attr.key.value.toLowerCase() === name.toLowerCase()
19
+ );
24
20
  },
25
21
  /**
26
22
  * Checks a node has attribute with the given name or not.
27
- * @param {ElementNode} node node
23
+ * @param {TagNode} node node
28
24
  * @param {string} name attribute name
29
25
  * @return {boolean} `true` if the node has a attribute, otherwise `false`.
30
26
  */
31
27
  hasAttr(node, name) {
32
- return !!node && (node.attrs || []).some((attr) => attr.name === name);
28
+ return (
29
+ !!node &&
30
+ (node.attributes || []).some(
31
+ (attr) => attr.key && attr.key.value === name
32
+ )
33
+ );
33
34
  },
34
35
  /**
35
36
  * Checks whether a node's all tokens are on the same line or not.
36
- * @param {ElementNode} node A node to check
37
+ * @param {AnyNode} node A node to check
37
38
  * @returns {boolean} `true` if a node's tokens are on the same line, otherwise `false`.
38
39
  */
39
40
  isNodeTokensOnSameLine(node) {
@@ -57,4 +58,41 @@ module.exports = {
57
58
  isCommentNode(node) {
58
59
  return !!(node && node.type === "comment");
59
60
  },
61
+
62
+ /**
63
+ *
64
+ * @param {TextNode | CommentContentNode} node
65
+ * @returns {LineNode[]}
66
+ */
67
+ splitToLineNodes(node) {
68
+ let start = node.range[0];
69
+ let line = node.loc.start.line;
70
+ const startCol = node.loc.start.column;
71
+
72
+ return node.value.split("\n").map((value, index) => {
73
+ const columnStart = index === 0 ? startCol : 0;
74
+ /**
75
+ * @type {LineNode}
76
+ */
77
+ const lineNode = {
78
+ type: "Line",
79
+ value,
80
+ range: [start, start + value.length],
81
+ loc: {
82
+ start: {
83
+ line,
84
+ column: columnStart,
85
+ },
86
+ end: {
87
+ line,
88
+ column: columnStart + value.length,
89
+ },
90
+ },
91
+ };
92
+
93
+ start += value.length + 1;
94
+ line += 1;
95
+ return lineNode;
96
+ });
97
+ },
60
98
  };