File indexing completed on 2024-04-28 04:00:54
0001 /* 0002 * voikkodict.cpp 0003 * 0004 * SPDX-FileCopyrightText: 2015 Jesse Jaara <jesse.jaara@gmail.com> 0005 * 0006 * SPDX-License-Identifier: LGPL-2.1-or-later 0007 */ 0008 0009 #include "voikkodict.h" 0010 #include "voikkodebug.h" 0011 0012 #include <QDir> 0013 #include <QList> 0014 #include <QStandardPaths> 0015 #ifdef Q_IS_WIN 0016 #include <QSysInfo> 0017 #endif 0018 0019 #include <QJsonArray> 0020 #include <QJsonDocument> 0021 #include <QJsonObject> 0022 0023 namespace 0024 { 0025 // QString literals used in loading and storing user dictionary 0026 inline const QString replacement_bad_str() Q_DECL_NOEXCEPT 0027 { 0028 return QStringLiteral("bad"); 0029 } 0030 0031 inline const QString replacement_good_str() Q_DECL_NOEXCEPT 0032 { 0033 return QStringLiteral("good"); 0034 } 0035 0036 inline const QString personal_words_str() Q_DECL_NOEXCEPT 0037 { 0038 return QStringLiteral("PersonalWords"); 0039 } 0040 0041 inline const QString replacements_str() Q_DECL_NOEXCEPT 0042 { 0043 return QStringLiteral("Replacements"); 0044 } 0045 0046 // Set path to: QStandardPaths::GenericDataLocation/Sonnet/Voikko-user-dictionary.json 0047 QString getUserDictionaryPath() Q_DECL_NOEXCEPT 0048 { 0049 QString directory = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); 0050 0051 #ifdef Q_OS_WIN 0052 // Resolve the windows' Roaming directory manually 0053 if (QSysInfo::windowsVersion() == QSysInfo::WV_XP || QSysInfo::windowsVersion() == QSysInfo::WV_2003) { 0054 // In Xp Roaming is "<user>/Application Data" 0055 // DataLocation: "<user>/Local Settings/Application Data" 0056 directory += QStringLiteral("/../../Application Data"); 0057 } else { 0058 directory += QStringLiteral("/../Roaming"); 0059 } 0060 #endif 0061 0062 directory += QStringLiteral("/Sonnet"); 0063 QDir path(directory); 0064 path.mkpath(path.absolutePath()); 0065 0066 return path.absoluteFilePath(QStringLiteral("Voikko-user-dictionary.json")); 0067 } 0068 0069 void addReplacementToNode(QJsonObject &languageNode, const QString &bad, const QString &good) Q_DECL_NOEXCEPT 0070 { 0071 QJsonObject pair; 0072 pair[replacement_bad_str()] = good; 0073 pair[replacement_good_str()] = bad; 0074 0075 auto replaceList = languageNode[replacements_str()].toArray(); 0076 replaceList.append(pair); 0077 languageNode[replacements_str()] = replaceList; 0078 } 0079 0080 void addPersonalWordToNode(QJsonObject &languageNode, const QString &word) Q_DECL_NOEXCEPT 0081 { 0082 auto arr = languageNode[personal_words_str()].toArray(); 0083 arr.append(word); 0084 languageNode[personal_words_str()] = arr; 0085 } 0086 0087 /** 0088 * Read and return the root json object from fileName. 0089 * 0090 * Returns an empty node in case of an IO error or the file is empty. 0091 */ 0092 QJsonObject readJsonRootObject(const QString &fileName) Q_DECL_NOEXCEPT 0093 { 0094 QFile userDictFile(fileName); 0095 0096 if (!userDictFile.exists()) { 0097 return QJsonObject(); // Nothing has been saved so far. 0098 } 0099 0100 if (!userDictFile.open(QIODevice::ReadOnly)) { 0101 qCWarning(SONNET_VOIKKO) << "Could not open personal dictionary. Failed to open file" << fileName; 0102 qCWarning(SONNET_VOIKKO) << "Reason:" << userDictFile.errorString(); 0103 return QJsonObject(); 0104 } 0105 0106 QJsonDocument dictDoc = QJsonDocument::fromJson(userDictFile.readAll()); 0107 userDictFile.close(); 0108 0109 return dictDoc.object(); 0110 } 0111 } 0112 0113 class VoikkoDictPrivate 0114 { 0115 public: 0116 VoikkoHandle *m_handle; 0117 const VoikkoDict *q; 0118 0119 QSet<QString> m_sessionWords; 0120 QSet<QString> m_personalWords; 0121 QHash<QString, QString> m_replacements; 0122 0123 QString m_userDictionaryFilepath; 0124 0125 // Used when converting Qstring to wchar_t strings 0126 QList<wchar_t> m_conversionBuffer; 0127 0128 VoikkoDictPrivate(const QString &language, const VoikkoDict *publicPart) Q_DECL_NOEXCEPT : q(publicPart), 0129 m_userDictionaryFilepath(getUserDictionaryPath()), 0130 m_conversionBuffer(256) 0131 { 0132 const char *error; 0133 m_handle = voikkoInit(&error, language.toUtf8().data(), nullptr); 0134 0135 if (error != nullptr) { 0136 qCWarning(SONNET_VOIKKO) << "Failed to initialize Voikko spelling backend. Reason:" << error; 0137 } else { // Continue to load user's own words 0138 loadUserDictionary(); 0139 } 0140 } 0141 0142 /** 0143 * Store a new ignored/personal word or replacement pair in the user's 0144 * dictionary m_userDictionaryFilepath. 0145 * 0146 * returns true on success else false 0147 */ 0148 bool storePersonal(const QString &personalWord, const QString &bad = QString(), const QString &good = QString()) const Q_DECL_NOEXCEPT 0149 { 0150 QFile userDictFile(m_userDictionaryFilepath); 0151 0152 if (!userDictFile.open(QIODevice::ReadWrite)) { 0153 qCWarning(SONNET_VOIKKO) << "Could not save personal dictionary. Failed to open file:" << m_userDictionaryFilepath; 0154 qCWarning(SONNET_VOIKKO) << "Reason:" << userDictFile.errorString(); 0155 return false; 0156 } 0157 0158 QJsonDocument dictDoc = QJsonDocument::fromJson(userDictFile.readAll()); 0159 auto root = readJsonRootObject(m_userDictionaryFilepath); 0160 auto languageNode = root[q->language()].toObject(); 0161 0162 // Empty value means we are storing a bad:good pair 0163 if (personalWord.isEmpty()) { 0164 addReplacementToNode(languageNode, bad, good); 0165 } else { 0166 addPersonalWordToNode(languageNode, personalWord); 0167 } 0168 0169 root[q->language()] = languageNode; 0170 dictDoc.setObject(root); 0171 0172 userDictFile.reset(); 0173 userDictFile.write(dictDoc.toJson()); 0174 userDictFile.close(); 0175 qCDebug(SONNET_VOIKKO) << "Changes to user dictionary saved to file: " << m_userDictionaryFilepath; 0176 0177 return true; 0178 } 0179 0180 /** 0181 * Load user's own personal words and replacement pairs from 0182 * m_userDictionaryFilepath. 0183 */ 0184 void loadUserDictionary() Q_DECL_NOEXCEPT 0185 { 0186 // If root is empty we will fail later on when checking if 0187 // languageNode is empty. 0188 auto root = readJsonRootObject(m_userDictionaryFilepath); 0189 auto languageNode = root[q->language()].toObject(); 0190 0191 if (languageNode.isEmpty()) { 0192 return; // Nothing to load 0193 } 0194 0195 loadUserWords(languageNode); 0196 loadUserReplacements(languageNode); 0197 } 0198 0199 /** 0200 * Convert the given QString to a \0 terminated wchar_t string. 0201 * Uses QList as a buffer and return it's internal data pointer. 0202 */ 0203 inline const wchar_t *QStringToWchar(const QString &str) Q_DECL_NOEXCEPT 0204 { 0205 m_conversionBuffer.resize(str.length() + 1); 0206 int size = str.toWCharArray(m_conversionBuffer.data()); 0207 m_conversionBuffer[size] = '\0'; 0208 0209 return m_conversionBuffer.constData(); 0210 } 0211 0212 private: 0213 /** 0214 * Extract and append user defined words from the languageNode. 0215 */ 0216 inline void loadUserWords(const QJsonObject &languageNode) Q_DECL_NOEXCEPT 0217 { 0218 const auto words = languageNode[personal_words_str()].toArray(); 0219 for (auto word : words) { 0220 m_personalWords.insert(word.toString()); 0221 } 0222 qCDebug(SONNET_VOIKKO) << QStringLiteral("Loaded %1 words from the user dictionary.").arg(words.size()); 0223 } 0224 0225 /** 0226 * Extract and append user defined replacement pairs from the languageNode. 0227 */ 0228 inline void loadUserReplacements(const QJsonObject &languageNode) Q_DECL_NOEXCEPT 0229 { 0230 const auto words = languageNode[replacements_str()].toArray(); 0231 for (auto pair : words) { 0232 m_replacements[pair.toObject()[replacement_bad_str()].toString()] = pair.toObject()[replacement_good_str()].toString(); 0233 } 0234 qCDebug(SONNET_VOIKKO) << QStringLiteral("Loaded %1 replacements from the user dictionary.").arg(words.size()); 0235 } 0236 }; 0237 0238 VoikkoDict::VoikkoDict(const QString &language) Q_DECL_NOEXCEPT : SpellerPlugin(language), d(new VoikkoDictPrivate(language, this)) 0239 { 0240 qCDebug(SONNET_VOIKKO) << "Loading dictionary for language:" << language; 0241 } 0242 0243 VoikkoDict::~VoikkoDict() 0244 { 0245 } 0246 0247 bool VoikkoDict::isCorrect(const QString &word) const 0248 { 0249 // Check the session word list and personal word list first 0250 if (d->m_sessionWords.contains(word) || d->m_personalWords.contains(word)) { 0251 return true; 0252 } 0253 0254 return voikkoSpellUcs4(d->m_handle, d->QStringToWchar(word)) == VOIKKO_SPELL_OK; 0255 } 0256 0257 QStringList VoikkoDict::suggest(const QString &word) const 0258 { 0259 QStringList suggestions; 0260 0261 auto userDictPos = d->m_replacements.constFind(word); 0262 if (userDictPos != d->m_replacements.constEnd()) { 0263 suggestions.append(*userDictPos); 0264 } 0265 0266 auto voikkoSuggestions = voikkoSuggestUcs4(d->m_handle, d->QStringToWchar(word)); 0267 0268 if (!voikkoSuggestions) { 0269 return suggestions; 0270 } 0271 0272 for (int i = 0; voikkoSuggestions[i] != nullptr; ++i) { 0273 QString suggestion = QString::fromWCharArray(voikkoSuggestions[i]); 0274 suggestions.append(suggestion); 0275 } 0276 qCDebug(SONNET_VOIKKO) << "Misspelled:" << word << "|Suggestons:" << suggestions.join(QLatin1String(", ")); 0277 0278 voikko_free_suggest_ucs4(voikkoSuggestions); 0279 0280 return suggestions; 0281 } 0282 0283 bool VoikkoDict::storeReplacement(const QString &bad, const QString &good) 0284 { 0285 qCDebug(SONNET_VOIKKO) << "Adding new replacement pair to user dictionary:" << bad << "->" << good; 0286 d->m_replacements[bad] = good; 0287 return d->storePersonal(QString(), bad, good); 0288 } 0289 0290 bool VoikkoDict::addToPersonal(const QString &word) 0291 { 0292 qCDebug(SONNET_VOIKKO()) << "Adding new word to user dictionary" << word; 0293 d->m_personalWords.insert(word); 0294 return d->storePersonal(word); 0295 } 0296 0297 bool VoikkoDict::addToSession(const QString &word) 0298 { 0299 qCDebug(SONNET_VOIKKO()) << "Adding new word to session dictionary" << word; 0300 d->m_sessionWords.insert(word); 0301 return true; 0302 } 0303 0304 bool VoikkoDict::initFailed() const Q_DECL_NOEXCEPT 0305 { 0306 return !d->m_handle; 0307 }