@html-eslint/eslint-plugin 0.20.0 → 0.22.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 +30 -12
  4. package/lib/rules/indent.js +7 -10
  5. package/lib/rules/index.js +8 -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 +9 -3
  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 +4 -4
  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
@@ -1,10 +1,7 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- * @typedef {import("es-html-parser").TagNode} TagNode
4
- */
5
-
1
+ const { NODE_TYPES } = require("@html-eslint/parser");
6
2
  const { RULE_CATEGORY } = require("../constants");
7
- const { NodeUtils } = require("./utils");
3
+ const { find } = require("./utils/array");
4
+ const { findAttr } = require("./utils/node");
8
5
 
9
6
  const MESSAGE_IDS = {
10
7
  MISSING: "missing",
@@ -12,14 +9,13 @@ const MESSAGE_IDS = {
12
9
  };
13
10
 
14
11
  /**
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.
12
+ * @param {ChildType<TagNode>} node
13
+ * @returns {node is TagNode}
18
14
  */
19
15
  function isMetaViewport(node) {
20
- if (node.type === "Tag" && node.name === "meta") {
21
- const nameAttribute = NodeUtils.findAttr(node, "name");
22
- return (
16
+ if (node.type === NODE_TYPES.Tag && node.name === "meta") {
17
+ const nameAttribute = findAttr(node, "name");
18
+ return !!(
23
19
  nameAttribute &&
24
20
  nameAttribute.value &&
25
21
  nameAttribute.value.value.toLowerCase() === "viewport"
@@ -57,7 +53,7 @@ module.exports = {
57
53
  return;
58
54
  }
59
55
 
60
- const metaViewport = node.children.find(isMetaViewport);
56
+ const metaViewport = find(node.children, isMetaViewport);
61
57
 
62
58
  if (!metaViewport) {
63
59
  context.report({
@@ -67,14 +63,17 @@ module.exports = {
67
63
  return;
68
64
  }
69
65
 
70
- const contentAttribute = NodeUtils.findAttr(metaViewport, "content");
71
- const isValueEmpty =
72
- !contentAttribute.value || !contentAttribute.value.value.length;
66
+ const contentAttribute = findAttr(metaViewport, "content");
67
+ const isAttributeEmpty =
68
+ !contentAttribute ||
69
+ !contentAttribute.value ||
70
+ !contentAttribute.value.value.length;
73
71
 
74
- if (isValueEmpty) {
75
- const reportTarget = !contentAttribute.value
76
- ? metaViewport
77
- : contentAttribute;
72
+ if (isAttributeEmpty) {
73
+ const reportTarget =
74
+ !contentAttribute || !contentAttribute.value
75
+ ? metaViewport
76
+ : contentAttribute;
78
77
  context.report({
79
78
  node: reportTarget,
80
79
  messageId: MESSAGE_IDS.EMPTY,
@@ -0,0 +1,135 @@
1
+ const { NODE_TYPES } = require("@html-eslint/parser");
2
+ const { RULE_CATEGORY } = require("../constants");
3
+ const { filter } = require("./utils/array");
4
+ const { findAttr } = require("./utils/node");
5
+
6
+ const MESSAGE_IDS = {
7
+ MISSING: "missing",
8
+ EMPTY: "empty",
9
+ };
10
+
11
+ const DEFAULT_REQUIRED_PROPERTIES = [
12
+ "og:title",
13
+ "og:type",
14
+ "og:url",
15
+ "og:image",
16
+ ];
17
+
18
+ /**
19
+ * @param {string[]} properties
20
+ * @returns {string[]}
21
+ */
22
+ function normalize(properties) {
23
+ return properties.map((prop) => {
24
+ if (prop.indexOf("og:") === 0) return prop;
25
+ return `og:${prop}`;
26
+ });
27
+ }
28
+
29
+ /**
30
+ * @type {Rule}
31
+ */
32
+ module.exports = {
33
+ meta: {
34
+ type: "code",
35
+
36
+ docs: {
37
+ description: 'Enforce to use `<meta name="viewport">` in `<head>`',
38
+ category: RULE_CATEGORY.SEO,
39
+ recommended: false,
40
+ },
41
+
42
+ fixable: null,
43
+ schema: [
44
+ {
45
+ type: "array",
46
+ items: {
47
+ type: "string",
48
+ },
49
+ uniqueItems: true,
50
+ },
51
+ ],
52
+ messages: {
53
+ [MESSAGE_IDS.MISSING]:
54
+ "Require use of meta tags for OGP. ({{properties}})",
55
+ [MESSAGE_IDS.EMPTY]: "Unexpected empty 'content' attribute",
56
+ },
57
+ },
58
+
59
+ create(context) {
60
+ /**
61
+ * @type {string[]}
62
+ */
63
+ const requiredProperties = normalize(
64
+ (context.options && context.options[0]) || DEFAULT_REQUIRED_PROPERTIES
65
+ );
66
+
67
+ /**
68
+ * @param {ChildType<TagNode>} node
69
+ * @returns {node is TagNode}
70
+ */
71
+ function isOgpMeta(node) {
72
+ const isMeta = node.type === NODE_TYPES.Tag && node.name === "meta";
73
+ const property = isMeta ? findAttr(node, "property") : undefined;
74
+ const hasOgProperty =
75
+ !!property &&
76
+ !!property.value &&
77
+ property.value.value.indexOf("og:") === 0;
78
+ return hasOgProperty;
79
+ }
80
+
81
+ return {
82
+ Tag(node) {
83
+ if (node.name !== "head") {
84
+ return;
85
+ }
86
+ const children = node.children;
87
+
88
+ const metaTags = filter(children, isOgpMeta);
89
+
90
+ const missingProperties = requiredProperties.filter((required) => {
91
+ return !metaTags.some((meta) => {
92
+ const property = findAttr(meta, "property");
93
+ if (property && property.value) {
94
+ return property.value.value === required;
95
+ }
96
+ return false;
97
+ });
98
+ });
99
+
100
+ const emptyContentMetaTags = metaTags.filter((meta) => {
101
+ const property = findAttr(meta, "property");
102
+ if (
103
+ property &&
104
+ property.value &&
105
+ requiredProperties.includes(property.value.value)
106
+ ) {
107
+ const content = findAttr(meta, "content");
108
+ return !content || !content.value || !content.value.value;
109
+ }
110
+ return false;
111
+ });
112
+
113
+ if (missingProperties.length) {
114
+ context.report({
115
+ node,
116
+ data: {
117
+ properties: missingProperties.join(", "),
118
+ },
119
+ messageId: MESSAGE_IDS.MISSING,
120
+ });
121
+ }
122
+
123
+ if (emptyContentMetaTags.length) {
124
+ emptyContentMetaTags.forEach((meta) => {
125
+ const content = findAttr(meta, "content");
126
+ context.report({
127
+ node: content || meta,
128
+ messageId: MESSAGE_IDS.EMPTY,
129
+ });
130
+ });
131
+ }
132
+ },
133
+ };
134
+ },
135
+ };
@@ -1,9 +1,6 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- * @typedef {import("es-html-parser").TagNode} TagNode
4
- * @typedef {import("es-html-parser").TextNode} TextNode
5
- */
1
+ const { NODE_TYPES } = require("@html-eslint/parser");
6
2
  const { RULE_CATEGORY } = require("../constants");
3
+ const { find } = require("./utils/array");
7
4
 
8
5
  const MESSAGE_IDS = {
9
6
  MISSING_TITLE: "missing",
@@ -11,21 +8,19 @@ const MESSAGE_IDS = {
11
8
  };
12
9
 
13
10
  /**
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
11
+ * @param {ChildType<TagNode>} node
12
+ * @returns {node is TagNode}
17
13
  */
18
- function isTitleTagNode(node) {
19
- return node.type === "Tag" && node.name === "title";
14
+ function isTitle(node) {
15
+ return node.type === NODE_TYPES.Tag && node.name === "title";
20
16
  }
21
17
 
22
18
  /**
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
19
+ * @param {ChildType<TagNode>} node
20
+ * @returns {node is TextNode}
26
21
  */
27
- function isNonEmptyTextNode(node) {
28
- return node.type === "Text" && node.value.trim().length > 0;
22
+ function isNonEmptyText(node) {
23
+ return node.type === NODE_TYPES.Text && node.value.trim().length > 0;
29
24
  }
30
25
 
31
26
  /**
@@ -55,9 +50,10 @@ module.exports = {
55
50
  if (node.name !== "head") {
56
51
  return;
57
52
  }
58
- const titleTag = node.children.find(isTitleTagNode);
59
53
 
60
- if (!titleTag) {
54
+ const title = find(node.children, isTitle);
55
+
56
+ if (!title) {
61
57
  context.report({
62
58
  node,
63
59
  messageId: MESSAGE_IDS.MISSING_TITLE,
@@ -65,15 +61,13 @@ module.exports = {
65
61
  return;
66
62
  }
67
63
 
68
- if (isTitleTagNode(titleTag)) {
69
- const titleContentText = titleTag.children.find(isNonEmptyTextNode);
64
+ const content = find(title.children, isNonEmptyText);
70
65
 
71
- if (!titleContentText) {
72
- context.report({
73
- node: titleTag,
74
- messageId: MESSAGE_IDS.EMPTY_TITLE,
75
- });
76
- }
66
+ if (!content) {
67
+ context.report({
68
+ node: title,
69
+ messageId: MESSAGE_IDS.EMPTY_TITLE,
70
+ });
77
71
  }
78
72
  },
79
73
  };
@@ -0,0 +1,158 @@
1
+ const { RULE_CATEGORY } = require("../constants");
2
+
3
+ const MESSAGE_IDS = {
4
+ UNSORTED: "unsorted",
5
+ };
6
+
7
+ /**
8
+ * @type {Rule}
9
+ */
10
+ module.exports = {
11
+ meta: {
12
+ type: "code",
13
+
14
+ docs: {
15
+ description: "Enforce attributes alphabetical sorting",
16
+ category: RULE_CATEGORY.STYLE,
17
+ recommended: false,
18
+ },
19
+ fixable: "code",
20
+ schema: [
21
+ {
22
+ type: "object",
23
+ properties: {
24
+ priority: {
25
+ type: "array",
26
+ items: {
27
+ type: "string",
28
+ uniqueItems: true,
29
+ },
30
+ },
31
+ },
32
+ },
33
+ ],
34
+ messages: {
35
+ [MESSAGE_IDS.UNSORTED]: "Attributes should be sorted alphabetically",
36
+ },
37
+ },
38
+ create(context) {
39
+ const sourceCode = context.getSourceCode();
40
+ const option = context.options[0] || {
41
+ priority: ["id", "type", "class", "style"],
42
+ };
43
+ /**
44
+ * @type {string[]}
45
+ */
46
+ const priority = option.priority;
47
+
48
+ /**
49
+ * @param {AttributeNode} attrA
50
+ * @param {AttributeNode} attrB
51
+ * @return {number}
52
+ */
53
+ function compare(attrA, attrB) {
54
+ const keyA = attrA.key.value;
55
+ const keyB = attrB.key.value;
56
+ const keyAReservedValue = priority.indexOf(keyA);
57
+ const keyBReservedValue = priority.indexOf(keyB);
58
+ if (keyAReservedValue >= 0 && keyBReservedValue >= 0) {
59
+ return keyAReservedValue - keyBReservedValue;
60
+ } else if (keyAReservedValue >= 0) {
61
+ return -1;
62
+ } else if (keyBReservedValue >= 0) {
63
+ return 1;
64
+ }
65
+ return keyA.localeCompare(keyB);
66
+ }
67
+
68
+ /**
69
+ * @param {string} source
70
+ * @param {AttributeNode[]} unsorted
71
+ * @param {AttributeNode[]} sorted
72
+ * @returns {string}
73
+ */
74
+ function getSortedCode(source, unsorted, sorted) {
75
+ let result = "";
76
+ unsorted.forEach((unsortedAttr, index) => {
77
+ const sortedAttr = sorted[index];
78
+ result += source.slice(sortedAttr.range[0], sortedAttr.range[1]);
79
+
80
+ const nextUnsortedAttr = unsorted[index + 1];
81
+ if (nextUnsortedAttr) {
82
+ result += source.slice(
83
+ unsortedAttr.range[1],
84
+ nextUnsortedAttr.range[0]
85
+ );
86
+ }
87
+ });
88
+ return result;
89
+ }
90
+
91
+ /**
92
+ * @param {RuleFixer} fixer
93
+ * @param {AttributeNode[]} unsorted
94
+ * @param {AttributeNode[]} sorted
95
+ */
96
+ function fix(fixer, unsorted, sorted) {
97
+ const source = sourceCode.getText();
98
+ return fixer.replaceTextRange(
99
+ [unsorted[0].range[0], unsorted[unsorted.length - 1].range[1]],
100
+ getSortedCode(source, unsorted, sorted)
101
+ );
102
+ }
103
+
104
+ /**
105
+ * @param {AttributeNode[]} before
106
+ * @param {AttributeNode[]} after
107
+ * @returns {boolean}
108
+ */
109
+ function isChanged(before, after) {
110
+ for (let i = 0; i < before.length; i++) {
111
+ if (before[i] !== after[i]) return true;
112
+ }
113
+ return false;
114
+ }
115
+
116
+ /**
117
+ * @param {AttributeNode[]} unsorted
118
+ */
119
+ function checkSorting(unsorted) {
120
+ if (unsorted.length <= 1) {
121
+ return;
122
+ }
123
+
124
+ const sorted = [...unsorted].sort(compare);
125
+
126
+ if (!isChanged(unsorted, sorted)) {
127
+ return;
128
+ }
129
+ const first = unsorted[0];
130
+ const last = unsorted[unsorted.length - 1];
131
+ context.report({
132
+ node: {
133
+ range: [first.range[0], last.range[1]],
134
+ loc: {
135
+ start: first.loc.start,
136
+ end: last.loc.end,
137
+ },
138
+ },
139
+ messageId: MESSAGE_IDS.UNSORTED,
140
+ fix(fixer) {
141
+ return fix(fixer, unsorted, sorted);
142
+ },
143
+ });
144
+ }
145
+
146
+ return {
147
+ ScriptTag(node) {
148
+ checkSorting(node.attributes);
149
+ },
150
+ Tag(node) {
151
+ checkSorting(node.attributes);
152
+ },
153
+ StyleTag(node) {
154
+ checkSorting(node.attributes);
155
+ },
156
+ };
157
+ },
158
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @template T
3
+ * @template {T} S
4
+ * @param {T[]} items
5
+ * @param {(node: T) => node is S} predicate
6
+ * @returns {S | undefined}
7
+ */
8
+ function find(items, predicate) {
9
+ return items.find(predicate);
10
+ }
11
+
12
+ /**
13
+ * @template T
14
+ * @template {T} S
15
+ * @param {T[]} items
16
+ * @param {(node: T) => node is S} predicate
17
+ * @returns {S[]}
18
+ */
19
+ function filter(items, predicate) {
20
+ return items.filter(predicate);
21
+ }
22
+
23
+ module.exports = {
24
+ find,
25
+ filter,
26
+ };
@@ -0,0 +1,70 @@
1
+ module.exports = {
2
+ /**
3
+ * @param {TagNode | ScriptTagNode | StyleTagNode} node
4
+ * @param {string} key
5
+ * @returns {AttributeNode | undefined}
6
+ */
7
+ findAttr(node, key) {
8
+ return node.attributes.find(
9
+ (attr) => attr.key && attr.key.value.toLowerCase() === key.toLowerCase()
10
+ );
11
+ },
12
+
13
+ /**
14
+ * Checks whether a node's all tokens are on the same line or not.
15
+ * @param {AnyNode} node A node to check
16
+ * @returns {boolean} `true` if a node's tokens are on the same line, otherwise `false`.
17
+ */
18
+ isNodeTokensOnSameLine(node) {
19
+ return node.loc.start.line === node.loc.end.line;
20
+ },
21
+
22
+ /**
23
+ *
24
+ * @param {TextNode | CommentContentNode} node
25
+ * @returns {LineNode[]}
26
+ */
27
+ splitToLineNodes(node) {
28
+ let start = node.range[0];
29
+ let line = node.loc.start.line;
30
+ const startCol = node.loc.start.column;
31
+
32
+ return node.value.split("\n").map((value, index) => {
33
+ const columnStart = index === 0 ? startCol : 0;
34
+ /**
35
+ * @type {LineNode}
36
+ */
37
+ const lineNode = {
38
+ type: "Line",
39
+ value,
40
+ range: [start, start + value.length],
41
+ loc: {
42
+ start: {
43
+ line,
44
+ column: columnStart,
45
+ },
46
+ end: {
47
+ line,
48
+ column: columnStart + value.length,
49
+ },
50
+ },
51
+ };
52
+
53
+ start += value.length + 1;
54
+ line += 1;
55
+ return lineNode;
56
+ });
57
+ },
58
+ /**
59
+ * Get location between two nodes.
60
+ * @param {BaseNode} before A node placed in before
61
+ * @param {BaseNode} after A node placed in after
62
+ * @returns {Location} location between two nodes.
63
+ */
64
+ getLocBetween(before, after) {
65
+ return {
66
+ start: before.loc.end,
67
+ end: after.loc.start,
68
+ };
69
+ },
70
+ };