color_lsp/
parser.rs

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    /// Create a new ColorNode
22    ///
23    /// `line`, `character` is 0-based
24    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
56/// Try to parse gpui color that values are 0..1
57fn try_parse_gpui_color(s: &str) -> Result<Color, ParseColorError> {
58    let s = s.trim();
59
60    /// Parse and ensure all value in 0..1
61    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
116/// Parse the text and return a list of ColorNode
117pub 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        // offset is 0-based character index
123        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                    // Find the hex color code
132                    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                    // Avoid `Ok(hsla(`, to get `hsla(`
146                    if token.contains('(') {
147                        token.clear();
148                    }
149
150                    token.push(c);
151                    match token.as_ref() {
152                        // Ref https://github.com/mazznoer/csscolorparser-rs
153                        "hsl(" | "hsla(" | "rgb(" | "rgba(" | "hwb(" | "hwba(" | "oklab("
154                        | "oklch(" | "lab(" | "lch(" | "hsv(" => {
155                            // Find until the closing parenthesis
156                            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}