1use alpine_html::STYLE;
2use alpine_markup::{Element, Node};
3use std::borrow::Cow;
4use std::collections::BTreeMap;
5use std::fmt;
6use std::fmt::Display;
7use std::ops::{Div, Mul};
8
9#[derive(Default, Debug, PartialEq)]
10pub struct Stylesheet {
11 rules: Vec<Rule>,
12}
13
14impl fmt::Display for Stylesheet {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 for rule in &self.rules {
17 write!(f, "{rule}")?;
18 }
19 Ok(())
20 }
21}
22
23impl From<Stylesheet> for Element {
24 fn from(stylesheet: Stylesheet) -> Self {
25 STYLE.push(stylesheet.to_string())
26 }
27}
28
29impl From<Stylesheet> for Node {
30 fn from(stylesheet: Stylesheet) -> Self {
31 Element::from(stylesheet).into()
32 }
33}
34
35impl Stylesheet {
36 #[must_use]
37 pub fn push(mut self, rule: Rule) -> Self {
38 self.rules.push(rule);
39 self
40 }
41
42 #[must_use]
43 pub fn rules(mut self, rules: impl IntoIterator<Item = Rule>) -> Self {
44 self.rules.extend(rules);
45 self
46 }
47}
48
49#[derive(Default, Debug, PartialEq)]
50pub struct Rule {
51 selectors: Vec<Cow<'static, str>>,
52 declarations: BTreeMap<String, String>,
53}
54
55impl fmt::Display for Rule {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 for (i, selector) in self.selectors.iter().enumerate() {
58 if i != 0 {
59 write!(f, ",")?;
60 }
61 write!(f, "{selector}")?;
62 }
63 write!(f, "{{")?;
64 for (i, (property, value)) in self.declarations.iter().enumerate() {
65 if i != 0 {
66 write!(f, ";")?;
67 }
68 write!(f, "{property}:{value}")?;
69 }
70 write!(f, "}}")?;
71 Ok(())
72 }
73}
74
75pub trait ToSelector {
76 fn to_selector(&self) -> Cow<'static, str>;
77}
78
79impl ToSelector for &'static str {
80 fn to_selector(&self) -> Cow<'static, str> {
81 Cow::Borrowed(self)
82 }
83}
84
85impl ToSelector for String {
86 fn to_selector(&self) -> Cow<'static, str> {
87 Cow::Owned(self.clone())
88 }
89}
90
91impl ToSelector for Cow<'static, str> {
92 fn to_selector(&self) -> Cow<'static, str> {
93 self.clone()
94 }
95}
96
97impl ToSelector for Element {
98 fn to_selector(&self) -> Cow<'static, str> {
99 self.tag().to_selector()
100 }
101}
102
103impl Rule {
104 #[must_use]
105 pub fn select(mut self, selector: impl ToSelector) -> Self {
106 self.selectors.push(selector.to_selector());
107 self
108 }
109
110 #[must_use]
111 pub fn insert(mut self, property: impl ToString, value: impl ToString) -> Self {
112 self.declarations
113 .insert(property.to_string(), value.to_string());
114 self
115 }
116
117 #[must_use]
118 pub fn styles(
119 mut self,
120 styles: impl IntoIterator<Item = (impl ToString, impl ToString)>,
121 ) -> Self {
122 for (property, value) in styles {
123 self = self.insert(property, value);
124 }
125 self
126 }
127}
128
129pub fn select(selector: impl ToSelector) -> Rule {
130 Rule::default().select(selector)
131}
132
133pub fn class(name: impl Display) -> Rule {
134 select(format!(".{name}"))
135}
136
137pub fn id(name: impl Display) -> Rule {
138 select(format!("#{name}"))
139}
140
141macro_rules! properties {
142 ($($func_name:ident, $prop_name:literal)*) => {$(
143 impl Rule {
144 #[must_use]
145 pub fn $func_name(self, value: impl ToString) -> Self {
146 self.insert($prop_name, value)
147 }
148 }
149 )*};
150}
151
152properties! {
153 align_items, "align-items"
154 background, "background"
155 background_color, "background-color"
156 block_size, "block-size"
157 border, "border"
158 border_bottom, "border-bottom"
159 border_collapse, "border-collapse"
160 border_color, "border-color"
161 border_left_color, "border-left-color"
162 border_right_color, "border-right-color"
163 border_top_color, "border-top-color"
164 border_bottom_color, "border-bottom-color"
165 border_left, "border-left"
166 border_radius, "border-radius"
167 border_bottom_left_radius, "border-bottom-left-radius"
168 border_bottom_right_radius, "border-bottom-right-radius"
169 border_top_left_radius, "border-top-left-radius"
170 border_top_right_radius, "border-top-right-radius"
171 border_right, "border-right"
172 border_spacing, "border-spacing"
173 border_top, "border-top"
174 box_sizing, "box-sizing"
175 color, "color"
176 display, "display"
177 flex, "flex"
178 flex_direction, "flex-direction"
179 font_family, "font-family"
180 font_size, "font-size"
181 font_weight, "font-weight"
182 height, "height"
183 justify_content, "justify-content"
184 line_height, "line-height"
185 list_style, "list-style"
186 margin, "margin"
187 margin_bottom, "margin-bottom"
188 margin_left, "margin-left"
189 margin_right, "margin-right"
190 margin_top, "margin-top"
191 max_width, "max-width"
192 padding, "padding"
193 padding_bottom, "padding-bottom"
194 padding_left, "padding-left"
195 padding_right, "padding-right"
196 padding_top, "padding-top"
197 scroll_behavior, "scroll-behavior"
198 scroll_snap_align, "scroll-snap-align"
199 scroll_snap_stop, "scroll-snap-stop"
200 scroll_snap_type, "scroll-snap-type"
201 text_align, "text-align"
202 text_decoration, "text-decoration"
203 width, "width"
204}
205
206#[derive(Debug, PartialEq)]
207pub struct Number {
208 value: f64,
209 unit: Option<&'static str>,
210}
211
212impl fmt::Display for Number {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 write!(f, "{}", self.value)?;
215 if let Some(unit) = self.unit {
216 write!(f, "{unit}")?;
217 }
218 Ok(())
219 }
220}
221
222impl Mul<f64> for Number {
223 type Output = Number;
224
225 fn mul(mut self, rhs: f64) -> Self::Output {
226 self.value *= rhs;
227 self
228 }
229}
230
231impl Mul<i32> for Number {
232 type Output = Number;
233
234 fn mul(self, rhs: i32) -> Self::Output {
235 self * f64::from(rhs)
236 }
237}
238
239impl Div<f64> for Number {
240 type Output = Number;
241
242 fn div(mut self, rhs: f64) -> Self::Output {
243 self.value /= rhs;
244 self
245 }
246}
247
248impl Div<i32> for Number {
249 type Output = Number;
250
251 fn div(self, rhs: i32) -> Self::Output {
252 self / f64::from(rhs)
253 }
254}
255
256impl Number {
257 #[must_use]
258 pub const fn new(value: f64, unit: Option<&'static str>) -> Self {
259 Self { value, unit }
260 }
261}
262
263#[must_use]
264pub const fn px(value: f64) -> Number {
265 Number::new(value, Some("px"))
266}
267
268#[must_use]
269pub const fn percent(value: f64) -> Number {
270 Number::new(value, Some("%"))
271}
272
273#[must_use]
274pub const fn em(value: f64) -> Number {
275 Number::new(value, Some("em"))
276}
277
278#[must_use]
279pub const fn rem(value: f64) -> Number {
280 Number::new(value, Some("rem"))
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 fn single_declaration_rule() -> Rule {
288 Rule {
289 selectors: vec![".m".into()],
290 declarations: vec![("margin".into(), "20px".into())].into_iter().collect(),
291 }
292 }
293
294 fn multiple_declaration_rule() -> Rule {
295 Rule {
296 selectors: vec!["table".into()],
297 declarations: vec![
298 ("border-spacing".into(), "0".into()),
299 ("border-collapse".into(), "collapse".into()),
300 ]
301 .into_iter()
302 .collect(),
303 }
304 }
305
306 #[test]
307 fn test_display_stylesheet() {
308 let stylesheet = Stylesheet {
309 rules: vec![single_declaration_rule(), multiple_declaration_rule()],
310 };
311 assert_eq!(
312 ".m{margin:20px}table{border-collapse:collapse;border-spacing:0}",
313 stylesheet.to_string()
314 );
315 }
316
317 #[test]
318 fn test_display_rule_single_declaration() {
319 let rule = single_declaration_rule();
320 assert_eq!(".m{margin:20px}", rule.to_string());
321 }
322
323 #[test]
324 fn test_display_rule_multiple_declarations() {
325 let rule = multiple_declaration_rule();
326 assert_eq!(
327 "table{border-collapse:collapse;border-spacing:0}",
328 rule.to_string()
329 );
330 }
331
332 #[test]
333 fn test_rule_builder() {
334 let rule = class("m").styles([("margin", px(20.0))]);
335 assert_eq!(single_declaration_rule(), rule);
336 }
337}