granit_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
14/// A trait for inputs that can provide borrowed slices with a specific lifetime.
15///
16/// This trait enables zero-copy (`Cow::Borrowed`) token values for inputs that keep a stable
17/// backing string. The key difference from [`Input::slice_bytes`] is that this method returns
18/// a slice with the input's original lifetime `'a`, not tied to `&self`.
19///
20/// For inputs that support zero-copy (like [`str::StrInput`]), this returns `Some(&'a str)`.
21/// For streaming inputs that don't have stable backing storage, this returns `None`.
22pub trait BorrowedInput<'a>: Input {
23 /// Return a borrowed slice of the underlying source between two byte offsets.
24 ///
25 /// Unlike [`Input::slice_bytes`], this returns a slice with the input's lifetime `'a`,
26 /// allowing the slice to outlive the borrow of `&self`.
27 ///
28 /// `start` and `end` are byte offsets as returned by [`Input::byte_offset`]. The interval is
29 /// half-open: `[start, end)`.
30 ///
31 /// Returns `None` if the input does not support zero-copy slicing.
32 #[must_use]
33 fn slice_borrowed(&self, start: usize, end: usize) -> Option<&'a str>;
34}
35
36pub use crate::char_traits::{
37 is_alpha, is_blank, is_blank_or_breakz, is_break, is_breakz, is_digit, is_flow, is_z,
38};
39
40/// Interface for a source of characters.
41///
42/// Hiding the input's implementation behind this trait allows input-specific optimizations, such
43/// as using `str` methods instead of manually transferring one `char` at a time to a buffer.
44/// Implementations with stable backing storage can also return borrowed `&str` slices and avoid
45/// allocating token values.
46pub trait Input {
47 /// A hint to the input source that we will need to read `count` characters.
48 ///
49 /// If the input is exhausted, `\0` can be used to pad the last characters and later returned.
50 /// The characters must not be consumed, but may be placed in an internal buffer.
51 ///
52 /// This method may be a no-op if buffering yields no performance improvement.
53 ///
54 /// Implementers of [`Input`] must _not_ expose a lookahead window larger than
55 /// [`Input::bufmaxlen`]. They may retain a larger window requested by an earlier call; callers
56 /// should use [`Input::buflen`] to observe the currently available window.
57 fn lookahead(&mut self, count: usize);
58
59 /// Return the number of characters in the active lookahead window.
60 ///
61 /// This is the number of characters that the input promises can be read through [`peek`] and
62 /// [`peek_nth`] after prior [`lookahead`] calls. It is not necessarily the number of source
63 /// characters remaining: inputs may keep the window available after consuming characters and
64 /// may pad positions past EOF with `\0`.
65 ///
66 /// [`lookahead`]: Input::lookahead
67 /// [`peek`]: Input::peek
68 /// [`peek_nth`]: Input::peek_nth
69 #[must_use]
70 fn buflen(&self) -> usize;
71
72 /// Return the maximum number of characters this input can buffer for lookahead.
73 #[must_use]
74 fn bufmaxlen(&self) -> usize;
75
76 /// Return whether the active lookahead window is empty.
77 ///
78 /// This is equivalent to `self.buflen() == 0`. It does not mean the underlying source is
79 /// exhausted: after a previous [`lookahead`] call, an input may keep a non-empty lookahead
80 /// window available even after all source characters have been consumed, with positions past
81 /// EOF observed as `\0`.
82 ///
83 /// [`lookahead`]: Input::lookahead
84 #[inline]
85 #[must_use]
86 fn buf_is_empty(&self) -> bool {
87 self.buflen() == 0
88 }
89
90 /// Read the next character from the logical input stream and return it directly.
91 ///
92 /// If an implementation has already fetched characters for lookahead, this consumes the
93 /// buffered stream front before reading farther from the underlying source.
94 #[must_use]
95 fn raw_read_ch(&mut self) -> char;
96
97 /// Read a non-breakz character from the input stream and return it directly.
98 ///
99 /// If an implementation has already fetched characters for lookahead, this consumes from the
100 /// buffered stream front before reading farther from the underlying source.
101 ///
102 /// If the next character is a breakz, it is either not consumed or placed into the buffer (if
103 /// any).
104 #[must_use]
105 fn raw_read_non_breakz_ch(&mut self) -> Option<char>;
106
107 /// Consume the next character.
108 fn skip(&mut self);
109
110 /// Consume the next `count` characters.
111 fn skip_n(&mut self, count: usize);
112
113 /// Return the next character, without consuming it.
114 ///
115 /// Users of the [`Input`] must make sure that the character has been loaded through a prior
116 /// call to [`Input::lookahead`]. Implementors of [`Input`] may assume that a valid call to
117 /// [`Input::lookahead`] has been made beforehand.
118 ///
119 /// # Return
120 /// If the input source is not exhausted, returns the next character to be fed into the
121 /// scanner. Otherwise, returns `\0`.
122 #[must_use]
123 fn peek(&self) -> char;
124
125 /// Return the `n`-th character in the buffer, without consuming it.
126 ///
127 /// This function assumes that the `n`-th character in the input has already been fetched through
128 /// [`Input::lookahead`].
129 #[must_use]
130 fn peek_nth(&self, n: usize) -> char;
131
132 /// Return the current byte offset in the underlying source, if available.
133 ///
134 /// This is an *optional* capability that enables zero-copy (`Cow::Borrowed`) token values
135 /// for inputs that keep a stable backing string (notably [`str::StrInput`]).
136 ///
137 /// The returned value (when `Some`) is the number of bytes that have been consumed so far,
138 /// i.e. an offset into the original source string.
139 ///
140 /// # Correctness contract
141 /// Implementations returning `Some(_)` must satisfy all of the following:
142 ///
143 /// - The offset is a valid UTF-8 boundary in the underlying source.
144 /// - The offset is monotonically non-decreasing as characters are consumed.
145 /// - The underlying source is stable for the duration of parsing (no reallocation/mutation)
146 /// so that slices returned by [`Input::slice_bytes`] remain valid.
147 ///
148 /// Inputs that cannot provide stable slicing (e.g. stream/iterator inputs) must return
149 /// `None`.
150 #[inline]
151 #[must_use]
152 fn byte_offset(&self) -> Option<usize> {
153 None
154 }
155
156 /// Return a borrowed slice of the underlying source between two byte offsets.
157 ///
158 /// This is an *optional* capability used to produce `Cow::Borrowed` values without
159 /// allocating.
160 ///
161 /// `start` and `end` are byte offsets as returned by [`Input::byte_offset`]. The interval is
162 /// half-open: `[start, end)`.
163 ///
164 /// # Correctness contract
165 /// Implementations returning `Some(&str)` must ensure:
166 ///
167 /// - `start <= end`.
168 /// - Both offsets are valid UTF-8 boundaries.
169 /// - The returned `&str` is a view into the stable underlying source associated with this
170 /// input.
171 ///
172 /// Implementations that return `None` from [`Input::byte_offset`] must also return `None`
173 /// here.
174 #[inline]
175 #[must_use]
176 fn slice_bytes(&self, _start: usize, _end: usize) -> Option<&str> {
177 None
178 }
179
180 /// Return whether this input may contain a `#` character.
181 ///
182 /// This is a conservative performance hint. Inputs that cannot answer cheaply should return
183 /// `true`, which keeps full comment handling enabled.
184 #[inline]
185 #[must_use]
186 fn may_contain_comments(&self) -> bool {
187 true
188 }
189
190 /// Look for the next character and return it.
191 ///
192 /// The character is not consumed.
193 /// Equivalent to calling [`Input::lookahead`] and [`Input::peek`].
194 #[inline]
195 #[must_use]
196 fn look_ch(&mut self) -> char {
197 self.lookahead(1);
198 self.peek()
199 }
200
201 /// Return whether the next character in the input source is equal to `c`.
202 ///
203 /// This function assumes that the next character in the input has already been fetched through
204 /// [`Input::lookahead`].
205 #[inline]
206 #[must_use]
207 fn next_char_is(&self, c: char) -> bool {
208 self.peek() == c
209 }
210
211 /// Return whether the `n`-th character in the input source is equal to `c`.
212 ///
213 /// This function assumes that the `n`-th character in the input has already been fetched through
214 /// [`Input::lookahead`].
215 #[inline]
216 #[must_use]
217 fn nth_char_is(&self, n: usize, c: char) -> bool {
218 self.peek_nth(n) == c
219 }
220
221 /// Return whether the next 2 characters in the input source match the given characters.
222 ///
223 /// This function assumes that the next 2 characters in the input have already been fetched
224 /// through [`Input::lookahead`].
225 #[inline]
226 #[must_use]
227 fn next_2_are(&self, c1: char, c2: char) -> bool {
228 assert!(self.buflen() >= 2);
229 self.peek() == c1 && self.peek_nth(1) == c2
230 }
231
232 /// Return whether the next 3 characters in the input source match the given characters.
233 ///
234 /// This function assumes that the next 3 characters in the input have already been fetched
235 /// through [`Input::lookahead`].
236 #[inline]
237 #[must_use]
238 fn next_3_are(&self, c1: char, c2: char, c3: char) -> bool {
239 assert!(self.buflen() >= 3);
240 self.peek() == c1 && self.peek_nth(1) == c2 && self.peek_nth(2) == c3
241 }
242
243 /// Check whether the next characters correspond to a document indicator.
244 ///
245 /// This function assumes that the next 4 characters in the input have already been fetched
246 /// through [`Input::lookahead`].
247 #[inline]
248 #[must_use]
249 fn next_is_document_indicator(&self) -> bool {
250 assert!(self.buflen() >= 4);
251 is_blank_or_breakz(self.peek_nth(3))
252 && (self.next_3_are('.', '.', '.') || self.next_3_are('-', '-', '-'))
253 }
254
255 /// Check whether the next characters correspond to a start of document.
256 ///
257 /// This function assumes that the next 4 characters in the input have already been fetched
258 /// through [`Input::lookahead`].
259 #[inline]
260 #[must_use]
261 fn next_is_document_start(&self) -> bool {
262 assert!(self.buflen() >= 4);
263 self.next_3_are('-', '-', '-') && is_blank_or_breakz(self.peek_nth(3))
264 }
265
266 /// Check whether the next characters correspond to an end of document.
267 ///
268 /// This function assumes that the next 4 characters in the input have already been fetched
269 /// through [`Input::lookahead`].
270 #[inline]
271 #[must_use]
272 fn next_is_document_end(&self) -> bool {
273 assert!(self.buflen() >= 4);
274 self.next_3_are('.', '.', '.') && is_blank_or_breakz(self.peek_nth(3))
275 }
276
277 /// Skip YAML whitespace up to the end of the current line.
278 ///
279 /// Inline comments are consumed only after at least one preceding YAML whitespace character.
280 ///
281 /// # Return
282 /// Return a tuple with the number of characters that were consumed and the result of skipping
283 /// whitespace. The number of characters returned can be used to advance the index and column,
284 /// since no end-of-line character will be consumed.
285 /// See [`SkipTabs`] for more details on the success variant.
286 ///
287 /// # Errors
288 /// Errors if a comment is encountered but it was not preceded by a whitespace. In that event,
289 /// the first tuple element will contain the number of characters consumed prior to reaching
290 /// the `#`.
291 fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, &'static str>) {
292 let mut encountered_tab = false;
293 let mut has_yaml_ws = false;
294 let mut chars_consumed = 0;
295 loop {
296 match self.look_ch() {
297 ' ' => {
298 has_yaml_ws = true;
299 self.skip();
300 }
301 '\t' if skip_tabs != SkipTabs::No => {
302 encountered_tab = true;
303 self.skip();
304 }
305 // YAML comments must be preceded by whitespace.
306 '#' if !encountered_tab && !has_yaml_ws => {
307 return (
308 chars_consumed,
309 Err("comments must be separated from other tokens by whitespace"),
310 );
311 }
312 '#' => {
313 self.skip(); // Skip over '#'
314 while !is_breakz(self.look_ch()) {
315 self.skip();
316 chars_consumed += 1;
317 }
318 }
319 _ => break,
320 }
321 chars_consumed += 1;
322 }
323
324 (
325 chars_consumed,
326 Ok(SkipTabs::Result(encountered_tab, has_yaml_ws)),
327 )
328 }
329
330 /// Skip YAML blank characters, stopping before comments, line breaks, or other content.
331 ///
332 /// This is the comment-aware counterpart to [`Input::skip_ws_to_eol`]: it preserves a
333 /// following `#` for the scanner to tokenize while still letting input implementations batch
334 /// the common run of spaces and tabs.
335 ///
336 /// # Return
337 /// Returns the number of consumed characters and a [`SkipTabs::Result`] describing whether
338 /// tabs and valid YAML whitespace (` `) were encountered.
339 fn skip_ws_to_eol_blanks(&mut self, skip_tabs: SkipTabs) -> (usize, SkipTabs) {
340 assert!(!matches!(skip_tabs, SkipTabs::Result(..)));
341
342 let mut encountered_tab = false;
343 let mut has_yaml_ws = false;
344 let mut chars_consumed = 0;
345
346 loop {
347 match self.look_ch() {
348 ' ' => {
349 has_yaml_ws = true;
350 chars_consumed += 1;
351 self.skip();
352 }
353 '\t' if skip_tabs != SkipTabs::No => {
354 encountered_tab = true;
355 chars_consumed += 1;
356 self.skip();
357 }
358 _ => break,
359 }
360 }
361
362 (
363 chars_consumed,
364 SkipTabs::Result(encountered_tab, has_yaml_ws),
365 )
366 }
367
368 /// Check whether the next characters may be part of a plain scalar.
369 ///
370 /// This function assumes we are not given a blankz character.
371 #[allow(clippy::inline_always)]
372 #[inline(always)]
373 fn next_can_be_plain_scalar(&self, in_flow: bool) -> bool {
374 let nc = self.peek_nth(1);
375 match self.peek() {
376 // indicators can end a plain scalar, see 7.3.3. Plain Style
377 ':' if is_blank_or_breakz(nc) || (in_flow && is_flow(nc)) => false,
378 c if in_flow && is_flow(c) => false,
379 _ => true,
380 }
381 }
382
383 /// Check whether the next character is [a blank] or [a break].
384 ///
385 /// The character must have previously been fetched through [`lookahead`]
386 ///
387 /// # Return
388 /// Returns true if the character is [a blank] or [a break], false otherwise.
389 ///
390 /// [`lookahead`]: Input::lookahead
391 /// [a blank]: is_blank
392 /// [a break]: is_break
393 #[inline]
394 fn next_is_blank_or_break(&self) -> bool {
395 is_blank(self.peek()) || is_break(self.peek())
396 }
397
398 /// Check whether the next character is [a blank] or [a breakz].
399 ///
400 /// The character must have previously been fetched through [`lookahead`]
401 ///
402 /// # Return
403 /// Returns true if the character is [a blank] or [a break], false otherwise.
404 ///
405 /// [`lookahead`]: Input::lookahead
406 /// [a blank]: is_blank
407 /// [a breakz]: is_breakz
408 #[inline]
409 fn next_is_blank_or_breakz(&self) -> bool {
410 is_blank(self.peek()) || is_breakz(self.peek())
411 }
412
413 /// Check whether the next character is [a blank].
414 ///
415 /// The character must have previously been fetched through [`lookahead`]
416 ///
417 /// # Return
418 /// Returns true if the character is [a blank], false otherwise.
419 ///
420 /// [`lookahead`]: Input::lookahead
421 /// [a blank]: is_blank
422 #[inline]
423 fn next_is_blank(&self) -> bool {
424 is_blank(self.peek())
425 }
426
427 /// Check whether the next character is [a break].
428 ///
429 /// The character must have previously been fetched through [`lookahead`]
430 ///
431 /// # Return
432 /// Returns true if the character is [a break], false otherwise.
433 ///
434 /// [`lookahead`]: Input::lookahead
435 /// [a break]: is_break
436 #[inline]
437 fn next_is_break(&self) -> bool {
438 is_break(self.peek())
439 }
440
441 /// Check whether the next character is [a breakz].
442 ///
443 /// The character must have previously been fetched through [`lookahead`]
444 ///
445 /// # Return
446 /// Returns true if the character is [a breakz], false otherwise.
447 ///
448 /// [`lookahead`]: Input::lookahead
449 /// [a breakz]: is_breakz
450 #[inline]
451 fn next_is_breakz(&self) -> bool {
452 is_breakz(self.peek())
453 }
454
455 /// Check whether the next character is [a z].
456 ///
457 /// The character must have previously been fetched through [`lookahead`]
458 ///
459 /// # Return
460 /// Returns true if the character is [a z], false otherwise.
461 ///
462 /// [`lookahead`]: Input::lookahead
463 /// [a z]: is_z
464 #[inline]
465 fn next_is_z(&self) -> bool {
466 is_z(self.peek())
467 }
468
469 /// Check whether the next character is [a flow].
470 ///
471 /// The character must have previously been fetched through [`lookahead`]
472 ///
473 /// # Return
474 /// Returns true if the character is [a flow], false otherwise.
475 ///
476 /// [`lookahead`]: Input::lookahead
477 /// [a flow]: is_flow
478 #[inline]
479 fn next_is_flow(&self) -> bool {
480 is_flow(self.peek())
481 }
482
483 /// Check whether the next character is [a digit].
484 ///
485 /// The character must have previously been fetched through [`lookahead`]
486 ///
487 /// # Return
488 /// Returns true if the character is [a digit], false otherwise.
489 ///
490 /// [`lookahead`]: Input::lookahead
491 /// [a digit]: is_digit
492 #[inline]
493 fn next_is_digit(&self) -> bool {
494 is_digit(self.peek())
495 }
496
497 /// Check whether the next character is [a letter].
498 ///
499 /// The character must have previously been fetched through [`lookahead`]
500 ///
501 /// # Return
502 /// Returns true if the character is [a letter], false otherwise.
503 ///
504 /// [`lookahead`]: Input::lookahead
505 /// [a letter]: is_alpha
506 #[inline]
507 fn next_is_alpha(&self) -> bool {
508 is_alpha(self.peek())
509 }
510
511 /// Skip characters from the input until a [breakz] is found.
512 ///
513 /// The characters are consumed from the input.
514 ///
515 /// # Return
516 /// Return the number of characters that were consumed. The number of characters returned can
517 /// be used to advance the index and column, since no end-of-line character will be consumed.
518 ///
519 /// [breakz]: is_breakz
520 #[inline]
521 fn skip_while_non_breakz(&mut self) -> usize {
522 let mut count = 0;
523 while !is_breakz(self.look_ch()) {
524 count += 1;
525 self.skip();
526 }
527 count
528 }
529
530 /// Skip characters from the input while [blanks] are found.
531 ///
532 /// The characters are consumed from the input.
533 ///
534 /// # Return
535 /// Return the number of characters that were consumed. The number of characters returned can
536 /// be used to advance the index and column, since no end-of-line character will be consumed.
537 ///
538 /// [blanks]: is_blank
539 fn skip_while_blank(&mut self) -> usize {
540 let mut n_bytes = 0;
541 while is_blank(self.look_ch()) {
542 n_bytes += self.peek().len_utf8();
543 self.skip();
544 }
545 n_bytes
546 }
547
548 /// Fetch characters from the input while we encounter letters and store them in `out`.
549 ///
550 /// The characters are consumed from the input.
551 ///
552 /// # Return
553 /// Return the number of characters that were consumed. The number of characters returned can
554 /// be used to advance the index and column, since no end-of-line character will be consumed.
555 fn fetch_while_is_alpha(&mut self, out: &mut String) -> usize {
556 let mut n_bytes = 0;
557 while is_alpha(self.look_ch()) {
558 let c = self.peek();
559 n_bytes += c.len_utf8();
560 out.push(c);
561 self.skip();
562 }
563 n_bytes
564 }
565
566 /// Fetch characters as long as they satisfy `is_yaml_non_space(c)`.
567 ///
568 /// The characters are consumed from the input.
569 ///
570 /// # Return
571 /// Return the number of characters that were consumed. The number of characters returned can
572 /// be used to advance the index and column, since no end-of-line character will be consumed.
573 fn fetch_while_is_yaml_non_space(&mut self, out: &mut String) -> usize {
574 let mut chars_consumed = 0;
575 loop {
576 let c = self.look_ch();
577 if !crate::char_traits::is_yaml_non_space(c) || is_z(c) {
578 break;
579 }
580 let c = self.peek();
581 out.push(c);
582 self.skip();
583 chars_consumed += 1;
584 }
585 chars_consumed
586 }
587
588 /// Fetch a chunk of plain scalar characters.
589 ///
590 /// This optimization method allows the input to batch process characters.
591 /// Returns (stopped, `chars_consumed`).
592 /// stopped is true if the chunk ended because of a non-plain-scalar character.
593 fn fetch_plain_scalar_chunk(
594 &mut self,
595 out: &mut String,
596 count: usize,
597 flow_level_gt_0: bool,
598 ) -> (bool, usize) {
599 let mut chars_consumed = 0;
600 for _ in 0..count {
601 self.lookahead(1);
602 if self.next_is_blank_or_breakz() || !self.next_can_be_plain_scalar(flow_level_gt_0) {
603 return (true, chars_consumed);
604 }
605 out.push(self.peek());
606 self.skip();
607 chars_consumed += 1;
608 }
609 (false, chars_consumed)
610 }
611}
612
613/// Behavior to adopt regarding treating tabs as whitespace.
614///
615/// Although tab is valid YAML whitespace, it does not always behave the same as a space.
616#[derive(Copy, Clone, Eq, PartialEq)]
617pub enum SkipTabs {
618 /// Skip all tabs as whitespace.
619 Yes,
620 /// Don't skip any tab. Return from the function when encountering one.
621 No,
622 /// Return value from the function.
623 Result(
624 /// Whether tabs were encountered.
625 bool,
626 /// Whether at least one valid YAML whitespace character has been encountered.
627 bool,
628 ),
629}
630
631impl SkipTabs {
632 /// Whether tabs were found while skipping whitespace.
633 ///
634 /// This function must be called after a call to `skip_ws_to_eol`.
635 #[must_use]
636 pub fn found_tabs(self) -> bool {
637 matches!(self, SkipTabs::Result(true, _))
638 }
639
640 /// Whether a valid YAML whitespace has been found in skipped-over content.
641 ///
642 /// This function must be called after a call to `skip_ws_to_eol`.
643 #[must_use]
644 pub fn has_valid_yaml_ws(self) -> bool {
645 matches!(self, SkipTabs::Result(_, true))
646 }
647}
648
649#[cfg(test)]
650mod tests {
651 use super::{Input, SkipTabs};
652
653 struct MinimalInput;
654
655 impl Input for MinimalInput {
656 fn lookahead(&mut self, _count: usize) {}
657
658 fn buflen(&self) -> usize {
659 0
660 }
661
662 fn bufmaxlen(&self) -> usize {
663 0
664 }
665
666 fn raw_read_ch(&mut self) -> char {
667 '\0'
668 }
669
670 fn raw_read_non_breakz_ch(&mut self) -> Option<char> {
671 None
672 }
673
674 fn skip(&mut self) {}
675
676 fn skip_n(&mut self, _count: usize) {}
677
678 fn peek(&self) -> char {
679 '\0'
680 }
681
682 fn peek_nth(&self, _n: usize) -> char {
683 '\0'
684 }
685 }
686
687 #[test]
688 fn default_slice_bytes_returns_none() {
689 let mut input = MinimalInput;
690
691 input.lookahead(4);
692 assert_eq!(input.buflen(), 0);
693 assert_eq!(input.bufmaxlen(), 0);
694 assert_eq!(input.raw_read_ch(), '\0');
695 assert_eq!(input.raw_read_non_breakz_ch(), None);
696 input.skip();
697 input.skip_n(2);
698 assert_eq!(input.peek(), '\0');
699 assert_eq!(input.peek_nth(1), '\0');
700 assert_eq!(input.byte_offset(), None);
701 assert_eq!(input.slice_bytes(0, 0), None);
702 }
703
704 #[test]
705 fn default_skip_ws_to_eol_rejects_unseparated_comment() {
706 let mut input = super::buffered::BufferedInput::new("#comment\n".chars());
707
708 let (consumed, result) = input.skip_ws_to_eol(SkipTabs::Yes);
709
710 assert_eq!(consumed, 0);
711 assert_eq!(
712 result.err(),
713 Some("comments must be separated from other tokens by whitespace")
714 );
715 assert_eq!(input.peek(), '#');
716 }
717}