diff options
Diffstat (limited to 'editor/spellchecker')
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ö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> |