css_structs/
css_rule.rs

1//! CSS Rule Parser
2//!
3//! This module provides parsing and representation for complete CSS rules
4//! (selector-declaration block pairs like `div { color: red; margin: 10px }` or 
5//! `h1.title, h2.subtitle { font-weight: bold; padding: 1em }`).
6//!
7//! ## Main API
8//! 
9//! - `CSSRule::from_string()` - Parse a CSS rule from a string
10//! - `CSSRule::new()` - Create a new rule programmatically  
11//! - `Display` trait implementation for converting back to CSS string
12//!
13//! ## Examples
14//!
15//! ```rust
16//! use css_structs::{CSSRule, CSSDeclarationList, CSSDeclaration};
17//! 
18//! // Parse from string
19//! let rule = CSSRule::from_string("div.container { color: red; margin: 10px }").unwrap();
20//! assert_eq!(rule.selector, "div.container");
21//! assert_eq!(rule.declarations.declarations.len(), 2);
22//!
23//! // Create programmatically  
24//! let mut declarations = CSSDeclarationList::from_string("padding: 1em").unwrap();
25//! let rule = CSSRule::new("h1", &declarations);
26//! println!("{}", rule); // "h1 { padding: 1em }"
27//! ```
28
29
30use 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}