// 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 "components/spellcheck/renderer/spellcheck.h" #include #include #include #include #include "base/bind.h" #include "base/command_line.h" #include "base/location.h" #include "base/logging.h" #include "base/macros.h" #include "base/single_thread_task_runner.h" #include "base/stl_util.h" #include "base/threading/thread_task_runner_handle.h" #include "build/build_config.h" #include "components/spellcheck/common/spellcheck_common.h" #include "components/spellcheck/common/spellcheck_features.h" #include "components/spellcheck/common/spellcheck_result.h" #include "components/spellcheck/common/spellcheck_switches.h" #include "components/spellcheck/renderer/spellcheck_language.h" #include "components/spellcheck/renderer/spellcheck_provider.h" #include "components/spellcheck/spellcheck_build_features.h" #include "content/public/common/service_manager_connection.h" #include "content/public/common/simple_connection_filter.h" #include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_frame_visitor.h" #include "content/public/renderer/render_thread.h" #include "services/service_manager/public/cpp/bind_source_info.h" #include "services/service_manager/public/cpp/binder_registry.h" #include "third_party/WebKit/public/platform/WebString.h" #include "third_party/WebKit/public/platform/WebVector.h" #include "third_party/WebKit/public/web/WebLocalFrame.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" 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::RenderFrameVisitor { public: explicit UpdateSpellcheckEnabled(bool enabled) : enabled_(enabled) {} bool Visit(content::RenderFrame* render_frame) override; private: bool enabled_; // New spellcheck-enabled state. DISALLOW_COPY_AND_ASSIGN(UpdateSpellcheckEnabled); }; bool UpdateSpellcheckEnabled::Visit(content::RenderFrame* render_frame) { SpellCheckProvider* provider = SpellCheckProvider::Get(render_frame); DCHECK(provider); provider->EnableSpellcheck(enabled_); return true; } class DocumentMarkersRemover : public content::RenderFrameVisitor { public: explicit DocumentMarkersRemover(const std::set& words); ~DocumentMarkersRemover() override {} bool Visit(content::RenderFrame* render_frame) override; private: WebVector words_; DISALLOW_COPY_AND_ASSIGN(DocumentMarkersRemover); }; DocumentMarkersRemover::DocumentMarkersRemover( const std::set& words) : words_(words.size()) { std::transform(words.begin(), words.end(), words_.begin(), [](const std::string& w) { return WebString::FromUTF8(w); }); } bool DocumentMarkersRemover::Visit(content::RenderFrame* render_frame) { // TODO(xiaochengh): Both nullptr checks seem unnecessary. if (render_frame && render_frame->GetWebFrame()) render_frame->GetWebFrame()->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() : spellcheck_enabled_(true) { if (!content::ChildThread::Get()) return; // Can be NULL in tests. auto* service_manager_connection = content::ChildThread::Get()->GetServiceManagerConnection(); DCHECK(service_manager_connection); auto registry = base::MakeUnique(); registry->AddInterface( base::Bind(&SpellCheck::SpellCheckerRequest, base::Unretained(this)), base::ThreadTaskRunnerHandle::Get()); service_manager_connection->AddConnectionFilter( base::MakeUnique(std::move(registry))); } SpellCheck::~SpellCheck() { } void SpellCheck::FillSuggestions( const std::vector>& suggestions_list, std::vector* 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 (!base::ContainsValue(*optional_suggestions, suggestion)) { optional_suggestions->push_back(suggestion); } if (optional_suggestions->size() >= spellcheck::kMaxSuggestions) { break; } } } void SpellCheck::SpellCheckerRequest( const service_manager::BindSourceInfo& source_info, spellcheck::mojom::SpellCheckerRequest request) { spellchecker_bindings_.AddBinding(this, std::move(request)); } void SpellCheck::Initialize( std::vector dictionaries, const std::vector& custom_words, bool enable) { languages_.clear(); for (const auto& dictionary : dictionaries) AddSpellcheckLanguage(std::move(dictionary->file), dictionary->language); custom_dictionary_.Init( std::set(custom_words.begin(), custom_words.end())); #if !BUILDFLAG(USE_BROWSER_SPELLCHECKER) PostDelayedSpellCheckTask(pending_request_param_.release()); #endif spellcheck_enabled_ = enable; UpdateSpellcheckEnabled updater(enable); content::RenderFrame::ForEach(&updater); } void SpellCheck::CustomDictionaryChanged( const std::vector& words_added, const std::vector& words_removed) { const std::set added(words_added.begin(), words_added.end()); custom_dictionary_.OnCustomDictionaryChanged( added, std::set(words_removed.begin(), words_removed.end())); if (added.empty()) return; DocumentMarkersRemover markersRemover(added); content::RenderFrame::ForEach(&markersRemover); } // 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(base::MakeUnique()); languages_.back()->Init(std::move(file), 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* 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> suggestions_list; // A vector to hold a language's misspelling suggestions between spellcheck // calls. std::vector 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 (auto 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* results) { #if !BUILDFLAG(USE_BROWSER_SPELLCHECKER) // Mac and Android have their own spell checkers,so this method won't be used DCHECK(results); std::vector 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)) { textcheck_results.push_back( WebTextCheckingResult(blink::kWebTextDecorationTypeSpelling, misspelling_start, misspelling_length)); } 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 } // OSX and Android use their own spell checkers #if !BUILDFLAG(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 (auto& language : languages_) initialize_if_needed |= language->InitializeIfNeeded(); return initialize_if_needed; } // OSX and Android don't have |pending_request_param_| #if !BUILDFLAG(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 !BUILDFLAG(USE_BROWSER_SPELLCHECKER) void SpellCheck::PerformSpellCheck(SpellcheckRequest* param) { DCHECK(param); if (languages_.empty() || std::find_if(languages_.begin(), languages_.end(), [](std::unique_ptr& language) { return !language->IsEnabled(); }) != languages_.end()) { param->completion()->DidCancelCheckingText(); } else { WebVector 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& spellcheck_results, WebVector* textcheck_results) { DCHECK(!line_text.empty()); std::vector results; for (const SpellCheckResult& spellcheck_result : spellcheck_results) { DCHECK_LE(static_cast(spellcheck_result.location), line_text.length()); DCHECK_LE(static_cast(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(decoration), line_offset + spellcheck_result.location, spellcheck_result.length, blink::WebString::FromUTF16(replacement))); } textcheck_results->Assign(results); } bool SpellCheck::IsSpellcheckEnabled() { #if defined(OS_ANDROID) if (!spellcheck::IsAndroidSpellCheckFeatureEnabled()) return false; #endif return spellcheck_enabled_; }