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);
}
}