From 0aed49212399119fc471a1bb66ea8733d22fb3d0 Mon Sep 17 00:00:00 2001 From: ConnorSkees <39542938+ConnorSkees@users.noreply.github.com> Date: Thu, 30 Apr 2020 19:36:34 -0400 Subject: [PATCH] move builtin fns to outer scope to reduce nesting --- src/builtin/color/hsl.rs | 1284 ++++++++++++++++---------------- src/builtin/color/opacity.rs | 288 ++++---- src/builtin/color/other.rs | 578 ++++++++------- src/builtin/color/rgb.rs | 1328 +++++++++++++++++----------------- src/builtin/list.rs | 538 +++++++------- src/builtin/map.rs | 256 +++---- src/builtin/math.rs | 346 +++++---- src/builtin/meta.rs | 392 +++++----- src/builtin/string.rs | 610 ++++++++-------- 9 files changed, 2759 insertions(+), 2861 deletions(-) diff --git a/src/builtin/color/hsl.rs b/src/builtin/color/hsl.rs index b61384c..5268d9c 100644 --- a/src/builtin/color/hsl.rs +++ b/src/builtin/color/hsl.rs @@ -12,569 +12,130 @@ use crate::selector::Selector; use crate::unit::Unit; use crate::value::{Number, Value}; -pub(crate) fn register(f: &mut GlobalFunctionMap) { - fn hsl(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - if args.is_empty() { - return Err(("Missing argument $channels.", args.span()).into()); - } - - if args.len() == 1 { - let mut channels = match arg!(args, scope, super_selector, 0, "channels") { - Value::List(v, ..) => v, - _ => return Err(("Missing argument $channels.", args.span()).into()), - }; - - if channels.len() > 3 { - return Err(( - format!( - "Only 3 elements allowed, but {} were passed.", - channels.len() - ), - args.span(), - ) - .into()); - } - - let lightness = match channels.pop() { - Some(Value::Dimension(n, _)) => n / Number::from(100), - Some(v) => { - return Err(( - format!( - "$lightness: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - None => return Err(("Missing element $lightness.", args.span()).into()), - }; - - let saturation = match channels.pop() { - Some(Value::Dimension(n, _)) => n / Number::from(100), - Some(v) => { - return Err(( - format!( - "$saturation: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - None => return Err(("Missing element $saturation.", args.span()).into()), - }; - - let hue = match channels.pop() { - Some(Value::Dimension(n, _)) => n, - Some(v) => { - return Err(( - format!("$hue: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - None => return Err(("Missing element $hue.", args.span()).into()), - }; - - Ok(Value::Color(Box::new(Color::from_hsla( - hue, - saturation, - lightness, - Number::one(), - )))) - } else { - let hue = match arg!(args, scope, super_selector, 0, "hue") { - Value::Dimension(n, _) => n, - v if v.is_special_function() => { - let saturation = arg!(args, scope, super_selector, 1, "saturation"); - let lightness = arg!(args, scope, super_selector, 2, "lightness"); - let mut string = format!( - "hsl({}, {}, {}", - v.to_css_string(args.span())?, - saturation.to_css_string(args.span())?, - lightness.to_css_string(args.span())? - ); - if !args.is_empty() { - string.push_str(", "); - string.push_str( - &arg!(args, scope, super_selector, 3, "alpha") - .to_css_string(args.span())?, - ); - } - string.push(')'); - return Ok(Value::Ident(string, QuoteKind::None)); - } - v => { - return Err(( - format!("$hue: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let saturation = match arg!(args, scope, super_selector, 1, "saturation") { - Value::Dimension(n, _) => n / Number::from(100), - v if v.is_special_function() => { - let lightness = arg!(args, scope, super_selector, 2, "lightness"); - let mut string = format!( - "hsl({}, {}, {}", - hue, - v.to_css_string(args.span())?, - lightness.to_css_string(args.span())? - ); - if !args.is_empty() { - string.push_str(", "); - string.push_str( - &arg!(args, scope, super_selector, 3, "alpha") - .to_css_string(args.span())?, - ); - } - string.push(')'); - return Ok(Value::Ident(string, QuoteKind::None)); - } - v => { - return Err(( - format!( - "$saturation: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - let lightness = match arg!(args, scope, super_selector, 2, "lightness") { - Value::Dimension(n, _) => n / Number::from(100), - v if v.is_special_function() => { - let mut string = format!( - "hsl({}, {}, {}", - hue, - saturation, - v.to_css_string(args.span())? - ); - if !args.is_empty() { - string.push_str(", "); - string.push_str( - &arg!(args, scope, super_selector, 3, "alpha") - .to_css_string(args.span())?, - ); - } - string.push(')'); - return Ok(Value::Ident(string, QuoteKind::None)); - } - v => { - return Err(( - format!( - "$lightness: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - let alpha = match arg!( - args, - scope, - super_selector, - 3, - "alpha" = Value::Dimension(Number::one(), 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.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v if v.is_special_function() => { - return Ok(Value::Ident( - format!( - "hsl({}, {}, {}, {})", - hue, - saturation, - lightness, - v.to_css_string(args.span())? - ), - QuoteKind::None, - )); - } - v => { - return Err(( - format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(Color::from_hsla( - hue, saturation, lightness, alpha, - )))) - } +fn hsl(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + if args.is_empty() { + return Err(("Missing argument $channels.", args.span()).into()); } - fn hsla(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - if args.is_empty() { - return Err(("Missing argument $channels.", args.span()).into()); - } - - if args.len() == 1 { - let mut channels = match arg!(args, scope, super_selector, 0, "channels") { - Value::List(v, ..) => v, - _ => return Err(("Missing argument $channels.", args.span()).into()), - }; - - if channels.len() > 3 { - return Err(( - format!( - "Only 3 elements allowed, but {} were passed.", - channels.len() - ), - args.span(), - ) - .into()); - } - - let lightness = match channels.pop() { - Some(Value::Dimension(n, _)) => n / Number::from(100), - Some(v) => { - return Err(( - format!( - "$lightness: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - None => return Err(("Missing element $lightness.", args.span()).into()), - }; - - let saturation = match channels.pop() { - Some(Value::Dimension(n, _)) => n / Number::from(100), - Some(v) => { - return Err(( - format!( - "$saturation: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - None => return Err(("Missing element $saturation.", args.span()).into()), - }; - - let hue = match channels.pop() { - Some(Value::Dimension(n, _)) => n, - Some(v) => { - return Err(( - format!("$hue: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - None => return Err(("Missing element $hue.", args.span()).into()), - }; - - Ok(Value::Color(Box::new(Color::from_hsla( - hue, - saturation, - lightness, - Number::one(), - )))) - } else { - let hue = match arg!(args, scope, super_selector, 0, "hue") { - Value::Dimension(n, _) => n, - v if v.is_special_function() => { - let saturation = arg!(args, scope, super_selector, 1, "saturation"); - let lightness = arg!(args, scope, super_selector, 2, "lightness"); - let mut string = format!( - "hsla({}, {}, {}", - v.to_css_string(args.span())?, - saturation.to_css_string(args.span())?, - lightness.to_css_string(args.span())? - ); - if !args.is_empty() { - string.push_str(", "); - string.push_str( - &arg!(args, scope, super_selector, 3, "alpha") - .to_css_string(args.span())?, - ); - } - string.push(')'); - return Ok(Value::Ident(string, QuoteKind::None)); - } - v => { - return Err(( - format!("$hue: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let saturation = match arg!(args, scope, super_selector, 1, "saturation") { - Value::Dimension(n, _) => n / Number::from(100), - v if v.is_special_function() => { - let lightness = arg!(args, scope, super_selector, 2, "lightness"); - let mut string = format!( - "hsla({}, {}, {}", - hue, - v.to_css_string(args.span())?, - lightness.to_css_string(args.span())? - ); - if !args.is_empty() { - string.push_str(", "); - string.push_str( - &arg!(args, scope, super_selector, 3, "alpha") - .to_css_string(args.span())?, - ); - } - string.push(')'); - return Ok(Value::Ident(string, QuoteKind::None)); - } - v => { - return Err(( - format!( - "$saturation: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - let lightness = match arg!(args, scope, super_selector, 2, "lightness") { - Value::Dimension(n, _) => n / Number::from(100), - v if v.is_special_function() => { - let mut string = format!( - "hsla({}, {}, {}", - hue, - saturation, - v.to_css_string(args.span())? - ); - if !args.is_empty() { - string.push_str(", "); - string.push_str( - &arg!(args, scope, super_selector, 3, "alpha") - .to_css_string(args.span())?, - ); - } - string.push(')'); - return Ok(Value::Ident(string, QuoteKind::None)); - } - v => { - return Err(( - format!( - "$lightness: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - let alpha = match arg!( - args, - scope, - super_selector, - 3, - "alpha" = Value::Dimension(Number::one(), 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.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v if v.is_special_function() => { - return Ok(Value::Ident( - format!( - "hsl({}, {}, {}, {})", - hue, - saturation, - lightness, - v.to_css_string(args.span())? - ), - QuoteKind::None, - )); - } - v => { - return Err(( - format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(Color::from_hsla( - hue, saturation, lightness, alpha, - )))) - } - } - - fn hue(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.hue(), Unit::Deg)), - v => Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()), - } - } - - fn saturation( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.saturation(), Unit::Percent)), - v => Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()), - } - } - - fn lightness( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.lightness(), Unit::Percent)), - v => Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()), - } - } - - fn adjust_hue( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(2)?; - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } + if args.len() == 1 { + let mut channels = match arg!(args, scope, super_selector, 0, "channels") { + Value::List(v, ..) => v, + _ => return Err(("Missing argument $channels.", args.span()).into()), }; - let degrees = match arg!(args, scope, super_selector, 1, "degrees") { - Value::Dimension(n, _) => n, - v => { - return Err(( - format!( - "$degrees: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(color.adjust_hue(degrees)))) - } - fn lighten(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(2)?; - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), - v => { - return Err(( - format!( - "$amount: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(color.lighten(amount)))) - } - - fn darken(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(2)?; - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), - v => { - return Err(( - format!( - "$amount: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(color.darken(amount)))) - } - - fn saturate(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(2)?; - if args.len() == 1 { - return Ok(Value::Ident( + if channels.len() > 3 { + return Err(( format!( - "saturate({})", - arg!(args, scope, super_selector, 0, "amount").to_css_string(args.span())? + "Only 3 elements allowed, but {} were passed.", + channels.len() ), - QuoteKind::None, - )); + args.span(), + ) + .into()); } - let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + let lightness = match channels.pop() { + Some(Value::Dimension(n, _)) => n / Number::from(100), + Some(v) => { + return Err(( + format!( + "$lightness: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $lightness.", args.span()).into()), + }; + + let saturation = match channels.pop() { + Some(Value::Dimension(n, _)) => n / Number::from(100), + Some(v) => { + return Err(( + format!( + "$saturation: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $saturation.", args.span()).into()), + }; + + let hue = match channels.pop() { + Some(Value::Dimension(n, _)) => n, + Some(v) => { + return Err(( + format!("$hue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $hue.", args.span()).into()), + }; + + Ok(Value::Color(Box::new(Color::from_hsla( + hue, + saturation, + lightness, + Number::one(), + )))) + } else { + let hue = match arg!(args, scope, super_selector, 0, "hue") { + Value::Dimension(n, _) => n, + v if v.is_special_function() => { + let saturation = arg!(args, scope, super_selector, 1, "saturation"); + let lightness = arg!(args, scope, super_selector, 2, "lightness"); + let mut string = format!( + "hsl({}, {}, {}", + v.to_css_string(args.span())?, + saturation.to_css_string(args.span())?, + lightness.to_css_string(args.span())? + ); + if !args.is_empty() { + string.push_str(", "); + string.push_str( + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, + ); + } + string.push(')'); + return Ok(Value::Ident(string, QuoteKind::None)); + } + v => { + return Err(( + format!("$hue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let saturation = match arg!(args, scope, super_selector, 1, "saturation") { + Value::Dimension(n, _) => n / Number::from(100), + v if v.is_special_function() => { + let lightness = arg!(args, scope, super_selector, 2, "lightness"); + let mut string = format!( + "hsl({}, {}, {}", + hue, + v.to_css_string(args.span())?, + lightness.to_css_string(args.span())? + ); + if !args.is_empty() { + string.push_str(", "); + string.push_str( + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, + ); + } + string.push(')'); + return Ok(Value::Ident(string, QuoteKind::None)); + } v => { return Err(( format!( - "$amount: {} is not a number.", + "$saturation: {} is not a number.", v.to_css_string(args.span())? ), args.span(), @@ -582,47 +143,29 @@ pub(crate) fn register(f: &mut GlobalFunctionMap) { .into()) } }; - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - Value::Dimension(n, u) => { - return Ok(Value::Ident( - format!("saturate({}{})", n, u), - QuoteKind::None, - )) + let lightness = match arg!(args, scope, super_selector, 2, "lightness") { + Value::Dimension(n, _) => n / Number::from(100), + v if v.is_special_function() => { + let mut string = format!( + "hsl({}, {}, {}", + hue, + saturation, + v.to_css_string(args.span())? + ); + if !args.is_empty() { + string.push_str(", "); + string.push_str( + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, + ); + } + string.push(')'); + return Ok(Value::Ident(string, QuoteKind::None)); } - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(color.saturate(amount)))) - } - - fn desaturate( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(2)?; - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), v => { return Err(( format!( - "$amount: {} is not a number.", + "$lightness: {} is not a number.", v.to_css_string(args.span())? ), args.span(), @@ -630,67 +173,175 @@ pub(crate) fn register(f: &mut GlobalFunctionMap) { .into()) } }; - Ok(Value::Color(Box::new(color.desaturate(amount)))) - } - - fn grayscale( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - let color = match arg!(args, scope, super_selector, 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.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(color.desaturate(Number::one())))) - } - - fn complement( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(color.complement()))) - } - - fn invert(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(2)?; - let weight = match arg!( + let alpha = match arg!( args, scope, super_selector, - 1, - "weight" = Value::Dimension(Number::from(100), Unit::Percent) + 3, + "alpha" = Value::Dimension(Number::one(), Unit::None) ) { - Value::Dimension(n, u) => bound!(args, "weight", n, u, 0, 100) / Number::from(100), + 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.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + v if v.is_special_function() => { + return Ok(Value::Ident( + format!( + "hsl({}, {}, {}, {})", + hue, + saturation, + lightness, + v.to_css_string(args.span())? + ), + QuoteKind::None, + )); + } + v => { + return Err(( + format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(Color::from_hsla( + hue, saturation, lightness, alpha, + )))) + } +} + +fn hsla(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + if args.is_empty() { + return Err(("Missing argument $channels.", args.span()).into()); + } + + if args.len() == 1 { + let mut channels = match arg!(args, scope, super_selector, 0, "channels") { + Value::List(v, ..) => v, + _ => return Err(("Missing argument $channels.", args.span()).into()), + }; + + if channels.len() > 3 { + return Err(( + format!( + "Only 3 elements allowed, but {} were passed.", + channels.len() + ), + args.span(), + ) + .into()); + } + + let lightness = match channels.pop() { + Some(Value::Dimension(n, _)) => n / Number::from(100), + Some(v) => { + return Err(( + format!( + "$lightness: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $lightness.", args.span()).into()), + }; + + let saturation = match channels.pop() { + Some(Value::Dimension(n, _)) => n / Number::from(100), + Some(v) => { + return Err(( + format!( + "$saturation: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $saturation.", args.span()).into()), + }; + + let hue = match channels.pop() { + Some(Value::Dimension(n, _)) => n, + Some(v) => { + return Err(( + format!("$hue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $hue.", args.span()).into()), + }; + + Ok(Value::Color(Box::new(Color::from_hsla( + hue, + saturation, + lightness, + Number::one(), + )))) + } else { + let hue = match arg!(args, scope, super_selector, 0, "hue") { + Value::Dimension(n, _) => n, + v if v.is_special_function() => { + let saturation = arg!(args, scope, super_selector, 1, "saturation"); + let lightness = arg!(args, scope, super_selector, 2, "lightness"); + let mut string = format!( + "hsla({}, {}, {}", + v.to_css_string(args.span())?, + saturation.to_css_string(args.span())?, + lightness.to_css_string(args.span())? + ); + if !args.is_empty() { + string.push_str(", "); + string.push_str( + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, + ); + } + string.push(')'); + return Ok(Value::Ident(string, QuoteKind::None)); + } + v => { + return Err(( + format!("$hue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let saturation = match arg!(args, scope, super_selector, 1, "saturation") { + Value::Dimension(n, _) => n / Number::from(100), + v if v.is_special_function() => { + let lightness = arg!(args, scope, super_selector, 2, "lightness"); + let mut string = format!( + "hsla({}, {}, {}", + hue, + v.to_css_string(args.span())?, + lightness.to_css_string(args.span())? + ); + if !args.is_empty() { + string.push_str(", "); + string.push_str( + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, + ); + } + string.push(')'); + return Ok(Value::Ident(string, QuoteKind::None)); + } v => { return Err(( format!( - "$weight: {} is not a number.", + "$saturation: {} is not a number.", v.to_css_string(args.span())? ), args.span(), @@ -698,24 +349,349 @@ pub(crate) fn register(f: &mut GlobalFunctionMap) { .into()) } }; - match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => Ok(Value::Color(Box::new(c.invert(weight)))), - Value::Dimension(n, Unit::Percent) => { - Ok(Value::Ident(format!("invert({}%)", n), QuoteKind::None)) + let lightness = match arg!(args, scope, super_selector, 2, "lightness") { + Value::Dimension(n, _) => n / Number::from(100), + v if v.is_special_function() => { + let mut string = format!( + "hsla({}, {}, {}", + hue, + saturation, + v.to_css_string(args.span())? + ); + if !args.is_empty() { + string.push_str(", "); + string.push_str( + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, + ); + } + string.push(')'); + return Ok(Value::Ident(string, QuoteKind::None)); } - Value::Dimension(..) => Err(( - "Only one argument may be passed to the plain-CSS invert() function.", - args.span(), - ) - .into()), - v => Err(( + v => { + return Err(( + format!( + "$lightness: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + let alpha = match arg!( + args, + scope, + super_selector, + 3, + "alpha" = Value::Dimension(Number::one(), 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.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + v if v.is_special_function() => { + return Ok(Value::Ident( + format!( + "hsl({}, {}, {}, {})", + hue, + saturation, + lightness, + v.to_css_string(args.span())? + ), + QuoteKind::None, + )); + } + v => { + return Err(( + format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(Color::from_hsla( + hue, saturation, lightness, alpha, + )))) + } +} + +fn hue(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.hue(), Unit::Deg)), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), + } +} + +fn saturation(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.saturation(), Unit::Percent)), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), + } +} + +fn lightness(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.lightness(), Unit::Percent)), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), + } +} + +fn adjust_hue(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v => { + return Err(( format!("$color: {} is not a color.", v.to_css_string(args.span())?), args.span(), ) - .into()), + .into()) } + }; + let degrees = match arg!(args, scope, super_selector, 1, "degrees") { + Value::Dimension(n, _) => n, + v => { + return Err(( + format!( + "$degrees: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color.adjust_hue(degrees)))) +} + +fn lighten(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let amount = match arg!(args, scope, super_selector, 1, "amount") { + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color.lighten(amount)))) +} + +fn darken(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let amount = match arg!(args, scope, super_selector, 1, "amount") { + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color.darken(amount)))) +} + +fn saturate(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + if args.len() == 1 { + return Ok(Value::Ident( + format!( + "saturate({})", + arg!(args, scope, super_selector, 0, "amount").to_css_string(args.span())? + ), + QuoteKind::None, + )); } + let amount = match arg!(args, scope, super_selector, 1, "amount") { + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + let color = match arg!(args, scope, super_selector, 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.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color.saturate(amount)))) +} + +fn desaturate(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let amount = match arg!(args, scope, super_selector, 1, "amount") { + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color.desaturate(amount)))) +} + +fn grayscale(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + let color = match arg!(args, scope, super_selector, 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.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color.desaturate(Number::one())))) +} + +fn complement(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color.complement()))) +} + +fn invert(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let weight = match arg!( + args, + scope, + super_selector, + 1, + "weight" = Value::Dimension(Number::from(100), Unit::Percent) + ) { + Value::Dimension(n, u) => bound!(args, "weight", n, u, 0, 100) / Number::from(100), + v => { + return Err(( + format!( + "$weight: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => Ok(Value::Color(Box::new(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.", + args.span(), + ) + .into()), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), + } +} + +pub(crate) fn register(f: &mut GlobalFunctionMap) { f.insert("hsl", Builtin::new(hsl)); f.insert("hsla", Builtin::new(hsla)); f.insert("hue", Builtin::new(hue)); diff --git a/src/builtin/color/opacity.rs b/src/builtin/color/opacity.rs index 0abc04f..e3203f8 100644 --- a/src/builtin/color/opacity.rs +++ b/src/builtin/color/opacity.rs @@ -10,151 +10,151 @@ use crate::unit::Unit; use crate::value::Number; use crate::value::Value; +fn alpha(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.alpha(), Unit::None)), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), + } +} + +fn opacity(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 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.to_css_string(args.span())?), + args.span(), + ) + .into()), + } +} + +fn opacify(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let amount = match arg!(args, scope, super_selector, 1, "amount") { + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 1), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color.fade_in(amount)))) +} + +fn fade_in(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let amount = match arg!(args, scope, super_selector, 1, "amount") { + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 1), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color.fade_in(amount)))) +} + +fn transparentize( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(2)?; + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let amount = match arg!(args, scope, super_selector, 1, "amount") { + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 1), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color.fade_out(amount)))) +} + +fn fade_out(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let amount = match arg!(args, scope, super_selector, 1, "amount") { + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 1), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color.fade_out(amount)))) +} + pub(crate) fn register(f: &mut GlobalFunctionMap) { - fn alpha(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.alpha(), Unit::None)), - v => Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()), - } - } - - fn opacity(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 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.to_css_string(args.span())?), - args.span(), - ) - .into()), - } - } - - fn opacify(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(2)?; - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 1), - v => { - return Err(( - format!( - "$amount: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(color.fade_in(amount)))) - } - - fn fade_in(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(2)?; - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 1), - v => { - return Err(( - format!( - "$amount: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(color.fade_in(amount)))) - } - - fn transparentize( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(2)?; - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 1), - v => { - return Err(( - format!( - "$amount: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(color.fade_out(amount)))) - } - - fn fade_out(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(2)?; - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 1), - v => { - return Err(( - format!( - "$amount: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(color.fade_out(amount)))) - } - f.insert("alpha", Builtin::new(alpha)); f.insert("opacity", Builtin::new(opacity)); f.insert("opacify", Builtin::new(opacify)); diff --git a/src/builtin/color/other.rs b/src/builtin/color/other.rs index 15e4e5e..0239c05 100644 --- a/src/builtin/color/other.rs +++ b/src/builtin/color/other.rs @@ -54,316 +54,302 @@ macro_rules! opt_hsl { }; } -pub(crate) fn register(f: &mut GlobalFunctionMap) { - fn change_color( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - if args.get_positional(1, scope, super_selector).is_some() { - return Err( - ("Only one positional argument is allowed. All other arguments must be passed by name.", args.span() - ).into()); - } - - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - - opt_rgba!(args, alpha, "alpha", 0, 1, scope, super_selector); - opt_rgba!(args, red, "red", 0, 255, scope, super_selector); - opt_rgba!(args, green, "green", 0, 255, scope, super_selector); - opt_rgba!(args, blue, "blue", 0, 255, scope, super_selector); - - if red.is_some() || green.is_some() || blue.is_some() { - return Ok(Value::Color(Box::new(Color::from_rgba( - red.unwrap_or(color.red()), - green.unwrap_or(color.green()), - blue.unwrap_or(color.blue()), - alpha.unwrap_or(color.alpha()), - )))); - } - - let hue = match named_arg!(args, scope, super_selector, "hue" = Value::Null) { - Value::Dimension(n, _) => Some(n), - Value::Null => None, - v => { - return Err(( - format!("$hue: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - - opt_hsl!( - args, - saturation, - "saturation", - 0, - 100, - scope, - super_selector - ); - opt_hsl!(args, luminance, "lightness", 0, 100, scope, super_selector); - - 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(Box::new(Color::from_hsla( - hue.unwrap_or(this_hue), - saturation.unwrap_or(this_saturation), - luminance.unwrap_or(this_luminance), - alpha.unwrap_or(this_alpha), - )))); - } - - Ok(Value::Color(if let Some(a) = alpha { - Box::new(color.with_alpha(a)) - } else { - color - })) +fn change_color(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + if args.get_positional(1, scope, super_selector).is_some() { + return Err(( + "Only one positional argument is allowed. All other arguments must be passed by name.", + args.span(), + ) + .into()); } - fn adjust_color( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - - opt_rgba!(args, alpha, "alpha", -1, 1, scope, super_selector); - opt_rgba!(args, red, "red", -255, 255, scope, super_selector); - opt_rgba!(args, green, "green", -255, 255, scope, super_selector); - opt_rgba!(args, blue, "blue", -255, 255, scope, super_selector); - - if red.is_some() || green.is_some() || blue.is_some() { - return Ok(Value::Color(Box::new(Color::from_rgba( - color.red() + red.unwrap_or(Number::zero()), - color.green() + green.unwrap_or(Number::zero()), - color.blue() + blue.unwrap_or(Number::zero()), - color.alpha() + alpha.unwrap_or(Number::zero()), - )))); + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) } + }; - let hue = match named_arg!(args, scope, super_selector, "hue" = Value::Null) { - Value::Dimension(n, _) => Some(n), - Value::Null => None, - v => { - return Err(( - format!("$hue: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; + opt_rgba!(args, alpha, "alpha", 0, 1, scope, super_selector); + opt_rgba!(args, red, "red", 0, 255, scope, super_selector); + opt_rgba!(args, green, "green", 0, 255, scope, super_selector); + opt_rgba!(args, blue, "blue", 0, 255, scope, super_selector); - opt_hsl!( - args, - saturation, - "saturation", - -100, - 100, - scope, - super_selector - ); - opt_hsl!( - args, - luminance, - "lightness", - -100, - 100, - scope, - super_selector - ); - - 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(Box::new(Color::from_hsla( - this_hue + hue.unwrap_or(Number::zero()), - this_saturation + saturation.unwrap_or(Number::zero()), - this_luminance + luminance.unwrap_or(Number::zero()), - this_alpha + alpha.unwrap_or(Number::zero()), - )))); - } - - Ok(Value::Color(if let Some(a) = alpha { - let temp_alpha = color.alpha(); - Box::new(color.with_alpha(temp_alpha + a)) - } else { - color - })) + if red.is_some() || green.is_some() || blue.is_some() { + return Ok(Value::Color(Box::new(Color::from_rgba( + red.unwrap_or(color.red()), + green.unwrap_or(color.green()), + blue.unwrap_or(color.blue()), + alpha.unwrap_or(color.alpha()), + )))); } - fn scale_color( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - fn scale(val: Number, by: Number, max: Number) -> Number { - if by.is_zero() { - return val; - } - val.clone() + (if by.is_positive() { max - val } else { val }) * by + let hue = match named_arg!(args, scope, super_selector, "hue" = Value::Null) { + Value::Dimension(n, _) => Some(n), + Value::Null => None, + v => { + return Err(( + format!("$hue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) } + }; - let span = args.span(); - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(span)?), - span, - ) - .into()) - } - }; + opt_hsl!( + args, + saturation, + "saturation", + 0, + 100, + scope, + super_selector + ); + opt_hsl!(args, luminance, "lightness", 0, 100, scope, super_selector); - macro_rules! opt_scale_arg { - ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal, $scope:ident, $super_selector:ident) => { - let $name = match named_arg!($args, $scope, $super_selector, $arg = Value::Null) { - Value::Dimension(n, Unit::Percent) => { - Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number::from(100)) - } - v @ Value::Dimension(..) => { - return Err(( - format!( - "${}: Expected {} to have unit \"%\".", - $arg, - v.to_css_string($args.span())? - ), - $args.span(), - ) - .into()) - } - Value::Null => None, - v => { - return Err(( - format!( - "${}: {} is not a number.", - $arg, - v.to_css_string($args.span())? - ), - $args.span(), - ) - .into()) - } - }; + 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(Box::new(Color::from_hsla( + hue.unwrap_or(this_hue), + saturation.unwrap_or(this_saturation), + luminance.unwrap_or(this_luminance), + alpha.unwrap_or(this_alpha), + )))); + } + + Ok(Value::Color(if let Some(a) = alpha { + Box::new(color.with_alpha(a)) + } else { + color + })) +} + +fn adjust_color(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + + opt_rgba!(args, alpha, "alpha", -1, 1, scope, super_selector); + opt_rgba!(args, red, "red", -255, 255, scope, super_selector); + opt_rgba!(args, green, "green", -255, 255, scope, super_selector); + opt_rgba!(args, blue, "blue", -255, 255, scope, super_selector); + + if red.is_some() || green.is_some() || blue.is_some() { + return Ok(Value::Color(Box::new(Color::from_rgba( + color.red() + red.unwrap_or(Number::zero()), + color.green() + green.unwrap_or(Number::zero()), + color.blue() + blue.unwrap_or(Number::zero()), + color.alpha() + alpha.unwrap_or(Number::zero()), + )))); + } + + let hue = match named_arg!(args, scope, super_selector, "hue" = Value::Null) { + Value::Dimension(n, _) => Some(n), + Value::Null => None, + v => { + return Err(( + format!("$hue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + + opt_hsl!( + args, + saturation, + "saturation", + -100, + 100, + scope, + super_selector + ); + opt_hsl!( + args, + luminance, + "lightness", + -100, + 100, + scope, + super_selector + ); + + 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(Box::new(Color::from_hsla( + this_hue + hue.unwrap_or(Number::zero()), + this_saturation + saturation.unwrap_or(Number::zero()), + this_luminance + luminance.unwrap_or(Number::zero()), + this_alpha + alpha.unwrap_or(Number::zero()), + )))); + } + + Ok(Value::Color(if let Some(a) = alpha { + let temp_alpha = color.alpha(); + Box::new(color.with_alpha(temp_alpha + a)) + } else { + color + })) +} + +fn scale_color(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + fn scale(val: Number, by: Number, max: Number) -> Number { + if by.is_zero() { + return val; + } + val.clone() + (if by.is_positive() { max - val } else { val }) * by + } + + let span = args.span(); + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(span)?), + span, + ) + .into()) + } + }; + + macro_rules! opt_scale_arg { + ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal, $scope:ident, $super_selector:ident) => { + let $name = match named_arg!($args, $scope, $super_selector, $arg = Value::Null) { + Value::Dimension(n, Unit::Percent) => { + Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number::from(100)) + } + v @ Value::Dimension(..) => { + return Err(( + format!( + "${}: Expected {} to have unit \"%\".", + $arg, + v.to_css_string($args.span())? + ), + $args.span(), + ) + .into()) + } + Value::Null => None, + v => { + return Err(( + format!( + "${}: {} is not a number.", + $arg, + v.to_css_string($args.span())? + ), + $args.span(), + ) + .into()) + } }; - } - - opt_scale_arg!(args, alpha, "alpha", -100, 100, scope, super_selector); - opt_scale_arg!(args, red, "red", -100, 100, scope, super_selector); - opt_scale_arg!(args, green, "green", -100, 100, scope, super_selector); - opt_scale_arg!(args, blue, "blue", -100, 100, scope, super_selector); - - if red.is_some() || green.is_some() || blue.is_some() { - return Ok(Value::Color(Box::new(Color::from_rgba( - scale( - color.red(), - red.unwrap_or(Number::zero()), - Number::from(255), - ), - scale( - color.green(), - green.unwrap_or(Number::zero()), - Number::from(255), - ), - scale( - color.blue(), - blue.unwrap_or(Number::zero()), - Number::from(255), - ), - scale( - color.alpha(), - alpha.unwrap_or(Number::zero()), - Number::one(), - ), - )))); - } - - opt_scale_arg!( - args, - saturation, - "saturation", - -100, - 100, - scope, - super_selector - ); - opt_scale_arg!( - args, - luminance, - "lightness", - -100, - 100, - scope, - super_selector - ); - - if 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(Box::new(Color::from_hsla( - scale(this_hue, Number::zero(), Number::from(360)), - scale( - this_saturation, - saturation.unwrap_or(Number::zero()), - Number::one(), - ), - scale( - this_luminance, - luminance.unwrap_or(Number::zero()), - Number::one(), - ), - scale(this_alpha, alpha.unwrap_or(Number::zero()), Number::one()), - )))); - } - - Ok(Value::Color(if let Some(a) = alpha { - let temp_alpha = color.alpha(); - Box::new(color.with_alpha(scale(temp_alpha, a, Number::one()))) - } else { - color - })) - } - - fn ie_hex_str( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } }; - Ok(Value::Ident(color.to_ie_hex_str(), QuoteKind::None)) } + opt_scale_arg!(args, alpha, "alpha", -100, 100, scope, super_selector); + opt_scale_arg!(args, red, "red", -100, 100, scope, super_selector); + opt_scale_arg!(args, green, "green", -100, 100, scope, super_selector); + opt_scale_arg!(args, blue, "blue", -100, 100, scope, super_selector); + + if red.is_some() || green.is_some() || blue.is_some() { + return Ok(Value::Color(Box::new(Color::from_rgba( + scale( + color.red(), + red.unwrap_or(Number::zero()), + Number::from(255), + ), + scale( + color.green(), + green.unwrap_or(Number::zero()), + Number::from(255), + ), + scale( + color.blue(), + blue.unwrap_or(Number::zero()), + Number::from(255), + ), + scale( + color.alpha(), + alpha.unwrap_or(Number::zero()), + Number::one(), + ), + )))); + } + + opt_scale_arg!( + args, + saturation, + "saturation", + -100, + 100, + scope, + super_selector + ); + opt_scale_arg!( + args, + luminance, + "lightness", + -100, + 100, + scope, + super_selector + ); + + if 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(Box::new(Color::from_hsla( + scale(this_hue, Number::zero(), Number::from(360)), + scale( + this_saturation, + saturation.unwrap_or(Number::zero()), + Number::one(), + ), + scale( + this_luminance, + luminance.unwrap_or(Number::zero()), + Number::one(), + ), + scale(this_alpha, alpha.unwrap_or(Number::zero()), Number::one()), + )))); + } + + Ok(Value::Color(if let Some(a) = alpha { + let temp_alpha = color.alpha(); + Box::new(color.with_alpha(scale(temp_alpha, a, Number::one()))) + } else { + color + })) +} + +fn ie_hex_str(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + Ok(Value::Ident(color.to_ie_hex_str(), QuoteKind::None)) +} + +pub(crate) fn register(f: &mut GlobalFunctionMap) { f.insert("change-color", Builtin::new(change_color)); f.insert("adjust-color", Builtin::new(adjust_color)); f.insert("scale-color", Builtin::new(scale_color)); diff --git a/src/builtin/color/rgb.rs b/src/builtin/color/rgb.rs index c760cb7..7bf0231 100644 --- a/src/builtin/color/rgb.rs +++ b/src/builtin/color/rgb.rs @@ -12,727 +12,715 @@ use crate::selector::Selector; use crate::unit::Unit; use crate::value::{Number, Value}; -pub(crate) fn register(f: &mut GlobalFunctionMap) { - fn rgb(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - if args.is_empty() { - return Err(("Missing argument $channels.", args.span()).into()); +fn rgb(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + if args.is_empty() { + return Err(("Missing argument $channels.", args.span()).into()); + } + + if args.len() == 1 { + let mut channels = match arg!(args, scope, super_selector, 0, "channels") { + Value::List(v, ..) => v, + _ => return Err(("Missing argument $channels.", args.span()).into()), + }; + + if channels.len() > 3 { + return Err(( + format!( + "Only 3 elements allowed, but {} were passed.", + channels.len() + ), + args.span(), + ) + .into()); } - if args.len() == 1 { - let mut channels = match arg!(args, scope, super_selector, 0, "channels") { - Value::List(v, ..) => v, - _ => return Err(("Missing argument $channels.", args.span()).into()), - }; + let blue = match channels.pop() { + Some(Value::Dimension(n, Unit::None)) => n, + Some(Value::Dimension(n, Unit::Percent)) => (n / Number::from(100)) * Number::from(255), + Some(v) if v.is_special_function() => { + let green = channels.pop().unwrap(); + let red = channels.pop().unwrap(); + return Ok(Value::Ident( + format!( + "rgb({}, {}, {})", + red.to_css_string(args.span())?, + green.to_css_string(args.span())?, + v.to_css_string(args.span())? + ), + QuoteKind::None, + )); + } + Some(v) => { + return Err(( + format!("$blue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $blue.", args.span()).into()), + }; - if channels.len() > 3 { + let green = match channels.pop() { + Some(Value::Dimension(n, Unit::None)) => n, + Some(Value::Dimension(n, Unit::Percent)) => (n / Number::from(100)) * Number::from(255), + Some(v) if v.is_special_function() => { + let string = match channels.pop() { + Some(red) => format!( + "rgb({}, {}, {})", + red.to_css_string(args.span())?, + v.to_css_string(args.span())?, + blue + ), + None => format!("rgb({} {})", v.to_css_string(args.span())?, blue), + }; + return Ok(Value::Ident(string, QuoteKind::None)); + } + Some(v) => { + return Err(( + format!("$green: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $green.", args.span()).into()), + }; + + let red = match channels.pop() { + Some(Value::Dimension(n, Unit::None)) => n, + Some(Value::Dimension(n, Unit::Percent)) => (n / Number::from(100)) * Number::from(255), + Some(v) if v.is_special_function() => { + return Ok(Value::Ident( + format!( + "rgb({}, {}, {})", + v.to_css_string(args.span())?, + green, + blue + ), + QuoteKind::None, + )); + } + Some(v) => { + return Err(( + format!("$red: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $red.", args.span()).into()), + }; + + let color = Color::from_rgba(red, green, blue, Number::one()); + + Ok(Value::Color(Box::new(color))) + } else if args.len() == 2 { + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v if v.is_special_function() => { + let alpha = arg!(args, scope, super_selector, 1, "alpha"); + return Ok(Value::Ident( + format!( + "rgb({}, {})", + v.to_css_string(args.span())?, + alpha.to_css_string(args.span())? + ), + QuoteKind::None, + )); + } + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let alpha = match arg!(args, scope, super_selector, 1, "alpha") { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => n / Number::from(100), + v @ Value::Dimension(..) => { return Err(( format!( - "Only 3 elements allowed, but {} were passed.", - channels.len() + "$alpha: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? ), args.span(), ) - .into()); + .into()) } - - let blue = match channels.pop() { - Some(Value::Dimension(n, Unit::None)) => n, - Some(Value::Dimension(n, Unit::Percent)) => { - (n / Number::from(100)) * Number::from(255) - } - Some(v) if v.is_special_function() => { - let green = channels.pop().unwrap(); - let red = channels.pop().unwrap(); - return Ok(Value::Ident( - format!( - "rgb({}, {}, {})", - red.to_css_string(args.span())?, - green.to_css_string(args.span())?, - v.to_css_string(args.span())? - ), - QuoteKind::None, - )); - } - Some(v) => { - return Err(( - format!("$blue: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - None => return Err(("Missing element $blue.", args.span()).into()), - }; - - let green = match channels.pop() { - Some(Value::Dimension(n, Unit::None)) => n, - Some(Value::Dimension(n, Unit::Percent)) => { - (n / Number::from(100)) * Number::from(255) - } - Some(v) if v.is_special_function() => { - let string = match channels.pop() { - Some(red) => format!( - "rgb({}, {}, {})", - red.to_css_string(args.span())?, - v.to_css_string(args.span())?, - blue - ), - None => format!("rgb({} {})", v.to_css_string(args.span())?, blue), - }; - return Ok(Value::Ident(string, QuoteKind::None)); - } - Some(v) => { - return Err(( - format!("$green: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - None => return Err(("Missing element $green.", args.span()).into()), - }; - - let red = match channels.pop() { - Some(Value::Dimension(n, Unit::None)) => n, - Some(Value::Dimension(n, Unit::Percent)) => { - (n / Number::from(100)) * Number::from(255) - } - Some(v) if v.is_special_function() => { - return Ok(Value::Ident( - format!( - "rgb({}, {}, {})", - v.to_css_string(args.span())?, - green, - blue - ), - QuoteKind::None, - )); - } - Some(v) => { - return Err(( - format!("$red: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - None => return Err(("Missing element $red.", args.span()).into()), - }; - - let color = Color::from_rgba(red, green, blue, Number::one()); - - Ok(Value::Color(Box::new(color))) - } else if args.len() == 2 { - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v if v.is_special_function() => { - let alpha = arg!(args, scope, super_selector, 1, "alpha"); - return Ok(Value::Ident( - format!( - "rgb({}, {})", - v.to_css_string(args.span())?, - alpha.to_css_string(args.span())? - ), - QuoteKind::None, - )); - } - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let alpha = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v if v.is_special_function() => { - return Ok(Value::Ident( - format!( - "rgb({}, {}, {}, {})", - color.red(), - color.green(), - color.blue(), - v.to_css_string(args.span())? - ), - QuoteKind::None, - )); - } - v => { - return Err(( - format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(color.with_alpha(alpha)))) - } else { - let red = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v if v.is_special_function() => { - let green = arg!(args, scope, super_selector, 1, "green"); - let blue = arg!(args, scope, super_selector, 2, "blue"); - let mut string = format!( - "rgb({}, {}, {}", - v.to_css_string(args.span())?, - green.to_css_string(args.span())?, - blue.to_css_string(args.span())? - ); - if !args.is_empty() { - string.push_str(", "); - string.push_str( - &arg!(args, scope, super_selector, 3, "alpha") - .to_css_string(args.span())?, - ); - } - string.push(')'); - return Ok(Value::Ident(string, QuoteKind::None)); - } - v => { - return Err(( - format!("$red: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let green = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v if v.is_special_function() => { - let blue = arg!(args, scope, super_selector, 2, "blue"); - let mut string = format!( - "rgb({}, {}, {}", - red, - v.to_css_string(args.span())?, - blue.to_css_string(args.span())? - ); - if !args.is_empty() { - string.push_str(", "); - string.push_str( - &arg!(args, scope, super_selector, 3, "alpha") - .to_css_string(args.span())?, - ); - } - string.push(')'); - return Ok(Value::Ident(string, QuoteKind::None)); - } - v => { - return Err(( - format!("$green: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let blue = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v if v.is_special_function() => { - let mut string = - format!("rgb({}, {}, {}", red, green, v.to_css_string(args.span())?); - if !args.is_empty() { - string.push_str(", "); - string.push_str( - &arg!(args, scope, super_selector, 3, "alpha") - .to_css_string(args.span())?, - ); - } - string.push(')'); - return Ok(Value::Ident(string, QuoteKind::None)); - } - v => { - return Err(( - format!("$blue: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let alpha = match arg!( - args, - scope, - super_selector, - 3, - "alpha" = Value::Dimension(Number::one(), 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.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v if v.is_special_function() => { - let string = format!( + v if v.is_special_function() => { + return Ok(Value::Ident( + format!( "rgb({}, {}, {}, {})", - red, - green, - blue, + color.red(), + color.green(), + color.blue(), v.to_css_string(args.span())? - ); - return Ok(Value::Ident(string, QuoteKind::None)); - } - v => { - return Err(( - format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(Color::from_rgba( - red, green, blue, alpha, - )))) - } - } - - fn rgba(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - if args.is_empty() { - return Err(("Missing argument $channels.", args.span()).into()); - } - - if args.len() == 1 { - let mut channels = match arg!(args, scope, super_selector, 0, "channels") { - Value::List(v, ..) => v, - _ => return Err(("Missing argument $channels.", args.span()).into()), - }; - - if channels.len() > 3 { + ), + QuoteKind::None, + )); + } + v => { + return Err(( + format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color.with_alpha(alpha)))) + } else { + let red = match arg!(args, scope, super_selector, 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!( - "Only 3 elements allowed, but {} were passed.", - channels.len() + "$red: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? ), args.span(), ) - .into()); + .into()) } - - let blue = match channels.pop() { - Some(Value::Dimension(n, Unit::None)) => n, - Some(Value::Dimension(n, Unit::Percent)) => { - (n / Number::from(100)) * Number::from(255) - } - Some(v) if v.is_special_function() => { - let green = channels.pop().unwrap(); - let red = channels.pop().unwrap(); - return Ok(Value::Ident( - format!( - "rgba({}, {}, {})", - red.to_css_string(args.span())?, - green.to_css_string(args.span())?, - v.to_css_string(args.span())? - ), - QuoteKind::None, - )); - } - Some(v) => { - return Err(( - format!("$blue: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - None => return Err(("Missing element $blue.", args.span()).into()), - }; - - let green = match channels.pop() { - Some(Value::Dimension(n, Unit::None)) => n, - Some(Value::Dimension(n, Unit::Percent)) => { - (n / Number::from(100)) * Number::from(255) - } - Some(v) if v.is_special_function() => { - let string = match channels.pop() { - Some(red) => format!( - "rgba({}, {}, {})", - red.to_css_string(args.span())?, - v.to_css_string(args.span())?, - blue - ), - None => format!("rgba({} {})", v.to_css_string(args.span())?, blue), - }; - return Ok(Value::Ident(string, QuoteKind::None)); - } - Some(v) => { - return Err(( - format!("$green: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - None => return Err(("Missing element $green.", args.span()).into()), - }; - - let red = match channels.pop() { - Some(Value::Dimension(n, Unit::None)) => n, - Some(Value::Dimension(n, Unit::Percent)) => { - (n / Number::from(100)) * Number::from(255) - } - Some(v) if v.is_special_function() => { - return Ok(Value::Ident( - format!( - "rgba({}, {}, {})", - v.to_css_string(args.span())?, - green, - blue - ), - QuoteKind::None, - )); - } - Some(v) => { - return Err(( - format!("$red: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - None => return Err(("Missing element $red.", args.span()).into()), - }; - - let color = Color::from_rgba(red, green, blue, Number::one()); - - Ok(Value::Color(Box::new(color))) - } else if args.len() == 2 { - let color = match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => c, - v if v.is_special_function() => { - let alpha = arg!(args, scope, super_selector, 1, "alpha"); - return Ok(Value::Ident( - format!( - "rgba({}, {})", - v.to_css_string(args.span())?, - alpha.to_css_string(args.span())? - ), - QuoteKind::None, - )); - } - v => { - return Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let alpha = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v if v.is_special_function() => { - return Ok(Value::Ident( - format!( - "rgba({}, {}, {}, {})", - color.red(), - color.green(), - color.blue(), - v.to_css_string(args.span())? - ), - QuoteKind::None, - )); - } - v => { - return Err(( - format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(color.with_alpha(alpha)))) - } else { - let red = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v if v.is_special_function() => { - let green = arg!(args, scope, super_selector, 1, "green"); - let blue = arg!(args, scope, super_selector, 2, "blue"); - let mut string = format!( - "rgba({}, {}, {}", - v.to_css_string(args.span())?, - green.to_css_string(args.span())?, - blue.to_css_string(args.span())? + v if v.is_special_function() => { + let green = arg!(args, scope, super_selector, 1, "green"); + let blue = arg!(args, scope, super_selector, 2, "blue"); + let mut string = format!( + "rgb({}, {}, {}", + v.to_css_string(args.span())?, + green.to_css_string(args.span())?, + blue.to_css_string(args.span())? + ); + if !args.is_empty() { + string.push_str(", "); + string.push_str( + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); - if !args.is_empty() { - string.push_str(", "); - string.push_str( - &arg!(args, scope, super_selector, 3, "alpha") - .to_css_string(args.span())?, - ); - } - string.push(')'); - return Ok(Value::Ident(string, QuoteKind::None)); } - v => { - return Err(( - format!("$red: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let green = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v if v.is_special_function() => { - let blue = arg!(args, scope, super_selector, 2, "blue"); - let mut string = format!( - "rgba({}, {}, {}", - red, - v.to_css_string(args.span())?, - blue.to_css_string(args.span())? - ); - if !args.is_empty() { - string.push_str(", "); - string.push_str( - &arg!(args, scope, super_selector, 3, "alpha") - .to_css_string(args.span())?, - ); - } - string.push(')'); - return Ok(Value::Ident(string, QuoteKind::None)); - } - v => { - return Err(( - format!("$green: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let blue = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v if v.is_special_function() => { - let mut string = - format!("rgba({}, {}, {}", red, green, v.to_css_string(args.span())?); - if !args.is_empty() { - string.push_str(", "); - string.push_str( - &arg!(args, scope, super_selector, 3, "alpha") - .to_css_string(args.span())?, - ); - } - string.push(')'); - return Ok(Value::Ident(string, QuoteKind::None)); - } - v => { - return Err(( - format!("$blue: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let alpha = match arg!( - args, - scope, - super_selector, - 3, - "alpha" = Value::Dimension(Number::one(), 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.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v if v.is_special_function() => { - let string = format!( - "rgba({}, {}, {}, {})", - red, - green, - blue, + string.push(')'); + return Ok(Value::Ident(string, QuoteKind::None)); + } + v => { + return Err(( + format!("$red: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let green = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + v if v.is_special_function() => { + let blue = arg!(args, scope, super_selector, 2, "blue"); + let mut string = format!( + "rgb({}, {}, {}", + red, + v.to_css_string(args.span())?, + blue.to_css_string(args.span())? + ); + if !args.is_empty() { + string.push_str(", "); + string.push_str( + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); - return Ok(Value::Ident(string, QuoteKind::None)); } - v => { - return Err(( - format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - Ok(Value::Color(Box::new(Color::from_rgba( - red, green, blue, alpha, - )))) - } - } - - fn red(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.red(), Unit::None)), - v => Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()), - } - } - - fn green(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.green(), Unit::None)), - v => Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()), - } - } - - fn blue(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "color") { - Value::Color(c) => Ok(Value::Dimension(c.blue(), Unit::None)), - v => Err(( - format!("$color: {} is not a color.", v.to_css_string(args.span())?), - args.span(), - ) - .into()), - } - } - - fn mix(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(3)?; - let color1 = match arg!(args, scope, super_selector, 0, "color1") { - Value::Color(c) => c, + string.push(')'); + return Ok(Value::Ident(string, QuoteKind::None)); + } v => { return Err(( - format!("$color1: {} is not a color.", v.to_css_string(args.span())?), + format!("$green: {} is not a number.", v.to_css_string(args.span())?), args.span(), ) .into()) } }; - - let color2 = match arg!(args, scope, super_selector, 1, "color2") { - Value::Color(c) => c, + let blue = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + v if v.is_special_function() => { + let mut string = + format!("rgb({}, {}, {}", red, green, v.to_css_string(args.span())?); + if !args.is_empty() { + string.push_str(", "); + string.push_str( + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, + ); + } + string.push(')'); + return Ok(Value::Ident(string, QuoteKind::None)); + } v => { return Err(( - format!("$color2: {} is not a color.", v.to_css_string(args.span())?), + format!("$blue: {} is not a number.", v.to_css_string(args.span())?), args.span(), ) .into()) } }; - - let weight = match arg!( + let alpha = match arg!( args, scope, super_selector, - 2, - "weight" = Value::Dimension(Number::from(50), Unit::None) + 3, + "alpha" = Value::Dimension(Number::one(), Unit::None) ) { - Value::Dimension(n, u) => bound!(args, "weight", n, u, 0, 100) / Number::from(100), - v => { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => n / Number::from(100), + v @ Value::Dimension(..) => { return Err(( format!( - "$weight: {} is not a number.", + "$alpha: Expected {} to have no units or \"%\".", v.to_css_string(args.span())? ), args.span(), ) .into()) } + v if v.is_special_function() => { + let string = format!( + "rgb({}, {}, {}, {})", + red, + green, + blue, + v.to_css_string(args.span())? + ); + return Ok(Value::Ident(string, QuoteKind::None)); + } + v => { + return Err(( + format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; - Ok(Value::Color(Box::new(color1.mix(&color2, weight)))) + Ok(Value::Color(Box::new(Color::from_rgba( + red, green, blue, alpha, + )))) + } +} + +fn rgba(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + if args.is_empty() { + return Err(("Missing argument $channels.", args.span()).into()); } + if args.len() == 1 { + let mut channels = match arg!(args, scope, super_selector, 0, "channels") { + Value::List(v, ..) => v, + _ => return Err(("Missing argument $channels.", args.span()).into()), + }; + + if channels.len() > 3 { + return Err(( + format!( + "Only 3 elements allowed, but {} were passed.", + channels.len() + ), + args.span(), + ) + .into()); + } + + let blue = match channels.pop() { + Some(Value::Dimension(n, Unit::None)) => n, + Some(Value::Dimension(n, Unit::Percent)) => (n / Number::from(100)) * Number::from(255), + Some(v) if v.is_special_function() => { + let green = channels.pop().unwrap(); + let red = channels.pop().unwrap(); + return Ok(Value::Ident( + format!( + "rgba({}, {}, {})", + red.to_css_string(args.span())?, + green.to_css_string(args.span())?, + v.to_css_string(args.span())? + ), + QuoteKind::None, + )); + } + Some(v) => { + return Err(( + format!("$blue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $blue.", args.span()).into()), + }; + + let green = match channels.pop() { + Some(Value::Dimension(n, Unit::None)) => n, + Some(Value::Dimension(n, Unit::Percent)) => (n / Number::from(100)) * Number::from(255), + Some(v) if v.is_special_function() => { + let string = match channels.pop() { + Some(red) => format!( + "rgba({}, {}, {})", + red.to_css_string(args.span())?, + v.to_css_string(args.span())?, + blue + ), + None => format!("rgba({} {})", v.to_css_string(args.span())?, blue), + }; + return Ok(Value::Ident(string, QuoteKind::None)); + } + Some(v) => { + return Err(( + format!("$green: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $green.", args.span()).into()), + }; + + let red = match channels.pop() { + Some(Value::Dimension(n, Unit::None)) => n, + Some(Value::Dimension(n, Unit::Percent)) => (n / Number::from(100)) * Number::from(255), + Some(v) if v.is_special_function() => { + return Ok(Value::Ident( + format!( + "rgba({}, {}, {})", + v.to_css_string(args.span())?, + green, + blue + ), + QuoteKind::None, + )); + } + Some(v) => { + return Err(( + format!("$red: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $red.", args.span()).into()), + }; + + let color = Color::from_rgba(red, green, blue, Number::one()); + + Ok(Value::Color(Box::new(color))) + } else if args.len() == 2 { + let color = match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => c, + v if v.is_special_function() => { + let alpha = arg!(args, scope, super_selector, 1, "alpha"); + return Ok(Value::Ident( + format!( + "rgba({}, {})", + v.to_css_string(args.span())?, + alpha.to_css_string(args.span())? + ), + QuoteKind::None, + )); + } + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let alpha = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + v if v.is_special_function() => { + return Ok(Value::Ident( + format!( + "rgba({}, {}, {}, {})", + color.red(), + color.green(), + color.blue(), + v.to_css_string(args.span())? + ), + QuoteKind::None, + )); + } + v => { + return Err(( + format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color.with_alpha(alpha)))) + } else { + let red = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + v if v.is_special_function() => { + let green = arg!(args, scope, super_selector, 1, "green"); + let blue = arg!(args, scope, super_selector, 2, "blue"); + let mut string = format!( + "rgba({}, {}, {}", + v.to_css_string(args.span())?, + green.to_css_string(args.span())?, + blue.to_css_string(args.span())? + ); + if !args.is_empty() { + string.push_str(", "); + string.push_str( + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, + ); + } + string.push(')'); + return Ok(Value::Ident(string, QuoteKind::None)); + } + v => { + return Err(( + format!("$red: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let green = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + v if v.is_special_function() => { + let blue = arg!(args, scope, super_selector, 2, "blue"); + let mut string = format!( + "rgba({}, {}, {}", + red, + v.to_css_string(args.span())?, + blue.to_css_string(args.span())? + ); + if !args.is_empty() { + string.push_str(", "); + string.push_str( + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, + ); + } + string.push(')'); + return Ok(Value::Ident(string, QuoteKind::None)); + } + v => { + return Err(( + format!("$green: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let blue = match arg!(args, scope, super_selector, 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.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + v if v.is_special_function() => { + let mut string = + format!("rgba({}, {}, {}", red, green, v.to_css_string(args.span())?); + if !args.is_empty() { + string.push_str(", "); + string.push_str( + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, + ); + } + string.push(')'); + return Ok(Value::Ident(string, QuoteKind::None)); + } + v => { + return Err(( + format!("$blue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let alpha = match arg!( + args, + scope, + super_selector, + 3, + "alpha" = Value::Dimension(Number::one(), 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.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + v if v.is_special_function() => { + let string = format!( + "rgba({}, {}, {}, {})", + red, + green, + blue, + v.to_css_string(args.span())? + ); + return Ok(Value::Ident(string, QuoteKind::None)); + } + v => { + return Err(( + format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(Color::from_rgba( + red, green, blue, alpha, + )))) + } +} + +fn red(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.red(), Unit::None)), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), + } +} + +fn green(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.green(), Unit::None)), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), + } +} + +fn blue(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "color") { + Value::Color(c) => Ok(Value::Dimension(c.blue(), Unit::None)), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), + } +} + +fn mix(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(3)?; + let color1 = match arg!(args, scope, super_selector, 0, "color1") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color1: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + + let color2 = match arg!(args, scope, super_selector, 1, "color2") { + Value::Color(c) => c, + v => { + return Err(( + format!("$color2: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + + let weight = match arg!( + args, + scope, + super_selector, + 2, + "weight" = Value::Dimension(Number::from(50), Unit::None) + ) { + Value::Dimension(n, u) => bound!(args, "weight", n, u, 0, 100) / Number::from(100), + v => { + return Err(( + format!( + "$weight: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + Ok(Value::Color(Box::new(color1.mix(&color2, weight)))) +} + +pub(crate) fn register(f: &mut GlobalFunctionMap) { f.insert("rgb", Builtin::new(rgb)); f.insert("rgba", Builtin::new(rgba)); f.insert("red", Builtin::new(red)); diff --git a/src/builtin/list.rs b/src/builtin/list.rs index 5ccbf08..3af8016 100644 --- a/src/builtin/list.rs +++ b/src/builtin/list.rs @@ -11,316 +11,312 @@ use crate::selector::Selector; use crate::unit::Unit; use crate::value::{Number, Value}; -pub(crate) fn register(f: &mut GlobalFunctionMap) { - fn length(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - let len = match arg!(args, scope, super_selector, 0, "list") { - Value::List(v, ..) => Number::from(v.len()), - Value::Map(m) => Number::from(m.len()), - _ => Number::one(), - }; - Ok(Value::Dimension(len, Unit::None)) +fn length(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + let len = match arg!(args, scope, super_selector, 0, "list") { + Value::List(v, ..) => Number::from(v.len()), + Value::Map(m) => Number::from(m.len()), + _ => Number::one(), + }; + Ok(Value::Dimension(len, Unit::None)) +} + +fn nth(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let list = match arg!(args, scope, super_selector, 0, "list") { + Value::List(v, ..) => v, + Value::Map(m) => m.entries(), + v => vec![v], + }; + let n = match arg!(args, scope, super_selector, 1, "n") { + Value::Dimension(num, _) => num, + v => { + return Err(( + format!("$n: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + + if n.is_zero() { + return Err(("$n: List index may not be 0.", args.span()).into()); } - fn nth(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(2)?; - let list = match arg!(args, scope, super_selector, 0, "list") { - Value::List(v, ..) => v, - Value::Map(m) => m.entries(), - v => vec![v], - }; - let n = match arg!(args, scope, super_selector, 1, "n") { - Value::Dimension(num, _) => num, - v => { + if n.abs() > Number::from(list.len()) { + return Err(( + format!( + "$n: Invalid index {} for a list with {} elements.", + n, + list.len() + ), + args.span(), + ) + .into()); + } + + if n.is_decimal() { + return Err((format!("$n: {} is not an int.", n), args.span()).into()); + } + + if n.is_positive() { + Ok(list[n.to_integer().to_usize().unwrap() - 1].clone()) + } else { + Ok(list[list.len() - n.abs().to_integer().to_usize().unwrap()].clone()) + } +} + +fn list_separator( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(1)?; + Ok(Value::Ident( + match arg!(args, scope, super_selector, 0, "list") { + Value::List(_, sep, ..) => sep.name(), + _ => ListSeparator::Space.name(), + } + .to_owned(), + QuoteKind::None, + )) +} + +fn set_nth(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(3)?; + let (mut list, sep, brackets) = match arg!(args, scope, super_selector, 0, "list") { + Value::List(v, sep, b) => (v, sep, b), + Value::Map(m) => (m.entries(), ListSeparator::Comma, Brackets::None), + v => (vec![v], ListSeparator::Space, Brackets::None), + }; + let n = match arg!(args, scope, super_selector, 1, "n") { + Value::Dimension(num, _) => num, + v => { + return Err(( + format!("$n: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + + if n.is_zero() { + return Err(("$n: List index may not be 0.", args.span()).into()); + } + + let len = list.len(); + + if n.abs() > Number::from(len) { + return Err(( + format!("$n: Invalid index {} for a list with {} elements.", n, len), + args.span(), + ) + .into()); + } + + if n.is_decimal() { + return Err((format!("$n: {} is not an int.", n), args.span()).into()); + } + + let val = arg!(args, scope, super_selector, 2, "value"); + + if n.is_positive() { + list[n.to_integer().to_usize().unwrap() - 1] = val; + } else { + list[len - n.abs().to_integer().to_usize().unwrap()] = val; + } + + Ok(Value::List(list, sep, brackets)) +} + +fn append(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(3)?; + let (mut list, sep, brackets) = match arg!(args, scope, super_selector, 0, "list") { + Value::List(v, sep, b) => (v, sep, b), + v => (vec![v], ListSeparator::Space, Brackets::None), + }; + let val = arg!(args, scope, super_selector, 1, "val"); + let sep = match arg!( + args, + scope, + super_selector, + 2, + "separator" = Value::Ident("auto".to_owned(), QuoteKind::None) + ) { + Value::Ident(s, ..) => match s.as_str() { + "auto" => sep, + "comma" => ListSeparator::Comma, + "space" => ListSeparator::Space, + _ => { return Err(( - format!("$n: {} is not a number.", v.to_css_string(args.span())?), + "$separator: Must be \"space\", \"comma\", or \"auto\".", args.span(), ) .into()) } - }; - - if n.is_zero() { - return Err(("$n: List index may not be 0.", args.span()).into()); - } - - if n.abs() > Number::from(list.len()) { + }, + v => { return Err(( format!( - "$n: Invalid index {} for a list with {} elements.", - n, - list.len() + "$separator: {} is not a string.", + v.to_css_string(args.span())? ), args.span(), ) - .into()); + .into()) } + }; - if n.is_decimal() { - return Err((format!("$n: {} is not an int.", n), args.span()).into()); - } + list.push(val); - if n.is_positive() { - Ok(list[n.to_integer().to_usize().unwrap() - 1].clone()) - } else { - Ok(list[list.len() - n.abs().to_integer().to_usize().unwrap()].clone()) - } - } + Ok(Value::List(list, sep, brackets)) +} - fn list_separator( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - Ok(Value::Ident( - match arg!(args, scope, super_selector, 0, "list") { - Value::List(_, sep, ..) => sep.name(), - _ => ListSeparator::Space.name(), +fn join(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(4)?; + let (mut list1, sep1, brackets) = match arg!(args, scope, super_selector, 0, "list1") { + Value::List(v, sep, brackets) => (v, sep, brackets), + Value::Map(m) => (m.entries(), ListSeparator::Comma, Brackets::None), + v => (vec![v], ListSeparator::Space, Brackets::None), + }; + let (list2, sep2) = match arg!(args, scope, super_selector, 1, "list2") { + Value::List(v, sep, ..) => (v, sep), + Value::Map(m) => (m.entries(), ListSeparator::Comma), + v => (vec![v], ListSeparator::Space), + }; + let sep = match arg!( + args, + scope, + super_selector, + 2, + "separator" = Value::Ident("auto".to_owned(), QuoteKind::None) + ) { + Value::Ident(s, ..) => match s.as_str() { + "auto" => { + if list1.is_empty() || (list1.len() == 1 && sep1 == ListSeparator::Space) { + sep2 + } else { + sep1 + } } - .to_owned(), - QuoteKind::None, - )) - } - - fn set_nth(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(3)?; - let (mut list, sep, brackets) = match arg!(args, scope, super_selector, 0, "list") { - Value::List(v, sep, b) => (v, sep, b), - Value::Map(m) => (m.entries(), ListSeparator::Comma, Brackets::None), - v => (vec![v], ListSeparator::Space, Brackets::None), - }; - let n = match arg!(args, scope, super_selector, 1, "n") { - Value::Dimension(num, _) => num, - v => { + "comma" => ListSeparator::Comma, + "space" => ListSeparator::Space, + _ => { return Err(( - format!("$n: {} is not a number.", v.to_css_string(args.span())?), + "$separator: Must be \"space\", \"comma\", or \"auto\".", args.span(), ) .into()) } - }; - - if n.is_zero() { - return Err(("$n: List index may not be 0.", args.span()).into()); - } - - let len = list.len(); - - if n.abs() > Number::from(len) { + }, + v => { return Err(( - format!("$n: Invalid index {} for a list with {} elements.", n, len), + format!( + "$separator: {} is not a string.", + v.to_css_string(args.span())? + ), args.span(), ) - .into()); + .into()) } + }; - if n.is_decimal() { - return Err((format!("$n: {} is not an int.", n), args.span()).into()); + let brackets = match arg!( + args, + scope, + super_selector, + 3, + "bracketed" = Value::Ident("auto".to_owned(), QuoteKind::None) + ) { + Value::Ident(s, ..) => match s.as_str() { + "auto" => brackets, + _ => Brackets::Bracketed, + }, + v => { + if v.is_true(args.span())? { + Brackets::Bracketed + } else { + Brackets::None + } } + }; - let val = arg!(args, scope, super_selector, 2, "value"); + list1.extend(list2); - if n.is_positive() { - list[n.to_integer().to_usize().unwrap() - 1] = val; - } else { - list[len - n.abs().to_integer().to_usize().unwrap()] = val; - } + Ok(Value::List(list1, sep, brackets)) +} - Ok(Value::List(list, sep, brackets)) - } - - fn append(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(3)?; - let (mut list, sep, brackets) = match arg!(args, scope, super_selector, 0, "list") { - Value::List(v, sep, b) => (v, sep, b), - v => (vec![v], ListSeparator::Space, Brackets::None), - }; - let val = arg!(args, scope, super_selector, 1, "val"); - let sep = match arg!( - args, - scope, - super_selector, - 2, - "separator" = Value::Ident("auto".to_owned(), QuoteKind::None) - ) { - Value::Ident(s, ..) => match s.as_str() { - "auto" => sep, - "comma" => ListSeparator::Comma, - "space" => ListSeparator::Space, - _ => { - return Err(( - "$separator: Must be \"space\", \"comma\", or \"auto\".", - args.span(), - ) - .into()) - } +fn is_bracketed(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + Ok(Value::bool( + match arg!(args, scope, super_selector, 0, "list") { + Value::List(.., brackets) => match brackets { + Brackets::Bracketed => true, + Brackets::None => false, }, - v => { - return Err(( - format!( - "$separator: {} is not a string.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; + _ => false, + }, + )) +} - list.push(val); +fn index(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let list = match arg!(args, scope, super_selector, 0, "list") { + Value::List(v, ..) => v, + Value::Map(m) => m.entries(), + v => vec![v], + }; + let value = arg!(args, scope, super_selector, 1, "value"); + // TODO: find a way around this unwrap. + // It should be impossible to hit as the arg is + // evaluated prior to checking equality, but + // it is still dirty. + // Potential input to fuzz: index(1px 1in 1cm, 96px + 1rem) + let index = match list + .into_iter() + .position(|v| v.equals(value.clone(), args.span()).unwrap()) + { + Some(v) => Number::from(v + 1), + None => return Ok(Value::Null), + }; + Ok(Value::Dimension(index, Unit::None)) +} - Ok(Value::List(list, sep, brackets)) - } - - fn join(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(4)?; - let (mut list1, sep1, brackets) = match arg!(args, scope, super_selector, 0, "list1") { - Value::List(v, sep, brackets) => (v, sep, brackets), - Value::Map(m) => (m.entries(), ListSeparator::Comma, Brackets::None), - v => (vec![v], ListSeparator::Space, Brackets::None), - }; - let (list2, sep2) = match arg!(args, scope, super_selector, 1, "list2") { - Value::List(v, sep, ..) => (v, sep), - Value::Map(m) => (m.entries(), ListSeparator::Comma), - v => (vec![v], ListSeparator::Space), - }; - let sep = match arg!( - args, - scope, - super_selector, - 2, - "separator" = Value::Ident("auto".to_owned(), QuoteKind::None) - ) { - Value::Ident(s, ..) => match s.as_str() { - "auto" => { - if list1.is_empty() || (list1.len() == 1 && sep1 == ListSeparator::Space) { - sep2 - } else { - sep1 - } - } - "comma" => ListSeparator::Comma, - "space" => ListSeparator::Space, - _ => { - return Err(( - "$separator: Must be \"space\", \"comma\", or \"auto\".", - args.span(), - ) - .into()) - } - }, - v => { - return Err(( - format!( - "$separator: {} is not a string.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - - let brackets = match arg!( - args, - scope, - super_selector, - 3, - "bracketed" = Value::Ident("auto".to_owned(), QuoteKind::None) - ) { - Value::Ident(s, ..) => match s.as_str() { - "auto" => brackets, - _ => Brackets::Bracketed, - }, - v => { - if v.is_true(args.span())? { - Brackets::Bracketed - } else { - Brackets::None - } - } - }; - - list1.extend(list2); - - Ok(Value::List(list1, sep, brackets)) - } - - fn is_bracketed( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - Ok(Value::bool( - match arg!(args, scope, super_selector, 0, "list") { - Value::List(.., brackets) => match brackets { - Brackets::Bracketed => true, - Brackets::None => false, - }, - _ => false, - }, - )) - } - - fn index(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(2)?; - let list = match arg!(args, scope, super_selector, 0, "list") { - Value::List(v, ..) => v, - Value::Map(m) => m.entries(), - v => vec![v], - }; - let value = arg!(args, scope, super_selector, 1, "value"); - // TODO: find a way around this unwrap. - // It should be impossible to hit as the arg is - // evaluated prior to checking equality, but - // it is still dirty. - // Potential input to fuzz: index(1px 1in 1cm, 96px + 1rem) - let index = match list - .into_iter() - .position(|v| v.equals(value.clone(), args.span()).unwrap()) - { - Some(v) => Number::from(v + 1), - None => return Ok(Value::Null), - }; - Ok(Value::Dimension(index, Unit::None)) - } - - fn zip(args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - let span = args.span(); - let lists = args - .get_variadic(scope, super_selector)? - .into_iter() - .map(|x| { - Ok(match x.node.eval(span)?.node { - Value::List(v, ..) => v, - Value::Map(m) => m.entries(), - v => vec![v], - }) +fn zip(args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + let span = args.span(); + let lists = args + .get_variadic(scope, super_selector)? + .into_iter() + .map(|x| { + Ok(match x.node.eval(span)?.node { + Value::List(v, ..) => v, + Value::Map(m) => m.entries(), + v => vec![v], }) - .collect::>>>()?; + }) + .collect::>>>()?; - let len = lists.iter().map(Vec::len).min().unwrap_or(0); + let len = lists.iter().map(Vec::len).min().unwrap_or(0); - if len == 0 { - return Ok(Value::List( - Vec::new(), - ListSeparator::Comma, - Brackets::None, - )); - } - - let result = (0..len) - .map(|i| { - let items = lists.iter().map(|v| v[i].clone()).collect(); - Value::List(items, ListSeparator::Space, Brackets::None) - }) - .collect(); - - Ok(Value::List(result, ListSeparator::Comma, Brackets::None)) + if len == 0 { + return Ok(Value::List( + Vec::new(), + ListSeparator::Comma, + Brackets::None, + )); } + let result = (0..len) + .map(|i| { + let items = lists.iter().map(|v| v[i].clone()).collect(); + Value::List(items, ListSeparator::Space, Brackets::None) + }) + .collect(); + + Ok(Value::List(result, ListSeparator::Comma, Brackets::None)) +} + +pub(crate) fn register(f: &mut GlobalFunctionMap) { f.insert("length", Builtin::new(length)); f.insert("nth", Builtin::new(nth)); f.insert("list-separator", Builtin::new(list_separator)); diff --git a/src/builtin/map.rs b/src/builtin/map.rs index eced788..15f44bc 100644 --- a/src/builtin/map.rs +++ b/src/builtin/map.rs @@ -8,144 +8,128 @@ use crate::scope::Scope; use crate::selector::Selector; use crate::value::{SassMap, Value}; -pub(crate) fn register(f: &mut GlobalFunctionMap) { - fn map_get(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(2)?; - let key = arg!(args, scope, super_selector, 1, "key"); - let map = match arg!(args, scope, super_selector, 0, "map") { - Value::Map(m) => m, - Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => { - return Err(( - format!("$map: {} is not a map.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - Ok(map.get(&key, args.span())?.unwrap_or(Value::Null)) - } - - fn map_has_key( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(2)?; - let key = arg!(args, scope, super_selector, 1, "key"); - let map = match arg!(args, scope, super_selector, 0, "map") { - Value::Map(m) => m, - Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => { - return Err(( - format!("$map: {} is not a map.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - Ok(Value::bool(map.get(&key, args.span())?.is_some())) - } - - fn map_keys(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - let map = match arg!(args, scope, super_selector, 0, "map") { - Value::Map(m) => m, - Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => { - return Err(( - format!("$map: {} is not a map.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - Ok(Value::List( - map.keys(), - ListSeparator::Comma, - Brackets::None, - )) - } - - fn map_values( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - let map = match arg!(args, scope, super_selector, 0, "map") { - Value::Map(m) => m, - Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => { - return Err(( - format!("$map: {} is not a map.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - Ok(Value::List( - map.values(), - ListSeparator::Comma, - Brackets::None, - )) - } - - fn map_merge( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(2)?; - let mut map1 = match arg!(args, scope, super_selector, 0, "map1") { - Value::Map(m) => m, - Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => { - return Err(( - format!("$map1: {} is not a map.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let map2 = match arg!(args, scope, super_selector, 1, "map2") { - Value::Map(m) => m, - Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => { - return Err(( - format!("$map2: {} is not a map.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - map1.merge(map2); - Ok(Value::Map(map1)) - } - - fn map_remove( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - let mut map = match arg!(args, scope, super_selector, 0, "map") { - Value::Map(m) => m, - Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => { - return Err(( - format!("$map: {} is not a map.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let keys = args.get_variadic(scope, super_selector)?; - for key in keys { - map.remove(&key); +fn map_get(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let key = arg!(args, scope, super_selector, 1, "key"); + let map = match arg!(args, scope, super_selector, 0, "map") { + Value::Map(m) => m, + Value::List(v, ..) if v.is_empty() => SassMap::new(), + v => { + return Err(( + format!("$map: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) } - Ok(Value::Map(map)) - } + }; + Ok(map.get(&key, args.span())?.unwrap_or(Value::Null)) +} +fn map_has_key(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let key = arg!(args, scope, super_selector, 1, "key"); + let map = match arg!(args, scope, super_selector, 0, "map") { + Value::Map(m) => m, + Value::List(v, ..) if v.is_empty() => SassMap::new(), + v => { + return Err(( + format!("$map: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + Ok(Value::bool(map.get(&key, args.span())?.is_some())) +} + +fn map_keys(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + let map = match arg!(args, scope, super_selector, 0, "map") { + Value::Map(m) => m, + Value::List(v, ..) if v.is_empty() => SassMap::new(), + v => { + return Err(( + format!("$map: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + Ok(Value::List( + map.keys(), + ListSeparator::Comma, + Brackets::None, + )) +} + +fn map_values(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + let map = match arg!(args, scope, super_selector, 0, "map") { + Value::Map(m) => m, + Value::List(v, ..) if v.is_empty() => SassMap::new(), + v => { + return Err(( + format!("$map: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + Ok(Value::List( + map.values(), + ListSeparator::Comma, + Brackets::None, + )) +} + +fn map_merge(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let mut map1 = match arg!(args, scope, super_selector, 0, "map1") { + Value::Map(m) => m, + Value::List(v, ..) if v.is_empty() => SassMap::new(), + v => { + return Err(( + format!("$map1: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let map2 = match arg!(args, scope, super_selector, 1, "map2") { + Value::Map(m) => m, + Value::List(v, ..) if v.is_empty() => SassMap::new(), + v => { + return Err(( + format!("$map2: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + map1.merge(map2); + Ok(Value::Map(map1)) +} + +fn map_remove(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + let mut map = match arg!(args, scope, super_selector, 0, "map") { + Value::Map(m) => m, + Value::List(v, ..) if v.is_empty() => SassMap::new(), + v => { + return Err(( + format!("$map: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + let keys = args.get_variadic(scope, super_selector)?; + for key in keys { + map.remove(&key); + } + Ok(Value::Map(map)) +} + +pub(crate) fn register(f: &mut GlobalFunctionMap) { f.insert("map-get", Builtin::new(map_get)); f.insert("map-has-key", Builtin::new(map_has_key)); f.insert("map-keys", Builtin::new(map_keys)); diff --git a/src/builtin/math.rs b/src/builtin/math.rs index b826f7f..2e949b2 100644 --- a/src/builtin/math.rs +++ b/src/builtin/math.rs @@ -13,191 +13,183 @@ use crate::selector::Selector; use crate::unit::Unit; use crate::value::{Number, Value}; -pub(crate) fn register(f: &mut GlobalFunctionMap) { - fn percentage( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - let num = match arg!(args, scope, super_selector, 0, "number") { - Value::Dimension(n, Unit::None) => n * Number::from(100), - v @ Value::Dimension(..) => { - return Err(( - format!( - "$number: Expected {} to have no units.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v => { - return Err(( - format!( - "$number: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - Ok(Value::Dimension(num, Unit::Percent)) - } - - fn round(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "number") { - Value::Dimension(n, u) => Ok(Value::Dimension(n.round(), u)), - v => Err(( - format!( - "$number: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()), - } - } - - fn ceil(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "number") { - Value::Dimension(n, u) => Ok(Value::Dimension(n.ceil(), u)), - v => Err(( - format!( - "$number: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()), - } - } - - fn floor(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "number") { - Value::Dimension(n, u) => Ok(Value::Dimension(n.floor(), u)), - v => Err(( - format!( - "$number: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()), - } - } - - fn abs(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "number") { - Value::Dimension(n, u) => Ok(Value::Dimension(n.abs(), u)), - v => Err(( - format!( - "$number: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()), - } - } - - fn comparable( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(2)?; - let unit1 = match arg!(args, scope, super_selector, 0, "number1") { - Value::Dimension(_, u) => u, - v => { - return Err(( - format!( - "$number1: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - let unit2 = match arg!(args, scope, super_selector, 1, "number2") { - Value::Dimension(_, u) => u, - v => { - return Err(( - format!( - "$number2: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - - Ok(Value::bool(unit1.comparable(&unit2))) - } - - // TODO: write tests for this - #[cfg(feature = "random")] - fn random(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - let limit = match arg!(args, scope, super_selector, 0, "limit" = Value::Null) { - Value::Dimension(n, _) => n, - Value::Null => { - let mut rng = rand::thread_rng(); - return Ok(Value::Dimension( - Number::from(rng.gen_range(0.0, 1.0)), - Unit::None, - )); - } - v => { - return Err(( - format!("$limit: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - - if limit.is_one() { - return Ok(Value::Dimension(Number::one(), Unit::None)); - } - - if limit.is_decimal() { - return Err((format!("$limit: {} is not an int.", limit), args.span()).into()); - } - - if limit.is_zero() || limit.is_negative() { +fn percentage(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + let num = match arg!(args, scope, super_selector, 0, "number") { + Value::Dimension(n, Unit::None) => n * Number::from(100), + v @ Value::Dimension(..) => { return Err(( - format!("$limit: Must be greater than 0, was {}.", limit), + format!( + "$number: Expected {} to have no units.", + v.to_css_string(args.span())? + ), args.span(), ) - .into()); + .into()) } + v => { + return Err(( + format!( + "$number: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + Ok(Value::Dimension(num, Unit::Percent)) +} - let limit = match limit.to_integer().to_u32() { - Some(n) => n, - None => { - return Err(( - format!("max must be in range 0 < max \u{2264} 2^32, was {}", limit), - args.span(), - ) - .into()) - } - }; +fn round(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "number") { + Value::Dimension(n, u) => Ok(Value::Dimension(n.round(), u)), + v => Err(( + format!( + "$number: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), + } +} - let mut rng = rand::thread_rng(); - Ok(Value::Dimension( - Number::from(rng.gen_range(0, limit) + 1), - Unit::None, - )) +fn ceil(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "number") { + Value::Dimension(n, u) => Ok(Value::Dimension(n.ceil(), u)), + v => Err(( + format!( + "$number: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), + } +} + +fn floor(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "number") { + Value::Dimension(n, u) => Ok(Value::Dimension(n.floor(), u)), + v => Err(( + format!( + "$number: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), + } +} + +fn abs(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "number") { + Value::Dimension(n, u) => Ok(Value::Dimension(n.abs(), u)), + v => Err(( + format!( + "$number: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), + } +} + +fn comparable(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let unit1 = match arg!(args, scope, super_selector, 0, "number1") { + Value::Dimension(_, u) => u, + v => { + return Err(( + format!( + "$number1: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + let unit2 = match arg!(args, scope, super_selector, 1, "number2") { + Value::Dimension(_, u) => u, + v => { + return Err(( + format!( + "$number2: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + + Ok(Value::bool(unit1.comparable(&unit2))) +} + +// TODO: write tests for this +#[cfg(feature = "random")] +fn random(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + let limit = match arg!(args, scope, super_selector, 0, "limit" = Value::Null) { + Value::Dimension(n, _) => n, + Value::Null => { + let mut rng = rand::thread_rng(); + return Ok(Value::Dimension( + Number::from(rng.gen_range(0.0, 1.0)), + Unit::None, + )); + } + v => { + return Err(( + format!("$limit: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + + if limit.is_one() { + return Ok(Value::Dimension(Number::one(), Unit::None)); } + if limit.is_decimal() { + return Err((format!("$limit: {} is not an int.", limit), args.span()).into()); + } + + if limit.is_zero() || limit.is_negative() { + return Err(( + format!("$limit: Must be greater than 0, was {}.", limit), + args.span(), + ) + .into()); + } + + let limit = match limit.to_integer().to_u32() { + Some(n) => n, + None => { + return Err(( + format!("max must be in range 0 < max \u{2264} 2^32, was {}", limit), + args.span(), + ) + .into()) + } + }; + + let mut rng = rand::thread_rng(); + Ok(Value::Dimension( + Number::from(rng.gen_range(0, limit) + 1), + Unit::None, + )) +} + +pub(crate) fn register(f: &mut GlobalFunctionMap) { f.insert("percentage", Builtin::new(percentage)); f.insert("round", Builtin::new(round)); f.insert("ceil", Builtin::new(ceil)); diff --git a/src/builtin/meta.rs b/src/builtin/meta.rs index 5984d35..2b6adc3 100644 --- a/src/builtin/meta.rs +++ b/src/builtin/meta.rs @@ -12,233 +12,225 @@ use crate::selector::Selector; use crate::unit::Unit; use crate::value::{SassFunction, Value}; -pub(crate) fn register(f: &mut GlobalFunctionMap) { - fn if_(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(3)?; - if arg!(args, scope, super_selector, 0, "condition").is_true(args.span())? { - Ok(arg!(args, scope, super_selector, 1, "if-true")) - } else { - Ok(arg!(args, scope, super_selector, 2, "if-false")) - } +fn if_(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(3)?; + if arg!(args, scope, super_selector, 0, "condition").is_true(args.span())? { + Ok(arg!(args, scope, super_selector, 1, "if-true")) + } else { + Ok(arg!(args, scope, super_selector, 2, "if-false")) } +} - fn feature_exists( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "feature") { - Value::Ident(s, _) => Ok(match s.as_str() { - // A local variable will shadow a global variable unless - // `!global` is used. - "global-variable-shadowing" => Value::True, - // the @extend rule will affect selectors nested in pseudo-classes - // like :not() - "extend-selector-pseudoclass" => Value::False, - // Full support for unit arithmetic using units defined in the - // [Values and Units Level 3][] spec. - "units-level-3" => Value::True, - // The Sass `@error` directive is supported. - "at-error" => 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" => Value::False, - _ => Value::False, - }), - v => Err(( +fn feature_exists( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "feature") { + Value::Ident(s, _) => Ok(match s.as_str() { + // A local variable will shadow a global variable unless + // `!global` is used. + "global-variable-shadowing" => Value::True, + // the @extend rule will affect selectors nested in pseudo-classes + // like :not() + "extend-selector-pseudoclass" => Value::False, + // Full support for unit arithmetic using units defined in the + // [Values and Units Level 3][] spec. + "units-level-3" => Value::True, + // The Sass `@error` directive is supported. + "at-error" => 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" => Value::False, + _ => Value::False, + }), + v => Err(( + format!( + "$feature: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), + } +} + +fn unit(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + let unit = match arg!(args, scope, super_selector, 0, "number") { + Value::Dimension(_, u) => u.to_string(), + v => { + return Err(( format!( - "$feature: {} is not a string.", + "$number: {} is not a number.", v.to_css_string(args.span())? ), args.span(), ) - .into()), + .into()) } - } + }; + Ok(Value::Ident(unit, QuoteKind::Quoted)) +} - fn unit(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - let unit = match arg!(args, scope, super_selector, 0, "number") { - Value::Dimension(_, u) => u.to_string(), - v => { - return Err(( - format!( - "$number: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - Ok(Value::Ident(unit, QuoteKind::Quoted)) - } +fn type_of(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + let value = arg!(args, scope, super_selector, 0, "value"); + Ok(Value::Ident( + value.kind(args.span())?.to_owned(), + QuoteKind::None, + )) +} - fn type_of(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - let value = arg!(args, scope, super_selector, 0, "value"); - Ok(Value::Ident( - value.kind(args.span())?.to_owned(), - QuoteKind::None, - )) +fn unitless(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "number") { + Value::Dimension(_, Unit::None) => Ok(Value::True), + Value::Dimension(_, _) => Ok(Value::False), + _ => Ok(Value::True), } - fn unitless(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "number") { - Value::Dimension(_, Unit::None) => Ok(Value::True), - Value::Dimension(_, _) => Ok(Value::False), - _ => Ok(Value::True), - } +} + +fn inspect(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + Ok(Value::Ident( + arg!(args, scope, super_selector, 0, "value").inspect(args.span())?, + QuoteKind::None, + )) +} + +fn variable_exists( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "name") { + Value::Ident(s, _) => Ok(Value::bool(scope.var_exists(&s))), + v => Err(( + format!("$name: {} is not a string.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } +} - fn inspect(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - Ok(Value::Ident( - arg!(args, scope, super_selector, 0, "value").inspect(args.span())?, - QuoteKind::None, - )) +fn global_variable_exists( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "name") { + Value::Ident(s, _) => Ok(Value::bool(global_var_exists(&s))), + v => Err(( + format!("$name: {} is not a string.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } +} - fn variable_exists( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "name") { - Value::Ident(s, _) => Ok(Value::bool(scope.var_exists(&s))), - v => Err(( - format!("$name: {} is not a string.", v.to_css_string(args.span())?), - args.span(), - ) - .into()), - } +fn mixin_exists(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + match arg!(args, scope, super_selector, 0, "name") { + Value::Ident(s, _) => Ok(Value::bool(scope.mixin_exists(&s))), + v => Err(( + format!("$name: {} is not a string.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } +} - fn global_variable_exists( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "name") { - Value::Ident(s, _) => Ok(Value::bool(global_var_exists(&s))), - v => Err(( - format!("$name: {} is not a string.", v.to_css_string(args.span())?), - args.span(), - ) - .into()), - } +fn function_exists( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(2)?; + match arg!(args, scope, super_selector, 0, "name") { + Value::Ident(s, _) => Ok(Value::bool( + scope.fn_exists(&s) || GLOBAL_FUNCTIONS.contains_key(s.as_str()), + )), + v => Err(( + format!("$name: {} is not a string.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } +} - fn mixin_exists( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(2)?; - match arg!(args, scope, super_selector, 0, "name") { - Value::Ident(s, _) => Ok(Value::bool(scope.mixin_exists(&s))), - v => Err(( - format!("$name: {} is not a string.", v.to_css_string(args.span())?), - args.span(), - ) - .into()), - } - } - - fn function_exists( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(2)?; - match arg!(args, scope, super_selector, 0, "name") { - Value::Ident(s, _) => Ok(Value::bool( - scope.fn_exists(&s) || GLOBAL_FUNCTIONS.contains_key(s.as_str()), - )), - v => Err(( - format!("$name: {} is not a string.", v.to_css_string(args.span())?), - args.span(), - ) - .into()), - } - } - - fn get_function( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(3)?; - let name = match arg!(args, scope, super_selector, 0, "name") { - Value::Ident(s, _) => s, - v => { - return Err(( - format!("$name: {} is not a string.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - let css = - arg!(args, scope, super_selector, 1, "css" = Value::False).is_true(args.span())?; - let module = match arg!(args, scope, super_selector, 2, "module" = Value::Null) { - Value::Ident(s, ..) => Some(s), - Value::Null => None, - v => { - return Err(( - format!( - "$module: {} is not a string.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - - if module.is_some() && css { +fn get_function(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(3)?; + let name = match arg!(args, scope, super_selector, 0, "name") { + Value::Ident(s, _) => s, + v => { return Err(( - "$css and $module may not both be passed at once.", + format!("$name: {} is not a string.", v.to_css_string(args.span())?), args.span(), ) - .into()); + .into()) } + }; + let css = arg!(args, scope, super_selector, 1, "css" = Value::False).is_true(args.span())?; + let module = match arg!(args, scope, super_selector, 2, "module" = Value::Null) { + Value::Ident(s, ..) => Some(s), + Value::Null => None, + v => { + return Err(( + format!( + "$module: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; - let func = match scope.get_fn(Spanned { - node: name.clone(), - span: args.span(), - }) { - Ok(f) => SassFunction::UserDefined(Box::new(f), name), - Err(..) => match GLOBAL_FUNCTIONS.get(name.as_str()) { - Some(f) => SassFunction::Builtin(f.clone(), name), - None => return Err((format!("Function not found: {}", name), args.span()).into()), - }, - }; - - Ok(Value::Function(func)) + if module.is_some() && css { + return Err(( + "$css and $module may not both be passed at once.", + args.span(), + ) + .into()); } - fn call(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - let func = match arg!(args, scope, super_selector, 0, "function") { - Value::Function(f) => f, - v => { - return Err(( - format!( - "$function: {} is not a function reference.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - func.call(args.decrement(), scope, super_selector) - } + let func = match scope.get_fn(Spanned { + node: name.clone(), + span: args.span(), + }) { + Ok(f) => SassFunction::UserDefined(Box::new(f), name), + Err(..) => match GLOBAL_FUNCTIONS.get(name.as_str()) { + Some(f) => SassFunction::Builtin(f.clone(), name), + None => return Err((format!("Function not found: {}", name), args.span()).into()), + }, + }; + Ok(Value::Function(func)) +} + +fn call(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + let func = match arg!(args, scope, super_selector, 0, "function") { + Value::Function(f) => f, + v => { + return Err(( + format!( + "$function: {} is not a function reference.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + func.call(args.decrement(), scope, super_selector) +} + +pub(crate) fn register(f: &mut GlobalFunctionMap) { f.insert("if", Builtin::new(if_)); f.insert("feature-exists", Builtin::new(feature_exists)); f.insert("unit", Builtin::new(unit)); diff --git a/src/builtin/string.rs b/src/builtin/string.rs index b21da82..0090285 100644 --- a/src/builtin/string.rs +++ b/src/builtin/string.rs @@ -15,365 +15,349 @@ use crate::selector::Selector; use crate::unit::Unit; use crate::value::{Number, Value}; -pub(crate) fn register(f: &mut GlobalFunctionMap) { - fn to_upper_case( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "string") { - Value::Ident(mut i, q) => { - i.make_ascii_uppercase(); - Ok(Value::Ident(i, q)) - } - v => Err(( +fn to_upper_case( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "string") { + Value::Ident(mut i, q) => { + i.make_ascii_uppercase(); + Ok(Value::Ident(i, q)) + } + v => Err(( + format!( + "$string: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), + } +} + +fn to_lower_case( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "string") { + Value::Ident(mut i, q) => { + i.make_ascii_lowercase(); + Ok(Value::Ident(i, q)) + } + v => Err(( + format!( + "$string: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), + } +} + +fn str_length(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "string") { + Value::Ident(i, _) => Ok(Value::Dimension( + Number::from(i.chars().count()), + Unit::None, + )), + v => Err(( + format!( + "$string: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), + } +} + +fn quote(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "string") { + Value::Ident(i, _) => Ok(Value::Ident(i, QuoteKind::Quoted)), + v => Err(( + format!( + "$string: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), + } +} + +fn unquote(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(1)?; + match arg!(args, scope, super_selector, 0, "string") { + i @ Value::Ident(..) => Ok(i.unquote()), + v => Err(( + format!( + "$string: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), + } +} + +fn str_slice(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(3)?; + let (string, quotes) = match arg!(args, scope, super_selector, 0, "string") { + Value::Ident(s, q) => (s, q), + v => { + return Err(( format!( "$string: {} is not a string.", v.to_css_string(args.span())? ), args.span(), ) - .into()), + .into()) } + }; + let str_len = string.chars().count(); + let start = match arg!(args, scope, super_selector, 1, "start-at") { + Value::Dimension(n, Unit::None) if n.is_decimal() => { + return Err((format!("{} is not an int.", n), args.span()).into()) + } + Value::Dimension(n, Unit::None) if n.is_positive() => { + n.to_integer().to_usize().unwrap_or(str_len + 1) + } + Value::Dimension(n, Unit::None) if n.is_zero() => 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.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + v => { + return Err(( + format!( + "$start-at: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + let mut end = match arg!(args, scope, super_selector, 2, "end-at" = Value::Null) { + Value::Dimension(n, Unit::None) if n.is_decimal() => { + return Err((format!("{} is not an int.", n), args.span()).into()) + } + Value::Dimension(n, Unit::None) if n.is_positive() => { + n.to_integer().to_usize().unwrap_or(str_len + 1) + } + Value::Dimension(n, Unit::None) if n.is_zero() => 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_or(str_len + 1), + v @ Value::Dimension(..) => { + return Err(( + format!( + "$end: Expected {} to have no units.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + Value::Null => str_len, + v => { + return Err(( + format!( + "$end-at: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + + if end > str_len { + end = str_len; } - fn to_lower_case( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "string") { - Value::Ident(mut i, q) => { - i.make_ascii_lowercase(); - Ok(Value::Ident(i, q)) - } - v => Err(( + if start > end || start > str_len { + Ok(Value::Ident(String::new(), quotes)) + } else { + Ok(Value::Ident( + string + .chars() + .skip(start - 1) + .take(end - start + 1) + .collect(), + quotes, + )) + } +} + +fn str_index(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(2)?; + let s1 = match arg!(args, scope, super_selector, 0, "string") { + Value::Ident(i, _) => i, + v => { + return Err(( format!( "$string: {} is not a string.", v.to_css_string(args.span())? ), args.span(), ) - .into()), + .into()) } - } + }; - fn str_length( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "string") { - Value::Ident(i, _) => Ok(Value::Dimension( - Number::from(i.chars().count()), - Unit::None, - )), - v => Err(( + let substr = match arg!(args, scope, super_selector, 1, "substring") { + Value::Ident(i, _) => i, + v => { + return Err(( + format!( + "$substring: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + }; + + Ok(match s1.find(&substr) { + Some(v) => Value::Dimension(Number::from(v + 1), Unit::None), + None => Value::Null, + }) +} + +fn str_insert(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + args.max_args(3)?; + let (s1, quotes) = match arg!(args, scope, super_selector, 0, "string") { + Value::Ident(i, q) => (i, q), + v => { + return Err(( format!( "$string: {} is not a string.", v.to_css_string(args.span())? ), args.span(), ) - .into()), + .into()) } - } + }; - fn quote(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "string") { - Value::Ident(i, _) => Ok(Value::Ident(i, QuoteKind::Quoted)), - v => Err(( + let substr = match arg!(args, scope, super_selector, 1, "insert") { + Value::Ident(i, _) => i, + v => { + return Err(( format!( - "$string: {} is not a string.", + "$insert: {} is not a string.", v.to_css_string(args.span())? ), args.span(), ) - .into()), + .into()) } - } + }; - fn unquote(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { - args.max_args(1)?; - match arg!(args, scope, super_selector, 0, "string") { - i @ Value::Ident(..) => Ok(i.unquote()), - v => Err(( + let index = match arg!(args, scope, super_selector, 2, "index") { + Value::Dimension(n, Unit::None) if n.is_decimal() => { + return Err((format!("$index: {} is not an int.", n), args.span()).into()) + } + Value::Dimension(n, Unit::None) => n, + v @ Value::Dimension(..) => { + return Err(( format!( - "$string: {} is not a string.", + "$index: Expected {} to have no units.", v.to_css_string(args.span())? ), args.span(), ) - .into()), + .into()) } + v => { + return Err(( + format!("$index: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + }; + + if s1.is_empty() { + return Ok(Value::Ident(substr, quotes)); } - fn str_slice( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(3)?; - let (string, quotes) = match arg!(args, scope, super_selector, 0, "string") { - Value::Ident(s, q) => (s, q), - v => { - return Err(( - format!( - "$string: {} is not a string.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - let str_len = string.chars().count(); - let start = match arg!(args, scope, super_selector, 1, "start-at") { - Value::Dimension(n, Unit::None) if n.is_decimal() => { - return Err((format!("{} is not an int.", n), args.span()).into()) - } - Value::Dimension(n, Unit::None) if n.is_positive() => { - n.to_integer().to_usize().unwrap_or(str_len + 1) - } - Value::Dimension(n, Unit::None) if n.is_zero() => 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()) + let len = s1.chars().count(); + + // Insert substring at char position, rather than byte position + let insert = |idx, s1: String, s2| { + s1.chars() + .enumerate() + .map(|(i, c)| { + if i + 1 == idx { + c.to_string() + s2 + } else if idx == 0 && i == 0 { + s2.to_string() + &c.to_string() + } else { + c.to_string() + } + }) + .collect::() + }; + + let string = if index.is_positive() { + insert( + index + .to_integer() .to_usize() - .unwrap(), - v @ Value::Dimension(..) => { - return Err(( - format!( - "$start: Expected {} to have no units.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v => { - return Err(( - format!( - "$start-at: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - let mut end = match arg!(args, scope, super_selector, 2, "end-at" = Value::Null) { - Value::Dimension(n, Unit::None) if n.is_decimal() => { - return Err((format!("{} is not an int.", n), args.span()).into()) - } - Value::Dimension(n, Unit::None) if n.is_positive() => { - n.to_integer().to_usize().unwrap_or(str_len + 1) - } - Value::Dimension(n, Unit::None) if n.is_zero() => 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_or(str_len + 1), - v @ Value::Dimension(..) => { - return Err(( - format!( - "$end: Expected {} to have no units.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - Value::Null => str_len, - v => { - return Err(( - format!( - "$end-at: {} is not a number.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - - if end > str_len { - end = str_len; - } - - if start > end || start > str_len { - Ok(Value::Ident(String::new(), quotes)) - } else { - Ok(Value::Ident( - string - .chars() - .skip(start - 1) - .take(end - start + 1) - .collect(), - quotes, - )) - } - } - - fn str_index( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(2)?; - let s1 = match arg!(args, scope, super_selector, 0, "string") { - Value::Ident(i, _) => i, - v => { - return Err(( - format!( - "$string: {} is not a string.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - - let substr = match arg!(args, scope, super_selector, 1, "substring") { - Value::Ident(i, _) => i, - v => { - return Err(( - format!( - "$substring: {} is not a string.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - - Ok(match s1.find(&substr) { - Some(v) => Value::Dimension(Number::from(v + 1), Unit::None), - None => Value::Null, - }) - } - - fn str_insert( - mut args: CallArgs, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - args.max_args(3)?; - let (s1, quotes) = match arg!(args, scope, super_selector, 0, "string") { - Value::Ident(i, q) => (i, q), - v => { - return Err(( - format!( - "$string: {} is not a string.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - - let substr = match arg!(args, scope, super_selector, 1, "insert") { - Value::Ident(i, _) => i, - v => { - return Err(( - format!( - "$insert: {} is not a string.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - }; - - let index = match arg!(args, scope, super_selector, 2, "index") { - Value::Dimension(n, Unit::None) if n.is_decimal() => { - return Err((format!("$index: {} is not an int.", n), args.span()).into()) - } - Value::Dimension(n, Unit::None) => n, - v @ Value::Dimension(..) => { - return Err(( - format!( - "$index: Expected {} to have no units.", - v.to_css_string(args.span())? - ), - args.span(), - ) - .into()) - } - v => { - return Err(( - format!("$index: {} is not a number.", v.to_css_string(args.span())?), - args.span(), - ) - .into()) - } - }; - - if s1.is_empty() { - return Ok(Value::Ident(substr, quotes)); - } - - let len = s1.chars().count(); - - // Insert substring at char position, rather than byte position - let insert = |idx, s1: String, s2| { - s1.chars() - .enumerate() - .map(|(i, c)| { - if i + 1 == idx { - c.to_string() + s2 - } else if idx == 0 && i == 0 { - s2.to_string() + &c.to_string() - } else { - c.to_string() - } - }) - .collect::() - }; - - let string = if index.is_positive() { - insert( - index - .to_integer() - .to_usize() - .unwrap_or(len + 1) - .min(len + 1) - - 1, - s1, - &substr, - ) - } else if index.is_zero() { + .unwrap_or(len + 1) + .min(len + 1) + - 1, + s1, + &substr, + ) + } else if index.is_zero() { + insert(0, s1, &substr) + } else { + let idx = index.abs().to_integer().to_usize().unwrap_or(len + 1); + if idx > len { insert(0, s1, &substr) } else { - let idx = index.abs().to_integer().to_usize().unwrap_or(len + 1); - if idx > len { - insert(0, s1, &substr) - } else { - insert(len - idx + 1, s1, &substr) - } - }; + insert(len - idx + 1, s1, &substr) + } + }; - Ok(Value::Ident(string, quotes)) - } + Ok(Value::Ident(string, quotes)) +} - #[cfg(feature = "random")] - fn unique_id(args: CallArgs, _: &Scope, _: &Selector) -> SassResult { - args.max_args(0)?; - let mut rng = thread_rng(); - let string = std::iter::repeat(()) - .map(|()| rng.sample(Alphanumeric)) - .take(7) - .collect(); - Ok(Value::Ident(string, QuoteKind::None)) - } +#[cfg(feature = "random")] +fn unique_id(args: CallArgs, _: &Scope, _: &Selector) -> SassResult { + args.max_args(0)?; + let mut rng = thread_rng(); + let string = std::iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .take(7) + .collect(); + Ok(Value::Ident(string, QuoteKind::None)) +} +pub(crate) fn register(f: &mut GlobalFunctionMap) { f.insert("to-upper-case", Builtin::new(to_upper_case)); f.insert("to-lower-case", Builtin::new(to_lower_case)); f.insert("str-length", Builtin::new(str_length));