@plumeria/eslint-plugin 18.2.9 → 18.2.10
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/README.md
CHANGED
|
@@ -112,6 +112,27 @@ properties in a vertical one, which is per element and cannot be read from the
|
|
|
112
112
|
source. The rule reads them as one property; disable the line where an element
|
|
113
113
|
is known to be vertical.
|
|
114
114
|
|
|
115
|
+
### expand-border-shorthands
|
|
116
|
+
|
|
117
|
+
Expands a border shorthand that bundles a width, a style and a color —
|
|
118
|
+
`border`, `borderBlock`, `borderInline`, and the eight edge forms — into the
|
|
119
|
+
three declarations it stands for. Those bundles are the only properties left
|
|
120
|
+
that cross an axis shorthand without either containing the other, so expanding
|
|
121
|
+
them turns the last unrankable pairs into ordinary shorthand-to-longhand ones.
|
|
122
|
+
|
|
123
|
+
Fixable. A value it cannot split, such as `var(--edge)` or `inherit`, is
|
|
124
|
+
reported without a fix: leaving it silent would let the expanded declarations
|
|
125
|
+
elsewhere outrank it. Not part of `recommended`.
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
borderTop: '1px solid red'
|
|
129
|
+
// becomes
|
|
130
|
+
borderTopWidth: '1px', borderTopStyle: 'solid', borderTopColor: 'red'
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A shorthand resets what it omits, so `borderBlock: 'solid'` expands with
|
|
134
|
+
`medium` and `currentcolor` written out.
|
|
135
|
+
|
|
115
136
|
### no-physical-properties / no-logical-properties
|
|
116
137
|
|
|
117
138
|
Disallow one of the two names a property can carry, so a project writes edges
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const props_require_import_1 = require("./rules/props-require-import");
|
|
3
|
+
const expand_border_shorthands_1 = require("./rules/expand-border-shorthands");
|
|
3
4
|
const no_combinator_1 = require("./rules/no-combinator");
|
|
4
5
|
const no_destructure_1 = require("./rules/no-destructure");
|
|
5
6
|
const no_inline_object_1 = require("./rules/no-inline-object");
|
|
@@ -17,6 +18,7 @@ const validate_values_1 = require("./rules/validate-values");
|
|
|
17
18
|
const validate_pseudos_1 = require("./rules/validate-pseudos");
|
|
18
19
|
const rules = {
|
|
19
20
|
'props-require-import': props_require_import_1.propsRequireImport,
|
|
21
|
+
'expand-border-shorthands': expand_border_shorthands_1.expandBorderShorthands,
|
|
20
22
|
'no-combinator': no_combinator_1.noCombinator,
|
|
21
23
|
'no-destructure': no_destructure_1.noDestructure,
|
|
22
24
|
'no-inline-object': no_inline_object_1.noInlineObject,
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.expandBorderShorthands = void 0;
|
|
4
|
+
const borderShorthand_1 = require("../util/borderShorthand");
|
|
5
|
+
const logicalPhysical_1 = require("../util/logicalPhysical");
|
|
6
|
+
const BUNDLES = new Set(borderShorthand_1.BORDER_BUNDLES);
|
|
7
|
+
exports.expandBorderShorthands = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Expand a border shorthand that bundles width, style and color into the three declarations it sets',
|
|
12
|
+
},
|
|
13
|
+
fixable: 'code',
|
|
14
|
+
messages: {
|
|
15
|
+
expand: "'{{ name }}' sets a width, a style and a color at once. It crosses the axis shorthands without either containing the other, so write the three declarations it stands for.",
|
|
16
|
+
opaque: "'{{ name }}' sets a width, a style and a color at once, and this value cannot be split. Write the three declarations yourself, or the ones this rule expands elsewhere will outrank it.",
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
const plumeriaAliases = {};
|
|
22
|
+
const sourceCode = context.sourceCode;
|
|
23
|
+
return {
|
|
24
|
+
ImportDeclaration(node) {
|
|
25
|
+
if (node.source.value === '@plumeria/core') {
|
|
26
|
+
node.specifiers.forEach((specifier) => {
|
|
27
|
+
if (specifier.type === 'ImportNamespaceSpecifier' ||
|
|
28
|
+
specifier.type === 'ImportDefaultSpecifier') {
|
|
29
|
+
plumeriaAliases[specifier.local.name] = 'NAMESPACE';
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const spec = specifier;
|
|
33
|
+
const importedName = spec.imported.type === 'Identifier'
|
|
34
|
+
? spec.imported.name
|
|
35
|
+
: String(spec.imported.value);
|
|
36
|
+
plumeriaAliases[specifier.local.name] = importedName;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
CallExpression(node) {
|
|
42
|
+
let isCssProperties = false;
|
|
43
|
+
if (node.callee.type === 'MemberExpression') {
|
|
44
|
+
if (node.callee.object.type === 'Identifier' &&
|
|
45
|
+
plumeriaAliases[node.callee.object.name] === 'NAMESPACE') {
|
|
46
|
+
const propertyName = node.callee.property.type === 'Identifier'
|
|
47
|
+
? node.callee.property.name
|
|
48
|
+
: null;
|
|
49
|
+
if (propertyName === 'create' ||
|
|
50
|
+
propertyName === 'keyframes' ||
|
|
51
|
+
propertyName === 'viewTransition') {
|
|
52
|
+
isCssProperties = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (node.callee.type === 'Identifier') {
|
|
57
|
+
const aliasName = plumeriaAliases[node.callee.name];
|
|
58
|
+
if (aliasName === 'create' ||
|
|
59
|
+
aliasName === 'keyframes' ||
|
|
60
|
+
aliasName === 'viewTransition') {
|
|
61
|
+
isCssProperties = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (isCssProperties) {
|
|
65
|
+
node.arguments.forEach((arg) => {
|
|
66
|
+
if (arg.type === 'ObjectExpression') {
|
|
67
|
+
arg.properties.forEach((prop) => {
|
|
68
|
+
if (prop.type === 'Property' &&
|
|
69
|
+
prop.value.type === 'ObjectExpression') {
|
|
70
|
+
checkStyleObject(prop.value);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
function checkStyleObject(node) {
|
|
79
|
+
node.properties.forEach((prop) => {
|
|
80
|
+
if (prop.type !== 'Property')
|
|
81
|
+
return;
|
|
82
|
+
if (prop.value.type === 'ObjectExpression') {
|
|
83
|
+
checkStyleObject(prop.value);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let name = '';
|
|
87
|
+
if (!prop.computed) {
|
|
88
|
+
name =
|
|
89
|
+
prop.key.type === 'Identifier'
|
|
90
|
+
? prop.key.name
|
|
91
|
+
: String(prop.key.value);
|
|
92
|
+
}
|
|
93
|
+
else if (prop.key.type === 'Literal' &&
|
|
94
|
+
typeof prop.key.value === 'string') {
|
|
95
|
+
name = prop.key.value;
|
|
96
|
+
}
|
|
97
|
+
const kebab = (0, logicalPhysical_1.toKebabCase)(name);
|
|
98
|
+
if (!BUNDLES.has(kebab))
|
|
99
|
+
return;
|
|
100
|
+
const literal = prop.value.type === 'Literal' && typeof prop.value.value === 'string'
|
|
101
|
+
? prop.value.value
|
|
102
|
+
: null;
|
|
103
|
+
const parts = literal === null ? null : (0, borderShorthand_1.splitBorderValue)(literal);
|
|
104
|
+
if (!parts) {
|
|
105
|
+
context.report({
|
|
106
|
+
node: prop.key,
|
|
107
|
+
messageId: 'opaque',
|
|
108
|
+
data: { name },
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const quote = sourceCode.getText(prop.value).trim().startsWith('"')
|
|
113
|
+
? '"'
|
|
114
|
+
: "'";
|
|
115
|
+
const indent = ' '.repeat(prop.loc.start.column);
|
|
116
|
+
const declarations = ['width', 'style', 'color']
|
|
117
|
+
.map((part) => `${(0, logicalPhysical_1.toCamelCase)(`${kebab}-${part}`)}: ${quote}${parts[part]}${quote}`)
|
|
118
|
+
.join(`,\n${indent}`);
|
|
119
|
+
context.report({
|
|
120
|
+
node: prop.key,
|
|
121
|
+
messageId: 'expand',
|
|
122
|
+
data: { name },
|
|
123
|
+
fix: (fixer) => fixer.replaceText(prop, declarations),
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.splitBorderValue = exports.BORDER_BUNDLES = void 0;
|
|
4
|
+
exports.BORDER_BUNDLES = [
|
|
5
|
+
'border',
|
|
6
|
+
'border-block',
|
|
7
|
+
'border-inline',
|
|
8
|
+
'border-top',
|
|
9
|
+
'border-bottom',
|
|
10
|
+
'border-left',
|
|
11
|
+
'border-right',
|
|
12
|
+
'border-block-start',
|
|
13
|
+
'border-block-end',
|
|
14
|
+
'border-inline-start',
|
|
15
|
+
'border-inline-end',
|
|
16
|
+
];
|
|
17
|
+
const STYLES = new Set([
|
|
18
|
+
'none',
|
|
19
|
+
'hidden',
|
|
20
|
+
'dotted',
|
|
21
|
+
'dashed',
|
|
22
|
+
'solid',
|
|
23
|
+
'double',
|
|
24
|
+
'groove',
|
|
25
|
+
'ridge',
|
|
26
|
+
'inset',
|
|
27
|
+
'outset',
|
|
28
|
+
]);
|
|
29
|
+
const WIDTH_KEYWORDS = new Set(['thin', 'medium', 'thick']);
|
|
30
|
+
const LENGTH = /^[+-]?(\d+\.?\d*|\.\d+)([a-z%]+)?$/i;
|
|
31
|
+
const INITIAL = { width: 'medium', style: 'none', color: 'currentcolor' };
|
|
32
|
+
const tokenize = (value) => {
|
|
33
|
+
const tokens = [];
|
|
34
|
+
let depth = 0;
|
|
35
|
+
let current = '';
|
|
36
|
+
for (const character of value.trim()) {
|
|
37
|
+
if (character === '(')
|
|
38
|
+
depth++;
|
|
39
|
+
if (character === ')')
|
|
40
|
+
depth--;
|
|
41
|
+
if (depth === 0 && /\s/.test(character)) {
|
|
42
|
+
if (current)
|
|
43
|
+
tokens.push(current);
|
|
44
|
+
current = '';
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
current += character;
|
|
48
|
+
}
|
|
49
|
+
if (current)
|
|
50
|
+
tokens.push(current);
|
|
51
|
+
return depth === 0 && tokens.length > 0 && tokens.length <= 3 ? tokens : null;
|
|
52
|
+
};
|
|
53
|
+
const isWidth = (token) => WIDTH_KEYWORDS.has(token) || LENGTH.test(token) || /^calc\(/i.test(token);
|
|
54
|
+
const isColor = (token) => /^(#|rgb|hsl|hwb|lab|lch|oklab|oklch|color\(|light-dark\()/i.test(token) ||
|
|
55
|
+
/^[a-z-]+$/i.test(token);
|
|
56
|
+
const splitBorderValue = (value) => {
|
|
57
|
+
const tokens = tokenize(value);
|
|
58
|
+
if (!tokens)
|
|
59
|
+
return null;
|
|
60
|
+
if (tokens.some((token) => /\bvar\(|^inherit$|^initial$|^unset$|^revert/i.test(token)))
|
|
61
|
+
return null;
|
|
62
|
+
const parts = {};
|
|
63
|
+
for (const token of tokens) {
|
|
64
|
+
const lower = token.toLowerCase();
|
|
65
|
+
if (!parts.style && STYLES.has(lower)) {
|
|
66
|
+
parts.style = token;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!parts.width && isWidth(lower)) {
|
|
70
|
+
parts.width = token;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!parts.color && isColor(token)) {
|
|
74
|
+
parts.color = token;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
width: parts.width ?? INITIAL.width,
|
|
81
|
+
style: parts.style ?? INITIAL.style,
|
|
82
|
+
color: parts.color ?? INITIAL.color,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
exports.splitBorderValue = splitBorderValue;
|