Skip to main content

stylex_css_parser/properties/
box_shadow.rs

1/*!
2CSS Box Shadow property parsing.
3
4Handles box-shadow property syntax including offset, blur, spread, color, and inset values.
5Supports multiple shadow values separated by commas.
6*/
7
8use crate::{
9  css_types::{Color, Length},
10  token_parser::TokenParser,
11  token_types::SimpleToken,
12};
13use std::fmt::{self, Display};
14
15/// Individual box shadow value
16#[derive(Debug, Clone, PartialEq)]
17pub struct BoxShadow {
18  pub offset_x: Length,
19  pub offset_y: Length,
20  pub blur_radius: Length,
21  pub spread_radius: Length,
22  pub color: Color,
23  pub inset: bool,
24}
25
26impl BoxShadow {
27  /// Create a new BoxShadow
28  pub fn new(
29    offset_x: Length,
30    offset_y: Length,
31    blur_radius: Length,
32    spread_radius: Length,
33    color: Color,
34    inset: bool,
35  ) -> Self {
36    Self {
37      offset_x,
38      offset_y,
39      blur_radius,
40      spread_radius,
41      color,
42      inset,
43    }
44  }
45
46  /// Create a simple box shadow with default values for optional parameters
47  pub fn simple(
48    offset_x: Length,
49    offset_y: Length,
50    blur_radius: Option<Length>,
51    spread_radius: Option<Length>,
52    color: Color,
53    inset: bool,
54  ) -> Self {
55    Self::new(
56      offset_x,
57      offset_y,
58      blur_radius.unwrap_or(Length::new(0.0, "px".to_string())),
59      spread_radius.unwrap_or(Length::new(0.0, "px".to_string())),
60      color,
61      inset,
62    )
63  }
64
65  /// Parser for box shadow values
66  pub fn parser() -> TokenParser<BoxShadow> {
67    let whitespace = TokenParser::<SimpleToken>::token(SimpleToken::Whitespace, Some("Whitespace"));
68
69    // Parse outer shadow: offsetX offsetY [blurRadius] [spreadRadius] color
70    let outer_shadow = {
71      let offset_x = Length::parser();
72      let offset_y = whitespace
73        .clone()
74        .flat_map(|_| Length::parser(), Some("offset_y"));
75      let blur_radius = whitespace
76        .clone()
77        .flat_map(|_| Length::parser(), Some("blur_radius"))
78        .optional();
79      let spread_radius = whitespace
80        .clone()
81        .flat_map(|_| Length::parser(), Some("spread_radius"))
82        .optional();
83      let color = whitespace
84        .clone()
85        .flat_map(|_| Color::parse(), Some("color"));
86
87      offset_x
88        .flat_map(
89          move |x| {
90            let x_clone = x.clone();
91            offset_y
92              .clone()
93              .map(move |y| (x_clone.clone(), y), Some("with_y"))
94          },
95          Some("x_step"),
96        )
97        .flat_map(
98          move |(x, y)| {
99            let x_clone = x.clone();
100            let y_clone = y.clone();
101            blur_radius.clone().map(
102              move |blur| (x_clone.clone(), y_clone.clone(), blur),
103              Some("with_blur"),
104            )
105          },
106          Some("blur_step"),
107        )
108        .flat_map(
109          move |(x, y, blur)| {
110            let x_clone = x.clone();
111            let y_clone = y.clone();
112            let blur_clone = blur.clone();
113            spread_radius.clone().map(
114              move |spread| (x_clone.clone(), y_clone.clone(), blur_clone.clone(), spread),
115              Some("with_spread"),
116            )
117          },
118          Some("spread_step"),
119        )
120        .flat_map(
121          move |(x, y, blur, spread)| {
122            let x_clone = x.clone();
123            let y_clone = y.clone();
124            let blur_clone = blur.clone();
125            let spread_clone = spread.clone();
126            color.clone().map(
127              move |color| {
128                BoxShadow::new(
129                  x_clone.clone(),
130                  y_clone.clone(),
131                  blur_clone
132                    .clone()
133                    .unwrap_or_else(|| Length::new(0.0, "px".to_string())),
134                  spread_clone
135                    .clone()
136                    .unwrap_or_else(|| Length::new(0.0, "px".to_string())),
137                  color,
138                  false,
139                )
140              },
141              Some("create_shadow"),
142            )
143          },
144          Some("color_step"),
145        )
146    };
147
148    let inset_shadow = {
149      let inset_keyword =
150        TokenParser::<SimpleToken>::token(SimpleToken::Ident("inset".to_string()), Some("Ident"))
151          .where_fn(
152            |token| {
153              if let SimpleToken::Ident(value) = token {
154                value == "inset"
155              } else {
156                false
157              }
158            },
159            Some("inset_check"),
160          );
161
162      let whitespace_for_inset = whitespace.clone();
163      let inset_keyword_for_inset = inset_keyword.clone();
164
165      let shadow_then_inset = outer_shadow.clone().flat_map(
166        move |shadow| {
167          let shadow_clone = shadow.clone();
168          let whitespace_clone = whitespace_for_inset.clone();
169          let inset_clone = inset_keyword_for_inset.clone();
170
171          whitespace_clone.flat_map(
172            move |_| {
173              let shadow_for_map = shadow_clone.clone();
174              inset_clone.map(
175                move |_| {
176                  BoxShadow::new(
177                    shadow_for_map.offset_x.clone(),
178                    shadow_for_map.offset_y.clone(),
179                    shadow_for_map.blur_radius.clone(),
180                    shadow_for_map.spread_radius.clone(),
181                    shadow_for_map.color.clone(),
182                    true,
183                  )
184                },
185                Some("to_inset"),
186              )
187            },
188            Some("add_inset"),
189          )
190        },
191        Some("shadow_then_inset"),
192      );
193
194      let whitespace_for_prefix = whitespace.clone();
195      let outer_shadow_for_prefix = outer_shadow.clone();
196
197      let inset_then_shadow = inset_keyword.flat_map(
198        move |_| {
199          let whitespace_clone = whitespace_for_prefix.clone();
200          let shadow_parser = outer_shadow_for_prefix.clone();
201
202          whitespace_clone.flat_map(
203            move |_| {
204              shadow_parser.map(
205                move |shadow| {
206                  BoxShadow::new(
207                    shadow.offset_x.clone(),
208                    shadow.offset_y.clone(),
209                    shadow.blur_radius.clone(),
210                    shadow.spread_radius.clone(),
211                    shadow.color.clone(),
212                    true,
213                  )
214                },
215                Some("to_inset_prefix"),
216              )
217            },
218            Some("add_inset_prefix"),
219          )
220        },
221        Some("inset_then_shadow"),
222      );
223
224      TokenParser::one_of(vec![shadow_then_inset, inset_then_shadow])
225    };
226
227    TokenParser::one_of(vec![inset_shadow, outer_shadow])
228  }
229}
230
231impl Display for BoxShadow {
232  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233    let inset_str = if self.inset { "inset " } else { "" };
234
235    // Only include blur and spread if they're not zero
236    let blur_str = if self.blur_radius.value != 0.0 {
237      format!(" {}", self.blur_radius)
238    } else {
239      String::new()
240    };
241
242    let spread_str = if self.spread_radius.value != 0.0 {
243      format!(" {}", self.spread_radius)
244    } else {
245      String::new()
246    };
247
248    write!(
249      f,
250      "{}{} {}{}{} {}",
251      inset_str, self.offset_x, self.offset_y, blur_str, spread_str, self.color
252    )
253  }
254}
255
256/// List of box shadows (comma-separated)
257#[derive(Debug, Clone, PartialEq)]
258pub struct BoxShadowList {
259  pub shadows: Vec<BoxShadow>,
260}
261
262impl BoxShadowList {
263  /// Create a new BoxShadowList
264  pub fn new(shadows: Vec<BoxShadow>) -> Self {
265    Self { shadows }
266  }
267
268  /// Parser for box shadow list (comma-separated shadows)
269  pub fn parser() -> TokenParser<BoxShadowList> {
270    let comma = TokenParser::<SimpleToken>::token(SimpleToken::Comma, Some("Comma"));
271    let whitespace = TokenParser::<SimpleToken>::token(SimpleToken::Whitespace, Some("Whitespace"));
272
273    // Parse "none" keyword
274    let none_parser =
275      TokenParser::<SimpleToken>::token(SimpleToken::Ident("none".to_string()), Some("none_ident"))
276        .where_fn(
277          |token| {
278            if let SimpleToken::Ident(value) = token {
279              value == "none"
280            } else {
281              false
282            }
283          },
284          Some("none_check"),
285        )
286        .map(
287          |_| BoxShadowList::new(Vec::new()),
288          Some("empty_shadow_list"),
289        );
290
291    // Parse comma with optional surrounding whitespace
292    let comma_separator =
293      comma.surrounded_by(whitespace.clone().optional(), Some(whitespace.optional()));
294
295    // Parse one or more shadows separated by commas
296    let shadow_list_parser =
297      TokenParser::one_or_more_separated_by(BoxShadow::parser(), comma_separator)
298        .map(BoxShadowList::new, Some("shadow_list"));
299
300    // Try "none" first, then shadow list
301    TokenParser::one_of(vec![none_parser, shadow_list_parser])
302  }
303}
304
305impl Display for BoxShadowList {
306  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307    if self.shadows.is_empty() {
308      write!(f, "none")
309    } else {
310      let shadow_strings: Vec<String> = self
311        .shadows
312        .iter()
313        .map(|shadow| shadow.to_string())
314        .collect();
315      write!(f, "{}", shadow_strings.join(", "))
316    }
317  }
318}
319
320#[cfg(test)]
321mod tests {
322  use super::*;
323  use crate::css_types::{HashColor, NamedColor};
324
325  #[test]
326  fn test_box_shadow_creation() {
327    let offset_x = Length::new(2.0, "px".to_string());
328    let offset_y = Length::new(4.0, "px".to_string());
329    let blur = Length::new(6.0, "px".to_string());
330    let spread = Length::new(0.0, "px".to_string());
331    let color = Color::Named(NamedColor::new("red".to_string()));
332
333    let shadow = BoxShadow::new(
334      offset_x.clone(),
335      offset_y.clone(),
336      blur.clone(),
337      spread.clone(),
338      color.clone(),
339      false,
340    );
341
342    assert_eq!(shadow.offset_x, offset_x);
343    assert_eq!(shadow.offset_y, offset_y);
344    assert_eq!(shadow.blur_radius, blur);
345    assert_eq!(shadow.spread_radius, spread);
346    assert_eq!(shadow.color, color);
347    assert!(!shadow.inset);
348  }
349
350  #[test]
351  fn test_box_shadow_simple_constructor() {
352    let offset_x = Length::new(1.0, "px".to_string());
353    let offset_y = Length::new(2.0, "px".to_string());
354    let color = Color::Named(NamedColor::new("black".to_string()));
355
356    let shadow = BoxShadow::simple(
357      offset_x.clone(),
358      offset_y.clone(),
359      None,
360      None,
361      color.clone(),
362      false,
363    );
364
365    assert_eq!(shadow.offset_x, offset_x);
366    assert_eq!(shadow.offset_y, offset_y);
367    assert_eq!(shadow.blur_radius.value, 0.0);
368    assert_eq!(shadow.spread_radius.value, 0.0);
369    assert_eq!(shadow.color, color);
370    assert!(!shadow.inset);
371  }
372
373  #[test]
374  fn test_box_shadow_inset() {
375    let offset_x = Length::new(1.0, "px".to_string());
376    let offset_y = Length::new(1.0, "px".to_string());
377    let blur = Length::new(3.0, "px".to_string());
378    let spread = Length::new(0.0, "px".to_string());
379    let color = Color::Hash(HashColor::new("#000000".to_string()));
380
381    let inset_shadow = BoxShadow::new(offset_x, offset_y, blur, spread, color, true);
382
383    assert!(inset_shadow.inset);
384  }
385
386  #[test]
387  fn test_box_shadow_display() {
388    let offset_x = Length::new(2.0, "px".to_string());
389    let offset_y = Length::new(4.0, "px".to_string());
390    let blur = Length::new(6.0, "px".to_string());
391    let spread = Length::new(2.0, "px".to_string());
392    let color = Color::Named(NamedColor::new("red".to_string()));
393
394    let shadow = BoxShadow::new(offset_x, offset_y, blur, spread, color, false);
395    assert_eq!(shadow.to_string(), "2px 4px 6px 2px red");
396
397    let inset_shadow = BoxShadow::new(
398      Length::new(1.0, "px".to_string()),
399      Length::new(1.0, "px".to_string()),
400      Length::new(2.0, "px".to_string()),
401      Length::new(0.0, "px".to_string()),
402      Color::Named(NamedColor::new("blue".to_string())),
403      true,
404    );
405    assert_eq!(inset_shadow.to_string(), "inset 1px 1px 2px blue");
406  }
407
408  #[test]
409  fn test_box_shadow_display_zero_values() {
410    let shadow = BoxShadow::new(
411      Length::new(1.0, "px".to_string()),
412      Length::new(2.0, "px".to_string()),
413      Length::new(0.0, "px".to_string()), // Zero blur
414      Length::new(0.0, "px".to_string()), // Zero spread
415      Color::Named(NamedColor::new("black".to_string())),
416      false,
417    );
418
419    // Should omit zero blur and spread values
420    assert_eq!(shadow.to_string(), "1px 2px black");
421  }
422
423  #[test]
424  fn test_box_shadow_list_creation() {
425    let shadow1 = BoxShadow::simple(
426      Length::new(1.0, "px".to_string()),
427      Length::new(1.0, "px".to_string()),
428      None,
429      None,
430      Color::Named(NamedColor::new("red".to_string())),
431      false,
432    );
433
434    let shadow2 = BoxShadow::simple(
435      Length::new(2.0, "px".to_string()),
436      Length::new(2.0, "px".to_string()),
437      Some(Length::new(4.0, "px".to_string())),
438      None,
439      Color::Named(NamedColor::new("blue".to_string())),
440      true,
441    );
442
443    let shadow_list = BoxShadowList::new(vec![shadow1, shadow2]);
444    assert_eq!(shadow_list.shadows.len(), 2);
445  }
446
447  #[test]
448  fn test_box_shadow_list_display() {
449    let shadow1 = BoxShadow::new(
450      Length::new(1.0, "px".to_string()),
451      Length::new(1.0, "px".to_string()),
452      Length::new(0.0, "px".to_string()),
453      Length::new(0.0, "px".to_string()),
454      Color::Named(NamedColor::new("red".to_string())),
455      false,
456    );
457
458    let shadow2 = BoxShadow::new(
459      Length::new(2.0, "px".to_string()),
460      Length::new(2.0, "px".to_string()),
461      Length::new(4.0, "px".to_string()),
462      Length::new(0.0, "px".to_string()),
463      Color::Named(NamedColor::new("blue".to_string())),
464      true,
465    );
466
467    let shadow_list = BoxShadowList::new(vec![shadow1, shadow2]);
468    assert_eq!(
469      shadow_list.to_string(),
470      "1px 1px red, inset 2px 2px 4px blue"
471    );
472  }
473
474  #[test]
475  fn test_box_shadow_parser_creation() {
476    // Basic test that parsers can be created
477    let _shadow_parser = BoxShadow::parser();
478    let _list_parser = BoxShadowList::parser();
479  }
480
481  #[test]
482  fn test_box_shadow_equality() {
483    let shadow1 = BoxShadow::simple(
484      Length::new(1.0, "px".to_string()),
485      Length::new(1.0, "px".to_string()),
486      None,
487      None,
488      Color::Named(NamedColor::new("red".to_string())),
489      false,
490    );
491
492    let shadow2 = BoxShadow::simple(
493      Length::new(1.0, "px".to_string()),
494      Length::new(1.0, "px".to_string()),
495      None,
496      None,
497      Color::Named(NamedColor::new("red".to_string())),
498      false,
499    );
500
501    let shadow3 = BoxShadow::simple(
502      Length::new(2.0, "px".to_string()),
503      Length::new(2.0, "px".to_string()),
504      None,
505      None,
506      Color::Named(NamedColor::new("red".to_string())),
507      false,
508    );
509
510    assert_eq!(shadow1, shadow2);
511    assert_ne!(shadow1, shadow3);
512  }
513
514  #[test]
515  fn test_box_shadow_common_values() {
516    // Test common box-shadow patterns
517
518    // Simple drop shadow
519    let drop_shadow = BoxShadow::simple(
520      Length::new(0.0, "px".to_string()),
521      Length::new(2.0, "px".to_string()),
522      Some(Length::new(4.0, "px".to_string())),
523      None,
524      Color::Hash(HashColor::new("#00000026".to_string())), // 15% opacity black
525      false,
526    );
527    assert!(!drop_shadow.inset);
528
529    // Inner shadow
530    let inner_shadow = BoxShadow::simple(
531      Length::new(0.0, "px".to_string()),
532      Length::new(1.0, "px".to_string()),
533      Some(Length::new(2.0, "px".to_string())),
534      None,
535      Color::Hash(HashColor::new("#0000001a".to_string())), // 10% opacity black
536      true,
537    );
538    assert!(inner_shadow.inset);
539
540    // No shadow (all zero)
541    let no_shadow = BoxShadow::simple(
542      Length::new(0.0, "px".to_string()),
543      Length::new(0.0, "px".to_string()),
544      None,
545      None,
546      Color::Named(NamedColor::new("transparent".to_string())),
547      false,
548    );
549    assert_eq!(no_shadow.offset_x.value, 0.0);
550    assert_eq!(no_shadow.offset_y.value, 0.0);
551  }
552
553  #[test]
554  fn test_box_shadow_with_spread() {
555    let shadow_with_spread = BoxShadow::new(
556      Length::new(0.0, "px".to_string()),
557      Length::new(0.0, "px".to_string()),
558      Length::new(10.0, "px".to_string()),
559      Length::new(5.0, "px".to_string()), // Positive spread
560      Color::Named(NamedColor::new("black".to_string())),
561      false,
562    );
563
564    assert_eq!(shadow_with_spread.spread_radius.value, 5.0);
565    assert_eq!(shadow_with_spread.to_string(), "0px 0px 10px 5px black");
566  }
567}