rumdl_lib/utils/
line_ending.rs1use std::borrow::Cow;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum LineEnding {
5 Lf,
6 Crlf,
7 Mixed,
8}
9
10pub fn detect_line_ending_enum(content: &str) -> LineEnding {
11 let bytes = content.as_bytes();
12 let mut has_crlf = false;
13 let mut has_standalone_lf = false;
14 let mut i = 0;
15
16 while i < bytes.len() {
17 if bytes[i] == b'\r' && i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
18 has_crlf = true;
19 i += 2;
20 } else if bytes[i] == b'\n' {
21 has_standalone_lf = true;
22 i += 1;
23 } else {
24 i += 1;
25 }
26 if has_crlf && has_standalone_lf {
28 return LineEnding::Mixed;
29 }
30 }
31
32 match (has_crlf, has_standalone_lf) {
33 (true, true) => LineEnding::Mixed,
34 (true, false) => LineEnding::Crlf,
35 (false, _) => LineEnding::Lf,
36 }
37}
38
39pub fn detect_line_ending(content: &str) -> &'static str {
40 let crlf_count = content.matches("\r\n").count();
42 let lf_count = content.matches('\n').count() - crlf_count;
43
44 if crlf_count > lf_count { "\r\n" } else { "\n" }
45}
46
47pub fn normalize_line_ending<'a>(content: &'a str, target: LineEnding) -> Cow<'a, str> {
48 match target {
49 LineEnding::Lf => {
50 if !content.contains('\r') {
51 Cow::Borrowed(content)
52 } else {
53 Cow::Owned(content.replace("\r\n", "\n"))
54 }
55 }
56 LineEnding::Crlf => {
57 let normalized = content.replace("\r\n", "\n");
59 Cow::Owned(normalized.replace('\n', "\r\n"))
60 }
61 LineEnding::Mixed => Cow::Borrowed(content),
62 }
63}
64
65pub fn ensure_consistent_line_endings(original: &str, modified: &str) -> String {
66 let original_ending = detect_line_ending_enum(original);
67
68 let target_ending = if original_ending == LineEnding::Mixed {
70 let crlf_count = original.matches("\r\n").count();
72 let lf_count = original.matches('\n').count() - crlf_count;
73 if crlf_count > lf_count {
74 LineEnding::Crlf
75 } else {
76 LineEnding::Lf
77 }
78 } else {
79 original_ending
80 };
81
82 let modified_ending = detect_line_ending_enum(modified);
83
84 if target_ending != modified_ending {
85 normalize_line_ending(modified, target_ending).into_owned()
86 } else {
87 modified.to_string()
88 }
89}
90
91pub fn get_line_ending_str(ending: LineEnding) -> &'static str {
92 match ending {
93 LineEnding::Lf => "\n",
94 LineEnding::Crlf => "\r\n",
95 LineEnding::Mixed => "\n", }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn test_detect_line_ending_enum() {
105 assert_eq!(detect_line_ending_enum("hello\nworld"), LineEnding::Lf);
106 assert_eq!(detect_line_ending_enum("hello\r\nworld"), LineEnding::Crlf);
107 assert_eq!(detect_line_ending_enum("hello\r\nworld\nmixed"), LineEnding::Mixed);
108 assert_eq!(detect_line_ending_enum("no line endings"), LineEnding::Lf);
109 }
110
111 #[test]
112 fn test_detect_line_ending() {
113 assert_eq!(detect_line_ending("hello\nworld"), "\n");
114 assert_eq!(detect_line_ending("hello\r\nworld"), "\r\n");
115 assert_eq!(detect_line_ending("hello\r\nworld\nmixed"), "\n"); assert_eq!(detect_line_ending("no line endings"), "\n");
117 }
118
119 #[test]
120 fn test_normalize_line_ending() {
121 assert_eq!(normalize_line_ending("hello\r\nworld", LineEnding::Lf), "hello\nworld");
122 assert_eq!(
123 normalize_line_ending("hello\nworld", LineEnding::Crlf),
124 "hello\r\nworld"
125 );
126 assert_eq!(
127 normalize_line_ending("hello\r\nworld\nmixed", LineEnding::Lf),
128 "hello\nworld\nmixed"
129 );
130 }
131
132 #[test]
133 fn test_ensure_consistent_line_endings() {
134 let original = "hello\r\nworld";
135 let modified = "hello\nworld\nextra";
136 assert_eq!(
137 ensure_consistent_line_endings(original, modified),
138 "hello\r\nworld\r\nextra"
139 );
140
141 let original = "hello\nworld";
142 let modified = "hello\r\nworld\r\nextra";
143 assert_eq!(
144 ensure_consistent_line_endings(original, modified),
145 "hello\nworld\nextra"
146 );
147 }
148}