#![no_std]
#![doc(html_root_url = "https://docs.rs/detect-newline/0.1.0")]
extern crate alloc;
use alloc::string::String;
use core::iter::FusedIterator;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Newline {
#[default]
Lf,
CrLf,
Cr,
}
impl Newline {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Newline::Lf => "\n",
Newline::CrLf => "\r\n",
Newline::Cr => "\r",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Counts {
pub lf: usize,
pub crlf: usize,
pub cr: usize,
}
#[must_use]
pub fn detect(text: &str) -> Option<Newline> {
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\r' => {
return Some(if bytes.get(i + 1) == Some(&b'\n') {
Newline::CrLf
} else {
Newline::Cr
});
}
b'\n' => return Some(Newline::Lf),
_ => i += 1,
}
}
None
}
#[must_use]
pub fn count(text: &str) -> Counts {
let bytes = text.as_bytes();
let mut c = Counts::default();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\r' => {
if bytes.get(i + 1) == Some(&b'\n') {
c.crlf += 1;
i += 2;
} else {
c.cr += 1;
i += 1;
}
}
b'\n' => {
c.lf += 1;
i += 1;
}
_ => i += 1,
}
}
c
}
#[must_use]
pub fn dominant(text: &str) -> Option<Newline> {
let bytes = text.as_bytes();
let mut stats = [
(0usize, usize::MAX, Newline::Lf),
(0usize, usize::MAX, Newline::CrLf),
(0usize, usize::MAX, Newline::Cr),
];
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\r' => {
let (slot, step) = if bytes.get(i + 1) == Some(&b'\n') {
(1, 2)
} else {
(2, 1)
};
bump(&mut stats[slot], i);
i += step;
}
b'\n' => {
bump(&mut stats[0], i);
i += 1;
}
_ => i += 1,
}
}
let mut best: Option<(usize, usize, Newline)> = None;
for &(c, idx, nl) in &stats {
if c == 0 {
continue;
}
let better = match best {
None => true,
Some((bc, bi, _)) => c > bc || (c == bc && idx < bi),
};
if better {
best = Some((c, idx, nl));
}
}
best.map(|(_, _, nl)| nl)
}
fn bump(slot: &mut (usize, usize, Newline), at: usize) {
if slot.0 == 0 {
slot.1 = at;
}
slot.0 += 1;
}
#[must_use]
pub fn normalize(text: &str, to: Newline) -> String {
let nl = to.as_str();
let mut out = String::with_capacity(text.len());
let mut rest = text;
loop {
match rest.find(['\n', '\r']) {
None => {
out.push_str(rest);
break;
}
Some(i) => {
out.push_str(&rest[..i]);
out.push_str(nl);
let bytes = rest.as_bytes();
let adv = if bytes[i] == b'\r' && bytes.get(i + 1) == Some(&b'\n') {
i + 2
} else {
i + 1
};
rest = &rest[adv..];
}
}
}
out
}
#[must_use]
pub fn to_lf(text: &str) -> String {
normalize(text, Newline::Lf)
}
#[must_use]
pub fn to_crlf(text: &str) -> String {
normalize(text, Newline::CrLf)
}
#[must_use]
pub fn to_cr(text: &str) -> String {
normalize(text, Newline::Cr)
}
#[must_use]
pub fn has_trailing_newline(text: &str) -> bool {
text.ends_with(['\n', '\r'])
}
#[must_use]
pub fn strip_trailing_newline(text: &str) -> &str {
if let Some(s) = text.strip_suffix("\r\n") {
s
} else if let Some(s) = text.strip_suffix(['\n', '\r']) {
s
} else {
text
}
}
#[must_use]
pub fn lines(text: &str) -> Lines<'_> {
Lines {
rest: text,
done: false,
}
}
#[derive(Debug, Clone)]
pub struct Lines<'a> {
rest: &'a str,
done: bool,
}
impl<'a> Iterator for Lines<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
if self.done {
return None;
}
match self.rest.find(['\n', '\r']) {
None => {
self.done = true;
if self.rest.is_empty() {
None
} else {
let line = self.rest;
self.rest = "";
Some(line)
}
}
Some(i) => {
let line = &self.rest[..i];
let bytes = self.rest.as_bytes();
let adv = if bytes[i] == b'\r' && bytes.get(i + 1) == Some(&b'\n') {
i + 2
} else {
i + 1
};
let after = &self.rest[adv..];
if after.is_empty() {
self.done = true; }
self.rest = after;
Some(line)
}
}
}
}
impl FusedIterator for Lines<'_> {}