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(¤t_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(¤t_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}