Skip to main content

oak_tailwind/lexer/
mod.rs

1#![doc = include_str!("readme.md")]
2/// Token types for the Tailwind language.
3pub mod token_type;
4
5use crate::{language::TailwindLanguage, lexer::token_type::TailwindTokenType};
6use oak_core::{Lexer, LexerCache, LexerState, OakError, lexer::LexOutput, source::Source};
7
8/// Lexer for the Tailwind language.
9#[derive(Clone, Debug, Default)]
10pub struct TailwindLexer {
11    /// Language configuration
12    pub config: TailwindLanguage,
13}
14
15pub(crate) type State<'a, S> = LexerState<'a, S, TailwindLanguage>;
16
17impl TailwindLexer {
18    /// Creates a new `TailwindLexer` with the given configuration.
19    pub fn new(config: TailwindLanguage) -> Self {
20        Self { config }
21    }
22}
23
24impl Lexer<TailwindLanguage> for TailwindLexer {
25    /// Tokenizes the source text into a sequence of Tailwind tokens.
26    fn lex<'a, S: Source + ?Sized>(&self, source: &S, _edits: &[oak_core::TextEdit], cache: &'a mut impl LexerCache<TailwindLanguage>) -> LexOutput<TailwindLanguage> {
27        let mut state = LexerState::new(source);
28        let result = self.run(&mut state);
29        if result.is_ok() {
30            state.add_eof()
31        }
32        state.finish_with_cache(result, cache)
33    }
34}
35
36impl TailwindLexer {
37    /// Runs the lexer logic.
38    pub fn run<S: Source + ?Sized>(&self, state: &mut State<'_, S>) -> Result<(), OakError> {
39        while state.not_at_end() {
40            let safe_point = state.get_position();
41
42            if self.skip_whitespace(state) {
43                continue;
44            }
45
46            if self.lex_comment(state) {
47                continue;
48            }
49
50            if self.lex_directive(state) {
51                continue;
52            }
53
54            if self.lex_tailwind_class_part(state) {
55                continue;
56            }
57
58            if self.lex_punctuation(state) {
59                continue;
60            }
61
62            state.advance_if_dead_lock(safe_point)
63        }
64
65        Ok(())
66    }
67
68    /// Skips whitespace characters.
69    pub fn skip_whitespace<S: Source + ?Sized>(&self, state: &mut State<'_, S>) -> bool {
70        let start = state.get_position();
71        let mut found = false;
72
73        while let Some(ch) = state.peek() {
74            if ch.is_whitespace() {
75                state.advance(ch.len_utf8());
76                found = true
77            }
78            else {
79                break;
80            }
81        }
82
83        if found {
84            state.add_token(TailwindTokenType::Whitespace, start, state.get_position())
85        }
86
87        found
88    }
89
90    /// Lexes a comment.
91    pub fn lex_comment<S: Source + ?Sized>(&self, state: &mut State<'_, S>) -> bool {
92        let start = state.get_position();
93        if state.consume_if_starts_with("/*") {
94            while state.not_at_end() {
95                if state.consume_if_starts_with("*/") {
96                    break;
97                }
98                if let Some(ch) = state.peek() {
99                    state.advance(ch.len_utf8())
100                }
101            }
102            state.add_token(TailwindTokenType::Comment, start, state.get_position());
103            return true;
104        }
105        if state.consume_if_starts_with("//") {
106            while state.not_at_end() {
107                if let Some(ch) = state.peek() {
108                    if ch == '\n' {
109                        break;
110                    }
111                    state.advance(ch.len_utf8())
112                }
113            }
114            state.add_token(TailwindTokenType::Comment, start, state.get_position());
115            return true;
116        }
117        false
118    }
119
120    /// Lexes a directive like @tailwind, @apply.
121    pub fn lex_directive<S: Source + ?Sized>(&self, state: &mut State<'_, S>) -> bool {
122        let start = state.get_position();
123        if state.consume_if_starts_with("@") {
124            while let Some(ch) = state.peek() {
125                if ch.is_alphabetic() || ch == '-' {
126                    state.advance(ch.len_utf8());
127                }
128                else {
129                    break;
130                }
131            }
132            state.add_token(TailwindTokenType::Directive, start, state.get_position());
133            return true;
134        }
135        false
136    }
137
138    /// Lexes a part of a Tailwind class (modifier, utility, or arbitrary value).
139    pub fn lex_tailwind_class_part<S: Source + ?Sized>(&self, state: &mut State<'_, S>) -> bool {
140        let start = state.get_position();
141
142        if state.consume_if_starts_with("!") {
143            state.add_token(TailwindTokenType::Important, start, state.get_position());
144            return true;
145        }
146
147        if state.peek() == Some('[') {
148            return self.lex_arbitrary_value(state);
149        }
150
151        // Try lexing a modifier or utility
152        let mut has_content = false;
153        let _current_pos = state.get_position();
154
155        while let Some(ch) = state.peek() {
156            if ch.is_alphanumeric() || ch == '-' || ch == '/' || ch == '.' || ch == '_' {
157                state.advance(ch.len_utf8());
158                has_content = true;
159
160                if state.peek() == Some(':') {
161                    state.advance(':'.len_utf8());
162                    state.add_token(TailwindTokenType::Modifier, start, state.get_position());
163                    return true;
164                }
165            }
166            else {
167                break;
168            }
169        }
170
171        if has_content {
172            state.add_token(TailwindTokenType::Utility, start, state.get_position());
173            return true;
174        }
175
176        false
177    }
178
179    /// Lexes an arbitrary value like [100px].
180    pub fn lex_arbitrary_value<S: Source + ?Sized>(&self, state: &mut State<'_, S>) -> bool {
181        let start = state.get_position();
182        if state.consume_if_starts_with("[") {
183            let mut depth = 1;
184            while state.not_at_end() && depth > 0 {
185                if let Some(ch) = state.peek() {
186                    if ch == '[' {
187                        depth += 1;
188                    }
189                    else if ch == ']' {
190                        depth -= 1;
191                    }
192                    state.advance(ch.len_utf8());
193                }
194                else {
195                    break;
196                }
197            }
198            state.add_token(TailwindTokenType::ArbitraryValue, start, state.get_position());
199            return true;
200        }
201        false
202    }
203
204    /// Lexes punctuation.
205    pub fn lex_punctuation<S: Source + ?Sized>(&self, state: &mut State<'_, S>) -> bool {
206        let start = state.get_position();
207
208        macro_rules! check {
209            ($s:expr, $t:ident) => {
210                if state.consume_if_starts_with($s) {
211                    state.add_token(TailwindTokenType::$t, start, state.get_position());
212                    return true;
213                }
214            };
215        }
216
217        check!("[", LeftBracket);
218        check!("]", RightBracket);
219        check!("(", LeftParen);
220        check!(")", RightParen);
221        check!(":", Colon);
222        check!(";", Semicolon);
223        check!("@", At);
224        check!("!", Bang);
225        check!("-", Dash);
226        check!("/", Slash);
227        check!(".", Dot);
228        check!("#", Hash);
229        check!(",", Comma);
230
231        false
232    }
233}