+2
-2
@@ -64,3 +64,3 @@ { | ||
| "types": "src/index.d.ts", | ||
| "version": "1.3.2" | ||
| } | ||
| "version": "1.3.3" | ||
| } |
+42
-7
| import postcss from 'postcss'; | ||
| import { normalizeProp, declarationKey } from './normalization.js'; | ||
| import { isIgnoredSelector, resolveIgnorePatterns } from './hacks.js'; | ||
| import { splitSelectors, selectorsAreMutuallyExclusive, selectorsLikelyDisjoint, resetSubjectIdentities } from './selectors.js'; | ||
| import { splitSelectors, hasSpacedTopLevelComma, selectorsAreMutuallyExclusive, selectorsLikelyDisjoint, resetSubjectIdentities } from './selectors.js'; | ||
| import { propertiesOverlap } from './shorthands.js'; | ||
@@ -362,4 +362,38 @@ | ||
| function joinSelectors(selectors, rule, multiline) { | ||
| if (!multiline) return selectors.join(', '); | ||
| // Detects whether this style sheet’s existing multi-selector rules put a | ||
| // space after the (inline) comma (`.a, .b {}`) or not (`.a,.b{}`, as a | ||
| // minifier writes it)—by majority vote among their first top-level commas, | ||
| // same “majority vote” approach as `typicalSeparator()` above. Only rules | ||
| // already written inline count—a rule using the multiline convention says | ||
| // nothing about inline comma spacing. | ||
| function usesSpacedCommas(root) { | ||
| let spaced = 0; | ||
| let tight = 0; | ||
| root.walkRules(rule => { | ||
| if (RE_MULTILINE_SELECTOR_SEPARATOR.test(rule.selector)) return; | ||
| const spacedComma = hasSpacedTopLevelComma(rule.selector); | ||
| if (spacedComma === null) return; | ||
| if (spacedComma) spaced++; else tight++; | ||
| }); | ||
| if (spaced || tight) return spaced >= tight; | ||
| // No existing multi-selector rule to learn the comma convention from | ||
| // (the merge about to run may be creating the file’s first one)—fall back | ||
| // to whether rules already omit the space before their opening brace | ||
| // (`.a{}` vs `.a {}`), which a minifier strips just as consistently as | ||
| // the space after a comma | ||
| return usesSpacedBraces(root); | ||
| } | ||
| function usesSpacedBraces(root) { | ||
| let spaced = 0; | ||
| let tight = 0; | ||
| root.walkRules(rule => { | ||
| if ((rule.raws.between ?? ' ') === '') tight++; else spaced++; | ||
| }); | ||
| return spaced >= tight; | ||
| } | ||
| function joinSelectors(selectors, rule, multiline, spacedCommas) { | ||
| if (!multiline) return selectors.join(spacedCommas ? ', ' : ','); | ||
| const indent = (rule.raws.before ?? '').match(RE_TRAILING_INDENT)[0]; | ||
@@ -438,2 +472,3 @@ return selectors.join(`,\n${indent}`); | ||
| const multilineSelectors = usesMultilineSelectors(root); | ||
| const spacedCommas = usesSpacedCommas(root); | ||
| const applied = []; | ||
@@ -685,3 +720,3 @@ const skipped = []; | ||
| const targetOriginalSelector = target.selector; | ||
| target.selector = joinSelectors(mergedSelectors, target, multilineSelectors); | ||
| target.selector = joinSelectors(mergedSelectors, target, multilineSelectors, spacedCommas); | ||
@@ -845,3 +880,3 @@ // All occurrences share a normalized key, so they’re equivalent by our | ||
| } | ||
| target.selector = joinSelectors(mergedSelectors, target, multilineSelectors); | ||
| target.selector = joinSelectors(mergedSelectors, target, multilineSelectors, spacedCommas); | ||
@@ -930,3 +965,3 @@ for (const decl of target.nodes) { | ||
| const mergedRule = lastRule.clone({ nodes: [] }); | ||
| mergedRule.selector = joinSelectors(mergedSelectors, lastRule, multilineSelectors); | ||
| mergedRule.selector = joinSelectors(mergedSelectors, lastRule, multilineSelectors, spacedCommas); | ||
| const lastDecl = runOccurrences.find(occ => occ.rule === lastRule).decl; | ||
@@ -1092,3 +1127,3 @@ lastDecl.remove(); | ||
| const anchorRule = hub.clone({ nodes: [] }); | ||
| anchorRule.selector = joinSelectors(mergedSelectors, hub, multilineSelectors); | ||
| anchorRule.selector = joinSelectors(mergedSelectors, hub, multilineSelectors, spacedCommas); | ||
| sharedDecl.remove(); | ||
@@ -1095,0 +1130,0 @@ anchorRule.append(sharedDecl); |
+28
-0
@@ -56,2 +56,30 @@ // Splits a selector list on top-level commas only, respecting commas nested | ||
| // Whether a selector list’s top-level commas are followed by whitespace | ||
| // (`.a, .b`) or not (`.a,.b`, as a minifier writes it)—checked against the | ||
| // first top-level comma only, on the assumption that one selector list | ||
| // doesn’t mix conventions. “null” when there’s no top-level comma to judge | ||
| // by. Reuses the same depth/quote/escape-aware scan as `splitSelectors()` | ||
| // above, so a comma nested inside `:is(a,b)`/`[attr="a,b"]` is never | ||
| // mistaken for the list’s own separator. | ||
| export function hasSpacedTopLevelComma(selectorList) { | ||
| let depth = 0; | ||
| let quote = null; | ||
| let escaped = false; | ||
| for (let i = 0; i < selectorList.length; i++) { | ||
| const char = selectorList[i]; | ||
| if (escaped) { escaped = false; continue; } | ||
| if (char === '\\') { escaped = true; continue; } | ||
| if (quote) { if (char === quote) quote = null; continue; } | ||
| if (char === '"' || char === '\'') { quote = char; continue; } | ||
| if (char === '(' || char === '[') depth++; | ||
| if (char === ')' || char === ']') depth = Math.max(0, depth - 1); | ||
| if (char === ',' && depth === 0) return /\s/.test(selectorList[i + 1] ?? ''); | ||
| } | ||
| return null; | ||
| } | ||
| // Matches one `[attr]`, `[attr=value]`, `[attr~=value]`, `[attr|=value]`, | ||
@@ -58,0 +86,0 @@ // `[attr^=value]`, `[attr$=value]`, or `[attr*=value]`, quoted or not, with |
164377
1.65%2654
2.12%