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