diff --git a/binding.gyp b/binding.gyp index 84fc1fc..a8defb0 100644 --- a/binding.gyp +++ b/binding.gyp @@ -50,12 +50,16 @@ 'src/spellchecker_linux.cc', 'src/transcoder_posix.cc', ], + 'cflags!': [ '-fno-exceptions' ], + 'cflags_cc!': [ '-fno-exceptions' ], }], ['OS=="mac"', { 'sources': [ 'src/spellchecker_mac.mm', 'src/transcoder_posix.cc', ], + 'cflags!': [ '-fno-exceptions' ], + 'cflags_cc!': [ '-fno-exceptions' ], 'link_settings': { 'libraries': [ '$(SDKROOT)/System/Library/Frameworks/AppKit.framework', diff --git a/lib/spellchecker.js b/lib/spellchecker.js index 90c72e0..7790976 100644 --- a/lib/spellchecker.js +++ b/lib/spellchecker.js @@ -17,6 +17,11 @@ var ensureDefaultSpellCheck = function() { setDictionary(lang, getDictionaryPath()); }; +var addDictionary = function(lang, dictPath) { + ensureDefaultSpellCheck(); + return defaultSpellcheck.addDictionary(lang, dictPath); +}; + var setDictionary = function(lang, dictPath) { ensureDefaultSpellCheck(); return defaultSpellcheck.setDictionary(lang, dictPath); @@ -72,6 +77,7 @@ var getDictionaryPath = function() { } module.exports = { + addDictionary: addDictionary, setDictionary: setDictionary, add: add, remove: remove, diff --git a/src/main.cc b/src/main.cc index 673837e..7c9e069 100644 --- a/src/main.cc +++ b/src/main.cc @@ -19,6 +19,25 @@ class Spellchecker : public Nan::ObjectWrap { info.GetReturnValue().Set(info.This()); } + static NAN_METHOD(AddDictionary) { + Nan::HandleScope scope; + + if (info.Length() < 1) { + return Nan::ThrowError("Bad argument"); + } + + Spellchecker* that = Nan::ObjectWrap::Unwrap(info.Holder()); + + std::string language = *String::Utf8Value(info[0]); + std::string directory = "."; + if (info.Length() > 1) { + directory = *String::Utf8Value(info[1]); + } + + bool result = that->impl->AddDictionary(language, directory); + info.GetReturnValue().Set(Nan::New(result)); + } + static NAN_METHOD(SetDictionary) { Nan::HandleScope scope; @@ -98,7 +117,7 @@ class Spellchecker : public Nan::ObjectWrap { that->impl->Add(word); return; } - + static NAN_METHOD(Remove) { Nan::HandleScope scope; if (info.Length() < 1) { @@ -174,6 +193,7 @@ class Spellchecker : public Nan::ObjectWrap { tpl->SetClassName(Nan::New("Spellchecker").ToLocalChecked()); tpl->InstanceTemplate()->SetInternalFieldCount(1); + Nan::SetMethod(tpl->InstanceTemplate(), "addDictionary", Spellchecker::AddDictionary); Nan::SetMethod(tpl->InstanceTemplate(), "setDictionary", Spellchecker::SetDictionary); Nan::SetMethod(tpl->InstanceTemplate(), "getAvailableDictionaries", Spellchecker::GetAvailableDictionaries); Nan::SetMethod(tpl->InstanceTemplate(), "getCorrectionsForMisspelling", Spellchecker::GetCorrectionsForMisspelling); diff --git a/src/spellchecker.h b/src/spellchecker.h index 80852ff..c77b4e5 100644 --- a/src/spellchecker.h +++ b/src/spellchecker.h @@ -1,6 +1,7 @@ #ifndef SRC_SPELLCHECKER_H_ #define SRC_SPELLCHECKER_H_ +#include #include #include #include @@ -14,6 +15,7 @@ struct MisspelledRange { class SpellcheckerImplementation { public: + virtual bool AddDictionary(const std::string& language, const std::string& path) = 0; virtual bool SetDictionary(const std::string& language, const std::string& path) = 0; virtual std::vector GetAvailableDictionaries(const std::string& path) = 0; @@ -29,7 +31,7 @@ class SpellcheckerImplementation { // NB: When using Hunspell, this will not modify the .dic file; custom words must be added each // time the spellchecker is created. Use a custom dictionary file. virtual void Add(const std::string& word) = 0; - + // Removes a word from the custom dictionary added by Add. // NB: When using Hunspell, this will not modify the .dic file; custom words must be added each // time the spellchecker is created. Use a custom dictionary file. diff --git a/src/spellchecker_hunspell.cc b/src/spellchecker_hunspell.cc index 0ca629b..2794ac8 100644 --- a/src/spellchecker_hunspell.cc +++ b/src/spellchecker_hunspell.cc @@ -1,16 +1,17 @@ #include #include #include +#include #include "../vendor/hunspell/src/hunspell/hunspell.hxx" #include "spellchecker_hunspell.h" namespace spellchecker { -HunspellSpellchecker::HunspellSpellchecker() : hunspell(NULL), transcoder(NewTranscoder()) { } +HunspellSpellchecker::HunspellSpellchecker() : transcoder(NewTranscoder()) { } HunspellSpellchecker::~HunspellSpellchecker() { - if (hunspell) { - delete hunspell; + for (size_t i = 0; i < hunspells.size(); ++i) { + delete hunspells[i].second; } if (transcoder) { @@ -18,12 +19,7 @@ HunspellSpellchecker::~HunspellSpellchecker() { } } -bool HunspellSpellchecker::SetDictionary(const std::string& language, const std::string& dirname) { - if (hunspell) { - delete hunspell; - hunspell = NULL; - } - +bool HunspellSpellchecker::AddDictionary(const std::string& language, const std::string& dirname) { // NB: Hunspell uses underscore to separate language and locale, and Win8 uses // dash - if they use the wrong one, just silently replace it for them std::string lang = language; @@ -39,25 +35,51 @@ bool HunspellSpellchecker::SetDictionary(const std::string& language, const std: } fclose(handle); - hunspell = new Hunspell(affixpath.c_str(), dpath.c_str()); + std::locale loc; + try { + // On Linux locale requires "UTF-8" suffix; e.g., "en_US.UTF-8" + loc = std::locale((lang + ".UTF-8").c_str()); + } catch (std::runtime_error & e) { + // On Windows locale names are different; e.g. en-US + std::string langDashed = language; + std::replace(langDashed.begin(), langDashed.end(), '_', '-'); + std::locale loc(langDashed.c_str()); + } + + Hunspell* hunspell = new Hunspell(affixpath.c_str(), dpath.c_str()); + hunspells.push_back(std::make_pair(loc, hunspell)); return true; } +bool HunspellSpellchecker::SetDictionary(const std::string& language, const std::string& dirname) { + for (size_t i = 0; i < hunspells.size(); ++i) { + delete hunspells[i].second; + } + hunspells.clear(); + + return AddDictionary(language, dirname); +} + std::vector HunspellSpellchecker::GetAvailableDictionaries(const std::string& path) { return std::vector(); } bool HunspellSpellchecker::IsMisspelled(const std::string& word) { - if (!hunspell) { - return false; + for (size_t i = 0; i < hunspells.size(); ++i) { + Hunspell* hunspell = hunspells[i].second; + bool misspelled = hunspell->spell(word.c_str()) == 0; + if (!misspelled) { + return false; + } } - return hunspell->spell(word.c_str()) == 0; + + return true; } std::vector HunspellSpellchecker::CheckSpelling(const uint16_t *utf16_text, size_t utf16_length) { std::vector result; - if (!hunspell || !transcoder) { + if (hunspells.empty() || !transcoder) { return result; } @@ -80,7 +102,7 @@ std::vector HunspellSpellchecker::CheckSpelling(const uint16_t break; case in_separator: - if (iswalpha(c)) { + if (isAlpha(c)) { word_start = i; state = in_word; } else if (!iswpunct(c) && !iswspace(c)) { @@ -89,20 +111,29 @@ std::vector HunspellSpellchecker::CheckSpelling(const uint16_t break; case in_word: - if (c == '\'' && iswalpha(utf16_text[i + 1])) { + if (c == '\'' && isAlpha(utf16_text[i + 1])) { i++; } else if (c == 0 || iswpunct(c) || iswspace(c)) { state = in_separator; bool converted = TranscodeUTF16ToUTF8(transcoder, (char *)utf8_buffer.data(), utf8_buffer.size(), utf16_text + word_start, i - word_start); if (converted) { - if (hunspell->spell(utf8_buffer.data()) == 0) { + bool all_misspelled = true; + for (size_t i = 0; i < hunspells.size(); ++i) { + Hunspell* hunspell = hunspells[i].second; + bool misspelled = hunspell->spell(utf8_buffer.data()) == 0; + if (!misspelled) { + all_misspelled = false; + break; + } + } + if (all_misspelled) { MisspelledRange range; range.start = word_start; range.end = i; result.push_back(range); } } - } else if (!iswalpha(c)) { + } else if (!isAlpha(c)) { state = unknown; } break; @@ -113,13 +144,15 @@ std::vector HunspellSpellchecker::CheckSpelling(const uint16_t } void HunspellSpellchecker::Add(const std::string& word) { - if (hunspell) { + if (!hunspells.empty()) { + Hunspell* hunspell = hunspells[0].second; hunspell->add(word.c_str()); } } void HunspellSpellchecker::Remove(const std::string& word) { - if (hunspell) { + if (!hunspells.empty()) { + Hunspell* hunspell = hunspells[0].second; hunspell->remove(word.c_str()); } } @@ -127,7 +160,9 @@ void HunspellSpellchecker::Remove(const std::string& word) { std::vector HunspellSpellchecker::GetCorrectionsForMisspelling(const std::string& word) { std::vector corrections; - if (hunspell) { + for (size_t i = 0; i < hunspells.size(); ++i) { + Hunspell* hunspell = hunspells[i].second; + char** slist; int size = hunspell->suggest(&slist, word.c_str()); @@ -141,4 +176,17 @@ std::vector HunspellSpellchecker::GetCorrectionsForMisspelling(cons return corrections; } +bool HunspellSpellchecker::isAlpha(std::wint_t c) const { + if (iswalpha(c)) { + return true; + } + for (size_t i = 0; i < hunspells.size(); ++i) { + std::locale loc = hunspells[i].first; + if (std::isalpha((wchar_t)c, loc)) { + return true; + } + } + return false; +} + } // namespace spellchecker diff --git a/src/spellchecker_hunspell.h b/src/spellchecker_hunspell.h index bcc9d65..7ab0d13 100644 --- a/src/spellchecker_hunspell.h +++ b/src/spellchecker_hunspell.h @@ -13,6 +13,7 @@ class HunspellSpellchecker : public SpellcheckerImplementation { HunspellSpellchecker(); ~HunspellSpellchecker(); + bool AddDictionary(const std::string& language, const std::string& path); bool SetDictionary(const std::string& language, const std::string& path); std::vector GetAvailableDictionaries(const std::string& path); std::vector GetCorrectionsForMisspelling(const std::string& word); @@ -22,7 +23,9 @@ class HunspellSpellchecker : public SpellcheckerImplementation { void Remove(const std::string& word); private: - Hunspell* hunspell; + bool isAlpha(std::wint_t c) const; + + std::vector> hunspells; Transcoder *transcoder; }; diff --git a/src/spellchecker_mac.h b/src/spellchecker_mac.h index 0d692d5..fc71ef4 100644 --- a/src/spellchecker_mac.h +++ b/src/spellchecker_mac.h @@ -13,6 +13,7 @@ class MacSpellchecker : public SpellcheckerImplementation { MacSpellchecker(); ~MacSpellchecker(); + bool AddDictionary(const std::string& language, const std::string& path); bool SetDictionary(const std::string& language, const std::string& path); std::vector GetAvailableDictionaries(const std::string& path); std::vector GetCorrectionsForMisspelling(const std::string& word); @@ -20,7 +21,7 @@ class MacSpellchecker : public SpellcheckerImplementation { std::vector CheckSpelling(const uint16_t *text, size_t length); void Add(const std::string& word); void Remove(const std::string& word); - + private: NSSpellChecker* spellChecker; NSString* spellCheckerLanguage; diff --git a/src/spellchecker_mac.mm b/src/spellchecker_mac.mm index a80e803..0342d9d 100644 --- a/src/spellchecker_mac.mm +++ b/src/spellchecker_mac.mm @@ -18,6 +18,11 @@ this->spellCheckerLanguage = nil; } +bool MacSpellchecker::AddDictionary(const std::string& language, const std::string& path) { + // TODO: write appropriate method + return this->SetDictionary(language, path); +} + bool MacSpellchecker::SetDictionary(const std::string& language, const std::string& path) { @autoreleasepool { [this->spellCheckerLanguage release]; diff --git a/src/spellchecker_win.cc b/src/spellchecker_win.cc index b0d0406..0fbea9f 100644 --- a/src/spellchecker_win.cc +++ b/src/spellchecker_win.cc @@ -20,6 +20,10 @@ namespace spellchecker { LONG g_COMRefcount = 0; bool g_COMFailed = false; +static bool compareMisspelledRanges(const MisspelledRange& lhs, const MisspelledRange& rhs) { + return lhs.start < rhs.start; +} + std::string ToUTF8(const std::wstring& string) { if (string.length() < 1) { return std::string(); @@ -56,7 +60,6 @@ std::wstring ToWString(const std::string& string) { WindowsSpellchecker::WindowsSpellchecker() { this->spellcheckerFactory = NULL; - this->currentSpellchecker = NULL; if (InterlockedIncrement(&g_COMRefcount) == 1) { g_COMFailed = FAILED(CoInitialize(NULL)); @@ -74,10 +77,11 @@ WindowsSpellchecker::WindowsSpellchecker() { } WindowsSpellchecker::~WindowsSpellchecker() { - if (this->currentSpellchecker) { - this->currentSpellchecker->Release(); - this->currentSpellchecker = NULL; + for (size_t i = 0; i < this->currentSpellcheckers.size(); ++i) { + ISpellChecker* currentSpellchecker = this->currentSpellcheckers[i]; + currentSpellchecker->Release(); } + this->currentSpellcheckers.clear(); if (this->spellcheckerFactory) { this->spellcheckerFactory->Release(); @@ -93,16 +97,11 @@ bool WindowsSpellchecker::IsSupported() { return !(g_COMFailed || (this->spellcheckerFactory == NULL)); } -bool WindowsSpellchecker::SetDictionary(const std::string& language, const std::string& path) { +bool WindowsSpellchecker::AddDictionary(const std::string& language, const std::string& path) { if (!this->spellcheckerFactory) { return false; } - if (this->currentSpellchecker != NULL) { - this->currentSpellchecker->Release(); - this->currentSpellchecker = NULL; - } - // Figure out if we have a dictionary installed for the language they want // NB: Hunspell uses underscore to separate language and locale, and Win8 uses // dash - if they use the wrong one, just silently replace it for them @@ -118,13 +117,29 @@ bool WindowsSpellchecker::SetDictionary(const std::string& language, const std:: if (!isSupported) return false; - if (FAILED(this->spellcheckerFactory->CreateSpellChecker(wlanguage.c_str(), &this->currentSpellchecker))) { + ISpellChecker* currentSpellchecker = NULL; + if (FAILED(this->spellcheckerFactory->CreateSpellChecker(wlanguage.c_str(), ¤tSpellchecker))) { return false; } + this->currentSpellcheckers.push_back(currentSpellchecker); return true; } +bool WindowsSpellchecker::SetDictionary(const std::string& language, const std::string& path) { + if (!this->spellcheckerFactory) { + return false; + } + + for (size_t i = 0; i < this->currentSpellcheckers.size(); ++i) { + ISpellChecker* currentSpellchecker = this->currentSpellcheckers[i]; + currentSpellchecker->Release(); + } + this->currentSpellcheckers.clear(); + + return AddDictionary(language, path); +} + std::vector WindowsSpellchecker::GetAvailableDictionaries(const std::string& path) { HRESULT hr; @@ -152,78 +167,107 @@ std::vector WindowsSpellchecker::GetAvailableDictionaries(const std } bool WindowsSpellchecker::IsMisspelled(const std::string& word) { - if (this->currentSpellchecker == NULL) { + if (this->currentSpellcheckers.empty()) { return false; } IEnumSpellingError* errors = NULL; std::wstring wword = ToWString(word); - if (FAILED(this->currentSpellchecker->Check(wword.c_str(), &errors))) { - return false; - } - bool ret; - - ISpellingError* dontcare; - HRESULT hr = errors->Next(&dontcare); - - switch (hr) { - case S_OK: - // S_OK == There are errors to examine - ret = true; - dontcare->Release(); - break; - case S_FALSE: - // Worked, but error free - ret = false; - break; - default: - // Something went pear-shaped - ret = false; - break; + for (size_t i = 0; i < this->currentSpellcheckers.size(); ++i) { + ISpellChecker* currentSpellchecker = this->currentSpellcheckers[i]; + errors = NULL; + if (FAILED(currentSpellchecker->Check(wword.c_str(), &errors))) { + continue; + } + + ISpellingError* dontcare; + HRESULT hr = errors->Next(&dontcare); + + switch (hr) { + case S_OK: + // S_OK == There are errors to examine + ret = true; + dontcare->Release(); + break; + case S_FALSE: + // Worked, but error free + ret = false; + break; + default: + // Something went pear-shaped + ret = false; + break; + } + + errors->Release(); + + if (ret == false) { + break; + } } - errors->Release(); return ret; } std::vector WindowsSpellchecker::CheckSpelling(const uint16_t *text, size_t length) { std::vector result; - if (this->currentSpellchecker == NULL) { + if (this->currentSpellcheckers.empty()) { return result; } IEnumSpellingError* errors = NULL; std::wstring wtext(reinterpret_cast(text), length); - if (FAILED(this->currentSpellchecker->Check(wtext.c_str(), &errors))) { - return result; - } - ISpellingError *error; - while (errors->Next(&error) == S_OK) { - ULONG start, length; - error->get_StartIndex(&start); - error->get_Length(&length); - - MisspelledRange range; - range.start = start; - range.end = start + length; - result.push_back(range); - error->Release(); + for (size_t i = 0; i < this->currentSpellcheckers.size(); ++i) { + ISpellChecker* currentSpellchecker = this->currentSpellcheckers[i]; + if (FAILED(currentSpellchecker->Check(wtext.c_str(), &errors))) { + continue; + } + + std::vector currentResult; + + ISpellingError* error; + while (errors->Next(&error) == S_OK) { + ULONG start, length; + error->get_StartIndex(&start); + error->get_Length(&length); + + MisspelledRange range; + range.start = start; + range.end = start + length; + currentResult.push_back(range); + error->Release(); + } + + errors->Release(); + + if (currentResult.empty()) { + return std::vector(); + } + if (result.empty()) { + std::swap(result, currentResult); + } else { + std::vector intersection; + std::set_intersection( + result.begin(), result.end(), + currentResult.begin(), currentResult.end(), + std::back_inserter(intersection), compareMisspelledRanges); + std::swap(result, intersection); + } } - errors->Release(); return result; } void WindowsSpellchecker::Add(const std::string& word) { - if (this->currentSpellchecker == NULL) { + if (this->currentSpellcheckers.empty()) { return; } std::wstring wword = ToWString(word); - this->currentSpellchecker->Add(wword.c_str()); + this->currentSpellcheckers[0]->Add(wword.c_str()); } void WindowsSpellchecker::Remove(const std::string& word) { @@ -233,37 +277,41 @@ void WindowsSpellchecker::Remove(const std::string& word) { std::vector WindowsSpellchecker::GetCorrectionsForMisspelling(const std::string& word) { - if (this->currentSpellchecker == NULL) { + if (this->currentSpellcheckers.empty()) { return std::vector(); } std::wstring& wword = ToWString(word); IEnumString* words = NULL; + std::vector ret; - HRESULT hr = this->currentSpellchecker->Suggest(wword.c_str(), &words); + for (size_t i = 0; i < this->currentSpellcheckers.size(); ++i) { + ISpellChecker* currentSpellchecker = this->currentSpellcheckers[i]; + words = NULL; + HRESULT hr = currentSpellchecker->Suggest(wword.c_str(), &words); - if (FAILED(hr)) { - return std::vector(); - } + if (FAILED(hr)) { + continue; + } - // NB: S_FALSE == word is spelled correctly - if (hr == S_FALSE) { - words->Release(); - return std::vector(); - } + // NB: S_FALSE == word is spelled correctly + if (hr == S_FALSE) { + words->Release(); + continue; + } - std::vector ret; + LPOLESTR correction; + while (words->Next(1, &correction, NULL) == S_OK) { + std::wstring wcorr; + wcorr.assign(correction); + ret.push_back(ToUTF8(wcorr)); - LPOLESTR correction; - while (words->Next(1, &correction, NULL) == S_OK) { - std::wstring wcorr; - wcorr.assign(correction); - ret.push_back(ToUTF8(wcorr)); + CoTaskMemFree(correction); + } - CoTaskMemFree(correction); + words->Release(); } - words->Release(); return ret; } diff --git a/src/spellchecker_win.h b/src/spellchecker_win.h index c5bbefb..ceb24b9 100644 --- a/src/spellchecker_win.h +++ b/src/spellchecker_win.h @@ -13,6 +13,7 @@ class WindowsSpellchecker : public SpellcheckerImplementation { WindowsSpellchecker(); ~WindowsSpellchecker(); + bool AddDictionary(const std::string& language, const std::string& path); bool SetDictionary(const std::string& language, const std::string& path); std::vector GetAvailableDictionaries(const std::string& path); @@ -23,7 +24,7 @@ class WindowsSpellchecker : public SpellcheckerImplementation { void Remove(const std::string& word); private: - ISpellChecker* currentSpellchecker; + std::vector currentSpellcheckers; ISpellCheckerFactory* spellcheckerFactory; };