diff --git a/src/builtin/color/hsl.rs b/src/builtin/color/hsl.rs index f8b7906..1ec2e4d 100644 --- a/src/builtin/color/hsl.rs +++ b/src/builtin/color/hsl.rs @@ -7,158 +7,227 @@ use crate::units::Unit; use crate::value::{Number, Value}; pub(crate) fn register(f: &mut HashMap) { - decl!(f "hsl", |args, _| { - let hue = match arg!(args, 0, "hue") { - Value::Dimension(n, _) => n, - v => return Err(format!("$hue: {} is not a number.", v).into()), - }; - let saturation = match arg!(args, 1, "saturation") { - Value::Dimension(n, _) => n / Number::from(100), - v => return Err(format!("$saturation: {} is not a number.", v).into()), - }; - let luminance = match arg!(args, 2, "luminance") { - Value::Dimension(n, _) => n / Number::from(100), - v => return Err(format!("$luminance: {} is not a number.", v).into()), - }; - let alpha = match arg!(args, 3, "alpha"=Value::Dimension(Number::from(1), Unit::None)) { - Value::Dimension(n, Unit::None) => n, - Value::Dimension(n, Unit::Percent) => n / Number::from(100), - v @ Value::Dimension(..) => return Err(format!("$alpha: Expected {} to have no units or \"%\".", v).into()), - v => return Err(format!("$alpha: {} is not a number.", v).into()), - }; - Ok(Value::Color(Color::from_hsla(hue, saturation, luminance, alpha))) - }); - decl!(f "hsla", |args, _| { - let hue = match arg!(args, 0, "hue") { - Value::Dimension(n, _) => n, - v => return Err(format!("$hue: {} is not a number.", v).into()), - }; - let saturation = match arg!(args, 1, "saturation") { - Value::Dimension(n, _) => n / Number::from(100), - v => return Err(format!("$saturation: {} is not a number.", v).into()), - }; - let luminance = match arg!(args, 2, "luminance") { - Value::Dimension(n, _) => n / Number::from(100), - v => return Err(format!("$luminance: {} is not a number.", v).into()), - }; - let alpha = match arg!(args, 3, "alpha") { - Value::Dimension(n, Unit::None) => n, - Value::Dimension(n, Unit::Percent) => n / Number::from(100), - v @ Value::Dimension(..) => return Err(format!("$alpha: Expected {} to have no units or \"%\".", v).into()), - v => return Err(format!("$alpha: {} is not a number.", v).into()), - }; - Ok(Value::Color(Color::from_hsla(hue, saturation, luminance, alpha))) - }); - decl!(f "hue", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.hue(), Unit::Deg)), - v => Err(format!("$color: {} is not a color.", v).into()), - } - }); - decl!(f "saturation", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.saturation(), Unit::Percent)), - v => Err(format!("$color: {} is not a color.", v).into()), - } - }); - decl!(f "lightness", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.lightness(), Unit::Percent)), - v => Err(format!("$color: {} is not a color.", v).into()), - } - }); - decl!(f "adjust-hue", |args, _| { - max_args!(args, 2); - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), - }; - let degrees = match arg!(args, 1, "degrees") { - Value::Dimension(n, _) => n, - v => return Err(format!("$degrees: {} is not a number.", v).into()), - }; - Ok(Value::Color(color.adjust_hue(degrees))) - }); - decl!(f "lighten", |args, _| { - max_args!(args, 2); - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), - }; - let amount = match arg!(args, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 100) / Number::from(100), - v => return Err(format!("$amount: {} is not a number.", v).into()) - }; - Ok(Value::Color(color.lighten(amount))) - }); - decl!(f "darken", |args, _| { - max_args!(args, 2); - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), - }; - let amount = match arg!(args, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 100) / Number::from(100), - v => return Err(format!("$amount: {} is not a number.", v).into()) - }; - Ok(Value::Color(color.darken(amount))) - }); - decl!(f "saturate", |args, _| { - max_args!(args, 2); - let amount = match arg!(args, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 100) / Number::from(100), - v => return Err(format!("$amount: {} is not a number.", v).into()) - }; - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - Value::Dimension(n, u) => return Ok(Value::Ident(format!("saturate({}{})", n, u), QuoteKind::None)), - v => return Err(format!("$color: {} is not a color.", v).into()), - }; - Ok(Value::Color(color.saturate(amount))) - }); - decl!(f "desaturate", |args, _| { - max_args!(args, 2); - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), - }; - let amount = match arg!(args, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 100) / Number::from(100), - v => return Err(format!("$amount: {} is not a number.", v).into()) - }; - Ok(Value::Color(color.desaturate(amount))) - }); - decl!(f "grayscale", |args, _| { - max_args!(args, 1); - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - Value::Dimension(n, u) => return Ok(Value::Ident(format!("grayscale({}{})", n, u), QuoteKind::None)), - v => return Err(format!("$color: {} is not a color.", v).into()), - }; - Ok(Value::Color(color.desaturate(Number::from(1)))) - }); - decl!(f "complement", |args, _| { - max_args!(args, 1); - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), - }; - Ok(Value::Color(color.complement())) - }); - decl!(f "invert", |args, _| { - max_args!(args, 2); - let weight = match arg!(args, 1, "weight"=Value::Dimension(Number::from(100), Unit::Percent)) { - Value::Dimension(n, u) => bound!("weight", n, u, 0, 100) / Number::from(100), - v => return Err(format!("$weight: {} is not a number.", v).into()), - }; - match arg!(args, 0, "color") { - Value::Color(c) => Ok(Value::Color(c.invert(weight))), - Value::Dimension(n, Unit::Percent) => Ok(Value::Ident(format!("invert({}%)", n), QuoteKind::None)), - Value::Dimension(..) => Err("Only one argument may be passed to the plain-CSS invert() function.".into()), - v => Err(format!("$color: {} is not a color.", v).into()), - } - }); + f.insert( + "hsl".to_owned(), + Box::new(|args, _| { + let hue = match arg!(args, 0, "hue") { + Value::Dimension(n, _) => n, + v => return Err(format!("$hue: {} is not a number.", v).into()), + }; + let saturation = match arg!(args, 1, "saturation") { + Value::Dimension(n, _) => n / Number::from(100), + v => return Err(format!("$saturation: {} is not a number.", v).into()), + }; + let luminance = match arg!(args, 2, "luminance") { + Value::Dimension(n, _) => n / Number::from(100), + v => return Err(format!("$luminance: {} is not a number.", v).into()), + }; + let alpha = match arg!( + args, + 3, + "alpha" = Value::Dimension(Number::from(1), Unit::None) + ) { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => n / Number::from(100), + v @ Value::Dimension(..) => { + return Err(format!("$alpha: Expected {} to have no units or \"%\".", v).into()) + } + v => return Err(format!("$alpha: {} is not a number.", v).into()), + }; + Ok(Value::Color(Color::from_hsla( + hue, saturation, luminance, alpha, + ))) + }), + ); + f.insert( + "hsla".to_owned(), + Box::new(|args, _| { + let hue = match arg!(args, 0, "hue") { + Value::Dimension(n, _) => n, + v => return Err(format!("$hue: {} is not a number.", v).into()), + }; + let saturation = match arg!(args, 1, "saturation") { + Value::Dimension(n, _) => n / Number::from(100), + v => return Err(format!("$saturation: {} is not a number.", v).into()), + }; + let luminance = match arg!(args, 2, "luminance") { + Value::Dimension(n, _) => n / Number::from(100), + v => return Err(format!("$luminance: {} is not a number.", v).into()), + }; + let alpha = match arg!(args, 3, "alpha") { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => n / Number::from(100), + v @ Value::Dimension(..) => { + return Err(format!("$alpha: Expected {} to have no units or \"%\".", v).into()) + } + v => return Err(format!("$alpha: {} is not a number.", v).into()), + }; + Ok(Value::Color(Color::from_hsla( + hue, saturation, luminance, alpha, + ))) + }), + ); + f.insert( + "hue".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.hue(), Unit::Deg)), + v => Err(format!("$color: {} is not a color.", v).into()), + } + }), + ); + f.insert( + "saturation".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.saturation(), Unit::Percent)), + v => Err(format!("$color: {} is not a color.", v).into()), + } + }), + ); + f.insert( + "lightness".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.lightness(), Unit::Percent)), + v => Err(format!("$color: {} is not a color.", v).into()), + } + }), + ); + f.insert( + "adjust-hue".to_owned(), + Box::new(|args, _| { + max_args!(args, 2); + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + let degrees = match arg!(args, 1, "degrees") { + Value::Dimension(n, _) => n, + v => return Err(format!("$degrees: {} is not a number.", v).into()), + }; + Ok(Value::Color(color.adjust_hue(degrees))) + }), + ); + f.insert( + "lighten".to_owned(), + Box::new(|args, _| { + max_args!(args, 2); + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + let amount = match arg!(args, 1, "amount") { + Value::Dimension(n, u) => bound!("amount", n, u, 0, 100) / Number::from(100), + v => return Err(format!("$amount: {} is not a number.", v).into()), + }; + Ok(Value::Color(color.lighten(amount))) + }), + ); + f.insert( + "darken".to_owned(), + Box::new(|args, _| { + max_args!(args, 2); + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + let amount = match arg!(args, 1, "amount") { + Value::Dimension(n, u) => bound!("amount", n, u, 0, 100) / Number::from(100), + v => return Err(format!("$amount: {} is not a number.", v).into()), + }; + Ok(Value::Color(color.darken(amount))) + }), + ); + f.insert( + "saturate".to_owned(), + Box::new(|args, _| { + max_args!(args, 2); + let amount = match arg!(args, 1, "amount") { + Value::Dimension(n, u) => bound!("amount", n, u, 0, 100) / Number::from(100), + v => return Err(format!("$amount: {} is not a number.", v).into()), + }; + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + Value::Dimension(n, u) => { + return Ok(Value::Ident( + format!("saturate({}{})", n, u), + QuoteKind::None, + )) + } + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + Ok(Value::Color(color.saturate(amount))) + }), + ); + f.insert( + "desaturate".to_owned(), + Box::new(|args, _| { + max_args!(args, 2); + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + let amount = match arg!(args, 1, "amount") { + Value::Dimension(n, u) => bound!("amount", n, u, 0, 100) / Number::from(100), + v => return Err(format!("$amount: {} is not a number.", v).into()), + }; + Ok(Value::Color(color.desaturate(amount))) + }), + ); + f.insert( + "grayscale".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + Value::Dimension(n, u) => { + return Ok(Value::Ident( + format!("grayscale({}{})", n, u), + QuoteKind::None, + )) + } + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + Ok(Value::Color(color.desaturate(Number::from(1)))) + }), + ); + f.insert( + "complement".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + Ok(Value::Color(color.complement())) + }), + ); + f.insert( + "invert".to_owned(), + Box::new(|args, _| { + max_args!(args, 2); + let weight = match arg!( + args, + 1, + "weight" = Value::Dimension(Number::from(100), Unit::Percent) + ) { + Value::Dimension(n, u) => bound!("weight", n, u, 0, 100) / Number::from(100), + v => return Err(format!("$weight: {} is not a number.", v).into()), + }; + match arg!(args, 0, "color") { + Value::Color(c) => Ok(Value::Color(c.invert(weight))), + Value::Dimension(n, Unit::Percent) => { + Ok(Value::Ident(format!("invert({}%)", n), QuoteKind::None)) + } + Value::Dimension(..) => Err( + "Only one argument may be passed to the plain-CSS invert() function.".into(), + ), + v => Err(format!("$color: {} is not a color.", v).into()), + } + }), + ); } diff --git a/src/builtin/color/opacity.rs b/src/builtin/color/opacity.rs index 42cf4db..5dd1892 100644 --- a/src/builtin/color/opacity.rs +++ b/src/builtin/color/opacity.rs @@ -7,67 +7,88 @@ use crate::value::Number; use crate::value::Value; pub(crate) fn register(f: &mut HashMap) { - decl!(f "alpha", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.alpha(), Unit::None)), - v => Err(format!("$color: {} is not a color.", v).into()), - } - }); - decl!(f "opacity", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.alpha(), Unit::None)), - Value::Dimension(num, unit) => Ok(Value::Ident(format!("opacity({}{})", num , unit), QuoteKind::None)), - v => Err(format!("$color: {} is not a color.", v).into()), - } - }); - decl!(f "opacify", |args, _| { - max_args!(args, 2); - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), - }; - let amount = match arg!(args, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 1), - v => return Err(format!("$amount: {} is not a number.", v).into()), - }; - Ok(Value::Color(color.fade_in(amount))) - }); - decl!(f "fade-in", |args, _| { - max_args!(args, 2); - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), - }; - let amount = match arg!(args, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 1), - v => return Err(format!("$amount: {} is not a number.", v).into()), - }; - Ok(Value::Color(color.fade_in(amount))) - }); - decl!(f "transparentize", |args, _| { - max_args!(args, 2); - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), - }; - let amount = match arg!(args, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 1), - v => return Err(format!("$amount: {} is not a number.", v).into()), - }; - Ok(Value::Color(color.fade_out(amount))) - }); - decl!(f "fade-out", |args, _| { - max_args!(args, 2); - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), - }; - let amount = match arg!(args, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 1), - v => return Err(format!("$amount: {} is not a number.", v).into()), - }; - Ok(Value::Color(color.fade_out(amount))) - }); + f.insert( + "alpha".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.alpha(), Unit::None)), + v => Err(format!("$color: {} is not a color.", v).into()), + } + }), + ); + f.insert( + "opacity".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.alpha(), Unit::None)), + Value::Dimension(num, unit) => Ok(Value::Ident( + format!("opacity({}{})", num, unit), + QuoteKind::None, + )), + v => Err(format!("$color: {} is not a color.", v).into()), + } + }), + ); + f.insert( + "opacify".to_owned(), + Box::new(|args, _| { + max_args!(args, 2); + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + let amount = match arg!(args, 1, "amount") { + Value::Dimension(n, u) => bound!("amount", n, u, 0, 1), + v => return Err(format!("$amount: {} is not a number.", v).into()), + }; + Ok(Value::Color(color.fade_in(amount))) + }), + ); + f.insert( + "fade-in".to_owned(), + Box::new(|args, _| { + max_args!(args, 2); + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + let amount = match arg!(args, 1, "amount") { + Value::Dimension(n, u) => bound!("amount", n, u, 0, 1), + v => return Err(format!("$amount: {} is not a number.", v).into()), + }; + Ok(Value::Color(color.fade_in(amount))) + }), + ); + f.insert( + "transparentize".to_owned(), + Box::new(|args, _| { + max_args!(args, 2); + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + let amount = match arg!(args, 1, "amount") { + Value::Dimension(n, u) => bound!("amount", n, u, 0, 1), + v => return Err(format!("$amount: {} is not a number.", v).into()), + }; + Ok(Value::Color(color.fade_out(amount))) + }), + ); + f.insert( + "fade-out".to_owned(), + Box::new(|args, _| { + max_args!(args, 2); + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + let amount = match arg!(args, 1, "amount") { + Value::Dimension(n, u) => bound!("amount", n, u, 0, 1), + v => return Err(format!("$amount: {} is not a number.", v).into()), + }; + Ok(Value::Color(color.fade_out(amount))) + }), + ); } diff --git a/src/builtin/color/other.rs b/src/builtin/color/other.rs index bcac7f1..7b5de06 100644 --- a/src/builtin/color/other.rs +++ b/src/builtin/color/other.rs @@ -36,7 +36,7 @@ macro_rules! opt_arg { } pub(crate) fn register(f: &mut HashMap) { - decl!(f "change-color", |args, _| { + f.insert("change-color".to_owned(), Box::new(|args, _| { if args.get("1").is_some() { return Err("Only one positional argument is allowed. All other arguments must be passed by name.".into()); } @@ -75,123 +75,150 @@ pub(crate) fn register(f: &mut HashMap) { } else { color })) - }); - decl!(f "adjust-color", |args, _| { - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), - }; + })); + f.insert( + "adjust-color".to_owned(), + Box::new(|args, _| { + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; - opt_arg!(args, alpha, "alpha", -1, 1); - opt_arg!(args, red, "red", -255, 255); - opt_arg!(args, green, "green", -255, 255); - opt_arg!(args, blue, "blue", -255, 255); + opt_arg!(args, alpha, "alpha", -1, 1); + opt_arg!(args, red, "red", -255, 255); + opt_arg!(args, green, "green", -255, 255); + opt_arg!(args, blue, "blue", -255, 255); - if red.is_some() || green.is_some() || blue.is_some() { - return - Ok(Value::Color( - Color::from_rgba( + if red.is_some() || green.is_some() || blue.is_some() { + return Ok(Value::Color(Color::from_rgba( color.red() + red.unwrap_or(Number::from(0)), color.green() + green.unwrap_or(Number::from(0)), color.blue() + blue.unwrap_or(Number::from(0)), - color.alpha() + alpha.unwrap_or(Number::from(0)) - ) - )) - } + color.alpha() + alpha.unwrap_or(Number::from(0)), + ))); + } - let hue = match arg!(args, -1, "hue"=Value::Null) { - Value::Dimension(n, Unit::None) - | Value::Dimension(n, Unit::Percent) - | Value::Dimension(n, Unit::Deg) => Some(n), - Value::Null => None, - _ => todo!("expected either unitless or % number for hue"), - }; + let hue = match arg!(args, -1, "hue" = Value::Null) { + Value::Dimension(n, Unit::None) + | Value::Dimension(n, Unit::Percent) + | Value::Dimension(n, Unit::Deg) => Some(n), + Value::Null => None, + _ => todo!("expected either unitless or % number for hue"), + }; - opt_arg!(hsl: args, saturation, "saturation", -100, 100); - opt_arg!(hsl: args, luminance, "lightness", -100, 100); + opt_arg!(hsl: args, saturation, "saturation", -100, 100); + opt_arg!(hsl: args, luminance, "lightness", -100, 100); - if hue.is_some() || saturation.is_some() || luminance.is_some() { - // Color::as_hsla() returns more exact values than Color::hue(), etc. - let (this_hue, this_saturation, this_luminance, this_alpha) = color.as_hsla(); - return Ok(Value::Color( - Color::from_hsla( + if hue.is_some() || saturation.is_some() || luminance.is_some() { + // Color::as_hsla() returns more exact values than Color::hue(), etc. + let (this_hue, this_saturation, this_luminance, this_alpha) = color.as_hsla(); + return Ok(Value::Color(Color::from_hsla( this_hue + hue.unwrap_or(Number::from(0)), this_saturation + saturation.unwrap_or(Number::from(0)), this_luminance + luminance.unwrap_or(Number::from(0)), - this_alpha + alpha.unwrap_or(Number::from(0)) - ) - )) - } + this_alpha + alpha.unwrap_or(Number::from(0)), + ))); + } - Ok(Value::Color(if let Some(a) = alpha { - let temp_alpha = color.alpha(); - color.with_alpha(temp_alpha + a) - } else { - color - })) - }); - decl!(f "scale-color", |args, _| { - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), - }; + Ok(Value::Color(if let Some(a) = alpha { + let temp_alpha = color.alpha(); + color.with_alpha(temp_alpha + a) + } else { + color + })) + }), + ); + f.insert( + "scale-color".to_owned(), + Box::new(|args, _| { + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; - opt_arg!(scale: args, alpha, "alpha", -100, 100); - opt_arg!(scale: args, red, "red", -100, 100); - opt_arg!(scale: args, green, "green", -100, 100); - opt_arg!(scale: args, blue, "blue", -100, 100); + opt_arg!(scale: args, alpha, "alpha", -100, 100); + opt_arg!(scale: args, red, "red", -100, 100); + opt_arg!(scale: args, green, "green", -100, 100); + opt_arg!(scale: args, blue, "blue", -100, 100); - if red.is_some() || green.is_some() || blue.is_some() { - return - Ok(Value::Color( - Color::from_rgba( - scale(color.red(), red.unwrap_or(Number::from(0)), Number::from(255)), - scale(color.green(), green.unwrap_or(Number::from(0)), Number::from(255)), - scale(color.blue(), blue.unwrap_or(Number::from(0)), Number::from(255)), - scale(color.alpha(), alpha.unwrap_or(Number::from(0)), Number::from(1)), - ) - )) - } + if red.is_some() || green.is_some() || blue.is_some() { + return Ok(Value::Color(Color::from_rgba( + scale( + color.red(), + red.unwrap_or(Number::from(0)), + Number::from(255), + ), + scale( + color.green(), + green.unwrap_or(Number::from(0)), + Number::from(255), + ), + scale( + color.blue(), + blue.unwrap_or(Number::from(0)), + Number::from(255), + ), + scale( + color.alpha(), + alpha.unwrap_or(Number::from(0)), + Number::from(1), + ), + ))); + } - let hue = match arg!(args, -1, "hue"=Value::Null) { - Value::Dimension(n, Unit::None) - | Value::Dimension(n, Unit::Percent) - | Value::Dimension(n, Unit::Deg) => Some(n), - Value::Null => None, - _ => todo!("expected either unitless or % number for hue"), - }; + let hue = match arg!(args, -1, "hue" = Value::Null) { + Value::Dimension(n, Unit::None) + | Value::Dimension(n, Unit::Percent) + | Value::Dimension(n, Unit::Deg) => Some(n), + Value::Null => None, + _ => todo!("expected either unitless or % number for hue"), + }; - opt_arg!(scale: args, saturation, "saturation", -100, 100); - opt_arg!(scale: args, luminance, "lightness", -100, 100); + opt_arg!(scale: args, saturation, "saturation", -100, 100); + opt_arg!(scale: args, luminance, "lightness", -100, 100); - if hue.is_some() || saturation.is_some() || luminance.is_some() { - // Color::as_hsla() returns more exact values than Color::hue(), etc. - let (this_hue, this_saturation, this_luminance, this_alpha) = color.as_hsla(); - return Ok(Value::Color( - Color::from_hsla( + if hue.is_some() || saturation.is_some() || luminance.is_some() { + // Color::as_hsla() returns more exact values than Color::hue(), etc. + let (this_hue, this_saturation, this_luminance, this_alpha) = color.as_hsla(); + return Ok(Value::Color(Color::from_hsla( scale(this_hue, hue.unwrap_or(Number::from(0)), Number::from(360)), - scale(this_saturation, saturation.unwrap_or(Number::from(0)), Number::from(1)), - scale(this_luminance, luminance.unwrap_or(Number::from(0)), Number::from(1)), - scale(this_alpha, alpha.unwrap_or(Number::from(0)), Number::from(1)), - ) - )) - } + scale( + this_saturation, + saturation.unwrap_or(Number::from(0)), + Number::from(1), + ), + scale( + this_luminance, + luminance.unwrap_or(Number::from(0)), + Number::from(1), + ), + scale( + this_alpha, + alpha.unwrap_or(Number::from(0)), + Number::from(1), + ), + ))); + } - Ok(Value::Color(if let Some(a) = alpha { - let temp_alpha = color.alpha(); - color.with_alpha(scale(temp_alpha, a, Number::from(1))) - } else { - color - })) - }); - decl!(f "ie-hex-str", |args, _| { - max_args!(args, 1); - let color = match arg!(args, 0, "color") { - Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), - }; - Ok(Value::Ident(color.to_ie_hex_str(), QuoteKind::None)) - }); + Ok(Value::Color(if let Some(a) = alpha { + let temp_alpha = color.alpha(); + color.with_alpha(scale(temp_alpha, a, Number::from(1))) + } else { + color + })) + }), + ); + f.insert( + "ie-hex-str".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + Ok(Value::Ident(color.to_ie_hex_str(), QuoteKind::None)) + }), + ); } fn scale(val: Number, by: Number, max: Number) -> Number { diff --git a/src/builtin/color/rgb.rs b/src/builtin/color/rgb.rs index ad38c59..c66bb73 100644 --- a/src/builtin/color/rgb.rs +++ b/src/builtin/color/rgb.rs @@ -6,191 +6,271 @@ use crate::units::Unit; use crate::value::{Number, Value}; pub(crate) fn register(f: &mut HashMap) { - decl!(f "rgb", |args, _| { - if args.len() == 1 { - let mut channels = match arg!(args, 0, "channels") { - Value::List(v, _) => v, - _ => return Err("Missing element $green.".into()) - }; + f.insert( + "rgb".to_owned(), + Box::new(|args, _| { + if args.len() == 1 { + let mut channels = match arg!(args, 0, "channels") { + Value::List(v, _) => v, + _ => return Err("Missing element $green.".into()), + }; - assert_eq!(channels.len(), 3_usize); + assert_eq!(channels.len(), 3_usize); - let blue = match channels.pop() { - Some(Value::Dimension(n, Unit::None)) => n, - Some(Value::Dimension(n, Unit::Percent)) => n / Number::from(100), - Some(v) => return Err(format!("$blue: {} is not a color", v).into()), - None => return Err("Missing element $blue.".into()), - }; + let blue = match channels.pop() { + Some(Value::Dimension(n, Unit::None)) => n, + Some(Value::Dimension(n, Unit::Percent)) => n / Number::from(100), + Some(v) => return Err(format!("$blue: {} is not a color", v).into()), + None => return Err("Missing element $blue.".into()), + }; - let green = match channels.pop() { - Some(Value::Dimension(n, Unit::None)) => n, - Some(Value::Dimension(n, Unit::Percent)) => n / Number::from(100), - Some(v) => return Err(format!("$green: {} is not a color", v).into()), - None => return Err("Missing element $green.".into()), - }; + let green = match channels.pop() { + Some(Value::Dimension(n, Unit::None)) => n, + Some(Value::Dimension(n, Unit::Percent)) => n / Number::from(100), + Some(v) => return Err(format!("$green: {} is not a color", v).into()), + None => return Err("Missing element $green.".into()), + }; - let red = match channels.pop() { - Some(Value::Dimension(n, Unit::None)) => n, - Some(Value::Dimension(n, Unit::Percent)) => n / Number::from(100), - Some(v) => return Err(format!("$red: {} is not a color", v).into()), - None => return Err("Missing element $red.".into()), - }; + let red = match channels.pop() { + Some(Value::Dimension(n, Unit::None)) => n, + Some(Value::Dimension(n, Unit::Percent)) => n / Number::from(100), + Some(v) => return Err(format!("$red: {} is not a color", v).into()), + None => return Err("Missing element $red.".into()), + }; - let color = Color::from_rgba(red, green, blue, Number::from(1)); + let color = Color::from_rgba(red, green, blue, Number::from(1)); - Ok(Value::Color(color)) + Ok(Value::Color(color)) + } else if args.len() == 2 { + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + let alpha = match arg!(args, 1, "alpha") { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => n / Number::from(100), + v @ Value::Dimension(..) => { + return Err( + format!("$alpha: Expected {} to have no units or \"%\".", v).into() + ) + } + v => return Err(format!("$alpha: {} is not a number.", v).into()), + }; + Ok(Value::Color(color.with_alpha(alpha))) + } else { + let red = match arg!(args, 0, "red") { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => { + (n / Number::from(100)) * Number::from(255) + } + v @ Value::Dimension(..) => { + return Err( + format!("$red: Expected {} to have no units or \"%\".", v).into() + ) + } + v => return Err(format!("$red: {} is not a number.", v).into()), + }; + let green = match arg!(args, 1, "green") { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => { + (n / Number::from(100)) * Number::from(255) + } + v @ Value::Dimension(..) => { + return Err( + format!("$green: Expected {} to have no units or \"%\".", v).into() + ) + } + v => return Err(format!("$green: {} is not a number.", v).into()), + }; + let blue = match arg!(args, 2, "blue") { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => { + (n / Number::from(100)) * Number::from(255) + } + v @ Value::Dimension(..) => { + return Err( + format!("$blue: Expected {} to have no units or \"%\".", v).into() + ) + } + v => return Err(format!("$blue: {} is not a number.", v).into()), + }; + let alpha = match arg!( + args, + 3, + "alpha" = Value::Dimension(Number::from(1), Unit::None) + ) { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => n / Number::from(100), + v @ Value::Dimension(..) => { + return Err( + format!("$alpha: Expected {} to have no units or \"%\".", v).into() + ) + } + v => return Err(format!("$alpha: {} is not a number.", v).into()), + }; + Ok(Value::Color(Color::from_rgba(red, green, blue, alpha))) + } + }), + ); + f.insert( + "rgba".to_owned(), + Box::new(|args, _| { + if args.len() == 1 { + let mut channels = match arg!(args, 0, "channels") { + Value::List(v, _) => v, + _ => return Err("Missing element $green.".into()), + }; - } else if args.len() == 2 { - let color = match arg!(args, 0, "color") { + assert_eq!(channels.len(), 3_usize); + + let blue = match channels.pop() { + Some(Value::Dimension(n, Unit::None)) => n, + Some(Value::Dimension(n, Unit::Percent)) => n / Number::from(100), + Some(v) => return Err(format!("$blue: {} is not a color", v).into()), + None => return Err("Missing element $blue.".into()), + }; + + let green = match channels.pop() { + Some(Value::Dimension(n, Unit::None)) => n, + Some(Value::Dimension(n, Unit::Percent)) => n / Number::from(100), + Some(v) => return Err(format!("$green: {} is not a color", v).into()), + None => return Err("Missing element $green.".into()), + }; + + let red = match channels.pop() { + Some(Value::Dimension(n, Unit::None)) => n, + Some(Value::Dimension(n, Unit::Percent)) => n / Number::from(100), + Some(v) => return Err(format!("$red: {} is not a color", v).into()), + None => return Err("Missing element $red.".into()), + }; + + let color = Color::from_rgba(red, green, blue, Number::from(1)); + + Ok(Value::Color(color)) + } else if args.len() == 2 { + let color = match arg!(args, 0, "color") { + Value::Color(c) => c, + v => return Err(format!("$color: {} is not a color.", v).into()), + }; + let alpha = match arg!(args, 1, "alpha") { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => n / Number::from(100), + v @ Value::Dimension(..) => { + return Err( + format!("$alpha: Expected {} to have no units or \"%\".", v).into() + ) + } + v => return Err(format!("$alpha: {} is not a number.", v).into()), + }; + Ok(Value::Color(color.with_alpha(alpha))) + } else { + let red = match arg!(args, 0, "red") { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => { + (n / Number::from(100)) * Number::from(255) + } + v @ Value::Dimension(..) => { + return Err( + format!("$red: Expected {} to have no units or \"%\".", v).into() + ) + } + v => return Err(format!("$red: {} is not a number.", v).into()), + }; + let green = match arg!(args, 1, "green") { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => { + (n / Number::from(100)) * Number::from(255) + } + v @ Value::Dimension(..) => { + return Err( + format!("$green: Expected {} to have no units or \"%\".", v).into() + ) + } + v => return Err(format!("$green: {} is not a number.", v).into()), + }; + let blue = match arg!(args, 2, "blue") { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => { + (n / Number::from(100)) * Number::from(255) + } + v @ Value::Dimension(..) => { + return Err( + format!("$blue: Expected {} to have no units or \"%\".", v).into() + ) + } + v => return Err(format!("$blue: {} is not a number.", v).into()), + }; + let alpha = match arg!( + args, + 3, + "alpha" = Value::Dimension(Number::from(1), Unit::None) + ) { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => n / Number::from(100), + v @ Value::Dimension(..) => { + return Err( + format!("$alpha: Expected {} to have no units or \"%\".", v).into() + ) + } + v => return Err(format!("$alpha: {} is not a number.", v).into()), + }; + Ok(Value::Color(Color::from_rgba(red, green, blue, alpha))) + } + }), + ); + f.insert( + "red".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.red(), Unit::None)), + v => Err(format!("$color: {} is not a color.", v).into()), + } + }), + ); + f.insert( + "green".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.green(), Unit::None)), + v => Err(format!("$color: {} is not a color.", v).into()), + } + }), + ); + f.insert( + "blue".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.blue(), Unit::None)), + v => Err(format!("$color: {} is not a color.", v).into()), + } + }), + ); + f.insert( + "mix".to_owned(), + Box::new(|args, _| { + max_args!(args, 3); + let color1 = match arg!(args, 0, "color1") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), - }; - let alpha = match arg!(args, 1, "alpha") { - Value::Dimension(n, Unit::None) => n, - Value::Dimension(n, Unit::Percent) => n / Number::from(100), - v @ Value::Dimension(..) => return Err(format!("$alpha: Expected {} to have no units or \"%\".", v).into()), - v => return Err(format!("$alpha: {} is not a number.", v).into()), - }; - Ok(Value::Color(color.with_alpha(alpha))) - } else { - let red = match arg!(args, 0, "red") { - Value::Dimension(n, Unit::None) => n, - Value::Dimension(n, Unit::Percent) => (n / Number::from(100)) * Number::from(255), - v @ Value::Dimension(..) => return Err(format!("$red: Expected {} to have no units or \"%\".", v).into()), - v => return Err(format!("$red: {} is not a number.", v).into()), - }; - let green = match arg!(args, 1, "green") { - Value::Dimension(n, Unit::None) => n, - Value::Dimension(n, Unit::Percent) => (n / Number::from(100)) * Number::from(255), - v @ Value::Dimension(..) => return Err(format!("$green: Expected {} to have no units or \"%\".", v).into()), - v => return Err(format!("$green: {} is not a number.", v).into()), - }; - let blue = match arg!(args, 2, "blue") { - Value::Dimension(n, Unit::None) => n, - Value::Dimension(n, Unit::Percent) => (n / Number::from(100)) * Number::from(255), - v @ Value::Dimension(..) => return Err(format!("$blue: Expected {} to have no units or \"%\".", v).into()), - v => return Err(format!("$blue: {} is not a number.", v).into()), - }; - let alpha = match arg!(args, 3, "alpha"=Value::Dimension(Number::from(1), Unit::None)) { - Value::Dimension(n, Unit::None) => n, - Value::Dimension(n, Unit::Percent) => n / Number::from(100), - v @ Value::Dimension(..) => return Err(format!("$alpha: Expected {} to have no units or \"%\".", v).into()), - v => return Err(format!("$alpha: {} is not a number.", v).into()), - }; - Ok(Value::Color(Color::from_rgba(red, green, blue, alpha))) - } - }); - decl!(f "rgba", |args, _| { - if args.len() == 1 { - let mut channels = match arg!(args, 0, "channels") { - Value::List(v, _) => v, - _ => return Err("Missing element $green.".into()) + v => return Err(format!("$color1: {} is not a color.", v).into()), }; - assert_eq!(channels.len(), 3_usize); - - let blue = match channels.pop() { - Some(Value::Dimension(n, Unit::None)) => n, - Some(Value::Dimension(n, Unit::Percent)) => n / Number::from(100), - Some(v) => return Err(format!("$blue: {} is not a color", v).into()), - None => return Err("Missing element $blue.".into()), - }; - - let green = match channels.pop() { - Some(Value::Dimension(n, Unit::None)) => n, - Some(Value::Dimension(n, Unit::Percent)) => n / Number::from(100), - Some(v) => return Err(format!("$green: {} is not a color", v).into()), - None => return Err("Missing element $green.".into()), - }; - - let red = match channels.pop() { - Some(Value::Dimension(n, Unit::None)) => n, - Some(Value::Dimension(n, Unit::Percent)) => n / Number::from(100), - Some(v) => return Err(format!("$red: {} is not a color", v).into()), - None => return Err("Missing element $red.".into()), - }; - - let color = Color::from_rgba(red, green, blue, Number::from(1)); - - Ok(Value::Color(color)) - - } else if args.len() == 2 { - let color = match arg!(args, 0, "color") { + let color2 = match arg!(args, 1, "color2") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => return Err(format!("$color2: {} is not a color.", v).into()), }; - let alpha = match arg!(args, 1, "alpha") { - Value::Dimension(n, Unit::None) => n, - Value::Dimension(n, Unit::Percent) => n / Number::from(100), - v @ Value::Dimension(..) => return Err(format!("$alpha: Expected {} to have no units or \"%\".", v).into()), - v => return Err(format!("$alpha: {} is not a number.", v).into()), - }; - Ok(Value::Color(color.with_alpha(alpha))) - } else { - let red = match arg!(args, 0, "red") { - Value::Dimension(n, Unit::None) => n, - Value::Dimension(n, Unit::Percent) => (n / Number::from(100)) * Number::from(255), - v @ Value::Dimension(..) => return Err(format!("$red: Expected {} to have no units or \"%\".", v).into()), - v => return Err(format!("$red: {} is not a number.", v).into()), - }; - let green = match arg!(args, 1, "green") { - Value::Dimension(n, Unit::None) => n, - Value::Dimension(n, Unit::Percent) => (n / Number::from(100)) * Number::from(255), - v @ Value::Dimension(..) => return Err(format!("$green: Expected {} to have no units or \"%\".", v).into()), - v => return Err(format!("$green: {} is not a number.", v).into()), - }; - let blue = match arg!(args, 2, "blue") { - Value::Dimension(n, Unit::None) => n, - Value::Dimension(n, Unit::Percent) => (n / Number::from(100)) * Number::from(255), - v @ Value::Dimension(..) => return Err(format!("$blue: Expected {} to have no units or \"%\".", v).into()), - v => return Err(format!("$blue: {} is not a number.", v).into()), - }; - let alpha = match arg!(args, 3, "alpha"=Value::Dimension(Number::from(1), Unit::None)) { - Value::Dimension(n, Unit::None) => n, - Value::Dimension(n, Unit::Percent) => n / Number::from(100), - v @ Value::Dimension(..) => return Err(format!("$alpha: Expected {} to have no units or \"%\".", v).into()), - v => return Err(format!("$alpha: {} is not a number.", v).into()), - }; - Ok(Value::Color(Color::from_rgba(red, green, blue, alpha))) - } - }); - decl!(f "red", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.red(), Unit::None)), - v => Err(format!("$color: {} is not a color.", v).into()), - } - }); - decl!(f "green", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.green(), Unit::None)), - v => Err(format!("$color: {} is not a color.", v).into()), - } - }); - decl!(f "blue", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.blue(), Unit::None)), - v => Err(format!("$color: {} is not a color.", v).into()), - } - }); - decl!(f "mix", |args, _| { - max_args!(args, 3); - let color1 = match arg!(args, 0, "color1") { - Value::Color(c) => c, - v => return Err(format!("$color1: {} is not a color.", v).into()), - }; - let color2 = match arg!(args, 1, "color2") { - Value::Color(c) => c, - v => return Err(format!("$color2: {} is not a color.", v).into()), - }; - - let weight = match arg!(args, 2, "weight"=Value::Dimension(Number::from(50), Unit::None)) { - Value::Dimension(n, u) => bound!("weight", n, u, 0, 100) / Number::from(100), - v => return Err(format!("$weight: {} is not a number.", v).into()), - }; - Ok(Value::Color(color1.mix(&color2, weight))) - }); + let weight = match arg!( + args, + 2, + "weight" = Value::Dimension(Number::from(50), Unit::None) + ) { + Value::Dimension(n, u) => bound!("weight", n, u, 0, 100) / Number::from(100), + v => return Err(format!("$weight: {} is not a number.", v).into()), + }; + Ok(Value::Color(color1.mix(&color2, weight))) + }), + ); } diff --git a/src/builtin/list.rs b/src/builtin/list.rs index 992ddf8..57bdfbd 100644 --- a/src/builtin/list.rs +++ b/src/builtin/list.rs @@ -5,11 +5,14 @@ use crate::units::Unit; use crate::value::{Number, Value}; pub(crate) fn register(f: &mut HashMap) { - decl!(f "length", |args, _| { - let len = match arg!(args, 0, "list") { - Value::List(v, _) => Number::from(v.len()), - _ => Number::from(1) - }; - Ok(Value::Dimension(len, Unit::None)) - }); + f.insert( + "length".to_owned(), + Box::new(|args, _| { + let len = match arg!(args, 0, "list") { + Value::List(v, _) => Number::from(v.len()), + _ => Number::from(1), + }; + Ok(Value::Dimension(len, Unit::None)) + }), + ); } diff --git a/src/builtin/macros.rs b/src/builtin/macros.rs index f237904..50a59b1 100644 --- a/src/builtin/macros.rs +++ b/src/builtin/macros.rs @@ -19,12 +19,6 @@ macro_rules! arg { }; } -macro_rules! decl { - ($f:ident $name:literal, $body:expr) => { - $f.insert($name.to_owned(), Box::new($body)); - }; -} - macro_rules! max_args { ($args:ident, $count:literal) => { if $args.len() > $count { diff --git a/src/builtin/math.rs b/src/builtin/math.rs index 7a30b18..6f857fa 100644 --- a/src/builtin/math.rs +++ b/src/builtin/math.rs @@ -5,54 +5,74 @@ use crate::units::Unit; use crate::value::{Number, Value}; pub(crate) fn register(f: &mut HashMap) { - decl!(f "percentage", |args, _| { - max_args!(args, 1); - let num = match arg!(args, 0, "number") { - Value::Dimension(n, Unit::None) => n * Number::from(100), - v @ Value::Dimension(..) => return Err(format!("$number: Expected {} to have no units.", v).into()), - v => return Err(format!("$number: {} is not a number.", v).into()), - }; - Ok(Value::Dimension(num, Unit::Percent)) - }); - decl!(f "round", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "number") { - Value::Dimension(n, u) => Ok(Value::Dimension(n.round(), u)), - v => Err(format!("$number: {} is not a number.", v).into()), - } - }); - decl!(f "ceil", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "number") { - Value::Dimension(n, u) => Ok(Value::Dimension(n.ceil(), u)), - v => Err(format!("$number: {} is not a number.", v).into()), - } - }); - decl!(f "floor", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "number") { - Value::Dimension(n, u) => Ok(Value::Dimension(n.floor(), u)), - v => Err(format!("$number: {} is not a number.", v).into()), - } - }); - decl!(f "abs", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "number") { - Value::Dimension(n, u) => Ok(Value::Dimension(n.abs(), u)), - v => Err(format!("$number: {} is not a number.", v).into()), - } - }); - decl!(f "comparable", |args, _| { - max_args!(args, 2); - let unit1 = match arg!(args, 0, "number1") { - Value::Dimension(_, u) => u, - v => return Err(format!("$number1: {} is not a number.", v).into()), - }; - let unit2 = match arg!(args, 1, "number2") { - Value::Dimension(_, u) => u, - v => return Err(format!("$number2: {} is not a number.", v).into()), - }; + f.insert( + "percentage".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + let num = match arg!(args, 0, "number") { + Value::Dimension(n, Unit::None) => n * Number::from(100), + v @ Value::Dimension(..) => { + return Err(format!("$number: Expected {} to have no units.", v).into()) + } + v => return Err(format!("$number: {} is not a number.", v).into()), + }; + Ok(Value::Dimension(num, Unit::Percent)) + }), + ); + f.insert( + "round".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "number") { + Value::Dimension(n, u) => Ok(Value::Dimension(n.round(), u)), + v => Err(format!("$number: {} is not a number.", v).into()), + } + }), + ); + f.insert( + "ceil".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "number") { + Value::Dimension(n, u) => Ok(Value::Dimension(n.ceil(), u)), + v => Err(format!("$number: {} is not a number.", v).into()), + } + }), + ); + f.insert( + "floor".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "number") { + Value::Dimension(n, u) => Ok(Value::Dimension(n.floor(), u)), + v => Err(format!("$number: {} is not a number.", v).into()), + } + }), + ); + f.insert( + "abs".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "number") { + Value::Dimension(n, u) => Ok(Value::Dimension(n.abs(), u)), + v => Err(format!("$number: {} is not a number.", v).into()), + } + }), + ); + f.insert( + "comparable".to_owned(), + Box::new(|args, _| { + max_args!(args, 2); + let unit1 = match arg!(args, 0, "number1") { + Value::Dimension(_, u) => u, + v => return Err(format!("$number1: {} is not a number.", v).into()), + }; + let unit2 = match arg!(args, 1, "number2") { + Value::Dimension(_, u) => u, + v => return Err(format!("$number2: {} is not a number.", v).into()), + }; - Ok(Value::bool(unit1.comparable(&unit2))) - }); + Ok(Value::bool(unit1.comparable(&unit2))) + }), + ); } diff --git a/src/builtin/meta.rs b/src/builtin/meta.rs index ebefa98..66abf13 100644 --- a/src/builtin/meta.rs +++ b/src/builtin/meta.rs @@ -6,78 +6,107 @@ use crate::units::Unit; use crate::value::Value; pub(crate) fn register(f: &mut HashMap) { - decl!(f "if", |args, _| { - max_args!(args, 3); - if arg!(args, 0, "condition").is_true()? { - Ok(arg!(args, 1, "if-true")) - } else { - Ok(arg!(args, 2, "if-false")) - } - }); - decl!(f "feature-exists", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "feature").unquote().to_string().as_str() { - // A local variable will shadow a global variable unless - // `!global` is used. - "global-variable-shadowing" => Ok(Value::False), - // the @extend rule will affect selectors nested in pseudo-classes - // like :not() - "extend-selector-pseudoclass" => Ok(Value::False), - // Full support for unit arithmetic using units defined in the - // [Values and Units Level 3][] spec. - "units-level-3" => Ok(Value::False), - // The Sass `@error` directive is supported. - "at-error" => Ok(Value::True), - // The "Custom Properties Level 1" spec is supported. This means - // that custom properties are parsed statically, with only - // interpolation treated as SassScript. - "custom-property" => Ok(Value::False), - _ => Ok(Value::False), - } - }); - decl!(f "unit", |args, _| { - max_args!(args, 1); - let unit = match arg!(args, 0, "number") { - Value::Dimension(_, u) => u.to_string(), - _ => String::new() - }; - Ok(Value::Ident(unit, QuoteKind::Double)) - }); - decl!(f "type-of", |args, _| { - max_args!(args, 1); - let value = arg!(args, 0, "value"); - Ok(Value::Ident(value.kind()?.to_owned(), QuoteKind::None)) - }); - decl!(f "unitless", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "number") { - Value::Dimension(_, Unit::None) => Ok(Value::True), - Value::Dimension(_, _) => Ok(Value::False), - _ => Ok(Value::True) - } - }); - decl!(f "inspect", |args, _| { - max_args!(args, 1); - let value = arg!(args, 0, "value"); - Ok(Value::Ident(value.to_string(), QuoteKind::None)) - }); - decl!(f "variable-exists", |args, scope| { - max_args!(args, 1); - let value = arg!(args, 0, "name"); - Ok(Value::bool(scope.var_exists(&value.to_string()))) - }); - decl!(f "mixin-exists", |args, scope| { - max_args!(args, 1); - let value = arg!(args, 0, "name"); - Ok(Value::bool(scope.mixin_exists(&value.to_string()))) - }); - decl!(f "function-exists", |args, scope| { - max_args!(args, 1); - let value = arg!(args, 0, "name"); - let s = value.unquote().to_string(); - Ok(Value::bool(scope.fn_exists(&s) || GLOBAL_FUNCTIONS.contains_key(&s))) - }); - decl!(f "call", |_args, _scope| { + f.insert( + "if".to_owned(), + Box::new(|args, _| { + max_args!(args, 3); + if arg!(args, 0, "condition").is_true()? { + Ok(arg!(args, 1, "if-true")) + } else { + Ok(arg!(args, 2, "if-false")) + } + }), + ); + f.insert( + "feature-exists".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "feature").unquote().to_string().as_str() { + // A local variable will shadow a global variable unless + // `!global` is used. + "global-variable-shadowing" => Ok(Value::False), + // the @extend rule will affect selectors nested in pseudo-classes + // like :not() + "extend-selector-pseudoclass" => Ok(Value::False), + // Full support for unit arithmetic using units defined in the + // [Values and Units Level 3][] spec. + "units-level-3" => Ok(Value::False), + // The Sass `@error` directive is supported. + "at-error" => Ok(Value::True), + // The "Custom Properties Level 1" spec is supported. This means + // that custom properties are parsed statically, with only + // interpolation treated as SassScript. + "custom-property" => Ok(Value::False), + _ => Ok(Value::False), + } + }), + ); + f.insert( + "unit".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + let unit = match arg!(args, 0, "number") { + Value::Dimension(_, u) => u.to_string(), + _ => String::new(), + }; + Ok(Value::Ident(unit, QuoteKind::Double)) + }), + ); + f.insert( + "type-of".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + let value = arg!(args, 0, "value"); + Ok(Value::Ident(value.kind()?.to_owned(), QuoteKind::None)) + }), + ); + f.insert( + "unitless".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "number") { + Value::Dimension(_, Unit::None) => Ok(Value::True), + Value::Dimension(_, _) => Ok(Value::False), + _ => Ok(Value::True), + } + }), + ); + f.insert( + "inspect".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + let value = arg!(args, 0, "value"); + Ok(Value::Ident(value.to_string(), QuoteKind::None)) + }), + ); + f.insert( + "variable-exists".to_owned(), + Box::new(|args, scope| { + max_args!(args, 1); + let value = arg!(args, 0, "name"); + Ok(Value::bool(scope.var_exists(&value.to_string()))) + }), + ); + f.insert( + "mixin-exists".to_owned(), + Box::new(|args, scope| { + max_args!(args, 1); + let value = arg!(args, 0, "name"); + Ok(Value::bool(scope.mixin_exists(&value.to_string()))) + }), + ); + f.insert( + "function-exists".to_owned(), + Box::new(|args, scope| { + max_args!(args, 1); + let value = arg!(args, 0, "name"); + let s = value.unquote().to_string(); + Ok(Value::bool( + scope.fn_exists(&s) || GLOBAL_FUNCTIONS.contains_key(&s), + )) + }), + ); + f.insert("call".to_owned(), Box::new(|_args, _scope| { todo!("builtin function `call()` is blocked on refactoring how call args are stored and parsed") // let func = arg!(args, 0, "function").to_string(); // let func = match scope.get_fn(&func) { @@ -88,5 +117,5 @@ pub(crate) fn register(f: &mut HashMap) { // }, // }; // Some(func.clone().args(&args).call()) - }); + })); } diff --git a/src/builtin/string.rs b/src/builtin/string.rs index 17d9364..3de1276 100644 --- a/src/builtin/string.rs +++ b/src/builtin/string.rs @@ -10,84 +10,120 @@ use crate::units::Unit; use crate::value::{Number, Value}; pub(crate) fn register(f: &mut HashMap) { - decl!(f "to-upper-case", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "string") { - Value::Ident(i, q) => Ok(Value::Ident(i.to_ascii_uppercase(), q)), - v => Err(format!("$string: {} is not a string.", v).into()), - } - }); - decl!(f "to-lower-case", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "string") { - Value::Ident(i, q) => Ok(Value::Ident(i.to_ascii_lowercase(), q)), - v => Err(format!("$string: {} is not a string.", v).into()), - } - }); - decl!(f "str-length", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "string") { - Value::Ident(i, _) => Ok(Value::Dimension(Number::from(i.len()), Unit::None)), - v => Err(format!("$string: {} is not a string.", v).into()), - } - }); - decl!(f "quote", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "string") { - Value::Ident(i, _) => Ok(Value::Ident(i, QuoteKind::Double)), - v => Err(format!("$string: {} is not a string.", v).into()), - } - }); - decl!(f "unquote", |args, _| { - max_args!(args, 1); - match arg!(args, 0, "string") { - Value::Ident(i, _) if i.is_empty() => Ok(Value::Null), - i @ Value::Ident(..) => Ok(i.unquote()), - v => Err(format!("$string: {} is not a string.", v).into()), - } - }); - decl!(f "str-slice", |args, _| { - max_args!(args, 3); - let (string, quotes) = match arg!(args, 0, "string") { - Value::Ident(s, q) => (s, q), - v => return Err(format!("$string: {} is not a string.", v).into()), - }; - let str_len = string.len(); - let start = match arg!(args, 1, "start-at") { - Value::Dimension(n, Unit::None) if n.is_decimal() => return Err(format!("{} is not an int.", n).into()), - Value::Dimension(n, Unit::None) if n.to_integer().is_positive() => n.to_integer().to_usize().unwrap(), - Value::Dimension(n, Unit::None) if n == Number::from(0) => 1_usize, - Value::Dimension(n, Unit::None) if n < -Number::from(str_len) => 1_usize, - Value::Dimension(n, Unit::None) => (BigInt::from(str_len + 1) + n.to_integer()).to_usize().unwrap(), - v @ Value::Dimension(..) => return Err(format!("$start: Expected {} to have no units.", v).into()), - v => return Err(format!("$start-at: {} is not a number.", v).into()), - }; - let mut end = match arg!(args, 2, "end-at"=Value::Null) { - Value::Dimension(n, Unit::None) if n.is_decimal() => return Err(format!("{} is not an int.", n).into()), - Value::Dimension(n, Unit::None) if n.to_integer().is_positive() => n.to_integer().to_usize().unwrap(), - Value::Dimension(n, Unit::None) if n == Number::from(0) => 0_usize, - Value::Dimension(n, Unit::None) if n < -Number::from(str_len) => 0_usize, - Value::Dimension(n, Unit::None) => (BigInt::from(str_len + 1) + n.to_integer()).to_usize().unwrap(), - v @ Value::Dimension(..) => return Err(format!("$end: Expected {} to have no units.", v).into()), - Value::Null => str_len, - v => return Err(format!("$end-at: {} is not a number.", v).into()), - }; - - if end > str_len { - end = str_len; - } - - if start > end || start > str_len { - match quotes { - QuoteKind::Double | QuoteKind::Single => Ok(Value::Ident(String::new(), QuoteKind::Double)), - QuoteKind::None => Ok(Value::Null), + f.insert( + "to-upper-case".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "string") { + Value::Ident(i, q) => Ok(Value::Ident(i.to_ascii_uppercase(), q)), + v => Err(format!("$string: {} is not a string.", v).into()), } - } else { - let s = string[start-1..end].to_string(); - match quotes { - QuoteKind::Double | QuoteKind::Single => Ok(Value::Ident(s, QuoteKind::Double)), - QuoteKind::None => Ok(Value::Ident(s, QuoteKind::None)), + }), + ); + f.insert( + "to-lower-case".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "string") { + Value::Ident(i, q) => Ok(Value::Ident(i.to_ascii_lowercase(), q)), + v => Err(format!("$string: {} is not a string.", v).into()), } - } - }); + }), + ); + f.insert( + "str-length".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "string") { + Value::Ident(i, _) => Ok(Value::Dimension(Number::from(i.len()), Unit::None)), + v => Err(format!("$string: {} is not a string.", v).into()), + } + }), + ); + f.insert( + "quote".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "string") { + Value::Ident(i, _) => Ok(Value::Ident(i, QuoteKind::Double)), + v => Err(format!("$string: {} is not a string.", v).into()), + } + }), + ); + f.insert( + "unquote".to_owned(), + Box::new(|args, _| { + max_args!(args, 1); + match arg!(args, 0, "string") { + Value::Ident(i, _) if i.is_empty() => Ok(Value::Null), + i @ Value::Ident(..) => Ok(i.unquote()), + v => Err(format!("$string: {} is not a string.", v).into()), + } + }), + ); + f.insert( + "str-slice".to_owned(), + Box::new(|args, _| { + max_args!(args, 3); + let (string, quotes) = match arg!(args, 0, "string") { + Value::Ident(s, q) => (s, q), + v => return Err(format!("$string: {} is not a string.", v).into()), + }; + let str_len = string.len(); + let start = match arg!(args, 1, "start-at") { + Value::Dimension(n, Unit::None) if n.is_decimal() => { + return Err(format!("{} is not an int.", n).into()) + } + Value::Dimension(n, Unit::None) if n.to_integer().is_positive() => { + n.to_integer().to_usize().unwrap() + } + Value::Dimension(n, Unit::None) if n == Number::from(0) => 1_usize, + Value::Dimension(n, Unit::None) if n < -Number::from(str_len) => 1_usize, + Value::Dimension(n, Unit::None) => (BigInt::from(str_len + 1) + n.to_integer()) + .to_usize() + .unwrap(), + v @ Value::Dimension(..) => { + return Err(format!("$start: Expected {} to have no units.", v).into()) + } + v => return Err(format!("$start-at: {} is not a number.", v).into()), + }; + let mut end = match arg!(args, 2, "end-at" = Value::Null) { + Value::Dimension(n, Unit::None) if n.is_decimal() => { + return Err(format!("{} is not an int.", n).into()) + } + Value::Dimension(n, Unit::None) if n.to_integer().is_positive() => { + n.to_integer().to_usize().unwrap() + } + Value::Dimension(n, Unit::None) if n == Number::from(0) => 0_usize, + Value::Dimension(n, Unit::None) if n < -Number::from(str_len) => 0_usize, + Value::Dimension(n, Unit::None) => (BigInt::from(str_len + 1) + n.to_integer()) + .to_usize() + .unwrap(), + v @ Value::Dimension(..) => { + return Err(format!("$end: Expected {} to have no units.", v).into()) + } + Value::Null => str_len, + v => return Err(format!("$end-at: {} is not a number.", v).into()), + }; + + if end > str_len { + end = str_len; + } + + if start > end || start > str_len { + match quotes { + QuoteKind::Double | QuoteKind::Single => { + Ok(Value::Ident(String::new(), QuoteKind::Double)) + } + QuoteKind::None => Ok(Value::Null), + } + } else { + let s = string[start - 1..end].to_string(); + match quotes { + QuoteKind::Double | QuoteKind::Single => Ok(Value::Ident(s, QuoteKind::Double)), + QuoteKind::None => Ok(Value::Ident(s, QuoteKind::None)), + } + } + }), + ); }