]> git.lyx.org Git - features.git/blob - src/HunspellChecker.cpp
#7170 add id() method for language; add variety of language to hunspell dictionary...
[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 "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 } // anon namespace
51
52
53 struct HunspellChecker::Private
54 {
55         Private();
56         ~Private();
57
58         const string dictPath(int selector);
59         bool haveLanguageFiles(string const & hpath);
60         bool haveDictionary(Language const * lang, string & hpath);
61         bool haveDictionary(Language const * lang);
62         Hunspell * addSpeller(Language const * lang, string & hpath);
63         Hunspell * addSpeller(Language const * lang);
64         Hunspell * speller(Language const * lang);
65         /// ignored words
66         bool isIgnored(WordLangTuple const & wl) const;
67         /// personal word list interface
68         void remove(WordLangTuple const & wl);
69         void insert(WordLangTuple const & wl);
70         bool learned(WordLangTuple const & wl);
71         /// the spellers
72         Spellers spellers_;
73         ///
74         IgnoreList ignored_;
75         ///
76         LangPersonalWordList personal_;
77
78         /// the location below system/user directory
79         /// there the aff+dic files lookup will happen
80         const string dictDirectory(void) const { return "dicts"; }
81         int maxLookupSelector(void) const { return 3; }
82 };
83
84
85 HunspellChecker::Private::Private()
86 {
87 }
88
89
90 HunspellChecker::Private::~Private()
91 {
92         Spellers::iterator it = spellers_.begin();
93         Spellers::iterator end = spellers_.end();
94
95         for (; it != end; ++it) {
96                 if ( 0 != it->second) delete it->second;
97         }
98
99         LangPersonalWordList::const_iterator pdit = personal_.begin();
100         LangPersonalWordList::const_iterator pdet = personal_.end();
101
102         for (; pdit != pdet; ++pdit) {
103                 if ( 0 == pdit->second)
104                         continue;
105                 PersonalWordList * pd = pdit->second;
106                 pd->save();
107                 delete pd;
108         }
109 }
110
111
112 bool HunspellChecker::Private::haveLanguageFiles(string const & hpath)
113 {
114         FileName const affix(hpath + ".aff");
115         FileName const dict(hpath + ".dic");
116         return affix.isReadableFile() && dict.isReadableFile();
117 }
118
119
120 const string HunspellChecker::Private::dictPath(int selector)
121 {
122         switch (selector) {
123         case 2:
124                 return addName(package().system_support().absFileName(),dictDirectory());
125                 break;
126         case 1:
127                 return addName(package().user_support().absFileName(),dictDirectory());
128                 break;
129         default:
130                 return lyxrc.hunspelldir_path;
131         }
132 }
133
134
135 bool HunspellChecker::Private::haveDictionary(Language const * lang, string & hpath)
136 {
137         if (hpath.empty()) {
138                 return false;
139         }
140
141         LYXERR(Debug::FILES, "check hunspell path: " << hpath << " for language " << lang);
142         string h_path = addName(hpath, lang->code() + "-" + lang->variety());
143         // first we try lang code+variety
144         if (!lang->variety().empty() && haveLanguageFiles(h_path)) {
145                 hpath = h_path;
146                 return true;
147         }
148         // next we try lang code only
149         h_path = addName(hpath, lang->code());
150         if (haveLanguageFiles(h_path)) {
151                 hpath = h_path;
152                 return true;
153         }
154         // last try with '_' replaced by '-'
155         h_path = addName(hpath, subst(lang->code(), '_', '-'));
156         if (!haveLanguageFiles(h_path)) {
157                 return false;
158         }
159         hpath = h_path;
160         return true;
161 }
162
163
164 bool HunspellChecker::Private::haveDictionary(Language const * lang)
165 {
166         bool result = false;
167         for ( int p = 0; !result && p < maxLookupSelector(); p++ ) {
168                 string lpath = dictPath(p);
169                 result = haveDictionary(lang, lpath);
170         }
171         // FIXME: if result is false... 
172         // we should indicate somehow that this language is not
173         // supported, probably by popping a warning. But we'll need to
174         // remember which warnings we've issued.
175         return result;
176 }
177
178
179 Hunspell * HunspellChecker::Private::speller(Language const * lang)
180 {
181         Spellers::iterator it = spellers_.find(lang->id());
182         if (it != spellers_.end())
183                 return it->second;
184
185         return addSpeller(lang);
186 }
187
188
189 Hunspell * HunspellChecker::Private::addSpeller(Language const * lang,string & path)
190 {
191         if (!haveDictionary(lang, path)) {
192                 spellers_[lang->id()] = 0;
193                 return 0;
194         }
195
196         FileName const affix(path + ".aff");
197         FileName const dict(path + ".dic");
198         Hunspell * h = new Hunspell(affix.absFileName().c_str(), dict.absFileName().c_str());
199         LYXERR(Debug::FILES, "Hunspell speller for langage " << lang << " at " << dict << " found");
200         spellers_[lang->id()] = h;
201         return h;
202 }
203
204
205 Hunspell * HunspellChecker::Private::addSpeller(Language const * lang)
206 {
207         Hunspell * h = 0;
208         for ( int p = 0; p < maxLookupSelector() && 0 == h; p++ ) {
209                 string lpath = dictPath(p);
210                 h = addSpeller(lang, lpath);
211         }
212         if (0 != h) {
213                 string const encoding = h->get_dic_encoding();
214                 PersonalWordList * pd = new PersonalWordList(lang->lang());
215                 pd->load();
216                 personal_[lang->lang()] = pd;
217                 docstring_list::const_iterator it = pd->begin();
218                 docstring_list::const_iterator et = pd->end();
219                 for (; it != et; ++it) {
220                         string const word_to_add = to_iconv_encoding(*it, encoding);
221                         h->add(word_to_add.c_str());
222                 }
223         }
224         return h;
225 }
226
227
228 bool HunspellChecker::Private::isIgnored(WordLangTuple const & wl) const
229 {
230         IgnoreList::const_iterator it = ignored_.begin();
231         for (; it != ignored_.end(); ++it) {
232                 if ((*it).lang()->code() != wl.lang()->code())
233                         continue;
234                 if ((*it).word() == wl.word())
235                         return true;
236         }
237         return false;
238 }
239
240 /// personal word list interface
241 void HunspellChecker::Private::remove(WordLangTuple const & wl)
242 {
243         Hunspell * h = speller(wl.lang());
244         if (!h)
245                 return;
246         string const encoding = h->get_dic_encoding();
247         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
248         h->remove(word_to_check.c_str());
249         PersonalWordList * pd = personal_[wl.lang()->lang()];
250         if (!pd)
251                 return;
252         pd->remove(wl.word());
253 }
254
255
256 void HunspellChecker::Private::insert(WordLangTuple const & wl)
257 {
258         Hunspell * h = speller(wl.lang());
259         if (!h)
260                 return;
261         string const encoding = h->get_dic_encoding();
262         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
263         h->add(word_to_check.c_str());
264         PersonalWordList * pd = personal_[wl.lang()->lang()];
265         if (!pd)
266                 return;
267         pd->insert(wl.word());
268 }
269
270
271 bool HunspellChecker::Private::learned(WordLangTuple const & wl)
272 {
273         PersonalWordList * pd = personal_[wl.lang()->lang()];
274         if (!pd)
275                 return false;
276         return pd->exists(wl.word());
277 }
278
279
280 HunspellChecker::HunspellChecker(): d(new Private)
281 {
282 }
283
284
285 HunspellChecker::~HunspellChecker()
286 {
287         delete d;
288 }
289
290
291 SpellChecker::Result HunspellChecker::check(WordLangTuple const & wl)
292 {
293         if (d->isIgnored(wl))
294                 return WORD_OK;
295
296         Hunspell * h = d->speller(wl.lang());
297         if (!h)
298                 return WORD_OK;
299         int info;
300
301         string const encoding = h->get_dic_encoding();
302         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
303
304         if (h->spell(word_to_check.c_str(), &info))
305                 return d->learned(wl) ? LEARNED_WORD : WORD_OK;
306
307         if (info & SPELL_COMPOUND) {
308                 // FIXME: What to do with that?
309                 LYXERR(Debug::FILES, "Hunspell compound word found " << word_to_check);
310         }
311         if (info & SPELL_FORBIDDEN) {
312                 // This was removed from personal dictionary
313                 LYXERR(Debug::FILES, "Hunspell explicit forbidden word found " << word_to_check);
314         }
315
316         return UNKNOWN_WORD;
317 }
318
319
320 void HunspellChecker::advanceChangeNumber()
321 {
322         nextChangeNumber();
323 }
324
325
326 void HunspellChecker::insert(WordLangTuple const & wl)
327 {
328         d->insert(wl);
329         LYXERR(Debug::GUI, "learn word: \"" << wl.word() << "\"") ;
330         advanceChangeNumber();
331 }
332
333
334 void HunspellChecker::remove(WordLangTuple const & wl)
335 {
336         d->remove(wl);
337         LYXERR(Debug::GUI, "unlearn word: \"" << wl.word() << "\"") ;
338         advanceChangeNumber();
339 }
340
341
342 void HunspellChecker::accept(WordLangTuple const & wl)
343 {
344         d->ignored_.push_back(wl);
345         LYXERR(Debug::GUI, "ignore word: \"" << wl.word() << "\"") ;
346         advanceChangeNumber();
347 }
348
349
350 void HunspellChecker::suggest(WordLangTuple const & wl,
351         docstring_list & suggestions)
352 {
353         suggestions.clear();
354         Hunspell * h = d->speller(wl.lang());
355         if (!h)
356                 return;
357         string const encoding = h->get_dic_encoding();
358         string const word_to_check = to_iconv_encoding(wl.word(), encoding);
359         char ** suggestion_list;
360         int const suggestion_number = h->suggest(&suggestion_list, word_to_check.c_str());
361         if (suggestion_number <= 0)
362                 return;
363         for (int i = 0; i != suggestion_number; ++i)
364                 suggestions.push_back(from_iconv_encoding(suggestion_list[i], encoding));
365         h->free_list(&suggestion_list, suggestion_number);
366 }
367
368
369 bool HunspellChecker::hasDictionary(Language const * lang) const
370 {
371         if (!lang)
372                 return false;
373         return (d->haveDictionary(lang));
374 }
375
376
377 docstring const HunspellChecker::error()
378 {
379         return docstring();
380 }
381
382
383 } // namespace lyx