Skip to main content

stylex_css_parser/css_types/
position.rs

1use stylex_macros::stylex_unreachable;
2
3use crate::css_types::length_percentage::{LengthPercentage, length_percentage_parser};
4/**
5 * CSS Position Type Parser
6 *
7 * Provides comprehensive position parsing for CSS layout properties.
8 * Covers all major CSS position parsing scenarios with Rust type safety.
9 */
10use crate::token_parser::TokenParser;
11use crate::token_types::SimpleToken;
12use std::fmt;
13
14#[derive(Debug, Clone, PartialEq)]
15pub enum HorizontalKeyword {
16  Left,
17  Center,
18  Right,
19}
20
21impl fmt::Display for HorizontalKeyword {
22  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23    let s = match self {
24      HorizontalKeyword::Left => "left",
25      HorizontalKeyword::Center => "center",
26      HorizontalKeyword::Right => "right",
27    };
28    write!(f, "{}", s)
29  }
30}
31
32impl HorizontalKeyword {
33  pub fn as_str(&self) -> &str {
34    match self {
35      HorizontalKeyword::Left => "left",
36      HorizontalKeyword::Center => "center",
37      HorizontalKeyword::Right => "right",
38    }
39  }
40}
41
42#[derive(Debug, Clone, PartialEq)]
43pub enum VerticalKeyword {
44  Top,
45  Center,
46  Bottom,
47}
48
49impl fmt::Display for VerticalKeyword {
50  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51    let s = match self {
52      VerticalKeyword::Top => "top",
53      VerticalKeyword::Center => "center",
54      VerticalKeyword::Bottom => "bottom",
55    };
56    write!(f, "{}", s)
57  }
58}
59
60impl VerticalKeyword {
61  pub fn as_str(&self) -> &str {
62    match self {
63      VerticalKeyword::Top => "top",
64      VerticalKeyword::Center => "center",
65      VerticalKeyword::Bottom => "bottom",
66    }
67  }
68}
69
70/// | LengthPercentage | HorizontalKeyword | [HorizontalKeyword, LengthPercentage]
71#[derive(Debug, Clone, PartialEq)]
72pub enum Horizontal {
73  Length(LengthPercentage),
74  Keyword(HorizontalKeyword),
75  KeywordWithOffset(HorizontalKeyword, LengthPercentage), // [HorizontalKeyword, LengthPercentage]
76}
77
78impl fmt::Display for Horizontal {
79  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80    match self {
81      Horizontal::Length(lp) => write!(f, "{}", lp),
82      Horizontal::Keyword(k) => write!(f, "{}", k),
83      Horizontal::KeywordWithOffset(k, lp) => write!(f, "{} {}", k, lp),
84    }
85  }
86}
87
88/// | LengthPercentage | VerticalKeyword | [VerticalKeyword, LengthPercentage]
89#[derive(Debug, Clone, PartialEq)]
90pub enum Vertical {
91  Length(LengthPercentage),
92  Keyword(VerticalKeyword),
93  KeywordWithOffset(VerticalKeyword, LengthPercentage), // [VerticalKeyword, LengthPercentage]
94}
95
96impl fmt::Display for Vertical {
97  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98    match self {
99      Vertical::Length(lp) => write!(f, "{}", lp),
100      Vertical::Keyword(k) => write!(f, "{}", k),
101      Vertical::KeywordWithOffset(k, lp) => write!(f, "{} {}", k, lp),
102    }
103  }
104}
105
106#[derive(Debug, Clone, PartialEq)]
107pub struct Position {
108  pub horizontal: Option<Horizontal>,
109  pub vertical: Option<Vertical>,
110}
111
112impl Position {
113  /// Create a new Position
114  pub fn new(horizontal: Option<Horizontal>, vertical: Option<Vertical>) -> Self {
115    Self {
116      horizontal,
117      vertical,
118    }
119  }
120
121  fn horizontal_keyword_parser() -> TokenParser<HorizontalKeyword> {
122    TokenParser::<SimpleToken>::token(SimpleToken::Ident(String::new()), Some("Ident"))
123      .where_fn(
124        |token| {
125          if let SimpleToken::Ident(value) = token {
126            matches!(value.as_str(), "left" | "center" | "right")
127          } else {
128            false
129          }
130        },
131        Some("horizontal_keyword"),
132      )
133      .map(
134        |token| {
135          if let SimpleToken::Ident(value) = token {
136            match value.as_str() {
137              "left" => HorizontalKeyword::Left,
138              "center" => HorizontalKeyword::Center,
139              "right" => HorizontalKeyword::Right,
140              _ => stylex_unreachable!(),
141            }
142          } else {
143            stylex_unreachable!()
144          }
145        },
146        Some("to_horizontal_keyword"),
147      )
148  }
149
150  fn vertical_keyword_parser() -> TokenParser<VerticalKeyword> {
151    TokenParser::<SimpleToken>::token(SimpleToken::Ident(String::new()), Some("Ident"))
152      .where_fn(
153        |token| {
154          if let SimpleToken::Ident(value) = token {
155            matches!(value.as_str(), "top" | "center" | "bottom")
156          } else {
157            false
158          }
159        },
160        Some("vertical_keyword"),
161      )
162      .map(
163        |token| {
164          if let SimpleToken::Ident(value) = token {
165            match value.as_str() {
166              "top" => VerticalKeyword::Top,
167              "center" => VerticalKeyword::Center,
168              "bottom" => VerticalKeyword::Bottom,
169              _ => stylex_unreachable!(),
170            }
171          } else {
172            stylex_unreachable!()
173          }
174        },
175        Some("to_vertical_keyword"),
176      )
177  }
178
179  /// Covers these key scenarios:
180  /// 1. Single keywords: "left", "top", "center", etc.
181  /// 2. Single lengths: "50%", "10px", etc.
182  /// 3. Two values: "left top", "50% 25%", "center 10px", etc.
183  /// 4. Keyword with offset: "left 10px", "top 20%", etc.
184  ///
185  /// while being much simpler and more maintainable in Rust.
186  pub fn parser() -> TokenParser<Position> {
187    // Strategy 1: Two-value positions (most common case)
188    // This covers: "left top", "50% 25%", "center 10px", etc.
189    let two_values = TokenParser::one_of(vec![
190      // Horizontal keyword then vertical keyword: "left top"
191      Self::horizontal_keyword_parser().flat_map(
192        |h| {
193          TokenParser::<SimpleToken>::token(SimpleToken::Whitespace, Some("ws")).flat_map(
194            move |_| {
195              let h_clone = h.clone();
196              Self::vertical_keyword_parser().map(
197                move |v| {
198                  Position::new(
199                    Some(Horizontal::Keyword(h_clone.clone())),
200                    Some(Vertical::Keyword(v)),
201                  )
202                },
203                Some("h_kw_v_kw"),
204              )
205            },
206            Some("ws_to_v_kw"),
207          )
208        },
209        Some("horizontal_vertical_keywords"),
210      ),
211      // Vertical keyword then horizontal keyword: "top left"
212      Self::vertical_keyword_parser().flat_map(
213        |v| {
214          TokenParser::<SimpleToken>::token(SimpleToken::Whitespace, Some("ws")).flat_map(
215            move |_| {
216              let v_clone = v.clone();
217              Self::horizontal_keyword_parser().map(
218                move |h| {
219                  Position::new(
220                    Some(Horizontal::Keyword(h)),
221                    Some(Vertical::Keyword(v_clone.clone())),
222                  )
223                },
224                Some("v_kw_h_kw"),
225              )
226            },
227            Some("ws_to_h_kw"),
228          )
229        },
230        Some("vertical_horizontal_keywords"),
231      ),
232      // Two length values: "50% 25%"
233      length_percentage_parser().flat_map(
234        |first| {
235          TokenParser::<SimpleToken>::token(SimpleToken::Whitespace, Some("ws")).flat_map(
236            move |_| {
237              let first_clone = first.clone();
238              length_percentage_parser().map(
239                move |second| {
240                  Position::new(
241                    Some(Horizontal::Length(first_clone.clone())),
242                    Some(Vertical::Length(second)),
243                  )
244                },
245                Some("two_lengths"),
246              )
247            },
248            Some("ws_to_second_length"),
249          )
250        },
251        Some("length_length"),
252      ),
253      // Length then vertical keyword: "50% top"
254      length_percentage_parser().flat_map(
255        |length| {
256          TokenParser::<SimpleToken>::token(SimpleToken::Whitespace, Some("ws")).flat_map(
257            move |_| {
258              let len_clone = length.clone();
259              Self::vertical_keyword_parser().map(
260                move |v| {
261                  Position::new(
262                    Some(Horizontal::Length(len_clone.clone())),
263                    Some(Vertical::Keyword(v)),
264                  )
265                },
266                Some("length_v_kw"),
267              )
268            },
269            Some("ws_to_v_kw"),
270          )
271        },
272        Some("length_vertical"),
273      ),
274      // Horizontal keyword then length: "left 25%"
275      Self::horizontal_keyword_parser().flat_map(
276        |h| {
277          TokenParser::<SimpleToken>::token(SimpleToken::Whitespace, Some("ws")).flat_map(
278            move |_| {
279              let h_clone = h.clone();
280              length_percentage_parser().map(
281                move |length| {
282                  Position::new(
283                    Some(Horizontal::Keyword(h_clone.clone())),
284                    Some(Vertical::Length(length)),
285                  )
286                },
287                Some("h_kw_length"),
288              )
289            },
290            Some("ws_to_length"),
291          )
292        },
293        Some("horizontal_length"),
294      ),
295    ]);
296
297    // Strategy 2: Single values
298    let single_values = TokenParser::one_of(vec![
299      // Single horizontal keyword: "left"
300      Self::horizontal_keyword_parser().map(
301        |h| Position::new(Some(Horizontal::Keyword(h)), None),
302        Some("single_h_keyword"),
303      ),
304      // Single vertical keyword: "top"
305      Self::vertical_keyword_parser().map(
306        |v| Position::new(None, Some(Vertical::Keyword(v))),
307        Some("single_v_keyword"),
308      ),
309      // Single length (applies to horizontal): "50%"
310      length_percentage_parser().map(
311        |lp| {
312          Position::new(
313            Some(Horizontal::Length(lp.clone())),
314            Some(Vertical::Length(lp)),
315          )
316        },
317        Some("single_length"),
318      ),
319    ]);
320
321    // Try two-value patterns first, then fall back to single values
322
323    two_values.or(single_values).map(
324      |either| match either {
325        crate::token_parser::Either::Left(pos) => pos,
326        crate::token_parser::Either::Right(pos) => pos,
327      },
328      Some("position_result"),
329    )
330  }
331}
332
333impl fmt::Display for Position {
334  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335    let parts: Vec<String> = [
336      self.horizontal.as_ref().map(|h| h.to_string()),
337      self.vertical.as_ref().map(|v| v.to_string()),
338    ]
339    .into_iter()
340    .flatten()
341    .collect();
342
343    write!(f, "{}", parts.join(" "))
344  }
345}
346
347pub fn position_parser() -> TokenParser<Position> {
348  Position::parser()
349}
350
351#[cfg(test)]
352mod tests {
353  use super::*;
354
355  #[test]
356  fn test_horizontal_keyword() {
357    let result = Position::horizontal_keyword_parser().parse("left");
358    assert!(result.is_ok());
359    assert_eq!(result.unwrap(), HorizontalKeyword::Left);
360  }
361
362  #[test]
363  fn test_vertical_keyword() {
364    let result = Position::vertical_keyword_parser().parse("top");
365    assert!(result.is_ok());
366    assert_eq!(result.unwrap(), VerticalKeyword::Top);
367  }
368
369  #[test]
370  fn test_position_basic() {
371    let result = Position::parser().parse("left");
372    assert!(result.is_ok());
373
374    let pos = result.unwrap();
375    assert!(pos.horizontal.is_some());
376    assert!(pos.vertical.is_none());
377  }
378
379  #[test]
380  fn test_position_display() {
381    let pos = Position::new(
382      Some(Horizontal::Keyword(HorizontalKeyword::Left)),
383      Some(Vertical::Keyword(VerticalKeyword::Top)),
384    );
385    assert_eq!(pos.to_string(), "left top");
386  }
387
388  #[test]
389  fn test_horizontal_keyword_as_str() {
390    let left = HorizontalKeyword::Left;
391    let center = HorizontalKeyword::Center;
392    let right = HorizontalKeyword::Right;
393
394    assert_eq!(left.as_str(), "left");
395    assert_eq!(center.as_str(), "center");
396    assert_eq!(right.as_str(), "right");
397  }
398
399  #[test]
400  fn test_vertical_keyword_as_str() {
401    let top = VerticalKeyword::Top;
402    let bottom = VerticalKeyword::Bottom;
403
404    assert_eq!(top.as_str(), "top");
405    assert_eq!(bottom.as_str(), "bottom");
406  }
407
408  #[test]
409  fn test_keyword_with_offset_display() {
410    let h = Horizontal::KeywordWithOffset(
411      HorizontalKeyword::Left,
412      LengthPercentage::Percentage(crate::css_types::Percentage::new(50.0)),
413    );
414    assert_eq!(h.to_string(), "left 50%");
415  }
416
417  #[test]
418  fn test_numbers_only() {
419    // This would test: "50% 25%" -> Position with both horizontal and vertical length
420    let pos = Position::new(
421      Some(Horizontal::Length(LengthPercentage::Percentage(
422        crate::css_types::Percentage::new(50.0),
423      ))),
424      Some(Vertical::Length(LengthPercentage::Percentage(
425        crate::css_types::Percentage::new(25.0),
426      ))),
427    );
428    assert_eq!(pos.to_string(), "50% 25%");
429  }
430
431  #[test]
432  fn test_two_keywords() {
433    let result = Position::parser().parse("left top");
434    if let Ok(pos) = result {
435      assert!(matches!(
436        pos.horizontal,
437        Some(Horizontal::Keyword(HorizontalKeyword::Left))
438      ));
439      assert!(matches!(
440        pos.vertical,
441        Some(Vertical::Keyword(VerticalKeyword::Top))
442      ));
443    }
444  }
445
446  #[test]
447  fn test_two_lengths() {
448    let result = Position::parser().parse("50%");
449    if let Ok(pos) = result {
450      // Single length should apply to both axes
451      assert!(matches!(pos.horizontal, Some(Horizontal::Length(_))));
452      assert!(matches!(pos.vertical, Some(Vertical::Length(_))));
453    }
454  }
455}