hangul_cd/
string.rs

1use thiserror::Error;
2
3use crate::{
4    jamo::{Jamo, JamoPosition},
5    word::*,
6};
7
8/// An error type for `StringComposer` operations.
9#[derive(Error, Debug, PartialEq, Eq)]
10pub enum StringError {
11    /// Occurs when there is an error related to word composition.
12    #[error("Word error: {0}")]
13    WordError(#[from] WordError),
14}
15
16/// A composer struct that manages the composition of strings of text
17/// consisting of multiple words, including both Hangul words and non-Hangul
18/// text.
19///
20/// The `StringComposer` maintains both a string of completed text and a
21/// `HangulWordComposer` for the current word being composed. If the currently
22/// active word is a Hangul word that has not yet been completed, pushing or
23/// popping characters will interact with the `HangulWordComposer` and directly
24/// update syllable blocks. Otherwise, Unicode characters will be directly
25/// added to or removed from the completed string.
26///
27/// **API:**
28/// ```rust
29/// use hangul::string::StringComposer;
30///
31/// let mut composer = StringComposer::new();
32///
33/// // Push characters to form Hangul syllables
34/// composer.push_char('ㅎ').unwrap();
35/// composer.push_char('ㅏ').unwrap();
36///
37/// // Get the composed string
38/// let result = composer.as_string().unwrap();
39/// assert_eq!(result, "하".to_string());
40///
41/// // Push non-Hangul characters
42/// composer.push_char(' ').unwrap();
43/// composer.push_char('!').unwrap();
44/// assert_eq!(composer.as_string().unwrap(), "하 !".to_string());
45///
46/// // Popping non-Hangul characters removes them from the completed string
47/// composer.pop().unwrap(); // removes '!'
48/// composer.pop().unwrap(); // removes ' '
49/// assert_eq!(composer.as_string().unwrap(), "하".to_string());
50///
51/// // Popping Hangul characters after they've been completed removes entire syllables
52/// composer.pop().unwrap(); // removes '하'
53/// assert_eq!(composer.as_string().unwrap(), "".to_string());
54///
55/// // Popping characters while a Hangul word is active removes jamo
56/// composer.push_char('ㅂ').unwrap();
57/// composer.push_char('ㅏ').unwrap();
58/// composer.push_char('ㅂ').unwrap();
59/// composer.pop().unwrap(); // removes 'ㅂ'
60/// assert_eq!(composer.as_string().unwrap(), "바".to_string());
61/// ```
62#[derive(Debug)]
63pub struct StringComposer {
64    completed: String,
65    current: HangulWordComposer,
66}
67
68impl StringComposer {
69    /// Creates a new, empty `StringComposer`.
70    pub fn new() -> Self {
71        Self {
72            completed: String::new(),
73            current: HangulWordComposer::new(),
74        }
75    }
76
77    /// Pushes a character to the `StringComposer`.
78    ///
79    /// If the character is part of a Hangul word, it will be composed into syllables.
80    /// Otherwise, it will be added directly to the completed string.
81    pub fn push_char(&mut self, c: char) -> Result<(), StringError> {
82        match self.current.push_char(c)? {
83            WordPushResult::Continue => Ok(()),
84            _ => self.handle_invalid_input(c),
85        }
86    }
87
88    /// Returns the composed string, combining completed text and the current word.
89    pub fn as_string(&self) -> Result<String, StringError> {
90        let mut result = self.completed.clone();
91        let current_string = self.current.as_string()?;
92        result.push_str(&current_string);
93        Ok(result)
94    }
95
96    /// Pops the last character from the `StringComposer` and returns it wrapped
97    /// within a `Result` and `Option`.
98    ///
99    /// If the current word is a Hangul word with uncompleted syllables, it will
100    /// remove the last jamo from the current syllable block. Otherwise, it will
101    /// remove the last character from the completed string.
102    pub fn pop(&mut self) -> Result<Option<char>, StringError> {
103        match self.current.pop()? {
104            Some(c) => Ok(c.char_modern(match c {
105                Jamo::Consonant(_) | Jamo::CompositeConsonant(_) => JamoPosition::Initial,
106                Jamo::Vowel(_) | Jamo::CompositeVowel(_) => JamoPosition::Vowel,
107            })),
108            None => match self.completed.pop() {
109                Some(c) => Ok(Some(c)),
110                None => Ok(None),
111            },
112        }
113    }
114
115    fn handle_invalid_input(&mut self, c: char) -> Result<(), StringError> {
116        let current_string = self.current.as_string()?;
117        self.completed.push_str(&current_string);
118        self.completed.push(c);
119        self.current = HangulWordComposer::new();
120        Ok(())
121    }
122}
123
124#[cfg(test)]
125mod test {
126    use super::*;
127
128    #[test]
129    fn test_no_new_words() {
130        let input = "ㅎㅏㄴㄱㅡㄹ";
131        let mut composer = StringComposer::new();
132        for c in input.chars() {
133            composer.push_char(c).unwrap();
134        }
135        let result = composer.as_string().unwrap();
136        assert_eq!(result, "한글".to_string());
137    }
138
139    #[test]
140    fn test_new_hangul_word() {
141        let input = "ㅎㅏㄴㄱㅡㄹ ㅇㅏㄴㄴㅕㅇㅎㅏㅅㅔㅇㅛ";
142        let mut composer = StringComposer::new();
143        for c in input.chars() {
144            composer.push_char(c).unwrap();
145        }
146        let result = composer.as_string().unwrap();
147        assert_eq!(result, "한글 안녕하세요".to_string());
148    }
149
150    #[test]
151    fn test_new_non_hangul_word() {
152        let input = "ㅎㅏㄴㄱㅡㄹ beans";
153        let mut composer = StringComposer::new();
154        for c in input.chars() {
155            composer.push_char(c).unwrap();
156        }
157        let result = composer.as_string().unwrap();
158        assert_eq!(result, "한글 beans".to_string());
159    }
160
161    #[test]
162    fn test_multiple_words() {
163        let input = "ㅎㅏㄴㄱㅡㄹ 123  \n ㅇㅏㄴㄴㅕㅇ!";
164        let mut composer = StringComposer::new();
165        for c in input.chars() {
166            composer.push_char(c).unwrap();
167        }
168        let result = composer.as_string().unwrap();
169        assert_eq!(result, "한글 123  \n 안녕!".to_string());
170    }
171
172    #[test]
173    fn test_backspace() {
174        let input = "ㅇㅏㄴㄴㅕㅇ ㄹㅏㅁㅕㄴ";
175        let mut composer = StringComposer::new();
176        for c in input.chars() {
177            composer.push_char(c).unwrap();
178        }
179        for _ in 0..7 {
180            composer.pop().unwrap();
181        }
182        let result = composer.as_string().unwrap();
183        assert_eq!(result, "안".to_string());
184    }
185}