X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=src%2FHunspellChecker.cpp;h=01ec19498b70e28075db5e5b8a9c6bb8c4c77e49;hb=4ed0312c51704780af1c452d3a82a84171b3725a;hp=5d42421c2c6dc8bbb8c032195e3a6e38919fd70c;hpb=5c59e8c7cb3a804d403061e76a34c0378320b9b0;p=lyx.git diff --git a/src/HunspellChecker.cpp b/src/HunspellChecker.cpp index 5d42421c2c..01ec19498b 100644 --- a/src/HunspellChecker.cpp +++ b/src/HunspellChecker.cpp @@ -11,20 +11,18 @@ #include #include "HunspellChecker.h" +#include "PersonalWordList.h" #include "LyXRC.h" #include "WordLangTuple.h" -#include "frontends/alert.h" - #include "support/debug.h" #include "support/docstring_list.h" #include "support/filetools.h" +#include "support/Package.h" #include "support/FileName.h" -#include "support/gettext.h" #include "support/lassert.h" #include "support/lstrings.h" -#include "support/os.h" #include @@ -41,183 +39,380 @@ namespace lyx { namespace { typedef map Spellers; -typedef vector IgnoreList; +typedef map LangPersonalWordList; + +docstring remap_result(docstring const & s) +{ + // substitute RIGHT SINGLE QUOTATION MARK + // by APOSTROPHE + return subst(s, 0x2019, 0x0027); +} + +} // namespace -} // anon namespace struct HunspellChecker::Private { - Private() {} - + Private(); ~Private(); - bool haveDictionary(string const & lang) const; - Hunspell * addSpeller(string const & lang); - Hunspell * speller(string const & lang); + void cleanCache(); + void setUserPath(std::string const & path); + const string dictPath(int selector); + bool haveLanguageFiles(string const & hpath); + bool haveDictionary(Language const * lang, string & hpath); + bool haveDictionary(Language const * lang); + int numDictionaries() const; + Hunspell * addSpeller(Language const * lang, string & hpath); + Hunspell * addSpeller(Language const * lang); + Hunspell * speller(Language const * lang); + Hunspell * lookup(Language const * lang); /// ignored words bool isIgnored(WordLangTuple const & wl) const; - + /// personal word list interface + void remove(WordLangTuple const & wl); + void insert(WordLangTuple const & wl); + bool learned(WordLangTuple const & wl); /// the spellers Spellers spellers_; /// - IgnoreList ignored_; + WordLangTable ignored_; + /// + LangPersonalWordList personal_; + /// + std::string user_path_; + + /// the location below system/user directory + /// there the aff+dic files lookup will happen + const string dictDirectory(void) const { return "dicts"; } + int maxLookupSelector(void) const { return 5; } + const string HunspellDictionaryName(Language const * lang) { + return lang->variety().empty() + ? lang->code() + : lang->code() + "-" + lang->variety(); + } + const string myspellPackageDictDirectory(void) { + return "/usr/share/myspell"; + } + const string hunspellPackageDictDirectory(void) { + return "/usr/share/hunspell"; + } }; +HunspellChecker::Private::Private() +{ + setUserPath(lyxrc.hunspelldir_path); +} + + HunspellChecker::Private::~Private() +{ + cleanCache(); +} + + +void HunspellChecker::Private::setUserPath(std::string const & path) +{ + if (user_path_ != lyxrc.hunspelldir_path) { + cleanCache(); + user_path_ = path; + } +} + + +void HunspellChecker::Private::cleanCache() { Spellers::iterator it = spellers_.begin(); Spellers::iterator end = spellers_.end(); for (; it != end; ++it) { delete it->second; + it->second = nullptr; + } + + LangPersonalWordList::const_iterator pdit = personal_.begin(); + LangPersonalWordList::const_iterator pdet = personal_.end(); + + for (; pdit != pdet; ++pdit) { + if (pdit->second == nullptr) + continue; + PersonalWordList * pd = pdit->second; + pd->save(); + delete pd; } } -namespace { -bool haveLanguageFiles(string const & hpath) +bool HunspellChecker::Private::haveLanguageFiles(string const & hpath) { FileName const affix(hpath + ".aff"); FileName const dict(hpath + ".dic"); - if (!affix.isReadableFile()) { - // FIXME: We should indicate somehow that this language is not - // supported. - LYXERR(Debug::FILES, "Hunspell affix file " << affix << " does not exist"); - return false; + return affix.isReadableFile() && dict.isReadableFile(); +} + + +const string HunspellChecker::Private::dictPath(int selector) +{ + switch (selector) { + case 4: + return hunspellPackageDictDirectory(); + case 3: + return myspellPackageDictDirectory(); + case 2: + return addName(package().system_support().absFileName(),dictDirectory()); + case 1: + return addName(package().user_support().absFileName(),dictDirectory()); + default: + return user_path_; } - if (!dict.isReadableFile()) { - LYXERR(Debug::FILES, "Hunspell dictionary file " << dict << " does not exist"); +} + + +bool HunspellChecker::Private::haveDictionary(Language const * lang, string & hpath) +{ + if (hpath.empty() || !lang) return false; + + if (lookup(lang)) return true; + + string d_name = HunspellDictionaryName(lang); + + LYXERR(Debug::FILES, "check hunspell path: " << hpath + << " for language " << lang->lang() << " with name " << d_name); + + string h_path = addName(hpath, d_name); + // first we try lang code+variety + if (haveLanguageFiles(h_path)) { + LYXERR(Debug::FILES, " found " << h_path); + hpath = h_path; + return true; } + // another try with code, '_' replaced by '-' + h_path = addName(hpath, subst(lang->code(), '_', '-')); + if (!haveLanguageFiles(h_path)) + return false; + LYXERR(Debug::FILES, " found " << h_path); + hpath = h_path; return true; } -} -bool HunspellChecker::Private::haveDictionary(string const & lang) const +bool HunspellChecker::Private::haveDictionary(Language const * lang) { - string hunspell_path = lyxrc.hunspelldir_path; - LYXERR(Debug::FILES, "hunspell path: " << external_path(hunspell_path)); - if (hunspell_path.empty()) { - // FIXME We'd like to issue a better error message here, but there seems - // to be a problem about thread safety, or something of the sort. If - // we issue the message using frontend::Alert, then the code comes - // back through here while the box is waiting, and causes some kind - // of crash. - static bool warned = false; - if (!warned) { - warned = true; - LYXERR0("Hunspell path not set."); - //frontend::Alert::error(_("Hunspell Path Not Found"), - // _("You must set the Hunspell dictionary path in Tools>Preferences>Paths.")); - } - return false; - } + bool result = false; - hunspell_path = external_path(addName(hunspell_path, lang)); - if (!haveLanguageFiles(hunspell_path)) { - // try with '_' replaced by '-' - hunspell_path = subst(hunspell_path, '_', '-'); - if (!haveLanguageFiles(hunspell_path)) { - // FIXME: We should indicate somehow that this language is not - // supported, probably by popping a warning. But we'll need to - // remember which warnings we've issued. - return false; - } + setUserPath(lyxrc.hunspelldir_path); + for (int p = 0; !result && p < maxLookupSelector(); ++p) { + string lpath = dictPath(p); + result = haveDictionary(lang, lpath); } - return true; + return result; } -Hunspell * HunspellChecker::Private::addSpeller(string const & lang) +Hunspell * HunspellChecker::Private::speller(Language const * lang) { - string hunspell_path = lyxrc.hunspelldir_path; + Hunspell * h = lookup(lang); + if (h) return h; - if (!haveDictionary(lang)) - return 0; + setUserPath(lyxrc.hunspelldir_path); + return addSpeller(lang); +} - FileName const affix(hunspell_path + ".aff"); - FileName const dict(hunspell_path + ".dic"); - Hunspell * h = new Hunspell(affix.absFilename().c_str(), dict.absFilename().c_str()); - spellers_[lang] = h; + +Hunspell * HunspellChecker::Private::lookup(Language const * lang) +{ + Spellers::iterator it = spellers_.find(lang->lang()); + return it != spellers_.end() ? it->second : nullptr; +} + + +Hunspell * HunspellChecker::Private::addSpeller(Language const * lang, string & path) +{ + if (!haveDictionary(lang, path)) { + spellers_[lang->lang()] = nullptr; + return nullptr; + } + + FileName const affix(path + ".aff"); + FileName const dict(path + ".dic"); + Hunspell * h = new Hunspell(affix.absFileName().c_str(), dict.absFileName().c_str()); + LYXERR(Debug::FILES, "Hunspell speller for langage " << lang << " at " << dict << " added."); + spellers_[lang->lang()] = h; return h; } -Hunspell * HunspellChecker::Private::speller(string const & lang) +Hunspell * HunspellChecker::Private::addSpeller(Language const * lang) { - Spellers::iterator it = spellers_.find(lang); - if (it != spellers_.end()) - return it->second; - - return addSpeller(lang); + Hunspell * h = nullptr; + for (int p = 0; p < maxLookupSelector() && nullptr == h; ++p) { + string lpath = dictPath(p); + h = addSpeller(lang, lpath); + } + if (h) { + string const encoding = h->get_dic_encoding(); + PersonalWordList * pd = new PersonalWordList(lang->lang()); + pd->load(); + personal_[lang->lang()] = pd; + docstring_list::const_iterator it = pd->begin(); + docstring_list::const_iterator et = pd->end(); + for (; it != et; ++it) { + string const word_to_add = to_iconv_encoding(*it, encoding); + h->add(word_to_add.c_str()); + } + } + return h; +} + + +int HunspellChecker::Private::numDictionaries() const +{ + int result = 0; + Spellers::const_iterator it = spellers_.begin(); + Spellers::const_iterator et = spellers_.end(); + + for (; it != et; ++it) + result += it->second != nullptr; + return result; } bool HunspellChecker::Private::isIgnored(WordLangTuple const & wl) const { - IgnoreList::const_iterator it = ignored_.begin(); + WordLangTable::const_iterator it = ignored_.begin(); for (; it != ignored_.end(); ++it) { - if ((*it).lang()->code() != wl.lang()->code()) + if (it->lang()->code() != wl.lang()->code()) continue; - if ((*it).word() == wl.word()) + if (it->word() == wl.word()) return true; } return false; } +/// personal word list interface +void HunspellChecker::Private::remove(WordLangTuple const & wl) +{ + Hunspell * h = speller(wl.lang()); + if (!h) + return; + string const encoding = h->get_dic_encoding(); + string const word_to_check = to_iconv_encoding(wl.word(), encoding); + h->remove(word_to_check.c_str()); + PersonalWordList * pd = personal_[wl.lang()->lang()]; + if (!pd) + return; + pd->remove(wl.word()); +} + -HunspellChecker::HunspellChecker(): d(new Private) +void HunspellChecker::Private::insert(WordLangTuple const & wl) { + Hunspell * h = speller(wl.lang()); + if (!h) + return; + string const encoding = h->get_dic_encoding(); + string const word_to_check = to_iconv_encoding(wl.word(), encoding); + h->add(word_to_check.c_str()); + PersonalWordList * pd = personal_[wl.lang()->lang()]; + if (!pd) + return; + pd->insert(wl.word()); } +bool HunspellChecker::Private::learned(WordLangTuple const & wl) +{ + PersonalWordList * pd = personal_[wl.lang()->lang()]; + if (!pd) + return false; + return pd->exists(wl.word()); +} + + +HunspellChecker::HunspellChecker() + : d(new Private) +{} + + HunspellChecker::~HunspellChecker() { delete d; } -SpellChecker::Result HunspellChecker::check(WordLangTuple const & wl) +SpellChecker::Result HunspellChecker::check(WordLangTuple const & wl, + vector const & docdict) { if (d->isIgnored(wl)) - return OK; + return WORD_OK; - string const word_to_check = to_utf8(wl.word()); - Hunspell * h = d->speller(wl.lang()->code()); + WordLangTable::const_iterator it = docdict.begin(); + for (; it != docdict.end(); ++it) { + if (it->lang()->code() != wl.lang()->code()) + continue; + if (it->word() == wl.word()) + return DOCUMENT_LEARNED_WORD; + } + + Hunspell * h = d->speller(wl.lang()); if (!h) - return OK; + return NO_DICTIONARY; int info; + + string const encoding = h->get_dic_encoding(); + string const word_to_check = to_iconv_encoding(wl.word(), encoding); + + LYXERR(Debug::GUI, "spellCheck: \"" << + wl.word() << "\", lang = " << wl.lang()->lang()) ; +#ifdef HAVE_HUNSPELL_CXXABI + if (h->spell(word_to_check, &info)) +#else if (h->spell(word_to_check.c_str(), &info)) - return OK; +#endif + return d->learned(wl) ? LEARNED_WORD : WORD_OK; if (info & SPELL_COMPOUND) { // FIXME: What to do with that? - LYXERR(Debug::FILES, "Hunspell compound word found " << word_to_check); + LYXERR(Debug::GUI, "Hunspell compound word found " << word_to_check); } if (info & SPELL_FORBIDDEN) { - // FIXME: What to do with that? - LYXERR(Debug::FILES, "Hunspell explicit forbidden word found " << word_to_check); + // This was removed from personal dictionary + LYXERR(Debug::GUI, "Hunspell explicit forbidden word found " << word_to_check); } return UNKNOWN_WORD; } +void HunspellChecker::advanceChangeNumber() +{ + nextChangeNumber(); +} + + void HunspellChecker::insert(WordLangTuple const & wl) { - string const word_to_check = to_utf8(wl.word()); - Hunspell * h = d->speller(wl.lang()->code()); - if (!h) - return; - h->add(word_to_check.c_str()); + d->insert(wl); + LYXERR(Debug::GUI, "learn word: \"" << wl.word() << "\"") ; + advanceChangeNumber(); +} + + +void HunspellChecker::remove(WordLangTuple const & wl) +{ + d->remove(wl); + LYXERR(Debug::GUI, "unlearn word: \"" << wl.word() << "\"") ; + advanceChangeNumber(); } void HunspellChecker::accept(WordLangTuple const & wl) { d->ignored_.push_back(wl); + LYXERR(Debug::GUI, "ignore word: \"" << wl.word() << "\"") ; + advanceChangeNumber(); } @@ -225,17 +420,49 @@ void HunspellChecker::suggest(WordLangTuple const & wl, docstring_list & suggestions) { suggestions.clear(); - string const word_to_check = to_utf8(wl.word()); - Hunspell * h = d->speller(wl.lang()->code()); + Hunspell * h = d->speller(wl.lang()); if (!h) return; + string const encoding = h->get_dic_encoding(); + string const word_to_check = to_iconv_encoding(wl.word(), encoding); +#ifdef HAVE_HUNSPELL_CXXABI + vector wlst = h->suggest(word_to_check); + for (auto const & s : wlst) + suggestions.push_back(remap_result(from_iconv_encoding(s, encoding))); +#else char ** suggestion_list; int const suggestion_number = h->suggest(&suggestion_list, word_to_check.c_str()); if (suggestion_number <= 0) return; for (int i = 0; i != suggestion_number; ++i) - suggestions.push_back(from_utf8(suggestion_list[i])); + suggestions.push_back(remap_result(from_iconv_encoding(suggestion_list[i], encoding))); + h->free_list(&suggestion_list, suggestion_number); +#endif +} + + +void HunspellChecker::stem(WordLangTuple const & wl, + docstring_list & suggestions) +{ + suggestions.clear(); + Hunspell * h = d->speller(wl.lang()); + if (!h) + return; + string const encoding = h->get_dic_encoding(); + string const word_to_check = to_iconv_encoding(wl.word(), encoding); +#ifdef HAVE_HUNSPELL_CXXABI + vector wlst = h->stem(word_to_check); + for (auto const & s : wlst) + suggestions.push_back(from_iconv_encoding(s, encoding)); +#else + char ** suggestion_list; + int const suggestion_number = h->stem(&suggestion_list, word_to_check.c_str()); + if (suggestion_number <= 0) + return; + for (int i = 0; i != suggestion_number; ++i) + suggestions.push_back(from_iconv_encoding(suggestion_list[i], encoding)); h->free_list(&suggestion_list, suggestion_number); +#endif } @@ -243,7 +470,13 @@ bool HunspellChecker::hasDictionary(Language const * lang) const { if (!lang) return false; - return (d->haveDictionary(lang->code())); + return d->haveDictionary(lang); +} + + +int HunspellChecker::numDictionaries() const +{ + return d->numDictionaries(); }