diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f23697..6d38b97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ # 0.12.2 (unreleased) - implement an import cache, significantly improving the performance of certain pathological cases +- slash lists can be compared using `==` +- resolve rounding errors for extremely large numbers +- potentially breaking bug fixes in certain color functions + - `color.hwb(..)` no longer allows whiteness or blackness values outside the bounds 0% to 100% + - `scale-color(..)` no longer allows the `$hue` argument. previously it was ignored + - `scale-color(..)`, `change-color(..)`, and `adjust-color(..)` no longer allow invalid combinations of arguments or unknown named arguments + - many functions that accept hues now convert other angle units (`rad`, `grad`, `turn`) to `deg`. previously the unit was ignored # 0.12.1 diff --git a/README.md b/README.md index 3db3595..e75d084 100644 --- a/README.md +++ b/README.md @@ -84,9 +84,9 @@ The spec runner does not work on Windows. Using a modified version of the spec runner that ignores warnings and error spans (but does include error messages), `grass` achieves the following results: ``` -2022-01-16 -PASSING: 6153 -FAILING: 752 +2022-01-17 +PASSING: 6271 +FAILING: 621 TOTAL: 6905 ``` diff --git a/crates/compiler/src/ast/args.rs b/crates/compiler/src/ast/args.rs index 1233eea..15b1cc3 100644 --- a/crates/compiler/src/ast/args.rs +++ b/crates/compiler/src/ast/args.rs @@ -267,13 +267,6 @@ impl ArgumentResult { } } - pub fn default_named_arg(&mut self, name: &'static str, default: Value) -> Value { - match self.get_named(name) { - Some(val) => val.node, - None => default, - } - } - pub fn get_variadic(self) -> SassResult>> { if let Some((name, _)) = self.named.iter().next() { return Err((format!("No argument named ${}.", name), self.span).into()); diff --git a/crates/compiler/src/builtin/functions/color/hsl.rs b/crates/compiler/src/builtin/functions/color/hsl.rs index 24f85f9..d383325 100644 --- a/crates/compiler/src/builtin/functions/color/hsl.rs +++ b/crates/compiler/src/builtin/functions/color/hsl.rs @@ -3,6 +3,7 @@ use std::collections::{BTreeMap, BTreeSet}; use crate::{builtin::builtin_imports::*, serializer::serialize_number, value::SassNumber}; use super::{ + angle_value, rgb::{function_string, parse_channels, percentage_or_unitless}, ParsedChannels, }; @@ -43,7 +44,7 @@ fn hsl_3_args( )); } - let hue = hue.assert_number_with_name("hue", span)?; + let hue = angle_value(hue, "hue", span)?; let saturation = saturation.assert_number_with_name("saturation", span)?; let lightness = lightness.assert_number_with_name("lightness", span)?; let alpha = percentage_or_unitless( @@ -55,7 +56,7 @@ fn hsl_3_args( )?; Ok(Value::Color(Arc::new(Color::from_hsla_fn( - Number(hue.num.rem_euclid(360.0)), + Number(hue.rem_euclid(360.0)), saturation.num / Number(100.0), lightness.num / Number(100.0), Number(alpha), @@ -162,10 +163,9 @@ pub(crate) fn adjust_hue(mut args: ArgumentResult, visitor: &mut Visitor) -> Sas let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; - let degrees = args - .get_err(1, "degrees")? - .assert_number_with_name("degrees", args.span())? - .num; + let degrees = angle_value(args.get_err(1, "degrees")?, "degrees", args.span())?; + + dbg!(degrees); Ok(Value::Color(Arc::new(color.adjust_hue(degrees)))) } @@ -176,12 +176,15 @@ fn lighten(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult .get_err(0, "color")? .assert_color_with_name("color", args.span())?; - let amount = args + let mut amount = args .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; - let amount = bound!(args, "amount", amount.num, amount.unit, 0, 100) / Number(100.0); - Ok(Value::Color(Arc::new(color.lighten(amount)))) + amount.assert_bounds("amount", 0.0, 100.0, args.span())?; + + amount.num /= Number(100.0); + + Ok(Value::Color(Arc::new(color.lighten(amount.num)))) } fn darken(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { @@ -225,27 +228,10 @@ fn saturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult c, - Value::Dimension(SassNumber { - num: n, - unit: u, - as_slash: _, - }) => { - // todo: this branch should be superfluous/incorrect - return Ok(Value::String( - format!("saturate({}{})", n.inspect(), u), - QuoteKind::None, - )); - } - v => { - return Err(( - format!("$color: {} is not a color.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }; + let color = args + .get_err(0, "color")? + .assert_color_with_name("color", args.span())?; + Ok(Value::Color(Arc::new(color.saturate(amount.num)))) } diff --git a/crates/compiler/src/builtin/functions/color/hwb.rs b/crates/compiler/src/builtin/functions/color/hwb.rs index f99b589..d75ecaa 100644 --- a/crates/compiler/src/builtin/functions/color/hwb.rs +++ b/crates/compiler/src/builtin/functions/color/hwb.rs @@ -1,6 +1,10 @@ use crate::builtin::builtin_imports::*; -use super::{rgb::parse_channels, ParsedChannels}; +use super::{ + angle_value, + rgb::{parse_channels, percentage_or_unitless}, + ParsedChannels, +}; pub(crate) fn blackness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; @@ -33,56 +37,31 @@ pub(crate) fn whiteness(mut args: ArgumentResult, visitor: &mut Visitor) -> Sass fn hwb_inner(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let span = args.span(); - let hue = match args.get(0, "hue") { - Some(v) => match v.node { - Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(), - Value::Dimension(SassNumber { num: n, .. }) => n, - v => { - return Err(( - format!("$hue: {} is not a number.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }, - None => return Err(("Missing element $hue.", args.span()).into()), - }; + let hue = angle_value(args.get_err(0, "hue")?, "hue", args.span())?; let whiteness = args .get_err(1, "whiteness")? .assert_number_with_name("whiteness", span)?; whiteness.assert_unit(&Unit::Percent, "whiteness", span)?; + whiteness.assert_bounds("whiteness", 0.0, 100.0, args.span())?; let blackness = args .get_err(2, "blackness")? .assert_number_with_name("blackness", span)?; blackness.assert_unit(&Unit::Percent, "blackness", span)?; + blackness.assert_bounds("blackness", 0.0, 100.0, args.span())?; - let alpha = match args.get(3, "alpha") { - Some(v) => match v.node { - Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(), - Value::Dimension(SassNumber { - num: n, - unit: Unit::Percent, - .. - }) => n / Number(100.0), - Value::Dimension(SassNumber { num: n, .. }) => n, - v => { - return Err(( - format!("$alpha: {} is not a number.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }, - None => Number::one(), - }; + let alpha = args + .default_arg(3, "alpha", Value::Dimension(SassNumber::new_unitless(1.0))) + .assert_number_with_name("alpha", args.span())?; + + let alpha = percentage_or_unitless(&alpha, 1.0, "alpha", args.span(), visitor)?; Ok(Value::Color(Arc::new(Color::from_hwb( hue, whiteness.num, blackness.num, - alpha, + Number(alpha), )))) } @@ -114,7 +93,10 @@ pub(crate) fn hwb(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult hwb_inner(args, visitor) } } - } else { + } else if args.len() == 3 || args.len() == 4 { hwb_inner(args, visitor) + } else { + args.max_args(1)?; + unreachable!() } } diff --git a/crates/compiler/src/builtin/functions/color/mod.rs b/crates/compiler/src/builtin/functions/color/mod.rs index 4158cdb..3c623d1 100644 --- a/crates/compiler/src/builtin/functions/color/mod.rs +++ b/crates/compiler/src/builtin/functions/color/mod.rs @@ -1,4 +1,10 @@ -use crate::value::Value; +use codemap::Span; + +use crate::{ + builtin::builtin_imports::Unit, + error::SassResult, + value::{conversion_factor, Number, Value}, +}; use super::GlobalFunctionMap; @@ -14,6 +20,18 @@ pub(crate) enum ParsedChannels { List(Vec), } +pub(crate) fn angle_value(num: Value, name: &str, span: Span) -> SassResult { + let angle = num.assert_number_with_name(name, span)?; + + if angle.has_compatible_units(&Unit::Deg) { + let factor = conversion_factor(&angle.unit, &Unit::Deg).unwrap(); + + return Ok(angle.num * Number(factor)); + } + + Ok(angle.num) +} + pub(crate) fn declare(f: &mut GlobalFunctionMap) { hsl::declare(f); opacity::declare(f); diff --git a/crates/compiler/src/builtin/functions/color/opacity.rs b/crates/compiler/src/builtin/functions/color/opacity.rs index 504bd40..4e7c79c 100644 --- a/crates/compiler/src/builtin/functions/color/opacity.rs +++ b/crates/compiler/src/builtin/functions/color/opacity.rs @@ -94,7 +94,7 @@ fn opacify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; - amount.assert_bounds("amount", 0.0, 1.0, args.span())?; + amount.assert_bounds_with_unit("amount", 0.0, 1.0, &Unit::None, args.span())?; Ok(Value::Color(Arc::new(color.fade_in(amount.num)))) } @@ -109,7 +109,7 @@ fn transparentize(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; - amount.assert_bounds("amount", 0.0, 1.0, args.span())?; + amount.assert_bounds_with_unit("amount", 0.0, 1.0, &Unit::None, args.span())?; Ok(Value::Color(Arc::new(color.fade_out(amount.num)))) } diff --git a/crates/compiler/src/builtin/functions/color/other.rs b/crates/compiler/src/builtin/functions/color/other.rs index 3f8351d..6838455 100644 --- a/crates/compiler/src/builtin/functions/color/other.rs +++ b/crates/compiler/src/builtin/functions/color/other.rs @@ -1,283 +1,244 @@ -use crate::builtin::builtin_imports::*; +use crate::{ + builtin::{builtin_imports::*, color::angle_value}, + utils::to_sentence, + value::fuzzy_round, +}; -macro_rules! opt_rgba { - ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { - let $name = match $args.default_named_arg($arg, Value::Null) { - Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(), - Value::Dimension(SassNumber { - num: n, unit: u, .. - }) => Some(bound!($args, $arg, n, u, $low, $high)), - Value::Null => None, - v => { - return Err(( - format!("${}: {} is not a number.", $arg, v.inspect($args.span())?), - $args.span(), - ) - .into()) - } - }; - }; +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum UpdateComponents { + Change, + Adjust, + Scale, } -macro_rules! opt_hsl { - ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { - let $name = match $args.default_named_arg($arg, Value::Null) { - Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(), - Value::Dimension(SassNumber { - num: n, unit: u, .. - }) => Some(bound!($args, $arg, n, u, $low, $high) / Number(100.0)), - Value::Null => None, - v => { - return Err(( - format!("${}: {} is not a number.", $arg, v.inspect($args.span())?), - $args.span(), - ) - .into()) - } - }; - }; -} - -pub(crate) fn change_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { - if args.get_positional(1).is_some() { - return Err(( - "Only one positional argument is allowed. All other arguments must be passed by name.", - args.span(), - ) - .into()); - } - - let color = args - .get_err(0, "color")? - .assert_color_with_name("color", args.span())?; - - opt_rgba!(args, alpha, "alpha", 0, 1); - opt_rgba!(args, red, "red", 0, 255); - opt_rgba!(args, green, "green", 0, 255); - opt_rgba!(args, blue, "blue", 0, 255); - - if red.is_some() || green.is_some() || blue.is_some() { - return Ok(Value::Color(Arc::new(Color::from_rgba( - red.unwrap_or_else(|| color.red()), - green.unwrap_or_else(|| color.green()), - blue.unwrap_or_else(|| color.blue()), - alpha.unwrap_or_else(|| color.alpha()), - )))); - } - - let hue = match args.default_named_arg("hue", Value::Null) { - Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(), - Value::Dimension(SassNumber { num: n, .. }) => Some(n), - Value::Null => None, - v => { - return Err(( - format!("$hue: {} is not a number.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }; - - opt_hsl!(args, saturation, "saturation", 0, 100); - opt_hsl!(args, lightness, "lightness", 0, 100); - - if hue.is_some() || saturation.is_some() || lightness.is_some() { - // Color::as_hsla() returns more exact values than Color::hue(), etc. - let (this_hue, this_saturation, this_lightness, this_alpha) = color.as_hsla(); - return Ok(Value::Color(Arc::new(Color::from_hsla( - hue.unwrap_or(this_hue), - saturation.unwrap_or(this_saturation), - lightness.unwrap_or(this_lightness), - alpha.unwrap_or(this_alpha), - )))); - } - - Ok(Value::Color(if let Some(a) = alpha { - Arc::new(color.with_alpha(a)) - } else { - color - })) -} - -pub(crate) fn adjust_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { - let color = args - .get_err(0, "color")? - .assert_color_with_name("color", args.span())?; - - opt_rgba!(args, alpha, "alpha", -1, 1); - opt_rgba!(args, red, "red", -255, 255); - opt_rgba!(args, green, "green", -255, 255); - opt_rgba!(args, blue, "blue", -255, 255); - - if red.is_some() || green.is_some() || blue.is_some() { - return Ok(Value::Color(Arc::new(Color::from_rgba( - color.red() + red.unwrap_or_else(Number::zero), - color.green() + green.unwrap_or_else(Number::zero), - color.blue() + blue.unwrap_or_else(Number::zero), - color.alpha() + alpha.unwrap_or_else(Number::zero), - )))); - } - - let hue = match args.default_named_arg("hue", Value::Null) { - Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(), - Value::Dimension(SassNumber { num: n, .. }) => Some(n), - Value::Null => None, - v => { - return Err(( - format!("$hue: {} is not a number.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }; - - opt_hsl!(args, saturation, "saturation", -100, 100); - opt_hsl!(args, lightness, "lightness", -100, 100); - - if hue.is_some() || saturation.is_some() || lightness.is_some() { - // Color::as_hsla() returns more exact values than Color::hue(), etc. - let (this_hue, this_saturation, this_lightness, this_alpha) = color.as_hsla(); - return Ok(Value::Color(Arc::new(Color::from_hsla( - this_hue + hue.unwrap_or_else(Number::zero), - this_saturation + saturation.unwrap_or_else(Number::zero), - this_lightness + lightness.unwrap_or_else(Number::zero), - this_alpha + alpha.unwrap_or_else(Number::zero), - )))); - } - - Ok(Value::Color(if let Some(a) = alpha { - let temp_alpha = color.alpha(); - Arc::new(color.with_alpha(temp_alpha + a)) - } else { - color - })) -} - -#[allow(clippy::cognitive_complexity)] -// todo: refactor into rgb and hsl? -pub(crate) fn scale_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { - pub(crate) fn scale(val: Number, by: Number, max: Number) -> Number { - if by.is_zero() { - return val; - } - val + (if by.is_positive() { max - val } else { val }) * by - } - - fn check_num(num: Spanned, name: &str, min: f64, max: f64) -> SassResult { - let span = num.span; - let mut num = num.node.assert_number_with_name(name, span)?; - - num.assert_unit(&Unit::Percent, name, span)?; - num.assert_bounds(name, min, max, span)?; - - num.num /= Number(100.0); - - Ok(num.num) - } - - fn get_arg( - args: &mut ArgumentResult, - name: &str, - min: f64, - max: f64, - ) -> SassResult> { - Ok(match args.get(usize::MAX, name) { - Some(v) => Some(check_num(v, name, min, max)?), - None => None, - }) - } - +fn update_components( + mut args: ArgumentResult, + visitor: &mut Visitor, + update: UpdateComponents, +) -> SassResult { let span = args.span(); let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; - let red = get_arg(&mut args, "red", -100.0, 100.0)?; - let green = get_arg(&mut args, "green", -100.0, 100.0)?; - let blue = get_arg(&mut args, "blue", -100.0, 100.0)?; - let alpha = get_arg(&mut args, "alpha", -100.0, 100.0)?; - - if red.is_some() || green.is_some() || blue.is_some() { - return Ok(Value::Color(Arc::new(Color::from_rgba( - scale(color.red(), red.unwrap_or_else(Number::zero), Number(255.0)), - scale( - color.green(), - green.unwrap_or_else(Number::zero), - Number(255.0), - ), - scale( - color.blue(), - blue.unwrap_or_else(Number::zero), - Number(255.0), - ), - scale( - color.alpha(), - alpha.unwrap_or_else(Number::zero), - Number::one(), - ), - )))); + // todo: what if color is also passed by name + if args.positional.len() > 1 { + return Err(( + "Only one positional argument is allowed. All other arguments must be passed by name.", + span, + ) + .into()); } - let saturation = get_arg(&mut args, "saturation", -100.0, 100.0)?; - let lightness = get_arg(&mut args, "lightness", -100.0, 100.0)?; + let check_num = |num: Spanned, + name: &str, + mut max: f64, + assert_percent: bool, + check_percent: bool| + -> SassResult { + let span = num.span; + let mut num = num.node.assert_number_with_name(name, span)?; - if saturation.is_some() || lightness.is_some() { - // Color::as_hsla() returns more exact values than Color::hue(), etc. + if update == UpdateComponents::Scale { + max = 100.0; + } + + if assert_percent || update == UpdateComponents::Scale { + num.assert_unit(&Unit::Percent, name, span)?; + num.assert_bounds( + name, + if update == UpdateComponents::Change { + 0.0 + } else { + -max + }, + max, + span, + )?; + } else { + num.assert_bounds_with_unit( + name, + if update == UpdateComponents::Change { + 0.0 + } else { + -max + }, + max, + if check_percent { + &Unit::Percent + } else { + &Unit::None + }, + span, + )?; + } + + // todo: hack to check if rgb channel + if max == 100.0 { + num.num /= Number(100.0); + } + + Ok(num.num) + }; + + let get_arg = |args: &mut ArgumentResult, + name: &str, + max: f64, + assert_percent: bool, + check_percent: bool| + -> SassResult> { + Ok(match args.get(usize::MAX, name) { + Some(v) => Some(check_num(v, name, max, assert_percent, check_percent)?), + None => None, + }) + }; + + let red = get_arg(&mut args, "red", 255.0, false, false)?; + let green = get_arg(&mut args, "green", 255.0, false, false)?; + let blue = get_arg(&mut args, "blue", 255.0, false, false)?; + let alpha = get_arg(&mut args, "alpha", 1.0, false, false)?; + + let hue = if update == UpdateComponents::Scale { + None + } else { + args.get(usize::MAX, "hue") + .map(|v| angle_value(v.node, "hue", v.span)) + .transpose()? + }; + + let saturation = get_arg(&mut args, "saturation", 100.0, false, true)?; + let lightness = get_arg(&mut args, "lightness", 100.0, false, true)?; + let whiteness = get_arg(&mut args, "whiteness", 100.0, true, true)?; + let blackness = get_arg(&mut args, "blackness", 100.0, true, true)?; + + if !args.named.is_empty() { + let argument_word = if args.named.len() == 1 { + "argument" + } else { + "arguments" + }; + + let argument_names = to_sentence( + args.named + .keys() + .map(|key| format!("${key}", key = key)) + .collect(), + "or", + ); + + return Err(( + format!( + "No {argument_word} named {argument_names}.", + argument_word = argument_word, + argument_names = argument_names + ), + span, + ) + .into()); + } + + let has_rgb = red.is_some() || green.is_some() || blue.is_some(); + let has_sl = saturation.is_some() || lightness.is_some(); + let has_wb = whiteness.is_some() || blackness.is_some(); + + if has_rgb && (has_sl || has_wb || hue.is_some()) { + let param_type = if has_wb { "HWB" } else { "HSL" }; + return Err(( + format!( + "RGB parameters may not be passed along with {} parameters.", + param_type + ), + span, + ) + .into()); + } + + if has_sl && has_wb { + return Err(( + "HSL parameters may not be passed along with HWB parameters.", + span, + ) + .into()); + } + + fn update_value( + current: Number, + param: Option, + max: f64, + update: UpdateComponents, + ) -> Number { + let param = match param { + Some(p) => p, + None => return current, + }; + + match update { + UpdateComponents::Change => param, + UpdateComponents::Adjust => (param + current).clamp(0.0, max), + UpdateComponents::Scale => { + current + + if param > Number(0.0) { + Number(max) - current + } else { + current + } * param + } + } + } + + fn update_rgb(current: Number, param: Option, update: UpdateComponents) -> Number { + Number(fuzzy_round(update_value(current, param, 255.0, update).0)) + } + + let color = if has_rgb { + Arc::new(Color::from_rgba( + update_rgb(color.red(), red, update), + update_rgb(color.green(), green, update), + update_rgb(color.blue(), blue, update), + update_value(color.alpha(), alpha, 1.0, update), + )) + } else if has_wb { + Arc::new(Color::from_hwb( + if update == UpdateComponents::Change { + hue.unwrap_or_else(|| color.hue()) + } else { + color.hue() + hue.unwrap_or_else(Number::zero) + }, + update_value(color.whiteness(), whiteness, 1.0, update) * Number(100.0), + update_value(color.blackness(), blackness, 1.0, update) * Number(100.0), + update_value(color.alpha(), alpha, 1.0, update), + )) + } else if hue.is_some() || has_sl { let (this_hue, this_saturation, this_lightness, this_alpha) = color.as_hsla(); - return Ok(Value::Color(Arc::new(Color::from_hsla( - scale(this_hue, Number::zero(), Number(360.0)), - scale( - this_saturation, - saturation.unwrap_or_else(Number::zero), - Number::one(), - ), - scale( - this_lightness, - lightness.unwrap_or_else(Number::zero), - Number::one(), - ), - scale( - this_alpha, - alpha.unwrap_or_else(Number::zero), - Number::one(), - ), - )))); - } - - let whiteness = get_arg(&mut args, "whiteness", -100.0, 100.0)?; - let blackness = get_arg(&mut args, "blackness", -100.0, 100.0)?; - - if whiteness.is_some() || blackness.is_some() { - let this_hue = color.hue(); - let this_whiteness = color.whiteness() * Number(100.0); - let this_blackness = color.blackness() * Number(100.0); - - return Ok(Value::Color(Arc::new(Color::from_hwb( - scale(this_hue, Number::zero(), Number(360.0)), - scale( - this_whiteness, - whiteness.unwrap_or_else(Number::zero), - Number(100.0), - ), - scale( - this_blackness, - blackness.unwrap_or_else(Number::zero), - Number(100.0), - ), - scale( - color.alpha(), - alpha.unwrap_or_else(Number::zero), - Number::one(), - ), - )))); - } - - Ok(Value::Color(if let Some(a) = alpha { - let temp_alpha = color.alpha(); - Arc::new(color.with_alpha(scale(temp_alpha, a, Number::one()))) + Arc::new(Color::from_hsla( + if update == UpdateComponents::Change { + hue.unwrap_or(this_hue) + } else { + this_hue + hue.unwrap_or_else(Number::zero) + }, + update_value(this_saturation, saturation, 1.0, update), + update_value(this_lightness, lightness, 1.0, update), + update_value(this_alpha, alpha, 1.0, update), + )) + } else if alpha.is_some() { + Arc::new(color.with_alpha(update_value(color.alpha(), alpha, 1.0, update))) } else { color - })) + }; + + Ok(Value::Color(color)) +} + +pub(crate) fn scale_color(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { + update_components(args, visitor, UpdateComponents::Scale) +} + +pub(crate) fn change_color(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { + update_components(args, visitor, UpdateComponents::Change) +} + +pub(crate) fn adjust_color(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { + update_components(args, visitor, UpdateComponents::Adjust) } pub(crate) fn ie_hex_str(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { diff --git a/crates/compiler/src/builtin/functions/color/rgb.rs b/crates/compiler/src/builtin/functions/color/rgb.rs index a971734..8e1912f 100644 --- a/crates/compiler/src/builtin/functions/color/rgb.rs +++ b/crates/compiler/src/builtin/functions/color/rgb.rs @@ -217,11 +217,7 @@ pub(crate) fn parse_channels( channels = list[0].clone(); let inner_alpha_from_slash_list = list[1].clone(); - if !alpha_from_slash_list - .as_ref() - .map(Value::is_special_function) - .unwrap_or(false) - { + if !inner_alpha_from_slash_list.is_special_function() { inner_alpha_from_slash_list .clone() .assert_number_with_name("alpha", span)?; @@ -395,11 +391,11 @@ pub(crate) fn mix(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult Value::Dimension(SassNumber::new_unitless(50.0)), ) { Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(), - Value::Dimension(SassNumber { - num: n, - unit: u, - as_slash: _, - }) => bound!(args, "weight", n, u, 0, 100) / Number(100.0), + Value::Dimension(mut num) => { + num.assert_bounds("weight", 0.0, 100.0, args.span())?; + num.num /= Number(100.0); + num.num + } v => { return Err(( format!( diff --git a/crates/compiler/src/builtin/functions/macros.rs b/crates/compiler/src/builtin/functions/macros.rs deleted file mode 100644 index 3f02cfa..0000000 --- a/crates/compiler/src/builtin/functions/macros.rs +++ /dev/null @@ -1,22 +0,0 @@ -macro_rules! bound { - ($args:ident, $name:literal, $arg:expr, $unit:expr, $low:literal, $high:literal) => { - if !($arg <= Number::from($high) && $arg >= Number::from($low)) { - return Err(( - format!( - "${}: Expected {}{} to be within {}{} and {}{}.", - $name, - $arg.inspect(), - $unit, - $low, - $unit, - $high, - $unit, - ), - $args.span(), - ) - .into()); - } else { - $arg - } - }; -} diff --git a/crates/compiler/src/builtin/functions/mod.rs b/crates/compiler/src/builtin/functions/mod.rs index 5ee7048..29d8f2f 100644 --- a/crates/compiler/src/builtin/functions/mod.rs +++ b/crates/compiler/src/builtin/functions/mod.rs @@ -10,9 +10,6 @@ use once_cell::sync::Lazy; use crate::{ast::ArgumentResult, error::SassResult, evaluate::Visitor, value::Value}; -#[macro_use] -mod macros; - pub mod color; pub mod list; pub mod map; diff --git a/crates/compiler/src/color/mod.rs b/crates/compiler/src/color/mod.rs index 6d14ce7..e30e835 100644 --- a/crates/compiler/src/color/mod.rs +++ b/crates/compiler/src/color/mod.rs @@ -373,6 +373,7 @@ impl Color { /// Create RGBA representation from HSLA values pub fn from_hsla(hue: Number, saturation: Number, lightness: Number, alpha: Number) -> Self { + let hue = hue % Number(360.0); let hsla = Hsl::new(hue, saturation.clamp(0.0, 1.0), lightness.clamp(0.0, 1.0)); let scaled_hue = hue.0 / 360.0; diff --git a/crates/compiler/src/common.rs b/crates/compiler/src/common.rs index c137eb1..013b5e9 100644 --- a/crates/compiler/src/common.rs +++ b/crates/compiler/src/common.rs @@ -86,7 +86,7 @@ pub(crate) enum Brackets { Bracketed, } -#[derive(Debug, Clone, Copy, Eq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub(crate) enum ListSeparator { Space, Comma, @@ -94,18 +94,6 @@ pub(crate) enum ListSeparator { Undecided, } -impl PartialEq for ListSeparator { - #[allow(clippy::match_like_matches_macro)] - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Space, Self::Space) => true, - (Self::Undecided, Self::Undecided) => true, - (Self::Comma, Self::Comma) => true, - _ => false, - } - } -} - impl ListSeparator { pub fn as_str(self) -> &'static str { match self { diff --git a/crates/compiler/src/serializer.rs b/crates/compiler/src/serializer.rs index 72f4855..22f4473 100644 --- a/crates/compiler/src/serializer.rs +++ b/crates/compiler/src/serializer.rs @@ -580,11 +580,17 @@ impl<'a> Serializer<'a> { .trim_end_matches('.'), ); } else { - buffer.push_str( - format!("{:.10}", num) - .trim_end_matches('0') - .trim_end_matches('.'), - ); + let p = 10.0_f64.powi(10); + + let n = (num * p).round() / p; + + let formatted = n.to_string(); + + if formatted.ends_with(".0") { + buffer.push_str(formatted.trim_end_matches('0').trim_end_matches('.')); + } else { + buffer.push_str(&formatted); + } } if buffer.is_empty() || buffer == "-" || buffer == "-0" { diff --git a/crates/compiler/src/value/sass_number.rs b/crates/compiler/src/value/sass_number.rs index 3ba7473..cd06f8f 100644 --- a/crates/compiler/src/value/sass_number.rs +++ b/crates/compiler/src/value/sass_number.rs @@ -173,6 +173,17 @@ impl SassNumber { } pub fn assert_bounds(&self, name: &str, min: f64, max: f64, span: Span) -> SassResult<()> { + self.assert_bounds_with_unit(name, min, max, &self.unit, span) + } + + pub fn assert_bounds_with_unit( + &self, + name: &str, + min: f64, + max: f64, + unit: &Unit, + span: Span, + ) -> SassResult<()> { if !(self.num <= Number(max) && self.num >= Number(min)) { return Err(( format!( @@ -180,9 +191,9 @@ impl SassNumber { name, inspect_number(self, &Options::default(), span)?, inspect_float(min, &Options::default(), span), - self.unit, + unit, inspect_float(max, &Options::default(), span), - self.unit, + unit, ), span, ) diff --git a/crates/lib/tests/color.rs b/crates/lib/tests/color.rs index ced9d2e..fe0c722 100644 --- a/crates/lib/tests/color.rs +++ b/crates/lib/tests/color.rs @@ -350,11 +350,35 @@ test!( "a {\n color: scale-color(hsl(120, 70%, 80%), $lightness: 50%);\n}\n", "a {\n color: #d4f7d4;\n}\n" ); +test!( + scale_color_neg_lightness_and_pos_saturation, + "a {\n color: scale-color(turquoise, $saturation: 24%, $lightness: -48%);\n}\n", + "a {\n color: #10867a;\n}\n" +); +error!( + scale_color_named_arg_hue, + "a {\n color: scale-color(red, $hue: 10%);\n}\n", "Error: No argument named $hue." +); test!( scale_color_negative, "a {\n color: scale-color(rgb(200, 150%, 170%), $green: -40%, $blue: 70%);\n}\n", "a {\n color: #c899ff;\n}\n" ); +test!( + change_color_named_arg_hue, + "a {\n color: change-color(blue, $hue: 150);\n}\n", + "a {\n color: #00ff80;\n}\n" +); +test!( + adjust_color_named_arg_hue, + "a {\n color: adjust-color(blue, $hue: 150);\n}\n", + "a {\n color: #ff8000;\n}\n" +); +test!( + change_color_negative_hue, + "a {\n color: change-color(red, $hue: -60);\n}\n", + "a {\n color: fuchsia;\n}\n" +); test!( scale_color_alpha, "a {\n color: scale-color(hsl(200, 70%, 80%), $saturation: -90%, $alpha: -30%);\n}\n", @@ -604,6 +628,14 @@ test!( }", "a {\n color: 0deg;\n color: 100%;\n color: 50%;\n color: #ffe6e6;\n color: 255;\n color: 230;\n color: 230;\n}\n" ); +test!( + slash_list_alpha, + "@use 'sass:list'; + a { + color: rgb(list.slash(1 2 3, var(--c))); + }", + "a {\n color: rgb(1, 2, 3, var(--c));\n}\n" +); test!( rgb_two_arg_nan_alpha, "a { @@ -635,6 +667,10 @@ error!( single_arg_saturate_expects_number, "a {\n color: saturate(red);\n}\n", "Error: $amount: red is not a number." ); +error!( + saturate_two_arg_first_is_number, + "a {\n color: saturate(1, 2);\n}\n", "Error: $color: 1 is not a color." +); error!( hex_color_starts_with_number_non_hex_digit_at_position_2, "a {\n color: #0zz;\n}\n", "Error: Expected hex digit." diff --git a/crates/lib/tests/color_hsl.rs b/crates/lib/tests/color_hsl.rs index f27d973..f88275f 100644 --- a/crates/lib/tests/color_hsl.rs +++ b/crates/lib/tests/color_hsl.rs @@ -285,17 +285,17 @@ test!( test!( hsl_with_turn_unit, "a {\n color: hsl(8turn, 25%, 50%);\n}\n", - "a {\n color: hsl(8deg, 25%, 50%);\n}\n" + "a {\n color: hsl(0deg, 25%, 50%);\n}\n" ); test!( hsl_with_rad_unit, "a {\n color: hsl(8rad, 25%, 50%);\n}\n", - "a {\n color: hsl(8deg, 25%, 50%);\n}\n" + "a {\n color: hsl(98.3662361047deg, 25%, 50%);\n}\n" ); test!( hsl_with_grad_unit, "a {\n color: hsl(8grad, 25%, 50%);\n}\n", - "a {\n color: hsl(8deg, 25%, 50%);\n}\n" + "a {\n color: hsl(7.2deg, 25%, 50%);\n}\n" ); test!( adjust_hue_nan, @@ -341,3 +341,18 @@ test!( "a {\n color: darken(rgb(50, 200, 100), 10);\n}\n", "a {\n color: #289f50;\n}\n" ); +test!( + hue_adjust_color_over_360, + "a {\n color: hue(adjust-color(blue, $hue: 150));\n}\n", + "a {\n color: 30deg;\n}\n" +); +test!( + adjust_hue_rad, + "a {\n color: adjust-hue(red, 60rad);\n}\n", + "a {\n color: #00b4ff;\n}\n" +); +test!( + hsl_hue_rad, + "a {\n color: hsl(60rad, 100%, 50%);\n}\n", + "a {\n color: hsl(197.7467707849deg, 100%, 50%);\n}\n" +); diff --git a/crates/lib/tests/color_hwb.rs b/crates/lib/tests/color_hwb.rs index 876f65a..10297a8 100644 --- a/crates/lib/tests/color_hwb.rs +++ b/crates/lib/tests/color_hwb.rs @@ -86,6 +86,11 @@ test!( "@use \"sass:color\";\na {\n color: color.hwb(180 30% 40% / 0);\n}\n", "a {\n color: rgba(77, 153, 153, 0);\n}\n" ); +test!( + hue_has_unit_rad, + "@use \"sass:color\";\na {\n color: color.hwb(1rad, 30%, 40%);\n}\n", + "a {\n color: #99964d;\n}\n" +); test!( scale_whiteness, "a {\n color: scale-color(#cc6666, $whiteness: 100%);\n}\n", @@ -96,3 +101,13 @@ error!( "@use \"sass:color\";\na {\n color: color.hwb(0, 0, 100);\n}\n", "Error: $whiteness: Expected 0 to have unit \"%\"." ); +error!( + hwb_two_args, + "@use \"sass:color\";\na {\n color: color.hwb(#123, 0.5);\n}\n", + "Error: Only 1 argument allowed, but 2 were passed." +); +error!( + hwb_blackness_too_high, + "@use \"sass:color\";\na {\n color: color.hwb(0, 30%, 101%, 0.5);\n}\n", + "Error: $blackness: Expected 101% to be within 0% and 100%." +); diff --git a/crates/lib/tests/list.rs b/crates/lib/tests/list.rs index 8a4684d..dba507d 100644 --- a/crates/lib/tests/list.rs +++ b/crates/lib/tests/list.rs @@ -445,6 +445,14 @@ test!( }", "a {\n color: comma;\n color: comma;\n}\n" ); +test!( + slash_list_are_equal, + "@use 'sass:list'; + a { + color: list.slash(a, b)==list.slash(a, b); + }", + "a {\n color: true;\n}\n" +); error!( nth_list_index_0, "a {\n color: nth(a b c, 0);\n}\n", "Error: $n: List index may not be 0." @@ -475,7 +483,7 @@ error!( "Error: $n: Invalid index 1px for a list with 0 elements." ); error!( - #[ignore = ""] + #[ignore = "we don't error"] empty_list_is_invalid, "a {\n color: ();\n}\n", "Error: () isn't a valid CSS value." ); diff --git a/crates/lib/tests/math-module.rs b/crates/lib/tests/math-module.rs index d37cdbc..58b7be2 100644 --- a/crates/lib/tests/math-module.rs +++ b/crates/lib/tests/math-module.rs @@ -54,7 +54,7 @@ test!( test!( sqrt_big_positive, "@use 'sass:math';\na {\n color: math.sqrt(9999999999999999999999999999999999999999999999999);\n}\n", - "a {\n color: 3162277660168379038695424;\n}\n" + "a {\n color: 3162277660168379000000000;\n}\n" ); test!( sqrt_big_negative, diff --git a/crates/lib/tests/number.rs b/crates/lib/tests/number.rs index fa8dd6c..fd4b98f 100644 --- a/crates/lib/tests/number.rs +++ b/crates/lib/tests/number.rs @@ -205,7 +205,6 @@ test!( "a {\n color: 0;\n color: true;\n}\n" ); test!( - #[ignore = "weird rounding issues"] scientific_notation_very_large_positive, "a {\n color: 1e100;\n}\n", "a {\n color: 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;\n}\n" );