blob: 49cfdecac8b21d67dda66ec87d3f454fb56d829c [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef LATINIME_DIC_NODE_H
#define LATINIME_DIC_NODE_H
#include "defines.h"
#include "suggest/core/dicnode/dic_node_profiler.h"
#include "suggest/core/dicnode/dic_node_release_listener.h"
#include "suggest/core/dicnode/internal/dic_node_state.h"
#include "suggest/core/dicnode/internal/dic_node_properties.h"
#include "suggest/core/dictionary/digraph_utils.h"
#include "utils/char_utils.h"
#if DEBUG_DICT
#define LOGI_SHOW_ADD_COST_PROP \
do { char charBuf[50]; \
INTS_TO_CHARS(getOutputWordBuf(), getNodeCodePointCount(), charBuf, NELEMS(charBuf)); \
AKLOGI("%20s, \"%c\", size = %03d, total = %03d, index(0) = %02d, dist = %.4f, %s,,", \
__FUNCTION__, getNodeCodePoint(), inputSize, getTotalInputIndex(), \
getInputIndex(0), getNormalizedCompoundDistance(), charBuf); } while (0)
#define DUMP_WORD_AND_SCORE(header) \
do { char charBuf[50]; char prevWordCharBuf[50]; \
INTS_TO_CHARS(getOutputWordBuf(), getNodeCodePointCount(), charBuf, NELEMS(charBuf)); \
INTS_TO_CHARS(mDicNodeState.mDicNodeStatePrevWord.mPrevWord, \
mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength(), prevWordCharBuf, \
NELEMS(prevWordCharBuf)); \
AKLOGI("#%8s, %5f, %5f, %5f, %5f, %s, %s, %d, %5f,", header, \
getSpatialDistanceForScoring(), getLanguageDistanceForScoring(), \
getNormalizedCompoundDistance(), getRawLength(), prevWordCharBuf, charBuf, \
getInputIndex(0), getNormalizedCompoundDistanceAfterFirstWord()); \
} while (0)
#else
#define LOGI_SHOW_ADD_COST_PROP
#define DUMP_WORD_AND_SCORE(header)
#endif
namespace latinime {
// This struct is purely a bucket to return values. No instances of this struct should be kept.
struct DicNode_InputStateG {
DicNode_InputStateG()
: mNeedsToUpdateInputStateG(false), mPointerId(0), mInputIndex(0),
mPrevCodePoint(0), mTerminalDiffCost(0.0f), mRawLength(0.0f),
mDoubleLetterLevel(NOT_A_DOUBLE_LETTER) {}
bool mNeedsToUpdateInputStateG;
int mPointerId;
int16_t mInputIndex;
int mPrevCodePoint;
float mTerminalDiffCost;
float mRawLength;
DoubleLetterLevel mDoubleLetterLevel;
};
class DicNode {
// Caveat: We define Weighting as a friend class of DicNode to let Weighting change
// the distance of DicNode.
// Caution!!! In general, we avoid using the "friend" access modifier.
// This is an exception to explicitly hide DicNode::addCost() from all classes but Weighting.
friend class Weighting;
public:
#if DEBUG_DICT
DicNodeProfiler mProfiler;
#endif
//////////////////
// Memory utils //
//////////////////
AK_FORCE_INLINE static void managedDelete(DicNode *node) {
node->remove();
}
// end
/////////////////
AK_FORCE_INLINE DicNode()
:
#if DEBUG_DICT
mProfiler(),
#endif
mDicNodeProperties(), mDicNodeState(), mIsCachedForNextSuggestion(false),
mIsUsed(false), mReleaseListener(0) {}
DicNode(const DicNode &dicNode);
DicNode &operator=(const DicNode &dicNode);
virtual ~DicNode() {}
// Init for copy
void initByCopy(const DicNode *dicNode) {
mIsUsed = true;
mIsCachedForNextSuggestion = dicNode->mIsCachedForNextSuggestion;
mDicNodeProperties.init(&dicNode->mDicNodeProperties);
mDicNodeState.init(&dicNode->mDicNodeState);
PROF_NODE_COPY(&dicNode->mProfiler, mProfiler);
}
// Init for root with prevWordNodePos which is used for bigram
void initAsRoot(const int rootGroupPos, const int prevWordNodePos) {
mIsUsed = true;
mIsCachedForNextSuggestion = false;
mDicNodeProperties.init(
NOT_A_DICT_POS /* pos */, rootGroupPos, NOT_A_CODE_POINT /* nodeCodePoint */,
NOT_A_PROBABILITY /* probability */, false /* isTerminal */,
true /* hasChildren */, false /* isBlacklistedOrNotAWord */, 0 /* depth */,
0 /* terminalDepth */);
mDicNodeState.init(prevWordNodePos);
PROF_NODE_RESET(mProfiler);
}
// Init for root with previous word
void initAsRootWithPreviousWord(DicNode *dicNode, const int rootGroupPos) {
mIsUsed = true;
mIsCachedForNextSuggestion = dicNode->mIsCachedForNextSuggestion;
mDicNodeProperties.init(
NOT_A_DICT_POS /* pos */, rootGroupPos, NOT_A_CODE_POINT /* nodeCodePoint */,
NOT_A_PROBABILITY /* probability */, false /* isTerminal */,
true /* hasChildren */, false /* isBlacklistedOrNotAWord */, 0 /* depth */,
0 /* terminalDepth */);
// TODO: Move to dicNodeState?
mDicNodeState.mDicNodeStateOutput.init(); // reset for next word
mDicNodeState.mDicNodeStateInput.init(
&dicNode->mDicNodeState.mDicNodeStateInput, true /* resetTerminalDiffCost */);
mDicNodeState.mDicNodeStateScoring.init(
&dicNode->mDicNodeState.mDicNodeStateScoring);
mDicNodeState.mDicNodeStatePrevWord.init(
dicNode->mDicNodeState.mDicNodeStatePrevWord.getPrevWordCount() + 1,
dicNode->mDicNodeProperties.getProbability(),
dicNode->mDicNodeProperties.getPos(),
dicNode->mDicNodeState.mDicNodeStatePrevWord.mPrevWord,
dicNode->mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength(),
dicNode->getOutputWordBuf(),
dicNode->mDicNodeProperties.getDepth(),
dicNode->mDicNodeState.mDicNodeStatePrevWord.getSecondWordFirstInputIndex(),
mDicNodeState.mDicNodeStateInput.getInputIndex(0) /* lastInputIndex */);
PROF_NODE_COPY(&dicNode->mProfiler, mProfiler);
}
void initAsPassingChild(DicNode *parentNode) {
mIsUsed = true;
mIsCachedForNextSuggestion = parentNode->mIsCachedForNextSuggestion;
const int c = parentNode->getNodeTypedCodePoint();
mDicNodeProperties.init(&parentNode->mDicNodeProperties, c);
mDicNodeState.init(&parentNode->mDicNodeState);
PROF_NODE_COPY(&parentNode->mProfiler, mProfiler);
}
void initAsChild(const DicNode *const dicNode, const int pos, const int childrenPos,
const int probability, const bool isTerminal, const bool hasChildren,
const bool isBlacklistedOrNotAWord, const uint16_t mergedNodeCodePointCount,
const int *const mergedNodeCodePoints) {
mIsUsed = true;
uint16_t newDepth = static_cast<uint16_t>(dicNode->getNodeCodePointCount() + 1);
mIsCachedForNextSuggestion = dicNode->mIsCachedForNextSuggestion;
const uint16_t newLeavingDepth = static_cast<uint16_t>(
dicNode->mDicNodeProperties.getLeavingDepth() + mergedNodeCodePointCount);
mDicNodeProperties.init(pos, childrenPos, mergedNodeCodePoints[0], probability,
isTerminal, hasChildren, isBlacklistedOrNotAWord, newDepth, newLeavingDepth);
mDicNodeState.init(&dicNode->mDicNodeState, mergedNodeCodePointCount,
mergedNodeCodePoints);
PROF_NODE_COPY(&dicNode->mProfiler, mProfiler);
}
AK_FORCE_INLINE void remove() {
mIsUsed = false;
if (mReleaseListener) {
mReleaseListener->onReleased(this);
}
}
bool isUsed() const {
return mIsUsed;
}
bool isRoot() const {
return getNodeCodePointCount() == 0;
}
bool hasChildren() const {
return mDicNodeProperties.hasChildren();
}
bool isLeavingNode() const {
ASSERT(getNodeCodePointCount() <= mDicNodeProperties.getLeavingDepth());
return getNodeCodePointCount() == mDicNodeProperties.getLeavingDepth();
}
AK_FORCE_INLINE bool isFirstLetter() const {
return getNodeCodePointCount() == 1;
}
bool isCached() const {
return mIsCachedForNextSuggestion;
}
void setCached() {
mIsCachedForNextSuggestion = true;
}
// Used to expand the node in DicNodeUtils
int getNodeTypedCodePoint() const {
return mDicNodeState.mDicNodeStateOutput.getCodePointAt(getNodeCodePointCount());
}
// Check if the current word and the previous word can be considered as a valid multiple word
// suggestion.
bool isValidMultipleWordSuggestion() const {
if (isBlacklistedOrNotAWord()) {
return false;
}
// Treat suggestion as invalid if the current and the previous word are single character
// words.
const int prevWordLen = mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength()
- mDicNodeState.mDicNodeStatePrevWord.getPrevWordStart() - 1;
const int currentWordLen = getNodeCodePointCount();
return (prevWordLen != 1 || currentWordLen != 1);
}
bool isFirstCharUppercase() const {
const int c = getOutputWordBuf()[0];
return CharUtils::isAsciiUpper(c);
}
bool isFirstWord() const {
return mDicNodeState.mDicNodeStatePrevWord.getPrevWordNodePos() == NOT_A_DICT_POS;
}
bool isCompletion(const int inputSize) const {
return mDicNodeState.mDicNodeStateInput.getInputIndex(0) >= inputSize;
}
bool canDoLookAheadCorrection(const int inputSize) const {
return mDicNodeState.mDicNodeStateInput.getInputIndex(0) < inputSize - 1;
}
// Used to get bigram probability in DicNodeUtils
int getPos() const {
return mDicNodeProperties.getPos();
}
// Used to get bigram probability in DicNodeUtils
int getPrevWordPos() const {
return mDicNodeState.mDicNodeStatePrevWord.getPrevWordNodePos();
}
// Used in DicNodeUtils
int getChildrenPos() const {
return mDicNodeProperties.getChildrenPos();
}
int getProbability() const {
return mDicNodeProperties.getProbability();
}
AK_FORCE_INLINE bool isTerminalWordNode() const {
const bool isTerminalNodes = mDicNodeProperties.isTerminal();
const int currentNodeDepth = getNodeCodePointCount();
const int terminalNodeDepth = mDicNodeProperties.getLeavingDepth();
return isTerminalNodes && currentNodeDepth > 0 && currentNodeDepth == terminalNodeDepth;
}
bool shouldBeFilteredBySafetyNetForBigram() const {
const uint16_t currentDepth = getNodeCodePointCount();
const int prevWordLen = mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength()
- mDicNodeState.mDicNodeStatePrevWord.getPrevWordStart() - 1;
return !(currentDepth > 0 && (currentDepth != 1 || prevWordLen != 1));
}
bool isTotalInputSizeExceedingLimit() const {
const int prevWordsLen = mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength();
const int currentWordDepth = getNodeCodePointCount();
// TODO: 3 can be 2? Needs to be investigated.
// TODO: Have a const variable for 3 (or 2)
return prevWordsLen + currentWordDepth > MAX_WORD_LENGTH - 3;
}
// TODO: This may be defective. Needs to be revised.
bool truncateNode(const DicNode *const topNode, const int inputCommitPoint) {
const int prevWordLenOfTop = mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength();
int newPrevWordStartIndex = inputCommitPoint;
int charCount = 0;
// Find new word start index
for (int i = 0; i < prevWordLenOfTop; ++i) {
const int c = mDicNodeState.mDicNodeStatePrevWord.getPrevWordCodePointAt(i);
// TODO: Check other separators.
if (c != KEYCODE_SPACE && c != KEYCODE_SINGLE_QUOTE) {
if (charCount == inputCommitPoint) {
newPrevWordStartIndex = i;
break;
}
++charCount;
}
}
if (!mDicNodeState.mDicNodeStatePrevWord.startsWith(
&topNode->mDicNodeState.mDicNodeStatePrevWord, newPrevWordStartIndex - 1)) {
// Node mismatch.
return false;
}
mDicNodeState.mDicNodeStateInput.truncate(inputCommitPoint);
mDicNodeState.mDicNodeStatePrevWord.truncate(newPrevWordStartIndex);
return true;
}
void outputResult(int *dest) const {
const uint16_t prevWordLength = mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength();
const uint16_t currentDepth = getNodeCodePointCount();
DicNodeUtils::appendTwoWords(mDicNodeState.mDicNodeStatePrevWord.mPrevWord,
prevWordLength, getOutputWordBuf(), currentDepth, dest);
DUMP_WORD_AND_SCORE("OUTPUT");
}
// "Total" in this context (and other methods in this class) means the whole suggestion. When
// this represents a multi-word suggestion, the referenced PtNode (in mDicNodeState) is only
// the one that corresponds to the last word of the suggestion, and all the previous words
// are concatenated together in mPrevWord - which contains a space at the end.
int getTotalNodeSpaceCount() const {
if (isFirstWord()) return 0;
return CharUtils::getSpaceCount(mDicNodeState.mDicNodeStatePrevWord.mPrevWord,
mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength());
}
int getSecondWordFirstInputIndex(const ProximityInfoState *const pInfoState) const {
const int inputIndex = mDicNodeState.mDicNodeStatePrevWord.getSecondWordFirstInputIndex();
if (inputIndex == NOT_AN_INDEX) {
return NOT_AN_INDEX;
} else {
return pInfoState->getInputIndexOfSampledPoint(inputIndex);
}
}
bool hasMultipleWords() const {
return mDicNodeState.mDicNodeStatePrevWord.getPrevWordCount() > 0;
}
int getProximityCorrectionCount() const {
return mDicNodeState.mDicNodeStateScoring.getProximityCorrectionCount();
}
int getEditCorrectionCount() const {
return mDicNodeState.mDicNodeStateScoring.getEditCorrectionCount();
}
// Used to prune nodes
float getNormalizedCompoundDistance() const {
return mDicNodeState.mDicNodeStateScoring.getNormalizedCompoundDistance();
}
// Used to prune nodes
float getNormalizedSpatialDistance() const {
return mDicNodeState.mDicNodeStateScoring.getSpatialDistance()
/ static_cast<float>(getInputIndex(0) + 1);
}
// Used to prune nodes
float getCompoundDistance() const {
return mDicNodeState.mDicNodeStateScoring.getCompoundDistance();
}
// Used to prune nodes
float getCompoundDistance(const float languageWeight) const {
return mDicNodeState.mDicNodeStateScoring.getCompoundDistance(languageWeight);
}
// Used to commit input partially
int getPrevWordNodePos() const {
return mDicNodeState.mDicNodeStatePrevWord.getPrevWordNodePos();
}
AK_FORCE_INLINE const int *getOutputWordBuf() const {
return mDicNodeState.mDicNodeStateOutput.mCodePointsBuf;
}
int getPrevCodePointG(int pointerId) const {
return mDicNodeState.mDicNodeStateInput.getPrevCodePoint(pointerId);
}
// Whether the current codepoint can be an intentional omission, in which case the traversal
// algorithm will always check for a possible omission here.
bool canBeIntentionalOmission() const {
return CharUtils::isIntentionalOmissionCodePoint(getNodeCodePoint());
}
// Whether the omission is so frequent that it should incur zero cost.
bool isZeroCostOmission() const {
// TODO: do not hardcode and read from header
return (getNodeCodePoint() == KEYCODE_SINGLE_QUOTE);
}
// TODO: remove
float getTerminalDiffCostG(int path) const {
return mDicNodeState.mDicNodeStateInput.getTerminalDiffCost(path);
}
//////////////////////
// Temporary getter //
// TODO: Remove //
//////////////////////
// TODO: Remove once touch path is merged into ProximityInfoState
// Note: Returned codepoint may be a digraph codepoint if the node is in a composite glyph.
int getNodeCodePoint() const {
const int codePoint = mDicNodeProperties.getNodeCodePoint();
const DigraphUtils::DigraphCodePointIndex digraphIndex =
mDicNodeState.mDicNodeStateScoring.getDigraphIndex();
if (digraphIndex == DigraphUtils::NOT_A_DIGRAPH_INDEX) {
return codePoint;
}
return DigraphUtils::getDigraphCodePointForIndex(codePoint, digraphIndex);
}
////////////////////////////////
// Utils for cost calculation //
////////////////////////////////
AK_FORCE_INLINE bool isSameNodeCodePoint(const DicNode *const dicNode) const {
return mDicNodeProperties.getNodeCodePoint()
== dicNode->mDicNodeProperties.getNodeCodePoint();
}
// TODO: remove
// TODO: rename getNextInputIndex
int16_t getInputIndex(int pointerId) const {
return mDicNodeState.mDicNodeStateInput.getInputIndex(pointerId);
}
////////////////////////////////////
// Getter of features for scoring //
////////////////////////////////////
float getSpatialDistanceForScoring() const {
return mDicNodeState.mDicNodeStateScoring.getSpatialDistance();
}
float getLanguageDistanceForScoring() const {
return mDicNodeState.mDicNodeStateScoring.getLanguageDistance();
}
// For space-aware gestures, we store the normalized distance at the char index
// that ends the first word of the suggestion. We call this the distance after
// first word.
float getNormalizedCompoundDistanceAfterFirstWord() const {
return mDicNodeState.mDicNodeStateScoring.getNormalizedCompoundDistanceAfterFirstWord();
}
float getLanguageDistanceRatePerWordForScoring() const {
const float langDist = getLanguageDistanceForScoring();
const float totalWordCount =
static_cast<float>(mDicNodeState.mDicNodeStatePrevWord.getPrevWordCount() + 1);
return langDist / totalWordCount;
}
float getRawLength() const {
return mDicNodeState.mDicNodeStateScoring.getRawLength();
}
bool isLessThanOneErrorForScoring() const {
return mDicNodeState.mDicNodeStateScoring.getEditCorrectionCount()
+ mDicNodeState.mDicNodeStateScoring.getProximityCorrectionCount() <= 1;
}
DoubleLetterLevel getDoubleLetterLevel() const {
return mDicNodeState.mDicNodeStateScoring.getDoubleLetterLevel();
}
void setDoubleLetterLevel(DoubleLetterLevel doubleLetterLevel) {
mDicNodeState.mDicNodeStateScoring.setDoubleLetterLevel(doubleLetterLevel);
}
bool isInDigraph() const {
return mDicNodeState.mDicNodeStateScoring.getDigraphIndex()
!= DigraphUtils::NOT_A_DIGRAPH_INDEX;
}
void advanceDigraphIndex() {
mDicNodeState.mDicNodeStateScoring.advanceDigraphIndex();
}
bool isExactMatch() const {
return mDicNodeState.mDicNodeStateScoring.isExactMatch();
}
bool isBlacklistedOrNotAWord() const {
return mDicNodeProperties.isBlacklistedOrNotAWord();
}
inline uint16_t getNodeCodePointCount() const {
return mDicNodeProperties.getDepth();
}
// Returns code point count including spaces
inline uint16_t getTotalNodeCodePointCount() const {
return getNodeCodePointCount() + mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength();
}
AK_FORCE_INLINE void dump(const char *tag) const {
#if DEBUG_DICT
DUMP_WORD_AND_SCORE(tag);
#if DEBUG_DUMP_ERROR
mProfiler.dump();
#endif
#endif
}
void setReleaseListener(DicNodeReleaseListener *releaseListener) {
mReleaseListener = releaseListener;
}
AK_FORCE_INLINE bool compare(const DicNode *right) {
if (!isUsed() && !right->isUsed()) {
// Compare pointer values here for stable comparison
return this > right;
}
if (!isUsed()) {
return true;
}
if (!right->isUsed()) {
return false;
}
// Promote exact matches to prevent them from being pruned.
const bool leftExactMatch = isExactMatch();
const bool rightExactMatch = right->isExactMatch();
if (leftExactMatch != rightExactMatch) {
return leftExactMatch;
}
const float diff =
right->getNormalizedCompoundDistance() - getNormalizedCompoundDistance();
static const float MIN_DIFF = 0.000001f;
if (diff > MIN_DIFF) {
return true;
} else if (diff < -MIN_DIFF) {
return false;
}
const int depth = getNodeCodePointCount();
const int depthDiff = right->getNodeCodePointCount() - depth;
if (depthDiff != 0) {
return depthDiff > 0;
}
for (int i = 0; i < depth; ++i) {
const int codePoint = mDicNodeState.mDicNodeStateOutput.getCodePointAt(i);
const int rightCodePoint = right->mDicNodeState.mDicNodeStateOutput.getCodePointAt(i);
if (codePoint != rightCodePoint) {
return rightCodePoint > codePoint;
}
}
// Compare pointer values here for stable comparison
return this > right;
}
private:
DicNodeProperties mDicNodeProperties;
DicNodeState mDicNodeState;
// TODO: Remove
bool mIsCachedForNextSuggestion;
bool mIsUsed;
DicNodeReleaseListener *mReleaseListener;
AK_FORCE_INLINE int getTotalInputIndex() const {
int index = 0;
for (int i = 0; i < MAX_POINTER_COUNT_G; i++) {
index += mDicNodeState.mDicNodeStateInput.getInputIndex(i);
}
return index;
}
// Caveat: Must not be called outside Weighting
// This restriction is guaranteed by "friend"
AK_FORCE_INLINE void addCost(const float spatialCost, const float languageCost,
const bool doNormalization, const int inputSize, const ErrorType errorType) {
if (DEBUG_GEO_FULL) {
LOGI_SHOW_ADD_COST_PROP;
}
mDicNodeState.mDicNodeStateScoring.addCost(spatialCost, languageCost, doNormalization,
inputSize, getTotalInputIndex(), errorType);
}
// Saves the current normalized compound distance for space-aware gestures.
// See getNormalizedCompoundDistanceAfterFirstWord for details.
AK_FORCE_INLINE void saveNormalizedCompoundDistanceAfterFirstWordIfNoneYet() {
mDicNodeState.mDicNodeStateScoring.saveNormalizedCompoundDistanceAfterFirstWordIfNoneYet();
}
// Caveat: Must not be called outside Weighting
// This restriction is guaranteed by "friend"
AK_FORCE_INLINE void forwardInputIndex(const int pointerId, const int count,
const bool overwritesPrevCodePointByNodeCodePoint) {
if (count == 0) {
return;
}
mDicNodeState.mDicNodeStateInput.forwardInputIndex(pointerId, count);
if (overwritesPrevCodePointByNodeCodePoint) {
mDicNodeState.mDicNodeStateInput.setPrevCodePoint(0, getNodeCodePoint());
}
}
AK_FORCE_INLINE void updateInputIndexG(const DicNode_InputStateG *const inputStateG) {
if (mDicNodeState.mDicNodeStatePrevWord.getPrevWordCount() == 1 && isFirstLetter()) {
mDicNodeState.mDicNodeStatePrevWord.setSecondWordFirstInputIndex(
inputStateG->mInputIndex);
}
mDicNodeState.mDicNodeStateInput.updateInputIndexG(inputStateG->mPointerId,
inputStateG->mInputIndex, inputStateG->mPrevCodePoint,
inputStateG->mTerminalDiffCost, inputStateG->mRawLength);
mDicNodeState.mDicNodeStateScoring.addRawLength(inputStateG->mRawLength);
mDicNodeState.mDicNodeStateScoring.setDoubleLetterLevel(inputStateG->mDoubleLetterLevel);
}
};
} // namespace latinime
#endif // LATINIME_DIC_NODE_H