use std::collections::HashMap;
use std::sync::LazyLock;
use crate::uktextnorm::lexicon;
use crate::uktextnorm::morphology::{pronunciation, TRANSLITERATION};
use crate::uktextnorm::readers::ABBREVIATIONS;
use crate::uktextnorm::text::{
capitalize_first_letter, compact_spaces_lower, is_latin, is_uk, is_upper_uk, lower_cp,
lower_text,
};
fn is_word_character(cp: char) -> bool {
is_uk(cp) || is_latin(cp) || cp.is_ascii_digit()
}
fn char_before(text: &str, index: usize) -> Option<char> {
text[..index].chars().next_back()
}
fn match_key_at(text: &str, start: usize, key: &str) -> Option<usize> {
let skip_spaces = |pos: &mut usize| {
while text[*pos..].starts_with(' ') {
*pos += 1;
}
};
let mut pos = start;
let mut rest = key;
while let Some(kc) = rest.chars().next() {
match kc {
' ' => {
skip_spaces(&mut pos);
rest = &rest[1..];
}
'.' => {
skip_spaces(&mut pos);
if !text[pos..].starts_with('.') {
return None;
}
pos += 1;
rest = &rest[1..];
if !rest.is_empty() {
skip_spaces(&mut pos);
}
}
_ => {
let tc = text[pos..].chars().next()?;
if lower_cp(kc) != lower_cp(tc) {
return None;
}
pos += tc.len_utf8();
rest = &rest[kc.len_utf8()..];
}
}
}
Some(pos)
}
pub fn normalize_abbreviations(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut i = 0;
'outer: while i < text.len() {
for &(key, _) in lexicon::ABBREVIATIONS.iter() {
let Some(pos) = match_key_at(text, i, key) else { continue };
let left_boundary = char_before(text, i).is_none_or(|cp| !is_word_character(cp));
let Some(key_start) = key.chars().next() else { continue };
let Some(key_end) = key.chars().next_back() else { continue };
let following = text[pos..].chars().next();
let right_boundary =
!is_word_character(key_end) || following.is_none_or(|cp| !is_word_character(cp));
if (is_word_character(key_start) && !left_boundary) || !right_boundary {
continue;
}
let Some(&expansion) = ABBREVIATIONS.get(&compact_spaces_lower(&text[i..pos])) else {
continue;
};
let Some(first) = text[i..].chars().next() else { break 'outer };
if is_upper_uk(first) {
out.push_str(&capitalize_first_letter(expansion));
} else {
out.push_str(expansion);
}
if key.ends_with('.') && pos == text.len() {
out.push('.');
}
i = pos;
continue 'outer;
}
let Some(cp) = text[i..].chars().next() else { break };
out.push(cp);
i += cp.len_utf8();
}
out
}
pub fn expand_abbreviations(text: &str) -> String {
const VOWELS: &str = "АЕЄИІЇОУЮЯ";
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(cp) = rest.chars().next() {
if !is_upper_uk(cp) {
out.push(cp);
rest = &rest[cp.len_utf8()..];
continue;
}
let run_len = rest.chars().take_while(|&c| is_upper_uk(c)).map(char::len_utf8).sum();
let (token, tail) = rest.split_at(run_len);
rest = tail;
if token.chars().count() < 2 || token.chars().any(|c| VOWELS.contains(c)) {
out.push_str(token);
continue;
}
let parts: Vec<_> = token.chars().filter_map(pronunciation).collect();
out.push_str(&parts.join(" "));
}
out
}
static LATIN_DIACRITICS: LazyLock<HashMap<char, &'static str>> = LazyLock::new(|| {
let groups: [(&str, &str); 11] = [
("áàâãåāăąÁÀÂÃÅĀĂĄ", "а"),
("äÄéèêëēėęÉÈÊËĒĖĘ", "е"),
("íìîïīÍÌÎÏĪ", "і"),
("óòôõöōÓÒÔÕÖŌ", "о"),
("úùûūÚÙÛŪ", "у"),
("üÜ", "ю"),
("çÇ", "с"),
("ñÑ", "нь"),
("ß", "сс"),
("łŁ", "л"),
("ýÿÝŸ", "и"),
];
groups.iter().flat_map(|&(chars, to)| chars.chars().map(move |c| (c, to))).collect()
});
pub fn transliterate_to_cyrillic(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut i = 0;
while i < text.len() {
let Some(cp) = text[i..].chars().next() else {
break;
};
if cp.is_ascii_alphabetic() {
let matched = [3usize, 2, 1].into_iter().find_map(|len| {
let end = i + len;
if end > text.len() || !text.is_char_boundary(end) {
return None;
}
let key = lower_text(&text[i..end]);
TRANSLITERATION.get(key.as_str()).map(|&value| (len, value))
});
if let Some((len, value)) = matched {
out.push_str(value);
i += len;
} else {
out.push(cp);
i += cp.len_utf8();
}
continue;
}
match LATIN_DIACRITICS.get(&cp) {
Some(value) => out.push_str(value),
None => out.push(cp),
}
i += cp.len_utf8();
}
out
}