@thejaredwilcurt/csslop 0.0.55 → 0.0.57
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
|
@@ -23,6 +23,127 @@ function removeIEMediaRules (rules) {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Matches the media type keyword that opens a normalized `@media` query, such as `screen` in `screen and (width>=768px)`.
|
|
28
|
+
*
|
|
29
|
+
* @type {RegExp}
|
|
30
|
+
*/
|
|
31
|
+
const LEADING_MEDIA_TYPE_PATTERN = /^[a-z-]+(?=\s|$)/i;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Matches one parenthesized media condition, allowing one level of nested parentheses such as `(not (color))`.
|
|
35
|
+
*
|
|
36
|
+
* @type {RegExp}
|
|
37
|
+
*/
|
|
38
|
+
const PARENTHESIZED_CONDITION_PATTERN = /\((?:[^()]|\([^()]*\))*\)/g;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Splits a `@media` query into the media type it names and the conditions it adds, so that two queries can be joined with `and`. Queries that use a list, `not`, or `only` cannot be joined this way.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} media The raw `@media` query string.
|
|
44
|
+
* @return {object|null} The `type` (null when none applies) and `conditions` text, or null when the query cannot be joined.
|
|
45
|
+
*/
|
|
46
|
+
function parseJoinableMediaQuery (media) {
|
|
47
|
+
const normalized = normalizeMedia(media);
|
|
48
|
+
const typeText = normalized.match(LEADING_MEDIA_TYPE_PATTERN)?.[0] ?? '';
|
|
49
|
+
const typeName = typeText.toLowerCase();
|
|
50
|
+
if (normalized.includes(',') || typeName === 'not' || typeName === 'only') {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// Drop the `and` that separates the media type from the conditions
|
|
54
|
+
const conditions = normalized.slice(typeText.length).replace(/^\s*and\s*/i, '');
|
|
55
|
+
// Anything left once the parenthesized conditions and `and` joiners are removed is an unsupported operator
|
|
56
|
+
const unparsedText = conditions.replace(PARENTHESIZED_CONDITION_PATTERN, '').replace(/\band\b/gi, '').trim();
|
|
57
|
+
if (unparsedText) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
// `all` matches every document, so it adds no restriction of its own
|
|
62
|
+
type: typeName && typeName !== 'all' ? typeName : null,
|
|
63
|
+
conditions
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reports whether two queries name different media types. A document has only one media type, so a block requiring both can never match.
|
|
69
|
+
*
|
|
70
|
+
* @param {object|null} outer The parsed enclosing query, from `parseJoinableMediaQuery`.
|
|
71
|
+
* @param {object|null} inner The parsed nested query, from `parseJoinableMediaQuery`.
|
|
72
|
+
* @return {boolean} True when the two queries name different media types.
|
|
73
|
+
*/
|
|
74
|
+
function areExclusiveMediaTypes (outer, inner) {
|
|
75
|
+
return Boolean(outer?.type && inner?.type && outer.type !== inner.type);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Joins a `@media` block nested directly inside another into the single query that matches the same documents, such as `screen and (width>=768px)`. A media type both queries share is written once.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} outerMedia The raw query of the enclosing `@media` block.
|
|
82
|
+
* @param {string} innerMedia The raw query of the nested `@media` block.
|
|
83
|
+
* @return {string|null} The joined query, or null when the pair must stay nested.
|
|
84
|
+
*/
|
|
85
|
+
function joinNestedMediaQueries (outerMedia, innerMedia) {
|
|
86
|
+
const outer = parseJoinableMediaQuery(outerMedia);
|
|
87
|
+
const inner = parseJoinableMediaQuery(innerMedia);
|
|
88
|
+
if (!outer || !inner || areExclusiveMediaTypes(outer, inner)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const parts = [outer.type || inner.type, outer.conditions, inner.conditions].filter(Boolean);
|
|
92
|
+
return parts.join(' and ') || 'all';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Reports whether an entry of a rule list is real content rather than the whitespace between entries.
|
|
97
|
+
*
|
|
98
|
+
* @param {object} entry An entry of a rule list.
|
|
99
|
+
* @return {boolean} True when the entry is not whitespace.
|
|
100
|
+
*/
|
|
101
|
+
function isContentEntry (entry) {
|
|
102
|
+
return entry.type !== 'whitespace';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Simplifies one `@media` block. A block whose only content is a single nested `@media` block is joined into one query, and the simplification repeats on the result. Nested `@media` blocks whose media type the block already excludes are removed.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} rule The `@media` rule node to simplify.
|
|
109
|
+
* @return {object} A new `@media` rule node.
|
|
110
|
+
*/
|
|
111
|
+
function simplifyMediaBlock (rule) {
|
|
112
|
+
const children = rule.rules || [];
|
|
113
|
+
const content = children.filter(isContentEntry);
|
|
114
|
+
if (content.length === 1 && content[0].type === 'media') {
|
|
115
|
+
const joinedMedia = joinNestedMediaQueries(rule.media, content[0].media);
|
|
116
|
+
if (joinedMedia !== null) {
|
|
117
|
+
return simplifyMediaBlock({ ...content[0], media: joinedMedia });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const outerQuery = parseJoinableMediaQuery(rule.media);
|
|
121
|
+
return {
|
|
122
|
+
...rule,
|
|
123
|
+
rules: children.filter((child) => {
|
|
124
|
+
return !(child.type === 'media' && areExclusiveMediaTypes(outerQuery, parseJoinableMediaQuery(child.media)));
|
|
125
|
+
})
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Simplifies the `@media` blocks in a rule list, dropping any block whose content was removed entirely as dead.
|
|
131
|
+
*
|
|
132
|
+
* @param {Array} rules The AST rule nodes to process.
|
|
133
|
+
* @return {Array} A new array of rules with nested `@media` blocks simplified.
|
|
134
|
+
*/
|
|
135
|
+
function simplifyNestedMediaRules (rules) {
|
|
136
|
+
return rules.flatMap((rule) => {
|
|
137
|
+
if (rule.type !== 'media') {
|
|
138
|
+
return [rule];
|
|
139
|
+
}
|
|
140
|
+
const simplified = simplifyMediaBlock(rule);
|
|
141
|
+
const hadContent = (rule.rules || []).some(isContentEntry);
|
|
142
|
+
const isEmptiedByDeadContent = hadContent && !simplified.rules.some(isContentEntry);
|
|
143
|
+
return isEmptiedByDeadContent ? [] : [simplified];
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
26
147
|
/**
|
|
27
148
|
* Merges adjacent `@media` rules that share an identical normalized query string and deduplicates their child selector rules.
|
|
28
149
|
*
|
|
@@ -126,5 +247,6 @@ export {
|
|
|
126
247
|
deduplicateKeyframes,
|
|
127
248
|
mergeLayerRules,
|
|
128
249
|
mergeMediaRules,
|
|
129
|
-
removeIEMediaRules
|
|
250
|
+
removeIEMediaRules,
|
|
251
|
+
simplifyNestedMediaRules
|
|
130
252
|
};
|
|
@@ -276,6 +276,40 @@ function createWrapperDeclarations (run) {
|
|
|
276
276
|
];
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Collects the run of consecutive sibling rules, starting at a given index,
|
|
281
|
+
* whose selectors can all nest under a candidate parent. Comments never affect
|
|
282
|
+
* the cascade, so they are passed over instead of ending the run.
|
|
283
|
+
*
|
|
284
|
+
* @param {Array} rules The sibling AST entries.
|
|
285
|
+
* @param {number} startIndex The index the run starts at.
|
|
286
|
+
* @param {function(string): (string|null)} matchNestedSelector The matcher built for the candidate parent.
|
|
287
|
+
* @return {object} The `run` of rules paired with their nested selectors, and the `end` index just past the run's last rule.
|
|
288
|
+
*/
|
|
289
|
+
function collectSiblingRun (rules, startIndex, matchNestedSelector) {
|
|
290
|
+
const run = [];
|
|
291
|
+
let end = startIndex;
|
|
292
|
+
let lookahead = startIndex;
|
|
293
|
+
while (lookahead < rules.length) {
|
|
294
|
+
const sibling = rules[lookahead];
|
|
295
|
+
if (sibling.type === 'comment') {
|
|
296
|
+
lookahead++;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (sibling.type !== 'rule' || !sibling.selectors?.length) {
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
const nestedSelectors = nestSelectorsUnderParent(sibling, matchNestedSelector);
|
|
303
|
+
if (!nestedSelectors) {
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
run.push({ sibling, nestedSelectors });
|
|
307
|
+
lookahead++;
|
|
308
|
+
end = lookahead;
|
|
309
|
+
}
|
|
310
|
+
return { end, run };
|
|
311
|
+
}
|
|
312
|
+
|
|
279
313
|
/**
|
|
280
314
|
* Groups consecutive sibling rules that share a common leading selector
|
|
281
315
|
* portion (such as `.foo` in `.foo .a`, `.foo .b`, or `.menu .item` in
|
|
@@ -308,20 +342,7 @@ function factorCommonParents (rules, allowTrailingCompound = false) {
|
|
|
308
342
|
}
|
|
309
343
|
const matchNestedSelector = createNestedSelectorMatcher(candidateParent);
|
|
310
344
|
// Collect the run of consecutive rules whose selectors can all nest under the candidate.
|
|
311
|
-
const run =
|
|
312
|
-
let lookahead = index;
|
|
313
|
-
while (lookahead < rules.length) {
|
|
314
|
-
const sibling = rules[lookahead];
|
|
315
|
-
if (sibling.type !== 'rule' || !sibling.selectors?.length) {
|
|
316
|
-
break;
|
|
317
|
-
}
|
|
318
|
-
const nestedSelectors = nestSelectorsUnderParent(sibling, matchNestedSelector);
|
|
319
|
-
if (!nestedSelectors) {
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
run.push({ sibling, nestedSelectors });
|
|
323
|
-
lookahead++;
|
|
324
|
-
}
|
|
345
|
+
const { end, run } = collectSiblingRun(rules, index, matchNestedSelector);
|
|
325
346
|
const selectorCount = run.reduce((total, entry) => {
|
|
326
347
|
return total + entry.nestedSelectors.length;
|
|
327
348
|
}, 0);
|
|
@@ -333,8 +354,11 @@ function factorCommonParents (rules, allowTrailingCompound = false) {
|
|
|
333
354
|
const savings = measureWrapperSavings(run, declarations, candidateParent);
|
|
334
355
|
if (savings > 0 && (!bestGrouping || savings > bestGrouping.savings)) {
|
|
335
356
|
bestGrouping = {
|
|
357
|
+
comments: rules.slice(index, end).filter((entry) => {
|
|
358
|
+
return entry.type === 'comment';
|
|
359
|
+
}),
|
|
336
360
|
declarations,
|
|
337
|
-
|
|
361
|
+
end,
|
|
338
362
|
parent: candidateParent,
|
|
339
363
|
savings
|
|
340
364
|
};
|
|
@@ -349,12 +373,12 @@ function factorCommonParents (rules, allowTrailingCompound = false) {
|
|
|
349
373
|
index++;
|
|
350
374
|
continue;
|
|
351
375
|
}
|
|
352
|
-
result.push({
|
|
376
|
+
result.push(...bestGrouping.comments, {
|
|
353
377
|
type: 'rule',
|
|
354
378
|
selectors: [bestGrouping.parent],
|
|
355
379
|
declarations: bestGrouping.declarations
|
|
356
380
|
});
|
|
357
|
-
index = bestGrouping.
|
|
381
|
+
index = bestGrouping.end;
|
|
358
382
|
}
|
|
359
383
|
return result;
|
|
360
384
|
}
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
deduplicateKeyframes,
|
|
7
7
|
mergeLayerRules,
|
|
8
8
|
mergeMediaRules,
|
|
9
|
-
removeIEMediaRules
|
|
9
|
+
removeIEMediaRules,
|
|
10
|
+
simplifyNestedMediaRules
|
|
10
11
|
} from './at-rules.js';
|
|
11
12
|
import { removeEmptyRules } from './empty-rules.js';
|
|
12
13
|
import { expandPureNestedRules } from './expand-nesting.js';
|
|
@@ -45,6 +46,7 @@ function clearSelectorCaches () {
|
|
|
45
46
|
function optimizeRuleList (rules) {
|
|
46
47
|
let result = expandPureNestedRules(rules);
|
|
47
48
|
result = removeIEMediaRules(result);
|
|
49
|
+
result = simplifyNestedMediaRules(result);
|
|
48
50
|
result = mergeLayerRules(result, mergeSelectorRules);
|
|
49
51
|
result = mergeMediaRules(result, mergeSelectorRules);
|
|
50
52
|
result = deduplicateKeyframes(result);
|
|
@@ -28,13 +28,13 @@ function nestSelectorRemainder (remainder) {
|
|
|
28
28
|
if (remainder.startsWith(':')) {
|
|
29
29
|
return '&' + remainder;
|
|
30
30
|
}
|
|
31
|
-
if (remainder.startsWith(' ')) {
|
|
32
|
-
return remainder.slice(1);
|
|
33
|
-
}
|
|
34
31
|
const combinatorMatch = remainder.match(LEADING_COMBINATOR_PATTERN);
|
|
35
32
|
if (combinatorMatch) {
|
|
36
33
|
return combinatorMatch[1] + combinatorMatch[2];
|
|
37
34
|
}
|
|
35
|
+
if (remainder.startsWith(' ')) {
|
|
36
|
+
return remainder.slice(1);
|
|
37
|
+
}
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
40
|
|