Coverage Report

Created: 2025-09-05 06:52

/src/textwrap/src/line_ending.rs
Line
Count
Source
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]
21
34.3k
    pub const fn as_str(&self) -> &'static str {
22
34.3k
        match self {
23
758
            Self::CRLF => "\r\n",
24
33.6k
            Self::LF => "\n",
25
        }
26
34.3k
    }
<textwrap::line_ending::LineEnding>::as_str
Line
Count
Source
21
27.8k
    pub const fn as_str(&self) -> &'static str {
22
27.8k
        match self {
23
578
            Self::CRLF => "\r\n",
24
27.2k
            Self::LF => "\n",
25
        }
26
27.8k
    }
<textwrap::line_ending::LineEnding>::as_str
Line
Count
Source
21
6.50k
    pub const fn as_str(&self) -> &'static str {
22
6.50k
        match self {
23
180
            Self::CRLF => "\r\n",
24
6.32k
            Self::LF => "\n",
25
        }
26
6.50k
    }
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
40
205k
    fn next(&mut self) -> Option<Self::Item> {
41
726k
        while let Some(lf) = self.0.find('\n') {
42
710k
            if lf == 0 || (lf == 1 && self.0.as_bytes()[lf - 1] == b'\r') {
43
520k
                self.0 = &self.0[(lf + 1)..];
44
520k
                continue;
45
190k
            }
46
190k
            let trimmed = match self.0.as_bytes()[lf - 1] {
47
4.40k
                b'\r' => (&self.0[..(lf - 1)], Some(LineEnding::CRLF)),
48
185k
                _ => (&self.0[..lf], Some(LineEnding::LF)),
49
            };
50
190k
            self.0 = &self.0[(lf + 1)..];
51
190k
            return Some(trimmed);
52
        }
53
15.0k
        if self.0.is_empty() {
54
7.97k
            None
55
        } else {
56
7.05k
            let line = std::mem::take(&mut self.0);
57
7.05k
            Some((line, None))
58
        }
59
205k
    }
60
}
61
62
#[cfg(test)]
63
mod tests {
64
    use super::*;
65
66
    #[test]
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]
80
    fn non_empty_lines_new_lines_only() {
81
        assert_eq!(NonEmptyLines("\r\n\n\n\r\n").next(), None);
82
    }
83
84
    #[test]
85
    fn non_empty_lines_no_input() {
86
        assert_eq!(NonEmptyLines("").next(), None);
87
    }
88
}