1use csscolorparser::Color as CssColor;
2use tower_lsp::lsp_types::{Color, ColorPresentation, Range, TextEdit};
3
4pub fn parse_color(value: &str) -> Option<Color> {
6 parse_csscolorparser(value.trim())
7}
8
9fn parse_csscolorparser(value: &str) -> Option<Color> {
10 let parsed: CssColor = value.parse().ok()?;
11 Some(Color {
12 red: parsed.r as f32,
13 green: parsed.g as f32,
14 blue: parsed.b as f32,
15 alpha: parsed.a as f32,
16 })
17}
18
19pub fn generate_color_presentations(color: Color, range: Range) -> Vec<ColorPresentation> {
21 let mut presentations = Vec::new();
22
23 let hex_str = format_color_as_hex(color);
24 presentations.push(ColorPresentation {
25 label: hex_str.clone(),
26 text_edit: Some(TextEdit {
27 range,
28 new_text: hex_str,
29 }),
30 additional_text_edits: None,
31 });
32
33 let rgb_str = format_color_as_rgb(color);
34 presentations.push(ColorPresentation {
35 label: rgb_str.clone(),
36 text_edit: Some(TextEdit {
37 range,
38 new_text: rgb_str,
39 }),
40 additional_text_edits: None,
41 });
42
43 let hsl_str = format_color_as_hsl(color);
44 presentations.push(ColorPresentation {
45 label: hsl_str.clone(),
46 text_edit: Some(TextEdit {
47 range,
48 new_text: hsl_str,
49 }),
50 additional_text_edits: None,
51 });
52
53 presentations
54}
55
56pub fn format_color_as_hex(color: Color) -> String {
57 let r = (color.red.clamp(0.0, 1.0) * 255.0).round() as u8;
58 let g = (color.green.clamp(0.0, 1.0) * 255.0).round() as u8;
59 let b = (color.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
60 let a = (color.alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
61
62 if a == 255 {
63 format!("#{:02x}{:02x}{:02x}", r, g, b)
64 } else {
65 format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a)
66 }
67}
68
69pub fn format_color_as_rgb(color: Color) -> String {
70 let r = (color.red.clamp(0.0, 1.0) * 255.0).round() as u8;
71 let g = (color.green.clamp(0.0, 1.0) * 255.0).round() as u8;
72 let b = (color.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
73 let a = color.alpha.clamp(0.0, 1.0);
74
75 if a >= 1.0 {
76 format!("rgb({}, {}, {})", r, g, b)
77 } else {
78 format!("rgba({}, {}, {}, {:.2})", r, g, b, a)
79 }
80}
81
82pub fn format_color_as_hsl(color: Color) -> String {
83 let (h, s, l) = rgb_to_hsl(color.red, color.green, color.blue);
84 let a = color.alpha.clamp(0.0, 1.0);
85
86 let h_deg = (h * 360.0).round();
87 let s_pct = (s * 100.0).round();
88 let l_pct = (l * 100.0).round();
89
90 if a >= 1.0 {
91 format!("hsl({}, {}%, {}%)", h_deg, s_pct, l_pct)
92 } else {
93 format!("hsla({}, {}%, {}%, {:.2})", h_deg, s_pct, l_pct, a)
94 }
95}
96
97fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
98 let max = r.max(g).max(b);
99 let min = r.min(g).min(b);
100 let l = (max + min) / 2.0;
101
102 if max == min {
103 return (0.0, 0.0, l);
104 }
105
106 let d = max - min;
107 let s = if l > 0.5 {
108 d / (2.0 - max - min)
109 } else {
110 d / (max + min)
111 };
112
113 let h = if max == r {
114 ((g - b) / d + if g < b { 6.0 } else { 0.0 }) / 6.0
115 } else if max == g {
116 ((b - r) / d + 2.0) / 6.0
117 } else {
118 ((r - g) / d + 4.0) / 6.0
119 };
120
121 (h, s, l)
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use tower_lsp::lsp_types::Position;
128
129 fn approx_eq(a: f32, b: f32) -> bool {
130 (a - b).abs() < 0.01
131 }
132
133 #[test]
134 fn parse_color_hex_and_named() {
135 let color = parse_color("#abc").expect("hex");
136 assert!(approx_eq(color.red, 0xAA as f32 / 255.0));
137 assert!(approx_eq(color.green, 0xBB as f32 / 255.0));
138 assert!(approx_eq(color.blue, 0xCC as f32 / 255.0));
139 assert!(approx_eq(color.alpha, 1.0));
140
141 let color = parse_color("#abcd").expect("hex with alpha");
142 assert!(approx_eq(color.red, 0xAA as f32 / 255.0));
143 assert!(approx_eq(color.alpha, 0xDD as f32 / 255.0));
144
145 let color = parse_color("blue").expect("named");
146 assert!(approx_eq(color.blue, 1.0));
147 assert!(approx_eq(color.red, 0.0));
148 }
149
150 #[test]
151 fn parse_color_rgb_variants() {
152 let color = parse_color("rgb(255, 0, 128)").expect("rgb");
153 assert!(approx_eq(color.red, 1.0));
154 assert!(approx_eq(color.green, 0.0));
155 assert!(approx_eq(color.blue, 128.0 / 255.0));
156
157 let color = parse_color("rgba(255, 0, 0, 0.5)").expect("rgba");
158 assert!(approx_eq(color.red, 1.0));
159 assert!(approx_eq(color.alpha, 0.5));
160
161 let color = parse_color("rgb(100%, 0%, 50%)").expect("rgb percent");
162 assert!(approx_eq(color.red, 1.0));
163 assert!(approx_eq(color.blue, 0.5));
164
165 let color = parse_color("rgba(255, 0, 0, 50%)").expect("rgba percent");
166 assert!(approx_eq(color.alpha, 0.5));
167 }
168
169 #[test]
170 fn generate_color_presentations_formats_output() {
171 let range = Range::new(Position::new(0, 0), Position::new(0, 4));
172 let color = Color {
173 red: 1.0,
174 green: 0.0,
175 blue: 0.5,
176 alpha: 1.0,
177 };
178 let presentations = generate_color_presentations(color, range);
179 assert_eq!(presentations.len(), 3);
180 assert!(presentations[0].label.starts_with('#'));
181 assert!(presentations[1].label.starts_with("rgb("));
182 assert!(presentations[2].label.starts_with("hsl("));
183
184 let color = Color {
185 red: 1.0,
186 green: 0.0,
187 blue: 0.0,
188 alpha: 0.5,
189 };
190 let presentations = generate_color_presentations(color, range);
191 assert_eq!(presentations.len(), 3);
192 assert!(presentations[0].label.starts_with('#'));
193 assert!(presentations[1].label.starts_with("rgba("));
194 assert!(presentations[2].label.starts_with("hsla("));
195 }
196
197 #[test]
198 fn format_color_hex_opaque_and_transparent() {
199 let color = Color {
201 red: 0.0,
202 green: 0.5,
203 blue: 1.0,
204 alpha: 1.0,
205 };
206 let hex = format_color_as_hex(color);
207 assert_eq!(hex, "#0080ff");
208
209 let color = Color {
211 red: 1.0,
212 green: 0.0,
213 blue: 0.0,
214 alpha: 0.5,
215 };
216 let hex = format_color_as_hex(color);
217 assert_eq!(hex, "#ff000080");
218 }
219
220 #[test]
221 fn format_color_rgb_with_alpha() {
222 let color = Color {
223 red: 0.5,
224 green: 0.5,
225 blue: 0.5,
226 alpha: 1.0,
227 };
228 let rgb = format_color_as_rgb(color);
229 assert_eq!(rgb, "rgb(128, 128, 128)");
230
231 let color = Color {
232 red: 1.0,
233 green: 0.0,
234 blue: 0.0,
235 alpha: 0.75,
236 };
237 let rgba = format_color_as_rgb(color);
238 assert_eq!(rgba, "rgba(255, 0, 0, 0.75)");
239 }
240
241 #[test]
242 fn format_color_hsl_with_alpha() {
243 let color = Color {
244 red: 1.0,
245 green: 0.0,
246 blue: 0.0,
247 alpha: 1.0,
248 };
249 let hsl = format_color_as_hsl(color);
250 assert!(hsl.starts_with("hsl("));
251 assert!(hsl.contains("0,") || hsl.contains("360,")); let color = Color {
254 red: 0.0,
255 green: 0.5,
256 blue: 1.0,
257 alpha: 0.5,
258 };
259 let hsla = format_color_as_hsl(color);
260 assert!(hsla.starts_with("hsla("));
261 assert!(hsla.contains("0.50"));
262 }
263
264 #[test]
265 fn rgb_to_hsl_conversion() {
266 let (h, s, l) = rgb_to_hsl(1.0, 0.0, 0.0);
268 assert!(approx_eq(h, 0.0));
269 assert!(approx_eq(s, 1.0));
270 assert!(approx_eq(l, 0.5));
271
272 let (h, s, l) = rgb_to_hsl(0.0, 1.0, 0.0);
274 assert!(approx_eq(h, 1.0 / 3.0));
275 assert!(approx_eq(s, 1.0));
276 assert!(approx_eq(l, 0.5));
277
278 let (_h, s, l) = rgb_to_hsl(0.5, 0.5, 0.5);
280 assert!(approx_eq(s, 0.0));
281 assert!(approx_eq(l, 0.5));
282 }
283
284 #[test]
285 fn parse_color_edge_cases() {
286 assert!(parse_color("not-a-color").is_none());
288 assert!(parse_color("").is_none());
289 assert!(parse_color("rgb(999, 999, 999)").is_some()); assert!(parse_color("rebeccapurple").is_some());
293 assert!(parse_color("aliceblue").is_some());
294
295 let color = parse_color("transparent").expect("transparent");
297 assert!(approx_eq(color.alpha, 0.0));
298 }
299
300 #[test]
301 fn color_clamping() {
302 let color = Color {
304 red: 1.5,
305 green: -0.5,
306 blue: 0.5,
307 alpha: 2.0,
308 };
309
310 let hex = format_color_as_hex(color);
311 assert!(hex.starts_with('#'));
312
313 let rgb = format_color_as_rgb(color);
314 assert!(rgb.contains("255")); assert!(rgb.contains("0")); }
317}