Skip to main content

stylex_transform/shared/transformers/
stylex_keyframes.rs

1use std::rc::Rc;
2
3use indexmap::IndexMap;
4use stylex_macros::stylex_panic;
5use swc_core::ecma::ast::Expr;
6
7use crate::shared::enums::data_structures::flat_compiled_styles_value::FlatCompiledStylesValue;
8use crate::shared::enums::data_structures::obj_map_type::ObjMapType;
9use crate::shared::structures::functions::{FunctionConfig, FunctionMap, FunctionType};
10use crate::shared::structures::pre_rule::PreRuleValue;
11use crate::shared::structures::state_manager::StateManager;
12use crate::shared::structures::types::FlatCompiledStyles;
13use crate::shared::utils::ast::convertors::{
14  convert_expr_to_str, convert_key_value_to_str, create_string_expr,
15};
16use crate::shared::utils::common::{create_hash, dashify};
17use crate::shared::utils::core::flat_map_expanded_shorthands::flat_map_expanded_shorthands;
18use crate::shared::utils::css::common::transform_value_cached;
19use crate::shared::utils::object::{
20  Pipe, obj_entries, obj_from_entries, obj_map, obj_map_keys_string,
21};
22use crate::shared::{
23  enums::data_structures::evaluate_result_value::EvaluateResultValue,
24  utils::common::downcast_style_options_to_state_manager,
25};
26use stylex_constants::constants::messages::{
27  ENTRY_MUST_BE_TUPLE, VALUE_MUST_BE_STRING, VALUES_MUST_BE_OBJECT,
28};
29use stylex_css::css::generate_ltr::generate_ltr;
30use stylex_css::css::generate_rtl::generate_rtl;
31use stylex_structures::order_pair::OrderPair;
32use stylex_structures::pair::Pair;
33use stylex_types::enums::data_structures::injectable_style::InjectableStyleKind;
34use stylex_types::structures::injectable_style::InjectableStyle;
35
36pub(crate) fn stylex_keyframes(
37  frames: &EvaluateResultValue,
38  state: &mut StateManager,
39) -> (String, InjectableStyleKind) {
40  let mut class_name_prefix = state.options.class_name_prefix.clone();
41
42  if class_name_prefix.is_empty() {
43    class_name_prefix = "x".to_string();
44  }
45
46  let Some(frames) = frames.as_expr().and_then(|expr| expr.as_object()) else {
47    stylex_panic!("{}", VALUES_MUST_BE_OBJECT)
48  };
49
50  let expanded_object = obj_map(ObjMapType::Object(frames.clone()), state, |frame, state| {
51    let Some((_, frame, _)) = frame.as_tuple() else {
52      stylex_panic!("{}", VALUES_MUST_BE_OBJECT)
53    };
54
55    let pipe_result = Pipe::create(frame)
56      .pipe(|frame| expand_frame_shorthands(frame, state))
57      .pipe(|entries| obj_map_keys_string(&entries, dashify))
58      .pipe(|entries| {
59        obj_map(
60          ObjMapType::Map(entries),
61          state,
62          |entry, state| match entry.as_ref() {
63            FlatCompiledStylesValue::KeyValue(pair) => {
64              Rc::new(FlatCompiledStylesValue::KeyValue(Pair::new(
65                pair.key.clone(),
66                transform_value_cached(pair.key.as_str(), pair.value.as_str(), state),
67              )))
68            },
69            _ => stylex_panic!("{}", ENTRY_MUST_BE_TUPLE),
70          },
71        )
72      })
73      .done();
74
75    let pairs = pipe_result
76      .into_iter()
77      .filter_map(|(_, value)| value.as_key_value().cloned())
78      .collect::<Vec<Pair>>();
79
80    Rc::new(FlatCompiledStylesValue::KeyValues(pairs))
81  });
82
83  let options = state.options.clone();
84
85  let ltr_styles = obj_map(
86    ObjMapType::Map(expanded_object.clone()),
87    state,
88    |frame, _| {
89      let Some(pairs) = frame.as_key_values() else {
90        stylex_panic!("{}", VALUES_MUST_BE_OBJECT)
91      };
92
93      let ltr_values = pairs
94        .iter()
95        .map(|pair| generate_ltr(pair, &options))
96        .collect();
97
98      Rc::new(FlatCompiledStylesValue::KeyValues(ltr_values))
99    },
100  );
101
102  let stable_styles = obj_map(
103    ObjMapType::Map(expanded_object.clone()),
104    state,
105    |frame, _| {
106      let Some(pairs) = frame.as_key_values() else {
107        stylex_panic!("{}", VALUES_MUST_BE_OBJECT)
108      };
109
110      let ltr_values = pairs
111        .iter()
112        .map(|pair| generate_ltr(pair, &Default::default()))
113        .collect();
114
115      Rc::new(FlatCompiledStylesValue::KeyValues(ltr_values))
116    },
117  );
118
119  let options = state.options.clone();
120
121  let rtl_styles = obj_map(
122    ObjMapType::Map(expanded_object.clone()),
123    state,
124    |frame, _| {
125      let Some(pairs) = frame.as_key_values() else {
126        stylex_panic!("{}", VALUES_MUST_BE_OBJECT)
127      };
128
129      let rtl_values = pairs
130        .iter()
131        .map(|pair| generate_rtl(pair, &options).unwrap_or_else(|| pair.clone()))
132        .collect();
133
134      Rc::new(FlatCompiledStylesValue::KeyValues(rtl_values))
135    },
136  );
137
138  let ltr_string = construct_keyframes_obj(&ltr_styles);
139  let rtl_string = construct_keyframes_obj(&rtl_styles);
140  let stable_string = construct_keyframes_obj(&stable_styles);
141
142  // NOTE: Use a direction-agnostic hash to keep LTR/RTL classnames stable across builds.
143  // NOTE: '<>' and '-B' is used to keep existing hashes stable.
144  // TODO: They should be removed in a future version.
145  let animation_name = format!(
146    "{}{}-B",
147    class_name_prefix,
148    create_hash(&format!("<>{}", stable_string))
149  );
150
151  let ltr = format!("@keyframes {}{{{}}}", animation_name, ltr_string);
152  let rtl = if ltr_string == rtl_string {
153    None
154  } else {
155    Some(format!("@keyframes {}{{{}}}", animation_name, rtl_string))
156  };
157
158  (
159    animation_name,
160    InjectableStyleKind::Regular(InjectableStyle {
161      ltr,
162      rtl,
163      priority: Some(0.0),
164    }),
165  )
166}
167
168fn construct_keyframes_obj(frames: &FlatCompiledStyles) -> String {
169  frames
170    .into_iter()
171    .map(|(key, value)| {
172      let value = match value.as_ref() {
173        FlatCompiledStylesValue::KeyValues(pairs) => pairs
174          .iter()
175          .map(|pair| {
176            if pair.key.is_empty() || pair.value.is_empty() {
177              String::default()
178            } else {
179              format!("{}:{};", pair.key, pair.value)
180            }
181          })
182          .collect::<Vec<String>>()
183          .join(""),
184        _ => stylex_panic!("Value must be a key value pair array"),
185      };
186
187      format!("{}{{{}}}", key, value)
188    })
189    .collect::<Vec<String>>()
190    .join("")
191}
192
193fn expand_frame_shorthands(frame: &Expr, state: &mut StateManager) -> IndexMap<String, String> {
194  let res: Vec<_> = obj_entries(&frame.clone())
195    .iter()
196    .flat_map(|pair| {
197      let key = convert_key_value_to_str(pair);
198      let value = match convert_expr_to_str(pair.value.as_ref(), state, &FunctionMap::default()) {
199        Some(v) => v,
200        None => stylex_panic!("{}", VALUE_MUST_BE_STRING),
201      };
202
203      flat_map_expanded_shorthands((key, PreRuleValue::String(value)), &state.options)
204        .into_iter()
205        .filter_map(|pair| {
206          pair.1.as_ref()?;
207
208          Some(pair)
209        })
210        .collect::<Vec<OrderPair>>()
211    })
212    .filter(|item| item.1.is_some())
213    .collect::<Vec<OrderPair>>();
214
215  obj_from_entries(&res)
216}
217
218pub(crate) fn get_keyframes_fn() -> FunctionConfig {
219  FunctionConfig {
220    fn_ptr: FunctionType::StylexExprFn(
221      |expr: Expr, local_state: &mut dyn stylex_types::traits::StyleOptions| -> Expr {
222        let state = downcast_style_options_to_state_manager(local_state);
223
224        let (animation_name, injected_style) =
225          stylex_keyframes(&EvaluateResultValue::Expr(expr), state);
226
227        state
228          .other_injected_css_rules
229          .insert(animation_name.clone(), Rc::new(injected_style));
230
231        create_string_expr(animation_name.as_str())
232      },
233    ),
234    takes_path: false,
235  }
236}