css_variable_lsp/
color.rs

1use csscolorparser::Color as CssColor;
2use tower_lsp::lsp_types::{Color, ColorPresentation, Range, TextEdit};
3
4/// Parse a CSS color value and return an LSP Color (simplified version)
5pub fn parse_color(value: &str) -> Option<Color> {
6    let value = value.trim();
7    let lower = value.to_lowercase();
8
9    // Try hex color
10    if let Some(hex) = lower.strip_prefix('#') {
11        return parse_hex(hex);
12    }
13
14    // Try rgb/rgba
15    if lower.starts_with("rgb") {
16        return parse_rgb(&lower);
17    }
18
19    // Try named colors (basic set)
20    parse_named_color(&lower).or_else(|| parse_csscolorparser(value))
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        // #RGB
39        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        // #RGBA
50        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        // #RRGGBB
62        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        // #RRGGBBAA
73        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    // Basic named colors
149    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
202/// Generate color presentations for color picker
203pub 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        // Opaque color (alpha = 255)
391        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        // Transparent color (alpha < 255)
401        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,")); // Red hue
443
444        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        // Pure red
458        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        // Pure green
464        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        // Gray (no saturation)
470        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        // Invalid colors should return None
478        assert!(parse_color("not-a-color").is_none());
479        assert!(parse_color("").is_none());
480        assert!(parse_color("rgb(999, 999, 999)").is_some()); // Clamped by parser
481
482        // Transparent keyword
483        let color = parse_color("transparent").expect("transparent");
484        assert!(approx_eq(color.alpha, 0.0));
485    }
486
487    #[test]
488    fn color_clamping() {
489        // Test that colors are properly clamped to [0, 1]
490        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")); // Red clamped to max
502        assert!(rgb.contains("0")); // Green clamped to min
503    }
504}