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