| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/renderer/spellchecker/spellcheck.h" |
| |
| #include <algorithm> |
| |
| #include "base/basictypes.h" |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/sys_info.h" |
| #include "base/thread_task_runner_handle.h" |
| #include "chrome/common/channel_info.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/spellcheck_common.h" |
| #include "chrome/common/spellcheck_messages.h" |
| #include "chrome/common/spellcheck_result.h" |
| #include "chrome/renderer/spellchecker/spellcheck_language.h" |
| #include "chrome/renderer/spellchecker/spellcheck_provider.h" |
| #include "components/version_info/version_info.h" |
| #include "content/public/renderer/render_thread.h" |
| #include "content/public/renderer/render_view.h" |
| #include "content/public/renderer/render_view_visitor.h" |
| #include "ipc/ipc_platform_file.h" |
| #include "third_party/WebKit/public/platform/WebString.h" |
| #include "third_party/WebKit/public/platform/WebVector.h" |
| #include "third_party/WebKit/public/web/WebTextCheckingCompletion.h" |
| #include "third_party/WebKit/public/web/WebTextCheckingResult.h" |
| #include "third_party/WebKit/public/web/WebTextDecorationType.h" |
| #include "third_party/WebKit/public/web/WebView.h" |
| |
| using blink::WebVector; |
| using blink::WebString; |
| using blink::WebTextCheckingResult; |
| using blink::WebTextDecorationType; |
| |
| namespace { |
| const int kNoOffset = 0; |
| const int kNoTag = 0; |
| |
| class UpdateSpellcheckEnabled : public content::RenderViewVisitor { |
| public: |
| explicit UpdateSpellcheckEnabled(bool enabled) : enabled_(enabled) {} |
| bool Visit(content::RenderView* render_view) override; |
| |
| private: |
| bool enabled_; // New spellcheck-enabled state. |
| DISALLOW_COPY_AND_ASSIGN(UpdateSpellcheckEnabled); |
| }; |
| |
| bool UpdateSpellcheckEnabled::Visit(content::RenderView* render_view) { |
| SpellCheckProvider* provider = SpellCheckProvider::Get(render_view); |
| DCHECK(provider); |
| provider->EnableSpellcheck(enabled_); |
| return true; |
| } |
| |
| class DocumentMarkersCollector : public content::RenderViewVisitor { |
| public: |
| DocumentMarkersCollector() {} |
| ~DocumentMarkersCollector() override {} |
| const std::vector<uint32>& markers() const { return markers_; } |
| bool Visit(content::RenderView* render_view) override; |
| |
| private: |
| std::vector<uint32> markers_; |
| DISALLOW_COPY_AND_ASSIGN(DocumentMarkersCollector); |
| }; |
| |
| bool DocumentMarkersCollector::Visit(content::RenderView* render_view) { |
| if (!render_view || !render_view->GetWebView()) |
| return true; |
| WebVector<uint32> markers; |
| render_view->GetWebView()->spellingMarkers(&markers); |
| for (size_t i = 0; i < markers.size(); ++i) |
| markers_.push_back(markers[i]); |
| // Visit all render views. |
| return true; |
| } |
| |
| class DocumentMarkersRemover : public content::RenderViewVisitor { |
| public: |
| explicit DocumentMarkersRemover(const std::set<std::string>& words); |
| ~DocumentMarkersRemover() override {} |
| bool Visit(content::RenderView* render_view) override; |
| |
| private: |
| WebVector<WebString> words_; |
| DISALLOW_COPY_AND_ASSIGN(DocumentMarkersRemover); |
| }; |
| |
| DocumentMarkersRemover::DocumentMarkersRemover( |
| const std::set<std::string>& words) |
| : words_(words.size()) { |
| std::transform(words.begin(), words.end(), words_.begin(), |
| [](const std::string& w) { return WebString::fromUTF8(w); }); |
| } |
| |
| bool DocumentMarkersRemover::Visit(content::RenderView* render_view) { |
| if (render_view && render_view->GetWebView()) |
| render_view->GetWebView()->removeSpellingMarkersUnderWords(words_); |
| return true; |
| } |
| |
| bool IsApostrophe(base::char16 c) { |
| const base::char16 kApostrophe = 0x27; |
| const base::char16 kRightSingleQuotationMark = 0x2019; |
| return c == kApostrophe || c == kRightSingleQuotationMark; |
| } |
| |
| // Makes sure that the apostrophes in the |spelling_suggestion| are the same |
| // type as in the |misspelled_word| and in the same order. Ignore differences in |
| // the number of apostrophes. |
| void PreserveOriginalApostropheTypes(const base::string16& misspelled_word, |
| base::string16* spelling_suggestion) { |
| auto it = spelling_suggestion->begin(); |
| for (const base::char16& c : misspelled_word) { |
| if (IsApostrophe(c)) { |
| it = std::find_if(it, spelling_suggestion->end(), IsApostrophe); |
| if (it == spelling_suggestion->end()) |
| return; |
| |
| *it++ = c; |
| } |
| } |
| } |
| |
| } // namespace |
| |
| class SpellCheck::SpellcheckRequest { |
| public: |
| SpellcheckRequest(const base::string16& text, |
| blink::WebTextCheckingCompletion* completion) |
| : text_(text), completion_(completion) { |
| DCHECK(completion); |
| } |
| ~SpellcheckRequest() {} |
| |
| base::string16 text() { return text_; } |
| blink::WebTextCheckingCompletion* completion() { return completion_; } |
| |
| private: |
| base::string16 text_; // Text to be checked in this task. |
| |
| // The interface to send the misspelled ranges to WebKit. |
| blink::WebTextCheckingCompletion* completion_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SpellcheckRequest); |
| }; |
| |
| |
| // Initializes SpellCheck object. |
| // spellcheck_enabled_ currently MUST be set to true, due to peculiarities of |
| // the initialization sequence. |
| // Since it defaults to true, newly created SpellCheckProviders will enable |
| // spellchecking. After the first word is typed, the provider requests a check, |
| // which in turn triggers the delayed initialization sequence in SpellCheck. |
| // This does send a message to the browser side, which triggers the creation |
| // of the SpellcheckService. That does create the observer for the preference |
| // responsible for enabling/disabling checking, which allows subsequent changes |
| // to that preference to be sent to all SpellCheckProviders. |
| // Setting |spellcheck_enabled_| to false by default prevents that mechanism, |
| // and as such the SpellCheckProviders will never be notified of different |
| // values. |
| // TODO(groby): Simplify this. |
| SpellCheck::SpellCheck() |
| : auto_spell_correct_turned_on_(false), |
| spellcheck_enabled_(true) { |
| } |
| |
| SpellCheck::~SpellCheck() { |
| } |
| |
| void SpellCheck::FillSuggestions( |
| const std::vector<std::vector<base::string16>>& suggestions_list, |
| std::vector<base::string16>* optional_suggestions) { |
| DCHECK(optional_suggestions); |
| size_t num_languages = suggestions_list.size(); |
| |
| // Compute maximum number of suggestions in a single language. |
| size_t max_suggestions = 0; |
| for (const auto& suggestions : suggestions_list) |
| max_suggestions = std::max(max_suggestions, suggestions.size()); |
| |
| for (size_t count = 0; count < (max_suggestions * num_languages); ++count) { |
| size_t language = count % num_languages; |
| size_t index = count / num_languages; |
| |
| if (suggestions_list[language].size() <= index) |
| continue; |
| |
| const base::string16& suggestion = suggestions_list[language][index]; |
| // Only add the suggestion if it's unique. |
| if (std::find(optional_suggestions->begin(), optional_suggestions->end(), |
| suggestion) == optional_suggestions->end()) { |
| optional_suggestions->push_back(suggestion); |
| } |
| if (optional_suggestions->size() >= |
| chrome::spellcheck_common::kMaxSuggestions) { |
| break; |
| } |
| } |
| } |
| |
| bool SpellCheck::OnControlMessageReceived(const IPC::Message& message) { |
| bool handled = true; |
| IPC_BEGIN_MESSAGE_MAP(SpellCheck, message) |
| IPC_MESSAGE_HANDLER(SpellCheckMsg_Init, OnInit) |
| IPC_MESSAGE_HANDLER(SpellCheckMsg_CustomDictionaryChanged, |
| OnCustomDictionaryChanged) |
| IPC_MESSAGE_HANDLER(SpellCheckMsg_EnableAutoSpellCorrect, |
| OnEnableAutoSpellCorrect) |
| IPC_MESSAGE_HANDLER(SpellCheckMsg_EnableSpellCheck, OnEnableSpellCheck) |
| IPC_MESSAGE_HANDLER(SpellCheckMsg_RequestDocumentMarkers, |
| OnRequestDocumentMarkers) |
| IPC_MESSAGE_UNHANDLED(handled = false) |
| IPC_END_MESSAGE_MAP() |
| |
| return handled; |
| } |
| |
| void SpellCheck::OnInit( |
| const std::vector<SpellCheckBDictLanguage>& bdict_languages, |
| const std::set<std::string>& custom_words, |
| bool auto_spell_correct) { |
| languages_.clear(); |
| for (const auto& bdict_language : bdict_languages) { |
| AddSpellcheckLanguage( |
| IPC::PlatformFileForTransitToFile(bdict_language.file), |
| bdict_language.language); |
| } |
| |
| custom_dictionary_.Init(custom_words); |
| auto_spell_correct_turned_on_ = auto_spell_correct; |
| #if !defined(USE_BROWSER_SPELLCHECKER) |
| PostDelayedSpellCheckTask(pending_request_param_.release()); |
| #endif |
| } |
| |
| void SpellCheck::OnCustomDictionaryChanged( |
| const std::set<std::string>& words_added, |
| const std::set<std::string>& words_removed) { |
| custom_dictionary_.OnCustomDictionaryChanged(words_added, words_removed); |
| if (words_added.empty()) |
| return; |
| DocumentMarkersRemover markersRemover(words_added); |
| content::RenderView::ForEach(&markersRemover); |
| } |
| |
| void SpellCheck::OnEnableAutoSpellCorrect(bool enable) { |
| auto_spell_correct_turned_on_ = enable; |
| } |
| |
| void SpellCheck::OnEnableSpellCheck(bool enable) { |
| spellcheck_enabled_ = enable; |
| UpdateSpellcheckEnabled updater(enable); |
| content::RenderView::ForEach(&updater); |
| } |
| |
| void SpellCheck::OnRequestDocumentMarkers() { |
| DocumentMarkersCollector collector; |
| content::RenderView::ForEach(&collector); |
| content::RenderThread::Get()->Send( |
| new SpellCheckHostMsg_RespondDocumentMarkers(collector.markers())); |
| } |
| |
| // TODO(groby): Make sure we always have a spelling engine, even before |
| // AddSpellcheckLanguage() is called. |
| void SpellCheck::AddSpellcheckLanguage(base::File file, |
| const std::string& language) { |
| languages_.push_back(new SpellcheckLanguage()); |
| languages_.back()->Init(file.Pass(), language); |
| } |
| |
| bool SpellCheck::SpellCheckWord( |
| const base::char16* text_begin, |
| int position_in_text, |
| int text_length, |
| int tag, |
| int* misspelling_start, |
| int* misspelling_len, |
| std::vector<base::string16>* optional_suggestions) { |
| DCHECK(text_length >= position_in_text); |
| DCHECK(misspelling_start && misspelling_len) << "Out vars must be given."; |
| |
| // Do nothing if we need to delay initialization. (Rather than blocking, |
| // report the word as correctly spelled.) |
| if (InitializeIfNeeded()) |
| return true; |
| |
| // These are for holding misspelling or skippable word positions and lengths |
| // between calls to SpellcheckLanguage::SpellCheckWord. |
| int possible_misspelling_start; |
| int possible_misspelling_len; |
| // The longest sequence of text that all languages agree is skippable. |
| int agreed_skippable_len; |
| // A vector of vectors containing spelling suggestions from different |
| // languages. |
| std::vector<std::vector<base::string16>> suggestions_list; |
| // A vector to hold a language's misspelling suggestions between spellcheck |
| // calls. |
| std::vector<base::string16> language_suggestions; |
| |
| // This loop only advances if all languages agree that a sequence of text is |
| // skippable. |
| for (; position_in_text <= text_length; |
| position_in_text += agreed_skippable_len) { |
| // Reseting |agreed_skippable_len| to the worst-case length each time |
| // prevents some unnecessary iterations. |
| agreed_skippable_len = text_length; |
| *misspelling_start = 0; |
| *misspelling_len = 0; |
| suggestions_list.clear(); |
| |
| for (ScopedVector<SpellcheckLanguage>::iterator language = |
| languages_.begin(); |
| language != languages_.end();) { |
| language_suggestions.clear(); |
| SpellcheckLanguage::SpellcheckWordResult result = |
| (*language)->SpellCheckWord( |
| text_begin, position_in_text, text_length, tag, |
| &possible_misspelling_start, &possible_misspelling_len, |
| optional_suggestions ? &language_suggestions : nullptr); |
| |
| switch (result) { |
| case SpellcheckLanguage::SpellcheckWordResult::IS_CORRECT: |
| *misspelling_start = 0; |
| *misspelling_len = 0; |
| return true; |
| case SpellcheckLanguage::SpellcheckWordResult::IS_SKIPPABLE: |
| agreed_skippable_len = |
| std::min(agreed_skippable_len, possible_misspelling_len); |
| // If true, this means the spellchecker moved past a word that was |
| // previously determined to be misspelled or skippable, which means |
| // another spellcheck language marked it as correct. |
| if (position_in_text != possible_misspelling_start) { |
| *misspelling_len = 0; |
| position_in_text = possible_misspelling_start; |
| suggestions_list.clear(); |
| language = languages_.begin(); |
| } else { |
| language++; |
| } |
| break; |
| case SpellcheckLanguage::SpellcheckWordResult::IS_MISSPELLED: |
| *misspelling_start = possible_misspelling_start; |
| *misspelling_len = possible_misspelling_len; |
| // If true, this means the spellchecker moved past a word that was |
| // previously determined to be misspelled or skippable, which means |
| // another spellcheck language marked it as correct. |
| if (position_in_text != *misspelling_start) { |
| suggestions_list.clear(); |
| language = languages_.begin(); |
| position_in_text = *misspelling_start; |
| } else { |
| suggestions_list.push_back(language_suggestions); |
| language++; |
| } |
| break; |
| } |
| } |
| |
| // If |*misspelling_len| is non-zero, that means at least one language |
| // marked a word misspelled and no other language considered it correct. |
| if (*misspelling_len != 0) { |
| if (optional_suggestions) |
| FillSuggestions(suggestions_list, optional_suggestions); |
| return false; |
| } |
| } |
| |
| NOTREACHED(); |
| return true; |
| } |
| |
| bool SpellCheck::SpellCheckParagraph( |
| const base::string16& text, |
| WebVector<WebTextCheckingResult>* results) { |
| #if !defined(USE_BROWSER_SPELLCHECKER) |
| // Mac and Android have their own spell checkers,so this method won't be used |
| DCHECK(results); |
| std::vector<WebTextCheckingResult> textcheck_results; |
| size_t length = text.length(); |
| size_t position_in_text = 0; |
| |
| // Spellcheck::SpellCheckWord() automatically breaks text into words and |
| // checks the spellings of the extracted words. This function sets the |
| // position and length of the first misspelled word and returns false when |
| // the text includes misspelled words. Therefore, we just repeat calling the |
| // function until it returns true to check the whole text. |
| int misspelling_start = 0; |
| int misspelling_length = 0; |
| while (position_in_text <= length) { |
| if (SpellCheckWord(text.c_str(), |
| position_in_text, |
| length, |
| kNoTag, |
| &misspelling_start, |
| &misspelling_length, |
| NULL)) { |
| results->assign(textcheck_results); |
| return true; |
| } |
| |
| if (!custom_dictionary_.SpellCheckWord( |
| text, misspelling_start, misspelling_length)) { |
| base::string16 replacement; |
| textcheck_results.push_back(WebTextCheckingResult( |
| blink::WebTextDecorationTypeSpelling, |
| misspelling_start, |
| misspelling_length, |
| replacement)); |
| } |
| position_in_text = misspelling_start + misspelling_length; |
| } |
| results->assign(textcheck_results); |
| return false; |
| #else |
| // This function is only invoked for spell checker functionality that runs |
| // on the render thread. OSX and Android builds don't have that. |
| NOTREACHED(); |
| return true; |
| #endif |
| } |
| |
| base::string16 SpellCheck::GetAutoCorrectionWord(const base::string16& word, |
| int tag) { |
| base::string16 autocorrect_word; |
| if (!auto_spell_correct_turned_on_) |
| return autocorrect_word; // Return the empty string. |
| |
| int word_length = static_cast<int>(word.size()); |
| if (word_length < 2 || |
| word_length > chrome::spellcheck_common::kMaxAutoCorrectWordSize) |
| return autocorrect_word; |
| |
| if (InitializeIfNeeded()) |
| return autocorrect_word; |
| |
| base::char16 misspelled_word[ |
| chrome::spellcheck_common::kMaxAutoCorrectWordSize + 1]; |
| const base::char16* word_char = word.c_str(); |
| for (int i = 0; i <= chrome::spellcheck_common::kMaxAutoCorrectWordSize; |
| ++i) { |
| if (i >= word_length) |
| misspelled_word[i] = 0; |
| else |
| misspelled_word[i] = word_char[i]; |
| } |
| |
| // Swap adjacent characters and spellcheck. |
| int misspelling_start, misspelling_len; |
| for (int i = 0; i < word_length - 1; i++) { |
| // Swap. |
| std::swap(misspelled_word[i], misspelled_word[i + 1]); |
| |
| // Check spelling. |
| misspelling_start = misspelling_len = 0; |
| SpellCheckWord(misspelled_word, kNoOffset, word_length, tag, |
| &misspelling_start, &misspelling_len, NULL); |
| |
| // Make decision: if only one swap produced a valid word, then we want to |
| // return it. If we found two or more, we don't do autocorrection. |
| if (misspelling_len == 0) { |
| if (autocorrect_word.empty()) { |
| autocorrect_word.assign(misspelled_word); |
| } else { |
| autocorrect_word.clear(); |
| break; |
| } |
| } |
| |
| // Restore the swapped characters. |
| std::swap(misspelled_word[i], misspelled_word[i + 1]); |
| } |
| return autocorrect_word; |
| } |
| |
| // OSX and Android use their own spell checkers |
| #if !defined(USE_BROWSER_SPELLCHECKER) |
| void SpellCheck::RequestTextChecking( |
| const base::string16& text, |
| blink::WebTextCheckingCompletion* completion) { |
| // Clean up the previous request before starting a new request. |
| if (pending_request_param_.get()) |
| pending_request_param_->completion()->didCancelCheckingText(); |
| |
| pending_request_param_.reset(new SpellcheckRequest( |
| text, completion)); |
| // We will check this text after we finish loading the hunspell dictionary. |
| if (InitializeIfNeeded()) |
| return; |
| |
| PostDelayedSpellCheckTask(pending_request_param_.release()); |
| } |
| #endif |
| |
| bool SpellCheck::InitializeIfNeeded() { |
| if (languages_.empty()) |
| return true; |
| |
| bool initialize_if_needed = false; |
| for (SpellcheckLanguage* language : languages_) |
| initialize_if_needed |= language->InitializeIfNeeded(); |
| |
| return initialize_if_needed; |
| } |
| |
| // OSX and Android don't have |pending_request_param_| |
| #if !defined(USE_BROWSER_SPELLCHECKER) |
| void SpellCheck::PostDelayedSpellCheckTask(SpellcheckRequest* request) { |
| if (!request) |
| return; |
| |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::Bind(&SpellCheck::PerformSpellCheck, AsWeakPtr(), |
| base::Owned(request))); |
| } |
| #endif |
| |
| // Mac and Android use their platform engines instead. |
| #if !defined(USE_BROWSER_SPELLCHECKER) |
| void SpellCheck::PerformSpellCheck(SpellcheckRequest* param) { |
| DCHECK(param); |
| |
| if (languages_.empty() || |
| std::find_if(languages_.begin(), languages_.end(), |
| [](SpellcheckLanguage* language) { |
| return !language->IsEnabled(); |
| }) != languages_.end()) { |
| param->completion()->didCancelCheckingText(); |
| } else { |
| WebVector<blink::WebTextCheckingResult> results; |
| SpellCheckParagraph(param->text(), &results); |
| param->completion()->didFinishCheckingText(results); |
| } |
| } |
| #endif |
| |
| void SpellCheck::CreateTextCheckingResults( |
| ResultFilter filter, |
| int line_offset, |
| const base::string16& line_text, |
| const std::vector<SpellCheckResult>& spellcheck_results, |
| WebVector<WebTextCheckingResult>* textcheck_results) { |
| DCHECK(!line_text.empty()); |
| |
| std::vector<WebTextCheckingResult> results; |
| for (const SpellCheckResult& spellcheck_result : spellcheck_results) { |
| DCHECK_LE(static_cast<size_t>(spellcheck_result.location), |
| line_text.length()); |
| DCHECK_LE(static_cast<size_t>(spellcheck_result.location + |
| spellcheck_result.length), |
| line_text.length()); |
| |
| const base::string16& misspelled_word = |
| line_text.substr(spellcheck_result.location, spellcheck_result.length); |
| base::string16 replacement = spellcheck_result.replacement; |
| SpellCheckResult::Decoration decoration = spellcheck_result.decoration; |
| |
| // Ignore words in custom dictionary. |
| if (custom_dictionary_.SpellCheckWord(misspelled_word, 0, |
| misspelled_word.length())) { |
| continue; |
| } |
| |
| // Use the same types of appostrophes as in the mispelled word. |
| PreserveOriginalApostropheTypes(misspelled_word, &replacement); |
| |
| // Ignore misspellings due the typographical apostrophe. |
| if (misspelled_word == replacement) |
| continue; |
| |
| if (filter == USE_NATIVE_CHECKER) { |
| // Double-check misspelled words with out spellchecker and attach grammar |
| // markers to them if our spellchecker tells us they are correct words, |
| // i.e. they are probably contextually-misspelled words. |
| int unused_misspelling_start = 0; |
| int unused_misspelling_length = 0; |
| if (decoration == SpellCheckResult::SPELLING && |
| SpellCheckWord(misspelled_word.c_str(), kNoOffset, |
| misspelled_word.length(), kNoTag, |
| &unused_misspelling_start, &unused_misspelling_length, |
| nullptr)) { |
| decoration = SpellCheckResult::GRAMMAR; |
| } |
| } |
| |
| results.push_back(WebTextCheckingResult( |
| static_cast<WebTextDecorationType>(decoration), |
| line_offset + spellcheck_result.location, spellcheck_result.length, |
| replacement, spellcheck_result.hash)); |
| } |
| |
| textcheck_results->assign(results); |
| } |
| |
| bool SpellCheck::IsSpellcheckEnabled() { |
| #if defined(OS_ANDROID) |
| if (base::SysInfo::IsLowEndDevice()) |
| return false; |
| |
| version_info::Channel channel = chrome::GetChannel(); |
| if (channel == version_info::Channel::DEV || |
| channel == version_info::Channel::CANARY) { |
| return true; |
| } else if (!base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kEnableAndroidSpellChecker)) { |
| return false; |
| } |
| #endif |
| return spellcheck_enabled_; |
| } |