@thejaredwilcurt/csslop 0.0.48 → 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.
- package/README.md +3 -4
- package/package.json +3 -3
- package/src/declarations/border.js +1 -1
- package/src/declarations/css-wide-keywords.js +37 -1
- package/src/declarations/fold.js +13 -3
- package/src/declarations/process.js +60 -7
- package/src/index.js +12 -7
- package/src/parser/source-search.js +6 -0
- 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/selectors.js +9 -0
- package/src/rules/stringify.js +138 -76
- package/src/utilities.js +9 -8
- 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 +162 -1678
- 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 -2031
- 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,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
|
+
};
|