1use csscolorparser::Color as CssColor;
2use ls_types::{Color, ColorPresentation, Range, TextEdit};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
5pub struct NormalizedColorKey {
6 pub red: u8,
7 pub green: u8,
8 pub blue: u8,
9 pub alpha: u8,
10}
11
12pub fn parse_color(value: &str) -> Option<Color> {
14 parse_csscolorparser(value.trim())
15}
16
17pub fn normalized_color_key(value: &str) -> Option<NormalizedColorKey> {
18 parse_color(value).map(normalize_color)
19}
20
21pub fn normalize_color(color: Color) -> NormalizedColorKey {
22 NormalizedColorKey {
23 red: color_channel_to_u8(color.red),
24 green: color_channel_to_u8(color.green),
25 blue: color_channel_to_u8(color.blue),
26 alpha: color_channel_to_u8(color.alpha),
27 }
28}
29
30pub fn color_from_key(key: NormalizedColorKey) -> Color {
31 Color {
32 red: key.red as f32 / 255.0,
33 green: key.green as f32 / 255.0,
34 blue: key.blue as f32 / 255.0,
35 alpha: key.alpha as f32 / 255.0,
36 }
37}
38
39fn strip_ascii_suffix_case_insensitive<'a>(value: &'a str, suffix: &str) -> Option<&'a str> {
40 let start = value.len().checked_sub(suffix.len())?;
41 value
42 .get(start..)
43 .filter(|tail| tail.eq_ignore_ascii_case(suffix))
44 .map(|_| &value[..start])
45}
46
47fn contains_nonfinite_numeric_token(value: &str) -> bool {
48 for raw_token in value.split(|character: char| {
49 character == '('
50 || character == ')'
51 || character == ','
52 || character == '/'
53 || character.is_ascii_whitespace()
54 }) {
55 let token = raw_token.trim();
56 if token.is_empty() {
57 continue;
58 }
59
60 let numeric = strip_ascii_suffix_case_insensitive(token, "%")
64 .or_else(|| strip_ascii_suffix_case_insensitive(token, "deg"))
65 .or_else(|| strip_ascii_suffix_case_insensitive(token, "grad"))
66 .or_else(|| strip_ascii_suffix_case_insensitive(token, "rad"))
67 .or_else(|| strip_ascii_suffix_case_insensitive(token, "turn"))
68 .unwrap_or(token);
69
70 let unsigned = numeric
71 .strip_prefix('+')
72 .or_else(|| numeric.strip_prefix('-'))
73 .unwrap_or(numeric);
74 if unsigned.eq_ignore_ascii_case("nan")
75 || unsigned.eq_ignore_ascii_case("inf")
76 || unsigned.eq_ignore_ascii_case("infinity")
77 {
78 return true;
79 }
80
81 if let Ok(number) = numeric.parse::<f64>() {
82 if !number.is_finite() {
83 return true;
84 }
85 }
86 }
87
88 false
89}
90
91fn parse_csscolorparser(value: &str) -> Option<Color> {
92 if contains_nonfinite_numeric_token(value) {
93 return None;
94 }
95
96 let parsed: CssColor = value.parse().ok()?;
97
98 let channels = [parsed.r, parsed.g, parsed.b, parsed.a];
104 if channels.iter().any(|channel| !channel.is_finite()) {
105 return None;
106 }
107
108 Some(Color {
109 red: parsed.r.clamp(0.0, 1.0) as f32,
110 green: parsed.g.clamp(0.0, 1.0) as f32,
111 blue: parsed.b.clamp(0.0, 1.0) as f32,
112 alpha: parsed.a.clamp(0.0, 1.0) as f32,
113 })
114}
115
116pub fn generate_color_presentations(color: Color, range: Range) -> Vec<ColorPresentation> {
118 let mut presentations = Vec::new();
119
120 let hex_str = format_color_as_hex(color);
121 presentations.push(ColorPresentation {
122 label: hex_str.clone(),
123 text_edit: Some(TextEdit {
124 range,
125 new_text: hex_str,
126 }),
127 additional_text_edits: None,
128 });
129
130 let rgb_str = format_color_as_rgb(color);
131 presentations.push(ColorPresentation {
132 label: rgb_str.clone(),
133 text_edit: Some(TextEdit {
134 range,
135 new_text: rgb_str,
136 }),
137 additional_text_edits: None,
138 });
139
140 let hsl_str = format_color_as_hsl(color);
141 presentations.push(ColorPresentation {
142 label: hsl_str.clone(),
143 text_edit: Some(TextEdit {
144 range,
145 new_text: hsl_str,
146 }),
147 additional_text_edits: None,
148 });
149
150 presentations
151}
152
153pub fn format_color_as_hex(color: Color) -> String {
154 let r = (color.red.clamp(0.0, 1.0) * 255.0).round() as u8;
155 let g = (color.green.clamp(0.0, 1.0) * 255.0).round() as u8;
156 let b = (color.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
157 let a = (color.alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
158
159 if a == 255 {
160 format!("#{:02x}{:02x}{:02x}", r, g, b)
161 } else {
162 format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a)
163 }
164}
165
166pub fn format_color_as_rgb(color: Color) -> String {
167 let r = (color.red.clamp(0.0, 1.0) * 255.0).round() as u8;
168 let g = (color.green.clamp(0.0, 1.0) * 255.0).round() as u8;
169 let b = (color.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
170 let a = color.alpha.clamp(0.0, 1.0);
171
172 if a >= 1.0 {
173 format!("rgb({}, {}, {})", r, g, b)
174 } else {
175 format!("rgba({}, {}, {}, {:.2})", r, g, b, a)
176 }
177}
178
179pub fn format_color_as_hsl(color: Color) -> String {
180 let (h, s, l) = rgb_to_hsl(color.red, color.green, color.blue);
181 let a = color.alpha.clamp(0.0, 1.0);
182
183 let h_deg = (h * 360.0).round();
184 let s_pct = (s * 100.0).round();
185 let l_pct = (l * 100.0).round();
186
187 if a >= 1.0 {
188 format!("hsl({}, {}%, {}%)", h_deg, s_pct, l_pct)
189 } else {
190 format!("hsla({}, {}%, {}%, {:.2})", h_deg, s_pct, l_pct, a)
191 }
192}
193
194fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
195 let max = r.max(g).max(b);
196 let min = r.min(g).min(b);
197 let l = (max + min) / 2.0;
198
199 if max == min {
200 return (0.0, 0.0, l);
201 }
202
203 let d = max - min;
204 let s = if l > 0.5 {
205 d / (2.0 - max - min)
206 } else {
207 d / (max + min)
208 };
209
210 let h = if max == r {
211 ((g - b) / d + if g < b { 6.0 } else { 0.0 }) / 6.0
212 } else if max == g {
213 ((b - r) / d + 2.0) / 6.0
214 } else {
215 ((r - g) / d + 4.0) / 6.0
216 };
217
218 (h, s, l)
219}
220
221fn color_channel_to_u8(channel: f32) -> u8 {
222 (channel.clamp(0.0, 1.0) * 255.0).round() as u8
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use ls_types::Position;
229
230 fn approx_eq(a: f32, b: f32) -> bool {
231 (a - b).abs() < 0.01
232 }
233
234 #[test]
235 fn parse_color_hex_and_named() {
236 let color = parse_color("#abc").expect("hex");
237 assert!(approx_eq(color.red, 0xAA as f32 / 255.0));
238 assert!(approx_eq(color.green, 0xBB as f32 / 255.0));
239 assert!(approx_eq(color.blue, 0xCC as f32 / 255.0));
240 assert!(approx_eq(color.alpha, 1.0));
241
242 let color = parse_color("#abcd").expect("hex with alpha");
243 assert!(approx_eq(color.red, 0xAA as f32 / 255.0));
244 assert!(approx_eq(color.alpha, 0xDD as f32 / 255.0));
245
246 let color = parse_color("blue").expect("named");
247 assert!(approx_eq(color.blue, 1.0));
248 assert!(approx_eq(color.red, 0.0));
249 }
250
251 #[test]
252 fn parse_color_rgb_variants() {
253 let color = parse_color("rgb(255, 0, 128)").expect("rgb");
254 assert!(approx_eq(color.red, 1.0));
255 assert!(approx_eq(color.green, 0.0));
256 assert!(approx_eq(color.blue, 128.0 / 255.0));
257
258 let color = parse_color("rgba(255, 0, 0, 0.5)").expect("rgba");
259 assert!(approx_eq(color.red, 1.0));
260 assert!(approx_eq(color.alpha, 0.5));
261
262 let color = parse_color("rgb(100%, 0%, 50%)").expect("rgb percent");
263 assert!(approx_eq(color.red, 1.0));
264 assert!(approx_eq(color.blue, 0.5));
265
266 let color = parse_color("rgba(255, 0, 0, 50%)").expect("rgba percent");
267 assert!(approx_eq(color.alpha, 0.5));
268 }
269
270 #[test]
271 fn normalized_color_key_matches_equivalent_inputs() {
272 let white = normalized_color_key("white").expect("white");
273 assert_eq!(white, normalized_color_key("#fff").expect("#fff"));
274 assert_eq!(white, normalized_color_key("#ffffff").expect("#ffffff"));
275 assert_eq!(
276 white,
277 normalized_color_key("rgb(255 255 255)").expect("rgb")
278 );
279 assert_eq!(white, normalized_color_key("hsl(0 0% 100%)").expect("hsl"));
280 }
281
282 #[test]
283 fn normalized_color_key_preserves_alpha() {
284 let translucent = normalized_color_key("rgba(255, 255, 255, 0.5)").expect("rgba");
285 assert_eq!(
286 translucent,
287 normalized_color_key("#ffffff80").expect("hex alpha")
288 );
289 assert_ne!(translucent, normalized_color_key("white").expect("white"));
290 }
291
292 #[test]
293 fn generate_color_presentations_formats_output() {
294 let range = Range::new(Position::new(0, 0), Position::new(0, 4));
295 let color = Color {
296 red: 1.0,
297 green: 0.0,
298 blue: 0.5,
299 alpha: 1.0,
300 };
301 let presentations = generate_color_presentations(color, range);
302 assert_eq!(presentations.len(), 3);
303 assert!(presentations[0].label.starts_with('#'));
304 assert!(presentations[1].label.starts_with("rgb("));
305 assert!(presentations[2].label.starts_with("hsl("));
306
307 let color = Color {
308 red: 1.0,
309 green: 0.0,
310 blue: 0.0,
311 alpha: 0.5,
312 };
313 let presentations = generate_color_presentations(color, range);
314 assert_eq!(presentations.len(), 3);
315 assert!(presentations[0].label.starts_with('#'));
316 assert!(presentations[1].label.starts_with("rgba("));
317 assert!(presentations[2].label.starts_with("hsla("));
318 }
319
320 #[test]
321 fn format_color_hex_opaque_and_transparent() {
322 let color = Color {
324 red: 0.0,
325 green: 0.5,
326 blue: 1.0,
327 alpha: 1.0,
328 };
329 let hex = format_color_as_hex(color);
330 assert_eq!(hex, "#0080ff");
331
332 let color = Color {
334 red: 1.0,
335 green: 0.0,
336 blue: 0.0,
337 alpha: 0.5,
338 };
339 let hex = format_color_as_hex(color);
340 assert_eq!(hex, "#ff000080");
341 }
342
343 #[test]
344 fn format_color_rgb_with_alpha() {
345 let color = Color {
346 red: 0.5,
347 green: 0.5,
348 blue: 0.5,
349 alpha: 1.0,
350 };
351 let rgb = format_color_as_rgb(color);
352 assert_eq!(rgb, "rgb(128, 128, 128)");
353
354 let color = Color {
355 red: 1.0,
356 green: 0.0,
357 blue: 0.0,
358 alpha: 0.75,
359 };
360 let rgba = format_color_as_rgb(color);
361 assert_eq!(rgba, "rgba(255, 0, 0, 0.75)");
362 }
363
364 #[test]
365 fn format_color_hsl_with_alpha() {
366 let color = Color {
367 red: 1.0,
368 green: 0.0,
369 blue: 0.0,
370 alpha: 1.0,
371 };
372 let hsl = format_color_as_hsl(color);
373 assert!(hsl.starts_with("hsl("));
374 assert!(hsl.contains("0,") || hsl.contains("360,")); let color = Color {
377 red: 0.0,
378 green: 0.5,
379 blue: 1.0,
380 alpha: 0.5,
381 };
382 let hsla = format_color_as_hsl(color);
383 assert!(hsla.starts_with("hsla("));
384 assert!(hsla.contains("0.50"));
385 }
386
387 #[test]
388 fn rgb_to_hsl_conversion() {
389 let (h, s, l) = rgb_to_hsl(1.0, 0.0, 0.0);
391 assert!(approx_eq(h, 0.0));
392 assert!(approx_eq(s, 1.0));
393 assert!(approx_eq(l, 0.5));
394
395 let (h, s, l) = rgb_to_hsl(0.0, 1.0, 0.0);
397 assert!(approx_eq(h, 1.0 / 3.0));
398 assert!(approx_eq(s, 1.0));
399 assert!(approx_eq(l, 0.5));
400
401 let (_h, s, l) = rgb_to_hsl(0.5, 0.5, 0.5);
403 assert!(approx_eq(s, 0.0));
404 assert!(approx_eq(l, 0.5));
405 }
406
407 #[test]
408 fn parse_color_edge_cases() {
409 assert!(parse_color("not-a-color").is_none());
411 assert!(parse_color("").is_none());
412 assert!(parse_color("rgb(999, 999, 999)").is_some()); assert!(parse_color("rebeccapurple").is_some());
416 assert!(parse_color("aliceblue").is_some());
417
418 let color = parse_color("transparent").expect("transparent");
420 assert!(approx_eq(color.alpha, 0.0));
421 }
422
423 #[test]
424 fn parse_color_rejects_nonfinite_channels_and_clamps_alpha() {
425 for value in [
426 "#gggggg",
427 "rgb(NaN, 0, 0)",
428 "rgb(inf, 0, 0)",
429 "rgb(infinity, 0, 0)",
430 "rgb(1e999, 0, 0)",
431 "hsl(0, NaN%, 50%)",
432 "hsl(infdeg, 50%, 50%)",
433 "hsl(infDEG, 50%, 50%)",
434 "hsl(1e999TURN, 50%, 50%)",
435 "hwb(0 0% 0% / NaN)",
436 "hwb(0 0% 0% / InFiNiTy)",
437 ] {
438 assert!(parse_color(value).is_none(), "{value}");
439 }
440
441 let color = parse_color("hwb(0 0% 0% / 2)").expect("finite HWB color");
442 assert!(approx_eq(color.alpha, 1.0));
443 for channel in [color.red, color.green, color.blue, color.alpha] {
444 assert!(channel.is_finite());
445 assert!((0.0..=1.0).contains(&channel));
446 }
447 }
448
449 #[test]
450 fn color_clamping() {
451 let color = Color {
453 red: 1.5,
454 green: -0.5,
455 blue: 0.5,
456 alpha: 2.0,
457 };
458
459 let hex = format_color_as_hex(color);
460 assert!(hex.starts_with('#'));
461
462 let rgb = format_color_as_rgb(color);
463 assert!(rgb.contains("255")); assert!(rgb.contains("0")); }
466}