use super::*;
pub(super) const NAMES: &[&str] = &[
"color-space",
"color-channel",
"color-to-space",
"color-is-legacy",
"color-is-missing",
"color-is-in-gamut",
"color-is-powerless",
"color-to-gamut",
"color-same",
];
pub(super) fn try_call_modern(
name: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Option<Result<Value, Error>> {
Some(match name {
"color-space" => fn_space(pos_args, named, pos),
"color-channel" => fn_color_channel(pos_args, named, pos),
"color-to-space" => fn_to_space(pos_args, named, pos),
"color-is-legacy" => fn_is_legacy(pos_args, named, pos),
"color-is-missing" => fn_is_missing(pos_args, named, pos),
"color-is-in-gamut" => fn_is_in_gamut(pos_args, named, pos),
"color-is-powerless" => fn_is_powerless(pos_args, named, pos),
"color-to-gamut" => fn_to_gamut(pos_args, named, pos),
"color-same" => fn_same(pos_args, named, pos),
_ => return None,
})
}
fn channel_name_arg(v: &Value, pos: Pos) -> Result<String, Error> {
match v {
Value::Str(s) if s.quoted => Ok(s.text.to_string()),
Value::Str(s) => Err(Error::at(
format!("$channel: Expected {} to be a quoted string.", s.text),
pos,
)),
other => Err(Error::at(
format!("$channel: {} is not a string.", other.to_css(false)),
pos,
)),
}
}
fn fn_space(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color"];
max_positional(pos_args, params.len(), pos)?;
let c = as_color(require(¶ms, pos_args, named, 0, "space", pos)?, pos)?;
Ok(Value::Str(crate::value::SassStr {
text: color_space_of(&c).name().to_string().into(),
quoted: false,
}))
}
fn fn_is_legacy(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color"];
max_positional(pos_args, params.len(), pos)?;
let c = as_color(require(¶ms, pos_args, named, 0, "is-legacy", pos)?, pos)?;
Ok(Value::Bool(color_space_of(&c).is_legacy()))
}
fn fn_to_space(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color", "space"];
let n = pos_args.len() + named.len();
if n > 2 {
return Err(Error::at(
format!("Only 2 arguments allowed, but {n} were passed."),
pos,
));
}
let c = as_color(require(¶ms, pos_args, named, 0, "to-space", pos)?, pos)?;
let space = space_arg(require(¶ms, pos_args, named, 1, "to-space", pos)?, pos)?;
let mc = legacy_to_modern(&c);
if mc.space == space {
return Ok(Value::Color(c));
}
let out = convert_to_space(&mc, space);
Ok(Value::Color(make_modern_in(out, space)))
}
fn fn_color_channel(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color", "channel", "space"];
let n = pos_args.len() + named.len();
if n > 3 {
return Err(Error::at(
format!("Only 3 arguments allowed, but {n} were passed."),
pos,
));
}
let c = as_color(require(¶ms, pos_args, named, 0, "channel", pos)?, pos)?;
let chan = channel_name_arg(require(¶ms, pos_args, named, 1, "channel", pos)?, pos)?;
let mc = legacy_to_modern(&c);
let space = match arg(¶ms, pos_args, named, 2) {
Some(v) => space_arg(v, pos)?,
None => mc.space,
};
if chan == "alpha" {
let a = mc.alpha.unwrap_or(0.0);
return Ok(Value::Number(Number::unitless(a)));
}
let target = convert_modern(&mc, space);
let idx = channel_index_in(space, &chan).ok_or_else(|| {
Error::at(
format!("$channel: Color {} has no channel named {chan}.", c.to_css(false)),
pos,
)
})?;
let raw = target.channels[idx].unwrap_or(0.0);
Ok(Value::Number(channel_number(space, idx, raw)))
}
fn fn_is_missing(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color", "channel"];
max_positional(pos_args, params.len(), pos)?;
let c = as_color(require(¶ms, pos_args, named, 0, "is-missing", pos)?, pos)?;
let chan = channel_name_arg(require(¶ms, pos_args, named, 1, "is-missing", pos)?, pos)?;
let mc = legacy_to_modern(&c);
let missing = if chan == "alpha" {
mc.alpha.is_none()
} else {
match channel_index_in(mc.space, &chan) {
Some(idx) => mc.channels[idx].is_none(),
None => {
return Err(Error::at(
format!(
"$channel: Color {} doesn't have a channel named \"{chan}\".",
c.to_css(false)
),
pos,
));
}
}
};
Ok(Value::Bool(missing))
}
fn fn_is_in_gamut(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color", "space"];
let n = pos_args.len() + named.len();
if n > 2 {
return Err(Error::at(
format!("Only 2 arguments allowed, but {n} were passed."),
pos,
));
}
let c = as_color(require(¶ms, pos_args, named, 0, "is-in-gamut", pos)?, pos)?;
let mc = legacy_to_modern(&c);
let space = match arg(¶ms, pos_args, named, 1) {
Some(v) => space_arg(v, pos)?,
None => mc.space,
};
Ok(Value::Bool(in_gamut(&mc, space)))
}
fn fn_is_powerless(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color", "channel", "space"];
let n = pos_args.len() + named.len();
if n > 3 {
return Err(Error::at(
format!("Only 3 arguments allowed, but {n} were passed."),
pos,
));
}
let c = as_color(require(¶ms, pos_args, named, 0, "is-powerless", pos)?, pos)?;
let chan = channel_name_arg(require(¶ms, pos_args, named, 1, "is-powerless", pos)?, pos)?;
let mc = legacy_to_modern(&c);
let space = match arg(¶ms, pos_args, named, 2) {
Some(v) => space_arg(v, pos)?,
None => mc.space,
};
let conv = convert_modern(&mc, space);
if chan == "alpha" {
return Ok(Value::Bool(false));
}
let idx = channel_index_in(space, &chan).ok_or_else(|| {
Error::at(
format!(
"$channel: Color {} doesn't have a channel named \"{chan}\".",
c.to_css(false)
),
pos,
)
})?;
Ok(Value::Bool(channel_powerless(space, idx, &conv)))
}
fn fn_same(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color1", "color2"];
max_positional(pos_args, params.len(), pos)?;
let c1 = as_color(require(¶ms, pos_args, named, 0, "same", pos)?, pos)?;
let c2 = as_color(require(¶ms, pos_args, named, 1, "same", pos)?, pos)?;
let m1 = legacy_to_modern(&c1);
let m2 = legacy_to_modern(&c2);
let fill0 = |m: &ModernColor| ModernColor {
space: m.space,
channels: [
Some(z(m.channels[0])),
Some(z(m.channels[1])),
Some(z(m.channels[2])),
],
alpha: m.alpha,
};
let x1 = convert_modern(&fill0(&m1), ColorSpace::XyzD65);
let x2 = convert_modern(&fill0(&m2), ColorSpace::XyzD65);
let close = |a: f64, b: f64| (a - b).abs() < 1e-7;
let same = close(z(x1.channels[0]), z(x2.channels[0]))
&& close(z(x1.channels[1]), z(x2.channels[1]))
&& close(z(x1.channels[2]), z(x2.channels[2]))
&& close(m1.alpha.unwrap_or(1.0), m2.alpha.unwrap_or(1.0));
Ok(Value::Bool(same))
}
fn fn_to_gamut(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["color", "space", "method"];
max_positional(pos_args, params.len(), pos)?;
let c = as_color(require(¶ms, pos_args, named, 0, "to-gamut", pos)?, pos)?;
let mc = legacy_to_modern(&c);
let space = match arg(¶ms, pos_args, named, 1) {
Some(v) => space_arg(v, pos)?,
None => mc.space,
};
let method = match arg(¶ms, pos_args, named, 2) {
Some(v) => v,
None => {
return Err(Error::at(
"$method: color.to-gamut() requires a $method argument for \
forwards-compatibility with changes in the CSS spec. Suggestion:\n\n\
$method: local-minde"
.to_string(),
pos,
))
}
};
let clip = match method {
Value::Str(s) if !s.quoted && s.text.eq_ignore_ascii_case("clip") => true,
Value::Str(s) if !s.quoted && s.text.eq_ignore_ascii_case("local-minde") => false,
Value::Str(s) if s.quoted => {
return Err(Error::at(
format!("$method: Expected \"{}\" to be an unquoted string.", s.text),
pos,
))
}
other => {
return Err(Error::at(
format!(
"$method: {} must be either clip or local-minde.",
other.to_css(false)
),
pos,
))
}
};
if channel_bounds(space).is_none() {
return Ok(Value::Color(c));
}
let work = convert_modern(&mc, space);
let mapped = if is_in_gamut(&work) {
work
} else if clip {
clip_in_own_space(&work)
} else {
gamut_map(&work)
};
let back = convert_modern_filled(&mapped, mc.space);
Ok(Value::Color(make_modern_in(back, mc.space)))
}
#[derive(Clone, Copy)]
pub(crate) enum ModifyOp {
Change,
Adjust,
Scale,
}
pub(crate) fn modify_in_space(
c: &Color,
space: ColorSpace,
chans: &[(String, &Value)],
op: ModifyOp,
pos: Pos,
) -> Result<Value, Error> {
modify_in_space_full(c, space, chans, op, false, true, pos)
}
pub(crate) fn modify_in_space_opt(
c: &Color,
space: ColorSpace,
chans: &[(String, &Value)],
op: ModifyOp,
powerless_check: bool,
pos: Pos,
) -> Result<Value, Error> {
modify_in_space_full(c, space, chans, op, powerless_check, false, pos)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn modify_in_space_full(
c: &Color,
space: ColorSpace,
chans: &[(String, &Value)],
op: ModifyOp,
powerless_check: bool,
legacy_format: bool,
pos: Pos,
) -> Result<Value, Error> {
let orig = legacy_to_modern(c);
let mut work = convert_modern(&orig, space);
if orig.space != space {
let missing_in_src = |cat: ChannelCategory| {
(0..3).any(|i| orig.channels[i].is_none() && channel_category(orig.space, i) == Some(cat))
};
for i in 0..3 {
if work.channels[i].is_none() {
let authored = channel_category(space, i).is_some_and(&missing_in_src);
if !authored {
work.channels[i] = Some(0.0);
}
}
}
}
let combining = matches!(op, ModifyOp::Adjust | ModifyOp::Scale);
let powerless_active = powerless_check && orig.space != space;
let orig_work = work.clone();
let missing_channel_error = |name: &str| {
let color = if powerless_active {
make_modern(null_powerless(&orig_work, space))
} else {
make_modern(orig_work.clone())
};
missing_channel_err(name, &Value::Color(color), pos)
};
for (name, v) in chans {
if name == "alpha" {
if combining && orig_work.alpha.is_none() {
return Err(missing_channel_error(name));
}
work.alpha = apply_alpha(work.alpha.unwrap_or(1.0), v, op, pos)?;
continue;
}
let idx = channel_index_in(space, name).ok_or_else(|| {
Error::at(
format!(
"${name}: Color space {} doesn't have a channel with this name.",
space.name()
),
pos,
)
})?;
if !matches!(op, ModifyOp::Scale) && !is_none_keyword(v) {
validate_modify_unit(space, idx, name, v, pos)?;
}
let powerless = powerless_active && channel_powerless(space, idx, &orig_work);
if combining && (orig_work.channels[idx].is_none() || powerless) {
return Err(missing_channel_error(name));
}
match op {
ModifyOp::Change => {
if is_none_keyword(v) {
work.channels[idx] = None;
} else {
work.channels[idx] = modify_channel_value(space, idx, v);
}
}
ModifyOp::Adjust => {
let amt = modify_channel_value(space, idx, v).unwrap_or(0.0);
let cur = work.channels[idx].unwrap_or(0.0);
work.channels[idx] = Some(clamp_adjust_channel(space, idx, cur + amt));
}
ModifyOp::Scale => {
let bounds = scale_bounds(space, idx)
.ok_or_else(|| Error::at(format!("${name}: Channel isn't scalable."), pos))?;
let factor = scale_pct(v, pos)?;
let cur = work.channels[idx].unwrap_or(0.0);
work.channels[idx] = Some(scale_to(cur, factor, bounds));
}
}
}
if matches!(op, ModifyOp::Change) && space == ColorSpace::Hwb {
if let (Some(wv), Some(bv)) = (work.channels[1], work.channels[2]) {
if wv + bv > 100.0 {
let t = wv + bv;
work.channels[1] = Some(wv / t * 100.0);
work.channels[2] = Some(bv / t * 100.0);
}
}
}
let dest = if legacy_format && !in_gamut(&work, ColorSpace::Rgb) {
space
} else {
orig.space
};
let back = convert_modern_filled(&work, dest);
Ok(Value::Color(make_modern_in(back, dest)))
}
pub(crate) fn missing_channel_err(name: &str, color: &Value, pos: Pos) -> Error {
Error::at(
format!(
"${name}: Because the CSS working group is still deciding on the best \
behavior, Sass doesn't currently support modifying missing channels \
(color: {}).",
color.to_css(false)
),
pos,
)
}
fn apply_alpha(cur: f64, v: &Value, op: ModifyOp, pos: Pos) -> Result<Option<f64>, Error> {
Ok(match op {
ModifyOp::Change => {
if is_none_keyword(v) {
return Ok(None);
}
match v {
Value::Number(n) => {
let max_disp = if n.unit() == "%" { 100.0 } else { 1.0 };
if n.value < 0.0 || n.value > max_disp {
let (b0, b1) = if n.unit() == "%" {
("0%".to_string(), "100%".to_string())
} else {
(format!("0{}", n.unit()), format!("1{}", n.unit()))
};
return Err(Error::at(
format!("$alpha: Expected {} to be within {b0} and {b1}.", n.to_css(false)),
pos,
));
}
Some(if n.unit() == "%" { n.value / 100.0 } else { n.value })
}
Value::Slash(n, _) => Some(n.value),
other => {
return Err(Error::at(
format!("$alpha: {} is not a number.", other.to_css(false)),
pos,
))
}
}
}
ModifyOp::Adjust => {
let amt = match v {
Value::Number(n) => n.value,
Value::Slash(n, _) => n.value,
other => {
return Err(Error::at(
format!("$alpha: {} is not a number.", other.to_css(false)),
pos,
))
}
};
Some((cur + amt).clamp(0.0, 1.0))
}
ModifyOp::Scale => {
let factor = scale_pct(v, pos)?;
Some(scale_to(cur, factor, (0.0, 1.0)))
}
})
}
pub(crate) fn invert_in_space(
c: &Color,
space: ColorSpace,
weight: f64,
powerless_check: bool,
pos: Pos,
) -> Result<Color, Error> {
let orig = legacy_to_modern(c);
let dest = orig.space;
let src = convert_modern(&orig, space);
let powerless_active = powerless_check && orig.space != space;
for idx in 0..3 {
if !invert_modifies(space, idx) {
continue;
}
let powerless = powerless_active && channel_powerless(space, idx, &src);
if src.channels[idx].is_none() || powerless {
let name = space.channel_names()[idx];
let color = if powerless_active {
make_modern(null_powerless(&src, space))
} else {
make_modern(src.clone())
};
return Err(missing_channel_err(name, &Value::Color(color), pos));
}
}
let inverted = invert_channels(space, &src);
if (weight - 1.0).abs() < 1e-11 {
let back = convert_modern_filled(&inverted, dest);
return Ok(make_modern_in(back, dest));
}
let mix = |a: Option<f64>, b: Option<f64>, hue: bool| -> Option<f64> {
match (a, b) {
(Some(x), Some(y)) => {
if hue {
Some(interpolate_hue(x, y, weight, HueMethod::Shorter))
} else {
Some(x * weight + y * (1.0 - weight))
}
}
_ => a.or(b),
}
};
let hue_idx = match space {
ColorSpace::Hsl | ColorSpace::Hwb => Some(0),
ColorSpace::Lch | ColorSpace::Oklch => Some(2),
_ => None,
};
let channels = [
mix(inverted.channels[0], src.channels[0], hue_idx == Some(0)),
mix(inverted.channels[1], src.channels[1], hue_idx == Some(1)),
mix(inverted.channels[2], src.channels[2], hue_idx == Some(2)),
];
let mc = ModernColor {
space,
channels,
alpha: src.alpha,
};
let back = convert_modern_filled(&mc, dest);
Ok(make_modern_in(back, dest))
}
pub(crate) fn grayscale_modern(c: &Color) -> Color {
let orig = legacy_to_modern(c);
let dest = orig.space;
let mut oklch = convert_modern(&orig, ColorSpace::Oklch);
oklch.channels[1] = Some(0.0);
let back = convert_modern(&oklch, dest);
make_modern_in(back, dest)
}