Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions fixtures/compress/restructure.merge/issue-417.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
Issue #417
Same class of bug as #478: extracting `border-bottom` and then
applying `border` afterwards resets the bottom edge.
*/

.class1 {
border-bottom: 1px solid black;
}

.class2 {
border: 0;
border-bottom: 1px solid black;
}

.class3 {
border: 0;
}
1 change: 1 addition & 0 deletions fixtures/compress/restructure.merge/issue-417.min.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.class1{border-bottom:1px solid #000}.class2{border:0;border-bottom:1px solid #000}.class3{border:0}
17 changes: 17 additions & 0 deletions fixtures/compress/restructure.merge/issue-478-all.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
Issue #478
`all` resets almost every property. Extracting `color`/`display` into
a shared rule in front of a leftover `all` would wipe them.
*/

.a {
all: unset;
color: red;
display: block;
}

.b {
all: initial;
color: red;
display: block;
}
1 change: 1 addition & 0 deletions fixtures/compress/restructure.merge/issue-478-all.min.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.a{all:unset;color:red;display:block}.b{all:initial;color:red;display:block}
19 changes: 19 additions & 0 deletions fixtures/compress/restructure.merge/issue-478-important.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
Issue #478
!important on the longhand makes order irrelevant, so it is still
safe to extract `background-size` in front of `background`.
*/

.a {
background: none;
background-size: 1 !important;
color: red;
display: block;
}

.b {
background: some;
background-size: 1 !important;
color: red;
display: block;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.a,.b{background-size:1!important;color:red;display:block}.a{background:0 0}.b{background:some}
19 changes: 19 additions & 0 deletions fixtures/compress/restructure.merge/issue-478-shared.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
Issue #478
Unrelated equal declarations can still be pulled out. The longhand
that a remaining shorthand would reset must stay after that shorthand.
*/

.a {
background: none;
background-size: cover;
color: red;
display: block;
}

.b {
background: url(x);
background-size: cover;
color: red;
display: block;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.a,.b{color:red;display:block}.a{background:0 0;background-size:cover}.b{background:url(x);background-size:cover}
16 changes: 16 additions & 0 deletions fixtures/compress/restructure.merge/issue-478-subset-before.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
Issue #478
Subset merge where the extra shorthand stays in the earlier rule is
safe: `background` then `background-size` keeps the longhand.
*/

.a {
background: none;
background-size: 1;
b: cccccccc;
}

.b {
background-size: 1;
b: cccccccc;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.a{background:0 0}.a,.b{background-size:1;b:cccccccc}
16 changes: 16 additions & 0 deletions fixtures/compress/restructure.merge/issue-478-subset.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
Issue #478
Subset merge: the later rule has an extra shorthand. Do not keep the
shared longhand in the merged rule before that shorthand.
*/

.a {
background-size: 1;
b: cccccccc;
}

.b {
background: some;
background-size: 1;
b: cccccccc;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.a,.b{b:cccccccc}.a{background-size:1}.b{background:some;background-size:1}
18 changes: 18 additions & 0 deletions fixtures/compress/restructure.merge/issue-478.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
Issue #478
Do not extract a longhand into a shared rule that sits before a
later shorthand. `background` resets `background-size`, so leaving
`background-size` first wipes it in the browser.
*/

.a {
background: none;
background-size: 1;
b: c;
}

.b {
background: some;
background-size: 1;
b: c;
}
1 change: 1 addition & 0 deletions fixtures/compress/restructure.merge/issue-478.min.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.a{background:0 0;background-size:1;b:c}.b{background:some;background-size:1;b:c}
4 changes: 2 additions & 2 deletions lib/restructure/8-restructRuleset.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { List, walk } from 'css-tree';
import {
unsafeToSkipNode,
isEqualSelectors,
compareDeclarations,
compareDeclarationsSafe,
addSelectors
} from './utils.js';

Expand Down Expand Up @@ -76,7 +76,7 @@ function processRule(node, item, list) {
}

// try to join by properties
const diff = compareDeclarations(block.children, prevBlock.children);
const diff = compareDeclarationsSafe(block.children, prevBlock.children, allowMergeUp);

// console.log(diff.eq, diff.ne1, diff.ne2);

Expand Down
194 changes: 194 additions & 0 deletions lib/restructure/propertyFamily.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { property as resolveProperty } from 'css-tree';

// Shorthand → the longhands it sets (and therefore resets when omitted).
// A leading "-" is treated as a suffix of the shorthand name, matching the
// historical property-family map on the unfinished `property-family` branch.
// Two properties overlap when their expanded atomic longhands intersect.
const SHORTHANDS = {
// CSS 2.1
'list-style': ['-type', '-position', '-image'],
'margin': ['-top', '-right', '-bottom', '-left'],
'padding': ['-top', '-right', '-bottom', '-left'],
'outline': ['-width', '-style', '-color'],
'border': ['-width', '-style', '-color', '-image'],
'border-width': ['border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'],
'border-style': ['border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style'],
'border-color': ['border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color'],
'border-top': ['-width', '-style', '-color'],
'border-right': ['-width', '-style', '-color'],
'border-bottom': ['-width', '-style', '-color'],
'border-left': ['-width', '-style', '-color'],

// CSS Backgrounds and Borders
'background': ['-image', '-position', '-size', '-repeat', '-origin', '-clip', '-attachment', '-color'],
'background-position': ['-x', '-y'],
'border-radius': ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'],
'border-image': ['-source', '-slice', '-width', '-outset', '-repeat'],

// CSS Fonts
'font': ['-style', '-variant', '-weight', '-stretch', '-size', 'line-height', '-family'],
'font-variant': ['-ligatures', '-alternates', '-caps', '-numeric', '-east-asian'],

// CSS Masking
'mask': ['-image', '-mode', '-position', '-size', '-repeat', '-origin', '-clip', '-composite'],
'mask-border': ['-source', '-slice', '-width', '-outset', '-repeat', '-mode'],

// CSS Multi-column
'columns': ['column-width', 'column-count'],
'column-rule': ['-width', '-style', '-color'],

// CSS Text Decoration
'text-decoration': ['-line', '-style', '-color', '-thickness'],
'text-emphasis': ['-style', '-color'],

// CSS Animations / Transitions
'animation': ['-name', '-duration', '-timing-function', '-delay', '-iteration-count', '-direction', '-fill-mode', '-play-state'],
'transition': ['-property', '-duration', '-timing-function', '-delay'],

// CSS Flexible Box / Grid
'flex': ['-grow', '-shrink', '-basis'],
'flex-flow': ['-direction', '-wrap'],
'gap': ['row-gap', 'column-gap'],
'grid-gap': ['grid-row-gap', 'grid-column-gap'],
'place-content': ['align-content', 'justify-content'],
'place-items': ['align-items', 'justify-items'],
'place-self': ['align-self', 'justify-self'],
'grid': ['grid-template-rows', 'grid-template-columns', 'grid-template-areas', 'grid-auto-rows', 'grid-auto-columns', 'grid-auto-flow'],
'grid-template': ['-rows', '-columns', '-areas'],
'grid-area': ['grid-row-start', 'grid-column-start', 'grid-row-end', 'grid-column-end'],
'grid-row': ['-start', '-end'],
'grid-column': ['-start', '-end'],

// CSS Overflow / Position
'overflow': ['-x', '-y'],
'inset': ['top', 'right', 'bottom', 'left'],

// CSS Logical
'margin-block': ['-start', '-end'],
'margin-inline': ['-start', '-end'],
'padding-block': ['-start', '-end'],
'padding-inline': ['-start', '-end'],
'inset-block': ['-start', '-end'],
'inset-inline': ['-start', '-end'],
'border-block': ['-width', '-style', '-color'],
'border-inline': ['-width', '-style', '-color'],
'border-block-width': ['border-block-start-width', 'border-block-end-width'],
'border-block-style': ['border-block-start-style', 'border-block-end-style'],
'border-block-color': ['border-block-start-color', 'border-block-end-color'],
'border-inline-width': ['border-inline-start-width', 'border-inline-end-width'],
'border-inline-style': ['border-inline-start-style', 'border-inline-end-style'],
'border-inline-color': ['border-inline-start-color', 'border-inline-end-color'],
'border-block-start': ['-width', '-style', '-color'],
'border-block-end': ['-width', '-style', '-color'],
'border-inline-start': ['-width', '-style', '-color'],
'border-inline-end': ['-width', '-style', '-color']
};

// `all` resets every property except these (CSS Cascading).
const ALL_EXEMPT = new Set(['unicode-bidi', 'direction']);

const atomCache = new Map();

function resolveChild(parent, child) {
// "-size" → "background-size"; leave full names and custom props alone
if (child.charCodeAt(0) === 45 && child.charCodeAt(1) !== 45) {
return parent + child;
}

return child;
}

function computeAtoms(basename, seen) {
if (seen.has(basename)) {
return [];
}

seen.add(basename);

const children = SHORTHANDS[basename];

if (!children) {
return [basename];
}

const atoms = [];

for (const child of children) {
atoms.push(...computeAtoms(resolveChild(basename, child), seen));
}

return atoms;
}

function getAtoms(basename) {
let atoms = atomCache.get(basename);

if (!atoms) {
atoms = computeAtoms(basename, new Set());
atomCache.set(basename, atoms);
}

return atoms;
}

function atomsIntersect(a, b) {
if (a === b) {
return true;
}

const atomsA = getAtoms(a);
const atomsB = getAtoms(b);
const setA = atomsA.length >= atomsB.length ? new Set(atomsA) : new Set(atomsB);
const other = atomsA.length >= atomsB.length ? atomsB : atomsA;

return other.some((atom) => setA.has(atom));
}

export function propertiesOverlap(propertyA, propertyB) {
const a = resolveProperty(propertyA);
const b = resolveProperty(propertyB);

if (a.custom || b.custom) {
return a.custom && b.custom && a.name === b.name;
}

if (a.basename === 'all' || b.basename === 'all') {
const other = a.basename === 'all' ? b.basename : a.basename;

return other === 'all' || !ALL_EXEMPT.has(other);
}

return atomsIntersect(a.basename, b.basename);
}

// True when `shorthandDecl` sets every atom of `longhandDecl`, so placing
// the shorthand later would reset the longhand. Different !important flags
// make cascade order irrelevant, so those pairs are not treated as unsafe.
export function isResettingShorthand(shorthandDecl, longhandDecl) {
if (Boolean(shorthandDecl.important) !== Boolean(longhandDecl.important)) {
return false;
}

const shorthand = resolveProperty(shorthandDecl.property);
const longhand = resolveProperty(longhandDecl.property);

if (shorthand.custom || longhand.custom) {
return false;
}

if (shorthand.basename === 'all') {
return longhand.basename !== 'all' && !ALL_EXEMPT.has(longhand.basename);
}

if (shorthand.basename === longhand.basename) {
return false;
}

if (!atomsIntersect(shorthand.basename, longhand.basename)) {
return false;
}

const shorthandAtoms = new Set(getAtoms(shorthand.basename));

return getAtoms(longhand.basename).every((atom) => shorthandAtoms.has(atom));
}
Loading