1use csscolorparser::Color as CssColor;
2use tower_lsp::lsp_types::{Color, ColorPresentation, Range, TextEdit};
3
4pub fn parse_color(value: &str) -> Option<Color> {
6 let value = value.trim();
7 let lower = value.to_lowercase();
8
9 if let Some(hex) = lower.strip_prefix('#') {
11 return parse_hex(hex);
12 }
13
14 if lower.starts_with("rgb") {
16 return parse_rgb(&lower);
17 }
18
19 parse_csscolorparser(value).or_else(|| parse_named_color(&lower))
21}
22
23fn parse_csscolorparser(value: &str) -> Option<Color> {
24 let parsed: CssColor = value.parse().ok()?;
25 Some(Color {
26 red: parsed.r as f32,
27 green: parsed.g as f32,
28 blue: parsed.b as f32,
29 alpha: parsed.a as f32,
30 })
31}
32
33fn parse_hex(hex: &str) -> Option<Color> {
34 let hex = hex.trim();
35 let len = hex.len();
36
37 if len == 3 {
38 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
40 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
41 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
42 Some(Color {
43 red: r as f32 / 255.0,
44 green: g as f32 / 255.0,
45 blue: b as f32 / 255.0,
46 alpha: 1.0,
47 })
48 } else if len == 4 {
49 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
51 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
52 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
53 let a = u8::from_str_radix(&hex[3..4].repeat(2), 16).ok()?;
54 Some(Color {
55 red: r as f32 / 255.0,
56 green: g as f32 / 255.0,
57 blue: b as f32 / 255.0,
58 alpha: a as f32 / 255.0,
59 })
60 } else if len == 6 {
61 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
63 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
64 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
65 Some(Color {
66 red: r as f32 / 255.0,
67 green: g as f32 / 255.0,
68 blue: b as f32 / 255.0,
69 alpha: 1.0,
70 })
71 } else if len == 8 {
72 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
74 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
75 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
76 let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
77 Some(Color {
78 red: r as f32 / 255.0,
79 green: g as f32 / 255.0,
80 blue: b as f32 / 255.0,
81 alpha: a as f32 / 255.0,
82 })
83 } else {
84 None
85 }
86}
87
88fn parse_rgb(value: &str) -> Option<Color> {
89 let inner = if let Some(rest) = value.strip_prefix("rgba") {
90 rest
91 } else if let Some(rest) = value.strip_prefix("rgb") {
92 rest
93 } else {
94 return None;
95 };
96
97 let inner = inner.trim_start().strip_prefix('(')?.strip_suffix(')')?;
98 let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
99
100 if parts.len() < 3 {
101 return None;
102 }
103
104 let parse_channel = |part: &str| -> Option<f32> {
105 if let Some(pct) = part.strip_suffix('%') {
106 let value = pct.trim().parse::<f32>().ok()?;
107 return Some((value / 100.0).clamp(0.0, 1.0));
108 }
109 let value = part.parse::<f32>().ok()?;
110 if value > 1.0 {
111 Some((value / 255.0).clamp(0.0, 1.0))
112 } else {
113 Some(value.clamp(0.0, 1.0))
114 }
115 };
116
117 let parse_alpha = |part: &str| -> Option<f32> {
118 if let Some(pct) = part.strip_suffix('%') {
119 let value = pct.trim().parse::<f32>().ok()?;
120 return Some((value / 100.0).clamp(0.0, 1.0));
121 }
122 let value = part.parse::<f32>().ok()?;
123 if value > 1.0 {
124 Some((value / 255.0).clamp(0.0, 1.0))
125 } else {
126 Some(value.clamp(0.0, 1.0))
127 }
128 };
129
130 let r = parse_channel(parts[0])?;
131 let g = parse_channel(parts[1])?;
132 let b = parse_channel(parts[2])?;
133 let a = if parts.len() > 3 {
134 parse_alpha(parts[3])?
135 } else {
136 1.0
137 };
138
139 Some(Color {
140 red: r,
141 green: g,
142 blue: b,
143 alpha: a,
144 })
145}
146
147fn parse_named_color(name: &str) -> Option<Color> {
148 match name.to_lowercase().as_str() {
150 "red" => Some(Color {
151 red: 1.0,
152 green: 0.0,
153 blue: 0.0,
154 alpha: 1.0,
155 }),
156 "green" => Some(Color {
157 red: 0.0,
158 green: 0.5,
159 blue: 0.0,
160 alpha: 1.0,
161 }),
162 "blue" => Some(Color {
163 red: 0.0,
164 green: 0.0,
165 blue: 1.0,
166 alpha: 1.0,
167 }),
168 "white" => Some(Color {
169 red: 1.0,
170 green: 1.0,
171 blue: 1.0,
172 alpha: 1.0,
173 }),
174 "black" => Some(Color {
175 red: 0.0,
176 green: 0.0,
177 blue: 0.0,
178 alpha: 1.0,
179 }),
180 "yellow" => Some(Color {
181 red: 1.0,
182 green: 1.0,
183 blue: 0.0,
184 alpha: 1.0,
185 }),
186 "cyan" => Some(Color {
187 red: 0.0,
188 green: 1.0,
189 blue: 1.0,
190 alpha: 1.0,
191 }),
192 "magenta" => Some(Color {
193 red: 1.0,
194 green: 0.0,
195 blue: 1.0,
196 alpha: 1.0,
197 }),
198 _ => None,
199 }
200}
201
202pub fn generate_color_presentations(color: Color, range: Range) -> Vec<ColorPresentation> {
204 let mut presentations = Vec::new();
205
206 let hex_str = format_color_as_hex(color);
207 presentations.push(ColorPresentation {
208 label: hex_str.clone(),
209 text_edit: Some(TextEdit {
210 range,
211 new_text: hex_str,
212 }),
213 additional_text_edits: None,
214 });
215
216 let rgb_str = format_color_as_rgb(color);
217 presentations.push(ColorPresentation {
218 label: rgb_str.clone(),
219 text_edit: Some(TextEdit {
220 range,
221 new_text: rgb_str,
222 }),
223 additional_text_edits: None,
224 });
225
226 let hsl_str = format_color_as_hsl(color);
227 presentations.push(ColorPresentation {
228 label: hsl_str.clone(),
229 text_edit: Some(TextEdit {
230 range,
231 new_text: hsl_str,
232 }),
233 additional_text_edits: None,
234 });
235
236 presentations
237}
238
239pub fn format_color_as_hex(color: Color) -> String {
240 let r = (color.red.clamp(0.0, 1.0) * 255.0).round() as u8;
241 let g = (color.green.clamp(0.0, 1.0) * 255.0).round() as u8;
242 let b = (color.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
243 let a = (color.alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
244
245 if a == 255 {
246 format!("#{:02x}{:02x}{:02x}", r, g, b)
247 } else {
248 format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a)
249 }
250}
251
252pub fn format_color_as_rgb(color: Color) -> String {
253 let r = (color.red.clamp(0.0, 1.0) * 255.0).round() as u8;
254 let g = (color.green.clamp(0.0, 1.0) * 255.0).round() as u8;
255 let b = (color.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
256 let a = color.alpha.clamp(0.0, 1.0);
257
258 if a >= 1.0 {
259 format!("rgb({}, {}, {})", r, g, b)
260 } else {
261 format!("rgba({}, {}, {}, {:.2})", r, g, b, a)
262 }
263}
264
265pub fn format_color_as_hsl(color: Color) -> String {
266 let (h, s, l) = rgb_to_hsl(color.red, color.green, color.blue);
267 let a = color.alpha.clamp(0.0, 1.0);
268
269 let h_deg = (h * 360.0).round();
270 let s_pct = (s * 100.0).round();
271 let l_pct = (l * 100.0).round();
272
273 if a >= 1.0 {
274 format!("hsl({}, {}%, {}%)", h_deg, s_pct, l_pct)
275 } else {
276 format!("hsla({}, {}%, {}%, {:.2})", h_deg, s_pct, l_pct, a)
277 }
278}
279
280fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
281 let max = r.max(g).max(b);
282 let min = r.min(g).min(b);
283 let l = (max + min) / 2.0;
284
285 if max == min {
286 return (0.0, 0.0, l);
287 }
288
289 let d = max - min;
290 let s = if l > 0.5 {
291 d / (2.0 - max - min)
292 } else {
293 d / (max + min)
294 };
295
296 let h = if max == r {
297 ((g - b) / d + if g < b { 6.0 } else { 0.0 }) / 6.0
298 } else if max == g {
299 ((b - r) / d + 2.0) / 6.0
300 } else {
301 ((r - g) / d + 4.0) / 6.0
302 };
303
304 (h, s, l)
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use tower_lsp::lsp_types::Position;
311
312 fn approx_eq(a: f32, b: f32) -> bool {
313 (a - b).abs() < 0.01
314 }
315
316 #[test]
317 fn parse_color_hex_and_named() {
318 let color = parse_color("#abc").expect("hex");
319 assert!(approx_eq(color.red, 0xAA as f32 / 255.0));
320 assert!(approx_eq(color.green, 0xBB as f32 / 255.0));
321 assert!(approx_eq(color.blue, 0xCC as f32 / 255.0));
322 assert!(approx_eq(color.alpha, 1.0));
323
324 let color = parse_color("#abcd").expect("hex with alpha");
325 assert!(approx_eq(color.red, 0xAA as f32 / 255.0));
326 assert!(approx_eq(color.alpha, 0xDD as f32 / 255.0));
327
328 let color = parse_color("blue").expect("named");
329 assert!(approx_eq(color.blue, 1.0));
330 assert!(approx_eq(color.red, 0.0));
331 }
332
333 #[test]
334 fn parse_color_rgb_variants() {
335 let color = parse_color("rgb(255, 0, 128)").expect("rgb");
336 assert!(approx_eq(color.red, 1.0));
337 assert!(approx_eq(color.green, 0.0));
338 assert!(approx_eq(color.blue, 128.0 / 255.0));
339
340 let color = parse_color("rgba(255, 0, 0, 0.5)").expect("rgba");
341 assert!(approx_eq(color.red, 1.0));
342 assert!(approx_eq(color.alpha, 0.5));
343
344 let color = parse_color("rgb(100%, 0%, 50%)").expect("rgb percent");
345 assert!(approx_eq(color.red, 1.0));
346 assert!(approx_eq(color.blue, 0.5));
347
348 let color = parse_color("rgba(255, 0, 0, 50%)").expect("rgba percent");
349 assert!(approx_eq(color.alpha, 0.5));
350 }
351
352 #[test]
353 fn parse_color_csscolorparser_fallback() {
354 let color = parse_color("hsl(0, 100%, 50%)").expect("csscolorparser");
355 assert!(approx_eq(color.red, 1.0));
356 assert!(approx_eq(color.green, 0.0));
357 assert!(approx_eq(color.blue, 0.0));
358 }
359
360 #[test]
361 fn generate_color_presentations_formats_output() {
362 let range = Range::new(Position::new(0, 0), Position::new(0, 4));
363 let color = Color {
364 red: 1.0,
365 green: 0.0,
366 blue: 0.5,
367 alpha: 1.0,
368 };
369 let presentations = generate_color_presentations(color, range);
370 assert_eq!(presentations.len(), 3);
371 assert!(presentations[0].label.starts_with('#'));
372 assert!(presentations[1].label.starts_with("rgb("));
373 assert!(presentations[2].label.starts_with("hsl("));
374
375 let color = Color {
376 red: 1.0,
377 green: 0.0,
378 blue: 0.0,
379 alpha: 0.5,
380 };
381 let presentations = generate_color_presentations(color, range);
382 assert_eq!(presentations.len(), 3);
383 assert!(presentations[0].label.starts_with('#'));
384 assert!(presentations[1].label.starts_with("rgba("));
385 assert!(presentations[2].label.starts_with("hsla("));
386 }
387
388 #[test]
389 fn format_color_hex_opaque_and_transparent() {
390 let color = Color {
392 red: 0.0,
393 green: 0.5,
394 blue: 1.0,
395 alpha: 1.0,
396 };
397 let hex = format_color_as_hex(color);
398 assert_eq!(hex, "#0080ff");
399
400 let color = Color {
402 red: 1.0,
403 green: 0.0,
404 blue: 0.0,
405 alpha: 0.5,
406 };
407 let hex = format_color_as_hex(color);
408 assert_eq!(hex, "#ff000080");
409 }
410
411 #[test]
412 fn format_color_rgb_with_alpha() {
413 let color = Color {
414 red: 0.5,
415 green: 0.5,
416 blue: 0.5,
417 alpha: 1.0,
418 };
419 let rgb = format_color_as_rgb(color);
420 assert_eq!(rgb, "rgb(128, 128, 128)");
421
422 let color = Color {
423 red: 1.0,
424 green: 0.0,
425 blue: 0.0,
426 alpha: 0.75,
427 };
428 let rgba = format_color_as_rgb(color);
429 assert_eq!(rgba, "rgba(255, 0, 0, 0.75)");
430 }
431
432 #[test]
433 fn format_color_hsl_with_alpha() {
434 let color = Color {
435 red: 1.0,
436 green: 0.0,
437 blue: 0.0,
438 alpha: 1.0,
439 };
440 let hsl = format_color_as_hsl(color);
441 assert!(hsl.starts_with("hsl("));
442 assert!(hsl.contains("0,") || hsl.contains("360,")); let color = Color {
445 red: 0.0,
446 green: 0.5,
447 blue: 1.0,
448 alpha: 0.5,
449 };
450 let hsla = format_color_as_hsl(color);
451 assert!(hsla.starts_with("hsla("));
452 assert!(hsla.contains("0.50"));
453 }
454
455 #[test]
456 fn rgb_to_hsl_conversion() {
457 let (h, s, l) = rgb_to_hsl(1.0, 0.0, 0.0);
459 assert!(approx_eq(h, 0.0));
460 assert!(approx_eq(s, 1.0));
461 assert!(approx_eq(l, 0.5));
462
463 let (h, s, l) = rgb_to_hsl(0.0, 1.0, 0.0);
465 assert!(approx_eq(h, 1.0 / 3.0));
466 assert!(approx_eq(s, 1.0));
467 assert!(approx_eq(l, 0.5));
468
469 let (_h, s, l) = rgb_to_hsl(0.5, 0.5, 0.5);
471 assert!(approx_eq(s, 0.0));
472 assert!(approx_eq(l, 0.5));
473 }
474
475 #[test]
476 fn parse_color_edge_cases() {
477 assert!(parse_color("not-a-color").is_none());
479 assert!(parse_color("").is_none());
480 assert!(parse_color("rgb(999, 999, 999)").is_some()); let color = parse_color("transparent").expect("transparent");
484 assert!(approx_eq(color.alpha, 0.0));
485 }
486
487 #[test]
488 fn color_clamping() {
489 let color = Color {
491 red: 1.5,
492 green: -0.5,
493 blue: 0.5,
494 alpha: 2.0,
495 };
496
497 let hex = format_color_as_hex(color);
498 assert!(hex.starts_with('#'));
499
500 let rgb = format_color_as_rgb(color);
501 assert!(rgb.contains("255")); assert!(rgb.contains("0")); }
504}