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