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
55 changes: 27 additions & 28 deletions src/css/properties/background.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<BackgroundClip, 1> = 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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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<Image, 1>,
images: SmallList<Image, 1>,
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));
Comment thread
robobun marked this conversation as resolved.
Resolved
} 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(..));
Comment thread
robobun marked this conversation as resolved.
Resolved

let mut maybe_color: Option<CssColor> = self.color.take();
let mut maybe_images: Option<SmallList<Image, 1>> = self.images.take();
let mut maybe_x_positions: Option<SmallList<HorizontalPosition, 1>> =
Expand Down Expand Up @@ -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();
}
Expand Down
85 changes: 85 additions & 0 deletions test/js/bun/css/css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3419,6 +3419,91 @@ 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(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) }",
".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", () => {
Expand Down
Loading