//! Shared text utilities for parsing and matching. /// Case-insensitive prefix check without allocation. /// /// Returns the remaining part of the string if `s` starts with `prefix` /// (case-insensitive), otherwise returns `None`. Safe for multi-byte UTF-8. pub fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> { if s.len() < prefix.len() || !s.is_char_boundary(prefix.len()) { return None; } let s_prefix = &s[..prefix.len()]; if s_prefix.eq_ignore_ascii_case(prefix) { Some(&s[prefix.len()..]) } else { None } } #[cfg(test)] mod tests { use super::*; #[test] fn test_basic_match() { assert_eq!(strip_prefix_ci("PROJECT:test", "project:"), Some("test")); assert_eq!(strip_prefix_ci("Project:test", "project:"), Some("test")); } #[test] fn test_no_match() { assert_eq!(strip_prefix_ci("proj:test", "project:"), None); assert_eq!(strip_prefix_ci("pr", "project:"), None); } #[test] fn test_multibyte_utf8_safe() { // Ensure no panic on multi-byte characters near prefix boundary assert_eq!(strip_prefix_ci("pr😀:H", "pri:"), None); assert_eq!(strip_prefix_ci("düe:tom", "due:"), None); } }