2 * \file AspellChecker.cpp
3 * This file is part of LyX, the document processor.
4 * Licence details can be found in the file COPYING.
6 * \author Kevin Atkinson
9 * Full author contact details are available in file CREDITS.
14 #include "AspellChecker.h"
15 #include "PersonalWordList.h"
18 #include "WordLangTuple.h"
20 #include "support/lassert.h"
21 #include "support/debug.h"
22 #include "support/docstring_list.h"
24 #include "support/filetools.h"
25 #include "support/Package.h"
26 #include "support/FileName.h"
27 #include "support/Path.h"
35 using namespace lyx::support;
42 AspellConfig * config;
43 AspellCanHaveError * e_speller;
44 docstring_list ignored_words_;
47 typedef std::map<std::string, Speller> Spellers;
48 typedef map<std::string, PersonalWordList *> LangPersonalWordList;
52 struct AspellChecker::Private
58 /// add a speller of the given language and variety
59 AspellSpeller * addSpeller(Language const * lang);
62 AspellSpeller * speller(Language const * lang);
64 /// create a unique ID from lang code and variety
65 string const spellerID(Language const * lang);
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);
74 SpellChecker::Result check(AspellSpeller * m,
75 string const & word) const;
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);
82 void accept(Speller & speller, WordLangTuple const & word);
87 LangPersonalWordList personal_;
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
104 const string osPackageDictDirectory(void) {
105 #ifdef USE_MACOSX_PACKAGING
106 return "/share/aspell";
108 return "/lib/aspell-0.60";
111 const string osPackageDataDirectory(void) { return "/lib/aspell-0.60"; }
116 AspellChecker::Private::~Private()
118 Spellers::iterator it = spellers_.begin();
119 Spellers::iterator end = spellers_.end();
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);
127 delete_aspell_config(it->second.config);
130 LangPersonalWordList::const_iterator pdit = personal_.begin();
131 LangPersonalWordList::const_iterator pdet = personal_.end();
133 for (; pdit != pdet; ++pdit) {
134 if ( 0 == pdit->second)
136 PersonalWordList * pd = pdit->second;
143 bool AspellChecker::Private::isValidDictionary(AspellConfig * config,
144 string const & lang, string const & variety)
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;
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)) {
163 delete_aspell_dict_info_enumeration(dels);
164 LYXERR(Debug::FILES, "aspell dictionary: " << lang << (have ? " yes" : " no"));
169 bool AspellChecker::Private::checkAspellData(AspellConfig * config,
170 string const & basepath, string const & datapath, string const & dictpath,
171 string const & lang, string const & variety)
173 FileName base(basepath);
174 bool have_dict = base.isDirectory() ;
177 FileName data(addPath(base.absFileName(), datapath));
178 FileName dict(addPath(base.absFileName(), dictpath));
179 have_dict = dict.isDirectory() && data.isDirectory();
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);
192 AspellConfig * AspellChecker::Private::getConfig(string const & lang, string const & variety)
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() ;
199 LYXERR(Debug::FILES, "aspell user dir: " << userdir);
200 have_dict = checkAspellData(config, userdir, dataDirectory(), dictDirectory(), lang, variety);
202 LYXERR(Debug::FILES, "aspell sysdir dir: " << sysdir);
203 have_dict = checkAspellData(config, sysdir, dataDirectory(), dictDirectory(), lang, variety);
206 // check for package data of OS installation
207 have_dict = checkAspellData(config, osPackageBase(), osPackageDataDirectory(), osPackageDictDirectory(), lang, variety);
213 void AspellChecker::Private::initSessionDictionary(
214 Speller const & speller,
215 PersonalWordList * pd)
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);
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);
234 AspellSpeller * AspellChecker::Private::addSpeller(Language const * lang)
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
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");
258 // Report run-together words as errors
259 aspell_config_replace(m.config, "run-together", "false");
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);
270 PersonalWordList * pd = new PersonalWordList(lang->lang());
272 personal_[lang->lang()] = pd;
273 initSessionDictionary(m, pd);
276 spellers_[spellerID(lang)] = m;
277 return m.e_speller ? to_aspell_speller(m.e_speller) : 0;
281 AspellSpeller * AspellChecker::Private::speller(Language const * lang)
283 Spellers::iterator it = spellers_.find(spellerID(lang));
284 if (it != spellers_.end())
285 return to_aspell_speller(it->second.e_speller);
287 return addSpeller(lang);
291 string const AspellChecker::Private::spellerID(Language const * lang)
293 return lang->code() + "-" + lang->variety();
297 SpellChecker::Result AspellChecker::Private::check(
298 AspellSpeller * m, string const & word)
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;
306 void AspellChecker::Private::accept(Speller & speller, WordLangTuple const & word)
308 speller.ignored_words_.push_back(word.word());
312 /// personal word list interface
313 void AspellChecker::Private::remove(WordLangTuple const & word)
315 PersonalWordList * pd = personal_[word.lang()->lang()];
318 pd->remove(word.word());
319 Spellers::iterator it = spellers_.find(spellerID(word.lang()));
320 if (it != spellers_.end()) {
321 initSessionDictionary(it->second, pd);
326 void AspellChecker::Private::insert(WordLangTuple const & word)
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()];
335 pd->insert(word.word());
339 bool AspellChecker::Private::learned(WordLangTuple const & word)
341 PersonalWordList * pd = personal_[word.lang()->lang()];
344 return pd->exists(word.word());
348 AspellChecker::AspellChecker(): d(new Private)
353 AspellChecker::~AspellChecker()
359 SpellChecker::Result AspellChecker::check(WordLangTuple const & word)
362 AspellSpeller * m = d->speller(word.lang());
367 if (word.word().empty())
368 // MSVC compiled Aspell doesn't like it.
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;
377 void AspellChecker::advanceChangeNumber()
383 void AspellChecker::insert(WordLangTuple const & word)
386 advanceChangeNumber();
390 void AspellChecker::accept(WordLangTuple const & word)
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();
402 void AspellChecker::suggest(WordLangTuple const & wl,
403 docstring_list & suggestions)
406 AspellSpeller * m = d->speller(wl.lang());
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))
419 char const * str = aspell_string_enumeration_next(els);
422 suggestions.push_back(from_utf8(str));
425 delete_aspell_string_enumeration(els);
428 void AspellChecker::remove(WordLangTuple const & word)
431 advanceChangeNumber();
434 bool AspellChecker::hasDictionary(Language const * lang) const
437 Spellers::iterator it = d->spellers_.begin();
438 Spellers::iterator end = d->spellers_.end();
441 for (; it != end && !have; ++it) {
442 have = it->second.config && d->isValidDictionary(it->second.config, lang->code(), lang->variety());
445 AspellConfig * config = d->getConfig(lang->code(), lang->variety());
446 have = d->isValidDictionary(config, lang->code(), lang->variety());
447 delete_aspell_config(config);
454 docstring const AspellChecker::error()
456 Spellers::iterator it = d->spellers_.begin();
457 Spellers::iterator end = d->spellers_.end();
458 char const * err = 0;
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);
465 // FIXME UNICODE: err is not in UTF8, but probably the locale encoding
466 return (err ? from_utf8(err) : docstring());