use super::color::{modify_in_space, modify_in_space_opt, space_arg, ModifyOp};
use super::{arg, as_color, clamp01, num, require, require_legacy_color};
use crate::error::Error;
use crate::scanner::Pos;
use crate::value::{Color, ColorSpace, Number, SassStr, Value};
fn modify_with_space(
c: &Color,
named: &[(String, Value)],
op: ModifyOp,
pos: Pos,
) -> Option<Result<Value, Error>> {
let space_arg_v = named.iter().find(|(n, _)| n == "space").map(|(_, v)| v);
let is_legacy = c.modern.as_ref().map(|m| m.space.is_legacy()).unwrap_or(true);
if space_arg_v.is_none() && is_legacy {
return None;
}
let space = match space_arg_v {
Some(v) => match space_arg(v, pos) {
Ok(s) => s,
Err(e) => return Some(Err(e)),
},
None => c.modern.as_ref().map(|m| m.space).unwrap_or(ColorSpace::Rgb),
};
let chans: Vec<(String, &Value)> = named
.iter()
.filter(|(n, _)| n != "space" && n != "color")
.map(|(n, v)| (n.clone(), v))
.collect();
Some(modify_in_space_opt(
c,
space,
&chans,
op,
space_arg_v.is_some(),
pos,
))
}
pub(super) fn computed(r: f64, g: f64, b: f64, a: f64) -> Color {
let mut c = Color::rgb(r, g, b, a);
c.repr = named_repr(r, g, b, a);
c
}
pub(super) fn named_repr(r: f64, g: f64, b: f64, a: f64) -> Option<String> {
if (a - 1.0).abs() >= f64::EPSILON {
return None;
}
let int = |v: f64| {
let r = v.round();
if (v - r).abs() < 1e-9 && (0.0..=255.0).contains(&r) {
Some(r as u16)
} else {
None
}
};
let (r, g, b) = (int(r)?, int(g)?, int(b)?);
let name = match (r, g, b) {
(0, 0, 0) => "black",
(0, 0, 128) => "navy",
(0, 0, 139) => "darkblue",
(0, 0, 205) => "mediumblue",
(0, 0, 255) => "blue",
(0, 100, 0) => "darkgreen",
(0, 128, 0) => "green",
(0, 128, 128) => "teal",
(0, 139, 139) => "darkcyan",
(0, 191, 255) => "deepskyblue",
(0, 206, 209) => "darkturquoise",
(0, 250, 154) => "mediumspringgreen",
(0, 255, 0) => "lime",
(0, 255, 127) => "springgreen",
(0, 255, 255) => "aqua",
(25, 25, 112) => "midnightblue",
(30, 144, 255) => "dodgerblue",
(32, 178, 170) => "lightseagreen",
(34, 139, 34) => "forestgreen",
(46, 139, 87) => "seagreen",
(47, 79, 79) => "darkslategray",
(50, 205, 50) => "limegreen",
(60, 179, 113) => "mediumseagreen",
(64, 224, 208) => "turquoise",
(65, 105, 225) => "royalblue",
(70, 130, 180) => "steelblue",
(72, 61, 139) => "darkslateblue",
(72, 209, 204) => "mediumturquoise",
(75, 0, 130) => "indigo",
(85, 107, 47) => "darkolivegreen",
(95, 158, 160) => "cadetblue",
(100, 149, 237) => "cornflowerblue",
(102, 51, 153) => "rebeccapurple",
(102, 205, 170) => "mediumaquamarine",
(105, 105, 105) => "dimgray",
(106, 90, 205) => "slateblue",
(107, 142, 35) => "olivedrab",
(112, 128, 144) => "slategray",
(119, 136, 153) => "lightslategray",
(123, 104, 238) => "mediumslateblue",
(124, 252, 0) => "lawngreen",
(127, 255, 0) => "chartreuse",
(127, 255, 212) => "aquamarine",
(128, 0, 0) => "maroon",
(128, 0, 128) => "purple",
(128, 128, 0) => "olive",
(128, 128, 128) => "gray",
(135, 206, 235) => "skyblue",
(135, 206, 250) => "lightskyblue",
(138, 43, 226) => "blueviolet",
(139, 0, 0) => "darkred",
(139, 0, 139) => "darkmagenta",
(139, 69, 19) => "saddlebrown",
(143, 188, 143) => "darkseagreen",
(144, 238, 144) => "lightgreen",
(147, 112, 219) => "mediumpurple",
(148, 0, 211) => "darkviolet",
(152, 251, 152) => "palegreen",
(153, 50, 204) => "darkorchid",
(154, 205, 50) => "yellowgreen",
(160, 82, 45) => "sienna",
(165, 42, 42) => "brown",
(169, 169, 169) => "darkgray",
(173, 216, 230) => "lightblue",
(173, 255, 47) => "greenyellow",
(175, 238, 238) => "paleturquoise",
(176, 196, 222) => "lightsteelblue",
(176, 224, 230) => "powderblue",
(178, 34, 34) => "firebrick",
(184, 134, 11) => "darkgoldenrod",
(186, 85, 211) => "mediumorchid",
(188, 143, 143) => "rosybrown",
(189, 183, 107) => "darkkhaki",
(192, 192, 192) => "silver",
(199, 21, 133) => "mediumvioletred",
(205, 92, 92) => "indianred",
(205, 133, 63) => "peru",
(210, 105, 30) => "chocolate",
(210, 180, 140) => "tan",
(211, 211, 211) => "lightgray",
(216, 191, 216) => "thistle",
(218, 112, 214) => "orchid",
(218, 165, 32) => "goldenrod",
(219, 112, 147) => "palevioletred",
(220, 20, 60) => "crimson",
(220, 220, 220) => "gainsboro",
(221, 160, 221) => "plum",
(222, 184, 135) => "burlywood",
(224, 255, 255) => "lightcyan",
(230, 230, 250) => "lavender",
(233, 150, 122) => "darksalmon",
(238, 130, 238) => "violet",
(238, 232, 170) => "palegoldenrod",
(240, 128, 128) => "lightcoral",
(240, 230, 140) => "khaki",
(240, 248, 255) => "aliceblue",
(240, 255, 240) => "honeydew",
(240, 255, 255) => "azure",
(244, 164, 96) => "sandybrown",
(245, 222, 179) => "wheat",
(245, 245, 220) => "beige",
(245, 245, 245) => "whitesmoke",
(245, 255, 250) => "mintcream",
(248, 248, 255) => "ghostwhite",
(250, 128, 114) => "salmon",
(250, 235, 215) => "antiquewhite",
(250, 240, 230) => "linen",
(250, 250, 210) => "lightgoldenrodyellow",
(253, 245, 230) => "oldlace",
(255, 0, 0) => "red",
(255, 0, 255) => "fuchsia",
(255, 20, 147) => "deeppink",
(255, 69, 0) => "orangered",
(255, 99, 71) => "tomato",
(255, 105, 180) => "hotpink",
(255, 127, 80) => "coral",
(255, 140, 0) => "darkorange",
(255, 160, 122) => "lightsalmon",
(255, 165, 0) => "orange",
(255, 182, 193) => "lightpink",
(255, 192, 203) => "pink",
(255, 215, 0) => "gold",
(255, 218, 185) => "peachpuff",
(255, 222, 173) => "navajowhite",
(255, 228, 181) => "moccasin",
(255, 228, 196) => "bisque",
(255, 228, 225) => "mistyrose",
(255, 235, 205) => "blanchedalmond",
(255, 239, 213) => "papayawhip",
(255, 240, 245) => "lavenderblush",
(255, 245, 238) => "seashell",
(255, 248, 220) => "cornsilk",
(255, 250, 205) => "lemonchiffon",
(255, 250, 240) => "floralwhite",
(255, 250, 250) => "snow",
(255, 255, 0) => "yellow",
(255, 255, 224) => "lightyellow",
(255, 255, 240) => "ivory",
(255, 255, 255) => "white",
_ => return None,
};
Some(name.to_string())
}
pub(super) const NAMES: &[&str] = &[
"adjust-hue",
"complement",
"invert",
"grayscale",
"saturate",
"desaturate",
"opacify",
"fade-in",
"transparentize",
"fade-out",
"hue",
"saturation",
"lightness",
"whiteness",
"blackness",
"opacity",
"ie-hex-str",
"scale-color",
"adjust-color",
"change-color",
];
pub(super) fn try_call(
name: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Option<Result<Value, Error>> {
Some(match name {
"adjust-hue" => fn_adjust_hue(pos_args, named, pos),
"complement" => fn_complement(pos_args, named, pos),
"invert" => return fn_invert(pos_args, named, pos),
"grayscale" => return fn_grayscale(pos_args, named, pos),
"saturate" => return fn_saturate(name, pos_args, named, pos, 1.0),
"desaturate" => fn_saturate_two(name, pos_args, named, pos, -1.0),
"opacify" | "fade-in" => fn_fade(name, pos_args, named, pos, 1.0),
"transparentize" | "fade-out" => fn_fade(name, pos_args, named, pos, -1.0),
"hue" | "saturation" | "lightness" | "whiteness" | "blackness" => {
fn_hsl_getter(name, pos_args, named, pos)
}
"opacity" => return fn_opacity(pos_args, named, pos),
"ie-hex-str" => fn_ie_hex_str(pos_args, named, pos),
"scale-color" => fn_scale_color(pos_args, named, pos),
"adjust-color" => fn_adjust_color(pos_args, named, pos),
"change-color" => fn_change_color(pos_args, named, pos),
_ => return None,
})
}
fn check_max_args(pos_args: &[Value], named: &[(String, Value)], max: usize, pos: Pos) -> Result<(), Error> {
let n = pos_args.len() + named.len();
if n > max {
let plural = if max == 1 { "argument" } else { "arguments" };
Err(Error::at(
format!("Only {max} {plural} allowed, but {n} were passed."),
pos,
))
} else {
Ok(())
}
}
fn is_css_special(v: &Value) -> bool {
match v {
Value::Number(_) | Value::Calc(_) | Value::Slash(_, _) => true,
Value::Str(s) => !s.quoted && s.text.contains('('),
_ => false,
}
}
fn plain_filter(name: &str, arg: &Value) -> Value {
Value::Str(SassStr {
text: format!("{name}({})", arg.to_css(false)).into(),
quoted: false,
})
}
fn fn_adjust_hue(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color", "degrees"];
check_max_args(pos_args, named, 2, pos)?;
let c = as_color(require(¶ms, pos_args, named, 0, "adjust-hue", pos)?, pos)?;
require_legacy_color(&c, "adjust-hue", pos)?;
let degrees = angle_degrees(require(¶ms, pos_args, named, 1, "adjust-hue", pos)?, pos)?;
Ok(Value::Color(rotate_hue(&c, degrees)))
}
fn angle_degrees(v: &Value, pos: Pos) -> Result<f64, Error> {
match v {
Value::Number(n) => Ok(match n.unit() {
"rad" => n.value.to_degrees(),
"grad" => n.value * 360.0 / 400.0,
"turn" => n.value * 360.0,
_ => n.value,
}),
other => Err(Error::at(
format!("{} is not a number.", other.to_css(false)),
pos,
)),
}
}
fn fn_complement(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color", "space"];
check_max_args(pos_args, named, 2, pos)?;
let c = as_color(require(¶ms, pos_args, named, 0, "complement", pos)?, pos)?;
let space_v = arg(¶ms, pos_args, named, 1);
let is_legacy = c.modern.as_ref().map(|m| m.space.is_legacy()).unwrap_or(true);
let space = match space_v {
Some(v) => space_arg(v, pos)?,
None if is_legacy => ColorSpace::Hsl,
None => {
return Err(Error::at(
format!(
"$space: To use color.complement() with non-legacy color {}, you must provide a $space.",
c.to_css(false)
),
pos,
))
}
};
if !matches!(
space,
ColorSpace::Hsl | ColorSpace::Hwb | ColorSpace::Lch | ColorSpace::Oklch
) {
return Err(Error::at(
format!("$space: Color space {} doesn't have a hue channel.", space.name()),
pos,
));
}
let deg = Value::Number(Number::with_unit(180.0, "deg".to_string()));
modify_in_space_opt(
&c,
space,
&[("hue".to_string(), °)],
ModifyOp::Adjust,
space_v.is_some(),
pos,
)
}
fn rotate_hue(c: &Color, degrees: f64) -> Color {
let (h, s, l) = c.to_hsl();
from_hsl(h + degrees, s, l, c.a)
}
fn from_hsl(h: f64, s: f64, l: f64, a: f64) -> Color {
let c = Color::from_hsl(h, s, l, a);
let mut c = c;
c.repr = named_repr(c.r, c.g, c.b, c.a);
c
}
fn fn_invert(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Option<Result<Value, Error>> {
let params = ["color", "weight"];
if let Err(e) = check_max_args(pos_args, named, 3, pos) {
return Some(Err(e));
}
let color = match require(¶ms, pos_args, named, 0, "invert", pos) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
if is_css_special(color) {
if pos_args.len() + named.len() > 1 {
return Some(Err(Error::at(
"Only one argument may be passed to the plain-CSS invert() function.".to_string(),
pos,
)));
}
return Some(Ok(plain_filter("invert", color)));
}
let params = ["color", "weight", "space"];
Some((|| {
let c = as_color(color, pos)?;
let weight = match arg(¶ms, pos_args, named, 1) {
Some(v) => {
let n = num(v, pos)?;
if !(0.0..=100.0).contains(&n) {
return Err(Error::at(
format!("$weight: Expected {} to be within 0% and 100%.", v.to_css(false)),
pos,
));
}
n
}
None => 100.0,
};
let w = (weight / 100.0).clamp(0.0, 1.0);
let space_v = arg(¶ms, pos_args, named, 2);
let is_legacy = c.modern.as_ref().map(|m| m.space.is_legacy()).unwrap_or(true);
if space_v.is_some() || !is_legacy {
let space = match space_v {
Some(v) => space_arg(v, pos)?,
None => {
return Err(Error::at(
format!(
"$color: To use color.invert() with non-legacy color {}, you must provide a $space.",
c.to_css(false)
),
pos,
))
}
};
return Ok(Value::Color(super::color::invert_in_space(
&c,
space,
w,
space_v.is_some(),
pos,
)?));
}
Ok(Value::Color(super::color::invert_in_space(
&c,
ColorSpace::Rgb,
w,
false,
pos,
)?))
})())
}
fn fn_grayscale(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Option<Result<Value, Error>> {
let params = ["color"];
if let Err(e) = check_max_args(pos_args, named, 1, pos) {
return Some(Err(e));
}
let color = match require(¶ms, pos_args, named, 0, "grayscale", pos) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
if is_css_special(color) {
return Some(Ok(plain_filter("grayscale", color)));
}
Some((|| {
let c = as_color(color, pos)?;
let is_legacy = c.modern.as_ref().map(|m| m.space.is_legacy()).unwrap_or(true);
if !is_legacy {
return Ok(Value::Color(super::color::grayscale_modern(&c)));
}
let zero = Value::Number(crate::value::Number::with_unit(0.0, "%"));
let chans: Vec<(String, &Value)> = vec![("saturation".to_string(), &zero)];
super::color::modify_in_space_full(
&c,
ColorSpace::Hsl,
&chans,
super::color::ModifyOp::Change,
false,
false,
pos,
)
})())
}
fn fn_saturate(
name: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
sign: f64,
) -> Option<Result<Value, Error>> {
if pos_args.len() + named.len() == 1 {
let arg0 = pos_args
.first()
.or_else(|| named.iter().find(|(n, _)| n == "amount").map(|(_, v)| v));
if let Some(v) = arg0 {
if is_css_special(v) {
return Some(Ok(plain_filter("saturate", v)));
}
}
}
Some(fn_saturate_two(name, pos_args, named, pos, sign))
}
fn fn_saturate_two(
name: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
sign: f64,
) -> Result<Value, Error> {
let params = ["color", "amount"];
check_max_args(pos_args, named, 2, pos)?;
let c = as_color(require(¶ms, pos_args, named, 0, name, pos)?, pos)?;
require_legacy_color(&c, name, pos)?;
let amount = require(¶ms, pos_args, named, 1, name, pos)?;
let amount = bounded(amount, 0.0, 100.0, pos)?;
let (h, s, l) = c.to_hsl();
let new_s = (s + sign * amount / 100.0).clamp(0.0, 1.0);
Ok(Value::Color(from_hsl(h, new_s, l, c.a)))
}
fn bounded(v: &Value, lo: f64, hi: f64, pos: Pos) -> Result<f64, Error> {
match v {
Value::Number(n) => {
if n.value < lo || n.value > hi {
Err(Error::at(
format!(
"$amount: Expected {} to be within {} and {}.",
n.to_css(false),
fmt_bound(lo),
fmt_bound(hi),
),
pos,
))
} else {
Ok(n.value)
}
}
other => Err(Error::at(
format!("$amount: {} is not a number.", other.to_css(false)),
pos,
)),
}
}
fn fmt_bound(v: f64) -> String {
if v.fract() == 0.0 {
format!("{}", v as i64)
} else {
format!("{v}")
}
}
fn fn_fade(
name: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
sign: f64,
) -> Result<Value, Error> {
let params = ["color", "amount"];
check_max_args(pos_args, named, 2, pos)?;
let c = as_color(require(¶ms, pos_args, named, 0, name, pos)?, pos)?;
require_legacy_color(&c, name, pos)?;
let amount = require(¶ms, pos_args, named, 1, name, pos)?;
let amount = bounded(amount, 0.0, 1.0, pos)?;
let a = clamp01(c.a + sign * amount);
Ok(Value::Color(computed(c.r, c.g, c.b, a)))
}
fn fn_hsl_getter(
name: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Result<Value, Error> {
let params = ["color"];
check_max_args(pos_args, named, 1, pos)?;
let c = as_color(require(¶ms, pos_args, named, 0, name, pos)?, pos)?;
let is_legacy = c.modern.as_ref().map(|m| m.space.is_legacy()).unwrap_or(true);
if !is_legacy {
let space = if matches!(name, "whiteness" | "blackness") {
"hwb"
} else {
"hsl"
};
return Err(Error::at(
format!(
"color.{name}() is only supported for legacy colors. Please use color.channel() \
instead with an explicit $space argument.\n\n\
color.channel($color, \"{name}\", $space: {space})"
),
pos,
));
}
if let Some(m) = &c.modern {
let idx = match (m.space, name) {
(crate::value::ColorSpace::Hsl, "hue") | (crate::value::ColorSpace::Hwb, "hue") => {
Some((0, "deg"))
}
(crate::value::ColorSpace::Hsl, "saturation") => Some((1, "%")),
(crate::value::ColorSpace::Hsl, "lightness") => Some((2, "%")),
(crate::value::ColorSpace::Hwb, "whiteness") => Some((1, "%")),
(crate::value::ColorSpace::Hwb, "blackness") => Some((2, "%")),
_ => None,
};
if let Some((i, unit)) = idx {
return Ok(Value::Number(Number::with_unit(
m.channels[i].unwrap_or(0.0),
unit.to_string(),
)));
}
}
let (h, s, l) = c.to_hsl();
let (value, unit) = match name {
"hue" => (h, "deg"),
"saturation" => (s * 100.0, "%"),
"lightness" => (l * 100.0, "%"),
"whiteness" => (c.r.min(c.g).min(c.b) / 255.0 * 100.0, "%"),
_ => ((1.0 - c.r.max(c.g).max(c.b) / 255.0) * 100.0, "%"),
};
Ok(Value::Number(Number::with_unit(value, unit.to_string())))
}
fn fn_opacity(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Option<Result<Value, Error>> {
let params = ["color"];
if let Err(e) = check_max_args(pos_args, named, 1, pos) {
return Some(Err(e));
}
let color = match require(¶ms, pos_args, named, 0, "opacity", pos) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
if is_css_special(color) {
return Some(Ok(plain_filter("opacity", color)));
}
Some((|| {
let c = as_color(color, pos)?;
Ok(Value::Number(Number::unitless(c.a)))
})())
}
fn fn_ie_hex_str(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color"];
check_max_args(pos_args, named, 1, pos)?;
let c = as_color(require(¶ms, pos_args, named, 0, "ie-hex-str", pos)?, pos)?;
let byte = |v: f64| v.round().clamp(0.0, 255.0) as u8;
let text = format!(
"#{:02X}{:02X}{:02X}{:02X}",
byte(c.a * 255.0),
byte(c.r),
byte(c.g),
byte(c.b)
);
Ok(Value::Str(SassStr {
text: text.into(),
quoted: false,
}))
}
#[derive(Clone, Copy, PartialEq)]
enum Space {
Rgb,
Hsl,
Hwb,
}
impl Space {
fn name(self) -> &'static str {
match self {
Space::Rgb => "rgb",
Space::Hsl => "hsl",
Space::Hwb => "hwb",
}
}
}
type ChannelArg<'v> = (&'v str, &'v Value);
fn channel_space(name: &str) -> Option<(Option<Space>, bool)> {
Some(match name {
"red" | "green" | "blue" => (Some(Space::Rgb), false),
"saturation" | "lightness" => (Some(Space::Hsl), false),
"whiteness" | "blackness" => (Some(Space::Hwb), false),
"hue" => (None, true),
"alpha" => return Some((None, false)),
_ => return None,
})
}
fn resolve_channels<'v>(
_fname: &str,
named: &'v [(String, Value)],
pos: Pos,
) -> Result<(Space, Vec<ChannelArg<'v>>), Error> {
let chans: Vec<ChannelArg<'v>> = named
.iter()
.filter(|(n, _)| n != "color")
.map(|(n, v)| (n.as_str(), v))
.collect();
let mut space: Option<Space> = None;
let mut has_hue = false;
for (n, _) in &chans {
match channel_space(n) {
Some((Some(s), _)) => {
space = Some(s);
break;
}
Some((None, true)) => has_hue = true,
_ => {}
}
}
let space = space.unwrap_or(if has_hue { Space::Hsl } else { Space::Rgb });
for (n, _) in &chans {
if *n == "alpha" {
continue;
}
let ok = match channel_space(n) {
Some((Some(s), _)) => s == space,
Some((None, true)) => space != Space::Rgb, _ => false,
};
if !ok {
return Err(Error::at(
format!(
"${n}: Color space {} doesn't have a channel with this name.",
space.name()
),
pos,
));
}
}
Ok((space, chans))
}
fn fn_adjust_color(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color"];
let c = as_color(require(¶ms, pos_args, named, 0, "adjust-color", pos)?, pos)?;
if pos_args.len() > 1 {
return Err(Error::at(
"Only one positional argument is allowed. All other arguments must be passed by name."
.to_string(),
pos,
));
}
if let Some(r) = modify_with_space(&c, named, super::color::ModifyOp::Adjust, pos) {
return r;
}
let (space, chans) = resolve_channels("adjust-color", named, pos)?;
let cspace = match space {
Space::Rgb => ColorSpace::Rgb,
Space::Hsl => ColorSpace::Hsl,
Space::Hwb => ColorSpace::Hwb,
};
let chan_args: Vec<(String, &Value)> = chans.iter().map(|(n, v)| (n.to_string(), *v)).collect();
modify_in_space(&c, cspace, &chan_args, super::color::ModifyOp::Adjust, pos)
}
fn fn_change_color(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color"];
let c = as_color(require(¶ms, pos_args, named, 0, "change-color", pos)?, pos)?;
if pos_args.len() > 1 {
return Err(Error::at(
"Only one positional argument is allowed. All other arguments must be passed by name."
.to_string(),
pos,
));
}
if let Some(r) = modify_with_space(&c, named, super::color::ModifyOp::Change, pos) {
return r;
}
let (space, chans) = resolve_channels("change-color", named, pos)?;
let cspace = match space {
Space::Rgb => ColorSpace::Rgb,
Space::Hsl => ColorSpace::Hsl,
Space::Hwb => ColorSpace::Hwb,
};
let chan_args: Vec<(String, &Value)> = chans.iter().map(|(n, v)| (n.to_string(), *v)).collect();
modify_in_space(&c, cspace, &chan_args, super::color::ModifyOp::Change, pos)
}
fn fn_scale_color(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color"];
let c = as_color(require(¶ms, pos_args, named, 0, "scale-color", pos)?, pos)?;
if pos_args.len() > 1 {
return Err(Error::at(
"Only one positional argument is allowed. All other arguments must be passed by name."
.to_string(),
pos,
));
}
if let Some(r) = modify_with_space(&c, named, super::color::ModifyOp::Scale, pos) {
return r;
}
let (space, chans) = resolve_channels("scale-color", named, pos)?;
let cspace = match space {
Space::Rgb => ColorSpace::Rgb,
Space::Hsl => ColorSpace::Hsl,
Space::Hwb => ColorSpace::Hwb,
};
{
let mc = super::color::legacy_to_modern(&c);
if mc.channels.iter().any(Option::is_none) && chans.iter().all(|(n, _)| *n == "alpha") {
let mut out = mc;
for (_, v) in &chans {
let factor = scale_factor("alpha", v, pos)?;
let a = out.alpha.unwrap_or(1.0);
out.alpha = Some(scale_toward(a, factor, 1.0));
}
let space = out.space;
return Ok(Value::Color(super::color::make_modern_in(out, space)));
}
}
let chan_args: Vec<(String, &Value)> = chans.iter().map(|(n, v)| (n.to_string(), *v)).collect();
modify_in_space(&c, cspace, &chan_args, super::color::ModifyOp::Scale, pos)
}
fn scale_toward(current: f64, factor: f64, max: f64) -> f64 {
if factor > 0.0 {
current + (max - current) * factor
} else {
current + current * factor
}
}
fn scale_factor(name: &str, v: &Value, pos: Pos) -> Result<f64, Error> {
match v {
Value::Number(n) => {
if n.unit() != "%" {
return Err(Error::at(
format!("${name}: Expected {} to have unit \"%\".", n.to_css(false)),
pos,
));
}
if n.value < -100.0 || n.value > 100.0 {
return Err(Error::at(
format!(
"${name}: Expected {} to be within -100% and 100%.",
n.to_css(false)
),
pos,
));
}
Ok(n.value / 100.0)
}
other => Err(Error::at(
format!("${name}: {} is not a number.", other.to_css(false)),
pos,
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::Color;
fn pos() -> Pos {
Pos { line: 1, col: 1 }
}
fn col(s: &str) -> Value {
let digits = s.strip_prefix('#').unwrap_or(s);
Value::Color(Color::from_hex(digits).expect("valid hex"))
}
fn n(v: f64, unit: &str) -> Value {
Value::Number(Number::with_unit(v, unit.to_string()))
}
fn css(name: &str, args: &[Value]) -> String {
try_call(name, args, &[], pos())
.expect("name owned by color_ext family")
.expect("no error")
.to_css(false)
}
#[test]
fn adjust_hue_and_complement() {
assert_eq!(css("adjust-hue", &[col("#6b717f"), n(60.0, "deg")]), "#796b7f");
assert_eq!(css("adjust-hue", &[col("#6b717f"), n(-60.0, "deg")]), "#6b7f79");
assert_eq!(css("complement", &[col("#6b717f")]), "#7f796b");
}
#[test]
fn invert_full_and_weighted() {
assert_eq!(css("invert", &[col("#b37399")]), "#4c8c66");
assert_eq!(
css("invert", &[col("#b37399"), n(80.0, "%")]),
"rgb(96.6, 135, 112.2)"
);
assert_eq!(
css("invert", &[col("#b37399"), n(50.0, "%")]),
"rgb(127.5, 127.5, 127.5)"
);
}
#[test]
fn grayscale_saturate_desaturate() {
assert_eq!(css("grayscale", &[col("#6b717f")]), "#757575");
assert_eq!(
css("saturate", &[col("#cc6699"), n(30.0, "%")]),
"rgb(234.6, 71.4, 153)"
);
assert_eq!(css("desaturate", &[col("#6b717f"), n(20.0, "%")]), "#757575");
}
#[test]
fn alpha_shifts() {
let rgba = Value::Color(Color::rgb(0.0, 51.0, 102.0, 0.7));
assert_eq!(
css("opacify", &[rgba.clone(), n(0.2, "")]),
"rgba(0, 51, 102, 0.9)"
);
assert_eq!(
css("fade-in", &[rgba.clone(), n(0.2, "")]),
"rgba(0, 51, 102, 0.9)"
);
assert_eq!(
css("transparentize", &[rgba.clone(), n(0.2, "")]),
"rgba(0, 51, 102, 0.5)"
);
assert_eq!(css("opacify", &[rgba, n(0.5, "")]), "#003366");
}
#[test]
fn hsl_getters_units() {
assert_eq!(css("hue", &[col("#6b717f")]), "222deg");
assert_eq!(css("saturation", &[col("#6b717f")]), "8.547008547%");
assert_eq!(css("lightness", &[col("#6b717f")]), "45.8823529412%");
}
#[test]
fn unowned_returns_none() {
assert!(try_call("not-a-color-fn", &[], &[], pos()).is_none());
}
}