use once_cell::sync::Lazy;
use regex::Regex;
#[inline]
pub fn is_bmp(s: &str) -> bool {
!s.as_bytes().iter().any(|&b| b >= 0xF0)
}
#[inline]
pub fn char_len(s: &str) -> usize {
if s.is_ascii() {
s.len()
} else if is_bmp(s) {
s.chars().count()
} else {
s.chars().map(char::len_utf16).sum()
}
}
pub fn char_slice(s: &str, start: usize, len: usize) -> String {
if len == 0 {
return String::new();
}
if s.is_ascii() {
if start >= s.len() {
return String::new();
}
let end = (start + len).min(s.len());
return s[start..end].to_string();
}
if is_bmp(s) {
return s.chars().skip(start).take(len).collect();
}
let units: Vec<u16> = s.encode_utf16().collect();
if start >= units.len() {
return String::new();
}
let end = (start + len).min(units.len());
String::from_utf16_lossy(&units[start..end])
}
pub fn char_substr_from(s: &str, start: usize) -> String {
if s.is_ascii() {
if start >= s.len() {
return String::new();
}
return s[start..].to_string();
}
if is_bmp(s) {
return s.chars().skip(start).collect();
}
let units: Vec<u16> = s.encode_utf16().collect();
if start >= units.len() {
return String::new();
}
String::from_utf16_lossy(&units[start..])
}
pub fn char_at(s: &str, i: usize) -> Option<char> {
if s.is_ascii() {
return s.as_bytes().get(i).copied().map(|b| b as char);
}
if is_bmp(s) {
return s.chars().nth(i);
}
let u = s.encode_utf16().nth(i)?;
char::decode_utf16(std::iter::once(u)).next()?.ok()
}
pub fn split_sections(text: &str) -> Vec<String> {
static RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)([\r\n]+|^[γ\s]+|[γ\s]+$|[γ\s]{2,})").unwrap());
let mut out = Vec::new();
let mut last = 0usize;
for m in RE.find_iter(text) {
if m.start() > last {
out.push(text[last..m.start()].to_string());
}
out.push(m.as_str().to_string());
last = m.end();
}
if last < text.len() {
out.push(text[last..].to_string());
}
if out.is_empty() {
vec![text.to_string()]
} else {
out
}
}
pub fn is_newline_only(s: &str) -> bool {
!s.is_empty() && s.chars().all(|c| c == '\r' || c == '\n')
}
pub fn leading_repeat_run(text: &str) -> Option<usize> {
let units: Vec<u16> = text.encode_utf16().collect();
let n = units.len();
if n < 6 {
return None;
}
for unit in 1..=n / 6 {
if n < unit * 6 {
continue;
}
let pattern = &units[..unit];
let mut times = 1;
let mut pos = unit;
while pos + unit <= n && &units[pos..pos + unit] == pattern {
times += 1;
pos += unit;
}
if times >= 6 {
return Some(pos);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_keeps_newlines() {
let parts = split_sections("a\nb");
assert_eq!(parts, vec!["a", "\n", "b"]);
}
#[test]
fn split_multiline_single_space() {
assert_eq!(split_sections("a \nb"), vec!["a", " ", "\n", "b"]);
assert_eq!(split_sections("a\n b"), vec!["a", "\n", " ", "b"]);
}
#[test]
fn utf16_len_emoji() {
assert_eq!(char_len("π"), 2);
assert_eq!(char_len("δΈ"), 1);
}
#[test]
fn bmp_slice_matches_utf16() {
let s = "ιι’εΌεabc";
assert_eq!(char_slice(s, 0, 2), "ιι’");
assert_eq!(char_slice(s, 2, 2), "εΌε");
assert_eq!(char_substr_from(s, 4), "abc");
assert_eq!(char_at(s, 0), Some('ι'));
}
#[test]
fn ascii_fast_path() {
let s = "http://example.com/path";
assert_eq!(char_len(s), s.len());
assert_eq!(char_slice(s, 0, 7), "http://");
assert_eq!(char_at(s, 4), Some(':'));
}
#[test]
fn repeat_run() {
let s = "εεεεεεεεx";
assert_eq!(leading_repeat_run(s), Some(8));
}
}