1use crate::{
9 css_types::{LengthPercentage, length_percentage_parser},
10 token_parser::TokenParser,
11 token_types::SimpleToken,
12};
13use std::fmt::{self, Display};
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct BorderRadiusIndividual {
18 pub horizontal: LengthPercentage,
19 pub vertical: LengthPercentage,
20}
21
22impl BorderRadiusIndividual {
23 pub fn new(horizontal: LengthPercentage, vertical: Option<LengthPercentage>) -> Self {
25 Self {
26 horizontal: horizontal.clone(),
27 vertical: vertical.unwrap_or(horizontal),
28 }
29 }
30
31 pub fn parser() -> TokenParser<BorderRadiusIndividual> {
33 let whitespace = TokenParser::<SimpleToken>::token(SimpleToken::Whitespace, Some("Whitespace"));
34
35 let first_value = length_percentage_parser();
37 let second_value_optional = whitespace
38 .clone()
39 .flat_map(|_| length_percentage_parser(), Some("second_value"))
40 .optional();
41
42 first_value.flat_map(
43 move |first| {
44 second_value_optional.clone().map(
45 move |second_opt| match second_opt {
46 Some(second) => BorderRadiusIndividual::new(first.clone(), Some(second)),
47 None => BorderRadiusIndividual::new(first.clone(), None),
48 },
49 Some("individual_radius"),
50 )
51 },
52 Some("individual_parser"),
53 )
54 }
55}
56
57impl Display for BorderRadiusIndividual {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 let horizontal = self.horizontal.to_string();
60 let vertical = self.vertical.to_string();
61
62 if horizontal == vertical {
63 write!(f, "{}", horizontal)
64 } else {
65 write!(f, "{} {}", horizontal, vertical)
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq)]
72pub struct BorderRadiusShorthand {
73 pub horizontal_top_left: LengthPercentage,
75 pub horizontal_top_right: LengthPercentage,
76 pub horizontal_bottom_right: LengthPercentage,
77 pub horizontal_bottom_left: LengthPercentage,
78
79 pub vertical_top_left: LengthPercentage,
81 pub vertical_top_right: LengthPercentage,
82 pub vertical_bottom_right: LengthPercentage,
83 pub vertical_bottom_left: LengthPercentage,
84}
85
86impl BorderRadiusShorthand {
87 #[allow(clippy::too_many_arguments)]
89 pub fn new(
90 horizontal_top_left: LengthPercentage,
91 horizontal_top_right: Option<LengthPercentage>,
92 horizontal_bottom_right: Option<LengthPercentage>,
93 horizontal_bottom_left: Option<LengthPercentage>,
94 vertical_top_left: Option<LengthPercentage>,
95 vertical_top_right: Option<LengthPercentage>,
96 vertical_bottom_right: Option<LengthPercentage>,
97 vertical_bottom_left: Option<LengthPercentage>,
98 ) -> Self {
99 let h_top_right = horizontal_top_right
101 .clone()
102 .unwrap_or(horizontal_top_left.clone());
103 let h_bottom_right = horizontal_bottom_right
104 .clone()
105 .unwrap_or(horizontal_top_left.clone());
106 let h_bottom_left = horizontal_bottom_left
107 .clone()
108 .unwrap_or(h_top_right.clone());
109
110 let v_top_left = vertical_top_left
111 .clone()
112 .unwrap_or(horizontal_top_left.clone());
113 let v_top_right = vertical_top_right.clone().unwrap_or(v_top_left.clone());
114 let v_bottom_right = vertical_bottom_right.clone().unwrap_or(v_top_left.clone());
115 let v_bottom_left = vertical_bottom_left.clone().unwrap_or(v_top_right.clone());
116
117 Self {
118 horizontal_top_left,
119 horizontal_top_right: h_top_right,
120 horizontal_bottom_right: h_bottom_right,
121 horizontal_bottom_left: h_bottom_left,
122 vertical_top_left: v_top_left,
123 vertical_top_right: v_top_right,
124 vertical_bottom_right: v_bottom_right,
125 vertical_bottom_left: v_bottom_left,
126 }
127 }
128
129 pub fn parser() -> TokenParser<BorderRadiusShorthand> {
130 let whitespace = TokenParser::<SimpleToken>::token(SimpleToken::Whitespace, Some("Whitespace"));
133 let slash = TokenParser::<SimpleToken>::token(SimpleToken::Delim('/'), Some("Slash"));
134
135 let space_separated_radii = {
137 let first_value = length_percentage_parser();
138 let remaining_values = TokenParser::<LengthPercentage>::zero_or_more(
139 whitespace
140 .clone()
141 .flat_map(|_| length_percentage_parser(), Some("next_value")),
142 );
143
144 first_value.flat_map(
145 move |first| {
146 let first_clone = first.clone();
147 remaining_values.clone().map(
148 move |rest| {
149 let mut all_values = vec![first_clone.clone()];
150 all_values.extend(rest);
151
152 let values = if all_values.len() > 4 {
154 all_values[..4].to_vec()
155 } else {
156 all_values
157 };
158
159 match values.len() {
161 1 => [
162 values[0].clone(),
163 values[0].clone(),
164 values[0].clone(),
165 values[0].clone(),
166 ],
167 2 => [
168 values[0].clone(),
169 values[1].clone(),
170 values[0].clone(),
171 values[1].clone(),
172 ],
173 3 => [
174 values[0].clone(),
175 values[1].clone(),
176 values[2].clone(),
177 values[1].clone(),
178 ],
179 4 => [
180 values[0].clone(),
181 values[1].clone(),
182 values[2].clone(),
183 values[3].clone(),
184 ],
185 _ => [
186 values[0].clone(),
187 values[0].clone(),
188 values[0].clone(),
189 values[0].clone(),
190 ],
191 }
192 },
193 Some("expand_radii"),
194 )
195 },
196 Some("space_separated"),
197 )
198 };
199
200 let slash_vertical = {
202 let whitespace_before_slash = whitespace.clone().optional();
203 let whitespace_after_slash = whitespace.clone().optional();
204 let slash_clone = slash.clone();
205 let radii_clone = space_separated_radii.clone();
206
207 whitespace_before_slash
208 .flat_map(move |_| slash_clone.clone(), Some("slash"))
209 .flat_map(
210 move |_| whitespace_after_slash.clone(),
211 Some("ws_after_slash"),
212 )
213 .flat_map(move |_| radii_clone.clone(), Some("vertical_radii"))
214 .optional()
215 };
216
217 space_separated_radii.clone().flat_map(
219 move |horizontal_radii| {
220 let h_radii = horizontal_radii.clone();
221 slash_vertical.clone().map(
222 move |vertical_opt| {
223 let [h_tl, h_tr, h_br, h_bl] = h_radii.clone();
224
225 match vertical_opt {
226 Some(vertical_radii) => {
227 let [v_tl, v_tr, v_br, v_bl] = vertical_radii;
228 BorderRadiusShorthand::new(
229 h_tl,
230 Some(h_tr),
231 Some(h_br),
232 Some(h_bl),
233 Some(v_tl),
234 Some(v_tr),
235 Some(v_br),
236 Some(v_bl),
237 )
238 },
239 None => {
240 BorderRadiusShorthand::new(
242 h_tl,
243 Some(h_tr),
244 Some(h_br),
245 Some(h_bl),
246 None,
247 None,
248 None,
249 None,
250 )
251 },
252 }
253 },
254 Some("with_vertical"),
255 )
256 },
257 Some("main_parser"),
258 )
259 }
260
261 fn to_shortest_string(&self) -> String {
263 let h_top_left = self.horizontal_top_left.to_string();
264 let h_top_right = self.horizontal_top_right.to_string();
265 let h_bottom_right = self.horizontal_bottom_right.to_string();
266 let h_bottom_left = self.horizontal_bottom_left.to_string();
267
268 let horizontal_str = if h_top_left == h_top_right
270 && h_top_right == h_bottom_right
271 && h_bottom_right == h_bottom_left
272 {
273 h_top_left.clone()
275 } else if h_top_left == h_bottom_right && h_top_right == h_bottom_left {
276 format!("{} {}", h_top_left, h_top_right)
278 } else if h_top_right == h_bottom_left {
279 format!("{} {} {}", h_top_left, h_top_right, h_bottom_right)
281 } else {
282 format!(
284 "{} {} {} {}",
285 h_top_left, h_top_right, h_bottom_right, h_bottom_left
286 )
287 };
288
289 let v_top_left = self.vertical_top_left.to_string();
290 let v_top_right = self.vertical_top_right.to_string();
291 let v_bottom_right = self.vertical_bottom_right.to_string();
292 let v_bottom_left = self.vertical_bottom_left.to_string();
293
294 let vertical_str = if v_top_left == v_top_right
296 && v_top_right == v_bottom_right
297 && v_bottom_right == v_bottom_left
298 {
299 v_top_left.clone()
301 } else if v_top_left == v_bottom_right && v_top_right == v_bottom_left {
302 format!("{} {}", v_top_left, v_top_right)
304 } else if v_top_right == v_bottom_left {
305 format!("{} {} {}", v_top_left, v_top_right, v_bottom_right)
307 } else {
308 format!(
310 "{} {} {} {}",
311 v_top_left, v_top_right, v_bottom_right, v_bottom_left
312 )
313 };
314
315 if horizontal_str == vertical_str {
317 horizontal_str
318 } else {
319 format!("{} / {}", horizontal_str, vertical_str)
320 }
321 }
322}
323
324impl Display for BorderRadiusShorthand {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 write!(f, "{}", self.to_shortest_string())
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::css_types::{Length, Percentage};
334
335 #[test]
336 fn test_border_radius_individual_creation() {
337 let length = LengthPercentage::Length(Length::new(10.0, "px".to_string()));
338 let radius = BorderRadiusIndividual::new(length.clone(), None);
339
340 assert_eq!(radius.horizontal, length);
341 assert_eq!(radius.vertical, length);
342 }
343
344 #[test]
345 fn test_border_radius_individual_different_values() {
346 let horizontal = LengthPercentage::Length(Length::new(10.0, "px".to_string()));
347 let vertical = LengthPercentage::Percentage(Percentage::new(50.0));
348 let radius = BorderRadiusIndividual::new(horizontal.clone(), Some(vertical.clone()));
349
350 assert_eq!(radius.horizontal, horizontal);
351 assert_eq!(radius.vertical, vertical);
352 }
353
354 #[test]
355 fn test_border_radius_individual_display() {
356 let length = LengthPercentage::Length(Length::new(5.0, "px".to_string()));
357 let radius = BorderRadiusIndividual::new(length, None);
358 assert_eq!(radius.to_string(), "5px");
359
360 let horizontal = LengthPercentage::Length(Length::new(10.0, "px".to_string()));
361 let vertical = LengthPercentage::Percentage(Percentage::new(20.0));
362 let radius2 = BorderRadiusIndividual::new(horizontal, Some(vertical));
363 assert_eq!(radius2.to_string(), "10px 20%");
364 }
365
366 #[test]
367 fn test_border_radius_shorthand_creation() {
368 let value = LengthPercentage::Length(Length::new(5.0, "px".to_string()));
369 let shorthand =
370 BorderRadiusShorthand::new(value.clone(), None, None, None, None, None, None, None);
371
372 assert_eq!(shorthand.horizontal_top_left, value);
374 assert_eq!(shorthand.horizontal_top_right, value);
375 assert_eq!(shorthand.horizontal_bottom_right, value);
376 assert_eq!(shorthand.horizontal_bottom_left, value);
377 assert_eq!(shorthand.vertical_top_left, value);
378 assert_eq!(shorthand.vertical_top_right, value);
379 assert_eq!(shorthand.vertical_bottom_right, value);
380 assert_eq!(shorthand.vertical_bottom_left, value);
381 }
382
383 #[test]
384 fn test_border_radius_shorthand_display_single_value() {
385 let value = LengthPercentage::Length(Length::new(5.0, "px".to_string()));
386 let shorthand = BorderRadiusShorthand::new(value, None, None, None, None, None, None, None);
387
388 assert_eq!(shorthand.to_string(), "5px");
389 }
390
391 #[test]
392 fn test_border_radius_shorthand_css_expansion() {
393 let top_left = LengthPercentage::Length(Length::new(1.0, "px".to_string()));
394 let top_right = LengthPercentage::Length(Length::new(2.0, "px".to_string()));
395
396 let shorthand = BorderRadiusShorthand::new(
397 top_left.clone(),
398 Some(top_right.clone()),
399 None, None, None,
402 None,
403 None,
404 None,
405 );
406
407 assert_eq!(shorthand.horizontal_top_left, top_left);
408 assert_eq!(shorthand.horizontal_top_right, top_right);
409 assert_eq!(shorthand.horizontal_bottom_right, top_left); assert_eq!(shorthand.horizontal_bottom_left, top_right); }
412
413 #[test]
414 fn test_border_radius_individual_parser_creation() {
415 let _parser = BorderRadiusIndividual::parser();
417 }
418
419 #[test]
420 fn test_border_radius_shorthand_parser_creation() {
421 let _parser = BorderRadiusShorthand::parser();
423 }
424
425 #[test]
426 fn test_border_radius_equality() {
427 let value1 = LengthPercentage::Length(Length::new(5.0, "px".to_string()));
428 let value2 = LengthPercentage::Length(Length::new(5.0, "px".to_string()));
429 let value3 = LengthPercentage::Length(Length::new(10.0, "px".to_string()));
430
431 let radius1 = BorderRadiusIndividual::new(value1.clone(), None);
432 let radius2 = BorderRadiusIndividual::new(value2, None);
433 let radius3 = BorderRadiusIndividual::new(value3, None);
434
435 assert_eq!(radius1, radius2);
436 assert_ne!(radius1, radius3);
437 }
438
439 #[test]
440 fn test_border_radius_common_css_values() {
441 let small = LengthPercentage::Length(Length::new(3.0, "px".to_string()));
443 let medium = LengthPercentage::Length(Length::new(6.0, "px".to_string()));
444 let large = LengthPercentage::Length(Length::new(12.0, "px".to_string()));
445 let circle = LengthPercentage::Percentage(Percentage::new(50.0));
446
447 let small_radius = BorderRadiusIndividual::new(small, None);
448 assert_eq!(small_radius.to_string(), "3px");
449
450 let medium_radius = BorderRadiusIndividual::new(medium, None);
451 assert_eq!(medium_radius.to_string(), "6px");
452
453 let large_radius = BorderRadiusIndividual::new(large, None);
454 assert_eq!(large_radius.to_string(), "12px");
455
456 let circle_radius = BorderRadiusIndividual::new(circle, None);
457 assert_eq!(circle_radius.to_string(), "50%");
458 }
459
460 #[test]
461 fn test_border_radius_elliptical() {
462 let horizontal = LengthPercentage::Length(Length::new(20.0, "px".to_string()));
464 let vertical = LengthPercentage::Length(Length::new(10.0, "px".to_string()));
465
466 let elliptical = BorderRadiusIndividual::new(horizontal, Some(vertical));
467 assert_eq!(elliptical.to_string(), "20px 10px");
468 }
469
470 #[test]
471 fn test_border_radius_mixed_units() {
472 let pixels = LengthPercentage::Length(Length::new(5.0, "px".to_string()));
474 let percentage = LengthPercentage::Percentage(Percentage::new(25.0));
475
476 let mixed = BorderRadiusIndividual::new(pixels, Some(percentage));
477 assert_eq!(mixed.to_string(), "5px 25%");
478 }
479
480 #[test]
481 fn test_border_radius_shorthand_slash_separated() {
482 let h_tl = LengthPercentage::Length(Length::new(10.0, "px".to_string()));
484 let h_tr = LengthPercentage::Length(Length::new(20.0, "px".to_string()));
485 let v_tl = LengthPercentage::Length(Length::new(5.0, "px".to_string()));
486 let v_tr = LengthPercentage::Length(Length::new(15.0, "px".to_string()));
487
488 let radius = BorderRadiusShorthand::new(
489 h_tl,
490 Some(h_tr),
491 None,
492 None,
493 Some(v_tl),
494 Some(v_tr),
495 None,
496 None,
497 );
498
499 let result = radius.to_string();
501 assert!(result.contains("/"));
502 assert!(result.contains("10px"));
503 assert!(result.contains("20px"));
504 assert!(result.contains("5px"));
505 assert!(result.contains("15px"));
506 }
507
508 #[test]
509 fn test_border_radius_shorthand_no_slash_when_same() {
510 let value = LengthPercentage::Length(Length::new(10.0, "px".to_string()));
512
513 let radius = BorderRadiusShorthand::new(
514 value.clone(),
515 None,
516 None,
517 None,
518 Some(value.clone()),
519 None,
520 None,
521 None,
522 );
523
524 let result = radius.to_string();
525 assert!(!result.contains("/"));
526 assert_eq!(result, "10px");
527 }
528
529 #[test]
530 fn test_border_radius_parser_creation() {
531 let _individual = BorderRadiusIndividual::parser();
533 let _shorthand = BorderRadiusShorthand::parser();
534 }
535}