Sonnet

voikkodict.cpp
1 /*
2  * voikkodict.cpp
3  *
4  * SPDX-FileCopyrightText: 2015 Jesse Jaara <[email protected]>
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 <QStandardPaths>
14 #include <QVector>
15 #ifdef Q_IS_WIN
16 #include <QSysInfo>
17 #endif
18 
19 #include <QJsonArray>
20 #include <QJsonDocument>
21 #include <QJsonObject>
22 
23 namespace
24 {
25 // QString literals used in loading and storing user dictionary
26 inline const QString replacement_bad_str() Q_DECL_NOEXCEPT
27 {
28  return QStringLiteral("bad");
29 }
30 
31 inline const QString replacement_good_str() Q_DECL_NOEXCEPT
32 {
33  return QStringLiteral("good");
34 }
35 
36 inline const QString personal_words_str() Q_DECL_NOEXCEPT
37 {
38  return QStringLiteral("PersonalWords");
39 }
40 
41 inline 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
47 QString getUserDictionaryPath() Q_DECL_NOEXCEPT
48 {
50 
51 #ifdef Q_OS_WIN
52  // Resolve the windows' Roaming directory manually
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 
69 void 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 
80 void 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  */
92 QJsonObject 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 
113 class VoikkoDictPrivate
114 {
115 public:
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  QVector<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 QVector 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 
212 private:
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 
238 VoikkoDict::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 
243 VoikkoDict::~VoikkoDict()
244 {
245 }
246 
247 bool 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 
257 QStringList 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 
283 bool 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 
290 bool 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 
297 bool 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 
304 bool VoikkoDict::initFailed() const Q_DECL_NOEXCEPT
305 {
306  return !d->m_handle;
307 }
void append(const T &value)
QJsonObject object() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QString writableLocation(QStandardPaths::StandardLocation type)
T * data()
const T * constData() const const
QString fromWCharArray(const wchar_t *string, int size)
bool isEmpty() const const
void resize(int size)
QString join(const QString &separator) const const
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QString path(const QString &relativePath)
QString & insert(int position, QChar ch)
QByteArray toJson() const const
void setObject(const QJsonObject &object)
QSet::iterator insert(const T &value)
QSysInfo::WinVersion windowsVersion()
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Oct 1 2023 03:55:48 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.