tor_netdoc/parse2/
lines.rs

1//! Version of `std::str::Lines` that tracks line numbers and has `remainder()`
2
3/// Version of `std::str::Lines` that tracks line numbers and has `remainder()`
4///
5/// Implements `Iterator`, returning one `str` for each line, with the `'\n'` removed.
6///
7/// Missing final newline is silently tolerated.
8#[derive(Debug, Clone)]
9pub struct Lines<'s> {
10    /// Line number at the start of `rest`
11    lno: usize,
12    /// The remaining part of the document
13    rest: &'s str,
14}
15
16/// Extension trait adding a method to `str`
17pub trait StrExt: AsRef<str> {
18    /// Remove `count` bytes from the end of `self`
19    ///
20    /// # Panics
21    ///
22    /// Panics if `count > self.len()`.
23    fn strip_end_counted(&self, count: usize) -> &str {
24        let s = self.as_ref();
25        &s[0..s.len().checked_sub(count).expect("stripping too much")]
26    }
27}
28impl StrExt for str {}
29
30/// Information about the next line we have peeked
31///
32/// To get the line as an actual string, pass this to `peeked_line`.
33///
34/// # Correctness
35///
36/// Each `Peeked` is only valid in conjunction with the `Lines` that returned it,
37/// and becomes invalidated if the `Lines` is modified
38/// (ie, it can be invalidated by calls that take `&mut Lines`).
39///
40/// Cloning a `Peeked` is hazrdous since using it twice would be wrong.
41///
42/// None of this is checked at compile- or run-time.
43// We could perhaps use lifetimes somehow to enforce this,
44// but `ItemStream` wants `Peeked` to be `'static` and `Clone`.
45#[derive(Debug, Clone, amplify::Getters)]
46pub struct Peeked {
47    /// The length of the next line
48    //
49    // # Invariant
50    //
51    // `rest[line_len]` is a newline, or `line_len` is `rest.len()`.
52    #[getter(as_copy)]
53    line_len: usize,
54}
55
56impl<'s> Lines<'s> {
57    /// Start reading lines from a document as a string
58    pub fn new(s: &'s str) -> Self {
59        Lines { lno: 1, rest: s }
60    }
61
62    /// Line number of the next line we'll read
63    pub fn peek_lno(&self) -> usize {
64        self.lno
65    }
66
67    /// Peek the next line
68    pub fn peek(&self) -> Option<Peeked> {
69        if self.rest.is_empty() {
70            None
71        } else if let Some(newline) = self.rest.find('\n') {
72            Some(Peeked { line_len: newline })
73        } else {
74            Some(Peeked {
75                line_len: self.rest.len(),
76            })
77        }
78    }
79
80    /// The rest of the file as a `str`
81    pub fn remaining(&self) -> &'s str {
82        self.rest
83    }
84
85    /// After `peek`, advance to the next line, consuming the one that was peeked
86    ///
87    /// # Correctness
88    ///
89    /// See [`Peeked`].
90    #[allow(clippy::needless_pass_by_value)] // Yes, we want to consume Peeked
91    pub fn consume_peeked(&mut self, peeked: Peeked) -> &'s str {
92        let line = self.peeked_line(&peeked);
93        self.rest = &self.rest[peeked.line_len..];
94        if !self.rest.is_empty() {
95            debug_assert!(self.rest.starts_with('\n'));
96            self.rest = &self.rest[1..];
97        }
98        self.lno += 1;
99        line
100    }
101
102    /// After `peek`, obtain the actual peeked line as a `str`
103    ///
104    /// As with [`<Lines as Iterator>::next`](Lines::next), does not include the newline.
105    // Rustdoc doesn't support linking` fully qualified syntax.
106    // https://github.com/rust-lang/rust/issues/74563
107    ///
108    /// # Correctness
109    ///
110    /// See [`Peeked`].
111    pub fn peeked_line(&self, peeked: &Peeked) -> &'s str {
112        &self.rest[0..peeked.line_len()]
113    }
114}
115
116impl<'s> Iterator for Lines<'s> {
117    type Item = &'s str;
118
119    fn next(&mut self) -> Option<&'s str> {
120        let peeked = self.peek()?;
121        let line = self.consume_peeked(peeked);
122        Some(line)
123    }
124}