Skip to main content

stylex_css/css/
generate_rtl.rs

1use log::warn;
2
3use stylex_constants::constants::{
4  cursor_flip::CURSOR_FLIP,
5  logical_to_rtl::{INLINE_TO_RTL, LOGICAL_TO_RTL},
6};
7use stylex_enums::style_resolution::StyleResolution;
8use stylex_regex::regex::LENGTH_UNIT_TESTER_REGEX;
9use stylex_structures::{pair::Pair, stylex_state_options::StyleXStateOptions};
10
11fn logical_to_physical_rtl(input: &str) -> Option<&str> {
12  match input {
13    "start" | "inline-start" => Some("right"),
14    "end" | "inline-end" => Some("left"),
15    _ => None,
16  }
17}
18
19fn property_to_rtl(pair: &Pair, options: &StyleXStateOptions) -> Option<Pair> {
20  if let Some(&ltr_key) = LOGICAL_TO_RTL.get(pair.key.as_str()) {
21    return Some(Pair::new(ltr_key.to_string(), pair.value.clone()));
22  }
23
24  match pair.key.as_str() {
25    "float" | "clear" => logical_to_physical_rtl(pair.value.as_str())
26      .map(|value| Pair::new(pair.key.clone(), value.to_string())),
27    "background-position" => {
28      let new_val = pair
29        .value
30        .split_whitespace()
31        .map(|word| match word {
32          "start" | "inset-inline-start" => "right",
33          "end" | "inset-inline-end" => "left",
34          _ => word,
35        })
36        .collect::<Vec<_>>()
37        .join(" ");
38      Some(Pair::new(pair.key.clone(), new_val))
39    },
40    "cursor" => {
41      if !options.enable_legacy_value_flipping {
42        return None;
43      }
44
45      CURSOR_FLIP
46        .get(pair.value.as_str())
47        .map(|val| Pair::new(pair.key.clone(), val.to_string()))
48    },
49    _ => shadows_flip(pair.key.as_str(), pair.value.as_str(), options),
50  }
51}
52
53pub fn generate_rtl(pair: &Pair, options: &StyleXStateOptions) -> Option<Pair> {
54  let enable_logical_styles_polyfill = options.enable_logical_styles_polyfill;
55  let style_resolution = &options.style_resolution;
56  let key = pair.key.as_str();
57
58  if style_resolution == &StyleResolution::LegacyExpandShorthands {
59    if !enable_logical_styles_polyfill {
60      return None;
61    }
62    if let Some(inline_to_rtl_value) = INLINE_TO_RTL.get(key) {
63      return Some(Pair::new(
64        inline_to_rtl_value.to_string(),
65        pair.value.clone(),
66      ));
67    }
68  }
69
70  property_to_rtl(pair, options)
71}
72
73fn shadows_flip(key: &str, val: &str, options: &StyleXStateOptions) -> Option<Pair> {
74  match key {
75    "box-shadow" | "text-shadow" => {
76      if !options.enable_legacy_value_flipping {
77        return None;
78      }
79
80      let rtl_val = flip_shadow(val);
81      rtl_val.map(|rtl_val| Pair::new(key.to_string(), rtl_val.to_string()))
82    },
83    _ => None,
84  }
85}
86
87fn flip_shadow(value: &str) -> Option<String> {
88  let defs: Vec<&str> = value.split(',').collect();
89  let mut built_defs = Vec::new();
90
91  for def in defs {
92    let mut parts = def
93      .split_whitespace()
94      .map(|x| x.to_string())
95      .collect::<Vec<String>>();
96
97    // NOTE: temporary solution, need to implement unit parser
98    let index = if is_unit(parts[0].as_str()) { 0 } else { 1 };
99
100    if index < parts.len() {
101      let flipped = flip_sign(&parts[index]);
102      parts[index] = flipped;
103    }
104    built_defs.push(parts.join(" "));
105  }
106
107  let rtl = built_defs.join(",");
108  if rtl != value { Some(rtl) } else { None }
109}
110
111fn is_unit(input: &str) -> bool {
112  LENGTH_UNIT_TESTER_REGEX
113    .is_match(input)
114    .unwrap_or_else(|err| {
115      warn!(
116        "Error matching LENGTH_UNIT_TESTER_REGEX for '{}': {}. Skipping pattern match.",
117        input, err
118      );
119
120      false
121    })
122}
123
124fn flip_sign(value: &str) -> String {
125  if value == "0" {
126    value.to_string()
127  } else if value.starts_with('-') {
128    value.strip_prefix('-').unwrap_or(value).to_string()
129  } else {
130    format!("-{}", value)
131  }
132}