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) # 0.12.2 (unreleased)
- implement an import cache, significantly improving the performance of certain pathological cases - 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 # 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: 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 2022-01-17
PASSING: 6153 PASSING: 6271
FAILING: 752 FAILING: 621
TOTAL: 6905 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>>> { pub fn get_variadic(self) -> SassResult<Vec<Spanned<Value>>> {
if let Some((name, _)) = self.named.iter().next() { if let Some((name, _)) = self.named.iter().next() {
return Err((format!("No argument named ${}.", name), self.span).into()); 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 crate::{builtin::builtin_imports::*, serializer::serialize_number, value::SassNumber};
use super::{ use super::{
angle_value,
rgb::{function_string, parse_channels, percentage_or_unitless}, rgb::{function_string, parse_channels, percentage_or_unitless},
ParsedChannels, 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 saturation = saturation.assert_number_with_name("saturation", span)?;
let lightness = lightness.assert_number_with_name("lightness", span)?; let lightness = lightness.assert_number_with_name("lightness", span)?;
let alpha = percentage_or_unitless( let alpha = percentage_or_unitless(
@ -55,7 +56,7 @@ fn hsl_3_args(
)?; )?;
Ok(Value::Color(Arc::new(Color::from_hsla_fn( 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), saturation.num / Number(100.0),
lightness.num / Number(100.0), lightness.num / Number(100.0),
Number(alpha), Number(alpha),
@ -162,10 +163,9 @@ pub(crate) fn adjust_hue(mut args: ArgumentResult, visitor: &mut Visitor) -> Sas
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())?;
let degrees = args let degrees = angle_value(args.get_err(1, "degrees")?, "degrees", args.span())?;
.get_err(1, "degrees")?
.assert_number_with_name("degrees", args.span())? dbg!(degrees);
.num;
Ok(Value::Color(Arc::new(color.adjust_hue(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")? .get_err(0, "color")?
.assert_color_with_name("color", args.span())?; .assert_color_with_name("color", args.span())?;
let amount = args let mut amount = args
.get_err(1, "amount")? .get_err(1, "amount")?
.assert_number_with_name("amount", args.span())?; .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> { 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); amount.num /= Number(100.0);
let color = match args.get_err(0, "color")? { let color = args
Value::Color(c) => c, .get_err(0, "color")?
Value::Dimension(SassNumber { .assert_color_with_name("color", args.span())?;
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())
}
};
Ok(Value::Color(Arc::new(color.saturate(amount.num)))) Ok(Value::Color(Arc::new(color.saturate(amount.num))))
} }

View File

@ -1,6 +1,10 @@
use crate::builtin::builtin_imports::*; 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> { pub(crate) fn blackness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?; 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> { fn hwb_inner(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
let span = args.span(); let span = args.span();
let hue = match args.get(0, "hue") { let hue = angle_value(args.get_err(0, "hue")?, "hue", args.span())?;
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 whiteness = args let whiteness = args
.get_err(1, "whiteness")? .get_err(1, "whiteness")?
.assert_number_with_name("whiteness", span)?; .assert_number_with_name("whiteness", span)?;
whiteness.assert_unit(&Unit::Percent, "whiteness", span)?; whiteness.assert_unit(&Unit::Percent, "whiteness", span)?;
whiteness.assert_bounds("whiteness", 0.0, 100.0, args.span())?;
let blackness = args let blackness = args
.get_err(2, "blackness")? .get_err(2, "blackness")?
.assert_number_with_name("blackness", span)?; .assert_number_with_name("blackness", span)?;
blackness.assert_unit(&Unit::Percent, "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") { let alpha = args
Some(v) => match v.node { .default_arg(3, "alpha", Value::Dimension(SassNumber::new_unitless(1.0)))
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(), .assert_number_with_name("alpha", args.span())?;
Value::Dimension(SassNumber {
num: n, let alpha = percentage_or_unitless(&alpha, 1.0, "alpha", args.span(), visitor)?;
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(),
};
Ok(Value::Color(Arc::new(Color::from_hwb( Ok(Value::Color(Arc::new(Color::from_hwb(
hue, hue,
whiteness.num, whiteness.num,
blackness.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) hwb_inner(args, visitor)
} }
} }
} else { } else if args.len() == 3 || args.len() == 4 {
hwb_inner(args, visitor) 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; use super::GlobalFunctionMap;
@ -14,6 +20,18 @@ pub(crate) enum ParsedChannels {
List(Vec<Value>), 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) { pub(crate) fn declare(f: &mut GlobalFunctionMap) {
hsl::declare(f); hsl::declare(f);
opacity::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")? .get_err(1, "amount")?
.assert_number_with_name("amount", args.span())?; .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)))) 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")? .get_err(1, "amount")?
.assert_number_with_name("amount", args.span())?; .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)))) 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 { #[derive(Debug, Copy, Clone, Eq, PartialEq)]
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { enum UpdateComponents {
let $name = match $args.default_named_arg($arg, Value::Null) { Change,
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(), Adjust,
Value::Dimension(SassNumber { Scale,
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())
}
};
};
} }
macro_rules! opt_hsl { fn update_components(
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { mut args: ArgumentResult,
let $name = match $args.default_named_arg($arg, Value::Null) { visitor: &mut Visitor,
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(), update: UpdateComponents,
Value::Dimension(SassNumber { ) -> SassResult<Value> {
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,
})
}
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())?;
let red = get_arg(&mut args, "red", -100.0, 100.0)?; // todo: what if color is also passed by name
let green = get_arg(&mut args, "green", -100.0, 100.0)?; if args.positional.len() > 1 {
let blue = get_arg(&mut args, "blue", -100.0, 100.0)?; return Err((
let alpha = get_arg(&mut args, "alpha", -100.0, 100.0)?; "Only one positional argument is allowed. All other arguments must be passed by name.",
span,
if red.is_some() || green.is_some() || blue.is_some() { )
return Ok(Value::Color(Arc::new(Color::from_rgba( .into());
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(),
),
))));
} }
let saturation = get_arg(&mut args, "saturation", -100.0, 100.0)?; let check_num = |num: Spanned<Value>,
let lightness = get_arg(&mut args, "lightness", -100.0, 100.0)?; 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() { if update == UpdateComponents::Scale {
// Color::as_hsla() returns more exact values than Color::hue(), etc. 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(); let (this_hue, this_saturation, this_lightness, this_alpha) = color.as_hsla();
return Ok(Value::Color(Arc::new(Color::from_hsla( Arc::new(Color::from_hsla(
scale(this_hue, Number::zero(), Number(360.0)), if update == UpdateComponents::Change {
scale( hue.unwrap_or(this_hue)
this_saturation, } else {
saturation.unwrap_or_else(Number::zero), this_hue + hue.unwrap_or_else(Number::zero)
Number::one(), },
), update_value(this_saturation, saturation, 1.0, update),
scale( update_value(this_lightness, lightness, 1.0, update),
this_lightness, update_value(this_alpha, alpha, 1.0, update),
lightness.unwrap_or_else(Number::zero), ))
Number::one(), } else if alpha.is_some() {
), Arc::new(color.with_alpha(update_value(color.alpha(), alpha, 1.0, update)))
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())))
} else { } else {
color 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> { 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(); channels = list[0].clone();
let inner_alpha_from_slash_list = list[1].clone(); let inner_alpha_from_slash_list = list[1].clone();
if !alpha_from_slash_list if !inner_alpha_from_slash_list.is_special_function() {
.as_ref()
.map(Value::is_special_function)
.unwrap_or(false)
{
inner_alpha_from_slash_list inner_alpha_from_slash_list
.clone() .clone()
.assert_number_with_name("alpha", span)?; .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::new_unitless(50.0)),
) { ) {
Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(), Value::Dimension(SassNumber { num: n, .. }) if n.is_nan() => todo!(),
Value::Dimension(SassNumber { Value::Dimension(mut num) => {
num: n, num.assert_bounds("weight", 0.0, 100.0, args.span())?;
unit: u, num.num /= Number(100.0);
as_slash: _, num.num
}) => bound!(args, "weight", n, u, 0, 100) / Number(100.0), }
v => { v => {
return Err(( return Err((
format!( 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}; use crate::{ast::ArgumentResult, error::SassResult, evaluate::Visitor, value::Value};
#[macro_use]
mod macros;
pub mod color; pub mod color;
pub mod list; pub mod list;
pub mod map; pub mod map;

View File

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

View File

@ -86,7 +86,7 @@ pub(crate) enum Brackets {
Bracketed, Bracketed,
} }
#[derive(Debug, Clone, Copy, Eq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) enum ListSeparator { pub(crate) enum ListSeparator {
Space, Space,
Comma, Comma,
@ -94,18 +94,6 @@ pub(crate) enum ListSeparator {
Undecided, 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 { impl ListSeparator {
pub fn as_str(self) -> &'static str { pub fn as_str(self) -> &'static str {
match self { match self {

View File

@ -580,11 +580,17 @@ impl<'a> Serializer<'a> {
.trim_end_matches('.'), .trim_end_matches('.'),
); );
} else { } else {
buffer.push_str( let p = 10.0_f64.powi(10);
format!("{:.10}", num)
.trim_end_matches('0') let n = (num * p).round() / p;
.trim_end_matches('.'),
); 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" { 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<()> { 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)) { if !(self.num <= Number(max) && self.num >= Number(min)) {
return Err(( return Err((
format!( format!(
@ -180,9 +191,9 @@ impl SassNumber {
name, name,
inspect_number(self, &Options::default(), span)?, inspect_number(self, &Options::default(), span)?,
inspect_float(min, &Options::default(), span), inspect_float(min, &Options::default(), span),
self.unit, unit,
inspect_float(max, &Options::default(), span), inspect_float(max, &Options::default(), span),
self.unit, unit,
), ),
span, span,
) )

View File

@ -350,11 +350,35 @@ test!(
"a {\n color: scale-color(hsl(120, 70%, 80%), $lightness: 50%);\n}\n", "a {\n color: scale-color(hsl(120, 70%, 80%), $lightness: 50%);\n}\n",
"a {\n color: #d4f7d4;\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!( test!(
scale_color_negative, scale_color_negative,
"a {\n color: scale-color(rgb(200, 150%, 170%), $green: -40%, $blue: 70%);\n}\n", "a {\n color: scale-color(rgb(200, 150%, 170%), $green: -40%, $blue: 70%);\n}\n",
"a {\n color: #c899ff;\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!( test!(
scale_color_alpha, scale_color_alpha,
"a {\n color: scale-color(hsl(200, 70%, 80%), $saturation: -90%, $alpha: -30%);\n}\n", "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" "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!( test!(
rgb_two_arg_nan_alpha, rgb_two_arg_nan_alpha,
"a { "a {
@ -635,6 +667,10 @@ error!(
single_arg_saturate_expects_number, single_arg_saturate_expects_number,
"a {\n color: saturate(red);\n}\n", "Error: $amount: red is not a 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!( error!(
hex_color_starts_with_number_non_hex_digit_at_position_2, hex_color_starts_with_number_non_hex_digit_at_position_2,
"a {\n color: #0zz;\n}\n", "Error: Expected hex digit." "a {\n color: #0zz;\n}\n", "Error: Expected hex digit."

View File

@ -285,17 +285,17 @@ test!(
test!( test!(
hsl_with_turn_unit, hsl_with_turn_unit,
"a {\n color: hsl(8turn, 25%, 50%);\n}\n", "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!( test!(
hsl_with_rad_unit, hsl_with_rad_unit,
"a {\n color: hsl(8rad, 25%, 50%);\n}\n", "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!( test!(
hsl_with_grad_unit, hsl_with_grad_unit,
"a {\n color: hsl(8grad, 25%, 50%);\n}\n", "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!( test!(
adjust_hue_nan, adjust_hue_nan,
@ -341,3 +341,18 @@ test!(
"a {\n color: darken(rgb(50, 200, 100), 10);\n}\n", "a {\n color: darken(rgb(50, 200, 100), 10);\n}\n",
"a {\n color: #289f50;\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", "@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!(
hue_has_unit_rad,
"@use \"sass:color\";\na {\n color: color.hwb(1rad, 30%, 40%);\n}\n",
"a {\n color: #99964d;\n}\n"
);
test!( test!(
scale_whiteness, scale_whiteness,
"a {\n color: scale-color(#cc6666, $whiteness: 100%);\n}\n", "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", "@use \"sass:color\";\na {\n color: color.hwb(0, 0, 100);\n}\n",
"Error: $whiteness: Expected 0 to have unit \"%\"." "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" "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!( error!(
nth_list_index_0, nth_list_index_0,
"a {\n color: nth(a b c, 0);\n}\n", "Error: $n: List index may not be 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: $n: Invalid index 1px for a list with 0 elements."
); );
error!( error!(
#[ignore = ""] #[ignore = "we don't error"]
empty_list_is_invalid, empty_list_is_invalid,
"a {\n color: ();\n}\n", "Error: () isn't a valid CSS value." "a {\n color: ();\n}\n", "Error: () isn't a valid CSS value."
); );

View File

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

View File

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