summaryrefslogtreecommitdiffstats
path: root/editor/spellchecker/EditorSpellCheck.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'editor/spellchecker/EditorSpellCheck.cpp')
-rw-r--r--editor/spellchecker/EditorSpellCheck.cpp1179
1 files changed, 1179 insertions, 0 deletions
diff --git a/editor/spellchecker/EditorSpellCheck.cpp b/editor/spellchecker/EditorSpellCheck.cpp
new file mode 100644
index 0000000000..35b4da06cf
--- /dev/null
+++ b/editor/spellchecker/EditorSpellCheck.cpp
@@ -0,0 +1,1179 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sts=2 sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EditorSpellCheck.h"
+
+#include "EditorBase.h" // for EditorBase
+#include "HTMLEditor.h" // for HTMLEditor
+#include "TextServicesDocument.h" // for TextServicesDocument
+
+#include "mozilla/Attributes.h" // for final
+#include "mozilla/dom/Element.h" // for Element
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/StaticRange.h"
+#include "mozilla/intl/Locale.h" // for mozilla::intl::Locale
+#include "mozilla/intl/LocaleService.h" // for retrieving app locale
+#include "mozilla/intl/OSPreferences.h" // for mozilla::intl::OSPreferences
+#include "mozilla/Logging.h" // for mozilla::LazyLogModule
+#include "mozilla/mozalloc.h" // for operator delete, etc
+#include "mozilla/mozSpellChecker.h" // for mozSpellChecker
+#include "mozilla/Preferences.h" // for Preferences
+
+#include "nsAString.h" // for nsAString::IsEmpty, etc
+#include "nsComponentManagerUtils.h" // for do_CreateInstance
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc
+#include "nsDependentSubstring.h" // for Substring
+#include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc
+#include "nsIContent.h" // for nsIContent
+#include "nsIContentPrefService2.h" // for nsIContentPrefService2, etc
+#include "mozilla/dom/Document.h" // for Document
+#include "nsIEditor.h" // for nsIEditor
+#include "nsILoadContext.h"
+#include "nsISupports.h" // for nsISupports
+#include "nsISupportsUtils.h" // for NS_ADDREF
+#include "nsIURI.h" // for nsIURI
+#include "nsThreadUtils.h" // for GetMainThreadSerialEventTarget
+#include "nsVariant.h" // for nsIWritableVariant, etc
+#include "nsLiteralString.h" // for NS_LITERAL_STRING, etc
+#include "nsRange.h"
+#include "nsReadableUtils.h" // for ToNewUnicode, EmptyString, etc
+#include "nsServiceManagerUtils.h" // for do_GetService
+#include "nsString.h" // for nsAutoString, nsString, etc
+#include "nsStringFwd.h" // for nsAFlatString
+#include "nsStyleUtil.h" // for nsStyleUtil
+#include "nsXULAppAPI.h" // for XRE_GetProcessType
+
+namespace mozilla {
+
+using namespace dom;
+using intl::LocaleService;
+using intl::OSPreferences;
+
+static mozilla::LazyLogModule sEditorSpellChecker("EditorSpellChecker");
+
+class UpdateDictionaryHolder {
+ private:
+ EditorSpellCheck* mSpellCheck;
+
+ public:
+ explicit UpdateDictionaryHolder(EditorSpellCheck* esc) : mSpellCheck(esc) {
+ if (mSpellCheck) {
+ mSpellCheck->BeginUpdateDictionary();
+ }
+ }
+
+ ~UpdateDictionaryHolder() {
+ if (mSpellCheck) {
+ mSpellCheck->EndUpdateDictionary();
+ }
+ }
+};
+
+#define CPS_PREF_NAME u"spellcheck.lang"_ns
+
+/**
+ * Gets the URI of aEditor's document.
+ */
+static nsIURI* GetDocumentURI(EditorBase* aEditor) {
+ MOZ_ASSERT(aEditor);
+
+ Document* doc = aEditor->AsEditorBase()->GetDocument();
+ if (NS_WARN_IF(!doc)) {
+ return nullptr;
+ }
+
+ return doc->GetDocumentURI();
+}
+
+static nsILoadContext* GetLoadContext(nsIEditor* aEditor) {
+ Document* doc = aEditor->AsEditorBase()->GetDocument();
+ if (NS_WARN_IF(!doc)) {
+ return nullptr;
+ }
+
+ return doc->GetLoadContext();
+}
+
+static nsCString DictionariesToString(
+ const nsTArray<nsCString>& aDictionaries) {
+ nsCString asString;
+ for (const auto& dictionary : aDictionaries) {
+ asString.Append(dictionary);
+ asString.Append(',');
+ }
+ return asString;
+}
+
+static void StringToDictionaries(const nsCString& aString,
+ nsTArray<nsCString>& aDictionaries) {
+ nsTArray<nsCString> asDictionaries;
+ for (const nsACString& token :
+ nsCCharSeparatedTokenizer(aString, ',').ToRange()) {
+ if (token.IsEmpty()) {
+ continue;
+ }
+ aDictionaries.AppendElement(token);
+ }
+}
+
+/**
+ * Fetches the dictionary stored in content prefs and maintains state during the
+ * fetch, which is asynchronous.
+ */
+class DictionaryFetcher final : public nsIContentPrefCallback2 {
+ public:
+ NS_DECL_ISUPPORTS
+
+ DictionaryFetcher(EditorSpellCheck* aSpellCheck,
+ nsIEditorSpellCheckCallback* aCallback, uint32_t aGroup)
+ : mCallback(aCallback), mGroup(aGroup), mSpellCheck(aSpellCheck) {}
+
+ NS_IMETHOD Fetch(nsIEditor* aEditor);
+
+ NS_IMETHOD HandleResult(nsIContentPref* aPref) override {
+ nsCOMPtr<nsIVariant> value;
+ nsresult rv = aPref->GetValue(getter_AddRefs(value));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString asString;
+ value->GetAsACString(asString);
+ StringToDictionaries(asString, mDictionaries);
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleCompletion(uint16_t reason) override {
+ mSpellCheck->DictionaryFetched(this);
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleError(nsresult error) override { return NS_OK; }
+
+ nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
+ uint32_t mGroup;
+ nsString mRootContentLang;
+ nsString mRootDocContentLang;
+ nsTArray<nsCString> mDictionaries;
+
+ private:
+ ~DictionaryFetcher() {}
+
+ RefPtr<EditorSpellCheck> mSpellCheck;
+};
+
+NS_IMPL_ISUPPORTS(DictionaryFetcher, nsIContentPrefCallback2)
+
+class ContentPrefInitializerRunnable final : public Runnable {
+ public:
+ ContentPrefInitializerRunnable(nsIEditor* aEditor,
+ nsIContentPrefCallback2* aCallback)
+ : Runnable("ContentPrefInitializerRunnable"),
+ mEditorBase(aEditor->AsEditorBase()),
+ mCallback(aCallback) {}
+
+ NS_IMETHOD Run() override {
+ if (mEditorBase->Destroyed()) {
+ mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ if (NS_WARN_IF(!contentPrefService)) {
+ mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> docUri = GetDocumentURI(mEditorBase);
+ if (NS_WARN_IF(!docUri)) {
+ mCallback->HandleError(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ nsAutoCString docUriSpec;
+ nsresult rv = docUri->GetSpec(docUriSpec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mCallback->HandleError(rv);
+ return NS_OK;
+ }
+
+ rv = contentPrefService->GetByDomainAndName(
+ NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME,
+ GetLoadContext(mEditorBase), mCallback);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mCallback->HandleError(rv);
+ return NS_OK;
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<EditorBase> mEditorBase;
+ nsCOMPtr<nsIContentPrefCallback2> mCallback;
+};
+
+NS_IMETHODIMP
+DictionaryFetcher::Fetch(nsIEditor* aEditor) {
+ NS_ENSURE_ARG_POINTER(aEditor);
+
+ nsCOMPtr<nsIRunnable> runnable =
+ new ContentPrefInitializerRunnable(aEditor, this);
+ NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000,
+ EventQueuePriority::Idle);
+
+ return NS_OK;
+}
+
+/**
+ * Stores the current dictionary for aEditor's document URL.
+ */
+static nsresult StoreCurrentDictionaries(
+ EditorBase* aEditorBase, const nsTArray<nsCString>& aDictionaries) {
+ NS_ENSURE_ARG_POINTER(aEditorBase);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
+ if (NS_WARN_IF(!docUri)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString docUriSpec;
+ rv = docUri->GetSpec(docUriSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<nsVariant> prefValue = new nsVariant();
+
+ nsCString asString = DictionariesToString(aDictionaries);
+ prefValue->SetAsAString(NS_ConvertUTF8toUTF16(asString));
+
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
+
+ return contentPrefService->Set(NS_ConvertUTF8toUTF16(docUriSpec),
+ CPS_PREF_NAME, prefValue,
+ GetLoadContext(aEditorBase), nullptr);
+}
+
+/**
+ * Forgets the current dictionary stored for aEditor's document URL.
+ */
+static nsresult ClearCurrentDictionaries(EditorBase* aEditorBase) {
+ NS_ENSURE_ARG_POINTER(aEditorBase);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
+ if (NS_WARN_IF(!docUri)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString docUriSpec;
+ rv = docUri->GetSpec(docUriSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
+
+ return contentPrefService->RemoveByDomainAndName(
+ NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME,
+ GetLoadContext(aEditorBase), nullptr);
+}
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorSpellCheck)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorSpellCheck)
+
+NS_INTERFACE_MAP_BEGIN(EditorSpellCheck)
+ NS_INTERFACE_MAP_ENTRY(nsIEditorSpellCheck)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditorSpellCheck)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(EditorSpellCheck)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(EditorSpellCheck, mEditor, mSpellChecker)
+
+EditorSpellCheck::EditorSpellCheck()
+ : mTxtSrvFilterType(0),
+ mSuggestedWordIndex(0),
+ mDictionaryIndex(0),
+ mDictionaryFetcherGroup(0),
+ mUpdateDictionaryRunning(false) {}
+
+EditorSpellCheck::~EditorSpellCheck() {
+ // Make sure we blow the spellchecker away, just in
+ // case it hasn't been destroyed already.
+ mSpellChecker = nullptr;
+}
+
+mozSpellChecker* EditorSpellCheck::GetSpellChecker() { return mSpellChecker; }
+
+// The problem is that if the spell checker does not exist, we can not tell
+// which dictionaries are installed. This function works around the problem,
+// allowing callers to ask if we can spell check without actually doing so (and
+// enabling or disabling UI as necessary). This just creates a spellcheck
+// object if needed and asks it for the dictionary list.
+NS_IMETHODIMP
+EditorSpellCheck::CanSpellCheck(bool* aCanSpellCheck) {
+ RefPtr<mozSpellChecker> spellChecker = mSpellChecker;
+ if (!spellChecker) {
+ spellChecker = mozSpellChecker::Create();
+ MOZ_ASSERT(spellChecker);
+ }
+ nsTArray<nsCString> dictList;
+ nsresult rv = spellChecker->GetDictionaryList(&dictList);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ *aCanSpellCheck = !dictList.IsEmpty();
+ return NS_OK;
+}
+
+// Instances of this class can be used as either runnables or RAII helpers.
+class CallbackCaller final : public Runnable {
+ public:
+ explicit CallbackCaller(nsIEditorSpellCheckCallback* aCallback)
+ : mozilla::Runnable("CallbackCaller"), mCallback(aCallback) {}
+
+ ~CallbackCaller() { Run(); }
+
+ NS_IMETHOD Run() override {
+ if (mCallback) {
+ mCallback->EditorSpellCheckDone();
+ mCallback = nullptr;
+ }
+ return NS_OK;
+ }
+
+ private:
+ nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
+};
+
+NS_IMETHODIMP
+EditorSpellCheck::InitSpellChecker(nsIEditor* aEditor,
+ bool aEnableSelectionChecking,
+ nsIEditorSpellCheckCallback* aCallback) {
+ NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER);
+ mEditor = aEditor->AsEditorBase();
+
+ RefPtr<Document> doc = mEditor->GetDocument();
+ if (NS_WARN_IF(!doc)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+
+ // We can spell check with any editor type
+ RefPtr<TextServicesDocument> textServicesDocument =
+ new TextServicesDocument();
+ textServicesDocument->SetFilterType(mTxtSrvFilterType);
+
+ // EditorBase::AddEditActionListener() needs to access mSpellChecker and
+ // mSpellChecker->GetTextServicesDocument(). Therefore, we need to
+ // initialize them before calling TextServicesDocument::InitWithEditor()
+ // since it calls EditorBase::AddEditActionListener().
+ mSpellChecker = mozSpellChecker::Create();
+ MOZ_ASSERT(mSpellChecker);
+ rv = mSpellChecker->SetDocument(textServicesDocument, true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Pass the editor to the text services document
+ rv = textServicesDocument->InitWithEditor(aEditor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aEnableSelectionChecking) {
+ // Find out if the section is collapsed or not.
+ // If it isn't, we want to spellcheck just the selection.
+
+ RefPtr<Selection> selection;
+ aEditor->GetSelection(getter_AddRefs(selection));
+ if (NS_WARN_IF(!selection)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (selection->RangeCount()) {
+ RefPtr<const nsRange> range = selection->GetRangeAt(0);
+ NS_ENSURE_STATE(range);
+
+ if (!range->Collapsed()) {
+ // We don't want to touch the range in the selection,
+ // so create a new copy of it.
+ RefPtr<StaticRange> staticRange =
+ StaticRange::Create(range, IgnoreErrors());
+ if (NS_WARN_IF(!staticRange)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Make sure the new range spans complete words.
+ rv = textServicesDocument->ExpandRangeToWordBoundaries(staticRange);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Now tell the text services that you only want
+ // to iterate over the text in this range.
+ rv = textServicesDocument->SetExtent(staticRange);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ }
+ }
+ // do not fail if UpdateCurrentDictionary fails because this method may
+ // succeed later.
+ rv = UpdateCurrentDictionary(aCallback);
+ if (NS_FAILED(rv) && aCallback) {
+ // However, if it does fail, we still need to call the callback since we
+ // discard the failure. Do it asynchronously so that the caller is always
+ // guaranteed async behavior.
+ RefPtr<CallbackCaller> caller = new CallbackCaller(aCallback);
+ rv = doc->Dispatch(TaskCategory::Other, caller.forget());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::GetNextMisspelledWord(nsAString& aNextMisspelledWord) {
+ MOZ_LOG(sEditorSpellChecker, LogLevel::Debug, ("%s", __FUNCTION__));
+
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ DeleteSuggestedWordList();
+ // Beware! This may flush notifications via synchronous
+ // ScrollSelectionIntoView.
+ RefPtr<mozSpellChecker> spellChecker(mSpellChecker);
+ return spellChecker->NextMisspelledWord(aNextMisspelledWord,
+ mSuggestedWordList);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::GetSuggestedWord(nsAString& aSuggestedWord) {
+ // XXX This is buggy if mSuggestedWordList.Length() is over INT32_MAX.
+ if (mSuggestedWordIndex < static_cast<int32_t>(mSuggestedWordList.Length())) {
+ aSuggestedWord = mSuggestedWordList[mSuggestedWordIndex];
+ mSuggestedWordIndex++;
+ } else {
+ // A blank string signals that there are no more strings
+ aSuggestedWord.Truncate();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::CheckCurrentWord(const nsAString& aSuggestedWord,
+ bool* aIsMisspelled) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ DeleteSuggestedWordList();
+ return mSpellChecker->CheckWord(aSuggestedWord, aIsMisspelled,
+ &mSuggestedWordList);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::Suggest(const nsAString& aSuggestedWord, uint32_t aCount,
+ JSContext* aCx, Promise** aPromise) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ mSpellChecker->Suggest(aSuggestedWord, aCount)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise](const CopyableTArray<nsString>& aSuggestions) {
+ promise->MaybeResolve(aSuggestions);
+ },
+ [promise](nsresult aError) {
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+RefPtr<CheckWordPromise> EditorSpellCheck::CheckCurrentWordsNoSuggest(
+ const nsTArray<nsString>& aSuggestedWords) {
+ if (NS_WARN_IF(!mSpellChecker)) {
+ return CheckWordPromise::CreateAndReject(NS_ERROR_NOT_INITIALIZED,
+ __func__);
+ }
+
+ return mSpellChecker->CheckWords(aSuggestedWords);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::ReplaceWord(const nsAString& aMisspelledWord,
+ const nsAString& aReplaceWord,
+ bool aAllOccurrences) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ RefPtr<mozSpellChecker> spellChecker(mSpellChecker);
+ return spellChecker->Replace(aMisspelledWord, aReplaceWord, aAllOccurrences);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::IgnoreWordAllOccurrences(const nsAString& aWord) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->IgnoreAll(aWord);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::GetPersonalDictionary() {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ // We can spell check with any editor type
+ mDictionaryList.Clear();
+ mDictionaryIndex = 0;
+ return mSpellChecker->GetPersonalDictionary(&mDictionaryList);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::GetPersonalDictionaryWord(nsAString& aDictionaryWord) {
+ // XXX This is buggy if mDictionaryList.Length() is over INT32_MAX.
+ if (mDictionaryIndex < static_cast<int32_t>(mDictionaryList.Length())) {
+ aDictionaryWord = mDictionaryList[mDictionaryIndex];
+ mDictionaryIndex++;
+ } else {
+ // A blank string signals that there are no more strings
+ aDictionaryWord.Truncate();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::AddWordToDictionary(const nsAString& aWord) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->AddWordToPersonalDictionary(aWord);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::RemoveWordFromDictionary(const nsAString& aWord) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->RemoveWordFromPersonalDictionary(aWord);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::GetDictionaryList(nsTArray<nsCString>& aList) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->GetDictionaryList(&aList);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::GetCurrentDictionaries(nsTArray<nsCString>& aDictionaries) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+ return mSpellChecker->GetCurrentDictionaries(aDictionaries);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::SetCurrentDictionaries(
+ const nsTArray<nsCString>& aDictionaries, JSContext* aCx,
+ Promise** aPromise) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
+
+ // The purpose of mUpdateDictionaryRunning is to avoid doing all of this if
+ // UpdateCurrentDictionary's helper method DictionaryFetched, which calls us,
+ // is on the stack. In other words: Only do this, if the user manually
+ // selected a dictionary to use.
+ if (!mUpdateDictionaryRunning) {
+ // Ignore pending dictionary fetchers by increasing this number.
+ mDictionaryFetcherGroup++;
+
+ uint32_t flags = 0;
+ mEditor->GetFlags(&flags);
+ if (!(flags & nsIEditor::eEditorMailMask)) {
+ bool contentPrefMatchesUserPref = true;
+ // Check if aDictionaries has the same languages as mPreferredLangs.
+ if (!aDictionaries.IsEmpty()) {
+ if (aDictionaries.Length() != mPreferredLangs.Length()) {
+ contentPrefMatchesUserPref = false;
+ } else {
+ for (const auto& dictName : aDictionaries) {
+ if (mPreferredLangs.IndexOf(dictName) ==
+ nsTArray<nsCString>::NoIndex) {
+ contentPrefMatchesUserPref = false;
+ break;
+ }
+ }
+ }
+ }
+ if (!contentPrefMatchesUserPref) {
+ // When user sets dictionary manually, we store this value associated
+ // with editor url, if it doesn't match the document language exactly.
+ // For example on "en" sites, we need to store "en-GB", otherwise
+ // the language might jump back to en-US although the user explicitly
+ // chose otherwise.
+ StoreCurrentDictionaries(mEditor, aDictionaries);
+#ifdef DEBUG_DICT
+ printf("***** Writing content preferences for |%s|\n",
+ DictionariesToString(aDictionaries).Data());
+#endif
+ } else {
+ // If user sets a dictionary matching the language defined by
+ // document, we consider content pref has been canceled, and we clear
+ // it.
+ ClearCurrentDictionaries(mEditor);
+#ifdef DEBUG_DICT
+ printf("***** Clearing content preferences for |%s|\n",
+ DictionariesToString(aDictionaries).Data());
+#endif
+ }
+
+ // Also store it in as a preference, so we can use it as a fallback.
+ // We don't want this for mail composer because it uses
+ // "spellchecker.dictionary" as a preference.
+ //
+ // XXX: Prefs can only be set in the parent process, so this condition is
+ // necessary to stop libpref from throwing errors. But this should
+ // probably be handled in a better way.
+ if (XRE_IsParentProcess()) {
+ nsCString asString = DictionariesToString(aDictionaries);
+ Preferences::SetCString("spellchecker.dictionary", asString);
+#ifdef DEBUG_DICT
+ printf("***** Possibly storing spellchecker.dictionary |%s|\n",
+ asString.Data());
+#endif
+ }
+ } else {
+ MOZ_ASSERT(flags & nsIEditor::eEditorMailMask);
+ // Since the mail editor can only influence the language selection by the
+ // html lang attribute, set the content-language document to persist
+ // multi language selections.
+ // XXX Why doesn't here use the document of the editor directly?
+ nsCOMPtr<nsIContent> anonymousDivOrEditingHost;
+ if (HTMLEditor* htmlEditor = mEditor->GetAsHTMLEditor()) {
+ anonymousDivOrEditingHost = htmlEditor->ComputeEditingHost();
+ } else {
+ anonymousDivOrEditingHost = mEditor->GetRoot();
+ }
+ RefPtr<Document> ownerDoc = anonymousDivOrEditingHost->OwnerDoc();
+ Document* parentDoc = ownerDoc->GetInProcessParentDocument();
+ if (parentDoc) {
+ parentDoc->SetHeaderData(
+ nsGkAtoms::headerContentLanguage,
+ NS_ConvertUTF8toUTF16(DictionariesToString(aDictionaries)));
+ }
+ }
+ }
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ mSpellChecker->SetCurrentDictionaries(aDictionaries)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise]() { promise->MaybeResolveWithUndefined(); },
+ [promise](nsresult aError) {
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::UninitSpellChecker() {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ // Cleanup - kill the spell checker
+ DeleteSuggestedWordList();
+ mDictionaryList.Clear();
+ mDictionaryIndex = 0;
+ mDictionaryFetcherGroup++;
+ mSpellChecker = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::SetFilterType(uint32_t aFilterType) {
+ mTxtSrvFilterType = aFilterType;
+ return NS_OK;
+}
+
+nsresult EditorSpellCheck::DeleteSuggestedWordList() {
+ mSuggestedWordList.Clear();
+ mSuggestedWordIndex = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::UpdateCurrentDictionary(
+ nsIEditorSpellCheckCallback* aCallback) {
+ if (NS_WARN_IF(!mSpellChecker)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv;
+
+ RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
+
+ // Get language with html5 algorithm
+ const RefPtr<Element> rootEditableElement =
+ [](const EditorBase& aEditorBase) -> Element* {
+ if (!aEditorBase.IsHTMLEditor()) {
+ return aEditorBase.GetRoot();
+ }
+ if (aEditorBase.IsMailEditor()) {
+ // Shouldn't run spellcheck in a mail editor without focus
+ // (bug 1507543)
+ // XXX Why doesn't here use the document of the editor directly?
+ Element* const editingHost =
+ aEditorBase.AsHTMLEditor()->ComputeEditingHost();
+ if (!editingHost) {
+ return nullptr;
+ }
+ // Try to get topmost document's document element for embedded mail
+ // editor (bug 967494)
+ Document* parentDoc =
+ editingHost->OwnerDoc()->GetInProcessParentDocument();
+ if (!parentDoc) {
+ return editingHost;
+ }
+ return parentDoc->GetDocumentElement();
+ }
+ return aEditorBase.AsHTMLEditor()->GetFocusedElement();
+ }(*mEditor);
+
+ if (!rootEditableElement) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<DictionaryFetcher> fetcher =
+ new DictionaryFetcher(this, aCallback, mDictionaryFetcherGroup);
+ rootEditableElement->GetLang(fetcher->mRootContentLang);
+ RefPtr<Document> doc = rootEditableElement->GetComposedDoc();
+ NS_ENSURE_STATE(doc);
+ doc->GetContentLanguage(fetcher->mRootDocContentLang);
+
+ rv = fetcher->Fetch(mEditor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+// Helper function that iterates over the list of dictionaries and sets the one
+// that matches based on a given comparison type.
+bool EditorSpellCheck::BuildDictionaryList(const nsACString& aDictName,
+ const nsTArray<nsCString>& aDictList,
+ enum dictCompare aCompareType,
+ nsTArray<nsCString>& aOutList) {
+ for (const auto& dictStr : aDictList) {
+ bool equals = false;
+ switch (aCompareType) {
+ case DICT_NORMAL_COMPARE:
+ equals = aDictName.Equals(dictStr);
+ break;
+ case DICT_COMPARE_CASE_INSENSITIVE:
+ equals = aDictName.Equals(dictStr, nsCaseInsensitiveCStringComparator);
+ break;
+ case DICT_COMPARE_DASHMATCH:
+ equals = nsStyleUtil::DashMatchCompare(
+ NS_ConvertUTF8toUTF16(dictStr), NS_ConvertUTF8toUTF16(aDictName),
+ nsCaseInsensitiveStringComparator);
+ break;
+ }
+ if (equals) {
+ // Avoid adding duplicates to aOutList.
+ if (aOutList.IndexOf(dictStr) == nsTArray<nsCString>::NoIndex) {
+ aOutList.AppendElement(dictStr);
+ }
+#ifdef DEBUG_DICT
+ printf("***** Trying |%s|.\n", dictStr.get());
+#endif
+ // We always break here. We tried to set the dictionary to an existing
+ // dictionary from the list. This must work, if it doesn't, there is
+ // no point trying another one.
+ return true;
+ }
+ }
+ return false;
+}
+
+nsresult EditorSpellCheck::DictionaryFetched(DictionaryFetcher* aFetcher) {
+ MOZ_ASSERT(aFetcher);
+ RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
+
+ BeginUpdateDictionary();
+
+ if (aFetcher->mGroup < mDictionaryFetcherGroup) {
+ // SetCurrentDictionary was called after the fetch started. Don't overwrite
+ // that dictionary with the fetched one.
+ EndUpdateDictionary();
+ if (aFetcher->mCallback) {
+ aFetcher->mCallback->EditorSpellCheckDone();
+ }
+ return NS_OK;
+ }
+
+ /*
+ * We try to derive the dictionary to use based on the following priorities:
+ * 1) Content preference, so the language the user set for the site before.
+ * (Introduced in bug 678842 and corrected in bug 717433.)
+ * 2) Language set by the website, or any other dictionary that partly
+ * matches that. (Introduced in bug 338427.)
+ * Eg. if the website is "en-GB", a user who only has "en-US" will get
+ * that. If the website is generic "en", the user will get one of the
+ * "en-*" installed. If application locale or system locale is "en-*",
+ * we get it. If others, it is (almost) random.
+ * However, we prefer what is stored in "spellchecker.dictionary",
+ * so if the user chose "en-AU" before, they will get "en-AU" on a plain
+ * "en" site. (Introduced in bug 682564.)
+ * If the site has multiple languages declared in its Content-Language
+ * header and there is no more specific lang tag in HTML, we try to
+ * enable a dictionary for every content language.
+ * 3) The value of "spellchecker.dictionary" which reflects a previous
+ * language choice of the user (on another site).
+ * (This was the original behaviour before the aforementioned bugs
+ * landed).
+ * 4) The user's locale.
+ * 5) Use the current dictionary that is currently set.
+ * 6) The content of the "LANG" environment variable (if set).
+ * 7) The first spell check dictionary installed.
+ */
+
+ // Get the language from the element or its closest parent according to:
+ // https://html.spec.whatwg.org/#attr-lang
+ // This is used in SetCurrentDictionaries.
+ nsCString contentLangs;
+ // Reset mPreferredLangs so we only get the current state.
+ mPreferredLangs.Clear();
+ CopyUTF16toUTF8(aFetcher->mRootContentLang, contentLangs);
+#ifdef DEBUG_DICT
+ printf("***** mPreferredLangs (element) |%s|\n", contentLangs.get());
+#endif
+ if (!contentLangs.IsEmpty()) {
+ mPreferredLangs.AppendElement(contentLangs);
+ } else {
+ // If no luck, try the "Content-Language" header.
+ CopyUTF16toUTF8(aFetcher->mRootDocContentLang, contentLangs);
+#ifdef DEBUG_DICT
+ printf("***** mPreferredLangs (content-language) |%s|\n",
+ contentLangs.get());
+#endif
+ StringToDictionaries(contentLangs, mPreferredLangs);
+ }
+
+ // We obtain a list of available dictionaries.
+ AutoTArray<nsCString, 8> dictList;
+ nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ EndUpdateDictionary();
+ if (aFetcher->mCallback) {
+ aFetcher->mCallback->EditorSpellCheckDone();
+ }
+ return rv;
+ }
+
+ // Priority 1:
+ // If we successfully fetched a dictionary from content prefs, do not go
+ // further. Use this exact dictionary.
+ // Don't use content preferences for editor with eEditorMailMask flag.
+ nsAutoCString dictName;
+ uint32_t flags;
+ mEditor->GetFlags(&flags);
+ if (!(flags & nsIEditor::eEditorMailMask)) {
+ if (!aFetcher->mDictionaries.IsEmpty()) {
+ RefPtr<EditorSpellCheck> self = this;
+ RefPtr<DictionaryFetcher> fetcher = aFetcher;
+ mSpellChecker->SetCurrentDictionaries(aFetcher->mDictionaries)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, fetcher]() {
+#ifdef DEBUG_DICT
+ printf("***** Assigned from content preferences |%s|\n",
+ DictionariesToString(fetcher->mDictionaries).Data());
+#endif
+ // We take an early exit here, so let's not forget to clear
+ // the word list.
+ self->DeleteSuggestedWordList();
+
+ self->EndUpdateDictionary();
+ if (fetcher->mCallback) {
+ fetcher->mCallback->EditorSpellCheckDone();
+ }
+ },
+ [self, fetcher](nsresult aError) {
+ if (aError == NS_ERROR_ABORT) {
+ return;
+ }
+ // May be dictionary was uninstalled ?
+ // Clear the content preference and continue.
+ ClearCurrentDictionaries(self->mEditor);
+
+ // Priority 2 or later will handled by the following
+ self->SetFallbackDictionary(fetcher);
+ });
+ return NS_OK;
+ }
+ }
+ SetFallbackDictionary(aFetcher);
+ return NS_OK;
+}
+
+void EditorSpellCheck::SetDictionarySucceeded(DictionaryFetcher* aFetcher) {
+ DeleteSuggestedWordList();
+ EndUpdateDictionary();
+ if (aFetcher->mCallback) {
+ aFetcher->mCallback->EditorSpellCheckDone();
+ }
+}
+
+void EditorSpellCheck::SetFallbackDictionary(DictionaryFetcher* aFetcher) {
+ MOZ_ASSERT(mUpdateDictionaryRunning);
+
+ AutoTArray<nsCString, 6> tryDictList;
+
+ // We obtain a list of available dictionaries.
+ AutoTArray<nsCString, 8> dictList;
+ nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ EndUpdateDictionary();
+ if (aFetcher->mCallback) {
+ aFetcher->mCallback->EditorSpellCheckDone();
+ }
+ return;
+ }
+
+ // Priority 2:
+ // After checking the content preferences, we use the languages of the element
+ // or document.
+
+ // Get the preference value.
+ nsAutoCString prefDictionariesAsString;
+ Preferences::GetLocalizedCString("spellchecker.dictionary",
+ prefDictionariesAsString);
+ nsTArray<nsCString> prefDictionaries;
+ StringToDictionaries(prefDictionariesAsString, prefDictionaries);
+
+ nsAutoCString appLocaleStr;
+ // We pick one dictionary for every language that the element or document
+ // indicates it contains.
+ for (const auto& dictName : mPreferredLangs) {
+ // RFC 5646 explicitly states that matches should be case-insensitive.
+ if (BuildDictionaryList(dictName, dictList, DICT_COMPARE_CASE_INSENSITIVE,
+ tryDictList)) {
+#ifdef DEBUG_DICT
+ printf("***** Trying from element/doc |%s| \n", dictName.get());
+#endif
+ continue;
+ }
+
+ // Required dictionary was not available. Try to get a dictionary
+ // matching at least language part of dictName.
+ mozilla::intl::Locale loc;
+ if (mozilla::intl::LocaleParser::TryParse(dictName, loc).isOk() &&
+ loc.Canonicalize().isOk()) {
+ Span<const char> language = loc.Language().Span();
+ nsAutoCString langCode(language.data(), language.size());
+
+ // Try dictionary.spellchecker preference, if it starts with langCode,
+ // so we don't just get any random dictionary matching the language.
+ bool didAppend = false;
+ for (const auto& dictionary : prefDictionaries) {
+ if (nsStyleUtil::DashMatchCompare(NS_ConvertUTF8toUTF16(dictionary),
+ NS_ConvertUTF8toUTF16(langCode),
+ nsTDefaultStringComparator)) {
+#ifdef DEBUG_DICT
+ printf(
+ "***** Trying preference value |%s| since it matches language "
+ "code\n",
+ dictionary.Data());
+#endif
+ if (BuildDictionaryList(dictionary, dictList,
+ DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
+ didAppend = true;
+ break;
+ }
+ }
+ }
+ if (didAppend) {
+ continue;
+ }
+
+ // Use the application locale dictionary when the required language
+ // equals applocation locale language.
+ LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
+ if (!appLocaleStr.IsEmpty()) {
+ mozilla::intl::Locale appLoc;
+ auto result =
+ mozilla::intl::LocaleParser::TryParse(appLocaleStr, appLoc);
+ if (result.isOk() && appLoc.Canonicalize().isOk() &&
+ loc.Language().Span() == appLoc.Language().Span()) {
+ if (BuildDictionaryList(appLocaleStr, dictList,
+ DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
+ continue;
+ }
+ }
+ }
+
+ // Use the system locale dictionary when the required language equlas
+ // system locale language.
+ nsAutoCString sysLocaleStr;
+ OSPreferences::GetInstance()->GetSystemLocale(sysLocaleStr);
+ if (!sysLocaleStr.IsEmpty()) {
+ mozilla::intl::Locale sysLoc;
+ auto result =
+ mozilla::intl::LocaleParser::TryParse(sysLocaleStr, sysLoc);
+ if (result.isOk() && sysLoc.Canonicalize().isOk() &&
+ loc.Language().Span() == sysLoc.Language().Span()) {
+ if (BuildDictionaryList(sysLocaleStr, dictList,
+ DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
+ continue;
+ }
+ }
+ }
+
+ // Use any dictionary with the required language.
+#ifdef DEBUG_DICT
+ printf("***** Trying to find match for language code |%s|\n",
+ langCode.get());
+#endif
+ BuildDictionaryList(langCode, dictList, DICT_COMPARE_DASHMATCH,
+ tryDictList);
+ }
+ }
+
+ RefPtr<EditorSpellCheck> self = this;
+ RefPtr<DictionaryFetcher> fetcher = aFetcher;
+ RefPtr<GenericPromise> promise;
+
+ if (tryDictList.IsEmpty()) {
+ // Proceed to priority 3 if the list of dictionaries is empty.
+ promise = GenericPromise::CreateAndReject(NS_ERROR_INVALID_ARG, __func__);
+ } else {
+ promise = mSpellChecker->SetCurrentDictionaries(tryDictList);
+ }
+
+ // If an error was thrown while setting the dictionary, just
+ // fail silently so that the spellchecker dialog is allowed to come
+ // up. The user can manually reset the language to their choice on
+ // the dialog if it is wrong.
+ promise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, fetcher]() { self->SetDictionarySucceeded(fetcher); },
+ [prefDictionaries = prefDictionaries.Clone(), dictList = dictList.Clone(),
+ self, fetcher]() {
+ // Build tryDictList with dictionaries for priorities 4 through 7.
+ // We'll use this list if there is no user preference or trying
+ // the user preference fails.
+ AutoTArray<nsCString, 6> tryDictList;
+
+ // Priority 4:
+ // As next fallback, try the current locale.
+ nsAutoCString appLocaleStr;
+ LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
+#ifdef DEBUG_DICT
+ printf("***** Trying locale |%s|\n", appLocaleStr.get());
+#endif
+ self->BuildDictionaryList(appLocaleStr, dictList,
+ DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
+
+ // Priority 5:
+ // If we have a current dictionary and we don't have no item in try
+ // list, don't try anything else.
+ nsTArray<nsCString> currentDictionaries;
+ self->GetCurrentDictionaries(currentDictionaries);
+ if (!currentDictionaries.IsEmpty() && tryDictList.IsEmpty()) {
+#ifdef DEBUG_DICT
+ printf("***** Retrieved current dict |%s|\n",
+ DictionariesToString(currentDictionaries).Data());
+#endif
+ self->EndUpdateDictionary();
+ if (fetcher->mCallback) {
+ fetcher->mCallback->EditorSpellCheckDone();
+ }
+ return;
+ }
+
+ // Priority 6:
+ // Try to get current dictionary from environment variable LANG.
+ // LANG = language[_territory][.charset]
+ char* env_lang = getenv("LANG");
+ if (env_lang) {
+ nsAutoCString lang(env_lang);
+ // Strip trailing charset, if there is any.
+ int32_t dot_pos = lang.FindChar('.');
+ if (dot_pos != -1) {
+ lang = Substring(lang, 0, dot_pos);
+ }
+
+ int32_t underScore = lang.FindChar('_');
+ if (underScore != -1) {
+ lang.Replace(underScore, 1, '-');
+#ifdef DEBUG_DICT
+ printf("***** Trying LANG from environment |%s|\n", lang.get());
+#endif
+ self->BuildDictionaryList(
+ lang, dictList, DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
+ }
+ }
+
+ // Priority 7:
+ // If it does not work, pick the first one.
+ if (!dictList.IsEmpty()) {
+ self->BuildDictionaryList(dictList[0], dictList, DICT_NORMAL_COMPARE,
+ tryDictList);
+#ifdef DEBUG_DICT
+ printf("***** Trying first of list |%s|\n", dictList[0].get());
+#endif
+ }
+
+ // Priority 3:
+ // If the document didn't supply a dictionary or the setting
+ // failed, try the user preference next.
+ if (!prefDictionaries.IsEmpty()) {
+ self->mSpellChecker->SetCurrentDictionaries(prefDictionaries)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, fetcher]() { self->SetDictionarySucceeded(fetcher); },
+ // Priority 3 failed, we'll use the list we built of
+ // priorities 4 to 7.
+ [tryDictList = tryDictList.Clone(), self, fetcher]() {
+ self->mSpellChecker
+ ->SetCurrentDictionaryFromList(tryDictList)
+ ->Then(GetMainThreadSerialEventTarget(), __func__,
+ [self, fetcher]() {
+ self->SetDictionarySucceeded(fetcher);
+ });
+ });
+ } else {
+ // We don't have a user preference, so we'll try the list we
+ // built of priorities 4 to 7.
+ self->mSpellChecker->SetCurrentDictionaryFromList(tryDictList)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, fetcher]() { self->SetDictionarySucceeded(fetcher); });
+ }
+ });
+}
+
+} // namespace mozilla