use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq)]
pub struct UnknownToken {
pub script: String,
pub token: char,
pub unicode: String,
pub position: usize,
pub is_extension: bool,
}
impl UnknownToken {
pub fn new(script: &str, token: char, position: usize, is_extension: bool) -> Self {
Self {
script: script.to_string(),
token,
unicode: format!("U+{:04X}", token as u32),
position,
is_extension,
}
}
pub fn format(&self) -> String {
if self.is_extension {
format!("[ext:{}:{}:{}]", self.script, self.token, self.unicode)
} else {
format!("[{}:{}:{}]", self.script, self.token, self.unicode)
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TransliterationMetadata {
pub unknown_tokens: Vec<UnknownToken>,
pub source_script: String,
pub target_script: String,
pub used_extensions: bool,
}
impl TransliterationMetadata {
pub fn new(source_script: &str, target_script: &str) -> Self {
Self {
unknown_tokens: Vec::new(),
source_script: source_script.to_string(),
target_script: target_script.to_string(),
used_extensions: false,
}
}
pub fn add_unknown(&mut self, token: UnknownToken) {
if token.is_extension {
self.used_extensions = true;
}
self.unknown_tokens.push(token);
}
pub fn unique_unknowns(&self) -> Vec<char> {
let mut unique: HashSet<char> = HashSet::new();
for token in &self.unknown_tokens {
unique.insert(token.token);
}
let mut result: Vec<char> = unique.into_iter().collect();
result.sort();
result
}
pub fn report(&self) -> String {
if self.unknown_tokens.is_empty() {
return format!(
"No unknown tokens found in {} → {} conversion",
self.source_script, self.target_script
);
}
let mut report = format!(
"Unknown tokens in {} → {} conversion:\n",
self.source_script, self.target_script
);
let unique = self.unique_unknowns();
for ch in unique {
let occurrences: Vec<&UnknownToken> = self
.unknown_tokens
.iter()
.filter(|t| t.token == ch)
.collect();
report.push_str(&format!(
" '{}' ({}) - {} occurrence(s)\n",
ch,
occurrences[0].unicode,
occurrences.len()
));
}
if self.used_extensions {
report.push_str("\nNote: Some unknown tokens came from runtime extensions\n");
}
report
}
}
#[derive(Debug, Clone)]
pub struct TransliterationResult {
pub output: String,
pub metadata: Option<TransliterationMetadata>,
}
impl TransliterationResult {
pub fn simple(output: String) -> Self {
Self {
output,
metadata: None,
}
}
pub fn with_metadata(output: String, metadata: TransliterationMetadata) -> Self {
Self {
output,
metadata: Some(metadata),
}
}
pub fn annotated_output(&self) -> String {
match &self.metadata {
None => self.output.clone(),
Some(metadata) => {
if metadata.unknown_tokens.is_empty() {
return self.output.clone();
}
let mut tokens = metadata.unknown_tokens.clone();
tokens.sort_by_key(|b| std::cmp::Reverse(b.position));
let mut result = self.output.clone();
for token in tokens {
let annotation = token.format();
if let Some(char_boundary) = result
.char_indices()
.find(|(i, _)| *i == token.position)
.and_then(|(i, _ch)| {
result
.char_indices()
.find(|(j, _)| *j > i)
.map(|(j, _)| j)
.or(Some(result.len()))
})
{
result.insert_str(char_boundary, &annotation);
}
}
result
}
}
}
}
pub trait UnknownHandler {
fn is_known_char(&self, script: &str, ch: char) -> bool;
fn process_with_unknowns(
&self,
script: &str,
input: &str,
is_extension: bool,
) -> (String, Vec<UnknownToken>) {
let mut output = String::new();
let mut unknowns = Vec::new();
for (pos, ch) in input.char_indices() {
if self.is_known_char(script, ch) {
output.push(ch);
} else {
output.push(ch); unknowns.push(UnknownToken::new(script, ch, pos, is_extension));
}
}
(output, unknowns)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unknown_token_creation() {
let token = UnknownToken::new("devanagari", '☺', 5, false);
assert_eq!(token.script, "devanagari");
assert_eq!(token.token, '☺');
assert_eq!(token.unicode, "U+263A");
assert_eq!(token.format(), "[devanagari:☺:U+263A]");
}
#[test]
fn test_extension_token_format() {
let token = UnknownToken::new("vedavms", '†', 10, true);
assert_eq!(token.format(), "[ext:vedavms:†:U+2020]");
}
#[test]
fn test_metadata_unique_unknowns() {
let mut metadata = TransliterationMetadata::new("source", "target");
metadata.add_unknown(UnknownToken::new("source", 'a', 0, false));
metadata.add_unknown(UnknownToken::new("source", 'b', 1, false));
metadata.add_unknown(UnknownToken::new("source", 'a', 2, false));
let unique = metadata.unique_unknowns();
assert_eq!(unique, vec!['a', 'b']);
}
#[test]
fn test_transliteration_result() {
let result = TransliterationResult::simple("dharma".to_string());
assert_eq!(result.output, "dharma");
assert!(result.metadata.is_none());
}
}