From 355dc8b1245584615c1e71a538db9091a5a0f7ec Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:53:15 +0000 Subject: [PATCH 1/2] css: write held prefixed background declarations at every flush BackgroundHandler holds a prefixed background value in a side list so that an unprefixed value with targets can drop it. Only finalize wrote that list, so a flush inside the rule (an unparsed value, a color or clip the targets reject) put the held values after the current one, and sometimes wrote one twice. flush now drains the list before it writes the typed value. The background shorthand arm records the new value after every flush decision, so the list and the typed value always describe the same declaration. --- src/css/properties/background.rs | 55 +++++++++++++------------- test/js/bun/css/css.test.ts | 68 ++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 28 deletions(-) diff --git a/src/css/properties/background.rs b/src/css/properties/background.rs index 6f8fef1774d6..e36808713b15 100644 --- a/src/css/properties/background.rs +++ b/src/css/properties/background.rs @@ -650,8 +650,8 @@ impl BackgroundHandler { self.color = Some(val.deep_clone(arena)); } Property::BackgroundImage(val) => { - self.background_helper(val, property, dest, context); - self.images = Some(val.deep_clone(arena)); + flush_helper!(self, images, val, dest, context); + self.set_images(val.deep_clone(arena), property, dest, context); } Property::BackgroundPosition(val) => { let len = val.len(); @@ -718,15 +718,15 @@ impl BackgroundHandler { for b in val.slice() { images.append_assume_capacity(b.image.deep_clone(arena)); } - self.background_helper(&images, property, dest, context); - let arena = dest.bump(); let color = val.last().unwrap().color.deep_clone(arena); - flush_helper!(self, color, &color, dest, context); - let arena = dest.bump(); let mut clips: SmallList = SmallList::init_capacity(val.len()); for b in val.slice() { clips.append_assume_capacity(b.clip); } + + // Every flush decision comes before the new value is recorded. + flush_helper!(self, images, &images, dest, context); + flush_helper!(self, color, &color, dest, context); let mut clips_vp = VendorPrefix::NONE; // Drop the `self.clips` borrow before calling flush(). let needs_flush = if let Some((existing_clips, existing_vp)) = &self.clips { @@ -741,7 +741,7 @@ impl BackgroundHandler { } self.color = Some(color); - self.images = Some(images); + self.set_images(images, property, dest, context); let len = val.len(); { let x_positions = init_small_list_helper!(self, x_positions, len); @@ -804,33 +804,46 @@ impl BackgroundHandler { true } - fn background_helper( + /// Records `images` as the current value. Call it after every flush + /// decision of the arm, so that `self.decls.last()` and `self.images` + /// always describe the same declaration. + /// + /// A prefixed declaration is held in `self.decls` as written. `flush` + /// writes the held ones ahead of the current value. An unprefixed value + /// with targets drops the held ones: the needed prefixes are generated. + fn set_images( &mut self, - val: &SmallList, + images: SmallList, property: &Property, dest: &mut DeclarationList, context: &mut PropertyHandlerContext, ) { - flush_helper!(self, images, val, dest, context); - - // Store prefixed properties. Clear if we hit an unprefixed property and we have - // targets. In this case, the necessary prefixes will be generated. - self.has_prefix = val.any(|item: &Image| item.has_vendor_prefix()); + self.has_prefix = images.any(|item: &Image| item.has_vendor_prefix()); if self.has_prefix { let arena = dest.bump(); self.decls.push(property.deep_clone(arena)); } else if context.targets.browsers.is_some() { self.decls.clear(); } + self.images = Some(images); } fn flush(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext) { if !self.has_any { + debug_assert!(self.decls.is_empty() && !self.has_prefix); return; } self.has_any = false; let arena = dest.bump(); + // Held prefixed declarations go out first. The last one mirrors + // `self.images`, which is written below with its fallbacks. + if self.has_prefix { + let _ = self.decls.pop(); + self.has_prefix = false; + } + dest.extend(self.decls.drain(..)); + let mut maybe_color: Option = self.color.take(); let mut maybe_images: Option> = self.images.take(); let mut maybe_x_positions: Option> = @@ -1136,20 +1149,6 @@ impl BackgroundHandler { dest: &mut DeclarationList, context: &mut PropertyHandlerContext, ) { - // If the last declaration is prefixed, pop the last value - // so it isn't duplicated when we flush. - if self.has_prefix { - let _ = self.decls.pop(); - // Drop handles deinit. - } - - let arena = dest.bump(); - for decl in self.decls.drain(..) { - // `Property` is not `Clone` here, so move out via drain. - let _ = arena; - dest.push(decl); - } - self.flush(dest, context); self.flushed_properties = BackgroundProperty::empty(); } diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 890ffa4dcd95..5cca434849a4 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -3419,6 +3419,74 @@ describe("css tests", () => { chrome: 99 << 16, }, ); + + // #46872: a prefixed `background` value is held back so that an + // unprefixed value with targets can drop it. Every flush inside the rule + // writes the held values first, so the cascade keeps the source order. + describe("prefixed values keep source order", () => { + const chrome = { chrome: 95 << 16 }; + + // A value that is kept as tokens flushes the typed values before it. + for (const targets of [undefined, chrome]) { + cssTest( + ".r { background: -webkit-linear-gradient(red, blue); background: -moz-linear-gradient(red, blue); background: -webkit-radial-gradient(center, circle cover, yellow, green) }", + ".r { background: -webkit-linear-gradient(red, #00f); background: -moz-linear-gradient(red, #00f); background: -webkit-radial-gradient(center, circle cover, yellow, green); }", + targets, + ); + cssTest( + ".r { background-image: -webkit-linear-gradient(red, blue); background-image: -moz-linear-gradient(red, blue); background-image: -webkit-radial-gradient(center, circle cover, yellow, green) }", + ".r { background-image: -webkit-linear-gradient(red, #00f); background-image: -moz-linear-gradient(red, #00f); background-image: -webkit-radial-gradient(center, circle cover, yellow, green); }", + targets, + ); + cssTest( + ".r { background: -webkit-linear-gradient(red, blue); background: -moz-linear-gradient(red, blue); background: -webkit-radial-gradient(20px 30px, yellow, green) }", + ".r { background: -webkit-linear-gradient(red, #00f); background: -moz-linear-gradient(red, #00f); background: -webkit-radial-gradient(20px 30px, #ff0, green); }", + targets, + ); + } + + // Without targets nothing is dropped, and nothing is written twice. + cssTest( + ".r { background: -webkit-linear-gradient(red, blue); background: -moz-linear-gradient(red, blue); background: -ms-linear-gradient(red, blue); background: linear-gradient(red, blue) }", + ".r { background: -webkit-linear-gradient(red, #00f); background: -moz-linear-gradient(red, #00f); background: -ms-linear-gradient(red, blue); background: linear-gradient(red, #00f); }", + ); + cssTest( + ".r { background: -webkit-linear-gradient(red, blue); background: -moz-linear-gradient(red, blue); background-image: url(a.png); background-position: var(--p) }", + '.r { background: -webkit-linear-gradient(red, #00f); background: -moz-linear-gradient(red, #00f); background: url("a.png"); background-position: var(--p); }', + ); + + // A color the targets do not support flushes the earlier value once. + prefix_test( + ".r { background: -webkit-linear-gradient(red, blue); background: -moz-linear-gradient(red, blue) lab(50% 0 0) }", + ".r { background: -webkit-linear-gradient(red, #00f); background: lab(50% 0 0) -moz-linear-gradient(red, #00f); }", + chrome, + ); + + // A background-clip with another prefix flushes the earlier values first. + prefix_test( + ".r { background: -webkit-linear-gradient(red, blue); background: -moz-linear-gradient(red, blue); background-clip: text; -webkit-background-clip: border-box }", + ".r { background: -webkit-linear-gradient(red, #00f); background: -moz-linear-gradient(red, #00f); -webkit-background-clip: text; background-clip: text; -webkit-background-clip: border-box; }", + chrome, + ); + + // A held value before a value kept as tokens is written, even when an + // unprefixed value follows. Only the values held at that point are dropped. + prefix_test( + ".r { background: -webkit-linear-gradient(red, blue); background: -moz-linear-gradient(red, blue); background: -ms-linear-gradient(red, blue); background: linear-gradient(red, blue) }", + ".r { background: -webkit-linear-gradient(red, #00f); background: -moz-linear-gradient(red, #00f); background: -ms-linear-gradient(red, blue); background: linear-gradient(red, #00f); }", + chrome, + ); + prefix_test( + ".r { background: -webkit-linear-gradient(red, blue); background: -moz-linear-gradient(red, blue); background: linear-gradient(red, blue) }", + ".r { background: linear-gradient(red, #00f); }", + chrome, + ); + prefix_test( + ".r { background: -webkit-linear-gradient(red, blue); background: -moz-linear-gradient(red, blue) }", + ".r { background: -webkit-linear-gradient(red, #00f); background: -moz-linear-gradient(red, #00f); }", + chrome, + ); + }); }); describe("flex", () => { From d1c3f07f2da89ca8a83c29ebf52963584ce0d995 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 25 Oct 2026 13:51:45 +0000 Subject: [PATCH 2/2] css: cover the image flush site in the background order tests --- test/js/bun/css/css.test.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 5cca434849a4..5e19615543e0 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -3439,12 +3439,29 @@ describe("css tests", () => { targets, ); cssTest( - ".r { background: -webkit-linear-gradient(red, blue); background: -moz-linear-gradient(red, blue); background: -webkit-radial-gradient(20px 30px, yellow, green) }", - ".r { background: -webkit-linear-gradient(red, #00f); background: -moz-linear-gradient(red, #00f); background: -webkit-radial-gradient(20px 30px, #ff0, green); }", + ".r { background: -webkit-linear-gradient(red, blue); background: -moz-linear-gradient(red, blue); background: -webkit-radial-gradient(circle, yellow, green) }", + ".r { background: -webkit-linear-gradient(red, #00f); background: -moz-linear-gradient(red, #00f); background: -webkit-radial-gradient(circle, #ff0, green); }", targets, ); } + // An image the targets do not support flushes the earlier values once. + prefix_test( + ".r { background: -webkit-linear-gradient(red, blue); background: -moz-linear-gradient(red, blue); background: -webkit-gradient(linear, left top, left bottom, from(red), to(blue)) }", + ".r { background: -webkit-linear-gradient(red, #00f); background: -moz-linear-gradient(red, #00f); background: -webkit-gradient(linear, left top, left bottom, from(red), to(#00f)); }", + chrome, + ); + prefix_test( + ".r { background-image: -webkit-linear-gradient(red, blue); background-image: -moz-linear-gradient(red, blue); background-image: -webkit-gradient(linear, left top, left bottom, from(red), to(blue)) }", + ".r { background-image: -webkit-linear-gradient(red, #00f); background-image: -moz-linear-gradient(red, #00f); background-image: -webkit-gradient(linear, left top, left bottom, from(red), to(#00f)); }", + chrome, + ); + prefix_test( + ".r { background-image: -webkit-linear-gradient(red, blue); background-image: -moz-linear-gradient(red, blue); background-image: -o-linear-gradient(lab(50% 0 0), blue); background-image: linear-gradient(red, blue) }", + ".r { background-image: -webkit-linear-gradient(red, #00f); background-image: -moz-linear-gradient(red, #00f); background-image: linear-gradient(red, #00f); }", + chrome, + ); + // Without targets nothing is dropped, and nothing is written twice. cssTest( ".r { background: -webkit-linear-gradient(red, blue); background: -moz-linear-gradient(red, blue); background: -ms-linear-gradient(red, blue); background: linear-gradient(red, blue) }",