any_lexer/lexers/
css.rs

1use text_scanner::{ext::CssScannerExt, Scanner};
2
3use crate::{impl_lexer_from_scanner, ScanToken, ScannerExt, TokenSpan};
4
5const DELIMITERS: [char; 6] = ['{', '}', '[', ']', '(', ')'];
6const PUNCTUATIONS: [char; 12] = [',', '.', ';', ':', '-', '+', '*', '=', '#', '!', '@', '%'];
7
8#[derive(PartialEq, Eq, Clone, Copy, Debug)]
9pub enum CssToken {
10    Space,
11    BlockComment,
12    Ident,
13    AtKeyword,
14    Hash,
15    String,
16    Number,
17    /// Punctuation e.g. `:`, `,`.
18    Punct,
19    /// Delimiter e.g. `{`, `}`, `[`, and `]`.
20    Delim,
21    /// Given valid CSS, then this variant should never be encountered. If
22    /// is is encountered, then check if an issue has already been submitted,
23    /// otherwise please [submit an issue].
24    ///
25    /// [submit an issue]: https://github.com/vallentin/colorblast/issues
26    Unknown,
27}
28
29impl ScanToken for CssToken {
30    fn scan_token<'text>(scanner: &mut Scanner<'text>) -> Option<(Self, TokenSpan<'text>)> {
31        let (r, _s) = scanner.skip_whitespace();
32        if !r.is_empty() {
33            return Some((Self::Space, scanner.span(r)));
34        }
35
36        if let Ok((r, _c)) = scanner.scan_css_identifier() {
37            return Some((Self::Ident, scanner.span(r)));
38        } else if let Ok((r, _c)) = scanner.scan_css_at_keyword() {
39            return Some((Self::AtKeyword, scanner.span(r)));
40        } else if let Ok((r, _c)) = scanner.scan_css_hash() {
41            return Some((Self::Hash, scanner.span(r)));
42        }
43
44        if let Ok((r, _c)) = scanner.scan_css_number() {
45            return Some((Self::Number, scanner.span(r)));
46        } else if let Ok((r, _c)) = scanner.scan_css_string() {
47            return Some((Self::String, scanner.span(r)));
48        }
49
50        if let Ok((r, _c)) = scanner.accept_char_any(&DELIMITERS) {
51            return Some((Self::Delim, scanner.span(r)));
52        } else if let Ok((r, _c)) = scanner.accept_char_any(&PUNCTUATIONS) {
53            return Some((Self::Punct, scanner.span(r)));
54        }
55
56        if let Ok((r, _c)) = scanner.scan_css_block_comment() {
57            return Some((Self::BlockComment, scanner.span(r)));
58        }
59
60        let (r, _c) = scanner.next().ok()?;
61        Some((Self::Unknown, scanner.span(r)))
62    }
63}
64
65/// CSS lexer producing [`CssToken`]s.
66///
67/// **Note:** Cloning `CssLexer` is essentially a copy, as it just contains
68/// a `&str` and a `usize` for its `cursor`. However, `Copy` is not
69/// implemented, to avoid accidentally copying immutable `CssLexer`s.
70///
71/// See also [`ScssLexer`].
72///
73/// [`ScssLexer`]: super::ScssLexer
74#[derive(Clone, Debug)]
75pub struct CssLexer<'text> {
76    scanner: Scanner<'text>,
77}
78
79impl<'text> CssLexer<'text> {
80    #[inline]
81    pub fn new(text: &'text str) -> Self {
82        Self {
83            scanner: Scanner::new(text),
84        }
85    }
86}
87
88impl_lexer_from_scanner!('text, CssLexer<'text>, CssToken, scanner);
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_css_lexer_spans() {
96        // This intentionally uses Rust code as input, as it is
97        // only testing that CssLexer returns all characters
98        let input = include_str!("../../../text-scanner/src/ext/rust.rs");
99        let mut output = String::new();
100
101        let lexer = CssLexer::new(input);
102        for (_tok, span) in lexer {
103            output.push_str(span.as_str());
104        }
105
106        assert_eq!(input, output);
107    }
108}