oak_tailwind/lexer/
mod.rs1use crate::{kind::TwigSyntaxKind, language::TwigLanguage};
2use oak_core::{Lexer, LexerCache, LexerState, OakError, lexer::LexOutput, source::Source};
3
4#[derive(Clone)]
5pub struct TwigLexer<'config> {
6 _config: &'config TwigLanguage,
8}
9
10type State<'a, S> = LexerState<'a, S, TwigLanguage>;
11
12impl<'config> TwigLexer<'config> {
13 pub fn new(config: &'config TwigLanguage) -> Self {
15 Self { _config: config }
16 }
17}
18
19impl<'config> Lexer<TwigLanguage> for TwigLexer<'config> {
20 fn lex<'a, S: Source + ?Sized>(&self, source: &'a S, _edits: &[oak_core::TextEdit], cache: &'a mut impl LexerCache<TwigLanguage>) -> LexOutput<TwigLanguage> {
21 let mut state = LexerState::new(source);
22 let result = self.run(&mut state);
23 if result.is_ok() {
24 state.add_eof();
25 }
26 state.finish_with_cache(result, cache)
27 }
28}
29
30impl<'config> TwigLexer<'config> {
31 fn run<'a, S: Source + ?Sized>(&self, state: &mut State<'a, S>) -> Result<(), OakError> {
32 while state.not_at_end() {
33 let safe_point = state.get_position();
34
35 if self.skip_whitespace(state) {
36 continue;
37 }
38
39 if self.skip_comment(state) {
40 continue;
41 }
42
43 if self.lex_string(state) {
44 continue;
45 }
46
47 if self.lex_number(state) {
48 continue;
49 }
50
51 if self.lex_punctuation(state) {
52 continue;
53 }
54
55 if self.lex_identifier(state) {
56 continue;
57 }
58
59 state.advance_if_dead_lock(safe_point);
60 }
61
62 Ok(())
63 }
64
65 fn skip_whitespace<'a, S: Source + ?Sized>(&self, state: &mut State<'a, S>) -> bool {
66 let start = state.get_position();
67 let mut found = false;
68
69 while let Some(ch) = state.peek() {
70 if ch.is_whitespace() {
71 state.advance(ch.len_utf8());
72 found = true;
73 }
74 else {
75 break;
76 }
77 }
78
79 if found {
80 state.add_token(TwigSyntaxKind::Whitespace, start, state.get_position());
81 }
82
83 found
84 }
85
86 fn skip_comment<'a, S: Source + ?Sized>(&self, state: &mut State<'a, S>) -> bool {
87 let start = state.get_position();
88 if state.consume_if_starts_with("{#") {
89 while state.not_at_end() {
90 if state.consume_if_starts_with("#}") {
91 break;
92 }
93 if let Some(ch) = state.peek() {
94 state.advance(ch.len_utf8());
95 }
96 }
97 state.add_token(TwigSyntaxKind::Comment, start, state.get_position());
98 return true;
99 }
100 false
101 }
102
103 fn lex_string<'a, S: Source + ?Sized>(&self, state: &mut State<'a, S>) -> bool {
104 let start = state.get_position();
105
106 if let Some(quote) = state.peek() {
107 if quote == '"' || quote == '\'' {
108 state.advance(1);
109
110 while let Some(ch) = state.peek() {
111 if ch == quote {
112 state.advance(1);
113 break;
114 }
115 else if ch == '\\' {
116 state.advance(1);
117 if let Some(_) = state.peek() {
118 state.advance(1);
119 }
120 }
121 else {
122 state.advance(ch.len_utf8());
123 }
124 }
125
126 state.add_token(TwigSyntaxKind::String, start, state.get_position());
127 return true;
128 }
129 }
130
131 false
132 }
133
134 fn lex_number<'a, S: Source + ?Sized>(&self, state: &mut State<'a, S>) -> bool {
135 let start = state.get_position();
136
137 if let Some(ch) = state.peek() {
138 if ch.is_ascii_digit() {
139 state.advance(1);
140
141 while let Some(ch) = state.peek() {
142 if ch.is_ascii_digit() || ch == '.' {
143 state.advance(1);
144 }
145 else {
146 break;
147 }
148 }
149
150 state.add_token(TwigSyntaxKind::Number, start, state.get_position());
151 return true;
152 }
153 }
154
155 false
156 }
157
158 fn lex_punctuation<'a, S: Source + ?Sized>(&self, state: &mut State<'a, S>) -> bool {
159 let start = state.get_position();
160 let rest = state.rest();
161
162 if rest.starts_with("{{") {
164 state.advance(2);
165 state.add_token(TwigSyntaxKind::DoubleLeftBrace, start, state.get_position());
166 return true;
167 }
168 if rest.starts_with("}}") {
169 state.advance(2);
170 state.add_token(TwigSyntaxKind::DoubleRightBrace, start, state.get_position());
171 return true;
172 }
173 if rest.starts_with("{%") {
174 state.advance(2);
175 state.add_token(TwigSyntaxKind::LeftBracePercent, start, state.get_position());
176 return true;
177 }
178 if rest.starts_with("%}") {
179 state.advance(2);
180 state.add_token(TwigSyntaxKind::PercentRightBrace, start, state.get_position());
181 return true;
182 }
183
184 if let Some(ch) = state.peek() {
186 let kind = match ch {
187 '{' => TwigSyntaxKind::LeftBrace,
188 '}' => TwigSyntaxKind::RightBrace,
189 '(' => TwigSyntaxKind::LeftParen,
190 ')' => TwigSyntaxKind::RightParen,
191 '[' => TwigSyntaxKind::LeftBracket,
192 ']' => TwigSyntaxKind::RightBracket,
193 ',' => TwigSyntaxKind::Comma,
194 '.' => TwigSyntaxKind::Dot,
195 ':' => TwigSyntaxKind::Colon,
196 ';' => TwigSyntaxKind::Semicolon,
197 '|' => TwigSyntaxKind::Pipe,
198 '=' => TwigSyntaxKind::Eq,
199 '+' => TwigSyntaxKind::Plus,
200 '-' => TwigSyntaxKind::Minus,
201 '*' => TwigSyntaxKind::Star,
202 '/' => TwigSyntaxKind::Slash,
203 '%' => TwigSyntaxKind::Percent,
204 '!' => TwigSyntaxKind::Bang,
205 '?' => TwigSyntaxKind::Question,
206 '<' => TwigSyntaxKind::Lt,
207 '>' => TwigSyntaxKind::Gt,
208 '&' => TwigSyntaxKind::Amp,
209 '^' => TwigSyntaxKind::Caret,
210 '~' => TwigSyntaxKind::Tilde,
211 _ => return false,
212 };
213
214 state.advance(1);
215 state.add_token(kind, start, state.get_position());
216 return true;
217 }
218
219 false
220 }
221
222 fn lex_identifier<'a, S: Source + ?Sized>(&self, state: &mut State<'a, S>) -> bool {
223 let start = state.get_position();
224
225 if let Some(ch) = state.peek() {
226 if ch.is_ascii_alphabetic() || ch == '_' {
227 state.advance(ch.len_utf8());
228
229 while let Some(ch) = state.peek() {
230 if ch.is_ascii_alphanumeric() || ch == '_' {
231 state.advance(ch.len_utf8());
232 }
233 else {
234 break;
235 }
236 }
237
238 let end = state.get_position();
239 let text = state.get_text_in((start..end).into());
240
241 let kind = match text.as_ref() {
243 "true" | "false" => TwigSyntaxKind::Boolean,
244 _ => TwigSyntaxKind::Identifier,
245 };
246 state.add_token(kind, start, end);
247 return true;
248 }
249 }
250 false
251 }
252}