@thejaredwilcurt/csslop 0.0.53 → 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
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,10 +32,10 @@
|
|
|
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",
|
|
@@ -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) {
|
|
@@ -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
|
}
|
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;
|