1use std::fmt;
31use nom::{
32 IResult,
33 bytes::complete::take_until,
34 character::complete::{char, multispace0},
35 sequence::{terminated, delimited},
36 Parser,
37};
38use crate::css_declaration_list::CSSDeclarationList;
39
40
41#[derive(Debug, Clone, PartialEq)]
42pub struct CSSRule {
43 pub selector: String,
44 pub declarations: CSSDeclarationList,
45}
46
47impl CSSRule {
48 fn parse_selector(input: &str) -> IResult<&str, String> {
49 let (input, selector) = terminated(take_until("{"), char('{')).parse(input)?;
50
51 Ok((input, selector.trim().to_string()))
52 }
53
54 fn parse_declarations_block(input: &str) -> IResult<&str, CSSDeclarationList> {
55 let (input, declarations) = terminated(
56 delimited(
57 multispace0,
58 CSSDeclarationList::parse,
59 multispace0
60 ),
61 char('}')
62 ).parse(input)?;
63
64 Ok((input, declarations))
65 }
66
67 pub(crate) fn parse(input: &str) -> IResult<&str, CSSRule> {
68 let (input, selector) = Self::parse_selector(input)?;
69 let (input, declarations) = Self::parse_declarations_block(input)?;
70
71 Ok((
72 input,
73 CSSRule {
74 selector,
75 declarations,
76 },
77 ))
78 }
79
80 pub fn from_string(input: &str) -> Result<CSSRule, String> {
81 let (_, css_rule) = Self::parse(input)
82 .map_err(|_| "Failed to parse CSS rule".to_string())?;
83
84 Ok(css_rule)
85 }
86
87 pub fn new(selector: &str, declarations: &CSSDeclarationList) -> Self {
88 CSSRule {
89 selector: selector.to_string(),
90 declarations: declarations.clone(),
91 }
92 }
93}
94
95impl fmt::Display for CSSRule {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(f, "{} {{ {} }}", self.selector, self.declarations.to_string())
98 }
99}
100
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::css_declaration::CSSDeclaration;
106
107
108 #[test]
109 fn test_parse_selector_simple_element() {
110 let input = "div{color: red}";
111 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
112
113 assert_eq!(remaining, "color: red}");
114 assert_eq!(selector, "div");
115 }
116
117 #[test]
118 fn test_parse_selector_class() {
119 let input = ".container{margin: 0}";
120 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
121
122 assert_eq!(remaining, "margin: 0}");
123 assert_eq!(selector, ".container");
124 }
125
126 #[test]
127 fn test_parse_selector_id() {
128 let input = "#header{background: blue}";
129 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
130
131 assert_eq!(remaining, "background: blue}");
132 assert_eq!(selector, "#header");
133 }
134
135 #[test]
136 fn test_parse_selector_complex() {
137 let input = "div.container > p:first-child{font-size: 16px}";
138 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
139
140 assert_eq!(remaining, "font-size: 16px}");
141 assert_eq!(selector, "div.container > p:first-child");
142 }
143
144 #[test]
145 fn test_parse_selector_with_spaces() {
146 let input = " div {padding: 10px}";
147 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
148
149 assert_eq!(remaining, "padding: 10px}");
150 assert_eq!(selector, "div");
151 }
152
153 #[test]
154 fn test_parse_selector_attribute() {
155 let input = "input[type=\"text\"]{border: 1px solid gray}";
156 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
157
158 assert_eq!(remaining, "border: 1px solid gray}");
159 assert_eq!(selector, "input[type=\"text\"]");
160 }
161
162 #[test]
163 fn test_parse_selector_pseudo_class() {
164 let input = "a:hover{color: red}";
165 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
166
167 assert_eq!(remaining, "color: red}");
168 assert_eq!(selector, "a:hover");
169 }
170
171 #[test]
172 fn test_parse_selector_child_combinator() {
173 let input = "div > p{margin-bottom: 1em}";
174 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
175
176 assert_eq!(remaining, "margin-bottom: 1em}");
177 assert_eq!(selector, "div > p");
178 }
179
180 #[test]
181 fn test_parse_selector_adjacent_sibling() {
182 let input = "h1 + p{font-weight: bold}";
183 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
184
185 assert_eq!(remaining, "font-weight: bold}");
186 assert_eq!(selector, "h1 + p");
187 }
188
189 #[test]
190 fn test_parse_selector_general_sibling() {
191 let input = "h1 ~ p{color: gray}";
192 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
193
194 assert_eq!(remaining, "color: gray}");
195 assert_eq!(selector, "h1 ~ p");
196 }
197
198 #[test]
199 fn test_parse_selector_empty_block() {
200 let input = "div{}";
201 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
202
203 assert_eq!(remaining, "}");
204 assert_eq!(selector, "div");
205 }
206
207 #[test]
208 fn test_parse_selector_with_newlines() {
209 let input = "\n .container\n {display: flex}";
210 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
211
212 assert_eq!(remaining, "display: flex}");
213 assert_eq!(selector, ".container");
214 }
215
216 #[test]
217 fn test_parse_selector_universal() {
218 let input = "*{box-sizing: border-box}";
219 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
220
221 assert_eq!(remaining, "box-sizing: border-box}");
222 assert_eq!(selector, "*");
223 }
224
225 #[test]
226 fn test_parse_selector_fails_no_opening_brace() {
227 let input = "div color: red";
228 let result = CSSRule::parse_selector(input);
229
230 assert!(result.is_err());
231 }
232
233 #[test]
234 fn test_parse_selector_fails_empty_input() {
235 let input = "";
236 let result = CSSRule::parse_selector(input);
237
238 assert!(result.is_err());
239 }
240
241 #[test]
242 fn test_parse_selector_with_tabs() {
243 let input = "\t.nav\t{position: fixed}";
244 let (remaining, selector) = CSSRule::parse_selector(input).unwrap();
245
246 assert_eq!(remaining, "position: fixed}");
247 assert_eq!(selector, ".nav");
248 }
249
250 #[test]
251 fn test_basic_rule() {
252 let input = "h1 { color: red; padding: 10px; }";
253 let (_, rule) = CSSRule::parse(input).unwrap();
254 assert_eq!(rule.selector, "h1");
255 assert_eq!(rule.declarations.declarations.len(), 2);
256 assert_eq!(rule.declarations.declarations[0], CSSDeclaration::new("color", "red", None));
257 assert_eq!(rule.declarations.declarations[1], CSSDeclaration::new("padding", "10px", None));
258 }
259
260 #[test]
261 fn test_rule_with_whitespace() {
262 let input = " div.my-class { margin : 0 auto ; padding : 1em ; }";
263 let (_, rule) = CSSRule::parse(input).unwrap();
264 assert_eq!(rule.selector, "div.my-class");
265 assert_eq!(rule.declarations.declarations.len(), 2);
266 assert_eq!(rule.declarations.declarations[0], CSSDeclaration::new("margin", "0 auto", None));
267 assert_eq!(rule.declarations.declarations[1], CSSDeclaration::new("padding", "1em", None));
268 }
269
270 #[test]
271 fn test_rule_no_trailing_semicolon() {
272 let input = "p { font-size: 16px; line-height: 1.5 }";
273 let (_, rule) = CSSRule::parse(input).unwrap();
274 assert_eq!(rule.selector, "p");
275 assert_eq!(rule.declarations.declarations.len(), 2);
276 assert_eq!(rule.declarations.declarations[0], CSSDeclaration::new("font-size", "16px", None));
277 assert_eq!(rule.declarations.declarations[1], CSSDeclaration::new("line-height", "1.5", None));
278 }
279
280 #[test]
281 fn test_empty_declarations() {
282 let input = ".empty { }";
283 let (_, rule) = CSSRule::parse(input).unwrap();
284 assert_eq!(rule.selector, ".empty");
285 assert!(rule.declarations.declarations.is_empty());
286 }
287
288 #[test]
289 fn test_rule_with_newlines() {
290 let input = r#"
291 .box {
292 border: 1px solid black;
293 background: white;
294 }
295 "#;
296 let (_, rule) = CSSRule::parse(input).unwrap();
297 assert_eq!(rule.selector.trim(), ".box");
298 assert_eq!(rule.declarations.declarations.len(), 2);
299 assert_eq!(rule.declarations.declarations[0], CSSDeclaration::new("border", "1px solid black", None));
300 assert_eq!(rule.declarations.declarations[1], CSSDeclaration::new("background", "white", None));
301 }
302
303 #[test]
304 fn test_rule_with_multiple_selectors() {
305 let input = "h1, h2, h3 { font-weight: bold; }";
306 let (_, rule) = CSSRule::parse(input).unwrap();
307 assert_eq!(rule.selector, "h1, h2, h3");
308 assert_eq!(rule.declarations.declarations.len(), 1);
309 assert_eq!(rule.declarations.declarations[0], CSSDeclaration::new("font-weight", "bold", None));
310 }
311}