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 # 0.12.0
- complete rewrite of parsing, evaluation, and serialization steps - 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. 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). 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 2022-12-26
PASSING: 6024 PASSING: 6077
FAILING: 881 FAILING: 828
TOTAL: 6905 TOTAL: 6905
``` ```

View File

@ -9,11 +9,8 @@ pub(crate) fn blackness(mut args: ArgumentResult, visitor: &mut Visitor) -> Sass
.get_err(0, "color")? .get_err(0, "color")?
.assert_color_with_name("color", args.span())?; .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 { Ok(Value::Dimension(SassNumber {
num: (blackness * 100), num: color.blackness() * 100,
unit: Unit::Percent, unit: Unit::Percent,
as_slash: None, as_slash: None,
})) }))
@ -26,10 +23,8 @@ pub(crate) fn whiteness(mut args: ArgumentResult, visitor: &mut Visitor) -> Sass
.get_err(0, "color")? .get_err(0, "color")?
.assert_color_with_name("color", args.span())?; .assert_color_with_name("color", args.span())?;
let whiteness = color.red().min(color.green()).min(color.blue()) / Number(255.0);
Ok(Value::Dimension(SassNumber { Ok(Value::Dimension(SassNumber {
num: (whiteness * 100), num: color.whiteness() * 100,
unit: Unit::Percent, unit: Unit::Percent,
as_slash: None, as_slash: None,
})) }))
@ -102,9 +97,11 @@ pub(crate) fn hwb(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult
visitor, visitor,
args.span(), args.span(),
)? { )? {
ParsedChannels::String(s) => { ParsedChannels::String(s) => Err((
Err((format!("Expected numeric channels, got {}", s), args.span()).into()) format!("Expected numeric channels, got \"{}\"", s),
} args.span(),
)
.into()),
ParsedChannels::List(list) => { ParsedChannels::List(list) => {
let args = ArgumentResult { let args = ArgumentResult {
positional: list, 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, 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. // 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( return Ok(Value::Color(Box::new(Color::from_hsla(
hue.unwrap_or(this_hue), hue.unwrap_or(this_hue),
saturation.unwrap_or(this_saturation), saturation.unwrap_or(this_saturation),
luminance.unwrap_or(this_luminance), lightness.unwrap_or(this_lightness),
alpha.unwrap_or(this_alpha), 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, 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. // 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( return Ok(Value::Color(Box::new(Color::from_hsla(
this_hue + hue.unwrap_or_else(Number::zero), this_hue + hue.unwrap_or_else(Number::zero),
this_saturation + saturation.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), 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 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 span = args.span();
let color = args let color = args
.get_err(0, "color")? .get_err(0, "color")?
.assert_color_with_name("color", args.span())?; .assert_color_with_name("color", args.span())?;
macro_rules! opt_scale_arg { let red = get_arg(&mut args, "red", -100.0, 100.0)?;
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { let green = get_arg(&mut args, "green", -100.0, 100.0)?;
let $name = match $args.default_named_arg($arg, Value::Null) { let blue = get_arg(&mut args, "blue", -100.0, 100.0)?;
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(), let alpha = get_arg(&mut args, "alpha", -100.0, 100.0)?;
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);
if red.is_some() || green.is_some() || blue.is_some() { if red.is_some() || green.is_some() || blue.is_some() {
return Ok(Value::Color(Box::new(Color::from_rgba( 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); let saturation = get_arg(&mut args, "saturation", -100.0, 100.0)?;
opt_scale_arg!(args, luminance, "lightness", -100, 100); 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. // 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( return Ok(Value::Color(Box::new(Color::from_hsla(
scale(this_hue, Number::zero(), Number(360.0)), scale(this_hue, Number::zero(), Number(360.0)),
scale( scale(
@ -240,8 +232,8 @@ pub(crate) fn scale_color(mut args: ArgumentResult, visitor: &mut Visitor) -> Sa
Number::one(), Number::one(),
), ),
scale( scale(
this_luminance, this_lightness,
luminance.unwrap_or_else(Number::zero), lightness.unwrap_or_else(Number::zero),
Number::one(), Number::one(),
), ),
scale( 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 { Ok(Value::Color(if let Some(a) = alpha {
let temp_alpha = color.alpha(); let temp_alpha = color.alpha();
Box::new(color.with_alpha(scale(temp_alpha, a, Number::one()))) 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, Brackets::None,
), ),
Value::Map(m) => (m.as_list(), ListSeparator::Comma, 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")? { let (n, unit) = match args.get_err(1, "n")? {
Value::Dimension(SassNumber { Value::Dimension(SassNumber {
@ -133,7 +133,7 @@ pub(crate) fn append(mut args: ArgumentResult, visitor: &mut Visitor) -> SassRes
args.max_args(3)?; args.max_args(3)?;
let (mut list, sep, brackets) = match args.get_err(0, "list")? { let (mut list, sep, brackets) = match args.get_err(0, "list")? {
Value::List(v, sep, b) => (v, sep, b), 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 val = args.get_err(1, "val")?;
let sep = match args.default_arg( 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("auto".to_owned(), QuoteKind::None),
) { ) {
Value::String(s, ..) => match s.as_str() { Value::String(s, ..) => match s.as_str() {
"auto" => sep, "auto" => {
if sep == ListSeparator::Undecided {
ListSeparator::Space
} else {
sep
}
}
"comma" => ListSeparator::Comma, "comma" => ListSeparator::Comma,
"space" => ListSeparator::Space, "space" => ListSeparator::Space,
"slash" => ListSeparator::Slash, "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")? { let (mut list1, sep1, brackets) = match args.get_err(0, "list1")? {
Value::List(v, sep, brackets) => (v, sep, brackets), Value::List(v, sep, brackets) => (v, sep, brackets),
Value::Map(m) => (m.as_list(), ListSeparator::Comma, 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 (list2, sep2) = match args.get_err(1, "list2")? { let (list2, sep2) = match args.get_err(1, "list2")? {
Value::List(v, sep, ..) => (v, sep), Value::List(v, sep, ..) => (v, sep),
Value::Map(m) => (m.as_list(), ListSeparator::Comma), Value::Map(m) => (m.as_list(), ListSeparator::Comma),
v => (vec![v], ListSeparator::Space), v => (vec![v], ListSeparator::Undecided),
}; };
let sep = match args.default_arg( let sep = match args.default_arg(
2, 2,
@ -187,10 +193,12 @@ pub(crate) fn join(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResul
) { ) {
Value::String(s, ..) => match s.as_str() { Value::String(s, ..) => match s.as_str() {
"auto" => { "auto" => {
if list1.is_empty() || (list1.len() == 1 && sep1 == ListSeparator::Space) { if sep1 != ListSeparator::Undecided {
sep1
} else if sep2 != ListSeparator::Undecided {
sep2 sep2
} else { } else {
sep1 ListSeparator::Space
} }
} }
"comma" => ListSeparator::Comma, "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 span = args.span();
let (string, quotes) = match args.get_err(0, "string")? { let (string, quotes) = args
Value::String(s, q) => (s, q), .get_err(0, "string")?
v => { .assert_string_with_name("string", args.span())?;
return Err((
format!("$string: {} is not a string.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let str_len = string.chars().count(); 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> { pub(crate) fn str_index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let s1 = match args.get_err(0, "string")? { let s1 = args
Value::String(i, _) => i, .get_err(0, "string")?
v => { .assert_string_with_name("string", args.span())?
return Err(( .0;
format!("$string: {} is not a string.", v.inspect(args.span())?),
args.span(), let substr = args
) .get_err(1, "substring")?
.into()) .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")? { Ok(Value::Dimension(SassNumber {
Value::String(i, _) => i, num: Number::from(char_position),
v => { unit: Unit::None,
return Err(( as_slash: None,
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,
})
} }
pub(crate) fn str_insert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> { pub(crate) fn str_insert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let span = args.span(); let span = args.span();
let (s1, quotes) = match args.get_err(0, "string")? { let (s1, quotes) = args
Value::String(i, q) => (i, q), .get_err(0, "string")?
v => { .assert_string_with_name("string", args.span())?;
return Err((
format!("$string: {} is not a string.", v.inspect(span)?),
span,
)
.into())
}
};
let substr = match args.get_err(1, "insert")? { let substr = args
Value::String(i, _) => i, .get_err(1, "insert")?
v => { .assert_string_with_name("insert", args.span())?
return Err(( .0;
format!("$insert: {} is not a string.", v.inspect(span)?),
span,
)
.into())
}
};
let index = args let index = args
.get_err(2, "index")? .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> { fn log(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let number = match args.get_err(0, "number")? { let span = args.span();
// Value::Dimension { num: n, .. } if n.is_nan() => todo!(),
Value::Dimension(SassNumber { let number = args
num, .get_err(0, "number")?
unit: Unit::None, .assert_number_with_name("number", span)?;
.. number.assert_no_units("number", span)?;
}) => num, let number = number.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 base = match args.default_arg(1, "base", Value::Null) { let base = match args.default_arg(1, "base", Value::Null) {
Value::Null => None, 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 => { v => {
return Err(( let base = v.assert_number_with_name("base", span)?;
format!("$base: {} is not a number.", v.inspect(args.span())?), base.assert_no_units("base", span)?;
args.span(), Some(base.num)
)
.into())
} }
}; };
@ -264,58 +229,20 @@ fn log(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
fn pow(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> { fn pow(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let base = match args.get_err(0, "base")? { let span = args.span();
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 exponent = match args.get_err(1, "exponent")? { let base = args
Value::Dimension(SassNumber { .get_err(0, "base")?
num, .assert_number_with_name("base", span)?;
unit: Unit::None, base.assert_no_units("base", span)?;
..
}) => num, let exponent = args
v @ Value::Dimension(SassNumber { .. }) => { .get_err(1, "exponent")?
return Err(( .assert_number_with_name("exponent", span)?;
format!( exponent.assert_no_units("exponent", span)?;
"$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())
}
};
Ok(Value::Dimension(SassNumber { Ok(Value::Dimension(SassNumber {
num: base.pow(exponent), num: base.num.pow(exponent.num),
unit: Unit::None, unit: Unit::None,
as_slash: 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> { fn sqrt(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; 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 { Ok(Value::Dimension(SassNumber {
Value::Dimension(SassNumber { num: number.num.sqrt(),
num, unit: Unit::None,
unit: Unit::None, as_slash: 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())
}
})
} }
macro_rules! trig_fn { macro_rules! trig_fn {
@ -400,136 +307,83 @@ trig_fn!(tan);
fn acos(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> { fn acos(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let number = args.get_err(0, "number")?;
Ok(match number { let span = args.span();
Value::Dimension(SassNumber {
num, let number = args
unit: Unit::None, .get_err(0, "number")?
.. .assert_number_with_name("number", span)?;
}) => Value::Dimension(SassNumber { number.assert_no_units("number", span)?;
num: if num > Number(1.0) || num < Number(-1.0) { let number = number.num;
Number(f64::NAN)
} else if num.is_one() { Ok(Value::Dimension(SassNumber {
Number::zero() num: if number > Number(1.0) || number < Number(-1.0) {
} else { Number(f64::NAN)
num.acos() } else if number.is_one() {
}, Number::zero()
unit: Unit::Deg, } else {
as_slash: None, number.acos()
}), },
v @ Value::Dimension(SassNumber { .. }) => { unit: Unit::Deg,
return Err(( as_slash: None,
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())
}
})
} }
fn asin(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> { fn asin(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let number = args.get_err(0, "number")?;
Ok(match number { let span = args.span();
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,
}));
}
Value::Dimension(SassNumber { let number = args
num: num.asin(), .get_err(0, "number")?
unit: Unit::Deg, .assert_number_with_name("number", span)?;
as_slash: None, number.assert_no_units("number", span)?;
}) let number = number.num;
}
v @ Value::Dimension(SassNumber { .. }) => { if number > Number(1.0) || number < Number(-1.0) {
return Err(( return Ok(Value::Dimension(SassNumber {
format!( num: Number(f64::NAN),
"$number: Expected {} to be unitless.", unit: Unit::Deg,
v.inspect(args.span())? as_slash: None,
), }));
args.span(), } else if number.is_zero() {
) return Ok(Value::Dimension(SassNumber {
.into()) num: Number::zero(),
} unit: Unit::Deg,
v => { as_slash: None,
return Err(( }));
format!("$number: {} is not a number.", v.inspect(args.span())?), }
args.span(),
) Ok(Value::Dimension(SassNumber {
.into()) num: number.asin(),
} unit: Unit::Deg,
}) as_slash: None,
}))
} }
fn atan(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> { fn atan(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let number = args.get_err(0, "number")?;
Ok(match number { let span = args.span();
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,
}));
}
Value::Dimension(SassNumber { let number = args
num: n.atan(), .get_err(0, "number")?
unit: Unit::Deg, .assert_number_with_name("number", span)?;
as_slash: None, number.assert_no_units("number", span)?;
})
} if number.num.is_zero() {
v @ Value::Dimension(SassNumber { .. }) => { return Ok(Value::Dimension(SassNumber {
return Err(( num: (Number::zero()),
format!( unit: Unit::Deg,
"$number: Expected {} to be unitless.", as_slash: None,
v.inspect(args.span())? }));
), }
args.span(),
) Ok(Value::Dimension(SassNumber {
.into()) num: number.num.atan(),
} unit: Unit::Deg,
v => { as_slash: None,
return Err(( }))
format!("$number: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
})
} }
fn atan2(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> { 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) 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)] #[allow(clippy::match_like_matches_macro)]
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
match (self, other) { 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, (Self::Comma, Self::Comma) => true,
_ => false, _ => false,
} }

View File

@ -1,5 +1,3 @@
#![allow(unused_variables)]
use std::cmp::Ordering; use std::cmp::Ordering;
use codemap::Span; 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 { Ok(match left {
Value::Dimension(SassNumber { Value::Dimension(SassNumber {
num, num,
@ -412,7 +410,7 @@ pub(crate) fn mul(left: Value, right: Value, options: &Options, span: Span) -> S
pub(crate) fn cmp( pub(crate) fn cmp(
left: &Value, left: &Value,
right: &Value, right: &Value,
options: &Options, _: &Options,
span: Span, span: Span,
op: BinaryOp, op: BinaryOp,
) -> SassResult<Value> { ) -> SassResult<Value> {
@ -469,7 +467,7 @@ pub(crate) fn div(left: Value, right: Value, options: &Options, span: Span) -> S
Value::Dimension(SassNumber { Value::Dimension(SassNumber {
num: num2, num: num2,
unit: unit2, unit: unit2,
as_slash: as_slash2, ..
}) => { }) => {
if unit2 == Unit::None { if unit2 == Unit::None {
return Ok(Value::Dimension(SassNumber { 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 { Ok(match left {
Value::Dimension(SassNumber { Value::Dimension(SassNumber {
num: n, num: n,

View File

@ -76,6 +76,15 @@ pub(crate) fn serialize_number(
Ok(serializer.finish_for_expr()) 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( pub(crate) fn inspect_number(
number: &SassNumber, number: &SassNumber,
options: &Options, options: &Options,

View File

@ -44,7 +44,7 @@ impl ArgList {
} }
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.elems.len() + self.keywords.len() self.elems.len()
} }
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
@ -52,7 +52,6 @@ impl ArgList {
} }
pub fn is_null(&self) -> bool { pub fn is_null(&self) -> bool {
// todo: include keywords
!self.is_empty() && (self.elems.iter().all(Value::is_null)) !self.is_empty() && (self.elems.iter().all(Value::is_null))
} }

View File

@ -270,7 +270,7 @@ impl SassCalculation {
return Err(( return Err((
format!( format!(
"Number {} isn't compatible with CSS calculations.", "Number {} isn't compatible with CSS calculations.",
value.to_css_string(span, false)? value.inspect(span)?
), ),
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 // todo: rename is_blank
pub fn is_null(&self) -> bool { pub fn is_null(&self) -> bool {
match self { match self {
@ -599,8 +618,8 @@ impl Value {
.join(", ") .join(", ")
)), )),
Value::Dimension(n) => Cow::Owned(inspect_number(n, &Options::default(), span)?), 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.elems.is_empty() => Cow::Borrowed("()"),
Value::ArgList(args) if args.len() == 1 => Cow::Owned(format!( Value::ArgList(args) if args.elems.len() == 1 => Cow::Owned(format!(
"({},)", "({},)",
args.elems args.elems
.iter() .iter()

View File

@ -4,7 +4,7 @@ use codemap::Span;
use crate::{ use crate::{
error::SassResult, error::SassResult,
serializer::inspect_number, serializer::{inspect_float, inspect_number},
unit::{are_any_convertible, known_compatibilities_by_unit, Unit, UNIT_CONVERSION_TABLE}, unit::{are_any_convertible, known_compatibilities_by_unit, Unit, UNIT_CONVERSION_TABLE},
Options, 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 { pub fn is_comparable_to(&self, other: &Self) -> bool {
self.unit.comparable(&other.unit) 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: hue(adjust-hue(hsla(200, 50%, 50%), (0/0)));\n}\n",
"a {\n color: NaNdeg;\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", "@use \"sass:color\";\na {\n color: color.hwb(180 30% 40% / 0);\n}\n",
"a {\n color: rgba(77, 153, 153, 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!( error!(
hwb_whiteness_missing_pct, hwb_whiteness_missing_pct,
"@use \"sass:color\";\na {\n color: color.hwb(0, 0, 100);\n}\n", "@use \"sass:color\";\na {\n color: color.hwb(0, 0, 100);\n}\n",

View File

@ -99,3 +99,7 @@ error!(
missing_closing_curly_brace, missing_closing_curly_brace,
"@each $i in 1 {", "Error: expected \"}\"." "@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!( error!(
extend_at_root_of_document, extend_at_root_of_document,
"@extend a;", "@extend a;", "Error: @extend may only be used within style rules."
"Error: @extend may only be used within style rules."
); );
// todo: extend_loop (massive test) // todo: extend_loop (massive test)

View File

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

View File

@ -228,11 +228,12 @@ error!(
invalid_toplevel_selector, invalid_toplevel_selector,
"@if true { & { } }", "Error: Top-level selectors may not contain the parent selector \"&\"." "@if true { & { } }", "Error: Top-level selectors may not contain the parent selector \"&\"."
); );
error!( test!(
#[ignore = "unsure what the exact rule is here wrt denying interpolation (@media allows this)"] treats_interpolated_if_as_unknown_at_rule,
denies_interpolated_at_rule, "@#{if} true { a { color: red; } }",
"@#{if} true { a { color: red; } }", "Error: expected \"(\"." "@if true {\n a {\n color: red;\n }\n}\n"
); );
test!( test!(
else_if_escaped_lower_i, else_if_escaped_lower_i,
r"@if false { r"@if false {

View File

@ -485,6 +485,11 @@ test!(
"@import \"foo.css\";", "@import \"foo.css\";",
"@import \"foo.css\";\n" "@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, "@import url(foo..);", "@import url(foo..);\n");
test!( test!(
import_url_interpolation, 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",
"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!( error!(
invalid_item_in_space_separated_list, invalid_item_in_space_separated_list,
"a {\n color: red color * #abc;\n}\n", "Error: Undefined operation \"color * #abc\"." "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", "a {\n color: random(1000000000000000001 - 1000000000000000000);\n}\n",
"Error: $limit: Must be greater than 0, was 0." "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" "\\\\ {\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!( error!(
a_n_plus_b_n_invalid_odd, a_n_plus_b_n_invalid_odd,
":nth-child(ofdd) {\n color: &;\n}\n", "Error: Expected \"odd\"." ":nth-child(ofdd) {\n color: &;\n}\n", "Error: Expected \"odd\"."

View File

@ -42,6 +42,11 @@ error!(
calc_retains_multiline_comment, calc_retains_multiline_comment,
"a {\n color: calc(/**/);\n}\n", "Error: Expected number, variable, function, or calculation." "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!( error!(
calc_nested_parens, calc_nested_parens,
"a {\n color: calc((((()))));\n}\n", "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\\\n oo\";\n}\n",
"a {\n color: \"f 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"
);