pub(crate) fn collapse_whitespace(text: &str) -> String {
let mut out = String::with_capacity(text.len());
collapse_whitespace_into(text, &mut out);
out
}
pub(crate) fn collapse_whitespace_into(text: &str, result: &mut String) {
result.clear();
result.reserve(text.len());
let mut prev_was_space = false;
let mut seen_non_ws = false;
for ch in text.chars() {
if is_fold_whitespace(ch) || is_blank_render(ch) {
if seen_non_ws && !prev_was_space {
result.push(' ');
prev_was_space = true;
}
} else {
result.push(ch);
prev_was_space = false;
seen_non_ws = true;
}
}
if result.ends_with(' ') {
result.truncate(result.len() - 1);
}
}
pub(crate) fn is_fold_whitespace(ch: char) -> bool {
let cp = ch as u32;
matches!(cp,
0x0009..=0x000D | 0x001C..=0x001F | 0x0085 | 0x0020 | 0x00A0 | 0x1680 | 0x2000..=0x200A | 0x2028 | 0x2029 | 0x202F | 0x205F | 0x3000 )
}
pub(crate) fn is_blank_render(ch: char) -> bool {
matches!(ch as u32, 0x2800 | 0x115F | 0x1160 | 0x3164 | 0xFFA0)
}
pub(crate) fn strip_control_chars(text: &str) -> String {
let mut out = String::new();
strip_control_chars_into(text, &mut out);
out
}
pub(crate) fn strip_control_chars_into(text: &str, out: &mut String) {
out.clear();
out.reserve(text.len());
out.extend(
text.chars()
.filter(|&ch| !ch.is_control() || is_fold_whitespace(ch)),
);
}
pub(crate) fn strip_zero_width_chars(text: &str) -> String {
let mut out = String::new();
strip_zero_width_chars_into(text, &mut out);
out
}
pub(crate) fn strip_zero_width_chars_into(text: &str, out: &mut String) {
out.clear();
if text.is_ascii() {
out.push_str(text);
return;
}
out.reserve(text.len()); out.extend(text.chars().filter(|&ch| !is_zero_width(ch)));
}
pub(crate) fn is_zero_width(ch: char) -> bool {
let cp = ch as u32;
cp.wrapping_sub(0x200B) <= 2 || cp.wrapping_sub(0x2060) <= 4 || cp == 0xFEFF || cp == 0x180E
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collapse_whitespace() {
assert_eq!(collapse_whitespace("hello world"), "hello world");
}
#[test]
fn test_line_controls_fold_not_delete() {
for sep in [
'\u{0009}', '\u{000A}', '\u{000B}', '\u{000C}', '\u{000D}', '\u{001C}', '\u{001D}', '\u{001E}', '\u{001F}', '\u{0085}', ] {
assert_eq!(
collapse_whitespace(&format!("a{sep}b")),
"a b",
"{:#06x} should fold to a space",
sep as u32
);
}
}
#[test]
fn test_blank_render_set_folds_to_space() {
for blank in ['\u{2800}', '\u{115F}', '\u{1160}', '\u{3164}', '\u{FFA0}'] {
assert_eq!(
collapse_whitespace(&format!("a{blank}b")),
"a b",
"{:#06x} should fold to a space",
blank as u32
);
}
}
#[test]
fn test_fold_only_preserves_zero_width_and_nonws_control() {
assert_eq!(collapse_whitespace("he\u{200B}llo"), "he\u{200B}llo");
assert_eq!(collapse_whitespace("a\u{2061}b"), "a\u{2061}b"); assert_eq!(collapse_whitespace("a\x00b"), "a\x00b"); }
#[test]
fn test_strip_control_preserves_fold_whitespace() {
assert_eq!(strip_control_chars("a\x00b"), "ab"); assert_eq!(strip_control_chars("a\u{0007}b"), "ab"); assert_eq!(strip_control_chars("a\rb"), "a\rb"); assert_eq!(strip_control_chars("a\u{000B}b"), "a\u{000B}b"); assert_eq!(strip_control_chars("a\u{0085}b"), "a\u{0085}b"); assert_eq!(strip_control_chars("a\tb\nc"), "a\tb\nc"); }
#[test]
fn is_zero_width_has_no_ascii() {
for c in 0u8..0x80 {
assert!(
!is_zero_width(c as char),
"ASCII {c:#04x} must not be zero-width"
);
}
}
#[test]
fn fold_sets_are_disjoint_from_zero_width() {
for cp in 0u32..=0x1_0000 {
let Some(ch) = char::from_u32(cp) else {
continue;
};
if is_fold_whitespace(ch) || is_blank_render(ch) {
assert!(
!is_zero_width(ch),
"{cp:#06x} is both a fold char and zero-width"
);
}
}
}
mod proptest_properties {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn collapse_whitespace_idempotent(s in "\\PC*") {
let once = collapse_whitespace(&s);
let twice = collapse_whitespace(&once);
prop_assert_eq!(&once, &twice);
}
#[test]
fn no_leading_trailing_whitespace(s in "\\PC*") {
let result = collapse_whitespace(&s);
if !result.is_empty() {
prop_assert_ne!(result.as_bytes()[0], b' ');
prop_assert_ne!(result.as_bytes()[result.len() - 1], b' ');
}
}
#[test]
fn no_consecutive_spaces(s in "\\PC*") {
let result = collapse_whitespace(&s);
prop_assert!(!result.contains(" "), "double space in: {result:?}");
}
#[test]
fn alphanumeric_passthrough(s in "[a-zA-Z0-9]{1,50}") {
let result = collapse_whitespace(&s);
prop_assert_eq!(&result, &s);
}
#[test]
fn idempotent_over_ws_and_blank_sets(
s in r"[ab\x09\x0a\x0b\x0c\x0d\x1c\x1d\x1e\x1f\u{0085}\u{00a0}\u{2000}\u{2028}\u{3000}\u{2800}\u{115f}\u{1160}\u{3164}\u{ffa0}]{0,32}"
) {
let once = collapse_whitespace(&s);
let twice = collapse_whitespace(&once);
prop_assert_eq!(&once, &twice);
}
}
}
}