From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- editor/spellchecker/EditorSpellCheck.cpp | 1179 ++++++++ editor/spellchecker/EditorSpellCheck.h | 99 + editor/spellchecker/FilteredContentIterator.cpp | 398 +++ editor/spellchecker/FilteredContentIterator.h | 82 + editor/spellchecker/TextServicesDocument.cpp | 2809 ++++++++++++++++++++ editor/spellchecker/TextServicesDocument.h | 438 +++ editor/spellchecker/moz.build | 34 + editor/spellchecker/nsComposeTxtSrvFilter.cpp | 64 + editor/spellchecker/nsComposeTxtSrvFilter.h | 45 + editor/spellchecker/nsIInlineSpellChecker.idl | 40 + editor/spellchecker/tests/bug1200533_subframe.html | 15 + editor/spellchecker/tests/bug1204147_subframe.html | 11 + .../spellchecker/tests/bug1204147_subframe2.html | 9 + editor/spellchecker/tests/bug678842_subframe.html | 8 + editor/spellchecker/tests/bug717433_subframe.html | 8 + editor/spellchecker/tests/chrome.ini | 9 + editor/spellchecker/tests/de-DE/de_DE.aff | 2 + editor/spellchecker/tests/de-DE/de_DE.dic | 6 + editor/spellchecker/tests/en-AU/en_AU.aff | 2 + editor/spellchecker/tests/en-AU/en_AU.dic | 4 + editor/spellchecker/tests/en-GB/en_GB.aff | 2 + editor/spellchecker/tests/en-GB/en_GB.dic | 4 + editor/spellchecker/tests/mochitest.ini | 68 + .../tests/multiple_content_languages_subframe.html | 13 + editor/spellchecker/tests/ru-RU/ru_RU.aff | 1 + editor/spellchecker/tests/ru-RU/ru_RU.dic | 2 + editor/spellchecker/tests/spellcheck.js | 36 + .../tests/test_async_UpdateCurrentDictionary.html | 60 + editor/spellchecker/tests/test_bug1100966.html | 74 + editor/spellchecker/tests/test_bug1154791.html | 74 + editor/spellchecker/tests/test_bug1200533.html | 163 ++ editor/spellchecker/tests/test_bug1204147.html | 115 + editor/spellchecker/tests/test_bug1205983.html | 134 + editor/spellchecker/tests/test_bug1209414.html | 144 + editor/spellchecker/tests/test_bug1219928.html | 69 + editor/spellchecker/tests/test_bug1365383.html | 46 + editor/spellchecker/tests/test_bug1368544.html | 91 + editor/spellchecker/tests/test_bug1402822.html | 114 + editor/spellchecker/tests/test_bug1418629.html | 210 ++ editor/spellchecker/tests/test_bug1497480.html | 94 + editor/spellchecker/tests/test_bug1602526.html | 57 + editor/spellchecker/tests/test_bug1761273.html | 96 + editor/spellchecker/tests/test_bug1773802.html | 96 + editor/spellchecker/tests/test_bug1837268.html | 79 + editor/spellchecker/tests/test_bug338427.html | 61 + editor/spellchecker/tests/test_bug366682.html | 65 + editor/spellchecker/tests/test_bug432225.html | 72 + editor/spellchecker/tests/test_bug484181.html | 73 + editor/spellchecker/tests/test_bug596333.html | 135 + editor/spellchecker/tests/test_bug636465.html | 54 + editor/spellchecker/tests/test_bug678842.html | 106 + editor/spellchecker/tests/test_bug697981.html | 137 + editor/spellchecker/tests/test_bug717433.html | 111 + .../tests/test_multiple_content_languages.html | 176 ++ .../test_nsIEditorSpellCheck_ReplaceWord.html | 64 + .../tests/test_spellcheck_after_edit.html | 198 ++ ...t_spellcheck_after_pressing_navigation_key.html | 77 + .../tests/test_spellcheck_selection.html | 36 + editor/spellchecker/tests/test_suggest.html | 42 + 59 files changed, 8511 insertions(+) create mode 100644 editor/spellchecker/EditorSpellCheck.cpp create mode 100644 editor/spellchecker/EditorSpellCheck.h create mode 100644 editor/spellchecker/FilteredContentIterator.cpp create mode 100644 editor/spellchecker/FilteredContentIterator.h create mode 100644 editor/spellchecker/TextServicesDocument.cpp create mode 100644 editor/spellchecker/TextServicesDocument.h create mode 100644 editor/spellchecker/moz.build create mode 100644 editor/spellchecker/nsComposeTxtSrvFilter.cpp create mode 100644 editor/spellchecker/nsComposeTxtSrvFilter.h create mode 100644 editor/spellchecker/nsIInlineSpellChecker.idl create mode 100644 editor/spellchecker/tests/bug1200533_subframe.html create mode 100644 editor/spellchecker/tests/bug1204147_subframe.html create mode 100644 editor/spellchecker/tests/bug1204147_subframe2.html create mode 100644 editor/spellchecker/tests/bug678842_subframe.html create mode 100644 editor/spellchecker/tests/bug717433_subframe.html create mode 100644 editor/spellchecker/tests/chrome.ini create mode 100644 editor/spellchecker/tests/de-DE/de_DE.aff create mode 100644 editor/spellchecker/tests/de-DE/de_DE.dic create mode 100644 editor/spellchecker/tests/en-AU/en_AU.aff create mode 100644 editor/spellchecker/tests/en-AU/en_AU.dic create mode 100644 editor/spellchecker/tests/en-GB/en_GB.aff create mode 100644 editor/spellchecker/tests/en-GB/en_GB.dic create mode 100644 editor/spellchecker/tests/mochitest.ini create mode 100644 editor/spellchecker/tests/multiple_content_languages_subframe.html create mode 100644 editor/spellchecker/tests/ru-RU/ru_RU.aff create mode 100644 editor/spellchecker/tests/ru-RU/ru_RU.dic create mode 100644 editor/spellchecker/tests/spellcheck.js create mode 100644 editor/spellchecker/tests/test_async_UpdateCurrentDictionary.html create mode 100644 editor/spellchecker/tests/test_bug1100966.html create mode 100644 editor/spellchecker/tests/test_bug1154791.html create mode 100644 editor/spellchecker/tests/test_bug1200533.html create mode 100644 editor/spellchecker/tests/test_bug1204147.html create mode 100644 editor/spellchecker/tests/test_bug1205983.html create mode 100644 editor/spellchecker/tests/test_bug1209414.html create mode 100644 editor/spellchecker/tests/test_bug1219928.html create mode 100644 editor/spellchecker/tests/test_bug1365383.html create mode 100644 editor/spellchecker/tests/test_bug1368544.html create mode 100644 editor/spellchecker/tests/test_bug1402822.html create mode 100644 editor/spellchecker/tests/test_bug1418629.html create mode 100644 editor/spellchecker/tests/test_bug1497480.html create mode 100644 editor/spellchecker/tests/test_bug1602526.html create mode 100644 editor/spellchecker/tests/test_bug1761273.html create mode 100644 editor/spellchecker/tests/test_bug1773802.html create mode 100644 editor/spellchecker/tests/test_bug1837268.html create mode 100644 editor/spellchecker/tests/test_bug338427.html create mode 100644 editor/spellchecker/tests/test_bug366682.html create mode 100644 editor/spellchecker/tests/test_bug432225.html create mode 100644 editor/spellchecker/tests/test_bug484181.html create mode 100644 editor/spellchecker/tests/test_bug596333.html create mode 100644 editor/spellchecker/tests/test_bug636465.html create mode 100644 editor/spellchecker/tests/test_bug678842.html create mode 100644 editor/spellchecker/tests/test_bug697981.html create mode 100644 editor/spellchecker/tests/test_bug717433.html create mode 100644 editor/spellchecker/tests/test_multiple_content_languages.html create mode 100644 editor/spellchecker/tests/test_nsIEditorSpellCheck_ReplaceWord.html create mode 100644 editor/spellchecker/tests/test_spellcheck_after_edit.html create mode 100644 editor/spellchecker/tests/test_spellcheck_after_pressing_navigation_key.html create mode 100644 editor/spellchecker/tests/test_spellcheck_selection.html create mode 100644 editor/spellchecker/tests/test_suggest.html (limited to 'editor/spellchecker') diff --git a/editor/spellchecker/EditorSpellCheck.cpp b/editor/spellchecker/EditorSpellCheck.cpp new file mode 100644 index 0000000000..35b4da06cf --- /dev/null +++ b/editor/spellchecker/EditorSpellCheck.cpp @@ -0,0 +1,1179 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sts=2 sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "EditorSpellCheck.h" + +#include "EditorBase.h" // for EditorBase +#include "HTMLEditor.h" // for HTMLEditor +#include "TextServicesDocument.h" // for TextServicesDocument + +#include "mozilla/Attributes.h" // for final +#include "mozilla/dom/Element.h" // for Element +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/StaticRange.h" +#include "mozilla/intl/Locale.h" // for mozilla::intl::Locale +#include "mozilla/intl/LocaleService.h" // for retrieving app locale +#include "mozilla/intl/OSPreferences.h" // for mozilla::intl::OSPreferences +#include "mozilla/Logging.h" // for mozilla::LazyLogModule +#include "mozilla/mozalloc.h" // for operator delete, etc +#include "mozilla/mozSpellChecker.h" // for mozSpellChecker +#include "mozilla/Preferences.h" // for Preferences + +#include "nsAString.h" // for nsAString::IsEmpty, etc +#include "nsComponentManagerUtils.h" // for do_CreateInstance +#include "nsDebug.h" // for NS_ENSURE_TRUE, etc +#include "nsDependentSubstring.h" // for Substring +#include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc +#include "nsIContent.h" // for nsIContent +#include "nsIContentPrefService2.h" // for nsIContentPrefService2, etc +#include "mozilla/dom/Document.h" // for Document +#include "nsIEditor.h" // for nsIEditor +#include "nsILoadContext.h" +#include "nsISupports.h" // for nsISupports +#include "nsISupportsUtils.h" // for NS_ADDREF +#include "nsIURI.h" // for nsIURI +#include "nsThreadUtils.h" // for GetMainThreadSerialEventTarget +#include "nsVariant.h" // for nsIWritableVariant, etc +#include "nsLiteralString.h" // for NS_LITERAL_STRING, etc +#include "nsRange.h" +#include "nsReadableUtils.h" // for ToNewUnicode, EmptyString, etc +#include "nsServiceManagerUtils.h" // for do_GetService +#include "nsString.h" // for nsAutoString, nsString, etc +#include "nsStringFwd.h" // for nsAFlatString +#include "nsStyleUtil.h" // for nsStyleUtil +#include "nsXULAppAPI.h" // for XRE_GetProcessType + +namespace mozilla { + +using namespace dom; +using intl::LocaleService; +using intl::OSPreferences; + +static mozilla::LazyLogModule sEditorSpellChecker("EditorSpellChecker"); + +class UpdateDictionaryHolder { + private: + EditorSpellCheck* mSpellCheck; + + public: + explicit UpdateDictionaryHolder(EditorSpellCheck* esc) : mSpellCheck(esc) { + if (mSpellCheck) { + mSpellCheck->BeginUpdateDictionary(); + } + } + + ~UpdateDictionaryHolder() { + if (mSpellCheck) { + mSpellCheck->EndUpdateDictionary(); + } + } +}; + +#define CPS_PREF_NAME u"spellcheck.lang"_ns + +/** + * Gets the URI of aEditor's document. + */ +static nsIURI* GetDocumentURI(EditorBase* aEditor) { + MOZ_ASSERT(aEditor); + + Document* doc = aEditor->AsEditorBase()->GetDocument(); + if (NS_WARN_IF(!doc)) { + return nullptr; + } + + return doc->GetDocumentURI(); +} + +static nsILoadContext* GetLoadContext(nsIEditor* aEditor) { + Document* doc = aEditor->AsEditorBase()->GetDocument(); + if (NS_WARN_IF(!doc)) { + return nullptr; + } + + return doc->GetLoadContext(); +} + +static nsCString DictionariesToString( + const nsTArray& aDictionaries) { + nsCString asString; + for (const auto& dictionary : aDictionaries) { + asString.Append(dictionary); + asString.Append(','); + } + return asString; +} + +static void StringToDictionaries(const nsCString& aString, + nsTArray& aDictionaries) { + nsTArray asDictionaries; + for (const nsACString& token : + nsCCharSeparatedTokenizer(aString, ',').ToRange()) { + if (token.IsEmpty()) { + continue; + } + aDictionaries.AppendElement(token); + } +} + +/** + * Fetches the dictionary stored in content prefs and maintains state during the + * fetch, which is asynchronous. + */ +class DictionaryFetcher final : public nsIContentPrefCallback2 { + public: + NS_DECL_ISUPPORTS + + DictionaryFetcher(EditorSpellCheck* aSpellCheck, + nsIEditorSpellCheckCallback* aCallback, uint32_t aGroup) + : mCallback(aCallback), mGroup(aGroup), mSpellCheck(aSpellCheck) {} + + NS_IMETHOD Fetch(nsIEditor* aEditor); + + NS_IMETHOD HandleResult(nsIContentPref* aPref) override { + nsCOMPtr value; + nsresult rv = aPref->GetValue(getter_AddRefs(value)); + NS_ENSURE_SUCCESS(rv, rv); + nsCString asString; + value->GetAsACString(asString); + StringToDictionaries(asString, mDictionaries); + return NS_OK; + } + + NS_IMETHOD HandleCompletion(uint16_t reason) override { + mSpellCheck->DictionaryFetched(this); + return NS_OK; + } + + NS_IMETHOD HandleError(nsresult error) override { return NS_OK; } + + nsCOMPtr mCallback; + uint32_t mGroup; + nsString mRootContentLang; + nsString mRootDocContentLang; + nsTArray mDictionaries; + + private: + ~DictionaryFetcher() {} + + RefPtr 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 contentPrefService = + do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID); + if (NS_WARN_IF(!contentPrefService)) { + mCallback->HandleError(NS_ERROR_NOT_AVAILABLE); + return NS_OK; + } + + nsCOMPtr 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 mEditorBase; + nsCOMPtr mCallback; +}; + +NS_IMETHODIMP +DictionaryFetcher::Fetch(nsIEditor* aEditor) { + NS_ENSURE_ARG_POINTER(aEditor); + + nsCOMPtr runnable = + new ContentPrefInitializerRunnable(aEditor, this); + NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000, + EventQueuePriority::Idle); + + return NS_OK; +} + +/** + * Stores the current dictionary for aEditor's document URL. + */ +static nsresult StoreCurrentDictionaries( + EditorBase* aEditorBase, const nsTArray& aDictionaries) { + NS_ENSURE_ARG_POINTER(aEditorBase); + + nsresult rv; + + nsCOMPtr docUri = GetDocumentURI(aEditorBase); + if (NS_WARN_IF(!docUri)) { + return NS_ERROR_FAILURE; + } + + nsAutoCString docUriSpec; + rv = docUri->GetSpec(docUriSpec); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr prefValue = new nsVariant(); + + nsCString asString = DictionariesToString(aDictionaries); + prefValue->SetAsAString(NS_ConvertUTF8toUTF16(asString)); + + nsCOMPtr contentPrefService = + do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID); + NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED); + + return contentPrefService->Set(NS_ConvertUTF8toUTF16(docUriSpec), + CPS_PREF_NAME, prefValue, + GetLoadContext(aEditorBase), nullptr); +} + +/** + * Forgets the current dictionary stored for aEditor's document URL. + */ +static nsresult ClearCurrentDictionaries(EditorBase* aEditorBase) { + NS_ENSURE_ARG_POINTER(aEditorBase); + + nsresult rv; + + nsCOMPtr docUri = GetDocumentURI(aEditorBase); + if (NS_WARN_IF(!docUri)) { + return NS_ERROR_FAILURE; + } + + nsAutoCString docUriSpec; + rv = docUri->GetSpec(docUriSpec); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr 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 spellChecker = mSpellChecker; + if (!spellChecker) { + spellChecker = mozSpellChecker::Create(); + MOZ_ASSERT(spellChecker); + } + nsTArray 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 mCallback; +}; + +NS_IMETHODIMP +EditorSpellCheck::InitSpellChecker(nsIEditor* aEditor, + bool aEnableSelectionChecking, + nsIEditorSpellCheckCallback* aCallback) { + NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER); + mEditor = aEditor->AsEditorBase(); + + RefPtr doc = mEditor->GetDocument(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + + // We can spell check with any editor type + RefPtr 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; + aEditor->GetSelection(getter_AddRefs(selection)); + if (NS_WARN_IF(!selection)) { + return NS_ERROR_FAILURE; + } + + if (selection->RangeCount()) { + RefPtr 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::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 caller = new CallbackCaller(aCallback); + rv = doc->Dispatch(TaskCategory::Other, caller.forget()); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorSpellCheck::GetNextMisspelledWord(nsAString& aNextMisspelledWord) { + MOZ_LOG(sEditorSpellChecker, LogLevel::Debug, ("%s", __FUNCTION__)); + + NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED); + + DeleteSuggestedWordList(); + // Beware! This may flush notifications via synchronous + // ScrollSelectionIntoView. + RefPtr 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(mSuggestedWordList.Length())) { + aSuggestedWord = mSuggestedWordList[mSuggestedWordIndex]; + mSuggestedWordIndex++; + } else { + // A blank string signals that there are no more strings + aSuggestedWord.Truncate(); + } + return NS_OK; +} + +NS_IMETHODIMP +EditorSpellCheck::CheckCurrentWord(const nsAString& aSuggestedWord, + bool* aIsMisspelled) { + NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED); + + DeleteSuggestedWordList(); + return mSpellChecker->CheckWord(aSuggestedWord, aIsMisspelled, + &mSuggestedWordList); +} + +NS_IMETHODIMP +EditorSpellCheck::Suggest(const nsAString& aSuggestedWord, uint32_t aCount, + JSContext* aCx, Promise** aPromise) { + NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED); + + nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!globalObject)) { + return NS_ERROR_UNEXPECTED; + } + + ErrorResult result; + RefPtr promise = Promise::Create(globalObject, result); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + + mSpellChecker->Suggest(aSuggestedWord, aCount) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [promise](const CopyableTArray& aSuggestions) { + promise->MaybeResolve(aSuggestions); + }, + [promise](nsresult aError) { + promise->MaybeReject(NS_ERROR_FAILURE); + }); + + promise.forget(aPromise); + return NS_OK; +} + +RefPtr EditorSpellCheck::CheckCurrentWordsNoSuggest( + const nsTArray& 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 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(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& aList) { + NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED); + + return mSpellChecker->GetDictionaryList(&aList); +} + +NS_IMETHODIMP +EditorSpellCheck::GetCurrentDictionaries(nsTArray& aDictionaries) { + NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED); + return mSpellChecker->GetCurrentDictionaries(aDictionaries); +} + +NS_IMETHODIMP +EditorSpellCheck::SetCurrentDictionaries( + const nsTArray& aDictionaries, JSContext* aCx, + Promise** aPromise) { + NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED); + + RefPtr kungFuDeathGrip = this; + + // The purpose of mUpdateDictionaryRunning is to avoid doing all of this if + // UpdateCurrentDictionary's helper method DictionaryFetched, which calls us, + // is on the stack. In other words: Only do this, if the user manually + // selected a dictionary to use. + if (!mUpdateDictionaryRunning) { + // Ignore pending dictionary fetchers by increasing this number. + mDictionaryFetcherGroup++; + + uint32_t flags = 0; + mEditor->GetFlags(&flags); + if (!(flags & nsIEditor::eEditorMailMask)) { + bool contentPrefMatchesUserPref = true; + // Check if aDictionaries has the same languages as mPreferredLangs. + if (!aDictionaries.IsEmpty()) { + if (aDictionaries.Length() != mPreferredLangs.Length()) { + contentPrefMatchesUserPref = false; + } else { + for (const auto& dictName : aDictionaries) { + if (mPreferredLangs.IndexOf(dictName) == + nsTArray::NoIndex) { + contentPrefMatchesUserPref = false; + break; + } + } + } + } + if (!contentPrefMatchesUserPref) { + // When user sets dictionary manually, we store this value associated + // with editor url, if it doesn't match the document language exactly. + // For example on "en" sites, we need to store "en-GB", otherwise + // the language might jump back to en-US although the user explicitly + // chose otherwise. + StoreCurrentDictionaries(mEditor, aDictionaries); +#ifdef DEBUG_DICT + printf("***** Writing content preferences for |%s|\n", + DictionariesToString(aDictionaries).Data()); +#endif + } else { + // If user sets a dictionary matching the language defined by + // document, we consider content pref has been canceled, and we clear + // it. + ClearCurrentDictionaries(mEditor); +#ifdef DEBUG_DICT + printf("***** Clearing content preferences for |%s|\n", + DictionariesToString(aDictionaries).Data()); +#endif + } + + // Also store it in as a preference, so we can use it as a fallback. + // We don't want this for mail composer because it uses + // "spellchecker.dictionary" as a preference. + // + // XXX: Prefs can only be set in the parent process, so this condition is + // necessary to stop libpref from throwing errors. But this should + // probably be handled in a better way. + if (XRE_IsParentProcess()) { + nsCString asString = DictionariesToString(aDictionaries); + Preferences::SetCString("spellchecker.dictionary", asString); +#ifdef DEBUG_DICT + printf("***** Possibly storing spellchecker.dictionary |%s|\n", + asString.Data()); +#endif + } + } else { + MOZ_ASSERT(flags & nsIEditor::eEditorMailMask); + // Since the mail editor can only influence the language selection by the + // html lang attribute, set the content-language document to persist + // multi language selections. + // XXX Why doesn't here use the document of the editor directly? + nsCOMPtr anonymousDivOrEditingHost; + if (HTMLEditor* htmlEditor = mEditor->GetAsHTMLEditor()) { + anonymousDivOrEditingHost = htmlEditor->ComputeEditingHost(); + } else { + anonymousDivOrEditingHost = mEditor->GetRoot(); + } + RefPtr ownerDoc = anonymousDivOrEditingHost->OwnerDoc(); + Document* parentDoc = ownerDoc->GetInProcessParentDocument(); + if (parentDoc) { + parentDoc->SetHeaderData( + nsGkAtoms::headerContentLanguage, + NS_ConvertUTF8toUTF16(DictionariesToString(aDictionaries))); + } + } + } + + nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!globalObject)) { + return NS_ERROR_UNEXPECTED; + } + + ErrorResult result; + RefPtr promise = Promise::Create(globalObject, result); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + + mSpellChecker->SetCurrentDictionaries(aDictionaries) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [promise]() { promise->MaybeResolveWithUndefined(); }, + [promise](nsresult aError) { + promise->MaybeReject(NS_ERROR_FAILURE); + }); + + promise.forget(aPromise); + return NS_OK; +} + +NS_IMETHODIMP +EditorSpellCheck::UninitSpellChecker() { + NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED); + + // Cleanup - kill the spell checker + DeleteSuggestedWordList(); + mDictionaryList.Clear(); + mDictionaryIndex = 0; + mDictionaryFetcherGroup++; + mSpellChecker = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +EditorSpellCheck::SetFilterType(uint32_t aFilterType) { + mTxtSrvFilterType = aFilterType; + return NS_OK; +} + +nsresult EditorSpellCheck::DeleteSuggestedWordList() { + mSuggestedWordList.Clear(); + mSuggestedWordIndex = 0; + return NS_OK; +} + +NS_IMETHODIMP +EditorSpellCheck::UpdateCurrentDictionary( + nsIEditorSpellCheckCallback* aCallback) { + if (NS_WARN_IF(!mSpellChecker)) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv; + + RefPtr kungFuDeathGrip = this; + + // Get language with html5 algorithm + const RefPtr rootEditableElement = + [](const EditorBase& aEditorBase) -> Element* { + if (!aEditorBase.IsHTMLEditor()) { + return aEditorBase.GetRoot(); + } + if (aEditorBase.IsMailEditor()) { + // Shouldn't run spellcheck in a mail editor without focus + // (bug 1507543) + // XXX Why doesn't here use the document of the editor directly? + Element* const editingHost = + aEditorBase.AsHTMLEditor()->ComputeEditingHost(); + if (!editingHost) { + return nullptr; + } + // Try to get topmost document's document element for embedded mail + // editor (bug 967494) + Document* parentDoc = + editingHost->OwnerDoc()->GetInProcessParentDocument(); + if (!parentDoc) { + return editingHost; + } + return parentDoc->GetDocumentElement(); + } + return aEditorBase.AsHTMLEditor()->GetFocusedElement(); + }(*mEditor); + + if (!rootEditableElement) { + return NS_ERROR_FAILURE; + } + + RefPtr fetcher = + new DictionaryFetcher(this, aCallback, mDictionaryFetcherGroup); + rootEditableElement->GetLang(fetcher->mRootContentLang); + RefPtr doc = rootEditableElement->GetComposedDoc(); + NS_ENSURE_STATE(doc); + doc->GetContentLanguage(fetcher->mRootDocContentLang); + + rv = fetcher->Fetch(mEditor); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// Helper function that iterates over the list of dictionaries and sets the one +// that matches based on a given comparison type. +bool EditorSpellCheck::BuildDictionaryList(const nsACString& aDictName, + const nsTArray& aDictList, + enum dictCompare aCompareType, + nsTArray& aOutList) { + for (const auto& dictStr : aDictList) { + bool equals = false; + switch (aCompareType) { + case DICT_NORMAL_COMPARE: + equals = aDictName.Equals(dictStr); + break; + case DICT_COMPARE_CASE_INSENSITIVE: + equals = aDictName.Equals(dictStr, nsCaseInsensitiveCStringComparator); + break; + case DICT_COMPARE_DASHMATCH: + equals = nsStyleUtil::DashMatchCompare( + NS_ConvertUTF8toUTF16(dictStr), NS_ConvertUTF8toUTF16(aDictName), + nsCaseInsensitiveStringComparator); + break; + } + if (equals) { + // Avoid adding duplicates to aOutList. + if (aOutList.IndexOf(dictStr) == nsTArray::NoIndex) { + aOutList.AppendElement(dictStr); + } +#ifdef DEBUG_DICT + printf("***** Trying |%s|.\n", dictStr.get()); +#endif + // We always break here. We tried to set the dictionary to an existing + // dictionary from the list. This must work, if it doesn't, there is + // no point trying another one. + return true; + } + } + return false; +} + +nsresult EditorSpellCheck::DictionaryFetched(DictionaryFetcher* aFetcher) { + MOZ_ASSERT(aFetcher); + RefPtr kungFuDeathGrip = this; + + BeginUpdateDictionary(); + + if (aFetcher->mGroup < mDictionaryFetcherGroup) { + // SetCurrentDictionary was called after the fetch started. Don't overwrite + // that dictionary with the fetched one. + EndUpdateDictionary(); + if (aFetcher->mCallback) { + aFetcher->mCallback->EditorSpellCheckDone(); + } + return NS_OK; + } + + /* + * We try to derive the dictionary to use based on the following priorities: + * 1) Content preference, so the language the user set for the site before. + * (Introduced in bug 678842 and corrected in bug 717433.) + * 2) Language set by the website, or any other dictionary that partly + * matches that. (Introduced in bug 338427.) + * Eg. if the website is "en-GB", a user who only has "en-US" will get + * that. If the website is generic "en", the user will get one of the + * "en-*" installed. If application locale or system locale is "en-*", + * we get it. If others, it is (almost) random. + * However, we prefer what is stored in "spellchecker.dictionary", + * so if the user chose "en-AU" before, they will get "en-AU" on a plain + * "en" site. (Introduced in bug 682564.) + * If the site has multiple languages declared in its Content-Language + * header and there is no more specific lang tag in HTML, we try to + * enable a dictionary for every content language. + * 3) The value of "spellchecker.dictionary" which reflects a previous + * language choice of the user (on another site). + * (This was the original behaviour before the aforementioned bugs + * landed). + * 4) The user's locale. + * 5) Use the current dictionary that is currently set. + * 6) The content of the "LANG" environment variable (if set). + * 7) The first spell check dictionary installed. + */ + + // Get the language from the element or its closest parent according to: + // https://html.spec.whatwg.org/#attr-lang + // This is used in SetCurrentDictionaries. + nsCString contentLangs; + // Reset mPreferredLangs so we only get the current state. + mPreferredLangs.Clear(); + CopyUTF16toUTF8(aFetcher->mRootContentLang, contentLangs); +#ifdef DEBUG_DICT + printf("***** mPreferredLangs (element) |%s|\n", contentLangs.get()); +#endif + if (!contentLangs.IsEmpty()) { + mPreferredLangs.AppendElement(contentLangs); + } else { + // If no luck, try the "Content-Language" header. + CopyUTF16toUTF8(aFetcher->mRootDocContentLang, contentLangs); +#ifdef DEBUG_DICT + printf("***** mPreferredLangs (content-language) |%s|\n", + contentLangs.get()); +#endif + StringToDictionaries(contentLangs, mPreferredLangs); + } + + // We obtain a list of available dictionaries. + AutoTArray dictList; + nsresult rv = mSpellChecker->GetDictionaryList(&dictList); + if (NS_WARN_IF(NS_FAILED(rv))) { + EndUpdateDictionary(); + if (aFetcher->mCallback) { + aFetcher->mCallback->EditorSpellCheckDone(); + } + return rv; + } + + // Priority 1: + // If we successfully fetched a dictionary from content prefs, do not go + // further. Use this exact dictionary. + // Don't use content preferences for editor with eEditorMailMask flag. + nsAutoCString dictName; + uint32_t flags; + mEditor->GetFlags(&flags); + if (!(flags & nsIEditor::eEditorMailMask)) { + if (!aFetcher->mDictionaries.IsEmpty()) { + RefPtr self = this; + RefPtr fetcher = aFetcher; + mSpellChecker->SetCurrentDictionaries(aFetcher->mDictionaries) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [self, fetcher]() { +#ifdef DEBUG_DICT + printf("***** Assigned from content preferences |%s|\n", + DictionariesToString(fetcher->mDictionaries).Data()); +#endif + // We take an early exit here, so let's not forget to clear + // the word list. + self->DeleteSuggestedWordList(); + + self->EndUpdateDictionary(); + if (fetcher->mCallback) { + fetcher->mCallback->EditorSpellCheckDone(); + } + }, + [self, fetcher](nsresult aError) { + if (aError == NS_ERROR_ABORT) { + return; + } + // May be dictionary was uninstalled ? + // Clear the content preference and continue. + ClearCurrentDictionaries(self->mEditor); + + // Priority 2 or later will handled by the following + self->SetFallbackDictionary(fetcher); + }); + return NS_OK; + } + } + SetFallbackDictionary(aFetcher); + return NS_OK; +} + +void EditorSpellCheck::SetDictionarySucceeded(DictionaryFetcher* aFetcher) { + DeleteSuggestedWordList(); + EndUpdateDictionary(); + if (aFetcher->mCallback) { + aFetcher->mCallback->EditorSpellCheckDone(); + } +} + +void EditorSpellCheck::SetFallbackDictionary(DictionaryFetcher* aFetcher) { + MOZ_ASSERT(mUpdateDictionaryRunning); + + AutoTArray tryDictList; + + // We obtain a list of available dictionaries. + AutoTArray dictList; + nsresult rv = mSpellChecker->GetDictionaryList(&dictList); + if (NS_WARN_IF(NS_FAILED(rv))) { + EndUpdateDictionary(); + if (aFetcher->mCallback) { + aFetcher->mCallback->EditorSpellCheckDone(); + } + return; + } + + // Priority 2: + // After checking the content preferences, we use the languages of the element + // or document. + + // Get the preference value. + nsAutoCString prefDictionariesAsString; + Preferences::GetLocalizedCString("spellchecker.dictionary", + prefDictionariesAsString); + nsTArray prefDictionaries; + StringToDictionaries(prefDictionariesAsString, prefDictionaries); + + nsAutoCString appLocaleStr; + // We pick one dictionary for every language that the element or document + // indicates it contains. + for (const auto& dictName : mPreferredLangs) { + // RFC 5646 explicitly states that matches should be case-insensitive. + if (BuildDictionaryList(dictName, dictList, DICT_COMPARE_CASE_INSENSITIVE, + tryDictList)) { +#ifdef DEBUG_DICT + printf("***** Trying from element/doc |%s| \n", dictName.get()); +#endif + continue; + } + + // Required dictionary was not available. Try to get a dictionary + // matching at least language part of dictName. + mozilla::intl::Locale loc; + if (mozilla::intl::LocaleParser::TryParse(dictName, loc).isOk() && + loc.Canonicalize().isOk()) { + Span language = loc.Language().Span(); + nsAutoCString langCode(language.data(), language.size()); + + // Try dictionary.spellchecker preference, if it starts with langCode, + // so we don't just get any random dictionary matching the language. + bool didAppend = false; + for (const auto& dictionary : prefDictionaries) { + if (nsStyleUtil::DashMatchCompare(NS_ConvertUTF8toUTF16(dictionary), + NS_ConvertUTF8toUTF16(langCode), + nsTDefaultStringComparator)) { +#ifdef DEBUG_DICT + printf( + "***** Trying preference value |%s| since it matches language " + "code\n", + dictionary.Data()); +#endif + if (BuildDictionaryList(dictionary, dictList, + DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) { + didAppend = true; + break; + } + } + } + if (didAppend) { + continue; + } + + // Use the application locale dictionary when the required language + // equals applocation locale language. + LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr); + if (!appLocaleStr.IsEmpty()) { + mozilla::intl::Locale appLoc; + auto result = + mozilla::intl::LocaleParser::TryParse(appLocaleStr, appLoc); + if (result.isOk() && appLoc.Canonicalize().isOk() && + loc.Language().Span() == appLoc.Language().Span()) { + if (BuildDictionaryList(appLocaleStr, dictList, + DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) { + continue; + } + } + } + + // Use the system locale dictionary when the required language equlas + // system locale language. + nsAutoCString sysLocaleStr; + OSPreferences::GetInstance()->GetSystemLocale(sysLocaleStr); + if (!sysLocaleStr.IsEmpty()) { + mozilla::intl::Locale sysLoc; + auto result = + mozilla::intl::LocaleParser::TryParse(sysLocaleStr, sysLoc); + if (result.isOk() && sysLoc.Canonicalize().isOk() && + loc.Language().Span() == sysLoc.Language().Span()) { + if (BuildDictionaryList(sysLocaleStr, dictList, + DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) { + continue; + } + } + } + + // Use any dictionary with the required language. +#ifdef DEBUG_DICT + printf("***** Trying to find match for language code |%s|\n", + langCode.get()); +#endif + BuildDictionaryList(langCode, dictList, DICT_COMPARE_DASHMATCH, + tryDictList); + } + } + + RefPtr self = this; + RefPtr fetcher = aFetcher; + RefPtr promise; + + if (tryDictList.IsEmpty()) { + // Proceed to priority 3 if the list of dictionaries is empty. + promise = GenericPromise::CreateAndReject(NS_ERROR_INVALID_ARG, __func__); + } else { + promise = mSpellChecker->SetCurrentDictionaries(tryDictList); + } + + // If an error was thrown while setting the dictionary, just + // fail silently so that the spellchecker dialog is allowed to come + // up. The user can manually reset the language to their choice on + // the dialog if it is wrong. + promise->Then( + GetMainThreadSerialEventTarget(), __func__, + [self, fetcher]() { self->SetDictionarySucceeded(fetcher); }, + [prefDictionaries = prefDictionaries.Clone(), dictList = dictList.Clone(), + self, fetcher]() { + // Build tryDictList with dictionaries for priorities 4 through 7. + // We'll use this list if there is no user preference or trying + // the user preference fails. + AutoTArray tryDictList; + + // Priority 4: + // As next fallback, try the current locale. + nsAutoCString appLocaleStr; + LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr); +#ifdef DEBUG_DICT + printf("***** Trying locale |%s|\n", appLocaleStr.get()); +#endif + self->BuildDictionaryList(appLocaleStr, dictList, + DICT_COMPARE_CASE_INSENSITIVE, tryDictList); + + // Priority 5: + // If we have a current dictionary and we don't have no item in try + // list, don't try anything else. + nsTArray currentDictionaries; + self->GetCurrentDictionaries(currentDictionaries); + if (!currentDictionaries.IsEmpty() && tryDictList.IsEmpty()) { +#ifdef DEBUG_DICT + printf("***** Retrieved current dict |%s|\n", + DictionariesToString(currentDictionaries).Data()); +#endif + self->EndUpdateDictionary(); + if (fetcher->mCallback) { + fetcher->mCallback->EditorSpellCheckDone(); + } + return; + } + + // Priority 6: + // Try to get current dictionary from environment variable LANG. + // LANG = language[_territory][.charset] + char* env_lang = getenv("LANG"); + if (env_lang) { + nsAutoCString lang(env_lang); + // Strip trailing charset, if there is any. + int32_t dot_pos = lang.FindChar('.'); + if (dot_pos != -1) { + lang = Substring(lang, 0, dot_pos); + } + + int32_t underScore = lang.FindChar('_'); + if (underScore != -1) { + lang.Replace(underScore, 1, '-'); +#ifdef DEBUG_DICT + printf("***** Trying LANG from environment |%s|\n", lang.get()); +#endif + self->BuildDictionaryList( + lang, dictList, DICT_COMPARE_CASE_INSENSITIVE, tryDictList); + } + } + + // Priority 7: + // If it does not work, pick the first one. + if (!dictList.IsEmpty()) { + self->BuildDictionaryList(dictList[0], dictList, DICT_NORMAL_COMPARE, + tryDictList); +#ifdef DEBUG_DICT + printf("***** Trying first of list |%s|\n", dictList[0].get()); +#endif + } + + // Priority 3: + // If the document didn't supply a dictionary or the setting + // failed, try the user preference next. + if (!prefDictionaries.IsEmpty()) { + self->mSpellChecker->SetCurrentDictionaries(prefDictionaries) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [self, fetcher]() { self->SetDictionarySucceeded(fetcher); }, + // Priority 3 failed, we'll use the list we built of + // priorities 4 to 7. + [tryDictList = tryDictList.Clone(), self, fetcher]() { + self->mSpellChecker + ->SetCurrentDictionaryFromList(tryDictList) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [self, fetcher]() { + self->SetDictionarySucceeded(fetcher); + }); + }); + } else { + // We don't have a user preference, so we'll try the list we + // built of priorities 4 to 7. + self->mSpellChecker->SetCurrentDictionaryFromList(tryDictList) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [self, fetcher]() { self->SetDictionarySucceeded(fetcher); }); + } + }); +} + +} // namespace mozilla diff --git a/editor/spellchecker/EditorSpellCheck.h b/editor/spellchecker/EditorSpellCheck.h new file mode 100644 index 0000000000..d9de5d9d40 --- /dev/null +++ b/editor/spellchecker/EditorSpellCheck.h @@ -0,0 +1,99 @@ +/* -*- 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 CheckCurrentWordsNoSuggest( + const nsTArray& aSuggestedWords); + + protected: + virtual ~EditorSpellCheck(); + + RefPtr mSpellChecker; + RefPtr mEditor; + + nsTArray mSuggestedWordList; + + // these are the words in the current personal dictionary, + // GetPersonalDictionary must be called to load them. + nsTArray mDictionaryList; + + nsTArray mPreferredLangs; + + uint32_t mTxtSrvFilterType; + int32_t mSuggestedWordIndex; + int32_t mDictionaryIndex; + uint32_t mDictionaryFetcherGroup; + + bool mUpdateDictionaryRunning; + + nsresult DeleteSuggestedWordList(); + + bool BuildDictionaryList(const nsACString& aDictName, + const nsTArray& aDictList, + enum dictCompare aCompareType, + nsTArray& aOutList); + + nsresult DictionaryFetched(DictionaryFetcher* aFetchState); + + void SetDictionarySucceeded(DictionaryFetcher* aFetcher); + 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..b0bfcd508a --- /dev/null +++ b/editor/spellchecker/FilteredContentIterator.cpp @@ -0,0 +1,398 @@ +/* -*- 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 + +#include "mozilla/ContentIterator.h" +#include "mozilla/dom/AbstractRange.h" +#include "mozilla/Maybe.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 "nsISupports.h" +#include "nsISupportsUtils.h" +#include "nsRange.h" + +namespace mozilla { + +using namespace dom; + +FilteredContentIterator::FilteredContentIterator( + UniquePtr aFilter) + : mCurrentIterator(nullptr), + mFilter(std::move(aFilter)), + mDidSkip(false), + mIsOutOfRange(false), + mDirection(eDirNotSet) {} + +FilteredContentIterator::~FilteredContentIterator() {} + +NS_IMPL_CYCLE_COLLECTION(FilteredContentIterator, mPostIterator, mPreIterator, + mRange) + +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 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); +} + +/////////////////////////////////////////////////////////////////////////// +// 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); + + nsIContent* parentContent = aContent->GetParent(); + if (MOZ_UNLIKELY(NS_WARN_IF(!parentContent))) { + return false; + } + Maybe offsetInParent = parentContent->ComputeIndexOf(aContent); + NS_WARNING_ASSERTION( + offsetInParent.isSome(), + "Content is not in the parent, is this called during a DOM mutation?"); + if (MOZ_UNLIKELY(NS_WARN_IF(offsetInParent.isNothing()))) { + return false; + } + + if (!aIsPreMode) { + MOZ_ASSERT(*offsetInParent != UINT32_MAX); + ++(*offsetInParent); + } + + const Maybe startRes = nsContentUtils::ComparePoints( + aStartContainer, aStartOffset, parentContent, *offsetInParent); + if (MOZ_UNLIKELY(NS_WARN_IF(!startRes))) { + return false; + } + const Maybe endRes = nsContentUtils::ComparePoints( + aEndContainer, aEndOffset, parentContent, *offsetInParent); + if (MOZ_UNLIKELY(NS_WARN_IF(!endRes))) { + return false; + } + return *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(aRange->StartOffset()), aRange->GetEndContainer(), + static_cast(aRange->EndOffset())); +} + +// Helper function to advance to the next or previous node +nsresult FilteredContentIterator::AdvanceNode(nsINode* aNode, + nsINode*& aNewNode, + eDirectionType aDir) { + nsCOMPtr 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 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 currentNode = aNode; + while (1) { + if (mFilter->Skip(aNode)) { + aDidSkip = true; + // Get the next/prev node and then + // see if we should skip that + nsCOMPtr 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 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..864cc57c2a --- /dev/null +++ b/editor/spellchecker/FilteredContentIterator.h @@ -0,0 +1,82 @@ +/* -*- 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 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; + + UniquePtr mFilter; + RefPtr 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..c348c70ad6 --- /dev/null +++ b/editor/spellchecker/TextServicesDocument.cpp @@ -0,0 +1,2809 @@ +/* -*- 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 "EditorBase.h" // for EditorBase +#include "EditorUtils.h" // for AutoTransactionBatchExternal +#include "FilteredContentIterator.h" // for FilteredContentIterator +#include "HTMLEditUtils.h" // for HTMLEditUtils +#include "JoinSplitNodeDirection.h" // for JoinNodesDirection + +#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc +#include "mozilla/IntegerRange.h" // for IntegerRange +#include "mozilla/mozalloc.h" // for operator new, etc +#include "mozilla/OwningNonNull.h" +#include "mozilla/UniquePtr.h" // for UniquePtr +#include "mozilla/dom/AbstractRange.h" // for AbstractRange +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/StaticRange.h" // for StaticRange +#include "mozilla/dom/Text.h" +#include "mozilla/intl/WordBreaker.h" // for WordRange, WordBreaker + +#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 "nsISupports.h" // for nsISupports +#include "nsISupportsUtils.h" // for NS_IF_ADDREF, NS_ADDREF, etc +#include "nsRange.h" // for nsRange +#include "nsString.h" // for nsString, nsAutoString +#include "nscore.h" // for nsresult, NS_IMETHODIMP, etc + +namespace mozilla { + +using namespace dom; + +/** + * OffsetEntry manages a range in a text node. It stores 2 offset values, + * one is offset in the text node, the other is offset in all text in + * the ancestor block of the text node. And the length is managing length + * in the text node, starting from the offset in text node. + * In other words, a text node may be managed by multiple instances of this + * class. + */ +class OffsetEntry final { + public: + OffsetEntry() = delete; + + /** + * @param aTextNode The text node which will be manged by the instance. + * @param aOffsetInTextInBlock + * Start offset in the text node which will be managed by + * the instance. + * @param aLength Length in the text node which will be managed by the + * instance. + */ + OffsetEntry(Text& aTextNode, uint32_t aOffsetInTextInBlock, uint32_t aLength) + : mTextNode(aTextNode), + mOffsetInTextNode(0), + mOffsetInTextInBlock(aOffsetInTextInBlock), + mLength(aLength), + mIsInsertedText(false), + mIsValid(true) {} + + /** + * EndOffsetInTextNode() returns end offset in the text node, which is + * managed by the instance. + */ + uint32_t EndOffsetInTextNode() const { return mOffsetInTextNode + mLength; } + + /** + * OffsetInTextNodeIsInRangeOrEndOffset() checks whether the offset in + * the text node is managed by the instance or not. + */ + bool OffsetInTextNodeIsInRangeOrEndOffset(uint32_t aOffsetInTextNode) const { + return aOffsetInTextNode >= mOffsetInTextNode && + aOffsetInTextNode <= EndOffsetInTextNode(); + } + + /** + * EndOffsetInTextInBlock() returns end offset in the all text in ancestor + * block of the text node, which is managed by the instance. + */ + uint32_t EndOffsetInTextInBlock() const { + return mOffsetInTextInBlock + mLength; + } + + /** + * OffsetInTextNodeIsInRangeOrEndOffset() checks whether the offset in + * the all text in ancestor block of the text node is managed by the instance + * or not. + */ + bool OffsetInTextInBlockIsInRangeOrEndOffset( + uint32_t aOffsetInTextInBlock) const { + return aOffsetInTextInBlock >= mOffsetInTextInBlock && + aOffsetInTextInBlock <= EndOffsetInTextInBlock(); + } + + OwningNonNull mTextNode; + uint32_t mOffsetInTextNode; + // Offset in all text in the closest ancestor block of mTextNode. + uint32_t mOffsetInTextInBlock; + uint32_t mLength; + bool mIsInsertedText; + bool mIsValid; +}; + +template +struct MOZ_STACK_CLASS ArrayLengthMutationGuard final { + ArrayLengthMutationGuard() = delete; + explicit ArrayLengthMutationGuard(const nsTArray& aArray) + : mArray(aArray), mOldLength(aArray.Length()) {} + ~ArrayLengthMutationGuard() { + if (mArray.Length() != mOldLength) { + MOZ_CRASH("The array length was changed unexpectedly"); + } + } + + private: + const nsTArray& mArray; + size_t mOldLength; +}; + +#define LockOffsetEntryArrayLengthInDebugBuild(aName, aArray) \ + DebugOnly>> const aName = \ + ArrayLengthMutationGuard>(aArray); + +TextServicesDocument::TextServicesDocument() + : mTxtSvcFilterType(0), mIteratorStatus(IteratorStatus::eDone) {} + +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, mEditorBase, + mFilteredIter, mPrevTextBlock, mNextTextBlock, mExtent) + +nsresult TextServicesDocument::InitWithEditor(nsIEditor* aEditor) { + nsCOMPtr 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 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; + } + } + + mEditorBase = aEditor->AsEditorBase(); + + 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 rngStartNode, rngEndNode; + uint32_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 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 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; + + OffsetEntryArray offsetTable; + nsAutoString blockStr; + Result result = offsetTable.Init( + *docFilteredIter, IteratorStatus::eValid, nullptr, &blockStr); + if (result.isErr()) { + return result.unwrapErr(); + } + + Result maybeWordRange = + offsetTable.FindWordRange( + blockStr, EditorRawDOMPoint(rngStartNode, rngStartOffset)); + offsetTable.Clear(); + if (maybeWordRange.isErr()) { + NS_WARNING( + "TextServicesDocument::OffsetEntryArray::FindWordRange() failed"); + return maybeWordRange.unwrapErr(); + } + rngStartNode = maybeWordRange.inspect().StartRef().GetContainerAs(); + rngStartOffset = maybeWordRange.inspect().StartRef().Offset(); + + // 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; + } + + result = offsetTable.Init(*docFilteredIter, IteratorStatus::eValid, nullptr, + &blockStr); + if (result.isErr()) { + return result.unwrapErr(); + } + + maybeWordRange = offsetTable.FindWordRange( + blockStr, EditorRawDOMPoint(rngEndNode, rngEndOffset)); + offsetTable.Clear(); + if (maybeWordRange.isErr()) { + NS_WARNING( + "TextServicesDocument::OffsetEntryArray::FindWordRange() failed"); + return maybeWordRange.unwrapErr(); + } + + // 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 != + maybeWordRange.inspect().StartRef().GetContainerAs() || + rngEndOffset != maybeWordRange.inspect().StartRef().Offset() || + (rngEndNode == rngStartNode && rngEndOffset == rngStartOffset)) { + rngEndNode = maybeWordRange.inspect().EndRef().GetContainerAs(); + rngEndOffset = maybeWordRange.inspect().EndRef().Offset(); + } + + // 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); + + Result result = + mOffsetTable.Init(*mFilteredIter, mIteratorStatus, mExtent, &aStr); + if (result.isErr()) { + NS_WARNING("OffsetEntryArray::Init() failed"); + return result.unwrapErr(); + } + mIteratorStatus = result.unwrap(); + 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, uint32_t* aSelOffset, + uint32_t* aSelLength) { + NS_ENSURE_TRUE(aSelStatus && aSelOffset && aSelLength, NS_ERROR_NULL_POINTER); + + mIteratorStatus = IteratorStatus::eDone; + + *aSelStatus = BlockSelectionStatus::eBlockNotFound; + *aSelOffset = *aSelLength = UINT32_MAX; + + if (!mSelCon || !mFilteredIter) { + return NS_ERROR_FAILURE; + } + + RefPtr selection = + mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL); + if (NS_WARN_IF(!selection)) { + return NS_ERROR_FAILURE; + } + + RefPtr range; + nsCOMPtr 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->AsText()); + if (NS_FAILED(rv)) { + return rv; + } + + rv = FirstTextNodeInCurrentBlock(mFilteredIter); + if (NS_FAILED(rv)) { + return rv; + } + + Result result = + mOffsetTable.Init(*mFilteredIter, IteratorStatus::eValid, mExtent); + if (result.isErr()) { + NS_WARNING("OffsetEntryArray::Init() failed"); + mIteratorStatus = IteratorStatus::eValid; // XXX + return result.unwrapErr(); + } + mIteratorStatus = result.unwrap(); + + 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 filteredIter; + rv = CreateFilteredContentIterator(range, getter_AddRefs(filteredIter)); + if (NS_FAILED(rv)) { + return rv; + } + + filteredIter->First(); + + Text* textNode = nullptr; + for (; !filteredIter->IsDone(); filteredIter->Next()) { + nsINode* currentNode = filteredIter->GetCurrentNode(); + if (currentNode->IsText()) { + textNode = currentNode->AsText(); + break; + } + } + + if (!textNode) { + return NS_OK; + } + + rv = mFilteredIter->PositionAt(textNode); + if (NS_FAILED(rv)) { + return rv; + } + + rv = FirstTextNodeInCurrentBlock(mFilteredIter); + if (NS_FAILED(rv)) { + return rv; + } + + Result result = mOffsetTable.Init( + *mFilteredIter, IteratorStatus::eValid, mExtent, nullptr); + if (result.isErr()) { + NS_WARNING("OffsetEntryArray::Init() failed"); + mIteratorStatus = IteratorStatus::eValid; // XXX + return result.unwrapErr(); + } + mIteratorStatus = result.inspect(); + + 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. + + const uint32_t rangeCount = selection->RangeCount(); + MOZ_ASSERT( + rangeCount, + "Selection is not collapsed, so, the range count should be 1 or larger"); + + // XXX: We may need to add some code here to make sure + // the ranges are sorted in document appearance order! + + for (const uint32_t i : Reversed(IntegerRange(rangeCount))) { + MOZ_ASSERT(selection->RangeCount() == rangeCount); + range = selection->GetRangeAt(i); + if (MOZ_UNLIKELY(!range)) { + return NS_OK; // XXX Really? + } + + // Create an iterator for the range. + + RefPtr 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. + + nsresult rv = mFilteredIter->PositionAt(filteredIter->GetCurrentNode()); + if (NS_FAILED(rv)) { + return rv; + } + + rv = FirstTextNodeInCurrentBlock(mFilteredIter); + if (NS_FAILED(rv)) { + return rv; + } + + mIteratorStatus = IteratorStatus::eValid; + + Result result = + mOffsetTable.Init(*mFilteredIter, IteratorStatus::eValid, mExtent); + if (result.isErr()) { + NS_WARNING("OffsetEntryArray::Init() failed"); + mIteratorStatus = IteratorStatus::eValid; // XXX + return result.unwrapErr(); + } + mIteratorStatus = result.unwrap(); + + return GetSelection(aSelStatus, aSelOffset, aSelLength); + } + } + } + + // 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 = rangeCount > 0 ? selection->GetRangeAt(rangeCount - 1) : nullptr; + 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 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. + nsresult rv = mFilteredIter->PositionAt(filteredIter->GetCurrentNode()); + if (NS_FAILED(rv)) { + return rv; + } + + rv = FirstTextNodeInCurrentBlock(mFilteredIter); + if (NS_FAILED(rv)) { + return rv; + } + + Result result = + mOffsetTable.Init(*mFilteredIter, IteratorStatus::eValid, mExtent); + if (result.isErr()) { + NS_WARNING("OffsetEntryArray::Init() failed"); + mIteratorStatus = IteratorStatus::eValid; // XXX + return result.unwrapErr(); + } + mIteratorStatus = result.unwrap(); + + rv = GetSelection(aSelStatus, aSelOffset, aSelLength); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "TextServicesDocument::GetSelection() failed"); + return rv; + } + } + + // 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(uint32_t aOffset, + uint32_t aLength) { + NS_ENSURE_TRUE(mSelCon, NS_ERROR_FAILURE); + + return SetSelectionInternal(aOffset, aLength, true); +} + +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::OffsetEntryArray::WillDeleteSelection() { + MOZ_ASSERT(mSelection.IsSet()); + MOZ_ASSERT(!mSelection.IsCollapsed()); + + for (size_t i = mSelection.StartIndex(); i <= mSelection.EndIndex(); i++) { + OffsetEntry* entry = ElementAt(i).get(); + if (i == mSelection.StartIndex()) { + // 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. + uint32_t selLength; + 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->EndOffsetInTextInBlock() - + mSelection.StartOffsetInTextInBlock(); + } + + if (selLength > 0) { + if (mSelection.StartOffsetInTextInBlock() > + entry->mOffsetInTextInBlock) { + // 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 = SplitElementAt(i, selLength); + if (NS_FAILED(rv)) { + NS_WARNING("selLength was invalid for the OffsetEntry"); + return rv; + } + + // Adjust selection indexes to account for new entry: + MOZ_DIAGNOSTIC_ASSERT(mSelection.StartIndex() + 1 < Length()); + MOZ_DIAGNOSTIC_ASSERT(mSelection.EndIndex() + 1 < Length()); + mSelection.SetIndexes(mSelection.StartIndex() + 1, + mSelection.EndIndex() + 1); + entry = ElementAt(++i).get(); + } + + if (mSelection.StartIndex() < mSelection.EndIndex()) { + // The entire entry is contained in the selection. Mark the + // entry invalid. + entry->mIsValid = false; + } + } + } + + if (i == mSelection.EndIndex()) { + 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. + + const uint32_t selLength = + mSelection.EndOffsetInTextInBlock() - entry->mOffsetInTextInBlock; + if (selLength) { + if (mSelection.EndOffsetInTextInBlock() < + entry->EndOffsetInTextInBlock()) { + // mOffsetInTextInBlock is guaranteed to be inside the selection, + // even when mSelection.IsInSameElement() is true. + nsresult rv = SplitElementAt(i, entry->mLength - selLength); + if (NS_FAILED(rv)) { + NS_WARNING( + "entry->mLength - selLength was invalid for the OffsetEntry"); + return rv; + } + + // Update the entry fields: + ElementAt(i + 1)->mOffsetInTextNode = entry->mOffsetInTextNode; + } + + if (mSelection.EndOffsetInTextInBlock() == + entry->EndOffsetInTextInBlock()) { + // The entire entry is contained in the selection. Mark the + // entry invalid. + entry->mIsValid = false; + } + } + } + } + + if (i != mSelection.StartIndex() && i != mSelection.EndIndex()) { + // The entire entry is contained in the selection. Mark the + // entry invalid. + entry->mIsValid = false; + } + } + + return NS_OK; +} + +nsresult TextServicesDocument::DeleteSelection() { + if (NS_WARN_IF(!mEditorBase) || + NS_WARN_IF(!mOffsetTable.mSelection.IsSet())) { + return NS_ERROR_FAILURE; + } + + if (mOffsetTable.mSelection.IsCollapsed()) { + 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 origStartNode, origEndNode; + uint32_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; + } + } + + if (NS_FAILED(mOffsetTable.WillDeleteSelection())) { + NS_WARNING( + "TextServicesDocument::OffsetEntryTable::WillDeleteSelection() failed"); + return NS_ERROR_FAILURE; + } + + // Make sure mFilteredIter always points to something valid! + AdjustContentIterator(); + + // Now delete the actual content! + OwningNonNull editorBase = *mEditorBase; + nsresult rv = editorBase->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 curStartNode, curEndNode; + uint32_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 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; + } + } + } + } + + OffsetEntry* entry = mOffsetTable.DidDeleteSelection(); + if (entry) { + SetSelection(mOffsetTable.mSelection.StartOffsetInTextInBlock(), 0); + } + + // Now remove any invalid entries from the offset table. + mOffsetTable.RemoveInvalidElements(); + return NS_OK; +} + +OffsetEntry* TextServicesDocument::OffsetEntryArray::DidDeleteSelection() { + MOZ_ASSERT(mSelection.IsSet()); + + // Move the caret to the end of the first valid entry. + // Start with SelectionStartIndex() since it may still be valid. + OffsetEntry* entry = nullptr; + for (size_t i = mSelection.StartIndex() + 1; !entry && i > 0; i--) { + entry = ElementAt(i - 1).get(); + if (!entry->mIsValid) { + entry = nullptr; + } else { + MOZ_DIAGNOSTIC_ASSERT(i - 1 < Length()); + mSelection.Set(i - 1, entry->EndOffsetInTextInBlock()); + } + } + + // If we still don't have a valid entry, move the caret + // to the next valid entry after the selection: + for (size_t i = mSelection.EndIndex(); !entry && i < Length(); i++) { + entry = ElementAt(i).get(); + if (!entry->mIsValid) { + entry = nullptr; + } else { + MOZ_DIAGNOSTIC_ASSERT(i < Length()); + mSelection.Set(i, entry->mOffsetInTextInBlock); + } + } + + if (!entry) { + // Uuughh we have no valid offset entry to place our + // caret ... just mark the selection invalid. + mSelection.Reset(); + } + + return entry; +} + +nsresult TextServicesDocument::InsertText(const nsAString& aText) { + if (NS_WARN_IF(!mEditorBase) || + NS_WARN_IF(!mOffsetTable.mSelection.IsSet())) { + 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. + + const bool wasSelectionCollapsed = mOffsetTable.mSelection.IsCollapsed(); + const uint32_t savedSelOffset = + mOffsetTable.mSelection.StartOffsetInTextInBlock(); + const uint32_t savedSelLength = mOffsetTable.mSelection.LengthInTextInBlock(); + + if (!wasSelectionCollapsed) { + // Collapse to the start of the current selection + // for the insert! + nsresult rv = + SetSelection(mOffsetTable.mSelection.StartOffsetInTextInBlock(), 0); + NS_ENSURE_SUCCESS(rv, rv); + } + + // AutoTransactionBatchExternal grabs mEditorBase, so, we don't need to grab + // the instance with local variable here. + OwningNonNull editorBase = *mEditorBase; + AutoTransactionBatchExternal treatAsOneTransaction(editorBase); + + nsresult rv = editorBase->InsertTextAsAction(aText); + if (NS_FAILED(rv)) { + NS_WARNING("InsertTextAsAction() failed"); + return rv; + } + + RefPtr selection = + mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL); + rv = mOffsetTable.DidInsertText(selection, aText); + if (NS_FAILED(rv)) { + NS_WARNING("TextServicesDocument::OffsetEntry::DidInsertText() failed"); + return rv; + } + + if (!wasSelectionCollapsed) { + nsresult rv = SetSelection(savedSelOffset, savedSelLength); + if (NS_FAILED(rv)) { + return rv; + } + + rv = DeleteSelection(); + if (NS_FAILED(rv)) { + return rv; + } + } + + return NS_OK; +} + +nsresult TextServicesDocument::OffsetEntryArray::DidInsertText( + dom::Selection* aSelection, const nsAString& aInsertedString) { + MOZ_ASSERT(mSelection.IsSet()); + + // When you touch this method, please make sure that the entry instance + // won't be deleted. If you know it'll be deleted, you should set it to + // `nullptr`. + OffsetEntry* entry = ElementAt(mSelection.StartIndex()).get(); + OwningNonNull const textNodeAtStartEntry = entry->mTextNode; + + NS_ASSERTION((entry->mIsValid), "Invalid insertion point!"); + + if (entry->mOffsetInTextInBlock == mSelection.StartOffsetInTextInBlock()) { + 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 += aInsertedString.Length(); + } else { + // Insert an inserted text offset entry before the current + // entry! + UniquePtr newInsertedTextEntry = + MakeUnique(entry->mTextNode, entry->mOffsetInTextInBlock, + aInsertedString.Length()); + newInsertedTextEntry->mIsInsertedText = true; + newInsertedTextEntry->mOffsetInTextNode = entry->mOffsetInTextNode; + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + InsertElementAt(mSelection.StartIndex(), std::move(newInsertedTextEntry)); + } + } else if (entry->EndOffsetInTextInBlock() == + mSelection.EndOffsetInTextInBlock()) { + // 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. + uint32_t nextIndex = mSelection.StartIndex() + 1; + OffsetEntry* insertedTextEntry = nullptr; + if (Length() > nextIndex) { + insertedTextEntry = ElementAt(nextIndex).get(); + if (!insertedTextEntry) { + return NS_ERROR_FAILURE; + } + + // Check if the entry is a match. If it isn't, set + // iEntry to zero. + if (!insertedTextEntry->mIsInsertedText || + insertedTextEntry->mOffsetInTextInBlock != + mSelection.StartOffsetInTextInBlock()) { + insertedTextEntry = nullptr; + } + } + + if (!insertedTextEntry) { + // We didn't find an inserted text offset entry, so + // create one. + UniquePtr newInsertedTextEntry = MakeUnique( + entry->mTextNode, mSelection.StartOffsetInTextInBlock(), 0); + newInsertedTextEntry->mOffsetInTextNode = entry->EndOffsetInTextNode(); + newInsertedTextEntry->mIsInsertedText = true; + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + insertedTextEntry = + InsertElementAt(nextIndex, std::move(newInsertedTextEntry))->get(); + } + + // We have a valid inserted text offset entry. Update its + // length, adjust the selection indexes, and make sure the + // caret is properly placed! + + insertedTextEntry->mLength += aInsertedString.Length(); + + MOZ_DIAGNOSTIC_ASSERT(nextIndex < Length()); + mSelection.SetIndex(nextIndex); + + if (!aSelection) { + return NS_OK; + } + + OwningNonNull textNode = insertedTextEntry->mTextNode; + nsresult rv = aSelection->CollapseInLimiter( + textNode, insertedTextEntry->EndOffsetInTextNode()); + if (NS_FAILED(rv)) { + NS_WARNING("Selection::CollapseInLimiter() failed"); + return rv; + } + } else if (entry->EndOffsetInTextInBlock() > + mSelection.StartOffsetInTextInBlock()) { + // 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! + nsresult rv = SplitElementAt(mSelection.StartIndex(), + entry->EndOffsetInTextInBlock() - + mSelection.StartOffsetInTextInBlock()); + if (NS_FAILED(rv)) { + NS_WARNING( + "entry->EndOffsetInTextInBlock() - " + "mSelection.StartOffsetInTextInBlock() was invalid for the " + "OffsetEntry"); + return rv; + } + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + UniquePtr& insertedTextEntry = *InsertElementAt( + mSelection.StartIndex() + 1, + MakeUnique(entry->mTextNode, + mSelection.StartOffsetInTextInBlock(), + aInsertedString.Length())); + LockOffsetEntryArrayLengthInDebugBuild(observer, *this); + insertedTextEntry->mIsInsertedText = true; + insertedTextEntry->mOffsetInTextNode = entry->EndOffsetInTextNode(); + MOZ_DIAGNOSTIC_ASSERT(mSelection.StartIndex() + 1 < Length()); + mSelection.SetIndex(mSelection.StartIndex() + 1); + } + + // We've just finished inserting an inserted text offset entry. + // update all entries with the same mTextNode pointer that follow + // it in the table! + + for (size_t i = mSelection.StartIndex() + 1; i < Length(); i++) { + const UniquePtr& entry = ElementAt(i); + LockOffsetEntryArrayLengthInDebugBuild(observer, *this); + if (entry->mTextNode != textNodeAtStartEntry) { + break; + } + if (entry->mIsValid) { + entry->mOffsetInTextNode += aInsertedString.Length(); + } + } + + return NS_OK; +} + +void TextServicesDocument::DidDeleteContent(const nsIContent& aChildContent) { + if (NS_WARN_IF(!mFilteredIter) || !aChildContent.IsText()) { + return; + } + + Maybe maybeNodeIndex = + mOffsetTable.FirstIndexOf(*aChildContent.AsText()); + if (maybeNodeIndex.isNothing()) { + // 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 == &aChildContent && + 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."); + } + + for (size_t nodeIndex = *maybeNodeIndex; nodeIndex < mOffsetTable.Length(); + nodeIndex++) { + const UniquePtr& entry = mOffsetTable[nodeIndex]; + LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable); + if (!entry) { + return; + } + + if (entry->mTextNode == &aChildContent) { + entry->mIsValid = false; + } + } +} + +void TextServicesDocument::DidJoinContents( + const EditorRawDOMPoint& aJoinedPoint, const nsIContent& aRemovedContent, + JoinNodesDirection aJoinNodesDirection) { + // Make sure that both nodes are text nodes -- otherwise we don't care. + if (!aJoinedPoint.IsInTextNode() || !aRemovedContent.IsText()) { + return; + } + + // Note: The editor merges the contents of the left node into the + // contents of the right. + + Maybe maybeRemovedIndex = + mOffsetTable.FirstIndexOf(*aRemovedContent.AsText()); + if (maybeRemovedIndex.isNothing()) { + // It's okay if the node isn't in the offset table, the + // editor could be cleaning house. + return; + } + + Maybe maybeJoinedIndex = + mOffsetTable.FirstIndexOf(*aJoinedPoint.ContainerAs()); + if (maybeJoinedIndex.isNothing()) { + // It's okay if the node isn't in the offset table, the + // editor could be cleaning house. + return; + } + + const size_t removedIndex = *maybeRemovedIndex; + const size_t joinedIndex = *maybeJoinedIndex; + + if (aJoinNodesDirection == JoinNodesDirection::LeftNodeIntoRightNode) { + if (MOZ_UNLIKELY(removedIndex > joinedIndex)) { + NS_ASSERTION(removedIndex < joinedIndex, "Indexes out of order."); + return; + } + NS_ASSERTION(mOffsetTable[joinedIndex]->mOffsetInTextNode == 0, + "Unexpected offset value for joinedIndex."); + } else { + if (MOZ_UNLIKELY(joinedIndex > removedIndex)) { + NS_ASSERTION(joinedIndex < removedIndex, "Indexes out of order."); + return; + } + NS_ASSERTION(mOffsetTable[removedIndex]->mOffsetInTextNode == 0, + "Unexpected offset value for rightIndex."); + } + + // Run through the table and change all entries referring to + // the removed node so that they now refer to the joined node, + // and adjust offsets if necessary. + const uint32_t movedTextDataLength = + aJoinNodesDirection == JoinNodesDirection::LeftNodeIntoRightNode + ? aJoinedPoint.Offset() + : aJoinedPoint.ContainerAs()->TextDataLength() - + aJoinedPoint.Offset(); + for (uint32_t i = removedIndex; i < mOffsetTable.Length(); i++) { + const UniquePtr& entry = mOffsetTable[i]; + LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable); + if (entry->mTextNode != aRemovedContent.AsText()) { + break; + } + if (entry->mIsValid) { + entry->mTextNode = aJoinedPoint.ContainerAs(); + if (aJoinNodesDirection == JoinNodesDirection::RightNodeIntoLeftNode) { + // The text was moved from aRemovedContent to end of the container of + // aJoinedPoint. + entry->mOffsetInTextNode += movedTextDataLength; + } + } + } + + if (aJoinNodesDirection == JoinNodesDirection::LeftNodeIntoRightNode) { + // The text was moved from aRemovedContent to start of the container of + // aJoinedPoint. + for (uint32_t i = joinedIndex; i < mOffsetTable.Length(); i++) { + const UniquePtr& entry = mOffsetTable[i]; + LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable); + if (entry->mTextNode != aJoinedPoint.ContainerAs()) { + break; + } + if (entry->mIsValid) { + entry->mOffsetInTextNode += movedTextDataLength; + } + } + } + + // Now check to see if the iterator is pointing to the + // left node. If it is, make it point to the joined node! + if (mFilteredIter->GetCurrentNode() == aRemovedContent.AsText()) { + mFilteredIter->PositionAt(aJoinedPoint.ContainerAs()); + } +} + +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 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 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 TextServicesDocument::CreateDocumentContentRange() { + nsCOMPtr node = GetDocumentContentRootNode(); + if (NS_WARN_IF(!node)) { + return nullptr; + } + + RefPtr range = nsRange::Create(node); + IgnoredErrorResult ignoredError; + range->SelectNodeContents(*node, ignoredError); + NS_WARNING_ASSERTION(!ignoredError.Failed(), "SelectNodeContents() failed"); + return range.forget(); +} + +already_AddRefed +TextServicesDocument::CreateDocumentContentRootToNodeOffsetRange( + nsINode* aParent, uint32_t aOffset, bool aToStart) { + if (NS_WARN_IF(!aParent)) { + return nullptr; + } + + nsCOMPtr bodyNode = GetDocumentContentRootNode(); + if (NS_WARN_IF(!bodyNode)) { + return nullptr; + } + + nsCOMPtr startNode; + nsCOMPtr 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 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 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 node = mFilteredIter->GetCurrentNode(); + NS_ENSURE_TRUE(node, NS_ERROR_FAILURE); + + Text* prevValidTextNode = nullptr; + Text* nextValidTextNode = nullptr; + bool foundEntry = false; + + const size_t tableLength = mOffsetTable.Length(); + for (size_t i = 0; i < tableLength && !nextValidTextNode; i++) { + UniquePtr& entry = mOffsetTable[i]; + LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable); + if (entry->mTextNode == 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) { + prevValidTextNode = entry->mTextNode; + } else { + nextValidTextNode = entry->mTextNode; + } + } + } + + Text* validTextNode = nullptr; + if (prevValidTextNode) { + validTextNode = prevValidTextNode; + } else if (nextValidTextNode) { + validTextNode = nextValidTextNode; + } + + if (validTextNode) { + nsresult rv = mFilteredIter->PositionAt(validTextNode); + 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::HasSameBlockNodeParent(Text& aTextNode1, + Text& aTextNode2) { + // XXX How about the case that both text nodes are orphan nodes? + if (aTextNode1.GetParent() == aTextNode2.GetParent()) { + return true; + } + + // I think that spellcheck should be available only in editable nodes. + // So, we also need to check whether they are in same editing host. + const Element* editableBlockElementOrInlineEditingHost1 = + HTMLEditUtils::GetAncestorElement( + aTextNode1, + HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost); + const Element* editableBlockElementOrInlineEditingHost2 = + HTMLEditUtils::GetAncestorElement( + aTextNode2, + HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost); + return editableBlockElementOrInlineEditingHost1 && + editableBlockElementOrInlineEditingHost1 == + editableBlockElementOrInlineEditingHost2; +} + +Result +TextServicesDocument::OffsetEntryArray::WillSetSelection( + uint32_t aOffsetInTextInBlock, uint32_t aLength) { + // Find start of selection in node offset terms: + EditorRawDOMPointInText newStart; + for (size_t i = 0; !newStart.IsSet() && i < Length(); i++) { + const UniquePtr& entry = ElementAt(i); + LockOffsetEntryArrayLengthInDebugBuild(observer, *this); + 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->mOffsetInTextInBlock == aOffsetInTextInBlock) { + newStart.Set(entry->mTextNode, entry->EndOffsetInTextNode()); + } + } else if (aOffsetInTextInBlock >= entry->mOffsetInTextInBlock) { + bool foundEntry = false; + if (aOffsetInTextInBlock < entry->EndOffsetInTextInBlock()) { + foundEntry = true; + } else if (aOffsetInTextInBlock == entry->EndOffsetInTextInBlock()) { + // Peek after this entry to see if we have any + // inserted text entries belonging to the same + // entry->mTextNode. If so, we have to place the selection + // after it! + if (i + 1 < Length()) { + const UniquePtr& nextEntry = ElementAt(i + 1); + LockOffsetEntryArrayLengthInDebugBuild(observer, *this); + if (!nextEntry->mIsValid || + nextEntry->mOffsetInTextInBlock != aOffsetInTextInBlock) { + // Next offset entry isn't an exact match, so we'll + // just use the current entry. + foundEntry = true; + } + } + } + + if (foundEntry) { + newStart.Set(entry->mTextNode, entry->mOffsetInTextNode + + aOffsetInTextInBlock - + entry->mOffsetInTextInBlock); + } + } + + if (newStart.IsSet()) { + MOZ_DIAGNOSTIC_ASSERT(i < Length()); + mSelection.Set(i, aOffsetInTextInBlock); + } + } + } + + if (NS_WARN_IF(!newStart.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + + if (!aLength) { + mSelection.CollapseToStart(); + return EditorRawDOMRangeInTexts(newStart); + } + + // Find the end of the selection in node offset terms: + EditorRawDOMPointInText newEnd; + const uint32_t endOffset = aOffsetInTextInBlock + aLength; + for (uint32_t i = Length(); !newEnd.IsSet() && i > 0; i--) { + const UniquePtr& entry = ElementAt(i - 1); + LockOffsetEntryArrayLengthInDebugBuild(observer, *this); + if (entry->mIsValid) { + if (entry->mIsInsertedText) { + if (entry->mOffsetInTextInBlock == + (newEnd.IsSet() ? newEnd.Offset() : 0)) { + // If the selection ends on an inserted text offset entry, + // the selection includes the entire entry! + newEnd.Set(entry->mTextNode, entry->EndOffsetInTextNode()); + } + } else if (entry->OffsetInTextInBlockIsInRangeOrEndOffset(endOffset)) { + newEnd.Set(entry->mTextNode, entry->mOffsetInTextNode + endOffset - + entry->mOffsetInTextInBlock); + } + + if (newEnd.IsSet()) { + MOZ_DIAGNOSTIC_ASSERT(mSelection.StartIndex() < Length()); + MOZ_DIAGNOSTIC_ASSERT(i - 1 < Length()); + mSelection.Set(mSelection.StartIndex(), i - 1, + mSelection.StartOffsetInTextInBlock(), endOffset); + } + } + } + + return newEnd.IsSet() ? EditorRawDOMRangeInTexts(newStart, newEnd) + : EditorRawDOMRangeInTexts(newStart); +} + +nsresult TextServicesDocument::SetSelectionInternal( + uint32_t aOffsetInTextInBlock, uint32_t aLength, bool aDoUpdate) { + if (NS_WARN_IF(!mSelCon)) { + return NS_ERROR_INVALID_ARG; + } + + Result newSelectionRange = + mOffsetTable.WillSetSelection(aOffsetInTextInBlock, aLength); + if (newSelectionRange.isErr()) { + NS_WARNING( + "TextServicesDocument::OffsetEntryArray::WillSetSelection() failed"); + return newSelectionRange.unwrapErr(); + } + + if (!aDoUpdate) { + return NS_OK; + } + + // XXX: If we ever get a SetSelection() method in nsIEditor, we should + // use it. + RefPtr selection = + mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL); + if (NS_WARN_IF(!selection)) { + return NS_ERROR_FAILURE; + } + + if (newSelectionRange.inspect().Collapsed()) { + nsresult rv = + selection->CollapseInLimiter(newSelectionRange.inspect().StartRef()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Selection::CollapseInLimiter() failed"); + return rv; + } + + ErrorResult error; + selection->SetStartAndEndInLimiter(newSelectionRange.inspect().StartRef(), + newSelectionRange.inspect().EndRef(), + error); + NS_WARNING_ASSERTION(!error.Failed(), + "Selection::SetStartAndEndInLimiter() failed"); + return error.StealNSResult(); +} + +nsresult TextServicesDocument::GetSelection(BlockSelectionStatus* aSelStatus, + uint32_t* aSelOffset, + uint32_t* aSelLength) { + NS_ENSURE_TRUE(aSelStatus && aSelOffset && aSelLength, NS_ERROR_NULL_POINTER); + + *aSelStatus = BlockSelectionStatus::eBlockNotFound; + *aSelOffset = UINT32_MAX; + *aSelLength = UINT32_MAX; + + NS_ENSURE_TRUE(mDocument && mSelCon, NS_ERROR_FAILURE); + + if (mIteratorStatus == IteratorStatus::eDone) { + return NS_OK; + } + + RefPtr selection = + mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + if (selection->IsCollapsed()) { + return GetCollapsedSelection(aSelStatus, aSelOffset, aSelLength); + } + + return GetUncollapsedSelection(aSelStatus, aSelOffset, aSelLength); +} + +nsresult TextServicesDocument::GetCollapsedSelection( + BlockSelectionStatus* aSelStatus, uint32_t* aSelOffset, + uint32_t* aSelLength) { + RefPtr 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 = UINT32_MAX; + + const uint32_t tableCount = mOffsetTable.Length(); + if (!tableCount) { + return NS_OK; + } + + // Get pointers to the first and last offset entries + // in the table. + + UniquePtr& eStart = mOffsetTable[0]; + UniquePtr& eEnd = + tableCount > 1 ? mOffsetTable[tableCount - 1] : eStart; + LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable); + + const uint32_t eStartOffset = eStart->mOffsetInTextNode; + const uint32_t eEndOffset = eEnd->EndOffsetInTextNode(); + + RefPtr range = selection->GetRangeAt(0); + NS_ENSURE_STATE(range); + + nsCOMPtr parent = range->GetStartContainer(); + MOZ_ASSERT(parent); + + uint32_t offset = range->StartOffset(); + + const Maybe e1s1 = nsContentUtils::ComparePoints( + eStart->mTextNode, eStartOffset, parent, offset); + const Maybe e2s1 = nsContentUtils::ComparePoints( + eEnd->mTextNode, eEndOffset, parent, offset); + + if (MOZ_UNLIKELY(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->IsText()) { + // Good news, the caret is in a text node. Look + // through the offset table for the entry that + // matches its parent and offset. + + for (uint32_t i = 0; i < tableCount; i++) { + const UniquePtr& entry = mOffsetTable[i]; + LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable); + if (entry->mTextNode == parent->AsText() && + entry->OffsetInTextNodeIsInRangeOrEndOffset(offset)) { + *aSelStatus = BlockSelectionStatus::eBlockContains; + *aSelOffset = + entry->mOffsetInTextInBlock + (offset - entry->mOffsetInTextNode); + *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. ). 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->mTextNode, eStartOffset, eEnd->mTextNode, + eEndOffset, IgnoreErrors()); + if (NS_WARN_IF(!range)) { + return NS_ERROR_FAILURE; + } + + RefPtr 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); + + nsresult 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 content = parent->AsContent(); + + nsresult 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. + + Text* textNode = nullptr; + for (; !filteredIter->IsDone(); filteredIter->Prev()) { + nsINode* current = filteredIter->GetCurrentNode(); + if (current->IsText()) { + textNode = current->AsText(); + break; + } + } + + if (textNode) { + // We found a node, now set the offset to the end + // of the text node. + offset = textNode->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. + + nsresult rv = filteredIter->PositionAt(saveNode); + NS_ENSURE_SUCCESS(rv, rv); + + textNode = nullptr; + for (; !filteredIter->IsDone(); filteredIter->Next()) { + nsINode* current = filteredIter->GetCurrentNode(); + if (current->IsText()) { + textNode = current->AsText(); + break; + } + } + NS_ENSURE_TRUE(textNode, NS_ERROR_FAILURE); + + // We found a text node, so set the offset to + // the beginning of the node. + offset = 0; + } + + for (size_t i = 0; i < tableCount; i++) { + const UniquePtr& entry = mOffsetTable[i]; + LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable); + if (entry->mTextNode == textNode && + entry->OffsetInTextNodeIsInRangeOrEndOffset(offset)) { + *aSelStatus = BlockSelectionStatus::eBlockContains; + *aSelOffset = + entry->mOffsetInTextInBlock + (offset - entry->mOffsetInTextNode); + *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, uint32_t* aSelOffset, + uint32_t* aSelLength) { + RefPtr range; + RefPtr 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 startContainer, endContainer; + + const size_t tableCount = mOffsetTable.Length(); + + // Get pointers to the first and last offset entries + // in the table. + + UniquePtr& eStart = mOffsetTable[0]; + UniquePtr& eEnd = + tableCount > 1 ? mOffsetTable[tableCount - 1] : eStart; + LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable); + + const uint32_t eStartOffset = eStart->mOffsetInTextNode; + const uint32_t eEndOffset = eEnd->EndOffsetInTextNode(); + + const uint32_t rangeCount = selection->RangeCount(); + MOZ_ASSERT(rangeCount); + + // Find the first range in the selection that intersects + // the current text block. + Maybe e1s2; + Maybe e2s1; + uint32_t startOffset, endOffset; + for (const uint32_t i : IntegerRange(rangeCount)) { + MOZ_ASSERT(selection->RangeCount() == rangeCount); + range = selection->GetRangeAt(i); + if (MOZ_UNLIKELY(NS_WARN_IF(!range))) { + return NS_ERROR_FAILURE; + } + + nsresult rv = + GetRangeEndPoints(range, getter_AddRefs(startContainer), &startOffset, + getter_AddRefs(endContainer), &endOffset); + + NS_ENSURE_SUCCESS(rv, rv); + + e1s2 = nsContentUtils::ComparePoints(eStart->mTextNode, eStartOffset, + endContainer, endOffset); + if (NS_WARN_IF(!e1s2)) { + return NS_ERROR_FAILURE; + } + + e2s1 = nsContentUtils::ComparePoints(eEnd->mTextNode, 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 = UINT32_MAX; + return NS_OK; + } + + // Now that we have an intersecting range, find out more info: + const Maybe e1s1 = nsContentUtils::ComparePoints( + eStart->mTextNode, eStartOffset, startContainer, startOffset); + if (NS_WARN_IF(!e1s1)) { + return NS_ERROR_FAILURE; + } + + const Maybe e2s2 = nsContentUtils::ComparePoints( + eEnd->mTextNode, 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 p1, p2; + uint32_t o1, o2; + + // The start of the range will be the rightmost + // start node. + + if (*e1s1 >= 0) { + p1 = eStart->mTextNode; + o1 = eStartOffset; + } else { + p1 = startContainer; + o1 = startOffset; + } + + // The end of the range will be the leftmost + // end node. + + if (*e2s2 <= 0) { + p2 = eEnd->mTextNode; + 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 filteredIter; + nsresult rv = + CreateFilteredContentIterator(range, getter_AddRefs(filteredIter)); + NS_ENSURE_SUCCESS(rv, rv); + + // Find the first text node in the range. + nsCOMPtr content; + filteredIter->First(); + if (!p1->IsText()) { + bool found = false; + for (; !filteredIter->IsDone(); filteredIter->Next()) { + nsINode* node = filteredIter->GetCurrentNode(); + if (node->IsText()) { + p1 = node->AsText(); + 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()) { + bool found = false; + for (; !filteredIter->IsDone(); filteredIter->Prev()) { + nsINode* node = filteredIter->GetCurrentNode(); + if (node->IsText()) { + p2 = node->AsText(); + o2 = p2->AsText()->Length(); + found = true; + + break; + } + } + NS_ENSURE_TRUE(found, NS_ERROR_FAILURE); + } + + bool found = false; + *aSelLength = 0; + + for (size_t i = 0; i < tableCount; i++) { + const UniquePtr& entry = mOffsetTable[i]; + LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable); + if (!found) { + if (entry->mTextNode == p1.get() && + entry->OffsetInTextNodeIsInRangeOrEndOffset(o1)) { + *aSelOffset = + entry->mOffsetInTextInBlock + (o1 - entry->mOffsetInTextNode); + if (p1 == p2 && entry->OffsetInTextNodeIsInRangeOrEndOffset(o2)) { + // 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->EndOffsetInTextNode() - o1; + found = true; + } + } else { // Found. + if (entry->mTextNode == p2.get() && + entry->OffsetInTextNodeIsInRangeOrEndOffset(o2)) { + // 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->mOffsetInTextNode; + break; + } + // The entire entry must be in the range. + *aSelLength += entry->mLength; + } + } + + return NS_OK; +} + +// static +nsresult TextServicesDocument::GetRangeEndPoints( + const AbstractRange* aAbstractRange, nsINode** aStartContainer, + uint32_t* aStartOffset, nsINode** aEndContainer, uint32_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 startContainer = aAbstractRange->GetStartContainer(); + if (NS_WARN_IF(!startContainer)) { + return NS_ERROR_FAILURE; + } + nsCOMPtr endContainer = aAbstractRange->GetEndContainer(); + if (NS_WARN_IF(!endContainer)) { + return NS_ERROR_FAILURE; + } + + startContainer.forget(aStartContainer); + endContainer.forget(aEndContainer); + *aStartOffset = aAbstractRange->StartOffset(); + *aEndOffset = 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); + + // Walk backwards over adjacent text nodes until + // we hit a block boundary: + RefPtr lastTextNode; + while (!aFilteredIter->IsDone()) { + nsCOMPtr content = + aFilteredIter->GetCurrentNode()->IsContent() + ? aFilteredIter->GetCurrentNode()->AsContent() + : nullptr; + if (lastTextNode && content && + (HTMLEditUtils::IsBlockElement(*content) || + content->IsHTMLElement(nsGkAtoms::br))) { + break; + } + if (content && content->IsText()) { + if (lastTextNode && !TextServicesDocument::HasSameBlockNodeParent( + *content->AsText(), *lastTextNode)) { + // We're done, the current text node is in a + // different block. + break; + } + lastTextNode = content->AsText(); + } + + aFilteredIter->Prev(); + + if (DidSkip(aFilteredIter)) { + break; + } + } + + if (lastTextNode) { + aFilteredIter->PositionAt(lastTextNode); + } + + // 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) { + bool crossedBlockBoundary = false; + + NS_ENSURE_TRUE(aFilteredIter, NS_ERROR_NULL_POINTER); + + ClearDidSkip(aFilteredIter); + + RefPtr previousTextNode; + while (!aFilteredIter->IsDone()) { + if (nsCOMPtr content = + aFilteredIter->GetCurrentNode()->IsContent() + ? aFilteredIter->GetCurrentNode()->AsContent() + : nullptr) { + if (content->IsText()) { + if (crossedBlockBoundary || + (previousTextNode && !TextServicesDocument::HasSameBlockNodeParent( + *previousTextNode, *content->AsText()))) { + break; + } + previousTextNode = content->AsText(); + } else if (!crossedBlockBoundary && + (HTMLEditUtils::IsBlockElement(*content) || + content->IsHTMLElement(nsGkAtoms::br))) { + 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 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 current = + mFilteredIter->GetCurrentNode()->IsContent() + ? mFilteredIter->GetCurrentNode()->AsContent() + : nullptr; + current.forget(aContent); + } + + // Restore the iterator: + return mFilteredIter->PositionAt(node); +} + +Result +TextServicesDocument::OffsetEntryArray::Init( + FilteredContentIterator& aFilteredIter, IteratorStatus aIteratorStatus, + nsRange* aIterRange, nsAString* aAllTextInBlock /* = nullptr */) { + Clear(); + + if (aAllTextInBlock) { + aAllTextInBlock->Truncate(); + } + + if (aIteratorStatus == IteratorStatus::eDone) { + return IteratorStatus::eDone; + } + + // 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 rngStartNode, rngEndNode; + uint32_t rngStartOffset = 0, rngEndOffset = 0; + if (aIterRange) { + nsresult rv = TextServicesDocument::GetRangeEndPoints( + aIterRange, getter_AddRefs(rngStartNode), &rngStartOffset, + getter_AddRefs(rngEndNode), &rngEndOffset); + if (NS_FAILED(rv)) { + NS_WARNING("TextServicesDocument::GetRangeEndPoints() failed"); + return Err(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 = + TextServicesDocument::FirstTextNodeInCurrentBlock(&aFilteredIter); + if (NS_FAILED(rv)) { + NS_WARNING("TextServicesDocument::FirstTextNodeInCurrentBlock() failed"); + return Err(rv); + } + + TextServicesDocument::ClearDidSkip(&aFilteredIter); + + uint32_t offset = 0; + RefPtr firstTextNode, previousTextNode; + while (!aFilteredIter.IsDone()) { + if (nsCOMPtr content = + aFilteredIter.GetCurrentNode()->IsContent() + ? aFilteredIter.GetCurrentNode()->AsContent() + : nullptr) { + if (HTMLEditUtils::IsBlockElement(*content) || + content->IsHTMLElement(nsGkAtoms::br)) { + break; + } + if (content->IsText()) { + if (previousTextNode && !TextServicesDocument::HasSameBlockNodeParent( + *previousTextNode, *content->AsText())) { + break; + } + + nsString str; + content->AsText()->GetNodeValue(str); + + // Add an entry for this text node into the offset table: + + UniquePtr& entry = *AppendElement( + MakeUnique(*content->AsText(), offset, str.Length())); + LockOffsetEntryArrayLengthInDebugBuild(observer, *this); + + // 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. + + uint32_t startOffset = 0; + uint32_t endOffset = str.Length(); + bool adjustStr = false; + + if (entry->mTextNode == rngStartNode) { + entry->mOffsetInTextNode = startOffset = rngStartOffset; + adjustStr = true; + } + + if (entry->mTextNode == rngEndNode) { + endOffset = rngEndOffset; + adjustStr = true; + } + + if (adjustStr) { + entry->mLength = endOffset - startOffset; + str = Substring(str, startOffset, entry->mLength); + } + + offset += str.Length(); + + if (aAllTextInBlock) { + // Append the text node's string to the output string: + if (!firstTextNode) { + *aAllTextInBlock = str; + } else { + *aAllTextInBlock += str; + } + } + + previousTextNode = content->AsText(); + + if (!firstTextNode) { + firstTextNode = content->AsText(); + } + } + } + + aFilteredIter.Next(); + + if (TextServicesDocument::DidSkip(&aFilteredIter)) { + break; + } + } + + if (firstTextNode) { + // Always leave the iterator pointing at the first + // text node of the current block! + aFilteredIter.PositionAt(firstTextNode); + return aIteratorStatus; + } + + // If we never ran across a text node, the iterator + // might have been pointing to something invalid to + // begin with. + return IteratorStatus::eDone; +} + +void TextServicesDocument::OffsetEntryArray::RemoveInvalidElements() { + for (size_t i = 0; i < Length();) { + if (ElementAt(i)->mIsValid) { + i++; + continue; + } + + RemoveElementAt(i); + if (!mSelection.IsSet()) { + continue; + } + if (mSelection.StartIndex() == i) { + NS_ASSERTION(false, "What should we do in this case?"); + mSelection.Reset(); + } else if (mSelection.StartIndex() > i) { + MOZ_DIAGNOSTIC_ASSERT(mSelection.StartIndex() - 1 < Length()); + MOZ_DIAGNOSTIC_ASSERT(mSelection.EndIndex() - 1 < Length()); + mSelection.SetIndexes(mSelection.StartIndex() - 1, + mSelection.EndIndex() - 1); + } else if (mSelection.EndIndex() >= i) { + MOZ_DIAGNOSTIC_ASSERT(mSelection.EndIndex() - 1 < Length()); + mSelection.SetIndexes(mSelection.StartIndex(), mSelection.EndIndex() - 1); + } + } +} + +nsresult TextServicesDocument::OffsetEntryArray::SplitElementAt( + size_t aIndex, uint32_t aOffsetInTextNode) { + OffsetEntry* leftEntry = ElementAt(aIndex).get(); + MOZ_ASSERT(leftEntry); + NS_ASSERTION((aOffsetInTextNode > 0), "aOffsetInTextNode == 0"); + NS_ASSERTION((aOffsetInTextNode < leftEntry->mLength), + "aOffsetInTextNode >= mLength"); + + if (aOffsetInTextNode < 1 || aOffsetInTextNode >= leftEntry->mLength) { + return NS_ERROR_FAILURE; + } + + const uint32_t oldLength = leftEntry->mLength - aOffsetInTextNode; + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + UniquePtr& rightEntry = *InsertElementAt( + aIndex + 1, + MakeUnique(leftEntry->mTextNode, + leftEntry->mOffsetInTextInBlock + oldLength, + aOffsetInTextNode)); + LockOffsetEntryArrayLengthInDebugBuild(observer, *this); + leftEntry->mLength = oldLength; + rightEntry->mOffsetInTextNode = leftEntry->mOffsetInTextNode + oldLength; + + return NS_OK; +} + +Maybe TextServicesDocument::OffsetEntryArray::FirstIndexOf( + const Text& aTextNode) const { + for (size_t i = 0; i < Length(); i++) { + if (ElementAt(i)->mTextNode == &aTextNode) { + return Some(i); + } + } + return Nothing(); +} + +// Spellchecker code has this. See bug 211343 +#define IS_NBSP_CHAR(c) (((unsigned char)0xa0) == (c)) + +Result +TextServicesDocument::OffsetEntryArray::FindWordRange( + nsAString& aAllTextInBlock, const EditorRawDOMPoint& aStartPointToScan) { + MOZ_ASSERT(aStartPointToScan.IsInTextNode()); + // 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. + Maybe maybeEntryIndex = + FirstIndexOf(*aStartPointToScan.ContainerAs()); + if (NS_WARN_IF(maybeEntryIndex.isNothing())) { + NS_WARNING( + "TextServicesDocument::OffsetEntryArray::FirstIndexOf() didn't find " + "entries"); + return Err(NS_ERROR_FAILURE); + } + + // Next we map offset into a string offset. + + const UniquePtr& entry = ElementAt(*maybeEntryIndex); + LockOffsetEntryArrayLengthInDebugBuild(observer, *this); + uint32_t strOffset = entry->mOffsetInTextInBlock + + aStartPointToScan.Offset() - entry->mOffsetInTextNode; + + // Now we use the word breaker to find the beginning and end + // of the word from our calculated string offset. + + const char16_t* str = aAllTextInBlock.BeginReading(); + uint32_t strLen = aAllTextInBlock.Length(); + MOZ_ASSERT(strOffset <= strLen, + "The string offset shouldn't be greater than the string length!"); + + intl::WordRange res = intl::WordBreaker::FindWord(str, strLen, strOffset); + + // Strip out the NBSPs at the ends + while (res.mBegin <= res.mEnd && IS_NBSP_CHAR(str[res.mBegin])) { + res.mBegin++; + } + if (str[res.mEnd] == static_cast(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. + + EditorDOMPointInText wordStart, wordEnd; + size_t lastIndex = Length() - 1; + for (size_t i = 0; i <= lastIndex; i++) { + // 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. + const UniquePtr& entry = ElementAt(i); + LockOffsetEntryArrayLengthInDebugBuild(observer, *this); + if (entry->mOffsetInTextInBlock <= res.mBegin && + (res.mBegin < entry->EndOffsetInTextInBlock() || + (res.mBegin == entry->EndOffsetInTextInBlock() && i == lastIndex))) { + wordStart.Set(entry->mTextNode, entry->mOffsetInTextNode + res.mBegin - + entry->mOffsetInTextInBlock); + } + + // Check to see if res.mEnd is within the range covered + // by this entry. + if (entry->mOffsetInTextInBlock <= res.mEnd && + res.mEnd <= entry->EndOffsetInTextInBlock()) { + if (res.mBegin == res.mEnd && + res.mEnd == entry->EndOffsetInTextInBlock() && i != lastIndex) { + // Wait for the next round so that we use the same entry + // we did for aWordStartNode. + continue; + } + + wordEnd.Set(entry->mTextNode, entry->mOffsetInTextNode + res.mEnd - + entry->mOffsetInTextInBlock); + break; + } + } + + return EditorDOMRangeInTexts(wordStart, wordEnd); +} + +/** + * 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)) || NS_WARN_IF(!aChild) || + !aChild->IsContent()) { + return NS_OK; + } + DidDeleteContent(*aChild->AsContent()); + return NS_OK; +} + +NS_IMETHODIMP TextServicesDocument::DidJoinContents( + const EditorRawDOMPoint& aJoinedPoint, const nsINode* aRemovedNode, + bool aLeftNodeWasRemoved) { + if (MOZ_UNLIKELY(NS_WARN_IF(!aJoinedPoint.IsSetAndValid()) || + NS_WARN_IF(!aRemovedNode->IsContent()))) { + return NS_OK; + } + DidJoinContents(aJoinedPoint, *aRemovedNode->AsContent(), + aLeftNodeWasRemoved + ? JoinNodesDirection::LeftNodeIntoRightNode + : JoinNodesDirection::RightNodeIntoLeftNode); + 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>& aRangesToDelete) { + return NS_OK; +} + +#undef LockOffsetEntryArrayLengthInDebugBuild + +} // namespace mozilla diff --git a/editor/spellchecker/TextServicesDocument.h b/editor/spellchecker/TextServicesDocument.h new file mode 100644 index 0000000000..7bc6aad7d5 --- /dev/null +++ b/editor/spellchecker/TextServicesDocument.h @@ -0,0 +1,438 @@ +/* -*- 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 "mozilla/Maybe.h" +#include "mozilla/UniquePtr.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 EditorBase; +class FilteredContentIterator; +class OffsetEntry; +enum class JoinNodesDirection; // Declared in HTMLEditHelpers.h + +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, + }; + + class OffsetEntryArray final : public nsTArray> { + public: + /** + * Init() initializes this array with aFilteredIter. + * + * @param[in] aIterRange Can be nullptr. + * @param[out] aAllTextInBlock + * Returns all text in the block. + */ + Result Init( + FilteredContentIterator& aFilteredIter, IteratorStatus aIteratorStatus, + nsRange* aIterRange, nsAString* aAllTextInBlock = nullptr); + + /** + * Returns index of first `OffsetEntry` which manages aTextNode. + */ + Maybe FirstIndexOf(const dom::Text& aTextNode) const; + + /** + * FindWordRange() returns a word range starting from aStartPointToScan + * in aAllTextInBlock. + */ + Result FindWordRange( + nsAString& aAllTextInBlock, const EditorRawDOMPoint& aStartPointToScan); + + /** + * SplitElementAt() splits an `OffsetEntry` at aIndex if aOffsetInTextNode + * is middle of the range in the text node. + * + * @param aIndex Index of the entry which you want to split. + * @param aOffsetInTextNode + * Offset in the text node. I.e., the offset should be + * greater than 0 and less than `mLength`. + */ + nsresult SplitElementAt(size_t aIndex, uint32_t aOffsetInTextNode); + + /** + * Remove all `OffsetEntry` elements whose `mIsValid` is set to false. + */ + void RemoveInvalidElements(); + + /** + * Called when non-collapsed selection will be deleted. + */ + nsresult WillDeleteSelection(); + + /** + * Called when non-collapsed selection is deleteded. + */ + OffsetEntry* DidDeleteSelection(); + + /** + * Called when aInsertedText is inserted. + */ + MOZ_CAN_RUN_SCRIPT nsresult DidInsertText(dom::Selection* aSelection, + const nsAString& aInsertedString); + + /** + * Called when selection range will be applied to the DOM Selection. + */ + Result WillSetSelection( + uint32_t aOffsetInTextInBlock, uint32_t aLength); + + class Selection final { + public: + size_t StartIndex() const { + MOZ_ASSERT(IsIndexesSet()); + return *mStartIndex; + } + size_t EndIndex() const { + MOZ_ASSERT(IsIndexesSet()); + return *mEndIndex; + } + + uint32_t StartOffsetInTextInBlock() const { + MOZ_ASSERT(IsSet()); + return *mStartOffsetInTextInBlock; + } + uint32_t EndOffsetInTextInBlock() const { + MOZ_ASSERT(IsSet()); + return *mEndOffsetInTextInBlock; + } + uint32_t LengthInTextInBlock() const { + MOZ_ASSERT(IsSet()); + return *mEndOffsetInTextInBlock - *mStartOffsetInTextInBlock; + } + + bool IsCollapsed() { + return !IsSet() || (IsInSameElement() && StartOffsetInTextInBlock() == + EndOffsetInTextInBlock()); + } + + bool IsIndexesSet() const { + return mStartIndex.isSome() && mEndIndex.isSome(); + } + bool IsSet() const { + return IsIndexesSet() && mStartOffsetInTextInBlock.isSome() && + mEndOffsetInTextInBlock.isSome(); + } + bool IsInSameElement() const { + return IsIndexesSet() && StartIndex() == EndIndex(); + } + + void Reset() { + mStartIndex.reset(); + mEndIndex.reset(); + mStartOffsetInTextInBlock.reset(); + mEndOffsetInTextInBlock.reset(); + } + void SetIndex(size_t aIndex) { mEndIndex = mStartIndex = Some(aIndex); } + void Set(size_t aIndex, uint32_t aOffsetInTextInBlock) { + mEndIndex = mStartIndex = Some(aIndex); + mStartOffsetInTextInBlock = mEndOffsetInTextInBlock = + Some(aOffsetInTextInBlock); + } + void SetIndexes(size_t aStartIndex, size_t aEndIndex) { + MOZ_DIAGNOSTIC_ASSERT(aStartIndex <= aEndIndex); + mStartIndex = Some(aStartIndex); + mEndIndex = Some(aEndIndex); + } + void Set(size_t aStartIndex, size_t aEndIndex, + uint32_t aStartOffsetInTextInBlock, + uint32_t aEndOffsetInTextInBlock) { + MOZ_DIAGNOSTIC_ASSERT(aStartIndex <= aEndIndex); + mStartIndex = Some(aStartIndex); + mEndIndex = Some(aEndIndex); + mStartOffsetInTextInBlock = Some(aStartOffsetInTextInBlock); + mEndOffsetInTextInBlock = Some(aEndOffsetInTextInBlock); + } + + void CollapseToStart() { + MOZ_ASSERT(mStartIndex.isSome()); + MOZ_ASSERT(mStartOffsetInTextInBlock.isSome()); + mEndIndex = mStartIndex; + mEndOffsetInTextInBlock = mStartOffsetInTextInBlock; + } + + private: + Maybe mStartIndex; + Maybe mEndIndex; + // Selected start and end offset in all text in a block element. + Maybe mStartOffsetInTextInBlock; + Maybe mEndOffsetInTextInBlock; + }; + Selection mSelection; + }; + + RefPtr mDocument; + nsCOMPtr mSelCon; + RefPtr mEditorBase; + RefPtr mFilteredIter; + nsCOMPtr mPrevTextBlock; + nsCOMPtr mNextTextBlock; + OffsetEntryArray mOffsetTable; + RefPtr mExtent; + + uint32_t mTxtSvcFilterType; + IteratorStatus mIteratorStatus; + + protected: + virtual ~TextServicesDocument() = default; + + 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, + uint32_t* aSelOffset, uint32_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(uint32_t aOffset, uint32_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 DidDeleteContent(const nsIContent& aChildContent); + void DidJoinContents(const EditorRawDOMPoint& aJoinedPoint, + const nsIContent& aRemovedContent, + JoinNodesDirection aJoinNodesDirection); + + private: + // 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, + uint32_t* aStartOffset, + nsINode** aEndContainer, + uint32_t* aEndOffset); + + nsresult CreateFilteredContentIterator( + const dom::AbstractRange* aAbstractRange, + FilteredContentIterator** aFilteredIter); + + dom::Element* GetDocumentContentRootNode() const; + already_AddRefed CreateDocumentContentRange(); + already_AddRefed 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 DidSkip(FilteredContentIterator* aFilteredIter); + static void ClearDidSkip(FilteredContentIterator* aFilteredIter); + + static bool HasSameBlockNodeParent(dom::Text& aTextNode1, + dom::Text& aTextNode2); + + MOZ_CAN_RUN_SCRIPT nsresult SetSelectionInternal(uint32_t aOffset, + uint32_t aLength, + bool aDoUpdate); + MOZ_CAN_RUN_SCRIPT nsresult GetSelection(BlockSelectionStatus* aSelStatus, + uint32_t* aSelOffset, + uint32_t* aSelLength); + MOZ_CAN_RUN_SCRIPT nsresult + GetCollapsedSelection(BlockSelectionStatus* aSelStatus, uint32_t* aSelOffset, + uint32_t* aSelLength); + nsresult GetUncollapsedSelection(BlockSelectionStatus* aSelStatus, + uint32_t* aSelOffset, uint32_t* aSelLength); +}; + +} // 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..8e4932c209 --- /dev/null +++ b/editor/spellchecker/moz.build @@ -0,0 +1,34 @@ +# -*- 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"] + +MOCHITEST_CHROME_MANIFESTS += ["tests/chrome.ini"] + +XPIDL_SOURCES += [ + "nsIInlineSpellChecker.idl", +] + +XPIDL_MODULE = "txtsvc" + +EXPORTS.mozilla += [ + "EditorSpellCheck.h", + "TextServicesDocument.h", +] + +UNIFIED_SOURCES += [ + "EditorSpellCheck.cpp", + "FilteredContentIterator.cpp", + "nsComposeTxtSrvFilter.cpp", + "TextServicesDocument.cpp", +] + +LOCAL_INCLUDES += [ + # For stop exposing libeditor's headers, allow to refer them directly + "/editor/libeditor", +] + +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::CreateHelper( + bool aIsForMail) { + auto filter = MakeUnique(); + 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 CreateNormalFilter() { + return CreateHelper(false); + } + static mozilla::UniquePtr 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 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..d6f66a793d --- /dev/null +++ b/editor/spellchecker/nsIInlineSpellChecker.idl @@ -0,0 +1,40 @@ +/* -*- 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 unsigned long aOffset); + [can_run_script] + void replaceWord(in Node aNode, + in unsigned long aOffset, + in AString aNewword); + void addWordToDictionary(in AString aWord); + void removeWordFromDictionary(in AString aWord); + + void ignoreWord(in AString aWord); + void ignoreWords(in Array aWordsToIgnore); + void updateCurrentDictionary(); + + readonly attribute boolean spellCheckPending; +}; 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 @@ + + + + + + + + + + + + + + + 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 @@ + + + + + + + + +
the presence of this div triggers the faulty code path
+ + 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 @@ + + + + + + + + + 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 @@ + + + + + + + + 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 @@ + + + + + + + + diff --git a/editor/spellchecker/tests/chrome.ini b/editor/spellchecker/tests/chrome.ini new file mode 100644 index 0000000000..76faf04121 --- /dev/null +++ b/editor/spellchecker/tests/chrome.ini @@ -0,0 +1,9 @@ +[DEFAULT] +prefs = + gfx.font_loader.delay=0 + +skip-if = os == 'android' + +[test_nsIEditorSpellCheck_ReplaceWord.html] +support-files = + spellcheck.js 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..6d3c53d71b --- /dev/null +++ b/editor/spellchecker/tests/mochitest.ini @@ -0,0 +1,68 @@ +[DEFAULT] +prefs = + gfx.font_loader.delay=0 + +skip-if = os == 'android' +support-files = + 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 + ru-RU/ru_RU.dic + ru-RU/ru_RU.aff + spellcheck.js + +[test_async_UpdateCurrentDictionary.html] +[test_bug1100966.html] +[test_bug1154791.html] +[test_bug1200533.html] +skip-if = + http3 +support-files = + bug1200533_subframe.html +[test_bug1204147.html] +skip-if = + http3 +support-files = + bug1204147_subframe.html + bug1204147_subframe2.html +[test_bug1205983.html] +[test_bug1209414.html] +[test_bug1219928.html] +skip-if = true +[test_bug1365383.html] +[test_bug1368544.html] +[test_bug1402822.html] +[test_bug1418629.html] +[test_bug1497480.html] +[test_bug1602526.html] +[test_bug1761273.html] +[test_bug1773802.html] +[test_bug1837268.html] +[test_bug366682.html] +[test_bug432225.html] +[test_bug484181.html] +[test_bug596333.html] +[test_bug636465.html] +[test_bug678842.html] +skip-if = + http3 +support-files = + bug678842_subframe.html +[test_bug697981.html] +[test_bug717433.html] +skip-if = + http3 +support-files = + bug717433_subframe.html +[test_multiple_content_languages.html] +skip-if = + http3 +support-files = + multiple_content_languages_subframe.html +[test_spellcheck_after_edit.html] +[test_spellcheck_after_pressing_navigation_key.html] +[test_spellcheck_selection.html] +[test_suggest.html] diff --git a/editor/spellchecker/tests/multiple_content_languages_subframe.html b/editor/spellchecker/tests/multiple_content_languages_subframe.html new file mode 100644 index 0000000000..da6a4ed664 --- /dev/null +++ b/editor/spellchecker/tests/multiple_content_languages_subframe.html @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/editor/spellchecker/tests/ru-RU/ru_RU.aff b/editor/spellchecker/tests/ru-RU/ru_RU.aff new file mode 100644 index 0000000000..c6cf721484 --- /dev/null +++ b/editor/spellchecker/tests/ru-RU/ru_RU.aff @@ -0,0 +1 @@ +SET KOI8-R diff --git a/editor/spellchecker/tests/ru-RU/ru_RU.dic b/editor/spellchecker/tests/ru-RU/ru_RU.dic new file mode 100644 index 0000000000..dbfe3a9f23 --- /dev/null +++ b/editor/spellchecker/tests/ru-RU/ru_RU.dic @@ -0,0 +1,2 @@ +1 +ÐÒÁ×ÉÌØÎÙÊ diff --git a/editor/spellchecker/tests/spellcheck.js b/editor/spellchecker/tests/spellcheck.js new file mode 100644 index 0000000000..b6a1586628 --- /dev/null +++ b/editor/spellchecker/tests/spellcheck.js @@ -0,0 +1,36 @@ +function isSpellingCheckOk(aEditor, aMisspelledWords, aTodo = false) { + var selcon = aEditor.selectionController; + var sel = selcon.getSelection(selcon.SELECTION_SPELLCHECK); + var numWords = sel.rangeCount; + + if (aTodo) { + todo_is( + numWords, + aMisspelledWords.length, + "Correct number of misspellings and words." + ); + } else { + is( + numWords, + aMisspelledWords.length, + "Correct number of misspellings and words." + ); + } + + if (numWords !== aMisspelledWords.length) { + return false; + } + + for (var i = 0; i < numWords; ++i) { + var word = String(sel.getRangeAt(i)); + if (aTodo) { + todo_is(word, aMisspelledWords[i], "Misspelling is what we think it is."); + } else { + is(word, aMisspelledWords[i], "Misspelling is what we think it is."); + } + if (word !== aMisspelledWords[i]) { + return false; + } + } + return true; +} diff --git a/editor/spellchecker/tests/test_async_UpdateCurrentDictionary.html b/editor/spellchecker/tests/test_async_UpdateCurrentDictionary.html new file mode 100644 index 0000000000..6f8371d2cd --- /dev/null +++ b/editor/spellchecker/tests/test_async_UpdateCurrentDictionary.html @@ -0,0 +1,60 @@ + + + + + Test for Bug 856270 - Async UpdateCurrentDictionary + + + + +Mozilla Bug 856270 +

+
+ +
+
+
+
+ + diff --git a/editor/spellchecker/tests/test_bug1100966.html b/editor/spellchecker/tests/test_bug1100966.html new file mode 100644 index 0000000000..4ed52968a0 --- /dev/null +++ b/editor/spellchecker/tests/test_bug1100966.html @@ -0,0 +1,74 @@ + + + + + Test for Bug 1100966 + + + + + +
+
+
+=====
+correct
+fivee sixx
+==== +
+
+
+ + + + + diff --git a/editor/spellchecker/tests/test_bug1154791.html b/editor/spellchecker/tests/test_bug1154791.html new file mode 100644 index 0000000000..7ed6c53e35 --- /dev/null +++ b/editor/spellchecker/tests/test_bug1154791.html @@ -0,0 +1,74 @@ + + + + + Test for Bug 1154791 + + + + + +
+
+ +
+thiss onee is stilll a +
+ +
+
+ + + + + diff --git a/editor/spellchecker/tests/test_bug1200533.html b/editor/spellchecker/tests/test_bug1200533.html new file mode 100644 index 0000000000..d97dea8ccd --- /dev/null +++ b/editor/spellchecker/tests/test_bug1200533.html @@ -0,0 +1,163 @@ + + + + + Test for Bug 1200533 + + + + +Mozilla Bug 1200533 +

+ + + +
+
+
+ + diff --git a/editor/spellchecker/tests/test_bug1204147.html b/editor/spellchecker/tests/test_bug1204147.html new file mode 100644 index 0000000000..3d5f0e120f --- /dev/null +++ b/editor/spellchecker/tests/test_bug1204147.html @@ -0,0 +1,115 @@ + + + + + Test for Bug 1204147 + + + + +Mozilla Bug 1204147 +

+ + + +
+
+
+ + diff --git a/editor/spellchecker/tests/test_bug1205983.html b/editor/spellchecker/tests/test_bug1205983.html new file mode 100644 index 0000000000..3de5e196bf --- /dev/null +++ b/editor/spellchecker/tests/test_bug1205983.html @@ -0,0 +1,134 @@ + + + + + Test for Bug 1205983 + + + + +Mozilla Bug 1205983 +

+ + +
German heute ist ein guter Tag
+ + +
+
+
+ + diff --git a/editor/spellchecker/tests/test_bug1209414.html b/editor/spellchecker/tests/test_bug1209414.html new file mode 100644 index 0000000000..b4b0cae947 --- /dev/null +++ b/editor/spellchecker/tests/test_bug1209414.html @@ -0,0 +1,144 @@ + + + + + Test for Bug 1209414 + + + + + +Mozilla Bug 1209414 +

+ + + + +
+
+
+ + diff --git a/editor/spellchecker/tests/test_bug1219928.html b/editor/spellchecker/tests/test_bug1219928.html new file mode 100644 index 0000000000..83ae8de908 --- /dev/null +++ b/editor/spellchecker/tests/test_bug1219928.html @@ -0,0 +1,69 @@ + + + + + Test for Bug 1219928 + + + + +Mozilla Bug 1219928 +

+ +
+

And here a missspelled word

+ +
+ +
+
+
+ + diff --git a/editor/spellchecker/tests/test_bug1365383.html b/editor/spellchecker/tests/test_bug1365383.html new file mode 100644 index 0000000000..d4333b4eb0 --- /dev/null +++ b/editor/spellchecker/tests/test_bug1365383.html @@ -0,0 +1,46 @@ + + + + + Test for Bug 1365383 + + + + + +Mozilla Bug 1365383 +

+
+ +
+
+
+
+
diff --git a/editor/spellchecker/tests/test_bug1368544.html b/editor/spellchecker/tests/test_bug1368544.html
new file mode 100644
index 0000000000..4a182615a8
--- /dev/null
+++ b/editor/spellchecker/tests/test_bug1368544.html
@@ -0,0 +1,91 @@
+
+
+
+
+  
+  
+  
+
+
+Mozilla Bug 1368544
+
+ +
+
+ + + + diff --git a/editor/spellchecker/tests/test_bug1402822.html b/editor/spellchecker/tests/test_bug1402822.html new file mode 100644 index 0000000000..6380060001 --- /dev/null +++ b/editor/spellchecker/tests/test_bug1402822.html @@ -0,0 +1,114 @@ + + + + + Test for Bug 1402822 + + + + + +Mozilla Bug 1402822 +

+ + + + +
+
+
+ + diff --git a/editor/spellchecker/tests/test_bug1418629.html b/editor/spellchecker/tests/test_bug1418629.html new file mode 100644 index 0000000000..442663f0c2 --- /dev/null +++ b/editor/spellchecker/tests/test_bug1418629.html @@ -0,0 +1,210 @@ + + + + Mozilla bug 1418629 + + + + + + +Mozilla Bug 1418629 +

+ + + + +
+ + + + diff --git a/editor/spellchecker/tests/test_bug1497480.html b/editor/spellchecker/tests/test_bug1497480.html new file mode 100644 index 0000000000..a7063271f4 --- /dev/null +++ b/editor/spellchecker/tests/test_bug1497480.html @@ -0,0 +1,94 @@ + + + + + + Test for Bug 1497480 + + + + + + + +Mozilla Bug 1497480 +

+ +
Bug 1497480
+
+ + + + diff --git a/editor/spellchecker/tests/test_bug1602526.html b/editor/spellchecker/tests/test_bug1602526.html new file mode 100644 index 0000000000..e8c2886d9d --- /dev/null +++ b/editor/spellchecker/tests/test_bug1602526.html @@ -0,0 +1,57 @@ + + + + Mozilla bug 1602526 + + + + + + +Mozilla Bug 1602526 +

+ + +
kkkkökkkk
+ + + + diff --git a/editor/spellchecker/tests/test_bug1761273.html b/editor/spellchecker/tests/test_bug1761273.html new file mode 100644 index 0000000000..543e56e810 --- /dev/null +++ b/editor/spellchecker/tests/test_bug1761273.html @@ -0,0 +1,96 @@ + + + + + Test for Bug 1761273 + + + + + +Mozilla Bug 1761273 +

+ + + + +
+
+
+ + diff --git a/editor/spellchecker/tests/test_bug1773802.html b/editor/spellchecker/tests/test_bug1773802.html new file mode 100644 index 0000000000..e31c819761 --- /dev/null +++ b/editor/spellchecker/tests/test_bug1773802.html @@ -0,0 +1,96 @@ + + + + + + Test for Bug 1773802 + + + + + +Mozilla Bug 1773802 +

+ + + + +
+
+
+ + diff --git a/editor/spellchecker/tests/test_bug1837268.html b/editor/spellchecker/tests/test_bug1837268.html new file mode 100644 index 0000000000..1467a328b5 --- /dev/null +++ b/editor/spellchecker/tests/test_bug1837268.html @@ -0,0 +1,79 @@ + + + + Mozilla bug 1837268 + + + + + + +Mozilla Bug 1837268 +

+ + +
aabbcc
+ + + + diff --git a/editor/spellchecker/tests/test_bug338427.html b/editor/spellchecker/tests/test_bug338427.html new file mode 100644 index 0000000000..dd869ffc11 --- /dev/null +++ b/editor/spellchecker/tests/test_bug338427.html @@ -0,0 +1,61 @@ + + + + + Test for Bug 338427 + + + + +Mozilla Bug 338427 +

+
+ + +
+
+
+
+ + diff --git a/editor/spellchecker/tests/test_bug366682.html b/editor/spellchecker/tests/test_bug366682.html new file mode 100644 index 0000000000..ac38bcf9d2 --- /dev/null +++ b/editor/spellchecker/tests/test_bug366682.html @@ -0,0 +1,65 @@ + + + + + Test for Bug 366682 + + + + + + +Mozilla Bug 366682 +

+ +
+
+
+ + + + + diff --git a/editor/spellchecker/tests/test_bug432225.html b/editor/spellchecker/tests/test_bug432225.html new file mode 100644 index 0000000000..2168429613 --- /dev/null +++ b/editor/spellchecker/tests/test_bug432225.html @@ -0,0 +1,72 @@ + + + + + Test for Bug 432225 + + + + + + +Mozilla Bug 432225 +

+ +
+
+
+ + + + + diff --git a/editor/spellchecker/tests/test_bug484181.html b/editor/spellchecker/tests/test_bug484181.html new file mode 100644 index 0000000000..30f0b359c8 --- /dev/null +++ b/editor/spellchecker/tests/test_bug484181.html @@ -0,0 +1,73 @@ + + + + + Test for Bug 484181 + + + + + + +Mozilla Bug 484181 +

+ +
+
+
+ +
I can haz cheezburger
+ + + diff --git a/editor/spellchecker/tests/test_bug596333.html b/editor/spellchecker/tests/test_bug596333.html new file mode 100644 index 0000000000..ce6714565b --- /dev/null +++ b/editor/spellchecker/tests/test_bug596333.html @@ -0,0 +1,135 @@ + + + + + Test for Bug 596333 + + + + + + +Mozilla Bug 596333 +

+ +
+
+
+ + + + + diff --git a/editor/spellchecker/tests/test_bug636465.html b/editor/spellchecker/tests/test_bug636465.html new file mode 100644 index 0000000000..97252d92df --- /dev/null +++ b/editor/spellchecker/tests/test_bug636465.html @@ -0,0 +1,54 @@ + +Mozilla bug 636465 + + + + +Mozilla Bug 636465 + + diff --git a/editor/spellchecker/tests/test_bug678842.html b/editor/spellchecker/tests/test_bug678842.html new file mode 100644 index 0000000000..f5f190ab0f --- /dev/null +++ b/editor/spellchecker/tests/test_bug678842.html @@ -0,0 +1,106 @@ + + + + + Test for Bug 678842 + + + + +Mozilla Bug 678842 +

+ + + +
+
+
+ + diff --git a/editor/spellchecker/tests/test_bug697981.html b/editor/spellchecker/tests/test_bug697981.html new file mode 100644 index 0000000000..044f82048b --- /dev/null +++ b/editor/spellchecker/tests/test_bug697981.html @@ -0,0 +1,137 @@ + + + + + Test for Bug 697981 + + + + +Mozilla Bug 697981 +

+ + + + + +
+
+
+ + diff --git a/editor/spellchecker/tests/test_bug717433.html b/editor/spellchecker/tests/test_bug717433.html new file mode 100644 index 0000000000..2f09cb70fd --- /dev/null +++ b/editor/spellchecker/tests/test_bug717433.html @@ -0,0 +1,111 @@ + + + + + Test for Bug 717433 + + + + +Mozilla Bug 717433 +

+ + + +
+
+
+ + diff --git a/editor/spellchecker/tests/test_multiple_content_languages.html b/editor/spellchecker/tests/test_multiple_content_languages.html new file mode 100644 index 0000000000..c3357dec91 --- /dev/null +++ b/editor/spellchecker/tests/test_multiple_content_languages.html @@ -0,0 +1,176 @@ + + + + Test for multiple Content-Language values + + + + +

+ + +
+
+
+ + diff --git a/editor/spellchecker/tests/test_nsIEditorSpellCheck_ReplaceWord.html b/editor/spellchecker/tests/test_nsIEditorSpellCheck_ReplaceWord.html new file mode 100644 index 0000000000..6088b97e3e --- /dev/null +++ b/editor/spellchecker/tests/test_nsIEditorSpellCheck_ReplaceWord.html @@ -0,0 +1,64 @@ + + + + Test for nsIEditorSpellCheck.ReplaceWord() + + + + +
+ + + diff --git a/editor/spellchecker/tests/test_spellcheck_after_edit.html b/editor/spellchecker/tests/test_spellcheck_after_edit.html new file mode 100644 index 0000000000..e4fa76d2e4 --- /dev/null +++ b/editor/spellchecker/tests/test_spellcheck_after_edit.html @@ -0,0 +1,198 @@ + + + + + Spellcheck result after edit + + + + + + + diff --git a/editor/spellchecker/tests/test_spellcheck_after_pressing_navigation_key.html b/editor/spellchecker/tests/test_spellcheck_after_pressing_navigation_key.html new file mode 100644 index 0000000000..2f0c3bed7d --- /dev/null +++ b/editor/spellchecker/tests/test_spellcheck_after_pressing_navigation_key.html @@ -0,0 +1,77 @@ + + + + + + Test for Bug 1729653 + + + + + + + + + diff --git a/editor/spellchecker/tests/test_spellcheck_selection.html b/editor/spellchecker/tests/test_spellcheck_selection.html new file mode 100644 index 0000000000..0d0887a8f3 --- /dev/null +++ b/editor/spellchecker/tests/test_spellcheck_selection.html @@ -0,0 +1,36 @@ + + +Bug 1779846: Test enableSelectionChecking=true on nsIEditorSpellCheck.InitSpellChecker + + + +
missspelled
+ + diff --git a/editor/spellchecker/tests/test_suggest.html b/editor/spellchecker/tests/test_suggest.html new file mode 100644 index 0000000000..2bdee93c1d --- /dev/null +++ b/editor/spellchecker/tests/test_suggest.html @@ -0,0 +1,42 @@ + + + + Test for nsIEditorSpellChecfker.sugget + + + + +

+ +
missspelled
+ +
+
+
+ + -- cgit v1.2.3