// This file is part of Desktop App Toolkit, // a set of libraries for developing nice desktop applications. // // Author: Nicholas Guriev , public domain, 2019 // License: CC0, https://creativecommons.org/publicdomain/zero/1.0/legalcode #include #include #include "spellcheck/platform/linux/linux_enchant.h" #include "spellcheck/platform/linux/spellcheck_linux.h" #include "base/debug_log.h" namespace Platform::Spellchecker { namespace { constexpr auto kHspell = "hspell"; constexpr auto kMySpell = "myspell"; constexpr auto kHunspell = "hunspell"; constexpr auto kOrdering = "hspell,aspell,hunspell,myspell"; constexpr auto kMaxValidators = 10; constexpr auto kMaxMySpellCount = 3; constexpr auto kMaxWordLength = 15; using DictPtr = std::unique_ptr; auto CheckProvider(DictPtr &validator, const std::string &provider) { auto p = validator->get_provider_name(); std::transform(begin(p), end(p), begin(p), ::tolower); return (p.find(provider) == 0); // startsWith. } auto IsHebrew(const QString &word) { // Words with mixed scripts will be automatically ignored, // so this check should be fine. return ::Spellchecker::WordScript(word) == QChar::Script_Hebrew; } class EnchantSpellChecker { public: auto knownLanguages(); bool checkSpelling(const QString &word); auto findSuggestions(const QString &word); void addWord(const QString &wordToAdd); void ignoreWord(const QString &word); void removeWord(const QString &word); bool isWordInDictionary(const QString &word); static EnchantSpellChecker *instance(); private: EnchantSpellChecker(); EnchantSpellChecker(const EnchantSpellChecker&) = delete; EnchantSpellChecker& operator =(const EnchantSpellChecker&) = delete; std::unique_ptr _brokerHandle; std::vector _validators; std::vector> _hspells; }; EnchantSpellChecker::EnchantSpellChecker() { if (!enchant::loader::do_explicit_linking()) return; std::set langs; _brokerHandle = std::make_unique(); _brokerHandle->list_dicts([]( const char *language, const char *provider, const char *description, const char *filename, void *our_payload) { static_cast(our_payload)->insert(language); }, &langs); _validators.reserve(langs.size()); try { std::string langTag = QLocale::system().name().toStdString(); _brokerHandle->set_ordering(langTag, kOrdering); _validators.push_back(DictPtr(_brokerHandle->request_dict(langTag))); langs.erase(langTag); } catch (const enchant::Exception &e) { // no first dictionary found } auto mySpellCount = 0; for (const std::string &language : langs) { try { _brokerHandle->set_ordering(language, kOrdering); auto validator = DictPtr(_brokerHandle->request_dict(language)); if (!validator) { continue; } if (CheckProvider(validator, kHspell)) { _hspells.push_back(validator.get()); } if (CheckProvider(validator, kMySpell) || CheckProvider(validator, kHunspell)) { if (mySpellCount > kMaxMySpellCount) { continue; } else { mySpellCount++; } } _validators.push_back(std::move(validator)); if (_validators.size() > kMaxValidators) { break; } } catch (const enchant::Exception &e) { DEBUG_LOG(("Catch after request_dict: %1").arg(e.what())); } } } EnchantSpellChecker *EnchantSpellChecker::instance() { static EnchantSpellChecker capsule; return &capsule; } auto EnchantSpellChecker::knownLanguages() { return _validators | ranges::views::transform([](const auto &validator) { return QString(validator->get_lang().c_str()); }) | ranges::to_vector; } bool EnchantSpellChecker::checkSpelling(const QString &word) { auto w = word.toStdString(); const auto checkWord = [&](const auto &validator, auto w) { try { return validator->check(w); } catch (const enchant::Exception &e) { DEBUG_LOG(("Catch after check '%1': %2").arg(word, e.what())); return true; } }; if (IsHebrew(word) && _hspells.size()) { return ranges::any_of(_hspells, [&](const auto &validator) { return checkWord(validator, w); }); } return ranges::any_of(_validators, [&](const auto &validator) { // Hspell is the spell checker that only checks words in Hebrew. // It returns 'true' for any non-Hebrew word, // so we should skip Hspell if a word is not in Hebrew. if (ranges::any_of(_hspells, [&](auto &v) { return v == validator.get(); })) { return false; } if (validator->get_lang().find("uk") == 0) { return false; } return checkWord(validator, w); }) || _validators.empty(); } auto EnchantSpellChecker::findSuggestions(const QString &word) { const auto wordScript = ::Spellchecker::WordScript(word); auto w = word.toStdString(); std::vector result; if (!_validators.size()) { return result; } const auto convertSuggestions = [&](auto suggestions) { for (const auto &replacement : suggestions) { if (result.size() >= kMaxSuggestions) { break; } if (!replacement.empty()) { result.push_back(replacement.c_str()); } } }; if (word.size() >= kMaxWordLength) { // The first element is the validator of the system language. auto *v = _validators[0].get(); const auto lang = QString::fromStdString(v->get_lang()); if (wordScript == ::Spellchecker::LocaleToScriptCode(lang)) { convertSuggestions(v->suggest(w)); } return result; } if (IsHebrew(word) && _hspells.size()) { for (const auto &h : _hspells) { convertSuggestions(h->suggest(w)); if (result.size()) { return result; } } } for (const auto &validator : _validators) { const auto lang = QString::fromStdString(validator->get_lang()); if (wordScript != ::Spellchecker::LocaleToScriptCode(lang)) { continue; } convertSuggestions(validator->suggest(w)); if (!result.empty()) { break; } } return result; } void EnchantSpellChecker::addWord(const QString &wordToAdd) { auto word = wordToAdd.toStdString(); auto &&first = _validators.at(0); first->add(word); first->add_to_session(word); } void EnchantSpellChecker::ignoreWord(const QString &word) { _validators.at(0)->add_to_session(word.toStdString()); } void EnchantSpellChecker::removeWord(const QString &word) { auto w = word.toStdString(); for (const auto &validator : _validators) { validator->remove_from_session(w); validator->remove(w); } } bool EnchantSpellChecker::isWordInDictionary(const QString &word) { auto w = word.toStdString(); return ranges::any_of(_validators, [&w](const auto &validator) { return validator->is_added(w); }); } } // namespace void Init() { } std::vector ActiveLanguages() { return EnchantSpellChecker::instance()->knownLanguages(); } void UpdateLanguages(std::vector languages) { ::Spellchecker::UpdateSupportedScripts(ActiveLanguages()); crl::async([=] { const auto result = ActiveLanguages(); crl::on_main([=] { ::Spellchecker::UpdateSupportedScripts(result); }); }); } bool CheckSpelling(const QString &wordToCheck) { return EnchantSpellChecker::instance()->checkSpelling(wordToCheck); } void FillSuggestionList( const QString &wrongWord, std::vector *variants) { *variants = EnchantSpellChecker::instance()->findSuggestions(wrongWord); } void AddWord(const QString &word) { EnchantSpellChecker::instance()->addWord(word); } void RemoveWord(const QString &word) { EnchantSpellChecker::instance()->removeWord(word); } void IgnoreWord(const QString &word) { EnchantSpellChecker::instance()->ignoreWord(word); } bool IsWordInDictionary(const QString &wordToCheck) { return EnchantSpellChecker::instance()->isWordInDictionary(wordToCheck); } void CheckSpellingText( const QString &text, MisspelledWords *misspelledWords) { *misspelledWords = ::Spellchecker::RangesFromText( text, ::Spellchecker::CheckSkipAndSpell); } bool IsSystemSpellchecker() { return true; } } // namespace Platform::Spellchecker