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