/* * voikkodict.cpp * * SPDX-FileCopyrightText: 2015 Jesse Jaara * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "voikkodict.h" #include "voikkodebug.h" #include #include #include #ifdef Q_IS_WIN #include #endif #include #include #include namespace { // QString literals used in loading and storing user dictionary inline const QString replacement_bad_str() Q_DECL_NOEXCEPT { return QStringLiteral("bad"); } inline const QString replacement_good_str() Q_DECL_NOEXCEPT { return QStringLiteral("good"); } inline const QString personal_words_str() Q_DECL_NOEXCEPT { return QStringLiteral("PersonalWords"); } inline const QString replacements_str() Q_DECL_NOEXCEPT { return QStringLiteral("Replacements"); } // Set path to: QStandardPaths::GenericDataLocation/Sonnet/Voikko-user-dictionary.json QString getUserDictionaryPath() Q_DECL_NOEXCEPT { QString directory = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); #ifdef Q_OS_WIN // Resolve the windows' Roaming directory manually if (QSysInfo::windowsVersion() == QSysInfo::WV_XP || QSysInfo::windowsVersion() == QSysInfo::WV_2003) { // In Xp Roaming is "/Application Data" // DataLocation: "/Local Settings/Application Data" directory += QStringLiteral("/../../Application Data"); } else { directory += QStringLiteral("/../Roaming"); } #endif directory += QStringLiteral("/Sonnet"); QDir path(directory); path.mkpath(path.absolutePath()); return path.absoluteFilePath(QStringLiteral("Voikko-user-dictionary.json")); } void addReplacementToNode(QJsonObject &languageNode, const QString &bad, const QString &good) Q_DECL_NOEXCEPT { QJsonObject pair; pair[replacement_bad_str()] = good; pair[replacement_good_str()] = bad; auto replaceList = languageNode[replacements_str()].toArray(); replaceList.append(pair); languageNode[replacements_str()] = replaceList; } void addPersonalWordToNode(QJsonObject &languageNode, const QString &word) Q_DECL_NOEXCEPT { auto arr = languageNode[personal_words_str()].toArray(); arr.append(word); languageNode[personal_words_str()] = arr; } /** * Read and return the root json object from fileName. * * Returns an empty node in case of an IO error or the file is empty. */ QJsonObject readJsonRootObject(const QString &fileName) Q_DECL_NOEXCEPT { QFile userDictFile(fileName); if (!userDictFile.exists()) { return QJsonObject(); // Nothing has been saved so far. } if (!userDictFile.open(QIODevice::ReadOnly)) { qCWarning(SONNET_VOIKKO) << "Could not open personal dictionary. Failed to open file" << fileName; qCWarning(SONNET_VOIKKO) << "Reason:" << userDictFile.errorString(); return QJsonObject(); } QJsonDocument dictDoc = QJsonDocument::fromJson(userDictFile.readAll()); userDictFile.close(); return dictDoc.object(); } } class VoikkoDictPrivate { public: VoikkoHandle *m_handle; const VoikkoDict *q; QSet m_sessionWords; QSet m_personalWords; QHash m_replacements; QString m_userDictionaryFilepath; // Used when converting Qstring to wchar_t strings QList m_conversionBuffer; VoikkoDictPrivate(const QString &language, const VoikkoDict *publicPart) Q_DECL_NOEXCEPT : q(publicPart), m_userDictionaryFilepath(getUserDictionaryPath()), m_conversionBuffer(256) { const char *error; m_handle = voikkoInit(&error, language.toUtf8().data(), nullptr); if (error != nullptr) { qCWarning(SONNET_VOIKKO) << "Failed to initialize Voikko spelling backend. Reason:" << error; } else { // Continue to load user's own words loadUserDictionary(); } } /** * Store a new ignored/personal word or replacement pair in the user's * dictionary m_userDictionaryFilepath. * * returns true on success else false */ bool storePersonal(const QString &personalWord, const QString &bad = QString(), const QString &good = QString()) const Q_DECL_NOEXCEPT { QFile userDictFile(m_userDictionaryFilepath); if (!userDictFile.open(QIODevice::ReadWrite)) { qCWarning(SONNET_VOIKKO) << "Could not save personal dictionary. Failed to open file:" << m_userDictionaryFilepath; qCWarning(SONNET_VOIKKO) << "Reason:" << userDictFile.errorString(); return false; } QJsonDocument dictDoc = QJsonDocument::fromJson(userDictFile.readAll()); auto root = readJsonRootObject(m_userDictionaryFilepath); auto languageNode = root[q->language()].toObject(); // Empty value means we are storing a bad:good pair if (personalWord.isEmpty()) { addReplacementToNode(languageNode, bad, good); } else { addPersonalWordToNode(languageNode, personalWord); } root[q->language()] = languageNode; dictDoc.setObject(root); userDictFile.reset(); userDictFile.write(dictDoc.toJson()); userDictFile.close(); qCDebug(SONNET_VOIKKO) << "Changes to user dictionary saved to file: " << m_userDictionaryFilepath; return true; } /** * Load user's own personal words and replacement pairs from * m_userDictionaryFilepath. */ void loadUserDictionary() Q_DECL_NOEXCEPT { // If root is empty we will fail later on when checking if // languageNode is empty. auto root = readJsonRootObject(m_userDictionaryFilepath); auto languageNode = root[q->language()].toObject(); if (languageNode.isEmpty()) { return; // Nothing to load } loadUserWords(languageNode); loadUserReplacements(languageNode); } /** * Convert the given QString to a \0 terminated wchar_t string. * Uses QList as a buffer and return it's internal data pointer. */ inline const wchar_t *QStringToWchar(const QString &str) Q_DECL_NOEXCEPT { m_conversionBuffer.resize(str.length() + 1); int size = str.toWCharArray(m_conversionBuffer.data()); m_conversionBuffer[size] = '\0'; return m_conversionBuffer.constData(); } private: /** * Extract and append user defined words from the languageNode. */ inline void loadUserWords(const QJsonObject &languageNode) Q_DECL_NOEXCEPT { const auto words = languageNode[personal_words_str()].toArray(); for (auto word : words) { m_personalWords.insert(word.toString()); } qCDebug(SONNET_VOIKKO) << QStringLiteral("Loaded %1 words from the user dictionary.").arg(words.size()); } /** * Extract and append user defined replacement pairs from the languageNode. */ inline void loadUserReplacements(const QJsonObject &languageNode) Q_DECL_NOEXCEPT { const auto words = languageNode[replacements_str()].toArray(); for (auto pair : words) { m_replacements[pair.toObject()[replacement_bad_str()].toString()] = pair.toObject()[replacement_good_str()].toString(); } qCDebug(SONNET_VOIKKO) << QStringLiteral("Loaded %1 replacements from the user dictionary.").arg(words.size()); } }; VoikkoDict::VoikkoDict(const QString &language) Q_DECL_NOEXCEPT : SpellerPlugin(language), d(new VoikkoDictPrivate(language, this)) { qCDebug(SONNET_VOIKKO) << "Loading dictionary for language:" << language; } VoikkoDict::~VoikkoDict() { } bool VoikkoDict::isCorrect(const QString &word) const { // Check the session word list and personal word list first if (d->m_sessionWords.contains(word) || d->m_personalWords.contains(word)) { return true; } return voikkoSpellUcs4(d->m_handle, d->QStringToWchar(word)) == VOIKKO_SPELL_OK; } QStringList VoikkoDict::suggest(const QString &word) const { QStringList suggestions; auto userDictPos = d->m_replacements.constFind(word); if (userDictPos != d->m_replacements.constEnd()) { suggestions.append(*userDictPos); } auto voikkoSuggestions = voikkoSuggestUcs4(d->m_handle, d->QStringToWchar(word)); if (!voikkoSuggestions) { return suggestions; } for (int i = 0; voikkoSuggestions[i] != nullptr; ++i) { QString suggestion = QString::fromWCharArray(voikkoSuggestions[i]); suggestions.append(suggestion); } qCDebug(SONNET_VOIKKO) << "Misspelled:" << word << "|Suggestons:" << suggestions.join(QLatin1String(", ")); voikko_free_suggest_ucs4(voikkoSuggestions); return suggestions; } bool VoikkoDict::storeReplacement(const QString &bad, const QString &good) { qCDebug(SONNET_VOIKKO) << "Adding new replacement pair to user dictionary:" << bad << "->" << good; d->m_replacements[bad] = good; return d->storePersonal(QString(), bad, good); } bool VoikkoDict::addToPersonal(const QString &word) { qCDebug(SONNET_VOIKKO()) << "Adding new word to user dictionary" << word; d->m_personalWords.insert(word); return d->storePersonal(word); } bool VoikkoDict::addToSession(const QString &word) { qCDebug(SONNET_VOIKKO()) << "Adding new word to session dictionary" << word; d->m_sessionWords.insert(word); return true; } bool VoikkoDict::initFailed() const Q_DECL_NOEXCEPT { return !d->m_handle; }