From 09bf526b19e498aa2c4e8861affd59d2982e1559 Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Tue, 8 Sep 2026 16:36:56 +0800 Subject: [PATCH 1/2] fix: deduplicate mask image prefixes --- src/lib.rs | 196 ++++++++++++++++++++++++++++++++++++++ src/properties/masking.rs | 51 +++++++++- 2 files changed, 244 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6416b7bae..504f5b52b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28460,6 +28460,202 @@ mod tests { assert_eq!(property, Property::ColorInterpolation(svg::ColorInterpolation::SRGB)); } + #[test] + fn test_mask_image_prefix_duplicates() { + let targets = Browsers { + chrome: Some(90 << 16), + ..Browsers::default() + }; + + for (source, expected) in [ + ( + r#" + .foo { + -webkit-mask-image: var(--image); + mask-image: var(--image); + } + "#, + indoc! { r#" + .foo { + -webkit-mask-image: var(--image); + mask-image: var(--image); + } + "#}, + ), + ( + r#" + .foo { + mask-image: linear-gradient(red, blue); + mask-image: linear-gradient(var(--start, red), blue); + -webkit-mask-image: linear-gradient(red, blue); + -webkit-mask-image: linear-gradient(var(--start, red), blue); + } + "#, + indoc! { r#" + .foo { + mask-image: linear-gradient(red, #00f); + mask-image: linear-gradient(var(--start, red), blue); + -webkit-mask-image: linear-gradient(red, #00f); + -webkit-mask-image: linear-gradient(var(--start, red), blue); + } + "#}, + ), + ( + r#" + .foo { + mask-image: var(--first, none); + mask-image: var(--second, none); + -webkit-mask-image: var(--first, none); + -webkit-mask-image: var(--second, none); + } + "#, + indoc! { r#" + .foo { + mask-image: var(--first, none); + mask-image: var(--second, none); + -webkit-mask-image: var(--first, none); + -webkit-mask-image: var(--second, none); + } + "#}, + ), + ( + r#" + .foo { + mask-image: var(--first); + -webkit-mask-image: var(--second); + } + "#, + indoc! { r#" + .foo { + -webkit-mask-image: var(--first); + mask-image: var(--first); + -webkit-mask-image: var(--second); + } + "#}, + ), + ( + r#" + .foo { + -webkit-mask-image: var(--image); + mask-image: none; + -webkit-mask-image: var(--image); + } + "#, + indoc! { r#" + .foo { + -webkit-mask-image: var(--image); + mask-image: none; + -webkit-mask-image: var(--image); + } + "#}, + ), + ( + r#" + .foo { + mask-image: var(--image); + mask-position: center; + -webkit-mask-image: var(--image); + } + "#, + indoc! { r#" + .foo { + -webkit-mask-image: var(--image); + mask-image: var(--image); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-image: var(--image); + } + "#}, + ), + ( + r#" + .foo { + mask-image: var(--image); + mask: none; + -webkit-mask-image: var(--image); + } + "#, + indoc! { r#" + .foo { + -webkit-mask-image: var(--image); + mask-image: var(--image); + -webkit-mask: none; + mask: none; + -webkit-mask-image: var(--image); + } + "#}, + ), + ( + r#" + .foo { + mask-image: var(--image) !important; + -webkit-mask-image: var(--image); + -webkit-mask-image: var(--image) !important; + } + "#, + indoc! { r#" + .foo { + -webkit-mask-image: var(--image); + mask-image: var(--image) !important; + -webkit-mask-image: var(--image) !important; + } + "#}, + ), + ] { + prefix_test(source, expected, targets); + prefix_test(expected, expected, targets); + } + + prefix_test( + r#" + .foo { + mask-image: var(--image); + mask: var(--mask); + -webkit-mask-image: var(--image); + } + "#, + indoc! { r#" + .foo { + -webkit-mask-image: var(--image); + mask-image: var(--image); + -webkit-mask: var(--mask); + mask: var(--mask); + -webkit-mask-image: var(--image); + } + "#}, + targets, + ); + + let source = r#" + .foo { + -webkit-mask-image: var(--image); + mask-image: var(--image); + } + "#; + test( + source, + indoc! { r#" + .foo { + -webkit-mask-image: var(--image); + mask-image: var(--image); + } + "#}, + ); + prefix_test( + source, + indoc! { r#" + .foo { + -webkit-mask-image: var(--image); + mask-image: var(--image); + } + "#}, + Browsers { + chrome: Some(130 << 16), + ..Browsers::default() + }, + ); + } + #[test] fn test_filter() { minify_test( diff --git a/src/properties/masking.rs b/src/properties/masking.rs index ff05e1f63..0b2cda2a0 100644 --- a/src/properties/masking.rs +++ b/src/properties/masking.rs @@ -703,7 +703,11 @@ impl<'i> PropertyHandler<'i> for MaskHandler<'i> { self .flushed_properties .insert(MaskProperty::try_from(&val.property_id).unwrap()); - dest.push(Property::Unparsed(unparsed)); + if matches!(unparsed.property_id, PropertyId::MaskImage(_)) { + self.push_mask_image(Property::Unparsed(unparsed), dest); + } else { + dest.push(Property::Unparsed(unparsed)); + } } Property::MaskBorderSource(val) => property!(border_source, val, &VendorPrefix::None), Property::WebKitMaskBoxImageSource(val, _) => property!(border_source, val, &VendorPrefix::WebKit), @@ -903,7 +907,7 @@ impl<'i> MaskHandler<'i> { if p.is_empty() { p = prefix; } - dest.push(Property::MaskImage(fallback, p)) + self.push_mask_image(Property::MaskImage(fallback, p), dest) } let p = images @@ -916,7 +920,7 @@ impl<'i> MaskHandler<'i> { } } - dest.push(Property::MaskImage(images, prefix)); + self.push_mask_image(Property::MaskImage(images, prefix), dest); self.flushed_properties.insert(MaskProperty::MaskImage); } } @@ -953,6 +957,47 @@ impl<'i> MaskHandler<'i> { } } + fn push_mask_image(&self, property: Property<'i>, dest: &mut DeclarationList<'i>) { + let prefix = property.property_id().prefix(); + for i in (0..dest.len()).rev() { + let adjacent = i == dest.len() - 1; + let previous_prefix = match (&mut dest[i], &property) { + (Property::MaskImage(previous, previous_prefix), Property::MaskImage(value, _)) => { + if previous != value { + continue; + } + previous_prefix + } + (Property::Unparsed(previous), Property::Unparsed(value)) => { + let PropertyId::MaskImage(previous_prefix) = &mut previous.property_id else { + break; + }; + if previous.value != value.value { + continue; + } + previous_prefix + } + (Property::MaskImage(..), _) => continue, + (Property::Unparsed(previous), _) if matches!(previous.property_id, PropertyId::MaskImage(_)) => continue, + _ => break, + }; + + // Keep the last occurrence of each prefix in an image fallback chain. + // Moving a later declaration earlier could change the cascade in browsers + // that treat the prefixed and unprefixed properties as aliases. + // Preserve nonadjacent declarations if no prefixes would remain, so the + // first value in the fallback chain does not change on a second minify. + if !adjacent && (*previous_prefix - prefix).is_empty() { + continue; + } + previous_prefix.remove(prefix); + if previous_prefix.is_empty() { + dest.remove(i); + } + } + dest.push(property); + } + fn flush_mask_shorthand( &self, masks: SmallVec<[Mask<'i>; 1]>, From 0e6ec3ab091a5b72bade9729ba15498b7ddfe3c8 Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Thu, 8 Oct 2026 11:33:32 +0800 Subject: [PATCH 2/2] fix: simplify mask image prefix deduplication --- src/lib.rs | 138 ++------------------------------------ src/properties/masking.rs | 60 ++++------------- 2 files changed, 20 insertions(+), 178 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 504f5b52b..09c95e8d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28469,12 +28469,7 @@ mod tests { for (source, expected) in [ ( - r#" - .foo { - -webkit-mask-image: var(--image); - mask-image: var(--image); - } - "#, + ".foo { -webkit-mask-image: var(--image); mask-image: var(--image) }", indoc! { r#" .foo { -webkit-mask-image: var(--image); @@ -28483,80 +28478,18 @@ mod tests { "#}, ), ( - r#" - .foo { - mask-image: linear-gradient(red, blue); - mask-image: linear-gradient(var(--start, red), blue); - -webkit-mask-image: linear-gradient(red, blue); - -webkit-mask-image: linear-gradient(var(--start, red), blue); - } - "#, - indoc! { r#" - .foo { - mask-image: linear-gradient(red, #00f); - mask-image: linear-gradient(var(--start, red), blue); - -webkit-mask-image: linear-gradient(red, #00f); - -webkit-mask-image: linear-gradient(var(--start, red), blue); - } - "#}, - ), - ( - r#" - .foo { - mask-image: var(--first, none); - mask-image: var(--second, none); - -webkit-mask-image: var(--first, none); - -webkit-mask-image: var(--second, none); - } - "#, - indoc! { r#" - .foo { - mask-image: var(--first, none); - mask-image: var(--second, none); - -webkit-mask-image: var(--first, none); - -webkit-mask-image: var(--second, none); - } - "#}, - ), - ( - r#" - .foo { - mask-image: var(--first); - -webkit-mask-image: var(--second); - } - "#, + ".foo { mask-image: var(--first); mask-image: var(--second) }", indoc! { r#" .foo { -webkit-mask-image: var(--first); mask-image: var(--first); -webkit-mask-image: var(--second); + mask-image: var(--second); } "#}, ), ( - r#" - .foo { - -webkit-mask-image: var(--image); - mask-image: none; - -webkit-mask-image: var(--image); - } - "#, - indoc! { r#" - .foo { - -webkit-mask-image: var(--image); - mask-image: none; - -webkit-mask-image: var(--image); - } - "#}, - ), - ( - r#" - .foo { - mask-image: var(--image); - mask-position: center; - -webkit-mask-image: var(--image); - } - "#, + ".foo { mask-image: var(--image); mask-position: center; -webkit-mask-image: var(--image) }", indoc! { r#" .foo { -webkit-mask-image: var(--image); @@ -28568,36 +28501,12 @@ mod tests { "#}, ), ( - r#" - .foo { - mask-image: var(--image); - mask: none; - -webkit-mask-image: var(--image); - } - "#, + ".foo { mask-image: var(--image) !important; -webkit-mask-image: var(--image) }", indoc! { r#" .foo { - -webkit-mask-image: var(--image); - mask-image: var(--image); - -webkit-mask: none; - mask: none; - -webkit-mask-image: var(--image); - } - "#}, - ), - ( - r#" - .foo { - mask-image: var(--image) !important; -webkit-mask-image: var(--image); -webkit-mask-image: var(--image) !important; - } - "#, - indoc! { r#" - .foo { - -webkit-mask-image: var(--image); mask-image: var(--image) !important; - -webkit-mask-image: var(--image) !important; } "#}, ), @@ -28607,42 +28516,7 @@ mod tests { } prefix_test( - r#" - .foo { - mask-image: var(--image); - mask: var(--mask); - -webkit-mask-image: var(--image); - } - "#, - indoc! { r#" - .foo { - -webkit-mask-image: var(--image); - mask-image: var(--image); - -webkit-mask: var(--mask); - mask: var(--mask); - -webkit-mask-image: var(--image); - } - "#}, - targets, - ); - - let source = r#" - .foo { - -webkit-mask-image: var(--image); - mask-image: var(--image); - } - "#; - test( - source, - indoc! { r#" - .foo { - -webkit-mask-image: var(--image); - mask-image: var(--image); - } - "#}, - ); - prefix_test( - source, + ".foo { -webkit-mask-image: var(--image); mask-image: var(--image) }", indoc! { r#" .foo { -webkit-mask-image: var(--image); diff --git a/src/properties/masking.rs b/src/properties/masking.rs index 0b2cda2a0..050e0d1f7 100644 --- a/src/properties/masking.rs +++ b/src/properties/masking.rs @@ -703,9 +703,18 @@ impl<'i> PropertyHandler<'i> for MaskHandler<'i> { self .flushed_properties .insert(MaskProperty::try_from(&val.property_id).unwrap()); - if matches!(unparsed.property_id, PropertyId::MaskImage(_)) { - self.push_mask_image(Property::Unparsed(unparsed), dest); - } else { + let mut merged = false; + if let (PropertyId::MaskImage(prefix), Some(Property::Unparsed(previous))) = + (&unparsed.property_id, dest.last_mut()) + { + if let PropertyId::MaskImage(previous_prefix) = &mut previous.property_id { + if previous.value == unparsed.value { + *previous_prefix |= *prefix; + merged = true; + } + } + } + if !merged { dest.push(Property::Unparsed(unparsed)); } } @@ -907,7 +916,7 @@ impl<'i> MaskHandler<'i> { if p.is_empty() { p = prefix; } - self.push_mask_image(Property::MaskImage(fallback, p), dest) + dest.push(Property::MaskImage(fallback, p)) } let p = images @@ -920,7 +929,7 @@ impl<'i> MaskHandler<'i> { } } - self.push_mask_image(Property::MaskImage(images, prefix), dest); + dest.push(Property::MaskImage(images, prefix)); self.flushed_properties.insert(MaskProperty::MaskImage); } } @@ -957,47 +966,6 @@ impl<'i> MaskHandler<'i> { } } - fn push_mask_image(&self, property: Property<'i>, dest: &mut DeclarationList<'i>) { - let prefix = property.property_id().prefix(); - for i in (0..dest.len()).rev() { - let adjacent = i == dest.len() - 1; - let previous_prefix = match (&mut dest[i], &property) { - (Property::MaskImage(previous, previous_prefix), Property::MaskImage(value, _)) => { - if previous != value { - continue; - } - previous_prefix - } - (Property::Unparsed(previous), Property::Unparsed(value)) => { - let PropertyId::MaskImage(previous_prefix) = &mut previous.property_id else { - break; - }; - if previous.value != value.value { - continue; - } - previous_prefix - } - (Property::MaskImage(..), _) => continue, - (Property::Unparsed(previous), _) if matches!(previous.property_id, PropertyId::MaskImage(_)) => continue, - _ => break, - }; - - // Keep the last occurrence of each prefix in an image fallback chain. - // Moving a later declaration earlier could change the cascade in browsers - // that treat the prefixed and unprefixed properties as aliases. - // Preserve nonadjacent declarations if no prefixes would remain, so the - // first value in the fallback chain does not change on a second minify. - if !adjacent && (*previous_prefix - prefix).is_empty() { - continue; - } - previous_prefix.remove(prefix); - if previous_prefix.is_empty() { - dest.remove(i); - } - } - dest.push(property); - } - fn flush_mask_shorthand( &self, masks: SmallVec<[Mask<'i>; 1]>,