Skip to main content

tor_netdoc/parse2/
lines.rs

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