cosmic_text/
line_ending.rs

1use core::ops::Range;
2
3/// Line ending
4#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
5pub enum LineEnding {
6    /// Use `\n` for line ending (POSIX-style)
7    #[default]
8    Lf,
9    /// Use `\r\n` for line ending (Windows-style)
10    CrLf,
11    /// Use `\r` for line ending (many legacy systems)
12    Cr,
13    /// Use `\n\r` for line ending (some legacy systems)
14    LfCr,
15    /// No line ending
16    None,
17}
18
19impl LineEnding {
20    /// Get the line ending as a str
21    pub fn as_str(&self) -> &'static str {
22        match self {
23            Self::Lf => "\n",
24            Self::CrLf => "\r\n",
25            Self::Cr => "\r",
26            Self::LfCr => "\n\r",
27            Self::None => "",
28        }
29    }
30}
31
32/// Iterator over lines terminated by [`LineEnding`]
33#[derive(Debug)]
34pub struct LineIter<'a> {
35    string: &'a str,
36    start: usize,
37    end: usize,
38}
39
40impl<'a> LineIter<'a> {
41    /// Create an iterator of lines in a string slice
42    pub fn new(string: &'a str) -> Self {
43        Self {
44            string,
45            start: 0,
46            end: string.len(),
47        }
48    }
49}
50
51impl Iterator for LineIter<'_> {
52    type Item = (Range<usize>, LineEnding);
53    fn next(&mut self) -> Option<Self::Item> {
54        let start = self.start;
55        match self.string[start..self.end].find(['\r', '\n']) {
56            Some(i) => {
57                let end = start + i;
58                self.start = end;
59                let after = &self.string[end..];
60                let ending = if after.starts_with("\r\n") {
61                    LineEnding::CrLf
62                } else if after.starts_with("\n\r") {
63                    LineEnding::LfCr
64                } else if after.starts_with("\n") {
65                    LineEnding::Lf
66                } else if after.starts_with("\r") {
67                    LineEnding::Cr
68                } else {
69                    //TODO: this should not be possible
70                    LineEnding::None
71                };
72                self.start += ending.as_str().len();
73                Some((start..end, ending))
74            }
75            None => {
76                if self.start < self.end {
77                    self.start = self.end;
78                    Some((start..self.end, LineEnding::None))
79                } else {
80                    None
81                }
82            }
83        }
84    }
85}
86
87//TODO: DoubleEndedIterator
88
89#[test]
90fn test_line_iter() {
91    let string = "LF\nCRLF\r\nCR\rLFCR\n\rNONE";
92    let mut iter = LineIter::new(string);
93    assert_eq!(iter.next(), Some((0..2, LineEnding::Lf)));
94    assert_eq!(iter.next(), Some((3..7, LineEnding::CrLf)));
95    assert_eq!(iter.next(), Some((9..11, LineEnding::Cr)));
96    assert_eq!(iter.next(), Some((12..16, LineEnding::LfCr)));
97    assert_eq!(iter.next(), Some((18..22, LineEnding::None)));
98}