1use stylex_macros::stylex_unreachable;
2
3use crate::css_types::length_percentage::{LengthPercentage, length_percentage_parser};
4use 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#[derive(Debug, Clone, PartialEq)]
72pub enum Horizontal {
73 Length(LengthPercentage),
74 Keyword(HorizontalKeyword),
75 KeywordWithOffset(HorizontalKeyword, LengthPercentage), }
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#[derive(Debug, Clone, PartialEq)]
90pub enum Vertical {
91 Length(LengthPercentage),
92 Keyword(VerticalKeyword),
93 KeywordWithOffset(VerticalKeyword, LengthPercentage), }
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 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 pub fn parser() -> TokenParser<Position> {
187 let two_values = TokenParser::one_of(vec![
190 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 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 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_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 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 let single_values = TokenParser::one_of(vec![
299 Self::horizontal_keyword_parser().map(
301 |h| Position::new(Some(Horizontal::Keyword(h)), None),
302 Some("single_h_keyword"),
303 ),
304 Self::vertical_keyword_parser().map(
306 |v| Position::new(None, Some(Vertical::Keyword(v))),
307 Some("single_v_keyword"),
308 ),
309 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 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 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 assert!(matches!(pos.horizontal, Some(Horizontal::Length(_))));
452 assert!(matches!(pos.vertical, Some(Vertical::Length(_))));
453 }
454 }
455}