]> git.lyx.org Git - features.git/blob - src/HunspellChecker.cpp
Allow removing words from the personal dictionary, that weren't previously added.
[features.git] / src / HunspellChecker.cpp
1 /**
2  * \file HunspellChecker.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Abdelrazak Younes
7  *
8  * Full author contact details are available in file CREDITS.
9  */
10
11 #include <config.h>
12
13 #include "HunspellChecker.h"
14 #include "PersonalWordList.h"
15
16 #include "LyXRC.h"
17 #include "WordLangTuple.h"
18
19 #include "support/debug.h"
20 #include "support/docstring_list.h"
21 #include "support/filetools.h"
22 #include "support/Package.h"
23 #include "support/FileName.h"
24 #include "support/lassert.h"
25 #include "support/lstrings.h"
26
27 #include <hunspell/hunspell.hxx>
28
29 #include <map>
30 #include <string>
31 #include <vector>
32
33 using namespace std;
34 using namespace lyx::support;
35 using namespace lyx::support::os;
36
37 namespace lyx {
38
39 namespace {
40
41 typedef map<std::string, Hunspell *> Spellers;
42 typedef map<std::string, PersonalWordList *> LangPersonalWordList;
43
44 docstring remap_result(docstring const & s)
45 {
46         // substitute RIGHT SINGLE QUOTATION MARK
47         // by APOSTROPHE
48         return subst(s, 0x2019, 0x0027);
49 }
50
51 } // namespace
52
53
54 struct HunspellChecker::Private
55 {
56         Private();
57         ~Private();
58
59         void cleanCache();
60         void setUserPath(std::string const & path);
61         const string dictPath(int selector);
62         bool haveLanguageFiles(string const & hpath);
63         bool haveDictionary(Language const * lang, string & hpath);
64         bool haveDictionary(Language const * lang);
65         int numDictionaries() const;
66         Hunspell * addSpeller(Language const * lang, string & hpath);
67         Hunspell * addSpeller(Language const * lang);
68         Hunspell * speller(Language const * lang);
69         Hunspell * lookup(Language const * lang);
70         /// ignored words
71         bool isIgnored(WordLangTuple const & wl) const;
72         /// personal word list interface
73         void remove(WordLangTuple const & wl);
74         void insert(WordLangTuple const & wl);
75         /// the spellers
76         Spellers spellers_;
77         ///
78         WordLangTable ignored_;
79         ///
80         LangPersonalWordList personal_;
81         ///
82         std::string user_path_;
83
84         /// the location below system/user directory
85         /// there the aff+dic files lookup will happen
86         const string dictDirectory(void) const { return "dicts"; }
87         int maxLookupSelector(void) const { return 5; }
88         const string HunspellDictionaryName(Language const * lang) {
89                 return lang->variety().empty()
90                         ? lang->code()
91                         : lang->code() + "-" + lang->variety();
92         }
93         const string myspellPackageDictDirectory(void) {
94                 return "/usr/share/myspell";
95         }
96         const string hunspellPackageDictDirectory(void) {
97                 return "/usr/share/hunspell";
98         }
99 };
100
101
102 HunspellChecker::Private::Private()
103 {
104         setUserPath(lyxrc.hunspelldir_path);
105 }
106
107
108 HunspellChecker::Private::~Private()
109 {
110         cleanCache();
111 }
112
113
114 void HunspellChecker::Private::setUserPath(std::string const & path)
115 {
116         if (user_path_ != lyxrc.hunspelldir_path) {
117                 cleanCache();
118                 user_path_ = path;
119         }
120 }
121
122
123 void HunspellChecker::Private::cleanCache()
124 {
125         Spellers::iterator it = spellers_.begin();
126         Spellers::iterator end = spellers_.end();
127
128         for (; it != end; ++it) {
129                 delete it->second;
130                 it->second = nullptr;
131         }
132
133         LangPersonalWordList::const_iterator pdit = personal_.begin();
134         LangPersonalWordList::const_iterator pdet = personal_.end();
135
136         for (; pdit != pdet; ++pdit) {
137                 if (pdit->second == nullptr)
138                         continue;
139                 PersonalWordList * pd = pdit->second;
140                 pd->save();
141                 delete pd;
142         }
143 }
144
145
146 bool HunspellChecker::Private::haveLanguageFiles(string const & hpath)
147 {
148         FileName const affix(hpath + ".aff");
149         FileName const dict(hpath + ".dic");
150         return affix.isReadableFile() && dict.isReadableFile();
151 }
152
153
154 const string HunspellChecker::Private::dictPath(int selector)
155 {
156         switch (selector) {
157         case 4:
158                 return hunspellPackageDictDirectory();
159         case 3:
160                 return myspellPackageDictDirectory();
161         case 2:
162                 return addName(package().system_support().absFileName(),dictDirectory());
163         case 1:
164                 return addName(package().user_support().absFileName(),dictDirectory());
165         default:
166                 return user_path_;
167         }
168 }
169
170
171 bool HunspellChecker::Private::haveDictionary(Language const * lang, string & hpath)
172 {
173         if (hpath.empty() || !lang)
174                 return false;
175
176         if (lookup(lang)) return true;
177
178         string d_name = HunspellDictionaryName(lang);
179
180         LYXERR(Debug::FILES, "check hunspell path: " << hpath
181                 << " for language " << lang->lang() << " with name " << d_name);
182
183         string h_path = addName(hpath, d_name);
184         // first we try lang code+variety
185         if (haveLanguageFiles(h_path)) {
186                 LYXERR(Debug::FILES, "  found " << h_path);
187                 hpath = h_path;
188                 return true;
189         }
190         // another try with code, '_' replaced by '-'
191         h_path = addName(hpath, subst(lang->code(), '_', '-'));
192         if (!haveLanguageFiles(h_path))
193                 return false;
194         LYXERR(Debug::FILES, "  found " << h_path);
195         hpath = h_path;
196         return true;
197 }
198
199
200 bool HunspellChecker::Private::haveDictionary(Language const * lang)
201 {
202         bool result = false;
203
204         setUserPath(lyxrc.hunspelldir_path);
205         for (int p = 0; !result && p < maxLookupSelector(); ++p) {
206                 string lpath = dictPath(p);
207                 result = haveDictionary(lang, lpath);
208         }
209         return result;
210 }
211
212
213 Hunspell * HunspellChecker::Private::speller(Language const * lang)
214 {
215         Hunspell * h = lookup(lang);
216         if (h) return h;
217
218         setUserPath(lyxrc.hunspelldir_path);
219         return addSpeller(lang);
220 }
221
222
223 Hunspell * HunspellChecker::Private::lookup(Language const * lang)
224 {
225         Spellers::iterator it = spellers_.find(lang->lang());
226         return it != spellers_.end() ? it->second : nullptr;
227 }
228
229
230 Hunspell * HunspellChecker::Private::addSpeller(Language const * lang, string & path)
231 {
232         if (!haveDictionary(lang, path)) {
233                 spellers_[lang->lang()] = nullptr;
234                 return nullptr;
235         }
236
237         FileName const affix(path + ".aff");
238         FileName const dict(path + ".dic");
239         Hunspell * h = new Hunspell(affix.absFileName().c_str(), dict.absFileName().c_str());
240         LYXERR(Debug::FILES, "Hunspell speller for langage " << lang << " at " << dict << " added.");
241         spellers_[lang->lang()] = h;
242         return h;
243 }
244
245
246 Hunspell * HunspellChecker::Private::addSpeller(Language const * lang)
247 {
248         Hunspell * h = nullptr;
249         for (int p = 0; p < maxLookupSelector() && nullptr == h; ++p) {
250                 string lpath = dictPath(p);
251                 h = addSpeller(lang, lpath);
252         }
253         if (h) {
254                 string const encoding = h->get_dic_encoding();
255                 PersonalWordList * pd = new PersonalWordList(lang->lang());
256                 pd->load();
257                 personal_[lang->lang()] = pd;
258                 docstring_list::const_iterator it = pd->includes_begin();
259                 docstring_list::const_iterator et = pd->includes_end();
260                 for (; it != et; ++it) {
261                         string const word_to_add = to_iconv_encoding(*it, encoding);
262                         h->add(word_to_add.c_str());
263                 }
264                 it = pd->excludes_begin();
265                 et = pd->excludes_end();
266                 for (; it != et; ++it) {
267                         string const word_to_add = to_iconv_encoding(*it, encoding);
268                         h->remove(word_to_add.c_str());
269                 }
270         }
271         return h;
272 }
273
274
275 int HunspellChecker::Private::numDictionaries() const
276 {
277         int result = 0;
278         Spellers::const_iterator it = spellers_.begin();
279         Spellers::const_iterator et = spellers_.end();
280
281         for (; it != et; ++it)
282                 result += it->second != nullptr;
283         return result;
284 }
285
286
287 bool HunspellChecker::Private::isIgnored(WordLangTuple const & wl) const
288 {
289         WordLangTable::const_iterator it = ignored_.begin();
290         for (; it != ignored_.end(); ++it) {
291                 if (it->lang()->code() != wl.lang()->code())
292                         continue;
293                 if (it->word() == wl.word())
294                         return true;
295         }
296         return false;
297 }
298
299 /// personal word list interface
300 void HunspellChecker::Private::remove(WordLangTuple const & wl)
301 {
302         Hunspell * h = speller(wl.lang());
303         if (!h)
304                 return;
305         string const encoding = h->get_dic_encoding();
306         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
307         h->remove(word_to_check.c_str());
308         PersonalWordList * pd = personal_[wl.lang()->lang()];
309         if (!pd)
310                 return;
311         pd->remove(wl.word());
312 }
313
314
315 void HunspellChecker::Private::insert(WordLangTuple const & wl)
316 {
317         Hunspell * h = speller(wl.lang());
318         if (!h)
319                 return;
320         string const encoding = h->get_dic_encoding();
321         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
322         h->add(word_to_check.c_str());
323         PersonalWordList * pd = personal_[wl.lang()->lang()];
324         if (!pd)
325                 return;
326         pd->insert(wl.word());
327 }
328
329
330 HunspellChecker::HunspellChecker()
331         : d(new Private)
332 {}
333
334
335 HunspellChecker::~HunspellChecker()
336 {
337         delete d;
338 }
339
340
341 SpellChecker::Result HunspellChecker::check(WordLangTuple const & wl,
342                                             vector<WordLangTuple> const & docdict)
343 {
344         if (d->isIgnored(wl))
345                 return WORD_OK;
346
347         WordLangTable::const_iterator it = docdict.begin();
348         for (; it != docdict.end(); ++it) {
349                 if (it->lang()->code() != wl.lang()->code())
350                         continue;
351                 if (it->word() == wl.word())
352                         return DOCUMENT_LEARNED_WORD;
353         }
354
355         Hunspell * h = d->speller(wl.lang());
356         if (!h)
357                 return NO_DICTIONARY;
358         int info;
359
360         string const encoding = h->get_dic_encoding();
361         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
362
363         LYXERR(Debug::GUI, "spellCheck: \"" <<
364                    wl.word() << "\", lang = " << wl.lang()->lang()) ;
365 #ifdef HAVE_HUNSPELL_CXXABI
366         if (h->spell(word_to_check, &info))
367 #else
368         if (h->spell(word_to_check.c_str(), &info))
369 #endif
370                 return WORD_OK;
371
372         if (info & SPELL_COMPOUND) {
373                 // FIXME: What to do with that?
374                 LYXERR(Debug::GUI, "Hunspell compound word found " << word_to_check);
375         }
376         if (info & SPELL_FORBIDDEN) {
377                 // This was removed from personal dictionary
378                 LYXERR(Debug::GUI, "Hunspell explicit forbidden word found " << word_to_check);
379         }
380
381         return UNKNOWN_WORD;
382 }
383
384
385 void HunspellChecker::advanceChangeNumber()
386 {
387         nextChangeNumber();
388 }
389
390
391 void HunspellChecker::insert(WordLangTuple const & wl)
392 {
393         d->insert(wl);
394         LYXERR(Debug::GUI, "learn word: \"" << wl.word() << "\"") ;
395         advanceChangeNumber();
396 }
397
398
399 void HunspellChecker::remove(WordLangTuple const & wl)
400 {
401         d->remove(wl);
402         LYXERR(Debug::GUI, "unlearn word: \"" << wl.word() << "\"") ;
403         advanceChangeNumber();
404 }
405
406
407 void HunspellChecker::accept(WordLangTuple const & wl)
408 {
409         d->ignored_.push_back(wl);
410         LYXERR(Debug::GUI, "ignore word: \"" << wl.word() << "\"") ;
411         advanceChangeNumber();
412 }
413
414
415 void HunspellChecker::suggest(WordLangTuple const & wl,
416         docstring_list & suggestions)
417 {
418         suggestions.clear();
419         Hunspell * h = d->speller(wl.lang());
420         if (!h)
421                 return;
422         string const encoding = h->get_dic_encoding();
423         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
424 #ifdef HAVE_HUNSPELL_CXXABI
425         vector<string> wlst = h->suggest(word_to_check);
426         for (auto const & s : wlst)
427                 suggestions.push_back(remap_result(from_iconv_encoding(s, encoding)));
428 #else
429         char ** suggestion_list;
430         int const suggestion_number = h->suggest(&suggestion_list, word_to_check.c_str());
431         if (suggestion_number <= 0)
432                 return;
433         for (int i = 0; i != suggestion_number; ++i)
434                 suggestions.push_back(remap_result(from_iconv_encoding(suggestion_list[i], encoding)));
435         h->free_list(&suggestion_list, suggestion_number);
436 #endif
437 }
438
439
440 void HunspellChecker::stem(WordLangTuple const & wl,
441         docstring_list & suggestions)
442 {
443         suggestions.clear();
444         Hunspell * h = d->speller(wl.lang());
445         if (!h)
446                 return;
447         string const encoding = h->get_dic_encoding();
448         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
449 #ifdef HAVE_HUNSPELL_CXXABI
450         vector<string> wlst = h->stem(word_to_check);
451         for (auto const & s : wlst)
452                 suggestions.push_back(from_iconv_encoding(s, encoding));
453 #else
454         char ** suggestion_list;
455         int const suggestion_number = h->stem(&suggestion_list, word_to_check.c_str());
456         if (suggestion_number <= 0)
457                 return;
458         for (int i = 0; i != suggestion_number; ++i)
459                 suggestions.push_back(from_iconv_encoding(suggestion_list[i], encoding));
460         h->free_list(&suggestion_list, suggestion_number);
461 #endif
462 }
463
464
465 bool HunspellChecker::hasDictionary(Language const * lang) const
466 {
467         if (!lang)
468                 return false;
469         return d->haveDictionary(lang);
470 }
471
472
473 int HunspellChecker::numDictionaries() const
474 {
475         return d->numDictionaries();
476 }
477
478
479 docstring const HunspellChecker::error()
480 {
481         return docstring();
482 }
483
484
485 } // namespace lyx