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}