Skip to main content

stylex_css_parser/css_types/
length.rs

1/*!
2CSS Length type parsing.
3
4Handles all CSS length units including font-relative, viewport-relative,
5*/
6
7use crate::{token_parser::TokenParser, token_types::SimpleToken};
8use std::fmt::{self, Display};
9
10/// Font-relative length units
11pub const UNITS_BASED_ON_FONT: &[&str] = &["ch", "em", "ex", "ic", "lh", "rem", "rlh"];
12
13pub const UNITS_BASED_ON_VIEWPORT: &[&str] = &[
14  "vh", "svh", "lvh", "dvh", "vw", "svw", "lvw", "dvw", "vmin", "svmin", "lvmin", "dvmin", "vmax",
15  "svmax", "lvmax", "dvmax",
16];
17
18/// Container-relative length units
19pub const UNITS_BASED_ON_CONTAINER: &[&str] = &["cqw", "cqi", "cqh", "cqb", "cqmin", "cqmax"];
20
21/// Absolute length units
22pub const UNITS_BASED_ON_ABSOLUTE_UNITS: &[&str] = &["px", "cm", "mm", "in", "pt"];
23
24/// CSS Length value with unit
25#[derive(Debug, Clone, PartialEq)]
26pub struct Length {
27  pub value: f32,
28  pub unit: String,
29}
30
31impl Length {
32  /// Create a new Length value
33  pub fn new(value: f32, unit: String) -> Self {
34    Self { value, unit }
35  }
36
37  /// All valid length units
38  pub fn units() -> Vec<&'static str> {
39    let mut units = Vec::new();
40    units.extend_from_slice(UNITS_BASED_ON_FONT);
41    units.extend_from_slice(UNITS_BASED_ON_VIEWPORT);
42    units.extend_from_slice(UNITS_BASED_ON_CONTAINER);
43    units.extend_from_slice(UNITS_BASED_ON_ABSOLUTE_UNITS);
44    units
45  }
46
47  /// Check if a unit is a valid length unit
48  pub fn is_valid_unit(unit: &str) -> bool {
49    Self::units().contains(&unit)
50  }
51
52  /// Parser for CSS length values
53  pub fn parser() -> TokenParser<Length> {
54    // Parser for dimension tokens with valid length units
55    let dimension_parser = TokenParser::<SimpleToken>::token(
56      SimpleToken::Dimension {
57        value: 0.0,
58        unit: String::new(),
59      },
60      Some("Dimension"),
61    )
62    .map(
63      |token| {
64        if let SimpleToken::Dimension { value, unit } = token {
65          Some((value as f32, unit))
66        } else {
67          None
68        }
69      },
70      Some("extract_dimension"),
71    )
72    .where_fn(
73      |opt| {
74        if let Some((_, unit)) = opt {
75          Self::is_valid_unit(unit)
76        } else {
77          false
78        }
79      },
80      Some("valid_length_unit"),
81    )
82    .map(
83      |opt| {
84        let (value, unit) = opt.unwrap();
85        Length::new(value, unit)
86      },
87      Some("to_length"),
88    );
89
90    // Parser for zero without unit (special case for lengths)
91    let zero_parser = TokenParser::<SimpleToken>::token(SimpleToken::Number(0.0), Some("Number"))
92      .where_fn(
93        |token| {
94          if let SimpleToken::Number(value) = token {
95            *value == 0.0
96          } else {
97            false
98          }
99        },
100        Some("zero_value"),
101      )
102      .map(|_| Length::new(0.0, String::new()), Some("zero_length"));
103
104    // Combine both parsers
105    TokenParser::one_of(vec![dimension_parser, zero_parser])
106  }
107}
108
109impl Display for Length {
110  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111    write!(f, "{}{}", self.value, self.unit)
112  }
113}
114
115#[cfg(test)]
116mod tests {
117  use super::*;
118
119  #[test]
120  fn test_length_creation() {
121    let len = Length::new(16.0, "px".to_string());
122    assert_eq!(len.value, 16.0);
123    assert_eq!(len.unit, "px");
124  }
125
126  #[test]
127  fn test_length_display() {
128    let len = Length::new(16.0, "px".to_string());
129    assert_eq!(len.to_string(), "16px");
130
131    let zero_len = Length::new(0.0, String::new());
132    assert_eq!(zero_len.to_string(), "0");
133  }
134
135  #[test]
136  fn test_valid_units() {
137    // Font-relative units
138    assert!(Length::is_valid_unit("em"));
139    assert!(Length::is_valid_unit("rem"));
140    assert!(Length::is_valid_unit("ch"));
141
142    assert!(Length::is_valid_unit("vh"));
143    assert!(Length::is_valid_unit("vw"));
144    assert!(Length::is_valid_unit("vmin"));
145
146    // Container units
147    assert!(Length::is_valid_unit("cqw"));
148    assert!(Length::is_valid_unit("cqh"));
149
150    // Absolute units
151    assert!(Length::is_valid_unit("px"));
152    assert!(Length::is_valid_unit("cm"));
153    assert!(Length::is_valid_unit("in"));
154
155    // Invalid units
156    assert!(!Length::is_valid_unit("invalid"));
157    assert!(!Length::is_valid_unit("deg"));
158    assert!(!Length::is_valid_unit("s"));
159  }
160
161  #[test]
162  fn test_units_constants() {
163    assert!(UNITS_BASED_ON_FONT.contains(&"em"));
164    assert!(UNITS_BASED_ON_FONT.contains(&"rem"));
165
166    assert!(UNITS_BASED_ON_VIEWPORT.contains(&"vh"));
167    assert!(UNITS_BASED_ON_VIEWPORT.contains(&"vw"));
168
169    assert!(UNITS_BASED_ON_CONTAINER.contains(&"cqw"));
170
171    assert!(UNITS_BASED_ON_ABSOLUTE_UNITS.contains(&"px"));
172    assert!(UNITS_BASED_ON_ABSOLUTE_UNITS.contains(&"cm"));
173  }
174
175  #[test]
176  fn test_length_parser_creation() {
177    // Basic test that parser can be created
178    let _parser = Length::parser();
179  }
180
181  #[test]
182  fn test_all_units_included() {
183    let all_units = Length::units();
184
185    // Should include all categories
186    assert!(all_units.len() > 20); // We have many units
187    assert!(all_units.contains(&"px"));
188    assert!(all_units.contains(&"em"));
189    assert!(all_units.contains(&"vh"));
190    assert!(all_units.contains(&"cqw"));
191  }
192}