1use super::value_matchers::{is_matching_color, is_matching_length, is_matching_var};
3
4use std::{borrow::Cow, fmt};
5
6const SHADOW_KEYWORDS: [&str; 5] = ["none", "inherit", "initial", "revert", "unset"];
7
8#[derive(Debug, PartialEq)]
9enum Shadow<'a> {
10 Raw([&'a str; 6]),
11 Keyword(&'a str),
12 Variable(&'a str),
13 Shorthand1 {
14 is_inset: bool,
15 offset_x: &'a str,
16 offset_y: &'a str,
17 color: Cow<'a, str>,
18 },
19 Shorthand2 {
20 is_inset: bool,
21 offset_x: &'a str,
22 offset_y: &'a str,
23 blur_radius: &'a str,
24 color: Cow<'a, str>,
25 },
26 Full {
27 is_inset: bool,
28 offset_x: &'a str,
29 offset_y: &'a str,
30 blur_radius: &'a str,
31 spread_radius: &'a str,
32 color: Cow<'a, str>,
33 },
34}
35
36impl Shadow<'_> {
37 fn new_raw() -> Self {
38 Self::Raw([""; 6])
39 }
40
41 fn parse(&self) -> Option<Self> {
43 if let Shadow::Raw(shadow) = self {
44 let (is_inset, shadow) = if shadow[0].is_empty() {
46 (false, &shadow[..])
47 } else if shadow[0] == "inset" {
48 (true, &shadow[1..])
49 } else {
50 (false, &shadow[..])
51 };
52
53 if shadow.is_empty()
55 && ((is_inset && shadow.len() > 6) || (!is_inset && shadow.len() > 5))
56 {
57 return None;
58 }
59
60 let len = shadow.iter().position(|p| p.is_empty()).unwrap_or(5);
61 if len == 1 {
62 if SHADOW_KEYWORDS.contains(&shadow[0]) {
64 Some(Shadow::Keyword(shadow[0]))
65 } else if is_matching_var(shadow[0]) {
66 Some(Shadow::Variable(shadow[0]))
67 } else {
68 None
69 }
70 } else if len == 3 {
71 if is_matching_length(shadow[0])
73 && is_matching_length(shadow[1])
74 && is_matching_color(shadow[2])
75 {
76 Some(Shadow::Shorthand1 {
77 is_inset,
78 offset_x: shadow[0],
79 offset_y: shadow[1],
80 color: Cow::Borrowed(shadow[2]),
81 })
82 } else {
83 None
84 }
85 } else if len == 4 {
86 if is_matching_length(shadow[0])
88 && is_matching_length(shadow[1])
89 && is_matching_length(shadow[2])
90 && is_matching_color(shadow[3])
91 {
92 Some(Shadow::Shorthand2 {
93 is_inset,
94 offset_x: shadow[0],
95 offset_y: shadow[1],
96 blur_radius: shadow[2],
97 color: Cow::Borrowed(shadow[3]),
98 })
99 } else {
100 None
101 }
102 } else if len == 5 {
103 if is_matching_length(shadow[0])
105 && is_matching_length(shadow[1])
106 && is_matching_length(shadow[2])
107 && is_matching_length(shadow[3])
108 && is_matching_color(shadow[4])
109 {
110 Some(Shadow::Full {
111 is_inset,
112 offset_x: shadow[0],
113 offset_y: shadow[1],
114 blur_radius: shadow[2],
115 spread_radius: shadow[3],
116 color: Cow::Borrowed(shadow[4]),
117 })
118 } else {
119 None
120 }
121 } else {
122 None
123 }
124 } else {
125 None
126 }
127 }
128}
129
130impl fmt::Display for Shadow<'_> {
131 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
132 match self {
133 Shadow::Raw(s) => write!(f, "{}", s.join(" ")),
134 Shadow::Keyword(keyword) => write!(f, "{keyword}"),
135 Shadow::Variable(variable) => write!(f, "{variable}"),
136 Shadow::Shorthand1 {
137 is_inset,
138 offset_x,
139 offset_y,
140 color,
141 } => write!(
142 f,
143 "{}{offset_x} {offset_y} {color}",
144 if *is_inset { "inset " } else { "" }
145 ),
146 Shadow::Shorthand2 {
147 is_inset,
148 offset_x,
149 offset_y,
150 blur_radius,
151 color,
152 } => write!(
153 f,
154 "{}{offset_x} {offset_y} {blur_radius} {color}",
155 if *is_inset { "inset " } else { "" }
156 ),
157 Shadow::Full {
158 is_inset,
159 offset_x,
160 offset_y,
161 blur_radius,
162 spread_radius,
163 color,
164 } => write!(
165 f,
166 "{}{offset_x} {offset_y} {blur_radius} {spread_radius} {color}",
167 if *is_inset { "inset " } else { "" }
168 ),
169 }
170 }
171}
172
173#[derive(Debug, PartialEq)]
175pub struct ShadowList<'a>(Vec<Shadow<'a>>);
176
177impl<'a> ShadowList<'a> {
178 pub fn parse(value: &'a str) -> Option<Self> {
188 let mut parenthesis_level = 0;
189 let mut last_index = 0;
190 let mut shadows = vec![Shadow::new_raw()];
191
192 for (ch_index, ch) in value.char_indices() {
193 match ch {
194 '(' => {
195 parenthesis_level += 1;
196 }
197 ')' => {
198 parenthesis_level -= 1;
199 }
200 ' ' if parenthesis_level == 0 => {
201 let Shadow::Raw(shadow) = shadows.last_mut()? else {
202 return None;
204 };
205
206 if !value[last_index..ch_index].is_empty() {
207 let index = shadow.iter().position(|p| p.is_empty()).unwrap_or(5);
209
210 shadow[index] = &value[last_index..ch_index];
212 }
213
214 last_index = ch_index + 1;
216 }
217 ',' if parenthesis_level == 0 => {
218 let Shadow::Raw(shadow) = shadows.last_mut()? else {
220 return None;
222 };
223
224 let index = shadow.iter().position(|p| p.is_empty()).unwrap_or(5);
226
227 shadow[index] = &value[last_index..ch_index];
229
230 if !shadow.iter().all(|p| p.is_empty()) {
232 let parsed_shadow = shadows.last()?.parse()?;
234 *shadows.last_mut()? = parsed_shadow;
235
236 shadows.push(Shadow::new_raw());
238 }
239
240 last_index = ch_index + 1;
242 }
243 _ => (),
244 }
245 }
246
247 if value.is_empty() {
248 return None;
249 }
250
251 if last_index != value.len() - 1 {
253 let Shadow::Raw(shadow) = shadows.last_mut()? else {
255 return None;
256 };
257 let index = shadow.iter().position(|p| p.is_empty()).unwrap_or(5);
258
259 shadow[index] = &value[last_index..value.len()];
261
262 let parsed_shadow = shadows.last()?.parse()?;
264 *shadows.last_mut()? = parsed_shadow;
265 }
266
267 Some(Self(shadows))
268 }
269
270 pub fn replace_all_colors(&mut self, new_color: &'a str) {
274 self.0.iter_mut().for_each(|shadow| match shadow {
275 Shadow::Shorthand1 { ref mut color, .. }
276 | Shadow::Shorthand2 { ref mut color, .. }
277 | Shadow::Full { ref mut color, .. } => *color = Cow::Owned(new_color.replace("{}", color)),
278 _ => (),
279 });
280 }
281}
282
283impl fmt::Display for ShadowList<'_> {
284 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
285 for (i, v) in self.0.iter().enumerate() {
286 write!(f, "{}{}", v, if i == self.0.len() - 1 { "" } else { "," })?;
287 }
288
289 Ok(())
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn parse_shadow_test() {
299 let shadow = "20px 35px 60px -15px rgba(0,0,0,0.3),0 72px rgba(0,2,42,0.2),inset 23px 42em 42px rgba(255,0,0,1)";
300 let result = ShadowList::parse(shadow).unwrap();
301 assert_eq!(
302 result,
303 ShadowList(vec![
304 Shadow::Full {
305 is_inset: false,
306 offset_x: "20px",
307 offset_y: "35px",
308 blur_radius: "60px",
309 spread_radius: "-15px",
310 color: Cow::Borrowed("rgba(0,0,0,0.3)"),
311 },
312 Shadow::Shorthand1 {
313 is_inset: false,
314 offset_x: "0",
315 offset_y: "72px",
316 color: Cow::Borrowed("rgba(0,2,42,0.2)"),
317 },
318 Shadow::Shorthand2 {
319 is_inset: true,
320 offset_x: "23px",
321 offset_y: "42em",
322 blur_radius: "42px",
323 color: Cow::Borrowed("rgba(255,0,0,1)"),
324 }
325 ])
326 );
327
328 assert_eq!(
329 ShadowList::parse("var(--a, 0 0 1px rgb(0, 0, 0)),1px 2px 3rem rgb(0, 0, 0)").unwrap(),
330 ShadowList(vec![
331 Shadow::Variable("var(--a, 0 0 1px rgb(0, 0, 0))"),
332 Shadow::Shorthand2 {
333 is_inset: false,
334 offset_x: "1px",
335 offset_y: "2px",
336 blur_radius: "3rem",
337 color: Cow::Borrowed("rgb(0, 0, 0)"),
338 },
339 ])
340 );
341
342 assert_eq!(
343 ShadowList::parse("none").unwrap(),
344 ShadowList(vec![Shadow::Keyword("none")])
345 );
346
347 assert_eq!(
348 ShadowList::parse("inset 0 5px 90px 40px rgba(0,0,0,0.2)").unwrap(),
349 ShadowList(vec![Shadow::Full {
350 is_inset: true,
351 offset_x: "0",
352 offset_y: "5px",
353 blur_radius: "90px",
354 spread_radius: "40px",
355 color: Cow::Borrowed("rgba(0,0,0,0.2)")
356 }])
357 );
358 }
359
360 #[test]
361 fn format_shadow() {
362 let shadow = "20px 35px 60px -15px rgba(0,0,0,0.3),0 72px rgba(0,2,42,0.2),inset 23px 42em rgba(255,0,0,1)";
363 let result = ShadowList::parse(shadow).unwrap();
364 assert_eq!(&result.to_string(), shadow);
365 }
366
367 #[test]
368 fn replace_all_colors() {
369 let shadow = "20px 35px 60px -15px rgba(0,0,0,0.3),0 72px rgba(0,2,42,0.2),inset 23px 42em rgba(255,0,0,1)";
370 let mut result = ShadowList::parse(shadow).unwrap();
371 result.replace_all_colors("var(--en-shadow, {})");
372 assert_eq!(&result.to_string(), "20px 35px 60px -15px var(--en-shadow, rgba(0,0,0,0.3)),0 72px var(--en-shadow, rgba(0,2,42,0.2)),inset 23px 42em var(--en-shadow, rgba(255,0,0,1))");
373 }
374
375 #[test]
376 fn empty_shadow_should_not_panic() {
377 let shadow = "";
378 let None = ShadowList::parse(shadow) else {
379 unreachable!("an empty shadow should not be parsed correctly");
380 };
381 }
382}