Skip to main content

ferroui_base/rendering/composition/expressions/
token_parser.rs

1//! Helper for the composition expression parser.
2
3/// Whether a character is white space as the reference runtime defines it
4/// (the Unicode `White_Space` set plus the information separators
5/// U+001C..U+001F).
6#[inline]
7fn is_white_space(ch: char) -> bool {
8    ch.is_whitespace() || ('\u{1c}'..='\u{1f}').contains(&ch)
9}
10
11#[inline]
12fn is_alpha_numeric(ch: u8) -> bool {
13    ch.is_ascii_alphanumeric()
14}
15
16/// Culture-invariant lower-casing of a single character (a character whose
17/// lower-case form is not a single character is left unchanged).
18#[inline]
19fn to_lower_invariant(ch: char) -> char {
20    if ch.is_ascii() {
21        return ch.to_ascii_lowercase();
22    }
23    let mut lower = ch.to_lowercase();
24    match (lower.next(), lower.next()) {
25        (Some(single), None) => single,
26        _ => ch,
27    }
28}
29
30/// Helper for the composition expression parser: a cursor over the text
31/// that consumes tokens from its start.
32///
33/// [`TokenParser::position`] and [`TokenParser::length`] count UTF-16 code
34/// units, the unit of the reference implementation.
35#[derive(Clone, Copy, Debug)]
36pub struct TokenParser<'a> {
37    s: &'a str,
38    position: i32,
39}
40
41impl<'a> TokenParser<'a> {
42    /// Creates a parser over the given text.
43    pub fn new(s: &'a str) -> Self {
44        Self { s, position: 0 }
45    }
46
47    /// The number of UTF-16 code units consumed so far.
48    pub fn position(&self) -> i32 {
49        self.position
50    }
51
52    /// Consumes `bytes` bytes that are `units` UTF-16 code units long.
53    #[inline]
54    fn advance_raw(&mut self, bytes: usize, units: usize) {
55        self.s = &self.s[bytes..];
56        self.position += units as i32;
57    }
58
59    #[inline]
60    fn first_byte(&self) -> Option<u8> {
61        self.s.as_bytes().first().copied()
62    }
63
64    pub fn skip_whitespace(&mut self) {
65        while let Some(ch) = self.s.chars().next() {
66            if is_white_space(ch) {
67                self.advance_raw(ch.len_utf8(), ch.len_utf16());
68            } else {
69                return;
70            }
71        }
72    }
73
74    pub fn next_is_whitespace(&self) -> bool {
75        self.s.chars().next().is_some_and(is_white_space)
76    }
77
78    /// Consumes the given character if it is next (after white space).
79    pub fn try_consume(&mut self, c: char) -> bool {
80        self.skip_whitespace();
81        if !self.s.starts_with(c) {
82            return false;
83        }
84
85        self.advance_raw(c.len_utf8(), c.len_utf16());
86        true
87    }
88
89    /// Consumes the given text if it is next (after white space).
90    pub fn try_consume_str(&mut self, s: &str) -> bool {
91        self.skip_whitespace();
92        if !self.s.starts_with(s) {
93            return false;
94        }
95
96        self.advance_raw(s.len(), s.encode_utf16().count());
97        true
98    }
99
100    /// Consumes whichever of the given characters is next (after white space).
101    pub fn try_consume_any(&mut self, chars: &str) -> Option<char> {
102        self.skip_whitespace();
103        let first = self.s.chars().next()?;
104
105        for c in chars.chars() {
106            if c == first {
107                self.advance_raw(c.len_utf8(), c.len_utf16());
108                return Some(c);
109            }
110        }
111
112        None
113    }
114
115    /// Consumes the given keyword if it is next (after white space) and is
116    /// not followed by a letter or a digit.
117    pub fn try_parse_keyword(&mut self, keyword: &str) -> bool {
118        self.skip_whitespace();
119        if !self.s.starts_with(keyword) {
120            return false;
121        }
122
123        if self.s.as_bytes().get(keyword.len()).copied().is_some_and(is_alpha_numeric) {
124            return false;
125        }
126
127        self.advance_raw(keyword.len(), keyword.encode_utf16().count());
128        true
129    }
130
131    /// Like [`TokenParser::try_parse_keyword`], ignoring the case of the
132    /// text; the keyword must be given in lower case.
133    pub fn try_parse_keyword_lower_case(&mut self, keyword_in_lower_case: &str) -> bool {
134        self.skip_whitespace();
135        let mut bytes = 0;
136        let mut units = 0;
137        let mut text = self.s.chars();
138        for expected in keyword_in_lower_case.chars() {
139            match text.next() {
140                Some(ch) if to_lower_invariant(ch) == expected => {
141                    bytes += ch.len_utf8();
142                    units += ch.len_utf16();
143                }
144                _ => return false,
145            }
146        }
147
148        if self.s.as_bytes().get(bytes).copied().is_some_and(is_alpha_numeric) {
149            return false;
150        }
151
152        self.advance_raw(bytes, units);
153        true
154    }
155
156    /// Consumes `c` UTF-16 code units.
157    pub fn advance(&mut self, c: i32) {
158        let mut bytes = 0;
159        let mut units = 0;
160        for ch in self.s.chars() {
161            if units >= c.max(0) as usize {
162                break;
163            }
164            bytes += ch.len_utf8();
165            units += ch.len_utf16();
166        }
167        self.advance_raw(bytes, units);
168    }
169
170    /// The number of UTF-16 code units left.
171    pub fn length(&self) -> i32 {
172        if self.s.is_ascii() {
173            self.s.len() as i32
174        } else {
175            self.s.encode_utf16().count() as i32
176        }
177    }
178
179    /// Consumes an identifier: an ASCII letter followed by ASCII letters,
180    /// digits and any of the extra valid characters.
181    pub fn try_parse_identifier_with(&mut self, extra_valid_chars: &str) -> Option<&'a str> {
182        self.skip_whitespace();
183        let first = self.first_byte()?;
184        if !first.is_ascii_alphabetic() {
185            return None;
186        }
187        let mut len = 1;
188        for ch in self.s[1..].chars() {
189            if (ch.is_ascii() && is_alpha_numeric(ch as u8)) || extra_valid_chars.contains(ch) {
190                len += ch.len_utf8();
191            } else {
192                break;
193            }
194        }
195
196        let res = &self.s[..len];
197        self.advance_raw(len, res.encode_utf16().count());
198        Some(res)
199    }
200
201    /// Consumes an identifier: an ASCII letter followed by ASCII letters and digits.
202    pub fn try_parse_identifier(&mut self) -> Option<&'a str> {
203        self.skip_whitespace();
204        let first = self.first_byte()?;
205        if !first.is_ascii_alphabetic() {
206            return None;
207        }
208        let bytes = self.s.as_bytes();
209        let mut len = 1;
210        while len < bytes.len() && is_alpha_numeric(bytes[len]) {
211            len += 1;
212        }
213
214        let res = &self.s[..len];
215        self.advance_raw(len, len);
216        Some(res)
217    }
218
219    /// Consumes a function name (an identifier that may contain dots)
220    /// together with the opening parenthesis that follows it. Nothing is
221    /// consumed when the name is not followed by a parenthesis.
222    pub fn try_parse_call(&mut self) -> Option<&'a str> {
223        self.skip_whitespace();
224        let first = self.first_byte()?;
225        if !first.is_ascii_alphabetic() {
226            return None;
227        }
228        let bytes = self.s.as_bytes();
229        let mut len = 1;
230        while len < bytes.len() && (is_alpha_numeric(bytes[len]) || bytes[len] == b'.') {
231            len += 1;
232        }
233
234        let res = &self.s[..len];
235
236        // Find '('
237        let mut consumed_bytes = len;
238        let mut consumed_units = len;
239        for ch in self.s[len..].chars() {
240            consumed_bytes += ch.len_utf8();
241            consumed_units += ch.len_utf16();
242            if is_white_space(ch) {
243                continue;
244            }
245            if ch == '(' {
246                self.advance_raw(consumed_bytes, consumed_units);
247                return Some(res);
248            }
249
250            return None;
251        }
252
253        None
254    }
255
256    /// The length of the number at the start of the text: digits, at most
257    /// one decimal point, and a leading minus sign. `None` when a minus sign
258    /// follows the number and `fail_on_inner_minus` is set.
259    ///
260    /// The scanned text is handed to the standard float parser, which agrees
261    /// with the reference runtime's invariant number parsing on every text
262    /// this scanner can produce (at least one digit is required; a value out
263    /// of range is infinite).
264    fn scan_number(&self, fail_on_inner_minus: bool) -> Option<usize> {
265        let mut len = 0;
266        let mut dot_count = 0;
267        for (c, &ch) in self.s.as_bytes().iter().enumerate() {
268            if ch.is_ascii_digit() {
269                len = c + 1;
270            } else if ch == b'.' && dot_count == 0 {
271                len = c + 1;
272                dot_count += 1;
273            } else if ch == b'-' {
274                if len != 0 {
275                    if fail_on_inner_minus {
276                        return None;
277                    }
278                    break;
279                }
280                len = c + 1;
281            } else {
282                break;
283            }
284        }
285        Some(len)
286    }
287
288    /// Consumes a single-precision number.
289    pub fn try_parse_float(&mut self) -> Option<f32> {
290        self.skip_whitespace();
291        if self.s.is_empty() {
292            return None;
293        }
294
295        let len = self.scan_number(false)?;
296        let res = self.s[..len].parse::<f32>().ok()?;
297        self.advance_raw(len, len);
298        Some(res)
299    }
300
301    /// Consumes a double-precision number.
302    pub fn try_parse_double(&mut self) -> Option<f64> {
303        self.skip_whitespace();
304        if self.s.is_empty() {
305            return None;
306        }
307
308        let len = self.scan_number(true)?;
309        let res = self.s[..len].parse::<f64>().ok()?;
310        self.advance_raw(len, len);
311        Some(res)
312    }
313
314    /// Whether only white space is left (which is consumed).
315    pub fn is_eof_with_whitespace(&mut self) -> bool {
316        self.skip_whitespace();
317        self.s.is_empty()
318    }
319
320    /// The text that has not been consumed yet.
321    pub fn as_str(&self) -> &'a str {
322        self.s
323    }
324}
325
326impl std::fmt::Display for TokenParser<'_> {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        f.write_str(self.s)
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn consumes_characters_and_strings_after_white_space() {
338        let mut p = TokenParser::new("  ( >= x");
339        assert!(!p.try_consume(')'));
340        assert_eq!(p.position(), 2);
341        assert!(p.try_consume('('));
342        assert!(!p.try_consume_str(">>"));
343        assert!(p.try_consume_str(">="));
344        assert_eq!(p.try_consume_any("+-"), None);
345        assert_eq!(p.try_consume_any("yx"), Some('x'));
346        assert_eq!(p.length(), 0);
347        assert_eq!(p.position(), 8);
348        assert!(!p.try_consume('x'));
349        assert_eq!(p.try_consume_any("x"), None);
350        assert!(p.is_eof_with_whitespace());
351    }
352
353    #[test]
354    fn white_space_follows_the_reference_definition() {
355        let mut p = TokenParser::new("\u{1c}\u{a0}\u{2003}\t\r\nx");
356        assert!(p.next_is_whitespace());
357        p.skip_whitespace();
358        assert_eq!(p.position(), 6);
359        assert_eq!(p.to_string(), "x");
360        assert!(!p.next_is_whitespace());
361    }
362
363    #[test]
364    fn parses_keywords() {
365        let mut p = TokenParser::new(" This.TARGET.Offset");
366        assert!(!p.try_parse_keyword("this.target"));
367        assert!(p.try_parse_keyword_lower_case("this.target"));
368        assert_eq!(p.as_str(), ".Offset");
369        assert_eq!(p.position(), 12);
370
371        let mut p = TokenParser::new("pixel");
372        assert!(!p.try_parse_keyword_lower_case("pi"));
373        assert!(!p.try_parse_keyword("pi"));
374        assert_eq!(p.position(), 0);
375
376        let mut p = TokenParser::new("pi");
377        assert!(p.try_parse_keyword("pi"));
378        assert_eq!(p.length(), 0);
379
380        let mut p = TokenParser::new("p");
381        assert!(!p.try_parse_keyword("pi"));
382        assert!(!p.try_parse_keyword_lower_case("pi"));
383
384        let mut p = TokenParser::new("PI*2");
385        assert!(p.try_parse_keyword_lower_case("pi"));
386        assert_eq!(p.as_str(), "*2");
387    }
388
389    #[test]
390    fn parses_identifiers() {
391        let mut p = TokenParser::new(" abc1_d");
392        assert_eq!(p.try_parse_identifier(), Some("abc1"));
393        assert_eq!(p.try_parse_identifier(), None);
394        assert_eq!(p.as_str(), "_d");
395
396        let mut p = TokenParser::new("abc1_d.e f");
397        assert_eq!(p.try_parse_identifier_with("_."), Some("abc1_d.e"));
398        assert_eq!(p.position(), 8);
399
400        assert_eq!(TokenParser::new("1abc").try_parse_identifier(), None);
401        assert_eq!(TokenParser::new("1abc").try_parse_identifier_with("1"), None);
402        assert_eq!(TokenParser::new("").try_parse_identifier(), None);
403    }
404
405    #[test]
406    fn parses_calls() {
407        let mut p = TokenParser::new(" Matrix3x2.CreateScale  (1)");
408        assert_eq!(p.try_parse_call(), Some("Matrix3x2.CreateScale"));
409        assert_eq!(p.as_str(), "1)");
410        assert_eq!(p.position(), 25);
411
412        // Nothing but leading white space is consumed when there is no parenthesis.
413        let mut p = TokenParser::new(" abc + (1)");
414        assert_eq!(p.try_parse_call(), None);
415        assert_eq!(p.as_str(), "abc + (1)");
416        assert_eq!(TokenParser::new("abc").try_parse_call(), None);
417        assert_eq!(TokenParser::new("(1)").try_parse_call(), None);
418    }
419
420    #[test]
421    fn parses_numbers() {
422        let mut p = TokenParser::new(" 4.0-0.5)");
423        assert_eq!(p.try_parse_float(), Some(4.0));
424        assert_eq!(p.as_str(), "-0.5)");
425        assert_eq!(p.try_parse_float(), Some(-0.5));
426        assert_eq!(p.try_parse_float(), None);
427        assert_eq!(p.as_str(), ")");
428
429        assert_eq!(TokenParser::new("1.").try_parse_float(), Some(1.0));
430        assert_eq!(TokenParser::new(".5").try_parse_float(), Some(0.5));
431        assert_eq!(TokenParser::new("-.5").try_parse_float(), Some(-0.5));
432        assert_eq!(TokenParser::new("0.1").try_parse_float(), Some(0.1));
433        assert_eq!(TokenParser::new("-").try_parse_float(), None);
434        assert_eq!(TokenParser::new(".").try_parse_float(), None);
435        assert_eq!(TokenParser::new("-.").try_parse_float(), None);
436        assert_eq!(TokenParser::new("").try_parse_float(), None);
437        assert_eq!(TokenParser::new("x").try_parse_float(), None);
438        assert_eq!(
439            TokenParser::new("99999999999999999999999999999999999999999999").try_parse_float(),
440            Some(f32::INFINITY)
441        );
442
443        let mut p = TokenParser::new("1.2.3");
444        assert_eq!(p.try_parse_float(), Some(1.2));
445        assert_eq!(p.as_str(), ".3");
446
447        assert_eq!(TokenParser::new("2.5e").try_parse_double(), Some(2.5));
448        assert_eq!(TokenParser::new("-2.5").try_parse_double(), Some(-2.5));
449        // Unlike the single-precision variant, a minus sign after the number fails.
450        assert_eq!(TokenParser::new("4.0-0.5").try_parse_double(), None);
451    }
452
453    #[test]
454    fn advance_counts_utf16_code_units() {
455        let mut p = TokenParser::new("a\u{e9}\u{1f600}b");
456        assert_eq!(p.length(), 5);
457        p.advance(2);
458        assert_eq!(p.position(), 2);
459        p.advance(2);
460        assert_eq!(p.position(), 4);
461        assert_eq!(p.as_str(), "b");
462    }
463}