Skip to main content

ferroui_base/utilities/
identifier_parser.rs

1//! Identifier and number parsing on top of [`CharacterReader`].
2
3use super::character_reader::{is_digit, is_identifier_part_category, is_letter, CharacterReader};
4
5impl<'a> CharacterReader<'a> {
6    /// Parses an identifier: a letter or `_` followed by letters, `_`, marks,
7    /// connector punctuation, format characters and decimal digits. Returns an
8    /// empty string (consuming nothing) when the input does not start with an
9    /// identifier.
10    pub fn parse_identifier(&mut self) -> &'a str {
11        if self.peek().is_some_and(is_valid_identifier_start) {
12            self.take_while(is_valid_identifier_char)
13        } else {
14            ""
15        }
16    }
17
18    /// Parses an identifier that may name a nested type, e.g. "Outer+Inner".
19    pub fn parse_type_identifier(&mut self) -> &'a str {
20        if self.peek().is_some_and(is_valid_identifier_start) {
21            self.take_while(|c| is_valid_identifier_char(c) || c == '+')
22        } else {
23            ""
24        }
25    }
26
27    /// Parses a run of decimal digits (possibly empty).
28    pub fn parse_number(&mut self) -> &'a str {
29        self.take_while(is_valid_number_char)
30    }
31}
32
33fn is_valid_identifier_start(c: char) -> bool {
34    is_letter(c) || c == '_'
35}
36
37fn is_valid_identifier_char(c: char) -> bool {
38    is_valid_identifier_start(c) || is_identifier_part_category(c)
39}
40
41fn is_valid_number_char(c: char) -> bool {
42    is_digit(c)
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn parses_identifier() {
51        let mut r = CharacterReader::new("_Foo1_b\u{0301}.Bar");
52        assert_eq!(r.parse_identifier(), "_Foo1_b\u{0301}");
53        assert_eq!(r.position(), 8);
54        assert_eq!(r.peek(), Some('.'));
55        assert_eq!(r.parse_identifier(), "");
56        assert_eq!(r.position(), 8);
57    }
58
59    #[test]
60    fn identifier_cannot_start_with_digit_or_symbol() {
61        assert_eq!(CharacterReader::new("1Foo").parse_identifier(), "");
62        assert_eq!(CharacterReader::new("%Foo").parse_identifier(), "");
63        assert_eq!(CharacterReader::new("-Foo").parse_identifier(), "");
64        assert_eq!(CharacterReader::new("").parse_identifier(), "");
65    }
66
67    #[test]
68    fn identifier_stops_at_plus_and_dash() {
69        assert_eq!(CharacterReader::new("Outer+Inner").parse_identifier(), "Outer");
70        assert_eq!(CharacterReader::new("min-width").parse_identifier(), "min");
71    }
72
73    #[test]
74    fn parses_unicode_identifier() {
75        assert_eq!(CharacterReader::new("Żółw9 x").parse_identifier(), "Żółw9");
76    }
77
78    #[test]
79    fn parses_type_identifier() {
80        let mut r = CharacterReader::new("Outer+Inner)");
81        assert_eq!(r.parse_type_identifier(), "Outer+Inner");
82        assert_eq!(r.peek(), Some(')'));
83        assert_eq!(CharacterReader::new("+Inner").parse_type_identifier(), "");
84        assert_eq!(CharacterReader::new("").parse_type_identifier(), "");
85    }
86
87    #[test]
88    fn parses_number() {
89        let mut r = CharacterReader::new("123px");
90        assert_eq!(r.parse_number(), "123");
91        assert_eq!(r.parse_number(), "");
92        assert_eq!(r.position(), 3);
93        assert_eq!(CharacterReader::new("-1").parse_number(), "");
94    }
95}