use cssparser::ToCss;
use gloo::history::query::FromQuery;
use heck::{ToKebabCase, ToLowerCamelCase, ToTrainCase};
use indexmap::map::Entry;
use indexmap::IndexMap;
use serde::Deserialize;
use std::fmt::Debug;
use std::ops::Index;
use std::str::FromStr;
use stylist::ast::{Sheet, ToStyleStr};
use stylist::Style;
use yew::Classes;
pub use crate::theme::sx;
use crate::theme::sx::sx_to_css::sx_to_css;
use crate::theme::theme_mode::ThemeMode;
use crate::theme::Theme;
mod sx_to_css;
mod sx_value;
mod sx_value_parsing;
use crate::system_props::{CssPropertyTranslator, SYSTEM_PROPERTIES};
use crate::utils::to_property;
pub use sx_value::*;
#[derive(Debug, Default, PartialEq, Clone)]
pub struct Sx {
props: IndexMap<String, SxValue>,
}
pub type Css = Sheet;
impl Sx {
pub fn insert<K: AsRef<str>, V: Into<SxValue>>(&mut self, key: K, value: V) {
let translated = SYSTEM_PROPERTIES.translate(key.as_ref());
let value = value.into();
for translated in translated {
self.props.insert(to_property(translated), value.clone());
}
}
pub fn merge(self, other: Self) -> Self {
let mut sx = self;
for (prop, value) in other.props {
match sx.props.entry(prop) {
Entry::Occupied(mut occ) => match occ.get_mut() {
SxValue::Nested(old_sx) => {
if let SxValue::Nested(sx) = value {
*old_sx = old_sx.clone().merge(sx);
}
}
_ => {}
},
Entry::Vacant(v) => {
v.insert(value);
}
}
}
sx
}
pub fn to_css(self, mode: &ThemeMode, theme: &Theme) -> Css {
let css = sx_to_css(self, mode, theme, None).expect("invalid sx");
Sheet::from_str(&css).unwrap()
}
pub fn properties(&self) -> impl IntoIterator<Item = &str> {
self.props.keys().map(|s| s.as_ref())
}
}
impl Index<&str> for Sx {
type Output = SxValue;
fn index(&self, index: &str) -> &Self::Output {
&self.props[index]
}
}
impl From<SxRef> for Classes {
fn from(value: SxRef) -> Self {
Classes::from(value.style)
}
}
#[macro_export]
macro_rules! sx {
(
$($json:tt)*
) => {
$crate::sx_internal!({ $($json)* })
}
}
#[macro_export]
#[doc(hidden)]
macro_rules! sx_internal {
(@object $object:ident () () ()) => {
};
(@object $object:ident [$key:ident] ($value:expr) , $($rest:tt)*) => {
let _ = $object.insert((stringify!($key)).trim(), sx_internal!($value));
sx_internal!(@object $object () ($($rest)*) ($($rest)*));
};
(@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => {
let _ = $object.insert(($($key)+), $value);
sx_internal!(@object $object () ($($rest)*) ($($rest)*));
};
(@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
sx_internal!(@object $object [$($key)+] (sx_internal!({$($map)*})) $($rest)*);
};
(@object $object:ident ($($key:tt)+) (: |$theme:ident| $func:expr , $($rest:tt)*) $copy:tt) => {
sx_internal!(@object $object [$($key)+] (sx_internal!(|$theme| $func)) $($rest)*);
};
(@object $object:ident ($($key:tt)+) (: |$theme:ident| $func:expr) $copy:tt) => {
sx_internal!(@object $object [$($key)+] (sx_internal!(|$theme| $func)));
};
(@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => {
sx_internal!(@object $object [$($key)+] (sx_internal!($value)) , $($rest)*);
};
(@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => {
sx_internal!(@object $object [$($key)+] ( sx_internal!($value) ) );
};
(@object $object:ident [$key:ident] ($value:expr)) => {
let _ = $object.insert((stringify!($key)).trim(), sx_internal!($value));
};
(@object $object:ident [$($key:tt)+] ($value:expr)) => {
let _ = $object.insert(($($key)+), sx_internal!($value));
};
(@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => {
sx_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*));
};
(@object $object:ident ($($key:tt)*) (: $($unexpected:tt)+) $copy:tt) => {
compile_error!("unexpected colon")
};
(@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => {
sx_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*));
};
({}) => {
crate::theme::sx::Sx::default()
};
({ $($tt:tt)+ }) => {
{
use $crate::theme::sx::*;
use $crate::{sx, sx_internal};
let mut sx: Sx = Sx::default();
sx_internal!(@object sx () ($($tt)+) ($($tt)+));
sx
}
};
(|$theme:ident| $expr:expr) => {
SxValue::Callback(FnSxValue::new(|$theme| $expr))
};
($expr:expr) => {
SxValue::try_from($expr).expect("could not create sxvalue")
};
}
#[derive(Debug, Clone)]
pub struct SxRef {
style: Style,
}
impl SxRef {
pub(crate) fn new(style: Style) -> Self {
Self { style }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_sx_with_macro() {
let sx = sx! {
width: "123.5%",
p: "background.body",
};
assert_eq!(
sx["p"],
SxValue::ThemeToken {
palette: "background".to_string(),
selector: "body".to_string()
}
)
}
#[test]
fn merge_sx() {
let base = sx! {
"bgcolor": "background.level1",
};
let merged = base.clone().merge(sx! {
"bgcolor": SxValue::var("sheet", "background-color", None)
});
assert_eq!(
&base["bgcolor"],
&SxValue::ThemeToken {
palette: "background".to_string(),
selector: "level1".to_string(),
}
);
}
#[test]
fn to_css() {
let theme = Theme::default();
let sx = sx! {
padding: "15px",
color: "background.body"
};
let style = sx.to_css(&ThemeMode::default(), &theme);
println!("style: {style:#?}");
}
#[test]
fn breakpoints_create_media_queries() {
let theme = Theme::new();
let sx = sx! {
padding: "15px",
md: {
padding: "20px"
}
};
let style = sx.to_css(&ThemeMode::default(), &theme);
println!("style: {style:#?}");
}
#[test]
fn sub_class() {
let theme = Theme::new();
let sx = sx! {
".box": {
"p": "10px"
}
};
let style = sx.to_css(&ThemeMode::default(), &theme);
println!("style: {style:#?}");
}
}