1 // encoding: utf-8
2 //
3 // Copyright RIME Developers
4 // Distributed under the BSD License
5 //
6 // 2011-11-21 GONG Chen <chen.sst@gmail.com>
7 //
8 #include <utf8.h>
9 #include <rime/commit_history.h>
10 #include <rime/common.h>
11 #include <rime/composition.h>
12 #include <rime/context.h>
13 #include <rime/engine.h>
14 #include <rime/key_event.h>
15 #include <rime/key_table.h>
16 #include <rime/menu.h>
17 #include <rime/schema.h>
18 #include <rime/translation.h>
19 #include <rime/gear/punctuator.h>
20
21 namespace rime {
22
LoadConfig(Engine * engine,bool load_symbols)23 void PunctConfig::LoadConfig(Engine* engine, bool load_symbols) {
24 bool full_shape = engine->context()->get_option("full_shape");
25 string shape(full_shape ? "full_shape" : "half_shape");
26 if (shape_ == shape)
27 return;
28 shape_ = shape;
29 Config* config = engine->schema()->config();
30 mapping_ = config->GetMap("punctuator/" + shape);
31 if (!mapping_) {
32 LOG(WARNING) << "missing punctuation mapping.";
33 }
34 if (load_symbols) {
35 symbols_ = config->GetMap("punctuator/symbols");
36 }
37 }
38
GetPunctDefinition(const string key)39 an<ConfigItem> PunctConfig::GetPunctDefinition(const string key) {
40 an<ConfigItem> result = mapping_ ? mapping_->Get(key) : nullptr;
41 return result ? result : symbols_ ? symbols_->Get(key) : nullptr;
42 }
43
Punctuator(const Ticket & ticket)44 Punctuator::Punctuator(const Ticket& ticket) : Processor(ticket) {
45 Config* config = engine_->schema()->config();
46 if (config) {
47 config->GetBool("punctuator/use_space", &use_space_);
48 }
49 config_.LoadConfig(engine_);
50 }
51
punctuation_is_translated(Context * ctx)52 static bool punctuation_is_translated(Context* ctx) {
53 Composition& comp = ctx->composition();
54 if (comp.empty() || !comp.back().HasTag("punct")) {
55 return false;
56 }
57 auto cand = comp.back().GetSelectedCandidate();
58 return cand && cand->type() == "punct";
59 }
60
ProcessKeyEvent(const KeyEvent & key_event)61 ProcessResult Punctuator::ProcessKeyEvent(const KeyEvent& key_event) {
62 if (key_event.release() || key_event.ctrl() || key_event.alt())
63 return kNoop;
64 int ch = key_event.keycode();
65 if (ch < 0x20 || ch >= 0x7f)
66 return kNoop;
67 Context *ctx = engine_->context();
68 if (ctx->get_option("ascii_punct")) {
69 return kNoop;
70 }
71 if (!use_space_ && ch == XK_space && ctx->IsComposing()) {
72 return kNoop;
73 }
74 if (ch == '.' || ch == ':') { // 3.14, 12:30
75 const CommitHistory& history(ctx->commit_history());
76 if (!history.empty()) {
77 const CommitRecord& cr(history.back());
78 if (cr.type == "thru" &&
79 cr.text.length() == 1 && isdigit(cr.text[0])) {
80 return kRejected;
81 }
82 }
83 }
84 config_.LoadConfig(engine_);
85 string punct_key(1, ch);
86 auto punct_definition = config_.GetPunctDefinition(punct_key);
87 if (!punct_definition)
88 return kNoop;
89 DLOG(INFO) << "punct key: '" << punct_key << "'";
90 if (!AlternatePunct(punct_key, punct_definition)) {
91 ctx->PushInput(ch) &&
92 punctuation_is_translated(ctx) &&
93 (ConfirmUniquePunct(punct_definition) ||
94 AutoCommitPunct(punct_definition) ||
95 PairPunct(punct_definition));
96 }
97 return kAccepted;
98 }
99
AlternatePunct(const string & key,const an<ConfigItem> & definition)100 bool Punctuator::AlternatePunct(const string& key,
101 const an<ConfigItem>& definition) {
102 if (!As<ConfigList>(definition))
103 return false;
104 Context* ctx = engine_->context();
105 Composition& comp = ctx->composition();
106 if (comp.empty())
107 return false;
108 Segment& segment(comp.back());
109 if (segment.status > Segment::kVoid &&
110 segment.HasTag("punct") &&
111 key == ctx->input().substr(segment.start, segment.end - segment.start)) {
112 if (!segment.menu ||
113 segment.menu->Prepare(segment.selected_index + 2) == 0) {
114 LOG(ERROR) << "missing candidate for punctuation '" << key << "'.";
115 return false;
116 }
117 DLOG(INFO) << "alternating punctuation '" << key << "'.";
118 (segment.selected_index += 1) %= segment.menu->candidate_count();
119 segment.status = Segment::kGuess;
120 return true;
121 }
122 return false;
123 }
124
ConfirmUniquePunct(const an<ConfigItem> & definition)125 bool Punctuator::ConfirmUniquePunct(const an<ConfigItem>& definition) {
126 if (!As<ConfigValue>(definition))
127 return false;
128 engine_->context()->ConfirmCurrentSelection();
129 return true;
130 }
131
AutoCommitPunct(const an<ConfigItem> & definition)132 bool Punctuator::AutoCommitPunct(const an<ConfigItem>& definition) {
133 auto map = As<ConfigMap>(definition);
134 if (!map || !map->HasKey("commit"))
135 return false;
136 engine_->context()->Commit();
137 return true;
138 }
139
PairPunct(const an<ConfigItem> & definition)140 bool Punctuator::PairPunct(const an<ConfigItem>& definition) {
141 auto map = As<ConfigMap>(definition);
142 if (!map || !map->HasKey("pair"))
143 return false;
144 Context* ctx = engine_->context();
145 Composition& comp = ctx->composition();
146 if (comp.empty())
147 return false;
148 Segment& segment(comp.back());
149 if (segment.status > Segment::kVoid && segment.HasTag("punct")) {
150 if (!segment.menu || segment.menu->Prepare(2) < 2) {
151 LOG(ERROR) << "missing candidate for paired punctuation.";
152 return false;
153 }
154 DLOG(INFO) << "alternating paired punctuation.";
155 auto& oddness(oddness_[definition]);
156 (segment.selected_index += oddness) %= 2;
157 oddness = 1 - oddness;
158 ctx->ConfirmCurrentSelection();
159 return true;
160 }
161 return false;
162 }
163
PunctSegmentor(const Ticket & ticket)164 PunctSegmentor::PunctSegmentor(const Ticket& ticket) : Segmentor(ticket) {
165 config_.LoadConfig(engine_);
166 }
167
Proceed(Segmentation * segmentation)168 bool PunctSegmentor::Proceed(Segmentation* segmentation) {
169 const string& input = segmentation->input();
170 int k = segmentation->GetCurrentStartPosition();
171 if (k == input.length())
172 return false; // no chance for others too
173 char ch = input[k];
174 if (ch < 0x20 || ch >= 0x7f)
175 return true;
176 config_.LoadConfig(engine_);
177 string punct_key(1, ch);
178 auto punct_definition = config_.GetPunctDefinition(punct_key);
179 if (!punct_definition)
180 return true;
181 {
182 Segment segment(k, k + 1);
183 DLOG(INFO) << "add a punctuation segment ["
184 << segment.start << ", " << segment.end << ")";
185 segment.tags.insert("punct");
186 segmentation->AddSegment(segment);
187 }
188 return false; // exclusive
189 }
190
PunctTranslator(const Ticket & ticket)191 PunctTranslator::PunctTranslator(const Ticket& ticket)
192 : Translator(ticket) {
193 const bool load_symbols = true;
194 config_.LoadConfig(engine_, load_symbols);
195 }
196
197 an<Candidate>
CreatePunctCandidate(const string & punct,const Segment & segment)198 CreatePunctCandidate(const string& punct, const Segment& segment) {
199 const char half_shape[] =
200 "\xe3\x80\x94\xe5\x8d\x8a\xe8\xa7\x92\xe3\x80\x95"; // 〔半角〕
201 const char full_shape[] =
202 "\xe3\x80\x94\xe5\x85\xa8\xe8\xa7\x92\xe3\x80\x95"; // 〔全角〕
203 bool is_half_shape = false;
204 bool is_full_shape = false;
205 const char* p = punct.c_str();
206 uint32_t ch = utf8::unchecked::next(p);
207 if (*p == '\0') { // length == 1 unicode character
208 bool is_ascii = (ch >= 0x20 && ch < 0x7F);
209 bool is_ideographic_space = (ch == 0x3000);
210 bool is_full_shape_ascii = (ch >= 0xFF01 && ch <= 0xFF5E);
211 bool is_half_shape_kana = (ch >= 0xFF65 && ch <= 0xFFDC);
212 is_half_shape = is_ascii || is_half_shape_kana;
213 is_full_shape = is_ideographic_space || is_full_shape_ascii;
214 }
215 bool one_key = (segment.end - segment.start == 1);
216 return New<SimpleCandidate>("punct",
217 segment.start,
218 segment.end,
219 punct,
220 (is_half_shape ? half_shape :
221 is_full_shape ? full_shape : ""),
222 one_key ? punct : "");
223 }
224
Query(const string & input,const Segment & segment)225 an<Translation> PunctTranslator::Query(const string& input,
226 const Segment& segment) {
227 if (!segment.HasTag("punct"))
228 return nullptr;
229 config_.LoadConfig(engine_);
230 auto definition = config_.GetPunctDefinition(input);
231 if (!definition)
232 return nullptr;
233 DLOG(INFO) << "populating punctuation candidates for '" << input << "'.";
234 auto translation = TranslateUniquePunct(input, segment,
235 As<ConfigValue>(definition));
236 if (!translation)
237 translation = TranslateAlternatingPunct(input, segment,
238 As<ConfigList>(definition));
239 if (!translation)
240 translation = TranslateAutoCommitPunct(input, segment,
241 As<ConfigMap>(definition));
242 if (!translation)
243 translation = TranslatePairedPunct(input, segment,
244 As<ConfigMap>(definition));
245 //if (translation) {
246 // const char tips[] =
247 // "\xe3\x80\x94\xe7\xac\xa6\xe8\x99\x9f\xe3\x80\x95"; // 〔符號〕
248 // const_cast<Segment*>(&segment)->prompt = tips;
249 //}
250 return translation;
251 }
252
253 an<Translation>
TranslateUniquePunct(const string & key,const Segment & segment,const an<ConfigValue> & definition)254 PunctTranslator::TranslateUniquePunct(const string& key,
255 const Segment& segment,
256 const an<ConfigValue>& definition) {
257 if (!definition)
258 return nullptr;
259 return New<UniqueTranslation>(
260 CreatePunctCandidate(definition->str(), segment));
261 }
262
263 an<Translation>
TranslateAlternatingPunct(const string & key,const Segment & segment,const an<ConfigList> & definition)264 PunctTranslator::TranslateAlternatingPunct(const string& key,
265 const Segment& segment,
266 const an<ConfigList>& definition) {
267 if (!definition)
268 return nullptr;
269 auto translation = New<FifoTranslation>();
270 for (size_t i = 0; i < definition->size(); ++i) {
271 auto value = definition->GetValueAt(i);
272 if (!value) {
273 LOG(WARNING) << "invalid alternating punct at index " << i
274 << " for '" << key << "'.";
275 continue;
276 }
277 translation->Append(CreatePunctCandidate(value->str(), segment));
278 }
279 if (!translation->size()) {
280 LOG(WARNING) << "empty candidate list for alternating punct '"
281 << key << "'.";
282 translation.reset();
283 }
284 return translation;
285 }
286
287 an<Translation>
TranslateAutoCommitPunct(const string & key,const Segment & segment,const an<ConfigMap> & definition)288 PunctTranslator::TranslateAutoCommitPunct(const string& key,
289 const Segment& segment,
290 const an<ConfigMap>& definition) {
291 if (!definition || !definition->HasKey("commit"))
292 return nullptr;
293 auto value = definition->GetValue("commit");
294 if (!value) {
295 LOG(WARNING) << "unrecognized punct definition for '" << key << "'.";
296 return nullptr;
297 }
298 return New<UniqueTranslation>(CreatePunctCandidate(value->str(), segment));
299 }
300
301 an<Translation>
TranslatePairedPunct(const string & key,const Segment & segment,const an<ConfigMap> & definition)302 PunctTranslator::TranslatePairedPunct(const string& key,
303 const Segment& segment,
304 const an<ConfigMap>& definition) {
305 if (!definition || !definition->HasKey("pair"))
306 return nullptr;
307 auto list = As<ConfigList>(definition->Get("pair"));
308 if (!list || list->size() != 2) {
309 LOG(WARNING) << "unrecognized pair definition for '" << key << "'.";
310 return nullptr;
311 }
312 auto translation = New<FifoTranslation>();
313 for (size_t i = 0; i < list->size(); ++i) {
314 auto value = list->GetValueAt(i);
315 if (!value) {
316 LOG(WARNING) << "invalid paired punct at index " << i
317 << " for '" << key << "'.";
318 continue;
319 }
320 translation->Append(CreatePunctCandidate(value->str(), segment));
321 }
322 if (translation->size() != 2) {
323 LOG(WARNING) << "invalid num of candidate for paired punct '"
324 << key << "'.";
325 translation.reset();
326 }
327 return translation;
328 }
329
330 } // namespace rime
331