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