@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.
- package/lib/constants/index.js +0 -2
- package/lib/rules/element-newline.js +30 -11
- package/lib/rules/id-naming-convention.js +17 -11
- package/lib/rules/indent.js +7 -10
- package/lib/rules/index.js +10 -0
- package/lib/rules/lowercase.js +93 -0
- package/lib/rules/no-abstract-roles.js +15 -13
- package/lib/rules/no-accesskey-attrs.js +5 -6
- package/lib/rules/no-aria-hidden-body.js +2 -6
- package/lib/rules/no-duplicate-attrs.js +3 -4
- package/lib/rules/no-duplicate-id.js +7 -7
- package/lib/rules/no-extra-spacing-attrs.js +29 -9
- package/lib/rules/no-inline-styles.js +5 -2
- package/lib/rules/no-multiple-empty-lines.js +3 -4
- package/lib/rules/no-multiple-h1.js +3 -4
- package/lib/rules/no-non-scalable-viewport.js +3 -7
- package/lib/rules/no-obsolete-tags.js +4 -2
- package/lib/rules/no-positive-tabindex.js +5 -6
- package/lib/rules/no-restricted-attr-values.js +147 -0
- package/lib/rules/no-restricted-attrs.js +13 -2
- package/lib/rules/no-script-style-type.js +69 -0
- package/lib/rules/no-skip-heading-levels.js +4 -5
- package/lib/rules/no-target-blank.js +5 -9
- package/lib/rules/no-trailing-spaces.js +0 -4
- package/lib/rules/quotes.js +24 -1
- package/lib/rules/require-attrs.js +18 -7
- package/lib/rules/require-button-type.js +2 -6
- package/lib/rules/require-closing-tags.js +8 -5
- package/lib/rules/require-doctype.js +4 -5
- package/lib/rules/require-frame-title.js +5 -2
- package/lib/rules/require-img-alt.js +10 -0
- package/lib/rules/require-lang.js +5 -6
- package/lib/rules/require-li-container.js +9 -2
- package/lib/rules/require-meta-charset.js +19 -13
- package/lib/rules/require-meta-description.js +9 -12
- package/lib/rules/require-meta-viewport.js +19 -20
- package/lib/rules/require-open-graph-protocol.js +135 -0
- package/lib/rules/require-title.js +19 -25
- package/lib/rules/sort-attrs.js +158 -0
- package/lib/rules/utils/array.js +26 -0
- package/lib/rules/utils/node.js +70 -0
- package/lib/types.d.ts +262 -246
- package/package.json +6 -5
- package/lib/constants/node-types.js +0 -13
- package/lib/rules/utils/index.js +0 -7
- package/lib/rules/utils/node-utils.js +0 -112
- /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 {
|
|
3
|
+
const { filter } = require("./utils/array");
|
|
4
|
+
const { findAttr } = require("./utils/node");
|
|
8
5
|
|
|
9
6
|
const MESSAGE_IDS = {
|
|
10
7
|
MISSING: "missing",
|
|
@@ -12,11 +9,11 @@ const MESSAGE_IDS = {
|
|
|
12
9
|
};
|
|
13
10
|
|
|
14
11
|
/**
|
|
15
|
-
* @param {TagNode
|
|
16
|
-
* @returns {
|
|
12
|
+
* @param {ChildType<TagNode>} node
|
|
13
|
+
* @returns {node is TagNode}
|
|
17
14
|
*/
|
|
18
15
|
function isMetaTagNode(node) {
|
|
19
|
-
return node.type ===
|
|
16
|
+
return node.type === NODE_TYPES.Tag && node.name === "meta";
|
|
20
17
|
}
|
|
21
18
|
|
|
22
19
|
/**
|
|
@@ -46,10 +43,10 @@ module.exports = {
|
|
|
46
43
|
if (node.name !== "head") {
|
|
47
44
|
return;
|
|
48
45
|
}
|
|
49
|
-
const metaTags = node.children
|
|
46
|
+
const metaTags = filter(node.children, isMetaTagNode);
|
|
50
47
|
|
|
51
48
|
const descriptionMetaTags = metaTags.filter((meta) => {
|
|
52
|
-
const nameAttr =
|
|
49
|
+
const nameAttr = findAttr(meta, "name");
|
|
53
50
|
return (
|
|
54
51
|
!!nameAttr &&
|
|
55
52
|
nameAttr.value &&
|
|
@@ -64,7 +61,7 @@ module.exports = {
|
|
|
64
61
|
});
|
|
65
62
|
} else {
|
|
66
63
|
descriptionMetaTags.forEach((meta) => {
|
|
67
|
-
const content =
|
|
64
|
+
const content = findAttr(meta, "content");
|
|
68
65
|
if (
|
|
69
66
|
!content ||
|
|
70
67
|
!content.value ||
|
|
@@ -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 {
|
|
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
|
-
*
|
|
16
|
-
* @
|
|
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 ===
|
|
21
|
-
const nameAttribute =
|
|
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
|
|
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 =
|
|
71
|
-
const
|
|
72
|
-
!contentAttribute
|
|
66
|
+
const contentAttribute = findAttr(metaViewport, "content");
|
|
67
|
+
const isAttributeEmpty =
|
|
68
|
+
!contentAttribute ||
|
|
69
|
+
!contentAttribute.value ||
|
|
70
|
+
!contentAttribute.value.value.length;
|
|
73
71
|
|
|
74
|
-
if (
|
|
75
|
-
const reportTarget =
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
*
|
|
15
|
-
* @
|
|
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
|
|
19
|
-
return node.type ===
|
|
14
|
+
function isTitle(node) {
|
|
15
|
+
return node.type === NODE_TYPES.Tag && node.name === "title";
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
/**
|
|
23
|
-
*
|
|
24
|
-
* @
|
|
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
|
|
28
|
-
return node.type ===
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
const titleContentText = titleTag.children.find(isNonEmptyTextNode);
|
|
64
|
+
const content = find(title.children, isNonEmptyText);
|
|
70
65
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
};
|