├── CMakeLists.txt ├── src ├── phonetic │ ├── mod.rs │ ├── method.rs │ └── suggestion.rs ├── fixed │ ├── mod.rs │ ├── chars.rs │ └── method.rs ├── lib.rs ├── data.rs ├── context.rs ├── keycodes.rs ├── config.rs ├── suggestion.rs ├── utility.rs └── ffi.rs ├── README.md ├── cbindgen.toml ├── .gitignore ├── Cargo.toml ├── .github └── workflows │ └── main.yml ├── data ├── Probhat.json └── suffix.json ├── include └── riti.h ├── riti.xcodeproj └── project.pbxproj └── LICENSE /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cargo_build(NAME riti) 2 | -------------------------------------------------------------------------------- /src/phonetic/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod method; 2 | mod suggestion; 3 | -------------------------------------------------------------------------------- /src/fixed/mod.rs: -------------------------------------------------------------------------------- 1 | mod chars; 2 | mod layout; 3 | pub(crate) mod method; 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "bench", feature(test))] 2 | 3 | pub mod config; 4 | pub mod context; 5 | pub(crate) mod data; 6 | mod ffi; 7 | mod fixed; 8 | pub mod keycodes; 9 | mod phonetic; 10 | pub mod suggestion; 11 | mod utility; 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # riti (রীতি) 2 | The Next Gen Bengali input method library written in Rust. It is used by [OpenBangla Keyboard](https://github.com/OpenBangla/OpenBangla-Keyboard). 3 | 4 | [![Build Status](https://github.com/OpenBangla/riti/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/OpenBangla/riti/actions?query=branch%3Amaster) 5 | [![Rust](https://img.shields.io/badge/rust-1.63.0%2B-blue.svg?maxAge=3600)](https://github.com/OpenBangla/riti) -------------------------------------------------------------------------------- /cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C" 2 | header = "/* Text to put at the beginning of the generated file. Probably a license. */" 3 | include_guard = "RITI_H" 4 | autogen_warning = """ 5 | /* 6 | * Warning, this file is autogenerated by cbindgen. Don't modify this manually. 7 | * Run this command to generate this file: cbindgen --config cbindgen.toml --output include/riti.h 8 | */ 9 | """ 10 | include_version = true 11 | line_length = 80 12 | tab_width = 4 13 | documentation_style = "c" 14 | style = "both" 15 | cpp_compat = true 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | ### VisualStudioCode template 13 | .vscode/* 14 | !.vscode/settings.json 15 | !.vscode/tasks.json 16 | !.vscode/launch.json 17 | !.vscode/extensions.json 18 | 19 | ### XCode 20 | *~ 21 | *.DS_Store 22 | .*.swp 23 | *mode1v3 24 | *pbxuser 25 | *perspectivev3 26 | *xcuserdata* 27 | *xcworkspace* 28 | 29 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "riti" 3 | version = "0.1.0" 4 | authors = ["Muhammad Mominul Huque "] 5 | license = "MPL-2.0" 6 | edition = "2021" 7 | rust-version = "1.63.0" 8 | 9 | [lib] 10 | crate-type = ["lib", "staticlib"] 11 | 12 | [features] 13 | bench = [] 14 | 15 | [profile.release] 16 | codegen-units = 1 17 | 18 | [dependencies] 19 | ahash = { version = "0.8", features = ["serde"] } 20 | emojicon = { version = "0.5", features = ["custom"] } 21 | serde_json = "1.0" 22 | regex = "~1.9" # For maintaining MSRV 23 | stringplus = "0.1" 24 | edit-distance = "2.1" 25 | okkhor = "0.7" 26 | poriborton = "0.2" 27 | upodesh = "0.4" 28 | 29 | [dev-dependencies] 30 | rustversion = "1.0" 31 | pretty_assertions = "1.4" 32 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | # Make sure CI fails on all warnings, including Clippy lints 6 | env: 7 | RUSTFLAGS: "-Dwarnings" 8 | 9 | jobs: 10 | test: 11 | name: Test Suite 12 | strategy: 13 | matrix: 14 | rust: 15 | - stable 16 | - 1.63.0 # MSRV for linux distributions (>= Debian 12) 17 | # - nightly # To keep the MSRV, we can't test with nightly as some downgraded crates fails to compile 18 | platform: [ubuntu-latest, windows-latest, macos-latest] 19 | runs-on: ${{ matrix.platform }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: dtolnay/rust-toolchain@master 23 | with: 24 | toolchain: ${{ matrix.rust }} 25 | - run: cargo test 26 | 27 | lint: 28 | name: Clippy Linting 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: dtolnay/rust-toolchain@stable 33 | with: 34 | components: "clippy" 35 | - run: cargo clippy 36 | -------------------------------------------------------------------------------- /src/data.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fs::read, path::PathBuf}; 2 | 3 | use ahash::RandomState; 4 | use emojicon::{BengaliEmoji, Emojicon}; 5 | 6 | use crate::config::Config; 7 | 8 | /// Data which is shared between the methods. 9 | pub(crate) struct Data { 10 | suffix: HashMap, 11 | autocorrect: HashMap, 12 | emojicon: Emojicon, 13 | bengali_emoji: BengaliEmoji, 14 | } 15 | 16 | impl Data { 17 | pub(crate) fn new(config: &Config) -> Data { 18 | // If the database directory is not set, initialize with empty values. 19 | if *config.get_database_dir() == PathBuf::default() { 20 | Data { 21 | suffix: HashMap::default(), 22 | autocorrect: HashMap::default(), 23 | emojicon: Emojicon::new(), 24 | bengali_emoji: BengaliEmoji::new(), 25 | } 26 | } else { 27 | Data { 28 | suffix: serde_json::from_slice(&read(config.get_suffix_data_path()).unwrap()) 29 | .unwrap(), 30 | autocorrect: serde_json::from_slice(&read(config.get_autocorrect_data()).unwrap()) 31 | .unwrap(), 32 | emojicon: Emojicon::new(), 33 | bengali_emoji: BengaliEmoji::new(), 34 | } 35 | } 36 | } 37 | 38 | pub(crate) fn find_suffix(&self, string: &str) -> Option<&str> { 39 | self.suffix.get(string).map(String::as_str) 40 | } 41 | 42 | /// Search for a `term` in the AutoCorrect dictionary. 43 | pub(crate) fn search_corrected(&self, term: &str) -> Option<&str> { 44 | self.autocorrect.get(term).map(String::as_str) 45 | } 46 | 47 | pub(crate) fn get_emoji_by_emoticon(&self, emoticon: &str) -> Option<&str> { 48 | self.emojicon.get_by_emoticon(emoticon) 49 | } 50 | 51 | pub(crate) fn get_emoji_by_name(&self, name: &str) -> Option> { 52 | self.emojicon.get_by_name(name) 53 | } 54 | 55 | pub(crate) fn get_emoji_by_bengali(&self, name: &str) -> Option> { 56 | self.bengali_emoji.get(name) 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use super::Data; 63 | use crate::config::get_phonetic_method_defaults; 64 | 65 | #[test] 66 | fn test_suffix() { 67 | let config = get_phonetic_method_defaults(); 68 | let db = Data::new(&config); 69 | 70 | assert_eq!(db.find_suffix("gulo"), Some("গুলো")); 71 | assert_eq!(db.find_suffix("er"), Some("ের")); 72 | assert_eq!(db.find_suffix("h"), None); 73 | } 74 | 75 | #[test] 76 | fn test_autocorrect() { 77 | let config = get_phonetic_method_defaults(); 78 | let db = Data::new(&config); 79 | 80 | assert_eq!(db.search_corrected("academy"), Some("oZakaDemi")); 81 | assert_eq!(db.search_corrected("\\nai\\"), None); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use crate::phonetic::method::PhoneticMethod; 4 | use crate::suggestion::Suggestion; 5 | use crate::{config::Config, data::Data, fixed::method::FixedMethod}; 6 | 7 | /// Context handle used for libRiti IM APIs 8 | pub struct RitiContext { 9 | method: RefCell>, 10 | config: Config, 11 | data: Data, 12 | } 13 | 14 | impl RitiContext { 15 | /// A new `RitiContext` instance. 16 | pub fn new_with_config(config: &Config) -> Self { 17 | let config = config.to_owned(); 18 | let data = Data::new(&config); 19 | let method = RefCell::new(::new(&config)); 20 | RitiContext { 21 | method, 22 | config, 23 | data, 24 | } 25 | } 26 | 27 | /// Get suggestion for key. 28 | pub fn get_suggestion_for_key(&self, key: u16, modifier: u8, selection: u8) -> Suggestion { 29 | self.method 30 | .borrow_mut() 31 | .get_suggestion(key, modifier, selection, &self.data, &self.config) 32 | } 33 | 34 | /// A candidate of the suggestion list was committed. 35 | /// 36 | /// `index`: index of the candidate. 37 | /// 38 | /// This function will end the ongoing input session. 39 | pub fn candidate_committed(&self, index: usize) { 40 | self.method 41 | .borrow_mut() 42 | .candidate_committed(index, &self.config) 43 | } 44 | 45 | /// Update the suggestion making engine. This would also look for changes 46 | /// in layout selection and AutoCorrect database. 47 | pub fn update_engine(&mut self, config: &Config) { 48 | // If the layout file has been changed. 49 | if self.config.layout_changed(config) { 50 | self.method.replace(::new(config)); 51 | } else { 52 | self.method.borrow_mut().update_engine(config); 53 | } 54 | 55 | // Update the config 56 | self.config = config.to_owned(); 57 | } 58 | 59 | /// Checks if there is an onging input session. 60 | pub fn ongoing_input_session(&self) -> bool { 61 | self.method.borrow().ongoing_input_session() 62 | } 63 | 64 | /// Finish the ongoing input session if any. 65 | pub fn finish_input_session(&self) { 66 | self.method.borrow_mut().finish_input_session(); 67 | } 68 | 69 | /// A BackSpace event. 70 | /// 71 | /// Returns a new `suggestion` after applying the BackSpace event. 72 | /// 73 | /// If the `ctrl` parameter is true then it deletes the whole word 74 | /// in composition currently and ends the ongoing input session. 75 | /// 76 | /// If the internal buffer becomes empty, this function will 77 | /// end the ongoing input session. 78 | pub fn backspace_event(&self, ctrl: bool) -> Suggestion { 79 | self.method 80 | .borrow_mut() 81 | .backspace_event(ctrl, &self.data, &self.config) 82 | } 83 | } 84 | 85 | pub(crate) trait Method { 86 | fn get_suggestion( 87 | &mut self, 88 | key: u16, 89 | modifier: u8, 90 | selection: u8, 91 | data: &Data, 92 | config: &Config, 93 | ) -> Suggestion; 94 | fn candidate_committed(&mut self, index: usize, config: &Config); 95 | fn update_engine(&mut self, config: &Config); 96 | fn ongoing_input_session(&self) -> bool; 97 | fn finish_input_session(&mut self); 98 | fn backspace_event(&mut self, ctrl: bool, data: &Data, config: &Config) -> Suggestion; 99 | } 100 | 101 | impl dyn Method { 102 | fn new(config: &Config) -> Box { 103 | if config.is_phonetic() { 104 | Box::new(PhoneticMethod::new(config)) 105 | } else { 106 | Box::new(FixedMethod::new(config)) 107 | } 108 | } 109 | } 110 | 111 | /// Shift modifier key. 112 | /// 113 | /// Used by the [`get_suggestion_for_key()`](struct.RitiContext.html#method.get_suggestion_for_key) function. 114 | pub const MODIFIER_SHIFT: u8 = 1 << 0; 115 | /// AltGr modifier key. 116 | /// 117 | /// Used by the [`get_suggestion_for_key()`](struct.RitiContext.html#method.get_suggestion_for_key) function. 118 | pub const MODIFIER_ALT_GR: u8 = 1 << 1; 119 | 120 | #[cfg(test)] 121 | mod tests { 122 | use super::*; 123 | use crate::{ 124 | config::{get_fixed_method_defaults, get_phonetic_method_defaults}, 125 | keycodes::{VC_E, VC_H, VC_L, VC_P}, 126 | }; 127 | 128 | #[test] 129 | fn test_layout_change() { 130 | // Load the context with a Phonetic layout. 131 | let config = get_phonetic_method_defaults(); 132 | let mut context = RitiContext::new_with_config(&config); 133 | 134 | context.get_suggestion_for_key(VC_H, 0, 0); 135 | context.get_suggestion_for_key(VC_E, 0, 0); 136 | context.get_suggestion_for_key(VC_L, 0, 0); 137 | let suggestion = context.get_suggestion_for_key(VC_P, 0, 0); 138 | context.finish_input_session(); 139 | assert_eq!(suggestion.get_suggestions(), ["হেল্প", "🆘"]); 140 | 141 | // Change the layout to Fixed layout 142 | let config = get_fixed_method_defaults(); 143 | context.update_engine(&config); 144 | 145 | context.get_suggestion_for_key(VC_H, 0, 0); 146 | context.get_suggestion_for_key(VC_E, 0, 0); 147 | context.get_suggestion_for_key(VC_L, 0, 0); 148 | let suggestion = context.get_suggestion_for_key(VC_P, 0, 0); 149 | context.finish_input_session(); 150 | assert_eq!(suggestion.get_suggestions(), ["হীলপ"]); 151 | } 152 | 153 | #[test] 154 | fn test_without_database_path() { 155 | let mut config = Config::default(); 156 | config.set_layout_file_path("avro_phonetic"); 157 | config.set_phonetic_suggestion(true); 158 | 159 | let context = RitiContext::new_with_config(&config); 160 | 161 | let suggestion = context.get_suggestion_for_key(VC_H, 0, 0); 162 | assert_eq!(suggestion.get_suggestions(), ["হ"]); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/fixed/chars.rs: -------------------------------------------------------------------------------- 1 | /* The Unicode Standard 8.0 2 | * http://www.unicode.org/charts/ 3 | * http://www.unicode.org/charts/PDF/U0980.pdf 4 | */ 5 | 6 | /*************************************************************** 7 | * The Bengali script is also known as Bangla. In Assam, the * 8 | * preferred name of the script is Asamiya or Assamese. The * 9 | * Assamese language has also been written historically using * 10 | * distinct regional scripts known as Kamrupi. * 11 | ***************************************************************/ 12 | 13 | /* We have changed some Character names according to Bangla language or phonetic equivalent. 14 | * Actual names are denoted by comments */ 15 | 16 | #![allow(dead_code)] 17 | 18 | /* Various signs */ 19 | pub(crate) const B_SIGN_ANJI: char = '\u{0980}'; 20 | pub(crate) const B_CHANDRA: char = '\u{0981}'; 21 | pub(crate) const B_ANUSHAR: char = '\u{0982}'; // BENGALI SIGN ANUSVARA 22 | pub(crate) const B_BISHARGA: char = '\u{0983}'; // BENGALI SIGN VISARGA 23 | 24 | /* Independent vowels */ 25 | pub(crate) const B_A: char = '\u{0985}'; 26 | pub(crate) const B_AA: char = '\u{0986}'; 27 | pub(crate) const B_I: char = '\u{0987}'; 28 | pub(crate) const B_II: char = '\u{0988}'; 29 | pub(crate) const B_U: char = '\u{0989}'; 30 | pub(crate) const B_UU: char = '\u{098A}'; 31 | pub(crate) const B_RRI: char = '\u{098B}'; // BENGALI LETTER VOCALIC R 32 | pub(crate) const B_VOCALIC_L: char = '\u{098C}'; 33 | pub(crate) const B_E: char = '\u{098F}'; 34 | pub(crate) const B_OI: char = '\u{0990}'; // BENGALI LETTER AI 35 | pub(crate) const B_O: char = '\u{0993}'; 36 | pub(crate) const B_OU: char = '\u{0994}'; 37 | 38 | /* Consonants */ 39 | pub(crate) const B_K: char = '\u{0995}'; 40 | pub(crate) const B_KH: char = '\u{0996}'; 41 | pub(crate) const B_G: char = '\u{0997}'; 42 | pub(crate) const B_GH: char = '\u{0998}'; 43 | pub(crate) const B_NGA: char = '\u{0999}'; 44 | pub(crate) const B_C: char = '\u{099A}'; 45 | pub(crate) const B_CH: char = '\u{099B}'; 46 | pub(crate) const B_J: char = '\u{099C}'; 47 | pub(crate) const B_JH: char = '\u{099D}'; 48 | pub(crate) const B_NYA: char = '\u{099E}'; 49 | pub(crate) const B_TT: char = '\u{099F}'; 50 | pub(crate) const B_TTH: char = '\u{09A0}'; 51 | pub(crate) const B_DD: char = '\u{09A1}'; 52 | pub(crate) const B_DDH: char = '\u{09A2}'; 53 | pub(crate) const B_NN: char = '\u{09A3}'; 54 | pub(crate) const B_T: char = '\u{09A4}'; 55 | pub(crate) const B_TH: char = '\u{09A5}'; 56 | pub(crate) const B_D: char = '\u{09A6}'; 57 | pub(crate) const B_DH: char = '\u{09A7}'; 58 | pub(crate) const B_N: char = '\u{09A8}'; 59 | pub(crate) const B_P: char = '\u{09AA}'; 60 | pub(crate) const B_PH: char = '\u{09AB}'; 61 | pub(crate) const B_B: char = '\u{09AC}'; 62 | pub(crate) const B_BH: char = '\u{09AD}'; 63 | pub(crate) const B_M: char = '\u{09AE}'; 64 | pub(crate) const B_Z: char = '\u{09AF}'; 65 | pub(crate) const B_R: char = '\u{09B0}'; 66 | pub(crate) const B_L: char = '\u{09B2}'; 67 | pub(crate) const B_SH: char = '\u{09B6}'; 68 | pub(crate) const B_SS: char = '\u{09B7}'; 69 | pub(crate) const B_S: char = '\u{09B8}'; 70 | pub(crate) const B_H: char = '\u{09B9}'; 71 | 72 | /* Various signs */ 73 | pub(crate) const B_SIGN_NUKTA: char = '\u{09BC}'; // for extending the alphabet to new letters 74 | pub(crate) const B_SIGN_AVAGRAHA: char = '\u{09BD}'; 75 | 76 | /* Dependent vowel signs (kars) */ 77 | pub(crate) const B_AA_KAR: char = '\u{09BE}'; 78 | pub(crate) const B_I_KAR: char = '\u{09BF}'; 79 | pub(crate) const B_II_KAR: char = '\u{09C0}'; 80 | pub(crate) const B_U_KAR: char = '\u{09C1}'; 81 | pub(crate) const B_UU_KAR: char = '\u{09C2}'; 82 | pub(crate) const B_RRI_KAR: char = '\u{09C3}'; // BENGALI VOWEL SIGN VOCALIC R 83 | pub(crate) const B_VOCALIC_RR: char = '\u{09C4}'; // BENGALI VOWEL SIGN VOCALIC RR 84 | pub(crate) const B_E_KAR: char = '\u{09C7}'; 85 | pub(crate) const B_OI_KAR: char = '\u{09C8}'; 86 | 87 | /* Two-part dependent vowel signs */ 88 | pub(crate) const B_O_KAR: char = '\u{09CB}'; 89 | pub(crate) const B_OU_KAR: char = '\u{09CC}'; // BENGALI VOWEL SIGN AU 90 | 91 | /* Virama or Hasant */ 92 | pub(crate) const B_HASANTA: char = '\u{09CD}'; 93 | 94 | /* Additional consonant */ 95 | pub(crate) const B_KHANDATTA: char = '\u{09CE}'; 96 | 97 | /* Sign */ 98 | pub(crate) const B_LENGTH_MARK: char = '\u{09D7}'; // BENGALI AU LENGTH MARK 99 | 100 | /* Additional consonants */ 101 | pub(crate) const B_RR: char = '\u{09DC}'; // BENGALI LETTER RRA 102 | pub(crate) const B_RH: char = '\u{09DD}'; // BENGALI LETTER RHA 103 | pub(crate) const B_Y: char = '\u{09DF}'; // BENGALI LETTER YYA 104 | 105 | /* Additional vowels for Sanskrit */ 106 | pub(crate) const B_SANSKRIT_RR: char = '\u{09E0}'; // BENGALI LETTER VOCALIC RR 107 | pub(crate) const B_SANSKRIT_LL: char = '\u{09E1}'; // BENGALI LETTER VOCALIC LL 108 | pub(crate) const B_SIGN_L: char = '\u{09E2}'; // BENGALI VOWEL SIGN VOCALIC L 109 | pub(crate) const B_SIGN_LL: char = '\u{09E3}'; // BENGALI VOWEL SIGN VOCALIC LL 110 | 111 | /* Reserved */ 112 | /**************************************************************** 113 | * For viram punctuation, use the generic Indic 0964 and 0965. * 114 | * Note that these punctuation marks are referred to as dahri * 115 | * and double dahri in Bangla. * 116 | ****************************************************************/ 117 | pub(crate) const B_DARI: char = '\u{0964}'; 118 | pub(crate) const B_DDARI: char = '\u{0965}'; 119 | 120 | /* Digits */ 121 | pub(crate) const B_0: char = '\u{09E6}'; 122 | pub(crate) const B_1: char = '\u{09E7}'; 123 | pub(crate) const B_2: char = '\u{09E8}'; 124 | pub(crate) const B_3: char = '\u{09E9}'; 125 | pub(crate) const B_4: char = '\u{09EA}'; 126 | pub(crate) const B_5: char = '\u{09EB}'; 127 | pub(crate) const B_6: char = '\u{09EC}'; 128 | pub(crate) const B_7: char = '\u{09ED}'; 129 | pub(crate) const B_8: char = '\u{09EE}'; 130 | pub(crate) const B_9: char = '\u{09EF}'; 131 | 132 | /* Additions for Assamese */ 133 | pub(crate) const B_RM: char = '\u{09F0}'; // BENGALI LETTER RA WITH MIDDLE DIAGONAL 134 | pub(crate) const B_RL: char = '\u{09F1}'; // BENGALI LETTER RA WITH LOWER DIAGONAL 135 | 136 | /* Currency signs */ 137 | pub(crate) const B_CRTAKA_M: char = '\u{09F2}'; // BENGALI RUPEE MARK = taka 138 | pub(crate) const B_CRTAKA: char = '\u{09F3}'; // BENGALI RUPEE SIGN = Bangladeshi taka 139 | 140 | /* Historic symbols for fractional values */ 141 | pub(crate) const B_CURRENCYNUMERATOR_ONE: char = '\u{09F4}'; 142 | pub(crate) const B_CURRENCYNUMERATOR_TWO: char = '\u{09F5}'; 143 | pub(crate) const B_CURRENCYNUMERATOR_THREE: char = '\u{09F6}'; 144 | pub(crate) const B_CURRENCYNUMERATOR_FOUR: char = '\u{09F7}'; 145 | pub(crate) const B_CURRENCYNUMERATOR_LESS: char = '\u{09F8}'; 146 | pub(crate) const B_CURRENCYNUMERATOR_SIXTEEN: char = '\u{09F9}'; 147 | 148 | /* Sign */ 149 | pub(crate) const B_SIGN_ISSHAR: char = '\u{09FA}'; 150 | 151 | /* Historic currency sign */ 152 | pub(crate) const B_CURRENCYGANDA: char = '\u{09FB}'; 153 | 154 | /* Unicode Addition */ 155 | pub(crate) const ZWJ: char = '\u{200D}'; 156 | pub(crate) const ZWNJ: char = '\u{200C}'; 157 | 158 | /// Is the provided `c` is a ligature making Kar? 159 | pub(crate) fn is_ligature_making_kar(c: char) -> bool { 160 | c == B_U_KAR || c == B_UU_KAR || c == B_RRI_KAR 161 | } 162 | -------------------------------------------------------------------------------- /data/Probhat.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "layout": { 4 | "developer": { 5 | "comment": "This custom layout file is for testing purposes.", 6 | "name": "" 7 | }, 8 | "image0": "", 9 | "image1": "", 10 | "name": "Probhat - with empty fields filled with 'E'", 11 | "version": "1.0" 12 | }, 13 | "type": "fixed", 14 | "version": "2" 15 | }, 16 | "layout": { 17 | "Key_0_AltGr": "৸", 18 | "Key_0_Normal": "০", 19 | "Key_1_AltGr": "৴", 20 | "Key_1_Normal": "১", 21 | "Key_2_AltGr": "৵", 22 | "Key_2_Normal": "২", 23 | "Key_3_AltGr": "৶", 24 | "Key_3_Normal": "৩", 25 | "Key_4_AltGr": "৷", 26 | "Key_4_Normal": "৪", 27 | "Key_5_AltGr": "E", 28 | "Key_5_Normal": "৫", 29 | "Key_6_AltGr": "E", 30 | "Key_6_Normal": "৬", 31 | "Key_7_AltGr": "E", 32 | "Key_7_Normal": "৭", 33 | "Key_8_AltGr": "E", 34 | "Key_8_Normal": "৮", 35 | "Key_9_AltGr": "E", 36 | "Key_9_Normal": "৯", 37 | "Key_A_AltGr": "ৠ", 38 | "Key_A_Normal": "অ", 39 | "Key_Ampersand_AltGr": "৺", 40 | "Key_Ampersand_Normal": "ঞ", 41 | "Key_Apostrophe_AltGr": "E", 42 | "Key_Apostrophe_Normal": "'", 43 | "Key_Asterisk_AltGr": "E", 44 | "Key_Asterisk_Normal": "ৎ", 45 | "Key_At_AltGr": "E", 46 | "Key_At_Normal": "@", 47 | "Key_B_AltGr": "E", 48 | "Key_B_Normal": "ভ", 49 | "Key_BackSlash_AltGr": "E", 50 | "Key_BackSlash_Normal": "‌", 51 | "Key_Bar_AltGr": "E", 52 | "Key_Bar_Normal": "॥", 53 | "Key_BraceLeft_AltGr": "E", 54 | "Key_BraceLeft_Normal": "ৈ", 55 | "Key_BraceRight_AltGr": "E", 56 | "Key_BraceRight_Normal": "ৌ", 57 | "Key_BracketLeft_AltGr": "E", 58 | "Key_BracketLeft_Normal": "ে", 59 | "Key_BracketRight_AltGr": "ৗ", 60 | "Key_BracketRight_Normal": "ো", 61 | "Key_C_AltGr": "E", 62 | "Key_C_Normal": "ছ", 63 | "Key_Circum_AltGr": "E", 64 | "Key_Circum_Normal": "^", 65 | "Key_Colon_AltGr": "E", 66 | "Key_Colon_Normal": ":", 67 | "Key_Comma_AltGr": "E", 68 | "Key_Comma_Normal": ",", 69 | "Key_D_AltGr": "ৢ", 70 | "Key_D_Normal": "ঢ", 71 | "Key_Dollar_AltGr": "৲", 72 | "Key_Dollar_Normal": "৳", 73 | "Key_E_AltGr": "E", 74 | "Key_E_Normal": "ঈ", 75 | "Key_Equals_AltGr": "E", 76 | "Key_Equals_Normal": "=", 77 | "Key_Exclaim_AltGr": "E", 78 | "Key_Exclaim_Normal": "!", 79 | "Key_F_AltGr": "E", 80 | "Key_F_Normal": "থ", 81 | "Key_G_AltGr": "E", 82 | "Key_G_Normal": "ঘ", 83 | "Key_Grave_AltGr": "E", 84 | "Key_Grave_Normal": "‍", 85 | "Key_Greater_AltGr": "E", 86 | "Key_Greater_Normal": "ঁ", 87 | "Key_H_AltGr": "E", 88 | "Key_H_Normal": "ঃ", 89 | "Key_Hash_AltGr": "E", 90 | "Key_Hash_Normal": "#", 91 | "Key_I_AltGr": "E", 92 | "Key_I_Normal": "ই", 93 | "Key_J_AltGr": "E", 94 | "Key_J_Normal": "ঝ", 95 | "Key_K_AltGr": "E", 96 | "Key_K_Normal": "খ", 97 | "Key_L_AltGr": "E", 98 | "Key_L_Normal": "ং", 99 | "Key_Less_AltGr": "E", 100 | "Key_Less_Normal": "ৃ", 101 | "Key_M_AltGr": "E", 102 | "Key_M_Normal": "ঙ", 103 | "Key_Minus_AltGr": "E", 104 | "Key_Minus_Normal": "-", 105 | "Key_N_AltGr": "E", 106 | "Key_N_Normal": "ণ", 107 | "Key_O_AltGr": "E", 108 | "Key_O_Normal": "ঔ", 109 | "Key_P_AltGr": "E", 110 | "Key_P_Normal": "ফ", 111 | "Key_ParenLeft_AltGr": "E", 112 | "Key_ParenLeft_Normal": "(", 113 | "Key_ParenRight_AltGr": "৹", 114 | "Key_ParenRight_Normal": ")", 115 | "Key_Percent_AltGr": "E", 116 | "Key_Percent_Normal": "%", 117 | "Key_Period_AltGr": "়", 118 | "Key_Period_Normal": "।", 119 | "Key_Plus_AltGr": "E", 120 | "Key_Plus_Normal": "+", 121 | "Key_Q_AltGr": "E", 122 | "Key_Q_Normal": "ধ", 123 | "Key_Question_AltGr": "E", 124 | "Key_Question_Normal": "?", 125 | "Key_Quote_AltGr": "E", 126 | "Key_Quote_Normal": "\"", 127 | "Key_R_AltGr": "E", 128 | "Key_R_Normal": "ড়", 129 | "Key_S_AltGr": "ৣ", 130 | "Key_S_Normal": "ষ", 131 | "Key_Semicolon_AltGr": "E", 132 | "Key_Semicolon_Normal": ";", 133 | "Key_Slash_AltGr": "E", 134 | "Key_Slash_Normal": "্", 135 | "Key_T_AltGr": "E", 136 | "Key_T_Normal": "ঠ", 137 | "Key_Tilde_AltGr": "E", 138 | "Key_Tilde_Normal": "~", 139 | "Key_U_AltGr": "E", 140 | "Key_U_Normal": "উ", 141 | "Key_UnderScore_AltGr": "E", 142 | "Key_UnderScore_Normal": "_", 143 | "Key_V_AltGr": "E", 144 | "Key_V_Normal": "ঋ", 145 | "Key_W_AltGr": "E", 146 | "Key_W_Normal": "ঊ", 147 | "Key_X_AltGr": "E", 148 | "Key_X_Normal": "ঢ়", 149 | "Key_Y_AltGr": "E", 150 | "Key_Y_Normal": "ঐ", 151 | "Key_Z_AltGr": "E", 152 | "Key_Z_Normal": "য", 153 | "Key_a_AltGr": "ঌ", 154 | "Key_a_Normal": "া", 155 | "Key_b_AltGr": "E", 156 | "Key_b_Normal": "ব", 157 | "Key_c_AltGr": "E", 158 | "Key_c_Normal": "চ", 159 | "Key_d_AltGr": "ৄ", 160 | "Key_d_Normal": "ড", 161 | "Key_e_AltGr": "E", 162 | "Key_e_Normal": "ী", 163 | "Key_f_AltGr": "E", 164 | "Key_f_Normal": "ত", 165 | "Key_g_AltGr": "E", 166 | "Key_g_Normal": "গ", 167 | "Key_h_AltGr": "ঽ", 168 | "Key_h_Normal": "হ", 169 | "Key_i_AltGr": "E", 170 | "Key_i_Normal": "ি", 171 | "Key_j_AltGr": "E", 172 | "Key_j_Normal": "জ", 173 | "Key_k_AltGr": "E", 174 | "Key_k_Normal": "ক", 175 | "Key_l_AltGr": "E", 176 | "Key_l_Normal": "ল", 177 | "Key_m_AltGr": "E", 178 | "Key_m_Normal": "ম", 179 | "Key_n_AltGr": "E", 180 | "Key_n_Normal": "ন", 181 | "Key_o_AltGr": "E", 182 | "Key_o_Normal": "ও", 183 | "Key_p_AltGr": "E", 184 | "Key_p_Normal": "প", 185 | "Key_q_AltGr": "E", 186 | "Key_q_Normal": "দ", 187 | "Key_r_AltGr": "E", 188 | "Key_r_Normal": "র", 189 | "Key_s_AltGr": "ৡ", 190 | "Key_s_Normal": "স", 191 | "Key_t_AltGr": "E", 192 | "Key_t_Normal": "ট", 193 | "Key_u_AltGr": "E", 194 | "Key_u_Normal": "ু", 195 | "Key_v_AltGr": "E", 196 | "Key_v_Normal": "আ", 197 | "Key_w_AltGr": "E", 198 | "Key_w_Normal": "ূ", 199 | "Key_x_AltGr": "E", 200 | "Key_x_Normal": "শ", 201 | "Key_y_AltGr": "E", 202 | "Key_y_Normal": "এ", 203 | "Key_z_AltGr": "E", 204 | "Key_z_Normal": "য়", 205 | "Num0": "০", 206 | "Num1": "১", 207 | "Num2": "২", 208 | "Num3": "৩", 209 | "Num4": "৪", 210 | "Num5": "৫", 211 | "Num6": "৬", 212 | "Num7": "৭", 213 | "Num8": "৮", 214 | "Num9": "৯", 215 | "NumAdd": "+", 216 | "NumDecimal": ".", 217 | "NumDivide": "/", 218 | "NumMultiply": "*", 219 | "NumSubtract": "-" 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/keycodes.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | //! Key codes 3 | 4 | // Begin Alphanumeric Zone 5 | pub const VC_GRAVE: u16 = 0x0029; // '`' 6 | pub const VC_TILDE: u16 = 0x0001; // '~' 7 | 8 | pub const VC_1: u16 = 0x0002; 9 | pub const VC_2: u16 = 0x0003; 10 | pub const VC_3: u16 = 0x0004; 11 | pub const VC_4: u16 = 0x0005; 12 | pub const VC_5: u16 = 0x0006; 13 | pub const VC_6: u16 = 0x0007; 14 | pub const VC_7: u16 = 0x0008; 15 | pub const VC_8: u16 = 0x0009; 16 | pub const VC_9: u16 = 0x000A; 17 | pub const VC_0: u16 = 0x000B; 18 | 19 | pub const VC_EXCLAIM: u16 = 0x003B; 20 | pub const VC_AT: u16 = 0x003C; 21 | pub const VC_HASH: u16 = 0x003D; 22 | pub const VC_DOLLAR: u16 = 0x003E; 23 | pub const VC_PERCENT: u16 = 0x003F; 24 | pub const VC_CIRCUM: u16 = 0x0040; 25 | pub const VC_AMPERSAND: u16 = 0x0041; 26 | pub const VC_ASTERISK: u16 = 0x0042; 27 | pub const VC_PAREN_LEFT: u16 = 0x0043; 28 | pub const VC_PAREN_RIGHT: u16 = 0x0044; 29 | pub const VC_UNDERSCORE: u16 = 0x0057; 30 | pub const VC_PLUS: u16 = 0x0058; 31 | 32 | pub const VC_MINUS: u16 = 0x000C; // '-' 33 | pub const VC_EQUALS: u16 = 0x000D; // '=' 34 | 35 | pub const VC_A: u16 = 0xA096; 36 | pub const VC_B: u16 = 0xA097; 37 | pub const VC_C: u16 = 0xA098; 38 | pub const VC_D: u16 = 0xA099; 39 | pub const VC_E: u16 = 0xA09A; 40 | pub const VC_F: u16 = 0xA09B; 41 | pub const VC_G: u16 = 0xA09C; 42 | pub const VC_H: u16 = 0xA09D; 43 | pub const VC_I: u16 = 0xA09E; 44 | pub const VC_J: u16 = 0xA09F; 45 | pub const VC_K: u16 = 0xA0A0; 46 | pub const VC_L: u16 = 0xA0A1; 47 | pub const VC_M: u16 = 0xA0A2; 48 | pub const VC_N: u16 = 0xA0A3; 49 | pub const VC_O: u16 = 0xA0A4; 50 | pub const VC_P: u16 = 0xA0A5; 51 | pub const VC_Q: u16 = 0xA0A6; 52 | pub const VC_R: u16 = 0xA0A7; 53 | pub const VC_S: u16 = 0xA0A8; 54 | pub const VC_T: u16 = 0xA0A9; 55 | pub const VC_U: u16 = 0xA0AA; 56 | pub const VC_V: u16 = 0xA0AB; 57 | pub const VC_W: u16 = 0xA0AC; 58 | pub const VC_X: u16 = 0xA0AD; 59 | pub const VC_Y: u16 = 0xA0AE; 60 | pub const VC_Z: u16 = 0xA0AF; 61 | 62 | pub const VC_A_SHIFT: u16 = 0xA0B4; 63 | pub const VC_B_SHIFT: u16 = 0xA0B5; 64 | pub const VC_C_SHIFT: u16 = 0xA0B6; 65 | pub const VC_D_SHIFT: u16 = 0xA0B7; 66 | pub const VC_E_SHIFT: u16 = 0xA0B8; 67 | pub const VC_F_SHIFT: u16 = 0xA0B9; 68 | pub const VC_G_SHIFT: u16 = 0xA0BA; 69 | pub const VC_H_SHIFT: u16 = 0xA0BB; 70 | pub const VC_I_SHIFT: u16 = 0xA0BC; 71 | pub const VC_J_SHIFT: u16 = 0xA0BD; 72 | pub const VC_K_SHIFT: u16 = 0xA0BE; 73 | pub const VC_L_SHIFT: u16 = 0xA0BF; 74 | pub const VC_M_SHIFT: u16 = 0xA0C0; 75 | pub const VC_N_SHIFT: u16 = 0xA0C1; 76 | pub const VC_O_SHIFT: u16 = 0xA0C2; 77 | pub const VC_P_SHIFT: u16 = 0xA0C3; 78 | pub const VC_Q_SHIFT: u16 = 0xA0C4; 79 | pub const VC_R_SHIFT: u16 = 0xA0C5; 80 | pub const VC_S_SHIFT: u16 = 0xA0C6; 81 | pub const VC_T_SHIFT: u16 = 0xA0C7; 82 | pub const VC_U_SHIFT: u16 = 0xA0C8; 83 | pub const VC_V_SHIFT: u16 = 0xA0C9; 84 | pub const VC_W_SHIFT: u16 = 0xA0CA; 85 | pub const VC_X_SHIFT: u16 = 0xA0CB; 86 | pub const VC_Y_SHIFT: u16 = 0xA0CC; 87 | pub const VC_Z_SHIFT: u16 = 0xA0CD; 88 | 89 | pub const VC_BRACKET_LEFT: u16 = 0x001A; // '[' 90 | pub const VC_BRACKET_RIGHT: u16 = 0x001B; // ']' 91 | pub const VC_BACK_SLASH: u16 = 0x002B; // '\' 92 | 93 | pub const VC_BRACE_LEFT: u16 = 0x005B; // '{' 94 | pub const VC_BRACE_RIGHT: u16 = 0x005C; // '}' 95 | pub const VC_BAR: u16 = 0x005D; // '|' 96 | 97 | pub const VC_SEMICOLON: u16 = 0x0027; // ';' 98 | pub const VC_APOSTROPHE: u16 = 0x0028; // ''' 99 | 100 | pub const VC_COMMA: u16 = 0x0033; // ',' 101 | pub const VC_PERIOD: u16 = 0x0034; // '.' 102 | pub const VC_SLASH: u16 = 0x0035; // '/' 103 | 104 | pub const VC_COLON: u16 = 0x0063; // ':' 105 | pub const VC_QUOTE: u16 = 0x0064; // '"' 106 | pub const VC_LESS: u16 = 0x0065; // '<' 107 | pub const VC_GREATER: u16 = 0x0066; // '>' 108 | pub const VC_QUESTION: u16 = 0x0067; // '?' 109 | 110 | // End Alphanumeric Zone 111 | 112 | // Begin Numeric Zone 113 | pub const VC_KP_DIVIDE: u16 = 0x0E35; 114 | pub const VC_KP_MULTIPLY: u16 = 0x0037; 115 | pub const VC_KP_SUBTRACT: u16 = 0x004A; 116 | pub const VC_KP_EQUALS: u16 = 0x0E0D; 117 | pub const VC_KP_ADD: u16 = 0x004E; 118 | pub const VC_KP_ENTER: u16 = 0x0E1C; 119 | pub const VC_KP_DECIMAL: u16 = 0x0053; 120 | 121 | pub const VC_KP_1: u16 = 0x004F; 122 | pub const VC_KP_2: u16 = 0x0050; 123 | pub const VC_KP_3: u16 = 0x0051; 124 | pub const VC_KP_4: u16 = 0x004B; 125 | pub const VC_KP_5: u16 = 0x004C; 126 | pub const VC_KP_6: u16 = 0x004D; 127 | pub const VC_KP_7: u16 = 0x0047; 128 | pub const VC_KP_8: u16 = 0x0048; 129 | pub const VC_KP_9: u16 = 0x0049; 130 | pub const VC_KP_0: u16 = 0x0052; 131 | // End Numeric Zone 132 | 133 | pub(crate) fn keycode_to_char(key: u16) -> char { 134 | match key { 135 | // Alphanumeric keys 136 | VC_GRAVE => '`', 137 | VC_TILDE => '~', 138 | VC_0 => '0', 139 | VC_PAREN_RIGHT => ')', 140 | VC_1 => '1', 141 | VC_EXCLAIM => '!', 142 | VC_2 => '2', 143 | VC_AT => '@', 144 | VC_3 => '3', 145 | VC_HASH => '#', 146 | VC_4 => '4', 147 | VC_DOLLAR => '$', 148 | VC_5 => '5', 149 | VC_PERCENT => '%', 150 | VC_6 => '6', 151 | VC_CIRCUM => '^', 152 | VC_7 => '7', 153 | VC_AMPERSAND => '&', 154 | VC_8 => '8', 155 | VC_ASTERISK => '*', 156 | VC_9 => '9', 157 | VC_PAREN_LEFT => '(', 158 | 159 | // Alphabet Keys 160 | VC_Q_SHIFT => 'Q', 161 | VC_Q => 'q', 162 | VC_W_SHIFT => 'W', 163 | VC_W => 'w', 164 | VC_E_SHIFT => 'E', 165 | VC_E => 'e', 166 | VC_R_SHIFT => 'R', 167 | VC_R => 'r', 168 | VC_T_SHIFT => 'T', 169 | VC_T => 't', 170 | VC_Y_SHIFT => 'Y', 171 | VC_Y => 'y', 172 | VC_U_SHIFT => 'U', 173 | VC_U => 'u', 174 | VC_I_SHIFT => 'I', 175 | VC_I => 'i', 176 | VC_O_SHIFT => 'O', 177 | VC_O => 'o', 178 | VC_P_SHIFT => 'P', 179 | VC_P => 'p', 180 | VC_A_SHIFT => 'A', 181 | VC_A => 'a', 182 | VC_S_SHIFT => 'S', 183 | VC_S => 's', 184 | VC_D_SHIFT => 'D', 185 | VC_D => 'd', 186 | VC_F_SHIFT => 'F', 187 | VC_F => 'f', 188 | VC_G_SHIFT => 'G', 189 | VC_G => 'g', 190 | VC_H_SHIFT => 'H', 191 | VC_H => 'h', 192 | VC_J_SHIFT => 'J', 193 | VC_J => 'j', 194 | VC_K_SHIFT => 'K', 195 | VC_K => 'k', 196 | VC_L_SHIFT => 'L', 197 | VC_L => 'l', 198 | VC_Z_SHIFT => 'Z', 199 | VC_Z => 'z', 200 | VC_X_SHIFT => 'X', 201 | VC_X => 'x', 202 | VC_C_SHIFT => 'C', 203 | VC_C => 'c', 204 | VC_V_SHIFT => 'V', 205 | VC_V => 'v', 206 | VC_B_SHIFT => 'B', 207 | VC_B => 'b', 208 | VC_N_SHIFT => 'N', 209 | VC_N => 'n', 210 | VC_M_SHIFT => 'M', 211 | VC_M => 'm', 212 | 213 | VC_MINUS => '-', 214 | VC_UNDERSCORE => '_', 215 | VC_EQUALS => '=', 216 | VC_PLUS => '+', 217 | 218 | VC_BRACKET_LEFT => '[', 219 | VC_BRACKET_RIGHT => ']', 220 | VC_BRACE_LEFT => '{', 221 | VC_BRACE_RIGHT => '}', 222 | VC_BACK_SLASH => '\\', 223 | VC_BAR => '|', 224 | 225 | VC_SEMICOLON => ';', 226 | VC_COLON => ':', 227 | VC_APOSTROPHE => '\'', 228 | VC_QUOTE => '\"', 229 | 230 | VC_COMMA => ',', 231 | VC_LESS => '<', 232 | VC_PERIOD => '.', 233 | VC_GREATER => '>', 234 | VC_SLASH => '/', 235 | VC_QUESTION => '?', 236 | 237 | // Keypad keys 238 | VC_KP_0 => '0', 239 | VC_KP_1 => '1', 240 | VC_KP_2 => '2', 241 | VC_KP_3 => '3', 242 | VC_KP_4 => '4', 243 | VC_KP_5 => '5', 244 | VC_KP_6 => '6', 245 | VC_KP_7 => '7', 246 | VC_KP_8 => '8', 247 | VC_KP_9 => '9', 248 | 249 | VC_KP_DIVIDE => '/', 250 | VC_KP_MULTIPLY => '*', 251 | VC_KP_SUBTRACT => '-', 252 | VC_KP_ADD => '+', 253 | VC_KP_DECIMAL => '.', 254 | 255 | _ => panic!("Got unknown key!"), 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/phonetic/method.rs: -------------------------------------------------------------------------------- 1 | // Phonetic Method 2 | use ahash::RandomState; 3 | use std::collections::HashMap; 4 | use std::fs::{write, File}; 5 | use std::time::SystemTime; 6 | 7 | use crate::config::Config; 8 | use crate::context::Method; 9 | use crate::data::Data; 10 | use crate::keycodes::keycode_to_char; 11 | use crate::phonetic::suggestion::PhoneticSuggestion; 12 | use crate::suggestion::Suggestion; 13 | use crate::utility::{read, SplittedString}; 14 | 15 | pub(crate) struct PhoneticMethod { 16 | buffer: String, 17 | suggestion: PhoneticSuggestion, 18 | // Candidate selections. 19 | selections: HashMap, 20 | // Last modification of the user's auto correct file. 21 | modified: SystemTime, 22 | // Previously selected candidate index of the current suggestion list. 23 | prev_selection: usize, 24 | } 25 | 26 | impl PhoneticMethod { 27 | /// Creates a new `PhoneticMethod` struct. 28 | pub(crate) fn new(config: &Config) -> Self { 29 | // Load candidate selections file. 30 | let selections = if let Ok(file) = std::fs::read(config.get_user_phonetic_selection_data()) 31 | { 32 | serde_json::from_slice(&file).unwrap() 33 | } else { 34 | HashMap::with_hasher(RandomState::new()) 35 | }; 36 | 37 | // Load user's auto correct file. 38 | let (modified, autocorrect) = { 39 | if let Ok(mut file) = File::open(config.get_user_phonetic_autocorrect()) { 40 | let modified = file.metadata().unwrap().modified().unwrap(); 41 | let autocorrect = serde_json::from_slice(&read(&mut file)).unwrap(); 42 | (modified, autocorrect) 43 | } else { 44 | ( 45 | SystemTime::UNIX_EPOCH, 46 | HashMap::with_hasher(RandomState::new()), 47 | ) 48 | } 49 | }; 50 | 51 | PhoneticMethod { 52 | buffer: String::with_capacity(20), 53 | suggestion: PhoneticSuggestion::new(autocorrect), 54 | selections, 55 | modified, 56 | prev_selection: 0, 57 | } 58 | } 59 | 60 | /// Returns `Suggestion` struct with suggestions. 61 | fn create_suggestion(&mut self, data: &Data, config: &Config) -> Suggestion { 62 | if config.get_phonetic_suggestion() { 63 | let (suggestions, selection) = 64 | self.suggestion 65 | .suggest(&self.buffer, data, &mut self.selections, config); 66 | 67 | self.prev_selection = selection; 68 | 69 | Suggestion::new( 70 | self.buffer.clone(), 71 | &suggestions, 72 | self.prev_selection, 73 | config.get_ansi_encoding(), 74 | ) 75 | } else { 76 | let suggestion = self.suggestion.suggest_only_phonetic(&self.buffer); 77 | 78 | Suggestion::new_lonely(suggestion, config.get_ansi_encoding()) 79 | } 80 | } 81 | } 82 | 83 | impl Method for PhoneticMethod { 84 | fn get_suggestion( 85 | &mut self, 86 | key: u16, 87 | _modifier: u8, 88 | selection: u8, 89 | data: &Data, 90 | config: &Config, 91 | ) -> Suggestion { 92 | let character = keycode_to_char(key); 93 | self.buffer.push(character); 94 | let mut suggestion = self.create_suggestion(data, config); 95 | 96 | // Preserve user's selection if the keypress was a punctuation mark 97 | if let Suggestion::Full { 98 | selection: ref mut sel, 99 | .. 100 | } = suggestion 101 | { 102 | if matches!( 103 | character, 104 | '.' | '?' | '!' | ',' | ':' | ';' | '-' | '_' | ')' | '}' | ']' | '\'' | '"' 105 | ) { 106 | *sel = selection.into(); 107 | } 108 | } 109 | 110 | suggestion 111 | } 112 | 113 | fn candidate_committed(&mut self, index: usize, config: &Config) { 114 | // Check if user has selected a different suggestion 115 | if self.prev_selection != index && config.get_phonetic_suggestion() { 116 | let suggestion = 117 | SplittedString::split(self.suggestion.suggestions[index].to_string(), true) 118 | .word() 119 | .to_string(); 120 | self.selections.insert( 121 | SplittedString::split(&self.buffer, false) 122 | .word() 123 | .to_string(), 124 | suggestion, 125 | ); 126 | write( 127 | config.get_user_phonetic_selection_data(), 128 | serde_json::to_string(&self.selections).unwrap(), 129 | ) 130 | .unwrap(); 131 | } 132 | 133 | // Reset to defaults 134 | self.buffer.clear(); 135 | } 136 | 137 | fn update_engine(&mut self, config: &Config) { 138 | if let Ok(mut file) = File::open(config.get_user_phonetic_autocorrect()) { 139 | let modified = file.metadata().unwrap().modified().unwrap(); 140 | // Update the auto correct entries if only the file was modified in the meantime. 141 | if modified > self.modified { 142 | self.suggestion.user_autocorrect = 143 | serde_json::from_slice(&read(&mut file)).unwrap(); 144 | self.modified = modified; 145 | } 146 | } 147 | } 148 | 149 | fn ongoing_input_session(&self) -> bool { 150 | !self.buffer.is_empty() 151 | } 152 | 153 | fn finish_input_session(&mut self) { 154 | self.buffer.clear(); 155 | } 156 | 157 | fn backspace_event(&mut self, ctrl: bool, data: &Data, config: &Config) -> Suggestion { 158 | if !self.buffer.is_empty() { 159 | // Whole word deletion: Ctrl + Backspace combination 160 | if ctrl { 161 | self.buffer.clear(); 162 | return Suggestion::empty(); 163 | } 164 | 165 | // Remove the last character. 166 | self.buffer.pop(); 167 | 168 | if self.buffer.is_empty() { 169 | // The buffer is now empty, so return empty suggestion. 170 | return Suggestion::empty(); 171 | } 172 | 173 | self.create_suggestion(data, config) 174 | } else { 175 | Suggestion::empty() 176 | } 177 | } 178 | } 179 | 180 | // Implement Default trait on PhoneticMethod for testing convenience. 181 | impl Default for PhoneticMethod { 182 | fn default() -> Self { 183 | PhoneticMethod { 184 | buffer: String::new(), 185 | suggestion: PhoneticSuggestion::new(HashMap::with_hasher(RandomState::new())), 186 | selections: HashMap::with_hasher(RandomState::new()), 187 | modified: SystemTime::UNIX_EPOCH, 188 | prev_selection: 0, 189 | } 190 | } 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use super::PhoneticMethod; 196 | use crate::config::get_phonetic_method_defaults; 197 | use crate::context::Method; 198 | use crate::data::Data; 199 | use crate::keycodes::{VC_COMMA, VC_R}; 200 | 201 | #[test] 202 | fn test_backspace() { 203 | let config = get_phonetic_method_defaults(); 204 | let data = Data::new(&config); 205 | let mut method = PhoneticMethod { 206 | buffer: "ab".to_string(), 207 | ..Default::default() 208 | }; 209 | 210 | assert!(!method.backspace_event(false, &data, &config).is_empty()); // a 211 | assert!(method.backspace_event(false, &data, &config).is_empty()); // Empty 212 | 213 | // Ctrl + Backspace 214 | method = PhoneticMethod { 215 | buffer: "ab".to_string(), 216 | ..Default::default() 217 | }; 218 | assert!(method.backspace_event(true, &data, &config).is_empty()); 219 | } 220 | 221 | #[test] 222 | fn test_preserve_selection() { 223 | let config = get_phonetic_method_defaults(); 224 | let data = Data::new(&config); 225 | let mut method = PhoneticMethod { 226 | buffer: "coffee".to_string(), 227 | ..Default::default() 228 | }; 229 | 230 | let suggestion = method.get_suggestion(VC_COMMA, 0, 3, &data, &config); 231 | assert_eq!(suggestion.previously_selected_index(), 3); 232 | 233 | method.backspace_event(false, &data, &config); 234 | 235 | let suggestion = method.get_suggestion(VC_R, 0, 3, &data, &config); 236 | assert_eq!(suggestion.previously_selected_index(), 0); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::var, 3 | fs::read_to_string, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use serde_json::Value; 8 | 9 | /// Config struct for configuring RitiContext. 10 | #[derive(Clone)] 11 | pub struct Config { 12 | layout: String, 13 | database_dir: PathBuf, 14 | user_dir: PathBuf, 15 | include_english: bool, 16 | phonetic_suggestion: bool, 17 | fixed_suggestion: bool, 18 | fixed_vowel: bool, 19 | fixed_chandra: bool, 20 | fixed_kar: bool, 21 | fixed_old_reph: bool, 22 | fixed_numpad: bool, 23 | fixed_kar_order: bool, 24 | // Output in ANSI encoding 25 | ansi: bool, 26 | smart_quote: bool, 27 | } 28 | 29 | impl Config { 30 | /// Sets the layout file path. 31 | /// For Avro Phonetic, it accepts the name `avro_phonetic`. 32 | /// 33 | /// Returns `true` if the layout file path or name is valid. 34 | pub fn set_layout_file_path(&mut self, layout: &str) -> bool { 35 | if layout == "avro_phonetic" || Path::new(layout).exists() { 36 | self.layout = layout.into(); 37 | true 38 | } else { 39 | false 40 | } 41 | } 42 | 43 | pub fn get_layout_file_path(&self) -> &str { 44 | &self.layout 45 | } 46 | 47 | /// Sets the database directory path. 48 | /// 49 | /// Returns `true` if the path exists. 50 | pub fn set_database_dir(&mut self, path: &str) -> bool { 51 | if Path::new(path).exists() { 52 | self.database_dir = path.into(); 53 | true 54 | } else { 55 | false 56 | } 57 | } 58 | 59 | /// Sets the user specific writable directory path. 60 | /// 61 | /// Returns `true` if the path exists. 62 | pub fn set_user_dir(&mut self, path: &str) -> bool { 63 | if Path::new(path).exists() { 64 | self.user_dir = path.into(); 65 | true 66 | } else { 67 | false 68 | } 69 | } 70 | 71 | pub fn get_database_dir(&self) -> &PathBuf { 72 | &self.database_dir 73 | } 74 | 75 | pub fn get_suffix_data_path(&self) -> PathBuf { 76 | self.database_dir.join("suffix.json") 77 | } 78 | 79 | pub fn get_autocorrect_data(&self) -> PathBuf { 80 | self.database_dir.join("autocorrect.json") 81 | } 82 | 83 | /// Get file path of user defined Auto Correct file. 84 | pub fn get_user_phonetic_autocorrect(&self) -> PathBuf { 85 | self.user_dir.join("autocorrect.json") 86 | } 87 | 88 | /// Get file path of user defined phonetic candidate selection file. 89 | pub fn get_user_phonetic_selection_data(&self) -> PathBuf { 90 | self.user_dir.join("phonetic-candidate-selection.json") 91 | } 92 | 93 | pub fn get_suggestion_include_english(&self) -> bool { 94 | // Mutually exclusive 95 | self.include_english && !self.ansi 96 | } 97 | 98 | pub fn set_suggestion_include_english(&mut self, include: bool) { 99 | self.include_english = include; 100 | } 101 | 102 | pub fn get_phonetic_suggestion(&self) -> bool { 103 | self.phonetic_suggestion 104 | } 105 | 106 | /// Set the config's phonetic database. 107 | pub fn set_phonetic_suggestion(&mut self, phonetic_suggestion: bool) { 108 | self.phonetic_suggestion = phonetic_suggestion; 109 | } 110 | 111 | /// Get the config's fixed database. 112 | pub fn get_fixed_suggestion(&self) -> bool { 113 | self.fixed_suggestion 114 | } 115 | 116 | /// Set the config's fixed database. 117 | pub fn set_fixed_suggestion(&mut self, fixed_suggestion: bool) { 118 | self.fixed_suggestion = fixed_suggestion; 119 | } 120 | 121 | /// Get the config's fixed vowel. 122 | pub fn get_fixed_automatic_vowel(&self) -> bool { 123 | self.fixed_vowel 124 | } 125 | 126 | /// Set the config's fixed vowel. 127 | pub fn set_fixed_automatic_vowel(&mut self, fixed_vowel: bool) { 128 | self.fixed_vowel = fixed_vowel; 129 | } 130 | 131 | /// Get the config's fixed chandra. 132 | pub fn get_fixed_automatic_chandra(&self) -> bool { 133 | self.fixed_chandra 134 | } 135 | 136 | /// Set the config's fixed chandra. 137 | pub fn set_fixed_automatic_chandra(&mut self, fixed_chandra: bool) { 138 | self.fixed_chandra = fixed_chandra; 139 | } 140 | 141 | /// Get the config's fixed kar. 142 | pub fn get_fixed_traditional_kar(&self) -> bool { 143 | self.fixed_kar 144 | } 145 | 146 | /// Set the config's fixed kar. 147 | pub fn set_fixed_traditional_kar(&mut self, fixed_kar: bool) { 148 | self.fixed_kar = fixed_kar; 149 | } 150 | 151 | /// Get the config's fixed old reph. 152 | pub fn get_fixed_old_reph(&self) -> bool { 153 | self.fixed_old_reph 154 | } 155 | 156 | /// Set the config's fixed old reph. 157 | pub fn set_fixed_old_reph(&mut self, fixed_old_reph: bool) { 158 | self.fixed_old_reph = fixed_old_reph; 159 | } 160 | 161 | /// Get the config's fixed numpad. 162 | pub fn get_fixed_numpad(&self) -> bool { 163 | self.fixed_numpad 164 | } 165 | 166 | /// Set the config's fixed numpad. 167 | pub fn set_fixed_numpad(&mut self, fixed_numpad: bool) { 168 | self.fixed_numpad = fixed_numpad; 169 | } 170 | 171 | /// Get the config's fixed kar order. 172 | pub fn get_fixed_old_kar_order(&self) -> bool { 173 | self.fixed_kar_order 174 | } 175 | 176 | /// Set the config's fixed kar order. 177 | pub fn set_fixed_old_kar_order(&mut self, fixed_kar_order: bool) { 178 | self.fixed_kar_order = fixed_kar_order; 179 | } 180 | 181 | /// Checks if the layout path had changed. 182 | pub fn layout_changed(&self, new_config: &Self) -> bool { 183 | self.layout != new_config.layout 184 | } 185 | 186 | /// Checks if the layout is phonetic 187 | pub fn is_phonetic(&self) -> bool { 188 | self.get_layout_file_path() == "avro_phonetic" 189 | } 190 | 191 | /// Give layout's `layout` json object, which contains the layout data. 192 | pub fn get_layout(&self) -> Option { 193 | if self.is_phonetic() { 194 | None 195 | } else { 196 | read_to_string(self.get_layout_file_path()) 197 | .ok() 198 | .and_then(|s| serde_json::from_str::(&s).ok()) 199 | .map(|v| v["layout"].to_owned()) 200 | } 201 | } 202 | 203 | /// Checks if ANSI encoding is enabled. 204 | pub fn get_ansi_encoding(&self) -> bool { 205 | self.ansi 206 | } 207 | 208 | /// Set the ANSI encoding configuration. 209 | pub fn set_ansi_encoding(&mut self, ansi: bool) { 210 | self.ansi = ansi; 211 | } 212 | 213 | /// Get the config's smart quote configuration. 214 | #[must_use] 215 | pub fn get_smart_quote(&self) -> bool { 216 | self.smart_quote 217 | } 218 | 219 | /// Set the config's smart quote. 220 | pub fn set_smart_quote(&mut self, smart_quote: bool) { 221 | self.smart_quote = smart_quote; 222 | } 223 | } 224 | 225 | pub fn get_user_data_dir() -> PathBuf { 226 | var("XDG_DATA_HOME") 227 | .ok() 228 | .or_else(|| var("HOME").ok().map(|path| path + "/.local/share")) 229 | .map(|path| path + "/openbangla-keyboard") 230 | .or_else(|| { 231 | // Windows 232 | var("localappdata") 233 | .ok() 234 | .map(|path| path + "/OpenBangla Keyboard") 235 | }) 236 | .unwrap() 237 | .into() 238 | } 239 | 240 | #[cfg(test)] 241 | pub fn get_phonetic_method_defaults() -> Config { 242 | Config { 243 | layout: "avro_phonetic".to_owned(), 244 | database_dir: format!("{}{}", env!("CARGO_MANIFEST_DIR"), "/data").into(), 245 | phonetic_suggestion: true, 246 | ..Default::default() 247 | } 248 | } 249 | 250 | #[cfg(test)] 251 | pub fn get_fixed_method_defaults() -> Config { 252 | Config { 253 | layout: format!("{}{}", env!("CARGO_MANIFEST_DIR"), "/data/Probhat.json"), 254 | database_dir: format!("{}{}", env!("CARGO_MANIFEST_DIR"), "/data").into(), 255 | fixed_suggestion: true, 256 | fixed_vowel: true, 257 | fixed_chandra: true, 258 | fixed_kar: true, 259 | fixed_numpad: true, 260 | fixed_old_reph: true, 261 | fixed_kar_order: false, 262 | ..Default::default() 263 | } 264 | } 265 | 266 | impl Default for Config { 267 | fn default() -> Self { 268 | Config { 269 | layout: Default::default(), 270 | database_dir: Default::default(), 271 | user_dir: get_user_data_dir(), 272 | include_english: false, 273 | fixed_suggestion: false, 274 | fixed_vowel: false, 275 | fixed_chandra: false, 276 | fixed_kar: false, 277 | fixed_numpad: false, 278 | fixed_old_reph: false, 279 | fixed_kar_order: false, 280 | phonetic_suggestion: false, 281 | ansi: false, 282 | smart_quote: true, 283 | } 284 | } 285 | } 286 | 287 | #[cfg(test)] 288 | mod tests { 289 | use super::*; 290 | 291 | #[test] 292 | #[cfg(target_os = "linux")] 293 | fn test_data_dir_linux() { 294 | assert_eq!( 295 | get_user_data_dir(), 296 | PathBuf::from(var("HOME").unwrap() + "/.local/share/openbangla-keyboard") 297 | ); 298 | std::env::set_var("XDG_DATA_HOME", "/non/existent"); 299 | assert_eq!( 300 | get_user_data_dir(), 301 | PathBuf::from("/non/existent/openbangla-keyboard") 302 | ); 303 | } 304 | 305 | #[test] 306 | #[cfg(target_os = "windows")] 307 | fn test_data_dir_windows() { 308 | assert_eq!( 309 | get_user_data_dir(), 310 | PathBuf::from(var("localappdata").unwrap() + "/OpenBangla Keyboard") 311 | ) 312 | } 313 | 314 | #[test] 315 | fn test_mutually_exclusive() { 316 | let mut config = Config::default(); 317 | 318 | config.set_suggestion_include_english(true); 319 | config.set_ansi_encoding(false); 320 | assert!(config.get_suggestion_include_english()); 321 | 322 | config.set_ansi_encoding(true); 323 | assert!(!config.get_suggestion_include_english()); 324 | } 325 | 326 | #[test] 327 | fn test_path_validation() { 328 | let mut config = Config::default(); 329 | 330 | assert!(!config.set_layout_file_path("non_existent")); 331 | assert!(config.set_layout_file_path("avro_phonetic")); 332 | 333 | assert!(!config.set_layout_file_path("/non_existent/Probhat.json")); 334 | assert!(config.set_layout_file_path(&format!( 335 | "{}{}", 336 | env!("CARGO_MANIFEST_DIR"), 337 | "/data/Probhat.json" 338 | ))); 339 | 340 | assert!(!config.set_database_dir("/non_existent")); 341 | assert!(config.set_database_dir(&format!("{}{}", env!("CARGO_MANIFEST_DIR"), "/data"))); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /include/riti.h: -------------------------------------------------------------------------------- 1 | /* Text to put at the beginning of the generated file. Probably a license. */ 2 | 3 | #ifndef RITI_H 4 | #define RITI_H 5 | 6 | /* Generated with cbindgen:0.27.0 */ 7 | 8 | /* 9 | * Warning, this file is autogenerated by cbindgen. Don't modify this manually. 10 | * Run this command to generate this file: cbindgen --config cbindgen.toml --output include/riti.h 11 | */ 12 | 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | /* 20 | Shift modifier key. 21 | 22 | Used by the [`get_suggestion_for_key()`](struct.RitiContext.html#method.get_suggestion_for_key) function. 23 | */ 24 | #define MODIFIER_SHIFT (1 << 0) 25 | 26 | /* 27 | AltGr modifier key. 28 | 29 | Used by the [`get_suggestion_for_key()`](struct.RitiContext.html#method.get_suggestion_for_key) function. 30 | */ 31 | #define MODIFIER_ALT_GR (1 << 1) 32 | 33 | #define VC_GRAVE 41 34 | 35 | #define VC_TILDE 1 36 | 37 | #define VC_1 2 38 | 39 | #define VC_2 3 40 | 41 | #define VC_3 4 42 | 43 | #define VC_4 5 44 | 45 | #define VC_5 6 46 | 47 | #define VC_6 7 48 | 49 | #define VC_7 8 50 | 51 | #define VC_8 9 52 | 53 | #define VC_9 10 54 | 55 | #define VC_0 11 56 | 57 | #define VC_EXCLAIM 59 58 | 59 | #define VC_AT 60 60 | 61 | #define VC_HASH 61 62 | 63 | #define VC_DOLLAR 62 64 | 65 | #define VC_PERCENT 63 66 | 67 | #define VC_CIRCUM 64 68 | 69 | #define VC_AMPERSAND 65 70 | 71 | #define VC_ASTERISK 66 72 | 73 | #define VC_PAREN_LEFT 67 74 | 75 | #define VC_PAREN_RIGHT 68 76 | 77 | #define VC_UNDERSCORE 87 78 | 79 | #define VC_PLUS 88 80 | 81 | #define VC_MINUS 12 82 | 83 | #define VC_EQUALS 13 84 | 85 | #define VC_A 41110 86 | 87 | #define VC_B 41111 88 | 89 | #define VC_C 41112 90 | 91 | #define VC_D 41113 92 | 93 | #define VC_E 41114 94 | 95 | #define VC_F 41115 96 | 97 | #define VC_G 41116 98 | 99 | #define VC_H 41117 100 | 101 | #define VC_I 41118 102 | 103 | #define VC_J 41119 104 | 105 | #define VC_K 41120 106 | 107 | #define VC_L 41121 108 | 109 | #define VC_M 41122 110 | 111 | #define VC_N 41123 112 | 113 | #define VC_O 41124 114 | 115 | #define VC_P 41125 116 | 117 | #define VC_Q 41126 118 | 119 | #define VC_R 41127 120 | 121 | #define VC_S 41128 122 | 123 | #define VC_T 41129 124 | 125 | #define VC_U 41130 126 | 127 | #define VC_V 41131 128 | 129 | #define VC_W 41132 130 | 131 | #define VC_X 41133 132 | 133 | #define VC_Y 41134 134 | 135 | #define VC_Z 41135 136 | 137 | #define VC_A_SHIFT 41140 138 | 139 | #define VC_B_SHIFT 41141 140 | 141 | #define VC_C_SHIFT 41142 142 | 143 | #define VC_D_SHIFT 41143 144 | 145 | #define VC_E_SHIFT 41144 146 | 147 | #define VC_F_SHIFT 41145 148 | 149 | #define VC_G_SHIFT 41146 150 | 151 | #define VC_H_SHIFT 41147 152 | 153 | #define VC_I_SHIFT 41148 154 | 155 | #define VC_J_SHIFT 41149 156 | 157 | #define VC_K_SHIFT 41150 158 | 159 | #define VC_L_SHIFT 41151 160 | 161 | #define VC_M_SHIFT 41152 162 | 163 | #define VC_N_SHIFT 41153 164 | 165 | #define VC_O_SHIFT 41154 166 | 167 | #define VC_P_SHIFT 41155 168 | 169 | #define VC_Q_SHIFT 41156 170 | 171 | #define VC_R_SHIFT 41157 172 | 173 | #define VC_S_SHIFT 41158 174 | 175 | #define VC_T_SHIFT 41159 176 | 177 | #define VC_U_SHIFT 41160 178 | 179 | #define VC_V_SHIFT 41161 180 | 181 | #define VC_W_SHIFT 41162 182 | 183 | #define VC_X_SHIFT 41163 184 | 185 | #define VC_Y_SHIFT 41164 186 | 187 | #define VC_Z_SHIFT 41165 188 | 189 | #define VC_BRACKET_LEFT 26 190 | 191 | #define VC_BRACKET_RIGHT 27 192 | 193 | #define VC_BACK_SLASH 43 194 | 195 | #define VC_BRACE_LEFT 91 196 | 197 | #define VC_BRACE_RIGHT 92 198 | 199 | #define VC_BAR 93 200 | 201 | #define VC_SEMICOLON 39 202 | 203 | #define VC_APOSTROPHE 40 204 | 205 | #define VC_COMMA 51 206 | 207 | #define VC_PERIOD 52 208 | 209 | #define VC_SLASH 53 210 | 211 | #define VC_COLON 99 212 | 213 | #define VC_QUOTE 100 214 | 215 | #define VC_LESS 101 216 | 217 | #define VC_GREATER 102 218 | 219 | #define VC_QUESTION 103 220 | 221 | #define VC_KP_DIVIDE 3637 222 | 223 | #define VC_KP_MULTIPLY 55 224 | 225 | #define VC_KP_SUBTRACT 74 226 | 227 | #define VC_KP_EQUALS 3597 228 | 229 | #define VC_KP_ADD 78 230 | 231 | #define VC_KP_ENTER 3612 232 | 233 | #define VC_KP_DECIMAL 83 234 | 235 | #define VC_KP_1 79 236 | 237 | #define VC_KP_2 80 238 | 239 | #define VC_KP_3 81 240 | 241 | #define VC_KP_4 75 242 | 243 | #define VC_KP_5 76 244 | 245 | #define VC_KP_6 77 246 | 247 | #define VC_KP_7 71 248 | 249 | #define VC_KP_8 72 250 | 251 | #define VC_KP_9 73 252 | 253 | #define VC_KP_0 82 254 | 255 | /* 256 | Config struct for configuring RitiContext. 257 | */ 258 | typedef struct Config Config; 259 | 260 | /* 261 | Context handle used for libRiti IM APIs 262 | */ 263 | typedef struct RitiContext RitiContext; 264 | 265 | /* 266 | Suggestions which are intended to be shown by the IM's candidate window. 267 | Suggestion is of two variants, the 'Full' one includes a list of suggestion and 268 | the 'Single' one is just a String. 269 | */ 270 | typedef struct Suggestion Suggestion; 271 | 272 | #ifdef __cplusplus 273 | extern "C" { 274 | #endif // __cplusplus 275 | 276 | /* 277 | Creates a new instance of RitiContext with a Config which is properly 278 | populated using `riti_config_set_*` set of functions. 279 | */ 280 | struct RitiContext *riti_context_new_with_config(const struct Config *ptr); 281 | 282 | void riti_context_free(struct RitiContext *ptr); 283 | 284 | /* 285 | Generates suggestion for `key` press. 286 | 287 | `modifier`: state of modifier keys 288 | `selection`: previously selected user selection index if available otherwise `0`. 289 | It is used to preserve user's candidate selection if the key is a punctuation character in Phonetic method. 290 | */ 291 | struct Suggestion *riti_get_suggestion_for_key(struct RitiContext *ptr, 292 | uint16_t key, 293 | uint8_t modifier, 294 | uint8_t selection); 295 | 296 | /* 297 | A candidate of the suggestion list was committed. 298 | 299 | `index`: index of the candidate. 300 | 301 | This function will end the ongoing input session. 302 | */ 303 | void riti_context_candidate_committed(struct RitiContext *ptr, uintptr_t index); 304 | 305 | /* 306 | Update the suggestion making engine. This would also look for changes 307 | in layout selection and AutoCorrect database. 308 | */ 309 | void riti_context_update_engine(struct RitiContext *ptr, 310 | const struct Config *config); 311 | 312 | /* 313 | Checks if there is an ongoing input session. 314 | */ 315 | bool riti_context_ongoing_input_session(struct RitiContext *ptr); 316 | 317 | /* 318 | Finish the ongoing input session if any. 319 | */ 320 | void riti_context_finish_input_session(struct RitiContext *ptr); 321 | 322 | /* 323 | A BackSpace event. 324 | 325 | Returns a new `suggestion` after applying the BackSpace event. 326 | 327 | If the `ctrl` parameter is true then it deletes the whole word 328 | in composition currently and ends the ongoing input session. 329 | 330 | If the internal buffer becomes empty, this function will 331 | end the ongoing input session. 332 | */ 333 | struct Suggestion *riti_context_backspace_event(struct RitiContext *ptr, 334 | bool ctrl); 335 | 336 | void riti_suggestion_free(struct Suggestion *ptr); 337 | 338 | /* 339 | Get the suggestion of the `index` from suggestions. 340 | */ 341 | char *riti_suggestion_get_suggestion(const struct Suggestion *ptr, 342 | uintptr_t index); 343 | 344 | /* 345 | Get the only suggestion of the *lonely* `Suggestion`. 346 | */ 347 | char *riti_suggestion_get_lonely_suggestion(const struct Suggestion *ptr); 348 | 349 | char *riti_suggestion_get_auxiliary_text(const struct Suggestion *ptr); 350 | 351 | /* 352 | Get the pre-edit text from the list of the `index'. 353 | 354 | This returns the lone suggestion if the suggestion is a lonely one. 355 | 356 | The main purpose of the function is to convert the returning suggestion into 357 | the ANSI encoding if it was specified when the instance of this `Suggestion` 358 | was created. 359 | */ 360 | char *riti_suggestion_get_pre_edit_text(const struct Suggestion *ptr, 361 | uintptr_t index); 362 | 363 | /* 364 | Free the allocated string. 365 | */ 366 | void riti_string_free(char *ptr); 367 | 368 | /* 369 | Returns index of the suggestion, which was previously selected. 370 | */ 371 | uintptr_t riti_suggestion_previously_selected_index(const struct Suggestion *ptr); 372 | 373 | uintptr_t riti_suggestion_get_length(const struct Suggestion *ptr); 374 | 375 | /* 376 | Returns `true` when the `Suggestion` struct is a **lonely** one, otherwise returns `false`. 377 | 378 | A *lonely* `Suggestion` struct means that the struct has only one suggestion. 379 | */ 380 | bool riti_suggestion_is_lonely(const struct Suggestion *ptr); 381 | 382 | bool riti_suggestion_is_empty(const struct Suggestion *ptr); 383 | 384 | /* 385 | Creates a new instance of Config which is used to initialize 386 | and to control the configuration of RitiContext. 387 | 388 | This function creates an instance of Config in an initial 389 | state which can't be used before populating the Config using 390 | `riti_config_set_*` set of functions. 391 | */ 392 | struct Config *riti_config_new(void); 393 | 394 | /* 395 | Free the allocated Config struct. 396 | */ 397 | void riti_config_free(struct Config *ptr); 398 | 399 | /* 400 | Sets the layout file path. 401 | For Avro Phonetic, it accepts the name `avro_phonetic`. 402 | 403 | Returns `true` if the layout file path or name is valid. 404 | */ 405 | bool riti_config_set_layout_file(struct Config *ptr, const char *path); 406 | 407 | /* 408 | Sets the database directory path. 409 | 410 | Returns `true` if the path exists. 411 | */ 412 | bool riti_config_set_database_dir(struct Config *ptr, const char *path); 413 | 414 | /* 415 | Sets the user specific writable directory path. 416 | 417 | Returns `true` if the path exists. 418 | */ 419 | bool riti_config_set_user_dir(struct Config *ptr, const char *path); 420 | 421 | void riti_config_set_suggestion_include_english(struct Config *ptr, 422 | bool option); 423 | 424 | void riti_config_set_phonetic_suggestion(struct Config *ptr, bool option); 425 | 426 | void riti_config_set_fixed_suggestion(struct Config *ptr, bool option); 427 | 428 | void riti_config_set_fixed_auto_vowel(struct Config *ptr, bool option); 429 | 430 | void riti_config_set_fixed_auto_chandra(struct Config *ptr, bool option); 431 | 432 | void riti_config_set_fixed_traditional_kar(struct Config *ptr, bool option); 433 | 434 | void riti_config_set_fixed_old_reph(struct Config *ptr, bool option); 435 | 436 | void riti_config_set_fixed_numpad(struct Config *ptr, bool option); 437 | 438 | void riti_config_set_fixed_old_kar_order(struct Config *ptr, bool option); 439 | 440 | void riti_config_set_ansi_encoding(struct Config *ptr, bool option); 441 | 442 | void riti_config_set_smart_quote(struct Config *ptr, bool option); 443 | 444 | #ifdef __cplusplus 445 | } // extern "C" 446 | #endif // __cplusplus 447 | 448 | #endif /* RITI_H */ 449 | -------------------------------------------------------------------------------- /src/suggestion.rs: -------------------------------------------------------------------------------- 1 | use edit_distance::edit_distance; 2 | use poriborton::bijoy2000::unicode_to_bijoy; 3 | use std::cmp::Ordering; 4 | 5 | /// Suggestions which are intended to be shown by the IM's candidate window. 6 | /// Suggestion is of two variants, the 'Full' one includes a list of suggestion and 7 | /// the 'Single' one is just a String. 8 | #[derive(Debug)] 9 | pub enum Suggestion { 10 | Full { 11 | auxiliary: String, 12 | suggestions: Vec, 13 | // Index of the last selected suggestion. 14 | selection: usize, 15 | // ANSI output 16 | ansi: bool, 17 | }, 18 | Single { 19 | suggestion: String, 20 | // ANSI output 21 | ansi: bool, 22 | }, 23 | } 24 | 25 | impl Suggestion { 26 | /// Creates a new `Suggestion` struct with given arguments. 27 | /// 28 | /// `auxiliary`: The auxiliary text. 29 | /// 30 | /// `suggestions`: Vector of suggestions. 31 | /// 32 | /// `selection`: Index of the last selected suggestion. 33 | /// 34 | /// `ansi`: Enable ANSI encoding conversion. 35 | pub fn new(auxiliary: String, suggestions: &[Rank], selection: usize, ansi: bool) -> Self { 36 | Self::Full { 37 | auxiliary, 38 | suggestions: suggestions 39 | .iter() 40 | .map(|r| r.to_string().to_owned()) 41 | .collect(), 42 | selection, 43 | ansi, 44 | } 45 | } 46 | 47 | /// Creates a new `Suggestion` struct with only one suggestion. 48 | /// 49 | /// *A lonely suggestion.* 😁 50 | /// 51 | /// `suggestion`: The suggestion. 52 | /// 53 | /// `ansi`: Enable ANSI encoding conversion. 54 | pub fn new_lonely(suggestion: String, ansi: bool) -> Self { 55 | Self::Single { suggestion, ansi } 56 | } 57 | 58 | /// Constructs an empty `Suggestion` struct. 59 | pub fn empty() -> Self { 60 | Self::Single { 61 | suggestion: String::new(), 62 | ansi: false, 63 | } 64 | } 65 | 66 | /// Returns `true` when the `Suggestion` struct is a **lonely** one, otherwise returns `false`. 67 | /// 68 | /// A *lonely* `Suggestion` struct means that the struct has only one suggestion. 69 | pub fn is_lonely(&self) -> bool { 70 | matches!(&self, Self::Single { .. }) 71 | } 72 | 73 | /// Returns `true` if the `Suggestion` struct is empty. 74 | pub fn is_empty(&self) -> bool { 75 | match &self { 76 | Self::Full { suggestions, .. } => suggestions.is_empty(), 77 | Self::Single { suggestion, .. } => suggestion.is_empty(), 78 | } 79 | } 80 | 81 | /// Get the suggestions as an iterator. 82 | pub fn get_suggestions(&self) -> &[String] { 83 | match &self { 84 | Self::Full { suggestions, .. } => suggestions, 85 | _ => panic!(), 86 | } 87 | } 88 | 89 | /// Get the only suggestion of the *lonely* `Suggestion`. 90 | pub fn get_lonely_suggestion(&self) -> &str { 91 | match &self { 92 | Self::Single { suggestion, .. } => suggestion, 93 | _ => panic!(), 94 | } 95 | } 96 | 97 | /// Get the auxiliary text. 98 | pub fn get_auxiliary_text(&self) -> &str { 99 | match &self { 100 | Self::Full { auxiliary, .. } => auxiliary, 101 | _ => panic!(), 102 | } 103 | } 104 | 105 | /// Get the pre-edit text from the list of the `index'. 106 | /// 107 | /// This returns the lone suggestion if the suggestion is a lonely one. 108 | /// 109 | /// The main purpose of the function is to convert the returning suggestion into 110 | /// the ANSI encoding if it was specified when the instance of this `Suggestion` 111 | /// was created. 112 | pub fn get_pre_edit_text(&self, index: usize) -> String { 113 | match self { 114 | Self::Full { 115 | suggestions, ansi, .. 116 | } if *ansi => unicode_to_bijoy(&suggestions[index]), 117 | Self::Full { suggestions, .. } => suggestions[index].to_owned(), 118 | Self::Single { suggestion, ansi } if *ansi => unicode_to_bijoy(suggestion), 119 | Self::Single { suggestion, .. } => suggestion.clone(), 120 | } 121 | } 122 | 123 | /// Returns index of the suggestion, which was previously selected. 124 | pub fn previously_selected_index(&self) -> usize { 125 | match &self { 126 | Self::Full { selection, .. } => *selection, 127 | _ => panic!(), 128 | } 129 | } 130 | 131 | /// Get the length of the suggestions contained. 132 | pub fn len(&self) -> usize { 133 | match &self { 134 | Self::Full { suggestions, .. } => suggestions.len(), 135 | _ => panic!(), 136 | } 137 | } 138 | } 139 | 140 | #[derive(Clone, Debug)] 141 | pub enum Rank { 142 | First(String), 143 | Emoji(String, u8), 144 | Other(String, u8), 145 | Last(String, u8), 146 | } 147 | 148 | impl Rank { 149 | /// Returns the suggestion item. 150 | pub(crate) fn to_string(&self) -> &str { 151 | match self { 152 | Rank::First(s) => s, 153 | Rank::Emoji(s, _) => s, 154 | Rank::Other(s, _) => s, 155 | Rank::Last(s, _) => s, 156 | } 157 | } 158 | 159 | /// A first ranked suggestion. 160 | pub(crate) fn first_ranked(item: String) -> Self { 161 | Rank::First(item) 162 | } 163 | 164 | /// A suggestion with a ranking calculated according to the `base` word. 165 | /// 166 | /// Uses edit distance to rank the `item`. 167 | pub(crate) fn new_suggestion(item: String, base: &str) -> Self { 168 | let distance = edit_distance(base, &item) * 10; 169 | Rank::Other(item, distance as u8) 170 | } 171 | 172 | /// An Emoji suggestion. 173 | pub(crate) fn emoji(item: String) -> Self { 174 | Rank::Emoji(item, 1) 175 | } 176 | 177 | /// An Emoji suggestion with custom ranking. 178 | pub(crate) fn emoji_ranked(item: String, rank: u8) -> Self { 179 | Rank::Emoji(item, rank) 180 | } 181 | 182 | /// A suggestion with a low `rank` ranking. 183 | pub(crate) fn last_ranked(item: String, rank: u8) -> Self { 184 | Rank::Last(item, rank) 185 | } 186 | 187 | /// Gives a mutable reference of the Rank's item. 188 | pub(crate) fn change_item(&mut self) -> &mut String { 189 | match self { 190 | Rank::First(s) => s, 191 | Rank::Emoji(s, _) => s, 192 | Rank::Other(s, _) => s, 193 | Rank::Last(s, _) => s, 194 | } 195 | } 196 | } 197 | 198 | impl PartialEq<&str> for Rank { 199 | fn eq(&self, other: &&str) -> bool { 200 | match self { 201 | Rank::First(s) => s == other, 202 | Rank::Emoji(s, _) => s == other, 203 | Rank::Other(s, _) => s == other, 204 | Rank::Last(s, _) => s == other, 205 | } 206 | } 207 | } 208 | 209 | impl Ord for Rank { 210 | fn cmp(&self, other: &Self) -> Ordering { 211 | match (self, other) { 212 | (Rank::First(_), Rank::First(_)) => Ordering::Equal, 213 | (Rank::First(_), Rank::Emoji(_, _)) => Ordering::Less, 214 | (Rank::Emoji(_, _), Rank::First(_)) => Ordering::Greater, 215 | (Rank::First(_), Rank::Other(_, _)) => Ordering::Less, 216 | (Rank::Other(_, _), Rank::First(_)) => Ordering::Greater, 217 | (Rank::First(_), Rank::Last(_, _)) => Ordering::Less, 218 | (Rank::Last(_, _), Rank::First(_)) => Ordering::Greater, 219 | 220 | (Rank::Emoji(_, _), Rank::Emoji(_, _)) => Ordering::Equal, 221 | (Rank::Emoji(_, e), Rank::Other(_, s)) => e.cmp(s), 222 | (Rank::Other(_, s), Rank::Emoji(_, e)) => s.cmp(e), 223 | (Rank::Emoji(_, _), Rank::Last(_, _)) => Ordering::Less, 224 | (Rank::Last(_, _), Rank::Emoji(_, _)) => Ordering::Greater, 225 | 226 | (Rank::Other(_, s1), Rank::Other(_, s2)) => s1.cmp(s2), 227 | (Rank::Other(_, _), Rank::Last(_, _)) => Ordering::Less, 228 | (Rank::Last(_, _), Rank::Other(_, _)) => Ordering::Greater, 229 | 230 | (Rank::Last(_, s1), Rank::Last(_, s2)) => s1.cmp(s2), 231 | } 232 | } 233 | } 234 | 235 | impl PartialOrd for Rank { 236 | fn partial_cmp(&self, other: &Self) -> Option { 237 | Some(self.cmp(other)) 238 | } 239 | } 240 | 241 | impl PartialEq for Rank { 242 | fn eq(&self, other: &Self) -> bool { 243 | self.to_string() == other.to_string() 244 | } 245 | } 246 | 247 | impl Eq for Rank {} 248 | 249 | #[cfg(test)] 250 | mod tests { 251 | use super::*; 252 | 253 | #[test] 254 | fn test_ansi_encoding() { 255 | let suggestion = Suggestion::new( 256 | "test".to_owned(), 257 | &[Rank::first_ranked("হাই".to_owned())], 258 | 0, 259 | true, 260 | ); 261 | assert_eq!(suggestion.get_pre_edit_text(0), "nvB"); 262 | 263 | let suggestion = Suggestion::new( 264 | "test".to_owned(), 265 | &[Rank::first_ranked("হাই".to_owned())], 266 | 0, 267 | false, 268 | ); 269 | assert_eq!(suggestion.get_pre_edit_text(0), "হাই"); 270 | 271 | let suggestion = Suggestion::new_lonely("হাই".to_owned(), true); 272 | assert_eq!(suggestion.get_pre_edit_text(0), "nvB"); 273 | 274 | let suggestion = Suggestion::new_lonely("হাই".to_owned(), false); 275 | assert_eq!(suggestion.get_pre_edit_text(0), "হাই"); 276 | } 277 | 278 | #[test] 279 | fn test_rank_trait_impl() { 280 | let r = Rank::Emoji("Happy".to_owned(), 1); 281 | assert_eq!(r, "Happy"); 282 | 283 | let mut vr1 = vec![ 284 | Rank::Last(":)".to_owned(), 2), 285 | Rank::Last("Thanks!".to_owned(), 1), 286 | Rank::Other("my".to_owned(), 10), 287 | Rank::Other("friend!".to_owned(), 20), 288 | Rank::First("Hello".to_owned()), 289 | Rank::Emoji("✋".to_owned(), 1), 290 | ]; 291 | vr1.sort_unstable(); 292 | assert_eq!( 293 | vr1, 294 | vec![ 295 | Rank::First("Hello".to_owned()), 296 | Rank::Emoji("✋".to_owned(), 1), 297 | Rank::Other("my".to_owned(), 10), 298 | Rank::Other("friend!".to_owned(), 20), 299 | Rank::Last("Thanks!".to_owned(), 1), 300 | Rank::Last(":)".to_owned(), 2) 301 | ] 302 | ); 303 | assert_eq!(vr1, ["Hello", "✋", "my", "friend!", "Thanks!", ":)"]); 304 | } 305 | 306 | #[test] 307 | fn test_ranked_sort() { 308 | let mut suggestion: Vec = ["ফইড়ে", "ফীরে", "ফিরে"] 309 | .iter() 310 | .map(|&s| Rank::new_suggestion(s.to_owned(), "ফিরে")) 311 | .collect(); 312 | suggestion.push(Rank::emoji("🔥".to_owned())); 313 | suggestion.sort_unstable(); 314 | assert_eq!(suggestion, ["ফিরে", "🔥", "ফীরে", "ফইড়ে"]); 315 | 316 | suggestion = ["অ্যা", "অ্যাঁ", "আ", "আঃ", "া", "এ"] 317 | .iter() 318 | .map(|&s| Rank::new_suggestion(s.to_owned(), "আ")) 319 | .collect(); 320 | suggestion.push(Rank::emoji("🅰️".to_owned())); 321 | suggestion.sort_unstable(); 322 | assert_eq!(suggestion, ["আ", "🅰️", "আঃ", "া", "এ", "অ্যা", "অ্যাঁ"]); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /riti.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | /* generated with cargo-xcode 1.11.0 */ 4 | archiveVersion = 1; 5 | classes = { 6 | }; 7 | objectVersion = 53; 8 | objects = { 9 | 10 | /* Begin PBXBuildFile section */ 11 | CA00132A29EB1056C3DE07A2 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CAF95239D67F3EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--lib"; }; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXBuildRule section */ 15 | CAF45239D67FAC6C1400ACA8 /* PBXBuildRule */ = { 16 | isa = PBXBuildRule; 17 | compilerSpec = com.apple.compilers.proxy.script; 18 | dependencyFile = "$(DERIVED_FILE_DIR)/$(ARCHS)-$(EXECUTABLE_NAME).d"; 19 | filePatterns = "*/Cargo.toml"; 20 | fileType = pattern.proxy; 21 | inputFiles = ( 22 | ); 23 | isEditable = 0; 24 | name = "Cargo project build"; 25 | outputFiles = ( 26 | "$(TARGET_BUILD_DIR)/$(EXECUTABLE_NAME)", 27 | ); 28 | runOncePerArchitecture = 0; 29 | script = "# generated with cargo-xcode 1.11.0\nset -euo pipefail;\nexport PATH=\"$HOME/.cargo/bin:$PATH:/usr/local/bin:/opt/homebrew/bin\";\n# don't use ios/watchos linker for build scripts and proc macros\nexport CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=/usr/bin/ld\nexport CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER=/usr/bin/ld\nexport NO_COLOR=1\n\ncase \"$PLATFORM_NAME\" in\n \"macosx\")\n CARGO_XCODE_TARGET_OS=darwin\n if [ \"${IS_MACCATALYST-NO}\" = YES ]; then\n CARGO_XCODE_TARGET_OS=ios-macabi\n fi\n ;;\n \"iphoneos\") CARGO_XCODE_TARGET_OS=ios ;;\n \"iphonesimulator\") CARGO_XCODE_TARGET_OS=ios-sim ;;\n \"appletvos\" | \"appletvsimulator\") CARGO_XCODE_TARGET_OS=tvos ;;\n \"watchos\") CARGO_XCODE_TARGET_OS=watchos ;;\n \"watchsimulator\") CARGO_XCODE_TARGET_OS=watchos-sim ;;\n \"xros\") CARGO_XCODE_TARGET_OS=visionos ;;\n \"xrsimulator\") CARGO_XCODE_TARGET_OS=visionos-sim ;;\n *)\n CARGO_XCODE_TARGET_OS=\"$PLATFORM_NAME\"\n echo >&2 \"warning: cargo-xcode needs to be updated to handle $PLATFORM_NAME\"\n ;;\nesac\n\nCARGO_XCODE_TARGET_TRIPLES=\"\"\nCARGO_XCODE_TARGET_FLAGS=\"\"\nLIPO_ARGS=\"\"\nfor arch in $ARCHS; do\n if [[ \"$arch\" == \"arm64\" ]]; then arch=aarch64; fi\n if [[ \"$arch\" == \"i386\" && \"$CARGO_XCODE_TARGET_OS\" != \"ios\" ]]; then arch=i686; fi\n triple=\"${arch}-apple-$CARGO_XCODE_TARGET_OS\"\n CARGO_XCODE_TARGET_TRIPLES+=\" $triple\"\n CARGO_XCODE_TARGET_FLAGS+=\" --target=$triple\"\n LIPO_ARGS+=\"$CARGO_TARGET_DIR/$triple/$CARGO_XCODE_BUILD_PROFILE/$CARGO_XCODE_CARGO_FILE_NAME\n\"\ndone\n\necho >&2 \"Cargo $CARGO_XCODE_BUILD_PROFILE $ACTION for $PLATFORM_NAME $ARCHS =$CARGO_XCODE_TARGET_TRIPLES; using ${SDK_NAMES:-}. \\$PATH is:\"\ntr >&2 : '\\n' <<<\"$PATH\"\n\nif command -v rustup &> /dev/null; then\n for triple in $CARGO_XCODE_TARGET_TRIPLES; do\n if ! rustup target list --installed | grep -Eq \"^$triple$\"; then\n echo >&2 \"warning: this build requires rustup toolchain for $triple, but it isn't installed (will try rustup next)\"\n rustup target add \"$triple\" || {\n echo >&2 \"warning: can't install $triple, will try nightly -Zbuild-std\";\n OTHER_INPUT_FILE_FLAGS+=\" -Zbuild-std\";\n if [ -z \"${RUSTUP_TOOLCHAIN:-}\" ]; then\n export RUSTUP_TOOLCHAIN=nightly\n fi\n break;\n }\n fi\n done\nfi\n\nif [ \"$CARGO_XCODE_BUILD_PROFILE\" = release ]; then\n OTHER_INPUT_FILE_FLAGS=\"$OTHER_INPUT_FILE_FLAGS --release\"\nfi\n\nif [ \"$ACTION\" = clean ]; then\n cargo clean --verbose --manifest-path=\"$SCRIPT_INPUT_FILE\" $CARGO_XCODE_TARGET_FLAGS $OTHER_INPUT_FILE_FLAGS;\n rm -f \"$SCRIPT_OUTPUT_FILE_0\"\n exit 0\nfi\n\n{ cargo build --manifest-path=\"$SCRIPT_INPUT_FILE\" --features=\"${CARGO_XCODE_FEATURES:-}\" $CARGO_XCODE_TARGET_FLAGS $OTHER_INPUT_FILE_FLAGS --verbose --message-format=short 2>&1 | sed -E 's/^([^ :]+:[0-9]+:[0-9]+: error)/\\1: /' >&2; } || { echo >&2 \"$SCRIPT_INPUT_FILE: error: cargo-xcode project build failed; $CARGO_XCODE_TARGET_TRIPLES\"; exit 1; }\n\ntr '\\n' '\\0' <<<\"$LIPO_ARGS\" | xargs -0 lipo -create -output \"$SCRIPT_OUTPUT_FILE_0\"\n\nif [ ${LD_DYLIB_INSTALL_NAME:+1} ]; then\n install_name_tool -id \"$LD_DYLIB_INSTALL_NAME\" \"$SCRIPT_OUTPUT_FILE_0\"\nfi\n\nDEP_FILE_DST=\"$DERIVED_FILE_DIR/${ARCHS}-${EXECUTABLE_NAME}.d\"\necho \"\" > \"$DEP_FILE_DST\"\nfor triple in $CARGO_XCODE_TARGET_TRIPLES; do\n BUILT_SRC=\"$CARGO_TARGET_DIR/$triple/$CARGO_XCODE_BUILD_PROFILE/$CARGO_XCODE_CARGO_FILE_NAME\"\n\n # cargo generates a dep file, but for its own path, so append our rename to it\n DEP_FILE_SRC=\"$CARGO_TARGET_DIR/$triple/$CARGO_XCODE_BUILD_PROFILE/$CARGO_XCODE_CARGO_DEP_FILE_NAME\"\n if [ -f \"$DEP_FILE_SRC\" ]; then\n cat \"$DEP_FILE_SRC\" >> \"$DEP_FILE_DST\"\n fi\n echo >> \"$DEP_FILE_DST\" \"${SCRIPT_OUTPUT_FILE_0/ /\\\\ /}: ${BUILT_SRC/ /\\\\ /}\"\ndone\ncat \"$DEP_FILE_DST\"\n\necho \"success: $ACTION of $SCRIPT_OUTPUT_FILE_0 for $CARGO_XCODE_TARGET_TRIPLES\"\n"; 30 | }; 31 | /* End PBXBuildRule section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | CA00B9937C4654FCDE10A9EB /* libriti.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libriti.a; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | CAF95239D67F3EF4668187A5 /* Cargo.toml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cargo.toml; path = Cargo.toml; sourceTree = ""; }; 36 | /* End PBXFileReference section */ 37 | 38 | /* Begin PBXGroup section */ 39 | CAF05239D67FD65BC3C892A8 = { 40 | isa = PBXGroup; 41 | children = ( 42 | CAF95239D67F3EF4668187A5 /* Cargo.toml */, 43 | CAF15239D67F22869D176AE5 /* Products */, 44 | CAF25239D67F98AF0B5890DB /* Frameworks */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | CAF15239D67F22869D176AE5 /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | CA00B9937C4654FCDE10A9EB /* libriti.a */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | CAF25239D67F98AF0B5890DB /* Frameworks */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | ); 60 | name = Frameworks; 61 | sourceTree = ""; 62 | }; 63 | /* End PBXGroup section */ 64 | 65 | /* Begin PBXNativeTarget section */ 66 | CA00B9937C461056C3DE07A2 /* riti.a (static library) */ = { 67 | isa = PBXNativeTarget; 68 | buildConfigurationList = CA005981F82A1056C3DE07A2 /* Build configuration list for PBXNativeTarget "riti.a (static library)" */; 69 | buildPhases = ( 70 | CA00AEF670C21056C3DE07A2 /* Sources */, 71 | ); 72 | buildRules = ( 73 | CAF45239D67FAC6C1400ACA8 /* PBXBuildRule */, 74 | ); 75 | dependencies = ( 76 | ); 77 | name = "riti.a (static library)"; 78 | productName = libriti.a; 79 | productReference = CA00B9937C4654FCDE10A9EB /* libriti.a */; 80 | productType = "com.apple.product-type.library.static"; 81 | }; 82 | /* End PBXNativeTarget section */ 83 | 84 | /* Begin PBXProject section */ 85 | CAF35239D67FE04653AD465F /* Project object */ = { 86 | isa = PBXProject; 87 | attributes = { 88 | BuildIndependentTargetsInParallel = YES; 89 | LastUpgradeCheck = 1510; 90 | TargetAttributes = { 91 | CA00B9937C461056C3DE07A2 = { 92 | CreatedOnToolsVersion = 9.2; 93 | ProvisioningStyle = Automatic; 94 | }; 95 | }; 96 | }; 97 | buildConfigurationList = CAF65239D67F80E02D6C7F57 /* Build configuration list for PBXProject "riti" */; 98 | compatibilityVersion = "Xcode 11.4"; 99 | developmentRegion = en; 100 | hasScannedForEncodings = 0; 101 | knownRegions = ( 102 | en, 103 | Base, 104 | ); 105 | mainGroup = CAF05239D67FD65BC3C892A8; 106 | productRefGroup = CAF15239D67F22869D176AE5 /* Products */; 107 | projectDirPath = ""; 108 | projectRoot = ""; 109 | targets = ( 110 | CA00B9937C461056C3DE07A2 /* riti.a (static library) */, 111 | ); 112 | }; 113 | /* End PBXProject section */ 114 | 115 | /* Begin PBXSourcesBuildPhase section */ 116 | CA00AEF670C21056C3DE07A2 /* Sources */ = { 117 | isa = PBXSourcesBuildPhase; 118 | buildActionMask = 2147483647; 119 | files = ( 120 | CA00132A29EB1056C3DE07A2 /* Cargo.toml in Sources */, 121 | ); 122 | runOnlyForDeploymentPostprocessing = 0; 123 | }; 124 | /* End PBXSourcesBuildPhase section */ 125 | 126 | /* Begin XCBuildConfiguration section */ 127 | CA00A0F7F57D1056C3DE07A2 /* Release */ = { 128 | isa = XCBuildConfiguration; 129 | buildSettings = { 130 | CARGO_XCODE_CARGO_DEP_FILE_NAME = libriti.d; 131 | CARGO_XCODE_CARGO_FILE_NAME = libriti.a; 132 | INSTALL_GROUP = ""; 133 | INSTALL_MODE_FLAG = ""; 134 | INSTALL_OWNER = ""; 135 | PRODUCT_NAME = riti; 136 | SKIP_INSTALL = YES; 137 | SUPPORTED_PLATFORMS = "xrsimulator xros watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos"; 138 | }; 139 | name = Release; 140 | }; 141 | CA004EBF41001056C3DE07A2 /* Debug */ = { 142 | isa = XCBuildConfiguration; 143 | buildSettings = { 144 | CARGO_XCODE_CARGO_DEP_FILE_NAME = libriti.d; 145 | CARGO_XCODE_CARGO_FILE_NAME = libriti.a; 146 | INSTALL_GROUP = ""; 147 | INSTALL_MODE_FLAG = ""; 148 | INSTALL_OWNER = ""; 149 | PRODUCT_NAME = riti; 150 | SKIP_INSTALL = YES; 151 | SUPPORTED_PLATFORMS = "xrsimulator xros watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos"; 152 | }; 153 | name = Debug; 154 | }; 155 | CAF7FE07D4CF3CC16B37690B /* Release */ = { 156 | isa = XCBuildConfiguration; 157 | buildSettings = { 158 | "ADDITIONAL_SDKS[sdk=i*]" = macosx; 159 | "ADDITIONAL_SDKS[sdk=w*]" = macosx; 160 | "ADDITIONAL_SDKS[sdk=x*]" = macosx; 161 | "ADDITIONAL_SDKS[sdk=a*]" = macosx; 162 | ALWAYS_SEARCH_USER_PATHS = NO; 163 | CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target"; 164 | CARGO_XCODE_BUILD_PROFILE = release; 165 | CARGO_XCODE_FEATURES = ""; 166 | CURRENT_PROJECT_VERSION = 0.1; 167 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 168 | MARKETING_VERSION = 0.1.0; 169 | PRODUCT_NAME = riti; 170 | RUSTUP_TOOLCHAIN = ""; 171 | SDKROOT = macosx; 172 | SUPPORTS_MACCATALYST = YES; 173 | }; 174 | name = Release; 175 | }; 176 | CAF8FE07D4CF228BE02872F8 /* Debug */ = { 177 | isa = XCBuildConfiguration; 178 | buildSettings = { 179 | "ADDITIONAL_SDKS[sdk=i*]" = macosx; 180 | "ADDITIONAL_SDKS[sdk=w*]" = macosx; 181 | "ADDITIONAL_SDKS[sdk=x*]" = macosx; 182 | "ADDITIONAL_SDKS[sdk=a*]" = macosx; 183 | ALWAYS_SEARCH_USER_PATHS = NO; 184 | CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target"; 185 | CARGO_XCODE_BUILD_PROFILE = debug; 186 | CARGO_XCODE_FEATURES = ""; 187 | CURRENT_PROJECT_VERSION = 0.1; 188 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 189 | MARKETING_VERSION = 0.1.0; 190 | ONLY_ACTIVE_ARCH = YES; 191 | PRODUCT_NAME = riti; 192 | RUSTUP_TOOLCHAIN = ""; 193 | SDKROOT = macosx; 194 | SUPPORTS_MACCATALYST = YES; 195 | }; 196 | name = Debug; 197 | }; 198 | /* End XCBuildConfiguration section */ 199 | 200 | /* Begin XCConfigurationList section */ 201 | CA005981F82A1056C3DE07A2 /* Build configuration list for PBXNativeTarget "riti.a (static library)" */ = { 202 | isa = XCConfigurationList; 203 | buildConfigurations = ( 204 | CA00A0F7F57D1056C3DE07A2 /* Release */, 205 | CA004EBF41001056C3DE07A2 /* Debug */, 206 | ); 207 | defaultConfigurationIsVisible = 0; 208 | defaultConfigurationName = Release; 209 | }; 210 | CAF65239D67F80E02D6C7F57 /* Build configuration list for PBXProject "riti" */ = { 211 | isa = XCConfigurationList; 212 | buildConfigurations = ( 213 | CAF7FE07D4CF3CC16B37690B /* Release */, 214 | CAF8FE07D4CF228BE02872F8 /* Debug */, 215 | ); 216 | defaultConfigurationIsVisible = 0; 217 | defaultConfigurationName = Release; 218 | }; 219 | /* End XCConfigurationList section */ 220 | }; 221 | rootObject = CAF35239D67FE04653AD465F /* Project object */; 222 | } 223 | -------------------------------------------------------------------------------- /src/utility.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, fs::File, io::Read, ops::Deref}; 2 | 3 | use crate::context::{MODIFIER_ALT_GR, MODIFIER_SHIFT}; 4 | 5 | /// Some utility functions which we implement on the `char` type. 6 | pub(crate) trait Utility { 7 | /// Checks the char for a vowel character. 8 | fn is_vowel(&self) -> bool; 9 | /// Checks the char for a kar character. 10 | fn is_kar(&self) -> bool; 11 | /// Checks the char for a pure consonant character. 12 | fn is_pure_consonant(&self) -> bool; 13 | } 14 | 15 | impl Utility for char { 16 | /// Checks the char for a vowel character. 17 | fn is_vowel(&self) -> bool { 18 | "\u{0985}\u{0986}\u{0987}\u{0988}\u{0989}\u{098A}\u{098B}\u{098F}\u{0990}\u{0993}\u{0994}\u{098C}\u{09E1}\u{09BE}\u{09BF}\u{09C0}\u{09C1}\u{09C2}\u{09C3}\u{09C7}\u{09C8}\u{09CB}\u{09CC}".contains(*self) 19 | } 20 | 21 | /// Checks the char for a kar character. 22 | fn is_kar(&self) -> bool { 23 | "\u{09BE}\u{09BF}\u{09C0}\u{09C1}\u{09C2}\u{09C3}\u{09C7}\u{09C8}\u{09CB}\u{09CC}\u{09C4}" 24 | .contains(*self) 25 | } 26 | 27 | /// Checks the char for a pure consonant character. 28 | fn is_pure_consonant(&self) -> bool { 29 | "\u{0995}\u{0996}\u{0997}\u{0998}\u{0999}\u{099A}\u{099B}\u{099C}\u{099D}\u{099E}\u{099F}\u{09A0}\u{09A1}\u{09A2}\u{09A3}\u{09A4}\u{09A5}\u{09A6}\u{09A7}\u{09A8}\u{09AA}\u{09AB}\u{09AC}\u{09AD}\u{09AE}\u{09AF}\u{09B0}\u{09B2}\u{09B6}\u{09B7}\u{09B8}\u{09B9}\u{09CE}\u{09DC}\u{09DD}\u{09DF}".contains(*self) 30 | } 31 | } 32 | 33 | /// Checks if the `vec` already has the `value` before inserting. 34 | /// If it does, then the `value` is not inserted. 35 | pub(crate) fn push_checked(vec: &mut Vec, value: T) { 36 | if !vec.contains(&value) { 37 | vec.push(value); 38 | } 39 | } 40 | 41 | /// Tuple of modifier keys. 42 | /// 43 | /// First is Shift, second is AltGr. 44 | pub(crate) type Modifiers = (bool, bool); 45 | 46 | /// Returns boolean tuples of the modifiers from the bit masked integer `modifier`. 47 | /// First is Shift, second is Ctrl and third is Alt. 48 | pub(crate) fn get_modifiers(modifier: u8) -> Modifiers { 49 | let shift = (modifier & MODIFIER_SHIFT) == MODIFIER_SHIFT; 50 | let alt_gr = (modifier & MODIFIER_ALT_GR) == MODIFIER_ALT_GR; 51 | 52 | (shift, alt_gr) 53 | } 54 | 55 | /// Read the entire contents of a file into a bytes vector. 56 | /// 57 | /// Optimized to allocate the required amount of capacity beforehand. 58 | pub(crate) fn read(file: &mut File) -> Vec { 59 | let len = file.metadata().map(|m| m.len() + 1).unwrap(); 60 | let mut buf = Vec::with_capacity(len as usize); 61 | file.read_to_end(&mut buf).unwrap(); 62 | buf 63 | } 64 | 65 | /// A meta characters splitted string. 66 | /// 67 | /// Meta characters (`-]~!@#%&*()_=+[{}'\";<>/?|.,।`) are splitted 68 | /// from a string as preceding and trailing parts. 69 | #[derive(Debug)] 70 | pub(crate) struct SplittedString<'a> { 71 | preceding: Cow<'a, str>, 72 | word: &'a str, 73 | trailing: Cow<'a, str>, 74 | } 75 | 76 | impl SplittedString<'_> { 77 | /// Split the string into three parts. 78 | /// This function splits preceding and trailing meta characters. 79 | /// 80 | /// `include_colon` argument controls the inclusion of colon as a trailing meta character. 81 | pub(crate) fn split(input: &str, include_colon: bool) -> SplittedString<'_> { 82 | const META: &str = "-]~!@#%&*()_=+[{}'\";<>/?|.,।"; 83 | 84 | let first_index = match input.find(|c| !META.contains(c)) { 85 | Some(i) => i, 86 | None => { 87 | // If no non-META/alphanumeric char is found, 88 | // the string has no middle or last part 89 | return SplittedString { 90 | preceding: input.into(), 91 | word: "", 92 | trailing: "".into(), 93 | }; 94 | } 95 | }; 96 | let (preceding, rest) = input.split_at(first_index); 97 | 98 | let mut escape = false; 99 | let mut last_index = rest.len(); 100 | for (i, c) in rest.char_indices().rev() { 101 | if !escape && c == '`' { 102 | // escape 103 | escape = true; // not updating the last index for escape 104 | } else if ((include_colon || escape) && c == ':') || META.contains(c) { 105 | // meta 106 | escape = false; 107 | last_index = i; 108 | } else { 109 | // alphanumeric 110 | break; 111 | } 112 | } 113 | let (word, trailing) = rest.split_at(last_index); 114 | 115 | SplittedString { 116 | preceding: preceding.into(), 117 | word, 118 | trailing: trailing.into(), 119 | } 120 | } 121 | 122 | /// Takes a closure to transform preceding and trailing parts. 123 | pub(crate) fn map(&mut self, func: impl Fn(&str, &str) -> (String, String)) { 124 | let (p, t) = (func)(self.preceding.deref(), self.trailing.deref()); 125 | self.preceding = Cow::Owned(p); 126 | self.trailing = Cow::Owned(t); 127 | } 128 | 129 | /// Returns trailing meta characters. 130 | pub(crate) fn preceding(&self) -> &str { 131 | self.preceding.deref() 132 | } 133 | 134 | /// Returns word without any preceding and trailing meta characters. 135 | pub(crate) fn word(&self) -> &str { 136 | self.word 137 | } 138 | 139 | /// Returns trailing meta characters. 140 | pub(crate) fn trailing(&self) -> &str { 141 | self.trailing.deref() 142 | } 143 | 144 | /// Returns splitted string as a tuple in `(preceding, word, trailing)` order. 145 | pub(crate) fn as_tuple(&self) -> (&str, &str, &str) { 146 | (self.preceding(), self.word(), self.trailing()) 147 | } 148 | } 149 | 150 | impl PartialEq<(&str, &str, &str)> for SplittedString<'_> { 151 | fn eq(&self, other: &(&str, &str, &str)) -> bool { 152 | self.preceding == other.0 && self.word == other.1 && self.trailing == other.2 153 | } 154 | } 155 | 156 | /// Convert preceding and trailing quotation marks(', ") into their curved form(‘, ’, “, ”) aka Smart Quote. 157 | pub(crate) fn smart_quoter(mut splitted: SplittedString) -> SplittedString { 158 | // If the middle part is empty, there is no need to convert. 159 | if splitted.word().is_empty() { 160 | return splitted; 161 | } 162 | 163 | // Convert preceding quotation mark(', ") into its curved form(‘, “). 164 | let mut preceding = String::with_capacity(splitted.preceding().len() + 3); 165 | for ch in splitted.preceding().chars() { 166 | match ch { 167 | '\'' => { 168 | preceding.push('‘'); 169 | } 170 | '"' => { 171 | preceding.push('“'); 172 | } 173 | _ => preceding.push(ch), 174 | } 175 | } 176 | 177 | // Convert trailing quotation mark(', ") into its curved form(’, ”). 178 | let mut trailing = String::with_capacity(splitted.trailing().len() + 3); 179 | for ch in splitted.trailing.chars() { 180 | match ch { 181 | '\'' => { 182 | trailing.push('’'); 183 | } 184 | '"' => { 185 | trailing.push('”'); 186 | } 187 | _ => trailing.push(ch), 188 | } 189 | } 190 | 191 | splitted.preceding = Cow::Owned(preceding); 192 | splitted.trailing = Cow::Owned(trailing); 193 | 194 | splitted 195 | } 196 | 197 | /// Clean a string by removing special characters. 198 | pub(crate) fn clean_string(string: &str) -> String { 199 | string 200 | .chars() 201 | .filter(|&c| !"|()[]{}^$*+?.~!@#%&-_='\";<>/\\,:`।\u{200C}".contains(c)) 202 | .collect() 203 | } 204 | 205 | #[cfg(test)] 206 | mod test { 207 | use super::{get_modifiers, smart_quoter, SplittedString, Utility}; 208 | use crate::context::{MODIFIER_ALT_GR, MODIFIER_SHIFT}; 209 | 210 | #[test] 211 | fn test_utilities() { 212 | assert!('আ'.is_vowel()); 213 | assert!(!'ক'.is_vowel()); 214 | assert!('া'.is_kar()); 215 | assert!(!'আ'.is_kar()); 216 | assert!('ক'.is_pure_consonant()); 217 | } 218 | 219 | #[test] 220 | fn test_get_modifiers() { 221 | assert_eq!(get_modifiers(MODIFIER_SHIFT), (true, false)); 222 | assert_eq!(get_modifiers(MODIFIER_ALT_GR), (false, true)); 223 | assert_eq!( 224 | get_modifiers(MODIFIER_SHIFT | MODIFIER_ALT_GR), 225 | (true, true) 226 | ); 227 | } 228 | 229 | #[test] 230 | fn test_splitted_string() { 231 | assert_eq!( 232 | SplittedString::split("[][][][]", false), 233 | ("[][][][]", "", "") 234 | ); 235 | assert_eq!(SplittedString::split("t*", false), ("", "t", "*")); 236 | assert_eq!(SplittedString::split("1", false), ("", "1", "")); 237 | assert_eq!( 238 | SplittedString::split("#\"percent%sign\"#", false), 239 | ("#\"", "percent%sign", "\"#") 240 | ); 241 | assert_eq!(SplittedString::split("*[মেটা]*", false), ("*[", "মেটা", "]*")); 242 | assert_eq!(SplittedString::split("text", false), ("", "text", "")); 243 | assert_eq!(SplittedString::split("kt:", false), ("", "kt:", "")); 244 | assert_eq!(SplittedString::split("kt:", true), ("", "kt", ":")); 245 | assert_eq!(SplittedString::split("kt:`", false), ("", "kt", ":`")); 246 | assert_eq!(SplittedString::split("kt:`", true), ("", "kt", ":`")); 247 | assert_eq!(SplittedString::split("kt::`", false), ("", "kt:", ":`")); 248 | assert_eq!(SplittedString::split("kt::`", true), ("", "kt", "::`")); 249 | assert_eq!(SplittedString::split("kt``", false), ("", "kt``", "")); 250 | assert_eq!(SplittedString::split("kt:``", false), ("", "kt:``", "")); 251 | assert_eq!( 252 | SplittedString::split("।ঃমেঃ।টাঃ।", false), 253 | ("।", "ঃমেঃ।টাঃ", "।") 254 | ); 255 | } 256 | 257 | #[test] 258 | fn test_smart_quoting() { 259 | assert_eq!( 260 | smart_quoter(SplittedString::split("\"", true)), 261 | ("\"".into(), "", "".into()) 262 | ); 263 | 264 | assert_eq!( 265 | smart_quoter(SplittedString::split("'Till", true)), 266 | ("‘".into(), "Till", "".into()) 267 | ); 268 | assert_eq!( 269 | smart_quoter(SplittedString::split("\"Hey", true)), 270 | ("“".into(), "Hey", "".into()) 271 | ); 272 | assert_eq!( 273 | smart_quoter(SplittedString::split("'\"Hey", true)), 274 | ("‘“".into(), "Hey", "".into()) 275 | ); 276 | 277 | assert_eq!( 278 | smart_quoter(SplittedString::split("finished'", true)), 279 | ("".into(), "finished", "’".into()) 280 | ); 281 | assert_eq!( 282 | smart_quoter(SplittedString::split("Hey\"", true)), 283 | ("".into(), "Hey", "”".into()) 284 | ); 285 | assert_eq!( 286 | smart_quoter(SplittedString::split("Hey'\"", true)), 287 | ("".into(), "Hey", "’”".into()) 288 | ); 289 | 290 | assert_eq!( 291 | smart_quoter(SplittedString::split("'Awkward'", true)), 292 | ("‘".into(), "Awkward", "’".into()) 293 | ); 294 | assert_eq!( 295 | smart_quoter(SplittedString::split("\"Nevertheless\"", true)), 296 | ("“".into(), "Nevertheless", "”".into()) 297 | ); 298 | 299 | assert_eq!( 300 | smart_quoter(SplittedString::split("\"'Quotation'\"", true)), 301 | ("“‘".into(), "Quotation", "’”".into()) 302 | ); 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/ffi.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{CStr, CString}; 2 | use std::os::raw::c_char; 3 | 4 | use crate::config::Config; 5 | use crate::context::RitiContext; 6 | use crate::suggestion::Suggestion; 7 | 8 | fn riti_free(ptr: *mut T) { 9 | if !ptr.is_null() { 10 | unsafe { 11 | drop(Box::from_raw(ptr)); 12 | } 13 | } 14 | } 15 | 16 | // FFI functions for handling the `RitiContext` structure. 17 | 18 | /// Creates a new instance of RitiContext with a Config which is properly 19 | /// populated using `riti_config_set_*` set of functions. 20 | #[no_mangle] 21 | pub extern "C" fn riti_context_new_with_config(ptr: *const Config) -> *mut RitiContext { 22 | let config = unsafe { 23 | assert!(!ptr.is_null()); 24 | &*ptr 25 | }; 26 | 27 | Box::into_raw(Box::new(RitiContext::new_with_config(config))) 28 | } 29 | 30 | #[no_mangle] 31 | pub extern "C" fn riti_context_free(ptr: *mut RitiContext) { 32 | riti_free(ptr) 33 | } 34 | 35 | /// Generates suggestion for `key` press. 36 | /// 37 | /// `modifier`: state of modifier keys 38 | /// `selection`: previously selected user selection index if available otherwise `0`. 39 | /// It is used to preserve user's candidate selection if the key is a punctuation character in Phonetic method. 40 | #[no_mangle] 41 | pub extern "C" fn riti_get_suggestion_for_key( 42 | ptr: *mut RitiContext, 43 | key: u16, 44 | modifier: u8, 45 | selection: u8, 46 | ) -> *mut Suggestion { 47 | let context = unsafe { 48 | assert!(!ptr.is_null()); 49 | &*ptr 50 | }; 51 | 52 | let suggestion = context.get_suggestion_for_key(key, modifier, selection); 53 | 54 | Box::into_raw(Box::new(suggestion)) 55 | } 56 | 57 | /// A candidate of the suggestion list was committed. 58 | /// 59 | /// `index`: index of the candidate. 60 | /// 61 | /// This function will end the ongoing input session. 62 | #[no_mangle] 63 | pub extern "C" fn riti_context_candidate_committed(ptr: *mut RitiContext, index: usize) { 64 | let context = unsafe { 65 | assert!(!ptr.is_null()); 66 | &*ptr 67 | }; 68 | 69 | context.candidate_committed(index) 70 | } 71 | 72 | /// Update the suggestion making engine. This would also look for changes 73 | /// in layout selection and AutoCorrect database. 74 | #[no_mangle] 75 | pub extern "C" fn riti_context_update_engine(ptr: *mut RitiContext, config: *const Config) { 76 | let context = unsafe { 77 | assert!(!ptr.is_null()); 78 | &mut *ptr 79 | }; 80 | 81 | let config = unsafe { 82 | assert!(!config.is_null()); 83 | &*config 84 | }; 85 | 86 | context.update_engine(config) 87 | } 88 | 89 | /// Checks if there is an ongoing input session. 90 | #[no_mangle] 91 | pub extern "C" fn riti_context_ongoing_input_session(ptr: *mut RitiContext) -> bool { 92 | let context = unsafe { 93 | assert!(!ptr.is_null()); 94 | &*ptr 95 | }; 96 | 97 | context.ongoing_input_session() 98 | } 99 | 100 | /// Finish the ongoing input session if any. 101 | #[no_mangle] 102 | pub extern "C" fn riti_context_finish_input_session(ptr: *mut RitiContext) { 103 | let context = unsafe { 104 | assert!(!ptr.is_null()); 105 | &*ptr 106 | }; 107 | 108 | context.finish_input_session() 109 | } 110 | 111 | /// A BackSpace event. 112 | /// 113 | /// Returns a new `suggestion` after applying the BackSpace event. 114 | /// 115 | /// If the `ctrl` parameter is true then it deletes the whole word 116 | /// in composition currently and ends the ongoing input session. 117 | /// 118 | /// If the internal buffer becomes empty, this function will 119 | /// end the ongoing input session. 120 | #[no_mangle] 121 | pub extern "C" fn riti_context_backspace_event( 122 | ptr: *mut RitiContext, 123 | ctrl: bool, 124 | ) -> *mut Suggestion { 125 | let context = unsafe { 126 | assert!(!ptr.is_null()); 127 | &*ptr 128 | }; 129 | 130 | let suggestion = context.backspace_event(ctrl); 131 | 132 | Box::into_raw(Box::new(suggestion)) 133 | } 134 | 135 | // FFI functions for handling the `Suggestion` structure. 136 | 137 | #[no_mangle] 138 | pub extern "C" fn riti_suggestion_free(ptr: *mut Suggestion) { 139 | riti_free(ptr) 140 | } 141 | 142 | /// Get the suggestion of the `index` from suggestions. 143 | #[no_mangle] 144 | pub extern "C" fn riti_suggestion_get_suggestion( 145 | ptr: *const Suggestion, 146 | index: usize, 147 | ) -> *mut c_char { 148 | let suggestion = unsafe { 149 | assert!(!ptr.is_null()); 150 | &*ptr 151 | }; 152 | 153 | let string = suggestion.get_suggestions()[index].clone(); 154 | 155 | unsafe { CString::from_vec_unchecked(string.into()).into_raw() } 156 | } 157 | 158 | /// Get the only suggestion of the *lonely* `Suggestion`. 159 | #[no_mangle] 160 | pub extern "C" fn riti_suggestion_get_lonely_suggestion(ptr: *const Suggestion) -> *mut c_char { 161 | let suggestion = unsafe { 162 | assert!(!ptr.is_null()); 163 | &*ptr 164 | }; 165 | 166 | unsafe { CString::from_vec_unchecked(suggestion.get_lonely_suggestion().into()).into_raw() } 167 | } 168 | 169 | #[no_mangle] 170 | pub extern "C" fn riti_suggestion_get_auxiliary_text(ptr: *const Suggestion) -> *mut c_char { 171 | let suggestion = unsafe { 172 | assert!(!ptr.is_null()); 173 | &*ptr 174 | }; 175 | 176 | unsafe { CString::from_vec_unchecked(suggestion.get_auxiliary_text().into()).into_raw() } 177 | } 178 | 179 | /// Get the pre-edit text from the list of the `index'. 180 | /// 181 | /// This returns the lone suggestion if the suggestion is a lonely one. 182 | /// 183 | /// The main purpose of the function is to convert the returning suggestion into 184 | /// the ANSI encoding if it was specified when the instance of this `Suggestion` 185 | /// was created. 186 | #[no_mangle] 187 | pub extern "C" fn riti_suggestion_get_pre_edit_text( 188 | ptr: *const Suggestion, 189 | index: usize, 190 | ) -> *mut c_char { 191 | let suggestion = unsafe { 192 | assert!(!ptr.is_null()); 193 | &*ptr 194 | }; 195 | 196 | unsafe { 197 | CString::from_vec_unchecked(suggestion.get_pre_edit_text(index).into_bytes()).into_raw() 198 | } 199 | } 200 | 201 | /// Free the allocated string. 202 | #[no_mangle] 203 | pub extern "C" fn riti_string_free(ptr: *mut c_char) { 204 | if ptr.is_null() { 205 | return; 206 | } 207 | 208 | unsafe { 209 | drop(CString::from_raw(ptr)); 210 | } 211 | } 212 | 213 | /// Returns index of the suggestion, which was previously selected. 214 | #[no_mangle] 215 | pub extern "C" fn riti_suggestion_previously_selected_index(ptr: *const Suggestion) -> usize { 216 | let suggestion = unsafe { 217 | assert!(!ptr.is_null()); 218 | &*ptr 219 | }; 220 | 221 | suggestion.previously_selected_index() 222 | } 223 | 224 | #[no_mangle] 225 | pub extern "C" fn riti_suggestion_get_length(ptr: *const Suggestion) -> usize { 226 | let suggestion = unsafe { 227 | assert!(!ptr.is_null()); 228 | &*ptr 229 | }; 230 | 231 | suggestion.len() 232 | } 233 | 234 | /// Returns `true` when the `Suggestion` struct is a **lonely** one, otherwise returns `false`. 235 | /// 236 | /// A *lonely* `Suggestion` struct means that the struct has only one suggestion. 237 | #[no_mangle] 238 | pub extern "C" fn riti_suggestion_is_lonely(ptr: *const Suggestion) -> bool { 239 | let suggestion = unsafe { 240 | assert!(!ptr.is_null()); 241 | &*ptr 242 | }; 243 | 244 | suggestion.is_lonely() 245 | } 246 | 247 | #[no_mangle] 248 | pub extern "C" fn riti_suggestion_is_empty(ptr: *const Suggestion) -> bool { 249 | let suggestion = unsafe { 250 | assert!(!ptr.is_null()); 251 | &*ptr 252 | }; 253 | 254 | suggestion.is_empty() 255 | } 256 | 257 | /// Creates a new instance of Config which is used to initialize 258 | /// and to control the configuration of RitiContext. 259 | /// 260 | /// This function creates an instance of Config in an initial 261 | /// state which can't be used before populating the Config using 262 | /// `riti_config_set_*` set of functions. 263 | #[no_mangle] 264 | pub extern "C" fn riti_config_new() -> *mut Config { 265 | Box::into_raw(Box::default()) 266 | } 267 | 268 | /// Free the allocated Config struct. 269 | #[no_mangle] 270 | pub extern "C" fn riti_config_free(ptr: *mut Config) { 271 | riti_free(ptr) 272 | } 273 | 274 | /// Sets the layout file path. 275 | /// For Avro Phonetic, it accepts the name `avro_phonetic`. 276 | /// 277 | /// Returns `true` if the layout file path or name is valid. 278 | #[no_mangle] 279 | pub extern "C" fn riti_config_set_layout_file(ptr: *mut Config, path: *const c_char) -> bool { 280 | let config = unsafe { 281 | assert!(!ptr.is_null()); 282 | &mut *ptr 283 | }; 284 | 285 | unsafe { 286 | let layout = CStr::from_ptr(path).to_str().unwrap(); 287 | config.set_layout_file_path(layout) 288 | } 289 | } 290 | 291 | /// Sets the database directory path. 292 | /// 293 | /// Returns `true` if the path exists. 294 | #[no_mangle] 295 | pub extern "C" fn riti_config_set_database_dir(ptr: *mut Config, path: *const c_char) -> bool { 296 | let config = unsafe { 297 | assert!(!ptr.is_null()); 298 | &mut *ptr 299 | }; 300 | 301 | unsafe { 302 | let path = CStr::from_ptr(path).to_str().unwrap(); 303 | config.set_database_dir(path) 304 | } 305 | } 306 | 307 | /// Sets the user specific writable directory path. 308 | /// 309 | /// Returns `true` if the path exists. 310 | #[no_mangle] 311 | pub extern "C" fn riti_config_set_user_dir(ptr: *mut Config, path: *const c_char) -> bool { 312 | let config = unsafe { 313 | assert!(!ptr.is_null()); 314 | &mut *ptr 315 | }; 316 | 317 | unsafe { 318 | let path = CStr::from_ptr(path).to_str().unwrap(); 319 | config.set_user_dir(path) 320 | } 321 | } 322 | 323 | #[no_mangle] 324 | pub extern "C" fn riti_config_set_suggestion_include_english(ptr: *mut Config, option: bool) { 325 | let config = unsafe { 326 | assert!(!ptr.is_null()); 327 | &mut *ptr 328 | }; 329 | 330 | config.set_suggestion_include_english(option); 331 | } 332 | 333 | #[no_mangle] 334 | pub extern "C" fn riti_config_set_phonetic_suggestion(ptr: *mut Config, option: bool) { 335 | let config = unsafe { 336 | assert!(!ptr.is_null()); 337 | &mut *ptr 338 | }; 339 | 340 | config.set_phonetic_suggestion(option); 341 | } 342 | 343 | #[no_mangle] 344 | pub extern "C" fn riti_config_set_fixed_suggestion(ptr: *mut Config, option: bool) { 345 | let config = unsafe { 346 | assert!(!ptr.is_null()); 347 | &mut *ptr 348 | }; 349 | 350 | config.set_fixed_suggestion(option); 351 | } 352 | 353 | #[no_mangle] 354 | pub extern "C" fn riti_config_set_fixed_auto_vowel(ptr: *mut Config, option: bool) { 355 | let config = unsafe { 356 | assert!(!ptr.is_null()); 357 | &mut *ptr 358 | }; 359 | 360 | config.set_fixed_automatic_vowel(option); 361 | } 362 | 363 | #[no_mangle] 364 | pub extern "C" fn riti_config_set_fixed_auto_chandra(ptr: *mut Config, option: bool) { 365 | let config = unsafe { 366 | assert!(!ptr.is_null()); 367 | &mut *ptr 368 | }; 369 | 370 | config.set_fixed_automatic_chandra(option); 371 | } 372 | 373 | #[no_mangle] 374 | pub extern "C" fn riti_config_set_fixed_traditional_kar(ptr: *mut Config, option: bool) { 375 | let config = unsafe { 376 | assert!(!ptr.is_null()); 377 | &mut *ptr 378 | }; 379 | 380 | config.set_fixed_traditional_kar(option); 381 | } 382 | 383 | #[no_mangle] 384 | pub extern "C" fn riti_config_set_fixed_old_reph(ptr: *mut Config, option: bool) { 385 | let config = unsafe { 386 | assert!(!ptr.is_null()); 387 | &mut *ptr 388 | }; 389 | 390 | config.set_fixed_old_reph(option); 391 | } 392 | 393 | #[no_mangle] 394 | pub extern "C" fn riti_config_set_fixed_numpad(ptr: *mut Config, option: bool) { 395 | let config = unsafe { 396 | assert!(!ptr.is_null()); 397 | &mut *ptr 398 | }; 399 | 400 | config.set_fixed_numpad(option); 401 | } 402 | 403 | #[no_mangle] 404 | pub extern "C" fn riti_config_set_fixed_old_kar_order(ptr: *mut Config, option: bool) { 405 | let config = unsafe { 406 | assert!(!ptr.is_null()); 407 | &mut *ptr 408 | }; 409 | 410 | config.set_fixed_old_kar_order(option); 411 | } 412 | 413 | #[no_mangle] 414 | pub extern "C" fn riti_config_set_ansi_encoding(ptr: *mut Config, option: bool) { 415 | let config = unsafe { 416 | assert!(!ptr.is_null()); 417 | &mut *ptr 418 | }; 419 | 420 | config.set_ansi_encoding(option); 421 | } 422 | 423 | #[no_mangle] 424 | pub extern "C" fn riti_config_set_smart_quote(ptr: *mut Config, option: bool) { 425 | let config = unsafe { 426 | assert!(!ptr.is_null()); 427 | &mut *ptr 428 | }; 429 | 430 | config.set_smart_quote(option); 431 | } 432 | -------------------------------------------------------------------------------- /data/suffix.json: -------------------------------------------------------------------------------- 1 | {"khanayo":"খানায়ও","somuhote":"সমূহতে","tukui":"টুকুই","brrindorao":"বৃন্দরাও","tukudwarao":"টুকুদ্বারাও","shobe":"সবে","sombondhiyoo":"সম্বন্ধীয়ও","yeteo":"য়েতেও","shombondhiyoo":"সম্বন্ধীয়ও","razi":"রাজি","tukundarai":"টুকুনদ্বারাই","gachike":"গাছিকে","bises":"বিশেষ","tukukei":"টুকুকেই","tukunei":"টুকুনেই","gulaneri":"গুলানেরই","gunke":"গুণকে","gacitei":"গাছিতেই","i":"ই","derkeo":"দেরকেও","shomuhe":"সমূহে","guno":"গুণও","shobi":"সবই","der":"দের","tukkei":"টুককেই","rajitei":"রাজিতেই","ei":"েই","borge":"বর্গে","malaro":"মালারও","yere":"য়েরে","gacidarai":"গাছিদ্বারাই","tar":"টার","sokolei":"সকলেই","dige":"দিগে","ta":"টা","gachari":"গাছারই","gaca":"গাছা","borrgera":"বর্গেরা","khanatei":"খানাতেই","shomuhokei":"সমূহকেই","shokoli":"সকলই","tukdwarai":"টুকদ্বারাই","gonrai":"গণরাই","gachhadara":"গাছাদ্বারা","ti":"টি","darai":"দ্বারাই","shob":"সব","gulikei":"গুলিকেই","gachai":"গাছাই","digorei":"দিগরেই","tare":"টারে","gunreo":"গুণরেও","sombondhiyoi":"সম্বন্ধীয়ই","sokolke":"সকলকে","gachhikei":"গাছিকেই","gachikeo":"গাছিকেও","gacakei":"গাছাকেই","gachhadarao":"গাছাদ্বারাও","borgokeo":"বর্গকেও","kulkeo":"কুলকেও","biseso":"বিশেষও","digere":"দিগেরে","guloro":"গুলোরও","mondoliri":"মণ্ডলীরই","derkei":"দেরকেই","gachio":"গাছিও","khaniri":"খানিরই","te":"তে","tukun":"টুকুন","sobero":"সবেরও","shobero":"সবেরও","sombondhiyo":"সম্বন্ধীয়","gachhireo":"গাছিরেও","digo":"দিগ","digetei":"দিগেতেই","punjero":"পুঞ্জেরও","borrgeo":"বর্গেও","gachadarao":"গাছাদ্বারাও","mondoli":"মণ্ডলী","bishishtoo":"বিশিষ্টও","gacadwara":"গাছাদ্বারা","gachhadwarao":"গাছাদ্বারাও","sohoo":"সহও","khanake":"খানাকে","gularo":"গুলারও","kuleo":"কুলেও","khanareo":"খানারেও","tukeri":"টুকেরই","goni":"গণই","shomuhei":"সমূহেই","khaner":"খানের","borgokei":"বর্গকেই","shokolkeo":"সকলকেও","gachare":"গাছারে","digero":"দিগেরও","guli":"গুলি","tuke":"টুকে","punjoke":"পুঞ্জকে","jatioi":"জাতীয়ই","erao":"েরাও","rajirai":"রাজিরাই","gachake":"গাছাকে","borrgeri":"বর্গেরই","shomuhoo":"সমূহও","gachhiteo":"গাছিতেও","suchok":"সূচক","rajiro":"রাজিরও","malarao":"মালারাও","somuhokeo":"সমূহকেও","tukuner":"টুকুনের","gachha":"গাছা","sokolerao":"সকলেরাও","gacai":"গাছাই","brrindokei":"বৃন্দকেই","tukunkeo":"টুকুনকেও","khaneri":"খানেরই","keo":"কেও","borrgero":"বর্গেরও","gachii":"গাছিই","gacidwarao":"গাছিদ্বারাও","tuktei":"টুকতেই","eri":"েরই","gachirei":"গাছিরেই","tai":"টাই","teo":"তেও","sober":"সবের","jatiyoi":"জাতীয়ই","borrgoi":"বর্গই","somuho":"সমূহ","shonkantoi":"সংক্রান্তই","rajikei":"রাজিকেই","shorupo":"স্বরূপও","gachhike":"গাছিকে","rajite":"রাজিতে","guliri":"গুলিরই","ro":"রও","digokei":"দিগকেই","digoi":"দিগই","tukundwarai":"টুকুনদ্বারাই","yi":"য়ই","brrindeo":"বৃন্দেও","digeo":"দিগেও","tukudarai":"টুকুদ্বারাই","derke":"দেরকে","yero":"য়েরও","kul":"কুল","gachhire":"গাছিরে","khanakei":"খানাকেই","brindokei":"বৃন্দকেই","brrinderi":"বৃন্দেরই","gacake":"গাছাকে","songkrantoi":"সংক্রান্তই","gulotei":"গুলোতেই","somuhokei":"সমূহকেই","rei":"রেই","borrge":"বর্গে","khanero":"খানেরও","shucok":"সূচক","brindo":"বৃন্দ","gachidwara":"গাছিদ্বারা","shomuhotei":"সমূহতেই","tukunkei":"টুকুনকেই","sobi":"সবই","somuhe":"সমূহে","khaniteo":"খানিতেও","r":"র","khanao":"খানাও","khanar":"খানার","gachhiro":"গাছিরও","sucok":"সূচক","sokoleri":"সকলেরই","gulanero":"গুলানেরও","soberi":"সবেরই","gulan":"গুলান","punjeo":"পুঞ্জেও","gachhateo":"গাছাতেও","gachhari":"গাছারই","dero":"দেরও","titeo":"টিতেও","khanarei":"খানারেই","gacireo":"গাছিরেও","brinder":"বৃন্দের","gacidwara":"গাছিদ্বারা","gachhi":"গাছি","mondolir":"মণ্ডলীর","ereo":"েরেও","bishishtoi":"বিশিষ্টই","gulor":"গুলোর","tukero":"টুকেরও","gulateo":"গুলাতেও","tari":"টারই","gonero":"গণেরও","gacharo":"গাছারও","gachadara":"গাছাদ্বারা","shokolrei":"সকলরেই","kulo":"কুলও","sonkantoi":"সংক্রান্তই","gacar":"গাছার","shomuhoi":"সমূহই","bishesh":"বিশেষ","gachikei":"গাছিকেই","malayi":"মালায়ই","yeo":"য়েও","tukke":"টুককে","digete":"দিগেতে","brrinde":"বৃন্দে","sokol":"সকল","bishesho":"বিশেষও","gachidarao":"গাছিদ্বারাও","rajiri":"রাজিরই","gunero":"গুণেরও","tukteo":"টুকতেও","sobei":"সবেই","mondoliro":"মণ্ডলীরও","gachidara":"গাছিদ্বারা","gacike":"গাছিকে","mondolikeo":"মণ্ডলীকেও","digoo":"দিগও","taro":"টারও","brindorai":"বৃন্দরাই","khanikei":"খানিকেই","kule":"কুলে","gacao":"গাছাও","gonra":"গণরা","guloteo":"গুলোতেও","brinderai":"বৃন্দেরাই","tuki":"টুকই","punjeri":"পুঞ্জেরই","shomuhore":"সমূহরে","shorupi":"স্বরূপই","khanayi":"খানায়ই","borrgerao":"বর্গেরাও","shombondhioo":"সম্বন্ধীয়ও","gulori":"গুলোরই","brrinder":"বৃন্দের","khanire":"খানিরে","tukre":"টুকরে","tei":"তেই","khanaro":"খানারও","sokolo":"সকলও","eteo":"েতেও","gacharei":"গাছারেই","gonkeo":"গণকেও","gachhidarao":"গাছিদ্বারাও","tukuri":"টুকুরই","khanari":"খানারই","gacidwarai":"গাছিদ্বারাই","gachhakeo":"গাছাকেও","shomuhero":"সমূহেরও","tukudara":"টুকুদ্বারা","gachhai":"গাছাই","sokolreo":"সকলরেও","tay":"টায়","tukdwarao":"টুকদ্বারাও","gacaro":"গাছারও","shombondhio":"সম্বন্ধীয়","gonrao":"গণরাও","shongkrantoi":"সংক্রান্তই","mondolio":"মণ্ডলীও","gachiteo":"গাছিতেও","brrindora":"বৃন্দরা","eo":"েও","shobkeo":"সবকেও","digoreo":"দিগরেও","gacadwarai":"গাছাদ্বারাই","shongkranto":"সংক্রান্ত","sokolre":"সকলরে","mala":"মালা","tukune":"টুকুনে","somuhero":"সমূহেরও","tukudwara":"টুকুদ্বারা","sokoler":"সকলের","ke":"কে","brindeo":"বৃন্দেও","punjokei":"পুঞ্জকেই","gachhitei":"গাছিতেই","sobkei":"সবকেই","digoke":"দিগকে","gon":"গণ","shomuher":"সমূহের","tayo":"টায়ও","khanio":"খানিও","tukuneo":"টুকুনেও","gono":"গণও","gachadwarai":"গাছাদ্বারাই","digeteo":"দিগেতেও","gachadwara":"গাছাদ্বারা","somuhoreo":"সমূহরেও","yo":"য়ও","shonkantoo":"সংক্রান্তও","razii":"রাজিই","korrtrriko":"কর্তৃকও","gulokei":"গুলোকেই","punjora":"পুঞ্জরা","yetei":"য়েতেই","tukunre":"টুকুনরে","brinderi":"বৃন্দেরই","borrger":"বর্গের","borgero":"বর্গেরও","punjote":"পুঞ্জতে","malara":"মালারা","sokoleo":"সকলেও","gunrei":"গুণরেই","sonkantoo":"সংক্রান্তও","brindoke":"বৃন্দকে","khana":"খানা","rajio":"রাজিও","malar":"মালার","malari":"মালারই","sokolrei":"সকলরেই","ete":"েতে","tio":"টিও","o":"ও","gacidara":"গাছিদ্বারা","shokolerao":"সকলেরাও","muloki":"মূলকই","shokolkei":"সকলকেই","shomuhoteo":"সমূহতেও","sombondhioi":"সম্বন্ধীয়ই","malai":"মালাই","gachhakei":"গাছাকেই","rajirao":"রাজিরাও","sucoko":"সূচকও","tayi":"টায়ই","brrindoi":"বৃন্দই","borrgokeo":"বর্গকেও","shuchoko":"সূচকও","digeri":"দিগেরই","shokoleri":"সকলেরই","gachhidara":"গাছিদ্বারা","gachireo":"গাছিরেও","tiri":"টিরই","khaneteo":"খানেতেও","somuhoi":"সমূহই","gachakei":"গাছাকেই","ri":"রই","tuk":"টুক","gulo":"গুলো","brindera":"বৃন্দেরা","brrindoo":"বৃন্দও","tukuke":"টুকুকে","kulke":"কুলকে","tukunero":"টুকুনেরও","shuchoki":"সূচকই","sokolera":"সকলেরা","kortrik":"কর্তৃক","tikeo":"টিকেও","yerao":"য়েরাও","jatio":"জাতীয়","tire":"টিরে","sorup":"স্বরূপ","brrinderai":"বৃন্দেরাই","rao":"রাও","tate":"টাতে","tukur":"টুকুর","kortriko":"কর্তৃকও","digereo":"দিগেরেও","gacari":"গাছারই","sobke":"সবকে","mondolira":"মণ্ডলীরা","sombondhioo":"সম্বন্ধীয়ও","malake":"মালাকে","sonkanto":"সংক্রান্ত","digore":"দিগরে","era":"েরা","tukunreo":"টুকুনরেও","khanateo":"খানাতেও","tiro":"টিরও","shobeo":"সবেও","punjorao":"পুঞ্জরাও","gachharo":"গাছারও","tukuro":"টুকুরও","yer":"য়ের","gachadwarao":"গাছাদ্বারাও","tukuneri":"টুকুনেরই","tukure":"টুকুরে","gachhio":"গাছিও","shokole":"সকলে","gachhadarai":"গাছাদ্বারাই","shobo":"সবও","shokoler":"সকলের","takei":"টাকেই","khani":"খানি","somuhore":"সমূহরে","guloke":"গুলোকে","borrgokei":"বর্গকেই","sworupo":"স্বরূপও","gacikei":"গাছিকেই","bisistoo":"বিশিষ্টও","bisistoi":"বিশিষ্টই","dwarao":"দ্বারাও","rai":"রাই","malate":"মালাতে","dwarai":"দ্বারাই","somuhoke":"সমূহকে","gachitei":"গাছিতেই","gacire":"গাছিরে","brindorao":"বৃন্দরাও","gulake":"গুলাকে","shworupo":"স্বরূপও","gacidarao":"গাছিদ্বারাও","borrgerai":"বর্গেরাই","gulakei":"গুলাকেই","punje":"পুঞ্জে","re":"রে","sobeo":"সবেও","gulikeo":"গুলিকেও","bisisto":"বিশিষ্ট","malao":"মালাও","malay":"মালায়","e":"ে","gacikeo":"গাছিকেও","tuker":"টুকের","gacir":"গাছির","khanetei":"খানেতেই","gaciteo":"গাছিতেও","shokolero":"সকলেরও","gachhao":"গাছাও","digerei":"দিগেরেই","khanete":"খানেতে","korrtrriki":"কর্তৃকই","gacadara":"গাছাদ্বারা","punjei":"পুঞ্জেই","brindero":"বৃন্দেরও","gachidarai":"গাছিদ্বারাই","take":"টাকে","gachhikeo":"গাছিকেও","mulok":"মূলক","jatioo":"জাতীয়ও","shomuho":"সমূহ","shohoi":"সহই","tii":"টিই","kulei":"কুলেই","sokolkeo":"সকলকেও","sokole":"সকলে","punjotei":"পুঞ্জতেই","y":"য়","somuhotei":"সমূহতেই","tukunke":"টুকুনকে","yei":"য়েই","borgera":"বর্গেরা","sobkeo":"সবকেও","gachhadwarai":"গাছাদ্বারাই","gachatei":"গাছাতেই","punjoo":"পুঞ্জও","tatei":"টাতেই","deri":"দেরই","khanir":"খানির","shomuhoke":"সমূহকে","gacateo":"গাছাতেও","shokolei":"সকলেই","gonkei":"গণকেই","gacio":"গাছিও","ero":"েরও","sombondhio":"সম্বন্ধীয়","goneo":"গণেও","shomuheri":"সমূহেরই","somuheri":"সমূহেরই","suchoki":"সূচকই","sokoli":"সকলই","borgo":"বর্গ","gulatei":"গুলাতেই","rajikeo":"রাজিকেও","gaci":"গাছি","sohoi":"সহই","gachateo":"গাছাতেও","shombondhioi":"সম্বন্ধীয়ই","shworupi":"স্বরূপই","tite":"টিতে","gachhareo":"গাছারেও","tukuni":"টুকুনই","gachhii":"গাছিই","tukreo":"টুকরেও","brinderao":"বৃন্দেরাও","gulitei":"গুলিতেই","soho":"সহ","digokeo":"দিগকেও","borger":"বর্গের","tikei":"টিকেই","reo":"রেও","gachhadwara":"গাছাদ্বারা","guner":"গুণের","rajira":"রাজিরা","gulano":"গুলানও","borgoke":"বর্গকে","khanitei":"খানিতেই","tukutei":"টুকুতেই","guliteo":"গুলিতেও","khanikeo":"খানিকেও","songkrantoo":"সংক্রান্তও","gacii":"গাছিই","gacate":"গাছাতে","shobkei":"সবকেই","guni":"গুণই","borgerao":"বর্গেরাও","malakei":"মালাকেই","shomuheo":"সমূহেও","tareo":"টারেও","gulakeo":"গুলাকেও","gulani":"গুলানই","tuku":"টুকু","gachhidwarai":"গাছিদ্বারাই","punjo":"পুঞ্জ","shokolo":"সকলও","shombondhiyoi":"সম্বন্ধীয়ই","tukuno":"টুকুনও","gachao":"গাছাও","tukei":"টুকেই","gonei":"গণেই","tireo":"টিরেও","shokolera":"সকলেরা","gacareo":"গাছারেও","guliro":"গুলিরও","sokolero":"সকলেরও","punjoi":"পুঞ্জই","tukuo":"টুকুও","khanakeo":"খানাকেও","tirei":"টিরেই","gachhatei":"গাছাতেই","gacadarai":"গাছাদ্বারাই","borgei":"বর্গেই","tukunrei":"টুকুনরেই","shober":"সবের","punjoteo":"পুঞ্জতেও","khanite":"খানিতে","shuchok":"সূচক","yereo":"য়েরেও","khanate":"খানাতে","gaciro":"গাছিরও","gachhidwara":"গাছিদ্বারা","gunre":"গুণরে","gachhirei":"গাছিরেই","tukdara":"টুকদ্বারা","gachidwarao":"গাছিদ্বারাও","gunkeo":"গুণকেও","tukukeo":"টুকুকেও","gachharei":"গাছারেই","guloo":"গুলোও","dwara":"দ্বারা","rajii":"রাজিই","khanai":"খানাই","kortriki":"কর্তৃকই","sokolkei":"সকলকেই","brindei":"বৃন্দেই","ra":"রা","ere":"েরে","somuher":"সমূহের","gulari":"গুলারই","shucoki":"সূচকই","jatiyoo":"জাতীয়ও","darao":"দ্বারাও","shokolre":"সকলরে","tukdarai":"টুকদ্বারাই","shokolerai":"সকলেরাই","brrindera":"বৃন্দেরা","titei":"টিতেই","gachar":"গাছার","gulite":"গুলিতে","gachhite":"গাছিতে","sucoki":"সূচকই","brindoo":"বৃন্দও","shokoleo":"সকলেও","shohoo":"সহও","mondolikei":"মণ্ডলীকেই","gachareo":"গাছারেও","gacha":"গাছা","mondolirai":"মণ্ডলীরাই","brrinderao":"বৃন্দেরাও","erai":"েরাই","kulkei":"কুলকেই","rajir":"রাজির","yete":"য়েতে","shobke":"সবকে","malayo":"মালায়ও","sorupi":"স্বরূপই","gacarei":"গাছারেই","shonkanto":"সংক্রান্ত","gulai":"গুলাই","kuler":"কুলের","malarai":"মালারাই","shobei":"সবেই","brindora":"বৃন্দরা","er":"ের","khanirei":"খানিরেই","shomuhote":"সমূহতে","borrgei":"বর্গেই","kuli":"কুলই","gachir":"গাছির","gachi":"গাছি","malatei":"মালাতেই","shombondhiyo":"সম্বন্ধীয়","khanike":"খানিকে","yerei":"য়েরেই","brindoi":"বৃন্দই","borgeri":"বর্গেরই","jatiyo":"জাতীয়","gulao":"গুলাও","brinde":"বৃন্দে","kuleri":"কুলেরই","punjorai":"পুঞ্জরাই","mondolirao":"মণ্ডলীরাও","khanay":"খানায়","gula":"গুলা","shomuhorei":"সমূহরেই","tukundwara":"টুকুনদ্বারা","tao":"টাও","tuko":"টুকও","khanare":"খানারে","brrindei":"বৃন্দেই","gacakeo":"গাছাকেও","kei":"কেই","sokolerai":"সকলেরাই","gachakeo":"গাছাকেও","gun":"গুণ","ye":"য়ে","sworup":"স্বরূপ","tukrei":"টুকরেই","digei":"দিগেই","gulokeo":"গুলোকেও","tukdarao":"টুকদ্বারাও","gulio":"গুলিও","gacare":"গাছারে","gachhidarai":"গাছিদ্বারাই","tukkeo":"টুককেও","razio":"রাজিও","mondolii":"মণ্ডলীই","gachhate":"গাছাতে","gone":"গণে","gulaner":"গুলানের","tukundwarao":"টুকুনদ্বারাও","takeo":"টাকেও","gulir":"গুলির","yeri":"য়েরই","goneri":"গণেরই","tukundara":"টুকুনদ্বারা","gachite":"গাছিতে","gulate":"গুলাতে","tukureo":"টুকুরেও","gacadwarao":"গাছাদ্বারাও","gulote":"গুলোতে","tukundarao":"টুকুনদ্বারাও","gachhidwarao":"গাছিদ্বারাও","rajiteo":"রাজিতেও","brrindero":"বৃন্দেরও","borgoi":"বর্গই","goner":"গণের","borgoo":"বর্গও","sobo":"সবও","tukurei":"টুকুরেই","sob":"সব","gacirei":"গাছিরেই","etei":"েতেই","dara":"দ্বারা","tir":"টির","gonke":"গণকে","shongkrantoo":"সংক্রান্তও","bisheshi":"বিশেষই","somuhoteo":"সমূহতেও","erei":"েরেই","tateo":"টাতেও","khaniro":"খানিরও","rajike":"রাজিকে","somuhei":"সমূহেই","gulike":"গুলিকে","gachire":"গাছিরে","diger":"দিগের","shomuhokeo":"সমূহকেও","gachiri":"গাছিরই","brrindokeo":"বৃন্দকেও","somuheo":"সমূহেও","shucoko":"সূচকও","tukte":"টুকতে","gular":"গুলার","tukudarao":"টুকুদ্বারাও","khanireo":"খানিরেও","gachiro":"গাছিরও","gacite":"গাছিতে","gachhiri":"গাছিরই","korrtrrik":"কর্তৃক","brindokeo":"বৃন্দকেও","shoberi":"সবেরই","tike":"টিকে","punjer":"পুঞ্জের","shokolreo":"সকলরেও","khanii":"খানিই","borrgo":"বর্গ","gachadarai":"গাছাদ্বারাই","tukudwarai":"টুকুদ্বারাই","muloko":"মূলকও","tukeo":"টুকেও","suchoko":"সূচকও","brrindo":"বৃন্দ","gaciri":"গাছিরই","sworupi":"স্বরূপই","malakeo":"মালাকেও","shomuhoreo":"সমূহরেও","tukute":"টুকুতে","sobe":"সবে","bisesi":"বিশেষই","shorup":"স্বরূপ","songkranto":"সংক্রান্ত","gacadarao":"গাছাদ্বারাও","gachhar":"গাছার","brrindoke":"বৃন্দকে","bishishto":"বিশিষ্ট","somuhoo":"সমূহও","tarei":"টারেই","guloi":"গুলোই","borrgoke":"বর্গকে","brrindorai":"বৃন্দরাই","shokol":"সকল","punjokeo":"পুঞ্জকেও","gulii":"গুলিই","somuhorei":"সমূহরেই","kulero":"কুলেরও","gachidwarai":"গাছিদ্বারাই","borgeo":"বর্গেও","gachhare":"গাছারে","sorupo":"স্বরূপও","gacatei":"গাছাতেই","tukuteo":"টুকুতেও","gachhir":"গাছির","borrgoo":"বর্গও","mondolike":"মণ্ডলীকে","borgerai":"বর্গেরাই","tukdwara":"টুকদ্বারা","gachhake":"গাছাকে","shokolke":"সকলকে","shworup":"স্বরূপ","raji":"রাজি","yera":"য়েরা","guneri":"গুণেরই","gachate":"গাছাতে","yerai":"য়েরাই","malateo":"মালাতেও","shoho":"সহ","gunkei":"গুণকেই"} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /src/phonetic/suggestion.rs: -------------------------------------------------------------------------------- 1 | // Suggestion making module. 2 | 3 | use ahash::RandomState; 4 | use okkhor::parser::Parser; 5 | use std::collections::HashMap; 6 | use upodesh::avro::Suggest; 7 | 8 | use crate::config::Config; 9 | use crate::data::Data; 10 | use crate::suggestion::Rank; 11 | use crate::utility::{push_checked, smart_quoter, SplittedString, Utility}; 12 | 13 | pub(crate) struct PhoneticSuggestion { 14 | pub(crate) suggestions: Vec, 15 | // Phonetic buffer. It's used to avoid allocations 16 | // for phonetic conversion every time. 17 | pbuffer: String, 18 | // Cache for storing dictionary searches. 19 | cache: HashMap, RandomState>, 20 | phonetic: Parser, 21 | dict: Suggest, 22 | // The user's auto-correct entries. 23 | pub(crate) user_autocorrect: HashMap, 24 | } 25 | 26 | impl PhoneticSuggestion { 27 | pub(crate) fn new(user_autocorrect: HashMap) -> Self { 28 | PhoneticSuggestion { 29 | suggestions: Vec::with_capacity(10), 30 | pbuffer: String::with_capacity(60), 31 | cache: HashMap::with_capacity_and_hasher(20, RandomState::new()), 32 | phonetic: Parser::new_phonetic(), 33 | dict: Suggest::new(), 34 | user_autocorrect, 35 | } 36 | } 37 | 38 | /// Add suffix(গুলো, মালা, etc.) to the dictionary suggestions and return them. 39 | /// 40 | /// This function gets the suggestion list from the stored cache. 41 | /// 42 | /// Handles Auto Corrected words specially. It includes them into 43 | /// the `self.corrects` directly to let them be one of the first suggestions. 44 | fn add_suffix_to_suggestions(&mut self, middle: &str, data: &Data) -> Vec { 45 | // Fill up the list with what we have from the cache. 46 | let mut list = self.cache.get(middle).cloned().unwrap_or_default(); 47 | 48 | if middle.len() > 2 { 49 | for i in 1..middle.len() { 50 | let suffix_key = &middle[i..]; 51 | 52 | if let Some(suffix) = data.find_suffix(suffix_key) { 53 | let key = &middle[..(middle.len() - suffix_key.len())]; 54 | if let Some(cache) = self.cache.get(key) { 55 | for base in cache { 56 | let base_rmc = base.to_string().chars().last().unwrap(); // Right most character. 57 | let suffix_lmc = suffix.chars().next().unwrap(); // Left most character. 58 | let mut word = String::with_capacity(middle.len() * 3); 59 | word.push_str(base.to_string()); 60 | match base_rmc { 61 | ch if ch.is_vowel() && suffix_lmc.is_kar() => { 62 | // Insert য় in between. 63 | word.push('য়'); 64 | } 65 | 'ৎ' => { 66 | // Replace ৎ with ত 67 | word.pop(); 68 | word.push('ত'); 69 | } 70 | 'ং' => { 71 | // Replace ং with ঙ 72 | word.pop(); 73 | word.push('ঙ'); 74 | } 75 | _ => (), 76 | } 77 | word.push_str(suffix); 78 | 79 | let mut new = base.clone(); 80 | // This changes the suggestion with the suffixed one while keeping the ranking intact. 81 | *new.change_item() = word; 82 | list.push(new); 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | list 90 | } 91 | 92 | /// Make suggestion from given `term` with only phonetic transliteration. 93 | pub(crate) fn suggest_only_phonetic(&mut self, term: &str) -> String { 94 | let string = SplittedString::split(term, false); 95 | 96 | self.phonetic.convert_into(string.word(), &mut self.pbuffer); 97 | 98 | format!( 99 | "{}{}{}", 100 | self.phonetic.convert(string.preceding()), 101 | self.pbuffer, 102 | self.phonetic.convert(string.trailing()) 103 | ) 104 | } 105 | 106 | pub(crate) fn suggest( 107 | &mut self, 108 | term: &str, 109 | data: &Data, 110 | selections: &mut HashMap, 111 | config: &Config, 112 | ) -> (Vec, usize) { 113 | let mut string = SplittedString::split(term, false); 114 | let mut typed_added = false; 115 | 116 | // Convert preceding and trailing meta characters into Bengali(phonetic representation). 117 | string.map(|p, t| (self.phonetic.convert(p), self.phonetic.convert(t))); 118 | 119 | // Smart Quoting feature 120 | if config.get_smart_quote() { 121 | string = smart_quoter(string); 122 | } 123 | 124 | self.suggestion_with_dict(&string, data); 125 | 126 | // Emoji addition with corresponding emoticon (if ANSI mode is not enabled). 127 | if !config.get_ansi_encoding() { 128 | if let Some(emoji) = data.get_emoji_by_emoticon(term) { 129 | // Add the emoticon 130 | self.suggestions.push(Rank::emoji(emoji.to_owned())); 131 | // Add the full term as the last ranked suggestion. 132 | self.suggestions.push(Rank::last_ranked(term.to_owned(), 1)); 133 | // Mark that we have added the typed text already (as the emoticon). 134 | typed_added = true; 135 | } else { 136 | // Emoji addition with it's English name 137 | if let Some(emojis) = data.get_emoji_by_name(string.word()) { 138 | let emojis = emojis 139 | .zip(1..) 140 | .map(|(s, r)| Rank::emoji_ranked(s.to_owned(), r)); 141 | self.suggestions.extend(emojis); 142 | } 143 | 144 | // Emoji addition with Bengali name 145 | let mut bn_emojis = Vec::with_capacity(10); 146 | 147 | for word in self.suggestions.iter() { 148 | if let Some(emojis) = data.get_emoji_by_bengali(word.to_string()) { 149 | let emojis = emojis 150 | .zip(1..) 151 | .map(|(s, r)| Rank::emoji_ranked(s.to_owned(), r)); 152 | bn_emojis.extend(emojis); 153 | } 154 | } 155 | 156 | if !bn_emojis.is_empty() { 157 | for emoji in bn_emojis { 158 | push_checked(&mut self.suggestions, emoji); 159 | } 160 | } 161 | } 162 | } 163 | 164 | // Add those preceding and trailing meta characters. 165 | if !typed_added && (!string.preceding().is_empty() || !string.trailing().is_empty()) { 166 | for item in self.suggestions.iter_mut() { 167 | *item.change_item() = format!( 168 | "{}{}{}", 169 | string.preceding(), 170 | item.to_string(), 171 | string.trailing() 172 | ); 173 | } 174 | } 175 | 176 | // Phonetic transliteration of the typed text (including preceding and trailing meta characters). 177 | push_checked( 178 | &mut self.suggestions, 179 | Rank::last_ranked( 180 | format!( 181 | "{}{}{}", 182 | string.preceding(), 183 | self.pbuffer, 184 | string.trailing() 185 | ), 186 | 2, 187 | ), 188 | ); 189 | 190 | // Include written English word if the feature is enabled and it is not included already. 191 | // Avoid including meta character suggestion twice, so check `term` is not equal to the 192 | // captured preceding characters 193 | if config.get_suggestion_include_english() && !typed_added && term != string.preceding() { 194 | self.suggestions 195 | .push(Rank::last_ranked(term.to_string(), 3)); 196 | } 197 | 198 | // Sort the suggestions. 199 | self.suggestions.sort(); 200 | 201 | let selection = self.get_prev_selection(&string, data, selections); 202 | 203 | (self.suggestions.clone(), selection) 204 | } 205 | 206 | /// Make suggestions from the given `splitted_string`. This will include dictionary and auto-correct suggestion. 207 | pub(crate) fn suggestion_with_dict(&mut self, string: &SplittedString, data: &Data) { 208 | self.suggestions.clear(); 209 | self.pbuffer.clear(); 210 | 211 | if string.word().is_empty() { 212 | // If the word is empty, return early. 213 | return; 214 | } 215 | 216 | self.phonetic.convert_into(string.word(), &mut self.pbuffer); 217 | 218 | let phonetic = self.pbuffer.clone(); 219 | 220 | // We always cache the suggestions for future reuse and for adding suffix to the suggestions. 221 | if !self.cache.contains_key(string.word()) { 222 | let mut suggestions: Vec = Vec::new(); 223 | 224 | // Auto Correct item. 225 | if let Some(correct) = self.search_corrected(string.word(), data) { 226 | let corrected = self.phonetic.convert(correct); 227 | // Treat it as the first priority. 228 | suggestions.push(Rank::first_ranked(corrected)); 229 | } 230 | 231 | self.include_from_dictionary(string.word(), &phonetic, &mut suggestions); 232 | // Add the suggestions into the cache. 233 | self.cache.insert(string.word().to_string(), suggestions); 234 | } 235 | 236 | let suffixed_suggestions = self.add_suffix_to_suggestions(string.word(), data); 237 | 238 | // Middle Items: Dictionary suggestions 239 | for suggestion in suffixed_suggestions { 240 | push_checked(&mut self.suggestions, suggestion); 241 | } 242 | 243 | // Last Item: Phonetic 244 | push_checked(&mut self.suggestions, Rank::last_ranked(phonetic, 2)); 245 | } 246 | 247 | pub(crate) fn get_prev_selection( 248 | &self, 249 | string: &SplittedString, 250 | data: &Data, 251 | selections: &mut HashMap, 252 | ) -> usize { 253 | let len = string.word().len(); 254 | let mut selected = String::with_capacity(len * 3); 255 | 256 | if let Some(item) = selections.get(string.word()) { 257 | selected.push_str(item); 258 | } else if len >= 2 { 259 | for i in 1..len { 260 | let test = &string.word()[len - i..len]; 261 | 262 | if let Some(suffix) = data.find_suffix(test) { 263 | let key = &string.word()[..len - test.len()]; 264 | 265 | if let Some(base) = selections.get(key) { 266 | let rmc = base.chars().last().unwrap(); 267 | let suffix_lmc = suffix.chars().next().unwrap(); 268 | selected.push_str(base); 269 | 270 | match rmc { 271 | ch if ch.is_vowel() && suffix_lmc.is_kar() => { 272 | // Insert য় in between. 273 | selected.push('য়'); 274 | } 275 | 'ৎ' => { 276 | // Replace ৎ with ত 277 | selected.pop(); 278 | selected.push('ত'); 279 | } 280 | 'ং' => { 281 | // Replace ং with ঙ 282 | selected.pop(); 283 | selected.push('ঙ'); 284 | } 285 | _ => (), 286 | } 287 | selected.push_str(suffix); 288 | 289 | // Save this for future reuse. 290 | selections.insert(string.word().to_string(), selected.to_string()); 291 | } 292 | } 293 | } 294 | } 295 | 296 | selected = format!("{}{}{}", string.preceding(), selected, string.trailing()); 297 | 298 | self.suggestions 299 | .iter() 300 | .position(|item| *item.to_string() == selected) 301 | .unwrap_or_default() 302 | } 303 | 304 | /// Find words from the dictionary with given `word` and rank them according the `base` word. 305 | pub(crate) fn include_from_dictionary( 306 | &mut self, 307 | word: &str, 308 | base: &str, 309 | suggestions: &mut Vec, 310 | ) { 311 | let mut items = self.dict.suggest(word); 312 | items.sort(); 313 | 314 | suggestions.extend(items.into_iter().map(|s| Rank::new_suggestion(s, base))); 315 | } 316 | 317 | /// Search for a `term` in AutoCorrect dictionary. 318 | /// 319 | /// This looks in the user defined AutoCorrect entries first. 320 | fn search_corrected<'a>(&'a self, term: &str, data: &'a Data) -> Option<&'a str> { 321 | self.user_autocorrect 322 | .get(term) 323 | .map(String::as_str) 324 | .or_else(|| data.search_corrected(term)) 325 | } 326 | } 327 | 328 | // Implement Default trait on PhoneticSuggestion, actually for testing convenience. 329 | impl Default for PhoneticSuggestion { 330 | fn default() -> Self { 331 | PhoneticSuggestion::new(HashMap::with_hasher(RandomState::new())) 332 | } 333 | } 334 | 335 | #[cfg(test)] 336 | mod tests { 337 | use ahash::RandomState; 338 | use std::collections::HashMap; 339 | 340 | use super::PhoneticSuggestion; 341 | use crate::config::get_phonetic_method_defaults; 342 | use crate::data::Data; 343 | use crate::suggestion::Rank; 344 | use crate::utility::SplittedString; 345 | 346 | #[test] 347 | fn test_suggestion_with_english() { 348 | let mut suggestion = PhoneticSuggestion::default(); 349 | let mut selections = HashMap::with_hasher(RandomState::new()); 350 | let mut config = get_phonetic_method_defaults(); 351 | let data = Data::new(&config); 352 | config.set_suggestion_include_english(true); 353 | 354 | suggestion.suggest(":)", &data, &mut selections, &config); 355 | assert_eq!(suggestion.suggestions, ["😃", ":)", "ঃ", "ঃ)"]); 356 | 357 | suggestion.suggest(";)", &data, &mut selections, &config); 358 | assert_eq!(suggestion.suggestions, ["😉", ";)"]); 359 | 360 | suggestion.suggest("{a}", &data, &mut selections, &config); 361 | assert_eq!( 362 | suggestion.suggestions, 363 | [ 364 | "{আ}", 365 | "{🅰️}", 366 | "{অ}", 367 | "{আঃ}", 368 | "{এ}", 369 | "{া}", 370 | "{অ্যা}", 371 | "{অ্যাঁ}", 372 | "{a}" 373 | ] 374 | ); 375 | 376 | suggestion.suggest("\"", &data, &mut selections, &config); 377 | assert_eq!(suggestion.suggestions, ["\""]); 378 | } 379 | 380 | #[test] 381 | fn test_suggestion_ansi() { 382 | let mut suggestion = PhoneticSuggestion::default(); 383 | let mut selections = HashMap::with_hasher(RandomState::new()); 384 | let mut config = get_phonetic_method_defaults(); 385 | let data = Data::new(&config); 386 | config.set_suggestion_include_english(true); 387 | config.set_ansi_encoding(true); 388 | 389 | suggestion.suggest(":)", &data, &mut selections, &config); 390 | assert_eq!(suggestion.suggestions, ["ঃ)"]); 391 | 392 | suggestion.suggest(";)", &data, &mut selections, &config); 393 | assert_eq!(suggestion.suggestions, [";)"]); 394 | 395 | suggestion.suggest("{a}", &data, &mut selections, &config); 396 | assert_eq!( 397 | suggestion.suggestions, 398 | ["{আ}", "{অ}", "{আঃ}", "{এ}", "{া}", "{অ্যা}", "{অ্যাঁ}"] 399 | ); 400 | } 401 | 402 | #[test] 403 | fn test_suggestion_smart_quotes() { 404 | let mut suggestion = PhoneticSuggestion::default(); 405 | let mut selections = HashMap::with_hasher(RandomState::new()); 406 | let mut config = get_phonetic_method_defaults(); 407 | let data = Data::new(&config); 408 | config.set_suggestion_include_english(true); 409 | config.set_smart_quote(true); 410 | 411 | suggestion.suggest("\"e\"", &data, &mut selections, &config); 412 | assert_eq!( 413 | suggestion.suggestions, 414 | ["“এ”", "“🅰\u{fe0f}”", "“ে”", "\"e\""] 415 | ); 416 | 417 | config.set_smart_quote(false); 418 | 419 | suggestion.suggest("\"e\"", &data, &mut selections, &config); 420 | assert_eq!( 421 | suggestion.suggestions, 422 | ["\"এ\"", "\"🅰\u{fe0f}\"", "\"ে\"", "\"e\""] 423 | ); 424 | } 425 | 426 | #[test] 427 | fn test_suggestion_only_phonetic() { 428 | let mut suggestion = PhoneticSuggestion::default(); 429 | 430 | assert_eq!(suggestion.suggest_only_phonetic("{kotha}"), "{কথা}"); 431 | assert_eq!(suggestion.suggest_only_phonetic(",ah,,"), ",আহ্‌"); 432 | } 433 | 434 | #[test] 435 | fn test_emoticon_and_emoji() { 436 | let mut suggestion = PhoneticSuggestion::default(); 437 | let mut selections = HashMap::with_hasher(RandomState::new()); 438 | let config = get_phonetic_method_defaults(); 439 | let data = Data::new(&config); 440 | 441 | suggestion.suggest(":)", &data, &mut selections, &config); 442 | assert_eq!(suggestion.suggestions, ["😃", ":)", "ঃ", "ঃ)"]); 443 | 444 | suggestion.suggest(";)", &data, &mut selections, &config); 445 | assert_eq!(suggestion.suggestions, ["😉", ";)"]); 446 | 447 | suggestion.suggest("smile", &data, &mut selections, &config); 448 | assert_eq!(suggestion.suggestions, ["😀", "😄", "স্মিলে"]); 449 | 450 | suggestion.suggest("cool", &data, &mut selections, &config); 451 | assert_eq!( 452 | suggestion.suggestions, 453 | ["চুল", "😎", "🆒", "চূল", "চোল", "ছুল", "ছুঁল", "ছোল"] 454 | ); 455 | 456 | suggestion.suggest("chup", &data, &mut selections, &config); 457 | assert_eq!(suggestion.suggestions, ["ছুপ", "🫢", "🙊", "🤐", "চুপ"]); 458 | 459 | suggestion.suggest("hasi", &data, &mut selections, &config); 460 | assert_eq!( 461 | suggestion.suggestions, 462 | ["হাসি", "☺\u{fe0f}", "😀", "😁", "😃", "😄", "🙂", "হাঁসি"] 463 | ); 464 | 465 | suggestion.suggest(".", &data, &mut selections, &config); 466 | assert_eq!(suggestion.suggestions, ["।"]); 467 | } 468 | 469 | #[test] 470 | fn test_suggestion() { 471 | let mut suggestion = PhoneticSuggestion::default(); 472 | let mut selections = HashMap::with_hasher(RandomState::new()); 473 | let config = get_phonetic_method_defaults(); 474 | let data = Data::new(&config); 475 | 476 | suggestion.suggest("a", &data, &mut selections, &config); 477 | assert_eq!( 478 | suggestion.suggestions, 479 | ["আ", "🅰️", "অ", "আঃ", "এ", "া", "অ্যা", "অ্যাঁ"] 480 | ); 481 | 482 | suggestion.suggest("as", &data, &mut selections, &config); 483 | assert_eq!( 484 | suggestion.suggestions, 485 | ["আস", "আশ", "এস", "আঁশ", "অশ্ব", "অশ্ম"] 486 | ); 487 | 488 | suggestion.suggest("asgulo", &data, &mut selections, &config); 489 | assert_eq!( 490 | suggestion.suggestions, 491 | [ 492 | "আসগুলো", 493 | "আশগুলো", 494 | "এসগুলো", 495 | "আঁশগুলো", 496 | "অশ্বগুলো", 497 | "অশ্মগুলো", 498 | "আসগুল" 499 | ] 500 | ); 501 | 502 | suggestion.suggest("(as)", &data, &mut selections, &config); 503 | assert_eq!( 504 | suggestion.suggestions, 505 | ["(আস)", "(আশ)", "(এস)", "(আঁশ)", "(অশ্ব)", "(অশ্ম)"] 506 | ); 507 | } 508 | 509 | #[test] 510 | fn test_suffix_suggestion() { 511 | let mut suggestion = PhoneticSuggestion::default(); 512 | let mut selections = HashMap::with_hasher(RandomState::new()); 513 | let config = get_phonetic_method_defaults(); 514 | let data = Data::new(&config); 515 | 516 | suggestion.suggest("a", &data, &mut selections, &config); 517 | suggestion.suggest("ap", &data, &mut selections, &config); 518 | suggestion.suggest("apn", &data, &mut selections, &config); 519 | suggestion.suggest("apni", &data, &mut selections, &config); 520 | assert_eq!(suggestion.suggestions, ["আপনি", "আপনই", "আপ্নি"]); 521 | 522 | suggestion.suggest("am", &data, &mut selections, &config); 523 | suggestion.suggest("ami", &data, &mut selections, &config); 524 | assert_eq!(suggestion.suggestions, ["আমি", "আমই", "অমি", "এমই"]); 525 | 526 | suggestion.suggest("kkhet", &data, &mut selections, &config); 527 | assert_eq!( 528 | suggestion.suggestions, 529 | ["ক্ষেত", "খেত", "খ্যাত", "খেট", "খ্যাঁত", "খেঁট", "খ্যাঁট"] 530 | ); 531 | 532 | suggestion.suggest("kkhetr", &data, &mut selections, &config); 533 | assert_eq!( 534 | suggestion.suggestions, 535 | [ 536 | "ক্ষেত্র", 537 | "ক্ষেতর", 538 | "খেতর", 539 | "খ্যাতর", 540 | "খেটর", 541 | "খ্যাঁতর", 542 | "খেঁটর", 543 | "খ্যাঁটর", 544 | ] 545 | ); 546 | 547 | /* TODO: Fix this 548 | suggestion.suggest("kkhetre", &data, &mut selections, &config); 549 | assert_eq!( 550 | suggestion.suggestions, 551 | [ 552 | "ক্ষেত্রে", 553 | "ক্ষেতরে", 554 | "খেতরে", 555 | "খ্যাতরে", 556 | "খেটরে", 557 | "খ্যাঁতরে", 558 | "খেঁটরে", 559 | "খ্যাঁটরে", 560 | ] 561 | );*/ 562 | 563 | suggestion.suggest("form", &data, &mut selections, &config); 564 | assert_eq!(suggestion.suggestions, ["ফর্ম", "ফরম"]); 565 | 566 | suggestion.suggest("forma", &data, &mut selections, &config); 567 | assert_eq!(suggestion.suggestions, ["ফরমা", "ফর্মা"]); 568 | 569 | suggestion.suggest("format", &data, &mut selections, &config); 570 | assert_eq!(suggestion.suggestions, ["ফরম্যাট", "ফরমাত"]); 571 | 572 | suggestion.suggest("formate", &data, &mut selections, &config); 573 | assert_eq!(suggestion.suggestions, ["ফরম্যাটে", "ফরমাতে", "ফর্মাতে"]); 574 | 575 | suggestion.suggest("formatt", &data, &mut selections, &config); 576 | assert_eq!(suggestion.suggestions, ["ফরম্যাট", "ফরমাত্ত"]); 577 | 578 | suggestion.suggest("formatte", &data, &mut selections, &config); 579 | assert_eq!(suggestion.suggestions, ["ফরম্যাটতে", "ফরম্যাটে", "ফরমাত্তে"]); 580 | 581 | suggestion.suggest("atm", &data, &mut selections, &config); 582 | assert_eq!( 583 | suggestion.suggestions, 584 | ["এটিএম", "আত্ম", "🏧", "⚛\u{fe0f}", "অ্যাটম"] 585 | ); 586 | 587 | suggestion.suggest("atme", &data, &mut selections, &config); 588 | assert_eq!(suggestion.suggestions, ["এটিএমে", "আত্মে", "অ্যাটমে"]); 589 | // Cache check 590 | suggestion.suggest("atm", &data, &mut selections, &config); 591 | assert_eq!( 592 | suggestion.suggestions, 593 | ["এটিএম", "আত্ম", "🏧", "⚛\u{fe0f}", "অ্যাটম"] 594 | ); 595 | } 596 | 597 | #[test] 598 | fn test_suffix() { 599 | let mut cache = HashMap::with_hasher(RandomState::new()); 600 | let config = get_phonetic_method_defaults(); 601 | let data = Data::new(&config); 602 | 603 | cache.insert( 604 | "computer".to_string(), 605 | vec![Rank::first_ranked("কম্পিউটার".to_string())], 606 | ); 607 | cache.insert("i".to_string(), vec![Rank::first_ranked("ই".to_string())]); 608 | cache.insert( 609 | "hothat".to_string(), 610 | vec![Rank::first_ranked("হঠাৎ".to_string())], 611 | ); 612 | cache.insert( 613 | "ebong".to_string(), 614 | vec![Rank::first_ranked("এবং".to_string())], 615 | ); 616 | 617 | let mut suggestion = PhoneticSuggestion { 618 | cache, 619 | ..Default::default() 620 | }; 621 | 622 | assert_eq!( 623 | suggestion.add_suffix_to_suggestions("computer", &data), 624 | ["কম্পিউটার"] 625 | ); 626 | assert_eq!( 627 | suggestion.add_suffix_to_suggestions("computere", &data), 628 | ["কম্পিউটারে"] 629 | ); 630 | assert_eq!( 631 | suggestion.add_suffix_to_suggestions("computergulo", &data), 632 | ["কম্পিউটারগুলো"] 633 | ); 634 | // kar => য় 635 | assert_eq!( 636 | suggestion.add_suffix_to_suggestions("iei", &data), 637 | vec!["ইয়েই"] 638 | ); 639 | // ৎ => ত 640 | assert_eq!( 641 | suggestion.add_suffix_to_suggestions("hothate", &data), 642 | ["হঠাতে"] 643 | ); 644 | // ং => ঙ 645 | assert_eq!( 646 | suggestion.add_suffix_to_suggestions("ebongmala", &data), 647 | ["এবঙমালা"] 648 | ); 649 | } 650 | 651 | #[test] 652 | fn test_prev_selected() { 653 | let mut suggestion = PhoneticSuggestion::default(); 654 | let mut selections = HashMap::with_hasher(RandomState::new()); 655 | let config = get_phonetic_method_defaults(); 656 | let data = Data::new(&config); 657 | 658 | selections.insert("onno".to_string(), "অন্য".to_string()); 659 | selections.insert("i".to_string(), "ই".to_string()); 660 | selections.insert("hothat".to_string(), "হঠাৎ".to_string()); 661 | selections.insert("ebong".to_string(), "এবং".to_string()); 662 | 663 | // Avoid meta characters 664 | suggestion.suggestions = vec![ 665 | Rank::Other("*অন্ন?!".to_string(), 0), 666 | Rank::Other("*অন্য?!".to_string(), 0), 667 | ]; 668 | assert_eq!( 669 | suggestion.get_prev_selection( 670 | &SplittedString::split("*onno?!", false), 671 | &data, 672 | &mut selections 673 | ), 674 | 1 675 | ); 676 | 677 | // With Suffix 678 | suggestion.suggestions = vec![ 679 | Rank::Other("ইএই".to_string(), 1), 680 | Rank::Other("ইয়েই".to_string(), 2), 681 | ]; 682 | assert_eq!( 683 | suggestion.get_prev_selection( 684 | &SplittedString::split("iei", false), 685 | &data, 686 | &mut selections 687 | ), 688 | 1 689 | ); 690 | 691 | suggestion.suggestions = vec![ 692 | Rank::Other("হোথাতে".to_string(), 0), 693 | Rank::Other("হথাতে".to_string(), 0), 694 | Rank::Other("হঠাতে".to_string(), 0), 695 | ]; 696 | assert_eq!( 697 | suggestion.get_prev_selection( 698 | &SplittedString::split("hothate", false), 699 | &data, 700 | &mut selections 701 | ), 702 | 2 703 | ); 704 | 705 | suggestion.suggestions = vec![ 706 | Rank::Other("এবংমালা".to_string(), 0), 707 | Rank::Other("এবঙমালা".to_string(), 0), 708 | ]; 709 | assert_eq!( 710 | suggestion.get_prev_selection( 711 | &SplittedString::split("ebongmala", false), 712 | &data, 713 | &mut selections 714 | ), 715 | 1 716 | ); 717 | 718 | // With Suffix + Avoid meta characters 719 | suggestion.suggestions = vec![ 720 | Rank::Other("*অন্নগুলো?!".to_string(), 0), 721 | Rank::Other("*অন্যগুলো?!".to_string(), 0), 722 | ]; 723 | assert_eq!( 724 | suggestion.get_prev_selection( 725 | &SplittedString::split("*onnogulo?!", false), 726 | &data, 727 | &mut selections 728 | ), 729 | 1 730 | ); 731 | } 732 | 733 | #[test] 734 | fn test_suggest_special_chars_selections() { 735 | let mut suggestion = PhoneticSuggestion::default(); 736 | let mut selections = HashMap::with_hasher(RandomState::new()); 737 | let config = get_phonetic_method_defaults(); 738 | let data = Data::new(&config); 739 | selections.insert("sesh".to_string(), "শেষ".to_string()); 740 | 741 | let (suggestions, selection) = suggestion.suggest("sesh", &data, &mut selections, &config); 742 | assert_eq!(suggestions, ["🏁", "🔚", "সেস", "শেষ", "সেশ"]); 743 | assert_eq!(selection, 3); 744 | 745 | let (suggestions, selection) = suggestion.suggest("sesh.", &data, &mut selections, &config); 746 | assert_eq!(suggestions, ["🏁।", "🔚।", "সেস।", "শেষ।", "সেশ।"]); 747 | assert_eq!(selection, 3); 748 | 749 | let (suggestions, _) = suggestion.suggest("sesh:", &data, &mut selections, &config); 750 | assert_eq!(suggestions, ["🏁", "🔚", "সেস", "শেষ", "সেশঃ"]); 751 | 752 | let (suggestions, selection) = 753 | suggestion.suggest("sesh:`", &data, &mut selections, &config); 754 | assert_eq!(suggestions, ["🏁:", "🔚:", "সেস:", "শেষ:", "সেশ:"]); 755 | assert_eq!(selection, 3); 756 | 757 | let (suggestions, _) = suggestion.suggest("6t``", &data, &mut selections, &config); 758 | assert_eq!(suggestions, ["৬ৎ"]); 759 | } 760 | 761 | #[test] 762 | fn test_database() { 763 | let mut suggestion = PhoneticSuggestion::default(); 764 | let mut suggestions = Vec::new(); 765 | 766 | suggestion.include_from_dictionary("a", "a", &mut suggestions); 767 | assert_eq!(suggestions, ["অ", "অ্যা", "অ্যাঁ", "আ", "আঃ", "এ", "া"]); 768 | suggestions.clear(); 769 | 770 | suggestion.include_from_dictionary("(", "", &mut suggestions); 771 | assert_eq!(suggestions, Vec::::new()); 772 | } 773 | } 774 | 775 | #[cfg(feature = "bench")] 776 | mod benches { 777 | extern crate test; 778 | 779 | use super::PhoneticSuggestion; 780 | use crate::{config::get_phonetic_method_defaults, data::Data, utility::SplittedString}; 781 | use test::{black_box, Bencher}; 782 | 783 | #[bench] 784 | fn bench_phonetic_a(b: &mut Bencher) { 785 | let mut suggestion = PhoneticSuggestion::default(); 786 | let config = get_phonetic_method_defaults(); 787 | let data = Data::new(&config); 788 | let term = SplittedString::split("a", false); 789 | 790 | b.iter(|| { 791 | suggestion.cache.clear(); 792 | suggestion.suggestion_with_dict(&term, &data); 793 | }) 794 | } 795 | 796 | #[bench] 797 | fn bench_phonetic_kkhet(b: &mut Bencher) { 798 | let mut suggestion = PhoneticSuggestion::default(); 799 | let config = get_phonetic_method_defaults(); 800 | let data = Data::new(&config); 801 | let term = SplittedString::split("kkhet", false); 802 | 803 | b.iter(|| { 804 | suggestion.cache.clear(); 805 | suggestion.suggestion_with_dict(&term, &data); 806 | }) 807 | } 808 | 809 | #[bench] 810 | fn bench_phonetic_bistari(b: &mut Bencher) { 811 | let mut suggestion = PhoneticSuggestion::default(); 812 | let config = get_phonetic_method_defaults(); 813 | let data = Data::new(&config); 814 | let term = SplittedString::split("bistari", false); 815 | 816 | b.iter(|| { 817 | suggestion.cache.clear(); 818 | suggestion.suggestion_with_dict(&term, &data); 819 | }) 820 | } 821 | 822 | #[bench] 823 | fn bench_phonetic_database_a(b: &mut Bencher) { 824 | let config = get_phonetic_method_defaults(); 825 | let mut suggestion = PhoneticSuggestion::default(); 826 | let data = Data::new(&config); 827 | b.iter(|| { 828 | let mut suggestions = Vec::new(); 829 | suggestion.include_from_dictionary("a", "", &mut suggestions); 830 | black_box(suggestions); 831 | }) 832 | } 833 | 834 | #[bench] 835 | fn bench_phonetic_database_aro(b: &mut Bencher) { 836 | let config = get_phonetic_method_defaults(); 837 | let mut suggestion = PhoneticSuggestion::default(); 838 | let data = Data::new(&config); 839 | b.iter(|| { 840 | let mut suggestions = Vec::new(); 841 | suggestion.include_from_dictionary("arO", "", &mut suggestions); 842 | black_box(suggestions); 843 | }) 844 | } 845 | 846 | #[bench] 847 | fn bench_phonetic_database_bistari(b: &mut Bencher) { 848 | let config = get_phonetic_method_defaults(); 849 | let mut suggestion = PhoneticSuggestion::default(); 850 | let data = Data::new(&config); 851 | b.iter(|| { 852 | let mut suggestions = Vec::new(); 853 | suggestion.include_from_dictionary("bistari", "", &mut suggestions); 854 | black_box(suggestions); 855 | }) 856 | } 857 | } 858 | -------------------------------------------------------------------------------- /src/fixed/method.rs: -------------------------------------------------------------------------------- 1 | use upodesh::bangla::suggest; 2 | 3 | use super::{chars::*, layout::Layout}; 4 | use crate::config::Config; 5 | use crate::suggestion::{Rank, Suggestion}; 6 | use crate::utility::{clean_string, get_modifiers, smart_quoter, SplittedString, Utility}; 7 | use crate::{context::Method, data::Data, keycodes::keycode_to_char}; 8 | 9 | const MARKS: &str = "`~!@#$%^+*-_=+\\|\"/;:,./?><()[]{}"; 10 | 11 | enum PendingKar { 12 | I, 13 | E, 14 | OI, 15 | } 16 | 17 | pub(crate) struct FixedMethod { 18 | buffer: String, 19 | typed: String, 20 | pending_kar: Option, 21 | suggestions: Vec, 22 | layout: Layout, 23 | } 24 | 25 | impl Method for FixedMethod { 26 | fn get_suggestion( 27 | &mut self, 28 | key: u16, 29 | modifier: u8, 30 | _selection: u8, 31 | data: &Data, 32 | config: &Config, 33 | ) -> Suggestion { 34 | let modifier = get_modifiers(modifier); 35 | 36 | if let Some(value) = 37 | self.layout 38 | .get_char_for_key(key, modifier.into(), config.get_fixed_numpad()) 39 | { 40 | self.process_key_value(&value, config); 41 | } else { 42 | return self.current_suggestion(config); 43 | } 44 | 45 | if config.get_fixed_suggestion() { 46 | self.typed.push(keycode_to_char(key)); 47 | } 48 | 49 | self.create_suggestion(data, config) 50 | } 51 | 52 | fn candidate_committed(&mut self, _index: usize, _: &Config) { 53 | self.buffer.clear(); 54 | self.typed.clear(); 55 | self.pending_kar = None; 56 | } 57 | 58 | fn update_engine(&mut self, _: &Config) { 59 | // 60 | } 61 | 62 | fn ongoing_input_session(&self) -> bool { 63 | !self.buffer.is_empty() || self.pending_kar.is_some() 64 | } 65 | 66 | fn finish_input_session(&mut self) { 67 | self.buffer.clear(); 68 | self.typed.clear(); 69 | self.pending_kar = None; 70 | } 71 | 72 | fn backspace_event(&mut self, ctrl: bool, data: &Data, config: &Config) -> Suggestion { 73 | if ctrl && !self.buffer.is_empty() { 74 | // Whole word deletion: Ctrl + Backspace combination 75 | self.buffer.clear(); 76 | self.typed.clear(); 77 | self.pending_kar = None; 78 | return Suggestion::empty(); 79 | } 80 | if self.pending_kar.is_some() { 81 | // Clear pending_kar. 82 | self.pending_kar = None; 83 | self.typed.pop(); 84 | if self.buffer.is_empty() { 85 | return Suggestion::empty(); 86 | } 87 | return self.create_suggestion(data, config); 88 | } 89 | if !self.buffer.is_empty() { 90 | // Remove the last character from buffer. 91 | self.buffer.pop(); 92 | self.typed.pop(); 93 | 94 | if self.buffer.is_empty() { 95 | // The buffer is now empty, so return empty suggestion. 96 | return Suggestion::empty(); 97 | } 98 | 99 | self.create_suggestion(data, config) 100 | } else { 101 | Suggestion::empty() 102 | } 103 | } 104 | } 105 | 106 | impl FixedMethod { 107 | /// Creates a new instance of `FixedMethod` with the given layout. 108 | pub(crate) fn new(config: &Config) -> Self { 109 | let layout = config.get_layout().and_then(Layout::parse).unwrap(); 110 | 111 | FixedMethod { 112 | buffer: String::with_capacity(20 * 3), // A Bengali character is 3 bytes in size. 113 | typed: String::with_capacity(20), 114 | pending_kar: None, 115 | suggestions: Vec::with_capacity(10), 116 | layout, 117 | } 118 | } 119 | 120 | fn create_suggestion(&mut self, data: &Data, config: &Config) -> Suggestion { 121 | if config.get_fixed_suggestion() { 122 | self.create_dictionary_suggestion(data, config) 123 | } else { 124 | Suggestion::new_lonely(self.buffer.clone(), config.get_ansi_encoding()) 125 | } 126 | } 127 | 128 | fn create_dictionary_suggestion(&mut self, data: &Data, config: &Config) -> Suggestion { 129 | let mut string = SplittedString::split(&self.buffer, true); 130 | 131 | // Smart Quoting feature 132 | if config.get_smart_quote() { 133 | string = smart_quoter(string); 134 | } 135 | 136 | let (first_part, word, last_part) = string.as_tuple(); 137 | 138 | self.suggestions.clear(); 139 | 140 | // Add the user's typed word. 141 | self.suggestions.push(Rank::first_ranked(word.to_string())); 142 | 143 | // Add suggestions from the dictionary while changing the Kar joinings if Traditional Kar Joining is set. 144 | let mut words = suggest(&clean_string(word)); 145 | words.sort_unstable(); 146 | 147 | if config.get_fixed_traditional_kar() { 148 | self.suggestions.extend(words.into_iter().map(|w| { 149 | // Check if the word has any of the ligature making Kars. 150 | let new = if w.chars().any(is_ligature_making_kar) { 151 | let mut temp = String::with_capacity(w.capacity()); 152 | for ch in w.chars() { 153 | if is_ligature_making_kar(ch) { 154 | temp.push(ZWNJ); 155 | } 156 | temp.push(ch); 157 | } 158 | temp 159 | } else { 160 | w.clone() 161 | }; 162 | 163 | Rank::new_suggestion(new, word) 164 | })); 165 | } else { 166 | self.suggestions.extend(words.into_iter().map(|s| Rank::new_suggestion(s.clone(), word))); 167 | } 168 | 169 | // Remove the duplicates if present. 170 | self.suggestions.dedup(); 171 | 172 | // Add preceding and trailing meta characters. 173 | if !first_part.is_empty() || !last_part.is_empty() { 174 | for suggestion in self.suggestions.iter_mut() { 175 | *suggestion.change_item() = 176 | format!("{}{}{}", first_part, suggestion.to_string(), last_part); 177 | } 178 | } 179 | 180 | if !config.get_ansi_encoding() { 181 | // Emoji addition with Emoticons. 182 | if let Some(emoji) = data.get_emoji_by_emoticon(&self.typed) { 183 | self.suggestions.push(Rank::emoji(emoji.to_owned())); 184 | } else if let Some(emojis) = data.get_emoji_by_bengali(word) { 185 | // Emoji addition with it's Bengali name. 186 | // Add preceding and trailing meta characters. 187 | let emojis = emojis 188 | .zip(1..) 189 | .map(|(s, r)| Rank::emoji_ranked(format!("{first_part}{s}{last_part}"), r)); 190 | self.suggestions.extend(emojis); 191 | } 192 | } 193 | 194 | // Sort the suggestions. 195 | self.suggestions.sort_unstable(); 196 | 197 | // Reduce the number of suggestions and add the typed english word at the end. 198 | // Also check that the typed text is not already included (may happen 199 | // when the control characters are typed). 200 | if config.get_suggestion_include_english() && self.buffer != self.typed { 201 | self.suggestions.truncate(8); 202 | self.suggestions 203 | .push(Rank::last_ranked(self.typed.clone(), 1)); 204 | } else { 205 | self.suggestions.truncate(9); 206 | } 207 | 208 | Suggestion::new( 209 | self.buffer.clone(), 210 | &self.suggestions, 211 | 0, 212 | config.get_ansi_encoding(), 213 | ) 214 | } 215 | 216 | fn current_suggestion(&self, config: &Config) -> Suggestion { 217 | if !self.buffer.is_empty() { 218 | if config.get_fixed_suggestion() { 219 | Suggestion::new( 220 | self.buffer.clone(), 221 | &self.suggestions, 222 | 0, 223 | config.get_ansi_encoding(), 224 | ) 225 | } else { 226 | Suggestion::new_lonely(self.buffer.clone(), config.get_ansi_encoding()) 227 | } 228 | } else { 229 | Suggestion::empty() 230 | } 231 | } 232 | 233 | /// Processes the `value` of the pressed key and updates the method's 234 | /// internal buffer which will be used when creating suggestion. 235 | fn process_key_value(&mut self, value: &str, config: &Config) { 236 | let rmc = self.buffer.chars().last().unwrap_or_default(); // Right most character 237 | 238 | // Zo-fola insertion 239 | if value == "\u{09CD}\u{09AF}" { 240 | // Check if র is not a part of a Ro-fola, if its not then add an ZWJ before 241 | // the Zo-fola to have the র‍্য form. 242 | if rmc == B_R && self.buffer.chars().rev().nth(1).unwrap_or_default() != B_HASANTA { 243 | self.buffer.push(ZWJ); 244 | } 245 | if config.get_fixed_old_kar_order() && is_left_standing_kar(rmc) { 246 | if let Some(kar) = self.buffer.pop() { 247 | self.buffer.push_str(value); 248 | self.buffer.push(kar); 249 | return; 250 | } 251 | } 252 | self.buffer.push_str(value); 253 | return; 254 | } 255 | 256 | // Old style Reph insertion 257 | if value == "\u{09B0}\u{09CD}" && config.get_fixed_old_reph() { 258 | self.insert_old_style_reph(); 259 | return; 260 | } 261 | 262 | if let Some(character) = value.chars().next() { 263 | // Kar insertion 264 | if character.is_kar() { 265 | // Old style Kar ordering 266 | if config.get_fixed_old_kar_order() { 267 | // Capture left standing kar in pending_kar. 268 | if rmc != B_HASANTA && is_left_standing_kar(character) { 269 | self.pending_kar = match character { 270 | B_I_KAR => Some(PendingKar::I), 271 | B_E_KAR => Some(PendingKar::E), 272 | B_OI_KAR => Some(PendingKar::OI), 273 | _ => None, 274 | }; 275 | return; 276 | } else if rmc == B_E_KAR && (character == B_AA_KAR || character == B_OU_KAR) { 277 | // Join two-part dependent vowel signs. 278 | self.buffer.pop(); 279 | match character { 280 | B_AA_KAR => self.buffer.push(B_O_KAR), 281 | B_OU_KAR => self.buffer.push(B_OU_KAR), 282 | _ => (), 283 | } 284 | return; 285 | } else if let Some(left_standing_kar) = &self.pending_kar { 286 | // Restore pending_kar. 287 | if rmc == B_HASANTA { 288 | self.buffer.pop(); 289 | self.buffer.push(match left_standing_kar { 290 | PendingKar::E => B_E_KAR, 291 | PendingKar::I => B_I_KAR, 292 | PendingKar::OI => B_OI_KAR, 293 | }); 294 | self.pending_kar = None; 295 | self.buffer.push(B_HASANTA); 296 | } else { 297 | // Unexpected case, destroy pending_kar or 298 | // form vowel from pending kar if applicable. 299 | if config.get_fixed_automatic_vowel() 300 | && (self.buffer.is_empty() || rmc.is_vowel() || MARKS.contains(rmc)) 301 | { 302 | self.buffer.push(match left_standing_kar { 303 | PendingKar::E => B_E, 304 | PendingKar::I => B_I, 305 | PendingKar::OI => B_OI, 306 | }); 307 | } 308 | self.pending_kar = None; 309 | self.process_key_value(value, config); 310 | return; 311 | } 312 | } 313 | } 314 | // Automatic Vowel Forming 315 | if config.get_fixed_automatic_vowel() 316 | && (self.buffer.is_empty() || rmc.is_vowel() || MARKS.contains(rmc)) 317 | { 318 | match character { 319 | B_AA_KAR => self.buffer.push(B_AA), 320 | B_I_KAR => self.buffer.push(B_I), 321 | B_II_KAR => self.buffer.push(B_II), 322 | B_U_KAR => self.buffer.push(B_U), 323 | B_UU_KAR => self.buffer.push(B_UU), 324 | B_RRI_KAR => self.buffer.push(B_RRI), 325 | B_E_KAR => self.buffer.push(B_E), 326 | B_OI_KAR => self.buffer.push(B_OI), 327 | B_O_KAR => self.buffer.push(B_O), 328 | B_OU_KAR => self.buffer.push(B_OU), 329 | _ => (), 330 | } 331 | } else if config.get_fixed_automatic_chandra() && rmc == B_CHANDRA { 332 | // Automatic Fix of Chandra Position 333 | self.buffer.pop(); 334 | self.buffer.push(character); 335 | self.buffer.push(B_CHANDRA); 336 | } else if rmc == B_HASANTA { 337 | // Vowel making with Hasanta + Kar 338 | match character { 339 | B_AA_KAR => { 340 | self.buffer.pop(); 341 | self.buffer.push(B_AA); 342 | } 343 | B_I_KAR => { 344 | self.buffer.pop(); 345 | self.buffer.push(B_I); 346 | } 347 | B_II_KAR => { 348 | self.buffer.pop(); 349 | self.buffer.push(B_II); 350 | } 351 | B_U_KAR => { 352 | self.buffer.pop(); 353 | self.buffer.push(B_U); 354 | } 355 | B_UU_KAR => { 356 | self.buffer.pop(); 357 | self.buffer.push(B_UU); 358 | } 359 | B_RRI_KAR => { 360 | self.buffer.pop(); 361 | self.buffer.push(B_RRI); 362 | } 363 | B_E_KAR => { 364 | self.buffer.pop(); 365 | self.buffer.push(B_E); 366 | } 367 | B_OI_KAR => { 368 | self.buffer.pop(); 369 | self.buffer.push(B_OI); 370 | } 371 | B_O_KAR => { 372 | self.buffer.pop(); 373 | self.buffer.push(B_O); 374 | } 375 | B_OU_KAR => { 376 | self.buffer.pop(); 377 | self.buffer.push(B_OU); 378 | } 379 | _ => (), 380 | } 381 | } else if config.get_fixed_traditional_kar() && rmc.is_pure_consonant() { 382 | // Traditional Kar Joining 383 | // In UNICODE it is known as "Blocking Bengali Consonant-Vowel Ligature" 384 | if is_ligature_making_kar(character) { 385 | self.buffer.push(ZWNJ); 386 | } 387 | self.buffer.push(character); 388 | } else { 389 | self.buffer.push(character); 390 | } 391 | return; 392 | } 393 | 394 | // Hasanta 395 | if character == B_HASANTA && rmc == B_HASANTA { 396 | self.buffer.push(ZWNJ); 397 | return; 398 | } 399 | 400 | // ঔ making with Hasanta + AU Length Mark 401 | if character == B_LENGTH_MARK && rmc == B_HASANTA { 402 | self.buffer.pop(); 403 | self.buffer.push(B_OU); 404 | return; 405 | } 406 | 407 | // Old style Kar ordering 408 | if config.get_fixed_old_kar_order() { 409 | if character == B_HASANTA && is_left_standing_kar(rmc) { 410 | if value.chars().count() == 1 { 411 | self.pending_kar = match self.buffer.pop() { 412 | Some(B_I_KAR) => Some(PendingKar::I), 413 | Some(B_E_KAR) => Some(PendingKar::E), 414 | Some(B_OI_KAR) => Some(PendingKar::OI), 415 | _ => None, 416 | }; 417 | self.buffer.push(character); 418 | } else if let Some(kar) = self.buffer.pop() { 419 | self.buffer.push_str(value); 420 | self.buffer.push(kar); 421 | } 422 | return; 423 | } else if rmc == B_E_KAR && character == B_LENGTH_MARK { 424 | self.buffer.pop(); 425 | self.buffer.push(B_OU_KAR); 426 | return; 427 | } 428 | } 429 | } 430 | 431 | // Old style Kar ordering 432 | if config.get_fixed_old_kar_order() { 433 | if let Some(left_standing_kar) = &self.pending_kar { 434 | self.buffer.push_str(value); 435 | if let Some(B_HASANTA) = value.chars().last() { 436 | // Continue to next consonant insertion if value ends with B_HASANTA, 437 | // for example, if value is reph(র + ্). 438 | return; 439 | } 440 | self.buffer.push(match left_standing_kar { 441 | PendingKar::E => B_E_KAR, 442 | PendingKar::I => B_I_KAR, 443 | PendingKar::OI => B_OI_KAR, 444 | }); 445 | self.pending_kar = None; 446 | return; 447 | } 448 | } 449 | 450 | self.buffer.push_str(value); 451 | } 452 | 453 | /// Checks if the Reph is moveable by the Reph insertion algorithm. 454 | fn is_reph_moveable(&self) -> bool { 455 | let mut buf_chars = self.buffer.chars().rev(); 456 | let right_most = buf_chars.next().unwrap(); 457 | let right_most = if right_most == B_CHANDRA { 458 | buf_chars.next().unwrap_or_default() 459 | } else { 460 | right_most 461 | }; 462 | let before_right_most = buf_chars.next().unwrap_or_default(); 463 | 464 | right_most.is_pure_consonant() 465 | || (right_most.is_vowel() && before_right_most.is_pure_consonant()) 466 | } 467 | 468 | /// Inserts Reph into the buffer in old style. 469 | fn insert_old_style_reph(&mut self) { 470 | let len = self.buffer.chars().count(); 471 | let reph_moveable = self.is_reph_moveable(); 472 | 473 | let mut constant = false; 474 | let mut vowel = false; 475 | let mut hasanta = false; 476 | let mut chandra = false; 477 | 478 | if reph_moveable { 479 | let mut step = 0; 480 | 481 | for (index, character) in self.buffer.chars().rev().enumerate() { 482 | if character.is_pure_consonant() { 483 | if constant && !hasanta { 484 | break; 485 | } 486 | constant = true; 487 | hasanta = false; // reset 488 | step += 1; 489 | continue; 490 | } else if character == B_HASANTA { 491 | hasanta = true; 492 | step += 1; 493 | continue; 494 | } else if character.is_vowel() { 495 | if vowel { 496 | break; 497 | } 498 | 499 | if index == 0 || chandra { 500 | vowel = true; 501 | step += 1; 502 | continue; 503 | } 504 | 505 | break; 506 | } else if character == B_CHANDRA { 507 | if index == 0 { 508 | chandra = true; 509 | step += 1; 510 | continue; 511 | } 512 | break; 513 | } 514 | } 515 | 516 | let temp: String = self.buffer.chars().skip(len - step).collect(); 517 | self.internal_backspace_step(step); 518 | self.buffer.push(B_R); 519 | self.buffer.push(B_HASANTA); 520 | self.buffer.push_str(&temp); 521 | } else { 522 | self.buffer.push(B_R); 523 | self.buffer.push(B_HASANTA); 524 | } 525 | } 526 | 527 | /// Removes the last `n` characters from the buffer. 528 | fn internal_backspace_step(&mut self, n: usize) { 529 | let len = self 530 | .buffer 531 | .chars() 532 | .rev() 533 | .take(n) 534 | .fold(0, |acc, x| acc + x.len_utf8()); 535 | let new_len = self.buffer.len() - len; 536 | self.buffer.truncate(new_len); 537 | } 538 | } 539 | 540 | // Implement Default trait on FixedMethod for testing convenience. 541 | #[cfg(test)] 542 | impl Default for FixedMethod { 543 | fn default() -> Self { 544 | let config = crate::config::get_fixed_method_defaults(); 545 | let layout = config.get_layout().and_then(Layout::parse).unwrap(); 546 | 547 | FixedMethod { 548 | buffer: String::new(), 549 | typed: String::new(), 550 | pending_kar: None, 551 | suggestions: Vec::new(), 552 | layout, 553 | } 554 | } 555 | } 556 | 557 | /// Is the provided `c` is a left standing Kar? 558 | fn is_left_standing_kar(c: char) -> bool { 559 | c == B_I_KAR || c == B_E_KAR || c == B_OI_KAR 560 | } 561 | 562 | #[cfg(test)] 563 | mod tests { 564 | use pretty_assertions::assert_eq; 565 | 566 | use super::FixedMethod; 567 | use crate::config::get_fixed_method_defaults; 568 | use crate::fixed::chars::*; 569 | use crate::{ 570 | context::Method, 571 | data::Data, 572 | keycodes::{VC_A, VC_I, VC_K, VC_M, VC_PAREN_LEFT, VC_PAREN_RIGHT, VC_QUOTE}, 573 | }; 574 | 575 | #[test] 576 | fn test_suggestions() { 577 | let mut method = FixedMethod::default(); 578 | let config = get_fixed_method_defaults(); 579 | let data = Data::new(&config); 580 | 581 | method.buffer = "[".to_string(); 582 | method.create_dictionary_suggestion(&data, &config); 583 | assert_eq!(method.suggestions, ["["]); 584 | 585 | method.buffer = "[আমি]".to_string(); 586 | method.create_dictionary_suggestion(&data, &config); 587 | assert_eq!(method.suggestions, ["[আমি]", "[আমিন]", "[আমির]", "[আমিষ]"]); 588 | 589 | method.buffer = "আমি:".to_string(); 590 | method.create_dictionary_suggestion(&data, &config); 591 | assert_eq!(method.suggestions, ["আমি:", "আমিন:", "আমির:", "আমিষ:"]); 592 | 593 | method.buffer = "আমি।".to_string(); 594 | method.create_dictionary_suggestion(&data, &config); 595 | assert_eq!(method.suggestions, ["আমি।", "আমিন।", "আমির।", "আমিষ।"]); 596 | 597 | // User written word should be the first one. 598 | method.buffer = "কম্পিউ".to_string(); 599 | method.create_dictionary_suggestion(&data, &config); 600 | assert_eq!( 601 | method.suggestions, 602 | ["কম্পিউ", "কম্পিউটার", "কম্পিউটিং", "কম্পিউটেশন", "কম্পিউটার্স"] 603 | ); 604 | } 605 | 606 | #[test] 607 | fn test_suggestions_with_english_word() { 608 | let mut method = FixedMethod::default(); 609 | let mut config = get_fixed_method_defaults(); 610 | let data = Data::new(&config); 611 | config.set_suggestion_include_english(true); 612 | 613 | method.get_suggestion(VC_A, 0, 0, &data, &config); 614 | method.get_suggestion(VC_M, 0, 0, &data, &config); 615 | method.get_suggestion(VC_I, 0, 0, &data, &config); 616 | assert_eq!(method.typed, "ami"); 617 | assert_eq!(method.suggestions, ["আমি", "আমিন", "আমির", "আমিষ", "ami"]); 618 | method.finish_input_session(); 619 | 620 | method.get_suggestion(VC_PAREN_LEFT, 0, 0, &data, &config); 621 | method.get_suggestion(VC_PAREN_RIGHT, 0, 0, &data, &config); 622 | assert_eq!(method.suggestions, ["()"]); 623 | } 624 | 625 | #[test] 626 | fn test_suggestion_smart_quote() { 627 | let mut method = FixedMethod::default(); 628 | let mut config = get_fixed_method_defaults(); 629 | let data = Data::new(&config); 630 | config.set_suggestion_include_english(true); 631 | 632 | config.set_smart_quote(true); 633 | method.get_suggestion(VC_QUOTE, 0, 0, &data, &config); 634 | method.get_suggestion(VC_K, 0, 0, &data, &config); 635 | method.get_suggestion(VC_QUOTE, 0, 0, &data, &config); 636 | assert_eq!(method.suggestions, ["“ক”","“কই”","“কও”","“কচ”","“কট”", "“কড”", "“কণ”", "“কত”", "\"k\""]); 637 | method.finish_input_session(); 638 | 639 | config.set_smart_quote(false); 640 | method.get_suggestion(VC_QUOTE, 0, 0, &data, &config); 641 | method.get_suggestion(VC_K, 0, 0, &data, &config); 642 | method.get_suggestion(VC_QUOTE, 0, 0, &data, &config); 643 | assert_eq!(method.suggestions, ["\"ক\"","\"কই\"", "\"কও\"","\"কচ\"","\"কট\"","\"কড\"","\"কণ\"","\"কত\"", "\"k\""]); 644 | } 645 | 646 | // The latest Rust version has incompatibility with the sorting order of the suggestions. 647 | // So, this sensitive test are disabled for the MSRV. 648 | #[rustversion::not(stable(1.63))] 649 | #[test] 650 | fn test_emojis() { 651 | use crate::keycodes::VC_SEMICOLON; // Don't know why Rust 1.63 errors on unused import in CI. 652 | 653 | let mut method = FixedMethod::default(); 654 | let mut config = get_fixed_method_defaults(); 655 | let data = Data::new(&config); 656 | config.set_fixed_traditional_kar(false); 657 | 658 | method.get_suggestion(VC_SEMICOLON, 0, 0, &data, &config); 659 | method.get_suggestion(VC_PAREN_RIGHT, 0, 0, &data, &config); 660 | assert_eq!(method.suggestions, [";)", "😉"]); 661 | method.finish_input_session(); 662 | 663 | method.buffer = "{লজ্জা}".to_owned(); 664 | method.create_dictionary_suggestion(&data, &config); 665 | assert_eq!( 666 | method.suggestions, 667 | [ 668 | "{লজ্জা}", 669 | "{😳}", 670 | "{লজ্জাকর}", 671 | "{লজ্জানত}", 672 | "{লজ্জালু}", 673 | "{লজ্জাজনক}", 674 | "{লজ্জাবতী}", 675 | "{লজ্জাবনত}", 676 | "{লজ্জাবশত}", 677 | ] 678 | ); 679 | 680 | method.buffer = "হাসি".to_owned(); 681 | method.create_dictionary_suggestion(&data, &config); 682 | assert_eq!( 683 | method.suggestions, 684 | [ 685 | "হাসি", 686 | "☺\u{fe0f}", 687 | "😀", 688 | "😁", 689 | "😃", 690 | "😄", 691 | "🙂", 692 | "হাসিত", 693 | "হাসিব" 694 | ] 695 | ); 696 | } 697 | 698 | // The latest Rust version has incompatibility with the sorting order of the suggestions. 699 | // So, this sensitive test are disabled for the MSRV. 700 | #[rustversion::not(stable(1.63))] 701 | #[test] 702 | fn test_suggestion_ansi() { 703 | let mut method = FixedMethod::default(); 704 | let mut config = get_fixed_method_defaults(); 705 | let data = Data::new(&config); 706 | config.set_suggestion_include_english(true); 707 | config.set_ansi_encoding(true); 708 | 709 | method.get_suggestion(VC_A, 0, 0, &data, &config); 710 | method.get_suggestion(VC_M, 0, 0, &data, &config); 711 | method.get_suggestion(VC_I, 0, 0, &data, &config); 712 | assert_eq!(method.typed, "ami"); 713 | assert_eq!(method.suggestions, ["আমি", "আমিন", "আমির", "আমিষ"]); 714 | method.finish_input_session(); 715 | 716 | method.buffer = "হাসি".to_owned(); 717 | method.create_dictionary_suggestion(&data, &config); 718 | assert_eq!( 719 | method.suggestions, 720 | [ 721 | "হাসি", 722 | "হাসিত", 723 | "হাসিব", 724 | "হাসিল", 725 | "হাসিস", 726 | "হাসিকা", 727 | "হাসিছে", 728 | "হাসিতে", 729 | "হাসিনা" 730 | ] 731 | ); 732 | } 733 | 734 | #[test] 735 | fn test_backspace() { 736 | let mut method = FixedMethod { 737 | buffer: "আমি".to_string(), 738 | typed: "ami".to_string(), 739 | ..Default::default() 740 | }; 741 | 742 | let mut config = get_fixed_method_defaults(); 743 | let data = Data::new(&config); 744 | config.set_fixed_suggestion(false); 745 | 746 | assert!(!method.backspace_event(false, &data, &config).is_empty()); // আম 747 | assert!(!method.backspace_event(false, &data, &config).is_empty()); // আ 748 | assert!(method.backspace_event(false, &data, &config).is_empty()); // Empty 749 | assert!(method.buffer.is_empty()); 750 | assert!(method.typed.is_empty()); 751 | 752 | // Ctrl + Backspace 753 | method = FixedMethod { 754 | buffer: "আমি".to_string(), 755 | typed: "ami".to_string(), 756 | ..Default::default() 757 | }; 758 | assert!(method.backspace_event(true, &data, &config).is_empty()); 759 | } 760 | 761 | #[test] 762 | fn test_reph_insertion() { 763 | let mut method = FixedMethod::default(); 764 | 765 | method.buffer = "অক".to_string(); 766 | method.insert_old_style_reph(); 767 | assert_eq!(method.buffer, "অর্ক".to_string()); 768 | 769 | method.buffer = "ক".to_string(); 770 | method.insert_old_style_reph(); 771 | assert_eq!(method.buffer, "র্ক".to_string()); 772 | 773 | method.buffer = "কত".to_string(); 774 | method.insert_old_style_reph(); 775 | assert_eq!(method.buffer, "কর্ত".to_string()); 776 | 777 | method.buffer = "অক্কা".to_string(); 778 | method.insert_old_style_reph(); 779 | assert_eq!(method.buffer, "অর্ক্কা".to_string()); 780 | 781 | method.buffer = "কক্ষ্ম".to_string(); 782 | method.insert_old_style_reph(); 783 | assert_eq!(method.buffer, "কর্ক্ষ্ম".to_string()); 784 | 785 | method.buffer = "কব্যা".to_string(); 786 | method.insert_old_style_reph(); 787 | assert_eq!(method.buffer, "কর্ব্যা".to_string()); 788 | 789 | method.buffer = "কব্যাঁ".to_string(); 790 | method.insert_old_style_reph(); 791 | assert_eq!(method.buffer, "কর্ব্যাঁ".to_string()); 792 | } 793 | 794 | #[test] 795 | fn test_features() { 796 | let mut method = FixedMethod::default(); 797 | let mut config = get_fixed_method_defaults(); 798 | 799 | // Automatic Vowel Forming 800 | method.buffer = "".to_string(); 801 | method.process_key_value(&B_AA_KAR.to_string(), &config); 802 | assert_eq!(method.buffer, B_AA.to_string()); 803 | 804 | method.buffer = "আ".to_string(); 805 | method.process_key_value(&B_I_KAR.to_string(), &config); 806 | assert_eq!(method.buffer, "আই".to_string()); 807 | 808 | // Automatic Chandra position 809 | method.buffer = "কঁ".to_string(); 810 | method.process_key_value(&B_AA_KAR.to_string(), &config); 811 | assert_eq!(method.buffer, "কাঁ".to_string()); 812 | 813 | // Traditional Kar joining 814 | method.buffer = "র".to_string(); 815 | method.process_key_value(&B_U_KAR.to_string(), &config); 816 | assert_eq!(method.buffer, "র‌ু".to_string()); 817 | 818 | // Without Traditional Kar joining 819 | config.set_fixed_traditional_kar(false); 820 | 821 | method.buffer = "র".to_string(); 822 | method.process_key_value(&B_U_KAR.to_string(), &config); 823 | assert_eq!(method.buffer, "রু".to_string()); 824 | 825 | // Vowel making with Hasanta 826 | method.buffer = "্".to_string(); 827 | method.process_key_value(&B_U_KAR.to_string(), &config); 828 | assert_eq!(method.buffer, "উ".to_string()); 829 | 830 | method.buffer = "্".to_string(); 831 | method.process_key_value(&B_LENGTH_MARK.to_string(), &config); 832 | assert_eq!(method.buffer, "ঔ".to_string()); 833 | 834 | // Double Hasanta for Hasanta + ZWNJ 835 | method.buffer = B_HASANTA.to_string(); 836 | method.process_key_value(&B_HASANTA.to_string(), &config); 837 | assert_eq!(method.buffer, "\u{09CD}\u{200C}".to_string()); 838 | 839 | // Others 840 | method.buffer = "ক".to_string(); 841 | method.process_key_value(&B_KH.to_string(), &config); 842 | assert_eq!(method.buffer, "কখ".to_string()); 843 | 844 | method.buffer = "ক".to_string(); 845 | method.process_key_value(&B_AA_KAR.to_string(), &config); 846 | assert_eq!(method.buffer, "কা".to_string()); 847 | } 848 | 849 | #[test] 850 | fn test_z_zofola() { 851 | let mut method = FixedMethod::default(); 852 | let mut config = get_fixed_method_defaults(); 853 | config.set_fixed_suggestion(false); 854 | 855 | method.buffer = "র্".to_string(); 856 | method.process_key_value("য", &config); 857 | assert_eq!(method.buffer, "র্য"); 858 | 859 | method.buffer = "র".to_string(); 860 | method.process_key_value("্য", &config); 861 | assert_eq!(method.buffer, "র‍্য"); 862 | 863 | // When the last characters constitute the Ro-fola 864 | method.buffer = "ক্র".to_string(); 865 | method.process_key_value("্য", &config); 866 | assert_eq!(method.buffer, "ক্র্য"); 867 | 868 | method.buffer = "খ্".to_string(); 869 | method.process_key_value("য", &config); 870 | assert_eq!(method.buffer, "খ্য"); 871 | 872 | method.buffer = "খ".to_string(); 873 | method.process_key_value("্য", &config); 874 | assert_eq!(method.buffer, "খ্য"); 875 | } 876 | 877 | #[test] 878 | fn test_suggestion_traditional_kar() { 879 | let mut method = FixedMethod::default(); 880 | let mut config = get_fixed_method_defaults(); 881 | let data = Data::new(&config); 882 | 883 | /* With Traditional Kar Joining */ 884 | method.process_key_value("হ", &config); 885 | method.process_key_value("ৃ", &config); 886 | method.process_key_value("দ", &config); 887 | method.create_dictionary_suggestion(&data, &config); 888 | assert_eq!(method.suggestions, ["হ‌ৃদ", "হ‌ৃদি", "হ‌ৃদয়"]); 889 | method.buffer.clear(); 890 | 891 | method.process_key_value("হ", &config); 892 | method.process_key_value("ু", &config); 893 | method.process_key_value("ল", &config); 894 | method.process_key_value("া", &config); 895 | method.create_dictionary_suggestion(&data, &config); 896 | assert_eq!(method.suggestions, ["হ‌ুলা", "হ‌ুলানো", "হ‌ুলাহ‌ুলি"]); 897 | method.buffer.clear(); 898 | 899 | method.process_key_value("র", &config); 900 | method.process_key_value("ূ", &config); 901 | method.create_dictionary_suggestion(&data, &config); 902 | assert_eq!(method.suggestions, ["র‌ূ", "র‌ূপ", "র‌ূহ"]); 903 | method.buffer.clear(); 904 | 905 | /* Without Traditional Kar Joining */ 906 | config.set_fixed_traditional_kar(false); 907 | 908 | method.process_key_value("হ", &config); 909 | method.process_key_value("ৃ", &config); 910 | method.process_key_value("দ", &config); 911 | method.create_dictionary_suggestion(&data, &config); 912 | assert_eq!(method.suggestions, ["হৃদ", "হৃদি", "হৃদয়"]); 913 | method.buffer.clear(); 914 | 915 | method.process_key_value("হ", &config); 916 | method.process_key_value("ু", &config); 917 | method.process_key_value("ল", &config); 918 | method.process_key_value("া", &config); 919 | method.create_dictionary_suggestion(&data, &config); 920 | assert_eq!(method.suggestions, ["হুলা", "হুলানো", "হুলাহুলি"]); 921 | method.buffer.clear(); 922 | 923 | method.process_key_value("র", &config); 924 | method.process_key_value("ূ", &config); 925 | method.create_dictionary_suggestion(&data, &config); 926 | assert_eq!(method.suggestions, ["রূ", "রূপ", "রূহ"]); 927 | method.buffer.clear(); 928 | } 929 | 930 | #[test] 931 | fn test_old_kar_order() { 932 | let mut method = FixedMethod::default(); 933 | let mut config = get_fixed_method_defaults(); 934 | let data = Data::new(&config); 935 | config.set_fixed_old_kar_order(true); 936 | 937 | method.buffer = "".to_string(); 938 | method.process_key_value("ৈ", &config); 939 | method.process_key_value("ক", &config); 940 | assert_eq!(method.buffer, "কৈ".to_string()); 941 | 942 | method.buffer = "তে".to_string(); 943 | method.process_key_value("া", &config); 944 | assert_eq!(method.buffer, "তো".to_string()); 945 | 946 | method.buffer = "মে".to_string(); 947 | method.process_key_value(&B_OU_KAR.to_string(), &config); 948 | assert_eq!(method.buffer, "মৌ".to_string()); 949 | 950 | method.buffer = "মে".to_string(); 951 | method.process_key_value("ৗ", &config); 952 | assert_eq!(method.buffer, "মৌ".to_string()); 953 | 954 | method.buffer = "সি".to_string(); 955 | method.process_key_value(&B_HASANTA.to_string(), &config); 956 | method.process_key_value("ক", &config); 957 | assert_eq!(method.buffer, "স্কি".to_string()); 958 | 959 | method.buffer = "".to_string(); 960 | method.process_key_value("ি", &config); 961 | method.process_key_value("স", &config); 962 | method.process_key_value(&B_HASANTA.to_string(), &config); 963 | method.process_key_value("ট", &config); 964 | method.process_key_value("ম", &config); 965 | assert_eq!(method.buffer, "স্টিম".to_string()); 966 | 967 | method.buffer = "তি".to_string(); 968 | method.process_key_value("্র", &config); 969 | assert_eq!(method.buffer, "ত্রি".to_string()); 970 | method.buffer = "তি".to_string(); 971 | method.process_key_value("\u{09CD}\u{09AF}", &config); 972 | assert_eq!(method.buffer, "ত্যি".to_string()); 973 | 974 | // Backspace 975 | method.buffer = "".to_string(); 976 | method.process_key_value("ে", &config); 977 | assert!(method.backspace_event(false, &data, &config).is_empty()); 978 | assert!(method.buffer.is_empty()); 979 | assert!(method.typed.is_empty()); 980 | 981 | method.buffer = "ক".to_string(); 982 | method.process_key_value("ি", &config); 983 | assert!(!method.backspace_event(false, &data, &config).is_empty()); 984 | assert_eq!(method.buffer, "ক".to_string()); 985 | 986 | method.buffer = "ক".to_string(); 987 | assert!(method.backspace_event(false, &data, &config).is_empty()); 988 | assert!(method.buffer.is_empty()); 989 | assert!(method.typed.is_empty()); 990 | 991 | // Vowel making with Hasanta 992 | method.buffer = "ক".to_string(); 993 | method.process_key_value(&B_HASANTA.to_string(), &config); 994 | method.process_key_value("ি", &config); 995 | assert_eq!(method.buffer, "কই".to_string()); 996 | 997 | method.buffer = "কে".to_string(); 998 | method.process_key_value(&B_HASANTA.to_string(), &config); 999 | method.process_key_value("ু", &config); 1000 | assert_eq!(method.buffer, "কেউ".to_string()); 1001 | 1002 | // Automatic Vowel Forming 1003 | method.buffer = "".to_string(); 1004 | method.process_key_value("ে", &config); 1005 | method.process_key_value("ো", &config); 1006 | assert_eq!(method.buffer, "এও".to_string()); 1007 | 1008 | // With Old style Reph 1009 | method.buffer = "দ".to_string(); 1010 | method.process_key_value("ি", &config); 1011 | method.process_key_value("জ", &config); 1012 | method.process_key_value("র্", &config); 1013 | assert_eq!(method.buffer, "দর্জি".to_string()); 1014 | 1015 | // Without Old style Reph 1016 | config.set_fixed_old_reph(false); 1017 | 1018 | method.buffer = "দ".to_string(); 1019 | method.process_key_value("ি", &config); 1020 | method.process_key_value("র্", &config); 1021 | method.process_key_value("জ", &config); 1022 | assert_eq!(method.buffer, "দর্জি".to_string()); 1023 | } 1024 | } 1025 | --------------------------------------------------------------------------------