@thejaredwilcurt/csslop 0.0.52 → 0.0.54
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 +6 -6
- package/src/declarations/border.js +28 -3
- package/src/declarations/process.js +16 -0
- package/src/rules/normalize.js +12 -0
- package/src/rules/optimize/at-rules.js +18 -1
- package/src/rules/optimize/factor-parents.js +111 -53
- package/src/rules/optimize/index.js +3 -1
- package/src/rules/optimize/merge-declarations.js +217 -2
- package/src/value/math/index.js +11 -9
- package/src/value/math/rewrite.js +6 -1
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.54",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"prestart": "node ./scripts/pre/start.js",
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
"codemirror": "^6.0.2",
|
|
33
33
|
"eslint": "^10.14.0",
|
|
34
34
|
"eslint-config-tjw-base": "^5.0.0",
|
|
35
|
-
"eslint-config-tjw-import-x": "^1.0.
|
|
35
|
+
"eslint-config-tjw-import-x": "^1.0.2",
|
|
36
36
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
37
|
-
"eslint-plugin-import-x": "^4.
|
|
38
|
-
"eslint-plugin-jsdoc": "^65.4.
|
|
37
|
+
"eslint-plugin-import-x": "^4.18.0",
|
|
38
|
+
"eslint-plugin-jsdoc": "^65.4.5",
|
|
39
39
|
"fflate": "^0.8.3",
|
|
40
40
|
"globals": "^17.14.0",
|
|
41
41
|
"pretty-ms": "^10.0.0",
|
|
42
42
|
"real-world-css-libraries": "^1.0.9",
|
|
43
|
-
"vite": "^8.3.
|
|
43
|
+
"vite": "^8.3.6"
|
|
44
44
|
},
|
|
45
45
|
"author": "The Jared Wilcurt",
|
|
46
46
|
"homepage": "https://github.com/TheJaredWilcurt/csslop#readme",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"packageManager": {
|
|
63
63
|
"name": "npm",
|
|
64
|
-
"version": "12.2.
|
|
64
|
+
"version": "12.2.2"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -12,13 +12,34 @@ import {
|
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
14
|
BORDER_EDGE_PROPERTIES,
|
|
15
|
-
CSS_WIDE_KEYWORDS
|
|
15
|
+
CSS_WIDE_KEYWORDS,
|
|
16
|
+
expandToLeafProperties
|
|
16
17
|
} from './config.js';
|
|
17
18
|
import { collectDeclaredProperties } from './lookup.js';
|
|
18
19
|
import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
|
|
19
20
|
|
|
20
21
|
const BORDER_TRIO_PROPERTIES = ['border-width', 'border-style', 'border-color'];
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Every property the `border` shorthand sets: the physical edge shorthands, such
|
|
25
|
+
* as `border-top`, and the longhands they expand into, such as `border-top-width`.
|
|
26
|
+
* A declaration of one of them beside the trio competes with the shorthand.
|
|
27
|
+
*
|
|
28
|
+
* @type {Set<string>}
|
|
29
|
+
*/
|
|
30
|
+
const BORDER_SHORTHAND_PROPERTIES = new Set([
|
|
31
|
+
...BORDER_EDGE_PROPERTIES,
|
|
32
|
+
...expandToLeafProperties('border')
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Matches the logical border longhands, such as `border-inline-start-color`
|
|
37
|
+
* and `border-block-width`, which set the same edges as the physical ones.
|
|
38
|
+
*
|
|
39
|
+
* @type {RegExp}
|
|
40
|
+
*/
|
|
41
|
+
const LOGICAL_BORDER_PATTERN = /^border-(?:inline|block)(?:-|$)/;
|
|
42
|
+
|
|
22
43
|
/**
|
|
23
44
|
* The keywords that state a border line style.
|
|
24
45
|
*
|
|
@@ -140,7 +161,7 @@ function collapseBorderTrioWithPerEdgeOverrides (declarations, context) {
|
|
|
140
161
|
return entry.property === property;
|
|
141
162
|
}).length !== 1;
|
|
142
163
|
}
|
|
143
|
-
return property === 'border' || (property
|
|
164
|
+
return property === 'border' || BORDER_SHORTHAND_PROPERTIES.has(property) || LOGICAL_BORDER_PATTERN.test(property);
|
|
144
165
|
});
|
|
145
166
|
if (hasCompetingBorderWrites) {
|
|
146
167
|
return declarations;
|
|
@@ -179,10 +200,14 @@ function collapseBorderTrioWithPerEdgeOverrides (declarations, context) {
|
|
|
179
200
|
}
|
|
180
201
|
|
|
181
202
|
const [widthComponents, styleComponents, colorComponents] = componentsByProperty;
|
|
203
|
+
// The `border` shorthand resets its color to currentcolor, so a color that
|
|
204
|
+
// states currentcolor is left out of the shorthand rather than repeated.
|
|
205
|
+
const statesShorthandColor = colorComponents.length === 1 &&
|
|
206
|
+
colorComponents[0].toLowerCase() !== INITIAL_BORDER_EDGE_PARTS['border-color'];
|
|
182
207
|
const borderValue = buildBorderShorthandValue(
|
|
183
208
|
widthComponents.length === 1 ? width : '',
|
|
184
209
|
styleComponents.length === 1 ? style : '',
|
|
185
|
-
|
|
210
|
+
statesShorthandColor ? colorComponents[0] : ''
|
|
186
211
|
) + importantSuffix;
|
|
187
212
|
const overrides = BORDER_TRIO_PROPERTIES.flatMap((property, index) => {
|
|
188
213
|
if (componentsByProperty[index].length === 1) {
|
|
@@ -186,6 +186,17 @@ function usesVendorPrefixedDisplayValue (value) {
|
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Checks if a declaration value contains the IE6-IE9 double !important hack.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} value The minified CSS value string.
|
|
193
|
+
* @return {boolean} Whether the value contains double !important.
|
|
194
|
+
*/
|
|
195
|
+
function hasDoubleImportant (value) {
|
|
196
|
+
// Match !important appearing twice in the value
|
|
197
|
+
return /!important.*!important/.test(value);
|
|
198
|
+
}
|
|
199
|
+
|
|
189
200
|
/**
|
|
190
201
|
* Normalizes a vendor-prefixed display value to its standard equivalent.
|
|
191
202
|
*
|
|
@@ -366,6 +377,11 @@ function deduplicateDeclarations (declarations) {
|
|
|
366
377
|
continue;
|
|
367
378
|
}
|
|
368
379
|
|
|
380
|
+
// IE6-IE9 double !important properties are invalid and should be removed
|
|
381
|
+
if (hasDoubleImportant(minifiedValue)) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
369
385
|
// A vendor-prefixed property that repeats the value of its unprefixed
|
|
370
386
|
// form is dead code for browsers that read the unprefixed one
|
|
371
387
|
if (propertyName.startsWith('-') && !propertyName.startsWith('--')) {
|
package/src/rules/normalize.js
CHANGED
|
@@ -141,6 +141,17 @@ function normalizeLayerNames (layerNames) {
|
|
|
141
141
|
return String(layerNames ?? '').trim().replace(/\s*,\s*/g, ',');
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Checks if a media query contains the IE10/IE11 exclusive `\0` hack.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} media The media query string to check.
|
|
148
|
+
* @return {boolean} True if the media query contains the IE hack.
|
|
149
|
+
*/
|
|
150
|
+
function containsIEHack (media) {
|
|
151
|
+
// Match the \0 character which is used as an IE10/IE11 exclusive hack
|
|
152
|
+
return /\\0/.test(media);
|
|
153
|
+
}
|
|
154
|
+
|
|
144
155
|
/**
|
|
145
156
|
* Normalizes a `@media` query string by collapsing whitespace, stripping the default "all and" prefix, and converting min/max-width to range syntax.
|
|
146
157
|
*
|
|
@@ -236,6 +247,7 @@ function canUnwrapSupports (supports) {
|
|
|
236
247
|
|
|
237
248
|
export {
|
|
238
249
|
canUnwrapSupports,
|
|
250
|
+
containsIEHack,
|
|
239
251
|
normalizeLayerNames,
|
|
240
252
|
normalizeMedia,
|
|
241
253
|
normalizeRuleSelectors,
|
|
@@ -3,10 +3,26 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
containsIEHack,
|
|
6
7
|
normalizeLayerNames,
|
|
7
8
|
normalizeMedia
|
|
8
9
|
} from '../normalize.js';
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Removes `@media` rules that contain IE10/IE11 exclusive hacks (the `\0` character).
|
|
13
|
+
*
|
|
14
|
+
* @param {Array} rules The AST rule nodes to process.
|
|
15
|
+
* @return {Array} A new array of rules with IE-exclusive media queries removed.
|
|
16
|
+
*/
|
|
17
|
+
function removeIEMediaRules (rules) {
|
|
18
|
+
return rules.filter((rule) => {
|
|
19
|
+
if (rule.type === 'media' && containsIEHack(rule.media)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
/**
|
|
11
27
|
* Merges adjacent `@media` rules that share an identical normalized query string and deduplicates their child selector rules.
|
|
12
28
|
*
|
|
@@ -109,5 +125,6 @@ function mergeLayerRules (rules, mergeSelectorRules) {
|
|
|
109
125
|
export {
|
|
110
126
|
deduplicateKeyframes,
|
|
111
127
|
mergeLayerRules,
|
|
112
|
-
mergeMediaRules
|
|
128
|
+
mergeMediaRules,
|
|
129
|
+
removeIEMediaRules
|
|
113
130
|
};
|
|
@@ -155,22 +155,78 @@ function collectSharedParentCandidates (rule) {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
|
-
* Counts
|
|
159
|
-
*
|
|
160
|
-
*
|
|
158
|
+
* Counts the characters a list of rules takes up as selector text. Each rule
|
|
159
|
+
* counts its selectors, its braces, and every rule nested within it, while
|
|
160
|
+
* plain declarations count for nothing.
|
|
161
161
|
*
|
|
162
|
-
* @param {Array}
|
|
163
|
-
* @
|
|
164
|
-
* @return {number} The character savings of the wrapper, or a non-positive number when it costs more than it saves.
|
|
162
|
+
* @param {Array} rules The AST entries to measure.
|
|
163
|
+
* @return {number} The number of characters the rules take up.
|
|
165
164
|
*/
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return
|
|
170
|
-
}
|
|
165
|
+
function measureRuleCharacters (rules) {
|
|
166
|
+
return rules.reduce((total, rule) => {
|
|
167
|
+
if (rule.type !== 'rule') {
|
|
168
|
+
return total;
|
|
169
|
+
}
|
|
170
|
+
const selectorText = (rule.selectors || []).map((selector) => {
|
|
171
|
+
return selector.trim();
|
|
172
|
+
}).join(',');
|
|
173
|
+
const nestedRules = (rule.declarations || []).filter((declaration) => {
|
|
174
|
+
return declaration.type === 'rule';
|
|
175
|
+
});
|
|
176
|
+
// The two braces wrap the rule's declarations
|
|
177
|
+
return total + selectorText.length + 2 + measureRuleCharacters(nestedRules);
|
|
171
178
|
}, 0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Counts how many characters wrapping a run behind a shared parent selector
|
|
183
|
+
* saves: the run is measured as it is written, against the declarations the
|
|
184
|
+
* wrapper would hold plus the parent written once with its braces.
|
|
185
|
+
*
|
|
186
|
+
* @param {Array} run The rules of the run paired with their nested selectors.
|
|
187
|
+
* @param {Array} declarations The declarations the wrapper would hold, including any factoring of the nested rules.
|
|
188
|
+
* @param {string} parent The shared parent selector string.
|
|
189
|
+
* @return {number} The character savings of the wrapper, or a non-positive number when it costs more than it saves.
|
|
190
|
+
*/
|
|
191
|
+
function measureWrapperSavings (run, declarations, parent) {
|
|
192
|
+
const charactersBefore = measureRuleCharacters(run.map((entry) => {
|
|
193
|
+
return entry.sibling;
|
|
194
|
+
}));
|
|
172
195
|
// The wrapper writes the shared selector once plus its surrounding braces.
|
|
173
|
-
|
|
196
|
+
const charactersAfter = measureRuleCharacters(declarations) + parent.length + 2;
|
|
197
|
+
return charactersBefore - charactersAfter;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Expresses every selector of a sibling rule relative to a candidate parent.
|
|
202
|
+
* A nested selector must be shorter than the selector it replaces, so each
|
|
203
|
+
* factoring pass leaves the rules smaller and factoring them again terminates.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} rule The sibling rule whose selectors are nested.
|
|
206
|
+
* @param {function(string): (string|null)} matchNestedSelector The matcher built for the candidate parent.
|
|
207
|
+
* @return {Array|null} The nested selectors, or null when any selector cannot nest under the parent.
|
|
208
|
+
*/
|
|
209
|
+
function nestSelectorsUnderParent (rule, matchNestedSelector) {
|
|
210
|
+
const nestedSelectors = [];
|
|
211
|
+
for (const selector of rule.selectors) {
|
|
212
|
+
const trimmedSelector = selector.trim();
|
|
213
|
+
const nestedSelector = matchNestedSelector(trimmedSelector);
|
|
214
|
+
if (nestedSelector === null || nestedSelector.length >= trimmedSelector.length) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
nestedSelectors.push(nestedSelector);
|
|
218
|
+
}
|
|
219
|
+
return nestedSelectors;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Re-expresses a run entry as a rule whose selectors are the nested selectors.
|
|
224
|
+
*
|
|
225
|
+
* @param {object} entry A run entry pairing a sibling rule with its nested selectors.
|
|
226
|
+
* @return {object} The rule, with its selectors replaced by the nested ones.
|
|
227
|
+
*/
|
|
228
|
+
function createNestedChild (entry) {
|
|
229
|
+
return { ...entry.sibling, selectors: entry.nestedSelectors };
|
|
174
230
|
}
|
|
175
231
|
|
|
176
232
|
/**
|
|
@@ -187,15 +243,49 @@ function orderRulesBySourcePosition (rules) {
|
|
|
187
243
|
});
|
|
188
244
|
}
|
|
189
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Builds the declarations of a wrapper that holds a run of sibling rules nested
|
|
248
|
+
* under their shared parent. A first rule whose only nested selector is `&` is
|
|
249
|
+
* the parent itself, so its declarations are hoisted into the wrapper. Otherwise
|
|
250
|
+
* the nested rules are factored again, so the selectors they still share can be
|
|
251
|
+
* wrapped in turn.
|
|
252
|
+
*
|
|
253
|
+
* @param {Array} run The rules of the run paired with their nested selectors.
|
|
254
|
+
* @return {Array} The declarations of the wrapper rule.
|
|
255
|
+
*/
|
|
256
|
+
function createWrapperDeclarations (run) {
|
|
257
|
+
const firstEntry = run[0];
|
|
258
|
+
const firstSelectorIsParent = (
|
|
259
|
+
firstEntry.nestedSelectors.length === 1 &&
|
|
260
|
+
firstEntry.nestedSelectors[0] === '&'
|
|
261
|
+
);
|
|
262
|
+
if (!firstSelectorIsParent) {
|
|
263
|
+
return factorCommonParents(run.map(createNestedChild), true);
|
|
264
|
+
}
|
|
265
|
+
const directDeclarations = firstEntry.sibling.declarations;
|
|
266
|
+
const directNestedRules = directDeclarations.filter((declaration) => {
|
|
267
|
+
return declaration.type === 'rule';
|
|
268
|
+
});
|
|
269
|
+
const directEntries = directDeclarations.filter((declaration) => {
|
|
270
|
+
return declaration.type !== 'rule';
|
|
271
|
+
});
|
|
272
|
+
const children = run.slice(1).map(createNestedChild);
|
|
273
|
+
return [
|
|
274
|
+
...directEntries,
|
|
275
|
+
...orderRulesBySourcePosition([...directNestedRules, ...children])
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
|
|
190
279
|
/**
|
|
191
280
|
* Groups consecutive sibling rules that share a common leading selector
|
|
192
281
|
* portion (such as `.foo` in `.foo .a`, `.foo .b`, or `.menu .item` in
|
|
193
282
|
* `.menu .item:hover .x`) into a synthesized parent rule with the shared
|
|
194
283
|
* portion factored out. Every viable prefix of the first rule's selector is
|
|
195
284
|
* tried as the parent, and the grouping that saves the most characters wins,
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* selectors nests under
|
|
285
|
+
* measured with the rules nested under it factored in turn, but only when it
|
|
286
|
+
* saves more than the wrapper itself costs. Rules with a comma-separated
|
|
287
|
+
* selector list can join a run when every one of their selectors nests under
|
|
288
|
+
* the parent.
|
|
199
289
|
*
|
|
200
290
|
* @param {Array} rules The flat AST rule nodes to factor.
|
|
201
291
|
* @param {boolean} allowTrailingCompound True when the rules are children of a factored wrapper, whose `&`-relative selectors may share a trailing compound that can be factored out in turn.
|
|
@@ -225,12 +315,8 @@ function factorCommonParents (rules, allowTrailingCompound = false) {
|
|
|
225
315
|
if (sibling.type !== 'rule' || !sibling.selectors?.length) {
|
|
226
316
|
break;
|
|
227
317
|
}
|
|
228
|
-
const nestedSelectors = sibling
|
|
229
|
-
|
|
230
|
-
});
|
|
231
|
-
if (nestedSelectors.some((nestedSelector) => {
|
|
232
|
-
return nestedSelector === null;
|
|
233
|
-
})) {
|
|
318
|
+
const nestedSelectors = nestSelectorsUnderParent(sibling, matchNestedSelector);
|
|
319
|
+
if (!nestedSelectors) {
|
|
234
320
|
break;
|
|
235
321
|
}
|
|
236
322
|
run.push({ sibling, nestedSelectors });
|
|
@@ -243,12 +329,13 @@ function factorCommonParents (rules, allowTrailingCompound = false) {
|
|
|
243
329
|
if (run.length < 2 && selectorCount < 2) {
|
|
244
330
|
continue;
|
|
245
331
|
}
|
|
246
|
-
const
|
|
332
|
+
const declarations = createWrapperDeclarations(run);
|
|
333
|
+
const savings = measureWrapperSavings(run, declarations, candidateParent);
|
|
247
334
|
if (savings > 0 && (!bestGrouping || savings > bestGrouping.savings)) {
|
|
248
335
|
bestGrouping = {
|
|
336
|
+
declarations,
|
|
249
337
|
lookahead,
|
|
250
338
|
parent: candidateParent,
|
|
251
|
-
run,
|
|
252
339
|
savings
|
|
253
340
|
};
|
|
254
341
|
}
|
|
@@ -262,39 +349,10 @@ function factorCommonParents (rules, allowTrailingCompound = false) {
|
|
|
262
349
|
index++;
|
|
263
350
|
continue;
|
|
264
351
|
}
|
|
265
|
-
const firstEntry = bestGrouping.run[0];
|
|
266
|
-
const firstSelectorIsParent = (
|
|
267
|
-
firstEntry.nestedSelectors.length === 1 &&
|
|
268
|
-
firstEntry.nestedSelectors[0] === '&'
|
|
269
|
-
);
|
|
270
|
-
const directDeclarations = firstSelectorIsParent ?
|
|
271
|
-
firstEntry.sibling.declarations :
|
|
272
|
-
[];
|
|
273
|
-
const entriesToNest = firstSelectorIsParent ?
|
|
274
|
-
bestGrouping.run.slice(1) :
|
|
275
|
-
bestGrouping.run;
|
|
276
|
-
const children = entriesToNest.map((entry) => {
|
|
277
|
-
return { ...entry.sibling, selectors: entry.nestedSelectors };
|
|
278
|
-
});
|
|
279
|
-
let declarations;
|
|
280
|
-
if (firstSelectorIsParent) {
|
|
281
|
-
const directNestedRules = directDeclarations.filter((declaration) => {
|
|
282
|
-
return declaration.type === 'rule';
|
|
283
|
-
});
|
|
284
|
-
const directEntries = directDeclarations.filter((declaration) => {
|
|
285
|
-
return declaration.type !== 'rule';
|
|
286
|
-
});
|
|
287
|
-
declarations = [
|
|
288
|
-
...directEntries,
|
|
289
|
-
...orderRulesBySourcePosition([...directNestedRules, ...children])
|
|
290
|
-
];
|
|
291
|
-
} else {
|
|
292
|
-
declarations = factorCommonParents(children, true);
|
|
293
|
-
}
|
|
294
352
|
result.push({
|
|
295
353
|
type: 'rule',
|
|
296
354
|
selectors: [bestGrouping.parent],
|
|
297
|
-
declarations
|
|
355
|
+
declarations: bestGrouping.declarations
|
|
298
356
|
});
|
|
299
357
|
index = bestGrouping.lookahead;
|
|
300
358
|
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import {
|
|
6
6
|
deduplicateKeyframes,
|
|
7
7
|
mergeLayerRules,
|
|
8
|
-
mergeMediaRules
|
|
8
|
+
mergeMediaRules,
|
|
9
|
+
removeIEMediaRules
|
|
9
10
|
} from './at-rules.js';
|
|
10
11
|
import { removeEmptyRules } from './empty-rules.js';
|
|
11
12
|
import { expandPureNestedRules } from './expand-nesting.js';
|
|
@@ -43,6 +44,7 @@ function clearSelectorCaches () {
|
|
|
43
44
|
*/
|
|
44
45
|
function optimizeRuleList (rules) {
|
|
45
46
|
let result = expandPureNestedRules(rules);
|
|
47
|
+
result = removeIEMediaRules(result);
|
|
46
48
|
result = mergeLayerRules(result, mergeSelectorRules);
|
|
47
49
|
result = mergeMediaRules(result, mergeSelectorRules);
|
|
48
50
|
result = deduplicateKeyframes(result);
|
|
@@ -175,7 +175,193 @@ function mergeRuleIntoPredecessor (previousRule, rule) {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
|
-
*
|
|
178
|
+
* Finds properties that are present in all adjacent rules with matching values.
|
|
179
|
+
*
|
|
180
|
+
* @param {Array} adjacentRules The consecutive rule nodes to compare.
|
|
181
|
+
* @return {Map} Map of property name to value for properties common to all rules.
|
|
182
|
+
*/
|
|
183
|
+
function findCommonProperties (adjacentRules) {
|
|
184
|
+
if (adjacentRules.length < 2) {
|
|
185
|
+
return new Map();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const firstRuleBody = partitionRuleBody(adjacentRules[0].declarations);
|
|
189
|
+
const commonProperties = new Map(
|
|
190
|
+
firstRuleBody.plainDeclarations.map((declaration) => {
|
|
191
|
+
return [declaration.property, (declaration.value || '').trim()];
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
for (let ruleIndex = 1; ruleIndex < adjacentRules.length; ruleIndex++) {
|
|
196
|
+
const currentRuleBody = partitionRuleBody(adjacentRules[ruleIndex].declarations);
|
|
197
|
+
const currentValueByProperty = new Map(
|
|
198
|
+
currentRuleBody.plainDeclarations.map((declaration) => {
|
|
199
|
+
return [declaration.property, (declaration.value || '').trim()];
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
for (const [property, value] of commonProperties) {
|
|
204
|
+
if (currentValueByProperty.get(property) !== value) {
|
|
205
|
+
commonProperties.delete(property);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return commonProperties;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Checks if a rule contains the `all` property, which resets all other properties.
|
|
215
|
+
*
|
|
216
|
+
* @param {object} rule The rule node to check.
|
|
217
|
+
* @return {boolean} True if the rule contains the `all` property.
|
|
218
|
+
*/
|
|
219
|
+
function hasAllProperty (rule) {
|
|
220
|
+
const ruleBody = partitionRuleBody(rule.declarations);
|
|
221
|
+
return ruleBody.plainDeclarations.some((declaration) => {
|
|
222
|
+
return declaration.property === 'all';
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Checks if any rule in a list contains the `all` property.
|
|
228
|
+
*
|
|
229
|
+
* @param {Array} rules The rule nodes to check.
|
|
230
|
+
* @return {boolean} True if any rule contains the `all` property.
|
|
231
|
+
*/
|
|
232
|
+
function anyRuleHasAllProperty (rules) {
|
|
233
|
+
return rules.some(hasAllProperty);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Checks if any rule has nested blocks.
|
|
238
|
+
*
|
|
239
|
+
* @param {Array} rules The rule nodes to check.
|
|
240
|
+
* @return {boolean} True if any rule has nested blocks.
|
|
241
|
+
*/
|
|
242
|
+
function anyRuleHasNestedBlocks (rules) {
|
|
243
|
+
return rules.some((rule) => {
|
|
244
|
+
const ruleBody = partitionRuleBody(rule.declarations);
|
|
245
|
+
return ruleBody.nestedBlocks.length > 0;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Combines adjacent rules that share common properties by creating a merged rule
|
|
251
|
+
* with all properties from the first rule, and keeping only the differing properties
|
|
252
|
+
* in each subsequent rule.
|
|
253
|
+
*
|
|
254
|
+
* @param {Array} adjacentRules The consecutive rule nodes to merge.
|
|
255
|
+
* @param {Map} commonProps The properties common to all rules.
|
|
256
|
+
* @return {Array} The rules after merging common properties.
|
|
257
|
+
*/
|
|
258
|
+
function mergeCommonProperties (adjacentRules, commonProps) {
|
|
259
|
+
if (commonProps.size === 0) {
|
|
260
|
+
return adjacentRules;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Skip merging if any rule has the `all` property
|
|
264
|
+
if (anyRuleHasAllProperty(adjacentRules)) {
|
|
265
|
+
return adjacentRules;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Skip merging if any rule has nested blocks
|
|
269
|
+
if (anyRuleHasNestedBlocks(adjacentRules)) {
|
|
270
|
+
return adjacentRules;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const firstRuleBody = partitionRuleBody(adjacentRules[0].declarations);
|
|
274
|
+
|
|
275
|
+
const combinedSelectors = [];
|
|
276
|
+
for (const rule of adjacentRules) {
|
|
277
|
+
combinedSelectors.push(...rule.selectors);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const mergedRule = {
|
|
281
|
+
...adjacentRules[0],
|
|
282
|
+
selectors: deduplicateSelectors(combinedSelectors),
|
|
283
|
+
declarations: firstRuleBody.plainDeclarations
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const result = [mergedRule];
|
|
287
|
+
|
|
288
|
+
for (let ruleIndex = 1; ruleIndex < adjacentRules.length; ruleIndex++) {
|
|
289
|
+
const rule = adjacentRules[ruleIndex];
|
|
290
|
+
const ruleBody = partitionRuleBody(rule.declarations);
|
|
291
|
+
const differingDeclarations = ruleBody.plainDeclarations.filter((declaration) => {
|
|
292
|
+
const firstRuleValue = firstRuleBody.plainDeclarations.find((decl) => {
|
|
293
|
+
return decl.property === declaration.property;
|
|
294
|
+
});
|
|
295
|
+
if (!firstRuleValue) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
return (declaration.value || '').trim() !== (firstRuleValue.value || '').trim();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (differingDeclarations.length > 0) {
|
|
302
|
+
result.push({
|
|
303
|
+
...rule,
|
|
304
|
+
declarations: differingDeclarations
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check if the merge actually reduces size
|
|
310
|
+
const originalSize = adjacentRules.reduce((sum, rule) => {
|
|
311
|
+
return sum + calculateRuleSize(rule);
|
|
312
|
+
}, 0);
|
|
313
|
+
|
|
314
|
+
const mergedSize = result.reduce((sum, rule) => {
|
|
315
|
+
return sum + calculateRuleSize(rule);
|
|
316
|
+
}, 0);
|
|
317
|
+
|
|
318
|
+
if (mergedSize >= originalSize) {
|
|
319
|
+
return adjacentRules;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Calculates the approximate character size of a rule's selectors.
|
|
327
|
+
*
|
|
328
|
+
* @param {object} rule The rule node.
|
|
329
|
+
* @return {number} The total character count of all selectors.
|
|
330
|
+
*/
|
|
331
|
+
function calculateSelectorSize (rule) {
|
|
332
|
+
if (!rule.selectors) {
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
return rule.selectors.join(',').length;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Calculates the approximate character size of a rule's declarations.
|
|
340
|
+
*
|
|
341
|
+
* @param {object} rule The rule node.
|
|
342
|
+
* @return {number} The total character count of all declarations.
|
|
343
|
+
*/
|
|
344
|
+
function calculateDeclarationSize (rule) {
|
|
345
|
+
const ruleBody = partitionRuleBody(rule.declarations);
|
|
346
|
+
let size = 0;
|
|
347
|
+
for (const declaration of ruleBody.plainDeclarations) {
|
|
348
|
+
size += declaration.property.length + 1 + (declaration.value || '').length;
|
|
349
|
+
}
|
|
350
|
+
return size;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Calculates the approximate character size of a rule.
|
|
355
|
+
*
|
|
356
|
+
* @param {object} rule The rule node.
|
|
357
|
+
* @return {number} The total character count of the rule.
|
|
358
|
+
*/
|
|
359
|
+
function calculateRuleSize (rule) {
|
|
360
|
+
return calculateSelectorSize(rule) + calculateDeclarationSize(rule);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Merges consecutive rules whose declarations are a subset of the following rule, combining their selectors and splitting out any extra declarations. Also combines common properties across adjacent rules.
|
|
179
365
|
*
|
|
180
366
|
* @param {Array} rules The AST rule nodes to merge.
|
|
181
367
|
* @return {Array} A new array of rules with declaration-compatible consecutive rules combined.
|
|
@@ -191,7 +377,36 @@ function mergeByDeclarations (rules) {
|
|
|
191
377
|
}
|
|
192
378
|
result.push(rule);
|
|
193
379
|
}
|
|
194
|
-
|
|
380
|
+
|
|
381
|
+
// Pass to combine common properties across adjacent rules
|
|
382
|
+
const finalResult = [];
|
|
383
|
+
let adjacentRules = [];
|
|
384
|
+
|
|
385
|
+
for (const rule of result) {
|
|
386
|
+
if (rule.type === 'rule' && rule.selectors?.length) {
|
|
387
|
+
adjacentRules.push(rule);
|
|
388
|
+
} else {
|
|
389
|
+
if (adjacentRules.length >= 2) {
|
|
390
|
+
const commonProps = findCommonProperties(adjacentRules);
|
|
391
|
+
const merged = mergeCommonProperties(adjacentRules, commonProps);
|
|
392
|
+
finalResult.push(...merged);
|
|
393
|
+
} else if (adjacentRules.length === 1) {
|
|
394
|
+
finalResult.push(adjacentRules[0]);
|
|
395
|
+
}
|
|
396
|
+
adjacentRules = [];
|
|
397
|
+
finalResult.push(rule);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (adjacentRules.length >= 2) {
|
|
402
|
+
const commonProps = findCommonProperties(adjacentRules);
|
|
403
|
+
const merged = mergeCommonProperties(adjacentRules, commonProps);
|
|
404
|
+
finalResult.push(...merged);
|
|
405
|
+
} else if (adjacentRules.length === 1) {
|
|
406
|
+
finalResult.push(adjacentRules[0]);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return finalResult;
|
|
195
410
|
}
|
|
196
411
|
|
|
197
412
|
export {
|
package/src/value/math/index.js
CHANGED
|
@@ -169,15 +169,17 @@ function simplifyMathFunctionsOnce (value, property) {
|
|
|
169
169
|
*/
|
|
170
170
|
function normalizeMathFunctions (value, property, originalValue = '') {
|
|
171
171
|
let result = value;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
172
|
+
if (value.includes('(')) {
|
|
173
|
+
let previous;
|
|
174
|
+
|
|
175
|
+
do {
|
|
176
|
+
previous = result;
|
|
177
|
+
// Resolving a math function leaves a plain value where it stood, which can
|
|
178
|
+
// bring the function it was nested in within reach of the next pass, so
|
|
179
|
+
// the value is simplified until it settles
|
|
180
|
+
result = simplifyMathFunctionsOnce(result, property);
|
|
181
|
+
} while (result !== previous);
|
|
182
|
+
}
|
|
181
183
|
|
|
182
184
|
// When calc() folded to an absolute-length unit (pt, pc, in, cm, mm, q), convert to pixels
|
|
183
185
|
if (originalValue.includes('calc(') && /^-?(?:\d+|\d*\.\d+)(pt|pc|in|cm|mm|q)$/i.test(result)) {
|
|
@@ -48,8 +48,13 @@ function rewriteMathFunctionContents (rewriteExpression, value) {
|
|
|
48
48
|
break;
|
|
49
49
|
}
|
|
50
50
|
const functionName = value.slice(findFunctionNameStart(value, openIndex), openIndex).toLowerCase();
|
|
51
|
+
if (!MATH_FUNCTION_NAMES.has(functionName)) {
|
|
52
|
+
result += value.slice(index, openIndex + 1);
|
|
53
|
+
index = openIndex + 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
51
56
|
const closeIndex = findMatchingParenthesis(value, openIndex);
|
|
52
|
-
if (
|
|
57
|
+
if (closeIndex === -1) {
|
|
53
58
|
result += value.slice(index, openIndex + 1);
|
|
54
59
|
index = openIndex + 1;
|
|
55
60
|
continue;
|