combine/parser/
range.rs

1//! Module containing zero-copy parsers.
2//!
3//! These parsers require the [`RangeStream`][] bound instead of a plain [`Stream`][].
4//!
5//! [`RangeStream`]: ../../stream/trait.RangeStream.html
6//! [`Stream`]: ../../stream/trait.Stream.html
7
8use crate::{
9    error::{
10        self, ParseError,
11        ParseResult::{self, *},
12        ResultExt, StreamError, Tracked,
13    },
14    lib::{convert::TryFrom, marker::PhantomData},
15    parser::ParseMode,
16};
17
18#[cfg(feature = "std")]
19use crate::lib::error::Error as StdError;
20
21#[cfg(not(feature = "std"))]
22use crate::lib::fmt;
23
24use crate::stream::{
25    uncons_range, uncons_while, uncons_while1, wrap_stream_error, Range as StreamRange,
26    RangeStream, StreamErrorFor, StreamOnce,
27};
28
29use crate::Parser;
30
31pub struct Range<Input>(Input::Range)
32where
33    Input: RangeStream;
34
35impl<Input> Parser<Input> for Range<Input>
36where
37    Input: RangeStream,
38    Input::Range: PartialEq + crate::stream::Range,
39{
40    type Output = Input::Range;
41    type PartialState = ();
42
43    #[inline]
44    fn parse_lazy(
45        &mut self,
46        input: &mut Input,
47    ) -> ParseResult<Self::Output, <Input as StreamOnce>::Error> {
48        use crate::stream::Range;
49
50        let position = input.position();
51        match input.uncons_range(self.0.len()) {
52            Ok(other) => {
53                if other == self.0 {
54                    CommitOk(other)
55                } else {
56                    PeekErr(Input::Error::empty(position).into())
57                }
58            }
59            Err(err) => wrap_stream_error(input, err),
60        }
61    }
62    fn add_error(&mut self, errors: &mut Tracked<<Input as StreamOnce>::Error>) {
63        // TODO Add unexpected message?
64        errors.error.add_expected(error::Range(self.0.clone()));
65    }
66}
67
68parser! {
69    #[derive(Clone)]
70    pub struct Recognize;
71    type PartialState = <RecognizeWithValue<P> as Parser<Input>>::PartialState;
72    /// Zero-copy parser which returns committed input range.
73    ///
74    /// [`combinator::recognize`][] is a non-`RangeStream` alternative.
75    ///
76    /// [`combinator::recognize`]: ../../parser/combinator/fn.recognize.html
77    /// ```
78    /// # extern crate combine;
79    /// # use combine::parser::range::recognize;
80    /// # use combine::parser::char::letter;
81    /// # use combine::*;
82    /// # fn main() {
83    /// let mut parser = recognize(skip_many1(letter()));
84    /// assert_eq!(parser.parse("hello world"), Ok(("hello", " world")));
85    /// assert!(parser.parse("!").is_err());
86    /// # }
87    /// ```
88    pub fn recognize[Input, P](parser: P)(Input) -> <Input as StreamOnce>::Range
89    where [
90        P: Parser<Input>,
91        Input: RangeStream,
92        <Input as StreamOnce>::Range: crate::stream::Range,
93    ]
94    {
95        recognize_with_value(parser).map(|(range, _)| range)
96    }
97}
98
99#[inline]
100fn parse_partial_range<M, F, G, S, Input>(
101    mode: M,
102    input: &mut Input,
103    distance_state: &mut usize,
104    state: S,
105    first: F,
106    resume: G,
107) -> ParseResult<Input::Range, Input::Error>
108where
109    M: ParseMode,
110    F: FnOnce(&mut Input, S) -> ParseResult<Input::Range, <Input as StreamOnce>::Error>,
111    G: FnOnce(&mut Input, S) -> ParseResult<Input::Range, <Input as StreamOnce>::Error>,
112    Input: RangeStream,
113{
114    let before = input.checkpoint();
115
116    if !input.is_partial() {
117        first(input, state)
118    } else if mode.is_first() || *distance_state == 0 {
119        let result = first(input, state);
120        if let CommitErr(_) = result {
121            *distance_state = input.distance(&before);
122            ctry!(input.reset(before).committed());
123        }
124        result
125    } else {
126        if input.uncons_range(*distance_state).is_err() {
127            panic!("recognize errored when restoring the input stream to its expected state");
128        }
129
130        match resume(input, state) {
131            CommitOk(_) | PeekOk(_) => (),
132            PeekErr(err) => return PeekErr(err),
133            CommitErr(err) => {
134                *distance_state = input.distance(&before);
135                ctry!(input.reset(before).committed());
136                return CommitErr(err);
137            }
138        }
139
140        let distance = input.distance(&before);
141        ctry!(input.reset(before).committed());
142        take(distance).parse_lazy(input).map(|range| {
143            *distance_state = 0;
144            range
145        })
146    }
147}
148
149#[derive(Clone)]
150pub struct RecognizeWithValue<P>(P);
151
152impl<Input, P> Parser<Input> for RecognizeWithValue<P>
153where
154    P: Parser<Input>,
155    Input: RangeStream,
156    <Input as StreamOnce>::Range: crate::stream::Range,
157{
158    type Output = (<Input as StreamOnce>::Range, P::Output);
159    type PartialState = (usize, P::PartialState);
160
161    parse_mode!(Input);
162    #[inline]
163    fn parse_mode<M>(
164        &mut self,
165        mode: M,
166        input: &mut Input,
167        state: &mut Self::PartialState,
168    ) -> ParseResult<Self::Output, <Input as StreamOnce>::Error>
169    where
170        M: ParseMode,
171    {
172        let (ref mut distance_state, ref mut child_state) = *state;
173
174        let before = input.checkpoint();
175        if !mode.is_first() && input.uncons_range(*distance_state).is_err() {
176            panic!("recognize errored when restoring the input stream to its expected state");
177        }
178
179        let value = match self.0.parse_mode(mode, input, child_state) {
180            CommitOk(x) | PeekOk(x) => x,
181            PeekErr(err) => return PeekErr(err),
182            CommitErr(err) => {
183                *distance_state = input.distance(&before);
184                ctry!(input.reset(before).committed());
185                return CommitErr(err);
186            }
187        };
188
189        let distance = input.distance(&before);
190        ctry!(input.reset(before).committed());
191        take(distance).parse_lazy(input).map(|range| {
192            *distance_state = 0;
193            (range, value)
194        })
195    }
196    fn add_error(&mut self, errors: &mut Tracked<<Input as StreamOnce>::Error>) {
197        self.0.add_error(errors)
198    }
199}
200
201/// Zero-copy parser which returns a pair: (committed input range, parsed value).
202///
203///
204/// [`combinator::recognize_with_value`] is a non-`RangeStream` alternative.
205///
206/// [`combinator::recognize_with_value`]: recognize_with_value
207/// ```
208/// # extern crate combine;
209/// # use combine::parser::range::recognize_with_value;
210/// # use combine::parser::char::{digit, char};
211/// # use combine::*;
212/// # fn main() {
213/// let mut parser = recognize_with_value((
214///     skip_many1(digit()),
215///     optional((attempt(char('.')), skip_many1(digit()))),
216/// ).map(|(_, opt)| opt.is_some()));
217///
218/// assert_eq!(parser.parse("1234!"), Ok((("1234", false), "!")));
219/// assert_eq!(parser.parse("1234.0001!"), Ok((("1234.0001", true), "!")));
220/// assert!(parser.parse("!").is_err());
221/// assert!(parser.parse("1234.").is_err());
222/// # }
223/// ```
224pub fn recognize_with_value<Input, P>(parser: P) -> RecognizeWithValue<P>
225where
226    P: Parser<Input>,
227    Input: RangeStream,
228    <Input as StreamOnce>::Range: crate::stream::Range,
229{
230    RecognizeWithValue(parser)
231}
232
233/// Zero-copy parser which reads a range of length `i.len()` and succeeds if `i` is equal to that
234/// range.
235///
236/// [`tokens`] is a non-`RangeStream` alternative.
237///
238/// [`tokens`]: super::token::tokens
239/// ```
240/// # extern crate combine;
241/// # use combine::parser::range::range;
242/// # use combine::*;
243/// # fn main() {
244/// let mut parser = range("hello");
245/// let result = parser.parse("hello world");
246/// assert_eq!(result, Ok(("hello", " world")));
247/// let result = parser.parse("hel world");
248/// assert!(result.is_err());
249/// # }
250/// ```
251pub fn range<Input>(i: Input::Range) -> Range<Input>
252where
253    Input: RangeStream,
254    Input::Range: PartialEq,
255{
256    Range(i)
257}
258
259pub struct Take<Input>(usize, PhantomData<fn(Input)>);
260impl<Input> Parser<Input> for Take<Input>
261where
262    Input: RangeStream,
263{
264    type Output = Input::Range;
265    type PartialState = ();
266
267    #[inline]
268    fn parse_lazy(
269        &mut self,
270        input: &mut Input,
271    ) -> ParseResult<Self::Output, <Input as StreamOnce>::Error> {
272        uncons_range(input, self.0)
273    }
274}
275
276/// Zero-copy parser which reads a range of length `n`.
277///
278/// [`count_min_max`][] is a non-`RangeStream` alternative.
279///
280/// [`count_min_max`]: ../../parser/repeat/fn.count_min_max.html
281/// ```
282/// # extern crate combine;
283/// # use combine::parser::range::take;
284/// # use combine::*;
285/// # fn main() {
286/// let mut parser = take(1);
287/// let result = parser.parse("1");
288/// assert_eq!(result, Ok(("1", "")));
289/// let mut parser = take(4);
290/// let result = parser.parse("123abc");
291/// assert_eq!(result, Ok(("123a", "bc")));
292/// let result = parser.parse("abc");
293/// assert!(result.is_err());
294/// # }
295/// ```
296pub fn take<Input>(n: usize) -> Take<Input>
297where
298    Input: RangeStream,
299{
300    Take(n, PhantomData)
301}
302
303pub struct TakeWhile<Input, F>(F, PhantomData<fn(Input) -> Input>);
304impl<Input, F> Parser<Input> for TakeWhile<Input, F>
305where
306    Input: RangeStream,
307    Input::Range: crate::stream::Range,
308    F: FnMut(Input::Token) -> bool,
309{
310    type Output = Input::Range;
311    type PartialState = usize;
312
313    parse_mode!(Input);
314    #[inline]
315    fn parse_mode_impl<M>(
316        &mut self,
317        mode: M,
318        input: &mut Input,
319        state: &mut Self::PartialState,
320    ) -> ParseResult<Self::Output, <Input as StreamOnce>::Error>
321    where
322        M: ParseMode,
323    {
324        parse_partial_range(
325            mode,
326            input,
327            state,
328            &mut self.0,
329            |input, predicate| uncons_while(input, predicate),
330            |input, predicate| uncons_while(input, predicate),
331        )
332    }
333}
334
335/// Zero-copy parser which reads a range of 0 or more tokens which satisfy `f`.
336///
337/// [`many`][] is a non-`RangeStream` alternative.
338///
339/// [`many`]: ../../parser/repeat/fn.many.html
340/// ```
341/// # extern crate combine;
342/// # use combine::parser::range::take_while;
343/// # use combine::*;
344/// # fn main() {
345/// let mut parser = take_while(|c: char| c.is_digit(10));
346/// let result = parser.parse("123abc");
347/// assert_eq!(result, Ok(("123", "abc")));
348/// let result = parser.parse("abc");
349/// assert_eq!(result, Ok(("", "abc")));
350/// # }
351/// ```
352pub fn take_while<Input, F>(f: F) -> TakeWhile<Input, F>
353where
354    Input: RangeStream,
355    Input::Range: crate::stream::Range,
356    F: FnMut(Input::Token) -> bool,
357{
358    TakeWhile(f, PhantomData)
359}
360
361pub struct TakeWhile1<Input, F>(F, PhantomData<fn(Input) -> Input>);
362impl<Input, F> Parser<Input> for TakeWhile1<Input, F>
363where
364    Input: RangeStream,
365    Input::Range: crate::stream::Range,
366    F: FnMut(Input::Token) -> bool,
367{
368    type Output = Input::Range;
369    type PartialState = usize;
370
371    parse_mode!(Input);
372    #[inline]
373    fn parse_mode_impl<M>(
374        &mut self,
375        mode: M,
376        input: &mut Input,
377        state: &mut Self::PartialState,
378    ) -> ParseResult<Self::Output, <Input as StreamOnce>::Error>
379    where
380        M: ParseMode,
381    {
382        parse_partial_range(
383            mode,
384            input,
385            state,
386            &mut self.0,
387            |input, predicate| uncons_while1(input, predicate),
388            |input, predicate| uncons_while(input, predicate),
389        )
390    }
391}
392
393/// Zero-copy parser which reads a range of 1 or more tokens which satisfy `f`.
394///
395/// [`many1`][] is a non-`RangeStream` alternative.
396///
397/// [`many1`]: ../../parser/repeat/fn.many1.html
398/// ```
399/// # extern crate combine;
400/// # use combine::parser::range::take_while1;
401/// # use combine::*;
402/// # fn main() {
403/// let mut parser = take_while1(|c: char| c.is_digit(10));
404/// let result = parser.parse("123abc");
405/// assert_eq!(result, Ok(("123", "abc")));
406/// let result = parser.parse("abc");
407/// assert!(result.is_err());
408/// # }
409/// ```
410pub fn take_while1<Input, F>(f: F) -> TakeWhile1<Input, F>
411where
412    Input: RangeStream,
413    Input::Range: crate::stream::Range,
414    F: FnMut(Input::Token) -> bool,
415{
416    TakeWhile1(f, PhantomData)
417}
418
419pub struct TakeUntilRange<Input>(Input::Range)
420where
421    Input: RangeStream;
422impl<Input> Parser<Input> for TakeUntilRange<Input>
423where
424    Input: RangeStream,
425    Input::Range: PartialEq + crate::stream::Range,
426{
427    type Output = Input::Range;
428    type PartialState = usize;
429
430    #[inline]
431    fn parse_partial(
432        &mut self,
433        input: &mut Input,
434        to_consume: &mut Self::PartialState,
435    ) -> ParseResult<Self::Output, <Input as StreamOnce>::Error> {
436        use crate::stream::Range;
437
438        let len = self.0.len();
439        let before = input.checkpoint();
440        let mut first_stream_error = None;
441
442        // Skip until the end of the last parse attempt
443        ctry!(uncons_range(input, *to_consume));
444
445        loop {
446            let look_ahead_input = input.checkpoint();
447
448            match input.uncons_range(len) {
449                Ok(xs) => {
450                    if xs == self.0 {
451                        let distance = input.distance(&before) - len;
452                        ctry!(input.reset(before).committed());
453
454                        if let Ok(committed) = input.uncons_range(distance) {
455                            if distance == 0 {
456                                return PeekOk(committed);
457                            } else {
458                                *to_consume = 0;
459                                return CommitOk(committed);
460                            }
461                        }
462
463                        // We are guaranteed able to uncons to_consume characters here
464                        // because we've already done it on look_ahead_input.
465                        unreachable!();
466                    } else {
467                        // Reset the stream back to where it was when we entered the top of the loop
468                        ctry!(input.reset(look_ahead_input).committed());
469
470                        // Advance the stream by one token
471                        if input.uncons().is_err() {
472                            unreachable!();
473                        }
474                    }
475                }
476                Err(first_error) => {
477                    // If we are unable to find a successful parse even after advancing with `uncons`
478                    // below we must reset the stream to its state before the first error.
479                    // If we don't we may try and match the range `::` against `:<EOF>` which would
480                    // fail as only one `:` is present at this parse attempt. But when we later resume
481                    // with more input we must start parsing again at the first time we errored so we
482                    // can see the entire `::`
483                    if first_stream_error.is_none() {
484                        first_stream_error = Some((first_error, input.distance(&before)));
485                    }
486
487                    // Reset the stream back to where it was when we entered the top of the loop
488                    ctry!(input.reset(look_ahead_input).committed());
489
490                    // See if we can advance anyway
491                    if input.uncons().is_err() {
492                        let (first_error, first_error_distance) = first_stream_error.unwrap();
493
494                        // Reset the stream
495                        ctry!(input.reset(before).committed());
496                        *to_consume = first_error_distance;
497
498                        // Return the original error if uncons failed
499                        return wrap_stream_error(input, first_error);
500                    }
501                }
502            };
503        }
504    }
505}
506
507/// Zero-copy parser which reads a range of 0 or more tokens until `r` is found.
508///
509/// The range `r` will not be committed. If `r` is not found, the parser will
510/// return an error.
511///
512/// [`repeat::take_until`][] is a non-`RangeStream` alternative.
513///
514/// [`repeat::take_until`]: ../../parser/repeat/fn.take_until.html
515/// ```
516/// # extern crate combine;
517/// # use combine::parser::range::{range, take_until_range};
518/// # use combine::*;
519/// # fn main() {
520/// let mut parser = take_until_range("\r\n");
521/// let result = parser.parse("To: user@example.com\r\n");
522/// assert_eq!(result, Ok(("To: user@example.com", "\r\n")));
523/// let result = parser.parse("Hello, world\n");
524/// assert!(result.is_err());
525/// # }
526/// ```
527pub fn take_until_range<Input>(r: Input::Range) -> TakeUntilRange<Input>
528where
529    Input: RangeStream,
530{
531    TakeUntilRange(r)
532}
533
534#[derive(Debug, PartialEq)]
535pub enum TakeRange {
536    /// Found the pattern at this offset
537    Found(usize),
538    /// Did not find the pattern but the parser can skip ahead to this offset.
539    NotFound(usize),
540}
541
542impl From<Option<usize>> for TakeRange {
543    fn from(opt: Option<usize>) -> TakeRange {
544        match opt {
545            Some(i) => TakeRange::Found(i),
546            None => TakeRange::NotFound(0),
547        }
548    }
549}
550
551pub struct TakeFn<F, Input> {
552    searcher: F,
553    _marker: PhantomData<fn(Input)>,
554}
555
556impl<Input, F, R> Parser<Input> for TakeFn<F, Input>
557where
558    F: FnMut(Input::Range) -> R,
559    R: Into<TakeRange>,
560    Input: RangeStream,
561    Input::Range: crate::stream::Range,
562{
563    type Output = Input::Range;
564    type PartialState = usize;
565
566    parse_mode!(Input);
567    #[inline]
568    fn parse_mode<M>(
569        &mut self,
570        mode: M,
571        input: &mut Input,
572        offset: &mut Self::PartialState,
573    ) -> ParseResult<Self::Output, <Input as StreamOnce>::Error>
574    where
575        M: ParseMode,
576    {
577        let checkpoint = input.checkpoint();
578
579        if mode.is_first() {
580            *offset = 0;
581        } else {
582            let _ = input.uncons_range(*offset);
583        }
584
585        match (self.searcher)(input.range()).into() {
586            TakeRange::Found(i) => {
587                ctry!(input.reset(checkpoint).committed());
588                let result = uncons_range(input, *offset + i);
589                if result.is_ok() {
590                    *offset = 0;
591                }
592                result
593            }
594            TakeRange::NotFound(next_offset) => {
595                *offset = next_offset;
596
597                let range = input.range();
598                let _ = input.uncons_range(range.len());
599                let position = input.position();
600                ctry!(input.reset(checkpoint).committed());
601
602                let err = Input::Error::from_error(position, StreamError::end_of_input());
603                if !input.is_partial() && range.is_empty() {
604                    PeekErr(err.into())
605                } else {
606                    CommitErr(err)
607                }
608            }
609        }
610    }
611}
612
613/// Searches the entire range using `searcher` and then consumes a range of `Some(n)`.
614/// If `f` can not find anything in the range it must return `None/NotFound` which indicates an end of input error.
615///
616/// If partial parsing is used the `TakeRange` enum can be returned instead of `Option`. By
617/// returning `TakeRange::NotFound(n)` it indicates that the input can skip ahead until `n`
618/// when parsing is next resumed.
619///
620/// See [`take_until_bytes`](../byte/fn.take_until_bytes.html) for a usecase.
621pub fn take_fn<F, R, Input>(searcher: F) -> TakeFn<F, Input>
622where
623    F: FnMut(Input::Range) -> R,
624    R: Into<TakeRange>,
625    Input: RangeStream,
626    Input::Range: crate::stream::Range,
627{
628    TakeFn {
629        searcher,
630        _marker: PhantomData,
631    }
632}
633
634#[cfg(feature = "std")]
635parser! {
636/// Takes a parser which parses a `length` then extracts a range of that length and returns it.
637/// Commonly used in binary formats
638///
639/// ```
640/// # use combine::parser::{byte::num::be_u16, range::length_prefix};
641/// # use combine::*;
642/// # fn main() {
643/// let mut input = Vec::new();
644/// input.extend_from_slice(&3u16.to_be_bytes());
645/// input.extend_from_slice(b"1234");
646///
647/// let mut parser = length_prefix(be_u16());
648/// let result = parser.parse(&input[..]);
649/// assert_eq!(result, Ok((&b"123"[..], &b"4"[..])));
650/// # }
651/// ```
652pub fn length_prefix[Input, P](len: P)(Input) -> Input::Range
653where [
654    Input: RangeStream,
655    P: Parser<Input>,
656    usize: TryFrom<P::Output>,
657    <usize as TryFrom<P::Output>>::Error: StdError + Send + Sync + 'static,
658]
659{
660    len
661        .and_then(|u| {
662            usize::try_from(u)
663                .map_err(StreamErrorFor::<Input>::other)
664        })
665        .then_partial(|&mut len| take(len))
666}
667}
668
669#[cfg(not(feature = "std"))]
670parser! {
671/// Takes a parser which parses a `length` then extracts a range of that length and returns it.
672/// Commonly used in binary formats
673///
674/// ```
675/// # use combine::parser::{byte::num::be_u16, range::length_prefix};
676/// # use combine::*;
677/// # fn main() {
678/// let mut input = Vec::new();
679/// input.extend_from_slice(&3u16.to_be_bytes());
680/// input.extend_from_slice(b"1234");
681///
682/// let mut parser = length_prefix(be_u16());
683/// let result = parser.parse(&input[..]);
684/// assert_eq!(result, Ok((&b"123"[..], &b"4"[..])));
685/// # }
686/// ```
687pub fn length_prefix[Input, P](len: P)(Input) -> Input::Range
688where [
689    Input: RangeStream,
690    P: Parser<Input>,
691    usize: TryFrom<P::Output>,
692    <usize as TryFrom<P::Output>>::Error: fmt::Display + Send + Sync + 'static,
693]
694{
695    len
696        .and_then(|u| {
697            usize::try_from(u)
698                .map_err(StreamErrorFor::<Input>::message_format)
699        })
700        .then_partial(|&mut len| take(len))
701}
702}
703
704#[cfg(test)]
705mod tests {
706
707    use crate::Parser;
708
709    use super::*;
710
711    #[test]
712    fn take_while_test() {
713        let result = take_while(|c: char| c.is_digit(10)).parse("123abc");
714        assert_eq!(result, Ok(("123", "abc")));
715        let result = take_while(|c: char| c.is_digit(10)).parse("abc");
716        assert_eq!(result, Ok(("", "abc")));
717    }
718
719    #[test]
720    fn take_while1_test() {
721        let result = take_while1(|c: char| c.is_digit(10)).parse("123abc");
722        assert_eq!(result, Ok(("123", "abc")));
723        let result = take_while1(|c: char| c.is_digit(10)).parse("abc");
724        assert!(result.is_err());
725    }
726
727    #[test]
728    fn range_string_no_char_boundary_error() {
729        let mut parser = range("hello");
730        let result = parser.parse("hell\u{00EE} world");
731        assert!(result.is_err());
732    }
733
734    #[test]
735    fn take_until_range_1() {
736        let result = take_until_range("\"").parse("Foo baz bar quux\"");
737        assert_eq!(result, Ok(("Foo baz bar quux", "\"")));
738    }
739
740    #[test]
741    fn take_until_range_2() {
742        let result = take_until_range("===").parse("if ((pointless_comparison == 3) === true) {");
743        assert_eq!(
744            result,
745            Ok(("if ((pointless_comparison == 3) ", "=== true) {"))
746        );
747    }
748
749    #[test]
750    fn take_until_range_unicode_1() {
751        let result = take_until_range("🦀")
752            .parse("😃 Ferris the friendly rustacean 🦀 and his snake friend 🐍");
753        assert_eq!(
754            result,
755            Ok((
756                "😃 Ferris the friendly rustacean ",
757                "🦀 and his snake friend 🐍"
758            ))
759        );
760    }
761
762    #[test]
763    fn take_until_range_unicode_2() {
764        let result = take_until_range("⁘⁙/⁘").parse("⚙️🛠️🦀=🏎️⁘⁙⁘⁘⁙/⁘⁘⁙/⁘");
765        assert_eq!(result, Ok(("⚙️🛠️🦀=🏎️⁘⁙⁘", "⁘⁙/⁘⁘⁙/⁘")));
766    }
767}