From 64ad2f244d4d2d46e91c1b554fb8982f2d11761e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:15:20 +0000 Subject: [PATCH 1/3] css: pair expanded transition entries with their own duration, delay and timing function `expand_properties` can turn one `transition-property` entry into several names. A logical shorthand becomes its physical longhands, and `mask-border` gets `-webkit-mask-box-image`. `get_transitions` matched the four lists by position, so every name after such an entry took the values of a different entry. `transition: inset 1s, opacity 2s` became `top 1s, bottom 2s, left 1s, right 2s, opacity 1s`. `expand_properties` now records, for each name, the index of the entry as written that the name came from. It starts the record when an entry first becomes several names. `get_transitions` reads the duration, delay and timing function at that index. The `transition-property` longhand output does not change. --- src/css/properties/transition.rs | 66 +++++--- test/bundler/css/transition-lowering.test.ts | 29 ++++ test/js/bun/css/css.test.ts | 152 +++++++++++++++++++ 3 files changed, 225 insertions(+), 22 deletions(-) create mode 100644 test/bundler/css/transition-lowering.test.ts diff --git a/src/css/properties/transition.rs b/src/css/properties/transition.rs index 48ff249cd9d7..d727f201854c 100644 --- a/src/css/properties/transition.rs +++ b/src/css/properties/transition.rs @@ -365,9 +365,10 @@ mod transition_handler_body { let mut _timing_functions: Option<(SmallList, VendorPrefix)> = self.timing_functions.take(); + let mut sources: Option> = None; let mut rtl_properties: Option> = if let Some(p) = &mut _properties { - expand_properties(&mut p.0, arena, context) + expand_properties(&mut p.0, &mut sources, arena, context) } else { None }; @@ -389,13 +390,20 @@ mod transition_handler_body { let intersection = *property_prefixes & *duration_prefixes & *delay_prefixes & *timing_prefixes; if !intersection.is_empty() { - let transitions = - get_transitions(arena, properties, durations, delays, timing_functions); + let transitions = get_transitions( + arena, + properties, + sources.as_deref(), + durations, + delays, + timing_functions, + ); if let Some(rtl_properties2) = &mut rtl_properties { let rtl_transitions = get_transitions( arena, rtl_properties2, + sources.as_deref(), durations, delays, timing_functions, @@ -468,40 +476,34 @@ mod transition_handler_body { fn get_transitions( arena: &bun_alloc::Arena, properties: &mut SmallList, + sources: Option<&[u32]>, durations: &mut SmallList, delays: &mut SmallList, timing_functions: &mut SmallList, ) -> SmallList { - #[inline] - fn cycle_bump(idx: &mut u32, len: u32) { - *idx = (*idx + 1) % len; - } - // transition-property determines the number of transitions. The values of other // properties are repeated to match this length. let mut transitions = SmallList::::init_capacity(1); - let mut durations_idx: u32 = 0; - let mut delays_idx: u32 = 0; - let mut timing_idx: u32 = 0; - for property_id in properties.slice() { - let duration = if durations.len() > durations_idx { - durations.at(durations_idx).deep_clone(arena) + for (index, property_id) in (0u32..).zip(properties.slice()) { + // Each name takes the values of its entry as written. lightningcss matches by position, which shifts them. + let source = sources.map_or(index, |sources| sources[index as usize]); + let duration = if !durations.is_empty() { + durations.at(source % durations.len()).deep_clone(arena) } else { Time::Seconds(0.0) }; - let delay = if delays.len() > delays_idx { - delays.at(delays_idx).deep_clone(arena) + let delay = if !delays.is_empty() { + delays.at(source % delays.len()).deep_clone(arena) } else { Time::Seconds(0.0) }; - let timing_function = if timing_functions.len() > timing_idx { - timing_functions.at(timing_idx).deep_clone(arena) + let timing_function = if !timing_functions.is_empty() { + timing_functions + .at(source % timing_functions.len()) + .deep_clone(arena) } else { EasingFunction::Ease }; - cycle_bump(&mut durations_idx, durations.len()); - cycle_bump(&mut delays_idx, delays.len()); - cycle_bump(&mut timing_idx, timing_functions.len()); let transition = Transition { property: property_id.deep_clone(arena), duration, @@ -525,6 +527,7 @@ mod transition_handler_body { fn expand_properties( properties: &mut SmallList, + sources: &mut Option>, arena: &bun_alloc::Arena, context: &mut PropertyHandlerContext, ) -> Option> { @@ -541,11 +544,24 @@ mod transition_handler_body { } } + // Once an entry becomes several names, each name keeps the index of its entry as written. + #[cold] + fn record_names(sources: &mut Option>, source: u32, names: u32, capacity: u32) { + let sources = sources.get_or_insert_with(|| { + let mut sources = Vec::with_capacity(capacity as usize); + sources.extend(0..source); + sources + }); + sources.resize(sources.len() + names as usize, source); + } + let mut rtl_properties: Option> = None; let mut i: u32 = 0; + let mut source: u32 = 0; // Expand logical properties in place. while i < properties.len() { + let start = i; let result = get_logical_properties(properties.at(i)); match result { LogicalPropertyId::Block(feature, block) @@ -555,7 +571,7 @@ mod transition_handler_body { if let Some(rtl) = &mut rtl_properties { replace(arena, rtl, block, i); } - i += 1; + i += u32::try_from(block.len()).expect("int cast"); } LogicalPropertyId::Inline(feature, ltr, rtl) if context.should_compile_logical(feature) => @@ -611,6 +627,12 @@ mod transition_handler_body { i += 1; } } + + let names = i - start; + if names > 1 || sources.is_some() { + record_names(sources, source, names, properties.len()); + } + source += 1; } rtl_properties diff --git a/test/bundler/css/transition-lowering.test.ts b/test/bundler/css/transition-lowering.test.ts new file mode 100644 index 000000000000..c214e1c1ce32 --- /dev/null +++ b/test/bundler/css/transition-lowering.test.ts @@ -0,0 +1,29 @@ +import { describe } from "bun:test"; +import { itBundled } from "../expectBundled"; + +describe("css", () => { + // The default browser targets add `-webkit-mask-box-image` in front of `mask-border`. Both keep the 2s. + for (const backend of ["cli", "api"] as const) { + itBundled(`css/transition-mask-border-keeps-its-duration-${backend}`, { + backend, + files: { + "index.css": /* css */ ` + .a { + transition: mask-border 2s, opacity 3s; + } + `, + }, + outdir: "/out", + entryPoints: ["/index.css"], + onAfterBundle(api) { + api.expectFile("/out/index.css").toMatchInlineSnapshot(` + "/* index.css */ + .a { + transition: -webkit-mask-box-image 2s, mask-border 2s, opacity 3s; + } + " + `); + }, + }); + } +}); diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 890ffa4dcd95..fa428e0af225 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -7159,6 +7159,158 @@ describe("css tests", () => { chrome: Some(95 << 16), }, ); + + // One entry of a `transition` can become several names: a logical shorthand becomes its physical longhands, + // and `mask-border` gets `-webkit-mask-box-image`. Each name keeps the values of the entry it came from. + describe("an entry that becomes several names keeps its duration, delay and timing function", () => { + const safari = (version: number) => ({ safari: Some(version << 16) }); + const rtlLangs = "ae ar arc bcc bqi ckb dv fa glk he ku mzn nqo pnb ps sd ug ur yi" + .split(" ") + .map(lang => `:lang(${lang})`) + .join(", "); + // The rules that one rule with an inline logical property becomes for Safari 8. + const directionRules = (ltr: string, rtl: string) => ` + .foo:not(:-webkit-any(${rtlLangs})) { ${ltr} } + .foo:not(:is(${rtlLangs})) { ${ltr} } + .foo:-webkit-any(${rtlLangs}) { ${rtl} } + .foo:is(${rtlLangs}) { ${rtl} } + `; + + prefix_test( + `.foo { transition: mask-border 1s, opacity 2s }`, + `.foo { transition: -webkit-mask-box-image 1s, mask-border 1s, opacity 2s; }`, + safari(15), + ); + prefix_test( + `.foo { transition: inset 1s, opacity 2s }`, + `.foo { transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s; }`, + safari(8), + ); + prefix_test( + `.foo { transition: margin-block 1s, opacity 2s, color 3s }`, + `.foo { transition: margin-top 1s, margin-bottom 1s, opacity 2s, color 3s; }`, + safari(8), + ); + prefix_test( + `.foo { transition: border-block-color 1s, border-inline-width 2s, opacity 3s }`, + `.foo { transition: border-top-color 1s, border-bottom-color 1s, border-left-width 2s, border-right-width 2s, opacity 3s; }`, + safari(8), + ); + prefix_test( + `.foo { transition: mask-composite 1s, mask-mode 2s, opacity 3s }`, + `.foo { transition: -webkit-mask-composite 1s, mask-composite 1s, -webkit-mask-source-type 2s, mask-mode 2s, opacity 3s; }`, + safari(15), + ); + prefix_test( + `.foo { transition: inset 1s linear 3s, opacity 2s ease-in 4s }`, + `.foo { transition: top 1s linear 3s, bottom 1s linear 3s, left 1s linear 3s, right 1s linear 3s, opacity 2s ease-in 4s; }`, + safari(8), + ); + prefix_test( + `.foo { + transition-property: inset, opacity; + transition-duration: 1s, 2s; + transition-delay: 3s, 4s; + transition-timing-function: linear, ease-in; + }`, + `.foo { transition: top 1s linear 3s, bottom 1s linear 3s, left 1s linear 3s, right 1s linear 3s, opacity 2s ease-in 4s; }`, + safari(8), + ); + // A shorter list repeats over the entries as written, and a value past the last entry is not used. + prefix_test( + `.foo { + transition-property: inset, opacity, color; + transition-duration: 1s, 2s; + transition-delay: 0s; + transition-timing-function: ease; + }`, + `.foo { transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s, color 1s; }`, + safari(8), + ); + prefix_test( + `.foo { + transition-property: inset; + transition-duration: 1s, 2s; + transition-delay: 0s; + transition-timing-function: ease; + }`, + `.foo { transition: top 1s, bottom 1s, left 1s, right 1s; }`, + safari(8), + ); + // The order as written stays, so the last entry for a property still decides. + prefix_test( + `.foo { transition: top 5s, inset 1s, left 7s }`, + `.foo { transition: top 5s, top 1s, bottom 1s, left 1s, right 1s, left 7s; }`, + safari(8), + ); + prefix_test( + `.foo { transition: all 1s, inset 2s }`, + `.foo { transition: all 1s, top 2s, bottom 2s, left 2s, right 2s; }`, + safari(8), + ); + prefix_test( + `.foo { transition: inset 1s, opacity 2s; transition-duration: 3s, 4s }`, + `.foo { transition: top 3s, bottom 3s, left 3s, right 3s, opacity 4s; }`, + safari(8), + ); + prefix_test( + `.foo { transition-duration: 3s, 4s; transition: inset 1s, opacity 2s }`, + `.foo { transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s; }`, + safari(8), + ); + // The handler writes what it holds when a prefixed value differs or a longhand is not parsed. + prefix_test( + `.foo { -webkit-transition: inset 1s, opacity 2s; transition: inset 3s, opacity 4s }`, + `.foo { + -webkit-transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s; + transition: top 3s, bottom 3s, left 3s, right 3s, opacity 4s; + }`, + safari(8), + ); + prefix_test( + `.foo { transition: inset 1s, opacity 2s; transition-timing-function: var(--ease) }`, + `.foo { + transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s; + transition-timing-function: var(--ease); + }`, + safari(8), + ); + prefix_test( + `.foo { transition: inset 1s, opacity 2s, transform 3s }`, + `.foo { + -webkit-transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s, -webkit-transform 3s, transform 3s; + transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s, -webkit-transform 3s, transform 3s; + }`, + safari(6), + ); + // Both direction rules, with the inline logical property in front of the entry and behind it. + prefix_test( + `.foo { transition: margin-inline-start 1s, inset 2s, opacity 3s }`, + directionRules( + `transition: margin-left 1s, top 2s, bottom 2s, left 2s, right 2s, opacity 3s;`, + `transition: margin-right 1s, top 2s, bottom 2s, left 2s, right 2s, opacity 3s;`, + ), + safari(8), + ); + prefix_test( + `.foo { transition: inset 1s, margin-inline-start 2s, opacity 3s }`, + directionRules( + `transition: top 1s, bottom 1s, left 1s, right 1s, margin-left 2s, opacity 3s;`, + `transition: top 1s, bottom 1s, left 1s, right 1s, margin-right 2s, opacity 3s;`, + ), + safari(8), + ); + // Nothing becomes several names without targets, or for targets that need nothing. + minify_test( + ".foo { transition: inset 1s, mask-border 2s, transform 3s }", + ".foo{transition:inset 1s,mask-border 2s,transform 3s}", + ); + prefix_test( + `.foo { transition: inset 1s, mask-border 2s, transform 3s, opacity 4s }`, + `.foo { transition: inset 1s, mask-border 2s, transform 3s, opacity 4s; }`, + { chrome: Some(130 << 16) }, + ); + }); }); describe("animation", () => { From 92d634a9bcf98e5a2586bbaf4fd0257739999d5d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:29:34 +0000 Subject: [PATCH 2/3] css: repeat the longhand transition values for the names of an expanded entry A `transition-property` list that is written as a longhand had the same fault as the shorthand. The expanded names were written beside the value lists as the author wrote them, so `transition-property: inset, opacity` with `transition-duration: 1s, 2s` gave five names for two durations. `flush` now rewrites each value list that it writes as a longhand, with the record that `expand_properties` fills: every name gets the value of its entry. A list that already does that stays as written. A property that is printed once per vendor prefix is still one entry of the list, so its value is not repeated. The bundler case moves into the existing mask test file. --- src/css/properties/transition.rs | 32 +++++++ test/bundler/css/mask-geometry-box.test.ts | 25 ++++++ test/bundler/css/transition-lowering.test.ts | 29 ------- test/js/bun/css/css.test.ts | 90 ++++++++++++++++++++ 4 files changed, 147 insertions(+), 29 deletions(-) delete mode 100644 test/bundler/css/transition-lowering.test.ts diff --git a/src/css/properties/transition.rs b/src/css/properties/transition.rs index d727f201854c..2e710b13583a 100644 --- a/src/css/properties/transition.rs +++ b/src/css/properties/transition.rs @@ -439,6 +439,13 @@ mod transition_handler_body { } } + // A value list that is written beside expanded names needs one value per name. + if let Some(sources) = &sources { + repeat_for_names(&mut _durations, sources); + repeat_for_names(&mut _delays, sources); + repeat_for_names(&mut _timing_functions, sources); + } + if let Some((durations, prefix)) = _durations.take() { if !prefix.is_empty() { dest.push(Property::TransitionDuration((durations, prefix))); @@ -525,6 +532,31 @@ mod transition_handler_body { transitions } + // Gives each name the value of its entry, unless the list as written already does. + #[cold] + fn repeat_for_names( + list: &mut Option<(SmallList, VendorPrefix)>, + sources: &[u32], + ) { + let Some((values, prefix)) = list else { return }; + let len = values.len(); + if prefix.is_empty() || len < 2 { + return; + } + let written = |index: u32| values.at(index % len); + if (0u32..) + .zip(sources) + .all(|(index, &source)| written(index) == written(source)) + { + return; + } + let repeated = sources + .iter() + .map(|&source| written(source).clone()) + .collect(); + *values = repeated; + } + fn expand_properties( properties: &mut SmallList, sources: &mut Option>, diff --git a/test/bundler/css/mask-geometry-box.test.ts b/test/bundler/css/mask-geometry-box.test.ts index ea5d70a8fca7..14ae96d28bda 100644 --- a/test/bundler/css/mask-geometry-box.test.ts +++ b/test/bundler/css/mask-geometry-box.test.ts @@ -41,4 +41,29 @@ describe("css", () => { expect(output).toContain("padding-box"); }, }); + + // The default browser targets add `-webkit-mask-box-image` in front of `mask-border`. Both keep the 2s. + for (const backend of ["cli", "api"] as const) { + itBundled(`css/transition-mask-border-keeps-its-duration-${backend}`, { + backend, + files: { + "index.css": /* css */ ` + .a { + transition: mask-border 2s, opacity 3s; + } + `, + }, + outdir: "/out", + entryPoints: ["/index.css"], + onAfterBundle(api) { + api.expectFile("/out/index.css").toMatchInlineSnapshot(` + "/* index.css */ + .a { + transition: -webkit-mask-box-image 2s, mask-border 2s, opacity 3s; + } + " + `); + }, + }); + } }); diff --git a/test/bundler/css/transition-lowering.test.ts b/test/bundler/css/transition-lowering.test.ts deleted file mode 100644 index c214e1c1ce32..000000000000 --- a/test/bundler/css/transition-lowering.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe } from "bun:test"; -import { itBundled } from "../expectBundled"; - -describe("css", () => { - // The default browser targets add `-webkit-mask-box-image` in front of `mask-border`. Both keep the 2s. - for (const backend of ["cli", "api"] as const) { - itBundled(`css/transition-mask-border-keeps-its-duration-${backend}`, { - backend, - files: { - "index.css": /* css */ ` - .a { - transition: mask-border 2s, opacity 3s; - } - `, - }, - outdir: "/out", - entryPoints: ["/index.css"], - onAfterBundle(api) { - api.expectFile("/out/index.css").toMatchInlineSnapshot(` - "/* index.css */ - .a { - transition: -webkit-mask-box-image 2s, mask-border 2s, opacity 3s; - } - " - `); - }, - }); - } -}); diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index fa428e0af225..2131dc180037 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { cssInternals } from "bun:internal-for-testing"; import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe } from "harness"; import { join } from "path"; @@ -7300,6 +7301,95 @@ describe("css tests", () => { ), safari(8), ); + // A value list that is written beside the names gets one value per name. + prefix_test( + `.foo { transition-property: inset, opacity; transition-duration: 1s, 2s }`, + `.foo { transition-property: top, bottom, left, right, opacity; transition-duration: 1s, 1s, 1s, 1s, 2s; }`, + safari(8), + ); + prefix_test( + `.foo { transition-property: mask-border, opacity; transition-duration: 1s, 2s }`, + `.foo { transition-property: -webkit-mask-box-image, mask-border, opacity; transition-duration: 1s, 1s, 2s; }`, + safari(15), + ); + prefix_test( + `.foo { + transition-property: inset, opacity; + transition-delay: 1s, 2s; + transition-timing-function: linear, ease-in; + }`, + `.foo { + transition-property: top, bottom, left, right, opacity; + transition-delay: 1s, 1s, 1s, 1s, 2s; + transition-timing-function: linear, linear, linear, linear, ease-in; + }`, + safari(8), + ); + prefix_test( + `.foo { transition-property: inset, opacity, color; transition-duration: 1s, 2s }`, + `.foo { transition-property: top, bottom, left, right, opacity, color; transition-duration: 1s, 1s, 1s, 1s, 2s, 1s; }`, + safari(8), + ); + prefix_test( + `.foo { transition-property: inset, opacity; transition-duration: 1s, 2s, 3s }`, + `.foo { transition-property: top, bottom, left, right, opacity; transition-duration: 1s, 1s, 1s, 1s, 2s; }`, + safari(8), + ); + // A list that already gives every name the value of its entry stays as written. + prefix_test( + `.foo { transition-property: inset, opacity; transition-duration: 1s, 1s; transition-delay: 3s, 4s }`, + `.foo { + transition-property: top, bottom, left, right, opacity; + transition-duration: 1s, 1s; + transition-delay: 3s, 3s, 3s, 3s, 4s; + }`, + safari(8), + ); + prefix_test( + `.foo { transition-property: margin-inline-start, inset, opacity; transition-duration: 1s, 2s, 3s }`, + `.foo { transition-duration: 1s, 2s, 2s, 2s, 2s, 3s; } + ${directionRules( + `transition-property: margin-left, top, bottom, left, right, opacity;`, + `transition-property: margin-right, top, bottom, left, right, opacity;`, + )}`, + safari(8), + ); + // A rule with the same selector brings the other two lists, and the merged block is minified again. + prefix_test( + `.foo { transition-property: inset, opacity; transition-duration: 1s, 2s } + .foo { transition-delay: 0s; transition-timing-function: ease }`, + `.foo { transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s; }`, + safari(8), + ); + test("a merged block keeps the pairs when the second pass adds `-webkit-mask-box-image` again", () => { + const output = cssInternals.minifyTest( + `.foo{transition-property:mask-border,opacity;transition-duration:1s,2s} + .foo{transition-delay:0s;transition-timing-function:ease}`, + "", + safari(15), + ); + const transitions = output.slice(output.indexOf(":") + 1, output.indexOf("}")).split(","); + expect(new Set(transitions)).toEqual(new Set(["-webkit-mask-box-image 1s", "mask-border 1s", "opacity 2s"])); + }); + + // Prints `color 1s, opacity 1s`: the later property list meets one duration per name of the first list. + cssTestTodo( + `.foo { transition: inset 1s, opacity 2s } .foo { transition-property: color, opacity }`, + `.foo { transition: color 1s, opacity 2s; }`, + safari(8), + ); + // Prints three names beside `1s, 2s`: a property with several vendor prefixes is one entry of the list. + cssTestTodo( + `.foo { transition-property: transform, opacity; transition-duration: 1s, 2s }`, + `.foo { + -webkit-transition-property: -webkit-transform, transform, opacity; + transition-property: -webkit-transform, transform, opacity; + -webkit-transition-duration: 1s, 1s, 2s; + transition-duration: 1s, 1s, 2s; + }`, + safari(6), + ); + // Nothing becomes several names without targets, or for targets that need nothing. minify_test( ".foo { transition: inset 1s, mask-border 2s, transform 3s }", From 7e116a00cfe4a721ef469b363bd9df828c4b7f79 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:08:03 +0000 Subject: [PATCH 3/3] css: leave the longhand transition value lists as written `flush` writes the `transition-duration`, `transition-delay` and `transition-timing-function` longhands as the rule wrote them again, as on main. Only the `transition` shorthand gives each expanded name the values of its entry. `get_transitions` asserts that the record has one index per property. --- src/css/properties/transition.rs | 33 +-------- test/js/bun/css/css.test.ts | 113 +++---------------------------- 2 files changed, 10 insertions(+), 136 deletions(-) diff --git a/src/css/properties/transition.rs b/src/css/properties/transition.rs index 2e710b13583a..dfbf06f6c5c7 100644 --- a/src/css/properties/transition.rs +++ b/src/css/properties/transition.rs @@ -439,13 +439,6 @@ mod transition_handler_body { } } - // A value list that is written beside expanded names needs one value per name. - if let Some(sources) = &sources { - repeat_for_names(&mut _durations, sources); - repeat_for_names(&mut _delays, sources); - repeat_for_names(&mut _timing_functions, sources); - } - if let Some((durations, prefix)) = _durations.take() { if !prefix.is_empty() { dest.push(Property::TransitionDuration((durations, prefix))); @@ -491,6 +484,7 @@ mod transition_handler_body { // transition-property determines the number of transitions. The values of other // properties are repeated to match this length. let mut transitions = SmallList::::init_capacity(1); + debug_assert!(sources.is_none_or(|sources| sources.len() == properties.slice().len())); for (index, property_id) in (0u32..).zip(properties.slice()) { // Each name takes the values of its entry as written. lightningcss matches by position, which shifts them. let source = sources.map_or(index, |sources| sources[index as usize]); @@ -532,31 +526,6 @@ mod transition_handler_body { transitions } - // Gives each name the value of its entry, unless the list as written already does. - #[cold] - fn repeat_for_names( - list: &mut Option<(SmallList, VendorPrefix)>, - sources: &[u32], - ) { - let Some((values, prefix)) = list else { return }; - let len = values.len(); - if prefix.is_empty() || len < 2 { - return; - } - let written = |index: u32| values.at(index % len); - if (0u32..) - .zip(sources) - .all(|(index, &source)| written(index) == written(source)) - { - return; - } - let repeated = sources - .iter() - .map(|&source| written(source).clone()) - .collect(); - *values = repeated; - } - fn expand_properties( properties: &mut SmallList, sources: &mut Option>, diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 2131dc180037..b2c18ed62520 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -2,7 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { cssInternals } from "bun:internal-for-testing"; import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe } from "harness"; import { join } from "path"; @@ -7207,36 +7206,24 @@ describe("css tests", () => { `.foo { transition: top 1s linear 3s, bottom 1s linear 3s, left 1s linear 3s, right 1s linear 3s, opacity 2s ease-in 4s; }`, safari(8), ); + // A shorter list repeats over the entries as written, and a value past the last entry is not used. prefix_test( - `.foo { - transition-property: inset, opacity; - transition-duration: 1s, 2s; - transition-delay: 3s, 4s; - transition-timing-function: linear, ease-in; - }`, - `.foo { transition: top 1s linear 3s, bottom 1s linear 3s, left 1s linear 3s, right 1s linear 3s, opacity 2s ease-in 4s; }`, + `.foo { transition: inset 1s, opacity 2s, color 3s; transition-duration: 4s, 5s }`, + `.foo { transition: top 4s, bottom 4s, left 4s, right 4s, opacity 5s, color 4s; }`, safari(8), ); - // A shorter list repeats over the entries as written, and a value past the last entry is not used. prefix_test( - `.foo { - transition-property: inset, opacity, color; - transition-duration: 1s, 2s; - transition-delay: 0s; - transition-timing-function: ease; - }`, - `.foo { transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s, color 1s; }`, + `.foo { transition: inset 1s; transition-duration: 4s, 5s }`, + `.foo { transition: top 4s, bottom 4s, left 4s, right 4s; }`, safari(8), ); + // daisyUI ships this shape in its megamenu, with `linear()` as the first easing. prefix_test( + `.foo { transition: inset .3s cubic-bezier(.2, .9, .3, 1.1), background-color .2s ease-out }`, `.foo { - transition-property: inset; - transition-duration: 1s, 2s; - transition-delay: 0s; - transition-timing-function: ease; + transition: top .3s cubic-bezier(.2, .9, .3, 1.1), bottom .3s cubic-bezier(.2, .9, .3, 1.1), left .3s cubic-bezier(.2, .9, .3, 1.1), right .3s cubic-bezier(.2, .9, .3, 1.1), background-color .2s ease-out; }`, - `.foo { transition: top 1s, bottom 1s, left 1s, right 1s; }`, - safari(8), + safari(14), ); // The order as written stays, so the last entry for a property still decides. prefix_test( @@ -7301,94 +7288,12 @@ describe("css tests", () => { ), safari(8), ); - // A value list that is written beside the names gets one value per name. - prefix_test( - `.foo { transition-property: inset, opacity; transition-duration: 1s, 2s }`, - `.foo { transition-property: top, bottom, left, right, opacity; transition-duration: 1s, 1s, 1s, 1s, 2s; }`, - safari(8), - ); - prefix_test( - `.foo { transition-property: mask-border, opacity; transition-duration: 1s, 2s }`, - `.foo { transition-property: -webkit-mask-box-image, mask-border, opacity; transition-duration: 1s, 1s, 2s; }`, - safari(15), - ); - prefix_test( - `.foo { - transition-property: inset, opacity; - transition-delay: 1s, 2s; - transition-timing-function: linear, ease-in; - }`, - `.foo { - transition-property: top, bottom, left, right, opacity; - transition-delay: 1s, 1s, 1s, 1s, 2s; - transition-timing-function: linear, linear, linear, linear, ease-in; - }`, - safari(8), - ); - prefix_test( - `.foo { transition-property: inset, opacity, color; transition-duration: 1s, 2s }`, - `.foo { transition-property: top, bottom, left, right, opacity, color; transition-duration: 1s, 1s, 1s, 1s, 2s, 1s; }`, - safari(8), - ); - prefix_test( - `.foo { transition-property: inset, opacity; transition-duration: 1s, 2s, 3s }`, - `.foo { transition-property: top, bottom, left, right, opacity; transition-duration: 1s, 1s, 1s, 1s, 2s; }`, - safari(8), - ); - // A list that already gives every name the value of its entry stays as written. - prefix_test( - `.foo { transition-property: inset, opacity; transition-duration: 1s, 1s; transition-delay: 3s, 4s }`, - `.foo { - transition-property: top, bottom, left, right, opacity; - transition-duration: 1s, 1s; - transition-delay: 3s, 3s, 3s, 3s, 4s; - }`, - safari(8), - ); - prefix_test( - `.foo { transition-property: margin-inline-start, inset, opacity; transition-duration: 1s, 2s, 3s }`, - `.foo { transition-duration: 1s, 2s, 2s, 2s, 2s, 3s; } - ${directionRules( - `transition-property: margin-left, top, bottom, left, right, opacity;`, - `transition-property: margin-right, top, bottom, left, right, opacity;`, - )}`, - safari(8), - ); - // A rule with the same selector brings the other two lists, and the merged block is minified again. - prefix_test( - `.foo { transition-property: inset, opacity; transition-duration: 1s, 2s } - .foo { transition-delay: 0s; transition-timing-function: ease }`, - `.foo { transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s; }`, - safari(8), - ); - test("a merged block keeps the pairs when the second pass adds `-webkit-mask-box-image` again", () => { - const output = cssInternals.minifyTest( - `.foo{transition-property:mask-border,opacity;transition-duration:1s,2s} - .foo{transition-delay:0s;transition-timing-function:ease}`, - "", - safari(15), - ); - const transitions = output.slice(output.indexOf(":") + 1, output.indexOf("}")).split(","); - expect(new Set(transitions)).toEqual(new Set(["-webkit-mask-box-image 1s", "mask-border 1s", "opacity 2s"])); - }); - // Prints `color 1s, opacity 1s`: the later property list meets one duration per name of the first list. cssTestTodo( `.foo { transition: inset 1s, opacity 2s } .foo { transition-property: color, opacity }`, `.foo { transition: color 1s, opacity 2s; }`, safari(8), ); - // Prints three names beside `1s, 2s`: a property with several vendor prefixes is one entry of the list. - cssTestTodo( - `.foo { transition-property: transform, opacity; transition-duration: 1s, 2s }`, - `.foo { - -webkit-transition-property: -webkit-transform, transform, opacity; - transition-property: -webkit-transform, transform, opacity; - -webkit-transition-duration: 1s, 1s, 2s; - transition-duration: 1s, 1s, 2s; - }`, - safari(6), - ); // Nothing becomes several names without targets, or for targets that need nothing. minify_test(