pub mod modules;
use modules::hub::HubTrait;
#[cfg(feature = "python")]
pub mod python_bindings;
#[cfg(feature = "wasm")]
pub mod wasm_bindings;
use modules::hub::Hub;
#[cfg(not(target_arch = "wasm32"))]
use modules::profiler::{OptimizationCache, Profiler, ProfilerConfig};
use modules::registry::{SchemaRegistry, SchemaRegistryTrait};
#[cfg(not(target_arch = "wasm32"))]
use modules::runtime::RuntimeCompiler;
use modules::schema::{Schema as RuntimeSchema, SchemaBuilder};
use modules::script_converter::ScriptConverterRegistry;
pub use modules::core::unknown_handler::{
TransliterationMetadata, TransliterationResult, UnknownToken,
};
#[derive(Debug, Clone)]
pub struct SchemaInfo {
pub name: String,
pub description: String,
pub script_type: String,
pub is_runtime_loaded: bool,
pub mapping_count: usize,
}
#[derive(Debug)]
pub enum ProcessorSource {
Static,
RuntimeCompiled(Box<dyn std::any::Any + Send + Sync>),
Dynamic,
}
pub struct Shlesha {
hub: Hub,
script_converter_registry: ScriptConverterRegistry,
registry: SchemaRegistry,
#[cfg(not(target_arch = "wasm32"))]
runtime_compiler: Option<RuntimeCompiler>,
processors: std::collections::HashMap<String, ProcessorSource>,
#[cfg(not(target_arch = "wasm32"))]
profiler: Option<Profiler>,
#[cfg(not(target_arch = "wasm32"))]
optimization_cache: OptimizationCache,
}
impl Shlesha {
pub fn new() -> Self {
let script_converter_registry = ScriptConverterRegistry::default();
let mut registry = SchemaRegistry::new();
if registry.load_schema("schemas/devanagari.yaml").is_err() {
}
Self {
hub: Hub::new(),
script_converter_registry,
registry,
#[cfg(not(target_arch = "wasm32"))]
runtime_compiler: RuntimeCompiler::new().ok(),
processors: std::collections::HashMap::new(),
#[cfg(not(target_arch = "wasm32"))]
profiler: None,
#[cfg(not(target_arch = "wasm32"))]
optimization_cache: OptimizationCache::new(),
}
}
pub fn transliterate(
&self,
text: &str,
from: &str,
to: &str,
) -> Result<String, Box<dyn std::error::Error>> {
#[cfg(not(target_arch = "wasm32"))]
{
use std::time::Instant;
let start_time = Instant::now();
let result = self
.optimization_cache
.apply_optimization(text, from, to, |text| {
self.transliterate_internal(text, from, to)
});
if let Some(ref profiler) = self.profiler {
let processing_time = start_time.elapsed();
profiler.record_conversion(from, to, text, processing_time);
}
result
}
#[cfg(target_arch = "wasm32")]
{
self.transliterate_internal(text, from, to)
}
}
fn transliterate_internal(
&self,
text: &str,
from: &str,
to: &str,
) -> Result<String, Box<dyn std::error::Error>> {
if from == to {
return Ok(text.to_string());
}
let hub_input = self.script_converter_registry.to_hub_with_schema_registry(
from,
text,
Some(&self.registry),
)?;
let final_hub_input = match (&hub_input, from, to) {
(modules::hub::HubFormat::AlphabetTokens(_), _, _)
if self.script_converter_registry.supports_script(to) =>
{
let tokens = match &hub_input {
modules::hub::HubFormat::AlphabetTokens(tokens) => tokens,
_ => return Err("Expected AlphabetTokens".into()),
};
if self.is_indic_script(to) {
let abugida_tokens = self.hub.alphabet_to_abugida_tokens(tokens)?;
modules::hub::HubFormat::AbugidaTokens(abugida_tokens)
} else {
hub_input
}
}
(modules::hub::HubFormat::AbugidaTokens(_), _, _)
if self.script_converter_registry.supports_script(to) =>
{
let tokens = match &hub_input {
modules::hub::HubFormat::AbugidaTokens(tokens) => tokens,
_ => return Err("Expected AbugidaTokens".into()),
};
if self.is_roman_script(to) {
let alphabet_tokens = self.hub.abugida_to_alphabet_tokens(tokens)?;
modules::hub::HubFormat::AlphabetTokens(alphabet_tokens)
} else {
hub_input
}
}
_ => hub_input,
};
let result = self
.script_converter_registry
.from_hub_with_schema_registry(to, &final_hub_input, Some(&self.registry))?;
Ok(result)
}
fn is_roman_script(&self, script: &str) -> bool {
modules::script_converter::is_roman_script(script)
}
fn is_indic_script(&self, script: &str) -> bool {
modules::script_converter::is_indic_script(script)
}
pub fn transliterate_with_metadata(
&self,
text: &str,
from: &str,
to: &str,
) -> Result<
crate::modules::core::unknown_handler::TransliterationResult,
Box<dyn std::error::Error>,
> {
let (hub_input, from_metadata) = self
.script_converter_registry
.to_hub_with_metadata(from, text)?;
let final_hub_input = match (&hub_input, from, to) {
(modules::hub::HubFormat::AlphabetTokens(_), _, _)
if self.script_converter_registry.supports_script(to) =>
{
let tokens = match &hub_input {
modules::hub::HubFormat::AlphabetTokens(tokens) => tokens,
_ => return Err("Expected AlphabetTokens".into()),
};
if self.is_indic_script(to) {
let abugida_tokens = self.hub.alphabet_to_abugida_tokens(tokens)?;
modules::hub::HubFormat::AbugidaTokens(abugida_tokens)
} else {
hub_input
}
}
(modules::hub::HubFormat::AbugidaTokens(_), _, _)
if self.script_converter_registry.supports_script(to) =>
{
let tokens = match &hub_input {
modules::hub::HubFormat::AbugidaTokens(tokens) => tokens,
_ => return Err("Expected AbugidaTokens".into()),
};
if self.is_roman_script(to) {
let alphabet_tokens = self.hub.abugida_to_alphabet_tokens(tokens)?;
modules::hub::HubFormat::AlphabetTokens(alphabet_tokens)
} else {
hub_input
}
}
_ => hub_input,
};
let (result, to_metadata) = match self
.script_converter_registry
.from_hub_with_metadata(to, &final_hub_input)
{
Ok(result) => (
result,
None::<modules::core::unknown_handler::TransliterationMetadata>,
),
Err(e) => {
return Err(format!("Conversion failed: {}", e).into());
}
};
let mut final_metadata =
modules::core::unknown_handler::TransliterationMetadata::new(from, to);
if let Some(result_metadata) = result.metadata {
final_metadata
.unknown_tokens
.extend(result_metadata.unknown_tokens);
}
if !from_metadata.unknown_tokens.is_empty() {
final_metadata
.unknown_tokens
.extend(from_metadata.unknown_tokens);
}
if let Some(hub_metadata) = to_metadata {
final_metadata
.unknown_tokens
.extend(hub_metadata.unknown_tokens);
}
Ok(modules::core::unknown_handler::TransliterationResult {
output: result.output,
metadata: Some(final_metadata),
})
}
pub fn load_schema_from_file(
&mut self,
file_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
self.registry.load_schema(file_path)?;
Ok(())
}
pub fn load_schema_from_string(
&mut self,
yaml_content: &str,
schema_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
self.registry
.load_schema_from_string(yaml_content, schema_name)?;
Ok(())
}
pub fn add_runtime_schema(
&mut self,
schema: RuntimeSchema,
) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(not(target_arch = "wasm32"))]
{
match &mut self.runtime_compiler {
Some(compiler) => {
match compiler.compile_schema(&schema) {
Ok(compiled) => {
self.processors.insert(
schema.metadata.name.clone(),
ProcessorSource::RuntimeCompiled(Box::new(compiled)),
);
return Ok(());
}
Err(_) => {
}
}
}
None => {
}
}
}
let registry_schema = self.convert_runtime_schema_to_registry(&schema);
let _ = self
.registry
.add_schema(schema.metadata.name.clone(), registry_schema);
self.processors
.insert(schema.metadata.name.clone(), ProcessorSource::Dynamic);
Ok(())
}
pub fn create_schema(&mut self, name: &str) -> SchemaBuilder {
SchemaBuilder::new(name)
}
fn convert_runtime_schema_to_registry(
&self,
runtime_schema: &RuntimeSchema,
) -> modules::registry::Schema {
use modules::registry::{Schema as RegistrySchema, SchemaMetadata as RegistryMetadata};
use rustc_hash::FxHashMap;
let mut flattened_mappings = FxHashMap::default();
for entries in runtime_schema.mappings.values() {
for (token, mapping) in entries {
let preferred_mapping = match mapping {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Array(arr) => arr
.first()
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
_ => continue,
};
flattened_mappings.insert(token.clone(), preferred_mapping);
}
}
RegistrySchema {
name: runtime_schema.metadata.name.clone(),
script_type: runtime_schema.metadata.script_type.clone(),
target: runtime_schema.target.clone(),
mappings: flattened_mappings,
metadata: RegistryMetadata {
name: runtime_schema.metadata.name.clone(),
script_type: runtime_schema.metadata.script_type.clone(),
has_implicit_a: false, description: runtime_schema.metadata.description.clone(),
aliases: None, },
}
}
pub fn list_supported_scripts(&self) -> Vec<String> {
let mut scripts = self
.script_converter_registry
.list_supported_scripts()
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
let runtime_scripts = self.registry.list_schemas_owned();
scripts.extend(runtime_scripts);
scripts.sort();
scripts.dedup();
scripts
}
pub fn supports_script(&self, script_name: &str) -> bool {
self.script_converter_registry
.supports_script_with_registry(script_name, Some(&self.registry))
|| self.registry.get_schema(script_name).is_some()
}
pub fn get_schema_info(&self, script_name: &str) -> Option<SchemaInfo> {
self.registry
.get_schema(script_name)
.map(|schema| SchemaInfo {
name: schema.metadata.name.clone(),
description: schema.metadata.description.clone().unwrap_or_default(),
script_type: schema.metadata.script_type.clone(),
is_runtime_loaded: true,
mapping_count: schema.mappings.values().map(|m| m.len()).sum(),
})
}
pub fn remove_schema(&mut self, script_name: &str) -> bool {
self.registry.remove_schema(script_name)
}
pub fn clear_runtime_schemas(&mut self) {
self.registry.clear();
}
pub fn with_registry(registry: SchemaRegistry) -> Self {
let script_converter_registry = ScriptConverterRegistry::default();
Self {
hub: Hub::new(),
script_converter_registry,
registry,
#[cfg(not(target_arch = "wasm32"))]
runtime_compiler: None, processors: std::collections::HashMap::new(),
#[cfg(not(target_arch = "wasm32"))]
profiler: None,
#[cfg(not(target_arch = "wasm32"))]
optimization_cache: OptimizationCache::new(),
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn enable_profiling(&mut self) {
self.profiler = Some(Profiler::new());
}
#[cfg(not(target_arch = "wasm32"))]
pub fn enable_profiling_with_config(&mut self, config: ProfilerConfig) {
self.profiler = Some(Profiler::with_config(config));
}
#[cfg(not(target_arch = "wasm32"))]
pub fn disable_profiling(&mut self) {
self.profiler = None;
}
#[cfg(not(target_arch = "wasm32"))]
pub fn get_profile_stats(
&self,
) -> Option<rustc_hash::FxHashMap<(String, String), modules::profiler::ProfileStats>> {
self.profiler.as_ref().map(|p| p.get_profile_stats())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn generate_optimizations(&self) -> Vec<modules::profiler::OptimizedLookupTable> {
self.profiler
.as_ref()
.map(|p| p.generate_optimizations())
.unwrap_or_default()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn load_optimization(&self, optimization: modules::profiler::OptimizedLookupTable) {
self.optimization_cache.load(optimization);
}
#[cfg(not(target_arch = "wasm32"))]
pub fn save_profiles(&self) {
if let Some(ref profiler) = self.profiler {
profiler.save_profiles();
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn with_profiling() -> Self {
let mut instance = Self::new();
instance.enable_profiling();
instance
}
}
impl Default for Shlesha {
fn default() -> Self {
Self::new()
}
}
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_info() {
println!("Shlesha version: {}", VERSION);
}
#[test]
fn test_transliterator_creation() {
let _transliterator = Shlesha::new();
}
#[test]
fn test_basic_metadata_collection() {
let transliterator = Shlesha::new();
let result = transliterator
.transliterate_with_metadata("अ", "devanagari", "iast")
.unwrap();
assert_eq!(result.output, "a");
assert!(result.metadata.is_some());
let metadata = result.metadata.unwrap();
assert_eq!(metadata.source_script, "devanagari");
assert_eq!(metadata.target_script, "iast");
assert!(metadata.unknown_tokens.is_empty());
}
#[test]
fn test_unknown_character_handling_at_all_levels() {
let transliterator = Shlesha::new();
let result = transliterator
.transliterate("धर्मkr", "devanagari", "iso15919")
.unwrap();
assert_eq!(result, "dharmakr");
let result = transliterator
.transliterate("धर्मkr", "devanagari", "gujarati")
.unwrap();
println!("Test input: धर्मkr");
println!("Expected: ધર્મkr");
println!("Actual: {}", result);
let simple_result = transliterator
.transliterate("धर्म", "devanagari", "gujarati")
.unwrap();
println!("\nSimple test: धर्म -> {}", simple_result);
println!("Expected: ધર્મ");
let roman_result = transliterator
.transliterate("धर्म", "devanagari", "iast")
.unwrap();
println!("To Roman: {}", roman_result);
println!("\nDEBUG: Character by character analysis");
for (i, ch) in "धर्म".chars().enumerate() {
println!(" [{}] {} (U+{:04X})", i, ch, ch as u32);
if ch == '्' {
println!(" ^ This is the virama character!");
}
}
assert_eq!(result, "ધર્મkr");
let result = transliterator
.transliterate("dharmaqx", "iast", "devanagari")
.unwrap();
assert_eq!(result, "धर्मqx");
let result = transliterator
.transliterate_with_metadata("धर्मkr", "devanagari", "iso15919")
.unwrap();
assert_eq!(result.output, "dharmakr");
assert!(result.metadata.is_some());
}
#[test]
fn test_mixed_content_graceful_handling() {
let transliterator = Shlesha::new();
let result = transliterator
.transliterate("धर्म hello world", "devanagari", "iso15919")
.unwrap();
assert_eq!(result, "dharma hello world");
let result = transliterator
.transliterate("धर्म! 123", "devanagari", "iso15919")
.unwrap();
assert_eq!(result, "dharma! 123");
let result = transliterator
.transliterate("xyz123", "devanagari", "iso15919")
.unwrap();
assert_eq!(result, "xyz123"); }
#[test]
fn test_virama_across_scripts() {
let transliterator = Shlesha::new();
let input = "धर्म";
println!("Testing virama handling across scripts:");
println!("Input: {} (Devanagari)", input);
let scripts = vec![
("bengali", "ধর্ম"),
("gujarati", "ધર્મ"),
("telugu", "ధర్మ"),
("kannada", "ಧರ್ಮ"),
("malayalam", "ധര്മ"),
];
for (script, expected) in scripts {
match transliterator.transliterate(input, "devanagari", script) {
Ok(result) => {
let chars_result: Vec<char> = result.chars().collect();
let chars_expected: Vec<char> = expected.chars().collect();
println!("\n{} script:", script);
println!(" Result: {} ({} chars)", result, chars_result.len());
println!(" Expected: {} ({} chars)", expected, chars_expected.len());
if chars_result.len() != chars_expected.len() {
println!(" ❌ Character count mismatch!");
for (i, ch) in chars_result.iter().enumerate() {
println!(" [{}] {} (U+{:04X})", i, ch, *ch as u32);
}
println!(" Expected breakdown:");
for (i, ch) in chars_expected.iter().enumerate() {
println!(" [{}] {} (U+{:04X})", i, ch, *ch as u32);
}
} else {
println!(" ✅ Character count matches");
}
}
Err(e) => println!("{}: Error - {}", script, e),
}
}
}
}
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
#[pymodule]
fn shlesha(m: &Bound<'_, PyModule>) -> PyResult<()> {
python_bindings::configure_module(m)
}