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