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