init
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
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
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "spellcheck/platform/linux/language_linux.h"
|
||||
|
||||
namespace Platform::Language {
|
||||
|
||||
Id Recognize(QStringView text) {
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace Platform::Language
|
||||
@@ -0,0 +1,13 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "spellcheck/platform/platform_language.h"
|
||||
|
||||
namespace Platform::Language {
|
||||
|
||||
} // namespace Platform::Language
|
||||
@@ -0,0 +1,251 @@
|
||||
/* enchant
|
||||
* Copyright (C) 2003 Dom Lachowicz
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, Dom Lachowicz
|
||||
* gives permission to link the code of this program with
|
||||
* non-LGPL Spelling Provider libraries (eg: a MSFT Office
|
||||
* spell checker backend) and distribute linked combinations including
|
||||
* the two. You must obey the GNU Lesser General Public License in all
|
||||
* respects for all of the code used other than said providers. If you modify
|
||||
* this file, you may extend this exception to your version of the
|
||||
* file, but you are not obligated to do so. If you do not wish to
|
||||
* do so, delete this exception statement from your version.
|
||||
*
|
||||
* Nicholas Guriev (email: guriev-ns@ya.ru) split the full <enchant++.h> header
|
||||
* into two files, linux_enchant.h and linux_enchant.cpp, to use within Desktop
|
||||
* App Toolkit. He also implemented explicit linking with dlopen/dlsym to avoid
|
||||
* rigid dependency on the Enchant library at runtime.
|
||||
*/
|
||||
|
||||
#include <enchant.h>
|
||||
#include "base/platform/linux/base_linux_library.h"
|
||||
#include "spellcheck/platform/linux/linux_enchant.h"
|
||||
|
||||
namespace {
|
||||
|
||||
struct {
|
||||
//decltype (enchant_broker_describe) * broker_describe;
|
||||
//decltype (enchant_broker_dict_exists) * broker_dict_exists;
|
||||
decltype (enchant_broker_free) * broker_free;
|
||||
decltype (enchant_broker_free_dict) * broker_free_dict;
|
||||
decltype (enchant_broker_get_error) * broker_get_error;
|
||||
decltype (enchant_broker_init) * broker_init;
|
||||
decltype (enchant_broker_list_dicts) * broker_list_dicts;
|
||||
decltype (enchant_broker_request_dict) * broker_request_dict;
|
||||
//decltype (enchant_broker_request_pwl_dict) * broker_request_pwl_dict;
|
||||
decltype (enchant_broker_set_ordering) * broker_set_ordering;
|
||||
decltype (enchant_dict_add) * dict_add;
|
||||
decltype (enchant_dict_add_to_session) * dict_add_to_session;
|
||||
decltype (enchant_dict_check) * dict_check;
|
||||
decltype (enchant_dict_describe) * dict_describe;
|
||||
decltype (enchant_dict_free_string_list) * dict_free_string_list;
|
||||
decltype (enchant_dict_get_error) * dict_get_error;
|
||||
decltype (enchant_dict_is_added) * dict_is_added;
|
||||
//decltype (enchant_dict_is_removed) * dict_is_removed;
|
||||
decltype (enchant_dict_remove) * dict_remove;
|
||||
decltype (enchant_dict_remove_from_session) * dict_remove_from_session;
|
||||
//decltype (enchant_dict_store_replacement) * dict_store_replacement;
|
||||
decltype (enchant_dict_suggest) * dict_suggest;
|
||||
} f_enchant;
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
enchant::Exception::Exception (const char * ex)
|
||||
: std::exception (), m_ex ("") {
|
||||
if (ex)
|
||||
m_ex = ex;
|
||||
}
|
||||
|
||||
enchant::Exception::~Exception () = default;
|
||||
|
||||
const char * enchant::Exception::what () const noexcept {
|
||||
return m_ex.c_str();
|
||||
}
|
||||
|
||||
enchant::Dict::Dict (EnchantDict * dict, EnchantBroker * broker)
|
||||
: m_dict (dict), m_broker (broker) {
|
||||
f_enchant.dict_describe (m_dict, s_describe_fn, this);
|
||||
}
|
||||
|
||||
enchant::Dict::~Dict () {
|
||||
f_enchant.broker_free_dict (m_broker, m_dict);
|
||||
}
|
||||
|
||||
bool enchant::Dict::check (const std::string & utf8word) {
|
||||
int val;
|
||||
|
||||
val = f_enchant.dict_check (m_dict, utf8word.c_str(), utf8word.size());
|
||||
if (val == 0)
|
||||
return true;
|
||||
else if (val > 0)
|
||||
return false;
|
||||
else {
|
||||
throw enchant::Exception (f_enchant.dict_get_error (m_dict));
|
||||
}
|
||||
|
||||
return false; // never reached
|
||||
}
|
||||
|
||||
void enchant::Dict::suggest (const std::string & utf8word,
|
||||
std::vector<std::string> & out_suggestions) {
|
||||
size_t n_suggs;
|
||||
char ** suggs;
|
||||
|
||||
out_suggestions.clear ();
|
||||
|
||||
suggs = f_enchant.dict_suggest (m_dict, utf8word.c_str(),
|
||||
utf8word.size(), &n_suggs);
|
||||
|
||||
if (suggs && n_suggs) {
|
||||
out_suggestions.reserve(n_suggs);
|
||||
|
||||
for (size_t i = 0; i < n_suggs; i++) {
|
||||
out_suggestions.push_back (suggs[i]);
|
||||
}
|
||||
|
||||
f_enchant.dict_free_string_list (m_dict, suggs);
|
||||
}
|
||||
}
|
||||
|
||||
void enchant::Dict::add (const std::string & utf8word) {
|
||||
f_enchant.dict_add (m_dict, utf8word.c_str(), utf8word.size());
|
||||
}
|
||||
|
||||
void enchant::Dict::add_to_session (const std::string & utf8word) {
|
||||
f_enchant.dict_add_to_session (m_dict, utf8word.c_str(), utf8word.size());
|
||||
}
|
||||
|
||||
bool enchant::Dict::is_added (const std::string & utf8word) {
|
||||
return f_enchant.dict_is_added (m_dict, utf8word.c_str(),
|
||||
utf8word.size());
|
||||
}
|
||||
|
||||
void enchant::Dict::remove (const std::string & utf8word) {
|
||||
f_enchant.dict_remove (m_dict, utf8word.c_str(), utf8word.size());
|
||||
}
|
||||
|
||||
void enchant::Dict::remove_from_session (const std::string & utf8word) {
|
||||
f_enchant.dict_remove_from_session (m_dict, utf8word.c_str(),
|
||||
utf8word.size());
|
||||
}
|
||||
|
||||
//bool enchant::Dict::is_removed (const std::string & utf8word) {
|
||||
// return f_enchant.dict_is_removed (m_dict, utf8word.c_str(),
|
||||
// utf8word.size());
|
||||
//}
|
||||
|
||||
//void enchant::Dict::store_replacement (const std::string & utf8bad,
|
||||
// const std::string & utf8good) {
|
||||
// f_enchant.dict_store_replacement (m_dict,
|
||||
// utf8bad.c_str(), utf8bad.size(),
|
||||
// utf8good.c_str(), utf8good.size());
|
||||
//}
|
||||
|
||||
enchant::Broker::Broker ()
|
||||
: m_broker (f_enchant.broker_init ())
|
||||
{
|
||||
}
|
||||
|
||||
enchant::Broker::~Broker () {
|
||||
f_enchant.broker_free (m_broker);
|
||||
}
|
||||
|
||||
enchant::Dict * enchant::Broker::request_dict (const std::string & lang) {
|
||||
EnchantDict * dict = f_enchant.broker_request_dict (m_broker, lang.c_str());
|
||||
|
||||
if (!dict) {
|
||||
throw enchant::Exception (f_enchant.broker_get_error (m_broker));
|
||||
return 0; // never reached
|
||||
}
|
||||
|
||||
return new Dict (dict, m_broker);
|
||||
}
|
||||
|
||||
//enchant::Dict * enchant::Broker::request_pwl_dict (const std::string & pwl) {
|
||||
// EnchantDict * dict = f_enchant.broker_request_pwl_dict (m_broker, pwl.c_str());
|
||||
//
|
||||
// if (!dict) {
|
||||
// throw enchant::Exception (f_enchant.broker_get_error (m_broker));
|
||||
// return 0; // never reached
|
||||
// }
|
||||
//
|
||||
// return new Dict (dict, m_broker);
|
||||
//}
|
||||
|
||||
//bool enchant::Broker::dict_exists (const std::string & lang) {
|
||||
// if (f_enchant.broker_dict_exists (m_broker, lang.c_str()))
|
||||
// return true;
|
||||
// return false;
|
||||
//}
|
||||
|
||||
void enchant::Broker::set_ordering (const std::string & tag, const std::string & ordering) {
|
||||
f_enchant.broker_set_ordering (m_broker, tag.c_str(), ordering.c_str());
|
||||
}
|
||||
|
||||
//void enchant::Broker::describe (EnchantBrokerDescribeFn fn, void * user_data) {
|
||||
// f_enchant.broker_describe (m_broker, fn, user_data);
|
||||
//}
|
||||
|
||||
void enchant::Broker::list_dicts (EnchantDictDescribeFn fn, void * user_data) {
|
||||
f_enchant.broker_list_dicts (m_broker, fn, user_data);
|
||||
}
|
||||
|
||||
#define GET_SYMBOL_enchant(func_name) \
|
||||
if (!base::Platform::LoadSymbol (handle, "enchant_" # func_name, f_enchant.func_name)) { \
|
||||
return false; \
|
||||
}
|
||||
|
||||
bool enchant::loader::do_explicit_linking () {
|
||||
static enum { NotLoadedYet, LoadSuccessful, LoadFailed = -1 } load_status;
|
||||
if (load_status == NotLoadedYet) {
|
||||
load_status = LoadFailed;
|
||||
const auto handle = base::Platform::LoadLibrary ("libenchant.so.1", RTLD_NODELETE)
|
||||
?: base::Platform::LoadLibrary ("libenchant-2.so.2", RTLD_NODELETE)
|
||||
?: base::Platform::LoadLibrary ("libenchant.so.2", RTLD_NODELETE);
|
||||
if (!handle) {
|
||||
// logs ?
|
||||
return false;
|
||||
}
|
||||
//GET_SYMBOL_enchant (broker_describe);
|
||||
//GET_SYMBOL_enchant (broker_dict_exists);
|
||||
GET_SYMBOL_enchant (broker_free);
|
||||
GET_SYMBOL_enchant (broker_free_dict);
|
||||
GET_SYMBOL_enchant (broker_get_error);
|
||||
GET_SYMBOL_enchant (broker_init);
|
||||
GET_SYMBOL_enchant (broker_list_dicts);
|
||||
GET_SYMBOL_enchant (broker_request_dict);
|
||||
//GET_SYMBOL_enchant (broker_request_pwl_dict);
|
||||
GET_SYMBOL_enchant (broker_set_ordering);
|
||||
GET_SYMBOL_enchant (dict_add);
|
||||
GET_SYMBOL_enchant (dict_add_to_session);
|
||||
GET_SYMBOL_enchant (dict_check);
|
||||
GET_SYMBOL_enchant (dict_describe);
|
||||
GET_SYMBOL_enchant (dict_free_string_list);
|
||||
GET_SYMBOL_enchant (dict_get_error);
|
||||
GET_SYMBOL_enchant (dict_is_added);
|
||||
//GET_SYMBOL_enchant (dict_is_removed);
|
||||
GET_SYMBOL_enchant (dict_remove);
|
||||
GET_SYMBOL_enchant (dict_remove_from_session);
|
||||
//GET_SYMBOL_enchant (dict_store_replacement);
|
||||
GET_SYMBOL_enchant (dict_suggest);
|
||||
load_status = LoadSuccessful;
|
||||
}
|
||||
return load_status == LoadSuccessful;
|
||||
}
|
||||
|
||||
// vi: ts=8 sw=8
|
||||
@@ -0,0 +1,201 @@
|
||||
/* enchant
|
||||
* Copyright (C) 2003 Dom Lachowicz
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, Dom Lachowicz
|
||||
* gives permission to link the code of this program with
|
||||
* non-LGPL Spelling Provider libraries (eg: a MSFT Office
|
||||
* spell checker backend) and distribute linked combinations including
|
||||
* the two. You must obey the GNU Lesser General Public License in all
|
||||
* respects for all of the code used other than said providers. If you modify
|
||||
* this file, you may extend this exception to your version of the
|
||||
* file, but you are not obligated to do so. If you do not wish to
|
||||
* do so, delete this exception statement from your version.
|
||||
*
|
||||
* Nicholas Guriev (email: guriev-ns@ya.ru) split the full <enchant++.h> header
|
||||
* into two files, linux_enchant.h and linux_enchant.cpp, to use within Desktop
|
||||
* App Toolkit. He also implemented explicit linking with dlopen/dlsym to avoid
|
||||
* rigid dependency on the Enchant library at runtime.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <exception>
|
||||
|
||||
#ifndef ENCHANT_H
|
||||
typedef struct str_enchant_broker EnchantBroker;
|
||||
typedef struct str_enchant_dict EnchantDict;
|
||||
|
||||
/**
|
||||
* EnchantBrokerDescribeFn
|
||||
* @provider_name: The provider's identifier, such as "ispell" or "aspell" in UTF8 encoding
|
||||
* @provider_desc: A description of the provider, such as "Aspell 0.53" in UTF8 encoding
|
||||
* @provider_dll_file: The provider's DLL filename in Glib file encoding (UTF8 on Windows)
|
||||
* @user_data: Supplied user data, or %null if you don't care
|
||||
*
|
||||
* Callback used to enumerate and describe Enchant's various providers
|
||||
*/
|
||||
typedef void (*EnchantBrokerDescribeFn) (const char * const provider_name,
|
||||
const char * const provider_desc,
|
||||
const char * const provider_dll_file,
|
||||
void * user_data);
|
||||
|
||||
/**
|
||||
* EnchantDictDescribeFn
|
||||
* @lang_tag: The dictionary's language tag (eg: en_US, de_AT, ...)
|
||||
* @provider_name: The provider's name (eg: Aspell) in UTF8 encoding
|
||||
* @provider_desc: The provider's description (eg: Aspell 0.50.3) in UTF8 encoding
|
||||
* @provider_file: The DLL/SO where this dict's provider was loaded from in Glib file encoding (UTF8 on Windows)
|
||||
* @user_data: Supplied user data, or %null if you don't care
|
||||
*
|
||||
* Callback used to describe an individual dictionary
|
||||
*/
|
||||
typedef void (*EnchantDictDescribeFn) (const char * const lang_tag,
|
||||
const char * const provider_name,
|
||||
const char * const provider_desc,
|
||||
const char * const provider_file,
|
||||
void * user_data);
|
||||
#endif // !ENCHANT_H
|
||||
|
||||
namespace enchant
|
||||
{
|
||||
class Broker;
|
||||
|
||||
class Exception : public std::exception
|
||||
{
|
||||
public:
|
||||
explicit Exception (const char * ex);
|
||||
virtual ~Exception () noexcept;
|
||||
virtual const char * what () const noexcept;
|
||||
|
||||
private:
|
||||
std::string m_ex;
|
||||
}; // class enchant::Exception
|
||||
|
||||
class Dict
|
||||
{
|
||||
friend class enchant::Broker;
|
||||
|
||||
public:
|
||||
|
||||
~Dict ();
|
||||
|
||||
bool check (const std::string & utf8word);
|
||||
void suggest (const std::string & utf8word,
|
||||
std::vector<std::string> & out_suggestions);
|
||||
|
||||
std::vector<std::string> suggest (const std::string & utf8word) {
|
||||
std::vector<std::string> result;
|
||||
suggest (utf8word, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void add (const std::string & utf8word);
|
||||
void add_to_session (const std::string & utf8word);
|
||||
bool is_added (const std::string & utf8word);
|
||||
void remove (const std::string & utf8word);
|
||||
void remove_from_session (const std::string & utf8word);
|
||||
bool is_removed (const std::string & utf8word);
|
||||
void store_replacement (const std::string & utf8bad,
|
||||
const std::string & utf8good);
|
||||
|
||||
const std::string & get_lang () const {
|
||||
return m_lang;
|
||||
}
|
||||
|
||||
const std::string & get_provider_name () const {
|
||||
return m_provider_name;
|
||||
}
|
||||
|
||||
const std::string & get_provider_desc () const {
|
||||
return m_provider_desc;
|
||||
}
|
||||
|
||||
const std::string & get_provider_file () const {
|
||||
return m_provider_file;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
// space reserved for API/ABI expansion
|
||||
void * _private[5];
|
||||
|
||||
static void s_describe_fn (const char * const lang,
|
||||
const char * const provider_name,
|
||||
const char * const provider_desc,
|
||||
const char * const provider_file,
|
||||
void * user_data) {
|
||||
enchant::Dict * dict = static_cast<enchant::Dict *> (user_data);
|
||||
|
||||
dict->m_lang = lang;
|
||||
dict->m_provider_name = provider_name;
|
||||
dict->m_provider_desc = provider_desc;
|
||||
dict->m_provider_file = provider_file;
|
||||
}
|
||||
|
||||
Dict (EnchantDict * dict, EnchantBroker * broker);
|
||||
|
||||
// private, unimplemented
|
||||
Dict () = delete;
|
||||
Dict (const Dict & rhs) = delete;
|
||||
Dict& operator=(const Dict & rhs) = delete;
|
||||
|
||||
EnchantDict * m_dict;
|
||||
EnchantBroker * m_broker;
|
||||
|
||||
std::string m_lang;
|
||||
std::string m_provider_name;
|
||||
std::string m_provider_desc;
|
||||
std::string m_provider_file;
|
||||
}; // class enchant::Dict
|
||||
|
||||
class Broker
|
||||
{
|
||||
|
||||
public:
|
||||
|
||||
Broker ();
|
||||
~Broker ();
|
||||
|
||||
Dict * request_dict (const std::string & lang);
|
||||
Dict * request_pwl_dict (const std::string & pwl);
|
||||
bool dict_exists (const std::string & lang);
|
||||
void set_ordering (const std::string & tag, const std::string & ordering);
|
||||
void describe (EnchantBrokerDescribeFn fn, void * user_data = nullptr);
|
||||
void list_dicts (EnchantDictDescribeFn fn, void * user_data = nullptr);
|
||||
|
||||
private:
|
||||
|
||||
// space reserved for API/ABI expansion
|
||||
void * _private[5];
|
||||
|
||||
// not implemented
|
||||
Broker (const Broker & rhs) = delete;
|
||||
Broker& operator=(const Broker & rhs) = delete;
|
||||
|
||||
EnchantBroker * m_broker;
|
||||
}; // class enchant::Broker
|
||||
|
||||
namespace loader {
|
||||
bool do_explicit_linking ();
|
||||
} // loader subnamespace
|
||||
|
||||
} // enchant namespace
|
||||
|
||||
// vi: ts=8 sw=8
|
||||
@@ -0,0 +1,289 @@
|
||||
// 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
|
||||
@@ -0,0 +1,13 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "spellcheck/platform/platform_spellcheck.h"
|
||||
|
||||
namespace Platform::Spellchecker {
|
||||
|
||||
} // namespace Platform::Spellchecker
|
||||
@@ -0,0 +1,13 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "spellcheck/platform/platform_language.h"
|
||||
|
||||
namespace Platform::Language {
|
||||
|
||||
} // namespace Platform::Language
|
||||
@@ -0,0 +1,46 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "spellcheck/platform/mac/language_mac.h"
|
||||
|
||||
#include "base/platform/mac/base_utilities_mac.h"
|
||||
|
||||
#import <NaturalLanguage/NLLanguageRecognizer.h>
|
||||
|
||||
using Platform::Q2NSString;
|
||||
using Platform::NS2QString;
|
||||
|
||||
namespace Platform::Language {
|
||||
|
||||
LanguageId Recognize(QStringView text) {
|
||||
if (@available(macOS 10.14, *)) {
|
||||
constexpr auto kMaxHypotheses = 3;
|
||||
static thread_local auto r = [] {
|
||||
return [[NLLanguageRecognizer alloc] init];
|
||||
}();
|
||||
|
||||
[r processString:Q2NSString(text)];
|
||||
const auto hypotheses =
|
||||
[r languageHypothesesWithMaximum:kMaxHypotheses];
|
||||
[r reset];
|
||||
auto maxProbability = 0.;
|
||||
auto language = NLLanguage(nil);
|
||||
for (NLLanguage lang in hypotheses) {
|
||||
const auto probability = [hypotheses[lang] floatValue];
|
||||
if (probability > maxProbability) {
|
||||
maxProbability = probability;
|
||||
language = lang;
|
||||
}
|
||||
}
|
||||
if (language) {
|
||||
return { QLocale(NS2QString(language)).language() };
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace Platform::Language
|
||||
@@ -0,0 +1,13 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "spellcheck/platform/platform_spellcheck.h"
|
||||
|
||||
namespace Platform::Spellchecker {
|
||||
|
||||
} // namespace Platform::Spellchecker
|
||||
@@ -0,0 +1,219 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "spellcheck/platform/mac/spellcheck_mac.h"
|
||||
|
||||
#include "base/platform/mac/base_utilities_mac.h"
|
||||
#include "spellcheck/third_party/language_cld3.h"
|
||||
|
||||
#import <AppKit/NSSpellChecker.h>
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
#include <QtCore/QLocale>
|
||||
#include <set>
|
||||
|
||||
using Platform::Q2NSString;
|
||||
using Platform::NS2QString;
|
||||
|
||||
namespace {
|
||||
|
||||
// +[NSSpellChecker sharedSpellChecker] can throw exceptions depending
|
||||
// on the state of the pasteboard, or possibly as a result of
|
||||
// third-party code (when setting up services entries). The following
|
||||
// receives nil if an exception is thrown, in which case
|
||||
// spell-checking will not work, but it also will not crash the
|
||||
// browser.
|
||||
NSSpellChecker *SharedSpellChecker() {
|
||||
@try {
|
||||
return [NSSpellChecker sharedSpellChecker];
|
||||
} @catch (id exception) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
inline auto SystemLanguages() {
|
||||
static auto languages = std::vector<QString>();
|
||||
if (!languages.size()) {
|
||||
const auto uiLanguages = QLocale::system().uiLanguages();
|
||||
languages = (
|
||||
uiLanguages
|
||||
) | ranges::views::transform([&](const auto &lang) {
|
||||
return lang.left(std::max(lang.indexOf('_'), lang.indexOf('-')));
|
||||
}) | ranges::views::unique | ranges::to_vector;
|
||||
}
|
||||
return languages;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto RegionalVariantMap() {
|
||||
static auto map = std::map<QString, QString>();
|
||||
static auto initialized = false;
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
const auto uiLanguages = QLocale::system().uiLanguages();
|
||||
for (const auto &ui : uiLanguages) {
|
||||
if (ui == u"en-AU"_q || ui == u"en_AU"_q) {
|
||||
map[u"en"_q] = ui;
|
||||
} else if (ui == u"en-GB"_q || ui == u"en_GB"_q) {
|
||||
if (!map.contains(u"en"_q)) {
|
||||
map[u"en"_q] = ui;
|
||||
}
|
||||
} else if (ui == u"en-CA"_q || ui == u"en_CA"_q) {
|
||||
if (!map.contains(u"en"_q)) {
|
||||
map[u"en"_q] = ui;
|
||||
}
|
||||
} else if (ui == u"pt-BR"_q || ui == u"pt_BR"_q) {
|
||||
map[u"pt"_q] = ui;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto PreferredRegionalVariant(const QString &lang) {
|
||||
const auto &map = RegionalVariantMap();
|
||||
if (const auto it = map.find(lang); it != map.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return lang;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace Platform::Spellchecker {
|
||||
|
||||
void Init() {
|
||||
}
|
||||
|
||||
std::vector<QString> ActiveLanguages() {
|
||||
return SystemLanguages();
|
||||
}
|
||||
|
||||
bool CheckSpelling(const QString &word) {
|
||||
if (@available(macOS 10.14, *)) {
|
||||
const auto lang = PreferredRegionalVariant(
|
||||
Language::Recognize(word).twoLetterCode());
|
||||
return [SharedSpellChecker()
|
||||
checkSpellingOfString:Q2NSString(word)
|
||||
startingAt:0
|
||||
language:Q2NSString(lang)
|
||||
wrap:false
|
||||
inSpellDocumentWithTag:0
|
||||
wordCount:nil].location == NSNotFound;
|
||||
}
|
||||
const auto wordLength = word.length();
|
||||
NSArray<NSTextCheckingResult*> *spellRanges =
|
||||
[SharedSpellChecker()
|
||||
checkString:Q2NSString(word)
|
||||
range:NSMakeRange(0, wordLength)
|
||||
types:NSTextCheckingTypeSpelling
|
||||
options:nil
|
||||
inSpellDocumentWithTag:0
|
||||
orthography:nil
|
||||
wordCount:nil];
|
||||
// If the length of the misspelled word == 0,
|
||||
// then there is no misspelled word.
|
||||
return (spellRanges.count == 0);
|
||||
}
|
||||
|
||||
|
||||
// There's no need to check the language on the Mac.
|
||||
void CheckSpellingText(
|
||||
const QString &text,
|
||||
MisspelledWords *misspelledWords) {
|
||||
// Probably never gonna be defined.
|
||||
#ifdef SPELLCHECKER_MAC_AUTO_CHECK_TEXT
|
||||
|
||||
NSArray<NSTextCheckingResult*> *spellRanges =
|
||||
[SharedSpellChecker()
|
||||
checkString:Q2NSString(text)
|
||||
range:NSMakeRange(0, text.length())
|
||||
types:NSTextCheckingTypeSpelling
|
||||
options:nil
|
||||
inSpellDocumentWithTag:0
|
||||
orthography:nil
|
||||
wordCount:nil];
|
||||
|
||||
misspelledWords->reserve(spellRanges.count);
|
||||
for (NSTextCheckingResult *result in spellRanges) {
|
||||
if (result.resultType != NSTextCheckingTypeSpelling) {
|
||||
continue;
|
||||
}
|
||||
misspelledWords->push_back({
|
||||
result.range.location,
|
||||
result.range.length});
|
||||
}
|
||||
|
||||
#else
|
||||
// Known Issue: Despite the explicitly defined parameter,
|
||||
// the correctness of a single word depends on the rest of the text.
|
||||
// For example, "testt testtttyy" - this string will be marked as correct.
|
||||
// But at the same time "testtttyy" will be marked as misspelled word.
|
||||
|
||||
// So we have to manually split the text into words and check them separately.
|
||||
*misspelledWords = ::Spellchecker::RangesFromText(
|
||||
text,
|
||||
::Spellchecker::CheckSkipAndSpell);
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
void FillSuggestionList(
|
||||
const QString &wrongWord,
|
||||
std::vector<QString> *optionalSuggestions) {
|
||||
|
||||
const auto wordRange = NSMakeRange(0, wrongWord.length());
|
||||
auto *nsWord = Q2NSString(wrongWord);
|
||||
const auto guesses = [&](auto *lang) {
|
||||
return [SharedSpellChecker() guessesForWordRange:wordRange
|
||||
inString:nsWord
|
||||
language:lang
|
||||
inSpellDocumentWithTag:0];
|
||||
};
|
||||
|
||||
auto wordCounter = 0;
|
||||
const auto wordScript = ::Spellchecker::WordScript(wrongWord);
|
||||
optionalSuggestions->reserve(kMaxSuggestions);
|
||||
|
||||
// for (NSString *lang in [SharedSpellChecker() availableLanguages]) {
|
||||
for (const auto &lang : SystemLanguages()) {
|
||||
if (wordScript != ::Spellchecker::LocaleToScriptCode(lang)) {
|
||||
continue;
|
||||
}
|
||||
for (NSString *guess in guesses(Q2NSString(lang))) {
|
||||
optionalSuggestions->push_back(NS2QString(guess));
|
||||
if (++wordCounter >= kMaxSuggestions) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AddWord(const QString &word) {
|
||||
[SharedSpellChecker() learnWord:Q2NSString(word)];
|
||||
}
|
||||
|
||||
void RemoveWord(const QString &word) {
|
||||
[SharedSpellChecker() unlearnWord:Q2NSString(word)];
|
||||
}
|
||||
|
||||
void IgnoreWord(const QString &word) {
|
||||
[SharedSpellChecker() ignoreWord:Q2NSString(word)
|
||||
inSpellDocumentWithTag:0];
|
||||
}
|
||||
|
||||
bool IsWordInDictionary(const QString &wordToCheck) {
|
||||
return [SharedSpellChecker() hasLearnedWord:Q2NSString(wordToCheck)];
|
||||
}
|
||||
|
||||
bool IsSystemSpellchecker() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdateLanguages(std::vector<int> languages) {
|
||||
::Spellchecker::UpdateSupportedScripts(SystemLanguages());
|
||||
}
|
||||
|
||||
} // namespace Platform::Spellchecker
|
||||
@@ -0,0 +1,15 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "spellcheck/spellcheck_types.h"
|
||||
|
||||
namespace Platform::Language {
|
||||
|
||||
[[nodiscard]] LanguageId Recognize(QStringView text);
|
||||
|
||||
} // namespace Platform::Language
|
||||
@@ -0,0 +1,36 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "spellcheck/spellcheck_types.h"
|
||||
#include "spellcheck/spellcheck_utils.h"
|
||||
|
||||
namespace Platform::Spellchecker {
|
||||
|
||||
constexpr auto kMaxSuggestions = 5;
|
||||
|
||||
[[nodiscard]] bool IsSystemSpellchecker();
|
||||
[[nodiscard]] bool CheckSpelling(const QString &wordToCheck);
|
||||
[[nodiscard]] bool IsWordInDictionary(const QString &wordToCheck);
|
||||
|
||||
void Init();
|
||||
std::vector<QString> ActiveLanguages();
|
||||
void FillSuggestionList(
|
||||
const QString &wrongWord,
|
||||
std::vector<QString> *optionalSuggestions);
|
||||
|
||||
void AddWord(const QString &word);
|
||||
void RemoveWord(const QString &word);
|
||||
void IgnoreWord(const QString &word);
|
||||
|
||||
void CheckSpellingText(
|
||||
const QString &text,
|
||||
MisspelledWords *misspelledWords);
|
||||
|
||||
void UpdateLanguages(std::vector<int> languages);
|
||||
|
||||
} // namespace Platform::Spellchecker
|
||||
143
Telegram/lib_spellcheck/spellcheck/platform/win/language_win.cpp
Normal file
143
Telegram/lib_spellcheck/spellcheck/platform/win/language_win.cpp
Normal file
@@ -0,0 +1,143 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "spellcheck/platform/win/language_win.h"
|
||||
|
||||
#include "base/platform/win/base_windows_safe_library.h"
|
||||
|
||||
#include <Windows.h>
|
||||
#include <ElsCore.h>
|
||||
#include <ElsSrvc.h> // ELS_GUID_LANGUAGE_DETECTION.
|
||||
|
||||
namespace Platform::Language {
|
||||
namespace {
|
||||
|
||||
HRESULT (__stdcall *MappingGetServices)(
|
||||
_In_opt_ PMAPPING_ENUM_OPTIONS pOptions,
|
||||
_Out_ PMAPPING_SERVICE_INFO *prgServices,
|
||||
_Out_ DWORD *pdwServicesCount
|
||||
);
|
||||
|
||||
HRESULT (__stdcall *MappingFreeServices)(
|
||||
_In_ PMAPPING_SERVICE_INFO pServiceInfo
|
||||
);
|
||||
|
||||
HRESULT (__stdcall *MappingRecognizeText)(
|
||||
_In_ PMAPPING_SERVICE_INFO pServiceInfo,
|
||||
_In_reads_(dwLength) LPCWSTR pszText,
|
||||
_In_ DWORD dwLength,
|
||||
_In_ DWORD dwIndex,
|
||||
_In_opt_ PMAPPING_OPTIONS pOptions,
|
||||
_Inout_ PMAPPING_PROPERTY_BAG pbag
|
||||
);
|
||||
|
||||
HRESULT (__stdcall *MappingFreePropertyBag)(
|
||||
_In_ PMAPPING_PROPERTY_BAG pBag
|
||||
);
|
||||
|
||||
[[nodiscard]] inline bool Supported() {
|
||||
static const auto Result = [] {
|
||||
#define LOAD_SYMBOL(lib, name) base::Platform::LoadMethod(lib, #name, name)
|
||||
const auto els = base::Platform::SafeLoadLibrary(L"elscore.dll");
|
||||
return LOAD_SYMBOL(els, MappingGetServices)
|
||||
&& LOAD_SYMBOL(els, MappingRecognizeText)
|
||||
&& LOAD_SYMBOL(els, MappingFreeServices)
|
||||
&& LOAD_SYMBOL(els, MappingFreePropertyBag);
|
||||
#undef LOAD_SYMBOL
|
||||
}();
|
||||
return Result;
|
||||
}
|
||||
|
||||
struct unique_services final {
|
||||
operator MAPPING_SERVICE_INFO *() {
|
||||
return services;
|
||||
}
|
||||
MAPPING_SERVICE_INFO **operator&() {
|
||||
return &services;
|
||||
}
|
||||
MAPPING_SERVICE_INFO *services = nullptr;
|
||||
unique_services() = default;
|
||||
unique_services(unique_services const &other) = delete;
|
||||
~unique_services() {
|
||||
if (services) {
|
||||
MappingFreeServices(services);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct unique_bag final : public MAPPING_PROPERTY_BAG {
|
||||
unique_bag() : MAPPING_PROPERTY_BAG{} {
|
||||
}
|
||||
unique_bag(unique_bag const &other) = delete;
|
||||
~unique_bag() {
|
||||
MappingFreePropertyBag(this);
|
||||
}
|
||||
};
|
||||
|
||||
inline void MappingRecognizeTextFromService(
|
||||
REFGUID service,
|
||||
LPCWSTR text,
|
||||
DWORD length,
|
||||
unique_bag &bag) {
|
||||
auto options = MAPPING_ENUM_OPTIONS{};
|
||||
options.Size = sizeof(options);
|
||||
options.pGuid = const_cast<GUID*>(&service);
|
||||
|
||||
auto dwServicesCount = DWORD(0);
|
||||
auto services = unique_services();
|
||||
|
||||
const auto hr = MappingGetServices(&options, &services, &dwServicesCount);
|
||||
if (FAILED(hr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bag.Size = sizeof(bag);
|
||||
MappingRecognizeText(services, text, length, 0, nullptr, &bag);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RecognizeTextLanguages(
|
||||
LPCWSTR text,
|
||||
DWORD length,
|
||||
Fn<void(LPCWSTR, int)> &&callback) {
|
||||
if (!length) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto bag = unique_bag();
|
||||
MappingRecognizeTextFromService(
|
||||
ELS_GUID_LANGUAGE_DETECTION,
|
||||
text,
|
||||
length,
|
||||
bag);
|
||||
|
||||
auto pos = reinterpret_cast<LPCWSTR>(bag.prgResultRanges[0].pData);
|
||||
for (; *pos;) {
|
||||
const auto len = wcslen(pos);
|
||||
if (len >= 2) {
|
||||
callback(pos, len);
|
||||
}
|
||||
pos += (len + 1);
|
||||
}
|
||||
}
|
||||
|
||||
Id Recognize(QStringView text) {
|
||||
if (Supported()) {
|
||||
auto locales = std::vector<QLocale>();
|
||||
RecognizeTextLanguages(
|
||||
(LPCWSTR)text.utf16(),
|
||||
DWORD(text.size()),
|
||||
[&](LPCWSTR r, int length) {
|
||||
// Cut complex result, e.g. "sr-Cyrl".
|
||||
locales.emplace_back(QString::fromWCharArray(r, 2));
|
||||
});
|
||||
return { locales[0].language() };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace Platform::Language
|
||||
@@ -0,0 +1,13 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "spellcheck/platform/platform_language.h"
|
||||
|
||||
namespace Platform::Language {
|
||||
|
||||
} // namespace Platform::Language
|
||||
@@ -0,0 +1,443 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "spellcheck/platform/win/spellcheck_win.h"
|
||||
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "spellcheck/third_party/hunspell_controller.h"
|
||||
|
||||
#include <wrl/client.h>
|
||||
#include <spellcheck.h>
|
||||
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QLocale>
|
||||
#include <QVector>
|
||||
|
||||
using namespace Microsoft::WRL;
|
||||
|
||||
namespace Platform::Spellchecker {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kChunk = 5000;
|
||||
|
||||
// Seems like ISpellChecker API has bugs for Persian language (aka Farsi).
|
||||
[[nodiscard]] inline bool IsPersianLanguage(const QString &langTag) {
|
||||
return langTag.startsWith(QStringLiteral("fa"));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline LPCWSTR Q2WString(QStringView string) {
|
||||
return (LPCWSTR)string.utf16();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline auto SystemLanguages() {
|
||||
const auto appdata = qEnvironmentVariable("appdata");
|
||||
const auto dir = QDir(appdata + QString("\\Microsoft\\Spelling"));
|
||||
auto list = QStringList(SystemLanguage());
|
||||
list << (dir.exists()
|
||||
? dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)
|
||||
: QLocale::system().uiLanguages());
|
||||
list.removeDuplicates();
|
||||
return list | ranges::to_vector;
|
||||
}
|
||||
|
||||
// WindowsSpellChecker class is used to store all the COM objects and
|
||||
// control their lifetime. The class also provides wrappers for
|
||||
// ISpellCheckerFactory and ISpellChecker APIs. All COM calls are on the
|
||||
// background thread.
|
||||
class WindowsSpellChecker {
|
||||
public:
|
||||
WindowsSpellChecker();
|
||||
|
||||
void addWord(LPCWSTR word);
|
||||
void removeWord(LPCWSTR word);
|
||||
void ignoreWord(LPCWSTR word);
|
||||
[[nodiscard]] bool checkSpelling(LPCWSTR word);
|
||||
void fillSuggestionList(
|
||||
LPCWSTR wrongWord,
|
||||
std::vector<QString> *optionalSuggestions);
|
||||
void checkSpellingText(
|
||||
LPCWSTR text,
|
||||
MisspelledWords *misspelledWordRanges,
|
||||
int offset);
|
||||
[[nodiscard]] std::vector<QString> systemLanguages();
|
||||
void chunkedCheckSpellingText(
|
||||
QStringView textView,
|
||||
MisspelledWords *misspelledWords);
|
||||
|
||||
private:
|
||||
void createFactory();
|
||||
[[nodiscard]] bool isLanguageSupported(const LPCWSTR &lang);
|
||||
void createSpellCheckers();
|
||||
|
||||
ComPtr<ISpellCheckerFactory> _spellcheckerFactory;
|
||||
std::vector<std::pair<QString, ComPtr<ISpellChecker>>> _spellcheckerMap;
|
||||
|
||||
};
|
||||
|
||||
WindowsSpellChecker::WindowsSpellChecker() {
|
||||
createFactory();
|
||||
createSpellCheckers();
|
||||
}
|
||||
|
||||
void WindowsSpellChecker::createFactory() {
|
||||
if (FAILED(CoCreateInstance(
|
||||
__uuidof(SpellCheckerFactory),
|
||||
nullptr,
|
||||
(CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER),
|
||||
IID_PPV_ARGS(&_spellcheckerFactory)))) {
|
||||
_spellcheckerFactory = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void WindowsSpellChecker::createSpellCheckers() {
|
||||
if (!_spellcheckerFactory) {
|
||||
return;
|
||||
}
|
||||
for (const auto &lang : SystemLanguages()) {
|
||||
const auto wlang = Q2WString(lang);
|
||||
if (!isLanguageSupported(wlang)) {
|
||||
continue;
|
||||
}
|
||||
if (ranges::contains(ranges::views::keys(_spellcheckerMap), lang)) {
|
||||
continue;
|
||||
}
|
||||
auto spellchecker = ComPtr<ISpellChecker>();
|
||||
auto hr = _spellcheckerFactory->CreateSpellChecker(
|
||||
wlang,
|
||||
&spellchecker);
|
||||
if (SUCCEEDED(hr)) {
|
||||
_spellcheckerMap.push_back({ lang, spellchecker });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool WindowsSpellChecker::isLanguageSupported(const LPCWSTR &lang) {
|
||||
if (!_spellcheckerFactory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto isSupported = (BOOL)false;
|
||||
auto hr = _spellcheckerFactory->IsSupported(lang, &isSupported);
|
||||
return SUCCEEDED(hr) && isSupported;
|
||||
}
|
||||
|
||||
void WindowsSpellChecker::fillSuggestionList(
|
||||
LPCWSTR wrongWord,
|
||||
std::vector<QString> *optionalSuggestions) {
|
||||
auto i = 0;
|
||||
for (const auto &[langTag, spellchecker] : _spellcheckerMap) {
|
||||
if (IsPersianLanguage(langTag)) {
|
||||
continue;
|
||||
}
|
||||
auto suggestions = ComPtr<IEnumString>();
|
||||
auto hr = spellchecker->Suggest(wrongWord, &suggestions);
|
||||
if (hr != S_OK) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
wchar_t *suggestion = nullptr;
|
||||
hr = suggestions->Next(1, &suggestion, nullptr);
|
||||
if (hr != S_OK) {
|
||||
break;
|
||||
}
|
||||
const auto guess = QString::fromWCharArray(
|
||||
suggestion,
|
||||
wcslen(suggestion));
|
||||
CoTaskMemFree(suggestion);
|
||||
if (!guess.isEmpty()) {
|
||||
optionalSuggestions->push_back(guess);
|
||||
if (++i >= kMaxSuggestions) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool WindowsSpellChecker::checkSpelling(LPCWSTR word) {
|
||||
for (const auto &[_, spellchecker] : _spellcheckerMap) {
|
||||
auto spellingErrors = ComPtr<IEnumSpellingError>();
|
||||
auto hr = spellchecker->Check(word, &spellingErrors);
|
||||
|
||||
if (SUCCEEDED(hr) && spellingErrors) {
|
||||
auto spellingError = ComPtr<ISpellingError>();
|
||||
auto startIndex = ULONG(0);
|
||||
auto errorLength = ULONG(0);
|
||||
auto action = CORRECTIVE_ACTION_NONE;
|
||||
hr = spellingErrors->Next(&spellingError);
|
||||
if (SUCCEEDED(hr) &&
|
||||
spellingError &&
|
||||
SUCCEEDED(spellingError->get_StartIndex(&startIndex)) &&
|
||||
SUCCEEDED(spellingError->get_Length(&errorLength)) &&
|
||||
SUCCEEDED(spellingError->get_CorrectiveAction(&action)) &&
|
||||
(action == CORRECTIVE_ACTION_GET_SUGGESTIONS ||
|
||||
action == CORRECTIVE_ACTION_REPLACE)) {
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void WindowsSpellChecker::checkSpellingText(
|
||||
LPCWSTR text,
|
||||
MisspelledWords *misspelledWordRanges,
|
||||
int offset) {
|
||||
// The spellchecker marks words not from its own language as misspelled.
|
||||
// So we only return words that are marked
|
||||
// as misspelled in all spellcheckers.
|
||||
auto misspelledWords = MisspelledWords();
|
||||
|
||||
constexpr auto isActionGood = [](auto action) {
|
||||
return action == CORRECTIVE_ACTION_GET_SUGGESTIONS
|
||||
|| action == CORRECTIVE_ACTION_REPLACE;
|
||||
};
|
||||
|
||||
for (const auto &[langTag, spellchecker] : _spellcheckerMap) {
|
||||
auto spellingErrors = ComPtr<IEnumSpellingError>();
|
||||
|
||||
auto hr = IsPersianLanguage(langTag)
|
||||
? spellchecker->Check(text, &spellingErrors)
|
||||
: spellchecker->ComprehensiveCheck(text, &spellingErrors);
|
||||
if (!(SUCCEEDED(hr) && spellingErrors)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto tempMisspelled = MisspelledWords();
|
||||
auto spellingError = ComPtr<ISpellingError>();
|
||||
for (; hr == S_OK; hr = spellingErrors->Next(&spellingError)) {
|
||||
auto startIndex = ULONG(0);
|
||||
auto errorLength = ULONG(0);
|
||||
auto action = CORRECTIVE_ACTION_NONE;
|
||||
|
||||
if (!(SUCCEEDED(hr)
|
||||
&& spellingError
|
||||
&& SUCCEEDED(spellingError->get_StartIndex(&startIndex))
|
||||
&& SUCCEEDED(spellingError->get_Length(&errorLength))
|
||||
&& SUCCEEDED(spellingError->get_CorrectiveAction(&action))
|
||||
&& isActionGood(action))) {
|
||||
continue;
|
||||
}
|
||||
const auto word = std::pair(
|
||||
(int)startIndex + offset,
|
||||
(int)errorLength);
|
||||
if (misspelledWords.empty()
|
||||
|| ranges::contains(misspelledWords, word)) {
|
||||
tempMisspelled.push_back(std::move(word));
|
||||
}
|
||||
}
|
||||
// If the tempMisspelled vector is empty at least once,
|
||||
// it means that the all words will be correct in the end
|
||||
// and it makes no sense to check other languages.
|
||||
if (tempMisspelled.empty()) {
|
||||
return;
|
||||
}
|
||||
misspelledWords = std::move(tempMisspelled);
|
||||
}
|
||||
if (offset) {
|
||||
for (auto &m : misspelledWords) {
|
||||
misspelledWordRanges->push_back(std::move(m));
|
||||
}
|
||||
} else {
|
||||
*misspelledWordRanges = misspelledWords;
|
||||
}
|
||||
}
|
||||
|
||||
void WindowsSpellChecker::addWord(LPCWSTR word) {
|
||||
for (const auto &[_, spellchecker] : _spellcheckerMap) {
|
||||
spellchecker->Add(word);
|
||||
}
|
||||
}
|
||||
|
||||
void WindowsSpellChecker::removeWord(LPCWSTR word) {
|
||||
for (const auto &[_, spellchecker] : _spellcheckerMap) {
|
||||
auto spellchecker2 = ComPtr<ISpellChecker2>();
|
||||
spellchecker->QueryInterface(IID_PPV_ARGS(&spellchecker2));
|
||||
if (spellchecker2) {
|
||||
spellchecker2->Remove(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WindowsSpellChecker::ignoreWord(LPCWSTR word) {
|
||||
for (const auto &[_, spellchecker] : _spellcheckerMap) {
|
||||
spellchecker->Ignore(word);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<QString> WindowsSpellChecker::systemLanguages() {
|
||||
return ranges::views::keys(_spellcheckerMap) | ranges::to_vector;
|
||||
}
|
||||
|
||||
void WindowsSpellChecker::chunkedCheckSpellingText(
|
||||
QStringView textView,
|
||||
MisspelledWords *misspelledWords) {
|
||||
auto i = 0;
|
||||
auto chunkBuffer = std::vector<wchar_t>();
|
||||
|
||||
while (i != textView.size()) {
|
||||
const auto provisionalChunkSize = std::min(
|
||||
kChunk,
|
||||
int(textView.size() - i));
|
||||
const auto chunkSize = [&] {
|
||||
const auto until = std::max(
|
||||
0,
|
||||
provisionalChunkSize - ::Spellchecker::kMaxWordSize);
|
||||
for (auto n = provisionalChunkSize; n > until; n--) {
|
||||
if (textView.at(i + n - 1).isLetterOrNumber()) {
|
||||
continue;
|
||||
} else {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
return provisionalChunkSize;
|
||||
}();
|
||||
const auto chunk = textView.mid(i, chunkSize);
|
||||
|
||||
chunkBuffer.resize(chunk.size() + 1);
|
||||
const auto count = chunk.toWCharArray(chunkBuffer.data());
|
||||
chunkBuffer[count] = '\0';
|
||||
|
||||
checkSpellingText(
|
||||
(LPCWSTR)chunkBuffer.data(),
|
||||
misspelledWords,
|
||||
i);
|
||||
i += chunk.size();
|
||||
}
|
||||
}
|
||||
|
||||
////// End of WindowsSpellChecker class.
|
||||
|
||||
WindowsSpellChecker &SharedSpellChecker() {
|
||||
static auto spellchecker = WindowsSpellChecker();
|
||||
return spellchecker;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// TODO: Add a better work with the Threading Models.
|
||||
// All COM objects should be created asynchronously
|
||||
// if we want to work with them asynchronously.
|
||||
// Some calls can be made in the main thread before spellchecking
|
||||
// (e.g. KnownLanguages), so we have to init it asynchronously first.
|
||||
void Init() {
|
||||
if (IsSystemSpellchecker()) {
|
||||
crl::async(SharedSpellChecker);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsSystemSpellchecker() {
|
||||
// Windows 7 does not support spellchecking.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/spellcheck/nn-spellcheck-ispellchecker
|
||||
return IsWindows8OrGreater();
|
||||
}
|
||||
|
||||
std::vector<QString> ActiveLanguages() {
|
||||
if (IsSystemSpellchecker()) {
|
||||
return SharedSpellChecker().systemLanguages();
|
||||
}
|
||||
return ThirdParty::ActiveLanguages();
|
||||
}
|
||||
|
||||
bool CheckSpelling(const QString &wordToCheck) {
|
||||
if (!IsSystemSpellchecker()) {
|
||||
return ThirdParty::CheckSpelling(wordToCheck);
|
||||
}
|
||||
return SharedSpellChecker().checkSpelling(Q2WString(wordToCheck));
|
||||
}
|
||||
|
||||
void FillSuggestionList(
|
||||
const QString &wrongWord,
|
||||
std::vector<QString> *optionalSuggestions) {
|
||||
if (IsSystemSpellchecker()) {
|
||||
SharedSpellChecker().fillSuggestionList(
|
||||
Q2WString(wrongWord),
|
||||
optionalSuggestions);
|
||||
return;
|
||||
}
|
||||
ThirdParty::FillSuggestionList(
|
||||
wrongWord,
|
||||
optionalSuggestions);
|
||||
}
|
||||
|
||||
void AddWord(const QString &word) {
|
||||
if (IsSystemSpellchecker()) {
|
||||
SharedSpellChecker().addWord(Q2WString(word));
|
||||
} else {
|
||||
ThirdParty::AddWord(word);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveWord(const QString &word) {
|
||||
if (IsSystemSpellchecker()) {
|
||||
SharedSpellChecker().removeWord(Q2WString(word));
|
||||
} else {
|
||||
ThirdParty::RemoveWord(word);
|
||||
}
|
||||
}
|
||||
|
||||
void IgnoreWord(const QString &word) {
|
||||
if (IsSystemSpellchecker()) {
|
||||
SharedSpellChecker().ignoreWord(Q2WString(word));
|
||||
} else {
|
||||
ThirdParty::IgnoreWord(word);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsWordInDictionary(const QString &wordToCheck) {
|
||||
if (IsSystemSpellchecker()) {
|
||||
// ISpellChecker can't check if a word is in the dictionary.
|
||||
return false;
|
||||
}
|
||||
return ThirdParty::IsWordInDictionary(wordToCheck);
|
||||
}
|
||||
|
||||
void UpdateLanguages(std::vector<int> languages) {
|
||||
if (!IsSystemSpellchecker()) {
|
||||
ThirdParty::UpdateLanguages(languages);
|
||||
return;
|
||||
}
|
||||
crl::async([=] {
|
||||
const auto result = ActiveLanguages();
|
||||
crl::on_main([=] {
|
||||
::Spellchecker::UpdateSupportedScripts(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void CheckSpellingText(
|
||||
const QString &text,
|
||||
MisspelledWords *misspelledWords) {
|
||||
if (IsSystemSpellchecker()) {
|
||||
// There are certain strings with a lot of 'paragraph separators'
|
||||
// that crash the native Windows spellchecker. We replace them
|
||||
// with spaces (no difference for the checking), they don't crash.
|
||||
const auto check = QString(text).replace(QChar(8233), QChar(32));
|
||||
if (check.size() > kChunk) {
|
||||
// On some versions of Windows 10,
|
||||
// checking large text with specific characters (e.g. @)
|
||||
// will throw the std::regex_error::error_complexity exception,
|
||||
// so we have to split the text.
|
||||
SharedSpellChecker().chunkedCheckSpellingText(
|
||||
check,
|
||||
misspelledWords);
|
||||
} else {
|
||||
SharedSpellChecker().checkSpellingText(
|
||||
(LPCWSTR)check.utf16(),
|
||||
misspelledWords,
|
||||
0);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
ThirdParty::CheckSpellingText(text, misspelledWords);
|
||||
}
|
||||
|
||||
|
||||
} // namespace Platform::Spellchecker
|
||||
@@ -0,0 +1,13 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "spellcheck/platform/platform_spellcheck.h"
|
||||
|
||||
namespace Platform::Spellchecker {
|
||||
|
||||
} // namespace Platform::Spellchecker
|
||||
Reference in New Issue
Block a user