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