resolve all failing color spec tests
This commit is contained in:
parent
a7c2ca7b82
commit
0de3d2709f
@ -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
|
||||
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
|
@ -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());
|
||||
|
@ -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))))
|
||||
}
|
||||
|
||||
|
@ -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!()
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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))))
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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!(
|
||||
|
@ -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
|
||||
}
|
||||
};
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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" {
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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."
|
||||
|
@ -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"
|
||||
);
|
||||
|
@ -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%."
|
||||
);
|
||||
|
@ -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."
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user