use super::{KeyBehavior, SyllableEditor};
use crate::{
input::{KeyboardEvent, keysym},
zhuyin::{Bopomofo, Syllable},
};
const MAX_PINYIN_LEN: usize = 10;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum PinyinVariant {
#[default]
HanyuPinyin,
ThlPinyin,
Mps2Pinyin,
}
#[derive(Default, Debug, Clone)]
pub struct Pinyin {
key_seq: String,
syllable: Syllable,
syllable_alt: Syllable,
variant: PinyinVariant,
}
impl Pinyin {
pub fn new() -> Pinyin {
Default::default()
}
pub fn hanyu() -> Pinyin {
Pinyin {
variant: PinyinVariant::HanyuPinyin,
..Default::default()
}
}
pub fn thl() -> Pinyin {
Pinyin {
variant: PinyinVariant::ThlPinyin,
..Default::default()
}
}
pub fn mps2() -> Pinyin {
Pinyin {
variant: PinyinVariant::Mps2Pinyin,
..Default::default()
}
}
pub fn alt(&self) -> Syllable {
self.syllable_alt
}
pub fn key_seq(&self) -> &String {
&self.key_seq
}
}
impl SyllableEditor for Pinyin {
fn key_press(&mut self, key: KeyboardEvent) -> KeyBehavior {
let ksym = &key.ksym;
if self.key_seq.is_empty() && !ksym.is_atoz() {
return KeyBehavior::KeyError;
}
if ![
keysym::SYM_SPACE,
keysym::SYM_1,
keysym::SYM_2,
keysym::SYM_3,
keysym::SYM_4,
keysym::SYM_5,
]
.contains(ksym)
{
if self.key_seq.len() == MAX_PINYIN_LEN {
return KeyBehavior::NoWord;
}
if !ksym.is_ascii() {
return KeyBehavior::KeyError;
}
self.key_seq.push(ksym.to_unicode());
return KeyBehavior::Absorb;
}
let tone = match ksym.to_digit() {
Some(2) => Some(Bopomofo::TONE2),
Some(3) => Some(Bopomofo::TONE3),
Some(4) => Some(Bopomofo::TONE4),
Some(5) => Some(Bopomofo::TONE5),
_ => None,
};
if let Some(entry) = match self.variant {
PinyinVariant::HanyuPinyin => table::HANYU_PINYIN_MAPPING.iter(),
PinyinVariant::ThlPinyin => table::THL_PINYIN_MAPPING.iter(),
PinyinVariant::Mps2Pinyin => table::MPS2_PINYIN_MAPPING.iter(),
}
.find(|entry| entry.pinyin == self.key_seq)
{
self.key_seq.clear();
self.syllable = entry.primary;
self.syllable_alt = entry.alt;
if let Some(tone) = tone {
self.syllable.update(tone);
self.syllable_alt.update(tone);
}
return KeyBehavior::Commit;
}
if let Some(entry) = table::COMMON_MAPPING
.iter()
.find(|entry| entry.pinyin == self.key_seq)
{
self.key_seq.clear();
self.syllable = entry.primary;
self.syllable_alt = entry.alt;
if let Some(tone) = tone {
self.syllable.update(tone);
self.syllable_alt.update(tone);
}
return KeyBehavior::Commit;
}
let initial = table::INITIAL_MAPPING
.iter()
.find(|entry| self.key_seq.starts_with(entry.pinyin));
let final_seq = match initial {
Some(entry) => self.key_seq.trim_start_matches(entry.pinyin),
None => &self.key_seq,
};
let fina = table::FINAL_MAPPING
.iter()
.find(|entry| final_seq == entry.pinyin);
if initial.is_none() && fina.is_none() {
self.key_seq.clear();
return KeyBehavior::Absorb;
}
let mut initial = initial.map(|i| i.initial);
let mut medial = fina.and_then(|f| f.medial);
let mut rime = fina.and_then(|f| f.rime);
if self.variant == PinyinVariant::HanyuPinyin
&& matches!(
(medial, rime),
(Some(Bopomofo::I), None) | (None, Some(Bopomofo::I))
)
{
match initial {
Some(Bopomofo::ZH) | Some(Bopomofo::CH) | Some(Bopomofo::SH)
| Some(Bopomofo::R) | Some(Bopomofo::Z) | Some(Bopomofo::C) | Some(Bopomofo::S) => {
medial.take();
rime.take();
}
_ => (),
}
}
if self.variant == PinyinVariant::HanyuPinyin {
match initial {
Some(Bopomofo::J) | Some(Bopomofo::Q) | Some(Bopomofo::X) => {
match (medial, rime) {
(Some(Bopomofo::U), Some(Bopomofo::AN))
| (Some(Bopomofo::U), Some(Bopomofo::EN))
| (Some(Bopomofo::U), None) => {
medial.replace(Bopomofo::IU);
}
_ => (),
};
}
_ => (),
}
}
match self.variant {
PinyinVariant::ThlPinyin | PinyinVariant::Mps2Pinyin => match medial {
Some(Bopomofo::I) | Some(Bopomofo::IU) => {
match initial {
Some(Bopomofo::S) | Some(Bopomofo::SH) => {
initial.replace(Bopomofo::X);
}
Some(Bopomofo::C) | Some(Bopomofo::CH) => {
initial.replace(Bopomofo::Q);
}
_ => (),
};
}
_ => {
if initial == Some(Bopomofo::J) {
initial.replace(Bopomofo::ZH);
}
}
},
PinyinVariant::HanyuPinyin => {}
}
match self.variant {
PinyinVariant::ThlPinyin | PinyinVariant::Mps2Pinyin => match initial {
Some(Bopomofo::B) | Some(Bopomofo::P) | Some(Bopomofo::M) | Some(Bopomofo::F) => {
match (medial, rime) {
(Some(Bopomofo::U), Some(Bopomofo::ENG))
| (Some(Bopomofo::U), Some(Bopomofo::O)) => {
medial.take();
}
_ => (),
};
}
_ => (),
},
_ => {}
}
self.key_seq.clear();
let mut builder = Syllable::builder();
if let Some(initial) = initial {
builder = builder.insert(initial).unwrap();
}
if let Some(medial) = medial {
builder = builder.insert(medial).unwrap();
}
if let Some(rime) = rime {
builder = builder.insert(rime).unwrap();
}
if let Some(tone) = tone {
builder = builder.insert(tone).unwrap();
}
self.syllable = builder.build();
self.syllable_alt = self.syllable;
KeyBehavior::Commit
}
fn fuzzy_key_press(&mut self, key: KeyboardEvent) -> KeyBehavior {
self.key_press(key)
}
fn is_empty(&self) -> bool {
self.key_seq.is_empty()
}
fn remove_last(&mut self) {
self.key_seq.pop();
}
fn clear(&mut self) {
self.key_seq.clear();
self.syllable.clear();
self.syllable_alt.clear();
}
fn read(&self) -> Syllable {
self.syllable
}
fn key_seq(&self) -> Option<String> {
Some(self.key_seq.clone())
}
fn clone(&self) -> Box<dyn SyllableEditor> {
Box::new(Clone::clone(self))
}
}
struct AmbiguousMapEntry {
pinyin: &'static str,
primary: Syllable,
alt: Syllable,
}
macro_rules! amb {
($pinyin:expr, $primary:expr, $alt:expr ) => {
AmbiguousMapEntry {
pinyin: $pinyin,
primary: $primary,
alt: $alt,
}
};
}
struct InitialMapEntry {
pinyin: &'static str,
initial: Bopomofo,
}
macro_rules! ini {
($pinyin:expr, $bopomofo:expr) => {
InitialMapEntry {
pinyin: $pinyin,
initial: $bopomofo,
}
};
}
struct FinalMapEntry {
pinyin: &'static str,
medial: Option<Bopomofo>,
rime: Option<Bopomofo>,
}
macro_rules! fin {
($pinyin:expr, $medial:expr, $rime:expr) => {
FinalMapEntry {
pinyin: $pinyin,
medial: $medial,
rime: $rime,
}
};
}
mod table {
use super::{AmbiguousMapEntry, FinalMapEntry, InitialMapEntry};
use crate::{syl, zhuyin::Bopomofo::*};
pub(super) const COMMON_MAPPING: [AmbiguousMapEntry; 18] = [
amb!("tzu", syl![Z], syl![Z, U]),
amb!("ssu", syl![S], syl![S, U]),
amb!("szu", syl![S], syl![S, U]),
amb!("e", syl![E], syl![EH]),
amb!("ch", syl![CH], syl![Q]),
amb!("sh", syl![SH], syl![X]),
amb!("c", syl![C], syl![Q]),
amb!("s", syl![S], syl![X]),
amb!("nu", syl![N, U], syl![N, IU]),
amb!("lu", syl![L, U], syl![L, IU]),
amb!("luan", syl![L, U, AN], syl![L, IU, AN]),
amb!("niu", syl![N, I, OU], syl![N, IU]),
amb!("liu", syl![L, I, OU], syl![L, IU]),
amb!("jiu", syl![J, I, OU], syl![J, IU]),
amb!("chiu", syl![Q, I, OU], syl![Q, IU]),
amb!("shiu", syl![X, I, OU], syl![X, IU]),
amb!("ju", syl![J, IU], syl![ZH, U]),
amb!("juan", syl![J, IU, AN], syl![ZH, U, AN]),
];
pub(super) const HANYU_PINYIN_MAPPING: [AmbiguousMapEntry; 4] = [
amb!("chi", syl![CH], syl![Q, I]),
amb!("shi", syl![SH], syl![X, I]),
amb!("ci", syl![C], syl![Q, I]),
amb!("si", syl![S], syl![X, I]),
];
pub(super) const THL_PINYIN_MAPPING: [AmbiguousMapEntry; 4] = [
amb!("chi", syl![Q, I], syl![CH]),
amb!("shi", syl![X, I], syl![SH]),
amb!("ci", syl![Q, I], syl![C]),
amb!("si", syl![X, I], syl![S]),
];
pub(super) const MPS2_PINYIN_MAPPING: [AmbiguousMapEntry; 13] = [
amb!("chi", syl![Q, I], syl![CH]),
amb!("shi", syl![X, I], syl![SH]),
amb!("ci", syl![Q, I], syl![C]),
amb!("si", syl![X, I], syl![S]),
amb!("niu", syl![N, IU], syl![N, I, OU]),
amb!("liu", syl![L, IU], syl![L, I, OU]),
amb!("jiu", syl![J, IU], syl![J, I, OU]),
amb!("chiu", syl![Q, IU], syl![Q, I, OU]),
amb!("shiu", syl![X, IU], syl![X, I, OU]),
amb!("ju", syl![ZH, U], syl![J, IU]),
amb!("juan", syl![ZH, U, AN], syl![J, IU, AN]),
amb!("juen", syl![ZH, U, EN], syl![J, IU, EN]),
amb!("tzu", syl![Z, U], syl![Z]),
];
pub(super) const INITIAL_MAPPING: [InitialMapEntry; 25] = [
ini!("tz", Z),
ini!("b", B),
ini!("p", P),
ini!("m", M),
ini!("f", F),
ini!("d", D),
ini!("ts", C),
ini!("t", T),
ini!("n", N),
ini!("l", L),
ini!("g", G),
ini!("k", K),
ini!("hs", X),
ini!("h", H),
ini!("jh", ZH),
ini!("j", J),
ini!("q", Q),
ini!("x", X),
ini!("zh", ZH),
ini!("ch", CH),
ini!("sh", SH),
ini!("r", R),
ini!("z", Z),
ini!("c", C),
ini!("s", S),
];
pub(super) const FINAL_MAPPING: [FinalMapEntry; 90] = [
fin!("uang", Some(U), Some(ANG)),
fin!("wang", Some(U), Some(ANG)),
fin!("weng", Some(U), Some(ENG)),
fin!("wong", Some(U), Some(ENG)),
fin!("ying", Some(I), Some(ENG)),
fin!("yung", Some(IU), Some(ENG)),
fin!("yong", Some(IU), Some(ENG)),
fin!("iung", Some(IU), Some(ENG)),
fin!("iong", Some(IU), Some(ENG)),
fin!("iang", Some(I), Some(ANG)),
fin!("yang", Some(I), Some(ANG)),
fin!("yuan", Some(IU), Some(AN)),
fin!("iuan", Some(IU), Some(AN)),
fin!("ing", Some(I), Some(ENG)),
fin!("iao", Some(I), Some(AU)),
fin!("iau", Some(I), Some(AU)),
fin!("yao", Some(I), Some(AU)),
fin!("yau", Some(I), Some(AU)),
fin!("yun", Some(IU), Some(EN)),
fin!("iun", Some(IU), Some(EN)),
fin!("vn", Some(IU), Some(EN)),
fin!("iou", Some(I), Some(OU)),
fin!("iu", Some(I), Some(OU)),
fin!("you", Some(I), Some(OU)),
fin!("io", Some(I), Some(O)),
fin!("yo", Some(I), Some(O)),
fin!("ian", Some(I), Some(AN)),
fin!("ien", Some(I), Some(AN)),
fin!("yan", Some(I), Some(AN)),
fin!("yen", Some(I), Some(AN)),
fin!("yin", Some(I), Some(EN)),
fin!("ang", None, Some(ANG)),
fin!("eng", None, Some(ENG)),
fin!("uei", Some(U), Some(EI)),
fin!("ui", Some(U), Some(EI)),
fin!("wei", Some(U), Some(EI)),
fin!("uen", Some(U), Some(EN)),
fin!("yueh", Some(IU), Some(EH)),
fin!("yue", Some(IU), Some(EH)),
fin!("iue", Some(IU), Some(EH)),
fin!("ueh", Some(IU), Some(EH)),
fin!("ue", Some(IU), Some(EH)),
fin!("ve", Some(IU), Some(EH)),
fin!("uai", Some(U), Some(AI)),
fin!("wai", Some(U), Some(AI)),
fin!("uan", Some(U), Some(AN)),
fin!("wan", Some(U), Some(AN)),
fin!("un", Some(U), Some(EN)),
fin!("wen", Some(U), Some(EN)),
fin!("wun", Some(U), Some(EN)),
fin!("ung", Some(U), Some(ENG)),
fin!("ong", Some(U), Some(ENG)),
fin!("van", Some(IU), Some(AN)),
fin!("er", None, Some(ER)),
fin!("ai", None, Some(AI)),
fin!("ei", None, Some(EI)),
fin!("ao", None, Some(AU)),
fin!("au", None, Some(AU)),
fin!("ou", None, Some(OU)),
fin!("an", None, Some(AN)),
fin!("en", None, Some(EN)),
fin!("yi", None, Some(I)),
fin!("ia", Some(I), Some(A)),
fin!("ya", Some(I), Some(A)),
fin!("ieh", Some(I), Some(EH)),
fin!("ie", Some(I), Some(EH)),
fin!("yeh", Some(I), Some(EH)),
fin!("ye", Some(I), Some(EH)),
fin!("in", Some(I), Some(EN)),
fin!("wu", Some(U), None),
fin!("ua", Some(U), Some(A)),
fin!("wa", Some(U), Some(A)),
fin!("uo", Some(U), Some(O)),
fin!("wo", Some(U), Some(O)),
fin!("yu", Some(IU), None),
fin!("ve", Some(IU), Some(EH)),
fin!("vn", Some(IU), Some(EN)),
fin!("ih", None, None),
fin!("a", None, Some(A)),
fin!("o", None, Some(O)),
fin!("eh", None, Some(EH)),
fin!("e", None, Some(E)),
fin!("v", Some(IU), None),
fin!("i", Some(I), None),
fin!("u", Some(U), None),
fin!("E", None, Some(EH)),
fin!("n", None, Some(EN)),
fin!("ng", None, Some(ENG)),
fin!("r", None, None),
fin!("z", None, None),
];
}
#[cfg(test)]
mod tests {
use super::Pinyin;
use crate::{
editor::zhuyin_layout::SyllableEditor,
input::{
KeyboardEvent,
keysym::{self, Keysym},
},
syl,
zhuyin::Bopomofo,
};
fn map_key(ksym: Keysym) -> KeyboardEvent {
KeyboardEvent::builder().ksym(ksym).build()
}
#[test]
fn hanyu_empty_rime_zi() {
let mut hanyu = Pinyin::hanyu();
hanyu.key_press(map_key(keysym::SYM_LOWER_Z));
hanyu.key_press(map_key(keysym::SYM_LOWER_I));
hanyu.key_press(map_key(keysym::SYM_1));
assert_eq!(syl![Bopomofo::Z], hanyu.read());
}
#[test]
fn hanyu_empty_rime_zhi() {
let mut hanyu = Pinyin::hanyu();
hanyu.key_press(map_key(keysym::SYM_LOWER_Z));
hanyu.key_press(map_key(keysym::SYM_LOWER_H));
hanyu.key_press(map_key(keysym::SYM_LOWER_I));
hanyu.key_press(map_key(keysym::SYM_1));
assert_eq!(syl![Bopomofo::ZH], hanyu.read());
}
#[test]
fn hanyu_uan_un_u() {
let mut hanyu = Pinyin::hanyu();
hanyu.key_press(map_key(Keysym::from('j')));
hanyu.key_press(map_key(Keysym::from('u')));
hanyu.key_press(map_key(Keysym::from('a')));
hanyu.key_press(map_key(Keysym::from('n')));
hanyu.key_press(map_key(Keysym::from('1')));
assert_eq!(syl![Bopomofo::J, Bopomofo::IU, Bopomofo::AN], hanyu.read());
hanyu.clear();
hanyu.key_press(map_key(Keysym::from('q')));
hanyu.key_press(map_key(Keysym::from('u')));
hanyu.key_press(map_key(Keysym::from('n')));
hanyu.key_press(map_key(Keysym::from('1')));
assert_eq!(syl![Bopomofo::Q, Bopomofo::IU, Bopomofo::EN], hanyu.read());
hanyu.clear();
hanyu.key_press(map_key(Keysym::from('x')));
hanyu.key_press(map_key(Keysym::from('u')));
hanyu.key_press(map_key(Keysym::from('1')));
assert_eq!(syl![Bopomofo::X, Bopomofo::IU], hanyu.read());
}
}