use std::sync::OnceLock;
use crate::types::{QueryRequest, SchemeKey, SchemeType};
use ewts::EwtsConverter;
const SPELLING_SYMBOLS_IDLE: &str = "'/";
const SPELLING_SYMBOLS_COMPOSING: &str = "'+-./";
const EWTS_UPPERCASE: &[u8] = b"ADHIMNRSTUWXY";
fn ewts_letter(letter: u8) -> bool {
(letter.is_ascii_lowercase() && !matches!(letter, b'q' | b'x'))
|| EWTS_UPPERCASE.contains(&letter)
}
fn converter() -> &'static EwtsConverter {
static CONVERTER: OnceLock<EwtsConverter> = OnceLock::new();
CONVERTER.get_or_init(EwtsConverter::create)
}
#[derive(Debug, Clone, Default)]
pub struct TibetanScheme {
raw: String,
raw_locked: bool,
}
impl TibetanScheme {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.raw.clear();
self.raw_locked = false;
}
pub fn is_composing(&self) -> bool {
!self.raw.is_empty()
}
pub fn raw_locked(&self) -> bool {
self.raw_locked
}
pub fn handle_key(&mut self, key: SchemeKey) -> bool {
match key {
SchemeKey::Letter(letter) if self.claims_letter(letter) => {
self.raw.push(char::from(letter));
true
}
SchemeKey::Symbol(symbol) if self.claims_symbol(symbol) => {
self.raw.push(char::from(symbol));
true
}
SchemeKey::Backspace => {
let changed = self.raw.pop().is_some();
if self.raw.is_empty() {
self.raw_locked = false;
}
changed
}
SchemeKey::Letter(_)
| SchemeKey::Symbol(_)
| SchemeKey::Apostrophe
| SchemeKey::Semicolon
| SchemeKey::Minus
| SchemeKey::Requery => false,
}
}
pub fn restore_raw(&mut self) -> bool {
if self.raw.is_empty() || self.raw_locked {
return false;
}
self.raw_locked = true;
true
}
pub fn spelling_symbols(&self) -> &'static str {
if self.is_composing() {
SPELLING_SYMBOLS_COMPOSING
} else {
SPELLING_SYMBOLS_IDLE
}
}
pub fn preedit(&self) -> String {
let mut display = String::new();
self.preedit_into(&mut display);
display
}
pub fn preedit_into(&self, display: &mut String) {
display.clear();
if self.raw_locked {
display.push_str(&self.raw);
return;
}
let converted = if self.raw.contains("kSh") {
converter().ewts_to_unicode(&self.raw.replace("kSh", "k+Sh"))
} else {
converter().ewts_to_unicode(&self.raw)
};
if converted.is_empty() {
display.push_str(&self.raw);
return;
}
display.push_str(&converted);
}
pub fn build_request(&self) -> QueryRequest {
let mut request = QueryRequest::default();
self.build_request_into(&mut request);
request
}
pub fn build_request_into(&self, request: &mut QueryRequest) {
request.scheme = SchemeType::Tibetan;
request.raw_input.clear();
request.raw_input.extend(
self.raw
.bytes()
.map(|byte| char::from(byte.to_ascii_lowercase())),
);
request.raw_input_with_cases.clone_from(&self.raw);
request.normalized_input.clone_from(&request.raw_input);
request.raw_segmentation.clone_from(&self.raw);
self.preedit_into(&mut request.normalized_segmentation);
request
.segmentation
.clone_from(&request.normalized_segmentation);
request.valid = self.is_composing();
}
pub fn set_raw_input(&mut self, raw: &str, raw_with_cases: &str) {
let source = if raw_with_cases.is_empty() {
raw
} else {
raw_with_cases
};
self.reset();
self.raw.reserve(source.len());
for key in source.bytes() {
if self.claims_letter(key) || self.claims_symbol(key) {
self.raw.push(char::from(key));
}
}
}
pub fn claims_letter(&self, letter: u8) -> bool {
if self.raw_locked {
letter.is_ascii_alphabetic()
} else {
ewts_letter(letter)
}
}
fn claims_symbol(&self, key: u8) -> bool {
key == b'\'' || (self.is_composing() && matches!(key, b'+' | b'.' | b'-'))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn typed(keys: &str) -> TibetanScheme {
let mut scheme = TibetanScheme::new();
for key in keys.bytes() {
scheme.handle_key(if key.is_ascii_alphabetic() {
SchemeKey::Letter(key)
} else {
SchemeKey::Symbol(key)
});
}
scheme
}
fn wylie(keys: &str) -> String {
typed(keys).preedit()
}
#[test]
fn syllables_convert_to_tibetan() {
let cases = [
("bkra", "བཀྲ"),
("shis", "ཤིས"),
("sangs", "སངས"),
("rgyas", "རྒྱས"),
("bsgrubs", "བསྒྲུབས"),
("'od", "འོད"),
("'gro", "འགྲོ"),
("g.yag", "གཡག"),
("gyag", "གྱག"),
("pa'i", "པའི"),
];
for (keys, expected) in cases {
assert_eq!(wylie(keys), expected, "{keys}");
}
}
#[test]
fn plus_stacks_and_uppercase_spells_sanskrit_letters_and_long_vowels() {
let cases = [
("pad+ma", "པདྨ"),
("badz+ra", "བཛྲ"),
("Ta", "ཊ"),
("Da", "ཌ"),
("Na", "ཎ"),
("Sha", "ཥ"),
("kA", "ཀཱ"),
("kI", "ཀཱི"),
("kU", "ཀཱུ"),
("oM", "ཨོཾ"),
("aH", "ཨཿ"),
("k-i", "ཀྀ"),
("kSha", "ཀྵ"),
("k+Sha", "ཀྵ"),
("lakSh+mI", "ལཀྵྨཱི"),
];
for (keys, expected) in cases {
assert_eq!(wylie(keys), expected, "{keys}");
}
}
#[test]
fn letters_ewts_cannot_spell_are_not_claimed() {
let scheme = typed("BOD");
assert_eq!(scheme.raw, "D");
assert_eq!(scheme.preedit(), "ཌ");
for letter in b'a'..=b'z' {
assert_eq!(
scheme.claims_letter(letter),
!matches!(letter, b'q' | b'x'),
"{}",
char::from(letter)
);
}
for letter in b'A'..=b'Z' {
let claimed = scheme.claims_letter(letter);
assert_eq!(
claimed,
b"ADHIMNRSTUWXY".contains(&letter),
"{}",
char::from(letter)
);
if claimed {
let keys = if letter == b'S' {
"kSha".to_owned()
} else {
format!("k{}", char::from(letter))
};
assert!(
!wylie(&keys).chars().any(|c| c.is_ascii_alphabetic()),
"{keys}"
);
}
}
assert_eq!(typed("kq").raw, "k");
assert_eq!(typed("kx").raw, "k");
let mut scheme = typed("ka");
assert!(scheme.restore_raw());
scheme.handle_key(SchemeKey::Letter(b'Q'));
assert_eq!(scheme.preedit(), "kaQ");
}
#[test]
fn backspace_removes_one_wylie_keystroke() {
let mut scheme = typed("bkra");
let mut shown = vec![scheme.preedit()];
while scheme.is_composing() {
scheme.handle_key(SchemeKey::Backspace);
shown.push(scheme.preedit());
}
assert_eq!(shown, ["བཀྲ", "བཀྲ", "བཀ", "བ", ""]);
let mut scheme = typed("bkra");
scheme.handle_key(SchemeKey::Backspace);
assert_eq!(scheme.raw, "bkr");
}
#[test]
fn symbols_other_than_the_apostrophe_spell_only_while_composing() {
let mut scheme = TibetanScheme::new();
assert_eq!(scheme.spelling_symbols(), "'/");
for key in *b"+.-/" {
scheme.handle_key(SchemeKey::Symbol(key));
assert!(!scheme.is_composing(), "{}", char::from(key));
}
scheme.handle_key(SchemeKey::Symbol(b'\''));
assert_eq!(scheme.raw, "'");
assert_eq!(scheme.spelling_symbols(), "'+-./");
scheme.handle_key(SchemeKey::Symbol(b'/'));
assert_eq!(scheme.raw, "'");
}
#[test]
fn esc_locks_the_raw_keys_and_a_second_esc_is_left_to_the_session() {
let mut scheme = typed("bod");
assert_eq!(scheme.preedit(), "བོད");
assert!(scheme.restore_raw());
assert!(scheme.raw_locked());
assert_eq!(scheme.preedit(), "bod");
scheme.handle_key(SchemeKey::Letter(b's'));
assert_eq!(scheme.preedit(), "bods");
assert!(!scheme.restore_raw());
for _ in 0..4 {
scheme.handle_key(SchemeKey::Backspace);
}
assert!(!scheme.raw_locked());
scheme.handle_key(SchemeKey::Letter(b'k'));
assert_eq!(scheme.preedit(), "ཀ");
assert!(!TibetanScheme::new().restore_raw());
}
#[test]
fn other_keys_are_ignored() {
let mut scheme = typed("ka");
for key in [
SchemeKey::Apostrophe,
SchemeKey::Semicolon,
SchemeKey::Minus,
SchemeKey::Requery,
SchemeKey::Letter(b'1'),
SchemeKey::Symbol(b' '),
SchemeKey::Symbol(b'1'),
SchemeKey::Symbol(b';'),
] {
scheme.handle_key(key);
}
assert_eq!(scheme.raw, "ka");
}
#[test]
fn set_raw_input_prefers_the_cased_spelling_and_drops_the_lock() {
let mut scheme = typed("abc");
assert!(scheme.restore_raw());
scheme.set_raw_input("ta", "Ta 1/");
assert!(!scheme.raw_locked());
assert_eq!(scheme.raw, "Ta");
assert_eq!(scheme.preedit(), "ཊ");
scheme.set_raw_input("+.pad+ma", "");
assert_eq!(scheme.raw, "pad+ma");
assert_eq!(scheme.preedit(), "པདྨ");
scheme.set_raw_input("", "BOD");
assert_eq!(scheme.raw, "D");
}
#[test]
fn set_raw_input_reserves_source_capacity() {
let source: String = (0..100)
.map(|index| if index % 5 == 3 { '_' } else { 'a' })
.collect();
let mut scheme = TibetanScheme::new();
scheme.set_raw_input(&source, "");
assert_eq!(scheme.raw.len(), 80);
assert_eq!(scheme.raw.capacity(), source.len());
}
#[test]
fn request_carries_the_keys_as_raw_input_and_the_display_as_segmentation() {
let request = typed("Sha").build_request();
assert_eq!(request.scheme, SchemeType::Tibetan);
assert_eq!(request.raw_input, "sha");
assert_eq!(request.raw_input_with_cases, "Sha");
assert_eq!(request.normalized_input, "sha");
assert_eq!(request.raw_segmentation, "Sha");
assert_eq!(request.segmentation, "ཥ");
assert_eq!(request.normalized_segmentation, "ཥ");
assert!(request.valid);
assert!(!TibetanScheme::default().build_request().valid);
}
#[test]
fn a_second_consonant_that_may_be_a_subscript_converts_in_every_build() {
for (keys, expected) in [("bw", "བཝ"), ("yw", "ཡཝ"), ("bwa", "བཝ")] {
assert_eq!(wylie(keys), expected, "{keys}");
}
}
}