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
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
17pub struct NormalizedLineEndingMap {
18 crlf_newline_offsets: Vec<usize>,
19}
20
21impl NormalizedLineEndingMap {
22 pub fn new(original: &str) -> Self {
23 let bytes = original.as_bytes();
24 let mut crlf_newline_offsets = Vec::new();
25 let mut original_offset = 0;
26 let mut normalized_offset = 0;
27
28 while original_offset < bytes.len() {
29 if bytes[original_offset] == b'\r'
30 && original_offset + 1 < bytes.len()
31 && bytes[original_offset + 1] == b'\n'
32 {
33 crlf_newline_offsets.push(normalized_offset);
34 original_offset += 2;
35 normalized_offset += 1;
36 } else {
37 original_offset += 1;
38 normalized_offset += 1;
39 }
40 }
41
42 Self { crlf_newline_offsets }
43 }
44
45 pub fn original_offset(&self, normalized_offset: usize) -> usize {
48 normalized_offset
49 + self
50 .crlf_newline_offsets
51 .partition_point(|newline_offset| *newline_offset < normalized_offset)
52 }
53}
54
55pub fn detect_line_ending_enum(content: &str) -> LineEnding {
56 let bytes = content.as_bytes();
57 let mut has_crlf = false;
58 let mut has_standalone_lf = false;
59 let mut i = 0;
60
61 while i < bytes.len() {
62 if bytes[i] == b'\r' && i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
63 has_crlf = true;
64 i += 2;
65 } else if bytes[i] == b'\n' {
66 has_standalone_lf = true;
67 i += 1;
68 } else {
69 i += 1;
70 }
71 if has_crlf && has_standalone_lf {
73 return LineEnding::Mixed;
74 }
75 }
76
77 match (has_crlf, has_standalone_lf) {
78 (true, true) => LineEnding::Mixed,
79 (true, false) => LineEnding::Crlf,
80 (false, _) => LineEnding::Lf,
81 }
82}
83
84pub fn detect_line_ending(content: &str) -> &'static str {
85 let crlf_count = content.matches("\r\n").count();
87 let lf_count = content.matches('\n').count() - crlf_count;
88
89 if crlf_count > lf_count { "\r\n" } else { "\n" }
90}
91
92pub fn normalize_line_ending<'a>(content: &'a str, target: LineEnding) -> Cow<'a, str> {
93 match target {
94 LineEnding::Lf => {
95 if !content.contains('\r') {
96 Cow::Borrowed(content)
97 } else {
98 Cow::Owned(content.replace("\r\n", "\n"))
99 }
100 }
101 LineEnding::Crlf => {
102 let normalized = content.replace("\r\n", "\n");
104 Cow::Owned(normalized.replace('\n', "\r\n"))
105 }
106 LineEnding::Mixed => Cow::Borrowed(content),
107 }
108}
109
110pub fn ensure_consistent_line_endings(original: &str, modified: &str) -> String {
111 let original_ending = detect_line_ending_enum(original);
112
113 let target_ending = if original_ending == LineEnding::Mixed {
115 let crlf_count = original.matches("\r\n").count();
117 let lf_count = original.matches('\n').count() - crlf_count;
118 if crlf_count > lf_count {
119 LineEnding::Crlf
120 } else {
121 LineEnding::Lf
122 }
123 } else {
124 original_ending
125 };
126
127 let modified_ending = detect_line_ending_enum(modified);
128
129 if target_ending != modified_ending {
130 normalize_line_ending(modified, target_ending).into_owned()
131 } else {
132 modified.to_string()
133 }
134}
135
136pub fn get_line_ending_str(ending: LineEnding) -> &'static str {
137 match ending {
138 LineEnding::Lf => "\n",
139 LineEnding::Crlf => "\r\n",
140 LineEnding::Mixed => "\n", }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_detect_line_ending_enum() {
150 assert_eq!(detect_line_ending_enum("hello\nworld"), LineEnding::Lf);
151 assert_eq!(detect_line_ending_enum("hello\r\nworld"), LineEnding::Crlf);
152 assert_eq!(detect_line_ending_enum("hello\r\nworld\nmixed"), LineEnding::Mixed);
153 assert_eq!(detect_line_ending_enum("no line endings"), LineEnding::Lf);
154 }
155
156 #[test]
157 fn normalized_line_ending_map_handles_mixed_input() {
158 let original = "a\r\nb\nc\r\n";
159 let map = NormalizedLineEndingMap::new(original);
160
161 assert_eq!(map.original_offset(1), 1);
164 assert_eq!(map.original_offset(2), 3);
165 assert_eq!(map.original_offset(4), 5);
166 assert_eq!(map.original_offset(6), 8);
167 }
168
169 #[test]
170 fn test_detect_line_ending() {
171 assert_eq!(detect_line_ending("hello\nworld"), "\n");
172 assert_eq!(detect_line_ending("hello\r\nworld"), "\r\n");
173 assert_eq!(detect_line_ending("hello\r\nworld\nmixed"), "\n"); assert_eq!(detect_line_ending("no line endings"), "\n");
175 }
176
177 #[test]
178 fn test_normalize_line_ending() {
179 assert_eq!(normalize_line_ending("hello\r\nworld", LineEnding::Lf), "hello\nworld");
180 assert_eq!(
181 normalize_line_ending("hello\nworld", LineEnding::Crlf),
182 "hello\r\nworld"
183 );
184 assert_eq!(
185 normalize_line_ending("hello\r\nworld\nmixed", LineEnding::Lf),
186 "hello\nworld\nmixed"
187 );
188 }
189
190 #[test]
191 fn test_ensure_consistent_line_endings() {
192 let original = "hello\r\nworld";
193 let modified = "hello\nworld\nextra";
194 assert_eq!(
195 ensure_consistent_line_endings(original, modified),
196 "hello\r\nworld\r\nextra"
197 );
198
199 let original = "hello\nworld";
200 let modified = "hello\r\nworld\r\nextra";
201 assert_eq!(
202 ensure_consistent_line_endings(original, modified),
203 "hello\nworld\nextra"
204 );
205 }
206}