Skip to main content

css_module_lexer/
lexer.rs

1use std::{iter::Rev, str::Chars};
2
3use crate::Range;
4
5pub const C_LINE_FEED: char = '\n';
6pub const C_CARRIAGE_RETURN: char = '\r';
7pub const C_FORM_FEED: char = '\u{c}';
8
9pub const C_TAB: char = '\t';
10pub const C_SPACE: char = ' ';
11
12pub const C_SOLIDUS: char = '/';
13pub const C_REVERSE_SOLIDUS: char = '\\';
14pub const C_ASTERISK: char = '*';
15
16pub const C_LEFT_PARENTHESIS: char = '(';
17pub const C_RIGHT_PARENTHESIS: char = ')';
18pub const C_LEFT_CURLY: char = '{';
19pub const C_RIGHT_CURLY: char = '}';
20pub const C_LEFT_SQUARE: char = '[';
21pub const C_RIGHT_SQUARE: char = ']';
22
23pub const C_QUOTATION_MARK: char = '"';
24pub const C_APOSTROPHE: char = '\'';
25
26pub const C_FULL_STOP: char = '.';
27pub const C_COLON: char = ':';
28pub const C_SEMICOLON: char = ';';
29pub const C_COMMA: char = ',';
30pub const C_PERCENTAGE: char = '%';
31pub const C_AT_SIGN: char = '@';
32
33pub const C_LOW_LINE: char = '_';
34pub const C_LOWER_A: char = 'a';
35pub const C_LOWER_E: char = 'e';
36pub const C_LOWER_F: char = 'f';
37pub const C_LOWER_Z: char = 'z';
38pub const C_UPPER_A: char = 'A';
39pub const C_UPPER_E: char = 'E';
40pub const C_UPPER_F: char = 'F';
41pub const C_UPPER_Z: char = 'Z';
42pub const C_0: char = '0';
43pub const C_9: char = '9';
44
45pub const C_NUMBER_SIGN: char = '#';
46pub const C_PLUS_SIGN: char = '+';
47pub const C_HYPHEN_MINUS: char = '-';
48
49pub const C_LESS_THAN_SIGN: char = '<';
50pub const C_GREATER_THAN_SIGN: char = '>';
51
52pub type Pos = u32;
53
54pub trait Visitor<'s> {
55    fn function(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
56    fn ident(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
57    fn url(
58        &mut self,
59        lexer: &mut Lexer<'s>,
60        start: Pos,
61        end: Pos,
62        content_start: Pos,
63        content_end: Pos,
64    ) -> Option<()>;
65    fn string(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
66    fn is_selector(&mut self, lexer: &mut Lexer<'s>) -> Option<bool>;
67    fn id(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
68    fn left_parenthesis(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
69    fn right_parenthesis(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
70    fn comma(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
71    fn class(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
72    fn pseudo_function(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
73    fn pseudo_class(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
74    fn semicolon(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
75    fn at_keyword(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
76    fn left_curly_bracket(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
77    fn right_curly_bracket(&mut self, lexer: &mut Lexer<'s>, start: Pos, end: Pos) -> Option<()>;
78}
79
80#[derive(Debug, Clone)]
81pub struct Lexer<'s, I: Iterator<Item = char> = Chars<'s>> {
82    value: &'s str,
83    iter: I,
84    cur_pos: Option<Pos>,
85    cur: Option<char>,
86    peek: Option<char>,
87    peek2: Option<char>,
88}
89
90impl<'s> Lexer<'s> {
91    pub fn new(value: &'s str) -> Self {
92        let mut iter = value.chars();
93        let peek = iter.next();
94        let peek2 = iter.next();
95        Self {
96            value,
97            iter,
98            cur_pos: None,
99            cur: None,
100            peek,
101            peek2,
102        }
103    }
104
105    pub fn turn_back(self, end: Pos) -> Lexer<'s, Rev<Chars<'s>>> {
106        let value = self.slice(0, end).unwrap();
107        let mut iter = value.chars().rev();
108        let peek = iter.next();
109        let peek2 = iter.next();
110        Lexer {
111            value,
112            iter,
113            cur_pos: None,
114            cur: None,
115            peek,
116            peek2,
117        }
118    }
119
120    pub fn slice(&self, start: Pos, end: Pos) -> Option<&'s str> {
121        Self::slice_range(self.value, &Range::new(start, end))
122    }
123
124    pub fn slice_range<'a>(input: &'a str, range: &Range) -> Option<&'a str> {
125        input.get(range.start as usize..range.end as usize)
126    }
127}
128
129impl<'s, I: Iterator<Item = char>> Lexer<'s, I> {
130    pub fn consume(&mut self) {
131        self.cur_pos = self.peek_pos();
132        self.cur = self.peek;
133        self.peek = self.peek2;
134        self.peek2 = self.iter.next();
135    }
136
137    pub fn cur_pos(&self) -> Option<Pos> {
138        self.cur_pos
139    }
140
141    pub fn cur(&self) -> Option<char> {
142        self.cur
143    }
144
145    pub fn peek_pos(&self) -> Option<Pos> {
146        if let Some(pos) = self.cur_pos() {
147            self.cur().map(|c| pos + c.len_utf8() as u32)
148        } else {
149            Some(0)
150        }
151    }
152
153    pub fn peek(&self) -> Option<char> {
154        self.peek
155    }
156
157    pub fn peek2_pos(&self) -> Option<Pos> {
158        self.peek_pos()
159            .and_then(|pos| self.peek().map(|c| pos + c.len_utf8() as u32))
160    }
161
162    pub fn peek2(&self) -> Option<char> {
163        self.peek2
164    }
165}
166
167impl<'s> Lexer<'s> {
168    pub fn lex<T: Visitor<'s>>(&mut self, visitor: &mut T) {
169        self.lex_impl(visitor);
170    }
171
172    fn lex_impl<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
173        self.consume();
174        while self.cur().is_some() {
175            self.consume_comments()?;
176            // https://drafts.csswg.org/css-syntax/#consume-token
177            match self.cur()? {
178                c if is_white_space(c) => self.consume_space()?,
179                C_QUOTATION_MARK => self.consume_string(visitor, C_QUOTATION_MARK)?,
180                C_NUMBER_SIGN => self.consume_number_sign(visitor)?,
181                C_APOSTROPHE => self.consume_string(visitor, C_APOSTROPHE)?,
182                C_LEFT_PARENTHESIS => self.consume_left_parenthesis(visitor)?,
183                C_RIGHT_PARENTHESIS => self.consume_right_parenthesis(visitor)?,
184                C_PLUS_SIGN => self.consume_plus_sign()?,
185                C_COMMA => self.consume_comma(visitor)?,
186                C_HYPHEN_MINUS => self.consume_minus(visitor)?,
187                C_FULL_STOP => self.consume_full_stop(visitor)?,
188                C_COLON => self.consume_potential_pseudo(visitor)?,
189                C_SEMICOLON => self.consume_semicolon(visitor)?,
190                C_LESS_THAN_SIGN => self.consume_less_than_sign()?,
191                C_AT_SIGN => self.consume_at_sign(visitor)?,
192                C_LEFT_SQUARE => self.consume_delim(),
193                C_REVERSE_SOLIDUS => self.consume_reverse_solidus(visitor)?,
194                C_RIGHT_SQUARE => self.consume_delim(),
195                C_LEFT_CURLY => self.consume_left_curly(visitor)?,
196                C_RIGHT_CURLY => self.consume_right_curly(visitor)?,
197                c if is_digit(c) => self.consume_numeric_token()?,
198                c if is_ident_start(c) => self.consume_ident_like(visitor)?,
199                _ => self.consume_delim(),
200            }
201        }
202        Some(())
203    }
204
205    pub fn consume_delim(&mut self) {
206        self.consume();
207    }
208
209    pub fn consume_numeric_token(&mut self) -> Option<()> {
210        self.consume_number()?;
211        let c = self.cur()?;
212        if start_ident_sequence(c, self.peek()?, self.peek2()?) {
213            return self.consume_ident_sequence();
214        }
215        if c == C_PERCENTAGE {
216            self.consume();
217        }
218        Some(())
219    }
220
221    pub fn consume_number(&mut self) -> Option<()> {
222        self.consume();
223        while is_digit(self.cur()?) {
224            self.consume();
225        }
226        if self.cur()? == C_FULL_STOP && is_digit(self.peek()?) {
227            self.consume();
228            self.consume();
229            while is_digit(self.cur()?) {
230                self.consume();
231            }
232        }
233        let c = self.cur()?;
234        if c == C_LOWER_E || c == C_UPPER_E {
235            let c = self.peek()?;
236            if is_digit(c) {
237                self.consume();
238            } else if c == C_HYPHEN_MINUS || c == C_PLUS_SIGN {
239                let c = self.peek2()?;
240                if is_digit(c) {
241                    self.consume();
242                    self.consume();
243                } else {
244                    return Some(());
245                }
246            } else {
247                return Some(());
248            }
249        } else {
250            return Some(());
251        }
252        self.consume();
253        while is_digit(self.cur()?) {
254            self.consume();
255        }
256        Some(())
257    }
258
259    pub fn consume_ident_sequence(&mut self) -> Option<()> {
260        loop {
261            let c = self.cur()?;
262            if maybe_valid_escape(c) {
263                self.consume();
264                self.consume_escaped()?;
265            } else if is_ident(c) {
266                self.consume();
267            } else {
268                return Some(());
269            }
270        }
271    }
272
273    pub fn consume_escaped(&mut self) -> Option<()> {
274        if is_hex_digit(self.cur()?) {
275            for _ in 1..5 {
276                self.consume();
277                if !is_hex_digit(self.cur()?) {
278                    break;
279                }
280            }
281            if is_white_space(self.cur()?) {
282                self.consume();
283            }
284        } else {
285            self.consume();
286        }
287        Some(())
288    }
289
290    pub fn consume_ident_like<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
291        let start = self.cur_pos()?;
292        self.consume_ident_sequence()?;
293        let peek_pos = self.peek_pos()?;
294        if self.cur_pos()? == start + 3 && self.slice(start, peek_pos)?.eq_ignore_ascii_case("url(")
295        {
296            self.consume();
297            while is_white_space(self.cur()?) {
298                self.consume();
299            }
300            let c = self.cur()?;
301            if c == C_QUOTATION_MARK || c == C_APOSTROPHE {
302                visitor.function(self, start, peek_pos)
303            } else {
304                self.consume_url(visitor, start)
305            }
306        } else if self.cur()? == C_LEFT_PARENTHESIS {
307            self.consume();
308            visitor.function(self, start, self.cur_pos()?)
309        } else {
310            visitor.ident(self, start, self.cur_pos()?)
311        }
312    }
313
314    pub fn consume_url<T: Visitor<'s>>(
315        self: &mut Lexer<'s>,
316        visitor: &mut T,
317        start: Pos,
318    ) -> Option<()> {
319        let content_start = self.cur_pos()?;
320        loop {
321            let c = self.cur()?;
322            if maybe_valid_escape(c) {
323                self.consume();
324                self.consume_escaped()?;
325            } else if is_white_space(c) {
326                let content_end = self.cur_pos()?;
327                self.consume();
328                while is_white_space(self.cur()?) {
329                    self.consume();
330                }
331                if self.cur()? != C_RIGHT_PARENTHESIS {
332                    return Some(());
333                }
334                self.consume();
335                return visitor.url(self, start, self.cur_pos()?, content_start, content_end);
336            } else if c == C_RIGHT_PARENTHESIS {
337                let content_end = self.cur_pos()?;
338                self.consume();
339                return visitor.url(self, start, self.cur_pos()?, content_start, content_end);
340            } else if c == C_LEFT_PARENTHESIS {
341                return Some(());
342            } else {
343                self.consume();
344            }
345        }
346    }
347
348    pub fn consume_string<T: Visitor<'s>>(&mut self, visitor: &mut T, end: char) -> Option<()> {
349        let start = self.cur_pos()?;
350        self.consume();
351        loop {
352            let c = self.cur()?;
353            if c == end {
354                self.consume();
355                break;
356            } else if is_new_line(c) {
357                break;
358            } else if c == C_REVERSE_SOLIDUS {
359                self.consume();
360                let c2 = self.cur()?;
361                if is_new_line(c2) {
362                    self.consume();
363                } else if are_valid_escape(c, c2) {
364                    self.consume_escaped()?;
365                }
366            } else {
367                self.consume();
368            }
369        }
370        visitor.string(self, start, self.cur_pos()?)
371    }
372
373    pub fn consume_number_sign<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
374        let c2 = self.peek()?;
375        let start = self.cur_pos()?;
376        if is_ident(c2) || are_valid_escape(c2, self.peek2()?) {
377            self.consume();
378            if !visitor.is_selector(self)? {
379                return Some(());
380            }
381            if !start_ident_sequence(self.cur()?, self.peek()?, self.peek2()?) {
382                return visitor.id(self, start, self.cur_pos()?);
383            }
384            self.consume_ident_sequence()?;
385            visitor.id(self, start, self.cur_pos()?)
386        } else {
387            self.consume_delim();
388            visitor.id(self, start, self.cur_pos()?)
389        }
390    }
391
392    pub fn consume_left_parenthesis<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
393        self.consume();
394        let end = self.cur_pos()?;
395        visitor.left_parenthesis(self, end - 1, end)
396    }
397
398    pub fn consume_right_parenthesis<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
399        self.consume();
400        let end = self.cur_pos()?;
401        visitor.right_parenthesis(self, end - 1, end)
402    }
403
404    pub fn consume_plus_sign(&mut self) -> Option<()> {
405        if start_number(self.cur()?, self.peek()?, self.peek2()?) {
406            self.consume_numeric_token()?;
407        } else {
408            self.consume_delim();
409        }
410        Some(())
411    }
412
413    pub fn consume_comma<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
414        self.consume();
415        let end = self.cur_pos()?;
416        visitor.comma(self, end - 1, end)
417    }
418
419    pub fn consume_minus<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
420        let c = self.cur()?;
421        let c2 = self.peek()?;
422        let c3 = self.peek2()?;
423        if start_number(c, c2, c3) {
424            self.consume_numeric_token()?;
425        } else if c2 == C_HYPHEN_MINUS && c3 == C_GREATER_THAN_SIGN {
426            self.consume();
427            self.consume();
428        } else if start_ident_sequence(c, c2, c3) {
429            self.consume_ident_like(visitor)?;
430        } else {
431            self.consume_delim();
432        }
433        Some(())
434    }
435
436    pub fn consume_full_stop<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
437        let c = self.cur()?;
438        let c2 = self.peek()?;
439        let c3 = self.peek2()?;
440        if start_number(c, c2, c3) {
441            return self.consume_numeric_token();
442        }
443        let start = self.cur_pos()?;
444        self.consume();
445        if !visitor.is_selector(self)? {
446            return Some(());
447        }
448        if !start_ident_sequence(c2, c3, self.peek2()?) {
449            return visitor.class(self, start, self.cur_pos()?);
450        }
451        self.consume_ident_sequence()?;
452        visitor.class(self, start, self.cur_pos()?)
453    }
454
455    pub fn consume_potential_pseudo<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
456        let start = self.cur_pos()?;
457        self.consume();
458        if !visitor.is_selector(self)?
459            || !start_ident_sequence(self.cur()?, self.peek()?, self.peek2()?)
460        {
461            return Some(());
462        }
463        self.consume_ident_sequence()?;
464        if self.cur()? == C_LEFT_PARENTHESIS {
465            self.consume();
466            visitor.pseudo_function(self, start, self.cur_pos()?)
467        } else {
468            visitor.pseudo_class(self, start, self.cur_pos()?)
469        }
470    }
471
472    pub fn consume_semicolon<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
473        self.consume();
474        let end = self.cur_pos()?;
475        visitor.semicolon(self, end - 1, end)
476    }
477
478    pub fn consume_less_than_sign(&mut self) -> Option<()> {
479        self.consume();
480        if self.cur()? == '!' && self.peek()? == '-' && self.peek2()? == '-' {
481            self.consume();
482            self.consume();
483            self.consume();
484        }
485        Some(())
486    }
487
488    pub fn consume_at_sign<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
489        let start = self.cur_pos()?;
490        self.consume();
491        if start_ident_sequence(self.cur()?, self.peek()?, self.peek2()?) {
492            self.consume_ident_sequence()?;
493            return visitor.at_keyword(self, start, self.cur_pos()?);
494        }
495        Some(())
496    }
497
498    pub fn consume_reverse_solidus<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
499        if are_valid_escape(self.cur()?, self.peek()?) {
500            self.consume_ident_like(visitor)?;
501        } else {
502            self.consume_delim();
503        }
504        Some(())
505    }
506
507    pub fn consume_left_curly<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
508        self.consume();
509        let end = self.cur_pos()?;
510        visitor.left_curly_bracket(self, end - 1, end)
511    }
512
513    pub fn consume_right_curly<T: Visitor<'s>>(&mut self, visitor: &mut T) -> Option<()> {
514        self.consume();
515        let end = self.cur_pos()?;
516        visitor.right_curly_bracket(self, end - 1, end)
517    }
518}
519
520impl<'s, I: Iterator<Item = char>> Lexer<'s, I> {
521    pub fn consume_comments(&mut self) -> Option<()> {
522        if self.cur()? == C_SOLIDUS && self.peek()? == C_ASTERISK {
523            self.consume();
524            loop {
525                self.consume();
526                let c = self.cur()?;
527                if c == C_ASTERISK && self.peek()? == C_SOLIDUS {
528                    self.consume();
529                    self.consume();
530                    break;
531                }
532            }
533        }
534        Some(())
535    }
536
537    pub fn consume_space(&mut self) -> Option<()> {
538        self.consume();
539        while is_white_space(self.cur()?) {
540            self.consume();
541        }
542        Some(())
543    }
544
545    pub fn consume_white_space_and_comments(&mut self) -> Option<()> {
546        loop {
547            self.consume_comments()?;
548            if is_white_space(self.cur()?) {
549                self.consume_space()?;
550            } else {
551                break;
552            }
553        }
554        Some(())
555    }
556}
557
558pub fn is_new_line(c: char) -> bool {
559    c == C_LINE_FEED || c == C_CARRIAGE_RETURN || c == C_FORM_FEED
560}
561
562pub fn is_space(c: char) -> bool {
563    c == C_TAB || c == C_SPACE
564}
565
566pub fn is_white_space(c: char) -> bool {
567    is_new_line(c) || is_space(c)
568}
569
570pub fn is_digit(c: char) -> bool {
571    c >= C_0 && c <= C_9
572}
573
574pub fn is_hex_digit(c: char) -> bool {
575    is_digit(c) || (c >= C_UPPER_A && c <= C_UPPER_F) || (c >= C_LOWER_A && c <= C_LOWER_F)
576}
577
578pub fn is_ident_start(c: char) -> bool {
579    c == C_LOW_LINE
580        || (c >= C_LOWER_A && c <= C_LOWER_Z)
581        || (c >= C_UPPER_A && c <= C_UPPER_Z)
582        || c > '\u{80}'
583}
584
585pub fn is_ident(c: char) -> bool {
586    is_ident_start(c) || is_digit(c) || c == C_HYPHEN_MINUS
587}
588
589pub fn start_ident_sequence(c1: char, c2: char, c3: char) -> bool {
590    if c1 == C_HYPHEN_MINUS {
591        is_ident_start(c2) || c2 == C_HYPHEN_MINUS || are_valid_escape(c2, c3)
592    } else {
593        is_ident_start(c1) || are_valid_escape(c1, c2)
594    }
595}
596
597pub fn maybe_valid_escape(c: char) -> bool {
598    c == C_REVERSE_SOLIDUS
599}
600
601pub fn are_valid_escape(c1: char, c2: char) -> bool {
602    c1 == C_REVERSE_SOLIDUS && !is_new_line(c2)
603}
604
605pub fn start_number(c1: char, c2: char, c3: char) -> bool {
606    if c1 == C_PLUS_SIGN || c1 == C_HYPHEN_MINUS {
607        is_digit(c2) || (c2 == C_FULL_STOP && is_digit(c3))
608    } else {
609        is_digit(c1) || (c1 == C_FULL_STOP && is_digit(c2))
610    }
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616    use indoc::indoc;
617    use std::fmt::Write as _;
618
619    fn assert_lexer_state<I: Iterator<Item = char>>(
620        lexer: &Lexer<'_, I>,
621        cur: Option<char>,
622        cur_pos: Option<Pos>,
623        peek: Option<char>,
624        peek_pos: Option<Pos>,
625        peek2: Option<char>,
626        peek2_pos: Option<Pos>,
627    ) {
628        assert_eq!(lexer.cur(), cur);
629        assert_eq!(lexer.cur_pos(), cur_pos);
630        assert_eq!(lexer.peek(), peek);
631        assert_eq!(lexer.peek_pos(), peek_pos);
632        assert_eq!(lexer.peek2(), peek2);
633        assert_eq!(lexer.peek2_pos(), peek2_pos);
634    }
635
636    #[derive(Default)]
637    struct Snapshot {
638        results: Vec<(String, String)>,
639    }
640
641    impl Snapshot {
642        pub fn add(&mut self, key: &str, value: &str) {
643            self.results.push((key.to_string(), value.to_string()))
644        }
645
646        pub fn snapshot(&self) -> String {
647            self.results
648                .iter()
649                .fold(String::new(), |mut output, (key, value)| {
650                    writeln!(output, "{key}: {value}").expect("writing to String cannot fail");
651                    output
652                })
653        }
654    }
655
656    impl Visitor<'_> for Snapshot {
657        fn function(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
658            self.add("function", lexer.slice(start, end)?);
659            Some(())
660        }
661
662        fn ident(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
663            self.add("ident", lexer.slice(start, end)?);
664            Some(())
665        }
666
667        fn url(
668            &mut self,
669            lexer: &mut Lexer,
670            _: Pos,
671            _: Pos,
672            content_start: Pos,
673            content_end: Pos,
674        ) -> Option<()> {
675            self.add("url", lexer.slice(content_start, content_end)?);
676            Some(())
677        }
678
679        fn string(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
680            self.add("string", lexer.slice(start, end)?);
681            Some(())
682        }
683
684        fn is_selector(&mut self, _: &mut Lexer) -> Option<bool> {
685            Some(true)
686        }
687
688        fn id(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
689            self.add("id", lexer.slice(start, end)?);
690            Some(())
691        }
692
693        fn left_parenthesis(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
694            self.add("left_parenthesis", lexer.slice(start, end)?);
695            Some(())
696        }
697
698        fn right_parenthesis(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
699            self.add("right_parenthesis", lexer.slice(start, end)?);
700            Some(())
701        }
702
703        fn comma(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
704            self.add("comma", lexer.slice(start, end)?);
705            Some(())
706        }
707
708        fn class(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
709            self.add("class", lexer.slice(start, end)?);
710            Some(())
711        }
712
713        fn pseudo_function(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
714            self.add("pseudo_function", lexer.slice(start, end)?);
715            Some(())
716        }
717
718        fn pseudo_class(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
719            self.add("pseudo_class", lexer.slice(start, end)?);
720            Some(())
721        }
722
723        fn semicolon(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
724            self.add("semicolon", lexer.slice(start, end)?);
725            Some(())
726        }
727
728        fn at_keyword(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
729            self.add("at_keyword", lexer.slice(start, end)?);
730            Some(())
731        }
732
733        fn left_curly_bracket(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
734            self.add("left_curly", lexer.slice(start, end)?);
735            Some(())
736        }
737
738        fn right_curly_bracket(&mut self, lexer: &mut Lexer, start: Pos, end: Pos) -> Option<()> {
739            self.add("right_curly", lexer.slice(start, end)?);
740            Some(())
741        }
742    }
743
744    fn assert_lexer_snapshot(input: &str, snapshot: &str) {
745        let mut s = Snapshot::default();
746        let mut l = Lexer::new(input);
747        l.lex(&mut s);
748        assert!(l.cur().is_none());
749        similar_asserts::assert_eq!(s.snapshot(), snapshot);
750    }
751
752    #[test]
753    fn lexer_state_1() {
754        let mut l = Lexer::new("");
755        assert_lexer_state(&l, None, None, None, Some(0), None, None);
756        l.consume();
757        assert_eq!(l.cur(), None);
758        assert_lexer_state(&l, None, Some(0), None, None, None, None);
759        l.consume();
760        assert_eq!(l.cur(), None);
761    }
762
763    #[test]
764    fn lexer_state_2() {
765        let mut l = Lexer::new("0壹👂삼");
766        assert_lexer_state(&l, None, None, Some('0'), Some(0), Some('壹'), Some(1));
767        l.consume();
768        assert_eq!(l.cur(), Some('0'));
769        assert_lexer_state(
770            &l,
771            Some('0'),
772            Some(0),
773            Some('壹'),
774            Some(1),
775            Some('👂'),
776            Some(4),
777        );
778        l.consume();
779        assert_eq!(l.cur(), Some('壹'));
780        assert_lexer_state(
781            &l,
782            Some('壹'),
783            Some(1),
784            Some('👂'),
785            Some(4),
786            Some('삼'),
787            Some(8),
788        );
789        l.consume();
790        assert_eq!(l.cur(), Some('👂'));
791        assert_lexer_state(&l, Some('👂'), Some(4), Some('삼'), Some(8), None, Some(11));
792        l.consume();
793        assert_eq!(l.cur(), Some('삼'));
794        assert_lexer_state(&l, Some('삼'), Some(8), None, Some(11), None, None);
795        l.consume();
796        assert_eq!(l.cur(), None);
797        assert_lexer_state(&l, None, Some(11), None, None, None, None);
798        l.consume();
799        assert_eq!(l.cur(), None);
800    }
801
802    #[test]
803    fn lexer_state_3() {
804        let l = Lexer::new("");
805        let mut l = l.turn_back(0);
806        assert_lexer_state(&l, None, None, None, Some(0), None, None);
807        l.consume();
808        assert_lexer_state(&l, None, Some(0), None, None, None, None);
809    }
810
811    #[test]
812    fn parse_urls() {
813        assert_lexer_snapshot(
814            indoc! {r#"
815            body {
816                background: url(
817                    https://example\2f4a8f.com\
818            /image.png
819                )
820            }
821            --element\ name.class\ name#_id {
822                background: url(  "https://example.com/some url \"with\" 'spaces'.png"   )  url('https://example.com/\'"quotes"\'.png');
823            }
824        "#},
825            indoc! {r#"
826            ident: body
827            left_curly: {
828            ident: background
829            url: https://example\2f4a8f.com\
830            /image.png
831            right_curly: }
832            ident: --element\ name
833            class: .class\ name
834            id: #_id
835            left_curly: {
836            ident: background
837            function: url(
838            string: "https://example.com/some url \"with\" 'spaces'.png"
839            right_parenthesis: )
840            function: url(
841            string: 'https://example.com/\'"quotes"\'.png'
842            right_parenthesis: )
843            semicolon: ;
844            right_curly: }
845        "#},
846        );
847    }
848
849    #[test]
850    fn parse_pseudo_functions() {
851        assert_lexer_snapshot(
852            indoc! {r#"
853            :local(.class#id, .class:not(*:hover)) { color: red; }
854            :import(something from ":somewhere") {}
855        "#},
856            indoc! {r#"
857            pseudo_function: :local(
858            class: .class
859            id: #id
860            comma: ,
861            class: .class
862            pseudo_function: :not(
863            pseudo_class: :hover
864            right_parenthesis: )
865            right_parenthesis: )
866            left_curly: {
867            ident: color
868            ident: red
869            semicolon: ;
870            right_curly: }
871            pseudo_function: :import(
872            ident: something
873            ident: from
874            string: ":somewhere"
875            right_parenthesis: )
876            left_curly: {
877            right_curly: }
878        "#},
879        );
880    }
881
882    #[test]
883    fn parse_at_rules() {
884        assert_lexer_snapshot(
885            indoc! {r#"
886            @media (max-size: 100px) {
887                @import "external.css";
888                body { color: red; }
889            }
890        "#},
891            indoc! {r#"
892            at_keyword: @media
893            left_parenthesis: (
894            ident: max-size
895            right_parenthesis: )
896            left_curly: {
897            at_keyword: @import
898            string: "external.css"
899            semicolon: ;
900            ident: body
901            left_curly: {
902            ident: color
903            ident: red
904            semicolon: ;
905            right_curly: }
906            right_curly: }
907        "#},
908        );
909    }
910
911    #[test]
912    fn parse_escape() {
913        assert_lexer_snapshot(
914            indoc! {r#"
915                body {
916                    a\
917                a: \
918                url(https://example\2f4a8f.com\
919                /image.png)
920                    b: url(#\
921                hash)
922                }
923            "#},
924            indoc! {r#"
925                ident: body
926                left_curly: {
927                ident: a\
928                a
929                url: https://example\2f4a8f.com\
930                /image.png
931                ident: b
932                url: #\
933                hash
934                right_curly: }
935            "#},
936        );
937    }
938
939    #[test]
940    fn parse_pseudo_elements() {
941        assert_lexer_snapshot(
942            indoc! {r#"
943                a::after {
944                    content: ' (' attr(href) ')';
945                }
946            "#},
947            indoc! {r#"
948                ident: a
949                pseudo_class: :after
950                left_curly: {
951                ident: content
952                string: ' ('
953                function: attr(
954                ident: href
955                right_parenthesis: )
956                string: ')'
957                semicolon: ;
958                right_curly: }
959            "#},
960        );
961    }
962
963    #[test]
964    fn parse_minimized_urls() {
965        assert_lexer_snapshot(
966            "body{background:url(./image.png)}",
967            indoc! {r#"
968                ident: body
969                left_curly: {
970                ident: background
971                pseudo_function: :url(
972                class: .
973                ident: image
974                class: .png
975                right_parenthesis: )
976                right_curly: }
977            "#},
978        );
979    }
980}