rishe 0.4.0

Rishe (ریشه): High-performance Persian NLP engine in Rust — tokenization, SBD, lemmatization, ezafe detection, informal→formal normalization, stemming, spell checking, embeddings
Documentation
use crate::colloquial::FUSED_PREP_CLITICS;
use crate::morphology::{MorphologyEngine, ATTACHED_NOUN_SUFFIXES, ZWNJ_NOUN_SUFFIXES};

const ZWNJ: &str = "\u{200C}";
const PREFIXES: &[&str] = &[
    "نمی", "می", "بر", "باز", "در", "فرو", "وا", "ور", "فرا", "پس", "پیش",
];

pub struct TokenSplitter;

impl TokenSplitter {
    pub fn split(token: &str, morph: &MorphologyEngine) -> Vec<String> {
        for &(colloq, (first, second)) in FUSED_PREP_CLITICS {
            if token == colloq {
                return vec![first.to_string(), second.to_string()];
            }
        }

        for &prefix in PREFIXES {
            if let Some(rest) = token.strip_prefix(prefix) {
                let rest = rest.strip_prefix(ZWNJ).unwrap_or(rest);
                if rest.is_empty() {
                    continue;
                }
                let valid = if prefix == "می" || prefix == "نمی" {
                    morph.verb_roots(rest).is_some()
                } else {
                    morph.is_known_noun(rest) || morph.verb_roots(rest).is_some()
                };
                if valid {
                    return vec![prefix.to_string(), rest.to_string()];
                }
            }
        }

        if let Some((first, second)) = token.split_once(ZWNJ) {
            if !first.is_empty()
                && !second.is_empty()
                && (morph.is_known_noun(first) || morph.is_known_noun(second))
            {
                return vec![first.to_string(), second.to_string()];
            }
        }

        for &suffix in ZWNJ_NOUN_SUFFIXES {
            if let Some(stem) = token.strip_suffix(suffix) {
                if !stem.is_empty() && morph.is_known_noun(stem) {
                    return vec![
                        stem.to_string(),
                        suffix.trim_start_matches(ZWNJ).to_string(),
                    ];
                }
            }
        }

        if morph.is_known_noun(token) {
            return vec![token.to_string()];
        }

        for &suffix in ATTACHED_NOUN_SUFFIXES {
            if let Some(stem) = token.strip_suffix(suffix) {
                if stem.chars().count() >= 2 && morph.is_known_noun(stem) {
                    return vec![stem.to_string(), suffix.to_string()];
                }
            }
        }

        vec![token.to_string()]
    }
}