resolve all failing color spec tests

This commit is contained in:
connorskees 2023-01-17 05:18:17 +00:00
parent a7c2ca7b82
commit 0de3d2709f
21 changed files with 403 additions and 406 deletions

View File

@ -8,6 +8,13 @@
# 0.12.2 (unreleased)
- implement an import cache, significantly improving the performance of certain pathological cases
- slash lists can be compared using `==`
- resolve rounding errors for extremely large numbers
- potentially breaking bug fixes in certain color functions
- `color.hwb(..)` no longer allows whiteness or blackness values outside the bounds 0% to 100%
- `scale-color(..)` no longer allows the `$hue` argument. previously it was ignored
- `scale-color(..)`, `change-color(..)`, and `adjust-color(..)` no longer allow invalid combinations of arguments or unknown named arguments
- many functions that accept hues now convert other angle units (`rad`, `grad`, `turn`) to `deg`. previously the unit was ignored
# 0.12.1

View File

@ -84,9 +84,9 @@ The spec runner does not work on Windows.
Using a modified version of the spec runner that ignores warnings and error spans (but does include error messages), `grass` achieves the following results:
```
2022-01-16
PASSING: 6153
FAILING: 752
2022-01-17
PASSING: 6271
FAILING: 621
TOTAL: 6905
```

View File

@ -267,13 +267,6 @@ impl ArgumentResult {
}
}
pub fn default_named_arg(&mut self, name: &'static str, default: Value) -> Value {
match self.get_named(name) {
Some(val) => val.node,
None => default,
}
}
pub fn get_variadic(self) -> SassResult<Vec<Spanned<Value>>> {
if let Some((name, _)) = self.named.iter().next() {
return Err((format!("No argument named ${}.", name), self.span).into());

View File

@ -3,6 +3,7 @@ use std::collections::{BTreeMap, BTreeSet};
use crate::{builtin::builtin_imports::*, serializer::serialize_number, value::SassNumber};
use super::{
angle_value,
rgb::{function_string, parse_channels, percentage_or_unitless},
ParsedChannels,
};
@ -43,7 +44,7 @@ fn hsl_3_args(
));
}
let hue = hue.assert_number_with_name("hue", span)?;
let hue = angle_value(hue, "hue", span)?;
let saturation = saturation.assert_number_with_name("saturation", span)?;
let lightness = lightness.assert_number_with_name("lightness", span)?;
let alpha = percentage_or_unitless(
@ -55,7 +56,7 @@ fn hsl_3_args(
)?;
Ok(Value::Color(Arc::new(Color::from_hsla_fn(
Number(hue.num.rem_euclid(360.0)),
Number(hue.rem_euclid(360.0)),
saturation.num / Number(100.0),
lightness.num / Number(100.0),
Number(alpha),
@ -162,10 +163,9 @@ pub(crate) fn adjust_hue(mut args: ArgumentResult, visitor: &mut Visitor) -> Sas
let color = args
.get_err(0, "color")?
.assert_color_with_name("color", args.span())?;
let degrees = args
.get_err(1, "degrees")?
.assert_number_with_name("degrees", args.span())?
.num;
let degrees = angle_value(args.get_err(1, "degrees")?, "degrees", args.span())?;
dbg!(degrees);
Ok(Value::Color(Arc::new(color.adjust_hue(degrees))))
}
@ -176,12 +176,15 @@ fn lighten(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value>
.get_err(0, "color")?
.assert_color_with_name("color", args.span())?;
let amount = args
let mut amount = args
.get_err(1, "amount")?
.assert_number_with_name("amount", args.span())?;
let amount = bound!(args, "amount", amount.num, amount.unit, 0, 100) / Number(100.0);
Ok(Value::Color(Arc::new(color.lighten(amount))))
amount.assert_bounds("amount", 0.0, 100.0, args.span())?;
amount.num /= Number(100.0);
Ok(Value::Color(Arc::new(color.lighten(amount.num))))
}
fn darken(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
@ -225,27 +228,10 @@ fn saturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value
amount.num /= Number(100.0);
let color = match args.get_err(0, "color")? {
Value::Color(c) => c,
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => {
// todo: this branch should be superfluous/incorrect
return Ok(Value::String(
format!("saturate({}{})", n.inspect(), u),
QuoteKind::None,
));
}
v => {
return Err((
format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let color = args
.get_err(0, "color")?
.assert_color_with_name("color", args.span())?;
Ok(Value::Color(Arc::new(color.saturate(amount.num))))
}

View File

@ -1,6 +1,10 @@
use crate::builtin::builtin_imports::*;
use super::{rgb::parse_channels, ParsedChannels};
use super::{
angle_value,
rgb::{parse_channels, percentage_or_unitless},
ParsedChannels,
};
pub(crate) fn blackness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?;
@ -33,56 +37,31 @@ pub(crate) fn whiteness(mut args: ArgumentResult, visitor: &mut Visitor) -> Sass
fn hwb_inner(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
let span = args.span();
let hue = match args.get(0, "hue") {
Some(v) => match v.node {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber { num: n, .. }) => n,
v => {
return Err((
format!("$hue: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
},
None => return Err(("Missing element $hue.", args.span()).into()),
};
let hue = angle_value(args.get_err(0, "hue")?, "hue", args.span())?;
let whiteness = args
.get_err(1, "whiteness")?
.assert_number_with_name("whiteness", span)?;
whiteness.assert_unit(&Unit::Percent, "whiteness", span)?;
whiteness.assert_bounds("whiteness", 0.0, 100.0, args.span())?;
let blackness = args
.get_err(2, "blackness")?
.assert_number_with_name("blackness", span)?;
blackness.assert_unit(&Unit::Percent, "blackness", span)?;
blackness.assert_bounds("blackness", 0.0, 100.0, args.span())?;
let alpha = match args.get(3, "alpha") {
Some(v) => match v.node {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num: n,
unit: Unit::Percent,
..
}) => n / Number(100.0),
Value::Dimension(SassNumber { num: n, .. }) => n,
v => {
return Err((
format!("$alpha: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
},
None => Number::one(),
};
let alpha = args
.default_arg(3, "alpha", Value::Dimension(SassNumber::new_unitless(1.0)))
.assert_number_with_name("alpha", args.span())?;
let alpha = percentage_or_unitless(&alpha, 1.0, "alpha", args.span(), visitor)?;
Ok(Value::Color(Arc::new(Color::from_hwb(
hue,
whiteness.num,
blackness.num,
alpha,
Number(alpha),
))))
}
@ -114,7 +93,10 @@ pub(crate) fn hwb(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult
hwb_inner(args, visitor)
}
}
} else {
} else if args.len() == 3 || args.len() == 4 {
hwb_inner(args, visitor)
} else {
args.max_args(1)?;
unreachable!()
}
}

View File

@ -1,4 +1,10 @@
use crate::value::Value;
use codemap::Span;
use crate::{
builtin::builtin_imports::Unit,
error::SassResult,
value::{conversion_factor, Number, Value},
};
use super::GlobalFunctionMap;
@ -14,6 +20,18 @@ pub(crate) enum ParsedChannels {
List(Vec<Value>),
}
pub(crate) fn angle_value(num: Value, name: &str, span: Span) -> SassResult<Number> {
let angle = num.assert_number_with_name(name, span)?;
if angle.has_compatible_units(&Unit::Deg) {
let factor = conversion_factor(&angle.unit, &Unit::Deg).unwrap();
return Ok(angle.num * Number(factor));
}
Ok(angle.num)
}
pub(crate) fn declare(f: &mut GlobalFunctionMap) {
hsl::declare(f);
opacity::declare(f);

View File

@ -94,7 +94,7 @@ fn opacify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value>
.get_err(1, "amount")?
.assert_number_with_name("amount", args.span())?;
amount.assert_bounds("amount", 0.0, 1.0, args.span())?;
amount.assert_bounds_with_unit("amount", 0.0, 1.0, &Unit::None, args.span())?;
Ok(Value::Color(Arc::new(color.fade_in(amount.num))))
}
@ -109,7 +109,7 @@ fn transparentize(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult
.get_err(1, "amount")?
.assert_number_with_name("amount", args.span())?;
amount.assert_bounds("amount", 0.0, 1.0, args.span())?;
amount.assert_bounds_with_unit("amount", 0.0, 1.0, &Unit::None, args.span())?;
Ok(Value::Color(Arc::new(color.fade_out(amount.num))))
}

View File

@ -1,283 +1,244 @@
use crate::builtin::builtin_imports::*;
use crate::{
builtin::{builtin_imports::*, color::angle_value},
utils::to_sentence,
value::fuzzy_round,
};
macro_rules! opt_rgba {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
let $name = match $args.default_named_arg($arg, Value::Null) {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num: n, unit: u, ..
}) => Some(bound!($args, $arg, n, u, $low, $high)),
Value::Null => None,
v => {
return Err((
format!("${}: {} is not a number.", $arg, v.inspect($args.span())?),
$args.span(),
)
.into())
}
};
};
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum UpdateComponents {
Change,
Adjust,
Scale,
}
macro_rules! opt_hsl {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
let $name = match $args.default_named_arg($arg, Value::Null) {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num: n, unit: u, ..
}) => Some(bound!($args, $arg, n, u, $low, $high) / Number(100.0)),
Value::Null => None,
v => {
return Err((
format!("${}: {} is not a number.", $arg, v.inspect($args.span())?),
$args.span(),
)
.into())
}
};
};
}
pub(crate) fn change_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
if args.get_positional(1).is_some() {
return Err((
"Only one positional argument is allowed. All other arguments must be passed by name.",
args.span(),
)
.into());
}
let color = args
.get_err(0, "color")?
.assert_color_with_name("color", args.span())?;
opt_rgba!(args, alpha, "alpha", 0, 1);
opt_rgba!(args, red, "red", 0, 255);
opt_rgba!(args, green, "green", 0, 255);
opt_rgba!(args, blue, "blue", 0, 255);
if red.is_some() || green.is_some() || blue.is_some() {
return Ok(Value::Color(Arc::new(Color::from_rgba(
red.unwrap_or_else(|| color.red()),
green.unwrap_or_else(|| color.green()),
blue.unwrap_or_else(|| color.blue()),
alpha.unwrap_or_else(|| color.alpha()),
))));
}
let hue = match args.default_named_arg("hue", Value::Null) {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber { num: n, .. }) => Some(n),
Value::Null => None,
v => {
return Err((
format!("$hue: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
opt_hsl!(args, saturation, "saturation", 0, 100);
opt_hsl!(args, lightness, "lightness", 0, 100);
if hue.is_some() || saturation.is_some() || lightness.is_some() {
// Color::as_hsla() returns more exact values than Color::hue(), etc.
let (this_hue, this_saturation, this_lightness, this_alpha) = color.as_hsla();
return Ok(Value::Color(Arc::new(Color::from_hsla(
hue.unwrap_or(this_hue),
saturation.unwrap_or(this_saturation),
lightness.unwrap_or(this_lightness),
alpha.unwrap_or(this_alpha),
))));
}
Ok(Value::Color(if let Some(a) = alpha {
Arc::new(color.with_alpha(a))
} else {
color
}))
}
pub(crate) fn adjust_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
let color = args
.get_err(0, "color")?
.assert_color_with_name("color", args.span())?;
opt_rgba!(args, alpha, "alpha", -1, 1);
opt_rgba!(args, red, "red", -255, 255);
opt_rgba!(args, green, "green", -255, 255);
opt_rgba!(args, blue, "blue", -255, 255);
if red.is_some() || green.is_some() || blue.is_some() {
return Ok(Value::Color(Arc::new(Color::from_rgba(
color.red() + red.unwrap_or_else(Number::zero),
color.green() + green.unwrap_or_else(Number::zero),
color.blue() + blue.unwrap_or_else(Number::zero),
color.alpha() + alpha.unwrap_or_else(Number::zero),
))));
}
let hue = match args.default_named_arg("hue", Value::Null) {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber { num: n, .. }) => Some(n),
Value::Null => None,
v => {
return Err((
format!("$hue: {} is not a number.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
opt_hsl!(args, saturation, "saturation", -100, 100);
opt_hsl!(args, lightness, "lightness", -100, 100);
if hue.is_some() || saturation.is_some() || lightness.is_some() {
// Color::as_hsla() returns more exact values than Color::hue(), etc.
let (this_hue, this_saturation, this_lightness, this_alpha) = color.as_hsla();
return Ok(Value::Color(Arc::new(Color::from_hsla(
this_hue + hue.unwrap_or_else(Number::zero),
this_saturation + saturation.unwrap_or_else(Number::zero),
this_lightness + lightness.unwrap_or_else(Number::zero),
this_alpha + alpha.unwrap_or_else(Number::zero),
))));
}
Ok(Value::Color(if let Some(a) = alpha {
let temp_alpha = color.alpha();
Arc::new(color.with_alpha(temp_alpha + a))
} else {
color
}))
}
#[allow(clippy::cognitive_complexity)]
// todo: refactor into rgb and hsl?
pub(crate) fn scale_color(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
pub(crate) fn scale(val: Number, by: Number, max: Number) -> Number {
if by.is_zero() {
return val;
}
val + (if by.is_positive() { max - val } else { val }) * by
}
fn check_num(num: Spanned<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,
})
}
fn update_components(
mut args: ArgumentResult,
visitor: &mut Visitor,
update: UpdateComponents,
) -> SassResult<Value> {
let span = args.span();
let color = args
.get_err(0, "color")?
.assert_color_with_name("color", args.span())?;
let red = get_arg(&mut args, "red", -100.0, 100.0)?;
let green = get_arg(&mut args, "green", -100.0, 100.0)?;
let blue = get_arg(&mut args, "blue", -100.0, 100.0)?;
let alpha = get_arg(&mut args, "alpha", -100.0, 100.0)?;
if red.is_some() || green.is_some() || blue.is_some() {
return Ok(Value::Color(Arc::new(Color::from_rgba(
scale(color.red(), red.unwrap_or_else(Number::zero), Number(255.0)),
scale(
color.green(),
green.unwrap_or_else(Number::zero),
Number(255.0),
),
scale(
color.blue(),
blue.unwrap_or_else(Number::zero),
Number(255.0),
),
scale(
color.alpha(),
alpha.unwrap_or_else(Number::zero),
Number::one(),
),
))));
// todo: what if color is also passed by name
if args.positional.len() > 1 {
return Err((
"Only one positional argument is allowed. All other arguments must be passed by name.",
span,
)
.into());
}
let saturation = get_arg(&mut args, "saturation", -100.0, 100.0)?;
let lightness = get_arg(&mut args, "lightness", -100.0, 100.0)?;
let check_num = |num: Spanned<Value>,
name: &str,
mut max: f64,
assert_percent: bool,
check_percent: bool|
-> SassResult<Number> {
let span = num.span;
let mut num = num.node.assert_number_with_name(name, span)?;
if saturation.is_some() || lightness.is_some() {
// Color::as_hsla() returns more exact values than Color::hue(), etc.
if update == UpdateComponents::Scale {
max = 100.0;
}
if assert_percent || update == UpdateComponents::Scale {
num.assert_unit(&Unit::Percent, name, span)?;
num.assert_bounds(
name,
if update == UpdateComponents::Change {
0.0
} else {
-max
},
max,
span,
)?;
} else {
num.assert_bounds_with_unit(
name,
if update == UpdateComponents::Change {
0.0
} else {
-max
},
max,
if check_percent {
&Unit::Percent
} else {
&Unit::None
},
span,
)?;
}
// todo: hack to check if rgb channel
if max == 100.0 {
num.num /= Number(100.0);
}
Ok(num.num)
};
let get_arg = |args: &mut ArgumentResult,
name: &str,
max: f64,
assert_percent: bool,
check_percent: bool|
-> SassResult<Option<Number>> {
Ok(match args.get(usize::MAX, name) {
Some(v) => Some(check_num(v, name, max, assert_percent, check_percent)?),
None => None,
})
};
let red = get_arg(&mut args, "red", 255.0, false, false)?;
let green = get_arg(&mut args, "green", 255.0, false, false)?;
let blue = get_arg(&mut args, "blue", 255.0, false, false)?;
let alpha = get_arg(&mut args, "alpha", 1.0, false, false)?;
let hue = if update == UpdateComponents::Scale {
None
} else {
args.get(usize::MAX, "hue")
.map(|v| angle_value(v.node, "hue", v.span))
.transpose()?
};
let saturation = get_arg(&mut args, "saturation", 100.0, false, true)?;
let lightness = get_arg(&mut args, "lightness", 100.0, false, true)?;
let whiteness = get_arg(&mut args, "whiteness", 100.0, true, true)?;
let blackness = get_arg(&mut args, "blackness", 100.0, true, true)?;
if !args.named.is_empty() {
let argument_word = if args.named.len() == 1 {
"argument"
} else {
"arguments"
};
let argument_names = to_sentence(
args.named
.keys()
.map(|key| format!("${key}", key = key))
.collect(),
"or",
);
return Err((
format!(
"No {argument_word} named {argument_names}.",
argument_word = argument_word,
argument_names = argument_names
),
span,
)
.into());
}
let has_rgb = red.is_some() || green.is_some() || blue.is_some();
let has_sl = saturation.is_some() || lightness.is_some();
let has_wb = whiteness.is_some() || blackness.is_some();
if has_rgb && (has_sl || has_wb || hue.is_some()) {
let param_type = if has_wb { "HWB" } else { "HSL" };
return Err((
format!(
"RGB parameters may not be passed along with {} parameters.",
param_type
),
span,
)
.into());
}
if has_sl && has_wb {
return Err((
"HSL parameters may not be passed along with HWB parameters.",
span,
)
.into());
}
fn update_value(
current: Number,
param: Option<Number>,
max: f64,
update: UpdateComponents,
) -> Number {
let param = match param {
Some(p) => p,
None => return current,
};
match update {
UpdateComponents::Change => param,
UpdateComponents::Adjust => (param + current).clamp(0.0, max),
UpdateComponents::Scale => {
current
+ if param > Number(0.0) {
Number(max) - current
} else {
current
} * param
}
}
}
fn update_rgb(current: Number, param: Option<Number>, update: UpdateComponents) -> Number {
Number(fuzzy_round(update_value(current, param, 255.0, update).0))
}
let color = if has_rgb {
Arc::new(Color::from_rgba(
update_rgb(color.red(), red, update),
update_rgb(color.green(), green, update),
update_rgb(color.blue(), blue, update),
update_value(color.alpha(), alpha, 1.0, update),
))
} else if has_wb {
Arc::new(Color::from_hwb(
if update == UpdateComponents::Change {
hue.unwrap_or_else(|| color.hue())
} else {
color.hue() + hue.unwrap_or_else(Number::zero)
},
update_value(color.whiteness(), whiteness, 1.0, update) * Number(100.0),
update_value(color.blackness(), blackness, 1.0, update) * Number(100.0),
update_value(color.alpha(), alpha, 1.0, update),
))
} else if hue.is_some() || has_sl {
let (this_hue, this_saturation, this_lightness, this_alpha) = color.as_hsla();
return Ok(Value::Color(Arc::new(Color::from_hsla(
scale(this_hue, Number::zero(), Number(360.0)),
scale(
this_saturation,
saturation.unwrap_or_else(Number::zero),
Number::one(),
),
scale(
this_lightness,
lightness.unwrap_or_else(Number::zero),
Number::one(),
),
scale(
this_alpha,
alpha.unwrap_or_else(Number::zero),
Number::one(),
),
))));
}
let whiteness = get_arg(&mut args, "whiteness", -100.0, 100.0)?;
let blackness = get_arg(&mut args, "blackness", -100.0, 100.0)?;
if whiteness.is_some() || blackness.is_some() {
let this_hue = color.hue();
let this_whiteness = color.whiteness() * Number(100.0);
let this_blackness = color.blackness() * Number(100.0);
return Ok(Value::Color(Arc::new(Color::from_hwb(
scale(this_hue, Number::zero(), Number(360.0)),
scale(
this_whiteness,
whiteness.unwrap_or_else(Number::zero),
Number(100.0),
),
scale(
this_blackness,
blackness.unwrap_or_else(Number::zero),
Number(100.0),
),
scale(
color.alpha(),
alpha.unwrap_or_else(Number::zero),
Number::one(),
),
))));
}
Ok(Value::Color(if let Some(a) = alpha {
let temp_alpha = color.alpha();
Arc::new(color.with_alpha(scale(temp_alpha, a, Number::one())))
Arc::new(Color::from_hsla(
if update == UpdateComponents::Change {
hue.unwrap_or(this_hue)
} else {
this_hue + hue.unwrap_or_else(Number::zero)
},
update_value(this_saturation, saturation, 1.0, update),
update_value(this_lightness, lightness, 1.0, update),
update_value(this_alpha, alpha, 1.0, update),
))
} else if alpha.is_some() {
Arc::new(color.with_alpha(update_value(color.alpha(), alpha, 1.0, update)))
} else {
color
}))
};
Ok(Value::Color(color))
}
pub(crate) fn scale_color(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
update_components(args, visitor, UpdateComponents::Scale)
}
pub(crate) fn change_color(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
update_components(args, visitor, UpdateComponents::Change)
}
pub(crate) fn adjust_color(args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
update_components(args, visitor, UpdateComponents::Adjust)
}
pub(crate) fn ie_hex_str(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {

View File

@ -217,11 +217,7 @@ pub(crate) fn parse_channels(
channels = list[0].clone();
let inner_alpha_from_slash_list = list[1].clone();
if !alpha_from_slash_list
.as_ref()
.map(Value::is_special_function)
.unwrap_or(false)
{
if !inner_alpha_from_slash_list.is_special_function() {
inner_alpha_from_slash_list
.clone()
.assert_number_with_name("alpha", span)?;
@ -395,11 +391,11 @@ pub(crate) fn mix(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult
Value::Dimension(SassNumber::new_unitless(50.0)),
) {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber {
num: n,
unit: u,
as_slash: _,
}) => bound!(args, "weight", n, u, 0, 100) / Number(100.0),
Value::Dimension(mut num) => {
num.assert_bounds("weight", 0.0, 100.0, args.span())?;
num.num /= Number(100.0);
num.num
}
v => {
return Err((
format!(

View File

@ -1,22 +0,0 @@
macro_rules! bound {
($args:ident, $name:literal, $arg:expr, $unit:expr, $low:literal, $high:literal) => {
if !($arg <= Number::from($high) && $arg >= Number::from($low)) {
return Err((
format!(
"${}: Expected {}{} to be within {}{} and {}{}.",
$name,
$arg.inspect(),
$unit,
$low,
$unit,
$high,
$unit,
),
$args.span(),
)
.into());
} else {
$arg
}
};
}

View File

@ -10,9 +10,6 @@ use once_cell::sync::Lazy;
use crate::{ast::ArgumentResult, error::SassResult, evaluate::Visitor, value::Value};
#[macro_use]
mod macros;
pub mod color;
pub mod list;
pub mod map;

View File

@ -373,6 +373,7 @@ impl Color {
/// Create RGBA representation from HSLA values
pub fn from_hsla(hue: Number, saturation: Number, lightness: Number, alpha: Number) -> Self {
let hue = hue % Number(360.0);
let hsla = Hsl::new(hue, saturation.clamp(0.0, 1.0), lightness.clamp(0.0, 1.0));
let scaled_hue = hue.0 / 360.0;

View File

@ -86,7 +86,7 @@ pub(crate) enum Brackets {
Bracketed,
}
#[derive(Debug, Clone, Copy, Eq)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) enum ListSeparator {
Space,
Comma,
@ -94,18 +94,6 @@ pub(crate) enum ListSeparator {
Undecided,
}
impl PartialEq for ListSeparator {
#[allow(clippy::match_like_matches_macro)]
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Space, Self::Space) => true,
(Self::Undecided, Self::Undecided) => true,
(Self::Comma, Self::Comma) => true,
_ => false,
}
}
}
impl ListSeparator {
pub fn as_str(self) -> &'static str {
match self {

View File

@ -580,11 +580,17 @@ impl<'a> Serializer<'a> {
.trim_end_matches('.'),
);
} else {
buffer.push_str(
format!("{:.10}", num)
.trim_end_matches('0')
.trim_end_matches('.'),
);
let p = 10.0_f64.powi(10);
let n = (num * p).round() / p;
let formatted = n.to_string();
if formatted.ends_with(".0") {
buffer.push_str(formatted.trim_end_matches('0').trim_end_matches('.'));
} else {
buffer.push_str(&formatted);
}
}
if buffer.is_empty() || buffer == "-" || buffer == "-0" {

View File

@ -173,6 +173,17 @@ impl SassNumber {
}
pub fn assert_bounds(&self, name: &str, min: f64, max: f64, span: Span) -> SassResult<()> {
self.assert_bounds_with_unit(name, min, max, &self.unit, span)
}
pub fn assert_bounds_with_unit(
&self,
name: &str,
min: f64,
max: f64,
unit: &Unit,
span: Span,
) -> SassResult<()> {
if !(self.num <= Number(max) && self.num >= Number(min)) {
return Err((
format!(
@ -180,9 +191,9 @@ impl SassNumber {
name,
inspect_number(self, &Options::default(), span)?,
inspect_float(min, &Options::default(), span),
self.unit,
unit,
inspect_float(max, &Options::default(), span),
self.unit,
unit,
),
span,
)

View File

@ -350,11 +350,35 @@ test!(
"a {\n color: scale-color(hsl(120, 70%, 80%), $lightness: 50%);\n}\n",
"a {\n color: #d4f7d4;\n}\n"
);
test!(
scale_color_neg_lightness_and_pos_saturation,
"a {\n color: scale-color(turquoise, $saturation: 24%, $lightness: -48%);\n}\n",
"a {\n color: #10867a;\n}\n"
);
error!(
scale_color_named_arg_hue,
"a {\n color: scale-color(red, $hue: 10%);\n}\n", "Error: No argument named $hue."
);
test!(
scale_color_negative,
"a {\n color: scale-color(rgb(200, 150%, 170%), $green: -40%, $blue: 70%);\n}\n",
"a {\n color: #c899ff;\n}\n"
);
test!(
change_color_named_arg_hue,
"a {\n color: change-color(blue, $hue: 150);\n}\n",
"a {\n color: #00ff80;\n}\n"
);
test!(
adjust_color_named_arg_hue,
"a {\n color: adjust-color(blue, $hue: 150);\n}\n",
"a {\n color: #ff8000;\n}\n"
);
test!(
change_color_negative_hue,
"a {\n color: change-color(red, $hue: -60);\n}\n",
"a {\n color: fuchsia;\n}\n"
);
test!(
scale_color_alpha,
"a {\n color: scale-color(hsl(200, 70%, 80%), $saturation: -90%, $alpha: -30%);\n}\n",
@ -604,6 +628,14 @@ test!(
}",
"a {\n color: 0deg;\n color: 100%;\n color: 50%;\n color: #ffe6e6;\n color: 255;\n color: 230;\n color: 230;\n}\n"
);
test!(
slash_list_alpha,
"@use 'sass:list';
a {
color: rgb(list.slash(1 2 3, var(--c)));
}",
"a {\n color: rgb(1, 2, 3, var(--c));\n}\n"
);
test!(
rgb_two_arg_nan_alpha,
"a {
@ -635,6 +667,10 @@ error!(
single_arg_saturate_expects_number,
"a {\n color: saturate(red);\n}\n", "Error: $amount: red is not a number."
);
error!(
saturate_two_arg_first_is_number,
"a {\n color: saturate(1, 2);\n}\n", "Error: $color: 1 is not a color."
);
error!(
hex_color_starts_with_number_non_hex_digit_at_position_2,
"a {\n color: #0zz;\n}\n", "Error: Expected hex digit."

View File

@ -285,17 +285,17 @@ test!(
test!(
hsl_with_turn_unit,
"a {\n color: hsl(8turn, 25%, 50%);\n}\n",
"a {\n color: hsl(8deg, 25%, 50%);\n}\n"
"a {\n color: hsl(0deg, 25%, 50%);\n}\n"
);
test!(
hsl_with_rad_unit,
"a {\n color: hsl(8rad, 25%, 50%);\n}\n",
"a {\n color: hsl(8deg, 25%, 50%);\n}\n"
"a {\n color: hsl(98.3662361047deg, 25%, 50%);\n}\n"
);
test!(
hsl_with_grad_unit,
"a {\n color: hsl(8grad, 25%, 50%);\n}\n",
"a {\n color: hsl(8deg, 25%, 50%);\n}\n"
"a {\n color: hsl(7.2deg, 25%, 50%);\n}\n"
);
test!(
adjust_hue_nan,
@ -341,3 +341,18 @@ test!(
"a {\n color: darken(rgb(50, 200, 100), 10);\n}\n",
"a {\n color: #289f50;\n}\n"
);
test!(
hue_adjust_color_over_360,
"a {\n color: hue(adjust-color(blue, $hue: 150));\n}\n",
"a {\n color: 30deg;\n}\n"
);
test!(
adjust_hue_rad,
"a {\n color: adjust-hue(red, 60rad);\n}\n",
"a {\n color: #00b4ff;\n}\n"
);
test!(
hsl_hue_rad,
"a {\n color: hsl(60rad, 100%, 50%);\n}\n",
"a {\n color: hsl(197.7467707849deg, 100%, 50%);\n}\n"
);

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!(
hue_has_unit_rad,
"@use \"sass:color\";\na {\n color: color.hwb(1rad, 30%, 40%);\n}\n",
"a {\n color: #99964d;\n}\n"
);
test!(
scale_whiteness,
"a {\n color: scale-color(#cc6666, $whiteness: 100%);\n}\n",
@ -96,3 +101,13 @@ error!(
"@use \"sass:color\";\na {\n color: color.hwb(0, 0, 100);\n}\n",
"Error: $whiteness: Expected 0 to have unit \"%\"."
);
error!(
hwb_two_args,
"@use \"sass:color\";\na {\n color: color.hwb(#123, 0.5);\n}\n",
"Error: Only 1 argument allowed, but 2 were passed."
);
error!(
hwb_blackness_too_high,
"@use \"sass:color\";\na {\n color: color.hwb(0, 30%, 101%, 0.5);\n}\n",
"Error: $blackness: Expected 101% to be within 0% and 100%."
);

View File

@ -445,6 +445,14 @@ test!(
}",
"a {\n color: comma;\n color: comma;\n}\n"
);
test!(
slash_list_are_equal,
"@use 'sass:list';
a {
color: list.slash(a, b)==list.slash(a, b);
}",
"a {\n color: true;\n}\n"
);
error!(
nth_list_index_0,
"a {\n color: nth(a b c, 0);\n}\n", "Error: $n: List index may not be 0."
@ -475,7 +483,7 @@ error!(
"Error: $n: Invalid index 1px for a list with 0 elements."
);
error!(
#[ignore = ""]
#[ignore = "we don't error"]
empty_list_is_invalid,
"a {\n color: ();\n}\n", "Error: () isn't a valid CSS value."
);

View File

@ -54,7 +54,7 @@ test!(
test!(
sqrt_big_positive,
"@use 'sass:math';\na {\n color: math.sqrt(9999999999999999999999999999999999999999999999999);\n}\n",
"a {\n color: 3162277660168379038695424;\n}\n"
"a {\n color: 3162277660168379000000000;\n}\n"
);
test!(
sqrt_big_negative,

View File

@ -205,7 +205,6 @@ test!(
"a {\n color: 0;\n color: true;\n}\n"
);
test!(
#[ignore = "weird rounding issues"]
scientific_notation_very_large_positive,
"a {\n color: 1e100;\n}\n", "a {\n color: 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;\n}\n"
);