]> git.lyx.org Git - lyx.git/blob - src/AspellChecker.cpp
* InsetTabular.cpp:
[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         /// create a unique ID from lang code and variety
65         string const spellerID(Language const * lang);
66
67         bool isValidDictionary(AspellConfig * config,
68                         string const & lang, string const & variety);
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         SpellChecker::Result check(AspellSpeller * m,
75                 string const & word) const;
76
77         void initSessionDictionary(Speller const & speller, PersonalWordList * pd);
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 "dict"; }
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::initSessionDictionary(
214         Speller const & speller,
215         PersonalWordList * pd)
216 {
217         AspellSpeller * aspell = to_aspell_speller(speller.e_speller);
218         aspell_speller_clear_session(aspell);
219         docstring_list::const_iterator it = pd->begin();
220         docstring_list::const_iterator et = pd->end();
221         for (; it != et; ++it) {
222                 string const word_to_add = to_utf8(*it);
223                 aspell_speller_add_to_session(aspell, word_to_add.c_str(), -1);
224         }
225         it = speller.ignored_words_.begin();
226         et = speller.ignored_words_.end();
227         for (; it != et; ++it) {
228                 string const word_to_add = to_utf8(*it);
229                 aspell_speller_add_to_session(aspell, word_to_add.c_str(), -1);
230         }
231 }
232
233
234 AspellSpeller * AspellChecker::Private::addSpeller(Language const * lang)
235 {
236         Speller m;
237         string const code = lang->code();
238         string const variety = lang->variety();
239         m.config = getConfig(code, variety);
240         // Aspell supports both languages and varieties (such as German
241         // old vs. new spelling). The respective naming convention is
242         // lang_REGION-variety (e.g. de_DE-alt).
243         aspell_config_replace(m.config, "lang", code.c_str());
244         if (!variety.empty())
245                 aspell_config_replace(m.config, "variety", variety.c_str());
246         // Set the encoding to utf-8.
247         // aspell does also understand "ucs-4", so we would not need a
248         // conversion in theory, but if this is used it expects all
249         // char const * arguments to be a cast from  uint const *, and it
250         // seems that this uint is not compatible with our char_type on some
251         // platforms (cygwin, OS X). Therefore we use utf-8, that does
252         // always work.
253         aspell_config_replace(m.config, "encoding", "utf-8");
254         if (lyxrc.spellchecker_accept_compound)
255                 // Consider run-together words as legal compounds
256                 aspell_config_replace(m.config, "run-together", "true");
257         else
258                 // Report run-together words as errors
259                 aspell_config_replace(m.config, "run-together", "false");
260
261         m.e_speller = new_aspell_speller(m.config);
262         if (aspell_error_number(m.e_speller) != 0) {
263                 // FIXME: We should indicate somehow that this language is not supported.
264                 LYXERR(Debug::FILES, "aspell error: " << aspell_error_message(m.e_speller));
265                 delete_aspell_can_have_error(m.e_speller);
266                 delete_aspell_config(m.config);
267                 m.config = 0;
268                 m.e_speller = 0;
269         } else {
270                 PersonalWordList * pd = new PersonalWordList(lang->lang());
271                 pd->load();
272                 personal_[lang->lang()] = pd;
273                 initSessionDictionary(m, pd);
274         }
275         
276         spellers_[spellerID(lang)] = m;
277         return m.e_speller ? to_aspell_speller(m.e_speller) : 0;
278 }
279
280
281 AspellSpeller * AspellChecker::Private::speller(Language const * lang)
282 {
283         Spellers::iterator it = spellers_.find(spellerID(lang));
284         if (it != spellers_.end())
285                 return to_aspell_speller(it->second.e_speller);
286         
287         return addSpeller(lang);
288 }
289
290
291 string const AspellChecker::Private::spellerID(Language const * lang)
292 {
293         return lang->code() + "-" + lang->variety();
294 }
295
296
297 SpellChecker::Result AspellChecker::Private::check(
298         AspellSpeller * m, string const & word) 
299         const
300 {
301         int const word_ok = aspell_speller_check(m, word.c_str(), -1);
302         LASSERT(word_ok != -1, /**/);
303         return (word_ok) ? WORD_OK : UNKNOWN_WORD;
304 }
305
306 void AspellChecker::Private::accept(Speller & speller, WordLangTuple const & word)
307 {
308         speller.ignored_words_.push_back(word.word());
309 }
310
311
312 /// personal word list interface
313 void AspellChecker::Private::remove(WordLangTuple const & word)
314 {
315         PersonalWordList * pd = personal_[word.lang()->lang()];
316         if (!pd)
317                 return;
318         pd->remove(word.word());
319         Spellers::iterator it = spellers_.find(spellerID(word.lang()));
320         if (it != spellers_.end()) {
321                 initSessionDictionary(it->second, pd);
322         }
323 }
324
325                 
326 void AspellChecker::Private::insert(WordLangTuple const & word)
327 {
328         Spellers::iterator it = spellers_.find(spellerID(word.lang()));
329         if (it != spellers_.end()) {
330                 AspellSpeller * speller = to_aspell_speller(it->second.e_speller);
331                 aspell_speller_add_to_session(speller, to_utf8(word.word()).c_str(), -1);
332                 PersonalWordList * pd = personal_[word.lang()->lang()];
333                 if (!pd)
334                         return;
335                 pd->insert(word.word());
336         }
337 }
338
339 bool AspellChecker::Private::learned(WordLangTuple const & word)
340 {
341         PersonalWordList * pd = personal_[word.lang()->lang()];
342         if (!pd)
343                 return false;
344         return pd->exists(word.word());
345 }
346
347
348 AspellChecker::AspellChecker(): d(new Private)
349 {
350 }
351
352
353 AspellChecker::~AspellChecker()
354 {
355         delete d;
356 }
357
358
359 SpellChecker::Result AspellChecker::check(WordLangTuple const & word)
360 {
361   
362         AspellSpeller * m = d->speller(word.lang());
363
364         if (!m)
365                 return WORD_OK;
366
367         if (word.word().empty())
368                 // MSVC compiled Aspell doesn't like it.
369                 return WORD_OK;
370
371         string const word_str = to_utf8(word.word());
372         SpellChecker::Result rc = d->check(m, word_str);
373         return (rc == WORD_OK && d->learned(word)) ? LEARNED_WORD : rc;
374 }
375
376
377 void AspellChecker::advanceChangeNumber()
378 {
379         nextChangeNumber();
380 }
381
382
383 void AspellChecker::insert(WordLangTuple const & word)
384 {
385         d->insert(word);
386         advanceChangeNumber();
387 }
388
389
390 void AspellChecker::accept(WordLangTuple const & word)
391 {
392         Spellers::iterator it = d->spellers_.find(d->spellerID(word.lang()));
393         if (it != d->spellers_.end()) {
394                 AspellSpeller * speller = to_aspell_speller(it->second.e_speller);
395                 aspell_speller_add_to_session(speller, to_utf8(word.word()).c_str(), -1);
396                 d->accept(it->second, word);
397                 advanceChangeNumber();
398         }
399 }
400
401
402 void AspellChecker::suggest(WordLangTuple const & wl,
403         docstring_list & suggestions)
404 {
405         suggestions.clear();
406         AspellSpeller * m = d->speller(wl.lang());
407
408         if (!m)
409                 return;
410
411         AspellWordList const * sugs =
412                 aspell_speller_suggest(m, to_utf8(wl.word()).c_str(), -1);
413         LASSERT(sugs != 0, /**/);
414         AspellStringEnumeration * els = aspell_word_list_elements(sugs);
415         if (!els || aspell_word_list_empty(sugs))
416                 return;
417
418         for (;;) {
419                 char const * str = aspell_string_enumeration_next(els);
420                 if (!str)
421                         break;
422                 suggestions.push_back(from_utf8(str));
423         }
424
425         delete_aspell_string_enumeration(els);
426 }
427
428 void AspellChecker::remove(WordLangTuple const & word)
429 {
430         d->remove(word);
431         advanceChangeNumber();
432 }
433
434 bool AspellChecker::hasDictionary(Language const * lang) const
435 {
436         bool have = false;
437         Spellers::iterator it = d->spellers_.begin();
438         Spellers::iterator end = d->spellers_.end();
439
440         if (lang) {
441                 for (; it != end && !have; ++it) {
442                         have = it->second.config && d->isValidDictionary(it->second.config, lang->code(), lang->variety());
443                 }
444                 if (!have) {
445                         AspellConfig * config = d->getConfig(lang->code(), lang->variety());
446                         have = d->isValidDictionary(config, lang->code(), lang->variety());
447                         delete_aspell_config(config);
448                 }
449         }
450         return have;
451 }
452
453
454 docstring const AspellChecker::error()
455 {
456         Spellers::iterator it = d->spellers_.begin();
457         Spellers::iterator end = d->spellers_.end();
458         char const * err = 0;
459
460         for (; it != end && 0 == err; ++it) {
461                 if (it->second.e_speller && aspell_error_number(it->second.e_speller) != 0)
462                         err = aspell_error_message(it->second.e_speller);
463         }
464
465         // FIXME UNICODE: err is not in UTF8, but probably the locale encoding
466         return (err ? from_utf8(err) : docstring());
467 }
468
469
470 } // namespace lyx