pub(crate) mod deprecate;
mod legacy;
mod math;
mod modern;
use super::color_ext::{computed, named_repr};
use super::{arg, as_color, channel, check_arity, num, require, require_legacy_color};
use crate::error::Error;
use crate::scanner::Pos;
use crate::value::{fmt_num, CalcNode, Color, ColorSpace, List, ListSep, ModernColor, Number, Value};
use legacy::*;
use math::*;
use modern::*;
pub(crate) use legacy::call_module_member;
pub(crate) use math::{
convert_modern, legacy_alpha_adjust, legacy_hsl_adjust, legacy_to_modern, make_modern_in, space_arg,
stored_alpha,
};
pub(crate) use modern::{
grayscale_modern, invert_in_space, missing_channel_err, modify_in_space, modify_in_space_full,
modify_in_space_opt, ModifyOp,
};
fn is_special(v: &Value) -> bool {
match v {
Value::Calc(_) => true,
Value::Str(s) => !s.quoted && s.text.contains('('),
_ => false,
}
}
fn is_var(v: &Value) -> bool {
matches!(v, Value::Str(s) if !s.quoted && {
let t = s.text.trim_start();
t.len() >= 4 && t[..4].eq_ignore_ascii_case("var(")
})
}
fn is_special_legacy(v: &Value) -> bool {
match v {
Value::Calc(node) => degenerate_const(node).is_none(),
other => is_special(other),
}
}
fn degenerate_const(node: &CalcNode) -> Option<f64> {
if let CalcNode::Str(s) = node {
return match s.trim().to_ascii_lowercase().as_str() {
"infinity" => Some(f64::INFINITY),
"-infinity" => Some(f64::NEG_INFINITY),
"nan" => Some(f64::NAN),
_ => None,
};
}
None
}
pub(super) fn is_none_keyword(v: &Value) -> bool {
matches!(v, Value::Str(s) if !s.quoted && s.text.eq_ignore_ascii_case("none"))
}
fn special_call(name: &str, args: &[&Value]) -> Value {
let parts: Vec<String> = args.iter().map(|v| v.to_css(false)).collect();
Value::Str(crate::value::SassStr {
text: format!("{name}({})", parts.join(", ")).into(),
quoted: false,
})
}
fn verbatim_call(name: &str, channels: &Value) -> Value {
Value::Str(crate::value::SassStr {
text: format!("{name}({})", channels.to_css(false)).into(),
quoted: false,
})
}
fn legacy_channel_name(names: &[&str], i: usize) -> String {
match names.get(i) {
Some(name) => format!("{name} channel"),
None => format!("channel {}", i + 1),
}
}
pub(super) const MODERN_NAMES: &[&str] = modern::NAMES;
pub(super) const NAMES: &[&str] = &[
"rgb",
"rgba",
"hsl",
"hsla",
"hwb",
"lab",
"lch",
"oklab",
"oklch",
"color",
"mix",
"lighten",
"darken",
"percentage",
"red",
"green",
"blue",
"alpha",
];
pub(super) fn try_call(
name: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Option<Result<Value, Error>> {
Some(match name {
"rgb" | "rgba" => fn_rgb(name, pos_args, named, pos),
"hsl" | "hsla" => fn_hsl(name, pos_args, named, pos),
"hwb" => fn_hwb(pos_args, named, pos),
"lab" | "lch" | "oklab" | "oklch" => fn_lab_family(name, pos_args, named, pos),
"color" => fn_color(pos_args, named, pos),
"mix" => fn_mix(pos_args, named, pos),
"lighten" => fn_adjust_lightness(name, pos_args, named, pos, 1.0),
"darken" => fn_adjust_lightness(name, pos_args, named, pos, -1.0),
"percentage" => fn_percentage(pos_args, named, pos),
"red" | "green" | "blue" => fn_channel(name, pos_args, named, pos),
"alpha" => fn_alpha(pos_args, named, pos),
_ => return try_call_modern(name, pos_args, named, pos),
})
}
fn alpha_value(v: &Value, pos: Pos) -> Result<f64, Error> {
if let Some(c) = degenerate_value(v) {
let pct = match channel_unit_number(v) {
Some(n) if !n.has_complex_units() && n.unit() == "%" => true,
Some(n) if !n.is_unitless() => {
return Err(Error::at(
format!(
"$alpha: Expected {} to have unit \"%\" or no units.",
v.to_css(false)
),
pos,
))
}
_ => false,
};
return Ok(clamp_alpha(if pct { c / 100.0 } else { c }));
}
match v {
Value::Number(num) | Value::Slash(num, _) => {
let raw = if !num.has_complex_units() && num.unit() == "%" {
num.value / 100.0
} else if num.is_unitless() {
num.value
} else {
return Err(Error::at(
format!(
"$alpha: Expected {} to have unit \"%\" or no units.",
num.to_css(false)
),
pos,
));
};
Ok(clamp_alpha(raw))
}
other => Err(Error::at(
format!("$alpha: {} is not a number.", channel_err_css(other)),
pos,
)),
}
}
fn clamp_alpha(v: f64) -> f64 {
if v.is_nan() {
0.0
} else {
crate::value::without_negative_zero(v.clamp(0.0, 1.0))
}
}
fn is_degenerate_calc(v: &Value) -> bool {
degenerate_value(v).is_some()
}
fn degenerate_value(v: &Value) -> Option<f64> {
match v {
Value::Number(n) | Value::Slash(n, _) if !n.value.is_finite() => Some(n.value),
Value::Calc(node) => match node {
CalcNode::Number(n) if !n.value.is_finite() => Some(n.value),
_ => degenerate_const(node),
},
_ => None,
}
}
fn channel_unit_number(v: &Value) -> Option<&Number> {
match v {
Value::Number(n) | Value::Slash(n, _) | Value::Calc(CalcNode::Number(n)) => Some(n),
_ => None,
}
}
fn normalize_channel(v: &Value, polar_hue: bool) -> Value {
let num = channel_unit_number(v);
let converts = match degenerate_value(v) {
Some(c) => c.is_nan() || (polar_hue && c.is_infinite()),
None => num.is_some_and(|n| n.value == 0.0 && n.value.is_sign_negative()),
};
if !converts {
return v.clone();
}
let zero = match num {
Some(n) => n.copy_units(0.0),
None => Number::unitless(0.0),
};
Value::Number(zero)
}
fn normalize_channels(comps: &[Value], polar_hue: Option<usize>) -> Vec<Value> {
comps
.iter()
.enumerate()
.map(|(i, v)| normalize_channel(v, polar_hue == Some(i)))
.collect()
}
fn validate_alpha_unit(alpha: Option<&Value>, pos: Pos) -> Result<(), Error> {
let Some(a) = alpha else { return Ok(()) };
if let Some(n) = channel_unit_number(a) {
if !n.is_unitless() && (n.has_complex_units() || n.unit() != "%") {
return Err(Error::at(
format!(
"$alpha: Expected {} to have unit \"%\" or no units.",
a.to_css(false)
),
pos,
));
}
}
Ok(())
}
fn channel_err_css(v: &Value) -> String {
match v {
Value::List(l) if l.items.len() > 1 && !l.bracketed => list_paren_css(v),
_ => v.to_css(false),
}
}
fn list_paren_css(v: &Value) -> String {
format!("({})", v.to_css(false))
}