Skip to main content

stylex_css_parser/css_types/
angle.rs

1/*!
2CSS Angle type parsing.
3
4Handles angle values with 'deg' (degrees), 'grad' (gradians), 'rad' (radians), and 'turn' (turns) units.
5*/
6
7use stylex_macros::stylex_unreachable;
8
9use crate::{token_parser::TokenParser, token_types::SimpleToken};
10use std::fmt::{self, Display};
11
12/// Valid angle units
13pub const ANGLE_UNITS: &[&str] = &["deg", "grad", "rad", "turn"];
14
15/// CSS Angle value with unit
16#[derive(Debug, Clone, PartialEq)]
17pub struct Angle {
18  pub value: f32,
19  pub unit: String, // "deg", "grad", "rad", or "turn"
20}
21
22impl Angle {
23  /// Create a new Angle value
24  pub fn new(value: f32, unit: String) -> Self {
25    Self { value, unit }
26  }
27
28  /// All valid angle units
29  pub fn units() -> &'static [&'static str] {
30    ANGLE_UNITS
31  }
32
33  /// Check if a unit is a valid angle unit
34  pub fn is_valid_unit(unit: &str) -> bool {
35    ANGLE_UNITS.contains(&unit)
36  }
37
38  /// Parser for CSS angle values
39  pub fn parser() -> TokenParser<Angle> {
40    // Parser for dimension tokens with valid angle units
41    let dimension_parser = TokenParser::<SimpleToken>::token(
42      SimpleToken::Dimension {
43        value: 0.0,
44        unit: String::new(),
45      },
46      Some("Dimension"),
47    )
48    .where_fn(
49      |token| {
50        if let SimpleToken::Dimension { unit, .. } = token {
51          Self::is_valid_unit(unit)
52        } else {
53          false
54        }
55      },
56      Some("valid_angle_unit"),
57    )
58    .map(
59      |token| {
60        if let SimpleToken::Dimension { value, unit } = token {
61          Angle::new(value as f32, unit)
62        } else {
63          stylex_unreachable!()
64        }
65      },
66      Some("to_angle"),
67    );
68
69    // Parser for zero without unit (special case for angles - defaults to 'deg')
70    let zero_parser = TokenParser::<SimpleToken>::token(SimpleToken::Number(0.0), Some("Number"))
71      .where_fn(
72        |token| {
73          if let SimpleToken::Number(value) = token {
74            *value == 0.0
75          } else {
76            false
77          }
78        },
79        Some("zero_value"),
80      )
81      .map(|_| Angle::new(0.0, "deg".to_string()), Some("zero_angle"));
82
83    // Combine both parsers
84    TokenParser::one_of(vec![dimension_parser, zero_parser])
85  }
86}
87
88impl Display for Angle {
89  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90    write!(f, "{}{}", self.value, self.unit)
91  }
92}
93
94#[cfg(test)]
95mod tests {
96  use super::*;
97
98  #[test]
99  fn test_angle_creation() {
100    let angle = Angle::new(45.0, "deg".to_string());
101    assert_eq!(angle.value, 45.0);
102    assert_eq!(angle.unit, "deg");
103  }
104
105  #[test]
106  fn test_angle_display() {
107    let degrees = Angle::new(45.0, "deg".to_string());
108    assert_eq!(degrees.to_string(), "45deg");
109
110    let radians = Angle::new(1.57, "rad".to_string());
111    assert_eq!(radians.to_string(), "1.57rad");
112
113    let gradians = Angle::new(100.0, "grad".to_string());
114    assert_eq!(gradians.to_string(), "100grad");
115
116    let turns = Angle::new(0.25, "turn".to_string());
117    assert_eq!(turns.to_string(), "0.25turn");
118
119    // Zero without unit
120    let zero_angle = Angle::new(0.0, "deg".to_string());
121    assert_eq!(zero_angle.to_string(), "0deg");
122  }
123
124  #[test]
125  fn test_valid_angle_units() {
126    assert!(Angle::is_valid_unit("deg"));
127    assert!(Angle::is_valid_unit("grad"));
128    assert!(Angle::is_valid_unit("rad"));
129    assert!(Angle::is_valid_unit("turn"));
130
131    // Invalid units
132    assert!(!Angle::is_valid_unit("px"));
133    assert!(!Angle::is_valid_unit("s"));
134    assert!(!Angle::is_valid_unit("Hz"));
135    assert!(!Angle::is_valid_unit("invalid"));
136  }
137
138  #[test]
139  fn test_angle_units_constant() {
140    let units = Angle::units();
141    assert_eq!(units.len(), 4);
142    assert!(units.contains(&"deg"));
143    assert!(units.contains(&"grad"));
144    assert!(units.contains(&"rad"));
145    assert!(units.contains(&"turn"));
146  }
147
148  #[test]
149  fn test_angle_parser_creation() {
150    // Basic test that parser can be created
151    let _parser = Angle::parser();
152  }
153
154  #[test]
155  fn test_angle_equality() {
156    let angle1 = Angle::new(45.0, "deg".to_string());
157    let angle2 = Angle::new(45.0, "deg".to_string());
158    let angle3 = Angle::new(90.0, "deg".to_string());
159    let angle4 = Angle::new(45.0, "rad".to_string());
160
161    assert_eq!(angle1, angle2);
162    assert_ne!(angle1, angle3);
163    assert_ne!(angle1, angle4);
164  }
165
166  #[test]
167  fn test_angle_units_coverage() {
168    // Test all standard CSS angle units are included
169    assert!(Angle::is_valid_unit("deg")); // degrees (most common)
170    assert!(Angle::is_valid_unit("grad")); // gradians
171    assert!(Angle::is_valid_unit("rad")); // radians
172    assert!(Angle::is_valid_unit("turn")); // full turns
173  }
174}