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 }