@thejaredwilcurt/csslop 0.0.45 → 0.0.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -7
- package/src/declarations/border.js +1 -1
- package/src/parser/source-search.js +7 -2
- package/src/rules/optimize.js +6 -3
- package/src/rules/stringify.js +130 -9
- package/src/value/math-expression.js +67 -14
- package/src/value/math.js +63 -3
- package/src/value/minify.js +38 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@thejaredwilcurt/csslop",
|
|
3
3
|
"main": "index.js",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.47",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"prestart": "node ./scripts/pre/start.js",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"proto": "proto pin node latest --resolve --tool-native && proto pin npm latest --resolve --tool-native && git status"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@codemirror/autocomplete": "^6.20.
|
|
26
|
+
"@codemirror/autocomplete": "^6.20.4",
|
|
27
27
|
"@codemirror/lang-css": "^6.3.1",
|
|
28
|
-
"@codemirror/view": "^6.43.
|
|
28
|
+
"@codemirror/view": "^6.43.15",
|
|
29
29
|
"@eslint/js": "^10.0.1",
|
|
30
30
|
"@node-projects/css-parser": "^5.2.0",
|
|
31
31
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"eslint-config-tjw-import-x": "^1.0.1",
|
|
36
36
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
37
37
|
"eslint-plugin-import-x": "^4.17.0",
|
|
38
|
-
"eslint-plugin-jsdoc": "^65.
|
|
38
|
+
"eslint-plugin-jsdoc": "^65.4.2",
|
|
39
39
|
"fflate": "^0.8.3",
|
|
40
40
|
"globals": "^17.13.0",
|
|
41
41
|
"pretty-ms": "^10.0.0",
|
|
42
42
|
"real-world-css-libraries": "^1.0.8",
|
|
43
|
-
"vite": "^8.3.
|
|
43
|
+
"vite": "^8.3.5"
|
|
44
44
|
},
|
|
45
45
|
"author": "The Jared Wilcurt",
|
|
46
46
|
"homepage": "https://github.com/TheJaredWilcurt/csslop#readme",
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"devEngines": {
|
|
58
58
|
"runtime": {
|
|
59
59
|
"name": "node",
|
|
60
|
-
"version": "26.12.
|
|
60
|
+
"version": "26.12.1"
|
|
61
61
|
},
|
|
62
62
|
"packageManager": {
|
|
63
63
|
"name": "npm",
|
|
64
|
-
"version": "12.2.
|
|
64
|
+
"version": "12.2.1"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -182,7 +182,7 @@ function collapseBorderTrioWithPerEdgeOverrides (declarations, context) {
|
|
|
182
182
|
const borderValue = buildBorderShorthandValue(
|
|
183
183
|
widthComponents.length === 1 ? width : '',
|
|
184
184
|
styleComponents.length === 1 ? style : '',
|
|
185
|
-
colorComponents[0]
|
|
185
|
+
colorComponents.length === 1 ? colorComponents[0] : ''
|
|
186
186
|
) + importantSuffix;
|
|
187
187
|
const overrides = BORDER_TRIO_PROPERTIES.flatMap((property, index) => {
|
|
188
188
|
if (componentsByProperty[index].length === 1) {
|
|
@@ -162,8 +162,8 @@ function startsUrlToken (source, index) {
|
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
164
|
* Finds the first of several delimiter characters that is written at the top
|
|
165
|
-
* level of the text, meaning it is not escaped, quoted,
|
|
166
|
-
* group.
|
|
165
|
+
* level of the text, meaning it is not escaped, quoted, inside a comment, or
|
|
166
|
+
* nested inside a group.
|
|
167
167
|
*
|
|
168
168
|
* @param {string} source The raw CSS text being scanned.
|
|
169
169
|
* @param {string} delimiters The delimiter characters to look for.
|
|
@@ -174,6 +174,11 @@ function findTopLevelDelimiter (source, delimiters, startIndex) {
|
|
|
174
174
|
let index = startIndex;
|
|
175
175
|
while (index < source.length) {
|
|
176
176
|
const character = source[index];
|
|
177
|
+
if (character === '/' && source[index + 1] === '*') {
|
|
178
|
+
const closingIndex = source.indexOf('*/', index + 2);
|
|
179
|
+
index = closingIndex === -1 ? source.length : closingIndex + 2;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
177
182
|
if (character === '\\') {
|
|
178
183
|
index += 2;
|
|
179
184
|
continue;
|
package/src/rules/optimize.js
CHANGED
|
@@ -262,6 +262,9 @@ function createNestedSelectorMatcher (parentSelector) {
|
|
|
262
262
|
|
|
263
263
|
return (childSelector) => {
|
|
264
264
|
const child = childSelector.trim();
|
|
265
|
+
if (child === parent) {
|
|
266
|
+
return '&';
|
|
267
|
+
}
|
|
265
268
|
if (child.startsWith(parent + ':') || child.startsWith(parent + '::')) {
|
|
266
269
|
return '&' + child.slice(parent.length);
|
|
267
270
|
}
|
|
@@ -497,7 +500,7 @@ function factorCommonParents (rules) {
|
|
|
497
500
|
while (index < rules.length) {
|
|
498
501
|
const rule = rules[index];
|
|
499
502
|
if (rule.type === 'rule' && rule.selectors?.length > 1) {
|
|
500
|
-
const candidateParent = extractLeadingCompound(rule.selectors[0]);
|
|
503
|
+
const candidateParent = extractLeadingCompound(rule.selectors[0]) || rule.selectors[0];
|
|
501
504
|
if (candidateParent) {
|
|
502
505
|
const matchNestedSelector = createNestedSelectorMatcher(candidateParent);
|
|
503
506
|
const childSelectors = rule.selectors.map(matchNestedSelector);
|
|
@@ -571,8 +574,8 @@ function factorCommonParents (rules) {
|
|
|
571
574
|
}
|
|
572
575
|
|
|
573
576
|
/**
|
|
574
|
-
* Shares a descendant prefix through `:is()` when
|
|
575
|
-
*
|
|
577
|
+
* Shares a descendant prefix through `:is()` when a nesting wrapper did not
|
|
578
|
+
* save enough space to factor the parent out of the selector list.
|
|
576
579
|
*
|
|
577
580
|
* @param {object} rule The style rule whose selector list may be shortened.
|
|
578
581
|
*/
|
package/src/rules/stringify.js
CHANGED
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { processDeclarations } from '../declarations/process.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
findMatchingParenthesis,
|
|
8
|
+
skipParenthesizedGroup,
|
|
9
|
+
skipQuotedString,
|
|
10
|
+
splitTopLevelCommaList,
|
|
11
|
+
startsUrlToken
|
|
12
|
+
} from '../parser/source-search.js';
|
|
13
|
+
import { tokenizeCssValue } from '../parser/tokenizer.js';
|
|
7
14
|
import { minifyValueForOutput } from '../value/minify.js';
|
|
15
|
+
import { elideRedundantSeparators } from '../value/tokens.js';
|
|
8
16
|
|
|
9
17
|
import {
|
|
10
18
|
collapseCustomPropertyWhitespace,
|
|
@@ -67,7 +75,20 @@ const LAYER_STATEMENT_PATTERN = /^@layer [^{}();'"]*;$/;
|
|
|
67
75
|
*/
|
|
68
76
|
function removeRedundantLayerStatementSemicolon (ruleStrings) {
|
|
69
77
|
const lastIndex = ruleStrings.length - 1;
|
|
70
|
-
if (lastIndex < 0
|
|
78
|
+
if (lastIndex < 0) {
|
|
79
|
+
return ruleStrings;
|
|
80
|
+
}
|
|
81
|
+
const lastRule = ruleStrings[lastIndex];
|
|
82
|
+
const isTrailingLayerStatement = LAYER_STATEMENT_PATTERN.test(lastRule);
|
|
83
|
+
const isTrailingImportStatement = (
|
|
84
|
+
lastRule.endsWith(';') &&
|
|
85
|
+
(
|
|
86
|
+
lastRule.startsWith('@import ') ||
|
|
87
|
+
lastRule.startsWith('@import"') ||
|
|
88
|
+
lastRule.startsWith('@import\'')
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
if (!isTrailingLayerStatement && !isTrailingImportStatement) {
|
|
71
92
|
return ruleStrings;
|
|
72
93
|
}
|
|
73
94
|
const result = [...ruleStrings];
|
|
@@ -75,6 +96,47 @@ function removeRedundantLayerStatementSemicolon (ruleStrings) {
|
|
|
75
96
|
return result;
|
|
76
97
|
}
|
|
77
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Normalizes every `supports()` condition in an import prelude while leaving
|
|
101
|
+
* nested functions and quoted strings to the condition parser.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} importStatement The import prelude being minified.
|
|
104
|
+
* @return {string} The import prelude with normalized conditions.
|
|
105
|
+
*/
|
|
106
|
+
function normalizeImportSupports (importStatement) {
|
|
107
|
+
let result = '';
|
|
108
|
+
let index = 0;
|
|
109
|
+
const normalizedStatement = importStatement.toLowerCase();
|
|
110
|
+
while (index < importStatement.length) {
|
|
111
|
+
let supportsIndex = -1;
|
|
112
|
+
let scanIndex = index;
|
|
113
|
+
while (scanIndex < importStatement.length) {
|
|
114
|
+
if (importStatement[scanIndex] === '"' || importStatement[scanIndex] === '\'') {
|
|
115
|
+
scanIndex = skipQuotedString(importStatement, scanIndex);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (normalizedStatement.startsWith('supports(', scanIndex)) {
|
|
119
|
+
supportsIndex = scanIndex;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
scanIndex++;
|
|
123
|
+
}
|
|
124
|
+
if (supportsIndex === -1) {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
const openIndex = supportsIndex + 'supports'.length;
|
|
128
|
+
const closeIndex = findMatchingParenthesis(importStatement, openIndex);
|
|
129
|
+
if (closeIndex === -1) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
const condition = importStatement.slice(openIndex + 1, closeIndex);
|
|
133
|
+
result += importStatement.slice(index, openIndex + 1);
|
|
134
|
+
result += normalizeSupports(condition) + ')';
|
|
135
|
+
index = closeIndex + 1;
|
|
136
|
+
}
|
|
137
|
+
return result + importStatement.slice(index);
|
|
138
|
+
}
|
|
139
|
+
|
|
78
140
|
/**
|
|
79
141
|
* Recursively stringifies child rules into a concatenated minified CSS string.
|
|
80
142
|
*
|
|
@@ -111,6 +173,67 @@ function minifyFunctionPrelude (prelude) {
|
|
|
111
173
|
return functionName + '(' + minifiedParameters.join(',') + ')';
|
|
112
174
|
}
|
|
113
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Removes comments between namespace tokens while leaving strings, URLs, and
|
|
178
|
+
* escaped characters intact.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} prelude The namespace prelude as written.
|
|
181
|
+
* @return {string} The prelude with comments replaced by separators.
|
|
182
|
+
*/
|
|
183
|
+
function removeNamespaceComments (prelude) {
|
|
184
|
+
let result = '';
|
|
185
|
+
let index = 0;
|
|
186
|
+
while (index < prelude.length) {
|
|
187
|
+
if (prelude.startsWith('/*', index)) {
|
|
188
|
+
const closingIndex = prelude.indexOf('*/', index + 2);
|
|
189
|
+
index = closingIndex === -1 ? prelude.length : closingIndex + 2;
|
|
190
|
+
result += ' ';
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
let end = index + 1;
|
|
194
|
+
if (prelude[index] === '"' || prelude[index] === '\'') {
|
|
195
|
+
end = skipQuotedString(prelude, index);
|
|
196
|
+
} else if (prelude[index] === '\\') {
|
|
197
|
+
end = Math.min(index + 2, prelude.length);
|
|
198
|
+
} else if (startsUrlToken(prelude, index)) {
|
|
199
|
+
end = skipParenthesizedGroup(prelude, index + 3);
|
|
200
|
+
}
|
|
201
|
+
result += prelude.slice(index, end);
|
|
202
|
+
index = end;
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Unwraps quoted namespace URLs and removes token separators without changing
|
|
209
|
+
* whitespace or escapes inside the namespace name's string.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} prelude The optional namespace prefix and its URL.
|
|
212
|
+
* @return {string} The minified namespace statement.
|
|
213
|
+
*/
|
|
214
|
+
function stringifyNamespace (prelude) {
|
|
215
|
+
const tokens = tokenizeCssValue(removeNamespaceComments(prelude)).filter((token) => {
|
|
216
|
+
return token.type !== 'whitespace';
|
|
217
|
+
});
|
|
218
|
+
const parts = ['@namespace'];
|
|
219
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
220
|
+
const token = tokens[index];
|
|
221
|
+
const isQuotedUrl = (
|
|
222
|
+
token.type === 'function' &&
|
|
223
|
+
token.text.toLowerCase() === 'url(' &&
|
|
224
|
+
tokens[index + 1]?.type === 'string' &&
|
|
225
|
+
tokens[index + 2]?.text === ')'
|
|
226
|
+
);
|
|
227
|
+
if (isQuotedUrl) {
|
|
228
|
+
parts.push(tokens[index + 1].text);
|
|
229
|
+
index += 2;
|
|
230
|
+
} else {
|
|
231
|
+
parts.push(token.text);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return elideRedundantSeparators(parts.join(' ')) + ';';
|
|
235
|
+
}
|
|
236
|
+
|
|
114
237
|
/**
|
|
115
238
|
* Stringifies a generic `at-rule` AST node into minified CSS, with specialized
|
|
116
239
|
* prelude minification for `@function` rules and generic whitespace handling
|
|
@@ -466,6 +589,10 @@ function stringifyRule (rule, context) {
|
|
|
466
589
|
return '@charset ' + rule.charset + ';';
|
|
467
590
|
}
|
|
468
591
|
|
|
592
|
+
if (rule.type === 'namespace') {
|
|
593
|
+
return stringifyNamespace(rule.namespace);
|
|
594
|
+
}
|
|
595
|
+
|
|
469
596
|
if (rule.type === 'import') {
|
|
470
597
|
let importStatement = rule.import;
|
|
471
598
|
// Unwrap url() with a quoted string to just the quoted string
|
|
@@ -478,13 +605,7 @@ function stringifyRule (rule, context) {
|
|
|
478
605
|
importStatement = importStatement.replace(/^(".*?"|'.*?') /, '$1');
|
|
479
606
|
// Remove space between closing paren and next at-rule condition keyword
|
|
480
607
|
importStatement = importStatement.replace(/\) ([a-zA-Z])/g, ')$1');
|
|
481
|
-
|
|
482
|
-
importStatement = importStatement.replace(
|
|
483
|
-
/supports\(([^()]*)\)/g,
|
|
484
|
-
(fullMatch, content) => {
|
|
485
|
-
return 'supports(' + content.replace(/\s*:\s*/g, ':') + ')';
|
|
486
|
-
}
|
|
487
|
-
);
|
|
608
|
+
importStatement = normalizeImportSupports(importStatement);
|
|
488
609
|
const startsWithQuote = importStatement.startsWith('"') || importStatement.startsWith('\'');
|
|
489
610
|
let importSeparator;
|
|
490
611
|
if (startsWithQuote) {
|
|
@@ -66,17 +66,29 @@ const MAXIMUM_NESTING_DEPTH = 32;
|
|
|
66
66
|
* length is held as the pixels it measures, since every length stating the
|
|
67
67
|
* same measurement then totals under the same unit.
|
|
68
68
|
*
|
|
69
|
-
* @param {number}
|
|
70
|
-
* @param {string}
|
|
71
|
-
* @
|
|
69
|
+
* @param {number} number The number the quantity is written from.
|
|
70
|
+
* @param {string} unit The unit the quantity is written with, or the empty string when it is a plain number.
|
|
71
|
+
* @param {boolean} preserveWrittenQuantity Whether to retain the original spelling for a standalone quantity.
|
|
72
|
+
* @return {Map} The unit totals the quantity states.
|
|
72
73
|
*/
|
|
73
|
-
function createUnitTotals (number, unit) {
|
|
74
|
+
function createUnitTotals (number, unit, preserveWrittenQuantity = false) {
|
|
74
75
|
const lowercasedUnit = unit.toLowerCase();
|
|
75
76
|
const pixels = convertAbsoluteLengthToPx(number, lowercasedUnit);
|
|
77
|
+
let totals;
|
|
76
78
|
if (pixels === null) {
|
|
77
|
-
|
|
79
|
+
totals = new Map([[lowercasedUnit, number]]);
|
|
80
|
+
} else {
|
|
81
|
+
totals = new Map([['px', pixels]]);
|
|
82
|
+
}
|
|
83
|
+
if (preserveWrittenQuantity) {
|
|
84
|
+
totals.writtenQuantity = {
|
|
85
|
+
number,
|
|
86
|
+
unit: lowercasedUnit,
|
|
87
|
+
canonicalUnit: pixels === null ? lowercasedUnit : 'px',
|
|
88
|
+
canonicalNumber: pixels === null ? number : pixels
|
|
89
|
+
};
|
|
78
90
|
}
|
|
79
|
-
return
|
|
91
|
+
return totals;
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
/**
|
|
@@ -162,6 +174,16 @@ function multiplyUnitTotals (left, right) {
|
|
|
162
174
|
if (isPlainNumber(left)) {
|
|
163
175
|
return scaleUnitTotals(right, left.get(UNITLESS));
|
|
164
176
|
}
|
|
177
|
+
const leftQuantity = readSingleQuantity(left);
|
|
178
|
+
const rightQuantity = readSingleQuantity(right);
|
|
179
|
+
if (leftQuantity && rightQuantity) {
|
|
180
|
+
if (leftQuantity.unit.startsWith('reciprocal:') && leftQuantity.unit.slice('reciprocal:'.length) === rightQuantity.unit) {
|
|
181
|
+
return createUnitTotals(leftQuantity.number * rightQuantity.number, UNITLESS);
|
|
182
|
+
}
|
|
183
|
+
if (rightQuantity.unit.startsWith('reciprocal:') && rightQuantity.unit.slice('reciprocal:'.length) === leftQuantity.unit) {
|
|
184
|
+
return createUnitTotals(leftQuantity.number * rightQuantity.number, UNITLESS);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
165
187
|
return null;
|
|
166
188
|
}
|
|
167
189
|
|
|
@@ -175,14 +197,28 @@ function multiplyUnitTotals (left, right) {
|
|
|
175
197
|
* @return {Map|null} The totals of the quotient, or null when the division cannot be worked out.
|
|
176
198
|
*/
|
|
177
199
|
function divideUnitTotals (left, right) {
|
|
178
|
-
if (
|
|
179
|
-
|
|
200
|
+
if (isPlainNumber(right)) {
|
|
201
|
+
const divisor = right.get(UNITLESS);
|
|
202
|
+
if (divisor === 0) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return scaleUnitTotals(left, 1 / divisor);
|
|
180
206
|
}
|
|
181
|
-
const
|
|
182
|
-
|
|
207
|
+
const dividendQuantity = readSingleQuantity(left);
|
|
208
|
+
const divisorQuantity = readSingleQuantity(right);
|
|
209
|
+
if (!dividendQuantity || !divisorQuantity || divisorQuantity.number === 0) {
|
|
183
210
|
return null;
|
|
184
211
|
}
|
|
185
|
-
|
|
212
|
+
if (dividendQuantity.unit === divisorQuantity.unit) {
|
|
213
|
+
return createUnitTotals(dividendQuantity.number / divisorQuantity.number, UNITLESS);
|
|
214
|
+
}
|
|
215
|
+
if (dividendQuantity.unit === UNITLESS) {
|
|
216
|
+
return createUnitTotals(
|
|
217
|
+
dividendQuantity.number / divisorQuantity.number,
|
|
218
|
+
'reciprocal:' + divisorQuantity.unit
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
186
222
|
}
|
|
187
223
|
|
|
188
224
|
/**
|
|
@@ -478,7 +514,7 @@ function parseOperand (reader) {
|
|
|
478
514
|
}
|
|
479
515
|
if (token.type === 'number') {
|
|
480
516
|
reader.index++;
|
|
481
|
-
return createUnitTotals(token.number, token.unit);
|
|
517
|
+
return createUnitTotals(token.number, token.unit, true);
|
|
482
518
|
}
|
|
483
519
|
if (token.type === 'identifier') {
|
|
484
520
|
reader.index++;
|
|
@@ -661,7 +697,8 @@ function evaluateMinimum (argumentList) {
|
|
|
661
697
|
if (!comparable) {
|
|
662
698
|
return null;
|
|
663
699
|
}
|
|
664
|
-
|
|
700
|
+
const winningIndex = comparable.numbers.indexOf(Math.min(...comparable.numbers));
|
|
701
|
+
return argumentList[winningIndex].value;
|
|
665
702
|
}
|
|
666
703
|
|
|
667
704
|
/**
|
|
@@ -675,7 +712,8 @@ function evaluateMaximum (argumentList) {
|
|
|
675
712
|
if (!comparable) {
|
|
676
713
|
return null;
|
|
677
714
|
}
|
|
678
|
-
|
|
715
|
+
const winningIndex = comparable.numbers.indexOf(Math.max(...comparable.numbers));
|
|
716
|
+
return argumentList[winningIndex].value;
|
|
679
717
|
}
|
|
680
718
|
|
|
681
719
|
/**
|
|
@@ -1043,7 +1081,22 @@ function writeTermNumber (total) {
|
|
|
1043
1081
|
* @return {string|null} The value the calculation resolved to, or null when it cannot be written faithfully.
|
|
1044
1082
|
*/
|
|
1045
1083
|
function formatUnitTotals (totals) {
|
|
1084
|
+
const writtenQuantity = totals.writtenQuantity;
|
|
1085
|
+
if (
|
|
1086
|
+
writtenQuantity &&
|
|
1087
|
+
totals.size === 1 &&
|
|
1088
|
+
totals.has(writtenQuantity.canonicalUnit) &&
|
|
1089
|
+
totals.get(writtenQuantity.canonicalUnit) === writtenQuantity.canonicalNumber
|
|
1090
|
+
) {
|
|
1091
|
+
return formatResolvedNumber(writtenQuantity.number) + writtenQuantity.unit;
|
|
1092
|
+
}
|
|
1046
1093
|
const mergedTotals = mergeUnitsOfSameKind(totals);
|
|
1094
|
+
const hasUnrepresentableReciprocalUnit = [...mergedTotals.keys()].some((unit) => {
|
|
1095
|
+
return unit.startsWith('reciprocal:');
|
|
1096
|
+
});
|
|
1097
|
+
if (hasUnrepresentableReciprocalUnit) {
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1047
1100
|
const outputTerms = collectOutputTerms(mergedTotals);
|
|
1048
1101
|
if (dropsPercentageBasis(mergedTotals, outputTerms)) {
|
|
1049
1102
|
return null;
|
package/src/value/math.js
CHANGED
|
@@ -453,6 +453,30 @@ const NON_NEGATIVE_PROPERTIES = new Set([
|
|
|
453
453
|
'width'
|
|
454
454
|
]);
|
|
455
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Properties whose computed math values must be integers.
|
|
458
|
+
*
|
|
459
|
+
* @type {Set<string>}
|
|
460
|
+
*/
|
|
461
|
+
const INTEGER_VALUE_PROPERTIES = new Set([
|
|
462
|
+
'column-count',
|
|
463
|
+
'order',
|
|
464
|
+
'orphans',
|
|
465
|
+
'widows',
|
|
466
|
+
'z-index'
|
|
467
|
+
]);
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* The inclusive bounds that CSS applies to numeric property values after
|
|
471
|
+
* resolving a math expression.
|
|
472
|
+
*
|
|
473
|
+
* @type {Map<string, {minimum?: number, maximum?: number}>}
|
|
474
|
+
*/
|
|
475
|
+
const NUMERIC_PROPERTY_RANGES = new Map([
|
|
476
|
+
['column-count', { minimum: 1 }],
|
|
477
|
+
['opacity', { minimum: 0, maximum: 1 }]
|
|
478
|
+
]);
|
|
479
|
+
|
|
456
480
|
/**
|
|
457
481
|
* Matches a value written as a single negative number, with or without the
|
|
458
482
|
* unit or percent sign that follows its digits.
|
|
@@ -461,6 +485,14 @@ const NON_NEGATIVE_PROPERTIES = new Set([
|
|
|
461
485
|
*/
|
|
462
486
|
const NEGATIVE_NUMBER_VALUE = /^-(?:\d+|\d*\.\d+)(?:[a-z]+|%)?$/i;
|
|
463
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Matches a resolved unitless number whose value can be rounded or clamped by
|
|
490
|
+
* a property's numeric range.
|
|
491
|
+
*
|
|
492
|
+
* @type {RegExp}
|
|
493
|
+
*/
|
|
494
|
+
const UNIT_LESS_NUMBER_VALUE = /^[+-]?(?:\d+|\d*\.\d+)$/;
|
|
495
|
+
|
|
464
496
|
/**
|
|
465
497
|
* Clamps what a math function resolved to into the range its property
|
|
466
498
|
* accepts. A negative length written out directly makes the declaration
|
|
@@ -475,11 +507,39 @@ const NEGATIVE_NUMBER_VALUE = /^-(?:\d+|\d*\.\d+)(?:[a-z]+|%)?$/i;
|
|
|
475
507
|
* @return {string} The resolved value, held inside the property's range.
|
|
476
508
|
*/
|
|
477
509
|
function clampResolvedValueToPropertyRange (resolvedValue, property) {
|
|
478
|
-
const
|
|
479
|
-
|
|
510
|
+
const normalizedProperty = String(property).toLowerCase();
|
|
511
|
+
const acceptsNegativeValues = !NON_NEGATIVE_PROPERTIES.has(normalizedProperty);
|
|
512
|
+
const hasPropertyRange = NUMERIC_PROPERTY_RANGES.has(normalizedProperty);
|
|
513
|
+
const range = NUMERIC_PROPERTY_RANGES.get(normalizedProperty) || {};
|
|
514
|
+
const isNegativeNonNegativeValue = !acceptsNegativeValues && NEGATIVE_NUMBER_VALUE.test(resolvedValue);
|
|
515
|
+
if (isNegativeNonNegativeValue) {
|
|
516
|
+
const isUnitlessNumber = UNIT_LESS_NUMBER_VALUE.test(resolvedValue);
|
|
517
|
+
if (!isUnitlessNumber) {
|
|
518
|
+
return formatResolvedNumber(range.minimum === undefined ? 0 : range.minimum);
|
|
519
|
+
}
|
|
520
|
+
if (!hasPropertyRange && !INTEGER_VALUE_PROPERTIES.has(normalizedProperty)) {
|
|
521
|
+
return '0';
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (!hasPropertyRange && !INTEGER_VALUE_PROPERTIES.has(normalizedProperty)) {
|
|
480
525
|
return resolvedValue;
|
|
481
526
|
}
|
|
482
|
-
|
|
527
|
+
if (!UNIT_LESS_NUMBER_VALUE.test(resolvedValue)) {
|
|
528
|
+
return resolvedValue;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const shouldRoundToInteger = INTEGER_VALUE_PROPERTIES.has(normalizedProperty);
|
|
532
|
+
let numericValue = Number(resolvedValue);
|
|
533
|
+
if (shouldRoundToInteger) {
|
|
534
|
+
numericValue = Math.round(numericValue);
|
|
535
|
+
}
|
|
536
|
+
if (range.minimum !== undefined) {
|
|
537
|
+
numericValue = Math.max(range.minimum, numericValue);
|
|
538
|
+
}
|
|
539
|
+
if (range.maximum !== undefined) {
|
|
540
|
+
numericValue = Math.min(range.maximum, numericValue);
|
|
541
|
+
}
|
|
542
|
+
return formatResolvedNumber(numericValue);
|
|
483
543
|
}
|
|
484
544
|
|
|
485
545
|
/**
|
package/src/value/minify.js
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import {
|
|
39
39
|
collapseShorthandParts,
|
|
40
40
|
convertAbsoluteLengthToPx,
|
|
41
|
+
formatCompactNumber,
|
|
41
42
|
formatResolvedNumber,
|
|
42
43
|
hasSubstitutedParts,
|
|
43
44
|
normalizeScaleComponent,
|
|
@@ -623,6 +624,40 @@ function isCssDigit (character) {
|
|
|
623
624
|
return character >= '0' && character <= '9';
|
|
624
625
|
}
|
|
625
626
|
|
|
627
|
+
/**
|
|
628
|
+
* Converts OKLCH lightness and chroma percentages when doing so is shorter,
|
|
629
|
+
* including the separator each spelling needs before the hue.
|
|
630
|
+
*
|
|
631
|
+
* @param {string} value The normalized arguments of an `oklch()` function.
|
|
632
|
+
* @return {string} The arguments with shorter percentage spellings.
|
|
633
|
+
*/
|
|
634
|
+
function minifyOklchPercentages (value) {
|
|
635
|
+
const slashIndex = value.indexOf('/');
|
|
636
|
+
const componentsText = slashIndex === -1 ? value : value.slice(0, slashIndex);
|
|
637
|
+
const alphaText = slashIndex === -1 ? '' : value.slice(slashIndex + 1);
|
|
638
|
+
const components = componentsText.trim().split(' ');
|
|
639
|
+
if (components.length !== 3) {
|
|
640
|
+
return value;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
let [lightness, chroma, hue] = components;
|
|
644
|
+
if (lightness.endsWith('%')) {
|
|
645
|
+
lightness = formatCompactNumber(Number.parseFloat(lightness) / 100);
|
|
646
|
+
}
|
|
647
|
+
if (chroma.endsWith('%')) {
|
|
648
|
+
const convertedChroma = formatCompactNumber(
|
|
649
|
+
Number.parseFloat(chroma) / 100 * OKLCH_CHROMA_PERCENT_REFERENCE
|
|
650
|
+
);
|
|
651
|
+
const convertedWithHueSeparator = convertedChroma.length + 1;
|
|
652
|
+
if (chroma.length < convertedWithHueSeparator) {
|
|
653
|
+
return [lightness, chroma + hue].join(' ') + (slashIndex === -1 ? '' : '/' + alphaText);
|
|
654
|
+
}
|
|
655
|
+
chroma = convertedChroma;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return [lightness, chroma, hue].join(' ') + (slashIndex === -1 ? '' : '/' + alphaText);
|
|
659
|
+
}
|
|
660
|
+
|
|
626
661
|
/**
|
|
627
662
|
* Converts `oklch()` colors that fall inside the sRGB gamut to their hex
|
|
628
663
|
* equivalent when that is shorter. Out-of-gamut colors have no sRGB
|
|
@@ -747,6 +782,9 @@ function convertColorsToHex (val) {
|
|
|
747
782
|
return integer;
|
|
748
783
|
});
|
|
749
784
|
minified = compactNegativeColorChannelSpacing(func, minified);
|
|
785
|
+
if (func.toLowerCase() === 'oklch') {
|
|
786
|
+
minified = minifyOklchPercentages(minified);
|
|
787
|
+
}
|
|
750
788
|
return func + '(' + minified.trim() + ')';
|
|
751
789
|
});
|
|
752
790
|
|