1 //! Line ending detection and conversion.
2 
3 use std::fmt::Debug;
4 
5 /// Supported line endings. Like in the Rust standard library, two line
6 /// endings are supported: `\r\n` and `\n`
7 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
8 pub enum LineEnding {
9     /// _Carriage return and line feed_ – a line ending sequence
10     /// historically used in Windows. Corresponds to the sequence
11     /// of ASCII control characters `0x0D 0x0A` or `\r\n`
12     CRLF,
13     /// _Line feed_ – a line ending historically used in Unix.
14     ///  Corresponds to the ASCII control character `0x0A` or `\n`
15     LF,
16 }
17 
18 impl LineEnding {
19     /// Turns this [`LineEnding`] value into its ASCII representation.
20     #[inline]
as_str(&self) -> &'static str21     pub const fn as_str(&self) -> &'static str {
22         match self {
23             Self::CRLF => "\r\n",
24             Self::LF => "\n",
25         }
26     }
27 }
28 
29 /// An iterator over the lines of a string, as tuples of string slice
30 /// and [`LineEnding`] value; it only emits non-empty lines (i.e. having
31 /// some content before the terminating `\r\n` or `\n`).
32 ///
33 /// This struct is used internally by the library.
34 #[derive(Debug, Clone, Copy)]
35 pub(crate) struct NonEmptyLines<'a>(pub &'a str);
36 
37 impl<'a> Iterator for NonEmptyLines<'a> {
38     type Item = (&'a str, Option<LineEnding>);
39 
next(&mut self) -> Option<Self::Item>40     fn next(&mut self) -> Option<Self::Item> {
41         while let Some(lf) = self.0.find('\n') {
42             if lf == 0 || (lf == 1 && self.0.as_bytes()[lf - 1] == b'\r') {
43                 self.0 = &self.0[(lf + 1)..];
44                 continue;
45             }
46             let trimmed = match self.0.as_bytes()[lf - 1] {
47                 b'\r' => (&self.0[..(lf - 1)], Some(LineEnding::CRLF)),
48                 _ => (&self.0[..lf], Some(LineEnding::LF)),
49             };
50             self.0 = &self.0[(lf + 1)..];
51             return Some(trimmed);
52         }
53         if self.0.is_empty() {
54             None
55         } else {
56             let line = std::mem::take(&mut self.0);
57             Some((line, None))
58         }
59     }
60 }
61 
62 #[cfg(test)]
63 mod tests {
64     use super::*;
65 
66     #[test]
non_empty_lines_full_case()67     fn non_empty_lines_full_case() {
68         assert_eq!(
69             NonEmptyLines("LF\nCRLF\r\n\r\n\nunterminated")
70                 .collect::<Vec<(&str, Option<LineEnding>)>>(),
71             vec![
72                 ("LF", Some(LineEnding::LF)),
73                 ("CRLF", Some(LineEnding::CRLF)),
74                 ("unterminated", None),
75             ]
76         );
77     }
78 
79     #[test]
non_empty_lines_new_lines_only()80     fn non_empty_lines_new_lines_only() {
81         assert_eq!(NonEmptyLines("\r\n\n\n\r\n").next(), None);
82     }
83 
84     #[test]
non_empty_lines_no_input()85     fn non_empty_lines_no_input() {
86         assert_eq!(NonEmptyLines("").next(), None);
87     }
88 }
89