ruwex 0.1.0

Fast Rust rewrite of wikiextractor: extract and clean text from Wikimedia XML dumps
Documentation
//! Ports of wikiextractor's `dropNested` (remove nested expressions like
//! tables) and `dropSpans` (bulk-remove collected spans).

use regex::Regex;

/// Removes matched `open…close` regions, handling nesting the same way as
/// the Python original — including its behavior on unbalanced input.
pub fn drop_nested(text: &str, open_re: &Regex, close_re: &Regex) -> String {
    let mut spans: Vec<(usize, usize)> = Vec::new();
    let mut nest = 0i64;
    let Some(mut start) = open_re.find(text) else {
        return text.to_string();
    };
    let mut end = close_re.find_at(text, start.end());
    let mut next = start;
    while let Some(mut e) = end {
        let Some(nxt) = open_re.find_at(text, next.end()) else {
            // termination: close all pending
            while nest > 0 {
                nest -= 1;
                match close_re.find_at(text, e.end()) {
                    Some(e0) => e = e0,
                    None => break,
                }
            }
            spans.push((start.start(), e.end()));
            break;
        };
        next = nxt;
        let mut new_end = Some(e);
        while e.end() < next.start() {
            // { } {
            if nest != 0 {
                nest -= 1;
                // try closing more
                let last = e.end();
                match close_re.find_at(text, e.end()) {
                    Some(e2) => {
                        e = e2;
                        new_end = Some(e2);
                    }
                    None => {
                        // unbalanced
                        let span = match spans.first() {
                            Some(&(s0, _)) => (s0, last),
                            None => (start.start(), last),
                        };
                        spans.clear();
                        spans.push(span);
                        break;
                    }
                }
            } else {
                spans.push((start.start(), e.end()));
                // advance start, find next close
                start = next;
                new_end = close_re.find_at(text, next.end());
                break; // { }
            }
        }
        if next.range() != start.range() {
            // { { }
            nest += 1;
        }
        end = new_end;
    }
    drop_spans(spans, text)
}

/// Removes the given (possibly nested/overlapping) spans from `text`.
pub fn drop_spans(mut spans: Vec<(usize, usize)>, text: &str) -> String {
    spans.sort_unstable();
    let mut result = String::with_capacity(text.len());
    let mut offset = 0;
    for (s, e) in spans {
        if offset <= s {
            // handle nesting
            if offset < s {
                result.push_str(&text[offset..s]);
            }
            offset = e;
        }
    }
    result.push_str(&text[offset..]);
    result
}

#[cfg(test)]
mod tests {
    use std::sync::LazyLock;

    use super::*;

    static OPEN_TABLE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)\{\|").unwrap());
    static CLOSE_TABLE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)\|\}").unwrap());

    #[test]
    fn drops_simple_table() {
        assert_eq!(
            drop_nested("before {| a |- b |} after", &OPEN_TABLE, &CLOSE_TABLE),
            "before  after"
        );
    }

    #[test]
    fn drops_nested_tables() {
        assert_eq!(
            drop_nested(
                "x {| outer {| inner |} more |} y",
                &OPEN_TABLE,
                &CLOSE_TABLE
            ),
            "x  y"
        );
    }

    #[test]
    fn drops_consecutive_tables() {
        assert_eq!(
            drop_nested("a {| one |} b {| two |} c", &OPEN_TABLE, &CLOSE_TABLE),
            "a  b  c"
        );
    }

    #[test]
    fn unbalanced_open_drops_to_last_close() {
        // Same as Python: an unclosed nested open swallows up to the last
        // found close.
        assert_eq!(
            drop_nested("a {| x {| y |} b", &OPEN_TABLE, &CLOSE_TABLE),
            "a  b"
        );
    }

    #[test]
    fn no_delimiters_returns_input() {
        assert_eq!(
            drop_nested("plain text", &OPEN_TABLE, &CLOSE_TABLE),
            "plain text"
        );
    }

    #[test]
    fn drop_spans_skips_nested_spans() {
        assert_eq!(
            drop_spans(vec![(2, 8), (4, 6), (10, 12)], "0123456789abcdef"),
            "0189cdef"
        );
    }
}