use crate::parser::Parser;
pub struct Edit<'a> {
pub pass_through: bool,
pub byte_backspaces: usize,
pub backspaces: usize,
pub output: &'a str,
}
impl Default for Edit<'_> {
fn default() -> Self {
Edit {
pass_through: true,
byte_backspaces: 0,
backspaces: 0,
output: "",
}
}
}
pub struct Editor {
parser: Parser,
input_buffer: String,
output: String,
}
impl Editor {
pub fn new_phonetic() -> Editor {
Editor {
parser: Parser::new_phonetic(),
input_buffer: String::new(),
output: String::new(),
}
}
fn reset(&mut self) {
self.input_buffer.clear();
self.output.clear();
}
pub fn put_char<'a>(&'a mut self, new_ch: char) -> Edit<'a> {
if !new_ch.is_ascii_graphic() {
self.reset();
Edit::default()
} else {
self.input_buffer.push(new_ch);
let output = self.parser.convert(&self.input_buffer);
let mut at = self
.output
.bytes()
.zip(output.bytes())
.take_while(|(a, b)| a == b)
.count();
while at > 0 && !self.output.is_char_boundary(at) {
at -= 1;
}
let byte_backspaces = self.output.len() - at;
let backspaces = self.output[at..].chars().count();
self.output = output;
Edit {
pass_through: false,
byte_backspaces,
backspaces,
output: &self.output[at..],
}
}
}
pub fn put_non_char<'a>(&'a mut self) -> Edit<'a> {
self.reset();
Edit::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn editor_test() {
let mut editor = Editor::new_phonetic();
editor.put_char('k');
assert_eq!("ক", editor.output);
editor.put_char('o');
assert_eq!("ক", editor.output);
editor.put_char('r');
assert_eq!("কর", editor.output);
editor.put_char('r');
assert_eq!("করর", editor.output);
editor.put_char('m');
assert_eq!("কর্ম", editor.output);
editor.put_char(' ');
assert_eq!("", editor.output);
}
fn type_word(editor: &mut Editor, keys: &str) -> String {
let parser = Parser::new_phonetic();
let mut raw = String::new();
let mut screen = String::new();
for key in keys.chars() {
raw.push(key);
let edit = editor.put_char(key);
let keep = screen.len() - edit.byte_backspaces;
screen.truncate(keep);
screen.push_str(edit.output);
assert_eq!(screen, parser.convert(&raw), "after {key:?} of {keys:?}");
}
screen
}
#[test]
fn converges_on_the_parser() {
for word in ["ami", "banglay", "kkhoma", "kxoma", "bangladesh", "3.14"] {
let mut editor = Editor::new_phonetic();
assert_eq!(
type_word(&mut editor, word),
Parser::new_phonetic().convert(word)
);
}
}
#[test]
fn a_vowel_sign_lands_on_the_base_already_on_screen() {
let mut editor = Editor::new_phonetic();
editor.put_char('a');
editor.put_char('m');
let edit = editor.put_char('i');
assert_eq!(edit.backspaces, 0);
assert_eq!(edit.output, "ি");
}
#[test]
fn a_reanalysed_conjunct_rewrites_only_its_tail() {
let mut editor = Editor::new_phonetic();
assert_eq!(editor.put_char('k').output, "ক");
let edit = editor.put_char('x');
assert_eq!(edit.backspaces, 0);
assert_eq!(edit.output, "\u{09CD}\u{09B7}");
}
#[test]
fn erasing_is_bounded_by_the_shared_prefix() {
let mut editor = Editor::new_phonetic();
let mut worst = 0;
for ch in "banglakobitarboi".chars() {
worst = worst.max(editor.put_char(ch).backspaces);
}
assert!(worst <= 2, "worst erase was {worst}");
}
#[test]
fn reset_leaves_the_screen_alone() {
let mut editor = Editor::new_phonetic();
editor.put_char('a');
editor.put_char('m');
editor.put_non_char();
assert!(editor.output.is_empty());
assert_eq!(editor.output, "");
assert_eq!(editor.put_char('i').backspaces, 0);
}
#[test]
fn a_word_never_reaches_back_across_a_reset() {
let mut editor = Editor::new_phonetic();
editor.put_char('k');
editor.put_non_char();
assert_eq!(editor.put_char('x').output, "এক্স");
}
#[test]
fn matches_the_previous_editor_behaviour() {
let mut editor = Editor::new_phonetic();
for (ch, expected) in [
('k', "ক"),
('o', "ক"),
('r', "কর"),
('r', "করর"),
('m', "কর্ম"),
] {
editor.put_char(ch);
assert_eq!(editor.output, expected, "after {ch:?}");
}
editor.put_non_char();
assert_eq!(editor.output, "");
}
#[test]
fn a_character_can_be_taken_without_changing_the_screen() {
let mut editor = Editor::new_phonetic();
editor.put_char('k');
let edit = editor.put_char('o');
assert_eq!(edit.backspaces, 0);
assert_eq!(edit.byte_backspaces, 0);
assert_eq!(edit.output, "");
assert!(!edit.pass_through, "the o was buffered");
assert_eq!(editor.put_char('r').output, "র");
}
#[test]
fn an_escape_can_erase_without_typing_anything() {
let mut editor = Editor::new_phonetic();
assert_eq!(editor.put_char('o').output, "অ");
let edit = editor.put_char('`');
assert_eq!(edit.backspaces, 1);
assert_eq!(edit.output, "");
assert!(!edit.pass_through, "there is still an অ to erase");
}
#[test]
fn the_two_counts_measure_the_same_tail() {
let mut editor = Editor::new_phonetic();
editor.put_char('o');
let edit = editor.put_char('`');
assert_eq!(edit.backspaces, 1);
assert_eq!(edit.byte_backspaces, 3);
}
#[test]
fn a_space_is_passed_through() {
let mut editor = Editor::new_phonetic();
for ch in "ami".chars() {
editor.put_char(ch);
}
let edit = editor.put_char(' ');
assert!(edit.pass_through);
assert_eq!(edit.backspaces, 0);
assert_eq!(edit.byte_backspaces, 0);
assert_eq!(edit.output, "");
}
#[test]
fn put_non_char_always_passes_through() {
let mut editor = Editor::new_phonetic();
assert!(editor.put_non_char().pass_through);
editor.put_char('k');
assert!(editor.put_non_char().pass_through);
}
#[test]
fn non_ascii_is_refused_rather_than_panicking() {
for ch in ['é', 'অ', '\u{1F600}'] {
let mut editor = Editor::new_phonetic();
editor.put_char('a');
assert!(editor.put_char(ch).pass_through, "{ch:?} should be ignored");
assert_eq!(editor.output, "", "{ch:?} should have ended the word");
}
}
#[test]
fn control_characters_never_enter_the_buffer() {
for ch in ['\u{8}', '\u{1b}', '\u{7f}'] {
let mut editor = Editor::new_phonetic();
editor.put_char('a');
assert!(editor.put_char(ch).pass_through, "{ch:?} should be ignored");
assert_eq!(editor.output, "", "{ch:?} should have ended the word");
}
}
#[test]
fn punctuation_and_escapes_survive_the_live_preview() {
for keys in ["ami.", "ami..", "bhalo:", "100$", "ka,,kha", "3.14", "o`"] {
let mut editor = Editor::new_phonetic();
assert_eq!(
type_word(&mut editor, keys),
Parser::new_phonetic().convert(keys),
"typing {keys:?}"
);
}
}
}