1use 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 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 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}