#[inline]
#[must_use]
pub const fn is_whitespace(ch: char) -> bool {
matches!(ch, ' ' | '\t' | '\n' | '\r')
}
#[inline]
#[must_use]
pub const fn is_whitespace_byte(b: u8) -> bool {
matches!(b, b' ' | b'\t' | b'\n' | b'\r')
}
#[inline]
#[must_use]
pub const fn is_name_start_char(ch: char) -> bool {
matches!(ch,
'A'..='Z'
| 'a'..='z'
| '_'
| ':'
| '\u{C0}'..='\u{D6}'
| '\u{D8}'..='\u{F6}'
| '\u{F8}'..='\u{2FF}'
| '\u{370}'..='\u{37D}'
| '\u{37F}'..='\u{1FFF}'
| '\u{200C}'..='\u{200D}'
| '\u{2070}'..='\u{218F}'
| '\u{2C00}'..='\u{2FEF}'
| '\u{3001}'..='\u{D7FF}'
| '\u{F900}'..='\u{FDCF}'
| '\u{FDF0}'..='\u{FFFD}'
| '\u{10000}'..='\u{EFFFF}'
)
}
#[inline]
#[must_use]
pub const fn is_name_char(ch: char) -> bool {
is_name_start_char(ch)
|| matches!(ch,
'0'..='9'
| '-'
| '.'
| '\u{B7}'
| '\u{0300}'..='\u{036F}'
| '\u{203F}'..='\u{2040}'
)
}
#[must_use]
pub fn skip_whitespace(input: &str) -> (&str, u32) {
let mut newlines = 0u32;
let mut chars = input.char_indices();
for (i, ch) in &mut chars {
if !is_whitespace(ch) {
return (&input[i..], newlines);
}
if ch == '\n' {
newlines += 1;
}
}
("", newlines)
}
#[must_use]
pub fn collapse_whitespace(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut in_whitespace = true;
for ch in input.chars() {
if is_whitespace(ch) {
if !in_whitespace && !result.is_empty() {
in_whitespace = true;
} else {
in_whitespace = true;
}
} else {
if in_whitespace && !result.is_empty() {
result.push(' ');
}
result.push(ch);
in_whitespace = false;
}
}
result
}
#[must_use]
pub fn read_name(input: &str) -> Option<(&str, &str)> {
let mut chars = input.chars();
let first = chars.next()?;
if !is_name_start_char(first) {
return None;
}
let mut end = first.len_utf8();
for ch in chars {
if !is_name_char(ch) {
break;
}
end += ch.len_utf8();
}
Some((&input[..end], &input[end..]))
}
#[inline]
#[must_use]
pub fn starts_with_bom(input: &str) -> bool {
input.as_bytes().starts_with(&[0xEF, 0xBB, 0xBF])
}
#[must_use]
pub fn strip_bom(input: &str) -> (&str, bool) {
if starts_with_bom(input) {
(&input[3..], true)
} else {
(input, false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn whitespace_chars() {
assert!(is_whitespace(' '));
assert!(is_whitespace('\t'));
assert!(is_whitespace('\n'));
assert!(is_whitespace('\r'));
assert!(!is_whitespace('a'));
assert!(!is_whitespace('0'));
assert!(!is_whitespace('\u{00A0}')); }
#[test]
fn whitespace_bytes() {
assert!(is_whitespace_byte(b' '));
assert!(is_whitespace_byte(b'\t'));
assert!(is_whitespace_byte(b'\n'));
assert!(is_whitespace_byte(b'\r'));
assert!(!is_whitespace_byte(b'a'));
}
#[test]
fn name_start_ascii() {
for ch in 'A'..='Z' {
assert!(is_name_start_char(ch), "Expected '{ch}' to be name start");
}
for ch in 'a'..='z' {
assert!(is_name_start_char(ch), "Expected '{ch}' to be name start");
}
assert!(is_name_start_char('_'));
assert!(is_name_start_char(':'));
}
#[test]
fn name_start_rejects_digits() {
for ch in '0'..='9' {
assert!(
!is_name_start_char(ch),
"Digit '{ch}' should NOT be name start"
);
}
}
#[test]
fn name_start_rejects_special() {
assert!(!is_name_start_char('-'));
assert!(!is_name_start_char('.'));
assert!(!is_name_start_char(' '));
assert!(!is_name_start_char('<'));
assert!(!is_name_start_char('>'));
}
#[test]
fn name_start_unicode() {
assert!(is_name_start_char('\u{C0}')); assert!(is_name_start_char('\u{D6}')); assert!(is_name_start_char('\u{4E00}')); }
#[test]
fn name_char_includes_digits_and_hyphen() {
assert!(is_name_char('0'));
assert!(is_name_char('9'));
assert!(is_name_char('-'));
assert!(is_name_char('.'));
assert!(is_name_char('\u{B7}')); }
#[test]
fn name_char_rejects_special() {
assert!(!is_name_char(' '));
assert!(!is_name_char('<'));
assert!(!is_name_char('>'));
assert!(!is_name_char('='));
assert!(!is_name_char('"'));
}
#[test]
fn skip_whitespace_basic() {
let (rest, nl) = skip_whitespace(" hello");
assert_eq!(rest, "hello");
assert_eq!(nl, 0);
}
#[test]
fn skip_whitespace_with_newlines() {
let (rest, nl) = skip_whitespace("\n\n hello");
assert_eq!(rest, "hello");
assert_eq!(nl, 2);
}
#[test]
fn skip_whitespace_no_whitespace() {
let (rest, nl) = skip_whitespace("hello");
assert_eq!(rest, "hello");
assert_eq!(nl, 0);
}
#[test]
fn skip_whitespace_all_whitespace() {
let (rest, nl) = skip_whitespace(" \n\t ");
assert_eq!(rest, "");
assert_eq!(nl, 1);
}
#[test]
fn skip_whitespace_empty() {
let (rest, nl) = skip_whitespace("");
assert_eq!(rest, "");
assert_eq!(nl, 0);
}
#[test]
fn collapse_basic() {
assert_eq!(collapse_whitespace("hello world"), "hello world");
}
#[test]
fn collapse_leading_trailing() {
assert_eq!(collapse_whitespace(" hello "), "hello");
}
#[test]
fn collapse_mixed_whitespace() {
assert_eq!(collapse_whitespace("\n\thello\n\tworld\n"), "hello world");
}
#[test]
fn collapse_all_whitespace() {
assert_eq!(collapse_whitespace(" "), "");
}
#[test]
fn collapse_empty() {
assert_eq!(collapse_whitespace(""), "");
}
#[test]
fn collapse_single_word() {
assert_eq!(collapse_whitespace("hello"), "hello");
}
#[test]
fn collapse_already_clean() {
assert_eq!(collapse_whitespace("a b c"), "a b c");
}
#[test]
fn read_name_simple() {
assert_eq!(read_name("element>"), Some(("element", ">")));
}
#[test]
fn read_name_with_namespace() {
assert_eq!(read_name("ns:tag attr"), Some(("ns:tag", " attr")));
}
#[test]
fn read_name_with_digits() {
assert_eq!(read_name("item123 "), Some(("item123", " ")));
}
#[test]
fn read_name_with_hyphen() {
assert_eq!(read_name("my-elem/>"), Some(("my-elem", "/>")));
}
#[test]
fn read_name_starts_with_digit() {
assert_eq!(read_name("123invalid"), None);
}
#[test]
fn read_name_empty() {
assert_eq!(read_name(""), None);
}
#[test]
fn read_name_underscore_start() {
assert_eq!(read_name("_private "), Some(("_private", " ")));
}
#[test]
fn read_name_unicode() {
assert_eq!(read_name("Ölpreis>"), Some(("Ölpreis", ">")));
}
#[test]
fn bom_detection() {
assert!(starts_with_bom("\u{FEFF}hello"));
assert!(!starts_with_bom("hello"));
assert!(!starts_with_bom(""));
assert!(!starts_with_bom("ab")); }
#[test]
fn bom_stripping() {
let (rest, had) = strip_bom("\u{FEFF}hello");
assert_eq!(rest, "hello");
assert!(had);
let (rest, had) = strip_bom("hello");
assert_eq!(rest, "hello");
assert!(!had);
}
}