@thejaredwilcurt/csslop 0.0.49 → 0.0.50

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.
Files changed (55) hide show
  1. package/README.md +0 -1
  2. package/package.json +1 -1
  3. package/src/declarations/border.js +1 -1
  4. package/src/index.js +7 -7
  5. package/src/rules/optimize/at-rules.js +113 -0
  6. package/src/rules/optimize/declaration-writes.js +70 -0
  7. package/src/rules/optimize/empty-rules.js +51 -0
  8. package/src/rules/optimize/expand-nesting.js +236 -0
  9. package/src/rules/optimize/factor-parents.js +343 -0
  10. package/src/rules/optimize/index.js +71 -0
  11. package/src/rules/optimize/merge-declarations.js +199 -0
  12. package/src/rules/optimize/merge-nested-rules.js +117 -0
  13. package/src/rules/optimize/merge-selectors.js +252 -0
  14. package/src/rules/optimize/nest-rules.js +202 -0
  15. package/src/rules/optimize/override-index.js +264 -0
  16. package/src/rules/optimize/prune-overridden.js +267 -0
  17. package/src/rules/optimize/selector-normalization.js +60 -0
  18. package/src/rules/optimize/specificity.js +280 -0
  19. package/src/{position-try.js → rules/position-try.js} +1 -1
  20. package/src/utilities.js +14 -0
  21. package/src/value/border.js +85 -0
  22. package/src/value/{colors.js → color/conversion.js} +3 -2
  23. package/src/value/color/functions.js +463 -0
  24. package/src/value/{light-dark.js → color/light-dark.js} +1 -1
  25. package/src/value/{color-mix.js → color/mix.js} +3 -3
  26. package/src/value/{relative-colors.js → color/relative.js} +3 -3
  27. package/src/value/color/shorten.js +93 -0
  28. package/src/value/gradients.js +1 -1
  29. package/src/value/hyphenate-limit-chars.js +72 -0
  30. package/src/value/initial-keywords.js +128 -0
  31. package/src/value/lengths.js +55 -0
  32. package/src/value/math/evaluate.js +36 -0
  33. package/src/value/math/fold.js +315 -0
  34. package/src/value/math/format.js +230 -0
  35. package/src/value/math/functions.js +302 -0
  36. package/src/value/math/index.js +219 -0
  37. package/src/value/math/parser.js +301 -0
  38. package/src/value/math/property-ranges.js +174 -0
  39. package/src/value/math/rewrite.js +222 -0
  40. package/src/value/math/tokenizer.js +160 -0
  41. package/src/value/math/unit-totals.js +194 -0
  42. package/src/value/minify.js +127 -1669
  43. package/src/value/numbers.js +28 -0
  44. package/src/value/positions.js +152 -0
  45. package/src/value/property-optimizations.js +485 -0
  46. package/src/value/segments.js +74 -0
  47. package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
  48. package/src/value/{svg-markup.js → svg/markup.js} +1 -1
  49. package/src/value/time.js +75 -0
  50. package/src/value/urls.js +242 -0
  51. package/src/value/whitespace.js +88 -0
  52. package/src/rules/optimize.js +0 -2314
  53. package/src/value/math-expression.js +0 -1156
  54. package/src/value/math.js +0 -886
  55. /package/src/value/{named-colors.js → color/named.js} +0 -0
@@ -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
+ };
@@ -0,0 +1,160 @@
1
+ /**
2
+ * @file Splits a CSS math function into the numbers, identifiers, function names, operators, and punctuation it is written from.
3
+ */
4
+
5
+ import {
6
+ PERCENTAGE,
7
+ UNITLESS
8
+ } from './unit-totals.js';
9
+
10
+ /**
11
+ * Matches a CSS number where the scanner stands: the digits it is written
12
+ * from, together with the fraction and the exponent it may be written with.
13
+ * A sign is left to the parser, which tells the sign of a number from an
14
+ * operator by whether whitespace stands between the two.
15
+ *
16
+ * @type {RegExp}
17
+ */
18
+ const NUMBER_AT_INDEX = /(?:\d+\.\d+|\.\d+|\d+)(?:e[+-]?\d+)?/iy;
19
+
20
+ /**
21
+ * Matches a CSS identifier where the scanner stands: a name of letters,
22
+ * digits, underscores, and hyphens that does not begin with a digit. A numeric
23
+ * constant, a function name, and a keyword argument are all written as
24
+ * identifiers.
25
+ *
26
+ * @type {RegExp}
27
+ */
28
+ const IDENTIFIER_AT_INDEX = /[a-z_][a-z0-9_-]*/iy;
29
+
30
+ /**
31
+ * Matches the unit written against a number where the scanner stands. Every
32
+ * CSS unit is named in letters alone, so a name holding anything else is a
33
+ * token of its own that a number cannot carry.
34
+ *
35
+ * @type {RegExp}
36
+ */
37
+ const UNIT_AT_INDEX = /[a-z]+/iy;
38
+
39
+ /**
40
+ * The characters CSS counts as whitespace, which separates the tokens of a
41
+ * calculation and tells an operator apart from the sign of a number.
42
+ *
43
+ * @type {Set<string>}
44
+ */
45
+ const WHITESPACE_CHARACTERS = new Set([' ', '\t', '\n', '\r', '\f']);
46
+
47
+ /**
48
+ * The characters that punctuate a calculation, and the kind of token each of
49
+ * them stands for.
50
+ *
51
+ * @type {Map<string, string>}
52
+ */
53
+ const PUNCTUATION_TOKEN_TYPES = new Map([
54
+ ['(', 'open'],
55
+ [')', 'close'],
56
+ [',', 'comma']
57
+ ]);
58
+
59
+ /**
60
+ * The arithmetic operators a calculation is written with.
61
+ *
62
+ * @type {Set<string>}
63
+ */
64
+ const OPERATOR_CHARACTERS = new Set(['+', '-', '*', '/']);
65
+
66
+ /**
67
+ * Reads the unit written directly against the number that ends at the given
68
+ * index, which is a percent sign or an identifier naming the unit. A number
69
+ * with nothing written against it carries no unit.
70
+ *
71
+ * @param {string} text The math function being tokenized.
72
+ * @param {number} index The index just past the number's digits.
73
+ * @return {object} The unit read and the index just past it.
74
+ */
75
+ function readUnitAtIndex (text, index) {
76
+ if (text[index] === PERCENTAGE) {
77
+ return { unit: PERCENTAGE, nextIndex: index + 1 };
78
+ }
79
+ UNIT_AT_INDEX.lastIndex = index;
80
+ const unitMatch = UNIT_AT_INDEX.exec(text);
81
+ if (!unitMatch) {
82
+ return { unit: UNITLESS, nextIndex: index };
83
+ }
84
+ return { unit: unitMatch[0], nextIndex: index + unitMatch[0].length };
85
+ }
86
+
87
+ /**
88
+ * Splits a math function into the tokens it is written from: its numbers with
89
+ * the units they carry, its identifiers, its function names, its operators,
90
+ * and its punctuation. Each token records whether whitespace precedes it,
91
+ * since that is what tells the sign of a number from an operator.
92
+ *
93
+ * @param {string} text The math function as it is written.
94
+ * @return {Array|null} The tokens of the math function, or null when it holds a character no calculation is written with.
95
+ */
96
+ function tokenizeMathExpression (text) {
97
+ const tokens = [];
98
+ let index = 0;
99
+ let precededBySpace = false;
100
+
101
+ while (index < text.length) {
102
+ const character = text[index];
103
+
104
+ if (WHITESPACE_CHARACTERS.has(character)) {
105
+ precededBySpace = true;
106
+ index++;
107
+ continue;
108
+ }
109
+
110
+ NUMBER_AT_INDEX.lastIndex = index;
111
+ const numberMatch = NUMBER_AT_INDEX.exec(text);
112
+ if (numberMatch) {
113
+ const { unit, nextIndex } = readUnitAtIndex(text, index + numberMatch[0].length);
114
+ tokens.push({
115
+ type: 'number',
116
+ number: Number(numberMatch[0]),
117
+ unit,
118
+ precededBySpace
119
+ });
120
+ index = nextIndex;
121
+ precededBySpace = false;
122
+ continue;
123
+ }
124
+
125
+ if (OPERATOR_CHARACTERS.has(character) || PUNCTUATION_TOKEN_TYPES.has(character)) {
126
+ tokens.push({
127
+ type: PUNCTUATION_TOKEN_TYPES.get(character) || 'operator',
128
+ operator: character,
129
+ precededBySpace
130
+ });
131
+ index++;
132
+ precededBySpace = false;
133
+ continue;
134
+ }
135
+
136
+ IDENTIFIER_AT_INDEX.lastIndex = index;
137
+ const identifierMatch = IDENTIFIER_AT_INDEX.exec(text);
138
+ if (!identifierMatch) {
139
+ return null;
140
+ }
141
+ index = index + identifierMatch[0].length;
142
+ // A name written directly against an opening parenthesis calls a function
143
+ const callsFunction = text[index] === '(';
144
+ if (callsFunction) {
145
+ index++;
146
+ }
147
+ tokens.push({
148
+ type: callsFunction ? 'function' : 'identifier',
149
+ name: identifierMatch[0].toLowerCase(),
150
+ precededBySpace
151
+ });
152
+ precededBySpace = false;
153
+ }
154
+
155
+ return tokens;
156
+ }
157
+
158
+ export {
159
+ tokenizeMathExpression
160
+ };
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @file Represents the value of a CSS calculation as totals keyed by unit, and works out the sums, products, and quotients of such values.
3
+ */
4
+
5
+ import { convertAbsoluteLengthToPx } from '../shared.js';
6
+
7
+ /**
8
+ * The unit a plain number is totalled under. A number carries no unit, and the
9
+ * empty string is the unit it does not carry.
10
+ *
11
+ * @type {string}
12
+ */
13
+ const UNITLESS = '';
14
+
15
+ /**
16
+ * The unit a percentage is totalled under. A percentage is only resolved
17
+ * against its basis once the element is laid out, so it is totalled as a unit
18
+ * of its own rather than as the number it is written from.
19
+ *
20
+ * @type {string}
21
+ */
22
+ const PERCENTAGE = '%';
23
+
24
+ /**
25
+ * Creates the totals of a value that states a single quantity, holding the
26
+ * number it is written from under the unit it is written with. An absolute
27
+ * length is held as the pixels it measures, since every length stating the
28
+ * same measurement then totals under the same unit.
29
+ *
30
+ * @param {number} number The number the quantity is written from.
31
+ * @param {string} unit The unit the quantity is written with, or the empty string when it is a plain number.
32
+ * @param {boolean} preserveWrittenQuantity Whether to retain the original spelling for a standalone quantity.
33
+ * @return {Map} The unit totals the quantity states.
34
+ */
35
+ function createUnitTotals (number, unit, preserveWrittenQuantity = false) {
36
+ const lowercasedUnit = unit.toLowerCase();
37
+ const pixels = convertAbsoluteLengthToPx(number, lowercasedUnit);
38
+ let totals;
39
+ if (pixels === null) {
40
+ totals = new Map([[lowercasedUnit, number]]);
41
+ } else {
42
+ totals = new Map([['px', pixels]]);
43
+ }
44
+ if (preserveWrittenQuantity) {
45
+ totals.writtenQuantity = {
46
+ number,
47
+ unit: lowercasedUnit,
48
+ canonicalUnit: pixels === null ? lowercasedUnit : 'px',
49
+ canonicalNumber: pixels === null ? number : pixels
50
+ };
51
+ }
52
+ return totals;
53
+ }
54
+
55
+ /**
56
+ * Reports whether a value states a plain number, which is what an operand has
57
+ * to be to scale or divide the quantity it is written against.
58
+ *
59
+ * @param {Map} value The unit totals the value holds.
60
+ * @return {boolean} Whether the value states a plain number.
61
+ */
62
+ function isPlainNumber (value) {
63
+ return value.size === 1 && value.has(UNITLESS);
64
+ }
65
+
66
+ /**
67
+ * Reads a value as the single quantity it states, which is what a function
68
+ * comparing its arguments needs each of them to be. A value totalling more
69
+ * than one unit is only resolved once its units are resolved against the
70
+ * element, so it states no single number to read.
71
+ *
72
+ * @param {Map} value The unit totals the value holds.
73
+ * @return {object|null} The number and the unit the value states, or null when it states more than one quantity.
74
+ */
75
+ function readSingleQuantity (value) {
76
+ if (value.size !== 1) {
77
+ return null;
78
+ }
79
+ const [[unit, number]] = [...value];
80
+ return { number, unit };
81
+ }
82
+
83
+ /**
84
+ * Scales every quantity a value states by a plain number, which is how a
85
+ * multiplication and a division are worked out.
86
+ *
87
+ * @param {Map} value The unit totals the value holds.
88
+ * @param {number} factor The number every total is scaled by.
89
+ * @return {Map} The scaled unit totals.
90
+ */
91
+ function scaleUnitTotals (value, factor) {
92
+ const scaled = new Map();
93
+ for (const [unit, coefficient] of value) {
94
+ scaled.set(unit, coefficient * factor);
95
+ }
96
+ return scaled;
97
+ }
98
+
99
+ /**
100
+ * Adds one value to another, totalling the quantities they state under the
101
+ * units they are written with. A sum of a plain number and a quantity carrying
102
+ * a unit states nothing a stylesheet can hold, so such a sum has no total to
103
+ * give.
104
+ *
105
+ * @param {Map} left The unit totals the value on the left holds.
106
+ * @param {Map} right The unit totals the value on the right holds.
107
+ * @param {number} sign The direction the right value is added in: 1 to add it, -1 to subtract it.
108
+ * @return {Map|null} The totals of the sum, or null when the two cannot be added.
109
+ */
110
+ function addUnitTotals (left, right, sign) {
111
+ const totals = new Map(left);
112
+ for (const [unit, coefficient] of right) {
113
+ totals.set(unit, (totals.get(unit) || 0) + (coefficient * sign));
114
+ }
115
+ const addsNumberToQuantity = totals.has(UNITLESS) && totals.size > 1;
116
+ if (addsNumberToQuantity) {
117
+ return null;
118
+ }
119
+ return totals;
120
+ }
121
+
122
+ /**
123
+ * Multiplies two values, which CSS only allows when one of them is a plain
124
+ * number: the product of two quantities carrying units measures an area or a
125
+ * rate, and no CSS property takes one.
126
+ *
127
+ * @param {Map} left The unit totals the value on the left holds.
128
+ * @param {Map} right The unit totals the value on the right holds.
129
+ * @return {Map|null} The totals of the product, or null when neither side is a plain number.
130
+ */
131
+ function multiplyUnitTotals (left, right) {
132
+ if (isPlainNumber(right)) {
133
+ return scaleUnitTotals(left, right.get(UNITLESS));
134
+ }
135
+ if (isPlainNumber(left)) {
136
+ return scaleUnitTotals(right, left.get(UNITLESS));
137
+ }
138
+ const leftQuantity = readSingleQuantity(left);
139
+ const rightQuantity = readSingleQuantity(right);
140
+ if (leftQuantity && rightQuantity) {
141
+ if (leftQuantity.unit.startsWith('reciprocal:') && leftQuantity.unit.slice('reciprocal:'.length) === rightQuantity.unit) {
142
+ return createUnitTotals(leftQuantity.number * rightQuantity.number, UNITLESS);
143
+ }
144
+ if (rightQuantity.unit.startsWith('reciprocal:') && rightQuantity.unit.slice('reciprocal:'.length) === leftQuantity.unit) {
145
+ return createUnitTotals(leftQuantity.number * rightQuantity.number, UNITLESS);
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+
151
+ /**
152
+ * Divides a value by another, which CSS only allows when the divisor is a
153
+ * plain number. A division by zero resolves to an infinity that no property
154
+ * takes, so it is left for the stylesheet to state as written.
155
+ *
156
+ * @param {Map} left The unit totals of the value being divided.
157
+ * @param {Map} right The unit totals of the value dividing it.
158
+ * @return {Map|null} The totals of the quotient, or null when the division cannot be worked out.
159
+ */
160
+ function divideUnitTotals (left, right) {
161
+ if (isPlainNumber(right)) {
162
+ const divisor = right.get(UNITLESS);
163
+ if (divisor === 0) {
164
+ return null;
165
+ }
166
+ return scaleUnitTotals(left, 1 / divisor);
167
+ }
168
+ const dividendQuantity = readSingleQuantity(left);
169
+ const divisorQuantity = readSingleQuantity(right);
170
+ if (!dividendQuantity || !divisorQuantity || divisorQuantity.number === 0) {
171
+ return null;
172
+ }
173
+ if (dividendQuantity.unit === divisorQuantity.unit) {
174
+ return createUnitTotals(dividendQuantity.number / divisorQuantity.number, UNITLESS);
175
+ }
176
+ if (dividendQuantity.unit === UNITLESS) {
177
+ return createUnitTotals(
178
+ dividendQuantity.number / divisorQuantity.number,
179
+ 'reciprocal:' + divisorQuantity.unit
180
+ );
181
+ }
182
+ return null;
183
+ }
184
+
185
+ export {
186
+ addUnitTotals,
187
+ createUnitTotals,
188
+ divideUnitTotals,
189
+ multiplyUnitTotals,
190
+ PERCENTAGE,
191
+ readSingleQuantity,
192
+ scaleUnitTotals,
193
+ UNITLESS
194
+ };