1use std::{
2 fmt::{self, Display},
3 ops::Deref,
4};
5
6use indexmap::IndexMap;
7use leptos::{attr::IntoAttributeValue, tachys::html::style::IntoStyle};
8
9fn style_map_to_string(map: &IndexMap<String, Option<String>>) -> String {
10 map.iter()
11 .filter_map(|(key, value)| {
12 value
13 .as_ref()
14 .and_then(|value| (!value.is_empty()).then_some(format!("{key}: {value};")))
15 })
16 .collect::<Vec<_>>()
17 .join(" ")
18}
19
20#[derive(Clone, Debug, PartialEq)]
21pub enum InnerStyle {
22 String(String),
23 Structured(IndexMap<String, Option<String>>),
24}
25
26impl InnerStyle {
27 pub fn with_defaults<I: Into<InnerStyle>>(self, defaults: I) -> Self {
28 let defaults: InnerStyle = defaults.into();
29
30 match (self, defaults) {
31 (Self::String(string), Self::String(default_string)) => {
32 Self::String(format!("{default_string} {string}"))
33 }
34 (Self::String(string), Self::Structured(default_map)) => {
35 Self::String(format!("{} {}", style_map_to_string(&default_map), string))
36 }
37 (Self::Structured(map), Self::String(default_string)) => {
38 Self::String(format!("{} {}", default_string, style_map_to_string(&map)))
39 }
40 (Self::Structured(map), Self::Structured(default_map)) => {
41 InnerStyle::Structured(default_map.into_iter().chain(map).collect())
42 }
43 }
44 }
45}
46
47impl Display for InnerStyle {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 match self {
50 Self::String(string) => write!(f, "{}", string),
51 Self::Structured(map) => write!(f, "{}", style_map_to_string(map),),
52 }
53 }
54}
55
56#[derive(Clone, Debug, Default, PartialEq)]
57pub struct Style(pub Option<InnerStyle>);
58
59impl Style {
60 pub fn new() -> Self {
61 Self::default()
62 }
63
64 pub fn with_defaults<I: Into<Self>>(self, defaults: I) -> Self {
65 let defaults: Self = defaults.into();
66
67 Style(match (self.0, defaults.0) {
68 (Some(style), Some(defaults)) => Some(style.with_defaults(defaults)),
69 (Some(style), None) => Some(style),
70 (None, Some(defaults)) => Some(defaults),
71 (None, None) => None,
72 })
73 }
74}
75
76impl Deref for Style {
77 type Target = Option<InnerStyle>;
78
79 fn deref(&self) -> &Self::Target {
80 &self.0
81 }
82}
83
84impl Display for Style {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 write!(
87 f,
88 "{}",
89 self.0
90 .as_ref()
91 .map(|inner_style| inner_style.to_string())
92 .unwrap_or_default(),
93 )
94 }
95}
96
97impl From<Option<&str>> for Style {
98 fn from(value: Option<&str>) -> Style {
99 Style(value.map(|value| InnerStyle::String(value.to_string())))
100 }
101}
102
103impl From<Option<String>> for Style {
104 fn from(value: Option<String>) -> Style {
105 Style(value.map(InnerStyle::String))
106 }
107}
108
109impl From<&str> for Style {
110 fn from(value: &str) -> Style {
111 Style(Some(InnerStyle::String(value.to_string())))
112 }
113}
114
115impl From<String> for Style {
116 fn from(value: String) -> Style {
117 Style(Some(InnerStyle::String(value)))
118 }
119}
120
121impl From<IndexMap<String, Option<String>>> for Style {
122 fn from(value: IndexMap<String, Option<String>>) -> Style {
123 Style(Some(InnerStyle::Structured(value)))
124 }
125}
126
127impl From<IndexMap<String, String>> for Style {
128 fn from(value: IndexMap<String, String>) -> Style {
129 Style(Some(InnerStyle::Structured(
130 value
131 .into_iter()
132 .map(|(key, value)| (key, Some(value)))
133 .collect(),
134 )))
135 }
136}
137
138impl<const N: usize> From<[(&str, Option<&str>); N]> for Style {
139 fn from(value: [(&str, Option<&str>); N]) -> Style {
140 Style(Some(InnerStyle::Structured(IndexMap::from_iter(
141 value.map(|(key, value)| (key.to_string(), value.map(|value| value.to_string()))),
142 ))))
143 }
144}
145
146impl<const N: usize> From<[(&str, &str); N]> for Style {
147 fn from(value: [(&str, &str); N]) -> Style {
148 Style(Some(InnerStyle::Structured(IndexMap::from_iter(
149 value.map(|(key, value)| (key.to_string(), Some(value.to_string()))),
150 ))))
151 }
152}
153
154impl<const N: usize> From<[(&str, Option<String>); N]> for Style {
155 fn from(value: [(&str, Option<String>); N]) -> Style {
156 Style(Some(InnerStyle::Structured(IndexMap::from_iter(
157 value.map(|(key, value)| (key.to_string(), value)),
158 ))))
159 }
160}
161
162impl<const N: usize> From<[(&str, String); N]> for Style {
163 fn from(value: [(&str, String); N]) -> Style {
164 Style(Some(InnerStyle::Structured(IndexMap::from_iter(
165 value.map(|(key, value)| (key.to_string(), Some(value))),
166 ))))
167 }
168}
169
170impl<const N: usize> From<[(String, Option<String>); N]> for Style {
171 fn from(value: [(String, Option<String>); N]) -> Style {
172 Style(Some(InnerStyle::Structured(IndexMap::from_iter(value))))
173 }
174}
175
176impl<const N: usize> From<[(String, String); N]> for Style {
177 fn from(value: [(String, String); N]) -> Style {
178 Style(Some(InnerStyle::Structured(IndexMap::from_iter(
179 value.map(|(key, value)| (key, Some(value))),
180 ))))
181 }
182}
183
184impl IntoAttributeValue for Style {
185 type Output = String;
186
187 fn into_attribute_value(self) -> Self::Output {
188 self.to_string()
189 }
190}
191
192impl IntoStyle for Style {
193 type AsyncOutput = Self;
194 type State = (leptos::tachys::renderer::types::Element, Self);
195 type Cloneable = Self;
196 type CloneableOwned = Self;
197
198 fn to_html(self, style: &mut String) {
199 style.push_str(&self.to_string());
200 }
201
202 fn hydrate<const FROM_SERVER: bool>(
203 self,
204 el: &leptos::tachys::renderer::types::Element,
205 ) -> Self::State {
206 (el.clone(), self)
207 }
208
209 fn build(self, el: &leptos::tachys::renderer::types::Element) -> Self::State {
210 leptos::tachys::renderer::Rndr::set_attribute(el, "style", &self.to_string());
211 (el.clone(), self)
212 }
213
214 fn rebuild(self, state: &mut Self::State) {
215 let (el, prev) = state;
216 if self != *prev {
217 leptos::tachys::renderer::Rndr::set_attribute(el, "style", &self.to_string());
218 }
219 *prev = self;
220 }
221
222 fn into_cloneable(self) -> Self::Cloneable {
223 self
224 }
225
226 fn into_cloneable_owned(self) -> Self::CloneableOwned {
227 self
228 }
229
230 fn dry_resolve(&mut self) {}
231
232 async fn resolve(self) -> Self::AsyncOutput {
233 self
234 }
235
236 fn reset(state: &mut Self::State) {
237 let (el, _prev) = state;
238 leptos::tachys::renderer::Rndr::remove_attribute(el, "style");
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn test_to_string() {
248 assert_eq!("", Style::default().to_string());
249
250 assert_eq!("", Style::from(None::<String>).to_string());
251
252 assert_eq!(
253 "margin: 1rem; padding: 0.5rem;",
254 Style::from(Some("margin: 1rem; padding: 0.5rem;")).to_string(),
255 );
256
257 assert_eq!(
258 "margin: 1rem; padding: 0.5rem;",
259 Style::from("margin: 1rem; padding: 0.5rem;").to_string(),
260 );
261
262 assert_eq!(
263 "color: white; border: 1px solid black;",
264 Style::from([
265 ("color", Some("white")),
266 ("background-color", None),
267 ("border", Some("1px solid black")),
268 ])
269 .to_string()
270 );
271
272 assert_eq!(
273 "color: white; background-color: gray; border: 1px solid black;",
274 Style::from([
275 ("color", "white"),
276 ("background-color", "gray"),
277 ("border", "1px solid black"),
278 ])
279 .to_string()
280 );
281 }
282
283 #[test]
284 fn test_with_defaults() {
285 assert_eq!(
287 Style::from("pointer-events: none; color: red;"),
288 Style::from("color: red;").with_defaults("pointer-events: none;"),
289 );
290 assert_eq!(
291 Style::from("color: blue; color: red;"),
292 Style::from("color: red;").with_defaults("color: blue;"),
293 );
294
295 assert_eq!(
297 Style::from("pointer-events: none; color: red;"),
298 Style::from("color: red;").with_defaults([("pointer-events", "none")]),
299 );
300 assert_eq!(
301 Style::from("color: blue; color: red;"),
302 Style::from("color: red;").with_defaults([("color", "blue")]),
303 );
304
305 assert_eq!(
307 Style::from("pointer-events: none; color: red;"),
308 Style::from([("color", "red")]).with_defaults("pointer-events: none;"),
309 );
310 assert_eq!(
311 Style::from("color: blue; color: red;"),
312 Style::from([("color", "red")]).with_defaults("color: blue;"),
313 );
314
315 assert_eq!(
317 Style::from([("pointer-events", "none"), ("color", "red")]),
318 Style::from([("color", "red")]).with_defaults([("pointer-events", "none")]),
319 );
320 assert_eq!(
321 Style::from([("color", "red")]),
322 Style::from([("color", "red")]).with_defaults([("color", "blue")]),
323 );
324
325 assert_eq!(
327 Style::from([("color", Some("red"))]),
328 Style::from([("color", Some("red"))]).with_defaults([("color", Some("blue"))]),
329 );
330 assert_eq!(
331 Style::from([("color", None::<String>)]),
332 Style::from([("color", None::<String>)]).with_defaults([("color", Some("blue"))]),
333 );
334 assert_eq!(
335 Style::from([("color", Some("red"))]),
336 Style::from([("color", Some("red"))]).with_defaults([("color", None::<String>)]),
337 );
338 assert_eq!(
339 Style::from([("color", None::<String>)]),
340 Style::from([("color", None::<String>)]).with_defaults([("color", None::<String>)]),
341 );
342 }
343
344 #[test]
345 fn test_into_attribute_value() {
346 assert_eq!(
347 Style::from("color: red; background-color: blue;").into_attribute_value(),
348 "color: red; background-color: blue;"
349 );
350
351 assert_eq!(
352 Style::from([("color", "red"), ("background-color", "blue")]).into_attribute_value(),
353 "color: red; background-color: blue;"
354 );
355 }
356}