Skip to main content

pest/
position.rs

1// pest. The Elegant Parser
2// Copyright (c) 2018 Dragoș Tiselice
3//
4// Licensed under the Apache License, Version 2.0
5// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
6// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. All files in the project carrying such notice may not be copied,
8// modified, or distributed except according to those terms.
9
10use core::cmp::Ordering;
11use core::fmt;
12use core::hash::{Hash, Hasher};
13use core::ops::Range;
14use core::ptr;
15use core::str;
16
17use crate::span;
18
19/// A cursor position in a `&str` which provides useful methods to manually parse that string.
20#[derive(Clone, Copy)]
21pub struct Position<'i> {
22    input: &'i str,
23    pos: usize,
24}
25
26impl<'i> Position<'i> {
27    /// Create a new `Position` without checking invariants. (Checked with `debug_assertions`.)
28    pub(crate) fn new_internal(input: &str, pos: usize) -> Position<'_> {
29        debug_assert!(input.get(pos..).is_some());
30        Position { input, pos }
31    }
32
33    /// Attempts to create a new `Position` at the given position. If the specified position is
34    /// an invalid index, or the specified position is not a valid UTF8 boundary, then None is
35    /// returned.
36    ///
37    /// # Examples
38    /// ```
39    /// # use pest::Position;
40    /// let cheart = '💖';
41    /// let heart = "💖";
42    /// assert_eq!(Position::new(heart, 1), None);
43    /// assert_ne!(Position::new(heart, cheart.len_utf8()), None);
44    /// ```
45    pub fn new(input: &str, pos: usize) -> Option<Position<'_>> {
46        input.get(pos..).map(|_| Position { input, pos })
47    }
48
49    /// Creates a `Position` at the start of a `&str`.
50    ///
51    /// # Examples
52    ///
53    /// ```
54    /// # use pest::Position;
55    /// let start = Position::from_start("");
56    /// assert_eq!(start.pos(), 0);
57    /// ```
58    #[inline]
59    pub fn from_start(input: &'i str) -> Position<'i> {
60        // Position 0 is always safe because it's always a valid UTF-8 border.
61        Position { input, pos: 0 }
62    }
63
64    /// Returns the byte position of this `Position` as a `usize`.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// # use pest::Position;
70    /// let input = "ab";
71    /// let mut start = Position::from_start(input);
72    ///
73    /// assert_eq!(start.pos(), 0);
74    /// ```
75    #[inline]
76    pub fn pos(&self) -> usize {
77        self.pos
78    }
79
80    /// Creates a `Span` from two `Position`s.
81    ///
82    /// # Panics
83    ///
84    /// Panics if the positions come from different inputs.
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// # use pest::Position;
90    /// let input = "ab";
91    /// let start = Position::from_start(input);
92    /// let span = start.span(&start.clone());
93    ///
94    /// assert_eq!(span.start(), 0);
95    /// assert_eq!(span.end(), 0);
96    /// ```
97    #[inline]
98    pub fn span(&self, other: &Position<'i>) -> span::Span<'i> {
99        if ptr::eq(self.input, other.input)
100        /* && self.input.get(self.pos..other.pos).is_some() */
101        {
102            span::Span::new_internal(self.input, self.pos, other.pos)
103        } else {
104            // TODO: maybe a panic if self.pos < other.pos
105            panic!("span created from positions from different inputs")
106        }
107    }
108
109    /// Returns the line and column number of this `Position`.
110    ///
111    /// This is an O(n) operation, where n is the number of chars in the input.
112    /// You better use [`pair.line_col()`](struct.Pair.html#method.line_col) instead.
113    ///
114    /// # Panics
115    ///
116    /// Panics if the position is out of bounds.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// # use pest;
122    /// # #[allow(non_camel_case_types)]
123    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
124    /// enum Rule {}
125    ///
126    /// let input = "\na";
127    /// let mut state: Box<pest::ParserState<'_, Rule>> = pest::ParserState::new(input);
128    /// let mut result = state.match_string("\na");
129    /// assert!(result.is_ok());
130    /// assert_eq!(result.unwrap().position().line_col(), (2, 2));
131    /// ```
132    #[inline]
133    pub fn line_col(&self) -> (usize, usize) {
134        if self.pos > self.input.len() {
135            panic!("position out of bounds");
136        }
137        let mut pos = self.pos;
138        let slice = &self.input[..pos];
139        let mut chars = slice.chars().peekable();
140
141        let mut line_col = (1, 1);
142
143        while pos != 0 {
144            match chars.next() {
145                Some('\r') => {
146                    if let Some(&'\n') = chars.peek() {
147                        chars.next();
148
149                        if pos == 1 {
150                            pos -= 1;
151                        } else {
152                            pos -= 2;
153                        }
154
155                        line_col = (line_col.0 + 1, 1);
156                    } else {
157                        pos -= 1;
158                        line_col = (line_col.0, line_col.1 + 1);
159                    }
160                }
161                Some('\n') => {
162                    pos -= 1;
163                    line_col = (line_col.0 + 1, 1);
164                }
165                Some(c) => {
166                    pos -= c.len_utf8();
167                    line_col = (line_col.0, line_col.1 + 1);
168                }
169                None => unreachable!(),
170            }
171        }
172
173        line_col
174    }
175
176    /// Returns the entire line of the input that contains this `Position`.
177    ///
178    /// # Panics
179    ///
180    /// Panics if the position is out of bounds (e.g., beyond the end of the input).
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// # use pest;
186    /// # #[allow(non_camel_case_types)]
187    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
188    /// enum Rule {}
189    ///
190    /// let input = "\na";
191    /// let mut state: Box<pest::ParserState<'_, Rule>> = pest::ParserState::new(input);
192    /// let mut result = state.match_string("\na");
193    /// assert!(result.is_ok());
194    /// assert_eq!(result.unwrap().position().line_of(), "a");
195    /// ```
196    #[inline]
197    pub fn line_of(&self) -> &'i str {
198        if self.pos > self.input.len() {
199            panic!("position out of bounds");
200        };
201        // Safe since start and end can only be valid UTF-8 borders.
202        &self.input[self.find_line_start()..self.find_line_end()]
203    }
204
205    pub(crate) fn find_line_start(&self) -> usize {
206        if self.input.is_empty() {
207            return 0;
208        };
209        // Position's pos is always a UTF-8 border.
210        let start = self
211            .input
212            .char_indices()
213            .rev()
214            .skip_while(|&(i, _)| i >= self.pos)
215            .find(|&(_, c)| c == '\n');
216        match start {
217            Some((i, _)) => i + 1,
218            None => 0,
219        }
220    }
221
222    pub(crate) fn find_line_end(&self) -> usize {
223        if self.input.is_empty() {
224            0
225        } else if self.pos == self.input.len() - 1 {
226            self.input.len()
227        } else {
228            // Position's pos is always a UTF-8 border.
229            let end = self
230                .input
231                .char_indices()
232                .skip_while(|&(i, _)| i < self.pos)
233                .find(|&(_, c)| c == '\n');
234            match end {
235                Some((i, _)) => i + 1,
236                None => self.input.len(),
237            }
238        }
239    }
240
241    /// Returns `true` when the `Position` points to the start of the input `&str`.
242    #[inline]
243    pub(crate) fn at_start(&self) -> bool {
244        self.pos == 0
245    }
246
247    /// Returns `true` when the `Position` points to the end of the input `&str`.
248    #[inline]
249    pub(crate) fn at_end(&self) -> bool {
250        self.pos == self.input.len()
251    }
252
253    /// Skips `n` `char`s from the `Position` and returns `true` if the skip was possible or `false`
254    /// otherwise. If the return value is `false`, `pos` will not be updated.
255    #[inline]
256    pub(crate) fn skip(&mut self, n: usize) -> bool {
257        let skipped = {
258            let mut len = 0;
259            // Position's pos is always a UTF-8 border.
260            let mut chars = self.input[self.pos..].chars();
261            for _ in 0..n {
262                if let Some(c) = chars.next() {
263                    len += c.len_utf8();
264                } else {
265                    return false;
266                }
267            }
268            len
269        };
270
271        self.pos += skipped;
272        true
273    }
274
275    /// Goes back `n` `char`s from the `Position` and returns `true` if the skip was possible or `false`
276    /// otherwise. If the return value is `false`, `pos` will not be updated.
277    #[inline]
278    pub(crate) fn skip_back(&mut self, n: usize) -> bool {
279        let skipped = {
280            let mut len = 0;
281            // Position's pos is always a UTF-8 border.
282            let mut chars = self.input[..self.pos].chars().rev();
283            for _ in 0..n {
284                if let Some(c) = chars.next() {
285                    len += c.len_utf8();
286                } else {
287                    return false;
288                }
289            }
290            len
291        };
292
293        self.pos -= skipped;
294        true
295    }
296
297    /// Skips until one of the given `strings` is found. If none of the `strings` can be found,
298    /// this function will return `false` but its `pos` will *still* be updated.
299    #[inline]
300    pub(crate) fn skip_until(&mut self, strings: &[&str]) -> bool {
301        #[cfg(not(feature = "memchr"))]
302        {
303            self.skip_until_basic(strings)
304        }
305        #[cfg(feature = "memchr")]
306        {
307            match strings {
308                [] => (),
309                [s1] => {
310                    if let Some(from) =
311                        memchr::memmem::find(&self.input.as_bytes()[self.pos..], s1.as_bytes())
312                    {
313                        self.pos += from;
314                        return true;
315                    }
316                }
317                [s1, s2] if !s1.is_empty() && !s2.is_empty() => {
318                    let b1 = s1.as_bytes()[0];
319                    let b2 = s2.as_bytes()[0];
320                    let miter = memchr::memchr2_iter(b1, b2, &self.input.as_bytes()[self.pos..]);
321                    for from in miter {
322                        let start = &self.input[self.pos + from..];
323                        if start.starts_with(s1) || start.starts_with(s2) {
324                            self.pos += from;
325                            return true;
326                        }
327                    }
328                }
329                [s1, s2, s3] if !s1.is_empty() && !s2.is_empty() && !s3.is_empty() => {
330                    let b1 = s1.as_bytes()[0];
331                    let b2 = s2.as_bytes()[0];
332                    let b3 = s3.as_bytes()[0];
333                    let miter =
334                        memchr::memchr3_iter(b1, b2, b3, &self.input.as_bytes()[self.pos..]);
335                    for from in miter {
336                        let start = &self.input[self.pos + from..];
337                        if start.starts_with(s1) || start.starts_with(s2) || start.starts_with(s3) {
338                            self.pos += from;
339                            return true;
340                        }
341                    }
342                }
343                _ => {
344                    return self.skip_until_basic(strings);
345                }
346            }
347            self.pos = self.input.len();
348            false
349        }
350    }
351
352    #[inline]
353    fn skip_until_basic(&mut self, strings: &[&str]) -> bool {
354        // TODO: optimize with Aho-Corasick, e.g. https://crates.io/crates/daachorse?
355        for from in self.pos..self.input.len() {
356            let bytes = if let Some(string) = self.input.get(from..) {
357                string.as_bytes()
358            } else {
359                continue;
360            };
361
362            for slice in strings.iter() {
363                let to = slice.len();
364                if Some(slice.as_bytes()) == bytes.get(0..to) {
365                    self.pos = from;
366                    return true;
367                }
368            }
369        }
370
371        self.pos = self.input.len();
372        false
373    }
374
375    /// Matches the char at the `Position` against a specified character and returns `true` if a match
376    /// was made. If no match was made, returns `false`.
377    /// `pos` will not be updated in either case.
378    #[inline]
379    pub(crate) fn match_char(&self, c: char) -> bool {
380        matches!(self.input[self.pos..].chars().next(), Some(cc) if c == cc)
381    }
382
383    /// Matches the char at the `Position` against a filter function and returns `true` if a match
384    /// was made. If no match was made, returns `false` and `pos` will not be updated.
385    #[inline]
386    pub(crate) fn match_char_by<F>(&mut self, f: F) -> bool
387    where
388        F: FnOnce(char) -> bool,
389    {
390        if let Some(c) = self.input[self.pos..].chars().next() {
391            if f(c) {
392                self.pos += c.len_utf8();
393                true
394            } else {
395                false
396            }
397        } else {
398            false
399        }
400    }
401
402    /// Matches `string` from the `Position` and returns `true` if a match was made or `false`
403    /// otherwise. If no match was made, `pos` will not be updated.
404    #[inline]
405    pub(crate) fn match_string(&mut self, string: &str) -> bool {
406        let to = self.pos + string.len();
407
408        if Some(string.as_bytes()) == self.input.as_bytes().get(self.pos..to) {
409            self.pos = to;
410            true
411        } else {
412            false
413        }
414    }
415
416    /// Case-insensitively matches `string` from the `Position` and returns `true` if a match was
417    /// made or `false` otherwise. If no match was made, `pos` will not be updated.
418    #[inline]
419    pub(crate) fn match_insensitive(&mut self, string: &str) -> bool {
420        let matched = {
421            let slice = &self.input[self.pos..];
422            if let Some(slice) = slice.get(0..string.len()) {
423                slice.eq_ignore_ascii_case(string)
424            } else {
425                false
426            }
427        };
428
429        if matched {
430            self.pos += string.len();
431            true
432        } else {
433            false
434        }
435    }
436
437    /// Matches `char` `range` from the `Position` and returns `true` if a match was made or `false`
438    /// otherwise. If no match was made, `pos` will not be updated.
439    #[inline]
440    pub(crate) fn match_range(&mut self, range: Range<char>) -> bool {
441        if let Some(c) = self.input[self.pos..].chars().next()
442            && range.start <= c
443            && c <= range.end
444        {
445            self.pos += c.len_utf8();
446            return true;
447        }
448
449        false
450    }
451}
452
453impl fmt::Debug for Position<'_> {
454    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455        f.debug_struct("Position").field("pos", &self.pos).finish()
456    }
457}
458
459impl<'i> PartialEq for Position<'i> {
460    fn eq(&self, other: &Position<'i>) -> bool {
461        ptr::eq(self.input, other.input) && self.pos == other.pos
462    }
463}
464
465impl Eq for Position<'_> {}
466
467#[allow(clippy::non_canonical_partial_ord_impl)]
468impl<'i> PartialOrd for Position<'i> {
469    fn partial_cmp(&self, other: &Position<'i>) -> Option<Ordering> {
470        if ptr::eq(self.input, other.input) {
471            self.pos.partial_cmp(&other.pos)
472        } else {
473            None
474        }
475    }
476}
477
478/// # Panics
479///
480/// Panics when comparing positions from different input strings.
481impl<'i> Ord for Position<'i> {
482    fn cmp(&self, other: &Position<'i>) -> Ordering {
483        self.partial_cmp(other)
484            .expect("cannot compare positions from different strs")
485    }
486}
487
488impl Hash for Position<'_> {
489    fn hash<H: Hasher>(&self, state: &mut H) {
490        (self.input as *const str).hash(state);
491        self.pos.hash(state);
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    #[test]
500    fn empty() {
501        let input = "";
502        assert!(Position::new(input, 0).unwrap().match_string(""));
503        assert!(!Position::new(input, 0).unwrap().match_string("a"));
504    }
505
506    #[test]
507    fn parts() {
508        let input = "asdasdf";
509
510        assert!(Position::new(input, 0).unwrap().match_string("asd"));
511        assert!(Position::new(input, 3).unwrap().match_string("asdf"));
512    }
513
514    #[test]
515    fn line_col() {
516        let input = "a\rb\nc\r\nd嗨";
517
518        assert_eq!(Position::new(input, 0).unwrap().line_col(), (1, 1));
519        assert_eq!(Position::new(input, 1).unwrap().line_col(), (1, 2));
520        assert_eq!(Position::new(input, 2).unwrap().line_col(), (1, 3));
521        assert_eq!(Position::new(input, 3).unwrap().line_col(), (1, 4));
522        assert_eq!(Position::new(input, 4).unwrap().line_col(), (2, 1));
523        assert_eq!(Position::new(input, 5).unwrap().line_col(), (2, 2));
524        assert_eq!(Position::new(input, 6).unwrap().line_col(), (2, 3));
525        assert_eq!(Position::new(input, 7).unwrap().line_col(), (3, 1));
526        assert_eq!(Position::new(input, 8).unwrap().line_col(), (3, 2));
527        assert_eq!(Position::new(input, 11).unwrap().line_col(), (3, 3));
528        let input = "abcd嗨";
529        assert_eq!(Position::new(input, 7).unwrap().line_col(), (1, 6));
530    }
531
532    #[test]
533    fn line_of() {
534        let input = "a\rb\nc\r\nd嗨";
535
536        assert_eq!(Position::new(input, 0).unwrap().line_of(), "a\rb\n");
537        assert_eq!(Position::new(input, 1).unwrap().line_of(), "a\rb\n");
538        assert_eq!(Position::new(input, 2).unwrap().line_of(), "a\rb\n");
539        assert_eq!(Position::new(input, 3).unwrap().line_of(), "a\rb\n");
540        assert_eq!(Position::new(input, 4).unwrap().line_of(), "c\r\n");
541        assert_eq!(Position::new(input, 5).unwrap().line_of(), "c\r\n");
542        assert_eq!(Position::new(input, 6).unwrap().line_of(), "c\r\n");
543        assert_eq!(Position::new(input, 7).unwrap().line_of(), "d嗨");
544        assert_eq!(Position::new(input, 8).unwrap().line_of(), "d嗨");
545        assert_eq!(Position::new(input, 11).unwrap().line_of(), "d嗨");
546    }
547
548    #[test]
549    fn line_of_empty() {
550        let input = "";
551
552        assert_eq!(Position::new(input, 0).unwrap().line_of(), "");
553    }
554
555    #[test]
556    fn line_of_new_line() {
557        let input = "\n";
558
559        assert_eq!(Position::new(input, 0).unwrap().line_of(), "\n");
560    }
561
562    #[test]
563    fn line_of_between_new_line() {
564        let input = "\n\n";
565
566        assert_eq!(Position::new(input, 1).unwrap().line_of(), "\n");
567    }
568
569    fn measure_skip(input: &str, pos: usize, n: usize) -> Option<usize> {
570        let mut p = Position::new(input, pos).unwrap();
571        if p.skip(n) { Some(p.pos - pos) } else { None }
572    }
573
574    #[test]
575    fn skip_empty() {
576        let input = "";
577
578        assert_eq!(measure_skip(input, 0, 0), Some(0));
579        assert_eq!(measure_skip(input, 0, 1), None);
580    }
581
582    #[test]
583    fn skip() {
584        let input = "d嗨";
585
586        assert_eq!(measure_skip(input, 0, 0), Some(0));
587        assert_eq!(measure_skip(input, 0, 1), Some(1));
588        assert_eq!(measure_skip(input, 1, 1), Some(3));
589    }
590
591    #[test]
592    fn skip_until() {
593        let input = "ab ac";
594        let pos = Position::from_start(input);
595
596        let mut test_pos = pos;
597        test_pos.skip_until(&["a", "b"]);
598        assert_eq!(test_pos.pos(), 0);
599
600        test_pos = pos;
601        test_pos.skip_until(&["b"]);
602        assert_eq!(test_pos.pos(), 1);
603
604        test_pos = pos;
605        test_pos.skip_until(&["ab"]);
606        assert_eq!(test_pos.pos(), 0);
607
608        test_pos = pos;
609        test_pos.skip_until(&["ac", "z"]);
610        assert_eq!(test_pos.pos(), 3);
611
612        test_pos = pos;
613        assert!(!test_pos.skip_until(&["z"]));
614        assert_eq!(test_pos.pos(), 5);
615    }
616
617    #[test]
618    fn skip_until_three_strings() {
619        let input = "xx yy b";
620        let pos = Position::from_start(input);
621
622        // All three strings non-empty: the three-string fast path must find "b".
623        let mut test_pos = pos;
624        assert!(test_pos.skip_until(&["a", "c", "b"]));
625        assert_eq!(test_pos.pos(), 6);
626
627        // A string that does not occur: nothing matches, position goes to the end.
628        test_pos = pos;
629        assert!(!test_pos.skip_until(&["q", "r", "z"]));
630        assert_eq!(test_pos.pos(), input.len());
631    }
632
633    #[test]
634    fn skip_until_empty_third_string() {
635        // The empty string matches at the current position, so skip_until must
636        // return true without advancing. Regression test for the inverted guard
637        // and the wrong `b3` binding in the three-string fast path.
638        let input = "xyb";
639        let pos = Position::from_start(input);
640
641        let mut test_pos = pos;
642        assert!(test_pos.skip_until(&["a", "b", ""]));
643        assert_eq!(test_pos.pos(), 0);
644
645        // Empty string first: still matches immediately.
646        test_pos = pos;
647        assert!(test_pos.skip_until(&["", "a", "b"]));
648        assert_eq!(test_pos.pos(), 0);
649
650        // Only the third string is empty, no "a"/"b" present: still matches at once.
651        let input2 = "xyz";
652        let pos2 = Position::from_start(input2);
653        let mut test_pos = pos2;
654        assert!(test_pos.skip_until(&["a", "b", ""]));
655        assert_eq!(test_pos.pos(), 0);
656    }
657
658    #[test]
659    fn match_range() {
660        let input = "b";
661
662        assert!(Position::new(input, 0).unwrap().match_range('a'..'c'));
663        assert!(Position::new(input, 0).unwrap().match_range('b'..'b'));
664        assert!(!Position::new(input, 0).unwrap().match_range('a'..'a'));
665        assert!(!Position::new(input, 0).unwrap().match_range('c'..'c'));
666        assert!(Position::new(input, 0).unwrap().match_range('a'..'嗨'));
667    }
668
669    #[test]
670    fn match_insensitive() {
671        let input = "AsdASdF";
672
673        assert!(Position::new(input, 0).unwrap().match_insensitive("asd"));
674        assert!(Position::new(input, 3).unwrap().match_insensitive("asdf"));
675    }
676
677    #[test]
678    fn cmp() {
679        let input = "a";
680        let start = Position::from_start(input);
681        let mut end = start;
682
683        assert!(end.skip(1));
684        let result = start.cmp(&end);
685
686        assert_eq!(result, Ordering::Less);
687    }
688
689    #[test]
690    #[should_panic]
691    fn cmp_panic() {
692        let input1 = "a";
693        let input2 = "b";
694        let pos1 = Position::from_start(input1);
695        let pos2 = Position::from_start(input2);
696
697        let _ = pos1.cmp(&pos2);
698    }
699
700    #[test]
701    #[cfg(feature = "std")]
702    fn hash() {
703        use std::collections::HashSet;
704
705        let input = "a";
706        let start = Position::from_start(input);
707        let mut positions = HashSet::new();
708
709        positions.insert(start);
710    }
711}