1use blitz_dom::{BaseDocument, NodeId};
13
14use crate::Result;
15use crate::element::attr_name;
16
17fn parse_declarations(style_attr: &str) -> Vec<(String, String)> {
22 style_attr
23 .split(';')
24 .filter_map(|decl| decl.split_once(':'))
25 .map(|(prop, value)| (prop.trim().to_string(), value.trim().to_string()))
26 .filter(|(prop, _)| !prop.is_empty())
27 .collect()
28}
29
30fn serialize_declarations(decls: &[(String, String)]) -> String {
31 decls
32 .iter()
33 .map(|(prop, value)| format!("{prop}: {value};"))
34 .collect::<Vec<_>>()
35 .join(" ")
36}
37
38fn style_attr(doc: &BaseDocument, node_id: NodeId) -> String {
39 doc.get_node(node_id)
40 .and_then(|node| node.attr(blitz_dom::local_name!("style")))
41 .unwrap_or_default()
42 .to_string()
43}
44
45fn update_style_attr(
46 doc: &mut BaseDocument,
47 node_id: NodeId,
48 f: impl FnOnce(&mut Vec<(String, String)>),
49) {
50 let mut decls = parse_declarations(&style_attr(doc, node_id));
51 f(&mut decls);
52 let new_style = serialize_declarations(&decls);
53 doc.mutate()
54 .set_attribute(node_id, attr_name("style"), &new_style);
55}
56
57pub fn get_property_value(doc: &BaseDocument, node: NodeId, name: &str) -> Result<String> {
62 Ok(parse_declarations(&style_attr(doc, node))
63 .into_iter()
64 .find(|(prop, _)| prop.eq_ignore_ascii_case(name))
65 .map(|(_, value)| value)
66 .unwrap_or_default())
67}
68
69pub fn set_property(doc: &mut BaseDocument, node: NodeId, name: &str, value: &str) -> Result<()> {
74 let value = value.to_owned();
75 update_style_attr(doc, node, |decls| {
76 decls.retain(|(prop, _)| !prop.eq_ignore_ascii_case(name));
77 if !value.is_empty() {
78 decls.push((name.to_ascii_lowercase(), value));
79 }
80 });
81 Ok(())
82}
83
84pub fn remove_property(doc: &mut BaseDocument, node: NodeId, name: &str) -> Result<String> {
88 let mut removed = String::new();
89 update_style_attr(doc, node, |decls| {
90 if let Some((_, value)) = decls
91 .iter()
92 .find(|(prop, _)| prop.eq_ignore_ascii_case(name))
93 {
94 removed = value.clone();
95 }
96 decls.retain(|(prop, _)| !prop.eq_ignore_ascii_case(name));
97 });
98 Ok(removed)
99}
100
101pub fn css_text(doc: &BaseDocument, node: NodeId) -> Result<String> {
103 Ok(style_attr(doc, node))
104}
105
106pub fn set_css_text(doc: &mut BaseDocument, node: NodeId, css: &str) -> Result<()> {
111 doc.mutate().set_attribute(node, attr_name("style"), css);
112 Ok(())
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::document;
119 use crate::element;
120 use crate::node;
121 use crate::test_support::skeleton;
122
123 fn styled(doc: &mut BaseDocument, parent: NodeId) -> NodeId {
124 let id = document::create_element(doc, "div").unwrap();
125 node::append_child(doc, parent, id).unwrap();
126 id
127 }
128
129 #[test]
130 fn set_property_then_get_property_value_round_trips() {
131 let (mut doc, _html, _head, body) = skeleton();
132 let id = styled(&mut doc, body);
133 set_property(&mut doc, id, "height", "70px").unwrap();
134 assert_eq!(get_property_value(&doc, id, "height").unwrap(), "70px");
135 }
136
137 #[test]
138 fn get_property_value_is_empty_for_an_unset_property() {
139 let (mut doc, _html, _head, body) = skeleton();
140 let id = styled(&mut doc, body);
141 assert_eq!(get_property_value(&doc, id, "height").unwrap(), "");
142 }
143
144 #[test]
145 fn set_property_replaces_rather_than_appends_a_duplicate() {
146 let (mut doc, _html, _head, body) = skeleton();
147 let id = styled(&mut doc, body);
148 set_property(&mut doc, id, "height", "10px").unwrap();
149 set_property(&mut doc, id, "height", "20px").unwrap();
150 assert_eq!(get_property_value(&doc, id, "height").unwrap(), "20px");
151 assert_eq!(css_text(&doc, id).unwrap().matches("height").count(), 1);
152 }
153
154 #[test]
156 fn setting_a_property_to_the_empty_string_removes_it() {
157 let (mut doc, _html, _head, body) = skeleton();
158 let id = styled(&mut doc, body);
159 set_property(&mut doc, id, "height", "10px").unwrap();
160 set_property(&mut doc, id, "height", "").unwrap();
161 assert_eq!(get_property_value(&doc, id, "height").unwrap(), "");
162 }
163
164 #[test]
165 fn remove_property_returns_what_it_removed() {
166 let (mut doc, _html, _head, body) = skeleton();
167 let id = styled(&mut doc, body);
168 set_property(&mut doc, id, "height", "10px").unwrap();
169 assert_eq!(remove_property(&mut doc, id, "height").unwrap(), "10px");
170 assert_eq!(remove_property(&mut doc, id, "height").unwrap(), "");
171 assert_eq!(get_property_value(&doc, id, "height").unwrap(), "");
172 }
173
174 #[test]
175 fn css_text_reads_the_inline_style_attribute() {
176 let (mut doc, _html, _head, body) = skeleton();
177 let id = styled(&mut doc, body);
178 element::set_attribute(&mut doc, id, "style", "color: red;").unwrap();
179 assert_eq!(css_text(&doc, id).unwrap(), "color: red;");
180 }
181
182 #[test]
183 fn set_css_text_replaces_every_declaration() {
184 let (mut doc, _html, _head, body) = skeleton();
185 let id = styled(&mut doc, body);
186 set_property(&mut doc, id, "height", "10px").unwrap();
187 set_css_text(&mut doc, id, "width: 5px;").unwrap();
188 assert_eq!(css_text(&doc, id).unwrap(), "width: 5px;");
189 assert_eq!(get_property_value(&doc, id, "height").unwrap(), "");
190 assert_eq!(get_property_value(&doc, id, "width").unwrap(), "5px");
191 }
192
193 #[test]
194 fn property_names_match_case_insensitively() {
195 let (mut doc, _html, _head, body) = skeleton();
196 let id = styled(&mut doc, body);
197 set_css_text(&mut doc, id, "Max-Height: 4px;").unwrap();
198 assert_eq!(get_property_value(&doc, id, "max-height").unwrap(), "4px");
199 assert_eq!(remove_property(&mut doc, id, "MAX-HEIGHT").unwrap(), "4px");
200 }
201}