]> git.lyx.org Git - features.git/blob - src/HunspellChecker.cpp
Fix compilation for clang 3.7 with libc++
[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 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 = nullptr;
134         }
135
136         LangPersonalWordList::const_iterator pdit = personal_.begin();
137         LangPersonalWordList::const_iterator pdet = personal_.end();
138
139         for (; pdit != pdet; ++pdit) {
140                 if (pdit->second == nullptr)
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 : nullptr;
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 = nullptr;
252         for (int p = 0; p < maxLookupSelector() && nullptr == 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 != nullptr;
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                                             vector<WordLangTuple> const & docdict)
349 {
350         if (d->isIgnored(wl))
351                 return WORD_OK;
352
353         IgnoreList::const_iterator it = docdict.begin();
354         for (; it != docdict.end(); ++it) {
355                 if (it->lang()->code() != wl.lang()->code())
356                         continue;
357                 if (it->word() == wl.word())
358                         return DOCUMENT_LEARNED_WORD;
359         }
360
361         Hunspell * h = d->speller(wl.lang());
362         if (!h)
363                 return NO_DICTIONARY;
364         int info;
365
366         string const encoding = h->get_dic_encoding();
367         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
368
369         LYXERR(Debug::GUI, "spellCheck: \"" <<
370                    wl.word() << "\", lang = " << wl.lang()->lang()) ;
371 #ifdef HAVE_HUNSPELL_CXXABI
372         if (h->spell(word_to_check, &info))
373 #else
374         if (h->spell(word_to_check.c_str(), &info))
375 #endif
376                 return d->learned(wl) ? LEARNED_WORD : WORD_OK;
377
378         if (info & SPELL_COMPOUND) {
379                 // FIXME: What to do with that?
380                 LYXERR(Debug::GUI, "Hunspell compound word found " << word_to_check);
381         }
382         if (info & SPELL_FORBIDDEN) {
383                 // This was removed from personal dictionary
384                 LYXERR(Debug::GUI, "Hunspell explicit forbidden word found " << word_to_check);
385         }
386
387         return UNKNOWN_WORD;
388 }
389
390
391 void HunspellChecker::advanceChangeNumber()
392 {
393         nextChangeNumber();
394 }
395
396
397 void HunspellChecker::insert(WordLangTuple const & wl)
398 {
399         d->insert(wl);
400         LYXERR(Debug::GUI, "learn word: \"" << wl.word() << "\"") ;
401         advanceChangeNumber();
402 }
403
404
405 void HunspellChecker::remove(WordLangTuple const & wl)
406 {
407         d->remove(wl);
408         LYXERR(Debug::GUI, "unlearn word: \"" << wl.word() << "\"") ;
409         advanceChangeNumber();
410 }
411
412
413 void HunspellChecker::accept(WordLangTuple const & wl)
414 {
415         d->ignored_.push_back(wl);
416         LYXERR(Debug::GUI, "ignore word: \"" << wl.word() << "\"") ;
417         advanceChangeNumber();
418 }
419
420
421 void HunspellChecker::suggest(WordLangTuple const & wl,
422         docstring_list & suggestions)
423 {
424         suggestions.clear();
425         Hunspell * h = d->speller(wl.lang());
426         if (!h)
427                 return;
428         string const encoding = h->get_dic_encoding();
429         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
430 #ifdef HAVE_HUNSPELL_CXXABI
431         vector<string> wlst = h->suggest(word_to_check);
432         for (auto const & s : wlst)
433                 suggestions.push_back(remap_result(from_iconv_encoding(s, encoding)));
434 #else
435         char ** suggestion_list;
436         int const suggestion_number = h->suggest(&suggestion_list, word_to_check.c_str());
437         if (suggestion_number <= 0)
438                 return;
439         for (int i = 0; i != suggestion_number; ++i)
440                 suggestions.push_back(remap_result(from_iconv_encoding(suggestion_list[i], encoding)));
441         h->free_list(&suggestion_list, suggestion_number);
442 #endif
443 }
444
445
446 void HunspellChecker::stem(WordLangTuple const & wl,
447         docstring_list & suggestions)
448 {
449         suggestions.clear();
450         Hunspell * h = d->speller(wl.lang());
451         if (!h)
452                 return;
453         string const encoding = h->get_dic_encoding();
454         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
455 #ifdef HAVE_HUNSPELL_CXXABI
456         vector<string> wlst = h->stem(word_to_check);
457         for (auto const & s : wlst)
458                 suggestions.push_back(from_iconv_encoding(s, encoding));
459 #else
460         char ** suggestion_list;
461         int const suggestion_number = h->stem(&suggestion_list, word_to_check.c_str());
462         if (suggestion_number <= 0)
463                 return;
464         for (int i = 0; i != suggestion_number; ++i)
465                 suggestions.push_back(from_iconv_encoding(suggestion_list[i], encoding));
466         h->free_list(&suggestion_list, suggestion_number);
467 #endif
468 }
469
470
471 bool HunspellChecker::hasDictionary(Language const * lang) const
472 {
473         if (!lang)
474                 return false;
475         return d->haveDictionary(lang);
476 }
477
478
479 int HunspellChecker::numDictionaries() const
480 {
481         return d->numDictionaries();
482 }
483
484
485 docstring const HunspellChecker::error()
486 {
487         return docstring();
488 }
489
490
491 } // namespace lyx