use super::sanitizer::{SanitizeResult, Sanitizer};
use super::text_boundary::{
is_explicit_hasant_signal_at, is_khanda_ta_suffix_signal_at, is_phonetic_mark_signal,
};
use super::tokenizer::{PhoneticUnit, Token, TokenType, Tokenizer};
use crate::definitions::{
conjuncts::{conjuncts, ConjunctDefinitions},
numerals::bengali_digit,
symbol_value,
};
mod boundary;
mod components;
mod conjunct;
mod parts;
mod syllable;
mod word;
use boundary::{is_decimal_separator, is_decimal_separator_at, TokenNumberBoundary};
pub struct Transliterator {
conjuncts: &'static ConjunctDefinitions,
sanitizer: Sanitizer,
tokenizer: Tokenizer,
}
struct TextRenderState {
current_word_start: Option<usize>,
current_word_end: usize,
current_word_is_number: bool,
current_word_ends_with_number: bool,
previous_boundary: TokenNumberBoundary,
phonetic_units: Vec<PhoneticUnit>,
}
impl TextRenderState {
fn new() -> Self {
Self {
current_word_start: None,
current_word_end: 0,
current_word_is_number: true,
current_word_ends_with_number: false,
previous_boundary: TokenNumberBoundary::default(),
phonetic_units: Vec::new(),
}
}
}
impl Transliterator {
pub fn new() -> Self {
Transliterator {
conjuncts: conjuncts(),
sanitizer: Sanitizer::default(),
tokenizer: Tokenizer::default(),
}
}
fn render_tokens(&self, tokens: &[Token]) -> String {
let mut result = String::with_capacity(estimated_render_capacity(tokens));
let mut phonetic_units = Vec::new();
for index in 0..tokens.len() {
self.render_token_at_into(&mut result, tokens, index, &mut phonetic_units);
}
result
}
fn render_token_at_into(
&self,
result: &mut String,
tokens: &[Token],
index: usize,
phonetic_units: &mut Vec<PhoneticUnit>,
) {
let token = &tokens[index];
match token.token_type {
TokenType::Word => {
self.transliterate_word_units_into(result, &token.content, phonetic_units);
}
TokenType::Whitespace => {
result.push_str(&token.content);
}
TokenType::Punctuation | TokenType::Symbol => {
if is_decimal_separator(tokens, index) {
result.push('.');
} else if let Some(bengali_symbol) = symbol_value(token.content.as_str()) {
result.push_str(bengali_symbol);
} else {
result.push_str(&token.content);
}
}
TokenType::Number => {
self.render_number_token(result, &token.content);
}
}
}
fn render_text(&self, text: &str) -> String {
self.render_text_inner::<false>(text)
.expect("unchecked render path should not reject input")
}
fn render_text_checked(&self, text: &str) -> Option<String> {
self.render_text_inner::<true>(text)
}
fn render_text_inner<const CHECK_INPUT: bool>(&self, text: &str) -> Option<String> {
let mut result = String::with_capacity(estimated_text_render_capacity(text));
let mut state = TextRenderState::new();
let mut i = 0;
while i < text.len() {
let character = text[i..].chars().next().unwrap();
let char_len = character.len_utf8();
if CHECK_INPUT && !self.sanitizer.is_allowed_char(character) {
return None;
}
if is_phonetic_mark_signal(character) {
if state.current_word_start.is_none() {
state.current_word_start = Some(i);
}
state.current_word_is_number = false;
state.current_word_ends_with_number = false;
state.current_word_end = i + char_len;
i += char_len;
continue;
}
if let Some(start) = state.current_word_start {
if is_khanda_ta_suffix_signal_at(
character,
text,
i,
&text[start..state.current_word_end],
) {
state.current_word_is_number = false;
state.current_word_ends_with_number = false;
i += 2;
state.current_word_end = i;
continue;
}
}
if is_explicit_hasant_signal_at(character, text, i) {
if state.current_word_start.is_none() {
state.current_word_start = Some(i);
}
state.current_word_is_number = false;
state.current_word_ends_with_number = false;
i += 2;
state.current_word_end = i;
continue;
}
if character == '.' {
let dot_run_len = ascii_dot_run_len(text, i);
if dot_run_len > 1 {
self.flush_current_word(&mut result, text, &mut state);
result.push_str(&text[i..i + dot_run_len]);
state.previous_boundary = TokenNumberBoundary::default();
i += dot_run_len;
continue;
}
}
if character.is_whitespace() {
self.flush_current_word(&mut result, text, &mut state);
result.push(character);
state.previous_boundary = TokenNumberBoundary::default();
} else if character.is_ascii_punctuation() {
let current_word = state.current_word_start.map(|_| {
TokenNumberBoundary::from_number_state(state.current_word_ends_with_number)
});
let is_decimal = is_decimal_separator_at(
text,
i,
char_len,
current_word,
state.previous_boundary,
);
self.flush_current_word(&mut result, text, &mut state);
if is_decimal {
result.push('.');
} else if let Some(bengali_symbol) = symbol_value(&text[i..i + char_len]) {
result.push_str(bengali_symbol);
} else {
result.push(character);
}
state.previous_boundary = TokenNumberBoundary::default();
} else if !character.is_alphanumeric() {
self.flush_current_word(&mut result, text, &mut state);
let symbol_text = &text[i..i + char_len];
if let Some(bengali_symbol) = symbol_value(symbol_text) {
result.push_str(bengali_symbol);
} else {
result.push_str(symbol_text);
}
state.previous_boundary = TokenNumberBoundary::default();
} else {
if state.current_word_start.is_none() {
state.current_word_start = Some(i);
state.current_word_is_number = true;
}
let is_number = character.is_numeric();
state.current_word_is_number &= is_number;
state.current_word_ends_with_number = is_number;
state.current_word_end = i + char_len;
}
i += char_len;
}
self.flush_current_word(&mut result, text, &mut state);
Some(result)
}
fn flush_current_word(&self, result: &mut String, text: &str, state: &mut TextRenderState) {
let Some(start) = state.current_word_start.take() else {
state.current_word_is_number = true;
state.current_word_ends_with_number = false;
return;
};
let current_word = &text[start..state.current_word_end];
let is_number = state.current_word_is_number;
let ends_with_number = state.current_word_ends_with_number;
state.current_word_is_number = true;
state.current_word_ends_with_number = false;
state.previous_boundary = TokenNumberBoundary::from_number_state(ends_with_number);
if is_number {
self.render_number_token(result, current_word);
} else {
self.transliterate_word_units_into(result, current_word, &mut state.phonetic_units);
}
}
pub fn transliterate_tokens(&self, tokens: &[Token]) -> String {
self.render_tokens(tokens)
}
pub fn transliterate_token_at(&self, tokens: &[Token], index: usize) -> Option<String> {
if index >= tokens.len() {
return None;
}
let token = &tokens[index];
let mut result = String::with_capacity(token.content.len().saturating_mul(3));
let mut phonetic_units = Vec::new();
self.render_token_at_into(&mut result, tokens, index, &mut phonetic_units);
Some(result)
}
fn render_number_token(&self, result: &mut String, content: &str) {
for digit in content.chars() {
if let Some(mapped) = bengali_digit(digit) {
result.push_str(mapped);
} else {
result.push(digit);
}
}
}
pub fn transliterate(&self, text: &str) -> String {
self.render_text_checked(text)
.unwrap_or_else(|| text.to_string())
}
pub fn tokenize(&self, text: &str) -> Vec<Token> {
self.tokenizer.tokenize_text(text)
}
pub fn tokenize_phonetic(&self, word: &str) -> Vec<PhoneticUnit> {
self.tokenizer.tokenize_word(word)
}
pub fn tokenize_phonetic_into(&self, word: &str, units: &mut Vec<PhoneticUnit>) {
self.tokenizer.tokenize_word_into(word, units);
}
pub fn sanitize(&self, text: &str) -> SanitizeResult {
self.sanitizer.sanitize(text)
}
pub fn transliterate_lenient(&self, text: &str) -> String {
let cleaned = self.sanitizer.clean(text);
self.render_text(&cleaned)
}
}
impl Default for Transliterator {
fn default() -> Self {
Self::new()
}
}
fn estimated_render_capacity(tokens: &[Token]) -> usize {
tokens.iter().fold(0usize, |capacity, token| {
capacity.saturating_add(token.content.len().saturating_mul(3))
})
}
fn estimated_text_render_capacity(text: &str) -> usize {
text.len().saturating_mul(3)
}
fn ascii_dot_run_len(text: &str, byte_index: usize) -> usize {
text[byte_index..]
.bytes()
.take_while(|byte| *byte == b'.')
.count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn direct_render_matches_tokenized_render_for_boundaries() {
let transliterator = Transliterator::new();
for input in [
"12.34 12.34.",
"k12.34 a1.b2",
"12.34.56 12 .34 12..34 1.a2",
"k,,k t`` T`` :^",
"rrkSh rrk,,Sh k,,w k,,y",
"t`` T`` kt``a T``o",
"^ami :shokal shesh^",
"rZyab rrYa Zya kZya rZga",
"boi bou kOI kOU kOko kok",
"ami\u{00a0}bangla\u{2003}lekhi ১২.৩৪",
"আমি kA লিখি।",
] {
let tokens = transliterator.tokenize(input);
assert_eq!(
transliterator.transliterate(input),
transliterator.transliterate_tokens(&tokens),
"direct render diverged from tokenized render for {input}"
);
}
}
}