Skip to main content

saphyr_parser/
input.rs

1//! Utilities to create a source of input to the parser.
2//!
3//! [`Input`] must be implemented for the parser to fetch input. Make sure your needs aren't
4//! covered by the [`BufferedInput`].
5
6use alloc::string::String;
7
8pub(crate) mod buffered;
9pub(crate) mod str;
10
11#[allow(clippy::module_name_repetitions)]
12pub use buffered::BufferedInput;
13
14pub use crate::char_traits::{
15    is_alpha, is_blank, is_blank_or_breakz, is_break, is_breakz, is_digit, is_flow, is_z,
16};
17
18/// Interface for a source of characters.
19///
20/// Hiding the input's implementation behind this trait allows mostly:
21///  * For input-specific optimizations (for instance, using `str` methods instead of manually
22///    transferring one `char` at a time to a buffer).
23///  * To return `&str`s referencing the input string, thus avoiding potentially costly
24///    allocations. Should users need an owned version of the data, they can always `.to_owned()`
25///    their YAML object.
26pub trait Input {
27    /// A hint to the input source that we will need to read `count` characters.
28    ///
29    /// If the input is exhausted, `\0` can be used to pad the last characters and later returned.
30    /// The characters must not be consumed, but may be placed in an internal buffer.
31    ///
32    /// This method may be a no-op if buffering yields no performance improvement.
33    ///
34    /// Implementers of [`Input`] must _not_ load more than `count` characters into the buffer. The
35    /// parser tracks how many characters are loaded in the buffer and acts accordingly.
36    fn lookahead(&mut self, count: usize);
37
38    /// Return the number of buffered characters in `self`.
39    #[must_use]
40    fn buflen(&self) -> usize;
41
42    /// Return the capacity of the buffer in `self`.
43    #[must_use]
44    fn bufmaxlen(&self) -> usize;
45
46    /// Return whether the buffer (!= stream) is empty.
47    #[inline]
48    #[must_use]
49    fn buf_is_empty(&self) -> bool {
50        self.buflen() == 0
51    }
52
53    /// Read a character from the input stream and return it directly.
54    ///
55    /// The internal buffer (if any) is bypassed.
56    #[must_use]
57    fn raw_read_ch(&mut self) -> char;
58
59    /// Read a non-breakz a character from the input stream and return it directly.
60    ///
61    /// The internal buffer (if any) is bypassed.
62    ///
63    /// If the next character is a breakz, it is either not consumed or placed into the buffer (if
64    /// any).
65    #[must_use]
66    fn raw_read_non_breakz_ch(&mut self) -> Option<char>;
67
68    /// Consume the next character.
69    fn skip(&mut self);
70
71    /// Consume the next `count` character.
72    fn skip_n(&mut self, count: usize);
73
74    /// Return the next character, without consuming it.
75    ///
76    /// Users of the [`Input`] must make sure that the character has been loaded through a prior
77    /// call to [`Input::lookahead`]. Implementors of [`Input`] may assume that a valid call to
78    /// [`Input::lookahead`] has been made beforehand.
79    ///
80    /// # Return
81    /// If the input source is not exhausted, returns the next character to be fed into the
82    /// scanner. Otherwise, returns `\0`.
83    #[must_use]
84    fn peek(&self) -> char;
85
86    /// Return the `n`-th character in the buffer, without consuming it.
87    ///
88    /// This function assumes that the n-th character in the input has already been fetched through
89    /// [`Input::lookahead`].
90    #[must_use]
91    fn peek_nth(&self, n: usize) -> char;
92
93    /// Look for the next character and return it.
94    ///
95    /// The character is not consumed.
96    /// Equivalent to calling [`Input::lookahead`] and [`Input::peek`].
97    #[inline]
98    #[must_use]
99    fn look_ch(&mut self) -> char {
100        self.lookahead(1);
101        self.peek()
102    }
103
104    /// Return whether the next character in the input source is equal to `c`.
105    ///
106    /// This function assumes that the next character in the input has already been fetched through
107    /// [`Input::lookahead`].
108    #[inline]
109    #[must_use]
110    fn next_char_is(&self, c: char) -> bool {
111        self.peek() == c
112    }
113
114    /// Return whether the `n`-th character in the input source is equal to `c`.
115    ///
116    /// This function assumes that the n-th character in the input has already been fetched through
117    /// [`Input::lookahead`].
118    #[inline]
119    #[must_use]
120    fn nth_char_is(&self, n: usize, c: char) -> bool {
121        self.peek_nth(n) == c
122    }
123
124    /// Return whether the next 2 characters in the input source match the given characters.
125    ///
126    /// This function assumes that the next 2 characters in the input have already been fetched
127    /// through [`Input::lookahead`].
128    #[inline]
129    #[must_use]
130    fn next_2_are(&self, c1: char, c2: char) -> bool {
131        assert!(self.buflen() >= 2);
132        self.peek() == c1 && self.peek_nth(1) == c2
133    }
134
135    /// Return whether the next 3 characters in the input source match the given characters.
136    ///
137    /// This function assumes that the next 3 characters in the input have already been fetched
138    /// through [`Input::lookahead`].
139    #[inline]
140    #[must_use]
141    fn next_3_are(&self, c1: char, c2: char, c3: char) -> bool {
142        assert!(self.buflen() >= 3);
143        self.peek() == c1 && self.peek_nth(1) == c2 && self.peek_nth(2) == c3
144    }
145
146    /// Check whether the next characters correspond to a document indicator.
147    ///
148    /// This function assumes that the next 4 characters in the input has already been fetched
149    /// through [`Input::lookahead`].
150    #[inline]
151    #[must_use]
152    fn next_is_document_indicator(&self) -> bool {
153        assert!(self.buflen() >= 4);
154        is_blank_or_breakz(self.peek_nth(3))
155            && (self.next_3_are('.', '.', '.') || self.next_3_are('-', '-', '-'))
156    }
157
158    /// Check whether the next characters correspond to a start of document.
159    ///
160    /// This function assumes that the next 4 characters in the input has already been fetched
161    /// through [`Input::lookahead`].
162    #[inline]
163    #[must_use]
164    fn next_is_document_start(&self) -> bool {
165        assert!(self.buflen() >= 4);
166        self.next_3_are('-', '-', '-') && is_blank_or_breakz(self.peek_nth(3))
167    }
168
169    /// Check whether the next characters correspond to an end of document.
170    ///
171    /// This function assumes that the next 4 characters in the input has already been fetched
172    /// through [`Input::lookahead`].
173    #[inline]
174    #[must_use]
175    fn next_is_document_end(&self) -> bool {
176        assert!(self.buflen() >= 4);
177        self.next_3_are('.', '.', '.') && is_blank_or_breakz(self.peek_nth(3))
178    }
179
180    /// Skip yaml whitespace at most up to eol. Also skips comments. Advances the input.
181    ///
182    /// # Return
183    /// Return a tuple with the number of characters that were consumed and the result of skipping
184    /// whitespace. The number of characters returned can be used to advance the index and column,
185    /// since no end-of-line character will be consumed.
186    /// See [`SkipTabs`] For more details on the success variant.
187    ///
188    /// # Errors
189    /// Errors if a comment is encountered but it was not preceded by a whitespace. In that event,
190    /// the first tuple element will contain the number of characters consumed prior to reaching
191    /// the `#`.
192    fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, &'static str>) {
193        let mut encountered_tab = false;
194        let mut has_yaml_ws = false;
195        let mut chars_consumed = 0;
196        loop {
197            match self.look_ch() {
198                ' ' => {
199                    has_yaml_ws = true;
200                    self.skip();
201                }
202                '\t' if skip_tabs != SkipTabs::No => {
203                    encountered_tab = true;
204                    self.skip();
205                }
206                // YAML comments must be preceded by whitespace.
207                '#' if !encountered_tab && !has_yaml_ws => {
208                    return (
209                        chars_consumed,
210                        Err("comments must be separated from other tokens by whitespace"),
211                    );
212                }
213                '#' => {
214                    self.skip(); // Skip over '#'
215                    while !is_breakz(self.look_ch()) {
216                        self.skip();
217                        chars_consumed += 1;
218                    }
219                }
220                _ => break,
221            }
222            chars_consumed += 1;
223        }
224
225        (
226            chars_consumed,
227            Ok(SkipTabs::Result(encountered_tab, has_yaml_ws)),
228        )
229    }
230
231    /// Check whether the next characters may be part of a plain scalar.
232    ///
233    /// This function assumes we are not given a blankz character.
234    #[allow(clippy::inline_always)]
235    #[inline(always)]
236    fn next_can_be_plain_scalar(&self, in_flow: bool) -> bool {
237        let nc = self.peek_nth(1);
238        match self.peek() {
239            // indicators can end a plain scalar, see 7.3.3. Plain Style
240            ':' if is_blank_or_breakz(nc) || (in_flow && is_flow(nc)) => false,
241            c if in_flow && is_flow(c) => false,
242            _ => true,
243        }
244    }
245
246    /// Check whether the next character is [a blank] or [a break].
247    ///
248    /// The character must have previously been fetched through [`lookahead`]
249    ///
250    /// # Return
251    /// Returns true if the character is [a blank] or [a break], false otherwise.
252    ///
253    /// [`lookahead`]: Input::lookahead
254    /// [a blank]: is_blank
255    /// [a break]: is_break
256    #[inline]
257    fn next_is_blank_or_break(&self) -> bool {
258        is_blank(self.peek()) || is_break(self.peek())
259    }
260
261    /// Check whether the next character is [a blank] or [a breakz].
262    ///
263    /// The character must have previously been fetched through [`lookahead`]
264    ///
265    /// # Return
266    /// Returns true if the character is [a blank] or [a break], false otherwise.
267    ///
268    /// [`lookahead`]: Input::lookahead
269    /// [a blank]: is_blank
270    /// [a breakz]: is_breakz
271    #[inline]
272    fn next_is_blank_or_breakz(&self) -> bool {
273        is_blank(self.peek()) || is_breakz(self.peek())
274    }
275
276    /// Check whether the next character is [a blank].
277    ///
278    /// The character must have previously been fetched through [`lookahead`]
279    ///
280    /// # Return
281    /// Returns true if the character is [a blank], false otherwise.
282    ///
283    /// [`lookahead`]: Input::lookahead
284    /// [a blank]: is_blank
285    #[inline]
286    fn next_is_blank(&self) -> bool {
287        is_blank(self.peek())
288    }
289
290    /// Check whether the next character is [a break].
291    ///
292    /// The character must have previously been fetched through [`lookahead`]
293    ///
294    /// # Return
295    /// Returns true if the character is [a break], false otherwise.
296    ///
297    /// [`lookahead`]: Input::lookahead
298    /// [a break]: is_break
299    #[inline]
300    fn next_is_break(&self) -> bool {
301        is_break(self.peek())
302    }
303
304    /// Check whether the next character is [a breakz].
305    ///
306    /// The character must have previously been fetched through [`lookahead`]
307    ///
308    /// # Return
309    /// Returns true if the character is [a breakz], false otherwise.
310    ///
311    /// [`lookahead`]: Input::lookahead
312    /// [a breakz]: is_breakz
313    #[inline]
314    fn next_is_breakz(&self) -> bool {
315        is_breakz(self.peek())
316    }
317
318    /// Check whether the next character is [a z].
319    ///
320    /// The character must have previously been fetched through [`lookahead`]
321    ///
322    /// # Return
323    /// Returns true if the character is [a z], false otherwise.
324    ///
325    /// [`lookahead`]: Input::lookahead
326    /// [a z]: is_z
327    #[inline]
328    fn next_is_z(&self) -> bool {
329        is_z(self.peek())
330    }
331
332    /// Check whether the next character is [a flow].
333    ///
334    /// The character must have previously been fetched through [`lookahead`]
335    ///
336    /// # Return
337    /// Returns true if the character is [a flow], false otherwise.
338    ///
339    /// [`lookahead`]: Input::lookahead
340    /// [a flow]: is_flow
341    #[inline]
342    fn next_is_flow(&self) -> bool {
343        is_flow(self.peek())
344    }
345
346    /// Check whether the next character is [a digit].
347    ///
348    /// The character must have previously been fetched through [`lookahead`]
349    ///
350    /// # Return
351    /// Returns true if the character is [a digit], false otherwise.
352    ///
353    /// [`lookahead`]: Input::lookahead
354    /// [a digit]: is_digit
355    #[inline]
356    fn next_is_digit(&self) -> bool {
357        is_digit(self.peek())
358    }
359
360    /// Check whether the next character is [a letter].
361    ///
362    /// The character must have previously been fetched through [`lookahead`]
363    ///
364    /// # Return
365    /// Returns true if the character is [a letter], false otherwise.
366    ///
367    /// [`lookahead`]: Input::lookahead
368    /// [a letter]: is_alpha
369    #[inline]
370    fn next_is_alpha(&self) -> bool {
371        is_alpha(self.peek())
372    }
373
374    /// Skip characters from the input until a [breakz] is found.
375    ///
376    /// The characters are consumed from the input.
377    ///
378    /// # Return
379    /// Return the number of characters that were consumed. The number of characters returned can
380    /// be used to advance the index and column, since no end-of-line character will be consumed.
381    ///
382    /// [breakz]: is_breakz
383    #[inline]
384    fn skip_while_non_breakz(&mut self) -> usize {
385        let mut count = 0;
386        while !is_breakz(self.look_ch()) {
387            count += 1;
388            self.skip();
389        }
390        count
391    }
392
393    /// Skip characters from the input while [blanks] are found.
394    ///
395    /// The characters are consumed from the input.
396    ///
397    /// # Return
398    /// Return the number of characters that were consumed. The number of characters returned can
399    /// be used to advance the index and column, since no end-of-line character will be consumed.
400    ///
401    /// [blanks]: is_blank
402    fn skip_while_blank(&mut self) -> usize {
403        let mut n_chars = 0;
404        while is_blank(self.look_ch()) {
405            n_chars += 1;
406            self.skip();
407        }
408        n_chars
409    }
410
411    /// Fetch characters from the input while we encounter letters and store them in `out`.
412    ///
413    /// The characters are consumed from the input.
414    ///
415    /// # Return
416    /// Return the number of characters that were consumed. The number of characters returned can
417    /// be used to advance the index and column, since no end-of-line character will be consumed.
418    fn fetch_while_is_alpha(&mut self, out: &mut String) -> usize {
419        let mut n_chars = 0;
420        while is_alpha(self.look_ch()) {
421            n_chars += 1;
422            out.push(self.peek());
423            self.skip();
424        }
425        n_chars
426    }
427
428    /// Fetch characters as long as they satisfy `is_yaml_non_space(c)`.
429    ///
430    /// The characters are consumed from the input.
431    ///
432    /// # Return
433    /// Return the number of characters that were consumed. The number of characters returned can
434    /// be used to advance the index and column, since no end-of-line character will be consumed.
435    fn fetch_while_is_yaml_non_space(&mut self, out: &mut String) -> usize {
436        let mut n_chars = 0;
437        while crate::char_traits::is_yaml_non_space(self.look_ch()) {
438            n_chars += 1;
439            out.push(self.peek());
440            self.skip();
441        }
442        n_chars
443    }
444}
445
446/// Behavior to adopt regarding treating tabs as whitespace.
447///
448/// Although tab is a valid yaml whitespace, it doesn't always behave the same as a space.
449#[derive(Copy, Clone, Eq, PartialEq)]
450pub enum SkipTabs {
451    /// Skip all tabs as whitespace.
452    Yes,
453    /// Don't skip any tab. Return from the function when encountering one.
454    No,
455    /// Return value from the function.
456    Result(
457        /// Whether tabs were encountered.
458        bool,
459        /// Whether at least 1 valid yaml whitespace has been encountered.
460        bool,
461    ),
462}
463
464impl SkipTabs {
465    /// Whether tabs were found while skipping whitespace.
466    ///
467    /// This function must be called after a call to `skip_ws_to_eol`.
468    #[must_use]
469    pub fn found_tabs(self) -> bool {
470        matches!(self, SkipTabs::Result(true, _))
471    }
472
473    /// Whether a valid YAML whitespace has been found in skipped-over content.
474    ///
475    /// This function must be called after a call to `skip_ws_to_eol`.
476    #[must_use]
477    pub fn has_valid_yaml_ws(self) -> bool {
478        matches!(self, SkipTabs::Result(_, true))
479    }
480}