css_structs/
css_declaration_list.rs

1//! CSS Declaration List Parser
2//!
3//! This module provides parsing and representation for CSS declaration lists
4//! (collections of property-value pairs typically found inside CSS rule blocks
5//! like `{ color: red; margin: 10px; padding: 5px !important }`).
6//!
7//! ## Main API
8//! 
9//! - `CSSDeclarationList::from_string()` - Parse a CSS declaration list from a string
10//! - `CSSDeclarationList::new()` - Create a new declaration list programmatically  
11//! - `remove_declaration()` - Remove declarations by property name
12//! - `Display` trait implementation for converting back to CSS string
13//!
14//! ## Examples
15//!
16//! ```rust
17//! use css_structs::CSSDeclarationList;
18//! 
19//! // Parse from string
20//! let list = CSSDeclarationList::from_string("color: red; margin: 10px; padding: 5px").unwrap();
21//! assert_eq!(list.declarations.len(), 3);
22//!
23//! // Create and modify programmatically  
24//! let mut list = CSSDeclarationList::from_string("color: red; margin: 10px").unwrap();
25//! list.remove_declaration("color");
26//! println!("{}", list); // "margin: 10px;"
27//!
28//! // Create a new empty declaration list
29//! let empty_list = CSSDeclarationList::new();
30//! assert!(empty_list.declarations.is_empty());
31//! ```
32
33
34use std::fmt;
35use nom::{
36  character::complete::{char, multispace0},
37  combinator::opt,
38  multi::many0,
39  sequence::{delimited, preceded},
40  IResult,
41  Parser, 
42};
43use crate::css_declaration::CSSDeclaration;
44
45
46#[derive(Debug, Clone, PartialEq)]
47pub struct CSSDeclarationList {
48  pub declarations: Vec<CSSDeclaration>,
49}
50
51impl CSSDeclarationList {
52  fn parse_declarations(input: &str) -> IResult<&str, Vec<CSSDeclaration>> {
53    many0(
54      preceded(
55        many0(delimited(multispace0, char(';'), multispace0)),
56        delimited(
57          multispace0,
58          CSSDeclaration::parse,
59          opt(char(';')),
60        )
61      )
62    ).parse(input)
63  }
64
65  pub(crate) fn parse(input: &str) -> IResult<&str, CSSDeclarationList> {
66    let (input, declarations) = Self::parse_declarations(input)?;
67
68    Ok((input, CSSDeclarationList { declarations }))
69  }
70
71  pub fn from_string(css_block: &str) -> Result<Self, String> {
72    let (_, declaration_list) = Self::parse(css_block)
73      .map_err(|_| "Failed to parse CSS declarations list".to_string())?;
74
75    Ok(declaration_list)
76  }
77
78  pub fn remove_declaration(&mut self, decl_name: &str) {
79    self.declarations.retain(|decl| decl.name != decl_name);
80  }
81
82  pub fn new() -> Self {
83    CSSDeclarationList {
84      declarations: Vec::new(),
85    }
86  }
87}
88
89impl fmt::Display for CSSDeclarationList {
90  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91    let list_str = self.declarations
92      .iter()
93      .map(|decl| decl.to_string())
94      .collect::<Vec<_>>()
95      .join(" ");
96
97    write!(f, "{}", list_str)
98  }
99}
100
101
102#[cfg(test)]
103mod tests {
104  use super::*;
105  use crate::css_declaration::CSSDeclaration;
106
107  #[test]
108  fn test_parse_declarations_single_declaration() {
109    let input = "color: red;";
110    let (remaining, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
111
112    assert_eq!(remaining, "");
113    assert_eq!(declarations.len(), 1);
114    assert_eq!(declarations[0], CSSDeclaration::new("color", "red", None));
115  }
116
117  #[test]
118  fn test_parse_declarations_multiple_declarations() {
119    let input = "color: red; background: blue; margin: 10px;";
120    let (remaining, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
121
122    assert_eq!(remaining, "");
123    assert_eq!(declarations.len(), 3);
124    assert_eq!(declarations[0], CSSDeclaration::new("color", "red", None));
125    assert_eq!(declarations[1], CSSDeclaration::new("background", "blue", None));
126    assert_eq!(declarations[2], CSSDeclaration::new("margin", "10px", None));
127  }
128
129  #[test]
130  fn test_parse_declarations_no_trailing_semicolon() {
131    let input = "font-size: 16px";
132    let (remaining, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
133
134    assert_eq!(remaining, "");
135    assert_eq!(declarations.len(), 1);
136    assert_eq!(declarations[0], CSSDeclaration::new("font-size", "16px", None));
137  }
138
139  #[test]
140  fn test_parse_declarations_mixed_semicolons() {
141    let input = "color: red; background: blue; padding: 5px";
142    let (remaining, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
143
144    assert_eq!(remaining, "");
145    assert_eq!(declarations.len(), 3);
146    assert_eq!(declarations[0], CSSDeclaration::new("color", "red", None));
147    assert_eq!(declarations[1], CSSDeclaration::new("background", "blue", None));
148    assert_eq!(declarations[2], CSSDeclaration::new("padding", "5px", None));
149  }
150
151  #[test]
152  fn test_parse_declarations_leading_whitespace() {
153    let input = "   color: red; background: blue;";
154    let (remaining, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
155
156    assert_eq!(remaining, "");
157    assert_eq!(declarations.len(), 2);
158    assert_eq!(declarations[0], CSSDeclaration::new("color", "red", None));
159    assert_eq!(declarations[1], CSSDeclaration::new("background", "blue", None));
160  }
161
162  #[test]
163  fn test_parse_declarations_whitespace_between() {
164    let input = "color: red;   background: blue;   padding: 10px;";
165    let (remaining, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
166
167    assert_eq!(remaining, "");
168    assert_eq!(declarations.len(), 3);
169    assert_eq!(declarations[0], CSSDeclaration::new("color", "red", None));
170    assert_eq!(declarations[1], CSSDeclaration::new("background", "blue", None));
171    assert_eq!(declarations[2], CSSDeclaration::new("padding", "10px", None));
172  }
173
174  #[test]
175  fn test_parse_declarations_trailing_whitespace() {
176    let input = "color: red; background: blue;   ";
177    let (remaining, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
178
179    assert_eq!(remaining, "   ");
180    assert_eq!(declarations.len(), 2);
181    assert_eq!(declarations[0], CSSDeclaration::new("color", "red", None));
182    assert_eq!(declarations[1], CSSDeclaration::new("background", "blue", None));
183  }
184
185  #[test]
186  fn test_parse_declarations_empty_input() {
187    let input = "";
188    let (remaining, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
189
190    assert_eq!(remaining, "");
191    assert_eq!(declarations.len(), 0);
192  }
193
194  #[test]
195  fn test_parse_declarations_whitespace_only() {
196    let input = "   \n  \t  ";
197    let (remaining, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
198
199    assert_eq!(remaining, "   \n  \t  ");
200    assert_eq!(declarations.len(), 0);
201  }
202
203  #[test]
204  fn test_parse_declarations_extra_semicolons() {
205    let input = "color: red;; background: blue;;";
206    let (_, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
207
208    println!("{:?}", declarations);
209    assert_eq!(declarations.len(), 2);
210    assert_eq!(declarations[0], CSSDeclaration::new("color", "red", None));
211    assert_eq!(declarations[1], CSSDeclaration::new("background", "blue", None));
212  }
213
214  #[test]
215  fn test_parse_declarations_newlines_and_tabs() {
216    let input = "\n\tcolor: red;\n\tbackground: blue;\n\t";
217    let (_, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
218
219    assert_eq!(declarations.len(), 2);
220    assert_eq!(declarations[0], CSSDeclaration::new("color", "red", None));
221    assert_eq!(declarations[1], CSSDeclaration::new("background", "blue", None));
222  }
223
224  #[test]
225  fn test_parse_declarations_partial_parse() {
226    let input = "color: red; background: blue; } extra content";
227    let (remaining, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
228
229    // Should parse valid declarations and leave remaining input
230    assert_eq!(remaining.trim(), "} extra content");
231    assert_eq!(declarations.len(), 2);
232    assert_eq!(declarations[0], CSSDeclaration::new("color", "red", None));
233    assert_eq!(declarations[1], CSSDeclaration::new("background", "blue", None));
234  }
235
236  #[test]
237  fn test_parse_declarations_single_semicolon() {
238    let input = ";";
239    let (_, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
240
241    // Should handle a single semicolon gracefully
242    assert_eq!(declarations.len(), 0);
243  }
244
245  #[test]
246  fn test_parse_declarations_semicolon_with_whitespace() {
247    let input = "  ;  ";
248    let (_, declarations) = CSSDeclarationList::parse_declarations(input).unwrap();
249
250    assert_eq!(declarations.len(), 0);
251  }
252
253  #[test]
254  fn test_single() {
255    let input = "color: red;";
256    let list = CSSDeclarationList::from_string(input).unwrap();
257    assert_eq!(list.declarations.len(), 1);
258    assert_eq!(list.declarations[0], CSSDeclaration::new("color", "red", None));
259  }
260
261  #[test]
262  fn test_multiple() {
263    let input = "color: red; background-color: blue; padding: 10px;";
264    let list = CSSDeclarationList::from_string(input).unwrap();
265    assert_eq!(list.declarations.len(), 3);
266    assert_eq!(list.declarations[0], CSSDeclaration::new("color", "red", None));
267    assert_eq!(list.declarations[1], CSSDeclaration::new("background-color", "blue", None));
268    assert_eq!(list.declarations[2], CSSDeclaration::new("padding", "10px", None));
269  }
270
271  #[test]
272  fn test_extra_whitespace() {
273    let input = "  margin :  0 auto  ;  padding :  1em ;  ";
274    let list = CSSDeclarationList::from_string(input).unwrap();
275    assert_eq!(list.declarations.len(), 2);
276    assert_eq!(list.declarations[0], CSSDeclaration::new("margin", "0 auto", None));
277    assert_eq!(list.declarations[1], CSSDeclaration::new("padding", "1em", None));
278  }
279
280  #[test]
281  fn test_no_trailing_semicolon() {
282    let input = "font-size: 16px; line-height: 1.5";
283    let list = CSSDeclarationList::from_string(input).unwrap();
284    assert_eq!(list.declarations.len(), 2);
285    assert_eq!(list.declarations[0], CSSDeclaration::new("font-size", "16px", None));
286    assert_eq!(list.declarations[1], CSSDeclaration::new("line-height", "1.5", None));
287  }
288
289  #[test]
290  fn test_empty_input() {
291    let input = "";
292    let list = CSSDeclarationList::from_string(input).unwrap();
293    assert_eq!(list.declarations.len(), 0);
294  }
295
296  #[test]
297  fn test_to_string_output() {
298    let input = "color: red; padding: 10px;";
299    let list = CSSDeclarationList::from_string(input).unwrap();
300    let output = list.to_string();
301    assert_eq!(output, "color: red; padding: 10px;");
302  }
303
304  #[test]
305  fn test_remove_declaration() {
306    let input = "color: red; padding: 10px;";
307    let mut list = CSSDeclarationList::from_string(input).unwrap();
308    list.remove_declaration("color");
309    assert_eq!(list.declarations.len(), 1);
310    assert_eq!(list.declarations[0], CSSDeclaration::new("padding", "10px", None));
311  }
312}