//! Text Cleaner — Safe UTF-8 text normalization kernel //! //! All operations use bounded buffers and validated UTF-8. //! No unsafe code. No raw pointer arithmetic. //! //! Operations: //! 1. Strip HTML tags //! 2. Unicode NFC normalization //! 3. Remove non-printable characters //! 4. Collapse whitespace //! 5. De-duplicate newlines //! 6. Trim use pyo3::prelude::*; use rayon::prelude::*; use regex::Regex; use unicode_normalization::UnicodeNormalization; use once_cell::sync::Lazy; // ── Compiled regexes (compiled once, used many times safely) ────────────────── /// Matches any HTML/XML tag static HTML_TAG: Lazy = Lazy::new(|| { Regex::new(r"<[^>]{0,2000}>").expect("HTML_TAG regex is valid") }); /// Matches HTML entities like & <   static HTML_ENTITY: Lazy = Lazy::new(|| { Regex::new(r"&(?:[a-zA-Z]{1,10}|#\d{1,6});").expect("HTML_ENTITY regex is valid") }); /// Matches sequences of 3+ newlines static EXCESS_NEWLINES: Lazy = Lazy::new(|| { Regex::new(r"\n{3,}").expect("EXCESS_NEWLINES regex is valid") }); /// Matches sequences of 2+ spaces/tabs (but not newlines) static EXCESS_SPACES: Lazy = Lazy::new(|| { Regex::new(r"[ \t]{2,}").expect("EXCESS_SPACES regex is valid") }); /// Matches non-printable ASCII control characters (except \n, \t) static CONTROL_CHARS: Lazy = Lazy::new(|| { Regex::new(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]").expect("CONTROL_CHARS regex is valid") }); // ── Core cleaning function (runs in Rust, no GIL held) ─────────────────────── /// Clean a single text string. /// /// Returns a cleaned version of the text with: /// - HTML stripped /// - Unicode normalized to NFC /// - Control characters removed /// - Excess whitespace collapsed fn clean_text_internal(text: &str) -> String { // 1. Bound input size (prevents pathological inputs from blowing the stack) let input = if text.len() > 10_000_000 { &text[..10_000_000] } else { text }; // 2. Strip HTML let no_html = HTML_TAG.replace_all(input, " "); let no_entities = HTML_ENTITY.replace_all(&no_html, " "); // 3. Unicode NFC normalization (safe: returns owned String) let normalized: String = no_entities.nfc().collect(); // 4. Remove non-printable control characters let no_ctrl = CONTROL_CHARS.replace_all(&normalized, ""); // 5. Collapse excess whitespace let single_spaces = EXCESS_SPACES.replace_all(&no_ctrl, " "); let clean_newlines = EXCESS_NEWLINES.replace_all(&single_spaces, "\n\n"); // 6. Trim clean_newlines.trim().to_string() } // ── Python-exposed functions ─────────────────────────────────────────────────── /// Clean a single text string. Releases GIL during heavy processing. /// /// Args: /// text (str): Raw text to clean /// /// Returns: /// str: Cleaned text #[pyfunction] pub fn clean_text(py: Python<'_>, text: String) -> String { // Release GIL — this is pure CPU work, no Python objects touched inside py.allow_threads(|| clean_text_internal(&text)) } /// Clean a batch of text strings in parallel using Rayon. /// /// This is 4–8x faster than calling clean_text() in a Python loop because: /// - All threads run without the GIL /// - Rayon work-steals across cores /// - No Python object allocation per item /// /// Args: /// texts (list[str]): List of raw text strings /// /// Returns: /// list[str]: List of cleaned strings, same order as input #[pyfunction] pub fn clean_batch(py: Python<'_>, texts: Vec) -> Vec { py.allow_threads(|| { texts.par_iter() .map(|t| clean_text_internal(t)) .collect() }) } // ── Unit Tests ──────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; #[test] fn test_strip_html() { let result = clean_text_internal("

Hello world

"); assert!(!result.contains('<'), "HTML tags should be removed"); assert!(result.contains("Hello"), "Text content preserved"); } #[test] fn test_unicode_nfc() { // Café with decomposed 'e + combining accent' vs precomposed 'é' let decomposed = "Cafe\u{0301}"; // decomposed let result = clean_text_internal(decomposed); let expected = "Caf\u{00E9}"; // NFC precomposed assert_eq!(result, expected, "Unicode should be NFC normalized"); } #[test] fn test_collapse_whitespace() { let result = clean_text_internal("hello world\n\n\n\nfoo"); assert!(!result.contains(" "), "Excess spaces collapsed"); assert!(!result.contains("\n\n\n"), "Excess newlines collapsed"); } #[test] fn test_control_chars_removed() { let result = clean_text_internal("hello\x00\x01\x07world"); assert!(!result.contains('\x00'), "Null bytes removed"); assert!(result.contains("helloworld"), "Text content preserved"); } #[test] fn test_empty_string() { assert_eq!(clean_text_internal(""), ""); } #[test] fn test_large_input_bounded() { // Should not panic or OOM on huge inputs let large = "a".repeat(20_000_000); let result = clean_text_internal(&large); assert!(result.len() <= 10_000_000, "Large input is bounded safely"); } }