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