@thejaredwilcurt/csslop 0.0.47 → 0.0.49
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 -3
- package/package.json +4 -4
- 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 +8 -3
- package/src/parser/source-search.js +6 -0
- package/src/rules/custom-properties.js +19 -0
- package/src/rules/normalize.js +34 -1
- package/src/rules/optimize.js +422 -139
- package/src/rules/selectors.js +9 -0
- package/src/rules/stringify.js +138 -76
- package/src/utilities.js +0 -13
- package/src/value/minify.js +138 -37
- package/src/value/numbers.js +154 -0
- package/src/value/tokens.js +78 -20
package/src/rules/optimize.js
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
splitTopLevelCommaList
|
|
13
13
|
} from '../parser/source-search.js';
|
|
14
14
|
import { tokenizeCssValue } from '../parser/tokenizer.js';
|
|
15
|
-
import { escapeRegexString } from '../utilities.js';
|
|
16
15
|
|
|
17
16
|
import {
|
|
18
17
|
normalizeLayerNames,
|
|
@@ -247,35 +246,57 @@ function expandPureNestedRules (inputRules) {
|
|
|
247
246
|
return result;
|
|
248
247
|
}
|
|
249
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Matches the part of a child selector that follows its parent when that part
|
|
251
|
+
* starts with an explicit combinator. Optional whitespace, then the combinator
|
|
252
|
+
* (`>`, `+`, or `~`) is captured, then optional whitespace, then the rest of
|
|
253
|
+
* the selector is captured.
|
|
254
|
+
*
|
|
255
|
+
* @type {RegExp}
|
|
256
|
+
*/
|
|
257
|
+
const LEADING_COMBINATOR_PATTERN = /^\s*([>+~])\s*(.+)$/;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Expresses the part of a child selector that follows its parent as the
|
|
261
|
+
* selector it would be once nested inside that parent.
|
|
262
|
+
*
|
|
263
|
+
* @param {string} remainder The child selector with the parent prefix removed.
|
|
264
|
+
* @return {string|null} The nested selector, or null when the remainder cannot be nested.
|
|
265
|
+
*/
|
|
266
|
+
function nestSelectorRemainder (remainder) {
|
|
267
|
+
if (!remainder) {
|
|
268
|
+
return '&';
|
|
269
|
+
}
|
|
270
|
+
if (remainder.startsWith(':')) {
|
|
271
|
+
return '&' + remainder;
|
|
272
|
+
}
|
|
273
|
+
if (remainder.startsWith(' ')) {
|
|
274
|
+
return remainder.slice(1);
|
|
275
|
+
}
|
|
276
|
+
const combinatorMatch = remainder.match(LEADING_COMBINATOR_PATTERN);
|
|
277
|
+
if (combinatorMatch) {
|
|
278
|
+
return combinatorMatch[1] + combinatorMatch[2];
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
250
283
|
/**
|
|
251
284
|
* Builds a reusable matcher that expresses child selectors as nested selectors
|
|
252
|
-
* relative to one parent.
|
|
253
|
-
*
|
|
285
|
+
* relative to one parent. Most children tested against a parent do not start
|
|
286
|
+
* with it at all, so that cheap prefix check runs before anything else.
|
|
254
287
|
*
|
|
255
288
|
* @param {string} parentSelector The parent selector string.
|
|
256
289
|
* @return {function(string): (string|null)} A matcher returning the nested selector, or null when the child cannot be nested.
|
|
257
290
|
*/
|
|
258
291
|
function createNestedSelectorMatcher (parentSelector) {
|
|
259
292
|
const parent = parentSelector.trim();
|
|
260
|
-
// Match a child selector that starts with the parent followed by a combinator (>, +, ~)
|
|
261
|
-
const combinatorPattern = new RegExp('^' + escapeRegexString(parent) + '\\s*([>+~])\\s*(.+)$');
|
|
262
293
|
|
|
263
294
|
return (childSelector) => {
|
|
264
295
|
const child = childSelector.trim();
|
|
265
|
-
if (child
|
|
266
|
-
return
|
|
267
|
-
}
|
|
268
|
-
if (child.startsWith(parent + ':') || child.startsWith(parent + '::')) {
|
|
269
|
-
return '&' + child.slice(parent.length);
|
|
270
|
-
}
|
|
271
|
-
if (child.startsWith(parent + ' ')) {
|
|
272
|
-
return child.slice(parent.length + 1);
|
|
273
|
-
}
|
|
274
|
-
const combinatorMatch = child.match(combinatorPattern);
|
|
275
|
-
if (combinatorMatch) {
|
|
276
|
-
return combinatorMatch[1] + combinatorMatch[2];
|
|
296
|
+
if (!child.startsWith(parent)) {
|
|
297
|
+
return null;
|
|
277
298
|
}
|
|
278
|
-
return
|
|
299
|
+
return nestSelectorRemainder(child.slice(parent.length));
|
|
279
300
|
};
|
|
280
301
|
}
|
|
281
302
|
|
|
@@ -457,118 +478,249 @@ function nestFlatRules (rules) {
|
|
|
457
478
|
}
|
|
458
479
|
|
|
459
480
|
/**
|
|
460
|
-
*
|
|
461
|
-
* the
|
|
462
|
-
*
|
|
463
|
-
* `[a~=b]`) are ignored so only structural combinators split the selector.
|
|
481
|
+
* Splits a selector into the part before its final top-level combinator and
|
|
482
|
+
* the trailing compound that follows it, so selectors that only differ in
|
|
483
|
+
* that leading part can share the trailing compound through nesting.
|
|
464
484
|
*
|
|
465
|
-
* @param {string} selector The selector string to
|
|
466
|
-
* @return {
|
|
485
|
+
* @param {string} selector The selector string to split.
|
|
486
|
+
* @return {object|null} The `head`, `combinator` (`' '` for descendant), and `tail`, or null when the selector has no top-level combinator.
|
|
467
487
|
*/
|
|
468
|
-
function
|
|
488
|
+
function splitSelectorTail (selector) {
|
|
469
489
|
const trimmed = selector.trim();
|
|
470
490
|
let bracketDepth = 0;
|
|
491
|
+
let parenthesisDepth = 0;
|
|
492
|
+
let combinatorStart = -1;
|
|
493
|
+
let combinator = '';
|
|
494
|
+
let tailStart = -1;
|
|
471
495
|
for (let index = 0; index < trimmed.length; index++) {
|
|
472
496
|
const character = trimmed[index];
|
|
473
|
-
if (character === '('
|
|
497
|
+
if (character === '(') {
|
|
498
|
+
parenthesisDepth++;
|
|
499
|
+
} else if (character === ')') {
|
|
500
|
+
parenthesisDepth--;
|
|
501
|
+
} else if (character === '[') {
|
|
474
502
|
bracketDepth++;
|
|
475
|
-
} else if (character === '
|
|
503
|
+
} else if (character === ']') {
|
|
476
504
|
bracketDepth--;
|
|
477
|
-
} else if (
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
505
|
+
} else if (parenthesisDepth === 0 && bracketDepth === 0) {
|
|
506
|
+
if (character === '>' || character === '+' || character === '~') {
|
|
507
|
+
// Whitespace written after an explicit combinator belongs to it
|
|
508
|
+
let end = index + 1;
|
|
509
|
+
while (end < trimmed.length && isSelectorWhitespace(trimmed[end])) {
|
|
510
|
+
end++;
|
|
511
|
+
}
|
|
512
|
+
combinator = character;
|
|
513
|
+
combinatorStart = index;
|
|
514
|
+
tailStart = end;
|
|
515
|
+
index = end - 1;
|
|
516
|
+
} else if (isSelectorWhitespace(character)) {
|
|
517
|
+
// Whitespace adjacent to an explicit combinator belongs to that
|
|
518
|
+
// combinator, so only a self-standing run is a descendant combinator
|
|
519
|
+
const previous = trimmed[index - 1];
|
|
520
|
+
let end = index;
|
|
521
|
+
while (end < trimmed.length && isSelectorWhitespace(trimmed[end])) {
|
|
522
|
+
end++;
|
|
523
|
+
}
|
|
524
|
+
const next = trimmed[end];
|
|
525
|
+
if (
|
|
526
|
+
next &&
|
|
527
|
+
next !== '>' && next !== '+' && next !== '~' &&
|
|
528
|
+
previous !== '>' && previous !== '+' && previous !== '~'
|
|
529
|
+
) {
|
|
530
|
+
combinator = ' ';
|
|
531
|
+
combinatorStart = index;
|
|
532
|
+
tailStart = end;
|
|
533
|
+
}
|
|
534
|
+
index = end - 1;
|
|
535
|
+
}
|
|
483
536
|
}
|
|
484
537
|
}
|
|
485
|
-
|
|
538
|
+
if (!combinator || combinatorStart === 0) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
const head = trimmed.slice(0, combinatorStart).trim();
|
|
542
|
+
const tail = trimmed.slice(tailStart).trim();
|
|
543
|
+
if (!head || !tail) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
combinator,
|
|
548
|
+
head,
|
|
549
|
+
tail
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Rewrites a multi-selector rule whose selectors only differ before their
|
|
555
|
+
* final combinator into a wrapper rule that nests the shared trailing
|
|
556
|
+
* compound once, such as `&:hover .x,&:focus .x` becoming
|
|
557
|
+
* `&:hover,&:focus{.x{...}}`. The heads join a comma list, which resolves
|
|
558
|
+
* like `:is()`, so they may only be grouped while they share one specificity.
|
|
559
|
+
*
|
|
560
|
+
* @param {object} rule The AST rule node whose selector list may be factored.
|
|
561
|
+
* @return {object} The rule, rewritten when factoring is valid and shorter.
|
|
562
|
+
*/
|
|
563
|
+
function factorCommonTrailingCompound (rule) {
|
|
564
|
+
if (rule.type !== 'rule' || !rule.selectors || rule.selectors.length < 2) {
|
|
565
|
+
return rule;
|
|
566
|
+
}
|
|
567
|
+
const splitSelectors = rule.selectors.map(splitSelectorTail);
|
|
568
|
+
if (splitSelectors.some((split) => {
|
|
569
|
+
return split === null;
|
|
570
|
+
})) {
|
|
571
|
+
return rule;
|
|
572
|
+
}
|
|
573
|
+
const first = splitSelectors[0];
|
|
574
|
+
const allShareTrailingCompound = splitSelectors.every((split) => {
|
|
575
|
+
return split.tail === first.tail && split.combinator === first.combinator;
|
|
576
|
+
});
|
|
577
|
+
if (!allShareTrailingCompound) {
|
|
578
|
+
return rule;
|
|
579
|
+
}
|
|
580
|
+
const heads = splitSelectors.map((split) => {
|
|
581
|
+
return split.head;
|
|
582
|
+
});
|
|
583
|
+
if (!selectorsShareSpecificity(heads)) {
|
|
584
|
+
return rule;
|
|
585
|
+
}
|
|
586
|
+
const nestedSelector = first.combinator === ' ' ? first.tail : first.combinator + first.tail;
|
|
587
|
+
const flatLength = rule.selectors.join(',').length;
|
|
588
|
+
const factoredLength = heads.join(',').length + nestedSelector.length + 2;
|
|
589
|
+
if (factoredLength >= flatLength) {
|
|
590
|
+
return rule;
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
...rule,
|
|
594
|
+
selectors: heads,
|
|
595
|
+
declarations: [{
|
|
596
|
+
type: 'rule',
|
|
597
|
+
selectors: [nestedSelector],
|
|
598
|
+
declarations: rule.declarations
|
|
599
|
+
}]
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Collects every selector prefix of a rule's first selector that could serve
|
|
605
|
+
* as a shared nesting parent, including the full selector itself, which can
|
|
606
|
+
* host sibling pseudo-class variants such as `.a` of `.a,.a:hover`.
|
|
607
|
+
*
|
|
608
|
+
* @param {object} rule The AST rule node whose candidates are collected.
|
|
609
|
+
* @return {Set} The candidate parent selector strings.
|
|
610
|
+
*/
|
|
611
|
+
function collectSharedParentCandidates (rule) {
|
|
612
|
+
const firstSelector = rule.selectors[0].trim();
|
|
613
|
+
const candidates = collectCandidateParentSelectors(firstSelector);
|
|
614
|
+
if (firstSelector) {
|
|
615
|
+
candidates.add(firstSelector);
|
|
616
|
+
}
|
|
617
|
+
return candidates;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Counts how many characters wrapping a run behind a shared parent selector
|
|
622
|
+
* saves: the parent is written once with its braces, and every wrapped
|
|
623
|
+
* selector shrinks to its nested form.
|
|
624
|
+
*
|
|
625
|
+
* @param {Array} run The rules of the run paired with their nested selectors.
|
|
626
|
+
* @param {string} parent The shared parent selector string.
|
|
627
|
+
* @return {number} The character savings of the wrapper, or a non-positive number when it costs more than it saves.
|
|
628
|
+
*/
|
|
629
|
+
function measureWrapperSavings (run, parent) {
|
|
630
|
+
const charactersSaved = run.reduce((total, entry) => {
|
|
631
|
+
return total + entry.sibling.selectors.reduce((sum, selector, position) => {
|
|
632
|
+
return sum + selector.trim().length - entry.nestedSelectors[position].length;
|
|
633
|
+
}, 0);
|
|
634
|
+
}, 0);
|
|
635
|
+
// The wrapper writes the shared selector once plus its surrounding braces.
|
|
636
|
+
return charactersSaved - (parent.length + 2);
|
|
486
637
|
}
|
|
487
638
|
|
|
488
639
|
/**
|
|
489
|
-
* Groups consecutive sibling rules that share a common leading
|
|
490
|
-
* (such as `.foo` in `.foo .a`, `.foo .b
|
|
491
|
-
*
|
|
492
|
-
*
|
|
640
|
+
* Groups consecutive sibling rules that share a common leading selector
|
|
641
|
+
* portion (such as `.foo` in `.foo .a`, `.foo .b`, or `.menu .item` in
|
|
642
|
+
* `.menu .item:hover .x`) into a synthesized parent rule with the shared
|
|
643
|
+
* portion factored out. Every viable prefix of the first rule's selector is
|
|
644
|
+
* tried as the parent, and the grouping that saves the most characters wins,
|
|
645
|
+
* but only when it saves more than the wrapper itself costs. Rules with a
|
|
646
|
+
* comma-separated selector list can join a run when every one of their
|
|
647
|
+
* selectors nests under the parent.
|
|
493
648
|
*
|
|
494
|
-
* @param {Array}
|
|
495
|
-
* @
|
|
649
|
+
* @param {Array} rules The flat AST rule nodes to factor.
|
|
650
|
+
* @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.
|
|
651
|
+
* @return {Array} A new array of rules with shared parent selectors factored into nesting wrappers.
|
|
496
652
|
*/
|
|
497
|
-
function factorCommonParents (rules) {
|
|
653
|
+
function factorCommonParents (rules, allowTrailingCompound = false) {
|
|
498
654
|
const result = [];
|
|
499
655
|
let index = 0;
|
|
500
656
|
while (index < rules.length) {
|
|
501
657
|
const rule = rules[index];
|
|
502
|
-
if (rule.type
|
|
503
|
-
const candidateParent = extractLeadingCompound(rule.selectors[0]) || rule.selectors[0];
|
|
504
|
-
if (candidateParent) {
|
|
505
|
-
const matchNestedSelector = createNestedSelectorMatcher(candidateParent);
|
|
506
|
-
const childSelectors = rule.selectors.map(matchNestedSelector);
|
|
507
|
-
const allMatch = childSelectors.every((selector) => {
|
|
508
|
-
return selector !== null;
|
|
509
|
-
});
|
|
510
|
-
const wrapperLength = candidateParent.length + 2 + childSelectors.join(',').length;
|
|
511
|
-
if (allMatch && !containsPseudoElement(candidateParent) && wrapperLength < rule.selectors.join(',').length) {
|
|
512
|
-
result.push({
|
|
513
|
-
type: 'rule',
|
|
514
|
-
selectors: [candidateParent],
|
|
515
|
-
declarations: [{ ...rule, selectors: childSelectors }]
|
|
516
|
-
});
|
|
517
|
-
index++;
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
// Only single-selector style rules can act as a factoring candidate.
|
|
523
|
-
if (rule.type !== 'rule' || rule.selectors?.length !== 1) {
|
|
524
|
-
result.push(rule);
|
|
525
|
-
index++;
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
const candidateParent = extractLeadingCompound(rule.selectors[0]);
|
|
529
|
-
if (candidateParent === null) {
|
|
658
|
+
if (rule.type !== 'rule' || !rule.selectors?.length) {
|
|
530
659
|
result.push(rule);
|
|
531
660
|
index++;
|
|
532
661
|
continue;
|
|
533
662
|
}
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
let lookahead = index;
|
|
539
|
-
while (lookahead < rules.length) {
|
|
540
|
-
const sibling = rules[lookahead];
|
|
541
|
-
if (sibling.type !== 'rule' || sibling.selectors?.length !== 1) {
|
|
542
|
-
break;
|
|
663
|
+
let bestGrouping = null;
|
|
664
|
+
for (const candidateParent of collectSharedParentCandidates(rule)) {
|
|
665
|
+
if (!candidateParent || containsPseudoElement(candidateParent)) {
|
|
666
|
+
continue;
|
|
543
667
|
}
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
668
|
+
const matchNestedSelector = createNestedSelectorMatcher(candidateParent);
|
|
669
|
+
// Collect the run of consecutive rules whose selectors can all nest under the candidate.
|
|
670
|
+
const run = [];
|
|
671
|
+
let lookahead = index;
|
|
672
|
+
while (lookahead < rules.length) {
|
|
673
|
+
const sibling = rules[lookahead];
|
|
674
|
+
if (sibling.type !== 'rule' || !sibling.selectors?.length) {
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
const nestedSelectors = sibling.selectors.map((selector) => {
|
|
678
|
+
return matchNestedSelector(selector.trim());
|
|
679
|
+
});
|
|
680
|
+
if (nestedSelectors.some((nestedSelector) => {
|
|
681
|
+
return nestedSelector === null;
|
|
682
|
+
})) {
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
run.push({ sibling, nestedSelectors });
|
|
686
|
+
lookahead++;
|
|
687
|
+
}
|
|
688
|
+
const selectorCount = run.reduce((total, entry) => {
|
|
689
|
+
return total + entry.nestedSelectors.length;
|
|
690
|
+
}, 0);
|
|
691
|
+
// A lone single-selector rule has nothing to share its parent with.
|
|
692
|
+
if (run.length < 2 && selectorCount < 2) {
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
const savings = measureWrapperSavings(run, candidateParent);
|
|
696
|
+
if (savings > 0 && (!bestGrouping || savings > bestGrouping.savings)) {
|
|
697
|
+
bestGrouping = {
|
|
698
|
+
lookahead,
|
|
699
|
+
parent: candidateParent,
|
|
700
|
+
run,
|
|
701
|
+
savings
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (!bestGrouping) {
|
|
706
|
+
if (allowTrailingCompound) {
|
|
707
|
+
result.push(factorCommonTrailingCompound(rule));
|
|
708
|
+
} else {
|
|
709
|
+
result.push(rule);
|
|
547
710
|
}
|
|
548
|
-
run.push(sibling);
|
|
549
|
-
nestedForms.push(nestedSelector);
|
|
550
|
-
lookahead++;
|
|
551
|
-
}
|
|
552
|
-
// The wrapper writes the shared selector once plus its surrounding braces.
|
|
553
|
-
const wrapperCost = candidateParent.length + 2;
|
|
554
|
-
const charactersSaved = run.reduce((total, sibling, position) => {
|
|
555
|
-
return total + sibling.selectors[0].trim().length - nestedForms[position].length;
|
|
556
|
-
}, 0);
|
|
557
|
-
if (run.length >= 2 && charactersSaved > wrapperCost) {
|
|
558
|
-
const children = run.map((sibling, position) => {
|
|
559
|
-
return { ...sibling, selectors: [nestedForms[position]] };
|
|
560
|
-
});
|
|
561
|
-
result.push({
|
|
562
|
-
type: 'rule',
|
|
563
|
-
selectors: [candidateParent],
|
|
564
|
-
// Recurse so deeper shared prefixes among the children also factor out.
|
|
565
|
-
declarations: factorCommonParents(children)
|
|
566
|
-
});
|
|
567
|
-
index = lookahead;
|
|
568
|
-
} else {
|
|
569
|
-
result.push(rule);
|
|
570
711
|
index++;
|
|
712
|
+
continue;
|
|
571
713
|
}
|
|
714
|
+
const children = bestGrouping.run.map((entry) => {
|
|
715
|
+
return { ...entry.sibling, selectors: entry.nestedSelectors };
|
|
716
|
+
});
|
|
717
|
+
result.push({
|
|
718
|
+
type: 'rule',
|
|
719
|
+
selectors: [bestGrouping.parent],
|
|
720
|
+
// Recurse so deeper shared prefixes among the children also factor out.
|
|
721
|
+
declarations: factorCommonParents(children, true)
|
|
722
|
+
});
|
|
723
|
+
index = bestGrouping.lookahead;
|
|
572
724
|
}
|
|
573
725
|
return result;
|
|
574
726
|
}
|
|
@@ -629,7 +781,8 @@ function mergeMediaRules (rules, mergeSelectorRules) {
|
|
|
629
781
|
result.push(rule);
|
|
630
782
|
}
|
|
631
783
|
} else {
|
|
632
|
-
|
|
784
|
+
// Clearing a map replaces its storage, so an already empty one is left alone
|
|
785
|
+
if (rule.type !== 'whitespace' && mediaMap.size) {
|
|
633
786
|
mediaMap.clear();
|
|
634
787
|
}
|
|
635
788
|
result.push(rule);
|
|
@@ -878,12 +1031,40 @@ function mergeByDeclarations (rules) {
|
|
|
878
1031
|
function skipSelectorName (text, start) {
|
|
879
1032
|
let index = start;
|
|
880
1033
|
// Advance over identifier characters (letters, digits, hyphen, underscore)
|
|
881
|
-
while (index < text.length && (
|
|
1034
|
+
while (index < text.length && isSelectorNameCharacter(text.charCodeAt(index))) {
|
|
882
1035
|
index++;
|
|
883
1036
|
}
|
|
884
1037
|
return index;
|
|
885
1038
|
}
|
|
886
1039
|
|
|
1040
|
+
/**
|
|
1041
|
+
* Reports whether a character code is an ASCII letter.
|
|
1042
|
+
*
|
|
1043
|
+
* @param {number} characterCode The UTF-16 code unit of the character.
|
|
1044
|
+
* @return {boolean} True for `a`-`z` and `A`-`Z`.
|
|
1045
|
+
*/
|
|
1046
|
+
function isAsciiLetter (characterCode) {
|
|
1047
|
+
return (
|
|
1048
|
+
(characterCode >= 97 && characterCode <= 122) ||
|
|
1049
|
+
(characterCode >= 65 && characterCode <= 90)
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Reports whether a character code can appear in a selector name, which here
|
|
1055
|
+
* means an ASCII letter, digit, hyphen, or underscore. Comparing character
|
|
1056
|
+
* codes avoids running a regular expression once per character.
|
|
1057
|
+
*
|
|
1058
|
+
* @param {number} characterCode The UTF-16 code unit of the character.
|
|
1059
|
+
* @return {boolean} True when the character continues a name.
|
|
1060
|
+
*/
|
|
1061
|
+
function isSelectorNameCharacter (characterCode) {
|
|
1062
|
+
const isDigit = characterCode >= 48 && characterCode <= 57;
|
|
1063
|
+
const isHyphen = characterCode === 45;
|
|
1064
|
+
const isUnderscore = characterCode === 95;
|
|
1065
|
+
return isAsciiLetter(characterCode) || isDigit || isHyphen || isUnderscore;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
887
1068
|
/**
|
|
888
1069
|
* Compares two specificity tuples [ids, classes, types] lexicographically.
|
|
889
1070
|
*
|
|
@@ -964,7 +1145,7 @@ function computeSpecificity (selector) {
|
|
|
964
1145
|
} else if (character === '*' || character === '&') {
|
|
965
1146
|
// Universal selector and nesting selector add no counted specificity here
|
|
966
1147
|
index++;
|
|
967
|
-
} else if ((
|
|
1148
|
+
} else if (isAsciiLetter(text.charCodeAt(index))) {
|
|
968
1149
|
specificity[2]++;
|
|
969
1150
|
index = skipSelectorName(text, index);
|
|
970
1151
|
} else {
|
|
@@ -976,21 +1157,84 @@ function computeSpecificity (selector) {
|
|
|
976
1157
|
}
|
|
977
1158
|
|
|
978
1159
|
/**
|
|
979
|
-
*
|
|
980
|
-
*
|
|
1160
|
+
* Memoizes the strongest and weakest specificity of each selector list seen
|
|
1161
|
+
* during the current stylesheet. The same rules are measured again on every
|
|
1162
|
+
* merge pass, and a selector list always has the same specificity, so each
|
|
1163
|
+
* distinct list is only split and scanned once per stylesheet.
|
|
1164
|
+
*
|
|
1165
|
+
* @type {Map<string, {strongest: Array, weakest: Array}>}
|
|
1166
|
+
*/
|
|
1167
|
+
const specificityRangeCache = new Map();
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Memoizes the normalized form of each selector seen during the current
|
|
1171
|
+
* stylesheet.
|
|
1172
|
+
*
|
|
1173
|
+
* @type {Map<string, string>}
|
|
1174
|
+
*/
|
|
1175
|
+
const normalizedSelectorCache = new Map();
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Discards every memoized specificity and normalized selector. The minifier
|
|
1179
|
+
* calls this around each stylesheet, so the caches do not outlive the pass that
|
|
1180
|
+
* filled them.
|
|
1181
|
+
*/
|
|
1182
|
+
function clearSelectorCaches () {
|
|
1183
|
+
specificityRangeCache.clear();
|
|
1184
|
+
normalizedSelectorCache.clear();
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Scans every selector of a comma-separated list for the strongest and the
|
|
1189
|
+
* weakest specificity among them. The returned tuples are frozen because they
|
|
1190
|
+
* are shared by everything that asks about the same list.
|
|
981
1191
|
*
|
|
982
1192
|
* @param {string} selectorList The selector list string.
|
|
983
|
-
* @return {
|
|
1193
|
+
* @return {object} The `strongest` and `weakest` [ids, classes, types] tuples.
|
|
984
1194
|
*/
|
|
985
|
-
function
|
|
986
|
-
let
|
|
1195
|
+
function computeSpecificityRange (selectorList) {
|
|
1196
|
+
let strongest = [0, 0, 0];
|
|
1197
|
+
let weakest = null;
|
|
987
1198
|
for (const selector of splitTopLevelCommaList(selectorList)) {
|
|
988
1199
|
const specificity = computeSpecificity(selector);
|
|
989
|
-
if (compareSpecificity(specificity,
|
|
990
|
-
|
|
1200
|
+
if (compareSpecificity(specificity, strongest) > 0) {
|
|
1201
|
+
strongest = specificity;
|
|
1202
|
+
}
|
|
1203
|
+
if (!weakest || compareSpecificity(specificity, weakest) < 0) {
|
|
1204
|
+
weakest = specificity;
|
|
991
1205
|
}
|
|
992
1206
|
}
|
|
993
|
-
return
|
|
1207
|
+
return {
|
|
1208
|
+
strongest: Object.freeze(strongest),
|
|
1209
|
+
weakest: Object.freeze(weakest || [0, 0, 0])
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Returns the strongest and weakest specificity of a selector list, measuring
|
|
1215
|
+
* it the first time it is asked about.
|
|
1216
|
+
*
|
|
1217
|
+
* @param {string} selectorList The selector list string.
|
|
1218
|
+
* @return {object} The `strongest` and `weakest` [ids, classes, types] tuples.
|
|
1219
|
+
*/
|
|
1220
|
+
function getSpecificityRange (selectorList) {
|
|
1221
|
+
let range = specificityRangeCache.get(selectorList);
|
|
1222
|
+
if (!range) {
|
|
1223
|
+
range = computeSpecificityRange(selectorList);
|
|
1224
|
+
specificityRangeCache.set(selectorList, range);
|
|
1225
|
+
}
|
|
1226
|
+
return range;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Returns the maximum specificity tuple across a comma-separated selector list,
|
|
1231
|
+
* matching how a comma-separated group behaves as `:is()`.
|
|
1232
|
+
*
|
|
1233
|
+
* @param {string} selectorList The selector list string.
|
|
1234
|
+
* @return {Array} The maximum [ids, classes, types] tuple.
|
|
1235
|
+
*/
|
|
1236
|
+
function maxSelectorSpecificity (selectorList) {
|
|
1237
|
+
return getSpecificityRange(selectorList).strongest;
|
|
994
1238
|
}
|
|
995
1239
|
|
|
996
1240
|
/**
|
|
@@ -1014,14 +1258,7 @@ function maximumRuleSpecificity (selectors) {
|
|
|
1014
1258
|
* @return {Array} The [ids, classes, types] specificity tuple.
|
|
1015
1259
|
*/
|
|
1016
1260
|
function minimumRuleSpecificity (selectors) {
|
|
1017
|
-
|
|
1018
|
-
for (const selector of splitTopLevelCommaList((selectors || []).join(','))) {
|
|
1019
|
-
const specificity = computeSpecificity(selector);
|
|
1020
|
-
if (!minimum || compareSpecificity(specificity, minimum) < 0) {
|
|
1021
|
-
minimum = specificity;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
return minimum || [0, 0, 0];
|
|
1261
|
+
return getSpecificityRange((selectors || []).join(',')).weakest;
|
|
1025
1262
|
}
|
|
1026
1263
|
|
|
1027
1264
|
/**
|
|
@@ -1300,11 +1537,11 @@ function absorbWriteProfile (profile, absorbedWrites) {
|
|
|
1300
1537
|
* position grows, so the first entry past a queried position is also the
|
|
1301
1538
|
* strongest one past it.
|
|
1302
1539
|
*
|
|
1303
|
-
* @
|
|
1540
|
+
* @param {Map} profileByRule The write profiles already built for rules, shared by every pass of one merge.
|
|
1541
|
+
* @return {object} An index exposing `recordRule` and `blocksRelocation`.
|
|
1304
1542
|
*/
|
|
1305
|
-
function createOverrideIndex () {
|
|
1543
|
+
function createOverrideIndex (profileByRule) {
|
|
1306
1544
|
const entriesByProperty = new Map();
|
|
1307
|
-
const profileByRule = new Map();
|
|
1308
1545
|
|
|
1309
1546
|
/**
|
|
1310
1547
|
* Records that a property is written at a position with a given specificity.
|
|
@@ -1357,8 +1594,9 @@ function createOverrideIndex () {
|
|
|
1357
1594
|
|
|
1358
1595
|
/**
|
|
1359
1596
|
* Returns a rule's write profile, building it the first time it is needed.
|
|
1360
|
-
* Rules only ever grow here, by absorbing another rule's declarations,
|
|
1361
|
-
*
|
|
1597
|
+
* Rules only ever grow here, by absorbing another rule's declarations, and
|
|
1598
|
+
* `absorbRule` folds those into the profile, so a profile stays valid for
|
|
1599
|
+
* every pass of the merge.
|
|
1362
1600
|
*
|
|
1363
1601
|
* @param {object} rule The AST rule node.
|
|
1364
1602
|
* @return {RuleWriteProfile} The rule's write profile.
|
|
@@ -1453,6 +1691,24 @@ function buildSelectorKey (rule) {
|
|
|
1453
1691
|
.join(',');
|
|
1454
1692
|
}
|
|
1455
1693
|
|
|
1694
|
+
/**
|
|
1695
|
+
* Returns a rule's selector key, building it the first time it is needed. A
|
|
1696
|
+
* merge only ever grows a rule's declarations, never its selectors, so a key
|
|
1697
|
+
* stays valid for every pass of the merge.
|
|
1698
|
+
*
|
|
1699
|
+
* @param {object} rule The AST rule node.
|
|
1700
|
+
* @param {Map} selectorKeyByRule The selector keys already built, shared by every pass of one merge.
|
|
1701
|
+
* @return {string} The normalized, order independent selector key.
|
|
1702
|
+
*/
|
|
1703
|
+
function getSelectorKey (rule, selectorKeyByRule) {
|
|
1704
|
+
let selectorKey = selectorKeyByRule.get(rule);
|
|
1705
|
+
if (selectorKey === undefined) {
|
|
1706
|
+
selectorKey = buildSelectorKey(rule);
|
|
1707
|
+
selectorKeyByRule.set(rule, selectorKey);
|
|
1708
|
+
}
|
|
1709
|
+
return selectorKey;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1456
1712
|
/**
|
|
1457
1713
|
* Placement for a merged rule whose combined declarations belong where the
|
|
1458
1714
|
* later of the two rules was, because the earlier declarations moved down.
|
|
@@ -1584,10 +1840,11 @@ function chooseMergePlacement (overrideIndex, earlierRule, laterRule, earlierPos
|
|
|
1584
1840
|
/**
|
|
1585
1841
|
* Merges rules with identical normalized selectors by combining their declarations, as long as `chooseMergePlacement` finds a spot for the combined rule that the cascade reads the same way. Non-rule entries that affect the cascade (like `@media`) break the merge window, while comments do not, since comments never influence how declarations apply.
|
|
1586
1842
|
*
|
|
1587
|
-
* @param {Array}
|
|
1588
|
-
* @
|
|
1843
|
+
* @param {Array} rules The AST rule nodes to merge.
|
|
1844
|
+
* @param {object} mergeCaches The write profiles and selector keys shared by every pass of one merge.
|
|
1845
|
+
* @return {Array} A new array of rules with same-selector rules combined.
|
|
1589
1846
|
*/
|
|
1590
|
-
function mergeSelectorRulePass (rules) {
|
|
1847
|
+
function mergeSelectorRulePass (rules, mergeCaches) {
|
|
1591
1848
|
// A rule merged at the later of the two positions moves to the end of the
|
|
1592
1849
|
// output. Its old slot is emptied instead of spliced out so that every
|
|
1593
1850
|
// recorded position stays valid, and the position map locates that slot
|
|
@@ -1595,20 +1852,21 @@ function mergeSelectorRulePass (rules) {
|
|
|
1595
1852
|
const slots = [];
|
|
1596
1853
|
const positionByRule = new Map();
|
|
1597
1854
|
const selectorMap = new Map();
|
|
1598
|
-
const overrideIndex = createOverrideIndex();
|
|
1855
|
+
const overrideIndex = createOverrideIndex(mergeCaches.profileByRule);
|
|
1599
1856
|
for (const rule of rules) {
|
|
1600
1857
|
if (rule.type === 'whitespace') {
|
|
1601
1858
|
continue;
|
|
1602
1859
|
}
|
|
1603
1860
|
if (rule.type !== 'rule') {
|
|
1604
1861
|
slots.push(rule);
|
|
1605
|
-
|
|
1862
|
+
// Clearing a map replaces its storage, so already empty maps are left alone
|
|
1863
|
+
if (rule.type !== 'comment' && selectorMap.size) {
|
|
1606
1864
|
selectorMap.clear();
|
|
1607
1865
|
positionByRule.clear();
|
|
1608
1866
|
}
|
|
1609
1867
|
continue;
|
|
1610
1868
|
}
|
|
1611
|
-
const selectorKey =
|
|
1869
|
+
const selectorKey = getSelectorKey(rule, mergeCaches.selectorKeyByRule);
|
|
1612
1870
|
const existingRule = selectorKey && selectorMap.get(selectorKey);
|
|
1613
1871
|
if (existingRule) {
|
|
1614
1872
|
const existingPosition = positionByRule.get(existingRule);
|
|
@@ -1651,11 +1909,15 @@ function mergeSelectorRulePass (rules) {
|
|
|
1651
1909
|
* @return {Array} Rules with all currently safe same-selector merges.
|
|
1652
1910
|
*/
|
|
1653
1911
|
function mergeSelectorRules (rules) {
|
|
1912
|
+
const mergeCaches = {
|
|
1913
|
+
profileByRule: new Map(),
|
|
1914
|
+
selectorKeyByRule: new Map()
|
|
1915
|
+
};
|
|
1654
1916
|
let previousRules = rules;
|
|
1655
|
-
let mergedRules = mergeSelectorRulePass(previousRules);
|
|
1917
|
+
let mergedRules = mergeSelectorRulePass(previousRules, mergeCaches);
|
|
1656
1918
|
while (mergedRules.length < previousRules.length) {
|
|
1657
1919
|
previousRules = mergedRules;
|
|
1658
|
-
mergedRules = mergeSelectorRulePass(previousRules);
|
|
1920
|
+
mergedRules = mergeSelectorRulePass(previousRules, mergeCaches);
|
|
1659
1921
|
}
|
|
1660
1922
|
return mergedRules;
|
|
1661
1923
|
}
|
|
@@ -1710,15 +1972,35 @@ function mergeLayerRules (rules, mergeSelectorRules) {
|
|
|
1710
1972
|
* @param {string} selector The raw selector string.
|
|
1711
1973
|
* @return {string} The normalized selector.
|
|
1712
1974
|
*/
|
|
1713
|
-
function
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1975
|
+
function computeNormalizedSelector (selector) {
|
|
1976
|
+
// Collapse each whitespace run into a single space
|
|
1977
|
+
const normalized = selector.trim().replace(/\s+/g, ' ');
|
|
1978
|
+
if (!normalized.includes('::')) {
|
|
1979
|
+
return normalized;
|
|
1980
|
+
}
|
|
1981
|
+
return normalized
|
|
1717
1982
|
// Convert double-colon ::before/::after to single-colon legacy form
|
|
1718
1983
|
.replace(/::before\b/g, ':before')
|
|
1719
1984
|
.replace(/::after\b/g, ':after');
|
|
1720
1985
|
}
|
|
1721
1986
|
|
|
1987
|
+
/**
|
|
1988
|
+
* Normalizes a selector string for consistent comparison, reusing the result
|
|
1989
|
+
* when the same selector was already normalized during this stylesheet. Every
|
|
1990
|
+
* merge pass compares the same selectors again, so each is only rewritten once.
|
|
1991
|
+
*
|
|
1992
|
+
* @param {string} selector The raw selector string.
|
|
1993
|
+
* @return {string} The normalized selector.
|
|
1994
|
+
*/
|
|
1995
|
+
function normalizeSelector (selector) {
|
|
1996
|
+
let normalized = normalizedSelectorCache.get(selector);
|
|
1997
|
+
if (normalized === undefined) {
|
|
1998
|
+
normalized = computeNormalizedSelector(selector);
|
|
1999
|
+
normalizedSelectorCache.set(selector, normalized);
|
|
2000
|
+
}
|
|
2001
|
+
return normalized;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1722
2004
|
/**
|
|
1723
2005
|
* Indexes, for every normalized selector in the stylesheet, the last rule that
|
|
1724
2006
|
* declares each property under it. Comparing that index against a rule's own
|
|
@@ -2015,6 +2297,7 @@ function optimizeRuleList (rules) {
|
|
|
2015
2297
|
}
|
|
2016
2298
|
|
|
2017
2299
|
export {
|
|
2300
|
+
clearSelectorCaches,
|
|
2018
2301
|
deduplicateKeyframes,
|
|
2019
2302
|
expandPureNestedRules,
|
|
2020
2303
|
factorCommonParents,
|