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