1use std::fmt;
30use crate::helpers::is_non_ascii;
31use nom::{
32 bytes::complete::{tag, is_not, take_while1, take_while},
33 character::complete::{char, multispace0},
34 combinator::{recognize, map, opt},
35 sequence::{delimited, preceded, separated_pair, pair},
36 IResult,
37 Parser,
38};
39
40#[derive(Debug, Clone, PartialEq)]
41pub struct CSSDeclaration {
42 pub name: String,
43 pub value: String,
44 pub important: bool,
45}
46
47impl CSSDeclaration {
48 fn parse_identifier(input: &str) -> IResult<&str, String> {
49 map(
50 recognize(
51 pair(
52 take_while1(|c: char| c.is_alphabetic() || c == '_' || c == '-' || is_non_ascii(c)),
54
55 take_while(|c: char| c.is_alphanumeric() || c == '-' || c == '_' || is_non_ascii(c)),
57 )
58 ),
59 |s: &str| s.to_string()
60 ).parse(input)
61 }
62
63 fn parse_value(input: &str) -> IResult<&str, (String, bool)> {
64 map(
65 pair(
66 map(is_not(";{}!"), |s: &str| s.trim().to_string()),
68
69 opt(preceded(
71 multispace0,
72 preceded(tag("!"), preceded(multispace0, tag("important")))
73 ))
74 ),
75 |(value, important)| (value, important.is_some())
76 ).parse(input)
77 }
78
79 fn parse_declaration(input: &str) -> IResult<&str, (String, (String, bool))> {
80 separated_pair(
81 preceded(multispace0, Self::parse_identifier),
82 delimited(multispace0, char(':'), multispace0),
83 Self::parse_value,
84 ).parse(input)
85 }
86
87 pub(crate) fn parse(input: &str) -> IResult<&str, CSSDeclaration> {
88 let (input, (name, (value, important))) = Self::parse_declaration(input)?;
89
90 Ok((input, CSSDeclaration { name, value, important }))
91 }
92
93 pub fn from_string(input: &str) -> Result<CSSDeclaration, String> {
94 let (_, decl) = Self::parse(input)
95 .map_err(|_| "Failed to parse CSS declaration".to_string())?;
96
97 Ok(decl)
98 }
99
100 pub fn new(name: &str, value: &str, important: Option<bool>) -> Self {
101 CSSDeclaration {
102 name: name.to_string(),
103 value: value.to_string(),
104 important: important.unwrap_or(false),
105 }
106 }
107
108}
109
110impl fmt::Display for CSSDeclaration {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 if self.important {
113 write!(f, "{}: {} !important;", self.name, self.value)
114 } else {
115 write!(f, "{}: {};", self.name, self.value)
116 }
117 }
118}
119
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn parse_identifier_simple_letter_identifier() {
127 let result = CSSDeclaration::parse_identifier("color");
128 assert!(result.is_ok());
129 let (remaining, identifier) = result.unwrap();
130 assert_eq!(identifier, "color");
131 assert_eq!(remaining, "");
132 }
133
134 #[test]
135 fn parse_identifier_hyphenated_identifier() {
136 let result = CSSDeclaration::parse_identifier("background-color");
137 assert!(result.is_ok());
138 let (remaining, identifier) = result.unwrap();
139 assert_eq!(identifier, "background-color");
140 assert_eq!(remaining, "");
141 }
142
143 #[test]
144 fn parse_identifier_vendor_prefixed_identifier() {
145 let result = CSSDeclaration::parse_identifier("-webkit-transform");
146 assert!(result.is_ok());
147 let (remaining, identifier) = result.unwrap();
148 assert_eq!(identifier, "-webkit-transform");
149 assert_eq!(remaining, "");
150 }
151
152 #[test]
153 fn parse_identifier_stops_at_colon() {
154 let result = CSSDeclaration::parse_identifier("color: red");
155 assert!(result.is_ok());
156 let (remaining, identifier) = result.unwrap();
157 assert_eq!(identifier, "color");
158 assert_eq!(remaining, ": red");
159 }
160
161 #[test]
162 fn parse_identifier_stops_at_semicolon() {
163 let result = CSSDeclaration::parse_identifier("color;");
164 assert!(result.is_ok());
165 let (remaining, identifier) = result.unwrap();
166 assert_eq!(identifier, "color");
167 assert_eq!(remaining, ";");
168 }
169
170 #[test]
171 fn parse_identifier_fails_on_empty_input() {
172 let result = CSSDeclaration::parse_identifier("");
173 assert!(result.is_err());
174 }
175
176 #[test]
177 fn parse_identifier_fails_starting_with_number() {
178 let result = CSSDeclaration::parse_identifier("123invalid");
179 assert!(result.is_err());
180 }
181
182 #[test]
183 fn parse_identifier_fails_starting_with_special_char() {
184 let result = CSSDeclaration::parse_identifier("@invalid");
185 assert!(result.is_err());
186 }
187
188 #[test]
189 fn parse_value_simple_value() {
190 let result = CSSDeclaration::parse_value("red");
191 assert!(result.is_ok());
192 let (remaining, (value, important)) = result.unwrap();
193 assert_eq!(value, "red");
194 assert_eq!(important, false);
195 assert_eq!(remaining, "");
196 }
197
198 #[test]
199 fn parse_value_with_whitespace() {
200 let result = CSSDeclaration::parse_value(" red ");
201 assert!(result.is_ok());
202 let (remaining, (value, important)) = result.unwrap();
203 assert_eq!(value, "red");
204 assert_eq!(important, false);
205 assert_eq!(remaining, "");
206 }
207
208 #[test]
209 fn parse_value_multiple_words() {
210 let result = CSSDeclaration::parse_value("1px solid red");
211 assert!(result.is_ok());
212 let (remaining, (value, important)) = result.unwrap();
213 assert_eq!(value, "1px solid red");
214 assert_eq!(important, false);
215 assert_eq!(remaining, "");
216 }
217
218 #[test]
219 fn parse_value_with_important() {
220 let result = CSSDeclaration::parse_value("red !important");
221 assert!(result.is_ok());
222 let (remaining, (value, important)) = result.unwrap();
223 assert_eq!(value, "red");
224 assert_eq!(important, true);
225 assert_eq!(remaining, "");
226 }
227
228 #[test]
229 fn parse_value_with_important_no_space() {
230 let result = CSSDeclaration::parse_value("red!important");
231 assert!(result.is_ok());
232 let (remaining, (value, important)) = result.unwrap();
233 assert_eq!(value, "red");
234 assert_eq!(important, true);
235 assert_eq!(remaining, "");
236 }
237
238 #[test]
239 fn parse_value_with_important_extra_spaces() {
240 let result = CSSDeclaration::parse_value("red ! important");
241 assert!(result.is_ok());
242 let (remaining, (value, important)) = result.unwrap();
243 assert_eq!(value, "red");
244 assert_eq!(important, true);
245 assert_eq!(remaining, "");
246 }
247
248 #[test]
249 fn parse_value_complex_with_important() {
250 let result = CSSDeclaration::parse_value("1px solid rgba(255, 0, 0, 0.5) !important");
251 assert!(result.is_ok());
252 let (remaining, (value, important)) = result.unwrap();
253 assert_eq!(value, "1px solid rgba(255, 0, 0, 0.5)");
254 assert_eq!(important, true);
255 assert_eq!(remaining, "");
256 }
257
258 #[test]
259 fn parse_value_stops_at_semicolon() {
260 let result = CSSDeclaration::parse_value("red; color: blue");
261 assert!(result.is_ok());
262 let (remaining, (value, important)) = result.unwrap();
263 assert_eq!(value, "red");
264 assert_eq!(important, false);
265 assert_eq!(remaining, "; color: blue");
266 }
267
268 #[test]
269 fn parse_value_stops_at_closing_brace() {
270 let result = CSSDeclaration::parse_value("red}");
271 assert!(result.is_ok());
272 let (remaining, (value, important)) = result.unwrap();
273 assert_eq!(value, "red");
274 assert_eq!(important, false);
275 assert_eq!(remaining, "}");
276 }
277
278 #[test]
279 fn parse_value_stops_at_opening_brace() {
280 let result = CSSDeclaration::parse_value("red{");
281 assert!(result.is_ok());
282 let (remaining, (value, important)) = result.unwrap();
283 assert_eq!(value, "red");
284 assert_eq!(important, false);
285 assert_eq!(remaining, "{");
286 }
287
288 #[test]
289 fn parse_value_numeric_value() {
290 let result = CSSDeclaration::parse_value("10px");
291 assert!(result.is_ok());
292 let (remaining, (value, important)) = result.unwrap();
293 assert_eq!(value, "10px");
294 assert_eq!(important, false);
295 assert_eq!(remaining, "");
296 }
297
298 #[test]
299 fn parse_value_hex_color() {
300 let result = CSSDeclaration::parse_value("#ff0000");
301 assert!(result.is_ok());
302 let (remaining, (value, important)) = result.unwrap();
303 assert_eq!(value, "#ff0000");
304 assert_eq!(important, false);
305 assert_eq!(remaining, "");
306 }
307
308 #[test]
309 fn parse_value_url() {
310 let result = CSSDeclaration::parse_value("url('image.png')");
311 assert!(result.is_ok());
312 let (remaining, (value, important)) = result.unwrap();
313 assert_eq!(value, "url('image.png')");
314 assert_eq!(important, false);
315 assert_eq!(remaining, "");
316 }
317
318 #[test]
319 fn parse_value_calc_expression() {
320 let result = CSSDeclaration::parse_value("calc(100% - 20px)");
321 assert!(result.is_ok());
322 let (remaining, (value, important)) = result.unwrap();
323 assert_eq!(value, "calc(100% - 20px)");
324 assert_eq!(important, false);
325 assert_eq!(remaining, "");
326 }
327
328 #[test]
329 fn parse_value_fails_on_empty_input() {
330 let result = CSSDeclaration::parse_value("");
331 assert!(result.is_err());
332 }
333
334 #[test]
335 fn parse_value_whitespace_with_important() {
336 let result = CSSDeclaration::parse_value(" 1px solid red !important ");
337 assert!(result.is_ok());
338 let (remaining, (value, important)) = result.unwrap();
339 assert_eq!(value, "1px solid red");
340 assert_eq!(important, true);
341 assert_eq!(remaining, " ");
342 }
343
344 #[test]
345 fn parse_declaration_simple() {
346 let result = CSSDeclaration::parse_declaration("color: red");
347 assert!(result.is_ok());
348 let (remaining, (name, (value, important))) = result.unwrap();
349 assert_eq!(name, "color");
350 assert_eq!(value, "red");
351 assert_eq!(important, false);
352 assert_eq!(remaining, "");
353 }
354
355 #[test]
356 fn parse_declaration_with_whitespace() {
357 let result = CSSDeclaration::parse_declaration(" color : red ");
358 assert!(result.is_ok());
359 let (remaining, (name, (value, important))) = result.unwrap();
360 assert_eq!(name, "color");
361 assert_eq!(value, "red");
362 assert_eq!(important, false);
363 assert_eq!(remaining, "");
364 }
365
366 #[test]
367 fn parse_declaration_hyphenated_property() {
368 let result = CSSDeclaration::parse_declaration("background-color: blue");
369 assert!(result.is_ok());
370 let (remaining, (name, (value, important))) = result.unwrap();
371 assert_eq!(name, "background-color");
372 assert_eq!(value, "blue");
373 assert_eq!(important, false);
374 assert_eq!(remaining, "");
375 }
376
377 #[test]
378 fn parse_declaration_vendor_prefix() {
379 let result = CSSDeclaration::parse_declaration("-webkit-transform: rotate(45deg)");
380 assert!(result.is_ok());
381 let (remaining, (name, (value, important))) = result.unwrap();
382 assert_eq!(name, "-webkit-transform");
383 assert_eq!(value, "rotate(45deg)");
384 assert_eq!(important, false);
385 assert_eq!(remaining, "");
386 }
387
388 #[test]
389 fn parse_declaration_with_important() {
390 let result = CSSDeclaration::parse_declaration("color: red !important");
391 assert!(result.is_ok());
392 let (remaining, (name, (value, important))) = result.unwrap();
393 assert_eq!(name, "color");
394 assert_eq!(value, "red");
395 assert_eq!(important, true);
396 assert_eq!(remaining, "");
397 }
398
399 #[test]
400 fn parse_declaration_complex_value() {
401 let result = CSSDeclaration::parse_declaration("border: 1px solid rgba(255, 0, 0, 0.5)");
402 assert!(result.is_ok());
403 let (remaining, (name, (value, important))) = result.unwrap();
404 assert_eq!(name, "border");
405 assert_eq!(value, "1px solid rgba(255, 0, 0, 0.5)");
406 assert_eq!(important, false);
407 assert_eq!(remaining, "");
408 }
409
410 #[test]
411 fn parse_declaration_complex_with_important() {
412 let result = CSSDeclaration::parse_declaration("margin: 10px 20px 30px 40px !important");
413 assert!(result.is_ok());
414 let (remaining, (name, (value, important))) = result.unwrap();
415 assert_eq!(name, "margin");
416 assert_eq!(value, "10px 20px 30px 40px");
417 assert_eq!(important, true);
418 assert_eq!(remaining, "");
419 }
420
421 #[test]
422 fn parse_declaration_no_space_around_colon() {
423 let result = CSSDeclaration::parse_declaration("color:red");
424 assert!(result.is_ok());
425 let (remaining, (name, (value, important))) = result.unwrap();
426 assert_eq!(name, "color");
427 assert_eq!(value, "red");
428 assert_eq!(important, false);
429 assert_eq!(remaining, "");
430 }
431
432 #[test]
433 fn parse_declaration_stops_at_semicolon() {
434 let result = CSSDeclaration::parse_declaration("color: red; margin: 10px");
435 assert!(result.is_ok());
436 let (remaining, (name, (value, important))) = result.unwrap();
437 assert_eq!(name, "color");
438 assert_eq!(value, "red");
439 assert_eq!(important, false);
440 assert_eq!(remaining, "; margin: 10px");
441 }
442
443 #[test]
444 fn parse_declaration_stops_at_closing_brace() {
445 let result = CSSDeclaration::parse_declaration("color: red}");
446 assert!(result.is_ok());
447 let (remaining, (name, (value, important))) = result.unwrap();
448 assert_eq!(name, "color");
449 assert_eq!(value, "red");
450 assert_eq!(important, false);
451 assert_eq!(remaining, "}");
452 }
453
454 #[test]
455 fn parse_declaration_underscore_property() {
456 let result = CSSDeclaration::parse_declaration("_private: value");
457 assert!(result.is_ok());
458 let (remaining, (name, (value, important))) = result.unwrap();
459 assert_eq!(name, "_private");
460 assert_eq!(value, "value");
461 assert_eq!(important, false);
462 assert_eq!(remaining, "");
463 }
464
465 #[test]
466 fn parse_declaration_non_ascii_property() {
467 let result = CSSDeclaration::parse_declaration("café: brown");
468 assert!(result.is_ok());
469 let (remaining, (name, (value, important))) = result.unwrap();
470 assert_eq!(name, "café");
471 assert_eq!(value, "brown");
472 assert_eq!(important, false);
473 assert_eq!(remaining, "");
474 }
475
476 #[test]
477 fn parse_declaration_numeric_value() {
478 let result = CSSDeclaration::parse_declaration("z-index: 999");
479 assert!(result.is_ok());
480 let (remaining, (name, (value, important))) = result.unwrap();
481 assert_eq!(name, "z-index");
482 assert_eq!(value, "999");
483 assert_eq!(important, false);
484 assert_eq!(remaining, "");
485 }
486
487 #[test]
488 fn parse_declaration_leading_whitespace() {
489 let result = CSSDeclaration::parse_declaration(" color: red");
490 assert!(result.is_ok());
491 let (remaining, (name, (value, important))) = result.unwrap();
492 assert_eq!(name, "color");
493 assert_eq!(value, "red");
494 assert_eq!(important, false);
495 assert_eq!(remaining, "");
496 }
497
498 #[test]
499 fn parse_declaration_fails_missing_colon() {
500 let result = CSSDeclaration::parse_declaration("color red");
501 assert!(result.is_err());
502 }
503
504 #[test]
505 fn parse_declaration_fails_empty_input() {
506 let result = CSSDeclaration::parse_declaration("");
507 assert!(result.is_err());
508 }
509
510 #[test]
511 fn parse_declaration_fails_no_property() {
512 let result = CSSDeclaration::parse_declaration(": red");
513 assert!(result.is_err());
514 }
515
516 #[test]
517 fn parse_declaration_url_value() {
518 let result = CSSDeclaration::parse_declaration("background-image: url('test.jpg')");
519 assert!(result.is_ok());
520 let (remaining, (name, (value, important))) = result.unwrap();
521 assert_eq!(name, "background-image");
522 assert_eq!(value, "url('test.jpg')");
523 assert_eq!(important, false);
524 assert_eq!(remaining, "");
525 }
526
527 #[test]
528 fn test_new() {
529 let decl = CSSDeclaration::new("x", "y", None);
530 assert_eq!(decl.name, "x");
531 assert_eq!(decl.value, "y");
532 assert_eq!(decl.important, false);
533
534 let decl_important = CSSDeclaration::new("x", "y", Some(true));
535 assert_eq!(decl_important.name, "x");
536 assert_eq!(decl_important.value, "y");
537 assert_eq!(decl_important.important, true);
538 }
539
540 #[test]
541 fn test_from_string_simple() {
542 let decl = CSSDeclaration::from_string("color: red;").unwrap();
543 assert_eq!(decl.name, "color");
544 assert_eq!(decl.value, "red");
545 assert_eq!(decl.important, false);
546 }
547
548 #[test]
549 fn test_from_string_values_with_whitespace() {
550 let decl = CSSDeclaration::from_string("border: 1px solid red;").unwrap();
551 assert_eq!(decl.name, "border");
552 assert_eq!(decl.value, "1px solid red");
553 assert_eq!(decl.important, false);
554 }
555
556 #[test]
557 fn test_from_string_no_semi() {
558 let decl = CSSDeclaration::from_string("color: red").unwrap();
559 assert_eq!(decl.name, "color");
560 assert_eq!(decl.value, "red");
561 assert_eq!(decl.important, false);
562 }
563
564 #[test]
565 fn test_from_string_numeric_val() {
566 let decl = CSSDeclaration::from_string("padding: 10px").unwrap();
567 assert_eq!(decl.name, "padding");
568 assert_eq!(decl.value, "10px");
569 assert_eq!(decl.important, false);
570 }
571
572 #[test]
573 fn test_from_string_prefix() {
574 let decl = CSSDeclaration::from_string("-webkit-transition: .2s all").unwrap();
575 assert_eq!(decl.name, "-webkit-transition");
576 assert_eq!(decl.value, ".2s all");
577 assert_eq!(decl.important, false);
578 }
579
580 #[test]
581 fn test_to_string() {
582 let decl = CSSDeclaration::from_string("color: red;").unwrap();
583 let decl_str = decl.to_string();
584 assert_eq!(decl_str, "color: red;");
585 }
586
587 #[test]
588 fn test_to_string_important() {
589 let decl = CSSDeclaration::new("color", "red", Some(true));
590 assert_eq!(decl.to_string(), "color: red !important;");
591 }
592}