Skip to main content

textwrap/
line_ending.rs

1//! Line ending detection and conversion.
2
3use core::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)]
8pub 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
18impl LineEnding {
19    /// Turns this [`LineEnding`] value into its ASCII representation.
20    #[inline]
21    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 a string
30/// slice and a [`LineEnding`] value. It only emits non-empty lines
31/// (i.e. having some content before the terminating `\r\n` or `\n`).
32///
33/// This struct is used internally by the library.
34#[derive(Debug, Clone, Copy)]
35pub(crate) struct NonEmptyLines<'a>(pub &'a str);
36
37impl<'a> Iterator for NonEmptyLines<'a> {
38    type Item = (&'a str, Option<LineEnding>);
39
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 = core::mem::take(&mut self.0);
57            Some((line, None))
58        }
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use alloc::vec;
66    use alloc::vec::Vec;
67
68    #[test]
69    fn non_empty_lines_full_case() {
70        assert_eq!(
71            NonEmptyLines("LF\nCRLF\r\n\r\n\nunterminated")
72                .collect::<Vec<(&str, Option<LineEnding>)>>(),
73            vec![
74                ("LF", Some(LineEnding::LF)),
75                ("CRLF", Some(LineEnding::CRLF)),
76                ("unterminated", None),
77            ]
78        );
79    }
80
81    #[test]
82    fn non_empty_lines_new_lines_only() {
83        assert_eq!(NonEmptyLines("\r\n\n\n\r\n").next(), None);
84    }
85
86    #[test]
87    fn non_empty_lines_no_input() {
88        assert_eq!(NonEmptyLines("").next(), None);
89    }
90}