Skip to main content

stylex_transform/shared/utils/css/normalizers/
base.rs

1use swc_core::{
2  common::DUMMY_SP,
3  css::{
4    ast::{
5      ComponentValue, Declaration, DeclarationName, Dimension, Function, Ident, Length,
6      ListOfComponentValues, Number, Stylesheet,
7    },
8    visit::{Fold, FoldWith},
9  },
10};
11
12use crate::shared::utils::common::dashify;
13use stylex_constants::constants::common::ROOT_FONT_SIZE;
14
15struct CssFolder {
16  enable_font_size_px_to_rem: bool,
17  parent_key: Option<String>,
18  is_function_arg: bool,
19  current_property: Option<String>,
20}
21
22impl CssFolder {
23  fn convert_font_size_to_rem_normalizer<'a>(
24    &'a mut self,
25    declaration: &'a mut Declaration,
26  ) -> &'a mut Declaration {
27    if let DeclarationName::Ident(ident) = &declaration.name
28      && (ident.value == "fontSize" || self.parent_key.as_deref() == Some("fontSize"))
29    {
30      self.parent_key = Some("fontSize".into());
31      declaration.value = declaration.value.clone().fold_children_with(self);
32      self.parent_key = None;
33    }
34
35    declaration
36  }
37}
38
39impl Fold for CssFolder {
40  fn fold_list_of_component_values(
41    &mut self,
42    list: ListOfComponentValues,
43  ) -> ListOfComponentValues {
44    list.fold_children_with(self)
45  }
46
47  fn fold_declaration(&mut self, mut declaration: Declaration) -> Declaration {
48    let declaration = kebab_case_normalizer(&mut declaration);
49
50    if self.enable_font_size_px_to_rem {
51      self.convert_font_size_to_rem_normalizer(declaration);
52    }
53
54    declaration.clone().fold_children_with(self)
55  }
56
57  fn fold_dimension(&mut self, mut dimension: Dimension) -> Dimension {
58    let dimension = timing_normalizer(&mut dimension);
59    let dimension = zero_dimension_normalizer(
60      dimension,
61      self.is_function_arg,
62      self.current_property.clone(),
63    );
64
65    dimension.clone().fold_children_with(self)
66  }
67
68  fn fold_length(&mut self, mut length: Length) -> Length {
69    if self.parent_key == Some("fontSize".into())
70      && length.unit.value.eq("px")
71      && length.value.value != 0.0
72    {
73      length = Length {
74        value: Number {
75          value: length.value.value / ROOT_FONT_SIZE as f64,
76          raw: None,
77          span: length.span,
78        },
79        unit: Ident {
80          value: "rem".into(),
81          raw: None,
82          span: length.span,
83        },
84        span: DUMMY_SP,
85      };
86    };
87
88    length
89  }
90
91  fn fold_function(&mut self, func: Function) -> Function {
92    self.is_function_arg = true;
93
94    let mut fnc = func;
95
96    // NOTE: only last css fucntion value should be folded
97    if let Some(last) = fnc.value.last_mut() {
98      *last = last.clone().fold_children_with(self);
99    }
100
101    self.is_function_arg = false;
102
103    fnc
104  }
105}
106
107fn timing_normalizer(dimension: &mut Dimension) -> &mut Dimension {
108  match dimension {
109    Dimension::Time(time) => {
110      if !time.unit.eq("ms") || time.value.value < 10.0 {
111        return dimension;
112      }
113
114      time.value = Number {
115        value: time.value.value / 1000.0,
116        raw: None,
117        span: DUMMY_SP,
118      };
119
120      time.unit = Ident {
121        span: DUMMY_SP,
122        value: "s".into(),
123        raw: None,
124      };
125
126      dimension
127    },
128    _ => dimension,
129  }
130}
131
132fn kebab_case_normalizer(declaration: &mut Declaration) -> &mut Declaration {
133  match &declaration.name {
134    DeclarationName::Ident(ident) => {
135      if !ident.value.eq("transitionProperty") && !ident.value.eq("willChange") {
136        return declaration;
137      }
138    },
139    DeclarationName::DashedIdent(_) => return declaration,
140  }
141
142  declaration.value.iter_mut().for_each(|value| {
143    if let ComponentValue::Ident(ident) = value
144      && !ident.value.starts_with("--")
145    {
146      ident.value = dashify(ident.value.as_str()).into();
147    }
148  });
149
150  declaration
151}
152
153pub(crate) fn base_normalizer(
154  ast: Stylesheet,
155  enable_font_size_px_to_rem: bool,
156  current_property: Option<&str>,
157) -> Stylesheet {
158  let mut folder = CssFolder {
159    enable_font_size_px_to_rem,
160    parent_key: None,
161    is_function_arg: false,
162    current_property: current_property.map(|p| p.to_string()),
163  };
164  ast.fold_with(&mut folder)
165}
166
167fn zero_dimension_normalizer(
168  dimension: &mut Dimension,
169  is_function_arg: bool,
170  current_property: Option<String>,
171) -> &mut Dimension {
172  // Skip normalization for CSS custom properties (variables starting with --)
173  if let Some(prop) = current_property
174    && prop.starts_with("--")
175  {
176    return dimension;
177  }
178
179  if is_function_arg {
180    return dimension;
181  }
182
183  match dimension {
184    Dimension::Length(length) => {
185      if length.value.value != 0.0 {
186        return dimension;
187      }
188
189      length.value = get_zero_dimension_value();
190      length.unit = get_zero_dimension_unit(&length.unit);
191
192      dimension
193    },
194    Dimension::Angle(angle) => {
195      if angle.value.value != 0.0 {
196        return dimension;
197      }
198
199      angle.value = get_zero_dimension_value();
200
201      angle.unit = Ident {
202        span: DUMMY_SP,
203        value: "deg".into(),
204        raw: None,
205      };
206
207      dimension
208    },
209    Dimension::Time(time) => {
210      if time.value.value != 0.0 {
211        return dimension;
212      }
213
214      time.value = get_zero_dimension_value();
215
216      time.unit = Ident {
217        span: DUMMY_SP,
218        value: "s".into(),
219        raw: None,
220      };
221
222      dimension
223    },
224    Dimension::Frequency(frequency) => {
225      if frequency.value.value != 0.0 {
226        return dimension;
227      }
228
229      frequency.value = get_zero_dimension_value();
230      frequency.unit = get_zero_dimension_unit(&frequency.unit);
231
232      dimension
233    },
234    Dimension::Resolution(resolution) => {
235      if resolution.value.value != 0.0 {
236        return dimension;
237      }
238
239      resolution.value = get_zero_dimension_value();
240      resolution.unit = get_zero_dimension_unit(&resolution.unit);
241
242      dimension
243    },
244    Dimension::Flex(flex) => {
245      if flex.value.value != 0.0 {
246        return dimension;
247      }
248
249      flex.value = get_zero_dimension_value();
250      flex.unit = get_zero_dimension_unit(&flex.unit);
251
252      dimension
253    },
254    Dimension::UnknownDimension(unknown) => {
255      if unknown.value.value != 0.0 {
256        return dimension;
257      }
258
259      unknown.value = get_zero_dimension_value();
260      unknown.unit = get_zero_dimension_unit(&unknown.unit);
261
262      dimension
263    },
264  }
265}
266
267fn get_zero_dimension_value() -> Number {
268  Number {
269    value: 0.0,
270    raw: None,
271    span: DUMMY_SP,
272  }
273}
274
275fn get_zero_dimension_unit(unit: &Ident) -> Ident {
276  if unit.value.eq("fr") {
277    return Ident {
278      value: "fr".into(),
279      raw: None,
280      span: DUMMY_SP,
281    };
282  }
283
284  if unit.value.eq("%") {
285    return Ident {
286      value: "%".into(),
287      raw: None,
288      span: DUMMY_SP,
289    };
290  }
291
292  Ident {
293    value: "".into(),
294    raw: None,
295    span: DUMMY_SP,
296  }
297}