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