1use csscolorparser::{Color, ParseColorError};
2use tower_lsp::lsp_types;
3
4#[derive(Debug, Clone)]
5pub struct ColorNode {
6 pub color: Color,
7 pub matched: String,
8 pub position: lsp_types::Position,
9}
10
11impl Eq for ColorNode {}
12impl PartialEq for ColorNode {
13 fn eq(&self, other: &Self) -> bool {
14 self.matched == other.matched
15 && self.position == other.position
16 && self.color.to_css_hex() == other.color.to_css_hex()
17 }
18}
19
20impl ColorNode {
21 pub fn new(matched: &str, color: Color, line: usize, character: usize) -> Self {
25 Self {
26 matched: matched.to_string(),
27 position: lsp_types::Position::new(line as u32, character as u32),
28 color,
29 }
30 }
31
32 #[allow(unused)]
33 pub fn must_parse(matched: &str, line: usize, col: usize) -> Self {
34 let color = try_parse_color(matched).expect("The `matched` should be a valid CSS color");
35 Self::new(matched, color, line, col)
36 }
37
38 pub fn lsp_color(&self) -> lsp_types::Color {
39 lsp_types::Color {
40 red: self.color.r,
41 green: self.color.g,
42 blue: self.color.b,
43 alpha: self.color.a,
44 }
45 }
46}
47
48fn try_parse_color(s: &str) -> Result<Color, ParseColorError> {
49 if let Ok(color) = try_parse_gpui_color(s) {
50 return Ok(color);
51 }
52
53 csscolorparser::parse(s)
54}
55
56fn try_parse_gpui_color(s: &str) -> Result<Color, ParseColorError> {
58 let s = s.trim();
59
60 fn parse_f8(s: &str) -> Option<f32> {
62 s.parse()
63 .ok()
64 .and_then(|v| (0.0..=1.0).contains(&v).then_some(v))
65 }
66
67 if let (Some(idx), Some(s)) = (s.find('('), s.strip_suffix(')')) {
68 let fname = &s[..idx].trim_end();
69 let mut params = s[idx + 1..]
70 .split(',')
71 .flat_map(str::split_ascii_whitespace);
72
73 let (Some(val0), Some(val1), Some(val2)) = (params.next(), params.next(), params.next())
74 else {
75 return Err(ParseColorError::InvalidFunction);
76 };
77
78 let alpha = if let Some(a) = params.next() {
79 if let Some(v) = parse_f8(a) {
80 v.clamp(0.0, 1.0)
81 } else {
82 return Err(ParseColorError::InvalidFunction);
83 }
84 } else {
85 1.0
86 };
87
88 if params.next().is_some() {
89 return Err(ParseColorError::InvalidFunction);
90 }
91
92 if fname.eq_ignore_ascii_case("rgb") || fname.eq_ignore_ascii_case("rgba") {
93 if let (Some(v0), Some(v1), Some(v2)) = (parse_f8(val0), parse_f8(val1), parse_f8(val2))
94 {
95 return Ok(Color::new(v0, v1, v2, alpha));
96 } else {
97 return Err(ParseColorError::InvalidFunction);
98 }
99 } else if fname.eq_ignore_ascii_case("hsl") || fname.eq_ignore_ascii_case("hsla") {
100 if let (Some(v0), Some(v1), Some(v2)) = (parse_f8(val0), parse_f8(val1), parse_f8(val2))
101 {
102 return Ok(Color::from_hsla(v0 * 360.0, v1, v2, alpha));
103 } else {
104 return Err(ParseColorError::InvalidFunction);
105 }
106 }
107 }
108
109 Err(ParseColorError::InvalidUnknown)
110}
111
112fn is_hex_char(c: &char) -> bool {
113 matches!(c, '#' | 'a'..='f' | 'A'..='F' | '0'..='9')
114}
115
116pub fn parse(text: &str) -> Vec<ColorNode> {
118 let mut nodes = Vec::new();
119
120 for (ix, line_text) in text.lines().enumerate() {
121 let line_len = line_text.len();
122 let mut offset = 0;
124 let mut token = String::new();
125 while offset < line_text.chars().count() {
126 let c = line_text.chars().nth(offset).unwrap_or(' ');
127 match c {
128 '#' => {
129 token.clear();
130
131 let hex = line_text
133 .chars()
134 .skip(offset)
135 .take_while(is_hex_char)
136 .take(9)
137 .collect::<String>();
138 if let Some(node) = match_color(&hex, ix, offset) {
139 nodes.push(node);
140 offset += hex.chars().count();
141 continue;
142 }
143 }
144 'a'..='z' | 'A'..='Z' | '(' => {
145 if token.contains('(') {
147 token.clear();
148 }
149
150 token.push(c);
151 match token.as_ref() {
152 "hsl(" | "hsla(" | "rgb(" | "rgba(" | "hwb(" | "hwba(" | "oklab("
154 | "oklch(" | "lab(" | "lch(" | "hsv(" => {
155 let end = line_text
157 .chars()
158 .skip(offset)
159 .position(|c| c == ')')
160 .unwrap_or(0);
161 let token_offset = offset.saturating_sub(token.chars().count()) + 1;
162
163 let range =
164 (offset + 1).min(line_len)..(offset + end + 1).min(line_len);
165 for c in line_text.chars().skip(range.start).take(range.len()) {
166 token.push(c)
167 }
168
169 if let Some(node) = match_color(&token, ix, token_offset) {
170 token.clear();
171 nodes.push(node);
172 offset += end + 1;
173 continue;
174 }
175 }
176 _ => {}
177 }
178 }
179 _ => {
180 token.clear();
181 }
182 }
183
184 offset += 1;
185 }
186 }
187
188 nodes
189}
190
191fn match_color(part: &str, line_ix: usize, character: usize) -> Option<ColorNode> {
192 if let Ok(color) = try_parse_color(part) {
193 Some(ColorNode::new(part, color, line_ix, character))
194 } else {
195 None
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use csscolorparser::Color;
202 use tower_lsp::lsp_types;
203
204 use crate::parser::{match_color, parse, try_parse_gpui_color, ColorNode};
205
206 #[test]
207 fn test_match_color() {
208 let cases = vec![
209 "#A0F0F0",
210 "#2eC8f1",
211 "#AAF0F0aa",
212 "#AAF0F033",
213 "#0f0E",
214 "#F2c",
215 "rgb(80%,80%,20%)",
216 "rgb(255 100 0)",
217 "rgba(255, 0, 0, 0.5)",
218 "rgb(100, 200, 100)",
219 "hsl(225, 100%, 70%)",
220 "hsla(20, 100%, 50%, .5)",
221 "hsla(1., 0.5, 0.5, 1.)",
222 ];
223
224 for case in cases {
225 assert!(match_color(case, 1, 1).is_some());
226 }
227
228 assert_eq!(
229 match_color("#e7b911", 1, 10),
230 Some(ColorNode::must_parse("#e7b911", 1, 10))
231 );
232 }
233
234 #[test]
235 fn test_try_parse_gpui_color() {
236 assert_eq!(
237 try_parse_gpui_color("rgb(0., 1., 0.2)"),
238 Ok(Color {
239 r: 0.,
240 g: 1.,
241 b: 0.2,
242 a: 1.
243 })
244 );
245 assert_eq!(
246 try_parse_gpui_color("rgb(0., 1., 0., 0.45)"),
247 Ok(Color {
248 r: 0.,
249 g: 1.,
250 b: 0.,
251 a: 0.45
252 })
253 );
254 assert!(try_parse_gpui_color("rgb(255., 220.0, 0.)").is_err());
255 assert!(try_parse_gpui_color("rgba(255., 120., 20.0, 1.)").is_err());
256
257 assert_eq!(
258 try_parse_gpui_color("hsl(0.48, 1., 0.45)"),
259 Ok(Color::new(0., 0.9, 0.79200006, 1.))
260 );
261 assert_eq!(
262 try_parse_gpui_color("hsla(0.48, 1., 0.45, 0.3)"),
263 Ok(Color::new(0., 0.9, 0.79200006, 0.3))
264 );
265 assert!(try_parse_gpui_color("hsl(240., 0., 50.0)").is_err());
266 assert!(try_parse_gpui_color("hsla(240., 0., 50.0, 1.)").is_err());
267 }
268
269 #[test]
270 fn test_must_parse() {
271 assert_eq!(
272 ColorNode::must_parse("hsla(.2, 0.5, 0.5, 1.)", 9, 11),
273 ColorNode {
274 matched: "hsla(.2, 0.5, 0.5, 1.)".to_string(),
275 color: Color::from_hsla(0.2 * 360., 0.5, 0.5, 1.),
276 position: lsp_types::Position::new(9, 11),
277 }
278 );
279
280 assert_eq!(
281 ColorNode::must_parse("rgba(1., 0.5, 0.5, 1.)", 9, 11),
282 ColorNode {
283 matched: "rgba(1., 0.5, 0.5, 1.)".to_string(),
284 color: Color::new(1., 0.5, 0.5, 1.),
285 position: lsp_types::Position::new(9, 11),
286 }
287 );
288 }
289
290 #[test]
291 fn test_parse() {
292 let colors = parse(include_str!("../../tests/test.json"));
293
294 assert_eq!(colors.len(), 9);
295 assert_eq!(colors[0], ColorNode::must_parse("#999", 1, 14));
296 assert_eq!(colors[1], ColorNode::must_parse("#FFFFFF", 2, 17));
297 assert_eq!(colors[2], ColorNode::must_parse("#ff003c99", 3, 12));
298 assert_eq!(colors[3], ColorNode::must_parse("#3cBD00", 4, 14));
299 assert_eq!(
300 colors[4],
301 ColorNode::must_parse("rgba(255, 252, 0, 0.5)", 5, 11)
302 );
303 assert_eq!(
304 colors[5],
305 ColorNode::must_parse("rgb(100, 200, 100)", 6, 10)
306 );
307 assert_eq!(
308 colors[6],
309 ColorNode::must_parse("hsla(20, 100%, 50%, .5)", 7, 11)
310 );
311 assert_eq!(
312 colors[7],
313 ColorNode::must_parse("hsl(225, 100%, 70%)", 8, 10)
314 );
315 assert_eq!(colors[8], ColorNode::must_parse("#EEAAFF", 9, 9));
316
317 let colors = parse(include_str!("../../tests/test.rs"));
318 assert_eq!(colors.len(), 5);
319 assert_eq!(
320 colors[0],
321 ColorNode::must_parse("hsla(0.3, 1.0, 0.5, 1.0)", 0, 9)
322 );
323 assert_eq!(
324 colors[1],
325 ColorNode::must_parse("hsla(0.58, 1.0, 0.5, 1.0)", 1, 9)
326 );
327 assert_eq!(
328 colors[2],
329 ColorNode::must_parse("hsla(0.85, 0.9, 0.6, 1.0)", 2, 9)
330 );
331 assert_eq!(
332 colors[3],
333 ColorNode::must_parse("hsla(0.75, 0.9, 0.65, 1.0)", 3, 12)
334 );
335 assert_eq!(
336 colors[4],
337 ColorNode::must_parse("hsla(0.45, 0.7, 0.75, 1.0)", 4, 13)
338 );
339 }
340}