css_structs/
stylesheet.rs

1//! CSS Stylesheet Parser
2//!
3//! This module provides parsing and representation for complete CSS stylesheets
4//! containing multiple CSS rules. A stylesheet represents the top-level structure
5//! that holds all CSS rules like `body { margin: 0; } .title { color: red; }`.
6//!
7//! ## Main API
8//! 
9//! - `Stylesheet::from_string()` - Parse a complete stylesheet from a CSS string
10//! - `Stylesheet::new()` - Create a new stylesheet programmatically with optional rules
11//! - `Display` trait implementation for converting back to CSS string format
12//!
13//! ## Examples
14//!
15//! ```rust
16//! use css_structs::Stylesheet;
17//! 
18//! // Parse from string
19//! let css = "body { margin: 0; padding: 0; } h1 { color: red; }";
20//! let stylesheet = Stylesheet::from_string(css).unwrap();
21//! assert_eq!(stylesheet.rules.len(), 2);
22//!
23//! // Create with existing rules 
24//! let stylesheet = Stylesheet::new(Some(stylesheet.rules.clone()));
25//! println!("{}", stylesheet);
26//!
27//! // Create empty stylesheet
28//! let empty = Stylesheet::new(None);
29//! assert!(empty.rules.is_empty());
30//! ```
31
32
33use std::fmt;
34use crate::css_rule::CSSRule;
35use nom::{
36  IResult,
37  multi::many0,
38  Parser,
39};
40
41
42#[derive(Debug, Clone, PartialEq)]
43pub struct Stylesheet {
44  pub rules: Vec<CSSRule>,
45}
46
47impl Stylesheet {  
48  fn parse(input: &str) -> IResult<&str, Vec<CSSRule>> {
49    many0(CSSRule::parse).parse(input)
50  }
51
52  pub fn from_string(input: &str) -> Result<Self, String> {
53    let (_, rules) = Self::parse(input)
54      .map_err(|_| "Failed to parse CSS".to_string())?;
55
56    Ok(Self { rules })
57  }
58
59  pub fn new(rules: Option<Vec<CSSRule>>) -> Self {
60    if let Some(rules) = rules {
61      Self { rules }
62    } else {
63      Self { rules: Vec::new() }
64    }
65  }
66}
67
68impl fmt::Display for Stylesheet {
69  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70    let stylesheet = self.rules
71      .iter()
72      .map(|decl| decl.to_string())
73      .collect::<Vec<_>>()
74      .join(" ");
75
76    write!(f, "{}", stylesheet)
77  }
78}
79
80
81#[cfg(test)]
82mod tests {
83  use super::*;
84  use crate::css_declaration::CSSDeclaration;
85
86  #[test]
87  fn test_empty_stylesheet() {
88    let input = "";
89    let result = Stylesheet::from_string(input).unwrap();
90    assert!(result.rules.is_empty());
91  }
92
93  #[test]
94  fn test_single_rule() {
95    let input = "body { margin: 0; padding: 0; }";
96    let result = Stylesheet::from_string(input).unwrap();
97    assert_eq!(result.rules.len(), 1);
98    let rule = &result.rules[0];
99    assert_eq!(rule.selector, "body");
100    assert_eq!(rule.declarations.declarations.len(), 2);
101    assert_eq!(rule.declarations.declarations[0], CSSDeclaration::new("margin", "0", None));
102    assert_eq!(rule.declarations.declarations[1], CSSDeclaration::new("padding", "0", None));
103  }
104
105  #[test]
106  fn test_multiple_rules() {
107    let input = r#"
108            h1 { color: red; }
109            p { font-size: 16px; }
110            .box { border: 1px solid black; background: white; }
111        "#;
112
113    let result = Stylesheet::from_string(input).unwrap();
114    assert_eq!(result.rules.len(), 3);
115
116    let rule1 = &result.rules[0];
117    assert_eq!(rule1.selector, "h1");
118    assert_eq!(rule1.declarations.declarations[0], CSSDeclaration::new("color", "red", None));
119
120    let rule2 = &result.rules[1];
121    assert_eq!(rule2.selector, "p");
122    assert_eq!(rule2.declarations.declarations[0], CSSDeclaration::new("font-size", "16px", None));
123
124    let rule3 = &result.rules[2];
125    assert_eq!(rule3.selector, ".box");
126    assert_eq!(rule3.declarations.declarations.len(), 2);
127    assert_eq!(rule3.declarations.declarations[0], CSSDeclaration::new("border", "1px solid black", None));
128    assert_eq!(rule3.declarations.declarations[1], CSSDeclaration::new("background", "white", None));
129  }
130
131  #[test]
132  fn test_whitespace_and_newlines() {
133    let input = r#"
134            .title {
135                font-weight: bold;
136                font-size: 24px;
137            }
138
139            .subtitle {
140                font-weight: normal;
141                font-size: 18px;
142            }
143        "#;
144
145    let result = Stylesheet::from_string(input).unwrap();
146    assert_eq!(result.rules.len(), 2);
147
148    let title_rule = &result.rules[0];
149    assert_eq!(title_rule.selector, ".title");
150    assert_eq!(title_rule.declarations.declarations[0], CSSDeclaration::new("font-weight", "bold", None));
151    assert_eq!(title_rule.declarations.declarations[1], CSSDeclaration::new("font-size", "24px", None));
152
153    let subtitle_rule = &result.rules[1];
154    assert_eq!(subtitle_rule.selector, ".subtitle");
155    assert_eq!(subtitle_rule.declarations.declarations[0], CSSDeclaration::new("font-weight", "normal", None));
156    assert_eq!(subtitle_rule.declarations.declarations[1], CSSDeclaration::new("font-size", "18px", None));
157  }
158
159  #[test]
160  #[should_panic]
161  fn test_malformed_css_returns_error() {
162    let input = "div { color: blue; padding: 10px ";
163    let result = std::panic::catch_unwind(|| Stylesheet::from_string(input));
164    assert!(result.is_err(), "Should panic due to missing closing brace");
165  }
166}