Sonnet

voikkodict.cpp
1/*
2 * voikkodict.cpp
3 *
4 * SPDX-FileCopyrightText: 2015 Jesse Jaara <jesse.jaara@gmail.com>
5 *
6 * SPDX-License-Identifier: LGPL-2.1-or-later
7 */
8
9#include "voikkodict.h"
10#include "voikkodebug.h"
11
12#include <QDir>
13#include <QList>
14#include <QStandardPaths>
15#ifdef Q_IS_WIN
16#include <QSysInfo>
17#endif
18
19#include <QJsonArray>
20#include <QJsonDocument>
21#include <QJsonObject>
22
23namespace
24{
25// QString literals used in loading and storing user dictionary
26inline const QString replacement_bad_str() Q_DECL_NOEXCEPT
27{
28 return QStringLiteral("bad");
29}
30
31inline const QString replacement_good_str() Q_DECL_NOEXCEPT
32{
33 return QStringLiteral("good");
34}
35
36inline const QString personal_words_str() Q_DECL_NOEXCEPT
37{
38 return QStringLiteral("PersonalWords");
39}
40
41inline const QString replacements_str() Q_DECL_NOEXCEPT
42{
43 return QStringLiteral("Replacements");
44}
45
46// Set path to: QStandardPaths::GenericDataLocation/Sonnet/Voikko-user-dictionary.json
47QString getUserDictionaryPath() Q_DECL_NOEXCEPT
48{
50
51#ifdef Q_OS_WIN
52 // Resolve the windows' Roaming directory manually
53 if (QSysInfo::windowsVersion() == QSysInfo::WV_XP || QSysInfo::windowsVersion() == QSysInfo::WV_2003) {
54 // In Xp Roaming is "<user>/Application Data"
55 // DataLocation: "<user>/Local Settings/Application Data"
56 directory += QStringLiteral("/../../Application Data");
57 } else {
58 directory += QStringLiteral("/../Roaming");
59 }
60#endif
61
62 directory += QStringLiteral("/Sonnet");
63 QDir path(directory);
64 path.mkpath(path.absolutePath());
65
66 return path.absoluteFilePath(QStringLiteral("Voikko-user-dictionary.json"));
67}
68
69void addReplacementToNode(QJsonObject &languageNode, const QString &bad, const QString &good) Q_DECL_NOEXCEPT
70{
71 QJsonObject pair;
72 pair[replacement_bad_str()] = good;
73 pair[replacement_good_str()] = bad;
74
75 auto replaceList = languageNode[replacements_str()].toArray();
76 replaceList.append(pair);
77 languageNode[replacements_str()] = replaceList;
78}
79
80void addPersonalWordToNode(QJsonObject &languageNode, const QString &word) Q_DECL_NOEXCEPT
81{
82 auto arr = languageNode[personal_words_str()].toArray();
83 arr.append(word);
84 languageNode[personal_words_str()] = arr;
85}
86
87/**
88 * Read and return the root json object from fileName.
89 *
90 * Returns an empty node in case of an IO error or the file is empty.
91 */
92QJsonObject readJsonRootObject(const QString &fileName) Q_DECL_NOEXCEPT
93{
94 QFile userDictFile(fileName);
95
96 if (!userDictFile.exists()) {
97 return QJsonObject(); // Nothing has been saved so far.
98 }
99
100 if (!userDictFile.open(QIODevice::ReadOnly)) {
101 qCWarning(SONNET_VOIKKO) << "Could not open personal dictionary. Failed to open file" << fileName;
102 qCWarning(SONNET_VOIKKO) << "Reason:" << userDictFile.errorString();
103 return QJsonObject();
104 }
105
106 QJsonDocument dictDoc = QJsonDocument::fromJson(userDictFile.readAll());
107 userDictFile.close();
108
109 return dictDoc.object();
110}
111}
112
113class VoikkoDictPrivate
114{
115public:
116 VoikkoHandle *m_handle;
117 const VoikkoDict *q;
118
119 QSet<QString> m_sessionWords;
120 QSet<QString> m_personalWords;
121 QHash<QString, QString> m_replacements;
122
123 QString m_userDictionaryFilepath;
124
125 // Used when converting Qstring to wchar_t strings
126 QList<wchar_t> m_conversionBuffer;
127
128 VoikkoDictPrivate(const QString &language, const VoikkoDict *publicPart) Q_DECL_NOEXCEPT : q(publicPart),
129 m_userDictionaryFilepath(getUserDictionaryPath()),
130 m_conversionBuffer(256)
131 {
132 const char *error;
133 m_handle = voikkoInit(&error, language.toUtf8().data(), nullptr);
134
135 if (error != nullptr) {
136 qCWarning(SONNET_VOIKKO) << "Failed to initialize Voikko spelling backend. Reason:" << error;
137 } else { // Continue to load user's own words
138 loadUserDictionary();
139 }
140 }
141
142 /**
143 * Store a new ignored/personal word or replacement pair in the user's
144 * dictionary m_userDictionaryFilepath.
145 *
146 * returns true on success else false
147 */
148 bool storePersonal(const QString &personalWord, const QString &bad = QString(), const QString &good = QString()) const Q_DECL_NOEXCEPT
149 {
150 QFile userDictFile(m_userDictionaryFilepath);
151
152 if (!userDictFile.open(QIODevice::ReadWrite)) {
153 qCWarning(SONNET_VOIKKO) << "Could not save personal dictionary. Failed to open file:" << m_userDictionaryFilepath;
154 qCWarning(SONNET_VOIKKO) << "Reason:" << userDictFile.errorString();
155 return false;
156 }
157
158 QJsonDocument dictDoc = QJsonDocument::fromJson(userDictFile.readAll());
159 auto root = readJsonRootObject(m_userDictionaryFilepath);
160 auto languageNode = root[q->language()].toObject();
161
162 // Empty value means we are storing a bad:good pair
163 if (personalWord.isEmpty()) {
164 addReplacementToNode(languageNode, bad, good);
165 } else {
166 addPersonalWordToNode(languageNode, personalWord);
167 }
168
169 root[q->language()] = languageNode;
170 dictDoc.setObject(root);
171
172 userDictFile.reset();
173 userDictFile.write(dictDoc.toJson());
174 userDictFile.close();
175 qCDebug(SONNET_VOIKKO) << "Changes to user dictionary saved to file: " << m_userDictionaryFilepath;
176
177 return true;
178 }
179
180 /**
181 * Load user's own personal words and replacement pairs from
182 * m_userDictionaryFilepath.
183 */
184 void loadUserDictionary() Q_DECL_NOEXCEPT
185 {
186 // If root is empty we will fail later on when checking if
187 // languageNode is empty.
188 auto root = readJsonRootObject(m_userDictionaryFilepath);
189 auto languageNode = root[q->language()].toObject();
190
191 if (languageNode.isEmpty()) {
192 return; // Nothing to load
193 }
194
195 loadUserWords(languageNode);
196 loadUserReplacements(languageNode);
197 }
198
199 /**
200 * Convert the given QString to a \0 terminated wchar_t string.
201 * Uses QList as a buffer and return it's internal data pointer.
202 */
203 inline const wchar_t *QStringToWchar(const QString &str) Q_DECL_NOEXCEPT
204 {
205 m_conversionBuffer.resize(str.length() + 1);
206 int size = str.toWCharArray(m_conversionBuffer.data());
207 m_conversionBuffer[size] = '\0';
208
209 return m_conversionBuffer.constData();
210 }
211
212private:
213 /**
214 * Extract and append user defined words from the languageNode.
215 */
216 inline void loadUserWords(const QJsonObject &languageNode) Q_DECL_NOEXCEPT
217 {
218 const auto words = languageNode[personal_words_str()].toArray();
219 for (auto word : words) {
220 m_personalWords.insert(word.toString());
221 }
222 qCDebug(SONNET_VOIKKO) << QStringLiteral("Loaded %1 words from the user dictionary.").arg(words.size());
223 }
224
225 /**
226 * Extract and append user defined replacement pairs from the languageNode.
227 */
228 inline void loadUserReplacements(const QJsonObject &languageNode) Q_DECL_NOEXCEPT
229 {
230 const auto words = languageNode[replacements_str()].toArray();
231 for (auto pair : words) {
232 m_replacements[pair.toObject()[replacement_bad_str()].toString()] = pair.toObject()[replacement_good_str()].toString();
233 }
234 qCDebug(SONNET_VOIKKO) << QStringLiteral("Loaded %1 replacements from the user dictionary.").arg(words.size());
235 }
236};
237
238VoikkoDict::VoikkoDict(const QString &language) Q_DECL_NOEXCEPT : SpellerPlugin(language), d(new VoikkoDictPrivate(language, this))
239{
240 qCDebug(SONNET_VOIKKO) << "Loading dictionary for language:" << language;
241}
242
243VoikkoDict::~VoikkoDict()
244{
245}
246
247bool VoikkoDict::isCorrect(const QString &word) const
248{
249 // Check the session word list and personal word list first
250 if (d->m_sessionWords.contains(word) || d->m_personalWords.contains(word)) {
251 return true;
252 }
253
254 return voikkoSpellUcs4(d->m_handle, d->QStringToWchar(word)) == VOIKKO_SPELL_OK;
255}
256
257QStringList VoikkoDict::suggest(const QString &word) const
258{
259 QStringList suggestions;
260
261 auto userDictPos = d->m_replacements.constFind(word);
262 if (userDictPos != d->m_replacements.constEnd()) {
263 suggestions.append(*userDictPos);
264 }
265
266 auto voikkoSuggestions = voikkoSuggestUcs4(d->m_handle, d->QStringToWchar(word));
267
268 if (!voikkoSuggestions) {
269 return suggestions;
270 }
271
272 for (int i = 0; voikkoSuggestions[i] != nullptr; ++i) {
273 QString suggestion = QString::fromWCharArray(voikkoSuggestions[i]);
274 suggestions.append(suggestion);
275 }
276 qCDebug(SONNET_VOIKKO) << "Misspelled:" << word << "|Suggestons:" << suggestions.join(QLatin1String(", "));
277
278 voikko_free_suggest_ucs4(voikkoSuggestions);
279
280 return suggestions;
281}
282
283bool VoikkoDict::storeReplacement(const QString &bad, const QString &good)
284{
285 qCDebug(SONNET_VOIKKO) << "Adding new replacement pair to user dictionary:" << bad << "->" << good;
286 d->m_replacements[bad] = good;
287 return d->storePersonal(QString(), bad, good);
288}
289
290bool VoikkoDict::addToPersonal(const QString &word)
291{
292 qCDebug(SONNET_VOIKKO()) << "Adding new word to user dictionary" << word;
293 d->m_personalWords.insert(word);
294 return d->storePersonal(word);
295}
296
297bool VoikkoDict::addToSession(const QString &word)
298{
299 qCDebug(SONNET_VOIKKO()) << "Adding new word to session dictionary" << word;
300 d->m_sessionWords.insert(word);
301 return true;
302}
303
304bool VoikkoDict::initFailed() const Q_DECL_NOEXCEPT
305{
306 return !d->m_handle;
307}
QString path(const QString &relativePath)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
void setObject(const QJsonObject &object)
QByteArray toJson(JsonFormat format) const const
void append(QList< T > &&value)
const_pointer constData() const const
pointer data()
void resize(qsizetype size)
iterator insert(const T &value)
QString writableLocation(StandardLocation type)
QString fromWCharArray(const wchar_t *string, qsizetype size)
bool isEmpty() const const
QString join(QChar separator) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:15:10 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.