///|
/// `color-mix()` (CSS Color 5) evaluation. Parses
/// `color-mix(in ,  

?,

?)` and mixes the two colors /// in the requested interpolation color space. Hue interpolation method /// (shorter/longer/...) is not /// yet honored; the shortest arc is always used. ///| /// Split `s` into segments at occurrences of `sep` that sit at parenthesis /// depth 0 (so commas / spaces inside nested color functions are preserved). fn split_depth0(s : String, sep : Char) -> Array[String] { let res : Array[String] = [] let mut depth = 0 let mut start = 0 let mut i = 0 for c in s { if c == '(' { depth = depth + 1 } else if c == ')' { if depth > 0 { depth = depth - 1 } } else if c == sep && depth == 0 { res.push(view_to_string(s.view(start_offset=start, end_offset=i))) start = i + 1 } i = i + 1 } res.push(view_to_string(s.view(start_offset=start))) res } ///| /// Map a `color-mix` interpolation-space spec (`in [ hue]`) to a /// ColorSpace. Returns None when the spec is malformed or the space is unknown. fn parse_interpolation_space(spec : String) -> ColorInterpolationSpace? { let s = spec.trim().to_lower() if !s.has_prefix("in ") { return None } let rest = view_to_string(s.view(start_offset=3)).trim().to_owned() // The first token is the color space; any remainder is the hue method. let tokens = split_depth0(rest, ' ').filter(fn(x) { x.trim().length() > 0 }) if tokens.length() == 0 { return None } match tokens[0].trim().to_owned() { "srgb" => Some(Srgb) "oklab" => Some(Oklab) "oklch" => Some(Oklch) "lab" => Some(Lab) "lch" => Some(Lch) _ => None } } ///| /// Parse one `color-mix` component: ` ?` (in either order). /// Returns the resolved color (None if unresolved) and the optional weight as a /// fraction (30% => 0.3). fn parse_mix_component(part : String) -> (@types.Color?, Double?) { let tokens = split_depth0(part.trim().to_owned(), ' ').filter(fn(x) { x.trim().length() > 0 }) let mut percent : Double? = None let color_tokens : Array[String] = [] for tok in tokens { let t = tok.trim().to_owned() if t.has_suffix("%") { let num = view_to_string( t.view(start_offset=0, end_offset=t.length() - 1), ) percent = Some(parse_number(num) / 100.0) } else { color_tokens.push(t) } } if color_tokens.length() == 0 { return (None, percent) } // Re-join color tokens (e.g. a function whose args were space-separated). let sb = StringBuilder::new() for i = 0; i < color_tokens.length(); i = i + 1 { if i > 0 { sb.write_char(' ') } sb.write_string(color_tokens[i]) } (parse_color_value(sb.to_string()).get_color(), percent) } ///| /// Parse and evaluate a `color-mix()` value. Returns None when it is not a /// color-mix or cannot be resolved (so the caller falls through). fn parse_color_mix(v : String) -> CssColorValue? { if !v.has_prefix("color-mix(") || !v.has_suffix(")") { return None } let inner = view_to_string(v.view(start_offset=10, end_offset=v.length() - 1)) let parts = split_depth0(inner, ',') if parts.length() != 3 { return None } let space = match parse_interpolation_space(parts[0]) { Some(sp) => sp None => return None } let (c1, p1) = parse_mix_component(parts[1]) let (c2, p2) = parse_mix_component(parts[2]) match (c1, c2) { (Some(col1), Some(col2)) => { // Resolve the two weights to fractions summing the mix ratio. let (w1, w2) = match (p1, p2) { (None, None) => (0.5, 0.5) (Some(a), None) => (a, 1.0 - a) (None, Some(b)) => (1.0 - b, b) (Some(a), Some(b)) => (a, b) } let total = w1 + w2 if total <= 0.0 { return None } let t = w2 / total Some(Resolved(interpolate_color_in(space, col1, col2, t))) } _ => None } }