spellcheck_linux.cpp 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. // This file is part of Desktop App Toolkit,
  2. // a set of libraries for developing nice desktop applications.
  3. //
  4. // Author: Nicholas Guriev <guriev-ns@ya.ru>, public domain, 2019
  5. // License: CC0, https://creativecommons.org/publicdomain/zero/1.0/legalcode
  6. #include <set>
  7. #include <QLocale>
  8. #include "spellcheck/platform/linux/linux_enchant.h"
  9. #include "spellcheck/platform/linux/spellcheck_linux.h"
  10. #include "base/debug_log.h"
  11. namespace Platform::Spellchecker {
  12. namespace {
  13. constexpr auto kHspell = "hspell";
  14. constexpr auto kMySpell = "myspell";
  15. constexpr auto kHunspell = "hunspell";
  16. constexpr auto kOrdering = "hspell,aspell,hunspell,myspell";
  17. constexpr auto kMaxValidators = 10;
  18. constexpr auto kMaxMySpellCount = 3;
  19. constexpr auto kMaxWordLength = 15;
  20. using DictPtr = std::unique_ptr<enchant::Dict>;
  21. auto CheckProvider(DictPtr &validator, const std::string &provider) {
  22. auto p = validator->get_provider_name();
  23. std::transform(begin(p), end(p), begin(p), ::tolower);
  24. return (p.find(provider) == 0); // startsWith.
  25. }
  26. auto IsHebrew(const QString &word) {
  27. // Words with mixed scripts will be automatically ignored,
  28. // so this check should be fine.
  29. return ::Spellchecker::WordScript(word) == QChar::Script_Hebrew;
  30. }
  31. class EnchantSpellChecker {
  32. public:
  33. auto knownLanguages();
  34. bool checkSpelling(const QString &word);
  35. auto findSuggestions(const QString &word);
  36. void addWord(const QString &wordToAdd);
  37. void ignoreWord(const QString &word);
  38. void removeWord(const QString &word);
  39. bool isWordInDictionary(const QString &word);
  40. static EnchantSpellChecker *instance();
  41. private:
  42. EnchantSpellChecker();
  43. EnchantSpellChecker(const EnchantSpellChecker&) = delete;
  44. EnchantSpellChecker& operator =(const EnchantSpellChecker&) = delete;
  45. std::unique_ptr<enchant::Broker> _brokerHandle;
  46. std::vector<DictPtr> _validators;
  47. std::vector<not_null<enchant::Dict*>> _hspells;
  48. };
  49. EnchantSpellChecker::EnchantSpellChecker() {
  50. if (!enchant::loader::do_explicit_linking()) return;
  51. std::set<std::string> langs;
  52. _brokerHandle = std::make_unique<enchant::Broker>();
  53. _brokerHandle->list_dicts([](
  54. const char *language,
  55. const char *provider,
  56. const char *description,
  57. const char *filename,
  58. void *our_payload) {
  59. static_cast<decltype(langs)*>(our_payload)->insert(language);
  60. }, &langs);
  61. _validators.reserve(langs.size());
  62. try {
  63. std::string langTag = QLocale::system().name().toStdString();
  64. _brokerHandle->set_ordering(langTag, kOrdering);
  65. _validators.push_back(DictPtr(_brokerHandle->request_dict(langTag)));
  66. langs.erase(langTag);
  67. } catch (const enchant::Exception &e) {
  68. // no first dictionary found
  69. }
  70. auto mySpellCount = 0;
  71. for (const std::string &language : langs) {
  72. try {
  73. _brokerHandle->set_ordering(language, kOrdering);
  74. auto validator = DictPtr(_brokerHandle->request_dict(language));
  75. if (!validator) {
  76. continue;
  77. }
  78. if (CheckProvider(validator, kHspell)) {
  79. _hspells.push_back(validator.get());
  80. }
  81. if (CheckProvider(validator, kMySpell)
  82. || CheckProvider(validator, kHunspell)) {
  83. if (mySpellCount > kMaxMySpellCount) {
  84. continue;
  85. } else {
  86. mySpellCount++;
  87. }
  88. }
  89. _validators.push_back(std::move(validator));
  90. if (_validators.size() > kMaxValidators) {
  91. break;
  92. }
  93. } catch (const enchant::Exception &e) {
  94. DEBUG_LOG(("Catch after request_dict: %1").arg(e.what()));
  95. }
  96. }
  97. }
  98. EnchantSpellChecker *EnchantSpellChecker::instance() {
  99. static EnchantSpellChecker capsule;
  100. return &capsule;
  101. }
  102. auto EnchantSpellChecker::knownLanguages() {
  103. return _validators | ranges::views::transform([](const auto &validator) {
  104. return QString(validator->get_lang().c_str());
  105. }) | ranges::to_vector;
  106. }
  107. bool EnchantSpellChecker::checkSpelling(const QString &word) {
  108. auto w = word.toStdString();
  109. const auto checkWord = [&](const auto &validator, auto w) {
  110. try {
  111. return validator->check(w);
  112. } catch (const enchant::Exception &e) {
  113. DEBUG_LOG(("Catch after check '%1': %2").arg(word, e.what()));
  114. return true;
  115. }
  116. };
  117. if (IsHebrew(word) && _hspells.size()) {
  118. return ranges::any_of(_hspells, [&](const auto &validator) {
  119. return checkWord(validator, w);
  120. });
  121. }
  122. return ranges::any_of(_validators, [&](const auto &validator) {
  123. // Hspell is the spell checker that only checks words in Hebrew.
  124. // It returns 'true' for any non-Hebrew word,
  125. // so we should skip Hspell if a word is not in Hebrew.
  126. if (ranges::any_of(_hspells, [&](auto &v) {
  127. return v == validator.get();
  128. })) {
  129. return false;
  130. }
  131. if (validator->get_lang().find("uk") == 0) {
  132. return false;
  133. }
  134. return checkWord(validator, w);
  135. }) || _validators.empty();
  136. }
  137. auto EnchantSpellChecker::findSuggestions(const QString &word) {
  138. const auto wordScript = ::Spellchecker::WordScript(word);
  139. auto w = word.toStdString();
  140. std::vector<QString> result;
  141. if (!_validators.size()) {
  142. return result;
  143. }
  144. const auto convertSuggestions = [&](auto suggestions) {
  145. for (const auto &replacement : suggestions) {
  146. if (result.size() >= kMaxSuggestions) {
  147. break;
  148. }
  149. if (!replacement.empty()) {
  150. result.push_back(replacement.c_str());
  151. }
  152. }
  153. };
  154. if (word.size() >= kMaxWordLength) {
  155. // The first element is the validator of the system language.
  156. auto *v = _validators[0].get();
  157. const auto lang = QString::fromStdString(v->get_lang());
  158. if (wordScript == ::Spellchecker::LocaleToScriptCode(lang)) {
  159. convertSuggestions(v->suggest(w));
  160. }
  161. return result;
  162. }
  163. if (IsHebrew(word) && _hspells.size()) {
  164. for (const auto &h : _hspells) {
  165. convertSuggestions(h->suggest(w));
  166. if (result.size()) {
  167. return result;
  168. }
  169. }
  170. }
  171. for (const auto &validator : _validators) {
  172. const auto lang = QString::fromStdString(validator->get_lang());
  173. if (wordScript != ::Spellchecker::LocaleToScriptCode(lang)) {
  174. continue;
  175. }
  176. convertSuggestions(validator->suggest(w));
  177. if (!result.empty()) {
  178. break;
  179. }
  180. }
  181. return result;
  182. }
  183. void EnchantSpellChecker::addWord(const QString &wordToAdd) {
  184. auto word = wordToAdd.toStdString();
  185. auto &&first = _validators.at(0);
  186. first->add(word);
  187. first->add_to_session(word);
  188. }
  189. void EnchantSpellChecker::ignoreWord(const QString &word) {
  190. _validators.at(0)->add_to_session(word.toStdString());
  191. }
  192. void EnchantSpellChecker::removeWord(const QString &word) {
  193. auto w = word.toStdString();
  194. for (const auto &validator : _validators) {
  195. validator->remove_from_session(w);
  196. validator->remove(w);
  197. }
  198. }
  199. bool EnchantSpellChecker::isWordInDictionary(const QString &word) {
  200. auto w = word.toStdString();
  201. return ranges::any_of(_validators, [&w](const auto &validator) {
  202. return validator->is_added(w);
  203. });
  204. }
  205. } // namespace
  206. void Init() {
  207. }
  208. std::vector<QString> ActiveLanguages() {
  209. return EnchantSpellChecker::instance()->knownLanguages();
  210. }
  211. void UpdateLanguages(std::vector<int> languages) {
  212. ::Spellchecker::UpdateSupportedScripts(ActiveLanguages());
  213. crl::async([=] {
  214. const auto result = ActiveLanguages();
  215. crl::on_main([=] {
  216. ::Spellchecker::UpdateSupportedScripts(result);
  217. });
  218. });
  219. }
  220. bool CheckSpelling(const QString &wordToCheck) {
  221. return EnchantSpellChecker::instance()->checkSpelling(wordToCheck);
  222. }
  223. void FillSuggestionList(
  224. const QString &wrongWord,
  225. std::vector<QString> *variants) {
  226. *variants = EnchantSpellChecker::instance()->findSuggestions(wrongWord);
  227. }
  228. void AddWord(const QString &word) {
  229. EnchantSpellChecker::instance()->addWord(word);
  230. }
  231. void RemoveWord(const QString &word) {
  232. EnchantSpellChecker::instance()->removeWord(word);
  233. }
  234. void IgnoreWord(const QString &word) {
  235. EnchantSpellChecker::instance()->ignoreWord(word);
  236. }
  237. bool IsWordInDictionary(const QString &wordToCheck) {
  238. return EnchantSpellChecker::instance()->isWordInDictionary(wordToCheck);
  239. }
  240. void CheckSpellingText(
  241. const QString &text,
  242. MisspelledWords *misspelledWords) {
  243. *misspelledWords = ::Spellchecker::RangesFromText(
  244. text,
  245. ::Spellchecker::CheckSkipAndSpell);
  246. }
  247. bool IsSystemSpellchecker() {
  248. return true;
  249. }
  250. } // namespace Platform::Spellchecker