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