]> git.lyx.org Git - lyx.git/blob - src/AspellChecker.cpp
listerrors.lyx : Update a link.
[lyx.git] / src / AspellChecker.cpp
1 /**
2  * \file AspellChecker.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Kevin Atkinson
7  * \author John Levon
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11
12 #include <config.h>
13
14 #include "AspellChecker.h"
15 #include "PersonalWordList.h"
16
17 #include "LyXRC.h"
18 #include "WordLangTuple.h"
19
20 #include "support/lassert.h"
21 #include "support/debug.h"
22 #include "support/lstrings.h"
23 #include "support/docstring_list.h"
24
25 #include "support/filetools.h"
26 #include "support/Package.h"
27 #include "support/FileName.h"
28 #include "support/Path.h"
29
30 #include <aspell.h>
31
32 #include <map>
33 #include <string>
34
35 using namespace std;
36 using namespace lyx::support;
37
38 namespace lyx {
39
40 namespace {
41
42 struct Speller {
43         AspellConfig * config;
44         AspellCanHaveError * e_speller;
45         bool accept_compound;
46         docstring_list ignored_words_;
47 };
48
49 typedef std::map<std::string, Speller> Spellers;
50 typedef map<std::string, PersonalWordList *> LangPersonalWordList;
51
52 } // anon namespace
53
54 struct AspellChecker::Private
55 {
56         Private() {}
57
58         ~Private();
59
60         /// add a speller of the given language and variety
61         AspellSpeller * addSpeller(Language const * lang);
62
63         ///
64         AspellSpeller * speller(Language const * lang);
65
66         bool isValidDictionary(AspellConfig * config,
67                         string const & lang, string const & variety);
68         bool checkAspellData(AspellConfig * config,
69                 string const & basepath, string const & datapath, string const & dictpath,
70                 string const & lang, string const & variety);
71         AspellConfig * getConfig(string const & lang, string const & variety);
72
73         string toAspellWord(docstring const & word) const;
74
75         SpellChecker::Result check(AspellSpeller * m,
76                 WordLangTuple const & word) const;
77
78         void initSessionDictionary(Speller const & speller, PersonalWordList * pd);
79         void addToSession(AspellCanHaveError * speller, docstring const & word);
80         void insert(WordLangTuple const & word);
81         void remove(WordLangTuple const & word);
82         bool learned(WordLangTuple const & word);
83
84         void accept(Speller & speller, WordLangTuple const & word);
85
86         /// the spellers
87         Spellers spellers_;
88
89         LangPersonalWordList personal_;
90
91         /// the location below system/user directory
92         /// there the rws files lookup will happen
93         const string dictDirectory(void) { return "dicts"; }
94         /// there the dat+cmap files lookup will happen
95         const string dataDirectory(void) { return "data"; }
96         /// os package directory constants
97         /// macports on Mac OS X or
98         /// aspell rpms on Linux
99         const string osPackageBase(void) {
100 #ifdef USE_MACOSX_PACKAGING
101                 return "/opt/local";
102 #else
103                 return "/usr";
104 #endif
105         }
106         const string osPackageDictDirectory(void) {
107 #ifdef USE_MACOSX_PACKAGING
108                 return "/share/aspell";
109 #else
110                 return "/lib/aspell-0.60";
111 #endif
112         }
113         const string osPackageDataDirectory(void) { return "/lib/aspell-0.60"; }
114
115 };
116
117
118 AspellChecker::Private::~Private()
119 {
120         Spellers::iterator it = spellers_.begin();
121         Spellers::iterator end = spellers_.end();
122
123         for (; it != end; ++it) {
124                 if (it->second.e_speller) {
125                         AspellSpeller * speller = to_aspell_speller(it->second.e_speller);
126                         aspell_speller_save_all_word_lists(speller);
127                         delete_aspell_can_have_error(it->second.e_speller);
128                 }
129                 delete_aspell_config(it->second.config);
130         }
131
132         LangPersonalWordList::const_iterator pdit = personal_.begin();
133         LangPersonalWordList::const_iterator pdet = personal_.end();
134         
135         for (; pdit != pdet; ++pdit) {
136                 if ( 0 == pdit->second)
137                         continue;
138                 PersonalWordList * pd = pdit->second;
139                 pd->save();
140                 delete pd;
141         }
142 }
143
144
145 bool AspellChecker::Private::isValidDictionary(AspellConfig * config,
146                 string const & lang, string const & variety)
147 {
148         bool have = false;
149         // code taken from aspell's list-dicts example
150         // the returned pointer should _not_ need to be deleted
151         AspellDictInfoList * dlist = get_aspell_dict_info_list(config);
152         AspellDictInfoEnumeration * dels = aspell_dict_info_list_elements(dlist);
153         const AspellDictInfo * entry;
154
155         while (0 != (entry = aspell_dict_info_enumeration_next(dels))) {
156                 LYXERR(Debug::DEBUG, "aspell dict:"
157                         << " name="    << entry->name
158                         << ",code="    << entry->code
159                         << ",variety=" << entry->jargon);
160                 if (entry->code == lang && (variety.empty() || entry->jargon == variety)) {
161                         have = true;
162                         break;
163                 }
164         }
165         delete_aspell_dict_info_enumeration(dels);
166         LYXERR(Debug::FILES, "aspell dictionary: " << lang << (have ? " yes" : " no"));
167         return have;
168 }
169
170
171 bool AspellChecker::Private::checkAspellData(AspellConfig * config,
172         string const & basepath, string const & datapath, string const & dictpath,
173         string const & lang, string const & variety)
174 {
175         FileName base(basepath);
176         bool have_dict = base.isDirectory() ;
177
178         if (have_dict) {
179                 FileName data(addPath(base.absFileName(), datapath));
180                 FileName dict(addPath(base.absFileName(), dictpath));
181                 have_dict = dict.isDirectory() && data.isDirectory();
182                 if (have_dict) {
183                         LYXERR(Debug::FILES, "aspell dict-dir: " << dict);
184                         LYXERR(Debug::FILES, "aspell data-dir: " << data);
185                         aspell_config_replace(config, "dict-dir", dict.absFileName().c_str());
186                         aspell_config_replace(config, "data-dir", data.absFileName().c_str());
187                         have_dict = isValidDictionary(config, lang, variety);
188                 }
189         }
190         return have_dict ;
191 }
192
193
194 AspellConfig * AspellChecker::Private::getConfig(string const & lang, string const & variety)
195 {
196         AspellConfig * config = new_aspell_config();
197         bool have_dict = false;
198         string const sysdir = lyx::support::package().system_support().absFileName() ;
199         string const userdir = lyx::support::package().user_support().absFileName() ;
200
201         LYXERR(Debug::FILES, "aspell user dir: " << userdir);
202         have_dict = checkAspellData(config, userdir, dataDirectory(), dictDirectory(), lang, variety);
203         if (!have_dict) {
204                 LYXERR(Debug::FILES, "aspell sysdir dir: " << sysdir);
205                 have_dict = checkAspellData(config, sysdir, dataDirectory(), dictDirectory(), lang, variety);
206         }
207         if (!have_dict) {
208                 // check for package data of OS installation
209                 have_dict = checkAspellData(config, osPackageBase(), osPackageDataDirectory(), osPackageDictDirectory(), lang, variety);
210         }
211         return config ;
212 }
213
214
215 void AspellChecker::Private::addToSession(AspellCanHaveError * speller, docstring const & word)
216 {
217         string const word_to_add = toAspellWord(word);
218         if(1 != aspell_speller_add_to_session(to_aspell_speller(speller), word_to_add.c_str(), -1))
219                 LYXERR(Debug::GUI, "aspell add to session: " << aspell_error_message(speller));
220 }
221
222
223 void AspellChecker::Private::initSessionDictionary(
224         Speller const & speller,
225         PersonalWordList * pd)
226 {
227         AspellSpeller * aspell = to_aspell_speller(speller.e_speller);
228         aspell_speller_clear_session(aspell);
229         docstring_list::const_iterator it = pd->begin();
230         docstring_list::const_iterator et = pd->end();
231         for (; it != et; ++it) {
232                 addToSession(speller.e_speller, *it);
233         }
234         it = speller.ignored_words_.begin();
235         et = speller.ignored_words_.end();
236         for (; it != et; ++it) {
237                 addToSession(speller.e_speller, *it);
238         }
239 }
240
241
242 AspellSpeller * AspellChecker::Private::addSpeller(Language const * lang)
243 {
244         Speller m;
245         string const code = lang->code();
246         string const variety = lang->variety();
247         m.config = getConfig(code, variety);
248         // Aspell supports both languages and varieties (such as German
249         // old vs. new spelling). The respective naming convention is
250         // lang_REGION-variety (e.g. de_DE-alt).
251         aspell_config_replace(m.config, "lang", code.c_str());
252         if (!variety.empty())
253                 aspell_config_replace(m.config, "variety", variety.c_str());
254         // Set the encoding to utf-8.
255         // aspell does also understand "ucs-4", so we would not need a
256         // conversion in theory, but if this is used it expects all
257         // char const * arguments to be a cast from  uint const *, and it
258         // seems that this uint is not compatible with our char_type on some
259         // platforms (cygwin, OS X). Therefore we use utf-8, that does
260         // always work.
261         aspell_config_replace(m.config, "encoding", "utf-8");
262         if (lyxrc.spellchecker_accept_compound)
263                 // Consider run-together words as legal compounds
264                 aspell_config_replace(m.config, "run-together", "true");
265         else
266                 // Report run-together words as errors
267                 aspell_config_replace(m.config, "run-together", "false");
268
269         m.accept_compound = lyxrc.spellchecker_accept_compound;
270         m.e_speller = new_aspell_speller(m.config);
271         if (aspell_error_number(m.e_speller) != 0) {
272                 // FIXME: We should indicate somehow that this language is not supported.
273                 LYXERR(Debug::FILES, "aspell error: " << aspell_error_message(m.e_speller));
274                 delete_aspell_can_have_error(m.e_speller);
275                 delete_aspell_config(m.config);
276                 m.config = 0;
277                 m.e_speller = 0;
278         } else {
279                 PersonalWordList * pd = new PersonalWordList(lang->lang());
280                 pd->load();
281                 personal_[lang->lang()] = pd;
282                 initSessionDictionary(m, pd);
283         }
284         
285         spellers_[lang->lang()] = m;
286         return m.e_speller ? to_aspell_speller(m.e_speller) : 0;
287 }
288
289
290 AspellSpeller * AspellChecker::Private::speller(Language const * lang)
291 {
292         Spellers::iterator it = spellers_.find(lang->lang());
293         if (it != spellers_.end()) {
294                 Speller aspell = it->second;
295                 if (lyxrc.spellchecker_accept_compound != aspell.accept_compound) {
296                         // spell checker setting changed... adjust run-together
297                         aspell.accept_compound = lyxrc.spellchecker_accept_compound;
298                         if (aspell.accept_compound)
299                                 // Consider run-together words as legal compounds
300                                 aspell_config_replace(aspell.config, "run-together", "true");
301                         else
302                                 // Report run-together words as errors
303                                 aspell_config_replace(aspell.config, "run-together", "false");
304                         AspellCanHaveError * e_speller = aspell.e_speller;
305                         aspell.e_speller = new_aspell_speller(aspell.config);
306                         delete_aspell_speller(to_aspell_speller(e_speller));
307                         spellers_[lang->lang()] = aspell;
308                 }
309                 return to_aspell_speller(aspell.e_speller);
310         }
311
312         return addSpeller(lang);
313 }
314
315
316 string AspellChecker::Private::toAspellWord(docstring const & word) const
317 {
318         size_t mpos;
319         string word_str = to_utf8(word);
320         while ((mpos = word_str.find('-')) != word_str.npos) {
321                 word_str.erase(mpos, 1);
322         }
323         return word_str;
324 }
325
326
327 SpellChecker::Result AspellChecker::Private::check(
328         AspellSpeller * m, WordLangTuple const & word) 
329         const
330 {
331         SpellChecker::Result result = WORD_OK;
332         docstring w1;
333         docstring rest = split(word.word(), w1, '-');
334         for (; result == WORD_OK;) {
335                 string const word_str = toAspellWord(w1);
336                 int const word_ok = aspell_speller_check(m, word_str.c_str(), -1);
337                 LASSERT(word_ok != -1, /**/);
338                 result = (word_ok) ? WORD_OK : UNKNOWN_WORD;
339                 if (rest.empty())
340                         break;
341                 rest = split(rest,w1,'-');
342         }
343         if (result == WORD_OK)
344                 return result;
345         string const word_str = toAspellWord(word.word());
346         int const word_ok = aspell_speller_check(m, word_str.c_str(), -1);
347         LASSERT(word_ok != -1, /**/);
348         return (word_ok) ? WORD_OK : UNKNOWN_WORD;
349 }
350
351 void AspellChecker::Private::accept(Speller & speller, WordLangTuple const & word)
352 {
353         speller.ignored_words_.push_back(word.word());
354 }
355
356
357 /// personal word list interface
358 void AspellChecker::Private::remove(WordLangTuple const & word)
359 {
360         PersonalWordList * pd = personal_[word.lang()->lang()];
361         if (!pd)
362                 return;
363         pd->remove(word.word());
364         Spellers::iterator it = spellers_.find(word.lang()->lang());
365         if (it != spellers_.end()) {
366                 initSessionDictionary(it->second, pd);
367         }
368 }
369
370                 
371 void AspellChecker::Private::insert(WordLangTuple const & word)
372 {
373         Spellers::iterator it = spellers_.find(word.lang()->lang());
374         if (it != spellers_.end()) {
375                 addToSession(it->second.e_speller, word.word());
376                 PersonalWordList * pd = personal_[word.lang()->lang()];
377                 if (!pd)
378                         return;
379                 pd->insert(word.word());
380         }
381 }
382
383 bool AspellChecker::Private::learned(WordLangTuple const & word)
384 {
385         PersonalWordList * pd = personal_[word.lang()->lang()];
386         if (!pd)
387                 return false;
388         return pd->exists(word.word());
389 }
390
391
392 AspellChecker::AspellChecker(): d(new Private)
393 {
394 }
395
396
397 AspellChecker::~AspellChecker()
398 {
399         delete d;
400 }
401
402
403 SpellChecker::Result AspellChecker::check(WordLangTuple const & word)
404 {
405   
406         AspellSpeller * m = d->speller(word.lang());
407
408         if (!m)
409                 return WORD_OK;
410
411         if (word.word().empty())
412                 // MSVC compiled Aspell doesn't like it.
413                 return WORD_OK;
414
415         SpellChecker::Result rc = d->check(m, word);
416         return (rc == WORD_OK && d->learned(word)) ? LEARNED_WORD : rc;
417 }
418
419
420 void AspellChecker::advanceChangeNumber()
421 {
422         nextChangeNumber();
423 }
424
425
426 void AspellChecker::insert(WordLangTuple const & word)
427 {
428         d->insert(word);
429         advanceChangeNumber();
430 }
431
432
433 void AspellChecker::accept(WordLangTuple const & word)
434 {
435         Spellers::iterator it = d->spellers_.find(word.lang()->lang());
436         if (it != d->spellers_.end()) {
437                 d->addToSession(it->second.e_speller, word.word());
438                 d->accept(it->second, word);
439                 advanceChangeNumber();
440         }
441 }
442
443
444 void AspellChecker::suggest(WordLangTuple const & wl,
445         docstring_list & suggestions)
446 {
447         suggestions.clear();
448         AspellSpeller * m = d->speller(wl.lang());
449
450         if (!m)
451                 return;
452
453         string const word = d->toAspellWord(wl.word());
454         AspellWordList const * sugs =
455                 aspell_speller_suggest(m, word.c_str(), -1);
456         LASSERT(sugs != 0, /**/);
457         AspellStringEnumeration * els = aspell_word_list_elements(sugs);
458         if (!els || aspell_word_list_empty(sugs))
459                 return;
460
461         for (;;) {
462                 char const * str = aspell_string_enumeration_next(els);
463                 if (!str)
464                         break;
465                 suggestions.push_back(from_utf8(str));
466         }
467
468         delete_aspell_string_enumeration(els);
469 }
470
471 void AspellChecker::remove(WordLangTuple const & word)
472 {
473         d->remove(word);
474         advanceChangeNumber();
475 }
476
477 bool AspellChecker::hasDictionary(Language const * lang) const
478 {
479         bool have = false;
480         Spellers::iterator it = d->spellers_.begin();
481         Spellers::iterator end = d->spellers_.end();
482
483         if (lang) {
484                 for (; it != end && !have; ++it) {
485                         have = it->second.config && d->isValidDictionary(it->second.config, lang->code(), lang->variety());
486                 }
487                 if (!have) {
488                         AspellConfig * config = d->getConfig(lang->code(), lang->variety());
489                         have = d->isValidDictionary(config, lang->code(), lang->variety());
490                         delete_aspell_config(config);
491                 }
492         }
493         return have;
494 }
495
496
497 docstring const AspellChecker::error()
498 {
499         Spellers::iterator it = d->spellers_.begin();
500         Spellers::iterator end = d->spellers_.end();
501         char const * err = 0;
502
503         for (; it != end && 0 == err; ++it) {
504                 if (it->second.e_speller && aspell_error_number(it->second.e_speller) != 0)
505                         err = aspell_error_message(it->second.e_speller);
506         }
507
508         // FIXME UNICODE: err is not in UTF8, but probably the locale encoding
509         return (err ? from_utf8(err) : docstring());
510 }
511
512
513 } // namespace lyx