css_structs/
stylesheet.rs1use 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}