]> git.lyx.org Git - lyx.git/blob - src/HunspellChecker.cpp
Support new hunspell C++ ABI if LyX is built against hunspell >= 1.5
[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 "frontends/alert.h"
20
21 #include "support/debug.h"
22 #include "support/docstring_list.h"
23 #include "support/filetools.h"
24 #include "support/Package.h"
25 #include "support/FileName.h"
26 #include "support/gettext.h"
27 #include "support/lassert.h"
28 #include "support/lstrings.h"
29 #include "support/os.h"
30
31 #include <hunspell/hunspell.hxx>
32
33 #include <map>
34 #include <string>
35 #include <vector>
36
37 using namespace std;
38 using namespace lyx::support;
39 using namespace lyx::support::os;
40
41 namespace lyx {
42
43 namespace {
44
45 typedef map<std::string, Hunspell *> Spellers;
46 typedef map<std::string, PersonalWordList *> LangPersonalWordList;
47
48 typedef vector<WordLangTuple> IgnoreList;
49
50 } // namespace
51
52
53 struct HunspellChecker::Private
54 {
55         Private();
56         ~Private();
57
58         void cleanCache();
59         void setUserPath(std::string const & path);
60         const string dictPath(int selector);
61         bool haveLanguageFiles(string const & hpath);
62         bool haveDictionary(Language const * lang, string & hpath);
63         bool haveDictionary(Language const * lang);
64         int numDictionaries() const;
65         Hunspell * addSpeller(Language const * lang, string & hpath);
66         Hunspell * addSpeller(Language const * lang);
67         Hunspell * speller(Language const * lang);
68         Hunspell * lookup(Language const * lang);
69         /// ignored words
70         bool isIgnored(WordLangTuple const & wl) const;
71         /// personal word list interface
72         void remove(WordLangTuple const & wl);
73         void insert(WordLangTuple const & wl);
74         bool learned(WordLangTuple const & wl);
75         /// the spellers
76         Spellers spellers_;
77         ///
78         IgnoreList 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 = 0;
131         }
132
133         LangPersonalWordList::const_iterator pdit = personal_.begin();
134         LangPersonalWordList::const_iterator pdet = personal_.end();
135
136         for (; pdit != pdet; ++pdit) {
137                 if ( 0 == pdit->second)
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 : 0;
227 }
228
229
230 Hunspell * HunspellChecker::Private::addSpeller(Language const * lang,string & path)
231 {
232         if (!haveDictionary(lang, path)) {
233                 spellers_[lang->lang()] = 0;
234                 return 0;
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 = 0;
249         for (int p = 0; p < maxLookupSelector() && 0 == 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->begin();
259                 docstring_list::const_iterator et = pd->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         }
265         return h;
266 }
267
268
269 int HunspellChecker::Private::numDictionaries() const
270 {
271         int result = 0;
272         Spellers::const_iterator it = spellers_.begin();
273         Spellers::const_iterator et = spellers_.end();
274
275         for (; it != et; ++it)
276                 result += it->second != 0;
277         return result;
278 }
279
280
281 bool HunspellChecker::Private::isIgnored(WordLangTuple const & wl) const
282 {
283         IgnoreList::const_iterator it = ignored_.begin();
284         for (; it != ignored_.end(); ++it) {
285                 if (it->lang()->code() != wl.lang()->code())
286                         continue;
287                 if (it->word() == wl.word())
288                         return true;
289         }
290         return false;
291 }
292
293 /// personal word list interface
294 void HunspellChecker::Private::remove(WordLangTuple const & wl)
295 {
296         Hunspell * h = speller(wl.lang());
297         if (!h)
298                 return;
299         string const encoding = h->get_dic_encoding();
300         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
301         h->remove(word_to_check.c_str());
302         PersonalWordList * pd = personal_[wl.lang()->lang()];
303         if (!pd)
304                 return;
305         pd->remove(wl.word());
306 }
307
308
309 void HunspellChecker::Private::insert(WordLangTuple const & wl)
310 {
311         Hunspell * h = speller(wl.lang());
312         if (!h)
313                 return;
314         string const encoding = h->get_dic_encoding();
315         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
316         h->add(word_to_check.c_str());
317         PersonalWordList * pd = personal_[wl.lang()->lang()];
318         if (!pd)
319                 return;
320         pd->insert(wl.word());
321 }
322
323
324 bool HunspellChecker::Private::learned(WordLangTuple const & wl)
325 {
326         PersonalWordList * pd = personal_[wl.lang()->lang()];
327         if (!pd)
328                 return false;
329         return pd->exists(wl.word());
330 }
331
332
333 HunspellChecker::HunspellChecker()
334         : d(new Private)
335 {}
336
337
338 HunspellChecker::~HunspellChecker()
339 {
340         delete d;
341 }
342
343
344 SpellChecker::Result HunspellChecker::check(WordLangTuple const & wl)
345 {
346         if (d->isIgnored(wl))
347                 return WORD_OK;
348
349         Hunspell * h = d->speller(wl.lang());
350         if (!h)
351                 return NO_DICTIONARY;
352         int info;
353
354         string const encoding = h->get_dic_encoding();
355         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
356
357         LYXERR(Debug::GUI, "spellCheck: \"" <<
358                    wl.word() << "\", lang = " << wl.lang()->lang()) ;
359 #ifdef HAVE_HUNSPELL_CXXABI
360         if (h->spell(word_to_check, &info))
361 #else
362         if (h->spell(word_to_check.c_str(), &info))
363 #endif
364                 return d->learned(wl) ? LEARNED_WORD : WORD_OK;
365
366         if (info & SPELL_COMPOUND) {
367                 // FIXME: What to do with that?
368                 LYXERR(Debug::GUI, "Hunspell compound word found " << word_to_check);
369         }
370         if (info & SPELL_FORBIDDEN) {
371                 // This was removed from personal dictionary
372                 LYXERR(Debug::GUI, "Hunspell explicit forbidden word found " << word_to_check);
373         }
374
375         return UNKNOWN_WORD;
376 }
377
378
379 void HunspellChecker::advanceChangeNumber()
380 {
381         nextChangeNumber();
382 }
383
384
385 void HunspellChecker::insert(WordLangTuple const & wl)
386 {
387         d->insert(wl);
388         LYXERR(Debug::GUI, "learn word: \"" << wl.word() << "\"") ;
389         advanceChangeNumber();
390 }
391
392
393 void HunspellChecker::remove(WordLangTuple const & wl)
394 {
395         d->remove(wl);
396         LYXERR(Debug::GUI, "unlearn word: \"" << wl.word() << "\"") ;
397         advanceChangeNumber();
398 }
399
400
401 void HunspellChecker::accept(WordLangTuple const & wl)
402 {
403         d->ignored_.push_back(wl);
404         LYXERR(Debug::GUI, "ignore word: \"" << wl.word() << "\"") ;
405         advanceChangeNumber();
406 }
407
408
409 void HunspellChecker::suggest(WordLangTuple const & wl,
410         docstring_list & suggestions)
411 {
412         suggestions.clear();
413         Hunspell * h = d->speller(wl.lang());
414         if (!h)
415                 return;
416         string const encoding = h->get_dic_encoding();
417         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
418 #ifdef HAVE_HUNSPELL_CXXABI
419         vector<string> wlst = h->suggest(word_to_check);
420         for (auto const s : wlst)
421                 suggestions.push_back(from_iconv_encoding(s, encoding));
422 #else
423         char ** suggestion_list;
424         int const suggestion_number = h->suggest(&suggestion_list, word_to_check.c_str());
425         if (suggestion_number <= 0)
426                 return;
427         for (int i = 0; i != suggestion_number; ++i)
428                 suggestions.push_back(from_iconv_encoding(suggestion_list[i], encoding));
429         h->free_list(&suggestion_list, suggestion_number);
430 #endif
431 }
432
433
434 void HunspellChecker::stem(WordLangTuple const & wl,
435         docstring_list & suggestions)
436 {
437         suggestions.clear();
438         Hunspell * h = d->speller(wl.lang());
439         if (!h)
440                 return;
441         string const encoding = h->get_dic_encoding();
442         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
443 #ifdef HAVE_HUNSPELL_CXXABI
444         vector<string> wlst = h->stem(word_to_check);
445         for (auto const s : wlst)
446                 suggestions.push_back(from_iconv_encoding(s, encoding));
447 #else
448         char ** suggestion_list;
449         int const suggestion_number = h->stem(&suggestion_list, word_to_check.c_str());
450         if (suggestion_number <= 0)
451                 return;
452         for (int i = 0; i != suggestion_number; ++i)
453                 suggestions.push_back(from_iconv_encoding(suggestion_list[i], encoding));
454         h->free_list(&suggestion_list, suggestion_number);
455 #endif
456 }
457
458
459 bool HunspellChecker::hasDictionary(Language const * lang) const
460 {
461         if (!lang)
462                 return false;
463         return d->haveDictionary(lang);
464 }
465
466
467 int HunspellChecker::numDictionaries() const
468 {
469         return d->numDictionaries();
470 }
471
472
473 docstring const HunspellChecker::error()
474 {
475         return docstring();
476 }
477
478
479 } // namespace lyx