1use core::fmt;
12
13use crate::to_css::ToCss;
14
15#[derive(Clone, Debug, PartialEq, Eq, Hash)]
21pub struct CssProp {
22 name: &'static str,
23 value: String,
24}
25
26impl CssProp {
27 pub(crate) fn new(name: &'static str, value: String) -> Self {
30 Self { name, value }
31 }
32
33 pub fn name(&self) -> &'static str {
35 self.name
36 }
37
38 pub fn value(&self) -> &str {
40 &self.value
41 }
42}
43
44impl ToCss for CssProp {
45 fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
46 dest.write_str(self.name)?;
47 dest.write_str(": ")?;
48 dest.write_str(&self.value)?;
49 dest.write_char(';')
50 }
51}
52
53#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
71pub struct Css {
72 props: Vec<CssProp>,
73}
74
75impl Css {
76 pub fn new() -> Self {
78 Self { props: Vec::new() }
79 }
80
81 pub(crate) fn push(mut self, name: &'static str, value: impl ToCss) -> Self {
84 self.props.push(CssProp::new(name, value.to_css_string()));
85 self
86 }
87
88 pub(crate) fn push_raw(mut self, name: &'static str, value: impl Into<String>) -> Self {
90 self.props.push(CssProp::new(name, value.into()));
91 self
92 }
93
94 pub fn raw(self, name: &'static str, value: impl Into<String>) -> Self {
103 self.push_raw(name, value)
104 }
105
106 pub fn is_empty(&self) -> bool {
108 self.props.is_empty()
109 }
110
111 pub fn len(&self) -> usize {
115 self.props.len()
116 }
117
118 pub fn entries(&self) -> impl Iterator<Item = &CssProp> {
122 self.props.iter()
123 }
124
125 pub fn resolved(&self) -> Vec<&CssProp> {
129 let mut seen: std::collections::HashSet<&'static str> = std::collections::HashSet::new();
132 let mut out: Vec<&CssProp> = Vec::new();
133 for prop in self.props.iter().rev() {
134 if seen.insert(prop.name) {
135 out.push(prop);
136 }
137 }
138 out.reverse();
139 out
140 }
141
142 pub fn merge(mut self, other: Css) -> Self {
146 self.props.extend(other.props);
147 self
148 }
149}
150
151impl ToCss for Css {
152 fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
153 let resolved = self.resolved();
154 for (i, prop) in resolved.iter().enumerate() {
155 if i > 0 {
156 dest.write_char(' ')?;
157 }
158 prop.to_css(dest)?;
159 }
160 Ok(())
161 }
162}
163
164impl fmt::Display for Css {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 ToCss::to_css(self, f)
167 }
168}
169
170impl From<Css> for String {
171 fn from(s: Css) -> Self {
172 s.to_css_string()
173 }
174}
175
176impl From<&Css> for String {
177 fn from(s: &Css) -> Self {
178 s.to_css_string()
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn empty_style_serializes_to_empty_string() {
188 assert_eq!(Css::new().to_css_string(), "");
189 assert!(Css::new().is_empty());
190 }
191
192 #[test]
193 fn raw_appends_a_declaration() {
194 let s = Css::new().raw("color", "red");
195 assert_eq!(s.to_css_string(), "color: red;");
196 assert!(!s.is_empty());
197 assert_eq!(s.len(), 1);
198 }
199
200 #[test]
201 fn multiple_distinct_properties_keep_order() {
202 let s = Css::new()
203 .raw("color", "red")
204 .raw("background-color", "blue");
205 assert_eq!(s.to_css_string(), "color: red; background-color: blue;");
206 }
207
208 #[test]
209 fn duplicate_property_uses_last_value() {
210 let s = Css::new()
211 .raw("color", "red")
212 .raw("color", "blue")
213 .raw("color", "green");
214 assert_eq!(s.to_css_string(), "color: green;");
215 assert_eq!(s.len(), 3);
218 assert_eq!(s.resolved().len(), 1);
219 }
220
221 #[test]
222 fn duplicate_property_preserves_position_of_last() {
223 let s = Css::new()
227 .raw("color", "red")
228 .raw("background-color", "white")
229 .raw("color", "blue");
230 assert_eq!(s.to_css_string(), "background-color: white; color: blue;");
231 }
232
233 #[test]
234 fn entries_iterates_all_in_order() {
235 let s = Css::new().raw("color", "red").raw("color", "blue");
236 let names: Vec<&str> = s.entries().map(|p| p.name()).collect();
237 assert_eq!(names, ["color", "color"]);
238 }
239
240 #[test]
241 fn merge_lets_other_win() {
242 let base = Css::new().raw("color", "red");
243 let overlay = Css::new().raw("color", "blue");
244 let merged = base.merge(overlay);
245 assert_eq!(merged.to_css_string(), "color: blue;");
246 }
247
248 #[test]
249 fn merge_preserves_distinct_props() {
250 let base = Css::new().raw("color", "red");
251 let overlay = Css::new().raw("background-color", "yellow");
252 let merged = base.merge(overlay);
253 assert_eq!(
254 merged.to_css_string(),
255 "color: red; background-color: yellow;"
256 );
257 }
258
259 #[test]
260 fn into_string_via_from_owned() {
261 let s = Css::new().raw("color", "red");
262 let css: String = s.into();
263 assert_eq!(css, "color: red;");
264 }
265
266 #[test]
267 fn into_string_via_from_borrowed() {
268 let s = Css::new().raw("color", "red");
269 let css: String = (&s).into();
270 assert_eq!(css, "color: red;");
271 }
272
273 #[test]
274 fn display_matches_to_css_string() {
275 let s = Css::new().raw("color", "red").raw("padding", "8px");
276 assert_eq!(format!("{s}"), s.to_css_string());
277 }
278
279 #[test]
280 fn style_prop_accessors() {
281 let s = Css::new().raw("color", "red");
282 let prop = s.entries().next().unwrap();
283 assert_eq!(prop.name(), "color");
284 assert_eq!(prop.value(), "red");
285 assert_eq!(prop.to_css_string(), "color: red;");
286 }
287}