// Copyright 2012 The Chromium Authors
// 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 <stddef.h>
#include <stdint.h>

#include <algorithm>
#include <memory>
#include <string_view>
#include <utility>
#include <vector>

#include "base/check_op.h"
#include "base/command_line.h"
#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.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/renderer/spellcheck_language.h"
#include "components/spellcheck/renderer/spellcheck_provider.h"
#include "components/spellcheck/renderer/spellcheck_renderer_metrics.h"
#include "components/spellcheck/spellcheck_buildflags.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_frame_visitor.h"
#include "content/public/renderer/render_thread.h"
#include "third_party/blink/public/platform/web_runtime_features.h"
#include "third_party/blink/public/platform/web_string.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_text_check_client.h"
#include "third_party/blink/public/web/web_text_checking_completion.h"
#include "third_party/blink/public/web/web_text_checking_result.h"
#include "third_party/blink/public/web/web_text_decoration_type.h"

using blink::WebString;
using blink::WebTextCheckingResult;
using blink::WebTextDecorationType;

namespace {

class UpdateSpellcheckEnabled : public content::RenderFrameVisitor {
 public:
  explicit UpdateSpellcheckEnabled(bool enabled) : enabled_(enabled) {}

  UpdateSpellcheckEnabled(const UpdateSpellcheckEnabled&) = delete;
  UpdateSpellcheckEnabled& operator=(const UpdateSpellcheckEnabled&) = delete;

  bool Visit(content::RenderFrame* render_frame) override;

 private:
  bool enabled_;  // New spellcheck-enabled state.
};

bool UpdateSpellcheckEnabled::Visit(content::RenderFrame* render_frame) {
  if (!enabled_) {
    if (render_frame && render_frame->GetWebFrame())
      render_frame->GetWebFrame()->RemoveSpellingMarkers();
  }
  return true;
}

std::vector<WebString> ConvertToWebStringFromUtf8(
    const std::set<std::string>& words) {
  return base::ToVector(words, &WebString::FromUtf8);
}

bool IsApostrophe(char16_t c) {
  const char16_t kApostrophe = 0x27;
  const char16_t 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 std::u16string& misspelled_word,
                                     std::u16string* spelling_suggestion) {
  auto it = spelling_suggestion->begin();
  for (const char16_t& c : misspelled_word) {
    if (IsApostrophe(c)) {
      it = std::find_if(it, spelling_suggestion->end(), IsApostrophe);
      if (it == spelling_suggestion->end())
        return;

      *it++ = c;
    }
  }
}

std::vector<WebString> FilterReplacementSuggestions(
    const std::u16string& misspelled_word,
    const std::vector<std::u16string>& replacements) {
  std::vector<WebString> replacements_filtered;
  for (std::u16string replacement : replacements) {
    // Use the same types of apostrophes as in the mispelled word.
    PreserveOriginalApostropheTypes(misspelled_word, &replacement);

    // Ignore suggestions that are just changing the apostrophe type
    // (straight vs. typographical)
    if (replacement == misspelled_word)
      continue;

    replacements_filtered.push_back(WebString::FromUtf16(replacement));
  }

  return replacements_filtered;
}

}  // namespace

class SpellCheck::SpellcheckRequest {
 public:
  SpellcheckRequest(
      const std::u16string& text,
      std::unique_ptr<blink::WebTextCheckingCompletion> completion,
      base::WeakPtr<SpellCheckProvider> provider,
      blink::WebTextCheckClient::ShouldForceRefreshTextCheckService
          should_force_refresh)
      : text_(text),
        completion_(std::move(completion)),
        start_ticks_(base::TimeTicks::Now()),
        provider_(provider),
        should_force_refresh_(should_force_refresh) {
    DCHECK(completion_);
  }

  SpellcheckRequest(const SpellcheckRequest&) = delete;
  SpellcheckRequest& operator=(const SpellcheckRequest&) = delete;

  ~SpellcheckRequest() = default;

  std::u16string text() { return text_; }
  blink::WebTextCheckingCompletion* completion() { return completion_.get(); }
  base::TimeTicks start_ticks() { return start_ticks_; }

  SpellCheckProvider* provider() { return provider_.get(); }
  blink::WebTextCheckClient::ShouldForceRefreshTextCheckService
  should_force_refresh() {
    return should_force_refresh_;
  }

 private:
  std::u16string text_;  // Text to be checked in this task.

  // The interface to send the misspelled ranges to WebKit.
  std::unique_ptr<blink::WebTextCheckingCompletion> completion_;

  // The time ticks at which this request was created
  base::TimeTicks start_ticks_;

  base::WeakPtr<SpellCheckProvider> provider_;

  blink::WebTextCheckClient::ShouldForceRefreshTextCheckService
      should_force_refresh_;
};


// 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(
    service_manager::LocalInterfaceProvider* embedder_provider)
    : embedder_provider_(embedder_provider), spellcheck_enabled_(true) {
  DCHECK(embedder_provider);
}

SpellCheck::~SpellCheck() = default;

void SpellCheck::BindReceiver(
    mojo::PendingReceiver<spellcheck::mojom::SpellChecker> receiver) {
  receivers_.Add(this, std::move(receiver));
}

void SpellCheck::Initialize(
    std::vector<spellcheck::mojom::SpellCheckBDictLanguagePtr> dictionaries,
    const std::vector<std::string>& custom_words,
    bool enable) {
  base::ScopedUmaHistogramTimer timer("SpellCheck.Renderer.InitializeTime");
  languages_.clear();

  for (const auto& dictionary : dictionaries)
    AddSpellcheckLanguage(std::move(dictionary->file), dictionary->language);

  if (base::FeatureList::IsEnabled(
          spellcheck::kAsyncSpellcheckCustomDictionaryInit)) {
    custom_dictionary_.InitAsync(custom_words);
  } else {
    custom_dictionary_.Init(
        std::set<std::string>(custom_words.begin(), custom_words.end()));
  }
#if BUILDFLAG(USE_RENDERER_SPELLCHECKER)
  if (!spellcheck::UseBrowserSpellChecker()) {
    PostDelayedSpellCheckTask(pending_request_param_.release());
  }
#endif

  spellcheck_enabled_ = enable;
  UpdateSpellcheckEnabled updater(enable);
  content::RenderFrame::ForEach(&updater);
}

void SpellCheck::SpellCheckCustomDictionaryChanged(
    const std::vector<std::string>& words_added,
    const std::vector<std::string>& words_removed) {
  if (blink::WebRuntimeFeatures::IsSpellCheckCustomDictionaryAPIEnabled()) {
    const std::set<std::string> added(words_added.begin(), words_added.end());
    NotifyDictionaryObservers(ConvertToWebStringFromUtf8(added));
  }
}

void SpellCheck::CustomDictionaryChanged(
    const std::vector<std::string>& words_added,
    const std::vector<std::string>& words_removed) {
  const std::set<std::string> added(words_added.begin(), words_added.end());
  NotifyDictionaryObservers(ConvertToWebStringFromUtf8(added));
  // Add or remove the word in the browser's custom dictionary
  custom_dictionary_.OnCustomDictionaryChanged(
      added, std::set<std::string>(words_removed.begin(), words_removed.end()));
}

// 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(
      std::make_unique<SpellcheckLanguage>(embedder_provider_));
  languages_.back()->Init(std::move(file), language);
}

bool SpellCheck::SpellCheckWord(std::u16string_view text,
                                spellcheck::mojom::SpellCheckHost& host,
                                size_t* misspelling_start,
                                size_t* misspelling_len,
                                std::nullptr_t null_suggestions_ptr) {
  return SpellCheckWord(
      text, host, misspelling_start, misspelling_len,
      static_cast<spellcheck::PerLanguageSuggestions*>(nullptr));
}

bool SpellCheck::SpellCheckWord(
    std::u16string_view text,
    spellcheck::mojom::SpellCheckHost& host,
    size_t* misspelling_start,
    size_t* misspelling_len,
    std::vector<std::u16string>* optional_suggestions) {
  if (!optional_suggestions) {
    return SpellCheckWord(text, host, misspelling_start, misspelling_len,
                          nullptr);
  }

  bool result;
  spellcheck::PerLanguageSuggestions per_language_suggestions;
  result = SpellCheckWord(text, host, misspelling_start, misspelling_len,
                          &per_language_suggestions);
  spellcheck::FillSuggestions(per_language_suggestions, optional_suggestions);

  return result;
}

bool SpellCheck::SpellCheckWord(
    std::u16string_view text,
    spellcheck::mojom::SpellCheckHost& host,
    size_t* misspelling_start,
    size_t* misspelling_len,
    spellcheck::PerLanguageSuggestions* optional_per_language_suggestions) {
  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;

  // To prevent an infinite loop below, ensure that at least one language is
  // enabled before starting the check. If no language is enabled, we should
  // never report a spelling mistake, so return true here.
  if (EnabledLanguageCount() == 0) {
    return true;
  }

  // These are for holding misspelling or skippable word positions and lengths
  // between calls to SpellcheckLanguage::SpellCheckWord.
  size_t possible_misspelling_start;
  size_t possible_misspelling_len;
  // The longest sequence of text that all languages agree is skippable.
  size_t agreed_skippable_len;
  // A vector of vectors containing spelling suggestions from different
  // languages.
  std::vector<std::vector<std::u16string>> suggestions_list;
  // A vector to hold a language's misspelling suggestions between spellcheck
  // calls.
  std::vector<std::u16string> language_suggestions;

  const size_t text_length = text.size();
  size_t position_in_text = 0;

  // 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();) {
#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
      if (!(*language)->IsEnabled()) {
        // In the case of hybrid spell checking on Windows, languages that are
        // handled on the browser side are marked as disabled on the renderer
        // side. We do not want to return IS_CORRECT for those languages, so we
        // simply skip them.
        language++;
        continue;
      }
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)

      size_t possible_misspelling_start_relative_to_substring = 0;
      language_suggestions.clear();
      SpellcheckLanguage::SpellcheckWordResult result =
          (*language)->SpellCheckWord(
              text.substr(position_in_text), host,
              &possible_misspelling_start_relative_to_substring,
              &possible_misspelling_len,
              optional_per_language_suggestions ? &language_suggestions
                                                : nullptr);
      // SpellCheckWord informs us of a misspelling index relative to the
      // substring of text which was passed in, so add `position_in_text` to
      // this value to get the offset relative to `text`.
      possible_misspelling_start =
          possible_misspelling_len > 0
              ? position_in_text +
                    possible_misspelling_start_relative_to_substring
              : 0;

      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_per_language_suggestions) {
        optional_per_language_suggestions->swap(suggestions_list);
      }
      return false;
    }

#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
    // If we're performing a hybrid spell check, we're only interested in
    // knowing whether some Hunspell languages considered this text range as
    // correctly spelled. If no misspellings were found, but the entire text was
    // skipped, it means that no Hunspell language considered this text
    // correct, so we should return false here.
    if (spellcheck::UseBrowserSpellChecker() &&
        EnabledLanguageCount() != LanguageCount() &&
        agreed_skippable_len == text_length) {
      return false;
    }
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
  }

  NOTREACHED();
}

#if BUILDFLAG(USE_RENDERER_SPELLCHECKER)
bool SpellCheck::SpellCheckParagraph(
    const std::u16string& text,
    spellcheck::mojom::SpellCheckHost& host,
    std::vector<WebTextCheckingResult>* results,
    const std::set<std::u16string>* document_custom_words) {
  DCHECK(results);
  std::vector<WebTextCheckingResult> textcheck_results;
  const size_t text_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.
  size_t misspelling_start = 0;
  size_t misspelling_length = 0;
  while (position_in_text <= text_length) {
    size_t misspelling_start_relative_to_substring = 0;
    bool spelled_correctly = SpellCheckWord(
        text.substr(position_in_text), host,
        &misspelling_start_relative_to_substring, &misspelling_length, nullptr);
    // SpellCheckWord informs us of a misspelling index relative to the
    // substring of text which was passed in, so add `position_in_text` to this
    // value to get the offset relative to `text`.
    misspelling_start =
        misspelling_length > 0
            ? position_in_text + misspelling_start_relative_to_substring
            : 0;
    if (spelled_correctly) {
      *results = std::move(textcheck_results);
      return true;
    }

    // A word is correctly spelled if it is in the profile custom dictionary
    // or in the per-document word set.
    const bool in_custom_dictionary =
        custom_dictionary_.SpellCheckWord(text, misspelling_start,
                                          misspelling_length) ||
        (document_custom_words && document_custom_words->contains(text.substr(
                                      misspelling_start, misspelling_length)));
    if (!in_custom_dictionary) {
      textcheck_results.emplace_back(
          blink::kWebTextDecorationTypeSpelling,
          base::checked_cast<int>(misspelling_start),
          base::checked_cast<int>(misspelling_length));
    }
    position_in_text = misspelling_start + misspelling_length;
  }
  *results = std::move(textcheck_results);
  return false;
}

void SpellCheck::RequestTextChecking(
    const std::u16string& text,
    std::unique_ptr<blink::WebTextCheckingCompletion> completion,
    base::WeakPtr<SpellCheckProvider> provider) {
  // Clean up the previous request before starting a new request.
  if (pending_request_param_)
    pending_request_param_->completion()->DidCancelCheckingText();

  pending_request_param_ = std::make_unique<SpellcheckRequest>(
      text, std::move(completion), std::move(provider),
      /*should_force_refresh=*/
      blink::WebTextCheckClient::ShouldForceRefreshTextCheckService::kNo);
  // 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;
}

#if BUILDFLAG(USE_RENDERER_SPELLCHECKER)
void SpellCheck::PostDelayedSpellCheckTask(SpellcheckRequest* request) {
  if (!request)
    return;

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(&SpellCheck::PerformSpellCheck, weak_factory_.GetWeakPtr(),
                     base::Owned(request)));
}
#endif

#if BUILDFLAG(USE_RENDERER_SPELLCHECKER)
void SpellCheck::PerformSpellCheck(SpellcheckRequest* param) {
  DCHECK(param);

  SpellCheckProvider* provider = param->provider();
  spellcheck::mojom::SpellCheckHost* host = nullptr;
  if (provider != nullptr) {
    // It is safe to provide this as a pointer here because
    // it will only be used synchronously in the SpellCheckParagraph
    // method.
    host = &provider->GetSpellCheckHost();
  }

  if (!host || languages_.empty() ||
      !std::ranges::all_of(languages_, &SpellcheckLanguage::IsEnabled)) {
    param->completion()->DidCancelCheckingText();
  } else {
    const std::set<std::u16string>& document_custom_words =
        provider->document_custom_words();
    std::vector<blink::WebTextCheckingResult> results;
    SpellCheckParagraph(param->text(), *host, &results, &document_custom_words);
    param->completion()->DidFinishCheckingText(results);
#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
    spellcheck_renderer_metrics::RecordSpellcheckDuration(
        base::TimeTicks::Now() - param->start_ticks(),
        /*used_hunspell=*/true, /*used_native=*/false);
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
  }
}
#endif

void SpellCheck::CreateTextCheckingResults(
    ResultFilter filter,
    spellcheck::mojom::SpellCheckHost& host,
    int line_offset,
    const std::u16string& line_text,
    const std::vector<SpellCheckResult>& spellcheck_results,
    std::vector<WebTextCheckingResult>* textcheck_results,
    const std::set<std::u16string>* document_custom_words) {
  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 std::u16string& misspelled_word =
        line_text.substr(spellcheck_result.location, spellcheck_result.length);
    const std::vector<std::u16string>& replacements =
        spellcheck_result.replacements;
    spellcheck::Decoration decoration = spellcheck_result.decoration;
#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
    // Ignore words that are in a script not supported by any of the enabled
    // spellcheck languages.
    if (spellcheck::UseBrowserSpellChecker() &&
        !IsWordInSupportedScript(misspelled_word)) {
      continue;
    }
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)

    // Ignore words in custom dictionary.
    if (custom_dictionary_.SpellCheckWord(misspelled_word, 0,
                                          misspelled_word.length())) {
      continue;
    }

    if (document_custom_words &&
        document_custom_words->contains(misspelled_word)) {
      continue;
    }

    std::vector<WebString> replacements_filtered =
        FilterReplacementSuggestions(misspelled_word, replacements);

    // If the spellchecker suggested replacements, but they were all just
    // changing apostrophe styles, ignore this misspelling. If there were never
    // any suggested replacements, keep the misspelling.
    if (replacements_filtered.empty() && !replacements.empty())
      continue;

    if (filter == USE_HUNSPELL_FOR_GRAMMAR) {
      // Double-check misspelled words with Hunspell and attach grammar markers
      // to them if Hunspell tells us they are correct words, i.e. they are
      // probably contextually-misspelled words.
      size_t unused_misspelling_start = 0;
      size_t unused_misspelling_length = 0;
      if (decoration == spellcheck::Decoration::SPELLING &&
          SpellCheckWord(misspelled_word, host, &unused_misspelling_start,
                         &unused_misspelling_length, nullptr)) {
        decoration = spellcheck::Decoration::GRAMMAR;
      }
    }
#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
    else if (filter == USE_HUNSPELL_FOR_HYBRID_CHECK &&
             spellcheck::UseBrowserSpellChecker() &&
             EnabledLanguageCount() > 0) {
      // Remove the suggestions that were generated by the native spell checker,
      // otherwise Blink will cache them without asking for the suggestions
      // from Hunspell.
      replacements_filtered.clear();

      // The native spell checker was not able to check all locales. Double-
      // check misspelled words with Hunspell for the unchecked locales
      // and remove the results if Hunspell tells us the words are correctly
      // spelled in those locales.
      size_t unused_misspelling_start = 0;
      size_t unused_misspelling_length = 0;

      if (SpellCheckWord(misspelled_word, host, &unused_misspelling_start,
                         &unused_misspelling_length, nullptr)) {
        // Correctly spelled in a Hunspell locale. If enhanced spell check was
        // used, turn the spelling mistake into a grammar mistake (local and
        // remote checks disagree, so the word is probably only contextually
        // misspelled). If enhanced spell check wasn't used, remove this
        // misspelling.
        if (spellcheck_result.spelling_service_used) {
          decoration = spellcheck::Decoration::GRAMMAR;
        } else {
          continue;
        }
      }
    }
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)

    results.push_back(WebTextCheckingResult(
        static_cast<WebTextDecorationType>(decoration),
        line_offset + spellcheck_result.location, spellcheck_result.length,
        replacements_filtered, spellcheck_result.should_hide_suggestion_menu));
  }

  *textcheck_results = std::move(results);
}

bool SpellCheck::IsSpellcheckEnabled() {
#if BUILDFLAG(IS_ANDROID)
  if (!spellcheck::IsAndroidSpellCheckFeatureEnabled()) return false;
#endif
  return spellcheck_enabled_;
}

void SpellCheck::AddDictionaryUpdateObserver(
    DictionaryUpdateObserver* observer) {
  return dictionary_update_observers_.AddObserver(observer);
}

void SpellCheck::RemoveDictionaryUpdateObserver(
    DictionaryUpdateObserver* observer) {
  return dictionary_update_observers_.RemoveObserver(observer);
}

size_t SpellCheck::LanguageCount() {
  return languages_.size();
}

size_t SpellCheck::EnabledLanguageCount() {
  return std::ranges::count_if(languages_, &SpellcheckLanguage::IsEnabled);
}

void SpellCheck::NotifyDictionaryObservers(
    const std::vector<WebString>& words_added) {
  for (auto& observer : dictionary_update_observers_) {
    observer.OnDictionaryUpdated(words_added);
  }
}

bool SpellCheck::IsWordInSupportedScript(const std::u16string& word) const {
  return std::ranges::any_of(languages_, [word](const auto& language) {
    return language->IsTextInSameScript(word);
  });
}
