alpine_css/
lib.rs

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}