ferroui_base/utilities/
character_reader.rs1#[inline]
24fn is_bmp(c: char) -> bool {
25 (c as u32) < 0x1_0000
26}
27
28#[inline]
31pub fn is_white_space(c: char) -> bool {
32 c.is_whitespace() || matches!(c, '\u{1C}'..='\u{1F}')
33}
34
35#[inline]
43pub fn is_letter(c: char) -> bool {
44 if c.is_ascii() {
45 return c.is_ascii_alphabetic();
46 }
47 is_bmp(c) && c.is_alphabetic() && !c.is_numeric() && !matches!(c, '\u{24B6}'..='\u{24E9}')
48}
49
50#[inline]
56pub fn is_digit(c: char) -> bool {
57 if c.is_ascii() {
58 return c.is_ascii_digit();
59 }
60 is_bmp(c) && c.is_numeric() && !c.is_alphabetic()
61}
62
63#[inline]
65fn is_connector_punctuation(c: char) -> bool {
66 matches!(
67 c,
68 '_' | '\u{203F}'
69 | '\u{2040}'
70 | '\u{2054}'
71 | '\u{FE33}'
72 | '\u{FE34}'
73 | '\u{FE4D}'..='\u{FE4F}'
74 | '\u{FF3F}'
75 )
76}
77
78#[inline]
80fn is_format(c: char) -> bool {
81 matches!(
82 c,
83 '\u{00AD}'
84 | '\u{0600}'..='\u{0605}'
85 | '\u{061C}'
86 | '\u{06DD}'
87 | '\u{070F}'
88 | '\u{0890}'..='\u{0891}'
89 | '\u{08E2}'
90 | '\u{180E}'
91 | '\u{200B}'..='\u{200F}'
92 | '\u{202A}'..='\u{202E}'
93 | '\u{2060}'..='\u{2064}'
94 | '\u{2066}'..='\u{206F}'
95 | '\u{FEFF}'
96 | '\u{FFF9}'..='\u{FFFB}'
97 )
98}
99
100#[inline]
108pub(crate) fn is_identifier_part_category(c: char) -> bool {
109 if c.is_ascii() {
110 return c.is_ascii_digit() || c == '_';
111 }
112 matches!(c, '\u{0300}'..='\u{036F}')
113 || is_connector_punctuation(c)
114 || is_format(c)
115 || is_digit(c)
116}
117
118#[derive(Clone, Debug)]
120pub struct CharacterReader<'a> {
121 s: &'a str,
122 position: i32,
123}
124
125impl<'a> CharacterReader<'a> {
126 #[inline]
128 pub fn new(s: &'a str) -> Self {
129 Self { s, position: 0 }
130 }
131
132 #[inline]
134 pub fn end(&self) -> bool {
135 self.s.is_empty()
136 }
137
138 #[inline]
140 pub fn peek(&self) -> Option<char> {
141 self.s.chars().next()
142 }
143
144 #[inline]
148 pub fn position(&self) -> i32 {
149 self.position
150 }
151
152 #[inline]
154 pub fn take(&mut self) -> Option<char> {
155 let taken = self.peek()?;
156 self.position += 1;
157 self.s = &self.s[taken.len_utf8()..];
158 Some(taken)
159 }
160
161 pub fn skip_whitespace(&mut self) {
163 let len = self.peek_whitespace().len();
164 self.advance(len);
165 }
166
167 #[inline]
169 pub fn take_if(&mut self, c: char) -> bool {
170 if self.peek() == Some(c) {
171 self.take();
172 true
173 } else {
174 false
175 }
176 }
177
178 pub fn take_if_str(&mut self, s: &str) -> bool {
180 if self.s.starts_with(s) {
181 self.advance(s.len());
182 true
183 } else {
184 false
185 }
186 }
187
188 #[inline]
190 pub fn take_if_fn(&mut self, condition: impl Fn(char) -> bool) -> bool {
191 match self.peek() {
192 Some(c) if condition(c) => {
193 self.take();
194 true
195 }
196 _ => false,
197 }
198 }
199
200 pub fn take_until(&mut self, c: char) -> &'a str {
203 let len = self.s.find(c).unwrap_or(self.s.len());
204 self.advance(len)
205 }
206
207 pub fn take_while(&mut self, condition: impl Fn(char) -> bool) -> &'a str {
210 let len = self
211 .s
212 .char_indices()
213 .find(|&(_, c)| !condition(c))
214 .map_or(self.s.len(), |(i, _)| i);
215 self.advance(len)
216 }
217
218 pub fn try_peek(&self, count: usize) -> &'a str {
221 match self.byte_offset(count) {
222 Some(len) => &self.s[..len],
223 None => "",
224 }
225 }
226
227 pub fn peek_whitespace(&self) -> &'a str {
229 let trimmed = self.s.trim_start_matches(is_white_space);
230 &self.s[..self.s.len() - trimmed.len()]
231 }
232
233 pub fn skip(&mut self, count: usize) {
241 match self.byte_offset(count) {
242 Some(len) => self.s = &self.s[len..],
243 None => panic!("CharacterReader::skip: count is past the end of the input"),
244 }
245 }
246
247 fn byte_offset(&self, count: usize) -> Option<usize> {
250 if count == 0 {
251 return Some(0);
252 }
253 self.s
254 .char_indices()
255 .nth(count - 1)
256 .map(|(i, c)| i + c.len_utf8())
257 }
258
259 fn advance(&mut self, len: usize) -> &'a str {
262 let (taken, rest) = self.s.split_at(len);
263 self.position += taken.chars().count() as i32;
264 self.s = rest;
265 taken
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn peek_take_and_position() {
275 let mut r = CharacterReader::new("aé𝒜");
276 assert!(!r.end());
277 assert_eq!(r.peek(), Some('a'));
278 assert_eq!(r.take(), Some('a'));
279 assert_eq!(r.position(), 1);
280 assert_eq!(r.take(), Some('é'));
281 assert_eq!(r.take(), Some('𝒜'));
282 assert_eq!(r.position(), 3);
283 assert!(r.end());
284 assert_eq!(r.peek(), None);
285 assert_eq!(r.take(), None);
286 assert_eq!(r.position(), 3);
287 }
288
289 #[test]
290 fn take_if_variants() {
291 let mut r = CharacterReader::new("?.ab");
292 assert!(!r.take_if('.'));
293 assert!(!r.take_if_str("?!"));
294 assert!(r.take_if_str("?."));
295 assert_eq!(r.position(), 2);
296 assert!(r.take_if('a'));
297 assert!(!r.take_if_fn(|c| c.is_ascii_digit()));
298 assert!(r.take_if_fn(|c| c == 'b'));
299 assert!(r.end());
300 assert!(!r.take_if('a'));
301 assert!(!r.take_if_fn(|_| true));
302 assert!(!r.take_if_str("a"));
303 assert!(r.take_if_str(""));
304 }
305
306 #[test]
307 fn take_until_and_take_while() {
308 let mut r = CharacterReader::new("żółw]rest");
309 assert_eq!(r.take_until(']'), "żółw");
310 assert_eq!(r.position(), 4);
311 assert_eq!(r.take(), Some(']'));
312 assert_eq!(r.take_while(|c| c != 's'), "re");
313 assert_eq!(r.take_until('#'), "st");
314 assert!(r.end());
315 assert_eq!(r.take_until('#'), "");
316 assert_eq!(r.take_while(|_| true), "");
317 assert_eq!(r.position(), 9);
318 }
319
320 #[test]
321 fn whitespace() {
322 let mut r = CharacterReader::new(" \t\u{00A0}\u{1F}x ");
323 assert_eq!(r.peek_whitespace(), " \t\u{00A0}\u{1F}");
324 assert_eq!(r.position(), 0);
325 r.skip_whitespace();
326 assert_eq!(r.position(), 4);
327 assert_eq!(r.peek(), Some('x'));
328 assert_eq!(r.peek_whitespace(), "");
329 r.skip_whitespace();
330 assert_eq!(r.position(), 4);
331 }
332
333 #[test]
334 fn try_peek_and_skip() {
335 let mut r = CharacterReader::new("aéb");
336 assert_eq!(r.try_peek(0), "");
337 assert_eq!(r.try_peek(2), "aé");
338 assert_eq!(r.try_peek(3), "aéb");
339 assert_eq!(r.try_peek(4), "");
340 r.skip(2);
341 assert_eq!(r.peek(), Some('b'));
342 assert_eq!(r.position(), 0);
344 }
345
346 #[test]
347 #[should_panic]
348 fn skip_past_end_panics() {
349 CharacterReader::new("a").skip(2);
350 }
351
352 #[test]
353 fn classification() {
354 assert!(is_letter('a') && is_letter('Z') && is_letter('ż') && is_letter('λ') && is_letter('中'));
355 assert!(!is_letter('_') && !is_letter('1') && !is_letter('Ⅷ') && !is_letter('Ⓐ'));
356 assert!(!is_letter('𝒜'));
357 assert!(is_digit('7') && is_digit('٣') && !is_digit('a') && !is_digit('Ⅷ'));
358 assert!(is_white_space(' ') && is_white_space('\u{2003}') && !is_white_space('x'));
359 assert!(is_identifier_part_category('\u{0301}'));
360 assert!(is_identifier_part_category('\u{203F}'));
361 assert!(is_identifier_part_category('\u{200D}'));
362 assert!(!is_identifier_part_category('-') && !is_identifier_part_category('a'));
363 }
364}