css_structs/
css_declaration.rs

1//! CSS Declaration Parser
2//!
3//! This module provides parsing and representation for individual CSS declarations
4//! (property-value pairs like `color: red` or `margin: 10px !important`).
5//!
6//! ## Main API
7//! 
8//! - `CSSDeclaration::from_string()` - Parse a CSS declaration from a string
9//! - `CSSDeclaration::new()` - Create a new declaration programmatically  
10//! - `Display` trait implementation for converting back to CSS string
11//!
12//! ## Examples
13//!
14//! ```rust
15//! use css_structs::CSSDeclaration;
16//! 
17//! // Parse from string
18//! let decl = CSSDeclaration::from_string("color: red !important").unwrap();
19//! assert_eq!(decl.name, "color");
20//! assert_eq!(decl.value, "red");
21//! assert_eq!(decl.important, true);
22//!
23//! // Create programmatically  
24//! let decl = CSSDeclaration::new("margin", "10px", None);
25//! println!("{}", decl); // "margin: 10px;"
26//! ```
27
28
29use 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          // First character: letter, underscore, dash or non-ASCII
53          take_while1(|c: char| c.is_alphabetic() || c == '_' || c == '-' || is_non_ascii(c)),
54
55          // Rest: letters, digits, hyphens, underscores, or non-ASCII
56          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        // Parse the main value (everything except !important)
67        map(is_not(";{}!"), |s: &str| s.trim().to_string()),
68
69        // Parse optional !important
70        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}