@thejaredwilcurt/csslop 0.0.49 → 0.0.51
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 +0 -1
- package/package.json +2 -2
- package/src/declarations/border.js +1 -1
- package/src/declarations/process.js +155 -1
- package/src/index.js +7 -7
- package/src/rules/optimize/at-rules.js +113 -0
- package/src/rules/optimize/declaration-writes.js +70 -0
- package/src/rules/optimize/empty-rules.js +51 -0
- package/src/rules/optimize/expand-nesting.js +236 -0
- package/src/rules/optimize/factor-parents.js +343 -0
- package/src/rules/optimize/index.js +71 -0
- package/src/rules/optimize/merge-declarations.js +199 -0
- package/src/rules/optimize/merge-nested-rules.js +117 -0
- package/src/rules/optimize/merge-selectors.js +252 -0
- package/src/rules/optimize/nest-rules.js +202 -0
- package/src/rules/optimize/override-index.js +264 -0
- package/src/rules/optimize/prune-overridden.js +267 -0
- package/src/rules/optimize/selector-normalization.js +60 -0
- package/src/rules/optimize/specificity.js +280 -0
- package/src/{position-try.js → rules/position-try.js} +1 -1
- package/src/rules/stringify.js +13 -0
- package/src/utilities.js +14 -0
- package/src/value/border.js +85 -0
- package/src/value/{colors.js → color/conversion.js} +3 -2
- package/src/value/color/functions.js +463 -0
- package/src/value/{light-dark.js → color/light-dark.js} +1 -1
- package/src/value/{color-mix.js → color/mix.js} +3 -3
- package/src/value/{relative-colors.js → color/relative.js} +3 -3
- package/src/value/color/shorten.js +93 -0
- package/src/value/gradients.js +1 -1
- package/src/value/hyphenate-limit-chars.js +72 -0
- package/src/value/initial-keywords.js +128 -0
- package/src/value/lengths.js +55 -0
- package/src/value/math/evaluate.js +36 -0
- package/src/value/math/fold.js +315 -0
- package/src/value/math/format.js +230 -0
- package/src/value/math/functions.js +302 -0
- package/src/value/math/index.js +219 -0
- package/src/value/math/parser.js +301 -0
- package/src/value/math/property-ranges.js +174 -0
- package/src/value/math/rewrite.js +222 -0
- package/src/value/math/tokenizer.js +160 -0
- package/src/value/math/unit-totals.js +194 -0
- package/src/value/minify.js +127 -1669
- package/src/value/numbers.js +28 -0
- package/src/value/positions.js +152 -0
- package/src/value/property-optimizations.js +485 -0
- package/src/value/segments.js +74 -0
- package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
- package/src/value/{svg-markup.js → svg/markup.js} +1 -1
- package/src/value/time.js +75 -0
- package/src/value/urls.js +242 -0
- package/src/value/whitespace.js +88 -0
- package/src/rules/optimize.js +0 -2314
- package/src/value/math-expression.js +0 -1156
- package/src/value/math.js +0 -886
- /package/src/value/{named-colors.js → color/named.js} +0 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Reads the tokens of a CSS math function as sums, products, and operands, resolving each into the unit totals it states.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { MATH_FUNCTION_EVALUATORS } from './functions.js';
|
|
6
|
+
import {
|
|
7
|
+
addUnitTotals,
|
|
8
|
+
createUnitTotals,
|
|
9
|
+
divideUnitTotals,
|
|
10
|
+
multiplyUnitTotals,
|
|
11
|
+
scaleUnitTotals,
|
|
12
|
+
UNITLESS
|
|
13
|
+
} from './unit-totals.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The numeric constants a calculation may name instead of writing out, as the
|
|
17
|
+
* keywords section of the CSS Values specification defines them:
|
|
18
|
+
* https://drafts.csswg.org/css-values/#calc-keywords.
|
|
19
|
+
*
|
|
20
|
+
* @type {Map<string, number>}
|
|
21
|
+
*/
|
|
22
|
+
const NUMERIC_CONSTANTS = new Map([
|
|
23
|
+
['e', Math.E],
|
|
24
|
+
['infinity', Infinity],
|
|
25
|
+
['nan', NaN],
|
|
26
|
+
['pi', Math.PI]
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The deepest a calculation may nest one set of parentheses inside another
|
|
31
|
+
* before the evaluator gives up on it. Every level of nesting is a level of
|
|
32
|
+
* recursion, and a stylesheet states nothing at a depth anywhere near this.
|
|
33
|
+
*
|
|
34
|
+
* @type {number}
|
|
35
|
+
*/
|
|
36
|
+
const MAXIMUM_NESTING_DEPTH = 32;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {object} ExpressionReader
|
|
40
|
+
* @property {Array} tokens The tokens of the math function, in the order they are written.
|
|
41
|
+
* @property {number} index The token the parser reads next.
|
|
42
|
+
* @property {number} depth How many sets of parentheses the parser is reading inside of.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reads the token the parser stands on, or one of the tokens after it.
|
|
47
|
+
*
|
|
48
|
+
* @param {ExpressionReader} reader The token reader.
|
|
49
|
+
* @param {number} offset How many tokens past the current one to read.
|
|
50
|
+
* @return {object|undefined} The token read, or undefined past the end of the expression.
|
|
51
|
+
*/
|
|
52
|
+
function peekToken (reader, offset = 0) {
|
|
53
|
+
return reader.tokens[reader.index + offset];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reports whether the parser stands on an operator that adds or subtracts what
|
|
58
|
+
* follows it from what precedes it. A sign written directly against its number
|
|
59
|
+
* is part of that number rather than an operator, so a value written with one
|
|
60
|
+
* sits against the value before it with no operator between them, which is not
|
|
61
|
+
* a calculation any browser reads.
|
|
62
|
+
*
|
|
63
|
+
* @param {ExpressionReader} reader The token reader.
|
|
64
|
+
* @return {boolean} Whether an addition or a subtraction follows.
|
|
65
|
+
*/
|
|
66
|
+
function readsAdditiveOperator (reader) {
|
|
67
|
+
const token = peekToken(reader);
|
|
68
|
+
const isAdditive = token?.type === 'operator' && (token.operator === '+' || token.operator === '-');
|
|
69
|
+
if (!isAdditive) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const operandToken = peekToken(reader, 1);
|
|
73
|
+
const signsItsNumber = operandToken?.type === 'number' && !operandToken.precededBySpace;
|
|
74
|
+
return !signsItsNumber;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reads a sum: the terms a calculation adds together, in the order they are
|
|
79
|
+
* written.
|
|
80
|
+
*
|
|
81
|
+
* @param {ExpressionReader} reader The token reader.
|
|
82
|
+
* @return {Map|null} The unit totals the sum states, or null when it cannot be resolved.
|
|
83
|
+
*/
|
|
84
|
+
function parseSum (reader) {
|
|
85
|
+
let value = parseProduct(reader);
|
|
86
|
+
if (!value) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
while (readsAdditiveOperator(reader)) {
|
|
90
|
+
const subtracts = peekToken(reader).operator === '-';
|
|
91
|
+
reader.index++;
|
|
92
|
+
const term = parseProduct(reader);
|
|
93
|
+
if (!term) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
value = addUnitTotals(value, term, subtracts ? -1 : 1);
|
|
97
|
+
if (!value) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reads a product: the operands a calculation multiplies and divides, which
|
|
106
|
+
* bind more tightly than the terms of a sum do.
|
|
107
|
+
*
|
|
108
|
+
* @param {ExpressionReader} reader The token reader.
|
|
109
|
+
* @return {Map|null} The unit totals the product states, or null when it cannot be resolved.
|
|
110
|
+
*/
|
|
111
|
+
function parseProduct (reader) {
|
|
112
|
+
let value = parseOperand(reader);
|
|
113
|
+
if (!value) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
let token = peekToken(reader);
|
|
117
|
+
while (token?.type === 'operator' && (token.operator === '*' || token.operator === '/')) {
|
|
118
|
+
reader.index++;
|
|
119
|
+
const operand = parseOperand(reader);
|
|
120
|
+
if (!operand) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (token.operator === '*') {
|
|
124
|
+
value = multiplyUnitTotals(value, operand);
|
|
125
|
+
} else {
|
|
126
|
+
value = divideUnitTotals(value, operand);
|
|
127
|
+
}
|
|
128
|
+
if (!value) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
token = peekToken(reader);
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reads the number a sign is written against, which is the only place a
|
|
138
|
+
* calculation states a sign: CSS has no operator that negates what follows it,
|
|
139
|
+
* so a sign standing apart from its number, or a second sign written in front
|
|
140
|
+
* of the first, is not a calculation any browser reads.
|
|
141
|
+
*
|
|
142
|
+
* @param {ExpressionReader} reader The token reader.
|
|
143
|
+
* @return {Map|null} The unit totals the signed number states, or null when the sign signs nothing.
|
|
144
|
+
*/
|
|
145
|
+
function parseSignedNumber (reader) {
|
|
146
|
+
const signToken = peekToken(reader);
|
|
147
|
+
const signedToken = peekToken(reader, 1);
|
|
148
|
+
const signsItsValue = (
|
|
149
|
+
(signedToken?.type === 'number' || signedToken?.type === 'identifier') &&
|
|
150
|
+
!signedToken.precededBySpace
|
|
151
|
+
);
|
|
152
|
+
if (!signsItsValue) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
reader.index++;
|
|
156
|
+
const value = parseOperand(reader);
|
|
157
|
+
if (!value) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (signToken.operator === '-') {
|
|
161
|
+
return scaleUnitTotals(value, -1);
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Reads a single operand: a signed number, a number, a numeric constant, a
|
|
168
|
+
* parenthesized sum, or another math function.
|
|
169
|
+
*
|
|
170
|
+
* @param {ExpressionReader} reader The token reader.
|
|
171
|
+
* @return {Map|null} The unit totals the operand states, or null when it cannot be resolved.
|
|
172
|
+
*/
|
|
173
|
+
function parseOperand (reader) {
|
|
174
|
+
const token = peekToken(reader);
|
|
175
|
+
if (!token) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
if (token.type === 'operator' && (token.operator === '+' || token.operator === '-')) {
|
|
179
|
+
return parseSignedNumber(reader);
|
|
180
|
+
}
|
|
181
|
+
if (token.type === 'number') {
|
|
182
|
+
reader.index++;
|
|
183
|
+
return createUnitTotals(token.number, token.unit, true);
|
|
184
|
+
}
|
|
185
|
+
if (token.type === 'identifier') {
|
|
186
|
+
reader.index++;
|
|
187
|
+
const constant = NUMERIC_CONSTANTS.get(token.name);
|
|
188
|
+
if (constant === undefined) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return createUnitTotals(constant, UNITLESS);
|
|
192
|
+
}
|
|
193
|
+
if (token.type === 'function') {
|
|
194
|
+
reader.index++;
|
|
195
|
+
return parseMathFunction(reader, token.name);
|
|
196
|
+
}
|
|
197
|
+
if (token.type === 'open') {
|
|
198
|
+
reader.index++;
|
|
199
|
+
return parseGroup(reader);
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reads a sum written inside parentheses, which is read as one operand of the
|
|
206
|
+
* calculation around it.
|
|
207
|
+
*
|
|
208
|
+
* @param {ExpressionReader} reader The token reader.
|
|
209
|
+
* @return {Map|null} The unit totals the group states, or null when it cannot be resolved.
|
|
210
|
+
*/
|
|
211
|
+
function parseGroup (reader) {
|
|
212
|
+
if (reader.depth >= MAXIMUM_NESTING_DEPTH) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
reader.depth++;
|
|
216
|
+
const value = parseSum(reader);
|
|
217
|
+
reader.depth--;
|
|
218
|
+
if (!value || peekToken(reader)?.type !== 'close') {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
reader.index++;
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Reads one argument of a math function, which is either a sum or a lone
|
|
227
|
+
* keyword such as the rounding strategy of `round()` or the open end of a
|
|
228
|
+
* `clamp()` range.
|
|
229
|
+
*
|
|
230
|
+
* @param {ExpressionReader} reader The token reader.
|
|
231
|
+
* @return {object|null} The argument read, or null when it cannot be resolved.
|
|
232
|
+
*/
|
|
233
|
+
function parseFunctionArgument (reader) {
|
|
234
|
+
const token = peekToken(reader);
|
|
235
|
+
const followingToken = peekToken(reader, 1);
|
|
236
|
+
const standsAlone = followingToken?.type === 'comma' || followingToken?.type === 'close';
|
|
237
|
+
const isKeyword = token?.type === 'identifier' && standsAlone && !NUMERIC_CONSTANTS.has(token.name);
|
|
238
|
+
if (isKeyword) {
|
|
239
|
+
reader.index++;
|
|
240
|
+
return { keyword: token.name, value: null };
|
|
241
|
+
}
|
|
242
|
+
const value = parseSum(reader);
|
|
243
|
+
if (!value) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return { keyword: null, value };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Reads the comma-separated arguments a math function is called with, up to
|
|
251
|
+
* the parenthesis that closes the call.
|
|
252
|
+
*
|
|
253
|
+
* @param {ExpressionReader} reader The token reader.
|
|
254
|
+
* @return {Array|null} The arguments read, or null when the call cannot be resolved.
|
|
255
|
+
*/
|
|
256
|
+
function parseFunctionArguments (reader) {
|
|
257
|
+
const argumentList = [];
|
|
258
|
+
while (reader.index < reader.tokens.length) {
|
|
259
|
+
const argument = parseFunctionArgument(reader);
|
|
260
|
+
if (!argument) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
argumentList.push(argument);
|
|
264
|
+
const token = peekToken(reader);
|
|
265
|
+
if (token?.type === 'close') {
|
|
266
|
+
reader.index++;
|
|
267
|
+
return argumentList;
|
|
268
|
+
}
|
|
269
|
+
if (token?.type !== 'comma') {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
reader.index++;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Reads the arguments of a math function and works out the value the function
|
|
279
|
+
* resolves to.
|
|
280
|
+
*
|
|
281
|
+
* @param {ExpressionReader} reader The token reader.
|
|
282
|
+
* @param {string} name The lowercased name of the function being called.
|
|
283
|
+
* @return {Map|null} The unit totals the function resolves to, or null when it cannot be resolved.
|
|
284
|
+
*/
|
|
285
|
+
function parseMathFunction (reader, name) {
|
|
286
|
+
const resolveFunction = MATH_FUNCTION_EVALUATORS.get(name);
|
|
287
|
+
if (!resolveFunction || reader.depth >= MAXIMUM_NESTING_DEPTH) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
reader.depth++;
|
|
291
|
+
const argumentList = parseFunctionArguments(reader);
|
|
292
|
+
reader.depth--;
|
|
293
|
+
if (!argumentList) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return resolveFunction(argumentList);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export {
|
|
300
|
+
parseSum
|
|
301
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Holds what a math function resolved to inside the range of values the property it is declared on accepts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { formatResolvedNumber } from '../shared.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The properties whose values every CSS specification defines as
|
|
9
|
+
* non-negative, so that a math function declared on one of them has its
|
|
10
|
+
* result clamped rather than read as written.
|
|
11
|
+
*
|
|
12
|
+
* @type {Set<string>}
|
|
13
|
+
*/
|
|
14
|
+
const NON_NEGATIVE_PROPERTIES = new Set([
|
|
15
|
+
'animation-duration',
|
|
16
|
+
'animation-iteration-count',
|
|
17
|
+
'background-size',
|
|
18
|
+
'block-size',
|
|
19
|
+
'border-block-end-width',
|
|
20
|
+
'border-block-start-width',
|
|
21
|
+
'border-block-width',
|
|
22
|
+
'border-bottom-left-radius',
|
|
23
|
+
'border-bottom-right-radius',
|
|
24
|
+
'border-bottom-width',
|
|
25
|
+
'border-end-end-radius',
|
|
26
|
+
'border-end-start-radius',
|
|
27
|
+
'border-image-outset',
|
|
28
|
+
'border-image-width',
|
|
29
|
+
'border-inline-end-width',
|
|
30
|
+
'border-inline-start-width',
|
|
31
|
+
'border-inline-width',
|
|
32
|
+
'border-left-width',
|
|
33
|
+
'border-radius',
|
|
34
|
+
'border-right-width',
|
|
35
|
+
'border-start-end-radius',
|
|
36
|
+
'border-start-start-radius',
|
|
37
|
+
'border-top-left-radius',
|
|
38
|
+
'border-top-right-radius',
|
|
39
|
+
'border-top-width',
|
|
40
|
+
'border-width',
|
|
41
|
+
'column-count',
|
|
42
|
+
'column-gap',
|
|
43
|
+
'column-rule-width',
|
|
44
|
+
'column-width',
|
|
45
|
+
'flex-basis',
|
|
46
|
+
'flex-grow',
|
|
47
|
+
'flex-shrink',
|
|
48
|
+
'font-size',
|
|
49
|
+
'gap',
|
|
50
|
+
'height',
|
|
51
|
+
'inline-size',
|
|
52
|
+
'line-height',
|
|
53
|
+
'mask-size',
|
|
54
|
+
'max-block-size',
|
|
55
|
+
'max-height',
|
|
56
|
+
'max-inline-size',
|
|
57
|
+
'max-width',
|
|
58
|
+
'min-block-size',
|
|
59
|
+
'min-height',
|
|
60
|
+
'min-inline-size',
|
|
61
|
+
'min-width',
|
|
62
|
+
'outline-width',
|
|
63
|
+
'padding',
|
|
64
|
+
'padding-block',
|
|
65
|
+
'padding-block-end',
|
|
66
|
+
'padding-block-start',
|
|
67
|
+
'padding-bottom',
|
|
68
|
+
'padding-inline',
|
|
69
|
+
'padding-inline-end',
|
|
70
|
+
'padding-inline-start',
|
|
71
|
+
'padding-left',
|
|
72
|
+
'padding-right',
|
|
73
|
+
'padding-top',
|
|
74
|
+
'perspective',
|
|
75
|
+
'row-gap',
|
|
76
|
+
'shape-margin',
|
|
77
|
+
'stroke-width',
|
|
78
|
+
'tab-size',
|
|
79
|
+
'transition-duration',
|
|
80
|
+
'width'
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Properties whose computed math values must be integers.
|
|
85
|
+
*
|
|
86
|
+
* @type {Set<string>}
|
|
87
|
+
*/
|
|
88
|
+
const INTEGER_VALUE_PROPERTIES = new Set([
|
|
89
|
+
'column-count',
|
|
90
|
+
'order',
|
|
91
|
+
'orphans',
|
|
92
|
+
'widows',
|
|
93
|
+
'z-index'
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The inclusive bounds that CSS applies to numeric property values after
|
|
98
|
+
* resolving a math expression.
|
|
99
|
+
*
|
|
100
|
+
* @type {Map<string, {minimum?: number, maximum?: number}>}
|
|
101
|
+
*/
|
|
102
|
+
const NUMERIC_PROPERTY_RANGES = new Map([
|
|
103
|
+
['column-count', { minimum: 1 }],
|
|
104
|
+
['opacity', { minimum: 0, maximum: 1 }]
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Matches a value written as a single negative number, with or without the
|
|
109
|
+
* unit or percent sign that follows its digits.
|
|
110
|
+
*
|
|
111
|
+
* @type {RegExp}
|
|
112
|
+
*/
|
|
113
|
+
const NEGATIVE_NUMBER_VALUE = /^-(?:\d+|\d*\.\d+)(?:[a-z]+|%)?$/i;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Matches a resolved unitless number whose value can be rounded or clamped by
|
|
117
|
+
* a property's numeric range.
|
|
118
|
+
*
|
|
119
|
+
* @type {RegExp}
|
|
120
|
+
*/
|
|
121
|
+
const UNIT_LESS_NUMBER_VALUE = /^[+-]?(?:\d+|\d*\.\d+)$/;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Clamps what a math function resolved to into the range its property
|
|
125
|
+
* accepts. A negative length written out directly makes the declaration
|
|
126
|
+
* invalid and the CSS engine throws it away, but a math function is allowed
|
|
127
|
+
* to resolve to one: its result is clamped to the nearest value the property
|
|
128
|
+
* does take, which on a property that only accepts non-negative values is
|
|
129
|
+
* zero, as the range checking section of the CSS Values specification
|
|
130
|
+
* describes: https://drafts.csswg.org/css-values/#calc-range.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} resolvedValue The value the math function resolved to.
|
|
133
|
+
* @param {string} property The property the math function is declared on.
|
|
134
|
+
* @return {string} The resolved value, held inside the property's range.
|
|
135
|
+
*/
|
|
136
|
+
function clampResolvedValueToPropertyRange (resolvedValue, property) {
|
|
137
|
+
const normalizedProperty = String(property).toLowerCase();
|
|
138
|
+
const acceptsNegativeValues = !NON_NEGATIVE_PROPERTIES.has(normalizedProperty);
|
|
139
|
+
const hasPropertyRange = NUMERIC_PROPERTY_RANGES.has(normalizedProperty);
|
|
140
|
+
const range = NUMERIC_PROPERTY_RANGES.get(normalizedProperty) || {};
|
|
141
|
+
const isNegativeNonNegativeValue = !acceptsNegativeValues && NEGATIVE_NUMBER_VALUE.test(resolvedValue);
|
|
142
|
+
if (isNegativeNonNegativeValue) {
|
|
143
|
+
const isUnitlessNumber = UNIT_LESS_NUMBER_VALUE.test(resolvedValue);
|
|
144
|
+
if (!isUnitlessNumber) {
|
|
145
|
+
return formatResolvedNumber(range.minimum === undefined ? 0 : range.minimum);
|
|
146
|
+
}
|
|
147
|
+
if (!hasPropertyRange && !INTEGER_VALUE_PROPERTIES.has(normalizedProperty)) {
|
|
148
|
+
return '0';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!hasPropertyRange && !INTEGER_VALUE_PROPERTIES.has(normalizedProperty)) {
|
|
152
|
+
return resolvedValue;
|
|
153
|
+
}
|
|
154
|
+
if (!UNIT_LESS_NUMBER_VALUE.test(resolvedValue)) {
|
|
155
|
+
return resolvedValue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const shouldRoundToInteger = INTEGER_VALUE_PROPERTIES.has(normalizedProperty);
|
|
159
|
+
let numericValue = Number(resolvedValue);
|
|
160
|
+
if (shouldRoundToInteger) {
|
|
161
|
+
numericValue = Math.round(numericValue);
|
|
162
|
+
}
|
|
163
|
+
if (range.minimum !== undefined) {
|
|
164
|
+
numericValue = Math.max(range.minimum, numericValue);
|
|
165
|
+
}
|
|
166
|
+
if (range.maximum !== undefined) {
|
|
167
|
+
numericValue = Math.min(range.maximum, numericValue);
|
|
168
|
+
}
|
|
169
|
+
return formatResolvedNumber(numericValue);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export {
|
|
173
|
+
clampResolvedValueToPropertyRange
|
|
174
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Rewrites the contents of CSS math functions into shorter equivalents, such as dropping redundant zeros from decimals and turning reciprocal multiplications into divisions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { findMatchingParenthesis } from '../../parser/source-search.js';
|
|
6
|
+
|
|
7
|
+
import { MATH_FUNCTION_NAMES } from './functions.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Matches a single character that may appear in a CSS function name.
|
|
11
|
+
*
|
|
12
|
+
* @type {RegExp}
|
|
13
|
+
*/
|
|
14
|
+
const FUNCTION_NAME_CHARACTER = /[a-zA-Z-]/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Finds where the function name that ends at an opening parenthesis begins.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} value The CSS value string being scanned.
|
|
20
|
+
* @param {number} openIndex The index of the opening parenthesis.
|
|
21
|
+
* @return {number} The index of the function name's first character.
|
|
22
|
+
*/
|
|
23
|
+
function findFunctionNameStart (value, openIndex) {
|
|
24
|
+
let index = openIndex;
|
|
25
|
+
while (index > 0 && FUNCTION_NAME_CHARACTER.test(value[index - 1])) {
|
|
26
|
+
index--;
|
|
27
|
+
}
|
|
28
|
+
return index;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Rewrites the contents of every math function in a value, leaving the rest of
|
|
33
|
+
* the value untouched. A `*` only means multiplication inside these functions,
|
|
34
|
+
* so an arithmetic rewrite may only reach the text between their parentheses.
|
|
35
|
+
* The whole of a math function is handed over at once, so a math function
|
|
36
|
+
* nested in another is rewritten as part of its parent.
|
|
37
|
+
*
|
|
38
|
+
* @param {function(string): string} rewriteExpression Receives a math function's contents and returns its replacement.
|
|
39
|
+
* @param {string} value The CSS value string to rewrite.
|
|
40
|
+
* @return {string} The value with every math function's contents rewritten.
|
|
41
|
+
*/
|
|
42
|
+
function rewriteMathFunctionContents (rewriteExpression, value) {
|
|
43
|
+
let result = '';
|
|
44
|
+
let index = 0;
|
|
45
|
+
while (index < value.length) {
|
|
46
|
+
const openIndex = value.indexOf('(', index);
|
|
47
|
+
if (openIndex === -1) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
const functionName = value.slice(findFunctionNameStart(value, openIndex), openIndex).toLowerCase();
|
|
51
|
+
const closeIndex = findMatchingParenthesis(value, openIndex);
|
|
52
|
+
if (!MATH_FUNCTION_NAMES.has(functionName) || closeIndex === -1) {
|
|
53
|
+
result += value.slice(index, openIndex + 1);
|
|
54
|
+
index = openIndex + 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
result += value.slice(index, openIndex + 1);
|
|
58
|
+
result += rewriteExpression(value.slice(openIndex + 1, closeIndex));
|
|
59
|
+
index = closeIndex;
|
|
60
|
+
}
|
|
61
|
+
return result + value.slice(index);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Matches a number written with a decimal point that begins a token of its
|
|
66
|
+
* own, capturing its sign, the digits before its decimal point, and the digits
|
|
67
|
+
* after it. The lookbehind keeps the match from starting partway through an
|
|
68
|
+
* identifier (`--x0.5`) or another number, and the lookahead keeps it from
|
|
69
|
+
* matching a fragment of a dotted name such as `0.5.png`.
|
|
70
|
+
*
|
|
71
|
+
* @type {RegExp}
|
|
72
|
+
*/
|
|
73
|
+
const DECIMAL_NUMBER = /(?<![\w.-])([+-]?)(\d*)\.(\d+)(?![\d.])/g;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Writes a decimal number in the fewest characters that state the same value,
|
|
77
|
+
* dropping the zeros before its whole part (`0.25` → `.25`) and after its
|
|
78
|
+
* fraction (`1.50` → `1.5`), along with a decimal point left with no fraction
|
|
79
|
+
* behind it (`2.0` → `2`). The digits that remain are kept as written, so no
|
|
80
|
+
* precision is lost.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} sign The sign written before the number, empty when there is none.
|
|
83
|
+
* @param {string} wholeDigits The digits written before the decimal point.
|
|
84
|
+
* @param {string} fractionDigits The digits written after the decimal point.
|
|
85
|
+
* @return {string} The number written as compactly as it can be.
|
|
86
|
+
*/
|
|
87
|
+
function shortenDecimalNumber (sign, wholeDigits, fractionDigits) {
|
|
88
|
+
// Leading zeros of the whole part
|
|
89
|
+
const wholePart = wholeDigits.replace(/^0+/, '');
|
|
90
|
+
// Trailing zeros of the fraction
|
|
91
|
+
const fractionPart = fractionDigits.replace(/0+$/, '');
|
|
92
|
+
if (!fractionPart) {
|
|
93
|
+
return sign + (wholePart || '0');
|
|
94
|
+
}
|
|
95
|
+
return sign + wholePart + '.' + fractionPart;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Shortens every decimal number in the contents of a math function. Numbers
|
|
100
|
+
* that follow an operator written without whitespace (`x*0.25s`) are out of
|
|
101
|
+
* reach of the general number shortening, which only looks past a separator.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} expression The contents of a math function.
|
|
104
|
+
* @return {string} The expression with every decimal number written as compactly as it can be.
|
|
105
|
+
*/
|
|
106
|
+
function shortenDecimalNumbers (expression) {
|
|
107
|
+
return expression.replace(DECIMAL_NUMBER, (match, sign, wholeDigits, fractionDigits) => {
|
|
108
|
+
return shortenDecimalNumber(sign, wholeDigits, fractionDigits);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Matches a multiplication by a decimal below one, capturing the digits after
|
|
114
|
+
* its decimal point and the unit written on it (so a multiplier with a unit
|
|
115
|
+
* can be recognized and left alone). The trailing lookahead keeps
|
|
116
|
+
* the match away from numbers that only begin this way, such as the exponent
|
|
117
|
+
* of `.5e3` or a unit name that is still being read.
|
|
118
|
+
*
|
|
119
|
+
* @type {RegExp}
|
|
120
|
+
*/
|
|
121
|
+
const FRACTIONAL_MULTIPLIER = /\s*\*\s*0*\.(\d+)([a-z%]*)(?![\w.%(])/gi;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The most decimal places a fraction may be built from while the power of ten
|
|
125
|
+
* it needs is still exactly representable as an integer.
|
|
126
|
+
*
|
|
127
|
+
* @type {number}
|
|
128
|
+
*/
|
|
129
|
+
const MAXIMUM_FRACTION_DIGITS = 15;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Computes the greatest common divisor of two non-negative integers, which is
|
|
133
|
+
* what a fraction has to be divided by to be stated in lowest terms.
|
|
134
|
+
*
|
|
135
|
+
* @param {number} first The first integer.
|
|
136
|
+
* @param {number} second The second integer.
|
|
137
|
+
* @return {number} The greatest common divisor of both integers.
|
|
138
|
+
*/
|
|
139
|
+
function greatestCommonDivisor (first, second) {
|
|
140
|
+
let dividend = first;
|
|
141
|
+
let divisor = second;
|
|
142
|
+
while (divisor) {
|
|
143
|
+
const remainder = dividend % divisor;
|
|
144
|
+
dividend = divisor;
|
|
145
|
+
divisor = remainder;
|
|
146
|
+
}
|
|
147
|
+
return dividend;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Converts the digits behind a decimal point into the whole number that
|
|
152
|
+
* dividing by scales a value exactly as multiplying by that decimal does.
|
|
153
|
+
* The digits `25` (of `.25`) state the fraction 25/100, which reduces to 1/4,
|
|
154
|
+
* so the divisor is `4`. A fraction that does not reduce to a numerator of one
|
|
155
|
+
* cannot be stated as a single division, and so has no divisor to give.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} fractionDigits The digits written after the decimal point.
|
|
158
|
+
* @return {number|null} The equivalent whole number divisor, or null when there is none.
|
|
159
|
+
*/
|
|
160
|
+
function reciprocalOfDecimalFraction (fractionDigits) {
|
|
161
|
+
if (fractionDigits.length > MAXIMUM_FRACTION_DIGITS) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const numerator = Number(fractionDigits);
|
|
165
|
+
const denominator = 10 ** fractionDigits.length;
|
|
166
|
+
const commonFactor = greatestCommonDivisor(numerator, denominator);
|
|
167
|
+
if (numerator / commonFactor !== 1) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return denominator / commonFactor;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Builds the division that scales a value exactly as multiplying it by a
|
|
175
|
+
* unitless decimal below one does, when that division is no longer to write
|
|
176
|
+
* than the multiplication. A multiplier carrying a unit is never rewritten:
|
|
177
|
+
* dividing by a dimension inverts its unit in the type of the result
|
|
178
|
+
* (`x*.25s` is a time, while `x/4s` is per unit of time), so the two would not
|
|
179
|
+
* be equivalent.
|
|
180
|
+
*
|
|
181
|
+
* @param {string} fractionDigits The digits written after the decimal point of the multiplier.
|
|
182
|
+
* @param {string} unit The unit written on the multiplier, empty when it is unitless.
|
|
183
|
+
* @return {string|null} The equivalent division, or null when there is none worth using.
|
|
184
|
+
*/
|
|
185
|
+
function divisionEquivalentToFractionalMultiplier (fractionDigits, unit) {
|
|
186
|
+
if (unit) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const divisor = reciprocalOfDecimalFraction(fractionDigits);
|
|
190
|
+
if (divisor === null) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const division = '/' + divisor;
|
|
194
|
+
// Compared against the shortest the multiplication could have been written
|
|
195
|
+
const multiplication = '*.' + fractionDigits;
|
|
196
|
+
if (division.length > multiplication.length) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
return division;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Rewrites a multiplication by a unitless decimal below one as a division by
|
|
204
|
+
* the whole number that decimal is the reciprocal of, which is never longer to
|
|
205
|
+
* write and usually shorter (`calc(var(--x)*.25)` → `calc(var(--x)/4)`).
|
|
206
|
+
* Multipliers with a unit are left as written, since dividing by a dimension
|
|
207
|
+
* would invert its unit.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} expression The contents of a math function.
|
|
210
|
+
* @return {string} The expression with reciprocal multipliers stated as divisions.
|
|
211
|
+
*/
|
|
212
|
+
function convertReciprocalMultiplicationToDivision (expression) {
|
|
213
|
+
return expression.replace(FRACTIONAL_MULTIPLIER, (match, fractionDigits, unit) => {
|
|
214
|
+
return divisionEquivalentToFractionalMultiplier(fractionDigits, unit) ?? match;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export {
|
|
219
|
+
convertReciprocalMultiplicationToDivision,
|
|
220
|
+
rewriteMathFunctionContents,
|
|
221
|
+
shortenDecimalNumbers
|
|
222
|
+
};
|