summaryrefslogtreecommitdiffstats
path: root/editor/spellchecker
diff options
context:
space:
mode:
Diffstat (limited to 'editor/spellchecker')
-rw-r--r--editor/spellchecker/EditorSpellCheck.cpp1000
-rw-r--r--editor/spellchecker/EditorSpellCheck.h98
-rw-r--r--editor/spellchecker/FilteredContentIterator.cpp406
-rw-r--r--editor/spellchecker/FilteredContentIterator.h88
-rw-r--r--editor/spellchecker/TextServicesDocument.cpp2918
-rw-r--r--editor/spellchecker/TextServicesDocument.h318
-rw-r--r--editor/spellchecker/moz.build27
-rw-r--r--editor/spellchecker/nsComposeTxtSrvFilter.cpp64
-rw-r--r--editor/spellchecker/nsComposeTxtSrvFilter.h45
-rw-r--r--editor/spellchecker/nsIInlineSpellChecker.idl38
-rw-r--r--editor/spellchecker/tests/.eslintrc.js5
-rw-r--r--editor/spellchecker/tests/bug1200533_subframe.html15
-rw-r--r--editor/spellchecker/tests/bug1204147_subframe.html11
-rw-r--r--editor/spellchecker/tests/bug1204147_subframe2.html9
-rw-r--r--editor/spellchecker/tests/bug678842_subframe.html8
-rw-r--r--editor/spellchecker/tests/bug717433_subframe.html8
-rw-r--r--editor/spellchecker/tests/de-DE/de_DE.aff2
-rw-r--r--editor/spellchecker/tests/de-DE/de_DE.dic6
-rw-r--r--editor/spellchecker/tests/en-AU/en_AU.aff2
-rw-r--r--editor/spellchecker/tests/en-AU/en_AU.dic4
-rw-r--r--editor/spellchecker/tests/en-GB/en_GB.aff2
-rw-r--r--editor/spellchecker/tests/en-GB/en_GB.dic4
-rw-r--r--editor/spellchecker/tests/mochitest.ini34
-rw-r--r--editor/spellchecker/tests/test_async_UpdateCurrentDictionary.html74
-rw-r--r--editor/spellchecker/tests/test_bug1200533.html159
-rw-r--r--editor/spellchecker/tests/test_bug1204147.html115
-rw-r--r--editor/spellchecker/tests/test_bug1205983.html135
-rw-r--r--editor/spellchecker/tests/test_bug1209414.html148
-rw-r--r--editor/spellchecker/tests/test_bug1219928.html69
-rw-r--r--editor/spellchecker/tests/test_bug1365383.html45
-rw-r--r--editor/spellchecker/tests/test_bug1418629.html209
-rw-r--r--editor/spellchecker/tests/test_bug1602526.html56
-rw-r--r--editor/spellchecker/tests/test_bug338427.html60
-rw-r--r--editor/spellchecker/tests/test_bug678842.html108
-rw-r--r--editor/spellchecker/tests/test_bug697981.html138
-rw-r--r--editor/spellchecker/tests/test_bug717433.html109
36 files changed, 6537 insertions, 0 deletions
diff --git a/editor/spellchecker/EditorSpellCheck.cpp b/editor/spellchecker/EditorSpellCheck.cpp
new file mode 100644
index 0000000000..0310ea31fe
--- /dev/null
+++ b/editor/spellchecker/EditorSpellCheck.cpp
@@ -0,0 +1,1000 @@
+/* -*- 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 "mozilla/Attributes.h" // for final
+#include "mozilla/EditorBase.h" // for EditorBase
+#include "mozilla/HTMLEditor.h" // for HTMLEditor
+#include "mozilla/dom/Element.h" // for Element
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/StaticRange.h"
+#include "mozilla/intl/LocaleService.h" // for retrieving app locale
+#include "mozilla/intl/MozLocale.h" // for mozilla::intl::Locale
+#include "mozilla/intl/OSPreferences.h" // for mozilla::intl::OSPreferences
+#include "mozilla/mozalloc.h" // for operator delete, etc
+#include "mozilla/mozSpellChecker.h" // for mozSpellChecker
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/TextServicesDocument.h" // for TextServicesDocument
+#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 "nsISupportsBase.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 "nsMemory.h" // for nsMemory
+#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;
+
+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();
+}
+
+/**
+ * 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);
+ value->GetAsAString(mDictionary);
+ 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;
+ nsString mDictionary;
+
+ 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 StoreCurrentDictionary(EditorBase* aEditorBase,
+ const nsACString& aDictionary) {
+ 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();
+ prefValue->SetAsAString(NS_ConvertUTF8toUTF16(aDictionary));
+
+ 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 ClearCurrentDictionary(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) {
+ 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);
+}
+
+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::GetCurrentDictionary(nsACString& aDictionary) {
+ NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
+
+ return mSpellChecker->GetCurrentDictionary(aDictionary);
+}
+
+NS_IMETHODIMP
+EditorSpellCheck::SetCurrentDictionary(const nsACString& aDictionary) {
+ 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)) {
+ if (!aDictionary.IsEmpty() &&
+ (mPreferredLang.IsEmpty() ||
+ !mPreferredLang.Equals(aDictionary,
+ nsCaseInsensitiveCStringComparator))) {
+ // 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.
+ StoreCurrentDictionary(mEditor, aDictionary);
+#ifdef DEBUG_DICT
+ printf("***** Writing content preferences for |%s|\n",
+ aDictionary.get());
+#endif
+ } else {
+ // If user sets a dictionary matching the language defined by
+ // document, we consider content pref has been canceled, and we clear
+ // it.
+ ClearCurrentDictionary(mEditor);
+#ifdef DEBUG_DICT
+ printf("***** Clearing content preferences for |%s|\n",
+ aDictionary.get());
+#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()) {
+ Preferences::SetCString("spellchecker.dictionary", aDictionary);
+#ifdef DEBUG_DICT
+ printf("***** Possibly storing spellchecker.dictionary |%s|\n",
+ aDictionary.get());
+#endif
+ }
+ }
+ }
+ return mSpellChecker->SetCurrentDictionary(aDictionary);
+}
+
+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;
+ uint32_t flags = 0;
+ mEditor->GetFlags(&flags);
+
+ // Get language with html5 algorithm
+ nsCOMPtr<nsIContent> rootContent;
+ HTMLEditor* htmlEditor = mEditor->AsHTMLEditor();
+ if (htmlEditor) {
+ if (flags & nsIEditor::eEditorMailMask) {
+ // Always determine the root content for a mail editor,
+ // even if not focused, to enable further processing below.
+ rootContent = htmlEditor->GetActiveEditingHost();
+ } else {
+ rootContent = htmlEditor->GetFocusedContent();
+ }
+ } else {
+ rootContent = mEditor->GetRoot();
+ }
+
+ if (!rootContent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Try to get topmost document's document element for embedded mail editor.
+ if (flags & nsIEditor::eEditorMailMask) {
+ RefPtr<Document> ownerDoc = rootContent->OwnerDoc();
+ Document* parentDoc = ownerDoc->GetInProcessParentDocument();
+ if (parentDoc) {
+ rootContent = parentDoc->GetDocumentElement();
+ if (!rootContent) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ RefPtr<DictionaryFetcher> fetcher =
+ new DictionaryFetcher(this, aCallback, mDictionaryFetcherGroup);
+ rootContent->GetLang(fetcher->mRootContentLang);
+ RefPtr<Document> doc = rootContent->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.
+void 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) {
+ aOutList.AppendElement(dictStr);
+#ifdef DEBUG_DICT
+ if (NS_SUCCEEDED(rv)) {
+ 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;
+ }
+ }
+}
+
+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.)
+ * 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 SetCurrentDictionary.
+ CopyUTF16toUTF8(aFetcher->mRootContentLang, mPreferredLang);
+#ifdef DEBUG_DICT
+ printf("***** mPreferredLang (element) |%s|\n", mPreferredLang.get());
+#endif
+
+ // If no luck, try the "Content-Language" header.
+ if (mPreferredLang.IsEmpty()) {
+ CopyUTF16toUTF8(aFetcher->mRootDocContentLang, mPreferredLang);
+#ifdef DEBUG_DICT
+ printf("***** mPreferredLang (content-language) |%s|\n",
+ mPreferredLang.get());
+#endif
+ }
+
+ // 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)) {
+ CopyUTF16toUTF8(aFetcher->mDictionary, dictName);
+ if (!dictName.IsEmpty()) {
+ AutoTArray<nsCString, 1> tryDictList;
+ BuildDictionaryList(dictName, dictList, DICT_NORMAL_COMPARE, tryDictList);
+
+ RefPtr<EditorSpellCheck> self = this;
+ RefPtr<DictionaryFetcher> fetcher = aFetcher;
+ mSpellChecker->SetCurrentDictionaryFromList(tryDictList)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, fetcher]() {
+#ifdef DEBUG_DICT
+ printf("***** Assigned from content preferences |%s|\n",
+ dictName.get());
+#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.
+ ClearCurrentDictionary(self->mEditor);
+
+ // Priority 2 or later will handled by the following
+ self->SetFallbackDictionary(fetcher);
+ });
+ return NS_OK;
+ }
+ }
+ SetFallbackDictionary(aFetcher);
+ return NS_OK;
+}
+
+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 language of the element
+ // or document.
+ nsAutoCString dictName(mPreferredLang);
+#ifdef DEBUG_DICT
+ printf("***** Assigned from element/doc |%s|\n", dictName.get());
+#endif
+
+ // Get the preference value.
+ nsAutoCString preferredDict;
+ Preferences::GetLocalizedCString("spellchecker.dictionary", preferredDict);
+
+ nsAutoCString appLocaleStr;
+ if (!dictName.IsEmpty()) {
+ // RFC 5646 explicitly states that matches should be case-insensitive.
+ BuildDictionaryList(dictName, dictList, DICT_COMPARE_CASE_INSENSITIVE,
+ tryDictList);
+
+#ifdef DEBUG_DICT
+ printf("***** Trying from element/doc |%s| \n", dictName.get());
+#endif
+
+ // Required dictionary was not available. Try to get a dictionary
+ // matching at least language part of dictName.
+ mozilla::intl::Locale loc = mozilla::intl::Locale(dictName);
+ nsAutoCString langCode(loc.GetLanguage());
+
+ // Try dictionary.spellchecker preference, if it starts with langCode,
+ // so we don't just get any random dictionary matching the language.
+ if (!preferredDict.IsEmpty() &&
+ nsStyleUtil::DashMatchCompare(NS_ConvertUTF8toUTF16(preferredDict),
+ NS_ConvertUTF8toUTF16(langCode),
+ nsTDefaultStringComparator)) {
+#ifdef DEBUG_DICT
+ printf(
+ "***** Trying preference value |%s| since it matches language code\n",
+ preferredDict.get());
+#endif
+ BuildDictionaryList(preferredDict, dictList,
+ DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
+ }
+
+ if (tryDictList.IsEmpty()) {
+ // Use the application locale dictionary when the required language
+ // equlas applocation locale language.
+ LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
+ if (!appLocaleStr.IsEmpty()) {
+ mozilla::intl::Locale appLoc = mozilla::intl::Locale(appLocaleStr);
+ if (langCode.Equals(appLoc.GetLanguage())) {
+ BuildDictionaryList(appLocaleStr, dictList,
+ DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
+ }
+ }
+
+ // 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 = mozilla::intl::Locale(sysLocaleStr);
+ if (langCode.Equals(sysLoc.GetLanguage())) {
+ BuildDictionaryList(sysLocaleStr, dictList,
+ DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
+ }
+ }
+ }
+
+ // 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);
+ }
+
+ // Priority 3:
+ // If the document didn't supply a dictionary or the setting failed,
+ // try the user preference next.
+ if (!preferredDict.IsEmpty()) {
+#ifdef DEBUG_DICT
+ printf("***** Trying preference value |%s|\n", preferredDict.get());
+#endif
+ BuildDictionaryList(preferredDict, dictList, DICT_NORMAL_COMPARE,
+ tryDictList);
+ }
+
+ // Priority 4:
+ // As next fallback, try the current locale.
+ if (appLocaleStr.IsEmpty()) {
+ LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
+ }
+#ifdef DEBUG_DICT
+ printf("***** Trying locale |%s|\n", appLocaleStr.get());
+#endif
+ 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.
+ nsAutoCString currentDictionary;
+ GetCurrentDictionary(currentDictionary);
+ if (!currentDictionary.IsEmpty() && tryDictList.IsEmpty()) {
+#ifdef DEBUG_DICT
+ printf("***** Retrieved current dict |%s|\n", currentDictionary.get());
+#endif
+ EndUpdateDictionary();
+ if (aFetcher->mCallback) {
+ aFetcher->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
+ BuildDictionaryList(lang, dictList, DICT_COMPARE_CASE_INSENSITIVE,
+ tryDictList);
+ }
+ }
+
+ // Priority 7:
+ // If it does not work, pick the first one.
+ if (!dictList.IsEmpty()) {
+ BuildDictionaryList(dictList[0], dictList, DICT_NORMAL_COMPARE,
+ tryDictList);
+#ifdef DEBUG_DICT
+ printf("***** Trying first of list |%s|\n", dictList[0].get());
+#endif
+ }
+
+ RefPtr<EditorSpellCheck> self = this;
+ RefPtr<DictionaryFetcher> fetcher = aFetcher;
+ mSpellChecker->SetCurrentDictionaryFromList(tryDictList)
+ ->Then(GetMainThreadSerialEventTarget(), __func__, [self, fetcher]() {
+ // 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.
+ self->DeleteSuggestedWordList();
+ self->EndUpdateDictionary();
+ if (fetcher->mCallback) {
+ fetcher->mCallback->EditorSpellCheckDone();
+ }
+ });
+}
+
+} // namespace mozilla
diff --git a/editor/spellchecker/EditorSpellCheck.h b/editor/spellchecker/EditorSpellCheck.h
new file mode 100644
index 0000000000..0188d7ddfe
--- /dev/null
+++ b/editor/spellchecker/EditorSpellCheck.h
@@ -0,0 +1,98 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_EditorSpellCheck_h
+#define mozilla_EditorSpellCheck_h
+
+#include "mozilla/mozSpellChecker.h" // for mozilla::CheckWordPromise
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsCycleCollectionParticipant.h"
+#include "nsIEditorSpellCheck.h" // for NS_DECL_NSIEDITORSPELLCHECK, etc
+#include "nsISupportsImpl.h"
+#include "nsString.h" // for nsString
+#include "nsTArray.h" // for nsTArray
+#include "nscore.h" // for nsresult
+
+class mozSpellChecker;
+class nsIEditor;
+
+namespace mozilla {
+
+class DictionaryFetcher;
+class EditorBase;
+
+enum dictCompare {
+ DICT_NORMAL_COMPARE,
+ DICT_COMPARE_CASE_INSENSITIVE,
+ DICT_COMPARE_DASHMATCH
+};
+
+class EditorSpellCheck final : public nsIEditorSpellCheck {
+ friend class DictionaryFetcher;
+
+ public:
+ EditorSpellCheck();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(EditorSpellCheck)
+
+ /* Declare all methods in the nsIEditorSpellCheck interface */
+ NS_DECL_NSIEDITORSPELLCHECK
+
+ mozSpellChecker* GetSpellChecker();
+
+ /**
+ * Like CheckCurrentWord, checks the word you give it, returning true via
+ * promise if it's misspelled.
+ * This is faster than CheckCurrentWord because it does not compute
+ * any suggestions.
+ *
+ * Watch out: this does not clear any suggestions left over from previous
+ * calls to CheckCurrentWord, so there may be suggestions, but they will be
+ * invalid.
+ */
+ RefPtr<mozilla::CheckWordPromise> CheckCurrentWordsNoSuggest(
+ const nsTArray<nsString>& aSuggestedWords);
+
+ protected:
+ virtual ~EditorSpellCheck();
+
+ RefPtr<mozSpellChecker> mSpellChecker;
+ RefPtr<EditorBase> mEditor;
+
+ nsTArray<nsString> mSuggestedWordList;
+
+ // these are the words in the current personal dictionary,
+ // GetPersonalDictionary must be called to load them.
+ nsTArray<nsString> mDictionaryList;
+
+ nsCString mPreferredLang;
+
+ uint32_t mTxtSrvFilterType;
+ int32_t mSuggestedWordIndex;
+ int32_t mDictionaryIndex;
+ uint32_t mDictionaryFetcherGroup;
+
+ bool mUpdateDictionaryRunning;
+
+ nsresult DeleteSuggestedWordList();
+
+ void BuildDictionaryList(const nsACString& aDictName,
+ const nsTArray<nsCString>& aDictList,
+ enum dictCompare aCompareType,
+ nsTArray<nsCString>& aOutList);
+
+ nsresult DictionaryFetched(DictionaryFetcher* aFetchState);
+
+ void SetFallbackDictionary(DictionaryFetcher* aFetcher);
+
+ public:
+ void BeginUpdateDictionary() { mUpdateDictionaryRunning = true; }
+ void EndUpdateDictionary() { mUpdateDictionaryRunning = false; }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_EditorSpellCheck_h
diff --git a/editor/spellchecker/FilteredContentIterator.cpp b/editor/spellchecker/FilteredContentIterator.cpp
new file mode 100644
index 0000000000..d2a5f948d9
--- /dev/null
+++ b/editor/spellchecker/FilteredContentIterator.cpp
@@ -0,0 +1,406 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "FilteredContentIterator.h"
+
+#include <utility>
+
+#include "mozilla/ContentIterator.h"
+#include "mozilla/dom/AbstractRange.h"
+#include "mozilla/mozalloc.h"
+#include "nsAtom.h"
+#include "nsComponentManagerUtils.h"
+#include "nsComposeTxtSrvFilter.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIContent.h"
+#include "nsINode.h"
+#include "nsISupportsBase.h"
+#include "nsISupportsUtils.h"
+#include "nsRange.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+FilteredContentIterator::FilteredContentIterator(
+ UniquePtr<nsComposeTxtSrvFilter> aFilter)
+ : mCurrentIterator(nullptr),
+ mFilter(std::move(aFilter)),
+ mDidSkip(false),
+ mIsOutOfRange(false),
+ mDirection(eDirNotSet) {}
+
+FilteredContentIterator::~FilteredContentIterator() {}
+
+NS_IMPL_CYCLE_COLLECTION(FilteredContentIterator, mPostIterator, mPreIterator,
+ mRange)
+
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(FilteredContentIterator, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(FilteredContentIterator, Release)
+
+nsresult FilteredContentIterator::Init(nsINode* aRoot) {
+ NS_ENSURE_ARG_POINTER(aRoot);
+ mIsOutOfRange = false;
+ mDirection = eForward;
+ mCurrentIterator = &mPreIterator;
+
+ mRange = nsRange::Create(aRoot);
+ mRange->SelectNode(*aRoot, IgnoreErrors());
+
+ nsresult rv = mPreIterator.Init(mRange);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mPostIterator.Init(mRange);
+}
+
+nsresult FilteredContentIterator::Init(const AbstractRange* aAbstractRange) {
+ if (NS_WARN_IF(!aAbstractRange)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (NS_WARN_IF(!aAbstractRange->IsPositioned())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mRange = nsRange::Create(aAbstractRange, IgnoreErrors());
+ if (NS_WARN_IF(!mRange)) {
+ return NS_ERROR_FAILURE;
+ }
+ return InitWithRange();
+}
+
+nsresult FilteredContentIterator::Init(nsINode* aStartContainer,
+ uint32_t aStartOffset,
+ nsINode* aEndContainer,
+ uint32_t aEndOffset) {
+ return Init(RawRangeBoundary(aStartContainer, aStartOffset),
+ RawRangeBoundary(aEndContainer, aEndOffset));
+}
+
+nsresult FilteredContentIterator::Init(const RawRangeBoundary& aStartBoundary,
+ const RawRangeBoundary& aEndBoundary) {
+ RefPtr<nsRange> range =
+ nsRange::Create(aStartBoundary, aEndBoundary, IgnoreErrors());
+ if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ MOZ_ASSERT(range->StartRef() == aStartBoundary);
+ MOZ_ASSERT(range->EndRef() == aEndBoundary);
+
+ mRange = std::move(range);
+
+ return InitWithRange();
+}
+
+nsresult FilteredContentIterator::InitWithRange() {
+ MOZ_ASSERT(mRange);
+ MOZ_ASSERT(mRange->IsPositioned());
+
+ mIsOutOfRange = false;
+ mDirection = eForward;
+ mCurrentIterator = &mPreIterator;
+
+ nsresult rv = mPreIterator.Init(mRange);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return mPostIterator.Init(mRange);
+}
+
+nsresult FilteredContentIterator::SwitchDirections(bool aChangeToForward) {
+ nsINode* node = mCurrentIterator->GetCurrentNode();
+
+ if (aChangeToForward) {
+ mCurrentIterator = &mPreIterator;
+ mDirection = eForward;
+ } else {
+ mCurrentIterator = &mPostIterator;
+ mDirection = eBackward;
+ }
+
+ if (node) {
+ nsresult rv = mCurrentIterator->PositionAt(node);
+ if (NS_FAILED(rv)) {
+ mIsOutOfRange = true;
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+void FilteredContentIterator::First() {
+ if (!mCurrentIterator) {
+ NS_ERROR("Missing iterator!");
+
+ return;
+ }
+
+ // If we are switching directions then
+ // we need to switch how we process the nodes
+ if (mDirection != eForward) {
+ mCurrentIterator = &mPreIterator;
+ mDirection = eForward;
+ mIsOutOfRange = false;
+ }
+
+ mCurrentIterator->First();
+
+ if (mCurrentIterator->IsDone()) {
+ return;
+ }
+
+ nsINode* currentNode = mCurrentIterator->GetCurrentNode();
+
+ bool didCross;
+ CheckAdvNode(currentNode, didCross, eForward);
+}
+
+void FilteredContentIterator::Last() {
+ if (!mCurrentIterator) {
+ NS_ERROR("Missing iterator!");
+
+ return;
+ }
+
+ // If we are switching directions then
+ // we need to switch how we process the nodes
+ if (mDirection != eBackward) {
+ mCurrentIterator = &mPostIterator;
+ mDirection = eBackward;
+ mIsOutOfRange = false;
+ }
+
+ mCurrentIterator->Last();
+
+ if (mCurrentIterator->IsDone()) {
+ return;
+ }
+
+ nsINode* currentNode = mCurrentIterator->GetCurrentNode();
+
+ bool didCross;
+ CheckAdvNode(currentNode, didCross, eBackward);
+}
+
+///////////////////////////////////////////////////////////////////////////
+// ContentToParentOffset: returns the content node's parent and offset.
+//
+static void ContentToParentOffset(nsIContent* aContent, nsIContent** aParent,
+ int32_t* aOffset) {
+ if (!aParent || !aOffset) return;
+
+ *aParent = nullptr;
+ *aOffset = 0;
+
+ if (!aContent) return;
+
+ nsCOMPtr<nsIContent> parent = aContent->GetParent();
+ if (!parent) return;
+
+ *aOffset = parent->ComputeIndexOf(aContent);
+ parent.forget(aParent);
+}
+
+///////////////////////////////////////////////////////////////////////////
+// ContentIsInTraversalRange: returns true if content is visited during
+// the traversal of the range in the specified mode.
+//
+static bool ContentIsInTraversalRange(nsIContent* aContent, bool aIsPreMode,
+ nsINode* aStartContainer,
+ int32_t aStartOffset,
+ nsINode* aEndContainer,
+ int32_t aEndOffset) {
+ NS_ENSURE_TRUE(aStartContainer && aEndContainer && aContent, false);
+
+ nsCOMPtr<nsIContent> parentNode;
+ int32_t indx = 0;
+
+ ContentToParentOffset(aContent, getter_AddRefs(parentNode), &indx);
+
+ NS_ENSURE_TRUE(parentNode, false);
+
+ if (!aIsPreMode) ++indx;
+
+ const Maybe<int32_t> startRes = nsContentUtils::ComparePoints(
+ aStartContainer, aStartOffset, parentNode, indx);
+ const Maybe<int32_t> endRes = nsContentUtils::ComparePoints(
+ aEndContainer, aEndOffset, parentNode, indx);
+ return !NS_WARN_IF(!startRes || !endRes) && (*startRes <= 0) &&
+ (*endRes >= 0);
+}
+
+static bool ContentIsInTraversalRange(nsRange* aRange, nsIContent* aNextContent,
+ bool aIsPreMode) {
+ // XXXbz we have a caller below (in AdvanceNode) who passes null for
+ // aNextContent!
+ NS_ENSURE_TRUE(aNextContent && aRange, false);
+
+ return ContentIsInTraversalRange(
+ aNextContent, aIsPreMode, aRange->GetStartContainer(),
+ static_cast<int32_t>(aRange->StartOffset()), aRange->GetEndContainer(),
+ static_cast<int32_t>(aRange->EndOffset()));
+}
+
+// Helper function to advance to the next or previous node
+nsresult FilteredContentIterator::AdvanceNode(nsINode* aNode,
+ nsINode*& aNewNode,
+ eDirectionType aDir) {
+ nsCOMPtr<nsIContent> nextNode;
+ if (aDir == eForward) {
+ nextNode = aNode->GetNextSibling();
+ } else {
+ nextNode = aNode->GetPreviousSibling();
+ }
+
+ if (nextNode) {
+ // If we got here, that means we found the nxt/prv node
+ // make sure it is in our DOMRange
+ bool intersects =
+ ContentIsInTraversalRange(mRange, nextNode, aDir == eForward);
+ if (intersects) {
+ aNewNode = nextNode;
+ NS_ADDREF(aNewNode);
+ return NS_OK;
+ }
+ } else {
+ // The next node was null so we need to walk up the parent(s)
+ nsCOMPtr<nsINode> parent = aNode->GetParentNode();
+ NS_ASSERTION(parent, "parent can't be nullptr");
+
+ // Make sure the parent is in the DOMRange before going further
+ // XXXbz why are we passing nextNode, not the parent??? If this gets fixed,
+ // then ContentIsInTraversalRange can stop null-checking its second arg.
+ bool intersects =
+ ContentIsInTraversalRange(mRange, nextNode, aDir == eForward);
+ if (intersects) {
+ // Now find the nxt/prv node after/before this node
+ nsresult rv = AdvanceNode(parent, aNewNode, aDir);
+ if (NS_SUCCEEDED(rv) && aNewNode) {
+ return NS_OK;
+ }
+ }
+ }
+
+ // if we get here it pretty much means
+ // we went out of the DOM Range
+ mIsOutOfRange = true;
+
+ return NS_ERROR_FAILURE;
+}
+
+// Helper function to see if the next/prev node should be skipped
+void FilteredContentIterator::CheckAdvNode(nsINode* aNode, bool& aDidSkip,
+ eDirectionType aDir) {
+ aDidSkip = false;
+ mIsOutOfRange = false;
+
+ if (aNode && mFilter) {
+ nsCOMPtr<nsINode> currentNode = aNode;
+ while (1) {
+ if (mFilter->Skip(aNode)) {
+ aDidSkip = true;
+ // Get the next/prev node and then
+ // see if we should skip that
+ nsCOMPtr<nsINode> advNode;
+ nsresult rv = AdvanceNode(aNode, *getter_AddRefs(advNode), aDir);
+ if (NS_SUCCEEDED(rv) && advNode) {
+ aNode = advNode;
+ } else {
+ return; // fell out of range
+ }
+ } else {
+ if (aNode != currentNode) {
+ nsCOMPtr<nsIContent> content(do_QueryInterface(aNode));
+ mCurrentIterator->PositionAt(content);
+ }
+ return; // found something
+ }
+ }
+ }
+}
+
+void FilteredContentIterator::Next() {
+ if (mIsOutOfRange || !mCurrentIterator) {
+ NS_ASSERTION(mCurrentIterator, "Missing iterator!");
+
+ return;
+ }
+
+ // If we are switching directions then
+ // we need to switch how we process the nodes
+ if (mDirection != eForward) {
+ nsresult rv = SwitchDirections(true);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ }
+
+ mCurrentIterator->Next();
+
+ if (mCurrentIterator->IsDone()) {
+ return;
+ }
+
+ // If we can't get the current node then
+ // don't check to see if we can skip it
+ nsINode* currentNode = mCurrentIterator->GetCurrentNode();
+
+ CheckAdvNode(currentNode, mDidSkip, eForward);
+}
+
+void FilteredContentIterator::Prev() {
+ if (mIsOutOfRange || !mCurrentIterator) {
+ NS_ASSERTION(mCurrentIterator, "Missing iterator!");
+
+ return;
+ }
+
+ // If we are switching directions then
+ // we need to switch how we process the nodes
+ if (mDirection != eBackward) {
+ nsresult rv = SwitchDirections(false);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ }
+
+ mCurrentIterator->Prev();
+
+ if (mCurrentIterator->IsDone()) {
+ return;
+ }
+
+ // If we can't get the current node then
+ // don't check to see if we can skip it
+ nsINode* currentNode = mCurrentIterator->GetCurrentNode();
+
+ CheckAdvNode(currentNode, mDidSkip, eBackward);
+}
+
+nsINode* FilteredContentIterator::GetCurrentNode() {
+ if (mIsOutOfRange || !mCurrentIterator) {
+ return nullptr;
+ }
+
+ return mCurrentIterator->GetCurrentNode();
+}
+
+bool FilteredContentIterator::IsDone() {
+ if (mIsOutOfRange || !mCurrentIterator) {
+ return true;
+ }
+
+ return mCurrentIterator->IsDone();
+}
+
+nsresult FilteredContentIterator::PositionAt(nsINode* aCurNode) {
+ NS_ENSURE_TRUE(mCurrentIterator, NS_ERROR_FAILURE);
+ mIsOutOfRange = false;
+ return mCurrentIterator->PositionAt(aCurNode);
+}
+
+} // namespace mozilla
diff --git a/editor/spellchecker/FilteredContentIterator.h b/editor/spellchecker/FilteredContentIterator.h
new file mode 100644
index 0000000000..2e2712dce4
--- /dev/null
+++ b/editor/spellchecker/FilteredContentIterator.h
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef FilteredContentIterator_h
+#define FilteredContentIterator_h
+
+#include "nsComposeTxtSrvFilter.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+#include "nscore.h"
+#include "mozilla/ContentIterator.h"
+#include "mozilla/UniquePtr.h"
+
+class nsAtom;
+class nsINode;
+class nsRange;
+
+namespace mozilla {
+
+namespace dom {
+class AbstractRange;
+}
+
+class FilteredContentIterator final {
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(FilteredContentIterator)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(FilteredContentIterator)
+
+ explicit FilteredContentIterator(UniquePtr<nsComposeTxtSrvFilter> aFilter);
+
+ nsresult Init(nsINode* aRoot);
+ nsresult Init(const dom::AbstractRange* aAbstractRange);
+ nsresult Init(nsINode* aStartContainer, uint32_t aStartOffset,
+ nsINode* aEndContainer, uint32_t aEndOffset);
+ nsresult Init(const RawRangeBoundary& aStartBoundary,
+ const RawRangeBoundary& aEndBoundary);
+ void First();
+ void Last();
+ void Next();
+ void Prev();
+ nsINode* GetCurrentNode();
+ bool IsDone();
+ nsresult PositionAt(nsINode* aCurNode);
+
+ /* Helpers */
+ bool DidSkip() { return mDidSkip; }
+ void ClearDidSkip() { mDidSkip = false; }
+
+ protected:
+ FilteredContentIterator()
+ : mDidSkip(false), mIsOutOfRange(false), mDirection{eDirNotSet} {}
+
+ virtual ~FilteredContentIterator();
+
+ /**
+ * Callers must guarantee that mRange isn't nullptr and it's positioned.
+ */
+ nsresult InitWithRange();
+
+ // enum to give us the direction
+ typedef enum { eDirNotSet, eForward, eBackward } eDirectionType;
+ nsresult AdvanceNode(nsINode* aNode, nsINode*& aNewNode, eDirectionType aDir);
+ void CheckAdvNode(nsINode* aNode, bool& aDidSkip, eDirectionType aDir);
+ nsresult SwitchDirections(bool aChangeToForward);
+
+ ContentIteratorBase* MOZ_NON_OWNING_REF mCurrentIterator;
+ PostContentIterator mPostIterator;
+ PreContentIterator mPreIterator;
+
+ RefPtr<nsAtom> mBlockQuoteAtom;
+ RefPtr<nsAtom> mScriptAtom;
+ RefPtr<nsAtom> mTextAreaAtom;
+ RefPtr<nsAtom> mSelectAreaAtom;
+ RefPtr<nsAtom> mMapAtom;
+
+ UniquePtr<nsComposeTxtSrvFilter> mFilter;
+ RefPtr<nsRange> mRange;
+ bool mDidSkip;
+ bool mIsOutOfRange;
+ eDirectionType mDirection;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef FilteredContentIterator_h
diff --git a/editor/spellchecker/TextServicesDocument.cpp b/editor/spellchecker/TextServicesDocument.cpp
new file mode 100644
index 0000000000..050a5f756d
--- /dev/null
+++ b/editor/spellchecker/TextServicesDocument.cpp
@@ -0,0 +1,2918 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "TextServicesDocument.h"
+
+#include "FilteredContentIterator.h" // for FilteredContentIterator
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/EditorUtils.h" // for AutoTransactionBatchExternal
+#include "mozilla/dom/AbstractRange.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/mozalloc.h" // for operator new, etc
+#include "mozilla/TextEditor.h" // for TextEditor
+#include "nsAString.h" // for nsAString::Length, etc
+#include "nsContentUtils.h" // for nsContentUtils
+#include "nsComposeTxtSrvFilter.h"
+#include "nsDebug.h" // for NS_ENSURE_TRUE, etc
+#include "nsDependentSubstring.h" // for Substring
+#include "nsError.h" // for NS_OK, NS_ERROR_FAILURE, etc
+#include "nsGenericHTMLElement.h" // for nsGenericHTMLElement
+#include "nsIContent.h" // for nsIContent, etc
+#include "nsID.h" // for NS_GET_IID
+#include "nsIEditor.h" // for nsIEditor, etc
+#include "nsIEditorSpellCheck.h" // for nsIEditorSpellCheck, etc
+#include "nsINode.h" // for nsINode
+#include "nsISelectionController.h" // for nsISelectionController, etc
+#include "nsISupportsBase.h" // for nsISupports
+#include "nsISupportsUtils.h" // for NS_IF_ADDREF, NS_ADDREF, etc
+#include "mozilla/intl/WordBreaker.h" // for WordRange, WordBreaker
+#include "nsRange.h" // for nsRange
+#include "nsString.h" // for nsString, nsAutoString
+#include "nscore.h" // for nsresult, NS_IMETHODIMP, etc
+#include "mozilla/UniquePtr.h" // for UniquePtr
+
+namespace mozilla {
+
+using namespace dom;
+
+class OffsetEntry final {
+ public:
+ OffsetEntry(nsINode* aNode, int32_t aOffset, int32_t aLength)
+ : mNode(aNode),
+ mNodeOffset(0),
+ mStrOffset(aOffset),
+ mLength(aLength),
+ mIsInsertedText(false),
+ mIsValid(true) {
+ if (mStrOffset < 1) {
+ mStrOffset = 0;
+ }
+ if (mLength < 1) {
+ mLength = 0;
+ }
+ }
+
+ virtual ~OffsetEntry() {}
+
+ nsINode* mNode;
+ int32_t mNodeOffset;
+ int32_t mStrOffset;
+ int32_t mLength;
+ bool mIsInsertedText;
+ bool mIsValid;
+};
+
+TextServicesDocument::TextServicesDocument()
+ : mTxtSvcFilterType(0),
+ mSelStartIndex(-1),
+ mSelStartOffset(-1),
+ mSelEndIndex(-1),
+ mSelEndOffset(-1),
+ mIteratorStatus(IteratorStatus::eDone) {}
+
+TextServicesDocument::~TextServicesDocument() {
+ ClearOffsetTable(&mOffsetTable);
+}
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TextServicesDocument)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TextServicesDocument)
+
+NS_INTERFACE_MAP_BEGIN(TextServicesDocument)
+ NS_INTERFACE_MAP_ENTRY(nsIEditActionListener)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditActionListener)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(TextServicesDocument)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(TextServicesDocument, mDocument, mSelCon, mTextEditor,
+ mFilteredIter, mPrevTextBlock, mNextTextBlock, mExtent)
+
+nsresult TextServicesDocument::InitWithEditor(nsIEditor* aEditor) {
+ nsCOMPtr<nsISelectionController> selCon;
+
+ NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER);
+
+ // Check to see if we already have an mSelCon. If we do, it
+ // better be the same one the editor uses!
+
+ nsresult rv = aEditor->GetSelectionController(getter_AddRefs(selCon));
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!selCon || (mSelCon && selCon != mSelCon)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mSelCon) {
+ mSelCon = selCon;
+ }
+
+ // Check to see if we already have an mDocument. If we do, it
+ // better be the same one the editor uses!
+
+ RefPtr<Document> doc = aEditor->AsEditorBase()->GetDocument();
+ if (!doc || (mDocument && doc != mDocument)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mDocument) {
+ mDocument = doc;
+
+ rv = CreateDocumentContentIterator(getter_AddRefs(mFilteredIter));
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::eDone;
+
+ rv = FirstBlock();
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ mTextEditor = aEditor->AsTextEditor();
+
+ rv = aEditor->AddEditActionListener(this);
+
+ return rv;
+}
+
+nsresult TextServicesDocument::SetExtent(const AbstractRange* aAbstractRange) {
+ MOZ_ASSERT(aAbstractRange);
+
+ if (NS_WARN_IF(!mDocument)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // We need to store a copy of aAbstractRange since we don't know where it
+ // came from.
+ mExtent = nsRange::Create(aAbstractRange, IgnoreErrors());
+ if (NS_WARN_IF(!mExtent)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Create a new iterator based on our new extent range.
+ nsresult rv =
+ CreateFilteredContentIterator(mExtent, getter_AddRefs(mFilteredIter));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Now position the iterator at the start of the first block
+ // in the range.
+ mIteratorStatus = IteratorStatus::eDone;
+
+ rv = FirstBlock();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "FirstBlock() failed");
+ return rv;
+}
+
+nsresult TextServicesDocument::ExpandRangeToWordBoundaries(
+ StaticRange* aStaticRange) {
+ MOZ_ASSERT(aStaticRange);
+
+ // Get the end points of the range.
+
+ nsCOMPtr<nsINode> rngStartNode, rngEndNode;
+ int32_t rngStartOffset, rngEndOffset;
+
+ nsresult rv = GetRangeEndPoints(aStaticRange, getter_AddRefs(rngStartNode),
+ &rngStartOffset, getter_AddRefs(rngEndNode),
+ &rngEndOffset);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Create a content iterator based on the range.
+ RefPtr<FilteredContentIterator> filteredIter;
+ rv =
+ CreateFilteredContentIterator(aStaticRange, getter_AddRefs(filteredIter));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Find the first text node in the range.
+ IteratorStatus iterStatus = IteratorStatus::eDone;
+ rv = FirstTextNode(filteredIter, &iterStatus);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (iterStatus == IteratorStatus::eDone) {
+ // No text was found so there's no adjustment necessary!
+ return NS_OK;
+ }
+
+ nsINode* firstText = filteredIter->GetCurrentNode();
+ if (NS_WARN_IF(!firstText)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Find the last text node in the range.
+
+ rv = LastTextNode(filteredIter, &iterStatus);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (iterStatus == IteratorStatus::eDone) {
+ // We should never get here because a first text block
+ // was found above.
+ NS_ASSERTION(false, "Found a first without a last!");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsINode* lastText = filteredIter->GetCurrentNode();
+ if (NS_WARN_IF(!lastText)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Now make sure our end points are in terms of text nodes in the range!
+
+ if (rngStartNode != firstText) {
+ // The range includes the start of the first text node!
+ rngStartNode = firstText;
+ rngStartOffset = 0;
+ }
+
+ if (rngEndNode != lastText) {
+ // The range includes the end of the last text node!
+ rngEndNode = lastText;
+ rngEndOffset = lastText->Length();
+ }
+
+ // Create a doc iterator so that we can scan beyond
+ // the bounds of the extent range.
+
+ RefPtr<FilteredContentIterator> docFilteredIter;
+ rv = CreateDocumentContentIterator(getter_AddRefs(docFilteredIter));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Grab all the text in the block containing our
+ // first text node.
+ rv = docFilteredIter->PositionAt(firstText);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ iterStatus = IteratorStatus::eValid;
+
+ nsTArray<OffsetEntry*> offsetTable;
+ nsAutoString blockStr;
+
+ rv = CreateOffsetTable(&offsetTable, docFilteredIter, &iterStatus, nullptr,
+ &blockStr);
+ if (NS_FAILED(rv)) {
+ ClearOffsetTable(&offsetTable);
+ return rv;
+ }
+
+ nsCOMPtr<nsINode> wordStartNode, wordEndNode;
+ int32_t wordStartOffset, wordEndOffset;
+
+ rv = FindWordBounds(&offsetTable, &blockStr, rngStartNode, rngStartOffset,
+ getter_AddRefs(wordStartNode), &wordStartOffset,
+ getter_AddRefs(wordEndNode), &wordEndOffset);
+
+ ClearOffsetTable(&offsetTable);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rngStartNode = wordStartNode;
+ rngStartOffset = wordStartOffset;
+
+ // Grab all the text in the block containing our
+ // last text node.
+
+ rv = docFilteredIter->PositionAt(lastText);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ iterStatus = IteratorStatus::eValid;
+
+ rv = CreateOffsetTable(&offsetTable, docFilteredIter, &iterStatus, nullptr,
+ &blockStr);
+ if (NS_FAILED(rv)) {
+ ClearOffsetTable(&offsetTable);
+ return rv;
+ }
+
+ rv = FindWordBounds(&offsetTable, &blockStr, rngEndNode, rngEndOffset,
+ getter_AddRefs(wordStartNode), &wordStartOffset,
+ getter_AddRefs(wordEndNode), &wordEndOffset);
+
+ ClearOffsetTable(&offsetTable);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // To prevent expanding the range too much, we only change
+ // rngEndNode and rngEndOffset if it isn't already at the start of the
+ // word and isn't equivalent to rngStartNode and rngStartOffset.
+
+ if (rngEndNode != wordStartNode || rngEndOffset != wordStartOffset ||
+ (rngEndNode == rngStartNode && rngEndOffset == rngStartOffset)) {
+ rngEndNode = wordEndNode;
+ rngEndOffset = wordEndOffset;
+ }
+
+ // Now adjust the range so that it uses our new end points.
+ rv = aStaticRange->SetStartAndEnd(rngStartNode, rngStartOffset, rngEndNode,
+ rngEndOffset);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to update the given range");
+ return rv;
+}
+
+nsresult TextServicesDocument::SetFilterType(uint32_t aFilterType) {
+ mTxtSvcFilterType = aFilterType;
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::GetCurrentTextBlock(nsAString& aStr) {
+ aStr.Truncate();
+
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ nsresult rv = CreateOffsetTable(&mOffsetTable, mFilteredIter,
+ &mIteratorStatus, mExtent, &aStr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::FirstBlock() {
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ nsresult rv = FirstTextNode(mFilteredIter, &mIteratorStatus);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Keep track of prev and next blocks, just in case
+ // the text service blows away the current block.
+
+ if (mIteratorStatus == IteratorStatus::eValid) {
+ mPrevTextBlock = nullptr;
+ rv = GetFirstTextNodeInNextBlock(getter_AddRefs(mNextTextBlock));
+ } else {
+ // There's no text block in the document!
+
+ mPrevTextBlock = nullptr;
+ mNextTextBlock = nullptr;
+ }
+
+ // XXX Result of FirstTextNode() or GetFirstTextNodeInNextBlock().
+ return rv;
+}
+
+nsresult TextServicesDocument::LastSelectedBlock(
+ BlockSelectionStatus* aSelStatus, int32_t* aSelOffset,
+ int32_t* aSelLength) {
+ NS_ENSURE_TRUE(aSelStatus && aSelOffset && aSelLength, NS_ERROR_NULL_POINTER);
+
+ mIteratorStatus = IteratorStatus::eDone;
+
+ *aSelStatus = BlockSelectionStatus::eBlockNotFound;
+ *aSelOffset = *aSelLength = -1;
+
+ if (!mSelCon || !mFilteredIter) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ if (NS_WARN_IF(!selection)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<const nsRange> range;
+ nsCOMPtr<nsINode> parent;
+
+ if (selection->IsCollapsed()) {
+ // We have a caret. Check if the caret is in a text node.
+ // If it is, make the text node's block the current block.
+ // If the caret isn't in a text node, search forwards in
+ // the document, till we find a text node.
+
+ range = selection->GetRangeAt(0);
+
+ if (!range) {
+ return NS_ERROR_FAILURE;
+ }
+
+ parent = range->GetStartContainer();
+ if (!parent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+ if (parent->IsText()) {
+ // The caret is in a text node. Find the beginning
+ // of the text block containing this text node and
+ // return.
+
+ rv = mFilteredIter->PositionAt(parent);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::eValid;
+
+ rv = CreateOffsetTable(&mOffsetTable, mFilteredIter, &mIteratorStatus,
+ mExtent, nullptr);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (*aSelStatus == BlockSelectionStatus::eBlockContains) {
+ rv = SetSelectionInternal(*aSelOffset, *aSelLength, false);
+ }
+ } else {
+ // The caret isn't in a text node. Create an iterator
+ // based on a range that extends from the current caret
+ // position to the end of the document, then walk forwards
+ // till you find a text node, then find the beginning of it's block.
+
+ range = CreateDocumentContentRootToNodeOffsetRange(
+ parent, range->StartOffset(), false);
+
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (range->Collapsed()) {
+ // If we get here, the range is collapsed because there is nothing after
+ // the caret! Just return NS_OK;
+ return NS_OK;
+ }
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ rv = CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ filteredIter->First();
+
+ nsIContent* content = nullptr;
+ for (; !filteredIter->IsDone(); filteredIter->Next()) {
+ nsINode* currentNode = filteredIter->GetCurrentNode();
+ if (currentNode->IsText()) {
+ content = currentNode->AsContent();
+ break;
+ }
+ }
+
+ if (!content) {
+ return NS_OK;
+ }
+
+ rv = mFilteredIter->PositionAt(content);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::eValid;
+
+ rv = CreateOffsetTable(&mOffsetTable, mFilteredIter, &mIteratorStatus,
+ mExtent, nullptr);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Result of SetSelectionInternal() in the |if| block or NS_OK.
+ return rv;
+ }
+
+ // If we get here, we have an uncollapsed selection!
+ // Look backwards through each range in the selection till you
+ // find the first text node. If you find one, find the
+ // beginning of its text block, and make it the current
+ // block.
+
+ int32_t rangeCount = static_cast<int32_t>(selection->RangeCount());
+ NS_ASSERTION(rangeCount > 0, "Unexpected range count!");
+
+ if (rangeCount <= 0) {
+ return NS_OK;
+ }
+
+ // XXX: We may need to add some code here to make sure
+ // the ranges are sorted in document appearance order!
+
+ for (int32_t i = rangeCount - 1; i >= 0; i--) {
+ // Get the i'th range from the selection.
+
+ range = selection->GetRangeAt(i);
+
+ if (!range) {
+ return NS_OK; // XXX Really?
+ }
+
+ // Create an iterator for the range.
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ nsresult rv =
+ CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ filteredIter->Last();
+
+ // Now walk through the range till we find a text node.
+
+ for (; !filteredIter->IsDone(); filteredIter->Prev()) {
+ if (filteredIter->GetCurrentNode()->NodeType() == nsINode::TEXT_NODE) {
+ // We found a text node, so position the document's
+ // iterator at the beginning of the block, then get
+ // the selection in terms of the string offset.
+
+ rv = mFilteredIter->PositionAt(filteredIter->GetCurrentNode());
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::eValid;
+
+ rv = CreateOffsetTable(&mOffsetTable, mFilteredIter, &mIteratorStatus,
+ mExtent, nullptr);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+
+ return rv;
+ }
+ }
+ }
+
+ // If we get here, we didn't find any text node in the selection!
+ // Create a range that extends from the end of the selection,
+ // to the end of the document, then iterate forwards through
+ // it till you find a text node!
+
+ range = selection->GetRangeAt(rangeCount - 1);
+
+ if (!range) {
+ return NS_ERROR_FAILURE;
+ }
+
+ parent = range->GetEndContainer();
+ if (!parent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ range = CreateDocumentContentRootToNodeOffsetRange(parent, range->EndOffset(),
+ false);
+
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (range->Collapsed()) {
+ // If we get here, the range is collapsed because there is nothing after
+ // the current selection! Just return NS_OK;
+ return NS_OK;
+ }
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ nsresult rv =
+ CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ filteredIter->First();
+
+ for (; !filteredIter->IsDone(); filteredIter->Next()) {
+ if (filteredIter->GetCurrentNode()->NodeType() == nsINode::TEXT_NODE) {
+ // We found a text node! Adjust the document's iterator to point
+ // to the beginning of its text block, then get the current selection.
+ rv = mFilteredIter->PositionAt(filteredIter->GetCurrentNode());
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = FirstTextNodeInCurrentBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::eValid;
+
+ rv = CreateOffsetTable(&mOffsetTable, mFilteredIter, &mIteratorStatus,
+ mExtent, nullptr);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = GetSelection(aSelStatus, aSelOffset, aSelLength);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return NS_OK;
+ }
+ }
+
+ // If we get here, we didn't find any block before or inside
+ // the selection! Just return OK.
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::PrevBlock() {
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ if (mIteratorStatus == IteratorStatus::eDone) {
+ return NS_OK;
+ }
+
+ switch (mIteratorStatus) {
+ case IteratorStatus::eValid:
+ case IteratorStatus::eNext: {
+ nsresult rv = FirstTextNodeInPrevBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return rv;
+ }
+
+ if (mFilteredIter->IsDone()) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return NS_OK;
+ }
+
+ mIteratorStatus = IteratorStatus::eValid;
+ break;
+ }
+ case IteratorStatus::ePrev:
+
+ // The iterator already points to the previous
+ // block, so don't do anything.
+
+ mIteratorStatus = IteratorStatus::eValid;
+ break;
+
+ default:
+
+ mIteratorStatus = IteratorStatus::eDone;
+ break;
+ }
+
+ // Keep track of prev and next blocks, just in case
+ // the text service blows away the current block.
+ nsresult rv = NS_OK;
+ if (mIteratorStatus == IteratorStatus::eValid) {
+ GetFirstTextNodeInPrevBlock(getter_AddRefs(mPrevTextBlock));
+ rv = GetFirstTextNodeInNextBlock(getter_AddRefs(mNextTextBlock));
+ } else {
+ // We must be done!
+ mPrevTextBlock = nullptr;
+ mNextTextBlock = nullptr;
+ }
+
+ // XXX The result of GetFirstTextNodeInNextBlock() or NS_OK.
+ return rv;
+}
+
+nsresult TextServicesDocument::NextBlock() {
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ if (mIteratorStatus == IteratorStatus::eDone) {
+ return NS_OK;
+ }
+
+ switch (mIteratorStatus) {
+ case IteratorStatus::eValid: {
+ // Advance the iterator to the next text block.
+
+ nsresult rv = FirstTextNodeInNextBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return rv;
+ }
+
+ if (mFilteredIter->IsDone()) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return NS_OK;
+ }
+
+ mIteratorStatus = IteratorStatus::eValid;
+ break;
+ }
+ case IteratorStatus::eNext:
+
+ // The iterator already points to the next block,
+ // so don't do anything to it!
+
+ mIteratorStatus = IteratorStatus::eValid;
+ break;
+
+ case IteratorStatus::ePrev:
+
+ // If the iterator is pointing to the previous block,
+ // we know that there is no next text block! Just
+ // fall through to the default case!
+
+ default:
+
+ mIteratorStatus = IteratorStatus::eDone;
+ break;
+ }
+
+ // Keep track of prev and next blocks, just in case
+ // the text service blows away the current block.
+ nsresult rv = NS_OK;
+ if (mIteratorStatus == IteratorStatus::eValid) {
+ GetFirstTextNodeInPrevBlock(getter_AddRefs(mPrevTextBlock));
+ rv = GetFirstTextNodeInNextBlock(getter_AddRefs(mNextTextBlock));
+ } else {
+ // We must be done.
+ mPrevTextBlock = nullptr;
+ mNextTextBlock = nullptr;
+ }
+
+ // The result of GetFirstTextNodeInNextBlock() or NS_OK.
+ return rv;
+}
+
+nsresult TextServicesDocument::IsDone(bool* aIsDone) {
+ NS_ENSURE_TRUE(aIsDone, NS_ERROR_NULL_POINTER);
+
+ *aIsDone = false;
+
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ *aIsDone = mIteratorStatus == IteratorStatus::eDone;
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::SetSelection(int32_t aOffset, int32_t aLength) {
+ NS_ENSURE_TRUE(mSelCon && aOffset >= 0 && aLength >= 0, NS_ERROR_FAILURE);
+
+ nsresult rv = SetSelectionInternal(aOffset, aLength, true);
+
+ //**** KDEBUG ****
+ // printf("\n * Sel: (%2d, %4d) (%2d, %4d)\n", mSelStartIndex,
+ // mSelStartOffset, mSelEndIndex, mSelEndOffset);
+ //**** KDEBUG ****
+
+ return rv;
+}
+
+nsresult TextServicesDocument::ScrollSelectionIntoView() {
+ NS_ENSURE_TRUE(mSelCon, NS_ERROR_FAILURE);
+
+ // After ScrollSelectionIntoView(), the pending notifications might be flushed
+ // and PresShell/PresContext/Frames may be dead. See bug 418470.
+ nsresult rv = mSelCon->ScrollSelectionIntoView(
+ nsISelectionController::SELECTION_NORMAL,
+ nsISelectionController::SELECTION_FOCUS_REGION,
+ nsISelectionController::SCROLL_SYNCHRONOUS);
+
+ return rv;
+}
+
+nsresult TextServicesDocument::DeleteSelection() {
+ if (NS_WARN_IF(!mTextEditor) || NS_WARN_IF(!SelectionIsValid())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (SelectionIsCollapsed()) {
+ return NS_OK;
+ }
+
+ // If we have an mExtent, save off its current set of
+ // end points so we can compare them against mExtent's
+ // set after the deletion of the content.
+
+ nsCOMPtr<nsINode> origStartNode, origEndNode;
+ int32_t origStartOffset = 0, origEndOffset = 0;
+
+ if (mExtent) {
+ nsresult rv = GetRangeEndPoints(
+ mExtent, getter_AddRefs(origStartNode), &origStartOffset,
+ getter_AddRefs(origEndNode), &origEndOffset);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ int32_t selLength;
+ OffsetEntry *entry, *newEntry;
+
+ for (int32_t i = mSelStartIndex; i <= mSelEndIndex; i++) {
+ entry = mOffsetTable[i];
+
+ if (i == mSelStartIndex) {
+ // Calculate the length of the selection. Note that the
+ // selection length can be zero if the start of the selection
+ // is at the very end of a text node entry.
+
+ if (entry->mIsInsertedText) {
+ // Inserted text offset entries have no width when
+ // talking in terms of string offsets! If the beginning
+ // of the selection is in an inserted text offset entry,
+ // the caret is always at the end of the entry!
+
+ selLength = 0;
+ } else {
+ selLength = entry->mLength - (mSelStartOffset - entry->mStrOffset);
+ }
+
+ if (selLength > 0 && mSelStartOffset > entry->mStrOffset) {
+ // Selection doesn't start at the beginning of the
+ // text node entry. We need to split this entry into
+ // two pieces, the piece before the selection, and
+ // the piece inside the selection.
+
+ nsresult rv = SplitOffsetEntry(i, selLength);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Adjust selection indexes to account for new entry:
+
+ ++mSelStartIndex;
+ ++mSelEndIndex;
+ ++i;
+
+ entry = mOffsetTable[i];
+ }
+
+ if (selLength > 0 && mSelStartIndex < mSelEndIndex) {
+ // The entire entry is contained in the selection. Mark the
+ // entry invalid.
+ entry->mIsValid = false;
+ }
+ }
+
+ if (i == mSelEndIndex) {
+ if (entry->mIsInsertedText) {
+ // Inserted text offset entries have no width when
+ // talking in terms of string offsets! If the end
+ // of the selection is in an inserted text offset entry,
+ // the selection includes the entire entry!
+
+ entry->mIsValid = false;
+ } else {
+ // Calculate the length of the selection. Note that the
+ // selection length can be zero if the end of the selection
+ // is at the very beginning of a text node entry.
+
+ selLength = mSelEndOffset - entry->mStrOffset;
+
+ if (selLength > 0 &&
+ mSelEndOffset < entry->mStrOffset + entry->mLength) {
+ // mStrOffset is guaranteed to be inside the selection, even
+ // when mSelStartIndex == mSelEndIndex.
+
+ nsresult rv = SplitOffsetEntry(i, entry->mLength - selLength);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Update the entry fields:
+
+ newEntry = mOffsetTable[i + 1];
+ newEntry->mNodeOffset = entry->mNodeOffset;
+ }
+
+ if (selLength > 0 &&
+ mSelEndOffset == entry->mStrOffset + entry->mLength) {
+ // The entire entry is contained in the selection. Mark the
+ // entry invalid.
+ entry->mIsValid = false;
+ }
+ }
+ }
+
+ if (i != mSelStartIndex && i != mSelEndIndex) {
+ // The entire entry is contained in the selection. Mark the
+ // entry invalid.
+ entry->mIsValid = false;
+ }
+ }
+
+ // Make sure mFilteredIter always points to something valid!
+
+ AdjustContentIterator();
+
+ // Now delete the actual content!
+ RefPtr<TextEditor> textEditor = mTextEditor;
+ nsresult rv = textEditor->DeleteSelectionAsAction(nsIEditor::ePrevious,
+ nsIEditor::eStrip);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Now that we've actually deleted the selected content,
+ // check to see if our mExtent has changed, if so, then
+ // we have to create a new content iterator!
+
+ if (origStartNode && origEndNode) {
+ nsCOMPtr<nsINode> curStartNode, curEndNode;
+ int32_t curStartOffset = 0, curEndOffset = 0;
+
+ rv = GetRangeEndPoints(mExtent, getter_AddRefs(curStartNode),
+ &curStartOffset, getter_AddRefs(curEndNode),
+ &curEndOffset);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (origStartNode != curStartNode || origEndNode != curEndNode) {
+ // The range has changed, so we need to create a new content
+ // iterator based on the new range.
+
+ nsCOMPtr<nsIContent> curContent;
+
+ if (mIteratorStatus != IteratorStatus::eDone) {
+ // The old iterator is still pointing to something valid,
+ // so get its current node so we can restore it after we
+ // create the new iterator!
+
+ curContent = mFilteredIter->GetCurrentNode()
+ ? mFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+ }
+
+ // Create the new iterator.
+
+ rv =
+ CreateFilteredContentIterator(mExtent, getter_AddRefs(mFilteredIter));
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Now make the new iterator point to the content node
+ // the old one was pointing at.
+
+ if (curContent) {
+ rv = mFilteredIter->PositionAt(curContent);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ } else {
+ mIteratorStatus = IteratorStatus::eValid;
+ }
+ }
+ }
+ }
+
+ entry = 0;
+
+ // Move the caret to the end of the first valid entry.
+ // Start with mSelStartIndex since it may still be valid.
+
+ for (int32_t i = mSelStartIndex; !entry && i >= 0; i--) {
+ entry = mOffsetTable[i];
+
+ if (!entry->mIsValid) {
+ entry = 0;
+ } else {
+ mSelStartIndex = mSelEndIndex = i;
+ mSelStartOffset = mSelEndOffset = entry->mStrOffset + entry->mLength;
+ }
+ }
+
+ // If we still don't have a valid entry, move the caret
+ // to the next valid entry after the selection:
+
+ for (int32_t i = mSelEndIndex;
+ !entry && i < static_cast<int32_t>(mOffsetTable.Length()); i++) {
+ entry = mOffsetTable[i];
+
+ if (!entry->mIsValid) {
+ entry = 0;
+ } else {
+ mSelStartIndex = mSelEndIndex = i;
+ mSelStartOffset = mSelEndOffset = entry->mStrOffset;
+ }
+ }
+
+ if (entry) {
+ SetSelection(mSelStartOffset, 0);
+ } else {
+ // Uuughh we have no valid offset entry to place our
+ // caret ... just mark the selection invalid.
+ mSelStartIndex = mSelEndIndex = -1;
+ mSelStartOffset = mSelEndOffset = -1;
+ }
+
+ // Now remove any invalid entries from the offset table.
+
+ rv = RemoveInvalidOffsetEntries();
+
+ //**** KDEBUG ****
+ // printf("\n---- After Delete\n");
+ // printf("Sel: (%2d, %4d) (%2d, %4d)\n", mSelStartIndex,
+ // mSelStartOffset, mSelEndIndex, mSelEndOffset);
+ // PrintOffsetTable();
+ //**** KDEBUG ****
+
+ return rv;
+}
+
+nsresult TextServicesDocument::InsertText(const nsAString& aText) {
+ if (NS_WARN_IF(!mTextEditor) || NS_WARN_IF(!SelectionIsValid())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If the selection is not collapsed, we need to save
+ // off the selection offsets so we can restore the
+ // selection and delete the selected content after we've
+ // inserted the new text. This is necessary to try and
+ // retain as much of the original style of the content
+ // being deleted.
+
+ bool collapsedSelection = SelectionIsCollapsed();
+ int32_t savedSelOffset = mSelStartOffset;
+ int32_t savedSelLength = mSelEndOffset - mSelStartOffset;
+
+ if (!collapsedSelection) {
+ // Collapse to the start of the current selection
+ // for the insert!
+
+ nsresult rv = SetSelection(mSelStartOffset, 0);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // AutoTransactionBatchExternal grabs mTextEditor, so, we don't need to grab
+ // the instance with local variable here.
+ RefPtr<TextEditor> textEditor = mTextEditor;
+ AutoTransactionBatchExternal treatAsOneTransaction(*textEditor);
+
+ nsresult rv = textEditor->InsertTextAsAction(aText);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("InsertTextAsAction() failed");
+ return rv;
+ }
+
+ int32_t strLength = aText.Length();
+
+ OffsetEntry* itEntry;
+ OffsetEntry* entry = mOffsetTable[mSelStartIndex];
+ void* node = entry->mNode;
+
+ NS_ASSERTION((entry->mIsValid), "Invalid insertion point!");
+
+ if (entry->mStrOffset == mSelStartOffset) {
+ if (entry->mIsInsertedText) {
+ // If the caret is in an inserted text offset entry,
+ // we simply insert the text at the end of the entry.
+ entry->mLength += strLength;
+ } else {
+ // Insert an inserted text offset entry before the current
+ // entry!
+ itEntry = new OffsetEntry(entry->mNode, entry->mStrOffset, strLength);
+ itEntry->mIsInsertedText = true;
+ itEntry->mNodeOffset = entry->mNodeOffset;
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mOffsetTable.InsertElementAt(mSelStartIndex, itEntry);
+ }
+ } else if (entry->mStrOffset + entry->mLength == mSelStartOffset) {
+ // We are inserting text at the end of the current offset entry.
+ // Look at the next valid entry in the table. If it's an inserted
+ // text entry, add to its length and adjust its node offset. If
+ // it isn't, add a new inserted text entry.
+
+ // XXX Rename this!
+ uint32_t i = mSelStartIndex + 1;
+ itEntry = 0;
+
+ if (mOffsetTable.Length() > i) {
+ itEntry = mOffsetTable[i];
+ if (!itEntry) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Check if the entry is a match. If it isn't, set
+ // iEntry to zero.
+ if (!itEntry->mIsInsertedText || itEntry->mStrOffset != mSelStartOffset) {
+ itEntry = 0;
+ }
+ }
+
+ if (!itEntry) {
+ // We didn't find an inserted text offset entry, so
+ // create one.
+ itEntry = new OffsetEntry(entry->mNode, mSelStartOffset, 0);
+ itEntry->mNodeOffset = entry->mNodeOffset + entry->mLength;
+ itEntry->mIsInsertedText = true;
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mOffsetTable.InsertElementAt(i, itEntry);
+ }
+
+ // We have a valid inserted text offset entry. Update its
+ // length, adjust the selection indexes, and make sure the
+ // caret is properly placed!
+
+ itEntry->mLength += strLength;
+
+ mSelStartIndex = mSelEndIndex = i;
+
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ if (NS_WARN_IF(!selection)) {
+ return rv;
+ }
+
+ RefPtr<nsINode> node = itEntry->mNode;
+ rv = selection->CollapseInLimiter(node,
+ itEntry->mNodeOffset + itEntry->mLength);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ } else if (entry->mStrOffset + entry->mLength > mSelStartOffset) {
+ // We are inserting text into the middle of the current offset entry.
+ // split the current entry into two parts, then insert an inserted text
+ // entry between them!
+
+ // XXX Rename this!
+ uint32_t i = entry->mLength - (mSelStartOffset - entry->mStrOffset);
+
+ rv = SplitOffsetEntry(mSelStartIndex, i);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ itEntry = new OffsetEntry(entry->mNode, mSelStartOffset, strLength);
+ itEntry->mIsInsertedText = true;
+ itEntry->mNodeOffset = entry->mNodeOffset + entry->mLength;
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mOffsetTable.InsertElementAt(mSelStartIndex + 1, itEntry);
+
+ mSelEndIndex = ++mSelStartIndex;
+ }
+
+ // We've just finished inserting an inserted text offset entry.
+ // update all entries with the same mNode pointer that follow
+ // it in the table!
+
+ for (size_t i = mSelStartIndex + 1; i < mOffsetTable.Length(); i++) {
+ entry = mOffsetTable[i];
+ if (entry->mNode != node) {
+ break;
+ }
+ if (entry->mIsValid) {
+ entry->mNodeOffset += strLength;
+ }
+ }
+
+ //**** KDEBUG ****
+ // printf("\n---- After Insert\n");
+ // printf("Sel: (%2d, %4d) (%2d, %4d)\n", mSelStartIndex,
+ // mSelStartOffset, mSelEndIndex, mSelEndOffset);
+ // PrintOffsetTable();
+ //**** KDEBUG ****
+
+ if (!collapsedSelection) {
+ rv = SetSelection(savedSelOffset, savedSelLength);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = DeleteSelection();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+void TextServicesDocument::DidDeleteNode(nsINode* aChild) {
+ if (NS_WARN_IF(!mFilteredIter)) {
+ return;
+ }
+
+ int32_t nodeIndex = 0;
+ bool hasEntry = false;
+ OffsetEntry* entry;
+
+ nsresult rv =
+ NodeHasOffsetEntry(&mOffsetTable, aChild, &hasEntry, &nodeIndex);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ if (!hasEntry) {
+ // It's okay if the node isn't in the offset table, the
+ // editor could be cleaning house.
+ return;
+ }
+
+ nsINode* node = mFilteredIter->GetCurrentNode();
+ if (node && node == aChild && mIteratorStatus != IteratorStatus::eDone) {
+ // XXX: This should never really happen because
+ // AdjustContentIterator() should have been called prior
+ // to the delete to try and position the iterator on the
+ // next valid text node in the offset table, and if there
+ // wasn't a next, it would've set mIteratorStatus to eIsDone.
+
+ NS_ERROR("DeleteNode called for current iterator node.");
+ }
+
+ int32_t tcount = mOffsetTable.Length();
+ while (nodeIndex < tcount) {
+ entry = mOffsetTable[nodeIndex];
+ if (!entry) {
+ return;
+ }
+
+ if (entry->mNode == aChild) {
+ entry->mIsValid = false;
+ }
+
+ nodeIndex++;
+ }
+}
+
+void TextServicesDocument::DidJoinNodes(nsINode& aLeftNode,
+ nsINode& aRightNode) {
+ // Make sure that both nodes are text nodes -- otherwise we don't care.
+ if (!aLeftNode.IsText() || !aRightNode.IsText()) {
+ return;
+ }
+
+ // Note: The editor merges the contents of the left node into the
+ // contents of the right.
+
+ int32_t leftIndex = 0;
+ int32_t rightIndex = 0;
+ bool leftHasEntry = false;
+ bool rightHasEntry = false;
+
+ nsresult rv =
+ NodeHasOffsetEntry(&mOffsetTable, &aLeftNode, &leftHasEntry, &leftIndex);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ if (!leftHasEntry) {
+ // It's okay if the node isn't in the offset table, the
+ // editor could be cleaning house.
+ return;
+ }
+
+ rv = NodeHasOffsetEntry(&mOffsetTable, &aRightNode, &rightHasEntry,
+ &rightIndex);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ if (!rightHasEntry) {
+ // It's okay if the node isn't in the offset table, the
+ // editor could be cleaning house.
+ return;
+ }
+
+ NS_ASSERTION(leftIndex < rightIndex, "Indexes out of order.");
+
+ if (leftIndex > rightIndex) {
+ // Don't know how to handle this situation.
+ return;
+ }
+
+ OffsetEntry* entry = mOffsetTable[rightIndex];
+ NS_ASSERTION(entry->mNodeOffset == 0,
+ "Unexpected offset value for rightIndex.");
+
+ // Run through the table and change all entries referring to
+ // the left node so that they now refer to the right node:
+ uint32_t nodeLength = aLeftNode.Length();
+ for (int32_t i = leftIndex; i < rightIndex; i++) {
+ entry = mOffsetTable[i];
+ if (entry->mNode != &aLeftNode) {
+ break;
+ }
+ if (entry->mIsValid) {
+ entry->mNode = &aRightNode;
+ }
+ }
+
+ // Run through the table and adjust the node offsets
+ // for all entries referring to the right node.
+ for (int32_t i = rightIndex; i < static_cast<int32_t>(mOffsetTable.Length());
+ i++) {
+ entry = mOffsetTable[i];
+ if (entry->mNode != &aRightNode) {
+ break;
+ }
+ if (entry->mIsValid) {
+ entry->mNodeOffset += nodeLength;
+ }
+ }
+
+ // Now check to see if the iterator is pointing to the
+ // left node. If it is, make it point to the right node!
+
+ if (mFilteredIter->GetCurrentNode() == &aLeftNode) {
+ mFilteredIter->PositionAt(&aRightNode);
+ }
+}
+
+nsresult TextServicesDocument::CreateFilteredContentIterator(
+ const AbstractRange* aAbstractRange,
+ FilteredContentIterator** aFilteredIter) {
+ if (NS_WARN_IF(!aAbstractRange) || NS_WARN_IF(!aFilteredIter)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aFilteredIter = nullptr;
+
+ UniquePtr<nsComposeTxtSrvFilter> composeFilter;
+ switch (mTxtSvcFilterType) {
+ case nsIEditorSpellCheck::FILTERTYPE_NORMAL:
+ composeFilter = nsComposeTxtSrvFilter::CreateNormalFilter();
+ break;
+ case nsIEditorSpellCheck::FILTERTYPE_MAIL:
+ composeFilter = nsComposeTxtSrvFilter::CreateMailFilter();
+ break;
+ }
+
+ // Create a FilteredContentIterator
+ // This class wraps the ContentIterator in order to give itself a chance
+ // to filter out certain content nodes
+ RefPtr<FilteredContentIterator> filter =
+ new FilteredContentIterator(std::move(composeFilter));
+
+ nsresult rv = filter->Init(aAbstractRange);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ filter.forget(aFilteredIter);
+ return NS_OK;
+}
+
+Element* TextServicesDocument::GetDocumentContentRootNode() const {
+ if (NS_WARN_IF(!mDocument)) {
+ return nullptr;
+ }
+
+ if (mDocument->IsHTMLOrXHTML()) {
+ Element* rootElement = mDocument->GetRootElement();
+ if (rootElement && rootElement->IsXULElement()) {
+ // HTML documents with root XUL elements should eventually be transitioned
+ // to a regular document structure, but for now the content root node will
+ // be the document element.
+ return mDocument->GetDocumentElement();
+ }
+ // For HTML documents, the content root node is the body.
+ return mDocument->GetBody();
+ }
+
+ // For non-HTML documents, the content root node will be the document element.
+ return mDocument->GetDocumentElement();
+}
+
+already_AddRefed<nsRange> TextServicesDocument::CreateDocumentContentRange() {
+ nsCOMPtr<nsINode> node = GetDocumentContentRootNode();
+ if (NS_WARN_IF(!node)) {
+ return nullptr;
+ }
+
+ RefPtr<nsRange> range = nsRange::Create(node);
+ IgnoredErrorResult ignoredError;
+ range->SelectNodeContents(*node, ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(), "SelectNodeContents() failed");
+ return range.forget();
+}
+
+already_AddRefed<nsRange>
+TextServicesDocument::CreateDocumentContentRootToNodeOffsetRange(
+ nsINode* aParent, uint32_t aOffset, bool aToStart) {
+ if (NS_WARN_IF(!aParent)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsINode> bodyNode = GetDocumentContentRootNode();
+ if (NS_WARN_IF(!bodyNode)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsINode> startNode;
+ nsCOMPtr<nsINode> endNode;
+ uint32_t startOffset, endOffset;
+
+ if (aToStart) {
+ // The range should begin at the start of the document
+ // and extend up until (aParent, aOffset).
+
+ startNode = bodyNode;
+ startOffset = 0;
+ endNode = aParent;
+ endOffset = aOffset;
+ } else {
+ // The range should begin at (aParent, aOffset) and
+ // extend to the end of the document.
+
+ startNode = aParent;
+ startOffset = aOffset;
+ endNode = bodyNode;
+ endOffset = endNode ? endNode->GetChildCount() : 0;
+ }
+
+ RefPtr<nsRange> range = nsRange::Create(startNode, startOffset, endNode,
+ endOffset, IgnoreErrors());
+ NS_WARNING_ASSERTION(range,
+ "nsRange::Create() failed to create new valid range");
+ return range.forget();
+}
+
+nsresult TextServicesDocument::CreateDocumentContentIterator(
+ FilteredContentIterator** aFilteredIter) {
+ NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER);
+
+ RefPtr<nsRange> range = CreateDocumentContentRange();
+ if (NS_WARN_IF(!range)) {
+ *aFilteredIter = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+
+ return CreateFilteredContentIterator(range, aFilteredIter);
+}
+
+nsresult TextServicesDocument::AdjustContentIterator() {
+ NS_ENSURE_TRUE(mFilteredIter, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsINode> node = mFilteredIter->GetCurrentNode();
+ NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
+
+ size_t tcount = mOffsetTable.Length();
+
+ nsINode* prevValidNode = nullptr;
+ nsINode* nextValidNode = nullptr;
+ bool foundEntry = false;
+ OffsetEntry* entry;
+
+ for (size_t i = 0; i < tcount && !nextValidNode; i++) {
+ entry = mOffsetTable[i];
+
+ NS_ENSURE_TRUE(entry, NS_ERROR_FAILURE);
+
+ if (entry->mNode == node) {
+ if (entry->mIsValid) {
+ // The iterator is still pointing to something valid!
+ // Do nothing!
+ return NS_OK;
+ }
+ // We found an invalid entry that points to
+ // the current iterator node. Stop looking for
+ // a previous valid node!
+ foundEntry = true;
+ }
+
+ if (entry->mIsValid) {
+ if (!foundEntry) {
+ prevValidNode = entry->mNode;
+ } else {
+ nextValidNode = entry->mNode;
+ }
+ }
+ }
+
+ nsCOMPtr<nsIContent> content;
+
+ if (prevValidNode) {
+ if (prevValidNode->IsContent()) {
+ content = prevValidNode->AsContent();
+ }
+ } else if (nextValidNode) {
+ if (nextValidNode->IsContent()) {
+ content = nextValidNode->AsContent();
+ }
+ }
+
+ if (content) {
+ nsresult rv = mFilteredIter->PositionAt(content);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ } else {
+ mIteratorStatus = IteratorStatus::eValid;
+ }
+ return rv;
+ }
+
+ // If we get here, there aren't any valid entries
+ // in the offset table! Try to position the iterator
+ // on the next text block first, then previous if
+ // one doesn't exist!
+
+ if (mNextTextBlock) {
+ nsresult rv = mFilteredIter->PositionAt(mNextTextBlock);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::eNext;
+ } else if (mPrevTextBlock) {
+ nsresult rv = mFilteredIter->PositionAt(mPrevTextBlock);
+
+ if (NS_FAILED(rv)) {
+ mIteratorStatus = IteratorStatus::eDone;
+ return rv;
+ }
+
+ mIteratorStatus = IteratorStatus::ePrev;
+ } else {
+ mIteratorStatus = IteratorStatus::eDone;
+ }
+ return NS_OK;
+}
+
+// static
+bool TextServicesDocument::DidSkip(FilteredContentIterator* aFilteredIter) {
+ return aFilteredIter && aFilteredIter->DidSkip();
+}
+
+// static
+void TextServicesDocument::ClearDidSkip(
+ FilteredContentIterator* aFilteredIter) {
+ // Clear filter's skip flag
+ if (aFilteredIter) {
+ aFilteredIter->ClearDidSkip();
+ }
+}
+
+// static
+bool TextServicesDocument::IsBlockNode(nsIContent* aContent) {
+ if (!aContent) {
+ NS_ERROR("How did a null pointer get passed to IsBlockNode?");
+ return false;
+ }
+
+ nsAtom* atom = aContent->NodeInfo()->NameAtom();
+
+ // clang-format off
+ return (nsGkAtoms::a != atom &&
+ nsGkAtoms::address != atom &&
+ nsGkAtoms::big != atom &&
+ nsGkAtoms::b != atom &&
+ nsGkAtoms::cite != atom &&
+ nsGkAtoms::code != atom &&
+ nsGkAtoms::dfn != atom &&
+ nsGkAtoms::em != atom &&
+ nsGkAtoms::font != atom &&
+ nsGkAtoms::i != atom &&
+ nsGkAtoms::kbd != atom &&
+ nsGkAtoms::nobr != atom &&
+ nsGkAtoms::s != atom &&
+ nsGkAtoms::samp != atom &&
+ nsGkAtoms::small != atom &&
+ nsGkAtoms::spacer != atom &&
+ nsGkAtoms::span != atom &&
+ nsGkAtoms::strike != atom &&
+ nsGkAtoms::strong != atom &&
+ nsGkAtoms::sub != atom &&
+ nsGkAtoms::sup != atom &&
+ nsGkAtoms::tt != atom &&
+ nsGkAtoms::u != atom &&
+ nsGkAtoms::var != atom &&
+ nsGkAtoms::wbr != atom);
+ // clang-format on
+}
+
+// static
+bool TextServicesDocument::HasSameBlockNodeParent(nsIContent* aContent1,
+ nsIContent* aContent2) {
+ nsIContent* p1 = aContent1->GetParent();
+ nsIContent* p2 = aContent2->GetParent();
+
+ // Quick test:
+
+ if (p1 == p2) {
+ return true;
+ }
+
+ // Walk up the parent hierarchy looking for closest block boundary node:
+
+ while (p1 && !IsBlockNode(p1)) {
+ p1 = p1->GetParent();
+ }
+
+ while (p2 && !IsBlockNode(p2)) {
+ p2 = p2->GetParent();
+ }
+
+ return p1 == p2;
+}
+
+// static
+bool TextServicesDocument::IsTextNode(nsIContent* aContent) {
+ NS_ENSURE_TRUE(aContent, false);
+ return nsINode::TEXT_NODE == aContent->NodeType();
+}
+
+nsresult TextServicesDocument::SetSelectionInternal(int32_t aOffset,
+ int32_t aLength,
+ bool aDoUpdate) {
+ if (NS_WARN_IF(!mSelCon) || NS_WARN_IF(aOffset < 0) ||
+ NS_WARN_IF(aLength < 0)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<nsINode> startNode;
+ int32_t startNodeOffset = 0;
+ OffsetEntry* entry;
+
+ // Find start of selection in node offset terms:
+
+ for (size_t i = 0; !startNode && i < mOffsetTable.Length(); i++) {
+ entry = mOffsetTable[i];
+ if (entry->mIsValid) {
+ if (entry->mIsInsertedText) {
+ // Caret can only be placed at the end of an
+ // inserted text offset entry, if the offsets
+ // match exactly!
+
+ if (entry->mStrOffset == aOffset) {
+ startNode = entry->mNode;
+ startNodeOffset = entry->mNodeOffset + entry->mLength;
+ }
+ } else if (aOffset >= entry->mStrOffset) {
+ bool foundEntry = false;
+ int32_t strEndOffset = entry->mStrOffset + entry->mLength;
+
+ if (aOffset < strEndOffset) {
+ foundEntry = true;
+ } else if (aOffset == strEndOffset) {
+ // Peek after this entry to see if we have any
+ // inserted text entries belonging to the same
+ // entry->mNode. If so, we have to place the selection
+ // after it!
+
+ if (i + 1 < mOffsetTable.Length()) {
+ OffsetEntry* nextEntry = mOffsetTable[i + 1];
+
+ if (!nextEntry->mIsValid || nextEntry->mStrOffset != aOffset) {
+ // Next offset entry isn't an exact match, so we'll
+ // just use the current entry.
+ foundEntry = true;
+ }
+ }
+ }
+
+ if (foundEntry) {
+ startNode = entry->mNode;
+ startNodeOffset = entry->mNodeOffset + aOffset - entry->mStrOffset;
+ }
+ }
+
+ if (startNode) {
+ mSelStartIndex = static_cast<int32_t>(i);
+ mSelStartOffset = aOffset;
+ }
+ }
+ }
+
+ NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);
+
+ // XXX: If we ever get a SetSelection() method in nsIEditor, we should
+ // use it.
+
+ RefPtr<Selection> selection;
+ if (aDoUpdate) {
+ selection = mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ if (NS_WARN_IF(!selection)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (!aLength) {
+ if (aDoUpdate) {
+ nsresult rv = selection->CollapseInLimiter(startNode, startNodeOffset);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ mSelEndIndex = mSelStartIndex;
+ mSelEndOffset = mSelStartOffset;
+ return NS_OK;
+ }
+
+ // Find the end of the selection in node offset terms:
+ nsCOMPtr<nsINode> endNode;
+ int32_t endNodeOffset = 0;
+ int32_t endOffset = aOffset + aLength;
+ for (int32_t i = mOffsetTable.Length() - 1; !endNode && i >= 0; i--) {
+ entry = mOffsetTable[i];
+
+ if (entry->mIsValid) {
+ if (entry->mIsInsertedText) {
+ if (entry->mStrOffset == endNodeOffset) {
+ // If the selection ends on an inserted text offset entry,
+ // the selection includes the entire entry!
+
+ endNode = entry->mNode;
+ endNodeOffset = entry->mNodeOffset + entry->mLength;
+ }
+ } else if (endOffset >= entry->mStrOffset &&
+ endOffset <= entry->mStrOffset + entry->mLength) {
+ endNode = entry->mNode;
+ endNodeOffset = entry->mNodeOffset + endOffset - entry->mStrOffset;
+ }
+
+ if (endNode) {
+ mSelEndIndex = i;
+ mSelEndOffset = endOffset;
+ }
+ }
+ }
+
+ if (!aDoUpdate) {
+ return NS_OK;
+ }
+
+ if (!endNode) {
+ nsresult rv = selection->CollapseInLimiter(startNode, startNodeOffset);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to collapse selection");
+ return rv;
+ }
+
+ ErrorResult error;
+ selection->SetStartAndEndInLimiter(
+ RawRangeBoundary(startNode, startNodeOffset),
+ RawRangeBoundary(endNode, endNodeOffset), error);
+ NS_WARNING_ASSERTION(!error.Failed(), "Failed to set selection");
+ return error.StealNSResult();
+}
+
+nsresult TextServicesDocument::GetSelection(BlockSelectionStatus* aSelStatus,
+ int32_t* aSelOffset,
+ int32_t* aSelLength) {
+ NS_ENSURE_TRUE(aSelStatus && aSelOffset && aSelLength, NS_ERROR_NULL_POINTER);
+
+ *aSelStatus = BlockSelectionStatus::eBlockNotFound;
+ *aSelOffset = -1;
+ *aSelLength = -1;
+
+ NS_ENSURE_TRUE(mDocument && mSelCon, NS_ERROR_FAILURE);
+
+ if (mIteratorStatus == IteratorStatus::eDone) {
+ return NS_OK;
+ }
+
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ nsresult rv;
+ if (selection->IsCollapsed()) {
+ rv = GetCollapsedSelection(aSelStatus, aSelOffset, aSelLength);
+ } else {
+ rv = GetUncollapsedSelection(aSelStatus, aSelOffset, aSelLength);
+ }
+
+ // XXX The result of GetCollapsedSelection() or GetUncollapsedSelection().
+ return rv;
+}
+
+nsresult TextServicesDocument::GetCollapsedSelection(
+ BlockSelectionStatus* aSelStatus, int32_t* aSelOffset,
+ int32_t* aSelLength) {
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ // The calling function should have done the GetIsCollapsed()
+ // check already. Just assume it's collapsed!
+ *aSelStatus = BlockSelectionStatus::eBlockOutside;
+ *aSelOffset = *aSelLength = -1;
+
+ int32_t tableCount = mOffsetTable.Length();
+
+ if (!tableCount) {
+ return NS_OK;
+ }
+
+ // Get pointers to the first and last offset entries
+ // in the table.
+
+ OffsetEntry* eStart = mOffsetTable[0];
+ OffsetEntry* eEnd;
+ if (tableCount > 1) {
+ eEnd = mOffsetTable[tableCount - 1];
+ } else {
+ eEnd = eStart;
+ }
+
+ int32_t eStartOffset = eStart->mNodeOffset;
+ int32_t eEndOffset = eEnd->mNodeOffset + eEnd->mLength;
+
+ RefPtr<const nsRange> range = selection->GetRangeAt(0);
+ NS_ENSURE_STATE(range);
+
+ nsCOMPtr<nsINode> parent = range->GetStartContainer();
+ MOZ_ASSERT(parent);
+
+ uint32_t offset = range->StartOffset();
+
+ const Maybe<int32_t> e1s1 = nsContentUtils::ComparePoints(
+ eStart->mNode, eStartOffset, parent, static_cast<int32_t>(offset));
+ const Maybe<int32_t> e2s1 = nsContentUtils::ComparePoints(
+ eEnd->mNode, eEndOffset, parent, static_cast<int32_t>(offset));
+
+ if (NS_WARN_IF(!e1s1) || NS_WARN_IF(!e2s1)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (*e1s1 > 0 || *e2s1 < 0) {
+ // We're done if the caret is outside the current text block.
+ return NS_OK;
+ }
+
+ if (parent->NodeType() == nsINode::TEXT_NODE) {
+ // Good news, the caret is in a text node. Look
+ // through the offset table for the entry that
+ // matches its parent and offset.
+
+ for (int32_t i = 0; i < tableCount; i++) {
+ OffsetEntry* entry = mOffsetTable[i];
+ NS_ENSURE_TRUE(entry, NS_ERROR_FAILURE);
+
+ if (entry->mNode == parent &&
+ entry->mNodeOffset <= static_cast<int32_t>(offset) &&
+ static_cast<int32_t>(offset) <= entry->mNodeOffset + entry->mLength) {
+ *aSelStatus = BlockSelectionStatus::eBlockContains;
+ *aSelOffset = entry->mStrOffset + (offset - entry->mNodeOffset);
+ *aSelLength = 0;
+
+ return NS_OK;
+ }
+ }
+
+ // If we get here, we didn't find a text node entry
+ // in our offset table that matched.
+
+ return NS_ERROR_FAILURE;
+ }
+
+ // The caret is in our text block, but it's positioned in some
+ // non-text node (ex. <b>). Create a range based on the start
+ // and end of the text block, then create an iterator based on
+ // this range, with its initial position set to the closest
+ // child of this non-text node. Then look for the closest text
+ // node.
+
+ range = nsRange::Create(eStart->mNode, eStartOffset, eEnd->mNode, eEndOffset,
+ IgnoreErrors());
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ nsresult rv =
+ CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIContent* saveNode;
+ if (parent->HasChildren()) {
+ // XXX: We need to make sure that all of parent's
+ // children are in the text block.
+
+ // If the parent has children, position the iterator
+ // on the child that is to the left of the offset.
+
+ nsIContent* content = range->GetChildAtStartOffset();
+ if (content && parent->GetFirstChild() != content) {
+ content = content->GetPreviousSibling();
+ }
+ NS_ENSURE_TRUE(content, NS_ERROR_FAILURE);
+
+ rv = filteredIter->PositionAt(content);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ saveNode = content;
+ } else {
+ // The parent has no children, so position the iterator
+ // on the parent.
+ NS_ENSURE_TRUE(parent->IsContent(), NS_ERROR_FAILURE);
+ nsCOMPtr<nsIContent> content = parent->AsContent();
+
+ rv = filteredIter->PositionAt(content);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ saveNode = content;
+ }
+
+ // Now iterate to the left, towards the beginning of
+ // the text block, to find the first text node you
+ // come across.
+
+ nsIContent* node = nullptr;
+ for (; !filteredIter->IsDone(); filteredIter->Prev()) {
+ nsINode* current = filteredIter->GetCurrentNode();
+ if (current->NodeType() == nsINode::TEXT_NODE) {
+ node = current->AsContent();
+ break;
+ }
+ }
+
+ if (node) {
+ // We found a node, now set the offset to the end
+ // of the text node.
+ offset = node->TextLength();
+ } else {
+ // We should never really get here, but I'm paranoid.
+
+ // We didn't find a text node above, so iterate to
+ // the right, towards the end of the text block, looking
+ // for a text node.
+
+ rv = filteredIter->PositionAt(saveNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ node = nullptr;
+ for (; !filteredIter->IsDone(); filteredIter->Next()) {
+ nsINode* current = filteredIter->GetCurrentNode();
+
+ if (current->NodeType() == nsINode::TEXT_NODE) {
+ node = current->AsContent();
+ break;
+ }
+ }
+
+ NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
+
+ // We found a text node, so set the offset to
+ // the beginning of the node.
+
+ offset = 0;
+ }
+
+ for (int32_t i = 0; i < tableCount; i++) {
+ OffsetEntry* entry = mOffsetTable[i];
+ NS_ENSURE_TRUE(entry, NS_ERROR_FAILURE);
+
+ if (entry->mNode == node &&
+ entry->mNodeOffset <= static_cast<int32_t>(offset) &&
+ static_cast<int32_t>(offset) <= entry->mNodeOffset + entry->mLength) {
+ *aSelStatus = BlockSelectionStatus::eBlockContains;
+ *aSelOffset = entry->mStrOffset + (offset - entry->mNodeOffset);
+ *aSelLength = 0;
+
+ // Now move the caret so that it is actually in the text node.
+ // We do this to keep things in sync.
+ //
+ // In most cases, the user shouldn't see any movement in the caret
+ // on screen.
+
+ return SetSelectionInternal(*aSelOffset, *aSelLength, true);
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+nsresult TextServicesDocument::GetUncollapsedSelection(
+ BlockSelectionStatus* aSelStatus, int32_t* aSelOffset,
+ int32_t* aSelLength) {
+ RefPtr<const nsRange> range;
+ OffsetEntry* entry;
+
+ RefPtr<Selection> selection =
+ mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
+ NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
+
+ // It is assumed that the calling function has made sure that the
+ // selection is not collapsed, and that the input params to this
+ // method are initialized to some defaults.
+
+ nsCOMPtr<nsINode> startContainer, endContainer;
+ int32_t startOffset, endOffset;
+ int32_t tableCount;
+
+ OffsetEntry *eStart, *eEnd;
+ int32_t eStartOffset, eEndOffset;
+
+ tableCount = mOffsetTable.Length();
+
+ // Get pointers to the first and last offset entries
+ // in the table.
+
+ eStart = mOffsetTable[0];
+
+ if (tableCount > 1) {
+ eEnd = mOffsetTable[tableCount - 1];
+ } else {
+ eEnd = eStart;
+ }
+
+ eStartOffset = eStart->mNodeOffset;
+ eEndOffset = eEnd->mNodeOffset + eEnd->mLength;
+
+ const uint32_t rangeCount = selection->RangeCount();
+
+ // Find the first range in the selection that intersects
+ // the current text block.
+ Maybe<int32_t> e1s2;
+ Maybe<int32_t> e2s1;
+ for (uint32_t i = 0; i < rangeCount; i++) {
+ range = selection->GetRangeAt(i);
+ NS_ENSURE_STATE(range);
+
+ nsresult rv =
+ GetRangeEndPoints(range, getter_AddRefs(startContainer), &startOffset,
+ getter_AddRefs(endContainer), &endOffset);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ e1s2 = nsContentUtils::ComparePoints(eStart->mNode, eStartOffset,
+ endContainer, endOffset);
+ if (NS_WARN_IF(!e1s2)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ e2s1 = nsContentUtils::ComparePoints(eEnd->mNode, eEndOffset,
+ startContainer, startOffset);
+ if (NS_WARN_IF(!e2s1)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Break out of the loop if the text block intersects the current range.
+
+ if (*e1s2 <= 0 && *e2s1 >= 0) {
+ break;
+ }
+ }
+
+ // We're done if we didn't find an intersecting range.
+
+ if (rangeCount < 1 || *e1s2 > 0 || *e2s1 < 0) {
+ *aSelStatus = BlockSelectionStatus::eBlockOutside;
+ *aSelOffset = *aSelLength = -1;
+ return NS_OK;
+ }
+
+ // Now that we have an intersecting range, find out more info:
+ const Maybe<int32_t> e1s1 = nsContentUtils::ComparePoints(
+ eStart->mNode, eStartOffset, startContainer, startOffset);
+ if (NS_WARN_IF(!e1s1)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const Maybe<int32_t> e2s2 = nsContentUtils::ComparePoints(
+ eEnd->mNode, eEndOffset, endContainer, endOffset);
+ if (NS_WARN_IF(!e2s2)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (rangeCount > 1) {
+ // There are multiple selection ranges, we only deal
+ // with the first one that intersects the current,
+ // text block, so mark this a as a partial.
+ *aSelStatus = BlockSelectionStatus::eBlockPartial;
+ } else if (*e1s1 > 0 && *e2s2 < 0) {
+ // The range extends beyond the start and
+ // end of the current text block.
+ *aSelStatus = BlockSelectionStatus::eBlockInside;
+ } else if (*e1s1 <= 0 && *e2s2 >= 0) {
+ // The current text block contains the entire
+ // range.
+ *aSelStatus = BlockSelectionStatus::eBlockContains;
+ } else {
+ // The range partially intersects the block.
+ *aSelStatus = BlockSelectionStatus::eBlockPartial;
+ }
+
+ // Now create a range based on the intersection of the
+ // text block and range:
+
+ nsCOMPtr<nsINode> p1, p2;
+ int32_t o1, o2;
+
+ // The start of the range will be the rightmost
+ // start node.
+
+ if (*e1s1 >= 0) {
+ p1 = eStart->mNode;
+ o1 = eStartOffset;
+ } else {
+ p1 = startContainer;
+ o1 = startOffset;
+ }
+
+ // The end of the range will be the leftmost
+ // end node.
+
+ if (*e2s2 <= 0) {
+ p2 = eEnd->mNode;
+ o2 = eEndOffset;
+ } else {
+ p2 = endContainer;
+ o2 = endOffset;
+ }
+
+ range = nsRange::Create(p1, o1, p2, o2, IgnoreErrors());
+ if (NS_WARN_IF(!range)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Now iterate over this range to figure out the selection's
+ // block offset and length.
+
+ RefPtr<FilteredContentIterator> filteredIter;
+ nsresult rv =
+ CreateFilteredContentIterator(range, getter_AddRefs(filteredIter));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Find the first text node in the range.
+
+ bool found;
+ nsCOMPtr<nsIContent> content;
+
+ filteredIter->First();
+
+ if (!p1->IsText()) {
+ found = false;
+
+ for (; !filteredIter->IsDone(); filteredIter->Next()) {
+ nsINode* node = filteredIter->GetCurrentNode();
+
+ if (node->IsText()) {
+ p1 = node;
+ o1 = 0;
+ found = true;
+
+ break;
+ }
+ }
+
+ NS_ENSURE_TRUE(found, NS_ERROR_FAILURE);
+ }
+
+ // Find the last text node in the range.
+
+ filteredIter->Last();
+
+ if (!p2->IsText()) {
+ found = false;
+ for (; !filteredIter->IsDone(); filteredIter->Prev()) {
+ nsINode* node = filteredIter->GetCurrentNode();
+ if (node->IsText()) {
+ p2 = node;
+ o2 = p2->Length();
+ found = true;
+
+ break;
+ }
+ }
+
+ NS_ENSURE_TRUE(found, NS_ERROR_FAILURE);
+ }
+
+ found = false;
+ *aSelLength = 0;
+
+ for (int32_t i = 0; i < tableCount; i++) {
+ entry = mOffsetTable[i];
+ NS_ENSURE_TRUE(entry, NS_ERROR_FAILURE);
+ if (!found) {
+ if (entry->mNode == p1.get() && entry->mNodeOffset <= o1 &&
+ o1 <= entry->mNodeOffset + entry->mLength) {
+ *aSelOffset = entry->mStrOffset + (o1 - entry->mNodeOffset);
+ if (p1 == p2 && entry->mNodeOffset <= o2 &&
+ o2 <= entry->mNodeOffset + entry->mLength) {
+ // The start and end of the range are in the same offset
+ // entry. Calculate the length of the range then we're done.
+ *aSelLength = o2 - o1;
+ break;
+ }
+ // Add the length of the sub string in this offset entry
+ // that follows the start of the range.
+ *aSelLength = entry->mLength - (o1 - entry->mNodeOffset);
+ found = true;
+ }
+ } else { // Found.
+ if (entry->mNode == p2.get() && entry->mNodeOffset <= o2 &&
+ o2 <= entry->mNodeOffset + entry->mLength) {
+ // We found the end of the range. Calculate the length of the
+ // sub string that is before the end of the range, then we're done.
+ *aSelLength += o2 - entry->mNodeOffset;
+ break;
+ }
+ // The entire entry must be in the range.
+ *aSelLength += entry->mLength;
+ }
+ }
+
+ return NS_OK;
+}
+
+bool TextServicesDocument::SelectionIsCollapsed() {
+ return mSelStartIndex == mSelEndIndex && mSelStartOffset == mSelEndOffset;
+}
+
+bool TextServicesDocument::SelectionIsValid() { return mSelStartIndex >= 0; }
+
+// static
+nsresult TextServicesDocument::GetRangeEndPoints(
+ const AbstractRange* aAbstractRange, nsINode** aStartContainer,
+ int32_t* aStartOffset, nsINode** aEndContainer, int32_t* aEndOffset) {
+ if (NS_WARN_IF(!aAbstractRange) || NS_WARN_IF(!aStartContainer) ||
+ NS_WARN_IF(!aEndContainer) || NS_WARN_IF(!aEndOffset)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<nsINode> startContainer = aAbstractRange->GetStartContainer();
+ if (NS_WARN_IF(!startContainer)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsINode> endContainer = aAbstractRange->GetEndContainer();
+ if (NS_WARN_IF(!endContainer)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ startContainer.forget(aStartContainer);
+ endContainer.forget(aEndContainer);
+ *aStartOffset = static_cast<int32_t>(aAbstractRange->StartOffset());
+ *aEndOffset = static_cast<int32_t>(aAbstractRange->EndOffset());
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::FirstTextNode(
+ FilteredContentIterator* aFilteredIter, IteratorStatus* aIteratorStatus) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = IteratorStatus::eDone;
+ }
+
+ for (aFilteredIter->First(); !aFilteredIter->IsDone();
+ aFilteredIter->Next()) {
+ if (aFilteredIter->GetCurrentNode()->NodeType() == nsINode::TEXT_NODE) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = IteratorStatus::eValid;
+ }
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::LastTextNode(
+ FilteredContentIterator* aFilteredIter, IteratorStatus* aIteratorStatus) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = IteratorStatus::eDone;
+ }
+
+ for (aFilteredIter->Last(); !aFilteredIter->IsDone(); aFilteredIter->Prev()) {
+ if (aFilteredIter->GetCurrentNode()->NodeType() == nsINode::TEXT_NODE) {
+ if (aIteratorStatus) {
+ *aIteratorStatus = IteratorStatus::eValid;
+ }
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::FirstTextNodeInCurrentBlock(
+ FilteredContentIterator* aFilteredIter) {
+ NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER);
+
+ ClearDidSkip(aFilteredIter);
+
+ nsCOMPtr<nsIContent> last;
+
+ // Walk backwards over adjacent text nodes until
+ // we hit a block boundary:
+
+ while (!aFilteredIter->IsDone()) {
+ nsCOMPtr<nsIContent> content =
+ aFilteredIter->GetCurrentNode()->IsContent()
+ ? aFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+ if (last && IsBlockNode(content)) {
+ break;
+ }
+ if (IsTextNode(content)) {
+ if (last && !HasSameBlockNodeParent(content, last)) {
+ // We're done, the current text node is in a
+ // different block.
+ break;
+ }
+ last = content;
+ }
+
+ aFilteredIter->Prev();
+
+ if (DidSkip(aFilteredIter)) {
+ break;
+ }
+ }
+
+ if (last) {
+ aFilteredIter->PositionAt(last);
+ }
+
+ // XXX: What should we return if last is null?
+
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::FirstTextNodeInPrevBlock(
+ FilteredContentIterator* aFilteredIter) {
+ NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER);
+
+ // XXX: What if mFilteredIter is not currently on a text node?
+
+ // Make sure mFilteredIter is pointing to the first text node in the
+ // current block:
+
+ nsresult rv = FirstTextNodeInCurrentBlock(aFilteredIter);
+
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
+
+ // Point mFilteredIter to the first node before the first text node:
+
+ aFilteredIter->Prev();
+
+ if (aFilteredIter->IsDone()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Now find the first text node of the next block:
+
+ return FirstTextNodeInCurrentBlock(aFilteredIter);
+}
+
+// static
+nsresult TextServicesDocument::FirstTextNodeInNextBlock(
+ FilteredContentIterator* aFilteredIter) {
+ nsCOMPtr<nsIContent> prev;
+ bool crossedBlockBoundary = false;
+
+ NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER);
+
+ ClearDidSkip(aFilteredIter);
+
+ while (!aFilteredIter->IsDone()) {
+ nsCOMPtr<nsIContent> content =
+ aFilteredIter->GetCurrentNode()->IsContent()
+ ? aFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+
+ if (IsTextNode(content)) {
+ if (crossedBlockBoundary ||
+ (prev && !HasSameBlockNodeParent(prev, content))) {
+ break;
+ }
+ prev = content;
+ } else if (!crossedBlockBoundary && IsBlockNode(content)) {
+ crossedBlockBoundary = true;
+ }
+
+ aFilteredIter->Next();
+
+ if (!crossedBlockBoundary && DidSkip(aFilteredIter)) {
+ crossedBlockBoundary = true;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::GetFirstTextNodeInPrevBlock(
+ nsIContent** aContent) {
+ NS_ENSURE_TRUE(aContent, NS_ERROR_NULL_POINTER);
+
+ *aContent = 0;
+
+ // Save the iterator's current content node so we can restore
+ // it when we are done:
+
+ nsINode* node = mFilteredIter->GetCurrentNode();
+
+ nsresult rv = FirstTextNodeInPrevBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ // Try to restore the iterator before returning.
+ mFilteredIter->PositionAt(node);
+ return rv;
+ }
+
+ if (!mFilteredIter->IsDone()) {
+ nsCOMPtr<nsIContent> current =
+ mFilteredIter->GetCurrentNode()->IsContent()
+ ? mFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+ current.forget(aContent);
+ }
+
+ // Restore the iterator:
+
+ return mFilteredIter->PositionAt(node);
+}
+
+nsresult TextServicesDocument::GetFirstTextNodeInNextBlock(
+ nsIContent** aContent) {
+ NS_ENSURE_TRUE(aContent, NS_ERROR_NULL_POINTER);
+
+ *aContent = 0;
+
+ // Save the iterator's current content node so we can restore
+ // it when we are done:
+
+ nsINode* node = mFilteredIter->GetCurrentNode();
+
+ nsresult rv = FirstTextNodeInNextBlock(mFilteredIter);
+
+ if (NS_FAILED(rv)) {
+ // Try to restore the iterator before returning.
+ mFilteredIter->PositionAt(node);
+ return rv;
+ }
+
+ if (!mFilteredIter->IsDone()) {
+ nsCOMPtr<nsIContent> current =
+ mFilteredIter->GetCurrentNode()->IsContent()
+ ? mFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+ current.forget(aContent);
+ }
+
+ // Restore the iterator:
+ return mFilteredIter->PositionAt(node);
+}
+
+nsresult TextServicesDocument::CreateOffsetTable(
+ nsTArray<OffsetEntry*>* aOffsetTable,
+ FilteredContentIterator* aFilteredIter, IteratorStatus* aIteratorStatus,
+ nsRange* aIterRange, nsAString* aStr) {
+ nsCOMPtr<nsIContent> first;
+ nsCOMPtr<nsIContent> prev;
+
+ NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER);
+
+ ClearOffsetTable(aOffsetTable);
+
+ if (aStr) {
+ aStr->Truncate();
+ }
+
+ if (*aIteratorStatus == IteratorStatus::eDone) {
+ return NS_OK;
+ }
+
+ // If we have an aIterRange, retrieve the endpoints so
+ // they can be used in the while loop below to trim entries
+ // for text nodes that are partially selected by aIterRange.
+
+ nsCOMPtr<nsINode> rngStartNode, rngEndNode;
+ int32_t rngStartOffset = 0, rngEndOffset = 0;
+
+ if (aIterRange) {
+ nsresult rv = GetRangeEndPoints(aIterRange, getter_AddRefs(rngStartNode),
+ &rngStartOffset, getter_AddRefs(rngEndNode),
+ &rngEndOffset);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // The text service could have added text nodes to the beginning
+ // of the current block and called this method again. Make sure
+ // we really are at the beginning of the current block:
+
+ nsresult rv = FirstTextNodeInCurrentBlock(aFilteredIter);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t offset = 0;
+
+ ClearDidSkip(aFilteredIter);
+
+ while (!aFilteredIter->IsDone()) {
+ nsCOMPtr<nsIContent> content =
+ aFilteredIter->GetCurrentNode()->IsContent()
+ ? aFilteredIter->GetCurrentNode()->AsContent()
+ : nullptr;
+ if (IsTextNode(content)) {
+ if (prev && !HasSameBlockNodeParent(prev, content)) {
+ break;
+ }
+
+ nsString str;
+ content->GetNodeValue(str);
+
+ // Add an entry for this text node into the offset table:
+
+ OffsetEntry* entry = new OffsetEntry(content, offset, str.Length());
+ aOffsetTable->AppendElement(entry);
+
+ // If one or both of the endpoints of the iteration range
+ // are in the text node for this entry, make sure the entry
+ // only accounts for the portion of the text node that is
+ // in the range.
+
+ int32_t startOffset = 0;
+ int32_t endOffset = str.Length();
+ bool adjustStr = false;
+
+ if (entry->mNode == rngStartNode) {
+ entry->mNodeOffset = startOffset = rngStartOffset;
+ adjustStr = true;
+ }
+
+ if (entry->mNode == rngEndNode) {
+ endOffset = rngEndOffset;
+ adjustStr = true;
+ }
+
+ if (adjustStr) {
+ entry->mLength = endOffset - startOffset;
+ str = Substring(str, startOffset, entry->mLength);
+ }
+
+ offset += str.Length();
+
+ if (aStr) {
+ // Append the text node's string to the output string:
+ if (!first) {
+ *aStr = str;
+ } else {
+ *aStr += str;
+ }
+ }
+
+ prev = content;
+
+ if (!first) {
+ first = content;
+ }
+ }
+ // XXX This should be checked before IsTextNode(), but IsBlockNode() returns
+ // true even if content is a text node. See bug 1311934.
+ else if (IsBlockNode(content)) {
+ break;
+ }
+
+ aFilteredIter->Next();
+
+ if (DidSkip(aFilteredIter)) {
+ break;
+ }
+ }
+
+ if (first) {
+ // Always leave the iterator pointing at the first
+ // text node of the current block!
+ aFilteredIter->PositionAt(first);
+ } else {
+ // If we never ran across a text node, the iterator
+ // might have been pointing to something invalid to
+ // begin with.
+ *aIteratorStatus = IteratorStatus::eDone;
+ }
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::RemoveInvalidOffsetEntries() {
+ for (size_t i = 0; i < mOffsetTable.Length();) {
+ OffsetEntry* entry = mOffsetTable[i];
+ if (!entry->mIsValid) {
+ mOffsetTable.RemoveElementAt(i);
+ if (mSelStartIndex >= 0 && static_cast<size_t>(mSelStartIndex) >= i) {
+ // We are deleting an entry that comes before
+ // mSelStartIndex, decrement mSelStartIndex so
+ // that it points to the correct entry!
+
+ NS_ASSERTION(i != static_cast<size_t>(mSelStartIndex),
+ "Invalid selection index.");
+
+ --mSelStartIndex;
+ --mSelEndIndex;
+ }
+ } else {
+ i++;
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::ClearOffsetTable(
+ nsTArray<OffsetEntry*>* aOffsetTable) {
+ for (size_t i = 0; i < aOffsetTable->Length(); i++) {
+ delete aOffsetTable->ElementAt(i);
+ }
+
+ aOffsetTable->Clear();
+
+ return NS_OK;
+}
+
+nsresult TextServicesDocument::SplitOffsetEntry(int32_t aTableIndex,
+ int32_t aNewEntryLength) {
+ OffsetEntry* entry = mOffsetTable[aTableIndex];
+
+ NS_ASSERTION((aNewEntryLength > 0), "aNewEntryLength <= 0");
+ NS_ASSERTION((aNewEntryLength < entry->mLength),
+ "aNewEntryLength >= mLength");
+
+ if (aNewEntryLength < 1 || aNewEntryLength >= entry->mLength) {
+ return NS_ERROR_FAILURE;
+ }
+
+ int32_t oldLength = entry->mLength - aNewEntryLength;
+
+ OffsetEntry* newEntry = new OffsetEntry(
+ entry->mNode, entry->mStrOffset + oldLength, aNewEntryLength);
+
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mOffsetTable.InsertElementAt(aTableIndex + 1, newEntry);
+
+ // Adjust entry fields:
+
+ entry->mLength = oldLength;
+ newEntry->mNodeOffset = entry->mNodeOffset + oldLength;
+
+ return NS_OK;
+}
+
+// static
+nsresult TextServicesDocument::NodeHasOffsetEntry(
+ nsTArray<OffsetEntry*>* aOffsetTable, nsINode* aNode, bool* aHasEntry,
+ int32_t* aEntryIndex) {
+ NS_ENSURE_TRUE(aNode && aHasEntry && aEntryIndex, NS_ERROR_NULL_POINTER);
+
+ for (size_t i = 0; i < aOffsetTable->Length(); i++) {
+ OffsetEntry* entry = (*aOffsetTable)[i];
+
+ NS_ENSURE_TRUE(entry, NS_ERROR_FAILURE);
+
+ if (entry->mNode == aNode) {
+ *aHasEntry = true;
+ *aEntryIndex = i;
+ return NS_OK;
+ }
+ }
+
+ *aHasEntry = false;
+ *aEntryIndex = -1;
+ return NS_OK;
+}
+
+// Spellchecker code has this. See bug 211343
+#define IS_NBSP_CHAR(c) (((unsigned char)0xa0) == (c))
+
+// static
+nsresult TextServicesDocument::FindWordBounds(
+ nsTArray<OffsetEntry*>* aOffsetTable, nsString* aBlockStr, nsINode* aNode,
+ int32_t aNodeOffset, nsINode** aWordStartNode, int32_t* aWordStartOffset,
+ nsINode** aWordEndNode, int32_t* aWordEndOffset) {
+ // Initialize return values.
+
+ if (aWordStartNode) {
+ *aWordStartNode = nullptr;
+ }
+ if (aWordStartOffset) {
+ *aWordStartOffset = 0;
+ }
+ if (aWordEndNode) {
+ *aWordEndNode = nullptr;
+ }
+ if (aWordEndOffset) {
+ *aWordEndOffset = 0;
+ }
+
+ int32_t entryIndex = 0;
+ bool hasEntry = false;
+
+ // It's assumed that aNode is a text node. The first thing
+ // we do is get its index in the offset table so we can
+ // calculate the dom point's string offset.
+
+ nsresult rv = NodeHasOffsetEntry(aOffsetTable, aNode, &hasEntry, &entryIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(hasEntry, NS_ERROR_FAILURE);
+
+ // Next we map aNodeOffset into a string offset.
+
+ OffsetEntry* entry = (*aOffsetTable)[entryIndex];
+ uint32_t strOffset = entry->mStrOffset + aNodeOffset - entry->mNodeOffset;
+
+ // Now we use the word breaker to find the beginning and end
+ // of the word from our calculated string offset.
+
+ const char16_t* str = aBlockStr->get();
+ uint32_t strLen = aBlockStr->Length();
+
+ mozilla::intl::WordBreaker* wordBreaker = nsContentUtils::WordBreaker();
+ mozilla::intl::WordRange res = wordBreaker->FindWord(str, strLen, strOffset);
+ if (res.mBegin > strLen) {
+ return str ? NS_ERROR_ILLEGAL_VALUE : NS_ERROR_NULL_POINTER;
+ }
+
+ // Strip out the NBSPs at the ends
+ while (res.mBegin <= res.mEnd && IS_NBSP_CHAR(str[res.mBegin])) {
+ res.mBegin++;
+ }
+ if (str[res.mEnd] == (unsigned char)0x20) {
+ uint32_t realEndWord = res.mEnd - 1;
+ while (realEndWord > res.mBegin && IS_NBSP_CHAR(str[realEndWord])) {
+ realEndWord--;
+ }
+ if (realEndWord < res.mEnd - 1) {
+ res.mEnd = realEndWord + 1;
+ }
+ }
+
+ // Now that we have the string offsets for the beginning
+ // and end of the word, run through the offset table and
+ // convert them back into dom points.
+
+ size_t lastIndex = aOffsetTable->Length() - 1;
+ for (size_t i = 0; i <= lastIndex; i++) {
+ entry = (*aOffsetTable)[i];
+
+ int32_t strEndOffset = entry->mStrOffset + entry->mLength;
+
+ // Check to see if res.mBegin is within the range covered
+ // by this entry. Note that if res.mBegin is after the last
+ // character covered by this entry, we will use the next
+ // entry if there is one.
+
+ if (uint32_t(entry->mStrOffset) <= res.mBegin &&
+ (res.mBegin < static_cast<uint32_t>(strEndOffset) ||
+ (res.mBegin == static_cast<uint32_t>(strEndOffset) &&
+ i == lastIndex))) {
+ if (aWordStartNode) {
+ *aWordStartNode = entry->mNode;
+ NS_IF_ADDREF(*aWordStartNode);
+ }
+
+ if (aWordStartOffset) {
+ *aWordStartOffset = entry->mNodeOffset + res.mBegin - entry->mStrOffset;
+ }
+
+ if (!aWordEndNode && !aWordEndOffset) {
+ // We've found our start entry, but if we're not looking
+ // for end entries, we're done.
+ break;
+ }
+ }
+
+ // Check to see if res.mEnd is within the range covered
+ // by this entry.
+
+ if (static_cast<uint32_t>(entry->mStrOffset) <= res.mEnd &&
+ res.mEnd <= static_cast<uint32_t>(strEndOffset)) {
+ if (res.mBegin == res.mEnd &&
+ res.mEnd == static_cast<uint32_t>(strEndOffset) && i != lastIndex) {
+ // Wait for the next round so that we use the same entry
+ // we did for aWordStartNode.
+ continue;
+ }
+
+ if (aWordEndNode) {
+ *aWordEndNode = entry->mNode;
+ NS_IF_ADDREF(*aWordEndNode);
+ }
+
+ if (aWordEndOffset) {
+ *aWordEndOffset = entry->mNodeOffset + res.mEnd - entry->mStrOffset;
+ }
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * nsIEditActionListener implementation:
+ * Don't implement the behavior directly here. The methods won't be called
+ * if the instance is created for inline spell checker created for editor.
+ * If you need to listen a new edit action, you need to add similar
+ * non-virtual method and you need to call it from EditorBase directly.
+ */
+
+NS_IMETHODIMP
+TextServicesDocument::DidDeleteNode(nsINode* aChild, nsresult aResult) {
+ if (NS_WARN_IF(NS_FAILED(aResult))) {
+ return NS_OK;
+ }
+ DidDeleteNode(aChild);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextServicesDocument::DidJoinNodes(nsINode* aLeftNode, nsINode* aRightNode,
+ nsINode* aParent, nsresult aResult) {
+ if (NS_WARN_IF(NS_FAILED(aResult))) {
+ return NS_OK;
+ }
+ if (NS_WARN_IF(!aLeftNode) || NS_WARN_IF(!aRightNode)) {
+ return NS_OK;
+ }
+ DidJoinNodes(*aLeftNode, *aRightNode);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextServicesDocument::DidInsertText(CharacterData* aTextNode, int32_t aOffset,
+ const nsAString& aString,
+ nsresult aResult) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextServicesDocument::WillDeleteText(CharacterData* aTextNode, int32_t aOffset,
+ int32_t aLength) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextServicesDocument::WillDeleteRanges(
+ const nsTArray<RefPtr<nsRange>>& aRangesToDelete) {
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/editor/spellchecker/TextServicesDocument.h b/editor/spellchecker/TextServicesDocument.h
new file mode 100644
index 0000000000..89214c71e2
--- /dev/null
+++ b/editor/spellchecker/TextServicesDocument.h
@@ -0,0 +1,318 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_TextServicesDocument_h
+#define mozilla_TextServicesDocument_h
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIEditActionListener.h"
+#include "nsISupportsImpl.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nscore.h"
+
+class nsIContent;
+class nsIEditor;
+class nsINode;
+class nsISelectionController;
+class nsRange;
+
+namespace mozilla {
+
+class FilteredContentIterator;
+class OffsetEntry;
+class TextEditor;
+
+namespace dom {
+class AbstractRange;
+class Document;
+class Element;
+class StaticRange;
+}; // namespace dom
+
+/**
+ * The TextServicesDocument presents the document in as a bunch of flattened
+ * text blocks. Each text block can be retrieved as an nsString.
+ */
+class TextServicesDocument final : public nsIEditActionListener {
+ private:
+ enum class IteratorStatus : uint8_t {
+ // No iterator (I), or iterator doesn't point to anything valid.
+ eDone = 0,
+ // I points to first text node (TN) in current block (CB).
+ eValid,
+ // No TN in CB, I points to first TN in prev block.
+ ePrev,
+ // No TN in CB, I points to first TN in next block.
+ eNext,
+ };
+
+ RefPtr<dom::Document> mDocument;
+ nsCOMPtr<nsISelectionController> mSelCon;
+ RefPtr<TextEditor> mTextEditor;
+ RefPtr<FilteredContentIterator> mFilteredIter;
+ nsCOMPtr<nsIContent> mPrevTextBlock;
+ nsCOMPtr<nsIContent> mNextTextBlock;
+ nsTArray<OffsetEntry*> mOffsetTable;
+ RefPtr<nsRange> mExtent;
+ uint32_t mTxtSvcFilterType;
+
+ int32_t mSelStartIndex;
+ int32_t mSelStartOffset;
+ int32_t mSelEndIndex;
+ int32_t mSelEndOffset;
+
+ IteratorStatus mIteratorStatus;
+
+ protected:
+ virtual ~TextServicesDocument();
+
+ public:
+ TextServicesDocument();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(TextServicesDocument)
+
+ /**
+ * Initializes the text services document to use a particular editor. The
+ * text services document will use the DOM document and presentation shell
+ * used by the editor.
+ *
+ * @param aEditor The editor to use.
+ */
+ nsresult InitWithEditor(nsIEditor* aEditor);
+
+ /**
+ * Sets the range/extent over which the text services document will iterate.
+ * Note that InitWithEditor() should have been called prior to calling this
+ * method. If this method is never called, the text services defaults to
+ * iterating over the entire document.
+ *
+ * @param aAbstractRange The range to use. aAbstractRange must point to a
+ * valid range object.
+ */
+ nsresult SetExtent(const dom::AbstractRange* aAbstractRange);
+
+ /**
+ * Expands the end points of the range so that it spans complete words. This
+ * call does not change any internal state of the text services document.
+ *
+ * @param aStaticRange [in/out] The range to be expanded/adjusted.
+ */
+ nsresult ExpandRangeToWordBoundaries(dom::StaticRange* aStaticRange);
+
+ /**
+ * Sets the filter type to be used while iterating over content.
+ * This will clear the current filter type if it's not either
+ * FILTERTYPE_NORMAL or FILTERTYPE_MAIL.
+ *
+ * @param aFilterType The filter type to be used while iterating over
+ * content.
+ */
+ nsresult SetFilterType(uint32_t aFilterType);
+
+ /**
+ * Returns the text in the current text block.
+ *
+ * @param aStr [OUT] This will contain the text.
+ */
+ nsresult GetCurrentTextBlock(nsAString& aStr);
+
+ /**
+ * Tells the document to point to the first text block in the document. This
+ * method does not adjust the current cursor position or selection.
+ */
+ nsresult FirstBlock();
+
+ enum class BlockSelectionStatus {
+ // There is no text block (TB) in or before the selection (S).
+ eBlockNotFound = 0,
+ // No TB in S, but found one before/after S.
+ eBlockOutside,
+ // S extends beyond the start and end of TB.
+ eBlockInside,
+ // TB contains entire S.
+ eBlockContains,
+ // S begins or ends in TB but extends outside of TB.
+ eBlockPartial,
+ };
+
+ /**
+ * Tells the document to point to the last text block that contains the
+ * current selection or caret.
+ *
+ * @param aSelectionStatus [OUT] This will contain the text block
+ * selection status.
+ * @param aSelectionOffset [OUT] This will contain the offset into the
+ * string returned by GetCurrentTextBlock() where
+ * the selection begins.
+ * @param aLength [OUT] This will contain the number of
+ * characters that are selected in the string.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ nsresult LastSelectedBlock(BlockSelectionStatus* aSelStatus,
+ int32_t* aSelOffset, int32_t* aSelLength);
+
+ /**
+ * Tells the document to point to the text block before the current one.
+ * This method will return NS_OK, even if there is no previous block.
+ * Callers should call IsDone() to check if we have gone beyond the first
+ * text block in the document.
+ */
+ nsresult PrevBlock();
+
+ /**
+ * Tells the document to point to the text block after the current one.
+ * This method will return NS_OK, even if there is no next block. Callers
+ * should call IsDone() to check if we have gone beyond the last text block
+ * in the document.
+ */
+ nsresult NextBlock();
+
+ /**
+ * IsDone() will always set aIsDone == false unless the document contains
+ * no text, PrevBlock() was called while the document was already pointing
+ * to the first text block in the document, or NextBlock() was called while
+ * the document was already pointing to the last text block in the document.
+ *
+ * @param aIsDone [OUT] This will contain the result.
+ */
+ nsresult IsDone(bool* aIsDone);
+
+ /**
+ * SetSelection() allows the caller to set the selection based on an offset
+ * into the string returned by GetCurrentTextBlock(). A length of zero
+ * places the cursor at that offset. A positive non-zero length "n" selects
+ * n characters in the string.
+ *
+ * @param aOffset Offset into string returned by
+ * GetCurrentTextBlock().
+ * @param aLength Number of characters selected.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ nsresult SetSelection(int32_t aOffset, int32_t aLength);
+
+ /**
+ * Scrolls the document so that the current selection is visible.
+ */
+ nsresult ScrollSelectionIntoView();
+
+ /**
+ * Deletes the text selected by SetSelection(). Calling DeleteSelection()
+ * with nothing selected, or with a collapsed selection (cursor) does
+ * nothing and returns NS_OK.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ nsresult DeleteSelection();
+
+ /**
+ * Inserts the given text at the current cursor position. If there is a
+ * selection, it will be deleted before the text is inserted.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ nsresult InsertText(const nsAString& aText);
+
+ /**
+ * nsIEditActionListener method implementations.
+ */
+ NS_DECL_NSIEDITACTIONLISTENER
+
+ /**
+ * Actual edit action listeners. When you add new method here for listening
+ * to new edit action, you need to make it called by EditorBase.
+ * Additionally, you need to call it from proper method of
+ * nsIEditActionListener too because if this is created not for inline
+ * spell checker of the editor, edit actions will be notified via
+ * nsIEditActionListener (slow path, though).
+ */
+ void DidDeleteNode(nsINode* aChild);
+ void DidJoinNodes(nsINode& aLeftNode, nsINode& aRightNode);
+
+ // TODO: We should get rid of this method since `aAbstractRange` has
+ // enough simple API to get them.
+ static nsresult GetRangeEndPoints(const dom::AbstractRange* aAbstractRange,
+ nsINode** aStartContainer,
+ int32_t* aStartOffset,
+ nsINode** aEndContainer,
+ int32_t* aEndOffset);
+
+ private:
+ nsresult CreateFilteredContentIterator(
+ const dom::AbstractRange* aAbstractRange,
+ FilteredContentIterator** aFilteredIter);
+
+ dom::Element* GetDocumentContentRootNode() const;
+ already_AddRefed<nsRange> CreateDocumentContentRange();
+ already_AddRefed<nsRange> CreateDocumentContentRootToNodeOffsetRange(
+ nsINode* aParent, uint32_t aOffset, bool aToStart);
+ nsresult CreateDocumentContentIterator(
+ FilteredContentIterator** aFilteredIter);
+
+ nsresult AdjustContentIterator();
+
+ static nsresult FirstTextNode(FilteredContentIterator* aFilteredIter,
+ IteratorStatus* aIteratorStatus);
+ static nsresult LastTextNode(FilteredContentIterator* aFilteredIter,
+ IteratorStatus* aIteratorStatus);
+
+ static nsresult FirstTextNodeInCurrentBlock(
+ FilteredContentIterator* aFilteredIter);
+ static nsresult FirstTextNodeInPrevBlock(
+ FilteredContentIterator* aFilteredIter);
+ static nsresult FirstTextNodeInNextBlock(
+ FilteredContentIterator* aFilteredIter);
+
+ nsresult GetFirstTextNodeInPrevBlock(nsIContent** aContent);
+ nsresult GetFirstTextNodeInNextBlock(nsIContent** aContent);
+
+ static bool IsBlockNode(nsIContent* aContent);
+ static bool IsTextNode(nsIContent* aContent);
+
+ static bool DidSkip(FilteredContentIterator* aFilteredIter);
+ static void ClearDidSkip(FilteredContentIterator* aFilteredIter);
+
+ static bool HasSameBlockNodeParent(nsIContent* aContent1,
+ nsIContent* aContent2);
+
+ MOZ_CAN_RUN_SCRIPT
+ nsresult SetSelectionInternal(int32_t aOffset, int32_t aLength,
+ bool aDoUpdate);
+ MOZ_CAN_RUN_SCRIPT
+ nsresult GetSelection(BlockSelectionStatus* aSelStatus, int32_t* aSelOffset,
+ int32_t* aSelLength);
+ MOZ_CAN_RUN_SCRIPT
+ nsresult GetCollapsedSelection(BlockSelectionStatus* aSelStatus,
+ int32_t* aSelOffset, int32_t* aSelLength);
+ nsresult GetUncollapsedSelection(BlockSelectionStatus* aSelStatus,
+ int32_t* aSelOffset, int32_t* aSelLength);
+
+ bool SelectionIsCollapsed();
+ bool SelectionIsValid();
+
+ static nsresult CreateOffsetTable(nsTArray<OffsetEntry*>* aOffsetTable,
+ FilteredContentIterator* aFilteredIter,
+ IteratorStatus* aIteratorStatus,
+ nsRange* aIterRange, nsAString* aStr);
+ static nsresult ClearOffsetTable(nsTArray<OffsetEntry*>* aOffsetTable);
+
+ static nsresult NodeHasOffsetEntry(nsTArray<OffsetEntry*>* aOffsetTable,
+ nsINode* aNode, bool* aHasEntry,
+ int32_t* aEntryIndex);
+
+ nsresult RemoveInvalidOffsetEntries();
+ nsresult SplitOffsetEntry(int32_t aTableIndex, int32_t aOffsetIntoEntry);
+
+ static nsresult FindWordBounds(nsTArray<OffsetEntry*>* aOffsetTable,
+ nsString* aBlockStr, nsINode* aNode,
+ int32_t aNodeOffset, nsINode** aWordStartNode,
+ int32_t* aWordStartOffset,
+ nsINode** aWordEndNode,
+ int32_t* aWordEndOffset);
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_TextServicesDocument_h
diff --git a/editor/spellchecker/moz.build b/editor/spellchecker/moz.build
new file mode 100644
index 0000000000..d7ba9dfa6d
--- /dev/null
+++ b/editor/spellchecker/moz.build
@@ -0,0 +1,27 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+MOCHITEST_MANIFESTS += ["tests/mochitest.ini"]
+
+XPIDL_SOURCES += [
+ "nsIInlineSpellChecker.idl",
+]
+
+XPIDL_MODULE = "txtsvc"
+
+EXPORTS.mozilla += [
+ "EditorSpellCheck.h",
+ "TextServicesDocument.h",
+]
+
+UNIFIED_SOURCES += [
+ "EditorSpellCheck.cpp",
+ "FilteredContentIterator.cpp",
+ "nsComposeTxtSrvFilter.cpp",
+ "TextServicesDocument.cpp",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/editor/spellchecker/nsComposeTxtSrvFilter.cpp b/editor/spellchecker/nsComposeTxtSrvFilter.cpp
new file mode 100644
index 0000000000..7ab6eae8cb
--- /dev/null
+++ b/editor/spellchecker/nsComposeTxtSrvFilter.cpp
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "nsComposeTxtSrvFilter.h"
+#include "nsError.h" // for NS_OK
+#include "nsIContent.h" // for nsIContent
+#include "nsLiteralString.h" // for NS_LITERAL_STRING
+#include "mozilla/dom/Element.h" // for nsIContent
+
+using namespace mozilla;
+
+bool nsComposeTxtSrvFilter::Skip(nsINode* aNode) const {
+ if (NS_WARN_IF(!aNode)) {
+ return false;
+ }
+
+ // Check to see if we can skip this node
+
+ if (aNode->IsAnyOfHTMLElements(nsGkAtoms::script, nsGkAtoms::textarea,
+ nsGkAtoms::select, nsGkAtoms::style,
+ nsGkAtoms::map)) {
+ return true;
+ }
+
+ if (!mIsForMail) {
+ return false;
+ }
+
+ // For nodes that are blockquotes, we must make sure
+ // their type is "cite"
+ if (aNode->IsHTMLElement(nsGkAtoms::blockquote)) {
+ return aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::cite, eIgnoreCase);
+ }
+
+ if (aNode->IsHTMLElement(nsGkAtoms::span)) {
+ if (aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::mozquote,
+ nsGkAtoms::_true, eIgnoreCase)) {
+ return true;
+ }
+
+ return aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
+ nsGkAtoms::mozsignature,
+ eCaseMatters);
+ }
+
+ if (aNode->IsHTMLElement(nsGkAtoms::table)) {
+ return aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
+ u"moz-email-headers-table"_ns,
+ eCaseMatters);
+ }
+
+ return false;
+}
+
+// static
+UniquePtr<nsComposeTxtSrvFilter> nsComposeTxtSrvFilter::CreateHelper(
+ bool aIsForMail) {
+ auto filter = MakeUnique<nsComposeTxtSrvFilter>();
+ filter->Init(aIsForMail);
+ return filter;
+}
diff --git a/editor/spellchecker/nsComposeTxtSrvFilter.h b/editor/spellchecker/nsComposeTxtSrvFilter.h
new file mode 100644
index 0000000000..b2de83451b
--- /dev/null
+++ b/editor/spellchecker/nsComposeTxtSrvFilter.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsComposeTxtSrvFilter_h__
+#define nsComposeTxtSrvFilter_h__
+
+#include "mozilla/UniquePtr.h"
+
+class nsINode;
+
+/**
+ * This class enables those using it to skip over certain nodes when
+ * traversing content.
+ *
+ * This filter is used to skip over various form control nodes and
+ * mail's cite nodes
+ */
+class nsComposeTxtSrvFilter final {
+ public:
+ static mozilla::UniquePtr<nsComposeTxtSrvFilter> CreateNormalFilter() {
+ return CreateHelper(false);
+ }
+ static mozilla::UniquePtr<nsComposeTxtSrvFilter> CreateMailFilter() {
+ return CreateHelper(true);
+ }
+
+ /**
+ * Indicates whether the content node should be skipped by the iterator
+ * @param aNode - node to skip
+ */
+ bool Skip(nsINode* aNode) const;
+
+ private:
+ // Helper - Intializer
+ void Init(bool aIsForMail) { mIsForMail = aIsForMail; }
+
+ static mozilla::UniquePtr<nsComposeTxtSrvFilter> CreateHelper(
+ bool aIsForMail);
+
+ bool mIsForMail = false;
+};
+
+#endif
diff --git a/editor/spellchecker/nsIInlineSpellChecker.idl b/editor/spellchecker/nsIInlineSpellChecker.idl
new file mode 100644
index 0000000000..b95bb954b4
--- /dev/null
+++ b/editor/spellchecker/nsIInlineSpellChecker.idl
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "nsISupports.idl"
+#include "domstubs.idl"
+
+interface nsIEditor;
+interface nsIEditorSpellCheck;
+
+webidl Node;
+webidl Range;
+
+[scriptable, uuid(b7b7a77c-40c4-4196-b0b7-b0338243b3fe)]
+interface nsIInlineSpellChecker : nsISupports
+{
+ readonly attribute nsIEditorSpellCheck spellChecker;
+
+ void init(in nsIEditor aEditor);
+ void cleanup(in boolean aDestroyingFrames);
+
+ attribute boolean enableRealTimeSpell;
+
+ void spellCheckRange(in Range aSelection);
+
+ Range getMisspelledWord(in Node aNode, in long aOffset);
+ [can_run_script]
+ void replaceWord(in Node aNode, in long aOffset, in AString aNewword);
+ void addWordToDictionary(in AString aWord);
+ void removeWordFromDictionary(in AString aWord);
+
+ void ignoreWord(in AString aWord);
+ void ignoreWords(in Array<AString> aWordsToIgnore);
+ void updateCurrentDictionary();
+
+ readonly attribute boolean spellCheckPending;
+};
diff --git a/editor/spellchecker/tests/.eslintrc.js b/editor/spellchecker/tests/.eslintrc.js
new file mode 100644
index 0000000000..845ed3f013
--- /dev/null
+++ b/editor/spellchecker/tests/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/mochitest-test"],
+};
diff --git a/editor/spellchecker/tests/bug1200533_subframe.html b/editor/spellchecker/tests/bug1200533_subframe.html
new file mode 100644
index 0000000000..f095c0601f
--- /dev/null
+++ b/editor/spellchecker/tests/bug1200533_subframe.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Language" content="en-US">
+</head>
+<body>
+<textarea id="none">root en-US</textarea>
+<textarea id="en-GB" lang="en-GB">root en-US, but element en-GB</textarea>
+<textarea id="en-gb" lang="en-gb">root en-US, but element en-gb (lower case)</textarea>
+<textarea id="en-ZA-not-avail" lang="en-ZA">root en-US, but element en-ZA (which is not installed)</textarea>
+<textarea id="en-generic" lang="en">root en-US, but element en</textarea>
+<textarea id="en" lang="en">root en-US, but element en</textarea>
+<textarea id="ko-not-avail" lang="ko">root en-US, but element ko (which is not installed)</textarea>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/bug1204147_subframe.html b/editor/spellchecker/tests/bug1204147_subframe.html
new file mode 100644
index 0000000000..a9b1225cd9
--- /dev/null
+++ b/editor/spellchecker/tests/bug1204147_subframe.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<textarea id="en-GB" lang="en-GB">element en-GB</textarea>
+<textarea id="en-US" lang="testing-XX">element should default to en-US</textarea>
+
+<div id="trouble-maker" contenteditable>the presence of this div triggers the faulty code path</div>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/bug1204147_subframe2.html b/editor/spellchecker/tests/bug1204147_subframe2.html
new file mode 100644
index 0000000000..935777bd99
--- /dev/null
+++ b/editor/spellchecker/tests/bug1204147_subframe2.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<textarea id="en-GB" lang="en-GB">element en-GB</textarea>
+<textarea id="en-US" lang="testing-XX">element should default to en-US</textarea>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/bug678842_subframe.html b/editor/spellchecker/tests/bug678842_subframe.html
new file mode 100644
index 0000000000..39d578ee41
--- /dev/null
+++ b/editor/spellchecker/tests/bug678842_subframe.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<textarea id="textarea" lang="testing-XXX"></textarea>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/bug717433_subframe.html b/editor/spellchecker/tests/bug717433_subframe.html
new file mode 100644
index 0000000000..3c2927e88f
--- /dev/null
+++ b/editor/spellchecker/tests/bug717433_subframe.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<textarea id="textarea" lang="en"></textarea>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/de-DE/de_DE.aff b/editor/spellchecker/tests/de-DE/de_DE.aff
new file mode 100644
index 0000000000..5dc6896b6d
--- /dev/null
+++ b/editor/spellchecker/tests/de-DE/de_DE.aff
@@ -0,0 +1,2 @@
+# Affix file for German English dictionary
+# Fake file, nothing here.
diff --git a/editor/spellchecker/tests/de-DE/de_DE.dic b/editor/spellchecker/tests/de-DE/de_DE.dic
new file mode 100644
index 0000000000..415c216861
--- /dev/null
+++ b/editor/spellchecker/tests/de-DE/de_DE.dic
@@ -0,0 +1,6 @@
+5
+ein
+guter
+heute
+ist
+Tag
diff --git a/editor/spellchecker/tests/en-AU/en_AU.aff b/editor/spellchecker/tests/en-AU/en_AU.aff
new file mode 100644
index 0000000000..e0c467248d
--- /dev/null
+++ b/editor/spellchecker/tests/en-AU/en_AU.aff
@@ -0,0 +1,2 @@
+# Affix file for British English dictionary
+# Fake file, nothing here.
diff --git a/editor/spellchecker/tests/en-AU/en_AU.dic b/editor/spellchecker/tests/en-AU/en_AU.dic
new file mode 100644
index 0000000000..0a1be725d4
--- /dev/null
+++ b/editor/spellchecker/tests/en-AU/en_AU.dic
@@ -0,0 +1,4 @@
+3
+Mary
+Paul
+Peter
diff --git a/editor/spellchecker/tests/en-GB/en_GB.aff b/editor/spellchecker/tests/en-GB/en_GB.aff
new file mode 100644
index 0000000000..e0c467248d
--- /dev/null
+++ b/editor/spellchecker/tests/en-GB/en_GB.aff
@@ -0,0 +1,2 @@
+# Affix file for British English dictionary
+# Fake file, nothing here.
diff --git a/editor/spellchecker/tests/en-GB/en_GB.dic b/editor/spellchecker/tests/en-GB/en_GB.dic
new file mode 100644
index 0000000000..0a1be725d4
--- /dev/null
+++ b/editor/spellchecker/tests/en-GB/en_GB.dic
@@ -0,0 +1,4 @@
+3
+Mary
+Paul
+Peter
diff --git a/editor/spellchecker/tests/mochitest.ini b/editor/spellchecker/tests/mochitest.ini
new file mode 100644
index 0000000000..a3bc9a683b
--- /dev/null
+++ b/editor/spellchecker/tests/mochitest.ini
@@ -0,0 +1,34 @@
+[DEFAULT]
+prefs =
+ gfx.font_loader.delay=0
+ gfx.font_loader.interval=0
+ gfx.font_rendering.fallback.async=false
+
+skip-if = os == 'android'
+support-files =
+ bug678842_subframe.html
+ bug717433_subframe.html
+ bug1200533_subframe.html
+ bug1204147_subframe.html
+ bug1204147_subframe2.html
+ en-GB/en_GB.dic
+ en-GB/en_GB.aff
+ en-AU/en_AU.dic
+ en-AU/en_AU.aff
+ de-DE/de_DE.dic
+ de-DE/de_DE.aff
+ !/editor/libeditor/tests/spellcheck.js
+
+[test_async_UpdateCurrentDictionary.html]
+[test_bug678842.html]
+[test_bug697981.html]
+[test_bug717433.html]
+[test_bug1200533.html]
+[test_bug1204147.html]
+[test_bug1205983.html]
+[test_bug1209414.html]
+[test_bug1219928.html]
+skip-if = e10s
+[test_bug1365383.html]
+[test_bug1418629.html]
+[test_bug1602526.html]
diff --git a/editor/spellchecker/tests/test_async_UpdateCurrentDictionary.html b/editor/spellchecker/tests/test_async_UpdateCurrentDictionary.html
new file mode 100644
index 0000000000..f95e353569
--- /dev/null
+++ b/editor/spellchecker/tests/test_async_UpdateCurrentDictionary.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=856270
+-->
+<head>
+ <title>Test for Bug 856270 - Async UpdateCurrentDictionary</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=856270">Mozilla Bug 856270</a>
+<p id="display"></p>
+<div id="content">
+<textarea id="editor" spellcheck="true"></textarea>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(start);
+
+function start() {
+ var textarea = document.getElementById("editor");
+ textarea.focus();
+
+ SpecialPowers.Cu.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck(textarea, function() {
+ var isc = SpecialPowers.wrap(textarea).editor.getInlineSpellChecker(false);
+ ok(isc, "Inline spell checker should exist after focus and spell check");
+ var sc = isc.spellChecker;
+ isnot(sc.GetCurrentDictionary(), lang,
+ "Current dictionary should not be set yet.");
+
+ // First, set the lang attribute on the textarea, call Update, and make
+ // sure the spell checker's language was updated appropriately.
+ var lang = "en-US";
+ textarea.setAttribute("lang", lang);
+ sc.UpdateCurrentDictionary(function() {
+ is(sc.GetCurrentDictionary(), lang,
+ "UpdateCurrentDictionary should set the current dictionary.");
+
+ // Second, make some Update calls, but then do a Set. The Set should
+ // effectively cancel the Updates, but the Updates' callbacks should be
+ // called nonetheless.
+ var numCalls = 3;
+ for (var i = 0; i < numCalls; i++) {
+ sc.UpdateCurrentDictionary(function() {
+ is(sc.GetCurrentDictionary(), "",
+ "No dictionary should be active after Update.");
+ if (--numCalls == 0) {
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ sc.SetCurrentDictionary("");
+ SimpleTest.finish();
+ }
+ });
+ }
+ try {
+ sc.SetCurrentDictionary("testing-XX");
+ } catch (err) {
+ // Set throws NS_ERROR_NOT_AVAILABLE because "testing-XX" isn't really
+ // an available dictionary.
+ }
+ is(sc.GetCurrentDictionary(), "",
+ "No dictionary should be active after Set.");
+ });
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1200533.html b/editor/spellchecker/tests/test_bug1200533.html
new file mode 100644
index 0000000000..107e54cb4e
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1200533.html
@@ -0,0 +1,159 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1200533
+-->
+<head>
+ <title>Test for Bug 1200533</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1200533">Mozilla Bug 1200533</a>
+<p id="display"></p>
+<iframe id="content"></iframe>
+
+</div>
+<pre id="test">
+<script class="testbody" ttype="application/javascript">
+
+/** Test for Bug 1200533 **/
+/** Visit the elements defined above and check the dictionary we got **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById("content");
+
+var tests = [
+ // text area, value of spellchecker.dictionary, result.
+ // Result: Document language.
+ [ "none", "", "en-US" ],
+ // Result: Element language.
+ [ "en-GB", "", "en-GB" ],
+ [ "en-gb", "", "en-GB" ],
+ // Result: Random en-* or en-US (if application locale is en-US).
+ [ "en-ZA-not-avail", "", "*" ],
+ [ "en-generic", "", "*" ],
+ [ "en", "", "*" ],
+ // Result: Locale.
+ [ "ko-not-avail", "", "en-US" ],
+
+ // Result: Preference value in all cases.
+ [ "en-ZA-not-avail", "en-AU", "en-AU" ],
+ [ "en-generic", "en-AU", "en-AU" ],
+ [ "ko-not-avail", "en-AU", "en-AU" ],
+
+ // Result: Random en-*.
+ [ "en-ZA-not-avail", "de-DE", "*" ],
+ [ "en-generic", "de-DE", "*" ],
+ // Result: Preference value.
+ [ "ko-not-avail", "de-DE", "de-DE" ],
+ ];
+
+var loadCount = 0;
+var retrying = false;
+var script;
+
+var loadListener = async function(evt) {
+ if (loadCount == 0) {
+ /* eslint-env mozilla/frame-script */
+ script = SpecialPowers.loadChromeScript(function() {
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install en-GB, en-AU and de-DE dictionaries.
+ var en_GB = dir.clone();
+ var en_AU = dir.clone();
+ var de_DE = dir.clone();
+ en_GB.append("en-GB");
+ en_AU.append("en-AU");
+ de_DE.append("de-DE");
+ hunspell.addDirectory(en_GB);
+ hunspell.addDirectory(en_AU);
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("check-existence",
+ () => [en_GB.exists(), en_AU.exists(),
+ de_DE.exists()]);
+ addMessageListener("destroy", () => {
+ hunspell.removeDirectory(en_GB);
+ hunspell.removeDirectory(en_AU);
+ hunspell.removeDirectory(de_DE);
+ });
+ });
+ var existenceChecks = await script.sendQuery("check-existence");
+ is(existenceChecks[0], true, "true expected (en-GB directory should exist)");
+ is(existenceChecks[1], true, "true expected (en-AU directory should exist)");
+ is(existenceChecks[2], true, "true expected (de-DE directory should exist)");
+ }
+
+ SpecialPowers.pushPrefEnv({set: [["spellchecker.dictionary", tests[loadCount][1]]]},
+ function() { continueTest(evt); });
+};
+
+function continueTest(evt) {
+ var doc = evt.target.contentDocument;
+ var elem = doc.getElementById(tests[loadCount][0]);
+ var editor = SpecialPowers.wrap(elem).editor;
+ editor.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor.getInlineSpellChecker(true);
+ const is_en_US = SpecialPowers.Services.locale.appLocaleAsBCP47 == "en-US";
+
+ SpecialPowers.Cu.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck(elem, function() {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ var dict = spellchecker.GetCurrentDictionary();
+ } catch (e) {}
+
+ if (!dict && !retrying) {
+ // It's possible for an asynchronous font-list update to cause a reflow
+ // that disrupts the async spell-check and results in not getting a
+ // current dictionary here; if that happens, we retry the same testcase
+ // by reloading the iframe without bumping loadCount.
+ info(`No current dictionary: retrying testcase ${loadCount}`);
+ retrying = true;
+ } else {
+ if (tests[loadCount][2] != "*") {
+ is(dict, tests[loadCount][2], "expected " + tests[loadCount][2]);
+ } else if (is_en_US && tests[loadCount][0].startsWith("en")) {
+ // Current application locale is en-US and content lang is en or
+ // en-unknown, so we should use en-US dictionary as default.
+ is(dict, "en-US", "expected en-US that is application locale");
+ } else {
+ var gotEn = (dict == "en-GB" || dict == "en-AU" || dict == "en-US");
+ is(gotEn, true, "expected en-AU or en-GB or en-US");
+ }
+
+ loadCount++;
+ retrying = false;
+ }
+
+ if (loadCount < tests.length) {
+ // Load the iframe again.
+ content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug1200533_subframe.html?firstload=false";
+ } else {
+ // Remove the fake dictionaries again, since it's otherwise picked up by later tests.
+ script.sendAsyncMessage("destroy");
+
+ SimpleTest.finish();
+ }
+ });
+}
+
+content.addEventListener("load", loadListener);
+
+content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug1200533_subframe.html?firstload=true";
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1204147.html b/editor/spellchecker/tests/test_bug1204147.html
new file mode 100644
index 0000000000..e009e1c432
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1204147.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1204147
+-->
+<head>
+ <title>Test for Bug 1204147</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1204147">Mozilla Bug 1204147</a>
+<p id="display"></p>
+<iframe id="content"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1204147 **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById("content");
+// Load a subframe containing an editor with using "en-GB". At first
+// load, it will set the dictionary to "en-GB". The bug was that a content preference
+// was also created. At second load, we check the dictionary for another element,
+// one that should use "en-US". With the bug corrected, we get "en-US", before
+// we got "en-GB" from the content preference.
+
+var firstLoad = true;
+var script;
+
+var loadListener = async function(evt) {
+ if (firstLoad) {
+ /* eslint-env mozilla/frame-script */
+ script = SpecialPowers.loadChromeScript(function() {
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install en-GB dictionary.
+ let en_GB = dir.clone();
+ en_GB.append("en-GB");
+ hunspell.addDirectory(en_GB);
+
+ addMessageListener("en_GB-exists", () => en_GB.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(en_GB));
+ });
+ is(await script.sendQuery("en_GB-exists"), true,
+ "true expected (en-GB directory should exist)");
+ }
+
+ var doc = evt.target.contentDocument;
+ var elem;
+ if (firstLoad) {
+ elem = doc.getElementById("en-GB");
+ } else {
+ elem = doc.getElementById("en-US");
+ }
+
+ var editor = SpecialPowers.wrap(elem).editor;
+ editor.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor.getInlineSpellChecker(true);
+
+ SpecialPowers.Cu.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck(elem, function() {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ var currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch (e) {}
+
+ if (firstLoad) {
+ firstLoad = false;
+
+ // First time around, the element's language should be used.
+ is(currentDictonary, "en-GB", "unexpected lang " + currentDictonary + " instead of en-GB");
+
+ // Note that on second load, we load a different page, which does NOT have the trouble-causing
+ // contenteditable in it. Sadly, loading the same page with the trouble-maker in it
+ // doesn't allow the retrieval of the spell check dictionary used for the element,
+ // because the trouble-maker causes the 'global' spell check dictionary to be set to "en-GB"
+ // (since it picks the first one from the list) before we have the chance to retrieve
+ // the dictionary for the element (which happens asynchonously after the spell check has completed).
+ content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug1204147_subframe2.html?firstload=false";
+ } else {
+ // Second time around, the element should default to en-US.
+ // Without the fix, the first run sets the content preference to en-GB for the whole site.
+ is(currentDictonary, "en-US", "unexpected lang " + currentDictonary + " instead of en-US");
+ content.removeEventListener("load", loadListener);
+
+ // Remove the fake en-GB dictionary again, since it's otherwise picked up by later tests.
+ script.sendAsyncMessage("destroy");
+
+ // Reset the preference, so the last value we set doesn't collide with the next test.
+ SimpleTest.finish();
+ }
+ });
+};
+
+content.addEventListener("load", loadListener);
+
+content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug1204147_subframe.html?firstload=true";
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1205983.html b/editor/spellchecker/tests/test_bug1205983.html
new file mode 100644
index 0000000000..e85bb5efdb
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1205983.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1205983
+-->
+<head>
+ <title>Test for Bug 1205983</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1205983">Mozilla Bug 1205983</a>
+<p id="display"></p>
+</div>
+
+<div contenteditable id="de-DE" lang="de-DE" onfocus="deFocus()">German heute ist ein guter Tag</div>
+<textarea id="en-US" lang="en-US" onfocus="enFocus()">Nogoodword today is a nice day</textarea>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function getMisspelledWords(editor) {
+ return editor.selectionController.getSelection(SpecialPowers.Ci.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+var elem_de;
+var editor_de;
+var selcon_de;
+var script;
+
+var onSpellCheck =
+ SpecialPowers.Cu.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm").onSpellCheck;
+
+/** Test for Bug 1205983 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ /* eslint-env mozilla/frame-script */
+ script = SpecialPowers.loadChromeScript(function() {
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install de-DE dictionary.
+ var de_DE = dir.clone();
+ de_DE.append("de-DE");
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("de_DE-exists", () => de_DE.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(de_DE));
+ });
+ is(await script.sendQuery("de_DE-exists"), true,
+ "true expected (de_DE directory should exist)");
+
+ document.getElementById("de-DE").focus();
+});
+
+function deFocus() {
+ elem_de = document.getElementById("de-DE");
+
+ onSpellCheck(elem_de, function() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ editor_de = editingSession.getEditorForWindow(window);
+ selcon_de = editor_de.selectionController;
+ var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
+
+ // Check that we spelled in German, so there is only one misspelled word.
+ is(sel.toString(), "German", "one misspelled word expected: German");
+
+ // Now focus the textarea, which requires English spelling.
+ document.getElementById("en-US").focus();
+ });
+}
+
+function enFocus() {
+ var elem_en = document.getElementById("en-US");
+ var editor_en =
+ SpecialPowers.wrap(elem_en)
+ .editor;
+ editor_en.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor_en.getInlineSpellChecker(true);
+
+ onSpellCheck(elem_en, function() {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ let currentDictonary;
+ try {
+ currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch (e) {}
+
+ // Check that the English dictionary is loaded and that the spell check has worked.
+ is(currentDictonary, "en-US", "expected en-US");
+ is(getMisspelledWords(editor_en), "Nogoodword", "one misspelled word expected: Nogoodword");
+
+ // So far all was boring. The important thing is whether the spell check result
+ // in the de-DE editor is still the same. After losing focus, no spell check
+ // updates should take place there.
+ var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
+ is(sel.toString(), "German", "one misspelled word expected: German");
+
+ // Remove the fake de_DE dictionary again.
+ script.sendAsyncMessage("destroy");
+
+ // Focus again, so the spelling gets updated, but before we need to kill the focus handler.
+ elem_de.onfocus = null;
+ elem_de.blur();
+ elem_de.focus();
+
+ // After removal, the de_DE editor should refresh the spelling with en-US.
+ onSpellCheck(elem_de, function() {
+ var endSel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
+ // eslint-disable-next-line no-useless-concat
+ is(endSel.toString(), "heute" + "ist" + "ein" + "guter",
+ "some misspelled words expected: heute ist ein guter");
+
+ // If we don't reset this, we cause massive leaks.
+ selcon_de = null;
+ editor_de = null;
+
+ SimpleTest.finish();
+ });
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1209414.html b/editor/spellchecker/tests/test_bug1209414.html
new file mode 100644
index 0000000000..aeb9b82274
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1209414.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1209414
+-->
+<head>
+ <title>Test for Bug 1209414</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1209414">Mozilla Bug 1209414</a>
+<p id="display"></p>
+</div>
+
+<textarea id="de-DE" lang="de-DE">heute ist ein guter Tag - today is a good day</textarea>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Ci = SpecialPowers.Ci;
+
+function getMisspelledWords(editor) {
+ return editor.selectionController.getSelection(Ci.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+var elem_de;
+var editor_de;
+var script;
+
+/** Test for Bug 1209414 **/
+/*
+ * All we want to do in this test is change the spelling using a right-click and selection from the menu.
+ * This is necessary since all the other tests use SetCurrentDictionary() which doesn't reflect
+ * user behaviour.
+ */
+
+var onSpellCheck =
+ SpecialPowers.Cu.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm").onSpellCheck;
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ /* global actorParent */
+ /* eslint-env mozilla/frame-script */
+ script = SpecialPowers.loadChromeScript(function() {
+ var chromeWin = actorParent.rootFrameLoader
+ .ownerElement.ownerGlobal.browsingContext.topChromeWindow;
+ var contextMenu = chromeWin.document.getElementById("contentAreaContextMenu");
+ contextMenu.addEventListener("popupshown",
+ () => sendAsyncMessage("popupshown"));
+
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install de-DE dictionary.
+ let de_DE = dir.clone();
+ de_DE.append("de-DE");
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("hidepopup", function() {
+ var state = contextMenu.state;
+
+ // Select Language from the menu. Take a look at
+ // toolkit/modules/InlineSpellChecker.jsm to see how the menu works.
+
+ contextMenu.ownerDocument.getElementById("spell-check-dictionary-en-US")
+ .doCommand();
+ contextMenu.hidePopup();
+
+ return state;
+ });
+ addMessageListener("destroy", () => hunspell.removeDirectory(de_DE));
+ addMessageListener("contextMenu-not-null", () => contextMenu != null);
+ addMessageListener("de_DE-exists", () => de_DE.exists());
+ });
+ is(await script.sendQuery("contextMenu-not-null"), true,
+ "Got context menu XUL");
+ is(await script.sendQuery("de_DE-exists"), true,
+ "true expected (de_DE directory should exist)");
+ script.addMessageListener("popupshown", handlePopup);
+
+ elem_de = document.getElementById("de-DE");
+ editor_de = SpecialPowers.wrap(elem_de).editor;
+ editor_de.setSpellcheckUserOverride(true);
+
+ onSpellCheck(elem_de, function() {
+ var inlineSpellChecker = editor_de.getInlineSpellChecker(true);
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ var currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch (e) {}
+
+ // Check that the German dictionary is loaded and that the spell check has worked.
+ is(currentDictonary, "de-DE", "expected de-DE");
+ // eslint-disable-next-line no-useless-concat
+ is(getMisspelledWords(editor_de), "today" + "is" + "a" + "good" + "day", "some misspelled words expected: today is a good day");
+
+ // Focus again, just to be sure that the context-click won't trigger another spell check.
+ elem_de.focus();
+
+ // Make sure all spell checking action is done before right-click to select the en-US dictionary.
+ onSpellCheck(elem_de, function() {
+ synthesizeMouse(elem_de, 2, 2, { type: "contextmenu", button: 2 }, window);
+ });
+ });
+});
+
+async function handlePopup() {
+ var state = await script.sendQuery("hidepopup");
+ is(state, "open", "checking if popup is open");
+
+ onSpellCheck(elem_de, function() {
+ var inlineSpellChecker = editor_de.getInlineSpellChecker(true);
+ var spellchecker = inlineSpellChecker.spellChecker;
+ let currentDictonary;
+ try {
+ currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch (e) {}
+
+ // Check that the English dictionary is loaded and that the spell check has worked.
+ is(currentDictonary, "en-US", "expected en-US");
+ // eslint-disable-next-line no-useless-concat
+ is(getMisspelledWords(editor_de), "heute" + "ist" + "ein" + "guter", "some misspelled words expected: heute ist ein guter");
+
+ // Remove the fake de_DE dictionary again.
+ script.sendAsyncMessage("destroy");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.SetCurrentDictionary("");
+ SimpleTest.finish();
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1219928.html b/editor/spellchecker/tests/test_bug1219928.html
new file mode 100644
index 0000000000..bbd8f80cd7
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1219928.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1219928
+-->
+<head>
+ <title>Test for Bug 1219928</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1219928">Mozilla Bug 1219928</a>
+<p id="display"></p>
+
+<div contenteditable id="en-US" lang="en-US">
+<p>And here a missspelled word</p>
+<style>
+<!-- and here another onnee in a style comment -->
+</style>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1219928 **/
+/* Very simple test to check that <style> blocks are skipped in the spell check */
+
+var spellchecker;
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var onSpellCheck =
+ SpecialPowers.Cu.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm", null).onSpellCheck;
+
+ var elem = document.getElementById("en-US");
+ elem.focus();
+
+ onSpellCheck(elem, function() {
+ var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+ var editor = editingSession.getEditorForWindow(window);
+ var selcon = editor.selectionController;
+ var sel = selcon.getSelection(selcon.SELECTION_SPELLCHECK);
+
+ is(sel.toString(), "missspelled", "one misspelled word expected: missspelled");
+
+ spellchecker = SpecialPowers.Cu.createSpellChecker();
+ spellchecker.setFilterType(spellchecker.FILTERTYPE_NORMAL);
+ spellchecker.InitSpellChecker(editor, false, spellCheckStarted);
+ });
+});
+
+function spellCheckStarted() {
+ var misspelledWord = spellchecker.GetNextMisspelledWord();
+ is(misspelledWord, "missspelled", "first misspelled word expected: missspelled");
+
+ // Without the fix, the next misspelled word was 'onnee', so we check that we don't get it.
+ misspelledWord = spellchecker.GetNextMisspelledWord();
+ isnot(misspelledWord, "onnee", "second misspelled word should not be: onnee");
+
+ spellchecker = "";
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1365383.html b/editor/spellchecker/tests/test_bug1365383.html
new file mode 100644
index 0000000000..5b3e238528
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1365383.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1365383
+-->
+<head>
+ <title>Test for Bug 1365383</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1365383">Mozilla Bug 1365383</a>
+<p id="display"></p>
+<div id="content">
+<textarea id="editor" spellcheck="true"></textarea>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ let textarea = document.getElementById("editor");
+ let editor = SpecialPowers.wrap(textarea).editor;
+
+ let spellChecker = SpecialPowers.Cu.createSpellChecker();
+
+ // Callback parameter isn't set
+ spellChecker.InitSpellChecker(editor, false);
+
+ textarea.focus();
+
+ SpecialPowers.Cu.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck(textarea, () => {
+ // Callback parameter isn't set
+ spellChecker.UpdateCurrentDictionary();
+
+ var canSpellCheck = spellChecker.canSpellCheck();
+ ok(canSpellCheck, "spellCheck is enabled");
+ SimpleTest.finish();
+ });
+});
+</script>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1418629.html b/editor/spellchecker/tests/test_bug1418629.html
new file mode 100644
index 0000000000..4f0014c45b
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1418629.html
@@ -0,0 +1,209 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Mozilla bug 1418629</title>
+ <link rel=stylesheet href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/AddTask.js"></script>
+ <script src="/tests/editor/libeditor/tests/spellcheck.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1418629">Mozilla Bug 1418629</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<input id="input1" spellcheck="true">
+<textarea id="textarea1"></textarea>
+<div id="edit1" contenteditable=true></div>
+
+<script>
+const {onSpellCheck} = SpecialPowers.Cu.import("resource://testing-common/AsyncSpellCheckTestHelper.jsm", {});
+
+SimpleTest.waitForExplicitFinish();
+
+function getEditor(input) {
+ if (input instanceof HTMLInputElement ||
+ input instanceof HTMLTextAreaElement) {
+ return SpecialPowers.wrap(input).editor;
+ }
+
+ return SpecialPowers.wrap(window).docShell.editor;
+}
+
+function resetEditableContent(input) {
+ if (input instanceof HTMLInputElement ||
+ input instanceof HTMLTextAreaElement) {
+ input.value = "";
+ return;
+ }
+ input.innerHTML = "";
+}
+
+async function test_with_single_quote(input) {
+ let misspeltWords = [];
+
+ input.focus();
+ resetEditableContent(input);
+
+ synthesizeKey("d");
+ synthesizeKey("o");
+ synthesizeKey("e");
+ synthesizeKey("s");
+
+ await new Promise((resolve) => { onSpellCheck(input, resolve); });
+ let editor = getEditor(input);
+ // isSpellingCheckOk is defined in spellcheck.js
+ ok(isSpellingCheckOk(editor, misspeltWords), "no misspelt words");
+
+ synthesizeKey("n");
+ synthesizeKey("\'");
+ is(input.value || input.textContent, "doesn\'", "");
+
+ await new Promise((resolve) => { onSpellCheck(input, resolve); });
+ // XXX This won't work since mozInlineSpellWordUtil::SplitDOM removes
+ // last single quote unfortunately that is during inputting.
+ // isSpellingCheckOk is defined in spellcheck.js
+ todo_is(isSpellingCheckOk(editor, misspeltWords, true), true,
+ "don't run spellchecker during inputting word");
+
+ synthesizeKey(" ");
+ is(input.value || input.textContent, "doesn\' ", "");
+
+ await new Promise((resolve) => { onSpellCheck(input, resolve); });
+ misspeltWords.push("doesn");
+ // isSpellingCheckOk is defined in spellcheck.js
+ ok(isSpellingCheckOk(editor, misspeltWords), "should run spellchecker");
+}
+
+async function test_with_twice_characters(input, ch) {
+ let misspeltWords = [];
+
+ input.focus();
+ resetEditableContent(input);
+
+ synthesizeKey("d");
+ synthesizeKey("o");
+ synthesizeKey("e");
+ synthesizeKey("s");
+ synthesizeKey("n");
+ synthesizeKey(ch);
+ synthesizeKey(ch);
+ is(input.value || input.textContent, "doesn" + ch + ch, "");
+
+ // trigger spellchecker
+ synthesizeKey(" ");
+
+ await new Promise((resolve) => { onSpellCheck(input, resolve); });
+ misspeltWords.push("doesn");
+ let editor = getEditor(input);
+ // isSpellingCheckOk is defined in spellcheck.js
+ ok(isSpellingCheckOk(editor, misspeltWords), "should run spellchecker");
+}
+
+async function test_between_single_quote(input) {
+ let misspeltWords = [];
+
+ input.focus();
+ resetEditableContent(input);
+
+ synthesizeKey("\'");
+ synthesizeKey("t");
+ synthesizeKey("e");
+ synthesizeKey("s");
+ synthesizeKey("t");
+ synthesizeKey("\'");
+
+ await new Promise((resolve) => { onSpellCheck(input, resolve); });
+ let editor = getEditor(input);
+ ok(isSpellingCheckOk(editor, misspeltWords),
+ "don't run spellchecker between single qoute");
+}
+
+async function test_with_email(input) {
+ let misspeltWords = [];
+
+ input.focus();
+ resetEditableContent(input);
+
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("@");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey(".");
+ synthesizeKey("c");
+ synthesizeKey("o");
+ synthesizeKey("m");
+
+ await new Promise((resolve) => { onSpellCheck(input, resolve); });
+ let editor = getEditor(input);
+ ok(isSpellingCheckOk(editor, misspeltWords),
+ "don't run spellchecker for email address");
+
+ synthesizeKey(" ");
+
+ await new Promise((resolve) => { onSpellCheck(input, resolve); });
+ ok(isSpellingCheckOk(editor, misspeltWords),
+ "no misspelt words due to email address");
+}
+
+async function test_with_url(input) {
+ let misspeltWords = [];
+
+ input.focus();
+ resetEditableContent(input);
+
+ synthesizeKey("h");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("p");
+ synthesizeKey(":");
+ synthesizeKey("/");
+ synthesizeKey("/");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey("t");
+ synthesizeKey(".");
+ synthesizeKey("c");
+ synthesizeKey("o");
+ synthesizeKey("m");
+
+ await new Promise((resolve) => { onSpellCheck(input, resolve); });
+ let editor = getEditor(input);
+ ok(isSpellingCheckOk(editor, misspeltWords),
+ "don't run spellchecker for URL");
+
+ synthesizeKey(" ");
+
+ await new Promise((resolve) => { onSpellCheck(input, resolve); });
+ ok(isSpellingCheckOk(editor, misspeltWords),
+ "no misspelt words due to URL");
+}
+
+SimpleTest.waitForFocus(() => {
+ for (let n of ["input1", "textarea1", "edit1"]) {
+ add_task(test_with_single_quote.bind(null,
+ document.getElementById(n)));
+ add_task(test_with_twice_characters.bind(null,
+ document.getElementById(n),
+ "\'"));
+ add_task(test_with_twice_characters.bind(null,
+ document.getElementById(n),
+ String.fromCharCode(0x2019)));
+ add_task(test_between_single_quote.bind(null,
+ document.getElementById(n)));
+ add_task(test_with_email.bind(null, document.getElementById(n)));
+ add_task(test_with_url.bind(null, document.getElementById(n)));
+ }
+});
+</script>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug1602526.html b/editor/spellchecker/tests/test_bug1602526.html
new file mode 100644
index 0000000000..c0324a8ab5
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1602526.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Mozilla bug 1602526</title>
+ <link rel=stylesheet href="/tests/SimpleTest/test.css">
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/AddTask.js"></script>
+ <script src="/tests/editor/libeditor/tests/spellcheck.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1602526">Mozilla Bug 1602526</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+
+</div>
+
+<div id="contenteditable" contenteditable=true>kkkk&#xf6;kkkk</div>
+
+<script>
+const {onSpellCheck} = SpecialPowers.Cu.import("resource://testing-common/AsyncSpellCheckTestHelper.jsm", {});
+
+SimpleTest.waitForExplicitFinish();
+
+function getEditor() {
+ return SpecialPowers.wrap(window).docShell.editor;
+}
+
+SimpleTest.waitForFocus(async () => {
+ let contenteditable = document.getElementById("contenteditable");
+ let misspeltWords = [];
+ misspeltWords.push("kkkk\u00f6kkkk");
+
+ contenteditable.focus();
+ window.getSelection().collapse(contenteditable.firstChild, contenteditable.firstChild.length);
+
+ synthesizeKey(" ");
+
+ // Run spell checker
+ await new Promise((resolve) => { onSpellCheck(contenteditable, resolve); });
+
+ synthesizeKey("a");
+ synthesizeKey("a");
+ synthesizeKey("a");
+
+ await new Promise((resolve) => { onSpellCheck(contenteditable, resolve); });
+ let editor = getEditor();
+ // isSpellingCheckOk is defined in spellcheck.js
+ // eslint-disable-next-line no-undef
+ ok(isSpellingCheckOk(editor, misspeltWords), "correct word is seleced as misspell");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug338427.html b/editor/spellchecker/tests/test_bug338427.html
new file mode 100644
index 0000000000..a316456460
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug338427.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=338427
+-->
+<head>
+ <title>Test for Bug 338427</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=338427">Mozilla Bug 338427</a>
+<p id="display"></p>
+<div id="content">
+<textarea id="editor" lang="testing-XX" spellcheck="true"></textarea>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 338427 **/
+function init() {
+ var onSpellCheck =
+ SpecialPowers.Cu.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck;
+ var textarea = document.getElementById("editor");
+ var editor = SpecialPowers.wrap(textarea).editor;
+ var spellchecker = editor.getInlineSpellChecker(true);
+ spellchecker.enableRealTimeSpell = true;
+ textarea.focus();
+
+ onSpellCheck(textarea, function() {
+ var list = spellchecker.spellChecker.GetDictionaryList();
+ ok(list.length > 0, "At least one dictionary should be present");
+
+ var lang = list[0];
+ spellchecker.spellChecker.SetCurrentDictionary(lang);
+
+ onSpellCheck(textarea, function() {
+ try {
+ var dictionary =
+ spellchecker.spellChecker.GetCurrentDictionary();
+ } catch (e) {}
+ is(dictionary, lang, "Unexpected spell check dictionary");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.spellChecker.SetCurrentDictionary("");
+ SimpleTest.finish();
+ });
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(init);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug678842.html b/editor/spellchecker/tests/test_bug678842.html
new file mode 100644
index 0000000000..a2b9c8db23
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug678842.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=678842
+-->
+<head>
+ <title>Test for Bug 678842</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=678842">Mozilla Bug 678842</a>
+<p id="display"></p>
+<iframe id="content"></iframe>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 678842 **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById("content");
+// load a subframe containing an editor with a defined unknown lang. At first
+// load, it will set dictionary to en-US. At second load, it will return current
+// dictionary. So, we can check, dictionary is correctly remembered between
+// loads.
+
+var firstLoad = true;
+var script;
+
+var loadListener = async function(evt) {
+ if (firstLoad) {
+ /* eslint-env mozilla/frame-script */
+ script = SpecialPowers.loadChromeScript(function() {
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install en-GB dictionary.
+ let en_GB = dir.clone();
+ en_GB.append("en-GB");
+ hunspell.addDirectory(en_GB);
+
+ addMessageListener("en_GB-exists", () => en_GB.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(en_GB));
+ });
+ is(await script.sendQuery("en_GB-exists"), true,
+ "true expected (en-GB directory should exist)");
+ }
+
+ var doc = evt.target.contentDocument;
+ var elem = doc.getElementById("textarea");
+ var editor = SpecialPowers.wrap(elem).editor;
+ editor.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor.getInlineSpellChecker(true);
+
+ SpecialPowers.Cu.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck(elem, function() {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ var currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch (e) {}
+
+ if (!currentDictonary) {
+ spellchecker.SetCurrentDictionary("en-US");
+ }
+
+ if (firstLoad) {
+ firstLoad = false;
+
+ // First time around, the dictionary defaults to the locale.
+ is(currentDictonary, "en-US", "unexpected lang " + currentDictonary + " instead of en-US");
+
+ // Select en-GB.
+ spellchecker.SetCurrentDictionary("en-GB");
+
+ content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug678842_subframe.html?firstload=false";
+ } else {
+ is(currentDictonary, "en-GB", "unexpected lang " + currentDictonary + " instead of en-GB");
+ content.removeEventListener("load", loadListener);
+
+ // Remove the fake en-GB dictionary again, since it's otherwise picked up by later tests.
+ script.sendAsyncMessage("destroy");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.SetCurrentDictionary("");
+ SimpleTest.finish();
+ }
+ });
+};
+
+content.addEventListener("load", loadListener);
+
+content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug678842_subframe.html?firstload=true";
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug697981.html b/editor/spellchecker/tests/test_bug697981.html
new file mode 100644
index 0000000000..7dcd1bca3a
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug697981.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=697981
+-->
+<head>
+ <title>Test for Bug 697981</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=697981">Mozilla Bug 697981</a>
+<p id="display"></p>
+</div>
+
+<textarea id="de-DE" lang="de-DE" onfocus="deFocus()">German heute ist ein guter Tag</textarea>
+<textarea id="en-US" lang="en-US" onfocus="enFocus()">Nogoodword today is a nice day</textarea>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function getMisspelledWords(editor) {
+ return editor.selectionController.getSelection(SpecialPowers.Ci.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+var elem_de;
+var editor_de;
+var script;
+
+var onSpellCheck =
+ SpecialPowers.Cu.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck;
+
+/** Test for Bug 697981 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async function() {
+ /* eslint-env mozilla/frame-script */
+ script = SpecialPowers.loadChromeScript(function() {
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install de-DE dictionary.
+ var de_DE = dir.clone();
+ de_DE.append("de-DE");
+ hunspell.addDirectory(de_DE);
+
+ addMessageListener("de_DE-exists", () => de_DE.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(de_DE));
+ });
+ is(await script.sendQuery("de_DE-exists"), true,
+ "true expected (de_DE directory should exist)");
+
+ document.getElementById("de-DE").focus();
+});
+
+function deFocus() {
+ elem_de = document.getElementById("de-DE");
+ editor_de = SpecialPowers.wrap(elem_de).editor;
+ editor_de.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor_de.getInlineSpellChecker(true);
+
+ onSpellCheck(elem_de, function() {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ var currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch (e) {}
+
+ // Check that the German dictionary is loaded and that the spell check has worked.
+ is(currentDictonary, "de-DE", "expected de-DE");
+ is(getMisspelledWords(editor_de), "German", "one misspelled word expected: German");
+
+ // Now focus the other textarea, which requires English spelling.
+ document.getElementById("en-US").focus();
+ });
+}
+
+function enFocus() {
+ var elem_en = document.getElementById("en-US");
+ var editor_en = SpecialPowers.wrap(elem_en).editor;
+ editor_en.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor_en.getInlineSpellChecker(true);
+
+ onSpellCheck(elem_en, function() {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ let currentDictonary;
+ try {
+ currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch (e) {}
+
+ // Check that the English dictionary is loaded and that the spell check has worked.
+ is(currentDictonary, "en-US", "expected en-US");
+ is(getMisspelledWords(editor_en), "Nogoodword", "one misspelled word expected: Nogoodword");
+
+ // So far all was boring. The important thing is whether the spell check result
+ // in the de-DE editor is still the same. After losing focus, no spell check
+ // updates should take place there.
+ is(getMisspelledWords(editor_de), "German", "one misspelled word expected: German");
+
+ // Remove the fake de_DE dictionary again.
+ script.sendAsyncMessage("destroy");
+
+ // Focus again, so the spelling gets updated, but before we need to kill the focus handler.
+ elem_de.onfocus = null;
+ elem_de.blur();
+ elem_de.focus();
+
+ // After removal, the de_DE editor should refresh the spelling with en-US.
+ onSpellCheck(elem_de, function() {
+ spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch (e) {}
+
+ // Check that the default English dictionary is loaded and that the spell check has worked.
+ is(currentDictonary, "en-US", "expected en-US");
+ // eslint-disable-next-line no-useless-concat
+ is(getMisspelledWords(editor_de), "heute" + "ist" + "ein" + "guter",
+ "some misspelled words expected: heute ist ein guter");
+
+ SimpleTest.finish();
+ });
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/editor/spellchecker/tests/test_bug717433.html b/editor/spellchecker/tests/test_bug717433.html
new file mode 100644
index 0000000000..3c55ace128
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug717433.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=717433
+-->
+<head>
+ <title>Test for Bug 717433</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=717433">Mozilla Bug 717433</a>
+<p id="display"></p>
+<iframe id="content"></iframe>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 717433 **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById("content");
+// Load a subframe containing an editor with language "en". At first
+// load, it will set the dictionary to en-GB or en-US. We set the other one.
+// At second load, it will return the current dictionary. We can check that the
+// dictionary is correctly remembered between loads.
+
+var firstLoad = true;
+var expected = "";
+var script;
+
+var loadListener = async function(evt) {
+ if (firstLoad) {
+ /* eslint-env mozilla/frame-script */
+ script = SpecialPowers.loadChromeScript(function() {
+ // eslint-disable-next-line mozilla/use-services
+ var dir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsIFile);
+ dir.append("tests");
+ dir.append("editor");
+ dir.append("spellchecker");
+ dir.append("tests");
+
+ var hunspell = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(Ci.mozISpellCheckingEngine);
+
+ // Install en-GB dictionary.
+ var en_GB = dir.clone();
+ en_GB.append("en-GB");
+ hunspell.addDirectory(en_GB);
+
+ addMessageListener("en_GB-exists", () => en_GB.exists());
+ addMessageListener("destroy", () => hunspell.removeDirectory(en_GB));
+ });
+ is(await script.sendQuery("en_GB-exists"), true,
+ "true expected (en-GB directory should exist)");
+ }
+
+ var doc = evt.target.contentDocument;
+ var elem = doc.getElementById("textarea");
+ var editor = SpecialPowers.wrap(elem).editor;
+ editor.setSpellcheckUserOverride(true);
+ var inlineSpellChecker = editor.getInlineSpellChecker(true);
+
+ SpecialPowers.Cu.import(
+ "resource://testing-common/AsyncSpellCheckTestHelper.jsm")
+ .onSpellCheck(elem, function() {
+ var spellchecker = inlineSpellChecker.spellChecker;
+ try {
+ var currentDictonary = spellchecker.GetCurrentDictionary();
+ } catch (e) {}
+
+ if (firstLoad) {
+ firstLoad = false;
+
+ // First time around, we get a random dictionary based on the language "en".
+ if (currentDictonary == "en-GB") {
+ spellchecker.SetCurrentDictionary("en-US");
+ expected = "en-US";
+ } else if (currentDictonary == "en-US") {
+ spellchecker.SetCurrentDictionary("en-GB");
+ expected = "en-GB";
+ } else {
+ is(true, false, "Neither en-US nor en-GB are current");
+ }
+ content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug717433_subframe.html?firstload=false";
+ } else {
+ is(currentDictonary, expected, expected + " expected");
+ content.removeEventListener("load", loadListener);
+
+ // Remove the fake en-GB dictionary again, since it's otherwise picked up by later tests.
+ script.sendAsyncMessage("destroy");
+
+ // This will clear the content preferences and reset "spellchecker.dictionary".
+ spellchecker.SetCurrentDictionary("");
+ SimpleTest.finish();
+ }
+ });
+};
+
+content.addEventListener("load", loadListener);
+
+content.src = "http://mochi.test:8888/tests/editor/spellchecker/tests/bug717433_subframe.html?firstload=true";
+
+</script>
+</pre>
+</body>
+</html>