alpine-css 0.1.0

CSS types for Alpine, a simple HTML template engine
Documentation
use alpine_html::STYLE;
use alpine_markup::{Element, Node};
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt;
use std::fmt::Display;
use std::ops::{Div, Mul};

#[derive(Default, Debug, PartialEq)]
pub struct Stylesheet {
    rules: Vec<Rule>,
}

impl fmt::Display for Stylesheet {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for rule in &self.rules {
            write!(f, "{rule}")?;
        }
        Ok(())
    }
}

impl From<Stylesheet> for Element {
    fn from(stylesheet: Stylesheet) -> Self {
        STYLE.push(stylesheet.to_string())
    }
}

impl From<Stylesheet> for Node {
    fn from(stylesheet: Stylesheet) -> Self {
        Element::from(stylesheet).into()
    }
}

impl Stylesheet {
    #[must_use]
    pub fn push(mut self, rule: Rule) -> Self {
        self.rules.push(rule);
        self
    }

    #[must_use]
    pub fn rules(mut self, rules: impl IntoIterator<Item = Rule>) -> Self {
        self.rules.extend(rules);
        self
    }
}

#[derive(Default, Debug, PartialEq)]
pub struct Rule {
    selectors: Vec<Cow<'static, str>>,
    declarations: BTreeMap<String, String>,
}

impl fmt::Display for Rule {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for (i, selector) in self.selectors.iter().enumerate() {
            if i != 0 {
                write!(f, ",")?;
            }
            write!(f, "{selector}")?;
        }
        write!(f, "{{")?;
        for (i, (property, value)) in self.declarations.iter().enumerate() {
            if i != 0 {
                write!(f, ";")?;
            }
            write!(f, "{property}:{value}")?;
        }
        write!(f, "}}")?;
        Ok(())
    }
}

pub trait ToSelector {
    fn to_selector(&self) -> Cow<'static, str>;
}

impl ToSelector for &'static str {
    fn to_selector(&self) -> Cow<'static, str> {
        Cow::Borrowed(self)
    }
}

impl ToSelector for String {
    fn to_selector(&self) -> Cow<'static, str> {
        Cow::Owned(self.clone())
    }
}

impl ToSelector for Cow<'static, str> {
    fn to_selector(&self) -> Cow<'static, str> {
        self.clone()
    }
}

impl ToSelector for Element {
    fn to_selector(&self) -> Cow<'static, str> {
        self.tag().to_selector()
    }
}

impl Rule {
    #[must_use]
    pub fn select(mut self, selector: impl ToSelector) -> Self {
        self.selectors.push(selector.to_selector());
        self
    }

    #[must_use]
    pub fn insert(mut self, property: impl ToString, value: impl ToString) -> Self {
        self.declarations
            .insert(property.to_string(), value.to_string());
        self
    }

    #[must_use]
    pub fn styles(
        mut self,
        styles: impl IntoIterator<Item = (impl ToString, impl ToString)>,
    ) -> Self {
        for (property, value) in styles {
            self = self.insert(property, value);
        }
        self
    }
}

pub fn select(selector: impl ToSelector) -> Rule {
    Rule::default().select(selector)
}

pub fn class(name: impl Display) -> Rule {
    select(format!(".{name}"))
}

pub fn id(name: impl Display) -> Rule {
    select(format!("#{name}"))
}

macro_rules! properties {
    ($($func_name:ident, $prop_name:literal)*) => {$(
        impl Rule {
            #[must_use]
            pub fn $func_name(self, value: impl ToString) -> Self {
                self.insert($prop_name, value)
            }
        }
    )*};
}

properties! {
    align_items, "align-items"
    background, "background"
    background_color, "background-color"
    block_size, "block-size"
    border, "border"
    border_bottom, "border-bottom"
    border_collapse, "border-collapse"
    border_color, "border-color"
    border_left_color, "border-left-color"
    border_right_color, "border-right-color"
    border_top_color, "border-top-color"
    border_bottom_color, "border-bottom-color"
    border_left, "border-left"
    border_radius, "border-radius"
    border_bottom_left_radius, "border-bottom-left-radius"
    border_bottom_right_radius, "border-bottom-right-radius"
    border_top_left_radius, "border-top-left-radius"
    border_top_right_radius, "border-top-right-radius"
    border_right, "border-right"
    border_spacing, "border-spacing"
    border_top, "border-top"
    box_sizing, "box-sizing"
    color, "color"
    display, "display"
    flex, "flex"
    flex_direction, "flex-direction"
    font_family, "font-family"
    font_size, "font-size"
    font_weight, "font-weight"
    height, "height"
    justify_content, "justify-content"
    line_height, "line-height"
    list_style, "list-style"
    margin, "margin"
    margin_bottom, "margin-bottom"
    margin_left, "margin-left"
    margin_right, "margin-right"
    margin_top, "margin-top"
    max_width, "max-width"
    padding, "padding"
    padding_bottom, "padding-bottom"
    padding_left, "padding-left"
    padding_right, "padding-right"
    padding_top, "padding-top"
    scroll_behavior, "scroll-behavior"
    scroll_snap_align, "scroll-snap-align"
    scroll_snap_stop, "scroll-snap-stop"
    scroll_snap_type, "scroll-snap-type"
    text_align, "text-align"
    text_decoration, "text-decoration"
    width, "width"
}

#[derive(Debug, PartialEq)]
pub struct Number {
    value: f64,
    unit: Option<&'static str>,
}

impl fmt::Display for Number {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.value)?;
        if let Some(unit) = self.unit {
            write!(f, "{unit}")?;
        }
        Ok(())
    }
}

impl Mul<f64> for Number {
    type Output = Number;

    fn mul(mut self, rhs: f64) -> Self::Output {
        self.value *= rhs;
        self
    }
}

impl Mul<i32> for Number {
    type Output = Number;

    fn mul(self, rhs: i32) -> Self::Output {
        self * f64::from(rhs)
    }
}

impl Div<f64> for Number {
    type Output = Number;

    fn div(mut self, rhs: f64) -> Self::Output {
        self.value /= rhs;
        self
    }
}

impl Div<i32> for Number {
    type Output = Number;

    fn div(self, rhs: i32) -> Self::Output {
        self / f64::from(rhs)
    }
}

impl Number {
    #[must_use]
    pub const fn new(value: f64, unit: Option<&'static str>) -> Self {
        Self { value, unit }
    }
}

#[must_use]
pub const fn px(value: f64) -> Number {
    Number::new(value, Some("px"))
}

#[must_use]
pub const fn percent(value: f64) -> Number {
    Number::new(value, Some("%"))
}

#[must_use]
pub const fn em(value: f64) -> Number {
    Number::new(value, Some("em"))
}

#[must_use]
pub const fn rem(value: f64) -> Number {
    Number::new(value, Some("rem"))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn single_declaration_rule() -> Rule {
        Rule {
            selectors: vec![".m".into()],
            declarations: vec![("margin".into(), "20px".into())].into_iter().collect(),
        }
    }

    fn multiple_declaration_rule() -> Rule {
        Rule {
            selectors: vec!["table".into()],
            declarations: vec![
                ("border-spacing".into(), "0".into()),
                ("border-collapse".into(), "collapse".into()),
            ]
            .into_iter()
            .collect(),
        }
    }

    #[test]
    fn test_display_stylesheet() {
        let stylesheet = Stylesheet {
            rules: vec![single_declaration_rule(), multiple_declaration_rule()],
        };
        assert_eq!(
            ".m{margin:20px}table{border-collapse:collapse;border-spacing:0}",
            stylesheet.to_string()
        );
    }

    #[test]
    fn test_display_rule_single_declaration() {
        let rule = single_declaration_rule();
        assert_eq!(".m{margin:20px}", rule.to_string());
    }

    #[test]
    fn test_display_rule_multiple_declarations() {
        let rule = multiple_declaration_rule();
        assert_eq!(
            "table{border-collapse:collapse;border-spacing:0}",
            rule.to_string()
        );
    }

    #[test]
    fn test_rule_builder() {
        let rule = class("m").styles([("margin", px(20.0))]);
        assert_eq!(single_declaration_rule(), rule);
    }
}