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