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 /// Return the next character in the input source, without consuming it, unless the input
94 /// source is exhausted.
95 ///
96 /// Unlike [`Input::peek`], this function does not conflate a NUL character with the
97 /// end-of-stream sentinel: a raw NUL character in the stream is returned as `Some('\0')`,
98 /// while an exhausted input source returns `None`. Padding characters introduced by
99 /// [`Input::lookahead`] are never returned.
100 ///
101 /// This function assumes that the next character in the input has already been fetched through
102 /// [`Input::lookahead`].
103 #[must_use]
104 fn peek_opt(&self) -> Option<char>;
105
106 /// Look for the next character and return it.
107 ///
108 /// The character is not consumed.
109 /// Equivalent to calling [`Input::lookahead`] and [`Input::peek`].
110 #[inline]
111 #[must_use]
112 fn look_ch(&mut self) -> char {
113 self.lookahead(1);
114 self.peek()
115 }
116
117 /// Return whether the next character in the input source is equal to `c`.
118 ///
119 /// This function assumes that the next character in the input has already been fetched through
120 /// [`Input::lookahead`].
121 #[inline]
122 #[must_use]
123 fn next_char_is(&self, c: char) -> bool {
124 self.peek() == c
125 }
126
127 /// Return whether the `n`-th character in the input source is equal to `c`.
128 ///
129 /// This function assumes that the n-th character in the input has already been fetched through
130 /// [`Input::lookahead`].
131 #[inline]
132 #[must_use]
133 fn nth_char_is(&self, n: usize, c: char) -> bool {
134 self.peek_nth(n) == c
135 }
136
137 /// Return whether the next 2 characters in the input source match the given characters.
138 ///
139 /// This function assumes that the next 2 characters in the input have already been fetched
140 /// through [`Input::lookahead`].
141 #[inline]
142 #[must_use]
143 fn next_2_are(&self, c1: char, c2: char) -> bool {
144 assert!(self.buflen() >= 2);
145 self.peek() == c1 && self.peek_nth(1) == c2
146 }
147
148 /// Return whether the next 3 characters in the input source match the given characters.
149 ///
150 /// This function assumes that the next 3 characters in the input have already been fetched
151 /// through [`Input::lookahead`].
152 #[inline]
153 #[must_use]
154 fn next_3_are(&self, c1: char, c2: char, c3: char) -> bool {
155 assert!(self.buflen() >= 3);
156 self.peek() == c1 && self.peek_nth(1) == c2 && self.peek_nth(2) == c3
157 }
158
159 /// Check whether the next characters correspond to a document indicator.
160 ///
161 /// This function assumes that the next 4 characters in the input has already been fetched
162 /// through [`Input::lookahead`].
163 #[inline]
164 #[must_use]
165 fn next_is_document_indicator(&self) -> bool {
166 assert!(self.buflen() >= 4);
167 is_blank_or_breakz(self.peek_nth(3))
168 && (self.next_3_are('.', '.', '.') || self.next_3_are('-', '-', '-'))
169 }
170
171 /// Check whether the next characters correspond to a start of document.
172 ///
173 /// This function assumes that the next 4 characters in the input has already been fetched
174 /// through [`Input::lookahead`].
175 #[inline]
176 #[must_use]
177 fn next_is_document_start(&self) -> bool {
178 assert!(self.buflen() >= 4);
179 self.next_3_are('-', '-', '-') && is_blank_or_breakz(self.peek_nth(3))
180 }
181
182 /// Check whether the next characters correspond to an end of document.
183 ///
184 /// This function assumes that the next 4 characters in the input has already been fetched
185 /// through [`Input::lookahead`].
186 #[inline]
187 #[must_use]
188 fn next_is_document_end(&self) -> bool {
189 assert!(self.buflen() >= 4);
190 self.next_3_are('.', '.', '.') && is_blank_or_breakz(self.peek_nth(3))
191 }
192
193 /// Skip yaml whitespace at most up to eol. Also skips comments. Advances the input.
194 ///
195 /// # Return
196 /// Return a tuple with the number of characters that were consumed and the result of skipping
197 /// whitespace. The number of characters returned can be used to advance the index and column,
198 /// since no end-of-line character will be consumed.
199 /// See [`SkipTabs`] For more details on the success variant.
200 ///
201 /// # Errors
202 /// Errors if a comment is encountered but it was not preceded by a whitespace. In that event,
203 /// the first tuple element will contain the number of characters consumed prior to reaching
204 /// the `#`.
205 fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, &'static str>) {
206 let mut encountered_tab = false;
207 let mut has_yaml_ws = false;
208 let mut chars_consumed = 0;
209 loop {
210 match self.look_ch() {
211 ' ' => {
212 has_yaml_ws = true;
213 self.skip();
214 }
215 '\t' if skip_tabs != SkipTabs::No => {
216 encountered_tab = true;
217 self.skip();
218 }
219 // YAML comments must be preceded by whitespace.
220 '#' if !encountered_tab && !has_yaml_ws => {
221 return (
222 chars_consumed,
223 Err("comments must be separated from other tokens by whitespace"),
224 );
225 }
226 '#' => {
227 self.skip(); // Skip over '#'
228 while !is_breakz(self.look_ch()) {
229 self.skip();
230 chars_consumed += 1;
231 }
232 }
233 _ => break,
234 }
235 chars_consumed += 1;
236 }
237
238 (
239 chars_consumed,
240 Ok(SkipTabs::Result(encountered_tab, has_yaml_ws)),
241 )
242 }
243
244 /// Check whether the next characters may be part of a plain scalar.
245 ///
246 /// This function assumes we are not given a blankz character.
247 #[allow(clippy::inline_always)]
248 #[inline(always)]
249 fn next_can_be_plain_scalar(&self, in_flow: bool) -> bool {
250 let nc = self.peek_nth(1);
251 match self.peek() {
252 // indicators can end a plain scalar, see 7.3.3. Plain Style
253 ':' if is_blank_or_breakz(nc) || (in_flow && is_flow(nc)) => false,
254 c if in_flow && is_flow(c) => false,
255 _ => true,
256 }
257 }
258
259 /// Check whether the next character is [a blank] or [a break].
260 ///
261 /// The character must have previously been fetched through [`lookahead`]
262 ///
263 /// # Return
264 /// Returns true if the character is [a blank] or [a break], false otherwise.
265 ///
266 /// [`lookahead`]: Input::lookahead
267 /// [a blank]: is_blank
268 /// [a break]: is_break
269 #[inline]
270 fn next_is_blank_or_break(&self) -> bool {
271 is_blank(self.peek()) || is_break(self.peek())
272 }
273
274 /// Check whether the next character is [a blank] or [a breakz].
275 ///
276 /// The character must have previously been fetched through [`lookahead`]
277 ///
278 /// # Return
279 /// Returns true if the character is [a blank] or [a break], false otherwise.
280 ///
281 /// [`lookahead`]: Input::lookahead
282 /// [a blank]: is_blank
283 /// [a breakz]: is_breakz
284 #[inline]
285 fn next_is_blank_or_breakz(&self) -> bool {
286 is_blank(self.peek()) || is_breakz(self.peek())
287 }
288
289 /// Check whether the next character is [a blank].
290 ///
291 /// The character must have previously been fetched through [`lookahead`]
292 ///
293 /// # Return
294 /// Returns true if the character is [a blank], false otherwise.
295 ///
296 /// [`lookahead`]: Input::lookahead
297 /// [a blank]: is_blank
298 #[inline]
299 fn next_is_blank(&self) -> bool {
300 is_blank(self.peek())
301 }
302
303 /// Check whether the next character is [a break].
304 ///
305 /// The character must have previously been fetched through [`lookahead`]
306 ///
307 /// # Return
308 /// Returns true if the character is [a break], false otherwise.
309 ///
310 /// [`lookahead`]: Input::lookahead
311 /// [a break]: is_break
312 #[inline]
313 fn next_is_break(&self) -> bool {
314 is_break(self.peek())
315 }
316
317 /// Check whether the next character is [a breakz].
318 ///
319 /// The character must have previously been fetched through [`lookahead`]
320 ///
321 /// # Return
322 /// Returns true if the character is [a breakz], false otherwise.
323 ///
324 /// [`lookahead`]: Input::lookahead
325 /// [a breakz]: is_breakz
326 #[inline]
327 fn next_is_breakz(&self) -> bool {
328 is_breakz(self.peek())
329 }
330
331 /// Check whether the next character is [a z].
332 ///
333 /// The character must have previously been fetched through [`lookahead`]
334 ///
335 /// # Return
336 /// Returns true if the character is [a z], false otherwise.
337 ///
338 /// [`lookahead`]: Input::lookahead
339 /// [a z]: is_z
340 #[inline]
341 fn next_is_z(&self) -> bool {
342 is_z(self.peek())
343 }
344
345 /// Check whether the next character is [a flow].
346 ///
347 /// The character must have previously been fetched through [`lookahead`]
348 ///
349 /// # Return
350 /// Returns true if the character is [a flow], false otherwise.
351 ///
352 /// [`lookahead`]: Input::lookahead
353 /// [a flow]: is_flow
354 #[inline]
355 fn next_is_flow(&self) -> bool {
356 is_flow(self.peek())
357 }
358
359 /// Check whether the next character is [a digit].
360 ///
361 /// The character must have previously been fetched through [`lookahead`]
362 ///
363 /// # Return
364 /// Returns true if the character is [a digit], false otherwise.
365 ///
366 /// [`lookahead`]: Input::lookahead
367 /// [a digit]: is_digit
368 #[inline]
369 fn next_is_digit(&self) -> bool {
370 is_digit(self.peek())
371 }
372
373 /// Check whether the next character is [a letter].
374 ///
375 /// The character must have previously been fetched through [`lookahead`]
376 ///
377 /// # Return
378 /// Returns true if the character is [a letter], false otherwise.
379 ///
380 /// [`lookahead`]: Input::lookahead
381 /// [a letter]: is_alpha
382 #[inline]
383 fn next_is_alpha(&self) -> bool {
384 is_alpha(self.peek())
385 }
386
387 /// Skip characters from the input until a [breakz] is found.
388 ///
389 /// The characters are consumed from the input.
390 ///
391 /// # Return
392 /// Return the number of characters that were consumed. The number of characters returned can
393 /// be used to advance the index and column, since no end-of-line character will be consumed.
394 ///
395 /// [breakz]: is_breakz
396 #[inline]
397 fn skip_while_non_breakz(&mut self) -> usize {
398 let mut count = 0;
399 while !is_breakz(self.look_ch()) {
400 count += 1;
401 self.skip();
402 }
403 count
404 }
405
406 /// Skip characters from the input while [blanks] are found.
407 ///
408 /// The characters are consumed from the input.
409 ///
410 /// # Return
411 /// Return the number of characters that were consumed. The number of characters returned can
412 /// be used to advance the index and column, since no end-of-line character will be consumed.
413 ///
414 /// [blanks]: is_blank
415 fn skip_while_blank(&mut self) -> usize {
416 let mut n_chars = 0;
417 while is_blank(self.look_ch()) {
418 n_chars += 1;
419 self.skip();
420 }
421 n_chars
422 }
423
424 /// Fetch characters from the input while we encounter letters and store them in `out`.
425 ///
426 /// The characters are consumed from the input.
427 ///
428 /// # Return
429 /// Return the number of characters that were consumed. The number of characters returned can
430 /// be used to advance the index and column, since no end-of-line character will be consumed.
431 fn fetch_while_is_alpha(&mut self, out: &mut String) -> usize {
432 let mut n_chars = 0;
433 while is_alpha(self.look_ch()) {
434 n_chars += 1;
435 out.push(self.peek());
436 self.skip();
437 }
438 n_chars
439 }
440
441 /// Fetch characters as long as they satisfy `is_yaml_non_space(c)`.
442 ///
443 /// The characters are consumed from the input.
444 ///
445 /// # Return
446 /// Return the number of characters that were consumed. The number of characters returned can
447 /// be used to advance the index and column, since no end-of-line character will be consumed.
448 fn fetch_while_is_yaml_non_space(&mut self, out: &mut String) -> usize {
449 let mut n_chars = 0;
450 while crate::char_traits::is_yaml_non_space(self.look_ch()) {
451 n_chars += 1;
452 out.push(self.peek());
453 self.skip();
454 }
455 n_chars
456 }
457}
458
459/// Behavior to adopt regarding treating tabs as whitespace.
460///
461/// Although tab is a valid yaml whitespace, it doesn't always behave the same as a space.
462#[derive(Copy, Clone, Eq, PartialEq)]
463pub enum SkipTabs {
464 /// Skip all tabs as whitespace.
465 Yes,
466 /// Don't skip any tab. Return from the function when encountering one.
467 No,
468 /// Return value from the function.
469 Result(
470 /// Whether tabs were encountered.
471 bool,
472 /// Whether at least 1 valid yaml whitespace has been encountered.
473 bool,
474 ),
475}
476
477impl SkipTabs {
478 /// Whether tabs were found while skipping whitespace.
479 ///
480 /// This function must be called after a call to `skip_ws_to_eol`.
481 #[must_use]
482 pub fn found_tabs(self) -> bool {
483 matches!(self, SkipTabs::Result(true, _))
484 }
485
486 /// Whether a valid YAML whitespace has been found in skipped-over content.
487 ///
488 /// This function must be called after a call to `skip_ws_to_eol`.
489 #[must_use]
490 pub fn has_valid_yaml_ws(self) -> bool {
491 matches!(self, SkipTabs::Result(_, true))
492 }
493}