1#![no_std]
21#![doc(html_root_url = "https://docs.rs/detect-newline/0.1.0")]
22
23extern crate alloc;
24
25use alloc::string::String;
26use core::iter::FusedIterator;
27
28#[cfg(doctest)]
30#[doc = include_str!("../README.md")]
31struct ReadmeDoctests;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
35pub enum Newline {
36 #[default]
38 Lf,
39 CrLf,
41 Cr,
43}
44
45impl Newline {
46 #[must_use]
48 pub const fn as_str(self) -> &'static str {
49 match self {
50 Newline::Lf => "\n",
51 Newline::CrLf => "\r\n",
52 Newline::Cr => "\r",
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
59pub struct Counts {
60 pub lf: usize,
62 pub crlf: usize,
64 pub cr: usize,
66}
67
68#[must_use]
74pub fn detect(text: &str) -> Option<Newline> {
75 let bytes = text.as_bytes();
76 let mut i = 0;
77 while i < bytes.len() {
78 match bytes[i] {
79 b'\r' => {
80 return Some(if bytes.get(i + 1) == Some(&b'\n') {
81 Newline::CrLf
82 } else {
83 Newline::Cr
84 });
85 }
86 b'\n' => return Some(Newline::Lf),
87 _ => i += 1,
88 }
89 }
90 None
91}
92
93#[must_use]
95pub fn count(text: &str) -> Counts {
96 let bytes = text.as_bytes();
97 let mut c = Counts::default();
98 let mut i = 0;
99 while i < bytes.len() {
100 match bytes[i] {
101 b'\r' => {
102 if bytes.get(i + 1) == Some(&b'\n') {
103 c.crlf += 1;
104 i += 2;
105 } else {
106 c.cr += 1;
107 i += 1;
108 }
109 }
110 b'\n' => {
111 c.lf += 1;
112 i += 1;
113 }
114 _ => i += 1,
115 }
116 }
117 c
118}
119
120#[must_use]
124pub fn dominant(text: &str) -> Option<Newline> {
125 let bytes = text.as_bytes();
126 let mut stats = [
128 (0usize, usize::MAX, Newline::Lf),
129 (0usize, usize::MAX, Newline::CrLf),
130 (0usize, usize::MAX, Newline::Cr),
131 ];
132 let mut i = 0;
133 while i < bytes.len() {
134 match bytes[i] {
135 b'\r' => {
136 let (slot, step) = if bytes.get(i + 1) == Some(&b'\n') {
137 (1, 2)
138 } else {
139 (2, 1)
140 };
141 bump(&mut stats[slot], i);
142 i += step;
143 }
144 b'\n' => {
145 bump(&mut stats[0], i);
146 i += 1;
147 }
148 _ => i += 1,
149 }
150 }
151
152 let mut best: Option<(usize, usize, Newline)> = None;
153 for &(c, idx, nl) in &stats {
154 if c == 0 {
155 continue;
156 }
157 let better = match best {
158 None => true,
159 Some((bc, bi, _)) => c > bc || (c == bc && idx < bi),
160 };
161 if better {
162 best = Some((c, idx, nl));
163 }
164 }
165 best.map(|(_, _, nl)| nl)
166}
167
168fn bump(slot: &mut (usize, usize, Newline), at: usize) {
170 if slot.0 == 0 {
171 slot.1 = at;
172 }
173 slot.0 += 1;
174}
175
176#[must_use]
183pub fn normalize(text: &str, to: Newline) -> String {
184 let nl = to.as_str();
185 let mut out = String::with_capacity(text.len());
186 let mut rest = text;
187 loop {
188 match rest.find(['\n', '\r']) {
189 None => {
190 out.push_str(rest);
191 break;
192 }
193 Some(i) => {
194 out.push_str(&rest[..i]);
195 out.push_str(nl);
196 let bytes = rest.as_bytes();
197 let adv = if bytes[i] == b'\r' && bytes.get(i + 1) == Some(&b'\n') {
198 i + 2
199 } else {
200 i + 1
201 };
202 rest = &rest[adv..];
203 }
204 }
205 }
206 out
207}
208
209#[must_use]
211pub fn to_lf(text: &str) -> String {
212 normalize(text, Newline::Lf)
213}
214
215#[must_use]
217pub fn to_crlf(text: &str) -> String {
218 normalize(text, Newline::CrLf)
219}
220
221#[must_use]
223pub fn to_cr(text: &str) -> String {
224 normalize(text, Newline::Cr)
225}
226
227#[must_use]
229pub fn has_trailing_newline(text: &str) -> bool {
230 text.ends_with(['\n', '\r'])
231}
232
233#[must_use]
235pub fn strip_trailing_newline(text: &str) -> &str {
236 if let Some(s) = text.strip_suffix("\r\n") {
237 s
238 } else if let Some(s) = text.strip_suffix(['\n', '\r']) {
239 s
240 } else {
241 text
242 }
243}
244
245#[must_use]
255pub fn lines(text: &str) -> Lines<'_> {
256 Lines {
257 rest: text,
258 done: false,
259 }
260}
261
262#[derive(Debug, Clone)]
264pub struct Lines<'a> {
265 rest: &'a str,
266 done: bool,
267}
268
269impl<'a> Iterator for Lines<'a> {
270 type Item = &'a str;
271
272 fn next(&mut self) -> Option<&'a str> {
273 if self.done {
274 return None;
275 }
276 match self.rest.find(['\n', '\r']) {
277 None => {
278 self.done = true;
279 if self.rest.is_empty() {
280 None
281 } else {
282 let line = self.rest;
283 self.rest = "";
284 Some(line)
285 }
286 }
287 Some(i) => {
288 let line = &self.rest[..i];
289 let bytes = self.rest.as_bytes();
290 let adv = if bytes[i] == b'\r' && bytes.get(i + 1) == Some(&b'\n') {
291 i + 2
292 } else {
293 i + 1
294 };
295 let after = &self.rest[adv..];
296 if after.is_empty() {
297 self.done = true; }
299 self.rest = after;
300 Some(line)
301 }
302 }
303 }
304}
305
306impl FusedIterator for Lines<'_> {}