improve code coverage, handle more builtin fn edge cases

This commit is contained in:
connorskees 2023-01-03 04:09:42 +00:00
parent 753c4960ca
commit 65c1a9e833
28 changed files with 496 additions and 388 deletions

View File

@ -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

View File

@ -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
```

View File

@ -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,

View File

@ -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<Value>, name: &str, min: f64, max: f64) -> SassResult<Number> {
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<Option<Number>> {
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())))

View File

@ -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,

View File

@ -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<Value> {
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<Value> {
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")?

View File

@ -189,55 +189,20 @@ fn hypot(args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
fn log(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
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<Value> {
fn pow(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
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<Value> {
fn sqrt(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {

View File

@ -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))
}
}

View File

@ -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,
}

View File

@ -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<Value> {
pub(crate) fn mul(left: Value, right: Value, _: &Options, span: Span) -> SassResult<Value> {
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<Value> {
@ -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<Value> {
pub(crate) fn rem(left: Value, right: Value, _: &Options, span: Span) -> SassResult<Value> {
Ok(match left {
Value::Dimension(SassNumber {
num: n,

View File

@ -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,

View File

@ -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))
}

View File

@ -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,
)

View File

@ -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()

View File

@ -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)
}

View File

@ -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."
);

View File

@ -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"
);

View File

@ -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",

View File

@ -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\"."
);

View File

@ -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)

View File

@ -215,4 +215,4 @@ error!(
@forward "foo";
"#,
"Error: @forward rules must be written before any other rules."
);
);

View File

@ -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 {

View File

@ -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,

View File

@ -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\"."

View File

@ -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."
);

View File

@ -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\"."

View File

@ -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",

View File

@ -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"
);