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