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}