use super::channels::Channels;
use super::{
check_alpha, check_channel, check_pct_range, eval_inner, is_not,
is_special, relative_color, CallError, CheckedArg, FunctionMap,
NumOrSpecial, ResolvedArgs,
};
use crate::css::{CallArgs, Value};
use crate::sass::{ArgsError, FormalArgs, Name};
use crate::value::{Color, RgbFormat, Rgba};
use crate::Scope;
pub fn register(f: &mut Scope) {
def_va!(f, _rgb(kwargs), |s| do_rgba(&name!(rgb), s));
def_va!(f, _rgba(kwargs), |s| do_rgba(&name!(rgba), s));
def!(f, red(color), |s| {
let c: Color = s.get(name!(color))?;
Ok(Value::scalar(c.to_rgba().red().round()))
});
def!(f, green(color), |s| {
let c: Color = s.get(name!(color))?;
Ok(Value::scalar(c.to_rgba().green().round()))
});
def!(f, blue(color), |s| {
let c: Color = s.get(name!(color))?;
Ok(Value::scalar(c.to_rgba().blue().round()))
});
def!(f, mix(color1, color2, weight = b"50%"), |s| {
let a: Color = s.get(name!(color1))?;
let a = a.to_rgba();
let b: Color = s.get(name!(color2))?;
let b = b.to_rgba();
let w = s.get_map(name!(weight), check_pct_range)?;
let w_a = {
let wa = a.alpha() - b.alpha();
let w2 = w * 2. - 1.;
let divis = w2 * wa + 1.;
if divis == 0. {
w
} else {
(((w2 + wa) / divis) + 1.) / 2.
}
};
let w_b = 1. - w_a;
let m_c = |c_a, c_b| w_a * c_a + w_b * c_b;
Ok(Rgba::new(
m_c(a.red(), b.red()),
m_c(a.green(), b.green()),
m_c(a.blue(), b.blue()),
a.alpha() * w + b.alpha() * (1. - w),
RgbFormat::Name,
)
.into())
});
def!(f, invert(color, weight = b"100%"), |s| {
match s.get(name!(color))? {
Value::Color(col, _) => {
let w = s.get_map(name!(weight), check_pct_range)?;
Ok(col.invert(w).into())
}
col => {
let w = s.get_map(name!(weight), check_pct_range)?;
if w == 1. {
match col {
v @ Value::Numeric(..) => {
Ok(Value::call("invert", [v]))
}
v => Err(is_not(&v, "a color")).named(name!(color)),
}
} else {
Err(CallError::msg("Only one argument may be passed to the plain-CSS invert() function."))
}
}
}
});
}
pub fn expose(m: &Scope, global: &mut FunctionMap) {
for (gname, lname) in &[
(name!(rgb), name!(_rgb)),
(name!(rgba), name!(_rgba)),
(name!(blue), name!(blue)),
(name!(green), name!(green)),
(name!(mix), name!(mix)),
(name!(red), name!(red)),
] {
global.insert(gname.clone(), m.get_lfunction(lname));
}
let mut f = Scope::builtin_module("sass:color");
def!(f, invert(color, weight = b"100%"), |s| {
match s.get(name!(color))? {
Value::Color(col, _) => {
let w = s.get_map(name!(weight), check_pct_range)?;
Ok(col.invert(w).into())
}
col => {
let w = s.get_map(name!(weight), check_pct_range)?;
if w == 1. {
NumOrSpecial::try_from(col)
.map_err(|e| is_not(e.value(), "a color"))
.named(name!(color))
.map(|v| Value::call("invert", [v]))
} else {
Err(CallError::msg("Only one argument may be passed to the plain-CSS invert() function."))
}
}
}
});
#[allow(clippy::single_element_loop)] for name in [name!(invert)] {
let f = f.get_lfunction(&name);
global.insert(name, f);
}
}
fn do_rgba(fn_name: &Name, s: &ResolvedArgs) -> Result<Value, CallError> {
let a1 = FormalArgs::new(vec![one_arg!(channels)]);
let a1b = FormalArgs::new(vec![one_arg!(color), one_arg!(alpha)]);
let a2 = FormalArgs::new(vec![
one_arg!(red),
one_arg!(green),
one_arg!(blue = b"null"),
one_arg!(alpha = b"null"),
]);
let a2_show = FormalArgs::new(vec![
one_arg!(red),
one_arg!(green),
one_arg!(blue),
one_arg!(alpha),
]);
let args = s.get_map(name!(kwargs), CallArgs::from_value)?;
if relative_color(&args) {
return Ok(Value::Call(fn_name.to_string(), args));
}
match eval_inner(fn_name, &a1, s, args.clone()) {
Ok(s) => Channels::try_from(s.get::<Value>(name!(channels))?)
.map_err(|e| e.conv(&["red", "green", "blue"]))
.and_then(|c| match c {
Channels::Data([h, s, l, a]) => {
rgba_from_values(fn_name, h, s, l, a)
}
Channels::Special(channels) => {
Ok(Value::call(fn_name.as_ref(), [channels]))
}
}),
Err(err @ CallError::Args(ArgsError::Missing(_), _)) => Err(err),
Err(_) => match eval_inner(fn_name, &a1b, s, args.clone()) {
Ok(s) => {
let c = s.get(name!(color))?;
let a = s.get(name!(alpha))?;
if is_special(&c) || is_special(&a) {
if let Ok(c) = Color::try_from(c.clone()) {
let c = c.to_rgba();
Ok(Value::call(
fn_name.as_ref(),
[
Value::scalar(c.red()),
Value::scalar(c.green()),
Value::scalar(c.blue()),
a,
],
))
} else {
Ok(Value::call(fn_name.as_ref(), [c, a]))
}
} else {
let mut c = Color::try_from(c).named(name!(color))?;
c.set_alpha(check_alpha(a).named(name!(alpha))?);
c.reset_source();
Ok(c.into())
}
}
Err(_) => {
let s = eval_inner(fn_name, &a2_show, s, args.clone())
.or_else(|e| {
eval_inner(fn_name, &a2, s, args).map_err(|_| e)
})?;
rgba_from_values(
fn_name,
s.get(name!(red))?,
s.get(name!(green))?,
s.get(name!(blue))?,
s.get(name!(alpha))?,
)
}
},
}
}
fn rgba_from_values(
fn_name: &Name,
r: Value,
g: Value,
b: Value,
a: Value,
) -> Result<Value, CallError> {
if is_special(&r) || is_special(&g) || is_special(&b) || is_special(&a) {
Ok(Value::call(
fn_name.as_ref(),
[r, g, b, a].into_iter().filter(|v| v != &Value::Null),
))
} else {
Ok(Rgba::new(
check_channel(r).named(name!(red))?,
check_channel(g).named(name!(green))?,
check_channel(b).named(name!(blue))?,
check_alpha(a).named(name!(alpha))?,
RgbFormat::Rgb,
)
.into())
}
}
#[cfg(test)]
mod test {
use crate::variablescope::test::do_evaluate;
#[test]
fn test_high() {
assert_eq!(
do_evaluate(&[], b"rgb(150%, 300, 256);"),
"rgb(255, 255, 255)"
);
}
#[test]
fn test_low() {
assert_eq!(do_evaluate(&[], b"rgb(-3, -2%, 0);"), "rgb(0, 0, 0)");
}
}