Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(30)

Side by Side Diff: third_party/prediction/suggest/core/dicnode/dic_node.h

Issue 1247903003: Add spellcheck and word suggestion to the prediction service (Closed) Base URL: https://github.com/domokit/mojo.git@master
Patch Set: Created 5 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 /*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #ifndef LATINIME_DIC_NODE_H
18 #define LATINIME_DIC_NODE_H
19
20 #include "third_party/prediction/defines.h"
21 #include "third_party/prediction/suggest/core/dicnode/dic_node_profiler.h"
22 #include "third_party/prediction/suggest/core/dicnode/dic_node_utils.h"
23 #include "third_party/prediction/suggest/core/dicnode/internal/dic_node_state.h"
24 #include "third_party/prediction/suggest/core/dicnode/internal/dic_node_properti es.h"
25 #include "third_party/prediction/suggest/core/dictionary/digraph_utils.h"
26 #include "third_party/prediction/suggest/core/dictionary/error_type_utils.h"
27 #include "third_party/prediction/suggest/core/layout/proximity_info_state.h"
28 #include "third_party/prediction/utils/char_utils.h"
29
30 #if DEBUG_DICT
31 #define LOGI_SHOW_ADD_COST_PROP \
32 do { \
33 char charBuf[50]; \
34 INTS_TO_CHARS(getOutputWordBuf(), getNodeCodePointCount(), charBuf, \
35 NELEMS(charBuf)); \
36 AKLOGI( \
37 "%20s, \"%c\", size = %03d, total = %03d, index(0) = %02d, dist = " \
38 "%.4f, %s,,", \
39 __FUNCTION__, getNodeCodePoint(), inputSize, getTotalInputIndex(), \
40 getInputIndex(0), getNormalizedCompoundDistance(), charBuf); \
41 } while (0)
42 #define DUMP_WORD_AND_SCORE(header) \
43 do { \
44 char charBuf[50]; \
45 INTS_TO_CHARS(getOutputWordBuf(), \
46 getNodeCodePointCount() + \
47 mDicNodeState.mDicNodeStateOutput.getPrevWordsLength(), \
48 charBuf, NELEMS(charBuf)); \
49 AKLOGI("#%8s, %5f, %5f, %5f, %5f, %s, %d, %5f,", header, \
50 getSpatialDistanceForScoring(), \
51 mDicNodeState.mDicNodeStateScoring.getLanguageDistance(), \
52 getNormalizedCompoundDistance(), getRawLength(), charBuf, \
53 getInputIndex(0), getNormalizedCompoundDistanceAfterFirstWord()); \
54 } while (0)
55 #else
56 #define LOGI_SHOW_ADD_COST_PROP
57 #define DUMP_WORD_AND_SCORE(header)
58 #endif
59
60 namespace latinime {
61
62 // This struct is purely a bucket to return values. No instances of this struct
63 // should be kept.
64 struct DicNode_InputStateG {
65 DicNode_InputStateG()
66 : mNeedsToUpdateInputStateG(false),
67 mPointerId(0),
68 mInputIndex(0),
69 mPrevCodePoint(0),
70 mTerminalDiffCost(0.0f),
71 mRawLength(0.0f),
72 mDoubleLetterLevel(NOT_A_DOUBLE_LETTER) {}
73
74 bool mNeedsToUpdateInputStateG;
75 int mPointerId;
76 int16_t mInputIndex;
77 int mPrevCodePoint;
78 float mTerminalDiffCost;
79 float mRawLength;
80 DoubleLetterLevel mDoubleLetterLevel;
81 };
82
83 class DicNode {
84 // Caveat: We define Weighting as a friend class of DicNode to let Weighting
85 // change
86 // the distance of DicNode.
87 // Caution!!! In general, we avoid using the "friend" access modifier.
88 // This is an exception to explicitly hide DicNode::addCost() from all classes
89 // but Weighting.
90 friend class Weighting;
91
92 public:
93 #if DEBUG_DICT
94 DicNodeProfiler mProfiler;
95 #endif
96
97 AK_FORCE_INLINE DicNode()
98 :
99 #if DEBUG_DICT
100 mProfiler(),
101 #endif
102 mDicNodeProperties(),
103 mDicNodeState(),
104 mIsCachedForNextSuggestion(false) {
105 }
106
107 DicNode(const DicNode& dicNode);
108 DicNode& operator=(const DicNode& dicNode);
109 ~DicNode() {}
110
111 // Init for copy
112 void initByCopy(const DicNode* const dicNode) {
113 mIsCachedForNextSuggestion = dicNode->mIsCachedForNextSuggestion;
114 mDicNodeProperties.initByCopy(&dicNode->mDicNodeProperties);
115 mDicNodeState.initByCopy(&dicNode->mDicNodeState);
116 PROF_NODE_COPY(&dicNode->mProfiler, mProfiler);
117 }
118
119 // Init for root with prevWordsPtNodePos which is used for n-gram
120 void initAsRoot(const int rootPtNodeArrayPos,
121 const int* const prevWordsPtNodePos) {
122 mIsCachedForNextSuggestion = false;
123 mDicNodeProperties.init(rootPtNodeArrayPos, prevWordsPtNodePos);
124 mDicNodeState.init();
125 PROF_NODE_RESET(mProfiler);
126 }
127
128 // Init for root with previous word
129 void initAsRootWithPreviousWord(const DicNode* const dicNode,
130 const int rootPtNodeArrayPos) {
131 mIsCachedForNextSuggestion = dicNode->mIsCachedForNextSuggestion;
132 int newPrevWordsPtNodePos[MAX_PREV_WORD_COUNT_FOR_N_GRAM];
133 newPrevWordsPtNodePos[0] = dicNode->mDicNodeProperties.getPtNodePos();
134 for (size_t i = 1; i < NELEMS(newPrevWordsPtNodePos); ++i) {
135 newPrevWordsPtNodePos[i] =
136 dicNode->getPrevWordsTerminalPtNodePos()[i - 1];
137 }
138 mDicNodeProperties.init(rootPtNodeArrayPos, newPrevWordsPtNodePos);
139 mDicNodeState.initAsRootWithPreviousWord(
140 &dicNode->mDicNodeState, dicNode->mDicNodeProperties.getDepth());
141 PROF_NODE_COPY(&dicNode->mProfiler, mProfiler);
142 }
143
144 void initAsPassingChild(const DicNode* parentDicNode) {
145 mIsCachedForNextSuggestion = parentDicNode->mIsCachedForNextSuggestion;
146 const int codePoint =
147 parentDicNode->mDicNodeState.mDicNodeStateOutput
148 .getCurrentWordCodePointAt(parentDicNode->getNodeCodePointCount());
149 mDicNodeProperties.init(&parentDicNode->mDicNodeProperties, codePoint);
150 mDicNodeState.initByCopy(&parentDicNode->mDicNodeState);
151 PROF_NODE_COPY(&parentDicNode->mProfiler, mProfiler);
152 }
153
154 void initAsChild(const DicNode* const dicNode,
155 const int ptNodePos,
156 const int childrenPtNodeArrayPos,
157 const int probability,
158 const bool isTerminal,
159 const bool hasChildren,
160 const bool isBlacklistedOrNotAWord,
161 const uint16_t mergedNodeCodePointCount,
162 const int* const mergedNodeCodePoints) {
163 uint16_t newDepth =
164 static_cast<uint16_t>(dicNode->getNodeCodePointCount() + 1);
165 mIsCachedForNextSuggestion = dicNode->mIsCachedForNextSuggestion;
166 const uint16_t newLeavingDepth =
167 static_cast<uint16_t>(dicNode->mDicNodeProperties.getLeavingDepth() +
168 mergedNodeCodePointCount);
169 mDicNodeProperties.init(
170 ptNodePos, childrenPtNodeArrayPos, mergedNodeCodePoints[0], probability,
171 isTerminal, hasChildren, isBlacklistedOrNotAWord, newDepth,
172 newLeavingDepth,
173 dicNode->mDicNodeProperties.getPrevWordsTerminalPtNodePos());
174 mDicNodeState.init(&dicNode->mDicNodeState, mergedNodeCodePointCount,
175 mergedNodeCodePoints);
176 PROF_NODE_COPY(&dicNode->mProfiler, mProfiler);
177 }
178
179 bool isRoot() const { return getNodeCodePointCount() == 0; }
180
181 bool hasChildren() const { return mDicNodeProperties.hasChildren(); }
182
183 bool isLeavingNode() const {
184 ASSERT(getNodeCodePointCount() <= mDicNodeProperties.getLeavingDepth());
185 return getNodeCodePointCount() == mDicNodeProperties.getLeavingDepth();
186 }
187
188 AK_FORCE_INLINE bool isFirstLetter() const {
189 return getNodeCodePointCount() == 1;
190 }
191
192 bool isCached() const { return mIsCachedForNextSuggestion; }
193
194 void setCached() { mIsCachedForNextSuggestion = true; }
195
196 // Check if the current word and the previous word can be considered as a
197 // valid multiple word
198 // suggestion.
199 bool isValidMultipleWordSuggestion() const {
200 if (isBlacklistedOrNotAWord()) {
201 return false;
202 }
203 // Treat suggestion as invalid if the current and the previous word are
204 // single character
205 // words.
206 const int prevWordLen =
207 mDicNodeState.mDicNodeStateOutput.getPrevWordsLength() -
208 mDicNodeState.mDicNodeStateOutput.getPrevWordStart() - 1;
209 const int currentWordLen = getNodeCodePointCount();
210 return (prevWordLen != 1 || currentWordLen != 1);
211 }
212
213 bool isFirstCharUppercase() const {
214 const int c =
215 mDicNodeState.mDicNodeStateOutput.getCurrentWordCodePointAt(0);
216 return CharUtils::isAsciiUpper(c);
217 }
218
219 bool isCompletion(const int inputSize) const {
220 return mDicNodeState.mDicNodeStateInput.getInputIndex(0) >= inputSize;
221 }
222
223 bool canDoLookAheadCorrection(const int inputSize) const {
224 return mDicNodeState.mDicNodeStateInput.getInputIndex(0) < inputSize - 1;
225 }
226
227 // Used to get n-gram probability in DicNodeUtils.
228 int getPtNodePos() const { return mDicNodeProperties.getPtNodePos(); }
229
230 // TODO: Use view class to return PtNodePos array.
231 const int* getPrevWordsTerminalPtNodePos() const {
232 return mDicNodeProperties.getPrevWordsTerminalPtNodePos();
233 }
234
235 // Used in DicNodeUtils
236 int getChildrenPtNodeArrayPos() const {
237 return mDicNodeProperties.getChildrenPtNodeArrayPos();
238 }
239
240 int getProbability() const { return mDicNodeProperties.getProbability(); }
241
242 AK_FORCE_INLINE bool isTerminalDicNode() const {
243 const bool isTerminalPtNode = mDicNodeProperties.isTerminal();
244 const int currentDicNodeDepth = getNodeCodePointCount();
245 const int terminalDicNodeDepth = mDicNodeProperties.getLeavingDepth();
246 return isTerminalPtNode && currentDicNodeDepth > 0 &&
247 currentDicNodeDepth == terminalDicNodeDepth;
248 }
249
250 bool shouldBeFilteredBySafetyNetForBigram() const {
251 const uint16_t currentDepth = getNodeCodePointCount();
252 const int prevWordLen =
253 mDicNodeState.mDicNodeStateOutput.getPrevWordsLength() -
254 mDicNodeState.mDicNodeStateOutput.getPrevWordStart() - 1;
255 return !(currentDepth > 0 && (currentDepth != 1 || prevWordLen != 1));
256 }
257
258 bool hasMatchedOrProximityCodePoints() const {
259 // This DicNode does not have matched or proximity code points when all code
260 // points have
261 // been handled as edit corrections or completion so far.
262 const int editCorrectionCount =
263 mDicNodeState.mDicNodeStateScoring.getEditCorrectionCount();
264 const int completionCount =
265 mDicNodeState.mDicNodeStateScoring.getCompletionCount();
266 return (editCorrectionCount + completionCount) < getNodeCodePointCount();
267 }
268
269 bool isTotalInputSizeExceedingLimit() const {
270 // TODO: 3 can be 2? Needs to be investigated.
271 // TODO: Have a const variable for 3 (or 2)
272 return getTotalNodeCodePointCount() > MAX_WORD_LENGTH - 3;
273 }
274
275 void outputResult(int* dest) const {
276 memmove(dest, getOutputWordBuf(),
277 getTotalNodeCodePointCount() * sizeof(dest[0]));
278 DUMP_WORD_AND_SCORE("OUTPUT");
279 }
280
281 // "Total" in this context (and other methods in this class) means the whole
282 // suggestion. When
283 // this represents a multi-word suggestion, the referenced PtNode (in
284 // mDicNodeState) is only
285 // the one that corresponds to the last word of the suggestion, and all the
286 // previous words
287 // are concatenated together in mDicNodeStateOutput.
288 int getTotalNodeSpaceCount() const {
289 if (!hasMultipleWords()) {
290 return 0;
291 }
292 return CharUtils::getSpaceCount(
293 mDicNodeState.mDicNodeStateOutput.getCodePointBuf(),
294 mDicNodeState.mDicNodeStateOutput.getPrevWordsLength());
295 }
296
297 int getSecondWordFirstInputIndex(
298 const ProximityInfoState* const pInfoState) const {
299 const int inputIndex =
300 mDicNodeState.mDicNodeStateOutput.getSecondWordFirstInputIndex();
301 if (inputIndex == NOT_AN_INDEX) {
302 return NOT_AN_INDEX;
303 } else {
304 return pInfoState->getInputIndexOfSampledPoint(inputIndex);
305 }
306 }
307
308 bool hasMultipleWords() const {
309 return mDicNodeState.mDicNodeStateOutput.getPrevWordCount() > 0;
310 }
311
312 int getProximityCorrectionCount() const {
313 return mDicNodeState.mDicNodeStateScoring.getProximityCorrectionCount();
314 }
315
316 int getEditCorrectionCount() const {
317 return mDicNodeState.mDicNodeStateScoring.getEditCorrectionCount();
318 }
319
320 // Used to prune nodes
321 float getNormalizedCompoundDistance() const {
322 return mDicNodeState.mDicNodeStateScoring.getNormalizedCompoundDistance();
323 }
324
325 // Used to prune nodes
326 float getNormalizedSpatialDistance() const {
327 return mDicNodeState.mDicNodeStateScoring.getSpatialDistance() /
328 static_cast<float>(getInputIndex(0) + 1);
329 }
330
331 // Used to prune nodes
332 float getCompoundDistance() const {
333 return mDicNodeState.mDicNodeStateScoring.getCompoundDistance();
334 }
335
336 // Used to prune nodes
337 float getCompoundDistance(const float languageWeight) const {
338 return mDicNodeState.mDicNodeStateScoring.getCompoundDistance(
339 languageWeight);
340 }
341
342 AK_FORCE_INLINE const int* getOutputWordBuf() const {
343 return mDicNodeState.mDicNodeStateOutput.getCodePointBuf();
344 }
345
346 int getPrevCodePointG(int pointerId) const {
347 return mDicNodeState.mDicNodeStateInput.getPrevCodePoint(pointerId);
348 }
349
350 // Whether the current codepoint can be an intentional omission, in which case
351 // the traversal
352 // algorithm will always check for a possible omission here.
353 bool canBeIntentionalOmission() const {
354 return CharUtils::isIntentionalOmissionCodePoint(getNodeCodePoint());
355 }
356
357 // Whether the omission is so frequent that it should incur zero cost.
358 bool isZeroCostOmission() const {
359 // TODO: do not hardcode and read from header
360 return (getNodeCodePoint() == KEYCODE_SINGLE_QUOTE);
361 }
362
363 // TODO: remove
364 float getTerminalDiffCostG(int path) const {
365 return mDicNodeState.mDicNodeStateInput.getTerminalDiffCost(path);
366 }
367
368 //////////////////////
369 // Temporary getter //
370 // TODO: Remove //
371 //////////////////////
372 // TODO: Remove once touch path is merged into ProximityInfoState
373 // Note: Returned codepoint may be a digraph codepoint if the node is in a
374 // composite glyph.
375 int getNodeCodePoint() const {
376 const int codePoint = mDicNodeProperties.getDicNodeCodePoint();
377 const DigraphUtils::DigraphCodePointIndex digraphIndex =
378 mDicNodeState.mDicNodeStateScoring.getDigraphIndex();
379 if (digraphIndex == DigraphUtils::NOT_A_DIGRAPH_INDEX) {
380 return codePoint;
381 }
382 return DigraphUtils::getDigraphCodePointForIndex(codePoint, digraphIndex);
383 }
384
385 ////////////////////////////////
386 // Utils for cost calculation //
387 ////////////////////////////////
388 AK_FORCE_INLINE bool isSameNodeCodePoint(const DicNode* const dicNode) const {
389 return mDicNodeProperties.getDicNodeCodePoint() ==
390 dicNode->mDicNodeProperties.getDicNodeCodePoint();
391 }
392
393 // TODO: remove
394 // TODO: rename getNextInputIndex
395 int16_t getInputIndex(int pointerId) const {
396 return mDicNodeState.mDicNodeStateInput.getInputIndex(pointerId);
397 }
398
399 ////////////////////////////////////
400 // Getter of features for scoring //
401 ////////////////////////////////////
402 float getSpatialDistanceForScoring() const {
403 return mDicNodeState.mDicNodeStateScoring.getSpatialDistance();
404 }
405
406 // For space-aware gestures, we store the normalized distance at the char
407 // index
408 // that ends the first word of the suggestion. We call this the distance after
409 // first word.
410 float getNormalizedCompoundDistanceAfterFirstWord() const {
411 return mDicNodeState.mDicNodeStateScoring
412 .getNormalizedCompoundDistanceAfterFirstWord();
413 }
414
415 float getRawLength() const {
416 return mDicNodeState.mDicNodeStateScoring.getRawLength();
417 }
418
419 DoubleLetterLevel getDoubleLetterLevel() const {
420 return mDicNodeState.mDicNodeStateScoring.getDoubleLetterLevel();
421 }
422
423 void setDoubleLetterLevel(DoubleLetterLevel doubleLetterLevel) {
424 mDicNodeState.mDicNodeStateScoring.setDoubleLetterLevel(doubleLetterLevel);
425 }
426
427 bool isInDigraph() const {
428 return mDicNodeState.mDicNodeStateScoring.getDigraphIndex() !=
429 DigraphUtils::NOT_A_DIGRAPH_INDEX;
430 }
431
432 void advanceDigraphIndex() {
433 mDicNodeState.mDicNodeStateScoring.advanceDigraphIndex();
434 }
435
436 ErrorTypeUtils::ErrorType getContainedErrorTypes() const {
437 return mDicNodeState.mDicNodeStateScoring.getContainedErrorTypes();
438 }
439
440 bool isBlacklistedOrNotAWord() const {
441 return mDicNodeProperties.isBlacklistedOrNotAWord();
442 }
443
444 inline uint16_t getNodeCodePointCount() const {
445 return mDicNodeProperties.getDepth();
446 }
447
448 // Returns code point count including spaces
449 inline uint16_t getTotalNodeCodePointCount() const {
450 return getNodeCodePointCount() +
451 mDicNodeState.mDicNodeStateOutput.getPrevWordsLength();
452 }
453
454 AK_FORCE_INLINE void dump(const char* tag) const {
455 #if DEBUG_DICT
456 DUMP_WORD_AND_SCORE(tag);
457 #if DEBUG_DUMP_ERROR
458 mProfiler.dump();
459 #endif
460 #endif
461 }
462
463 AK_FORCE_INLINE bool compare(const DicNode* right) const {
464 // Promote exact matches to prevent them from being pruned.
465 const bool leftExactMatch =
466 ErrorTypeUtils::isExactMatch(getContainedErrorTypes());
467 const bool rightExactMatch =
468 ErrorTypeUtils::isExactMatch(right->getContainedErrorTypes());
469 if (leftExactMatch != rightExactMatch) {
470 return leftExactMatch;
471 }
472 const float diff = right->getNormalizedCompoundDistance() -
473 getNormalizedCompoundDistance();
474 static const float MIN_DIFF = 0.000001f;
475 if (diff > MIN_DIFF) {
476 return true;
477 } else if (diff < -MIN_DIFF) {
478 return false;
479 }
480 const int depth = getNodeCodePointCount();
481 const int depthDiff = right->getNodeCodePointCount() - depth;
482 if (depthDiff != 0) {
483 return depthDiff > 0;
484 }
485 for (int i = 0; i < depth; ++i) {
486 const int codePoint =
487 mDicNodeState.mDicNodeStateOutput.getCurrentWordCodePointAt(i);
488 const int rightCodePoint =
489 right->mDicNodeState.mDicNodeStateOutput.getCurrentWordCodePointAt(i);
490 if (codePoint != rightCodePoint) {
491 return rightCodePoint > codePoint;
492 }
493 }
494 // Compare pointer values here for stable comparison
495 return this > right;
496 }
497
498 private:
499 DicNodeProperties mDicNodeProperties;
500 DicNodeState mDicNodeState;
501 // TODO: Remove
502 bool mIsCachedForNextSuggestion;
503
504 AK_FORCE_INLINE int getTotalInputIndex() const {
505 int index = 0;
506 for (int i = 0; i < MAX_POINTER_COUNT_G; i++) {
507 index += mDicNodeState.mDicNodeStateInput.getInputIndex(i);
508 }
509 return index;
510 }
511
512 // Caveat: Must not be called outside Weighting
513 // This restriction is guaranteed by "friend"
514 AK_FORCE_INLINE void addCost(const float spatialCost,
515 const float languageCost,
516 const bool doNormalization,
517 const int inputSize,
518 const ErrorTypeUtils::ErrorType errorType) {
519 if (DEBUG_GEO_FULL) {
520 LOGI_SHOW_ADD_COST_PROP;
521 }
522 mDicNodeState.mDicNodeStateScoring.addCost(spatialCost, languageCost,
523 doNormalization, inputSize,
524 getTotalInputIndex(), errorType);
525 }
526
527 // Saves the current normalized compound distance for space-aware gestures.
528 // See getNormalizedCompoundDistanceAfterFirstWord for details.
529 AK_FORCE_INLINE void saveNormalizedCompoundDistanceAfterFirstWordIfNoneYet() {
530 mDicNodeState.mDicNodeStateScoring
531 .saveNormalizedCompoundDistanceAfterFirstWordIfNoneYet();
532 }
533
534 // Caveat: Must not be called outside Weighting
535 // This restriction is guaranteed by "friend"
536 AK_FORCE_INLINE void forwardInputIndex(
537 const int pointerId,
538 const int count,
539 const bool overwritesPrevCodePointByNodeCodePoint) {
540 if (count == 0) {
541 return;
542 }
543 mDicNodeState.mDicNodeStateInput.forwardInputIndex(pointerId, count);
544 if (overwritesPrevCodePointByNodeCodePoint) {
545 mDicNodeState.mDicNodeStateInput.setPrevCodePoint(0, getNodeCodePoint());
546 }
547 }
548
549 AK_FORCE_INLINE void updateInputIndexG(
550 const DicNode_InputStateG* const inputStateG) {
551 if (mDicNodeState.mDicNodeStateOutput.getPrevWordCount() == 1 &&
552 isFirstLetter()) {
553 mDicNodeState.mDicNodeStateOutput.setSecondWordFirstInputIndex(
554 inputStateG->mInputIndex);
555 }
556 mDicNodeState.mDicNodeStateInput.updateInputIndexG(
557 inputStateG->mPointerId, inputStateG->mInputIndex,
558 inputStateG->mPrevCodePoint, inputStateG->mTerminalDiffCost,
559 inputStateG->mRawLength);
560 mDicNodeState.mDicNodeStateScoring.addRawLength(inputStateG->mRawLength);
561 mDicNodeState.mDicNodeStateScoring.setDoubleLetterLevel(
562 inputStateG->mDoubleLetterLevel);
563 }
564 };
565 } // namespace latinime
566 #endif // LATINIME_DIC_NODE_H
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698