postcss-minify-gradients 9.0.2 → 9.0.4
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/package.json +5 -4
- package/src/index.js +325 -302
- package/src/isKnownColor.js +79 -0
- package/types/index.d.ts +25 -5
- package/types/index.d.ts.map +1 -1
- package/types/isKnownColor.d.ts +12 -0
- package/types/isKnownColor.d.ts.map +1 -0
- package/src/isColorStop.js +0 -77
- package/types/isColorStop.d.ts +0 -8
- package/types/isColorStop.d.ts.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postcss-minify-gradients",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.4",
|
|
4
4
|
"description": "Minify gradient parameters with PostCSS.",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"url": "git+https://github.com/cssnano/cssnano.git"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@colordx/core": "^
|
|
37
|
-
"
|
|
38
|
-
"
|
|
36
|
+
"@colordx/core": "^7.2.0",
|
|
37
|
+
"@csstools/css-tokenizer": "^4.0.2",
|
|
38
|
+
"cssnano-utils": "^8.0.0"
|
|
39
39
|
},
|
|
40
40
|
"bugs": {
|
|
41
41
|
"url": "https://github.com/cssnano/cssnano/issues"
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"type": "module",
|
|
53
53
|
"scripts": {
|
|
54
|
+
"fuzz": "node ./script/fuzz.js",
|
|
54
55
|
"test": "node --test"
|
|
55
56
|
}
|
|
56
57
|
}
|
package/src/index.js
CHANGED
|
@@ -1,376 +1,401 @@
|
|
|
1
|
-
import
|
|
1
|
+
/** @import {CSSToken} from '@csstools/css-tokenizer' */
|
|
2
2
|
import cssnanoUtils from 'cssnano-utils';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
|
|
3
|
+
import isKnownColor from './isKnownColor.js';
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
TokenType,
|
|
7
|
+
applyEdits,
|
|
8
|
+
asciiLowerCase,
|
|
9
|
+
decoded,
|
|
10
|
+
lengthUnits,
|
|
11
|
+
numeric,
|
|
12
|
+
tokenEnd,
|
|
13
|
+
} = cssnanoUtils;
|
|
14
|
+
/** @type {typeof cssnanoUtils.balancedTokens} */
|
|
15
|
+
const balancedTokens = cssnanoUtils.balancedTokens;
|
|
16
|
+
|
|
17
|
+
const directions = new Map([
|
|
7
18
|
['top', '0deg'],
|
|
8
19
|
['right', '90deg'],
|
|
9
20
|
['bottom', '180deg'],
|
|
10
21
|
['left', '270deg'],
|
|
11
22
|
]);
|
|
23
|
+
const gradientNames = new Set([
|
|
24
|
+
'linear-gradient',
|
|
25
|
+
'repeating-linear-gradient',
|
|
26
|
+
'-webkit-linear-gradient',
|
|
27
|
+
'-webkit-repeating-linear-gradient',
|
|
28
|
+
'radial-gradient',
|
|
29
|
+
'repeating-radial-gradient',
|
|
30
|
+
'conic-gradient',
|
|
31
|
+
'repeating-conic-gradient',
|
|
32
|
+
'-webkit-radial-gradient',
|
|
33
|
+
'-webkit-repeating-radial-gradient',
|
|
34
|
+
]);
|
|
35
|
+
const variableFunctions = new Set(['var', 'env']);
|
|
36
|
+
// Only these functions can compute a position, so any other leading function
|
|
37
|
+
// such as `rgb()` or `var()` cannot be one.
|
|
38
|
+
const positionFunctions = new Set(['calc', 'clamp', 'max', 'min']);
|
|
39
|
+
const gradientSourceRegex = /[gG][rR][aA][dD][iI][eE][nN][tT]/v;
|
|
40
|
+
// Identifiers that may start a gradient line, shape or size specification, so
|
|
41
|
+
// the argument carrying one is never a colour stop slot.
|
|
42
|
+
const lineKeywords = new Set([
|
|
43
|
+
'at',
|
|
44
|
+
'bottom',
|
|
45
|
+
'center',
|
|
46
|
+
'circle',
|
|
47
|
+
'closest-corner',
|
|
48
|
+
'closest-side',
|
|
49
|
+
'ellipse',
|
|
50
|
+
'farthest-corner',
|
|
51
|
+
'farthest-side',
|
|
52
|
+
'from',
|
|
53
|
+
'in',
|
|
54
|
+
'left',
|
|
55
|
+
'right',
|
|
56
|
+
'to',
|
|
57
|
+
'top',
|
|
58
|
+
]);
|
|
12
59
|
|
|
60
|
+
/** @typedef {{start: number, end: number, text: string}} SourceEdit */
|
|
13
61
|
/**
|
|
14
|
-
*
|
|
62
|
+
* A gradient stop before fixup: the argument it came from, the extent of its
|
|
63
|
+
* colour, and the position tokens following that colour.
|
|
15
64
|
*
|
|
16
|
-
* @
|
|
17
|
-
* @
|
|
18
|
-
* @
|
|
65
|
+
* @typedef {object} ColorStop
|
|
66
|
+
* @property {number} argIndex Comma-separated argument the stop came from.
|
|
67
|
+
* @property {number} colorEnd Offset just past the colour in the declaration value.
|
|
68
|
+
* @property {readonly number[]} position Positions following the colour.
|
|
19
69
|
*/
|
|
20
|
-
function isLessThan(a, b) {
|
|
21
|
-
return (
|
|
22
|
-
a.unit.toLowerCase() === b.unit.toLowerCase() &&
|
|
23
|
-
Number.parseFloat(a.number) >= Number.parseFloat(b.number)
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
70
|
/**
|
|
28
|
-
*
|
|
71
|
+
* A position as a number and its unit, as reported by `numeric()`.
|
|
29
72
|
*
|
|
30
|
-
* @
|
|
31
|
-
* @param {valueParser.Dimension} b
|
|
32
|
-
* @returns {boolean | undefined}
|
|
73
|
+
* @typedef {{number: number, unit: string}} PositionValue
|
|
33
74
|
*/
|
|
34
|
-
function isLessThanOrEqual(a, b) {
|
|
35
|
-
if (a.unit.toLowerCase() === b.unit.toLowerCase()) {
|
|
36
|
-
return isLessThan(a, b);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const aNumber = Number.parseFloat(a.number);
|
|
40
|
-
const bNumber = Number.parseFloat(b.number);
|
|
41
|
-
|
|
42
|
-
if (aNumber === 0) {
|
|
43
|
-
return bNumber <= 0;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (bNumber === 0) {
|
|
47
|
-
return aNumber >= 0;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
75
|
|
|
51
76
|
/**
|
|
52
|
-
*
|
|
77
|
+
* Significant tokens of a range, skipping whitespace, comments and the contents
|
|
78
|
+
* of any balanced frame the range opens.
|
|
53
79
|
*
|
|
54
|
-
* @param {
|
|
55
|
-
* @
|
|
80
|
+
* @param {readonly CSSToken[]} input
|
|
81
|
+
* @param {NonNullable<ReturnType<typeof balancedTokens>>} structure
|
|
82
|
+
* @param {{startIndex: number, endIndex: number}} range
|
|
83
|
+
* @return {number[]} Indexes of the significant tokens in `input`.
|
|
56
84
|
*/
|
|
57
|
-
function
|
|
58
|
-
|
|
85
|
+
function significant(input, structure, range) {
|
|
86
|
+
const result = [];
|
|
87
|
+
for (let index = range.startIndex; index < range.endIndex; index++) {
|
|
88
|
+
const token = input[index];
|
|
89
|
+
if (token[0] !== TokenType.Whitespace && token[0] !== TokenType.Comment) {
|
|
90
|
+
result.push(index);
|
|
91
|
+
}
|
|
92
|
+
const frameEnd = structure.endForOpening(index);
|
|
93
|
+
if (frameEnd !== undefined) index = frameEnd;
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
59
96
|
}
|
|
60
97
|
|
|
61
98
|
/**
|
|
62
|
-
*
|
|
99
|
+
* Whether an argument starts a line, shape or size specification instead of a
|
|
100
|
+
* colour. Numbers cannot start a colour, and every other token here has to be
|
|
101
|
+
* an identifier the gradient grammar reserves for its first argument.
|
|
63
102
|
*
|
|
64
|
-
* @param {
|
|
65
|
-
* @
|
|
103
|
+
* @param {readonly CSSToken[]} input
|
|
104
|
+
* @param {readonly number[]} parts Offsets of an argument's significant tokens.
|
|
105
|
+
* @return {boolean}
|
|
66
106
|
*/
|
|
67
|
-
function
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return
|
|
76
|
-
first.value.toLowerCase()
|
|
77
|
-
);
|
|
107
|
+
function startsLineSpecification(input, parts) {
|
|
108
|
+
const token = input[parts[0]];
|
|
109
|
+
if (!token) return false;
|
|
110
|
+
if (
|
|
111
|
+
token[0] === TokenType.Dimension ||
|
|
112
|
+
token[0] === TokenType.Number ||
|
|
113
|
+
token[0] === TokenType.Percentage
|
|
114
|
+
) {
|
|
115
|
+
return true;
|
|
78
116
|
}
|
|
79
|
-
|
|
80
|
-
|
|
117
|
+
return (
|
|
118
|
+
token[0] === TokenType.Ident &&
|
|
119
|
+
lineKeywords.has(asciiLowerCase(decoded(token)))
|
|
120
|
+
);
|
|
81
121
|
}
|
|
82
122
|
|
|
83
123
|
/**
|
|
84
|
-
*
|
|
124
|
+
* A zero length and a zero percentage name the same gradient position, which is
|
|
125
|
+
* also what the unitless zero this plugin emits denotes. Angles and other
|
|
126
|
+
* dimensions only share a spelling with it.
|
|
85
127
|
*
|
|
86
|
-
* @param {
|
|
87
|
-
* @
|
|
88
|
-
* @returns {import('postcss-value-parser').Node[]}
|
|
128
|
+
* @param {string} unit
|
|
129
|
+
* @return {boolean}
|
|
89
130
|
*/
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
});
|
|
131
|
+
function isPositionZeroUnit(unit) {
|
|
132
|
+
const lowered = asciiLowerCase(unit);
|
|
133
|
+
return lowered === '' || lowered === '%' || lengthUnits.has(lowered);
|
|
94
134
|
}
|
|
95
135
|
|
|
96
136
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
137
|
+
* Whether two positions are on the same scale, so their numbers can be ordered.
|
|
138
|
+
* Only a length and a percentage naming zero are interchangeable.
|
|
99
139
|
*
|
|
100
|
-
* @param {
|
|
101
|
-
* @param {
|
|
102
|
-
* @
|
|
140
|
+
* @param {PositionValue} current
|
|
141
|
+
* @param {PositionValue} largest
|
|
142
|
+
* @return {boolean}
|
|
103
143
|
*/
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (!largestPosition) {
|
|
116
|
-
return currentPosition;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const isFixedUp = isLessThanOrEqual(largestPosition, currentPosition);
|
|
120
|
-
|
|
121
|
-
if (isFixedUp) {
|
|
122
|
-
position.value = '0';
|
|
123
|
-
return largestPosition;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return isFixedUp === undefined ? undefined : currentPosition;
|
|
144
|
+
function positionsComparable(current, largest) {
|
|
145
|
+
const unit = asciiLowerCase(current.unit);
|
|
146
|
+
const largestUnit = asciiLowerCase(largest.unit);
|
|
147
|
+
if (unit === largestUnit) return true;
|
|
148
|
+
return (
|
|
149
|
+
(current.number === 0 || largest.number === 0) &&
|
|
150
|
+
isPositionZeroUnit(unit) &&
|
|
151
|
+
isPositionZeroUnit(largestUnit)
|
|
152
|
+
);
|
|
127
153
|
}
|
|
128
154
|
|
|
129
155
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* used value can only be known during layout.
|
|
156
|
+
* Offset just past the leading colour of an argument, so a functional colour
|
|
157
|
+
* such as `rgb(0 0 0 / 50%)` ends at its closing parenthesis.
|
|
133
158
|
*
|
|
134
|
-
* @param {
|
|
135
|
-
* @
|
|
159
|
+
* @param {readonly CSSToken[]} input
|
|
160
|
+
* @param {NonNullable<ReturnType<typeof balancedTokens>>} structure
|
|
161
|
+
* @param {readonly number[]} parts Offsets of an argument's significant tokens.
|
|
162
|
+
* @return {number}
|
|
136
163
|
*/
|
|
137
|
-
function
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const stops = [];
|
|
145
|
-
let largestPosition = /** @type {valueParser.Dimension | undefined} */ (
|
|
146
|
-
undefined
|
|
147
|
-
);
|
|
148
|
-
let hasSeenStop = false;
|
|
149
|
-
|
|
150
|
-
for (const argument of args.slice(firstStopIndex)) {
|
|
151
|
-
const colorStop = isColorStopArgument(argument);
|
|
152
|
-
const positions = getPositions(argument, colorStop);
|
|
153
|
-
|
|
154
|
-
if (colorStop) {
|
|
155
|
-
stops.push({ argument, positions });
|
|
156
|
-
|
|
157
|
-
if (!hasSeenStop && positions.length === 0) {
|
|
158
|
-
largestPosition = /** @type {valueParser.Dimension} */ (
|
|
159
|
-
valueParser.unit('0%')
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
hasSeenStop = true;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (positions.length === 0) {
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
for (const position of positions) {
|
|
171
|
-
largestPosition = updateLargestPosition(largestPosition, position);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const firstStop = stops[0];
|
|
176
|
-
const lastStop = stops.at(-1);
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
firstStop &&
|
|
180
|
-
firstStop.positions.length === 1 &&
|
|
181
|
-
firstStop.argument.length === 3 &&
|
|
182
|
-
firstStop.positions[0].value === '0%'
|
|
183
|
-
) {
|
|
184
|
-
firstStop.argument[1].value = firstStop.positions[0].value = '';
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
lastStop &&
|
|
189
|
-
lastStop.positions.length === 1 &&
|
|
190
|
-
lastStop.argument.length === 3 &&
|
|
191
|
-
lastStop.positions[0].value === '100%'
|
|
192
|
-
) {
|
|
193
|
-
lastStop.argument[1].value = lastStop.positions[0].value = '';
|
|
194
|
-
}
|
|
164
|
+
function colorEnd(input, structure, parts) {
|
|
165
|
+
const leading = parts[0];
|
|
166
|
+
const component = input[leading];
|
|
167
|
+
if (component[0] !== TokenType.Function) return component[3];
|
|
168
|
+
const closing = structure.endForOpening(leading);
|
|
169
|
+
return closing === undefined ? component[3] : input[closing][3];
|
|
195
170
|
}
|
|
196
171
|
|
|
197
172
|
/**
|
|
198
|
-
*
|
|
173
|
+
* A `to <side>` line specification is an angle in fewer bytes.
|
|
199
174
|
*
|
|
200
|
-
* @param {
|
|
201
|
-
* @
|
|
175
|
+
* @param {readonly CSSToken[]} input
|
|
176
|
+
* @param {readonly number[]} parts Significant tokens of the first argument.
|
|
177
|
+
* @return {SourceEdit | undefined}
|
|
202
178
|
*/
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
179
|
+
function lineDirectionEdit(input, parts) {
|
|
180
|
+
if (parts.length !== 2) return undefined;
|
|
181
|
+
const from = input[parts[0]];
|
|
182
|
+
const side = input[parts[1]];
|
|
183
|
+
if (
|
|
184
|
+
from[0] !== TokenType.Ident ||
|
|
185
|
+
side[0] !== TokenType.Ident ||
|
|
186
|
+
asciiLowerCase(decoded(from)) !== 'to'
|
|
187
|
+
)
|
|
188
|
+
return undefined;
|
|
189
|
+
const direction = directions.get(asciiLowerCase(decoded(side)));
|
|
190
|
+
if (!direction) return undefined;
|
|
191
|
+
return { start: from[2], end: tokenEnd(side), text: direction };
|
|
208
192
|
}
|
|
209
193
|
|
|
210
194
|
/**
|
|
211
|
-
*
|
|
195
|
+
* Colour stop fixup raises a position to the largest position before it, so a
|
|
196
|
+
* position at or below that non-negative maximum can be written as a zero.
|
|
212
197
|
*
|
|
213
|
-
* @param {
|
|
214
|
-
* @
|
|
198
|
+
* @param {readonly CSSToken[]} input
|
|
199
|
+
* @param {readonly number[]} position Offsets of the position tokens.
|
|
200
|
+
* @param {PositionValue | undefined} maximum Running maximum of the preceding positions.
|
|
201
|
+
* @param {Map<number, SourceEdit>} edits Collects the positions reduced to a zero.
|
|
202
|
+
* @return {PositionValue | undefined} The maximum the following stops clamp to.
|
|
215
203
|
*/
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
204
|
+
function zeroPositionEdits(input, position, maximum, edits) {
|
|
205
|
+
let largest = maximum;
|
|
206
|
+
for (const index of position) {
|
|
207
|
+
const token = input[index];
|
|
208
|
+
const current = numeric(token);
|
|
209
|
+
if (!current) {
|
|
210
|
+
largest = undefined;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
// A zero clamps to any non-negative maximum whatever units name them, so
|
|
214
|
+
// the maximum survives the rewritten spelling instead of resetting on the
|
|
215
|
+
// next pass.
|
|
216
|
+
if (
|
|
217
|
+
largest &&
|
|
218
|
+
largest.number >= 0 &&
|
|
219
|
+
current.number === 0 &&
|
|
220
|
+
isPositionZeroUnit(current.unit)
|
|
221
|
+
) {
|
|
222
|
+
edits.set(index, { start: token[2], end: tokenEnd(token), text: '0' });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (largest && !positionsComparable(current, largest)) {
|
|
226
|
+
largest = undefined;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (largest && largest.number >= 0 && largest.number >= current.number) {
|
|
230
|
+
edits.set(index, { start: token[2], end: tokenEnd(token), text: '0' });
|
|
231
|
+
} else {
|
|
232
|
+
largest = current;
|
|
233
|
+
}
|
|
220
234
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return false;
|
|
235
|
+
return largest;
|
|
224
236
|
}
|
|
225
237
|
|
|
226
238
|
/**
|
|
227
|
-
*
|
|
239
|
+
* Collect the colour stops of one gradient argument list, the count of leading
|
|
240
|
+
* arguments naming the line or shape instead of a stop, and the positions that
|
|
241
|
+
* colour stop fixup clamps to a zero.
|
|
228
242
|
*
|
|
229
|
-
* @param {
|
|
230
|
-
* @
|
|
243
|
+
* @param {string} source
|
|
244
|
+
* @param {readonly CSSToken[]} input
|
|
245
|
+
* @param {NonNullable<ReturnType<typeof balancedTokens>>} structure
|
|
246
|
+
* @param {{startIndex: number, endIndex: number}[]} args
|
|
247
|
+
* @param {boolean} linear Whether the line specification may hold an angle.
|
|
248
|
+
* @return {{stops: ColorStop[], lineSpecifications: number, zeroEdits: Map<number, SourceEdit>, directionEdit: SourceEdit | undefined}}
|
|
231
249
|
*/
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
250
|
+
function collectColorStops(source, input, structure, args, linear) {
|
|
251
|
+
/** @type {ColorStop[]} */
|
|
252
|
+
const stops = [];
|
|
253
|
+
/** @type {Map<number, SourceEdit>} */
|
|
254
|
+
const zeroEdits = new Map();
|
|
255
|
+
/** @type {SourceEdit | undefined} */
|
|
256
|
+
let directionEdit;
|
|
257
|
+
// A stop may only drop its start position when nothing but these arguments
|
|
258
|
+
// precede it.
|
|
259
|
+
let lineSpecifications = 0;
|
|
260
|
+
/** @type {PositionValue | undefined} */
|
|
261
|
+
let largest;
|
|
262
|
+
for (const [argIndex, range] of args.entries()) {
|
|
263
|
+
const parts = significant(input, structure, range);
|
|
264
|
+
if (!parts.length) continue;
|
|
265
|
+
if (linear && argIndex === 0)
|
|
266
|
+
directionEdit = lineDirectionEdit(input, parts);
|
|
267
|
+
const end = colorEnd(input, structure, parts);
|
|
268
|
+
const leading = input[parts[0]];
|
|
269
|
+
const color = asciiLowerCase(source.slice(leading[2], end + 1));
|
|
270
|
+
const isColor =
|
|
271
|
+
isKnownColor(color) ||
|
|
272
|
+
(leading[0] === TokenType.Function &&
|
|
273
|
+
!positionFunctions.has(asciiLowerCase(decoded(leading))));
|
|
274
|
+
const position = isColor ? parts.slice(1) : parts;
|
|
275
|
+
if (
|
|
276
|
+
!isColor &&
|
|
277
|
+
argIndex === lineSpecifications &&
|
|
278
|
+
startsLineSpecification(input, parts)
|
|
279
|
+
)
|
|
280
|
+
lineSpecifications = argIndex + 1;
|
|
281
|
+
if (isColor) {
|
|
282
|
+
// A stop without a position sits at the running maximum, which starts at zero.
|
|
283
|
+
if (!position.length && !largest) largest = { number: 0, unit: '%' };
|
|
284
|
+
stops.push({ argIndex, colorEnd: end, position });
|
|
285
|
+
}
|
|
286
|
+
// Positions before the first stop belong to the line specification.
|
|
287
|
+
if (!stops.length) continue;
|
|
288
|
+
largest = zeroPositionEdits(input, position, largest, zeroEdits);
|
|
289
|
+
}
|
|
290
|
+
return { stops, lineSpecifications, zeroEdits, directionEdit };
|
|
236
291
|
}
|
|
237
292
|
|
|
238
293
|
/**
|
|
239
|
-
*
|
|
294
|
+
* The first colour stop defaults to the zero position and the last one to 100%,
|
|
295
|
+
* so either can drop a position already spelling its default. A single-stop
|
|
296
|
+
* gradient holds one stop in both slots, hence the slots by index and the
|
|
297
|
+
* zero edit dropped in favour of the wider removal.
|
|
240
298
|
*
|
|
241
|
-
* @param {
|
|
242
|
-
* @
|
|
299
|
+
* @param {readonly CSSToken[]} input
|
|
300
|
+
* @param {readonly ColorStop[]} stops
|
|
301
|
+
* @param {number} lineSpecifications
|
|
302
|
+
* @param {number} args
|
|
303
|
+
* @param {Map<number, SourceEdit>} zeroEdits
|
|
304
|
+
* @return {SourceEdit[]}
|
|
243
305
|
*/
|
|
244
|
-
function
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
color = arg[0].value;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (arg[2].type === 'function') {
|
|
261
|
-
stop = `${arg[2].value}(${valueParser.stringify(arg[2].nodes)})`;
|
|
262
|
-
} else {
|
|
263
|
-
stop = arg[2].value;
|
|
264
|
-
}
|
|
265
|
-
} else {
|
|
266
|
-
if (arg[0].type === 'function') {
|
|
267
|
-
// eslint-disable-next-line no-useless-assignment
|
|
268
|
-
color = `${arg[0].value}(${valueParser.stringify(arg[0].nodes)})`;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
color = arg[0].value;
|
|
306
|
+
function boundaryStopEdits(input, stops, lineSpecifications, args, zeroEdits) {
|
|
307
|
+
/** @type {SourceEdit[]} */
|
|
308
|
+
const edits = [];
|
|
309
|
+
for (const slot of new Set([0, stops.length - 1])) {
|
|
310
|
+
const stop = stops[slot];
|
|
311
|
+
if (!stop || stop.position.length !== 1) continue;
|
|
312
|
+
const [index] = stop.position;
|
|
313
|
+
const position = input[index];
|
|
314
|
+
const value = numeric(position);
|
|
315
|
+
if (!value) continue;
|
|
316
|
+
let removable = false;
|
|
317
|
+
if (slot === 0 && stop.argIndex === lineSpecifications) {
|
|
318
|
+
removable = value.number === 0 && isPositionZeroUnit(value.unit);
|
|
272
319
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (!colorStop || !arg[2]) {
|
|
282
|
-
continue;
|
|
320
|
+
if (
|
|
321
|
+
slot === stops.length - 1 &&
|
|
322
|
+
stop.argIndex === args - 1 &&
|
|
323
|
+
value.number === 100 &&
|
|
324
|
+
asciiLowerCase(value.unit) === '%'
|
|
325
|
+
) {
|
|
326
|
+
removable = true;
|
|
283
327
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (previousStop && thisStop && isLessThan(previousStop, thisStop)) {
|
|
294
|
-
arg[2].value = '0';
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
previousStop = thisStop;
|
|
328
|
+
if (!removable) continue;
|
|
329
|
+
// Removing the position from the colour onwards also covers a zero edit
|
|
330
|
+
// emitted for the same token.
|
|
331
|
+
zeroEdits.delete(index);
|
|
332
|
+
edits.push({
|
|
333
|
+
start: stop.colorEnd + 1,
|
|
334
|
+
end: tokenEnd(position),
|
|
335
|
+
text: '',
|
|
336
|
+
});
|
|
298
337
|
}
|
|
299
|
-
|
|
300
|
-
return false;
|
|
338
|
+
return edits;
|
|
301
339
|
}
|
|
302
340
|
|
|
303
341
|
/**
|
|
304
342
|
* @param {import('postcss').Declaration} decl
|
|
305
|
-
* @return {void}
|
|
306
343
|
*/
|
|
307
344
|
function optimise(decl) {
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (!normalizedValue.includes('gradient')) {
|
|
345
|
+
const source = decl.value;
|
|
346
|
+
if (!source || !gradientSourceRegex.test(source)) return;
|
|
347
|
+
const structure = balancedTokens(source);
|
|
348
|
+
if (!structure) return;
|
|
349
|
+
const { tokens: input } = structure;
|
|
350
|
+
if (
|
|
351
|
+
input.some(
|
|
352
|
+
(token) =>
|
|
353
|
+
token[0] === TokenType.Function &&
|
|
354
|
+
variableFunctions.has(asciiLowerCase(decoded(token)))
|
|
355
|
+
)
|
|
356
|
+
)
|
|
321
357
|
return;
|
|
358
|
+
/** @type {SourceEdit[]} */
|
|
359
|
+
const replacements = [];
|
|
360
|
+
// Nested gradients are visited in token order; their argument ranges come
|
|
361
|
+
// from the shared delimiter map.
|
|
362
|
+
for (let index = 0; index < input.length; index++) {
|
|
363
|
+
const token = input[index];
|
|
364
|
+
if (token[0] !== TokenType.Function) continue;
|
|
365
|
+
const name = asciiLowerCase(decoded(token));
|
|
366
|
+
if (!gradientNames.has(name)) continue;
|
|
367
|
+
const end = structure.endForOpening(index);
|
|
368
|
+
if (end === undefined) continue;
|
|
369
|
+
const args = structure.topLevelSegments(index + 1, end);
|
|
370
|
+
const { stops, lineSpecifications, zeroEdits, directionEdit } =
|
|
371
|
+
collectColorStops(
|
|
372
|
+
source,
|
|
373
|
+
input,
|
|
374
|
+
structure,
|
|
375
|
+
args,
|
|
376
|
+
name.includes('linear')
|
|
377
|
+
);
|
|
378
|
+
if (directionEdit) replacements.push(directionEdit);
|
|
379
|
+
replacements.push(
|
|
380
|
+
...boundaryStopEdits(
|
|
381
|
+
input,
|
|
382
|
+
stops,
|
|
383
|
+
lineSpecifications,
|
|
384
|
+
args.length,
|
|
385
|
+
zeroEdits
|
|
386
|
+
),
|
|
387
|
+
...zeroEdits.values()
|
|
388
|
+
);
|
|
322
389
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (node.type !== 'function' || !node.nodes.length) {
|
|
327
|
-
return false;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const lowerCasedValue = node.value.toLowerCase();
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
lowerCasedValue === 'linear-gradient' ||
|
|
334
|
-
lowerCasedValue === 'repeating-linear-gradient' ||
|
|
335
|
-
lowerCasedValue === '-webkit-linear-gradient' ||
|
|
336
|
-
lowerCasedValue === '-webkit-repeating-linear-gradient'
|
|
337
|
-
) {
|
|
338
|
-
return optimizeLinearGradient(node);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (
|
|
342
|
-
lowerCasedValue === 'radial-gradient' ||
|
|
343
|
-
lowerCasedValue === 'repeating-radial-gradient'
|
|
344
|
-
) {
|
|
345
|
-
return optimizeRadialGradient(node);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (
|
|
349
|
-
lowerCasedValue === '-webkit-radial-gradient' ||
|
|
350
|
-
lowerCasedValue === '-webkit-repeating-radial-gradient'
|
|
351
|
-
) {
|
|
352
|
-
return optimizeWebkitRadialGradient(node);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (
|
|
356
|
-
lowerCasedValue === 'conic-gradient' ||
|
|
357
|
-
lowerCasedValue === 'repeating-conic-gradient'
|
|
358
|
-
) {
|
|
359
|
-
optimizeColorStops(getArguments(node));
|
|
360
|
-
}
|
|
361
|
-
return false;
|
|
362
|
-
})
|
|
363
|
-
.toString();
|
|
390
|
+
// With nothing to rewrite, neither the edit pass nor the write can change the
|
|
391
|
+
// declaration.
|
|
392
|
+
if (replacements.length) decl.value = applyEdits(source, replacements);
|
|
364
393
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
*/
|
|
394
|
+
|
|
395
|
+
/** @return {import('postcss').Plugin} */
|
|
368
396
|
function pluginCreator() {
|
|
369
397
|
return {
|
|
370
398
|
postcssPlugin: 'postcss-minify-gradients',
|
|
371
|
-
/**
|
|
372
|
-
* @param {import('postcss').Root} css
|
|
373
|
-
*/
|
|
374
399
|
OnceExit(css) {
|
|
375
400
|
css.walkDecls(optimise);
|
|
376
401
|
},
|
|
@@ -378,6 +403,4 @@ function pluginCreator() {
|
|
|
378
403
|
}
|
|
379
404
|
/** @type {true} */
|
|
380
405
|
pluginCreator.postcss = true;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
export { moduleExports as default, moduleExports as 'module.exports' };
|
|
406
|
+
export { pluginCreator as default, pluginCreator as 'module.exports' };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { colordx as colord, extend } from '@colordx/core';
|
|
2
|
+
import hwbPlugin from '@colordx/core/plugins/hwb';
|
|
3
|
+
import namesPlugin from '@colordx/core/plugins/names';
|
|
4
|
+
|
|
5
|
+
extend([
|
|
6
|
+
/** @type {import('@colordx/core').Plugin} */ (
|
|
7
|
+
/** @type {unknown} */ (hwbPlugin)
|
|
8
|
+
),
|
|
9
|
+
/** @type {import('@colordx/core').Plugin} */ (
|
|
10
|
+
/** @type {unknown} */ (namesPlugin)
|
|
11
|
+
),
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
/* A gradient stop may hold `currentColor` and the `<system-color>` keywords,
|
|
15
|
+
* which resolve only at used-value time, so no colour parser can validate
|
|
16
|
+
* them; the keyword list carries them. Plugin registration is process-global
|
|
17
|
+
* and `postcss-colormin` shares it, so colordx loads only the plugins this
|
|
18
|
+
* module needs — otherwise colormin would gamut-map wide-gamut colours.
|
|
19
|
+
* Colour notations colordx cannot parse remain valid stop colours.
|
|
20
|
+
* `postcss-merge-longhand` duplicates this list on purpose. */
|
|
21
|
+
export const dynamicColorKeywords = new Set([
|
|
22
|
+
'currentcolor',
|
|
23
|
+
'accentcolor',
|
|
24
|
+
'accentcolortext',
|
|
25
|
+
'activetext',
|
|
26
|
+
'buttonborder',
|
|
27
|
+
'buttonface',
|
|
28
|
+
'buttontext',
|
|
29
|
+
'canvas',
|
|
30
|
+
'canvastext',
|
|
31
|
+
'field',
|
|
32
|
+
'fieldtext',
|
|
33
|
+
'graytext',
|
|
34
|
+
'highlight',
|
|
35
|
+
'highlighttext',
|
|
36
|
+
'linktext',
|
|
37
|
+
'mark',
|
|
38
|
+
'marktext',
|
|
39
|
+
'selecteditem',
|
|
40
|
+
'selecteditemtext',
|
|
41
|
+
'visitedtext',
|
|
42
|
+
// Deprecated system colors (CSS Color 3 / CSS Color 4 § 3.3.1)
|
|
43
|
+
'activeborder',
|
|
44
|
+
'activecaption',
|
|
45
|
+
'appworkspace',
|
|
46
|
+
'background',
|
|
47
|
+
'buttonhighlight',
|
|
48
|
+
'buttonshadow',
|
|
49
|
+
'captiontext',
|
|
50
|
+
'inactiveborder',
|
|
51
|
+
'inactivecaption',
|
|
52
|
+
'inactivecaptiontext',
|
|
53
|
+
'infobackground',
|
|
54
|
+
'infotext',
|
|
55
|
+
'menu',
|
|
56
|
+
'menutext',
|
|
57
|
+
'scrollbar',
|
|
58
|
+
'threeddarkshadow',
|
|
59
|
+
'threedface',
|
|
60
|
+
'threedhighlight',
|
|
61
|
+
'threedlightshadow',
|
|
62
|
+
'threedshadow',
|
|
63
|
+
'window',
|
|
64
|
+
'windowframe',
|
|
65
|
+
'windowtext',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether `color` is a `<color>` a gradient stop may hold, including the
|
|
70
|
+
* keywords that only resolve at used-value time. Arguments beginning with a
|
|
71
|
+
* function other than `calc()`, `clamp()`, `max()` or `min()` are treated as
|
|
72
|
+
* colour stops without this check.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} color
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
export default function isKnownColor(color) {
|
|
78
|
+
return dynamicColorKeywords.has(color) || colord(color).isValid();
|
|
79
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export type SourceEdit = {
|
|
2
|
+
start: number;
|
|
3
|
+
end: number;
|
|
4
|
+
text: string;
|
|
5
|
+
};
|
|
6
|
+
export type ColorStop = {
|
|
7
|
+
/**
|
|
8
|
+
* Comma-separated argument the stop came from.
|
|
9
|
+
*/
|
|
10
|
+
argIndex: number;
|
|
11
|
+
/**
|
|
12
|
+
* Offset just past the colour in the declaration value.
|
|
13
|
+
*/
|
|
14
|
+
colorEnd: number;
|
|
15
|
+
/**
|
|
16
|
+
* Positions following the colour.
|
|
17
|
+
*/
|
|
18
|
+
position: readonly number[];
|
|
19
|
+
};
|
|
20
|
+
export type PositionValue = {
|
|
21
|
+
number: number;
|
|
22
|
+
unit: string;
|
|
23
|
+
};
|
|
24
|
+
/** @return {import('postcss').Plugin} */
|
|
4
25
|
declare function pluginCreator(): import('postcss').Plugin;
|
|
5
26
|
declare namespace pluginCreator {
|
|
6
27
|
var postcss: true;
|
|
7
28
|
}
|
|
8
|
-
|
|
9
|
-
export { moduleExports as default, moduleExports as 'module.exports' };
|
|
29
|
+
export { pluginCreator as default, pluginCreator as 'module.exports' };
|
|
10
30
|
//# sourceMappingURL=index.d.ts.map
|
package/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AA2DI,YAAsD,UAAU,GAAtD;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAAY;AAKjE,YAAkB,SAAS,GAC3B;;;;IAAmB,QAAQ,EAAhB,MAAM,CACjB;;;;IAAmB,QAAQ,EAAhB,MAAM,CACjB;;;;IAA8B,QAAQ,EAA3B,SAAS,MAAM,EAAE,CAC9B;CAAA,CAAA;AAIE,YAA0C,aAAa,GAA7C;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAAe;AAkU1D,yCAAyC;AACzC,iBAAS,aAAa,IADT,OAAO,SAAS,EAAE,MAAM,CAQpC;kBAPQ,aAAa;iBAQX,IAAI;;AAEf,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,aAAa,IAAI,gBAAgB,EAAE,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const dynamicColorKeywords: Set<string>;
|
|
2
|
+
/**
|
|
3
|
+
* Whether `color` is a `<color>` a gradient stop may hold, including the
|
|
4
|
+
* keywords that only resolve at used-value time. Arguments beginning with a
|
|
5
|
+
* function other than `calc()`, `clamp()`, `max()` or `min()` are treated as
|
|
6
|
+
* colour stops without this check.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} color
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export default function isKnownColor(color: string): boolean;
|
|
12
|
+
//# sourceMappingURL=isKnownColor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isKnownColor.d.ts","sourceRoot":"","sources":["../src/isKnownColor.js"],"names":[],"mappings":"AAoBA,eAAO,MAAM,oBAAoB,aA6C/B,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,KAAK,EAH/B,MAG+B,GAF7B,OAAO,CAInB"}
|
package/src/isColorStop.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import postcssValueParser from 'postcss-value-parser';
|
|
2
|
-
import { colordx as colord, extend } from '@colordx/core';
|
|
3
|
-
import hwbPlugin from '@colordx/core/plugins/hwb';
|
|
4
|
-
import namesPlugin from '@colordx/core/plugins/names';
|
|
5
|
-
|
|
6
|
-
const { unit } = postcssValueParser;
|
|
7
|
-
extend([
|
|
8
|
-
/** @type {import('@colordx/core').Plugin} */ (
|
|
9
|
-
/** @type {unknown} */ (hwbPlugin)
|
|
10
|
-
),
|
|
11
|
-
/** @type {import('@colordx/core').Plugin} */ (
|
|
12
|
-
/** @type {unknown} */ (namesPlugin)
|
|
13
|
-
),
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
|
-
/* Code derived from https://www.npmjs.com/package/is-color-stop Licensed under MIT license */
|
|
17
|
-
|
|
18
|
-
const lengthUnits = new Set([
|
|
19
|
-
'PX',
|
|
20
|
-
'IN',
|
|
21
|
-
'CM',
|
|
22
|
-
'MM',
|
|
23
|
-
'EM',
|
|
24
|
-
'REM',
|
|
25
|
-
'POINTS',
|
|
26
|
-
'PC',
|
|
27
|
-
'EX',
|
|
28
|
-
'CH',
|
|
29
|
-
'VW',
|
|
30
|
-
'VH',
|
|
31
|
-
'VMIN',
|
|
32
|
-
'VMAX',
|
|
33
|
-
'%',
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
const colorStopRegex = /^calc\(\S+\)$/g;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @param {string} input
|
|
40
|
-
* @return {boolean}
|
|
41
|
-
*/
|
|
42
|
-
function isCSSLengthUnit(input) {
|
|
43
|
-
return lengthUnits.has(input.toUpperCase());
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* @param {string|undefined} str
|
|
47
|
-
* @return {boolean}
|
|
48
|
-
*/
|
|
49
|
-
function isStop(str) {
|
|
50
|
-
if (str) {
|
|
51
|
-
let colorStop = false;
|
|
52
|
-
const node = unit(str);
|
|
53
|
-
if (node) {
|
|
54
|
-
const number = Number(node.number);
|
|
55
|
-
if (
|
|
56
|
-
number === 0 ||
|
|
57
|
-
(!Number.isNaN(number) && isCSSLengthUnit(node.unit))
|
|
58
|
-
) {
|
|
59
|
-
colorStop = true;
|
|
60
|
-
}
|
|
61
|
-
} else {
|
|
62
|
-
colorStop = colorStopRegex.test(str);
|
|
63
|
-
}
|
|
64
|
-
return colorStop;
|
|
65
|
-
}
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* @param {string} color
|
|
70
|
-
* @param {string} [stop]
|
|
71
|
-
* @return {boolean}
|
|
72
|
-
*/
|
|
73
|
-
function isColorStop(color, stop) {
|
|
74
|
-
return colord(color).isValid() && isStop(stop);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export default isColorStop;
|
package/types/isColorStop.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"isColorStop.d.ts","sourceRoot":"","sources":["../src/isColorStop.js"],"names":[],"mappings":"AAmEA;;;;GAIG;AACH,iBAAS,WAAW,CAAC,KAAK,EAJf,MAIe,EAAE,IAAI,AAH7B,CACA,EADQ,MAGqB,GAFpB,OAAO,CAIlB;eAEc,WAAW"}
|