Skip to main content

pochoir_common/
stream_parser.rs

1use std::{
2    ops::Range,
3    path::{Path, PathBuf},
4};
5
6use crate::{Error, Result, Spanned};
7
8#[derive(Debug, Clone)]
9pub struct StreamParser<'a> {
10    file_path: PathBuf,
11    value: &'a str,
12    index: usize,
13}
14
15impl<'a> StreamParser<'a> {
16    pub fn new<T: AsRef<Path>>(file_path: T, value: &'a str) -> Self {
17        Self {
18            file_path: file_path.as_ref().into(),
19            value,
20            index: 0,
21        }
22    }
23
24    /// Get the file path containing the current text.
25    pub fn file_path(&self) -> &Path {
26        &self.file_path
27    }
28
29    /// Get the current index.
30    pub fn index(&self) -> usize {
31        self.index
32    }
33
34    /// Set the current index.
35    pub fn set_index(&mut self, index: usize) {
36        self.index = index;
37    }
38
39    /// Returns the next byte without advancing the index.
40    ///
41    /// # Errors
42    ///
43    /// Returns an error if EOI is found.
44    pub fn next(&self) -> Result<char> {
45        self.value.as_bytes().get(self.index).copied().map_or_else(
46            || {
47                Err(Spanned::new(Error::UnexpectedEoi)
48                    .with_span(if self.value.is_empty() {
49                        0..0
50                    } else {
51                        self.value.len() - 1..self.value.len()
52                    })
53                    .with_file_path(&self.file_path))
54            },
55            |v| Ok(v as char),
56        )
57    }
58
59    /// Returns the next byte while advancing the index.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if EOI is found.
64    #[allow(clippy::missing_panics_doc)]
65    pub fn take_next(&mut self) -> Result<char> {
66        if self.index < self.value.len() {
67            self.index += 1;
68
69            Ok(self.value.as_bytes().get(self.index - 1).copied().unwrap() as char)
70        } else {
71            Err(Spanned::new(Error::UnexpectedEoi)
72                .with_span(if self.value.is_empty() {
73                    0..0
74                } else {
75                    self.value.len() - 1..self.value.len()
76                })
77                .with_file_path(&self.file_path))
78        }
79    }
80
81    /// Peek the next `n` bytes.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if EOI is found.
86    pub fn peek(&self, n: usize) -> Result<&'a str> {
87        self.value.get(self.index..self.index + n).ok_or(
88            Spanned::new(Error::UnexpectedEoi)
89                .with_span(if self.value.is_empty() {
90                    0..0
91                } else {
92                    self.value.len() - 1..self.value.len()
93                })
94                .with_file_path(&self.file_path),
95        )
96    }
97
98    /// Returns if the next bytes match the provided value.
99    ///
100    /// If EOI is reached, `false` is returned.
101    pub fn peek_exact(&self, val: &str) -> bool {
102        self.value.get(self.index..self.index + val.len()) == Some(val)
103    }
104
105    /// Peek the next `n` bytes starting at `e` bytes from the current index.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if EOI is found.
110    pub fn peek_early(&self, n: usize, e: usize) -> Result<&'a str> {
111        self.value.get(self.index + e..self.index + e + n).ok_or(
112            Spanned::new(Error::UnexpectedEoi)
113                .with_span(if self.value.is_empty() {
114                    0..0
115                } else {
116                    self.value.len() - 1..self.value.len()
117                })
118                .with_file_path(&self.file_path),
119        )
120    }
121
122    /// Returns if the next bytes starting at `e` from the current index match the provided value.
123    ///
124    /// If EOI is reached, `false` is returned.
125    pub fn peek_early_exact(&self, val: &str, e: usize) -> bool {
126        self.value.get(self.index + e..self.index + e + val.len()) == Some(val)
127    }
128
129    /// Advance the index of the stream of `n` bytes.
130    ///
131    /// # Errors
132    ///
133    /// Returns an error if EOI is found.
134    pub fn take(&mut self, n: usize) -> Result<()> {
135        if self.index < self.value.len() {
136            self.index += n;
137            Ok(())
138        } else {
139            Err(Spanned::new(Error::UnexpectedEoi)
140                .with_span(if self.value.is_empty() {
141                    0..0
142                } else {
143                    self.value.len() - 1..self.value.len()
144                })
145                .with_file_path(&self.file_path))
146        }
147    }
148
149    /// Check if the next characters match the value.
150    /// If the values match, advance the current index of the length of the value and return true,
151    /// otherwise return false.
152    ///
153    /// # Errors
154    ///
155    /// Returns an error if EOI is found or if the next characters does not match the string given
156    /// as argument.
157    #[allow(clippy::missing_panics_doc)]
158    pub fn take_exact(&mut self, val: &str) -> Result<()> {
159        if self.index + val.len() <= self.value.len() {
160            let span = self.index
161                ..self.index
162                    + val.len()
163                    + (0..4)
164                        .find(|n| self.value.is_char_boundary(self.index + val.len() + n))
165                        .expect("a unicode codepoint should at most be 4 bytes");
166            let value = &self.value[span.clone()];
167
168            if value == val {
169                self.index += val.len();
170                Ok(())
171            } else {
172                Err(Spanned::new(Error::UnexpectedInput {
173                    expected: val.to_string(),
174                    found: value.to_string(),
175                })
176                .with_span(span)
177                .with_file_path(&self.file_path))
178            }
179        } else {
180            Err(Spanned::new(Error::ExpectedFoundEoi {
181                expected: val.to_string(),
182            })
183            .with_span(if self.value.is_empty() {
184                0..0
185            } else {
186                self.value.len() - 1..self.value.len()
187            })
188            .with_file_path(&self.file_path))
189        }
190    }
191
192    /// Advance the string so that the index will point to a non-space character.
193    pub fn trim(&mut self) {
194        self.take_while(|(_, ch)| char::is_whitespace(ch));
195    }
196
197    pub fn is_eoi(&self) -> bool {
198        self.index >= self.value.len()
199    }
200
201    /// Get a range of text in the inner content.
202    ///
203    /// # Errors
204    ///
205    /// Returns an error if EOI is found.
206    pub fn get_range(&self, range: Range<usize>) -> Result<&'a str> {
207        self.value.get(range).ok_or_else(|| {
208            Spanned::new(Error::UnexpectedEoi)
209                .with_span(if self.value.is_empty() {
210                    0..0
211                } else {
212                    self.value.len() - 1..self.value.len()
213                })
214                .with_file_path(&self.file_path)
215        })
216    }
217
218    pub fn take_while<F: FnMut((usize, char)) -> bool>(&mut self, mut f: F) -> &'a str {
219        let old_index = self.index;
220        let new_index = &self.value[self.index..]
221            .char_indices()
222            .find_map(|(i, ch)| {
223                if f((self.index + i, ch)) {
224                    None
225                } else {
226                    Some(self.index + i)
227                }
228            })
229            .unwrap_or(self.value.len());
230
231        self.index = *new_index;
232        &self.value[old_index..*new_index]
233    }
234
235    pub fn take_while_peekable<F: FnMut(usize, char, Option<char>) -> bool>(
236        &mut self,
237        mut f: F,
238    ) -> &'a str {
239        let old_index = self.index;
240        let mut iter = self.value[self.index..].char_indices().peekable();
241
242        while let Some(i) = iter.next() {
243            if !f(i.0, i.1, iter.peek().map(|(_, ch)| *ch)) {
244                self.index += i.0;
245                return &self.value[old_index..self.index];
246            }
247        }
248
249        self.index = self.value.len();
250        &self.value[old_index..]
251    }
252
253    /// Take all the data until `val` is found.
254    ///
255    /// Returns the data taken.
256    ///
257    /// # Errors
258    ///
259    /// Returns an error if EOI is found.
260    pub fn take_until(&mut self, val: &str) -> Result<&'a str> {
261        let old_index = self.index;
262        let mut new_index = old_index;
263
264        while let Some(value) = &self.value.get(new_index..new_index + val.len()) {
265            if *value == val {
266                self.index = new_index;
267                return Ok(&self.value[old_index..self.index]);
268            }
269
270            new_index += 1;
271        }
272
273        Err(Spanned::new(Error::ExpectedFoundEoi {
274            expected: val.to_string(),
275        })
276        .with_span(if self.value.is_empty() {
277            0..0
278        } else {
279            self.value.len() - 1..self.value.len()
280        })
281        .with_file_path(&self.file_path))
282    }
283
284    /// Take all the data until `val` is found.
285    ///
286    /// Returns the index of the first character of `val` in the data.
287    ///
288    /// # Errors
289    ///
290    /// Returns an error if EOI is found.
291    pub fn take_until_index(&mut self, val: &str) -> Result<usize> {
292        let old_index = self.index;
293        let mut new_index = old_index;
294
295        while let Some(value) = &self.value.get(new_index..new_index + val.len()) {
296            if *value == val {
297                self.index = new_index;
298                return Ok(self.index);
299            }
300
301            new_index += 1;
302        }
303
304        Err(Spanned::new(Error::ExpectedFoundEoi {
305            expected: val.to_string(),
306        })
307        .with_span(if self.value.is_empty() {
308            0..0
309        } else {
310            self.value.len() - 1..self.value.len()
311        })
312        .with_file_path(&self.file_path))
313    }
314
315    pub fn take_until_eoi(&mut self) -> &'a str {
316        let old_index = self.index;
317        self.index = self.value.len();
318        &self.value[old_index..]
319    }
320}