diff --git a/CHANGELOG.md b/CHANGELOG.md index 765f947..cd1477d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ --> +# 0.12.1 (unreleased) + +- improve error message for complex units in calculations +- more accurate formatting of named arguments in arglists when passed to `inspect(..)` +- support `$whiteness` and `$blackness` as arguments to `scale-color(..)` +- more accurate list separator from `join(..)` +- resolve unicode edge cases in `str-index(..)` + # 0.12.0 - complete rewrite of parsing, evaluation, and serialization steps diff --git a/README.md b/README.md index 06190ec..1d63ccc 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ a bug except for in the case of error messages and error spans. Every commit of `grass` is tested against bootstrap v5.0.2, and every release is tested against the last 2,500 commits of bootstrap's `main` branch. -That said, there are a number of known missing features and bugs. The rough edges of `grass` largely include `@forward` and more complex uses of `@uses`. We support basic usage of these rules, but more advanced features such as `@import`ing modules containing `@forward` with prefixes may not behave as expected. +That said, there are a number of known missing features and bugs. The rough edges of `grass` largely include `@forward` and more complex uses of `@use`. We support basic usage of these rules, but more advanced features such as `@import`ing modules containing `@forward` with prefixes may not behave as expected. All known missing features and bugs are tracked in [#19](https://github.com/connorskees/grass/issues/19). @@ -71,8 +71,8 @@ Using a modified version of the spec runner that ignores warnings and error span ``` 2022-12-26 -PASSING: 6024 -FAILING: 881 +PASSING: 6077 +FAILING: 828 TOTAL: 6905 ``` diff --git a/src/builtin/functions/color/hwb.rs b/src/builtin/functions/color/hwb.rs index a1f8326..cceb0ea 100644 --- a/src/builtin/functions/color/hwb.rs +++ b/src/builtin/functions/color/hwb.rs @@ -9,11 +9,8 @@ pub(crate) fn blackness(mut args: ArgumentResult, visitor: &mut Visitor) -> Sass .get_err(0, "color")? .assert_color_with_name("color", args.span())?; - let blackness = - Number(1.0) - (color.red().max(color.green()).max(color.blue()) / Number(255.0)); - Ok(Value::Dimension(SassNumber { - num: (blackness * 100), + num: color.blackness() * 100, unit: Unit::Percent, as_slash: None, })) @@ -26,10 +23,8 @@ pub(crate) fn whiteness(mut args: ArgumentResult, visitor: &mut Visitor) -> Sass .get_err(0, "color")? .assert_color_with_name("color", args.span())?; - let whiteness = color.red().min(color.green()).min(color.blue()) / Number(255.0); - Ok(Value::Dimension(SassNumber { - num: (whiteness * 100), + num: color.whiteness() * 100, unit: Unit::Percent, as_slash: None, })) @@ -102,9 +97,11 @@ pub(crate) fn hwb(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult visitor, args.span(), )? { - ParsedChannels::String(s) => { - Err((format!("Expected numeric channels, got {}", s), args.span()).into()) - } + ParsedChannels::String(s) => Err(( + format!("Expected numeric channels, got \"{}\"", s), + args.span(), + ) + .into()), ParsedChannels::List(list) => { let args = ArgumentResult { positional: list, diff --git a/src/builtin/functions/color/other.rs b/src/builtin/functions/color/other.rs index 57f8c3e..76a73df 100644 --- a/src/builtin/functions/color/other.rs +++ b/src/builtin/functions/color/other.rs @@ -79,15 +79,15 @@ pub(crate) fn change_color(mut args: ArgumentResult, visitor: &mut Visitor) -> S }; opt_hsl!(args, saturation, "saturation", 0, 100); - opt_hsl!(args, luminance, "lightness", 0, 100); + opt_hsl!(args, lightness, "lightness", 0, 100); - if hue.is_some() || saturation.is_some() || luminance.is_some() { + 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_luminance, this_alpha) = color.as_hsla(); + let (this_hue, this_saturation, this_lightness, 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), + lightness.unwrap_or(this_lightness), alpha.unwrap_or(this_alpha), )))); } @@ -132,15 +132,15 @@ pub(crate) fn adjust_color(mut args: ArgumentResult, visitor: &mut Visitor) -> S }; opt_hsl!(args, saturation, "saturation", -100, 100); - opt_hsl!(args, luminance, "lightness", -100, 100); + opt_hsl!(args, lightness, "lightness", -100, 100); - if hue.is_some() || saturation.is_some() || luminance.is_some() { + 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_luminance, this_alpha) = color.as_hsla(); + let (this_hue, this_saturation, this_lightness, this_alpha) = color.as_hsla(); return Ok(Value::Color(Box::new(Color::from_hsla( this_hue + hue.unwrap_or_else(Number::zero), this_saturation + saturation.unwrap_or_else(Number::zero), - this_luminance + luminance.unwrap_or_else(Number::zero), + this_lightness + lightness.unwrap_or_else(Number::zero), this_alpha + alpha.unwrap_or_else(Number::zero), )))); } @@ -163,47 +163,39 @@ pub(crate) fn scale_color(mut args: ArgumentResult, visitor: &mut Visitor) -> Sa 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, + }) + } + let span = args.span(); let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; - macro_rules! opt_scale_arg { - ($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: Unit::Percent, - .. - }) => Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number(100.0)), - v @ Value::Dimension { .. } => { - return Err(( - format!( - "${}: Expected {} to have unit \"%\".", - $arg, - v.inspect($args.span())? - ), - $args.span(), - ) - .into()) - } - Value::Null => None, - v => { - return Err(( - format!("${}: {} is not a number.", $arg, v.inspect($args.span())?), - $args.span(), - ) - .into()) - } - }; - }; - } - - opt_scale_arg!(args, alpha, "alpha", -100, 100); - opt_scale_arg!(args, red, "red", -100, 100); - opt_scale_arg!(args, green, "green", -100, 100); - opt_scale_arg!(args, blue, "blue", -100, 100); + 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(Box::new(Color::from_rgba( @@ -226,12 +218,12 @@ pub(crate) fn scale_color(mut args: ArgumentResult, visitor: &mut Visitor) -> Sa )))); } - opt_scale_arg!(args, saturation, "saturation", -100, 100); - opt_scale_arg!(args, luminance, "lightness", -100, 100); + let saturation = get_arg(&mut args, "saturation", -100.0, 100.0)?; + let lightness = get_arg(&mut args, "lightness", -100.0, 100.0)?; - if saturation.is_some() || luminance.is_some() { + if saturation.is_some() || lightness.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(); + let (this_hue, this_saturation, this_lightness, this_alpha) = color.as_hsla(); return Ok(Value::Color(Box::new(Color::from_hsla( scale(this_hue, Number::zero(), Number(360.0)), scale( @@ -240,8 +232,8 @@ pub(crate) fn scale_color(mut args: ArgumentResult, visitor: &mut Visitor) -> Sa Number::one(), ), scale( - this_luminance, - luminance.unwrap_or_else(Number::zero), + this_lightness, + lightness.unwrap_or_else(Number::zero), Number::one(), ), scale( @@ -252,6 +244,34 @@ pub(crate) fn scale_color(mut args: ArgumentResult, visitor: &mut Visitor) -> Sa )))); } + 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(Box::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(); Box::new(color.with_alpha(scale(temp_alpha, a, Number::one()))) diff --git a/src/builtin/functions/list.rs b/src/builtin/functions/list.rs index 32dfc7b..012f06e 100644 --- a/src/builtin/functions/list.rs +++ b/src/builtin/functions/list.rs @@ -77,7 +77,7 @@ pub(crate) fn set_nth(mut args: ArgumentResult, visitor: &mut Visitor) -> SassRe Brackets::None, ), Value::Map(m) => (m.as_list(), ListSeparator::Comma, Brackets::None), - v => (vec![v], ListSeparator::Space, Brackets::None), + v => (vec![v], ListSeparator::Undecided, Brackets::None), }; let (n, unit) = match args.get_err(1, "n")? { Value::Dimension(SassNumber { @@ -133,7 +133,7 @@ pub(crate) fn append(mut args: ArgumentResult, visitor: &mut Visitor) -> SassRes args.max_args(3)?; let (mut list, sep, brackets) = match args.get_err(0, "list")? { Value::List(v, sep, b) => (v, sep, b), - v => (vec![v], ListSeparator::Space, Brackets::None), + v => (vec![v], ListSeparator::Undecided, Brackets::None), }; let val = args.get_err(1, "val")?; let sep = match args.default_arg( @@ -142,7 +142,13 @@ pub(crate) fn append(mut args: ArgumentResult, visitor: &mut Visitor) -> SassRes Value::String("auto".to_owned(), QuoteKind::None), ) { Value::String(s, ..) => match s.as_str() { - "auto" => sep, + "auto" => { + if sep == ListSeparator::Undecided { + ListSeparator::Space + } else { + sep + } + } "comma" => ListSeparator::Comma, "space" => ListSeparator::Space, "slash" => ListSeparator::Slash, @@ -173,12 +179,12 @@ pub(crate) fn join(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResul let (mut list1, sep1, brackets) = match args.get_err(0, "list1")? { Value::List(v, sep, brackets) => (v, sep, brackets), Value::Map(m) => (m.as_list(), ListSeparator::Comma, Brackets::None), - v => (vec![v], ListSeparator::Space, Brackets::None), + v => (vec![v], ListSeparator::Undecided, Brackets::None), }; let (list2, sep2) = match args.get_err(1, "list2")? { Value::List(v, sep, ..) => (v, sep), Value::Map(m) => (m.as_list(), ListSeparator::Comma), - v => (vec![v], ListSeparator::Space), + v => (vec![v], ListSeparator::Undecided), }; let sep = match args.default_arg( 2, @@ -187,10 +193,12 @@ pub(crate) fn join(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResul ) { Value::String(s, ..) => match s.as_str() { "auto" => { - if list1.is_empty() || (list1.len() == 1 && sep1 == ListSeparator::Space) { + if sep1 != ListSeparator::Undecided { + sep1 + } else if sep2 != ListSeparator::Undecided { sep2 } else { - sep1 + ListSeparator::Space } } "comma" => ListSeparator::Comma, diff --git a/src/builtin/functions/string.rs b/src/builtin/functions/string.rs index 3d8375c..52798e8 100644 --- a/src/builtin/functions/string.rs +++ b/src/builtin/functions/string.rs @@ -75,16 +75,9 @@ pub(crate) fn str_slice(mut args: ArgumentResult, visitor: &mut Visitor) -> Sass let span = args.span(); - let (string, quotes) = match args.get_err(0, "string")? { - Value::String(s, q) => (s, q), - v => { - return Err(( - format!("$string: {} is not a string.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }; + let (string, quotes) = args + .get_err(0, "string")? + .assert_string_with_name("string", args.span())?; let str_len = string.chars().count(); @@ -141,63 +134,40 @@ pub(crate) fn str_slice(mut args: ArgumentResult, visitor: &mut Visitor) -> Sass pub(crate) fn str_index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; - let s1 = match args.get_err(0, "string")? { - Value::String(i, _) => i, - v => { - return Err(( - format!("$string: {} is not a string.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } + let s1 = args + .get_err(0, "string")? + .assert_string_with_name("string", args.span())? + .0; + + let substr = args + .get_err(1, "substring")? + .assert_string_with_name("substring", args.span())? + .0; + + let char_position = match s1.find(&substr) { + Some(i) => s1[0..i].chars().count() + 1, + None => return Ok(Value::Null), }; - let substr = match args.get_err(1, "substring")? { - Value::String(i, _) => i, - v => { - return Err(( - format!("$substring: {} is not a string.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }; - - Ok(match s1.find(&substr) { - Some(v) => Value::Dimension(SassNumber { - num: (Number::from(v + 1)), - unit: Unit::None, - as_slash: None, - }), - None => Value::Null, - }) + Ok(Value::Dimension(SassNumber { + num: Number::from(char_position), + unit: Unit::None, + as_slash: None, + })) } pub(crate) fn str_insert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let span = args.span(); - let (s1, quotes) = match args.get_err(0, "string")? { - Value::String(i, q) => (i, q), - v => { - return Err(( - format!("$string: {} is not a string.", v.inspect(span)?), - span, - ) - .into()) - } - }; + let (s1, quotes) = args + .get_err(0, "string")? + .assert_string_with_name("string", args.span())?; - let substr = match args.get_err(1, "insert")? { - Value::String(i, _) => i, - v => { - return Err(( - format!("$insert: {} is not a string.", v.inspect(span)?), - span, - ) - .into()) - } - }; + let substr = args + .get_err(1, "insert")? + .assert_string_with_name("insert", args.span())? + .0; let index = args .get_err(2, "index")? diff --git a/src/builtin/modules/math.rs b/src/builtin/modules/math.rs index a801ed5..a3a32a7 100644 --- a/src/builtin/modules/math.rs +++ b/src/builtin/modules/math.rs @@ -189,55 +189,20 @@ fn hypot(args: ArgumentResult, _: &mut Visitor) -> SassResult { fn log(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(2)?; - let number = match args.get_err(0, "number")? { - // Value::Dimension { num: n, .. } if n.is_nan() => todo!(), - Value::Dimension(SassNumber { - num, - unit: Unit::None, - .. - }) => num, - v @ Value::Dimension(SassNumber { .. }) => { - return Err(( - format!( - "$number: Expected {} to be unitless.", - v.inspect(args.span())? - ), - args.span(), - ) - .into()) - } - v => { - return Err(( - format!("$number: {} is not a number.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }; + let span = args.span(); + + let number = args + .get_err(0, "number")? + .assert_number_with_name("number", span)?; + number.assert_no_units("number", span)?; + let number = number.num; let base = match args.default_arg(1, "base", Value::Null) { Value::Null => None, - Value::Dimension(SassNumber { - num, - unit: Unit::None, - .. - }) => Some(num), - v @ Value::Dimension(SassNumber { .. }) => { - return Err(( - format!( - "$number: Expected {} to be unitless.", - v.inspect(args.span())? - ), - args.span(), - ) - .into()) - } v => { - return Err(( - format!("$base: {} is not a number.", v.inspect(args.span())?), - args.span(), - ) - .into()) + let base = v.assert_number_with_name("base", span)?; + base.assert_no_units("base", span)?; + Some(base.num) } }; @@ -264,58 +229,20 @@ fn log(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { fn pow(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(2)?; - let base = match args.get_err(0, "base")? { - Value::Dimension(SassNumber { - num, - unit: Unit::None, - .. - }) => num, - v @ Value::Dimension(SassNumber { .. }) => { - return Err(( - format!( - "$base: Expected {} to have no units.", - v.inspect(args.span())? - ), - args.span(), - ) - .into()) - } - v => { - return Err(( - format!("$base: {} is not a number.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }; + let span = args.span(); - let exponent = match args.get_err(1, "exponent")? { - Value::Dimension(SassNumber { - num, - unit: Unit::None, - .. - }) => num, - v @ Value::Dimension(SassNumber { .. }) => { - return Err(( - format!( - "$exponent: Expected {} to have no units.", - v.inspect(args.span())? - ), - args.span(), - ) - .into()) - } - v => { - return Err(( - format!("$exponent: {} is not a number.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }; + let base = args + .get_err(0, "base")? + .assert_number_with_name("base", span)?; + base.assert_no_units("base", span)?; + + let exponent = args + .get_err(1, "exponent")? + .assert_number_with_name("exponent", span)?; + exponent.assert_no_units("exponent", span)?; Ok(Value::Dimension(SassNumber { - num: base.pow(exponent), + num: base.num.pow(exponent.num), unit: Unit::None, as_slash: None, })) @@ -323,36 +250,16 @@ fn pow(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { fn sqrt(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; - let number = args.get_err(0, "number")?; + let number = args + .get_err(0, "number")? + .assert_number_with_name("number", args.span())?; + number.assert_no_units("number", args.span())?; - Ok(match number { - Value::Dimension(SassNumber { - num, - unit: Unit::None, - .. - }) => Value::Dimension(SassNumber { - num: num.sqrt(), - unit: Unit::None, - as_slash: None, - }), - v @ Value::Dimension(SassNumber { .. }) => { - return Err(( - format!( - "$number: Expected {} to have no units.", - v.inspect(args.span())? - ), - args.span(), - ) - .into()) - } - v => { - return Err(( - format!("$number: {} is not a number.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }) + Ok(Value::Dimension(SassNumber { + num: number.num.sqrt(), + unit: Unit::None, + as_slash: None, + })) } macro_rules! trig_fn { @@ -400,136 +307,83 @@ trig_fn!(tan); fn acos(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; - let number = args.get_err(0, "number")?; - Ok(match number { - Value::Dimension(SassNumber { - num, - unit: Unit::None, - .. - }) => Value::Dimension(SassNumber { - num: if num > Number(1.0) || num < Number(-1.0) { - Number(f64::NAN) - } else if num.is_one() { - Number::zero() - } else { - num.acos() - }, - unit: Unit::Deg, - as_slash: None, - }), - v @ Value::Dimension(SassNumber { .. }) => { - return Err(( - format!( - "$number: Expected {} to be unitless.", - v.inspect(args.span())? - ), - args.span(), - ) - .into()) - } - v => { - return Err(( - format!("$number: {} is not a number.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }) + let span = args.span(); + + let number = args + .get_err(0, "number")? + .assert_number_with_name("number", span)?; + number.assert_no_units("number", span)?; + let number = number.num; + + Ok(Value::Dimension(SassNumber { + num: if number > Number(1.0) || number < Number(-1.0) { + Number(f64::NAN) + } else if number.is_one() { + Number::zero() + } else { + number.acos() + }, + unit: Unit::Deg, + as_slash: None, + })) } fn asin(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; - let number = args.get_err(0, "number")?; - Ok(match number { - Value::Dimension(SassNumber { - num, - unit: Unit::None, - .. - }) => { - if num > Number(1.0) || num < Number(-1.0) { - return Ok(Value::Dimension(SassNumber { - num: Number(f64::NAN), - unit: Unit::Deg, - as_slash: None, - })); - } else if num.is_zero() { - return Ok(Value::Dimension(SassNumber { - num: Number::zero(), - unit: Unit::Deg, - as_slash: None, - })); - } + let span = args.span(); - Value::Dimension(SassNumber { - num: num.asin(), - unit: Unit::Deg, - as_slash: None, - }) - } - v @ Value::Dimension(SassNumber { .. }) => { - return Err(( - format!( - "$number: Expected {} to be unitless.", - v.inspect(args.span())? - ), - args.span(), - ) - .into()) - } - v => { - return Err(( - format!("$number: {} is not a number.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }) + let number = args + .get_err(0, "number")? + .assert_number_with_name("number", span)?; + number.assert_no_units("number", span)?; + let number = number.num; + + if number > Number(1.0) || number < Number(-1.0) { + return Ok(Value::Dimension(SassNumber { + num: Number(f64::NAN), + unit: Unit::Deg, + as_slash: None, + })); + } else if number.is_zero() { + return Ok(Value::Dimension(SassNumber { + num: Number::zero(), + unit: Unit::Deg, + as_slash: None, + })); + } + + Ok(Value::Dimension(SassNumber { + num: number.asin(), + unit: Unit::Deg, + as_slash: None, + })) } fn atan(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; - let number = args.get_err(0, "number")?; - Ok(match number { - Value::Dimension(SassNumber { - num: n, - unit: Unit::None, - .. - }) => { - if n.is_zero() { - return Ok(Value::Dimension(SassNumber { - num: (Number::zero()), - unit: Unit::Deg, - as_slash: None, - })); - } + let span = args.span(); - Value::Dimension(SassNumber { - num: n.atan(), - unit: Unit::Deg, - as_slash: None, - }) - } - v @ Value::Dimension(SassNumber { .. }) => { - return Err(( - format!( - "$number: Expected {} to be unitless.", - v.inspect(args.span())? - ), - args.span(), - ) - .into()) - } - v => { - return Err(( - format!("$number: {} is not a number.", v.inspect(args.span())?), - args.span(), - ) - .into()) - } - }) + let number = args + .get_err(0, "number")? + .assert_number_with_name("number", span)?; + number.assert_no_units("number", span)?; + + if number.num.is_zero() { + return Ok(Value::Dimension(SassNumber { + num: (Number::zero()), + unit: Unit::Deg, + as_slash: None, + })); + } + + Ok(Value::Dimension(SassNumber { + num: number.num.atan(), + unit: Unit::Deg, + as_slash: None, + })) } fn atan2(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { diff --git a/src/color/mod.rs b/src/color/mod.rs index fab61ce..2afa55c 100644 --- a/src/color/mod.rs +++ b/src/color/mod.rs @@ -505,4 +505,12 @@ impl Color { Color::new_rgba(red, green, blue, alpha, ColorFormat::Infer) } + + pub fn whiteness(&self) -> Number { + self.red().min(self.green()).min(self.blue()) / Number(255.0) + } + + pub fn blackness(&self) -> Number { + Number(1.0) - (self.red().max(self.green()).max(self.blue()) / Number(255.0)) + } } diff --git a/src/common.rs b/src/common.rs index 1e78a5a..9394c2b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -98,7 +98,8 @@ impl PartialEq for ListSeparator { #[allow(clippy::match_like_matches_macro)] fn eq(&self, other: &Self) -> bool { match (self, other) { - (Self::Space | Self::Undecided, Self::Space | Self::Undecided) => true, + (Self::Space, Self::Space) => true, + (Self::Undecided, Self::Undecided) => true, (Self::Comma, Self::Comma) => true, _ => false, } diff --git a/src/evaluate/bin_op.rs b/src/evaluate/bin_op.rs index ae2e042..31f6bd5 100644 --- a/src/evaluate/bin_op.rs +++ b/src/evaluate/bin_op.rs @@ -1,5 +1,3 @@ -#![allow(unused_variables)] - use std::cmp::Ordering; use codemap::Span; @@ -350,7 +348,7 @@ pub(crate) fn sub(left: Value, right: Value, options: &Options, span: Span) -> S }) } -pub(crate) fn mul(left: Value, right: Value, options: &Options, span: Span) -> SassResult { +pub(crate) fn mul(left: Value, right: Value, _: &Options, span: Span) -> SassResult { Ok(match left { Value::Dimension(SassNumber { num, @@ -412,7 +410,7 @@ pub(crate) fn mul(left: Value, right: Value, options: &Options, span: Span) -> S pub(crate) fn cmp( left: &Value, right: &Value, - options: &Options, + _: &Options, span: Span, op: BinaryOp, ) -> SassResult { @@ -469,7 +467,7 @@ pub(crate) fn div(left: Value, right: Value, options: &Options, span: Span) -> S Value::Dimension(SassNumber { num: num2, unit: unit2, - as_slash: as_slash2, + .. }) => { if unit2 == Unit::None { return Ok(Value::Dimension(SassNumber { @@ -540,7 +538,7 @@ pub(crate) fn div(left: Value, right: Value, options: &Options, span: Span) -> S }) } -pub(crate) fn rem(left: Value, right: Value, options: &Options, span: Span) -> SassResult { +pub(crate) fn rem(left: Value, right: Value, _: &Options, span: Span) -> SassResult { Ok(match left { Value::Dimension(SassNumber { num: n, diff --git a/src/serializer.rs b/src/serializer.rs index c038f78..9866054 100644 --- a/src/serializer.rs +++ b/src/serializer.rs @@ -76,6 +76,15 @@ pub(crate) fn serialize_number( Ok(serializer.finish_for_expr()) } +pub(crate) fn inspect_float(number: f64, options: &Options, span: Span) -> String { + let map = CodeMap::new(); + let mut serializer = Serializer::new(options, &map, true, span); + + serializer.write_float(number); + + serializer.finish_for_expr() +} + pub(crate) fn inspect_number( number: &SassNumber, options: &Options, diff --git a/src/value/arglist.rs b/src/value/arglist.rs index af0c75b..4d0a069 100644 --- a/src/value/arglist.rs +++ b/src/value/arglist.rs @@ -44,7 +44,7 @@ impl ArgList { } pub fn len(&self) -> usize { - self.elems.len() + self.keywords.len() + self.elems.len() } pub fn is_empty(&self) -> bool { @@ -52,7 +52,6 @@ impl ArgList { } pub fn is_null(&self) -> bool { - // todo: include keywords !self.is_empty() && (self.elems.iter().all(Value::is_null)) } diff --git a/src/value/calculation.rs b/src/value/calculation.rs index c938b3c..1706730 100644 --- a/src/value/calculation.rs +++ b/src/value/calculation.rs @@ -270,7 +270,7 @@ impl SassCalculation { return Err(( format!( "Number {} isn't compatible with CSS calculations.", - value.to_css_string(span, false)? + value.inspect(span)? ), span, ) diff --git a/src/value/mod.rs b/src/value/mod.rs index a98c6fd..742e878 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -236,6 +236,25 @@ impl Value { } } + pub fn assert_string_with_name( + self, + name: &str, + span: Span, + ) -> SassResult<(String, QuoteKind)> { + match self { + Value::String(s, quotes) => Ok((s, quotes)), + _ => Err(( + format!( + "${name}: {} is not a string.", + self.inspect(span)?, + name = name, + ), + span, + ) + .into()), + } + } + // todo: rename is_blank pub fn is_null(&self) -> bool { match self { @@ -599,8 +618,8 @@ impl Value { .join(", ") )), Value::Dimension(n) => Cow::Owned(inspect_number(n, &Options::default(), span)?), - Value::ArgList(args) if args.is_empty() => Cow::Borrowed("()"), - Value::ArgList(args) if args.len() == 1 => Cow::Owned(format!( + Value::ArgList(args) if args.elems.is_empty() => Cow::Borrowed("()"), + Value::ArgList(args) if args.elems.len() == 1 => Cow::Owned(format!( "({},)", args.elems .iter() diff --git a/src/value/sass_number.rs b/src/value/sass_number.rs index a498fd4..8085270 100644 --- a/src/value/sass_number.rs +++ b/src/value/sass_number.rs @@ -4,7 +4,7 @@ use codemap::Span; use crate::{ error::SassResult, - serializer::inspect_number, + serializer::{inspect_float, inspect_number}, unit::{are_any_convertible, known_compatibilities_by_unit, Unit, UNIT_CONVERSION_TABLE}, Options, }; @@ -162,6 +162,26 @@ impl SassNumber { } } + pub fn assert_bounds(&self, name: &str, min: f64, max: f64, span: Span) -> SassResult<()> { + if !(self.num <= Number(max) && self.num >= Number(min)) { + return Err(( + format!( + "${}: Expected {} to be within {}{} and {}{}.", + name, + inspect_number(self, &Options::default(), span)?, + inspect_float(min, &Options::default(), span), + self.unit, + inspect_float(max, &Options::default(), span), + self.unit, + ), + span, + ) + .into()); + } + + Ok(()) + } + pub fn is_comparable_to(&self, other: &Self) -> bool { self.unit.comparable(&other.unit) } diff --git a/tests/arglist.rs b/tests/arglist.rs index cf0912c..115607d 100644 --- a/tests/arglist.rs +++ b/tests/arglist.rs @@ -92,3 +92,51 @@ test!( }", "" ); +test!( + keyword_args_no_positional, + "@mixin foo($a...) { + pos: inspect($a); + kw: inspect(keywords($a)); + } + + a { + @include foo($a: b); + }", + "a {\n pos: ();\n kw: (a: b);\n}\n" +); +test!( + keyword_args_one_positional, + "@mixin foo($a...) { + pos: inspect($a); + kw: inspect(keywords($a)); + } + + a { + @include foo(a, $b: c); + }", + "a {\n pos: (a,);\n kw: (b: c);\n}\n" +); +test!( + keyword_args_length_no_positional, + "@mixin foo($a...) { + pos: length($a); + kw: length(keywords($a)); + } + + a { + @include foo($a: b); + }", + "a {\n pos: 0;\n kw: 1;\n}\n" +); +error!( + keyword_args_no_positional_is_invalid, + "@mixin foo($a...) { + pos: $a; + kw: length(keywords($a)); + } + + a { + @include foo($a: b); + }", + "Error: () isn't a valid CSS value." +); diff --git a/tests/color_hsl.rs b/tests/color_hsl.rs index f0b62b7..f27d973 100644 --- a/tests/color_hsl.rs +++ b/tests/color_hsl.rs @@ -307,3 +307,37 @@ test!( "a {\n color: hue(adjust-hue(hsla(200, 50%, 50%), (0/0)));\n}\n", "a {\n color: NaNdeg;\n}\n" ); +test!( + hsl_special_two_arg_var_first, + "a {\n color: hsl(var(--foo), --bar);\n}\n", + "a {\n color: hsl(var(--foo), --bar);\n}\n" +); +test!( + hsl_special_two_arg_var_second, + "a {\n color: hsl(--foo, var(--bar));\n}\n", + "a {\n color: hsl(--foo, var(--bar));\n}\n" +); +error!( + hsl_special_two_arg_neither_var, + "a {\n color: hsl(--foo, --bar);\n}\n", "Error: Missing argument $lightness." +); +test!( + hsl_special_four_arg_var_last, + "a {\n color: hsl(a, b, c, var(--bar));\n}\n", + "a {\n color: hsl(a, b, c, var(--bar));\n}\n" +); +test!( + hsl_special_three_arg_var_last, + "a {\n color: hsl(a, b, var(--bar));\n}\n", + "a {\n color: hsl(a, b, var(--bar));\n}\n" +); +test!( + darken_all_channels_equal, + "a {\n color: darken(#fff, 10);\n}\n", + "a {\n color: #e6e6e6;\n}\n" +); +test!( + darken_green_channel_max, + "a {\n color: darken(rgb(50, 200, 100), 10);\n}\n", + "a {\n color: #289f50;\n}\n" +); diff --git a/tests/color_hwb.rs b/tests/color_hwb.rs index a6c161e..876f65a 100644 --- a/tests/color_hwb.rs +++ b/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!( + scale_whiteness, + "a {\n color: scale-color(#cc6666, $whiteness: 100%);\n}\n", + "a {\n color: #d5d5d5;\n}\n" +); error!( hwb_whiteness_missing_pct, "@use \"sass:color\";\na {\n color: color.hwb(0, 0, 100);\n}\n", diff --git a/tests/each.rs b/tests/each.rs index ca0474a..a43b5cf 100644 --- a/tests/each.rs +++ b/tests/each.rs @@ -99,3 +99,7 @@ error!( missing_closing_curly_brace, "@each $i in 1 {", "Error: expected \"}\"." ); +error!( + in_has_characters_after, + "@each $i inaa 0 1 2 {}", "Error: Expected \"in\"." +); diff --git a/tests/extend.rs b/tests/extend.rs index 759a549..c9c5331 100644 --- a/tests/extend.rs +++ b/tests/extend.rs @@ -1974,8 +1974,7 @@ error!( ); error!( extend_at_root_of_document, - "@extend a;", - "Error: @extend may only be used within style rules." + "@extend a;", "Error: @extend may only be used within style rules." ); // todo: extend_loop (massive test) diff --git a/tests/forward.rs b/tests/forward.rs index 622f062..d4f0109 100644 --- a/tests/forward.rs +++ b/tests/forward.rs @@ -215,4 +215,4 @@ error!( @forward "foo"; "#, "Error: @forward rules must be written before any other rules." -); \ No newline at end of file +); diff --git a/tests/if.rs b/tests/if.rs index 483bab2..0b9a5c7 100644 --- a/tests/if.rs +++ b/tests/if.rs @@ -228,11 +228,12 @@ error!( invalid_toplevel_selector, "@if true { & { } }", "Error: Top-level selectors may not contain the parent selector \"&\"." ); -error!( - #[ignore = "unsure what the exact rule is here wrt denying interpolation (@media allows this)"] - denies_interpolated_at_rule, - "@#{if} true { a { color: red; } }", "Error: expected \"(\"." +test!( + treats_interpolated_if_as_unknown_at_rule, + "@#{if} true { a { color: red; } }", + "@if true {\n a {\n color: red;\n }\n}\n" ); + test!( else_if_escaped_lower_i, r"@if false { diff --git a/tests/imports.rs b/tests/imports.rs index 4893912..a562750 100644 --- a/tests/imports.rs +++ b/tests/imports.rs @@ -485,6 +485,11 @@ test!( "@import \"foo.css\";", "@import \"foo.css\";\n" ); +test!( + newline_in_plain_css, + "@import \"fo\\\no.css\";", + "@import \"fo\\\no.css\";\n" +); test!(import_url, "@import url(foo..);", "@import url(foo..);\n"); test!( import_url_interpolation, diff --git a/tests/list.rs b/tests/list.rs index bd63c71..2d5a023 100644 --- a/tests/list.rs +++ b/tests/list.rs @@ -437,6 +437,18 @@ test!( "a {\n color: a, A, \"Noto Color Emoji\";\n}\n", "a {\n color: a, A, \"Noto Color Emoji\";\n}\n" ); +test!( + list_separator_of_empty_list_after_join, + "a { + color: list-separator(join(join((), (), comma), 1 2)); + color: list-separator(join(join((), (), comma), (1, 2))); + }", + "a {\n color: comma;\n color: comma;\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." +); error!( invalid_item_in_space_separated_list, "a {\n color: red color * #abc;\n}\n", "Error: Undefined operation \"color * #abc\"." diff --git a/tests/math.rs b/tests/math.rs index 04ecc99..fb0e525 100644 --- a/tests/math.rs +++ b/tests/math.rs @@ -126,3 +126,35 @@ error!( "a {\n color: random(1000000000000000001 - 1000000000000000000);\n}\n", "Error: $limit: Must be greater than 0, was 0." ); +error!( + percentage_non_number_arg, + "a {\n color: percentage(a);\n}\n", "Error: $number: a is not a number." +); +error!( + round_non_number_arg, + "a {\n color: round(a);\n}\n", "Error: $number: a is not a number." +); +error!( + ceil_non_number_arg, + "a {\n color: ceil(a);\n}\n", "Error: $number: a is not a number." +); +error!( + floor_non_number_arg, + "a {\n color: floor(a);\n}\n", "Error: $number: a is not a number." +); +error!( + abs_non_number_arg, + "a {\n color: abs(a);\n}\n", "Error: $number: a is not a number." +); +error!( + comparable_non_number_arg_both, + "a {\n color: comparable(a, b);\n}\n", "Error: $number1: a is not a number." +); +error!( + comparable_non_number_arg_first, + "a {\n color: comparable(a, 1);\n}\n", "Error: $number1: a is not a number." +); +error!( + comparable_non_number_arg_last, + "a {\n color: comparable(1, b);\n}\n", "Error: $number2: b is not a number." +); diff --git a/tests/selectors.rs b/tests/selectors.rs index accb7ff..dfeed8e 100644 --- a/tests/selectors.rs +++ b/tests/selectors.rs @@ -885,6 +885,48 @@ test!( }"#, "\\\\ {\n color: \\\\;\n}\n" ); +test!( + pseudo_element_double_quotes, + r#"::foo("red") { + color: &; + }"#, + "::foo(\"red\") {\n color: ::foo(\"red\");\n}\n" +); +test!( + pseudo_element_single_quotes, + r#"::foo('red') { + color: &; + }"#, + "::foo(\"red\") {\n color: ::foo(\"red\");\n}\n" +); +test!( + pseudo_element_loud_comments, + r#"::foo(/**/a/**/b/**/) { + color: &; + }"#, + "::foo(a/**/b/**/) {\n color: ::foo(a/**/b/**/);\n}\n" +); +test!( + pseudo_element_forward_slash, + r#"::foo(/a/b/) { + color: &; + }"#, + "::foo(/a/b/) {\n color: ::foo(/a/b/);\n}\n" +); +error!( + pseudo_element_interpolated_semicolon_no_brackets, + r#"::foo(#{";"}) { + color: &; + }"#, + r#"Error: expected ")"."# +); +test!( + pseudo_element_interpolated_semicolon_with_parens, + r#"::foo((#{";"})) { + color: &; + }"#, + "::foo((;)) {\n color: ::foo((;));\n}\n" +); error!( a_n_plus_b_n_invalid_odd, ":nth-child(ofdd) {\n color: &;\n}\n", "Error: Expected \"odd\"." diff --git a/tests/special-functions.rs b/tests/special-functions.rs index 221bab1..a7db4cf 100644 --- a/tests/special-functions.rs +++ b/tests/special-functions.rs @@ -42,6 +42,11 @@ error!( calc_retains_multiline_comment, "a {\n color: calc(/**/);\n}\n", "Error: Expected number, variable, function, or calculation." ); +error!( + calc_complex_unit, + "a {\n color: calc(1% + 1px * 2px);\n}\n", + "Error: Number 2px*px isn't compatible with CSS calculations." +); error!( calc_nested_parens, "a {\n color: calc((((()))));\n}\n", diff --git a/tests/strings.rs b/tests/strings.rs index 1f6fb81..bdc00ee 100644 --- a/tests/strings.rs +++ b/tests/strings.rs @@ -244,3 +244,13 @@ test!( "a {\n color: \"f\\\n oo\";\n}\n", "a {\n color: \"f oo\";\n}\n" ); +test!( + str_index_double_width_character, + "a {\n color: str-index(\"👭a\", \"a\");\n}\n", + "a {\n color: 2;\n}\n" +); +test!( + str_index_combining_character, + "a {\n color: str-index(\"c\\0308 a\", \"a\");\n}\n", + "a {\n color: 3;\n}\n" +);