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