Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
290 lines
7.8 KiB
C++
290 lines
7.8 KiB
C++
// This file is part of Desktop App Toolkit,
|
|
// a set of libraries for developing nice desktop applications.
|
|
//
|
|
// Author: Nicholas Guriev <guriev-ns@ya.ru>, public domain, 2019
|
|
// License: CC0, https://creativecommons.org/publicdomain/zero/1.0/legalcode
|
|
|
|
#include <set>
|
|
#include <QLocale>
|
|
|
|
#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<enchant::Dict>;
|
|
|
|
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<enchant::Broker> _brokerHandle;
|
|
std::vector<DictPtr> _validators;
|
|
|
|
std::vector<not_null<enchant::Dict*>> _hspells;
|
|
};
|
|
|
|
EnchantSpellChecker::EnchantSpellChecker() {
|
|
if (!enchant::loader::do_explicit_linking()) return;
|
|
std::set<std::string> langs;
|
|
_brokerHandle = std::make_unique<enchant::Broker>();
|
|
_brokerHandle->list_dicts([](
|
|
const char *language,
|
|
const char *provider,
|
|
const char *description,
|
|
const char *filename,
|
|
void *our_payload) {
|
|
static_cast<decltype(langs)*>(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<QString> 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<QString> ActiveLanguages() {
|
|
return EnchantSpellChecker::instance()->knownLanguages();
|
|
}
|
|
|
|
void UpdateLanguages(std::vector<int> 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<QString> *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
|