@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,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Folds the constant arithmetic of a `calc()` into the shortest way of writing the value it states, weighing the resolved value against the calculation itself.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
convertAbsoluteLengthToPx,
|
|
7
|
+
formatResolvedDimension,
|
|
8
|
+
formatResolvedNumber
|
|
9
|
+
} from '../shared.js';
|
|
10
|
+
|
|
11
|
+
import { simplifyCalc } from './evaluate.js';
|
|
12
|
+
import { formatUnitTotals } from './format.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Matches a whole expression that divides a percentage by a plain number,
|
|
16
|
+
* capturing the percentage and the divisor it is shared out between.
|
|
17
|
+
*
|
|
18
|
+
* @type {RegExp}
|
|
19
|
+
*/
|
|
20
|
+
const PERCENTAGE_DIVISION = /^([+-]?(?:\d+|\d*\.\d+))%\s*\/\s*([+-]?(?:\d+|\d*\.\d+))$/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Picks between what a calculation resolved to and the calculation itself,
|
|
24
|
+
* keeping whichever takes fewer characters to write. A tie goes to the
|
|
25
|
+
* resolved value, which states the same thing without the arithmetic.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} resolved The value the calculation resolves to.
|
|
28
|
+
* @param {string} calculation The calculation, written as compactly as it can be.
|
|
29
|
+
* @return {string} The shorter of the two.
|
|
30
|
+
*/
|
|
31
|
+
function shorterOfResolutionAndCalculation (resolved, calculation) {
|
|
32
|
+
if (resolved.length <= calculation.length) {
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
return calculation;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Writes a calculation as compactly as its syntax allows, squeezing out every
|
|
40
|
+
* space but the ones a `+` or a `-` needs to be read as an operator.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} inner The contents of the `calc()`.
|
|
43
|
+
* @return {string} The whole `calc()`, written as compactly as it can be.
|
|
44
|
+
*/
|
|
45
|
+
function compactCalculation (inner) {
|
|
46
|
+
// Remove whitespace around multiplication and division operators
|
|
47
|
+
const compactInner = inner.replace(/\s+/g, ' ').trim().replace(/\s*([*/])\s*/g, '$1');
|
|
48
|
+
return 'calc(' + compactInner + ')';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Picks between a calculation and what resolving its arithmetic came to. A
|
|
53
|
+
* resolved value states the same thing without any arithmetic, so it wins a
|
|
54
|
+
* tie, but a result that is itself a calculation only states the same terms
|
|
55
|
+
* another way, and writing them out again for no saving gains nothing.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} resolved The value the calculation resolved to.
|
|
58
|
+
* @param {string} calculation The calculation, written as compactly as it can be.
|
|
59
|
+
* @return {string} Whichever of the two the value is written as.
|
|
60
|
+
*/
|
|
61
|
+
function chooseResolvedCalculation (resolved, calculation) {
|
|
62
|
+
// A resolution that kept its function form left terms behind that only
|
|
63
|
+
// resolve once the element they are measured against is laid out
|
|
64
|
+
if (resolved.startsWith('calc(') && resolved.length >= calculation.length) {
|
|
65
|
+
return calculation;
|
|
66
|
+
}
|
|
67
|
+
return shorterOfResolutionAndCalculation(resolved, calculation);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolves a percentage divided by a number into the single percentage it
|
|
72
|
+
* equals, which is how `calc(100%/16)` comes to be written as `6.25%`. The
|
|
73
|
+
* division stays as it was written when the result is no shorter, and when
|
|
74
|
+
* the divisor is zero, since that resolves to nothing a stylesheet can state.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} expression The contents of a `calc()`, with its whitespace collapsed.
|
|
77
|
+
* @return {string|null} The shorter of the resolved percentage and the division, or null when the expression is not one.
|
|
78
|
+
*/
|
|
79
|
+
function resolvePercentageDivision (expression) {
|
|
80
|
+
const division = expression.match(PERCENTAGE_DIVISION);
|
|
81
|
+
if (!division) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const [, percentage, divisor] = division;
|
|
85
|
+
const calculation = 'calc(' + percentage + '%/' + divisor + ')';
|
|
86
|
+
if (Number(divisor) === 0) {
|
|
87
|
+
return calculation;
|
|
88
|
+
}
|
|
89
|
+
const resolved = formatResolvedNumber(Number(percentage) / Number(divisor)) + '%';
|
|
90
|
+
return shorterOfResolutionAndCalculation(resolved, calculation);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Attempts to simplify a calc() expression by combining like-unit terms and evaluating pure arithmetic, returning the simplified string or null if folding is not possible.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} expression The expression inside calc() to attempt folding.
|
|
97
|
+
* @return {string|null} The simplified expression, or null if it cannot be folded.
|
|
98
|
+
*/
|
|
99
|
+
function tryFoldCalcExpression (expression) {
|
|
100
|
+
let folded = expression.trim();
|
|
101
|
+
let previous;
|
|
102
|
+
|
|
103
|
+
do {
|
|
104
|
+
previous = folded;
|
|
105
|
+
// Remove innermost non-nested parentheses (flatten simple grouping)
|
|
106
|
+
folded = folded.replace(/\(([^()]+)\)/g, '$1');
|
|
107
|
+
// Fold: <number> * <number><unit> → computed result in same unit
|
|
108
|
+
folded = folded.replace(/(-?(?:\d*\.\d+|\d+))\s*\*\s*(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q|%)/gi, (match, multiplier, amount, unit) => {
|
|
109
|
+
return formatResolvedDimension(parseFloat(multiplier) * parseFloat(amount), unit);
|
|
110
|
+
});
|
|
111
|
+
// Fold: <number><unit> * <number> → computed result in same unit
|
|
112
|
+
folded = folded.replace(/(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q|%)\s*\*\s*(-?(?:\d*\.\d+|\d+))/gi, (match, amount, unit, multiplier) => {
|
|
113
|
+
return formatResolvedDimension(parseFloat(multiplier) * parseFloat(amount), unit);
|
|
114
|
+
});
|
|
115
|
+
// Fold: <number><unit> / <number> → computed result in same unit
|
|
116
|
+
folded = folded.replace(/(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q)\s*\/\s*(-?(?:\d*\.\d+|\d+))/gi, (match, value, unit, divisor) => {
|
|
117
|
+
return formatResolvedDimension(parseFloat(value) / parseFloat(divisor), unit);
|
|
118
|
+
});
|
|
119
|
+
// Collapse zero-with-unit terms (e.g. 0px, 0%) to plain 0 or remove them
|
|
120
|
+
folded = folded.replace(/(^|[+-])\s*0(?:px|pt|pc|in|cm|mm|q|%)\b/g, (match, sign) => {
|
|
121
|
+
if (sign && sign !== '+') {
|
|
122
|
+
return sign + ' 0';
|
|
123
|
+
}
|
|
124
|
+
return '';
|
|
125
|
+
});
|
|
126
|
+
// Remove additive zero terms
|
|
127
|
+
folded = folded.replace(/\+\s*0\b/g, '');
|
|
128
|
+
// Remove subtractive zero terms
|
|
129
|
+
folded = folded.replace(/-\s*0\b/g, '');
|
|
130
|
+
// Collapse whitespace
|
|
131
|
+
folded = folded.replace(/\s+/g, ' ').trim();
|
|
132
|
+
} while (folded !== previous);
|
|
133
|
+
|
|
134
|
+
// Simplify trivial identity division: 1 / 1 / <dimension> → <dimension>
|
|
135
|
+
if (/^1\s*\/\s*1\s*\/\s*(-?(?:\d*\.\d+|\d+)(?:px|pt|pc|in|cm|mm|q))$/i.test(folded)) {
|
|
136
|
+
return folded.replace(/^1\s*\/\s*1\s*\/\s*/i, '');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Remove all whitespace for validation
|
|
140
|
+
const normalized = folded.replace(/\s+/g, '');
|
|
141
|
+
// Validate that the expression is a simple sequence of signed terms with optional units
|
|
142
|
+
if (!/^[+-]?(?:\d*\.\d+|\d+)(?:[a-z%]+)?(?:[+-](?:\d*\.\d+|\d+)(?:[a-z%]+)?)*$/i.test(normalized)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Extract each signed term with its optional unit
|
|
147
|
+
const terms = normalized.match(/[+-]?(?:\d*\.\d+|\d+)(?:[a-z%]+)?/gi) || [];
|
|
148
|
+
const totals = new Map();
|
|
149
|
+
|
|
150
|
+
for (const term of terms) {
|
|
151
|
+
// Parse each term into sign, number, and unit parts
|
|
152
|
+
const match = term.match(/^([+-]?)(\d*\.\d+|\d+)([a-z%]+)?$/i);
|
|
153
|
+
if (!match) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const [, sign, rawNumber, rawUnit = ''] = match;
|
|
157
|
+
let number = parseFloat(rawNumber) * (sign === '-' ? -1 : 1);
|
|
158
|
+
let unit = rawUnit.toLowerCase();
|
|
159
|
+
|
|
160
|
+
if (unit && unit !== '%' && unit !== 'px') {
|
|
161
|
+
const pxValue = convertAbsoluteLengthToPx(number, unit);
|
|
162
|
+
if (pxValue === null) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
number = pxValue;
|
|
166
|
+
unit = 'px';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
totals.set(unit, (totals.get(unit) || 0) + number);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return formatUnitTotals(totals);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Matches a whole expression that scales a percentage by a plain number,
|
|
177
|
+
* written either way around. Both operands are already as short as they can
|
|
178
|
+
* be, so the expression is only ever squeezed, never resolved.
|
|
179
|
+
*
|
|
180
|
+
* @type {RegExp}
|
|
181
|
+
*/
|
|
182
|
+
const PERCENTAGE_MULTIPLICATION = /^(?:-?(?:\d*\.\d+|\d+)%\s*\*\s*-?(?:\d*\.\d+|\d+)|-?(?:\d*\.\d+|\d+)\s*\*\s*-?(?:\d*\.\d+|\d+)%)$/;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Matches a value that is a single percentage, which is all a calculation
|
|
186
|
+
* leaves behind once every term of it has resolved into one.
|
|
187
|
+
*
|
|
188
|
+
* @type {RegExp}
|
|
189
|
+
*/
|
|
190
|
+
const RESOLVED_PERCENTAGE = /^-?(?:\d+|\d*\.\d+)%$/;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Chooses how a calculation that shares a percentage out resolves. The
|
|
194
|
+
* library states the result to the full precision a float carries, which is
|
|
195
|
+
* both longer than the calculation it replaces and finer than a stylesheet
|
|
196
|
+
* has any use for, so the result is rounded into the budget a resolved number
|
|
197
|
+
* is written within and kept only when it is the shorter of the two.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} simplified The result the calculation library returned.
|
|
200
|
+
* @param {string} compactInner The contents of the `calc()`, with its whitespace collapsed.
|
|
201
|
+
* @param {string} match The whole `calc()` as it was written.
|
|
202
|
+
* @return {string} How the resolved calculation is written.
|
|
203
|
+
*/
|
|
204
|
+
function chooseResolvedPercentage (simplified, compactInner, match) {
|
|
205
|
+
// A division sign written after a percentage, which is what the resolved percentage was shared out by
|
|
206
|
+
const dividesAPercentage = RESOLVED_PERCENTAGE.test(simplified) && /%\s*\//.test(match);
|
|
207
|
+
if (!dividesAPercentage) {
|
|
208
|
+
return simplified;
|
|
209
|
+
}
|
|
210
|
+
// Remove whitespace around division operator
|
|
211
|
+
const calculation = 'calc(' + compactInner.replace(/\s*\/\s*/g, '/') + ')';
|
|
212
|
+
const resolved = formatResolvedNumber(parseFloat(simplified)) + '%';
|
|
213
|
+
return shorterOfResolutionAndCalculation(resolved, calculation);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Resolves the contents of a `calc()` into the shortest way of writing the
|
|
218
|
+
* same value, which is either the value the arithmetic works out to or the
|
|
219
|
+
* calculation itself with the whitespace squeezed out of it.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} match The whole `calc()` as it was written.
|
|
222
|
+
* @param {string} compactInner The contents of the `calc()`, with its whitespace collapsed.
|
|
223
|
+
* @return {string} The shortest way of writing what the calculation states.
|
|
224
|
+
*/
|
|
225
|
+
function resolveCalcExpression (match, compactInner) {
|
|
226
|
+
if (PERCENTAGE_MULTIPLICATION.test(compactInner)) {
|
|
227
|
+
return compactCalculation(compactInner);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const dividedPercentage = resolvePercentageDivision(compactInner);
|
|
231
|
+
if (dividedPercentage) {
|
|
232
|
+
return dividedPercentage;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const folded = tryFoldCalcExpression(compactInner);
|
|
236
|
+
if (folded) {
|
|
237
|
+
return folded;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const simplified = simplifyCalc(match);
|
|
241
|
+
if (simplified === null) {
|
|
242
|
+
return match;
|
|
243
|
+
}
|
|
244
|
+
const resolved = chooseResolvedPercentage(simplified, compactInner, match);
|
|
245
|
+
return chooseResolvedCalculation(resolved, compactCalculation(compactInner));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Simplifies a standalone calc() value by resolving its arithmetic, flattening nested calc expressions, converting absolute length units to pixels, and folding constant terms.
|
|
250
|
+
*
|
|
251
|
+
* @param {string} value The CSS value string, already known to be a single `calc()`.
|
|
252
|
+
* @return {string} The simplified value, or the original value if simplification is not applicable.
|
|
253
|
+
*/
|
|
254
|
+
function foldStandaloneCalc (value) {
|
|
255
|
+
let inner = value.slice(5, -1).trim();
|
|
256
|
+
|
|
257
|
+
// Preserve percent-times-number expressions, only stripping whitespace around operators
|
|
258
|
+
if (PERCENTAGE_MULTIPLICATION.test(inner.replace(/\s+/g, ' ').trim())) {
|
|
259
|
+
return compactCalculation(inner);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Resolve the calculation as it was written, which is the only reading of it
|
|
263
|
+
// that keeps the grouping its parentheses state. Everything below reads a
|
|
264
|
+
// flattened copy of the expression, where that grouping is already gone.
|
|
265
|
+
const resolved = simplifyCalc(value);
|
|
266
|
+
if (resolved !== null) {
|
|
267
|
+
return chooseResolvedCalculation(resolved, compactCalculation(inner));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// If no nested function calls and no multiplication/division involving parens, try flattening
|
|
271
|
+
if (!/[A-Za-z-]+\(/.test(inner) && !/[*/]\s*\(|\)\s*[*/]/.test(inner)) {
|
|
272
|
+
// Remove all parentheses and collapse whitespace for folding
|
|
273
|
+
const flattened = inner.replace(/[()]/g, '').replace(/\s+/g, ' ').trim();
|
|
274
|
+
const foldedFlattened = tryFoldCalcExpression(flattened);
|
|
275
|
+
if (foldedFlattened) {
|
|
276
|
+
return foldedFlattened;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let previous;
|
|
281
|
+
|
|
282
|
+
do {
|
|
283
|
+
previous = inner;
|
|
284
|
+
// Fold innermost parenthesized sub-expressions that aren't function calls
|
|
285
|
+
inner = inner.replace(/(^|[^A-Za-z-])\(([^()]+)\)/g, (match, prefix, content) => {
|
|
286
|
+
const trimmed = content.trim();
|
|
287
|
+
const folded = tryFoldCalcExpression(trimmed);
|
|
288
|
+
if (folded) {
|
|
289
|
+
if (folded.startsWith('calc(') && folded.endsWith(')')) {
|
|
290
|
+
return prefix + trimmed;
|
|
291
|
+
}
|
|
292
|
+
return prefix + folded;
|
|
293
|
+
}
|
|
294
|
+
return prefix + trimmed;
|
|
295
|
+
});
|
|
296
|
+
} while (inner !== previous);
|
|
297
|
+
|
|
298
|
+
const folded = tryFoldCalcExpression(inner);
|
|
299
|
+
if (folded) {
|
|
300
|
+
return folded;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Collapse whitespace and check for percent-division expressions
|
|
304
|
+
const dividedPercentage = resolvePercentageDivision(inner.replace(/\s+/g, ' ').trim());
|
|
305
|
+
if (dividedPercentage) {
|
|
306
|
+
return dividedPercentage;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return compactCalculation(inner);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export {
|
|
313
|
+
foldStandaloneCalc,
|
|
314
|
+
resolveCalcExpression
|
|
315
|
+
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Writes the unit totals a calculation resolved to as the shortest CSS value that states them.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { formatResolvedNumber } from '../shared.js';
|
|
6
|
+
|
|
7
|
+
import { PERCENTAGE } from './unit-totals.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The smallest total a term is written with. A term totalling less than this
|
|
11
|
+
* is the rounding error binary floating point arithmetic leaves behind rather
|
|
12
|
+
* than a quantity the stylesheet states.
|
|
13
|
+
*
|
|
14
|
+
* @type {number}
|
|
15
|
+
*/
|
|
16
|
+
const NEGLIGIBLE_TOTAL = 1e-12;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The unit each kind of quantity is canonically stated in, and how much of
|
|
20
|
+
* that unit one of the unit written measures. Absolute lengths are absent
|
|
21
|
+
* because every length is already totalled in the pixels it measures.
|
|
22
|
+
*
|
|
23
|
+
* @type {Map<string, {unit: string, perUnit: number}>}
|
|
24
|
+
*/
|
|
25
|
+
const CANONICAL_UNITS = new Map([
|
|
26
|
+
['s', { unit: 's', perUnit: 1 }],
|
|
27
|
+
['ms', { unit: 's', perUnit: 1 / 1000 }],
|
|
28
|
+
['deg', { unit: 'deg', perUnit: 1 }],
|
|
29
|
+
['grad', { unit: 'deg', perUnit: 360 / 400 }],
|
|
30
|
+
['rad', { unit: 'deg', perUnit: 180 / Math.PI }],
|
|
31
|
+
['turn', { unit: 'deg', perUnit: 360 }],
|
|
32
|
+
['hz', { unit: 'hz', perUnit: 1 }],
|
|
33
|
+
['khz', { unit: 'hz', perUnit: 1000 }],
|
|
34
|
+
['dppx', { unit: 'dppx', perUnit: 1 }],
|
|
35
|
+
['x', { unit: 'dppx', perUnit: 1 }],
|
|
36
|
+
['dpi', { unit: 'dppx', perUnit: 1 / 96 }],
|
|
37
|
+
['dpcm', { unit: 'dppx', perUnit: 2.54 / 96 }]
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Counts how many units of each kind of quantity a calculation totalled, so
|
|
42
|
+
* that the kinds it states in more than one unit can be told from the ones it
|
|
43
|
+
* states in a single unit.
|
|
44
|
+
*
|
|
45
|
+
* @param {Map} totals The unit totals the calculation resolved to.
|
|
46
|
+
* @return {Map} How many units each canonical unit was totalled from.
|
|
47
|
+
*/
|
|
48
|
+
function countUnitsByKind (totals) {
|
|
49
|
+
const counts = new Map();
|
|
50
|
+
for (const unit of totals.keys()) {
|
|
51
|
+
const conversion = CANONICAL_UNITS.get(unit);
|
|
52
|
+
if (!conversion) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
counts.set(conversion.unit, (counts.get(conversion.unit) || 0) + 1);
|
|
56
|
+
}
|
|
57
|
+
return counts;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Restates the totals of every kind of quantity a calculation measured in two
|
|
62
|
+
* units of, so that terms measuring the same thing are totalled together
|
|
63
|
+
* (`calc(1s - 200ms)` comes to `.8s`). A kind measured in a single unit keeps
|
|
64
|
+
* that unit: restating `2ms` as `.002s` measures the same time in more
|
|
65
|
+
* characters than it was written with.
|
|
66
|
+
*
|
|
67
|
+
* @param {Map} totals The unit totals the calculation resolved to.
|
|
68
|
+
* @return {Map} The totals, with each kind of quantity stated in one unit.
|
|
69
|
+
*/
|
|
70
|
+
function mergeUnitsOfSameKind (totals) {
|
|
71
|
+
const unitCounts = countUnitsByKind(totals);
|
|
72
|
+
const measuredInSeveralUnits = [...unitCounts.values()].some((count) => {
|
|
73
|
+
return count > 1;
|
|
74
|
+
});
|
|
75
|
+
if (!measuredInSeveralUnits) {
|
|
76
|
+
return totals;
|
|
77
|
+
}
|
|
78
|
+
const merged = new Map();
|
|
79
|
+
for (const [unit, total] of totals) {
|
|
80
|
+
const conversion = CANONICAL_UNITS.get(unit);
|
|
81
|
+
const restatesUnit = conversion && unitCounts.get(conversion.unit) > 1;
|
|
82
|
+
const mergedUnit = restatesUnit ? conversion.unit : unit;
|
|
83
|
+
const mergedTotal = restatesUnit ? total * conversion.perUnit : total;
|
|
84
|
+
merged.set(mergedUnit, (merged.get(mergedUnit) || 0) + mergedTotal);
|
|
85
|
+
}
|
|
86
|
+
return merged;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Collects the terms a resolved calculation is written from, leaving out the
|
|
91
|
+
* ones that totalled to nothing. A unit is totalled from where it is first
|
|
92
|
+
* written, so the terms keep the order the calculation states them in, which
|
|
93
|
+
* is both the order the author chose and one fewer difference between what
|
|
94
|
+
* was written and what is written back out.
|
|
95
|
+
*
|
|
96
|
+
* @param {Map} totals The unit totals the calculation resolved to.
|
|
97
|
+
* @return {Array} The terms to write, each holding its unit and its total.
|
|
98
|
+
*/
|
|
99
|
+
function collectOutputTerms (totals) {
|
|
100
|
+
const terms = [];
|
|
101
|
+
for (const [unit, total] of totals) {
|
|
102
|
+
if (Math.abs(total) < NEGLIGIBLE_TOTAL) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
terms.push({ total, unit });
|
|
106
|
+
}
|
|
107
|
+
return leadWithAddedTerm(terms);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Moves an added term to the front of a calculation that a subtracted one
|
|
112
|
+
* leads. Every term but the first is written after the operator adding or
|
|
113
|
+
* subtracting it, so a term written first states its own sign, and the minus
|
|
114
|
+
* sign of a subtracted one is a character the sum saves by being led by a
|
|
115
|
+
* term that is added instead.
|
|
116
|
+
*
|
|
117
|
+
* @param {Array} terms The terms to write, in the order the calculation states them.
|
|
118
|
+
* @return {Array} The same terms, led by one that is added whenever any of them is.
|
|
119
|
+
*/
|
|
120
|
+
function leadWithAddedTerm (terms) {
|
|
121
|
+
if (!terms.length || terms[0].total > 0) {
|
|
122
|
+
return terms;
|
|
123
|
+
}
|
|
124
|
+
const leadingIndex = terms.findIndex((term) => {
|
|
125
|
+
return term.total > 0;
|
|
126
|
+
});
|
|
127
|
+
if (leadingIndex === -1) {
|
|
128
|
+
return terms;
|
|
129
|
+
}
|
|
130
|
+
const reordered = [...terms];
|
|
131
|
+
const [addedTerm] = reordered.splice(leadingIndex, 1);
|
|
132
|
+
reordered.unshift(addedTerm);
|
|
133
|
+
return reordered;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reports whether writing only the terms collected would drop a percentage the
|
|
138
|
+
* calculation was written with. A percentage that cancels out still ties the
|
|
139
|
+
* value to the basis the percentage is resolved against, so a calculation
|
|
140
|
+
* keeping other terms cannot be written without it.
|
|
141
|
+
*
|
|
142
|
+
* @param {Map} totals The unit totals the calculation resolved to.
|
|
143
|
+
* @param {Array} outputTerms The terms that would be written.
|
|
144
|
+
* @return {boolean} Whether a percentage the calculation states would be lost.
|
|
145
|
+
*/
|
|
146
|
+
function dropsPercentageBasis (totals, outputTerms) {
|
|
147
|
+
if (!totals.has(PERCENTAGE) || !outputTerms.length) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return !outputTerms.some((term) => {
|
|
151
|
+
return term.unit === PERCENTAGE;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Writes the total of a term as the number a stylesheet states it with. A
|
|
157
|
+
* total that no written number states faithfully — one that is infinite, one
|
|
158
|
+
* beyond the integers a double holds exactly, and one that rounds away to
|
|
159
|
+
* nothing — has no number to be written as.
|
|
160
|
+
*
|
|
161
|
+
* @param {number} total The total the term resolved to.
|
|
162
|
+
* @return {string|null} The number as it is written into the stylesheet, or null when it cannot be written.
|
|
163
|
+
*/
|
|
164
|
+
function writeTermNumber (total) {
|
|
165
|
+
if (!Number.isFinite(total) || Math.abs(total) > Number.MAX_SAFE_INTEGER) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const written = formatResolvedNumber(total);
|
|
169
|
+
const roundsAwayToNothing = total !== 0 && parseFloat(written) === 0;
|
|
170
|
+
if (roundsAwayToNothing) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return written;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Writes the unit totals a calculation resolved to as the shortest value
|
|
178
|
+
* stating them: the lone quantity they came to, or, when units that only
|
|
179
|
+
* resolve against the element are left over, a `calc()` summing what remains.
|
|
180
|
+
*
|
|
181
|
+
* @param {Map} totals The unit totals the calculation resolved to.
|
|
182
|
+
* @return {string|null} The value the calculation resolved to, or null when it cannot be written faithfully.
|
|
183
|
+
*/
|
|
184
|
+
function formatUnitTotals (totals) {
|
|
185
|
+
const writtenQuantity = totals.writtenQuantity;
|
|
186
|
+
if (
|
|
187
|
+
writtenQuantity &&
|
|
188
|
+
totals.size === 1 &&
|
|
189
|
+
totals.has(writtenQuantity.canonicalUnit) &&
|
|
190
|
+
totals.get(writtenQuantity.canonicalUnit) === writtenQuantity.canonicalNumber
|
|
191
|
+
) {
|
|
192
|
+
return formatResolvedNumber(writtenQuantity.number) + writtenQuantity.unit;
|
|
193
|
+
}
|
|
194
|
+
const mergedTotals = mergeUnitsOfSameKind(totals);
|
|
195
|
+
const hasUnrepresentableReciprocalUnit = [...mergedTotals.keys()].some((unit) => {
|
|
196
|
+
return unit.startsWith('reciprocal:');
|
|
197
|
+
});
|
|
198
|
+
if (hasUnrepresentableReciprocalUnit) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const outputTerms = collectOutputTerms(mergedTotals);
|
|
202
|
+
if (dropsPercentageBasis(mergedTotals, outputTerms)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
if (!outputTerms.length) {
|
|
206
|
+
return '0';
|
|
207
|
+
}
|
|
208
|
+
const [leadingTerm, ...trailingTerms] = outputTerms;
|
|
209
|
+
const leadingNumber = writeTermNumber(leadingTerm.total);
|
|
210
|
+
if (leadingNumber === null) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
let written = leadingNumber + leadingTerm.unit;
|
|
214
|
+
for (const term of trailingTerms) {
|
|
215
|
+
const number = writeTermNumber(Math.abs(term.total));
|
|
216
|
+
if (number === null) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
// Both operators have to be surrounded by whitespace to be read as one
|
|
220
|
+
written = written + (term.total < 0 ? ' - ' : ' + ') + number + term.unit;
|
|
221
|
+
}
|
|
222
|
+
if (trailingTerms.length) {
|
|
223
|
+
return 'calc(' + written + ')';
|
|
224
|
+
}
|
|
225
|
+
return written;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export {
|
|
229
|
+
formatUnitTotals
|
|
230
|
+
};
|