#[inline]
pub(crate) fn is_ktav_whitespace(c: char) -> bool {
if c.is_ascii() {
return matches!(
c,
'\u{0009}' | '\u{000A}' | '\u{000B}' | '\u{000C}' | '\u{000D}' | '\u{0020}'
);
}
matches!(
c,
'\u{0085}' | '\u{00A0}' | '\u{1680}' | '\u{2000}'
..='\u{200A}' | '\u{2028}' | '\u{2029}' | '\u{202F}' | '\u{205F}' | '\u{3000}'
)
}
#[inline]
pub(crate) fn is_inline_whitespace(c: char) -> bool {
is_ktav_whitespace(c) && c != '\u{000A}' && c != '\u{000D}'
}
#[inline]
pub(crate) fn inline_whitespace_ascii(b: u8) -> bool {
b == b' ' || b == b'\t' || b == 0x0B || b == 0x0C
}
pub(crate) fn common_leading_whitespace_prefix_len<'a>(
lines: impl IntoIterator<Item = &'a str>,
) -> usize {
let mut iter = lines
.into_iter()
.filter(|l| !l.trim_matches(is_ktav_whitespace).is_empty());
let first = match iter.next() {
Some(l) => leading_whitespace_run(l),
None => return 0,
};
let mut len = first.len();
for line in iter {
let other = leading_whitespace_run(line);
len = shared_prefix_bytes(first, other, len);
if len == 0 {
break;
}
}
len
}
fn leading_whitespace_run(s: &str) -> &str {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if inline_whitespace_ascii(b) {
i += 1;
} else if b < 0x80 {
break;
} else {
match s[i..].chars().next() {
Some(c) if is_inline_whitespace(c) => i += c.len_utf8(),
_ => break,
}
}
}
&s[..i]
}
fn shared_prefix_bytes(a: &str, b: &str, cap: usize) -> usize {
let (ab, bb) = (a.as_bytes(), b.as_bytes());
let lim = cap.min(ab.len()).min(bb.len());
let mut i = 0;
while i < lim && ab[i] < 0x80 && ab[i] == bb[i] {
i += 1;
}
if i < lim && ab[i] >= 0x80 {
let mut ac = a[i..].chars();
let mut bc = b[i..].chars();
while i < lim {
match (ac.next(), bc.next()) {
(Some(x), Some(y)) if x == y => i += x.len_utf8(),
_ => break,
}
}
}
i
}
#[cfg(test)]
mod tests {
use super::*;
const SET: [char; 25] = [
'\u{0009}', '\u{000A}', '\u{000B}', '\u{000C}', '\u{000D}', '\u{0020}', '\u{0085}',
'\u{00A0}', '\u{1680}', '\u{2000}', '\u{2001}', '\u{2002}', '\u{2003}', '\u{2004}',
'\u{2005}', '\u{2006}', '\u{2007}', '\u{2008}', '\u{2009}', '\u{200A}', '\u{2028}',
'\u{2029}', '\u{202F}', '\u{205F}', '\u{3000}',
];
#[test]
fn full_set_is_exactly_the_25_listed_code_points() {
for &c in &SET {
assert!(
is_ktav_whitespace(c),
"U+{:04X} must be § 3.3 whitespace",
c as u32
);
}
for c in [
'\u{0008}', '\u{000E}', '\u{001F}', '\u{0021}', '\u{0084}', '\u{0086}', '\u{009F}',
'\u{00A1}', '\u{167F}', '\u{1681}', '\u{180E}', '\u{1FFF}', '\u{200B}', '\u{2027}',
'\u{202A}', '\u{202E}', '\u{2030}', '\u{205E}', '\u{2060}', '\u{2FFF}', '\u{3001}',
'\u{FEFF}', '\u{E000}',
] {
assert!(
!is_ktav_whitespace(c),
"U+{:04X} must NOT be § 3.3 whitespace",
c as u32
);
}
}
#[test]
fn inline_view_is_full_set_minus_exactly_lf_and_cr() {
for &c in &SET {
let expected = c != '\u{000A}' && c != '\u{000D}';
assert_eq!(
is_inline_whitespace(c),
expected,
"inline view diverges on U+{:04X}",
c as u32
);
}
assert!(!is_inline_whitespace('\u{000A}'));
assert!(!is_inline_whitespace('\u{000D}'));
}
#[test]
fn ascii_byte_fast_path_matches_the_inline_view() {
for b in 0u8..128 {
assert_eq!(
inline_whitespace_ascii(b),
is_inline_whitespace(b as char),
"byte fast path diverges from the inline view at 0x{:02X}",
b
);
}
}
#[test]
fn host_whitespace_currently_matches_the_frozen_list() {
for cp in 0u32..=0x10FFFF {
if let Some(c) = char::from_u32(cp) {
assert_eq!(
is_ktav_whitespace(c),
c.is_whitespace(),
"host `char::is_whitespace` diverges from the § 3.3 list at U+{:04X}",
cp
);
}
}
}
#[test]
fn r8_f4_capacity_subtraction_stays_in_u32_width_where_the_product_does_not() {
let indent = " ".repeat(65_536);
let non_blank = format!("{indent}x");
let lines: Vec<&str> = std::iter::once(non_blank.as_str())
.chain(std::iter::repeat("").take(65_535))
.collect();
assert_eq!(lines.len(), 65_536);
let common_len = common_leading_whitespace_prefix_len(lines.iter().copied());
assert_eq!(common_len, 65_536);
let old = u32::checked_mul(common_len as u32, lines.len() as u32);
assert_eq!(
old, None,
"premise: common_len * lines.len() overflows u32 here"
);
let new_cap: u32 = lines
.iter()
.filter(|l| !l.trim_matches(is_ktav_whitespace).is_empty())
.map(|l| l.len() as u32 - common_len as u32)
.sum::<u32>()
.checked_add(lines.len() as u32)
.expect("new formula must stay within u32 width");
assert_eq!(new_cap, 1 + 65_536);
}
}