diff --git a/src/class.rs b/src/class.rs index 080c6c2..b5ce108 100644 --- a/src/class.rs +++ b/src/class.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use crate::{ - media_queries::{ReducedMotion, Responsive}, + media_queries::{wrap_in_query, ReducedMotion, Responsive}, modifiers::Modifiers, Zephyr, ZephyrError, }; @@ -74,6 +74,8 @@ impl<'a> Class<'a> { .replace('$', "\\$") .replace('\'', "\\'") .replace('*', "\\*") + .replace('<', "\\<") + .replace('@', "\\@") .replace('%', "\\%"); r.insert(0, '.'); r @@ -119,16 +121,19 @@ impl<'a> Class<'a> { } pub fn generate_with_media_query(&self, z: &Zephyr) -> Result { - let mut css = self.generate(z)?; + let css = self.generate(z)?; + dbg!(&self.modifiers); + + let mut queries: Vec = vec![]; if let Some(r) = &self.modifiers.responsive { - css = r.wrap(&css); + queries.extend(r.queries()); } if let Some(r) = &self.modifiers.reduced_motion { - css = r.wrap(&css); + queries.extend(r.queries().iter().map(ToString::to_string)); } - Ok(css) + Ok(wrap_in_query(css, &queries)) } } diff --git a/src/defaults.rs b/src/defaults.rs index e98cd8d..5d3bd61 100644 --- a/src/defaults.rs +++ b/src/defaults.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::SpecialDeclaration; fn vec_to_hashmap(v: &[(&str, &str)]) -> HashMap { - v.into_iter() + v.iter() .map(|(a, b)| (a.to_string(), b.to_string())) .collect::>() } diff --git a/src/media_queries.rs b/src/media_queries.rs index 8beb046..f956176 100644 --- a/src/media_queries.rs +++ b/src/media_queries.rs @@ -1,5 +1,11 @@ #[derive(PartialEq, Debug)] -pub(crate) enum Responsive { +pub(crate) struct Responsive { + breakpoint: Breakpoint, + range: Range, +} + +#[derive(PartialEq, Debug)] +pub(crate) enum Breakpoint { Sm, Md, Lg, @@ -7,27 +13,87 @@ pub(crate) enum Responsive { Xxl, } +#[derive(PartialEq, Debug, Default)] +pub(crate) enum Range { + #[default] + Gte, + Lt, + Exact, +} + impl Responsive { - pub fn wrap(&self, css: &str) -> String { - match self { - Responsive::Sm => wrap_in_query(css, "min-width:640px"), - Responsive::Md => wrap_in_query(css, "min-width:768px"), - Responsive::Lg => wrap_in_query(css, "min-width:1024px"), - Responsive::Xl => wrap_in_query(css, "min-width:1280px"), - Responsive::Xxl => wrap_in_query(css, "min-width:1536px"), + pub fn queries(&self) -> Vec { + match self.range { + Range::Gte => vec![format!("min-width:{}px", self.breakpoint.width())], + Range::Lt => vec![format!("max-width:{}.9px", self.breakpoint.width() - 1)], + Range::Exact => { + if let Some(n) = self.breakpoint.next() { + vec![ + format!("min-width:{}px", self.breakpoint.width()), + format!("max-width:{}.9px", n.width() - 1), + ] + } else { + vec![format!("min-width:{}px", self.breakpoint.width())] + } + } } } + pub fn from_str(s: &str) -> Option { + if s.is_empty() { + return None; + } + + if let Some(p) = s.strip_prefix('<') { + Some(Responsive { + breakpoint: Breakpoint::from_str(p)?, + range: Range::Lt, + }) + } else if let Some(p) = s.strip_prefix('@') { + Some(Responsive { + breakpoint: Breakpoint::from_str(p)?, + range: Range::Exact, + }) + } else { + Some(Responsive { + breakpoint: Breakpoint::from_str(s)?, + range: Range::Gte, + }) + } + } +} + +impl Breakpoint { pub fn from_str(s: &str) -> Option { match s { - "sm" => Some(Responsive::Sm), - "md" => Some(Responsive::Md), - "lg" => Some(Responsive::Lg), - "xl" => Some(Responsive::Xl), - "xxl" => Some(Responsive::Xxl), + "sm" => Some(Self::Sm), + "md" => Some(Self::Md), + "lg" => Some(Self::Lg), + "xl" => Some(Self::Xl), + "xxl" => Some(Self::Xxl), _ => None, } } + + fn width(&self) -> u16 { + match self { + Breakpoint::Sm => 640, + Breakpoint::Md => 768, + Breakpoint::Lg => 1024, + Breakpoint::Xl => 1280, + Breakpoint::Xxl => 1536, + } + } + + const fn next(&self) -> Option { + match self { + Breakpoint::Sm => Some(Breakpoint::Md), + Breakpoint::Md => Some(Breakpoint::Lg), + Breakpoint::Lg => Some(Breakpoint::Xl), + Breakpoint::Xl => Some(Breakpoint::Xxl), + Breakpoint::Xxl => None, + } + } } #[derive(PartialEq, Debug)] @@ -37,10 +103,10 @@ pub enum ReducedMotion { } impl ReducedMotion { - pub fn wrap(&self, css: &str) -> String { + pub fn queries(&self) -> &[&str] { match self { - Self::MotionReduce => wrap_in_query(css, "prefers-reduced-motion:reduce"), - Self::MotionSafe => wrap_in_query(css, "prefers-reduced-motion:no-preference"), + Self::MotionReduce => &["prefers-reduced-motion:reduce"], + Self::MotionSafe => &["prefers-reduced-motion:no-preference"], } } pub fn from_str(s: &str) -> Option { @@ -52,6 +118,70 @@ impl ReducedMotion { } } -fn wrap_in_query(css: &str, query: &str) -> String { - format!("@media({query}){{{css}}}") +pub(crate) fn wrap_in_query(css: String, queries: &[String]) -> String { + if queries.is_empty() { + return css; + } + let query = queries + .iter() + .map(|s| format!("({s})")) + .collect::>() + .join("and"); + format!("@media{query}{{{css}}}") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_responsive() { + let r = Responsive::from_str(" Modifiers<'a> { let mut reduced_motion = None; for m in &all { - responsive = Responsive::from_str(m); - reduced_motion = ReducedMotion::from_str(m); + responsive = responsive.or_else(|| Responsive::from_str(m)); + reduced_motion = reduced_motion.or_else(|| ReducedMotion::from_str(m)); } Self { diff --git a/src/tests.rs b/src/tests.rs index c4448ed..71373ef 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -121,6 +121,24 @@ fn generate_with_media_query() { classes, r#"@media(min-width:640px){.m\[1rem\]sm{margin:1rem}}"# ); + + let classes = z.generate_classes(["m[1rem]