summaryrefslogtreecommitdiffstats
path: root/extensions/spellcheck/src
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/spellcheck/src')
-rw-r--r--extensions/spellcheck/src/moz.build34
-rw-r--r--extensions/spellcheck/src/mozEnglishWordUtils.cpp284
-rw-r--r--extensions/spellcheck/src/mozEnglishWordUtils.h42
-rw-r--r--extensions/spellcheck/src/mozInlineSpellChecker.cpp2043
-rw-r--r--extensions/spellcheck/src/mozInlineSpellChecker.h272
-rw-r--r--extensions/spellcheck/src/mozInlineSpellWordUtil.cpp1085
-rw-r--r--extensions/spellcheck/src/mozInlineSpellWordUtil.h179
-rw-r--r--extensions/spellcheck/src/mozPersonalDictionary.cpp471
-rw-r--r--extensions/spellcheck/src/mozPersonalDictionary.h83
-rw-r--r--extensions/spellcheck/src/mozSpellChecker.cpp565
-rw-r--r--extensions/spellcheck/src/mozSpellChecker.h76
-rw-r--r--extensions/spellcheck/src/mozSpellCheckerFactory.cpp73
-rw-r--r--extensions/spellcheck/src/mozSpellI18NManager.cpp32
-rw-r--r--extensions/spellcheck/src/mozSpellI18NManager.h30
14 files changed, 5269 insertions, 0 deletions
diff --git a/extensions/spellcheck/src/moz.build b/extensions/spellcheck/src/moz.build
new file mode 100644
index 000000000..8d6cef588
--- /dev/null
+++ b/extensions/spellcheck/src/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/.
+
+include('/ipc/chromium/chromium-config.mozbuild')
+SOURCES += [
+ 'mozEnglishWordUtils.cpp',
+ 'mozInlineSpellChecker.cpp',
+ 'mozInlineSpellWordUtil.cpp',
+ 'mozPersonalDictionary.cpp',
+ 'mozSpellChecker.cpp',
+ 'mozSpellCheckerFactory.cpp',
+ 'mozSpellI18NManager.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
+
+if CONFIG['MOZ_SYSTEM_HUNSPELL']:
+ CXXFLAGS += CONFIG['MOZ_HUNSPELL_CFLAGS']
+else:
+ LOCAL_INCLUDES += ['../hunspell/src']
+
+LOCAL_INCLUDES += [
+ '../hunspell/glue',
+ '/dom/base',
+]
+EXPORTS.mozilla += [
+ 'mozSpellChecker.h',
+]
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
diff --git a/extensions/spellcheck/src/mozEnglishWordUtils.cpp b/extensions/spellcheck/src/mozEnglishWordUtils.cpp
new file mode 100644
index 000000000..5033b247b
--- /dev/null
+++ b/extensions/spellcheck/src/mozEnglishWordUtils.cpp
@@ -0,0 +1,284 @@
+/* -*- 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 "mozEnglishWordUtils.h"
+#include "nsReadableUtils.h"
+#include "nsIServiceManager.h"
+#include "nsUnicharUtils.h"
+#include "nsUnicharUtilCIID.h"
+#include "nsUnicodeProperties.h"
+#include "nsCRT.h"
+#include "mozilla/Likely.h"
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(mozEnglishWordUtils)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(mozEnglishWordUtils)
+
+NS_INTERFACE_MAP_BEGIN(mozEnglishWordUtils)
+ NS_INTERFACE_MAP_ENTRY(mozISpellI18NUtil)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, mozISpellI18NUtil)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozEnglishWordUtils)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(mozEnglishWordUtils,
+ mURLDetector)
+
+mozEnglishWordUtils::mozEnglishWordUtils()
+{
+ mLanguage.AssignLiteral("en");
+
+ nsresult rv;
+ mURLDetector = do_CreateInstance(MOZ_TXTTOHTMLCONV_CONTRACTID, &rv);
+}
+
+mozEnglishWordUtils::~mozEnglishWordUtils()
+{
+}
+
+NS_IMETHODIMP mozEnglishWordUtils::GetLanguage(char16_t * *aLanguage)
+{
+ NS_ENSURE_ARG_POINTER(aLanguage);
+
+ *aLanguage = ToNewUnicode(mLanguage);
+ if (!*aLanguage) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+// return the possible root forms of aWord.
+NS_IMETHODIMP mozEnglishWordUtils::GetRootForm(const char16_t *aWord, uint32_t type, char16_t ***words, uint32_t *count)
+{
+ nsAutoString word(aWord);
+ char16_t **tmpPtr;
+ int32_t length = word.Length();
+
+ *count = 0;
+
+ mozEnglishWordUtils::myspCapitalization ct = captype(word);
+ switch (ct)
+ {
+ case HuhCap:
+ case NoCap:
+ tmpPtr = (char16_t **)moz_xmalloc(sizeof(char16_t *));
+ if (!tmpPtr)
+ return NS_ERROR_OUT_OF_MEMORY;
+ tmpPtr[0] = ToNewUnicode(word);
+ if (!tmpPtr[0]) {
+ NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(0, tmpPtr);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ *words = tmpPtr;
+ *count = 1;
+ break;
+
+
+ case AllCap:
+ tmpPtr = (char16_t **)moz_xmalloc(sizeof(char16_t *) * 3);
+ if (!tmpPtr)
+ return NS_ERROR_OUT_OF_MEMORY;
+ tmpPtr[0] = ToNewUnicode(word);
+ if (!tmpPtr[0]) {
+ NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(0, tmpPtr);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ ToLowerCase(tmpPtr[0], tmpPtr[0], length);
+
+ tmpPtr[1] = ToNewUnicode(word);
+ if (!tmpPtr[1]) {
+ NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(1, tmpPtr);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ ToLowerCase(tmpPtr[1], tmpPtr[1], length);
+ ToUpperCase(tmpPtr[1], tmpPtr[1], 1);
+
+ tmpPtr[2] = ToNewUnicode(word);
+ if (!tmpPtr[2]) {
+ NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(2, tmpPtr);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ *words = tmpPtr;
+ *count = 3;
+ break;
+
+ case InitCap:
+ tmpPtr = (char16_t **)moz_xmalloc(sizeof(char16_t *) * 2);
+ if (!tmpPtr)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ tmpPtr[0] = ToNewUnicode(word);
+ if (!tmpPtr[0]) {
+ NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(0, tmpPtr);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ ToLowerCase(tmpPtr[0], tmpPtr[0], length);
+
+ tmpPtr[1] = ToNewUnicode(word);
+ if (!tmpPtr[1]) {
+ NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(1, tmpPtr);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ *words = tmpPtr;
+ *count = 2;
+ break;
+ default:
+ return NS_ERROR_FAILURE; // should never get here;
+ }
+ return NS_OK;
+}
+
+// This needs vast improvement
+bool mozEnglishWordUtils::ucIsAlpha(char16_t aChar)
+{
+ // XXX we have to fix callers to handle the full Unicode range
+ return nsIUGenCategory::kLetter == mozilla::unicode::GetGenCategory(aChar);
+}
+
+NS_IMETHODIMP mozEnglishWordUtils::FindNextWord(const char16_t *word, uint32_t length, uint32_t offset, int32_t *begin, int32_t *end)
+{
+ const char16_t *p = word + offset;
+ const char16_t *endbuf = word + length;
+ const char16_t *startWord=p;
+ if(p<endbuf){
+ // XXX These loops should be modified to handle non-BMP characters.
+ // if previous character is a word character, need to advance out of the word
+ if (offset > 0 && ucIsAlpha(*(p-1))) {
+ while (p < endbuf && ucIsAlpha(*p))
+ p++;
+ }
+ while((p < endbuf) && (!ucIsAlpha(*p)))
+ {
+ p++;
+ }
+ startWord=p;
+ while((p < endbuf) && ((ucIsAlpha(*p))||(*p=='\'')))
+ {
+ p++;
+ }
+
+ // we could be trying to break down a url, we don't want to break a url into parts,
+ // instead we want to find out if it really is a url and if so, skip it, advancing startWord
+ // to a point after the url.
+
+ // before we spend more time looking to see if the word is a url, look for a url identifer
+ // and make sure that identifer isn't the last character in the word fragment.
+ if ( (*p == ':' || *p == '@' || *p == '.') && p < endbuf - 1) {
+
+ // ok, we have a possible url...do more research to find out if we really have one
+ // and determine the length of the url so we can skip over it.
+
+ if (mURLDetector)
+ {
+ int32_t startPos = -1;
+ int32_t endPos = -1;
+
+ mURLDetector->FindURLInPlaintext(startWord, endbuf - startWord, p - startWord, &startPos, &endPos);
+
+ // ok, if we got a url, adjust the array bounds, skip the current url text and find the next word again
+ if (startPos != -1 && endPos != -1) {
+ startWord = p + endPos + 1; // skip over the url
+ p = startWord; // reset p
+
+ // now recursively call FindNextWord to search for the next word now that we have skipped the url
+ return FindNextWord(word, length, startWord - word, begin, end);
+ }
+ }
+ }
+
+ while((p > startWord)&&(*(p-1) == '\'')){ // trim trailing apostrophes
+ p--;
+ }
+ }
+ else{
+ startWord = endbuf;
+ }
+ if(startWord == endbuf){
+ *begin = -1;
+ *end = -1;
+ }
+ else{
+ *begin = startWord-word;
+ *end = p-word;
+ }
+ return NS_OK;
+}
+
+mozEnglishWordUtils::myspCapitalization
+mozEnglishWordUtils::captype(const nsString &word)
+{
+ char16_t* lword=ToNewUnicode(word);
+ ToUpperCase(lword,lword,word.Length());
+ if(word.Equals(lword)){
+ free(lword);
+ return AllCap;
+ }
+
+ ToLowerCase(lword,lword,word.Length());
+ if(word.Equals(lword)){
+ free(lword);
+ return NoCap;
+ }
+ int32_t length=word.Length();
+ if(Substring(word,1,length-1).Equals(lword+1)){
+ free(lword);
+ return InitCap;
+ }
+ free(lword);
+ return HuhCap;
+}
+
+// Convert the list of words in iwords to the same capitalization aWord and
+// return them in owords.
+NS_IMETHODIMP mozEnglishWordUtils::FromRootForm(const char16_t *aWord, const char16_t **iwords, uint32_t icount, char16_t ***owords, uint32_t *ocount)
+{
+ nsAutoString word(aWord);
+ nsresult rv = NS_OK;
+
+ int32_t length;
+ char16_t **tmpPtr = (char16_t **)moz_xmalloc(sizeof(char16_t *)*icount);
+ if (!tmpPtr)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ mozEnglishWordUtils::myspCapitalization ct = captype(word);
+ for(uint32_t i = 0; i < icount; ++i) {
+ length = NS_strlen(iwords[i]);
+ tmpPtr[i] = (char16_t *) moz_xmalloc(sizeof(char16_t) * (length + 1));
+ if (MOZ_UNLIKELY(!tmpPtr[i])) {
+ NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(i, tmpPtr);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ memcpy(tmpPtr[i], iwords[i], (length + 1) * sizeof(char16_t));
+
+ nsAutoString capTest(tmpPtr[i]);
+ mozEnglishWordUtils::myspCapitalization newCt=captype(capTest);
+ if(newCt == NoCap){
+ switch(ct)
+ {
+ case HuhCap:
+ case NoCap:
+ break;
+ case AllCap:
+ ToUpperCase(tmpPtr[i],tmpPtr[i],length);
+ rv = NS_OK;
+ break;
+ case InitCap:
+ ToUpperCase(tmpPtr[i],tmpPtr[i],1);
+ rv = NS_OK;
+ break;
+ default:
+ rv = NS_ERROR_FAILURE; // should never get here;
+ break;
+
+ }
+ }
+ }
+ if (NS_SUCCEEDED(rv)){
+ *owords = tmpPtr;
+ *ocount = icount;
+ }
+ return rv;
+}
+
diff --git a/extensions/spellcheck/src/mozEnglishWordUtils.h b/extensions/spellcheck/src/mozEnglishWordUtils.h
new file mode 100644
index 000000000..0bc3ddfba
--- /dev/null
+++ b/extensions/spellcheck/src/mozEnglishWordUtils.h
@@ -0,0 +1,42 @@
+/* -*- 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 mozEnglishWordUtils_h__
+#define mozEnglishWordUtils_h__
+
+#include "nsCOMPtr.h"
+#include "mozISpellI18NUtil.h"
+#include "nsIUnicodeEncoder.h"
+#include "nsIUnicodeDecoder.h"
+#include "nsString.h"
+
+#include "mozITXTToHTMLConv.h"
+#include "nsCycleCollectionParticipant.h"
+
+class mozEnglishWordUtils : public mozISpellI18NUtil
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_MOZISPELLI18NUTIL
+ NS_DECL_CYCLE_COLLECTION_CLASS(mozEnglishWordUtils)
+
+ mozEnglishWordUtils();
+ /* additional members */
+ enum myspCapitalization{
+ NoCap,InitCap,AllCap,HuhCap
+ };
+
+protected:
+ virtual ~mozEnglishWordUtils();
+
+ mozEnglishWordUtils::myspCapitalization captype(const nsString &word);
+ bool ucIsAlpha(char16_t aChar);
+
+ nsString mLanguage;
+ nsString mCharset;
+ nsCOMPtr<mozITXTToHTMLConv> mURLDetector; // used to detect urls so the spell checker can skip them.
+};
+
+#endif
diff --git a/extensions/spellcheck/src/mozInlineSpellChecker.cpp b/extensions/spellcheck/src/mozInlineSpellChecker.cpp
new file mode 100644
index 000000000..96011a37e
--- /dev/null
+++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp
@@ -0,0 +1,2043 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=2 sts=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/. */
+
+/**
+ * This class is called by the editor to handle spellchecking after various
+ * events. The main entrypoint is SpellCheckAfterEditorChange, which is called
+ * when the text is changed.
+ *
+ * It is VERY IMPORTANT that we do NOT do any operations that might cause DOM
+ * notifications to be flushed when we are called from the editor. This is
+ * because the call might originate from a frame, and flushing the
+ * notifications might cause that frame to be deleted.
+ *
+ * Using the WordUtil class to find words causes DOM notifications to be
+ * flushed because it asks for style information. As a result, we post an event
+ * and do all of the spellchecking in that event handler, which occurs later.
+ * We store all DOM pointers in ranges because they are kept up-to-date with
+ * DOM changes that may have happened while the event was on the queue.
+ *
+ * We also allow the spellcheck to be suspended and resumed later. This makes
+ * large pastes or initializations with a lot of text not hang the browser UI.
+ *
+ * An optimization is the mNeedsCheckAfterNavigation flag. This is set to
+ * true when we get any change, and false once there is no possibility
+ * something changed that we need to check on navigation. Navigation events
+ * tend to be a little tricky because we want to check the current word on
+ * exit if something has changed. If we navigate inside the word, we don't want
+ * to do anything. As a result, this flag is cleared in FinishNavigationEvent
+ * when we know that we are checking as a result of navigation.
+ */
+
+#include "mozilla/EditorBase.h"
+#include "mozilla/EditorUtils.h"
+#include "mozilla/Services.h"
+#include "mozilla/dom/Selection.h"
+#include "mozInlineSpellChecker.h"
+#include "mozInlineSpellWordUtil.h"
+#include "mozISpellI18NManager.h"
+#include "nsCOMPtr.h"
+#include "nsCRT.h"
+#include "nsIDOMNode.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMHTMLElement.h"
+#include "nsIDOMMouseEvent.h"
+#include "nsIDOMKeyEvent.h"
+#include "nsIDOMNode.h"
+#include "nsIDOMNodeList.h"
+#include "nsIDOMEvent.h"
+#include "nsGenericHTMLElement.h"
+#include "nsRange.h"
+#include "nsIPlaintextEditor.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIRunnable.h"
+#include "nsISelection.h"
+#include "nsISelectionPrivate.h"
+#include "nsISelectionController.h"
+#include "nsIServiceManager.h"
+#include "nsITextServicesFilter.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsIContent.h"
+#include "nsRange.h"
+#include "nsContentUtils.h"
+#include "nsIObserverService.h"
+#include "nsITextControlElement.h"
+#include "prtime.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+// Set to spew messages to the console about what is happening.
+//#define DEBUG_INLINESPELL
+
+// the number of milliseconds that we will take at once to do spellchecking
+#define INLINESPELL_CHECK_TIMEOUT 50
+
+// The number of words to check before we look at the time to see if
+// INLINESPELL_CHECK_TIMEOUT ms have elapsed. This prevents us from spending
+// too much time checking the clock. Note that misspelled words count for
+// more than one word in this calculation.
+#define INLINESPELL_TIMEOUT_CHECK_FREQUENCY 50
+
+// This number is the number of checked words a misspelled word counts for
+// when we're checking the time to see if the alloted time is up for
+// spellchecking. Misspelled words take longer to process since we have to
+// create a range, so they count more. The exact number isn't very important
+// since this just controls how often we check the current time.
+#define MISSPELLED_WORD_COUNT_PENALTY 4
+
+// These notifications are broadcast when spell check starts and ends. STARTED
+// must always be followed by ENDED.
+#define INLINESPELL_STARTED_TOPIC "inlineSpellChecker-spellCheck-started"
+#define INLINESPELL_ENDED_TOPIC "inlineSpellChecker-spellCheck-ended"
+
+static bool ContentIsDescendantOf(nsINode* aPossibleDescendant,
+ nsINode* aPossibleAncestor);
+
+static const char kMaxSpellCheckSelectionSize[] = "extensions.spellcheck.inline.max-misspellings";
+
+mozInlineSpellStatus::mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker)
+ : mSpellChecker(aSpellChecker), mWordCount(0)
+{
+}
+
+// mozInlineSpellStatus::InitForEditorChange
+//
+// This is the most complicated case. For changes, we need to compute the
+// range of stuff that changed based on the old and new caret positions,
+// as well as use a range possibly provided by the editor (start and end,
+// which are usually nullptr) to get a range with the union of these.
+
+nsresult
+mozInlineSpellStatus::InitForEditorChange(
+ EditAction aAction,
+ nsIDOMNode* aAnchorNode, int32_t aAnchorOffset,
+ nsIDOMNode* aPreviousNode, int32_t aPreviousOffset,
+ nsIDOMNode* aStartNode, int32_t aStartOffset,
+ nsIDOMNode* aEndNode, int32_t aEndOffset)
+{
+ nsresult rv;
+
+ nsCOMPtr<nsIDOMDocument> doc;
+ rv = GetDocument(getter_AddRefs(doc));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // save the anchor point as a range so we can find the current word later
+ rv = PositionToCollapsedRange(doc, aAnchorNode, aAnchorOffset,
+ getter_AddRefs(mAnchorRange));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsINode> prevNode = do_QueryInterface(aPreviousNode);
+ NS_ENSURE_STATE(prevNode);
+
+ bool deleted = aAction == EditAction::deleteSelection;
+ if (aAction == EditAction::insertIMEText) {
+ // IME may remove the previous node if it cancels composition when
+ // there is no text around the composition.
+ deleted = !prevNode->IsInComposedDoc();
+ }
+
+ if (deleted) {
+ // Deletes are easy, the range is just the current anchor. We set the range
+ // to check to be empty, FinishInitOnEvent will fill in the range to be
+ // the current word.
+ mOp = eOpChangeDelete;
+ mRange = nullptr;
+ return NS_OK;
+ }
+
+ mOp = eOpChange;
+
+ // range to check
+ mRange = new nsRange(prevNode);
+
+ // ...we need to put the start and end in the correct order
+ int16_t cmpResult;
+ rv = mAnchorRange->ComparePoint(aPreviousNode, aPreviousOffset, &cmpResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cmpResult < 0) {
+ // previous anchor node is before the current anchor
+ rv = mRange->SetStart(aPreviousNode, aPreviousOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mRange->SetEnd(aAnchorNode, aAnchorOffset);
+ } else {
+ // previous anchor node is after (or the same as) the current anchor
+ rv = mRange->SetStart(aAnchorNode, aAnchorOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mRange->SetEnd(aPreviousNode, aPreviousOffset);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // On insert save this range: DoSpellCheck optimizes things in this range.
+ // Otherwise, just leave this nullptr.
+ if (aAction == EditAction::insertText)
+ mCreatedRange = mRange;
+
+ // if we were given a range, we need to expand our range to encompass it
+ if (aStartNode && aEndNode) {
+ rv = mRange->ComparePoint(aStartNode, aStartOffset, &cmpResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cmpResult < 0) { // given range starts before
+ rv = mRange->SetStart(aStartNode, aStartOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = mRange->ComparePoint(aEndNode, aEndOffset, &cmpResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (cmpResult > 0) { // given range ends after
+ rv = mRange->SetEnd(aEndNode, aEndOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
+
+// mozInlineSpellStatis::InitForNavigation
+//
+// For navigation events, we just need to store the new and old positions.
+//
+// In some cases, we detect that we shouldn't check. If this event should
+// not be processed, *aContinue will be false.
+
+nsresult
+mozInlineSpellStatus::InitForNavigation(
+ bool aForceCheck, int32_t aNewPositionOffset,
+ nsIDOMNode* aOldAnchorNode, int32_t aOldAnchorOffset,
+ nsIDOMNode* aNewAnchorNode, int32_t aNewAnchorOffset,
+ bool* aContinue)
+{
+ nsresult rv;
+ mOp = eOpNavigation;
+
+ mForceNavigationWordCheck = aForceCheck;
+ mNewNavigationPositionOffset = aNewPositionOffset;
+
+ // get the root node for checking
+ nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIDOMElement> rootElt;
+ rv = editor->GetRootElement(getter_AddRefs(rootElt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // the anchor node might not be in the DOM anymore, check
+ nsCOMPtr<nsINode> root = do_QueryInterface(rootElt, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsINode> currentAnchor = do_QueryInterface(aOldAnchorNode, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (root && currentAnchor && ! ContentIsDescendantOf(currentAnchor, root)) {
+ *aContinue = false;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMDocument> doc;
+ rv = GetDocument(getter_AddRefs(doc));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = PositionToCollapsedRange(doc, aOldAnchorNode, aOldAnchorOffset,
+ getter_AddRefs(mOldNavigationAnchorRange));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = PositionToCollapsedRange(doc, aNewAnchorNode, aNewAnchorOffset,
+ getter_AddRefs(mAnchorRange));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aContinue = true;
+ return NS_OK;
+}
+
+// mozInlineSpellStatus::InitForSelection
+//
+// It is easy for selections since we always re-check the spellcheck
+// selection.
+
+nsresult
+mozInlineSpellStatus::InitForSelection()
+{
+ mOp = eOpSelection;
+ return NS_OK;
+}
+
+// mozInlineSpellStatus::InitForRange
+//
+// Called to cause the spellcheck of the given range. This will look like
+// a change operation over the given range.
+
+nsresult
+mozInlineSpellStatus::InitForRange(nsRange* aRange)
+{
+ mOp = eOpChange;
+ mRange = aRange;
+ return NS_OK;
+}
+
+// mozInlineSpellStatus::FinishInitOnEvent
+//
+// Called when the event is triggered to complete initialization that
+// might require the WordUtil. This calls to the operation-specific
+// initializer, and also sets the range to be the entire element if it
+// is nullptr.
+//
+// Watch out: the range might still be nullptr if there is nothing to do,
+// the caller will have to check for this.
+
+nsresult
+mozInlineSpellStatus::FinishInitOnEvent(mozInlineSpellWordUtil& aWordUtil)
+{
+ nsresult rv;
+ if (! mRange) {
+ rv = mSpellChecker->MakeSpellCheckRange(nullptr, 0, nullptr, 0,
+ getter_AddRefs(mRange));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ switch (mOp) {
+ case eOpChange:
+ if (mAnchorRange)
+ return FillNoCheckRangeFromAnchor(aWordUtil);
+ break;
+ case eOpChangeDelete:
+ if (mAnchorRange) {
+ rv = FillNoCheckRangeFromAnchor(aWordUtil);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Delete events will have no range for the changed text (because it was
+ // deleted), and InitForEditorChange will set it to nullptr. Here, we select
+ // the entire word to cause any underlining to be removed.
+ mRange = mNoCheckRange;
+ break;
+ case eOpNavigation:
+ return FinishNavigationEvent(aWordUtil);
+ case eOpSelection:
+ // this gets special handling in ResumeCheck
+ break;
+ case eOpResume:
+ // everything should be initialized already in this case
+ break;
+ default:
+ NS_NOTREACHED("Bad operation");
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ return NS_OK;
+}
+
+// mozInlineSpellStatus::FinishNavigationEvent
+//
+// This verifies that we need to check the word at the previous caret
+// position. Now that we have the word util, we can find the word belonging
+// to the previous caret position. If the new position is inside that word,
+// we don't want to do anything. In this case, we'll nullptr out mRange so
+// that the caller will know not to continue.
+//
+// Notice that we don't set mNoCheckRange. We check here whether the cursor
+// is in the word that needs checking, so it isn't necessary. Plus, the
+// spellchecker isn't guaranteed to only check the given word, and it could
+// remove the underline from the new word under the cursor.
+
+nsresult
+mozInlineSpellStatus::FinishNavigationEvent(mozInlineSpellWordUtil& aWordUtil)
+{
+ nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor);
+ if (! editor)
+ return NS_ERROR_FAILURE; // editor is gone
+
+ NS_ASSERTION(mAnchorRange, "No anchor for navigation!");
+ nsCOMPtr<nsIDOMNode> newAnchorNode, oldAnchorNode;
+ int32_t newAnchorOffset, oldAnchorOffset;
+
+ // get the DOM position of the old caret, the range should be collapsed
+ nsresult rv = mOldNavigationAnchorRange->GetStartContainer(
+ getter_AddRefs(oldAnchorNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mOldNavigationAnchorRange->GetStartOffset(&oldAnchorOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // find the word on the old caret position, this is the one that we MAY need
+ // to check
+ RefPtr<nsRange> oldWord;
+ rv = aWordUtil.GetRangeForWord(oldAnchorNode, oldAnchorOffset,
+ getter_AddRefs(oldWord));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // aWordUtil.GetRangeForWord flushes pending notifications, check editor again.
+ editor = do_QueryReferent(mSpellChecker->mEditor);
+ if (! editor)
+ return NS_ERROR_FAILURE; // editor is gone
+
+ // get the DOM position of the new caret, the range should be collapsed
+ rv = mAnchorRange->GetStartContainer(getter_AddRefs(newAnchorNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mAnchorRange->GetStartOffset(&newAnchorOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // see if the new cursor position is in the word of the old cursor position
+ bool isInRange = false;
+ if (! mForceNavigationWordCheck) {
+ rv = oldWord->IsPointInRange(newAnchorNode,
+ newAnchorOffset + mNewNavigationPositionOffset,
+ &isInRange);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (isInRange) {
+ // caller should give up
+ mRange = nullptr;
+ } else {
+ // check the old word
+ mRange = oldWord;
+
+ // Once we've spellchecked the current word, we don't need to spellcheck
+ // for any more navigation events.
+ mSpellChecker->mNeedsCheckAfterNavigation = false;
+ }
+ return NS_OK;
+}
+
+// mozInlineSpellStatus::FillNoCheckRangeFromAnchor
+//
+// Given the mAnchorRange object, computes the range of the word it is on
+// (if any) and fills that range into mNoCheckRange. This is used for
+// change and navigation events to know which word we should skip spell
+// checking on
+
+nsresult
+mozInlineSpellStatus::FillNoCheckRangeFromAnchor(
+ mozInlineSpellWordUtil& aWordUtil)
+{
+ nsCOMPtr<nsIDOMNode> anchorNode;
+ nsresult rv = mAnchorRange->GetStartContainer(getter_AddRefs(anchorNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t anchorOffset;
+ rv = mAnchorRange->GetStartOffset(&anchorOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return aWordUtil.GetRangeForWord(anchorNode, anchorOffset,
+ getter_AddRefs(mNoCheckRange));
+}
+
+// mozInlineSpellStatus::GetDocument
+//
+// Returns the nsIDOMDocument object for the document for the
+// current spellchecker.
+
+nsresult
+mozInlineSpellStatus::GetDocument(nsIDOMDocument** aDocument)
+{
+ nsresult rv;
+ *aDocument = nullptr;
+ if (! mSpellChecker->mEditor)
+ return NS_ERROR_UNEXPECTED;
+
+ nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDOMDocument> domDoc;
+ rv = editor->GetDocument(getter_AddRefs(domDoc));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(domDoc, NS_ERROR_NULL_POINTER);
+ domDoc.forget(aDocument);
+ return NS_OK;
+}
+
+// mozInlineSpellStatus::PositionToCollapsedRange
+//
+// Converts a given DOM position to a collapsed range covering that
+// position. We use ranges to store DOM positions becuase they stay
+// updated as the DOM is changed.
+
+nsresult
+mozInlineSpellStatus::PositionToCollapsedRange(nsIDOMDocument* aDocument,
+ nsIDOMNode* aNode, int32_t aOffset, nsIDOMRange** aRange)
+{
+ *aRange = nullptr;
+ nsCOMPtr<nsIDOMRange> range;
+ nsresult rv = aDocument->CreateRange(getter_AddRefs(range));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = range->SetStart(aNode, aOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = range->SetEnd(aNode, aOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ range.swap(*aRange);
+ return NS_OK;
+}
+
+// mozInlineSpellResume
+
+class mozInlineSpellResume : public Runnable
+{
+public:
+ mozInlineSpellResume(const mozInlineSpellStatus& aStatus,
+ uint32_t aDisabledAsyncToken)
+ : mDisabledAsyncToken(aDisabledAsyncToken), mStatus(aStatus) {}
+
+ nsresult Post()
+ {
+ return NS_DispatchToMainThread(this);
+ }
+
+ NS_IMETHOD Run() override
+ {
+ // Discard the resumption if the spell checker was disabled after the
+ // resumption was scheduled.
+ if (mDisabledAsyncToken == mStatus.mSpellChecker->mDisabledAsyncToken) {
+ mStatus.mSpellChecker->ResumeCheck(&mStatus);
+ }
+ return NS_OK;
+ }
+
+private:
+ uint32_t mDisabledAsyncToken;
+ mozInlineSpellStatus mStatus;
+};
+
+// Used as the nsIEditorSpellCheck::InitSpellChecker callback.
+class InitEditorSpellCheckCallback final : public nsIEditorSpellCheckCallback
+{
+ ~InitEditorSpellCheckCallback() {}
+public:
+ NS_DECL_ISUPPORTS
+
+ explicit InitEditorSpellCheckCallback(mozInlineSpellChecker* aSpellChecker)
+ : mSpellChecker(aSpellChecker) {}
+
+ NS_IMETHOD EditorSpellCheckDone() override
+ {
+ return mSpellChecker ? mSpellChecker->EditorSpellCheckInited() : NS_OK;
+ }
+
+ void Cancel()
+ {
+ mSpellChecker = nullptr;
+ }
+
+private:
+ RefPtr<mozInlineSpellChecker> mSpellChecker;
+};
+NS_IMPL_ISUPPORTS(InitEditorSpellCheckCallback, nsIEditorSpellCheckCallback)
+
+
+NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker)
+ NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker)
+ NS_INTERFACE_MAP_ENTRY(nsIEditActionListener)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozInlineSpellChecker)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(mozInlineSpellChecker)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(mozInlineSpellChecker)
+
+NS_IMPL_CYCLE_COLLECTION(mozInlineSpellChecker,
+ mSpellCheck,
+ mTreeWalker,
+ mCurrentSelectionAnchorNode)
+
+mozInlineSpellChecker::SpellCheckingState
+ mozInlineSpellChecker::gCanEnableSpellChecking =
+ mozInlineSpellChecker::SpellCheck_Uninitialized;
+
+mozInlineSpellChecker::mozInlineSpellChecker() :
+ mNumWordsInSpellSelection(0),
+ mMaxNumWordsInSpellSelection(250),
+ mNumPendingSpellChecks(0),
+ mNumPendingUpdateCurrentDictionary(0),
+ mDisabledAsyncToken(0),
+ mNeedsCheckAfterNavigation(false),
+ mFullSpellCheckScheduled(false)
+{
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefs)
+ prefs->GetIntPref(kMaxSpellCheckSelectionSize, &mMaxNumWordsInSpellSelection);
+ mMaxMisspellingsPerCheck = mMaxNumWordsInSpellSelection * 3 / 4;
+}
+
+mozInlineSpellChecker::~mozInlineSpellChecker()
+{
+}
+
+NS_IMETHODIMP
+mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck **aSpellCheck)
+{
+ *aSpellCheck = mSpellCheck;
+ NS_IF_ADDREF(*aSpellCheck);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+mozInlineSpellChecker::Init(nsIEditor *aEditor)
+{
+ mEditor = do_GetWeakReference(aEditor);
+ return NS_OK;
+}
+
+// mozInlineSpellChecker::Cleanup
+//
+// Called by the editor when the editor is going away. This is important
+// because we remove listeners. We do NOT clean up anything else in this
+// function, because it can get called while DoSpellCheck is running!
+//
+// Getting the style information there can cause DOM notifications to be
+// flushed, which can cause editors to go away which will bring us here.
+// We can not do anything that will cause DoSpellCheck to freak out.
+
+nsresult mozInlineSpellChecker::Cleanup(bool aDestroyingFrames)
+{
+ mNumWordsInSpellSelection = 0;
+ nsCOMPtr<nsISelection> spellCheckSelection;
+ nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
+ if (NS_FAILED(rv)) {
+ // Ensure we still unregister event listeners (but return a failure code)
+ UnregisterEventListeners();
+ } else {
+ if (!aDestroyingFrames) {
+ spellCheckSelection->RemoveAllRanges();
+ }
+
+ rv = UnregisterEventListeners();
+ }
+
+ // Notify ENDED observers now. If we wait to notify as we normally do when
+ // these async operations finish, then in the meantime the editor may create
+ // another inline spell checker and cause more STARTED and ENDED
+ // notifications to be broadcast. Interleaved notifications for the same
+ // editor but different inline spell checkers could easily confuse
+ // observers. They may receive two consecutive STARTED notifications for
+ // example, which we guarantee will not happen.
+
+ nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
+ if (mPendingSpellCheck) {
+ // Cancel the pending editor spell checker initialization.
+ mPendingSpellCheck = nullptr;
+ mPendingInitEditorSpellCheckCallback->Cancel();
+ mPendingInitEditorSpellCheckCallback = nullptr;
+ ChangeNumPendingSpellChecks(-1, editor);
+ }
+
+ // Increment this token so that pending UpdateCurrentDictionary calls and
+ // scheduled spell checks are discarded when they finish.
+ mDisabledAsyncToken++;
+
+ if (mNumPendingUpdateCurrentDictionary > 0) {
+ // Account for pending UpdateCurrentDictionary calls.
+ ChangeNumPendingSpellChecks(-mNumPendingUpdateCurrentDictionary, editor);
+ mNumPendingUpdateCurrentDictionary = 0;
+ }
+ if (mNumPendingSpellChecks > 0) {
+ // If mNumPendingSpellChecks is still > 0 at this point, the remainder is
+ // pending scheduled spell checks.
+ ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, editor);
+ }
+
+ mEditor = nullptr;
+ mFullSpellCheckScheduled = false;
+
+ return rv;
+}
+
+// mozInlineSpellChecker::CanEnableInlineSpellChecking
+//
+// This function can be called to see if it seems likely that we can enable
+// spellchecking before actually creating the InlineSpellChecking objects.
+//
+// The problem is that we can't get the dictionary list without actually
+// creating a whole bunch of spellchecking objects. This function tries to
+// do that and caches the result so we don't have to keep allocating those
+// objects if there are no dictionaries or spellchecking.
+//
+// Whenever dictionaries are added or removed at runtime, this value must be
+// updated before an observer notification is sent out about the change, to
+// avoid editors getting a wrong cached result.
+
+bool // static
+mozInlineSpellChecker::CanEnableInlineSpellChecking()
+{
+ nsresult rv;
+ if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
+ gCanEnableSpellChecking = SpellCheck_NotAvailable;
+
+ nsCOMPtr<nsIEditorSpellCheck> spellchecker =
+ do_CreateInstance("@mozilla.org/editor/editorspellchecker;1", &rv);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool canSpellCheck = false;
+ rv = spellchecker->CanSpellCheck(&canSpellCheck);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ if (canSpellCheck)
+ gCanEnableSpellChecking = SpellCheck_Available;
+ }
+ return (gCanEnableSpellChecking == SpellCheck_Available);
+}
+
+void // static
+mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking()
+{
+ gCanEnableSpellChecking = SpellCheck_Uninitialized;
+}
+
+// mozInlineSpellChecker::RegisterEventListeners
+//
+// The inline spell checker listens to mouse events and keyboard navigation+ // events.
+
+nsresult
+mozInlineSpellChecker::RegisterEventListeners()
+{
+ nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
+ NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
+
+ editor->AddEditActionListener(this);
+
+ nsCOMPtr<nsIDOMDocument> doc;
+ nsresult rv = editor->GetDocument(getter_AddRefs(doc));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<EventTarget> piTarget = do_QueryInterface(doc, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ piTarget->AddEventListener(NS_LITERAL_STRING("blur"), this,
+ true, false);
+ piTarget->AddEventListener(NS_LITERAL_STRING("click"), this,
+ false, false);
+ piTarget->AddEventListener(NS_LITERAL_STRING("keypress"), this,
+ false, false);
+ return NS_OK;
+}
+
+// mozInlineSpellChecker::UnregisterEventListeners
+
+nsresult
+mozInlineSpellChecker::UnregisterEventListeners()
+{
+ nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
+ NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
+
+ editor->RemoveEditActionListener(this);
+
+ nsCOMPtr<nsIDOMDocument> doc;
+ editor->GetDocument(getter_AddRefs(doc));
+ NS_ENSURE_TRUE(doc, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<EventTarget> piTarget = do_QueryInterface(doc);
+ NS_ENSURE_TRUE(piTarget, NS_ERROR_NULL_POINTER);
+
+ piTarget->RemoveEventListener(NS_LITERAL_STRING("blur"), this, true);
+ piTarget->RemoveEventListener(NS_LITERAL_STRING("click"), this, false);
+ piTarget->RemoveEventListener(NS_LITERAL_STRING("keypress"), this, false);
+ return NS_OK;
+}
+
+// mozInlineSpellChecker::GetEnableRealTimeSpell
+
+NS_IMETHODIMP
+mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled)
+{
+ NS_ENSURE_ARG_POINTER(aEnabled);
+ *aEnabled = mSpellCheck != nullptr || mPendingSpellCheck != nullptr;
+ return NS_OK;
+}
+
+// mozInlineSpellChecker::SetEnableRealTimeSpell
+
+NS_IMETHODIMP
+mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled)
+{
+ if (!aEnabled) {
+ mSpellCheck = nullptr;
+ return Cleanup(false);
+ }
+
+ if (mSpellCheck) {
+ // spellcheck the current contents. SpellCheckRange doesn't supply a created
+ // range to DoSpellCheck, which in our case is the entire range. But this
+ // optimization doesn't matter because there is nothing in the spellcheck
+ // selection when starting, which triggers a better optimization.
+ return SpellCheckRange(nullptr);
+ }
+
+ if (mPendingSpellCheck) {
+ // The editor spell checker is already being initialized.
+ return NS_OK;
+ }
+
+ mPendingSpellCheck =
+ do_CreateInstance("@mozilla.org/editor/editorspellchecker;1");
+ NS_ENSURE_STATE(mPendingSpellCheck);
+
+ nsCOMPtr<nsITextServicesFilter> filter =
+ do_CreateInstance("@mozilla.org/editor/txtsrvfiltermail;1");
+ if (!filter) {
+ mPendingSpellCheck = nullptr;
+ NS_ENSURE_STATE(filter);
+ }
+ mPendingSpellCheck->SetFilter(filter);
+
+ mPendingInitEditorSpellCheckCallback = new InitEditorSpellCheckCallback(this);
+ if (!mPendingInitEditorSpellCheckCallback) {
+ mPendingSpellCheck = nullptr;
+ NS_ENSURE_STATE(mPendingInitEditorSpellCheckCallback);
+ }
+
+ nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
+ nsresult rv = mPendingSpellCheck->InitSpellChecker(
+ editor, false, mPendingInitEditorSpellCheckCallback);
+ if (NS_FAILED(rv)) {
+ mPendingSpellCheck = nullptr;
+ mPendingInitEditorSpellCheckCallback = nullptr;
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ ChangeNumPendingSpellChecks(1);
+
+ return NS_OK;
+}
+
+// Called when nsIEditorSpellCheck::InitSpellChecker completes.
+nsresult
+mozInlineSpellChecker::EditorSpellCheckInited()
+{
+ NS_ASSERTION(mPendingSpellCheck, "Spell check should be pending!");
+
+ // spell checking is enabled, register our event listeners to track navigation
+ RegisterEventListeners();
+
+ mSpellCheck = mPendingSpellCheck;
+ mPendingSpellCheck = nullptr;
+ mPendingInitEditorSpellCheckCallback = nullptr;
+ ChangeNumPendingSpellChecks(-1);
+
+ // spellcheck the current contents. SpellCheckRange doesn't supply a created
+ // range to DoSpellCheck, which in our case is the entire range. But this
+ // optimization doesn't matter because there is nothing in the spellcheck
+ // selection when starting, which triggers a better optimization.
+ return SpellCheckRange(nullptr);
+}
+
+// Changes the number of pending spell checks by the given delta. If the number
+// becomes zero or nonzero, observers are notified. See NotifyObservers for
+// info on the aEditor parameter.
+void
+mozInlineSpellChecker::ChangeNumPendingSpellChecks(int32_t aDelta,
+ nsIEditor* aEditor)
+{
+ int8_t oldNumPending = mNumPendingSpellChecks;
+ mNumPendingSpellChecks += aDelta;
+ NS_ASSERTION(mNumPendingSpellChecks >= 0,
+ "Unbalanced ChangeNumPendingSpellChecks calls!");
+ if (oldNumPending == 0 && mNumPendingSpellChecks > 0) {
+ NotifyObservers(INLINESPELL_STARTED_TOPIC, aEditor);
+ } else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) {
+ NotifyObservers(INLINESPELL_ENDED_TOPIC, aEditor);
+ }
+}
+
+// Broadcasts the given topic to observers. aEditor is passed to observers if
+// nonnull; otherwise mEditor is passed.
+void
+mozInlineSpellChecker::NotifyObservers(const char* aTopic, nsIEditor* aEditor)
+{
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (!os)
+ return;
+ nsCOMPtr<nsIEditor> editor = aEditor;
+ if (!editor) {
+ editor = do_QueryReferent(mEditor);
+ }
+ os->NotifyObservers(editor, aTopic, nullptr);
+}
+
+// mozInlineSpellChecker::SpellCheckAfterEditorChange
+//
+// Called by the editor when nearly anything happens to change the content.
+//
+// The start and end positions specify a range for the thing that happened,
+// but these are usually nullptr, even when you'd think they would be useful
+// because you want the range (for example, pasting). We ignore them in
+// this case.
+
+NS_IMETHODIMP
+mozInlineSpellChecker::SpellCheckAfterEditorChange(
+ int32_t aAction, nsISelection *aSelection,
+ nsIDOMNode *aPreviousSelectedNode, int32_t aPreviousSelectedOffset,
+ nsIDOMNode *aStartNode, int32_t aStartOffset,
+ nsIDOMNode *aEndNode, int32_t aEndOffset)
+{
+ nsresult rv;
+ NS_ENSURE_ARG_POINTER(aSelection);
+ if (!mSpellCheck)
+ return NS_OK; // disabling spell checking is not an error
+
+ // this means something has changed, and we never check the current word,
+ // therefore, we should spellcheck for subsequent caret navigations
+ mNeedsCheckAfterNavigation = true;
+
+ // the anchor node is the position of the caret
+ nsCOMPtr<nsIDOMNode> anchorNode;
+ rv = aSelection->GetAnchorNode(getter_AddRefs(anchorNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t anchorOffset;
+ rv = aSelection->GetAnchorOffset(&anchorOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozInlineSpellStatus status(this);
+ rv = status.InitForEditorChange((EditAction)aAction,
+ anchorNode, anchorOffset,
+ aPreviousSelectedNode, aPreviousSelectedOffset,
+ aStartNode, aStartOffset,
+ aEndNode, aEndOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = ScheduleSpellCheck(status);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // remember the current caret position after every change
+ SaveCurrentSelectionPosition();
+ return NS_OK;
+}
+
+// mozInlineSpellChecker::SpellCheckRange
+//
+// Spellchecks all the words in the given range.
+// Supply a nullptr range and this will check the entire editor.
+
+nsresult
+mozInlineSpellChecker::SpellCheckRange(nsIDOMRange* aRange)
+{
+ if (!mSpellCheck) {
+ NS_WARNING_ASSERTION(
+ mPendingSpellCheck,
+ "Trying to spellcheck, but checking seems to be disabled");
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ mozInlineSpellStatus status(this);
+ nsRange* range = static_cast<nsRange*>(aRange);
+ nsresult rv = status.InitForRange(range);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ScheduleSpellCheck(status);
+}
+
+// mozInlineSpellChecker::GetMisspelledWord
+
+NS_IMETHODIMP
+mozInlineSpellChecker::GetMisspelledWord(nsIDOMNode *aNode, int32_t aOffset,
+ nsIDOMRange **newword)
+{
+ NS_ENSURE_ARG_POINTER(aNode);
+ nsCOMPtr<nsISelection> spellCheckSelection;
+ nsresult res = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
+ NS_ENSURE_SUCCESS(res, res);
+
+ return IsPointInSelection(spellCheckSelection, aNode, aOffset, newword);
+}
+
+// mozInlineSpellChecker::ReplaceWord
+
+NS_IMETHODIMP
+mozInlineSpellChecker::ReplaceWord(nsIDOMNode *aNode, int32_t aOffset,
+ const nsAString &newword)
+{
+ nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
+ NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(newword.Length() != 0, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMRange> range;
+ nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range));
+ NS_ENSURE_SUCCESS(res, res);
+
+ if (range)
+ {
+ // This range was retrieved from the spellchecker selection. As
+ // ranges cannot be shared between selections, we must clone it
+ // before adding it to the editor's selection.
+ nsCOMPtr<nsIDOMRange> editorRange;
+ res = range->CloneRange(getter_AddRefs(editorRange));
+ NS_ENSURE_SUCCESS(res, res);
+
+ AutoPlaceHolderBatch phb(editor, nullptr);
+
+ nsCOMPtr<nsISelection> selection;
+ res = editor->GetSelection(getter_AddRefs(selection));
+ NS_ENSURE_SUCCESS(res, res);
+ selection->RemoveAllRanges();
+ selection->AddRange(editorRange);
+ editor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip);
+
+ nsCOMPtr<nsIPlaintextEditor> textEditor(do_QueryReferent(mEditor));
+ if (textEditor)
+ textEditor->InsertText(newword);
+ }
+
+ return NS_OK;
+}
+
+// mozInlineSpellChecker::AddWordToDictionary
+
+NS_IMETHODIMP
+mozInlineSpellChecker::AddWordToDictionary(const nsAString &word)
+{
+ NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
+
+ nsAutoString wordstr(word);
+ nsresult rv = mSpellCheck->AddWordToDictionary(wordstr.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozInlineSpellStatus status(this);
+ rv = status.InitForSelection();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ScheduleSpellCheck(status);
+}
+
+// mozInlineSpellChecker::RemoveWordFromDictionary
+
+NS_IMETHODIMP
+mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString &word)
+{
+ NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
+
+ nsAutoString wordstr(word);
+ nsresult rv = mSpellCheck->RemoveWordFromDictionary(wordstr.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozInlineSpellStatus status(this);
+ rv = status.InitForRange(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ScheduleSpellCheck(status);
+}
+
+// mozInlineSpellChecker::IgnoreWord
+
+NS_IMETHODIMP
+mozInlineSpellChecker::IgnoreWord(const nsAString &word)
+{
+ NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
+
+ nsAutoString wordstr(word);
+ nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(wordstr.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozInlineSpellStatus status(this);
+ rv = status.InitForSelection();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ScheduleSpellCheck(status);
+}
+
+// mozInlineSpellChecker::IgnoreWords
+
+NS_IMETHODIMP
+mozInlineSpellChecker::IgnoreWords(const char16_t **aWordsToIgnore,
+ uint32_t aCount)
+{
+ NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
+
+ // add each word to the ignore list and then recheck the document
+ for (uint32_t index = 0; index < aCount; index++)
+ mSpellCheck->IgnoreWordAllOccurrences(aWordsToIgnore[index]);
+
+ mozInlineSpellStatus status(this);
+ nsresult rv = status.InitForSelection();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ScheduleSpellCheck(status);
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::WillCreateNode(const nsAString & aTag, nsIDOMNode *aParent, int32_t aPosition)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::DidCreateNode(const nsAString & aTag, nsIDOMNode *aNode, nsIDOMNode *aParent,
+ int32_t aPosition, nsresult aResult)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::WillInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent,
+ int32_t aPosition)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::DidInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent,
+ int32_t aPosition, nsresult aResult)
+{
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::WillDeleteNode(nsIDOMNode *aChild)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::DidDeleteNode(nsIDOMNode *aChild, nsresult aResult)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::WillSplitNode(nsIDOMNode *aExistingRightNode, int32_t aOffset)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+mozInlineSpellChecker::DidSplitNode(nsIDOMNode *aExistingRightNode,
+ int32_t aOffset,
+ nsIDOMNode *aNewLeftNode, nsresult aResult)
+{
+ return SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0);
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::WillJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, nsIDOMNode *aParent)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::DidJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode,
+ nsIDOMNode *aParent, nsresult aResult)
+{
+ return SpellCheckBetweenNodes(aRightNode, 0, aRightNode, 0);
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::WillInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset, const nsAString & aString)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::DidInsertText(nsIDOMCharacterData *aTextNode, int32_t aOffset,
+ const nsAString & aString, nsresult aResult)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::WillDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::DidDeleteText(nsIDOMCharacterData *aTextNode, int32_t aOffset, int32_t aLength, nsresult aResult)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::WillDeleteSelection(nsISelection *aSelection)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::DidDeleteSelection(nsISelection *aSelection)
+{
+ return NS_OK;
+}
+
+// mozInlineSpellChecker::MakeSpellCheckRange
+//
+// Given begin and end positions, this function constructs a range as
+// required for ScheduleSpellCheck. If the start and end nodes are nullptr,
+// then the entire range will be selected, and you can supply -1 as the
+// offset to the end range to select all of that node.
+//
+// If the resulting range would be empty, nullptr is put into *aRange and the
+// function succeeds.
+
+nsresult
+mozInlineSpellChecker::MakeSpellCheckRange(
+ nsIDOMNode* aStartNode, int32_t aStartOffset,
+ nsIDOMNode* aEndNode, int32_t aEndOffset,
+ nsRange** aRange)
+{
+ nsresult rv;
+ *aRange = nullptr;
+
+ nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
+ NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIDOMDocument> doc;
+ rv = editor->GetDocument(getter_AddRefs(doc));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIDOMRange> range;
+ rv = doc->CreateRange(getter_AddRefs(range));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // possibly use full range of the editor
+ nsCOMPtr<nsIDOMElement> rootElem;
+ if (! aStartNode || ! aEndNode) {
+ rv = editor->GetRootElement(getter_AddRefs(rootElem));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aStartNode = rootElem;
+ aStartOffset = 0;
+
+ aEndNode = rootElem;
+ aEndOffset = -1;
+ }
+
+ if (aEndOffset == -1) {
+ nsCOMPtr<nsIDOMNodeList> childNodes;
+ rv = aEndNode->GetChildNodes(getter_AddRefs(childNodes));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t childCount;
+ rv = childNodes->GetLength(&childCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aEndOffset = childCount;
+ }
+
+ // sometimes we are are requested to check an empty range (possibly an empty
+ // document). This will result in assertions later.
+ if (aStartNode == aEndNode && aStartOffset == aEndOffset)
+ return NS_OK;
+
+ rv = range->SetStart(aStartNode, aStartOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aEndOffset)
+ rv = range->SetEnd(aEndNode, aEndOffset);
+ else
+ rv = range->SetEndAfter(aEndNode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aRange = static_cast<nsRange*>(range.forget().take());
+ return NS_OK;
+}
+
+nsresult
+mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode,
+ int32_t aStartOffset,
+ nsIDOMNode *aEndNode,
+ int32_t aEndOffset)
+{
+ RefPtr<nsRange> range;
+ nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset,
+ aEndNode, aEndOffset,
+ getter_AddRefs(range));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (! range)
+ return NS_OK; // range is empty: nothing to do
+
+ mozInlineSpellStatus status(this);
+ rv = status.InitForRange(range);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ScheduleSpellCheck(status);
+}
+
+// mozInlineSpellChecker::ShouldSpellCheckNode
+//
+// There are certain conditions when we don't want to spell check a node. In
+// particular quotations, moz signatures, etc. This routine returns false
+// for these cases.
+
+bool
+mozInlineSpellChecker::ShouldSpellCheckNode(nsIEditor* aEditor,
+ nsINode *aNode)
+{
+ MOZ_ASSERT(aNode);
+ if (!aNode->IsContent())
+ return true;
+
+ nsIContent *content = aNode->AsContent();
+
+ uint32_t flags;
+ aEditor->GetFlags(&flags);
+ if (flags & nsIPlaintextEditor::eEditorMailMask) {
+ nsIContent *parent = content->GetParent();
+ while (parent) {
+ if (parent->IsHTMLElement(nsGkAtoms::blockquote) &&
+ parent->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::type,
+ nsGkAtoms::cite,
+ eIgnoreCase)) {
+ return false;
+ }
+ if (parent->IsHTMLElement(nsGkAtoms::pre) &&
+ parent->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::_class,
+ nsGkAtoms::mozsignature,
+ eIgnoreCase)) {
+ return false;
+ }
+
+ parent = parent->GetParent();
+ }
+ } else {
+ // Check spelling only if the node is editable, and GetSpellcheck() is true
+ // on the nearest HTMLElement ancestor.
+ if (!content->IsEditable()) {
+ return false;
+ }
+
+ // Make sure that we can always turn on spell checking for inputs/textareas.
+ // Note that because of the previous check, at this point we know that the
+ // node is editable.
+ if (content->IsInAnonymousSubtree()) {
+ nsIContent *node = content->GetParent();
+ while (node && node->IsInNativeAnonymousSubtree()) {
+ node = node->GetParent();
+ }
+ nsCOMPtr<nsITextControlElement> textControl = do_QueryInterface(node);
+ if (textControl) {
+ return true;
+ }
+ }
+
+ // Get HTML element ancestor (might be aNode itself, although probably that
+ // has to be a text node in real life here)
+ nsIContent *parent = content;
+ while (!parent->IsHTMLElement()) {
+ parent = parent->GetParent();
+ if (!parent) {
+ return true;
+ }
+ }
+
+ // See if it's spellcheckable
+ return static_cast<nsGenericHTMLElement *>(parent)->Spellcheck();
+ }
+
+ return true;
+}
+
+// mozInlineSpellChecker::ScheduleSpellCheck
+//
+// This is called by code to do the actual spellchecking. We will set up
+// the proper structures for calls to DoSpellCheck.
+
+nsresult
+mozInlineSpellChecker::ScheduleSpellCheck(const mozInlineSpellStatus& aStatus)
+{
+ if (mFullSpellCheckScheduled) {
+ // Just ignore this; we're going to spell-check everything anyway
+ return NS_OK;
+ }
+
+ RefPtr<mozInlineSpellResume> resume =
+ new mozInlineSpellResume(aStatus, mDisabledAsyncToken);
+ NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);
+
+ nsresult rv = resume->Post();
+ if (NS_SUCCEEDED(rv)) {
+ if (aStatus.IsFullSpellCheck()) {
+ // We're going to check everything. Suppress further spell-check attempts
+ // until that happens.
+ mFullSpellCheckScheduled = true;
+ }
+ ChangeNumPendingSpellChecks(1);
+ }
+ return rv;
+}
+
+// mozInlineSpellChecker::DoSpellCheckSelection
+//
+// Called to re-check all misspelled words. We iterate over all ranges in
+// the selection and call DoSpellCheck on them. This is used when a word
+// is ignored or added to the dictionary: all instances of that word should
+// be removed from the selection.
+//
+// FIXME-PERFORMANCE: This takes as long as it takes and is not resumable.
+// Typically, checking this small amount of text is relatively fast, but
+// for large numbers of words, a lag may be noticeable.
+
+nsresult
+mozInlineSpellChecker::DoSpellCheckSelection(mozInlineSpellWordUtil& aWordUtil,
+ Selection* aSpellCheckSelection,
+ mozInlineSpellStatus* aStatus)
+{
+ nsresult rv;
+
+ // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges.
+ mNumWordsInSpellSelection = 0;
+
+ // Since we could be modifying the ranges for the spellCheckSelection while
+ // looping on the spell check selection, keep a separate array of range
+ // elements inside the selection
+ nsTArray<RefPtr<nsRange>> ranges;
+
+ int32_t count = aSpellCheckSelection->RangeCount();
+
+ for (int32_t idx = 0; idx < count; idx++) {
+ nsRange *range = aSpellCheckSelection->GetRangeAt(idx);
+ if (range) {
+ ranges.AppendElement(range);
+ }
+ }
+
+ // We have saved the ranges above. Clearing the spellcheck selection here
+ // isn't necessary (rechecking each word will modify it as necessary) but
+ // provides better performance. By ensuring that no ranges need to be
+ // removed in DoSpellCheck, we can save checking range inclusion which is
+ // slow.
+ aSpellCheckSelection->RemoveAllRanges();
+
+ // We use this state object for all calls, and just update its range. Note
+ // that we don't need to call FinishInit since we will be filling in the
+ // necessary information.
+ mozInlineSpellStatus status(this);
+ rv = status.InitForRange(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool doneChecking;
+ for (int32_t idx = 0; idx < count; idx++) {
+ // We can consider this word as "added" since we know it has no spell
+ // check range over it that needs to be deleted. All the old ranges
+ // were cleared above. We also need to clear the word count so that we
+ // check all words instead of stopping early.
+ status.mRange = ranges[idx];
+ rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, &status,
+ &doneChecking);
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(doneChecking,
+ "We gave the spellchecker one word, but it didn't finish checking?!?!");
+
+ status.mWordCount = 0;
+ }
+
+ return NS_OK;
+}
+
+// mozInlineSpellChecker::DoSpellCheck
+//
+// This function checks words intersecting the given range, excluding those
+// inside mStatus->mNoCheckRange (can be nullptr). Words inside aNoCheckRange
+// will have any spell selection removed (this is used to hide the
+// underlining for the word that the caret is in). aNoCheckRange should be
+// on word boundaries.
+//
+// mResume->mCreatedRange is a possibly nullptr range of new text that was
+// inserted. Inside this range, we don't bother to check whether things are
+// inside the spellcheck selection, which speeds up large paste operations
+// considerably.
+//
+// Normal case when editing text by typing
+// h e l l o w o r k d h o w a r e y o u
+// ^ caret
+// [-------] mRange
+// [-------] mNoCheckRange
+// -> does nothing (range is the same as the no check range)
+//
+// Case when pasting:
+// [---------- pasted text ----------]
+// h e l l o w o r k d h o w a r e y o u
+// ^ caret
+// [---] aNoCheckRange
+// -> recheck all words in range except those in aNoCheckRange
+//
+// If checking is complete, *aDoneChecking will be set. If there is more
+// but we ran out of time, this will be false and the range will be
+// updated with the stuff that still needs checking.
+
+nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil,
+ Selection *aSpellCheckSelection,
+ mozInlineSpellStatus* aStatus,
+ bool* aDoneChecking)
+{
+ *aDoneChecking = true;
+
+ NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
+
+ // get the editor for ShouldSpellCheckNode, this may fail in reasonable
+ // circumstances since the editor could have gone away
+ nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
+ if (! editor)
+ return NS_ERROR_FAILURE;
+
+ if (aStatus->mRange->Collapsed())
+ return NS_OK;
+
+ // see if the selection has any ranges, if not, then we can optimize checking
+ // range inclusion later (we have no ranges when we are initially checking or
+ // when there are no misspelled words yet).
+ int32_t originalRangeCount = aSpellCheckSelection->RangeCount();
+
+ // set the starting DOM position to be the beginning of our range
+ {
+ // Scope for the node/offset pairs here so they don't get
+ // accidentally used later
+ nsINode* beginNode = aStatus->mRange->GetStartParent();
+ int32_t beginOffset = aStatus->mRange->StartOffset();
+ nsINode* endNode = aStatus->mRange->GetEndParent();
+ int32_t endOffset = aStatus->mRange->EndOffset();
+
+ // Now check that we're still looking at a range that's under
+ // aWordUtil.GetRootNode()
+ nsINode* rootNode = aWordUtil.GetRootNode();
+ if (!nsContentUtils::ContentIsDescendantOf(beginNode, rootNode) ||
+ !nsContentUtils::ContentIsDescendantOf(endNode, rootNode)) {
+ // Just bail out and don't try to spell-check this
+ return NS_OK;
+ }
+
+ aWordUtil.SetEnd(endNode, endOffset);
+ aWordUtil.SetPosition(beginNode, beginOffset);
+ }
+
+ // aWordUtil.SetPosition flushes pending notifications, check editor again.
+ // XXX Uhhh, *we're* holding a strong ref to the editor.
+ editor = do_QueryReferent(mEditor);
+ if (! editor)
+ return NS_ERROR_FAILURE;
+
+ int32_t wordsSinceTimeCheck = 0;
+ PRTime beginTime = PR_Now();
+
+ nsAutoString wordText;
+ RefPtr<nsRange> wordRange;
+ bool dontCheckWord;
+ while (NS_SUCCEEDED(aWordUtil.GetNextWord(wordText,
+ getter_AddRefs(wordRange),
+ &dontCheckWord)) &&
+ wordRange) {
+ wordsSinceTimeCheck++;
+
+ // get the range for the current word.
+ nsINode *beginNode;
+ nsINode *endNode;
+ int32_t beginOffset, endOffset;
+
+ ErrorResult erv;
+ beginNode = wordRange->GetStartContainer(erv);
+ endNode = wordRange->GetEndContainer(erv);
+ beginOffset = wordRange->GetStartOffset(erv);
+ endOffset = wordRange->GetEndOffset(erv);
+
+#ifdef DEBUG_INLINESPELL
+ printf("->Got word \"%s\"", NS_ConvertUTF16toUTF8(wordText).get());
+ if (dontCheckWord)
+ printf(" (not checking)");
+ printf("\n");
+#endif
+
+ // see if there is a spellcheck range that already intersects the word
+ // and remove it. We only need to remove old ranges, so don't bother if
+ // there were no ranges when we started out.
+ if (originalRangeCount > 0) {
+ // likewise, if this word is inside new text, we won't bother testing
+ if (!aStatus->mCreatedRange ||
+ !aStatus->mCreatedRange->IsPointInRange(*beginNode, beginOffset, erv)) {
+ nsTArray<RefPtr<nsRange>> ranges;
+ aSpellCheckSelection->GetRangesForInterval(*beginNode, beginOffset,
+ *endNode, endOffset,
+ true, ranges, erv);
+ ENSURE_SUCCESS(erv, erv.StealNSResult());
+ for (uint32_t i = 0; i < ranges.Length(); i++)
+ RemoveRange(aSpellCheckSelection, ranges[i]);
+ }
+ }
+
+ // some words are special and don't need checking
+ if (dontCheckWord)
+ continue;
+
+ // some nodes we don't spellcheck
+ if (!ShouldSpellCheckNode(editor, beginNode))
+ continue;
+
+ // Don't check spelling if we're inside the noCheckRange. This needs to
+ // be done after we clear any old selection because the excluded word
+ // might have been previously marked.
+ //
+ // We do a simple check to see if the beginning of our word is in the
+ // exclusion range. Because the exclusion range is a multiple of a word,
+ // this is sufficient.
+ if (aStatus->mNoCheckRange &&
+ aStatus->mNoCheckRange->IsPointInRange(*beginNode, beginOffset, erv)) {
+ continue;
+ }
+
+ // check spelling and add to selection if misspelled
+ bool isMisspelled;
+ aWordUtil.NormalizeWord(wordText);
+ nsresult rv = mSpellCheck->CheckCurrentWordNoSuggest(wordText.get(), &isMisspelled);
+ if (NS_FAILED(rv))
+ continue;
+
+ if (isMisspelled) {
+ // misspelled words count extra toward the max
+ wordsSinceTimeCheck += MISSPELLED_WORD_COUNT_PENALTY;
+ AddRange(aSpellCheckSelection, wordRange);
+
+ aStatus->mWordCount ++;
+ if (aStatus->mWordCount >= mMaxMisspellingsPerCheck ||
+ SpellCheckSelectionIsFull())
+ break;
+ }
+
+ // see if we've run out of time, only check every N words for perf
+ if (wordsSinceTimeCheck >= INLINESPELL_TIMEOUT_CHECK_FREQUENCY) {
+ wordsSinceTimeCheck = 0;
+ if (PR_Now() > PRTime(beginTime + INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC)) {
+ // stop checking, our time limit has been exceeded
+
+ // move the range to encompass the stuff that needs checking
+ rv = aStatus->mRange->SetStart(endNode, endOffset);
+ if (NS_FAILED(rv)) {
+ // The range might be unhappy because the beginning is after the
+ // end. This is possible when the requested end was in the middle
+ // of a word, just ignore this situation and assume we're done.
+ return NS_OK;
+ }
+ *aDoneChecking = false;
+ return NS_OK;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+// An RAII helper that calls ChangeNumPendingSpellChecks on destruction.
+class AutoChangeNumPendingSpellChecks
+{
+public:
+ AutoChangeNumPendingSpellChecks(mozInlineSpellChecker* aSpellChecker,
+ int32_t aDelta)
+ : mSpellChecker(aSpellChecker), mDelta(aDelta) {}
+
+ ~AutoChangeNumPendingSpellChecks()
+ {
+ mSpellChecker->ChangeNumPendingSpellChecks(mDelta);
+ }
+
+private:
+ RefPtr<mozInlineSpellChecker> mSpellChecker;
+ int32_t mDelta;
+};
+
+// mozInlineSpellChecker::ResumeCheck
+//
+// Called by the resume event when it fires. We will try to pick up where
+// the last resume left off.
+
+nsresult
+mozInlineSpellChecker::ResumeCheck(mozInlineSpellStatus* aStatus)
+{
+ // Observers should be notified that spell check has ended only after spell
+ // check is done below, but since there are many early returns in this method
+ // and the number of pending spell checks must be decremented regardless of
+ // whether the spell check actually happens, use this RAII object.
+ AutoChangeNumPendingSpellChecks autoChangeNumPending(this, -1);
+
+ if (aStatus->IsFullSpellCheck()) {
+ // Allow posting new spellcheck resume events from inside
+ // ResumeCheck, now that we're actually firing.
+ NS_ASSERTION(mFullSpellCheckScheduled,
+ "How could this be false? The full spell check is "
+ "calling us!!");
+ mFullSpellCheckScheduled = false;
+ }
+
+ if (! mSpellCheck)
+ return NS_OK; // spell checking has been turned off
+
+ nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
+ if (! editor)
+ return NS_OK; // editor is gone
+
+ mozInlineSpellWordUtil wordUtil;
+ nsresult rv = wordUtil.Init(mEditor);
+ if (NS_FAILED(rv))
+ return NS_OK; // editor doesn't like us, don't assert
+
+ nsCOMPtr<nsISelection> spellCheckSelectionRef;
+ rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelectionRef));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ auto spellCheckSelection =
+ static_cast<Selection *>(spellCheckSelectionRef.get());
+
+ nsAutoString currentDictionary;
+ rv = mSpellCheck->GetCurrentDictionary(currentDictionary);
+ if (NS_FAILED(rv)) {
+ // no active dictionary
+ int32_t count = spellCheckSelection->RangeCount();
+ for (int32_t index = count - 1; index >= 0; index--) {
+ nsRange *checkRange = spellCheckSelection->GetRangeAt(index);
+ if (checkRange) {
+ RemoveRange(spellCheckSelection, checkRange);
+ }
+ }
+ return NS_OK;
+ }
+
+ CleanupRangesInSelection(spellCheckSelection);
+
+ rv = aStatus->FinishInitOnEvent(wordUtil);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (! aStatus->mRange)
+ return NS_OK; // empty range, nothing to do
+
+ bool doneChecking = true;
+ if (aStatus->mOp == mozInlineSpellStatus::eOpSelection)
+ rv = DoSpellCheckSelection(wordUtil, spellCheckSelection, aStatus);
+ else
+ rv = DoSpellCheck(wordUtil, spellCheckSelection, aStatus, &doneChecking);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (! doneChecking)
+ rv = ScheduleSpellCheck(*aStatus);
+ return rv;
+}
+
+// mozInlineSpellChecker::IsPointInSelection
+//
+// Determines if a given (node,offset) point is inside the given
+// selection. If so, the specific range of the selection that
+// intersects is places in *aRange. (There may be multiple disjoint
+// ranges in a selection.)
+//
+// If there is no intersection, *aRange will be nullptr.
+
+nsresult
+mozInlineSpellChecker::IsPointInSelection(nsISelection *aSelection,
+ nsIDOMNode *aNode,
+ int32_t aOffset,
+ nsIDOMRange **aRange)
+{
+ *aRange = nullptr;
+
+ nsCOMPtr<nsISelectionPrivate> privSel(do_QueryInterface(aSelection));
+
+ nsTArray<nsRange*> ranges;
+ nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
+ nsresult rv = privSel->GetRangesForIntervalArray(node, aOffset, node, aOffset,
+ true, &ranges);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (ranges.Length() == 0)
+ return NS_OK; // no matches
+
+ // there may be more than one range returned, and we don't know what do
+ // do with that, so just get the first one
+ NS_ADDREF(*aRange = ranges[0]);
+ return NS_OK;
+}
+
+nsresult
+mozInlineSpellChecker::CleanupRangesInSelection(Selection *aSelection)
+{
+ // integrity check - remove ranges that have collapsed to nothing. This
+ // can happen if the node containing a highlighted word was removed.
+ if (!aSelection)
+ return NS_ERROR_FAILURE;
+
+ int32_t count = aSelection->RangeCount();
+
+ for (int32_t index = 0; index < count; index++)
+ {
+ nsRange *checkRange = aSelection->GetRangeAt(index);
+ if (checkRange)
+ {
+ if (checkRange->Collapsed())
+ {
+ RemoveRange(aSelection, checkRange);
+ index--;
+ count--;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+
+// mozInlineSpellChecker::RemoveRange
+//
+// For performance reasons, we have an upper bound on the number of word
+// ranges in the spell check selection. When removing a range from the
+// selection, we need to decrement mNumWordsInSpellSelection
+
+nsresult
+mozInlineSpellChecker::RemoveRange(Selection *aSpellCheckSelection,
+ nsRange *aRange)
+{
+ NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
+ NS_ENSURE_ARG_POINTER(aRange);
+
+ ErrorResult rv;
+ aSpellCheckSelection->RemoveRange(*aRange, rv);
+ if (!rv.Failed() && mNumWordsInSpellSelection)
+ mNumWordsInSpellSelection--;
+
+ return rv.StealNSResult();
+}
+
+
+// mozInlineSpellChecker::AddRange
+//
+// For performance reasons, we have an upper bound on the number of word
+// ranges we'll add to the spell check selection. Once we reach that upper
+// bound, stop adding the ranges
+
+nsresult
+mozInlineSpellChecker::AddRange(nsISelection* aSpellCheckSelection,
+ nsIDOMRange* aRange)
+{
+ NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
+ NS_ENSURE_ARG_POINTER(aRange);
+
+ nsresult rv = NS_OK;
+
+ if (!SpellCheckSelectionIsFull())
+ {
+ rv = aSpellCheckSelection->AddRange(aRange);
+ if (NS_SUCCEEDED(rv))
+ mNumWordsInSpellSelection++;
+ }
+
+ return rv;
+}
+
+nsresult mozInlineSpellChecker::GetSpellCheckSelection(nsISelection ** aSpellCheckSelection)
+{
+ nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
+ NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsISelectionController> selcon;
+ nsresult rv = editor->GetSelectionController(getter_AddRefs(selcon));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return selcon->GetSelection(nsISelectionController::SELECTION_SPELLCHECK, aSpellCheckSelection);
+}
+
+nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition()
+{
+ nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
+ NS_ENSURE_TRUE(editor, NS_OK);
+
+ // figure out the old caret position based on the current selection
+ nsCOMPtr<nsISelection> selection;
+ nsresult rv = editor->GetSelection(getter_AddRefs(selection));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = selection->GetFocusNode(getter_AddRefs(mCurrentSelectionAnchorNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ selection->GetFocusOffset(&mCurrentSelectionOffset);
+
+ return NS_OK;
+}
+
+// This is a copy of nsContentUtils::ContentIsDescendantOf. Another crime
+// for XPCOM's rap sheet
+bool // static
+ContentIsDescendantOf(nsINode* aPossibleDescendant,
+ nsINode* aPossibleAncestor)
+{
+ NS_PRECONDITION(aPossibleDescendant, "The possible descendant is null!");
+ NS_PRECONDITION(aPossibleAncestor, "The possible ancestor is null!");
+
+ do {
+ if (aPossibleDescendant == aPossibleAncestor)
+ return true;
+ aPossibleDescendant = aPossibleDescendant->GetParentNode();
+ } while (aPossibleDescendant);
+
+ return false;
+}
+
+// mozInlineSpellChecker::HandleNavigationEvent
+//
+// Acts upon mouse clicks and keyboard navigation changes, spell checking
+// the previous word if the new navigation location moves us to another
+// word.
+//
+// This is complicated by the fact that our mouse events are happening after
+// selection has been changed to account for the mouse click. But keyboard
+// events are happening before the caret selection has changed. Working
+// around this by letting keyboard events setting forceWordSpellCheck to
+// true. aNewPositionOffset also tries to work around this for the
+// DOM_VK_RIGHT and DOM_VK_LEFT cases.
+
+nsresult
+mozInlineSpellChecker::HandleNavigationEvent(bool aForceWordSpellCheck,
+ int32_t aNewPositionOffset)
+{
+ nsresult rv;
+
+ // If we already handled the navigation event and there is no possibility
+ // anything has changed since then, we don't have to do anything. This
+ // optimization makes a noticeable difference when you hold down a navigation
+ // key like Page Down.
+ if (! mNeedsCheckAfterNavigation)
+ return NS_OK;
+
+ nsCOMPtr<nsIDOMNode> currentAnchorNode = mCurrentSelectionAnchorNode;
+ int32_t currentAnchorOffset = mCurrentSelectionOffset;
+
+ // now remember the new focus position resulting from the event
+ rv = SaveCurrentSelectionPosition();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool shouldPost;
+ mozInlineSpellStatus status(this);
+ rv = status.InitForNavigation(aForceWordSpellCheck, aNewPositionOffset,
+ currentAnchorNode, currentAnchorOffset,
+ mCurrentSelectionAnchorNode, mCurrentSelectionOffset,
+ &shouldPost);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (shouldPost) {
+ rv = ScheduleSpellCheck(status);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(nsIDOMEvent* aEvent)
+{
+ nsAutoString eventType;
+ aEvent->GetType(eventType);
+
+ if (eventType.EqualsLiteral("blur")) {
+ return Blur(aEvent);
+ }
+ if (eventType.EqualsLiteral("click")) {
+ return MouseClick(aEvent);
+ }
+ if (eventType.EqualsLiteral("keypress")) {
+ return KeyPress(aEvent);
+ }
+
+ return NS_OK;
+}
+
+nsresult mozInlineSpellChecker::Blur(nsIDOMEvent* aEvent)
+{
+ // force spellcheck on blur, for instance when tabbing out of a textbox
+ HandleNavigationEvent(true);
+ return NS_OK;
+}
+
+nsresult mozInlineSpellChecker::MouseClick(nsIDOMEvent *aMouseEvent)
+{
+ nsCOMPtr<nsIDOMMouseEvent>mouseEvent = do_QueryInterface(aMouseEvent);
+ NS_ENSURE_TRUE(mouseEvent, NS_OK);
+
+ // ignore any errors from HandleNavigationEvent as we don't want to prevent
+ // anyone else from seeing this event.
+ int16_t button;
+ mouseEvent->GetButton(&button);
+ HandleNavigationEvent(button != 0);
+ return NS_OK;
+}
+
+nsresult mozInlineSpellChecker::KeyPress(nsIDOMEvent* aKeyEvent)
+{
+ nsCOMPtr<nsIDOMKeyEvent>keyEvent = do_QueryInterface(aKeyEvent);
+ NS_ENSURE_TRUE(keyEvent, NS_OK);
+
+ uint32_t keyCode;
+ keyEvent->GetKeyCode(&keyCode);
+
+ // we only care about navigation keys that moved selection
+ switch (keyCode)
+ {
+ case nsIDOMKeyEvent::DOM_VK_RIGHT:
+ case nsIDOMKeyEvent::DOM_VK_LEFT:
+ HandleNavigationEvent(false, keyCode == nsIDOMKeyEvent::DOM_VK_RIGHT ? 1 : -1);
+ break;
+ case nsIDOMKeyEvent::DOM_VK_UP:
+ case nsIDOMKeyEvent::DOM_VK_DOWN:
+ case nsIDOMKeyEvent::DOM_VK_HOME:
+ case nsIDOMKeyEvent::DOM_VK_END:
+ case nsIDOMKeyEvent::DOM_VK_PAGE_UP:
+ case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN:
+ HandleNavigationEvent(true /* force a spelling correction */);
+ break;
+ }
+
+ return NS_OK;
+}
+
+// Used as the nsIEditorSpellCheck::UpdateCurrentDictionary callback.
+class UpdateCurrentDictionaryCallback final : public nsIEditorSpellCheckCallback
+{
+public:
+ NS_DECL_ISUPPORTS
+
+ explicit UpdateCurrentDictionaryCallback(mozInlineSpellChecker* aSpellChecker,
+ uint32_t aDisabledAsyncToken)
+ : mSpellChecker(aSpellChecker), mDisabledAsyncToken(aDisabledAsyncToken) {}
+
+ NS_IMETHOD EditorSpellCheckDone() override
+ {
+ // Ignore this callback if SetEnableRealTimeSpell(false) was called after
+ // the UpdateCurrentDictionary call that triggered it.
+ return mSpellChecker->mDisabledAsyncToken > mDisabledAsyncToken ?
+ NS_OK :
+ mSpellChecker->CurrentDictionaryUpdated();
+ }
+
+private:
+ ~UpdateCurrentDictionaryCallback() {}
+
+ RefPtr<mozInlineSpellChecker> mSpellChecker;
+ uint32_t mDisabledAsyncToken;
+};
+NS_IMPL_ISUPPORTS(UpdateCurrentDictionaryCallback, nsIEditorSpellCheckCallback)
+
+NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary()
+{
+ // mSpellCheck is null and mPendingSpellCheck is nonnull while the spell
+ // checker is being initialized. Calling UpdateCurrentDictionary on
+ // mPendingSpellCheck simply queues the dictionary update after the init.
+ nsCOMPtr<nsIEditorSpellCheck> spellCheck = mSpellCheck ? mSpellCheck :
+ mPendingSpellCheck;
+ if (!spellCheck) {
+ return NS_OK;
+ }
+
+ if (NS_FAILED(spellCheck->GetCurrentDictionary(mPreviousDictionary))) {
+ mPreviousDictionary.Truncate();
+ }
+
+ RefPtr<UpdateCurrentDictionaryCallback> cb =
+ new UpdateCurrentDictionaryCallback(this, mDisabledAsyncToken);
+ NS_ENSURE_STATE(cb);
+ nsresult rv = spellCheck->UpdateCurrentDictionary(cb);
+ if (NS_FAILED(rv)) {
+ cb = nullptr;
+ return rv;
+ }
+ mNumPendingUpdateCurrentDictionary++;
+ ChangeNumPendingSpellChecks(1);
+
+ return NS_OK;
+}
+
+// Called when nsIEditorSpellCheck::UpdateCurrentDictionary completes.
+nsresult mozInlineSpellChecker::CurrentDictionaryUpdated()
+{
+ mNumPendingUpdateCurrentDictionary--;
+ NS_ASSERTION(mNumPendingUpdateCurrentDictionary >= 0,
+ "CurrentDictionaryUpdated called without corresponding "
+ "UpdateCurrentDictionary call!");
+ ChangeNumPendingSpellChecks(-1);
+
+ nsAutoString currentDictionary;
+ if (!mSpellCheck ||
+ NS_FAILED(mSpellCheck->GetCurrentDictionary(currentDictionary))) {
+ currentDictionary.Truncate();
+ }
+
+ nsresult rv = SpellCheckRange(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+mozInlineSpellChecker::GetSpellCheckPending(bool* aPending)
+{
+ *aPending = mNumPendingSpellChecks > 0;
+ return NS_OK;
+}
diff --git a/extensions/spellcheck/src/mozInlineSpellChecker.h b/extensions/spellcheck/src/mozInlineSpellChecker.h
new file mode 100644
index 000000000..86d91c2c0
--- /dev/null
+++ b/extensions/spellcheck/src/mozInlineSpellChecker.h
@@ -0,0 +1,272 @@
+/* -*- 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 __mozinlinespellchecker_h__
+#define __mozinlinespellchecker_h__
+
+#include "mozilla/EditorBase.h"
+#include "nsRange.h"
+#include "nsIEditorSpellCheck.h"
+#include "nsIEditActionListener.h"
+#include "nsIInlineSpellChecker.h"
+#include "nsIDOMTreeWalker.h"
+#include "nsWeakReference.h"
+#include "nsIDOMEventListener.h"
+#include "nsWeakReference.h"
+#include "mozISpellI18NUtil.h"
+#include "nsCycleCollectionParticipant.h"
+
+// X.h defines KeyPress
+#ifdef KeyPress
+#undef KeyPress
+#endif
+
+class mozInlineSpellWordUtil;
+class mozInlineSpellChecker;
+class mozInlineSpellResume;
+class InitEditorSpellCheckCallback;
+class UpdateCurrentDictionaryCallback;
+class mozInlineSpellResume;
+
+class mozInlineSpellStatus
+{
+public:
+ explicit mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker);
+
+ nsresult InitForEditorChange(EditAction aAction,
+ nsIDOMNode* aAnchorNode, int32_t aAnchorOffset,
+ nsIDOMNode* aPreviousNode, int32_t aPreviousOffset,
+ nsIDOMNode* aStartNode, int32_t aStartOffset,
+ nsIDOMNode* aEndNode, int32_t aEndOffset);
+ nsresult InitForNavigation(bool aForceCheck, int32_t aNewPositionOffset,
+ nsIDOMNode* aOldAnchorNode, int32_t aOldAnchorOffset,
+ nsIDOMNode* aNewAnchorNode, int32_t aNewAnchorOffset,
+ bool* aContinue);
+ nsresult InitForSelection();
+ nsresult InitForRange(nsRange* aRange);
+
+ nsresult FinishInitOnEvent(mozInlineSpellWordUtil& aWordUtil);
+
+ // Return true if we plan to spell-check everything
+ bool IsFullSpellCheck() const {
+ return mOp == eOpChange && !mRange;
+ }
+
+ RefPtr<mozInlineSpellChecker> mSpellChecker;
+
+ // The total number of words checked in this sequence, using this tally tells
+ // us when to stop. This count is preserved as we continue checking in new
+ // messages.
+ int32_t mWordCount;
+
+ // what happened?
+ enum Operation { eOpChange, // for SpellCheckAfterChange except deleteSelection
+ eOpChangeDelete, // for SpellCheckAfterChange deleteSelection
+ eOpNavigation, // for HandleNavigationEvent
+ eOpSelection, // re-check all misspelled words
+ eOpResume }; // for resuming a previously started check
+ Operation mOp;
+
+ // Used for events where we have already computed the range to use. It can
+ // also be nullptr in these cases where we need to check the entire range.
+ RefPtr<nsRange> mRange;
+
+ // If we happen to know something was inserted, this is that range.
+ // Can be nullptr (this only allows an optimization, so not setting doesn't hurt)
+ RefPtr<nsRange> mCreatedRange;
+
+ // Contains the range computed for the current word. Can be nullptr.
+ RefPtr<nsRange> mNoCheckRange;
+
+ // Indicates the position of the cursor for the event (so we can compute
+ // mNoCheckRange). It can be nullptr if we don't care about the cursor position
+ // (such as for the intial check of everything).
+ //
+ // For mOp == eOpNavigation, this is the NEW position of the cursor
+ nsCOMPtr<nsIDOMRange> mAnchorRange;
+
+ // -----
+ // The following members are only for navigation events and are only
+ // stored for FinishNavigationEvent to initialize the other members.
+ // -----
+
+ // this is the OLD position of the cursor
+ nsCOMPtr<nsIDOMRange> mOldNavigationAnchorRange;
+
+ // Set when we should force checking the current word. See
+ // mozInlineSpellChecker::HandleNavigationEvent for a description of why we
+ // have this.
+ bool mForceNavigationWordCheck;
+
+ // Contains the offset passed in to HandleNavigationEvent
+ int32_t mNewNavigationPositionOffset;
+
+protected:
+ nsresult FinishNavigationEvent(mozInlineSpellWordUtil& aWordUtil);
+
+ nsresult FillNoCheckRangeFromAnchor(mozInlineSpellWordUtil& aWordUtil);
+
+ nsresult GetDocument(nsIDOMDocument** aDocument);
+ nsresult PositionToCollapsedRange(nsIDOMDocument* aDocument,
+ nsIDOMNode* aNode, int32_t aOffset,
+ nsIDOMRange** aRange);
+};
+
+class mozInlineSpellChecker final : public nsIInlineSpellChecker,
+ public nsIEditActionListener,
+ public nsIDOMEventListener,
+ public nsSupportsWeakReference
+{
+private:
+ friend class mozInlineSpellStatus;
+ friend class InitEditorSpellCheckCallback;
+ friend class UpdateCurrentDictionaryCallback;
+ friend class AutoChangeNumPendingSpellChecks;
+ friend class mozInlineSpellResume;
+
+ // Access with CanEnableInlineSpellChecking
+ enum SpellCheckingState { SpellCheck_Uninitialized = -1,
+ SpellCheck_NotAvailable = 0,
+ SpellCheck_Available = 1};
+ static SpellCheckingState gCanEnableSpellChecking;
+
+ nsWeakPtr mEditor;
+ nsCOMPtr<nsIEditorSpellCheck> mSpellCheck;
+ nsCOMPtr<nsIEditorSpellCheck> mPendingSpellCheck;
+ nsCOMPtr<nsIDOMTreeWalker> mTreeWalker;
+ nsCOMPtr<mozISpellI18NUtil> mConverter;
+
+ int32_t mNumWordsInSpellSelection;
+ int32_t mMaxNumWordsInSpellSelection;
+
+ // How many misspellings we can add at once. This is often less than the max
+ // total number of misspellings. When you have a large textarea prepopulated
+ // with text with many misspellings, we can hit this limit. By making it
+ // lower than the total number of misspelled words, new text typed by the
+ // user can also have spellchecking in it.
+ int32_t mMaxMisspellingsPerCheck;
+
+ // we need to keep track of the current text position in the document
+ // so we can spell check the old word when the user clicks around the document.
+ nsCOMPtr<nsIDOMNode> mCurrentSelectionAnchorNode;
+ int32_t mCurrentSelectionOffset;
+
+ // Tracks the number of pending spell checks *and* async operations that may
+ // lead to spell checks, like updating the current dictionary. This is
+ // necessary so that observers can know when to wait for spell check to
+ // complete.
+ int32_t mNumPendingSpellChecks;
+
+ // The number of calls to UpdateCurrentDictionary that haven't finished yet.
+ int32_t mNumPendingUpdateCurrentDictionary;
+
+ // This number is incremented each time the spell checker is disabled so that
+ // pending scheduled spell checks and UpdateCurrentDictionary calls can be
+ // ignored when they finish.
+ uint32_t mDisabledAsyncToken;
+
+ // When mPendingSpellCheck is non-null, this is the callback passed when
+ // it was initialized.
+ RefPtr<InitEditorSpellCheckCallback> mPendingInitEditorSpellCheckCallback;
+
+ // Set when we have spellchecked after the last edit operation. See the
+ // commment at the top of the .cpp file for more info.
+ bool mNeedsCheckAfterNavigation;
+
+ // Set when we have a pending mozInlineSpellResume which will check
+ // the whole document.
+ bool mFullSpellCheckScheduled;
+
+ // Maintains state during the asynchronous UpdateCurrentDictionary call.
+ nsString mPreviousDictionary;
+
+public:
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_NSIEDITACTIONLISTENER
+ NS_DECL_NSIINLINESPELLCHECKER
+ NS_DECL_NSIDOMEVENTLISTENER
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(mozInlineSpellChecker, nsIDOMEventListener)
+
+ // returns true if there are any spell checking dictionaries available
+ static bool CanEnableInlineSpellChecking();
+ // update the cached value whenever the list of available dictionaries changes
+ static void UpdateCanEnableInlineSpellChecking();
+
+ nsresult Blur(nsIDOMEvent* aEvent);
+ nsresult MouseClick(nsIDOMEvent* aMouseEvent);
+ nsresult KeyPress(nsIDOMEvent* aKeyEvent);
+
+ mozInlineSpellChecker();
+
+ // spell checks all of the words between two nodes
+ nsresult SpellCheckBetweenNodes(nsIDOMNode *aStartNode,
+ int32_t aStartOffset,
+ nsIDOMNode *aEndNode,
+ int32_t aEndOffset);
+
+ // examines the dom node in question and returns true if the inline spell
+ // checker should skip the node (i.e. the text is inside of a block quote
+ // or an e-mail signature...)
+ bool ShouldSpellCheckNode(nsIEditor* aEditor, nsINode *aNode);
+
+ nsresult SpellCheckAfterChange(nsIDOMNode* aCursorNode, int32_t aCursorOffset,
+ nsIDOMNode* aPreviousNode, int32_t aPreviousOffset,
+ nsISelection* aSpellCheckSelection);
+
+ // spell check the text contained within aRange, potentially scheduling
+ // another check in the future if the time threshold is reached
+ nsresult ScheduleSpellCheck(const mozInlineSpellStatus& aStatus);
+
+ nsresult DoSpellCheckSelection(mozInlineSpellWordUtil& aWordUtil,
+ mozilla::dom::Selection* aSpellCheckSelection,
+ mozInlineSpellStatus* aStatus);
+ nsresult DoSpellCheck(mozInlineSpellWordUtil& aWordUtil,
+ mozilla::dom::Selection *aSpellCheckSelection,
+ mozInlineSpellStatus* aStatus,
+ bool* aDoneChecking);
+
+ // helper routine to determine if a point is inside of the passed in selection.
+ nsresult IsPointInSelection(nsISelection *aSelection,
+ nsIDOMNode *aNode,
+ int32_t aOffset,
+ nsIDOMRange **aRange);
+
+ nsresult CleanupRangesInSelection(mozilla::dom::Selection *aSelection);
+
+ nsresult RemoveRange(mozilla::dom::Selection *aSpellCheckSelection,
+ nsRange *aRange);
+ nsresult AddRange(nsISelection *aSpellCheckSelection, nsIDOMRange * aRange);
+ bool SpellCheckSelectionIsFull() { return mNumWordsInSpellSelection >= mMaxNumWordsInSpellSelection; }
+
+ nsresult MakeSpellCheckRange(nsIDOMNode* aStartNode, int32_t aStartOffset,
+ nsIDOMNode* aEndNode, int32_t aEndOffset,
+ nsRange** aRange);
+
+ // DOM and editor event registration helper routines
+ nsresult RegisterEventListeners();
+ nsresult UnregisterEventListeners();
+ nsresult HandleNavigationEvent(bool aForceWordSpellCheck, int32_t aNewPositionOffset = 0);
+
+ nsresult GetSpellCheckSelection(nsISelection ** aSpellCheckSelection);
+ nsresult SaveCurrentSelectionPosition();
+
+ nsresult ResumeCheck(mozInlineSpellStatus* aStatus);
+
+protected:
+ virtual ~mozInlineSpellChecker();
+
+ // called when async nsIEditorSpellCheck methods complete
+ nsresult EditorSpellCheckInited();
+ nsresult CurrentDictionaryUpdated();
+
+ // track the number of pending spell checks and async operations that may lead
+ // to spell checks, notifying observers accordingly
+ void ChangeNumPendingSpellChecks(int32_t aDelta,
+ nsIEditor* aEditor = nullptr);
+ void NotifyObservers(const char* aTopic, nsIEditor* aEditor);
+};
+
+#endif /* __mozinlinespellchecker_h__ */
diff --git a/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp b/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp
new file mode 100644
index 000000000..3aef1533d
--- /dev/null
+++ b/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp
@@ -0,0 +1,1085 @@
+/* -*- 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 "mozInlineSpellWordUtil.h"
+#include "nsDebug.h"
+#include "nsIAtom.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIDOMCSSStyleDeclaration.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMRange.h"
+#include "nsIEditor.h"
+#include "nsIDOMNode.h"
+#include "nsUnicharUtilCIID.h"
+#include "nsUnicodeProperties.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIContent.h"
+#include "nsTextFragment.h"
+#include "mozilla/dom/Element.h"
+#include "nsRange.h"
+#include "nsContentUtils.h"
+#include "nsIFrame.h"
+#include <algorithm>
+#include "mozilla/BinarySearch.h"
+
+using namespace mozilla;
+
+// IsIgnorableCharacter
+//
+// These characters are ones that we should ignore in input.
+
+inline bool IsIgnorableCharacter(char16_t ch)
+{
+ return (ch == 0xAD || // SOFT HYPHEN
+ ch == 0x1806); // MONGOLIAN TODO SOFT HYPHEN
+}
+
+// IsConditionalPunctuation
+//
+// Some characters (like apostrophes) require characters on each side to be
+// part of a word, and are otherwise punctuation.
+
+inline bool IsConditionalPunctuation(char16_t ch)
+{
+ return (ch == '\'' ||
+ ch == 0x2019 || // RIGHT SINGLE QUOTATION MARK
+ ch == 0x00B7); // MIDDLE DOT
+}
+
+// mozInlineSpellWordUtil::Init
+
+nsresult
+mozInlineSpellWordUtil::Init(nsWeakPtr aWeakEditor)
+{
+ nsresult rv;
+
+ // getting the editor can fail commonly because the editor was detached, so
+ // don't assert
+ nsCOMPtr<nsIEditor> editor = do_QueryReferent(aWeakEditor, &rv);
+ if (NS_FAILED(rv))
+ return rv;
+
+ nsCOMPtr<nsIDOMDocument> domDoc;
+ rv = editor->GetDocument(getter_AddRefs(domDoc));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(domDoc, NS_ERROR_NULL_POINTER);
+
+ mDOMDocument = domDoc;
+ mDocument = do_QueryInterface(domDoc);
+
+ // Find the root node for the editor. For contenteditable we'll need something
+ // cleverer here.
+ nsCOMPtr<nsIDOMElement> rootElt;
+ rv = editor->GetRootElement(getter_AddRefs(rootElt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsINode> rootNode = do_QueryInterface(rootElt);
+ mRootNode = rootNode;
+ NS_ASSERTION(mRootNode, "GetRootElement returned null *and* claimed to suceed!");
+ return NS_OK;
+}
+
+static inline bool
+IsTextNode(nsINode* aNode)
+{
+ return aNode->IsNodeOfType(nsINode::eTEXT);
+}
+
+typedef void (* OnLeaveNodeFunPtr)(nsINode* aNode, void* aClosure);
+
+// Find the next node in the DOM tree in preorder.
+// Calls OnLeaveNodeFunPtr when the traversal leaves a node, which is
+// why we can't just use GetNextNode here, sadly.
+static nsINode*
+FindNextNode(nsINode* aNode, nsINode* aRoot,
+ OnLeaveNodeFunPtr aOnLeaveNode, void* aClosure)
+{
+ NS_PRECONDITION(aNode, "Null starting node?");
+
+ nsINode* next = aNode->GetFirstChild();
+ if (next)
+ return next;
+
+ // Don't look at siblings or otherwise outside of aRoot
+ if (aNode == aRoot)
+ return nullptr;
+
+ next = aNode->GetNextSibling();
+ if (next)
+ return next;
+
+ // Go up
+ for (;;) {
+ if (aOnLeaveNode) {
+ aOnLeaveNode(aNode, aClosure);
+ }
+
+ next = aNode->GetParent();
+ if (next == aRoot || ! next)
+ return nullptr;
+ aNode = next;
+
+ next = aNode->GetNextSibling();
+ if (next)
+ return next;
+ }
+}
+
+// aNode is not a text node. Find the first text node starting at aNode/aOffset
+// in a preorder DOM traversal.
+static nsINode*
+FindNextTextNode(nsINode* aNode, int32_t aOffset, nsINode* aRoot)
+{
+ NS_PRECONDITION(aNode, "Null starting node?");
+ NS_ASSERTION(!IsTextNode(aNode), "FindNextTextNode should start with a non-text node");
+
+ nsINode* checkNode;
+ // Need to start at the aOffset'th child
+ nsIContent* child = aNode->GetChildAt(aOffset);
+
+ if (child) {
+ checkNode = child;
+ } else {
+ // aOffset was beyond the end of the child list.
+ // goto next node after the last descendant of aNode in
+ // a preorder DOM traversal.
+ checkNode = aNode->GetNextNonChildNode(aRoot);
+ }
+
+ while (checkNode && !IsTextNode(checkNode)) {
+ checkNode = checkNode->GetNextNode(aRoot);
+ }
+ return checkNode;
+}
+
+// mozInlineSpellWordUtil::SetEnd
+//
+// We have two ranges "hard" and "soft". The hard boundary is simply
+// the scope of the root node. The soft boundary is that which is set
+// by the caller of this class by calling this function. If this function is
+// not called, the soft boundary is the same as the hard boundary.
+//
+// When we reach the soft boundary (mSoftEnd), we keep
+// going until we reach the end of a word. This allows the caller to set the
+// end of the range to anything, and we will always check whole multiples of
+// words. When we reach the hard boundary we stop no matter what.
+//
+// There is no beginning soft boundary. This is because we only go to the
+// previous node once, when finding the previous word boundary in
+// SetPosition(). You might think of the soft boundary as being this initial
+// position.
+
+nsresult
+mozInlineSpellWordUtil::SetEnd(nsINode* aEndNode, int32_t aEndOffset)
+{
+ NS_PRECONDITION(aEndNode, "Null end node?");
+
+ NS_ASSERTION(mRootNode, "Not initialized");
+
+ InvalidateWords();
+
+ if (!IsTextNode(aEndNode)) {
+ // End at the start of the first text node after aEndNode/aEndOffset.
+ aEndNode = FindNextTextNode(aEndNode, aEndOffset, mRootNode);
+ aEndOffset = 0;
+ }
+ mSoftEnd = NodeOffset(aEndNode, aEndOffset);
+ return NS_OK;
+}
+
+nsresult
+mozInlineSpellWordUtil::SetPosition(nsINode* aNode, int32_t aOffset)
+{
+ InvalidateWords();
+
+ if (!IsTextNode(aNode)) {
+ // Start at the start of the first text node after aNode/aOffset.
+ aNode = FindNextTextNode(aNode, aOffset, mRootNode);
+ aOffset = 0;
+ }
+ mSoftBegin = NodeOffset(aNode, aOffset);
+
+ nsresult rv = EnsureWords();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ int32_t textOffset = MapDOMPositionToSoftTextOffset(mSoftBegin);
+ if (textOffset < 0)
+ return NS_OK;
+ mNextWordIndex = FindRealWordContaining(textOffset, HINT_END, true);
+ return NS_OK;
+}
+
+nsresult
+mozInlineSpellWordUtil::EnsureWords()
+{
+ if (mSoftTextValid)
+ return NS_OK;
+ BuildSoftText();
+ nsresult rv = BuildRealWords();
+ if (NS_FAILED(rv)) {
+ mRealWords.Clear();
+ return rv;
+ }
+ mSoftTextValid = true;
+ return NS_OK;
+}
+
+nsresult
+mozInlineSpellWordUtil::MakeRangeForWord(const RealWord& aWord, nsRange** aRange)
+{
+ NodeOffset begin = MapSoftTextOffsetToDOMPosition(aWord.mSoftTextOffset, HINT_BEGIN);
+ NodeOffset end = MapSoftTextOffsetToDOMPosition(aWord.EndOffset(), HINT_END);
+ return MakeRange(begin, end, aRange);
+}
+
+// mozInlineSpellWordUtil::GetRangeForWord
+
+nsresult
+mozInlineSpellWordUtil::GetRangeForWord(nsIDOMNode* aWordNode,
+ int32_t aWordOffset,
+ nsRange** aRange)
+{
+ // Set our soft end and start
+ nsCOMPtr<nsINode> wordNode = do_QueryInterface(aWordNode);
+ NodeOffset pt = NodeOffset(wordNode, aWordOffset);
+
+ if (!mSoftTextValid || pt != mSoftBegin || pt != mSoftEnd) {
+ InvalidateWords();
+ mSoftBegin = mSoftEnd = pt;
+ nsresult rv = EnsureWords();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ int32_t offset = MapDOMPositionToSoftTextOffset(pt);
+ if (offset < 0)
+ return MakeRange(pt, pt, aRange);
+ int32_t wordIndex = FindRealWordContaining(offset, HINT_BEGIN, false);
+ if (wordIndex < 0)
+ return MakeRange(pt, pt, aRange);
+ return MakeRangeForWord(mRealWords[wordIndex], aRange);
+}
+
+// This is to fix characters that the spellchecker may not like
+static void
+NormalizeWord(const nsSubstring& aInput, int32_t aPos, int32_t aLen, nsAString& aOutput)
+{
+ aOutput.Truncate();
+ for (int32_t i = 0; i < aLen; i++) {
+ char16_t ch = aInput.CharAt(i + aPos);
+
+ // remove ignorable characters from the word
+ if (IsIgnorableCharacter(ch))
+ continue;
+
+ // the spellchecker doesn't handle curly apostrophes in all languages
+ if (ch == 0x2019) { // RIGHT SINGLE QUOTATION MARK
+ ch = '\'';
+ }
+
+ aOutput.Append(ch);
+ }
+}
+
+// mozInlineSpellWordUtil::GetNextWord
+//
+// FIXME-optimization: we shouldn't have to generate a range every single
+// time. It would be better if the inline spellchecker didn't require a
+// range unless the word was misspelled. This may or may not be possible.
+
+nsresult
+mozInlineSpellWordUtil::GetNextWord(nsAString& aText, nsRange** aRange,
+ bool* aSkipChecking)
+{
+#ifdef DEBUG_SPELLCHECK
+ printf("GetNextWord called; mNextWordIndex=%d\n", mNextWordIndex);
+#endif
+
+ if (mNextWordIndex < 0 ||
+ mNextWordIndex >= int32_t(mRealWords.Length())) {
+ mNextWordIndex = -1;
+ *aRange = nullptr;
+ *aSkipChecking = true;
+ return NS_OK;
+ }
+
+ const RealWord& word = mRealWords[mNextWordIndex];
+ nsresult rv = MakeRangeForWord(word, aRange);
+ NS_ENSURE_SUCCESS(rv, rv);
+ ++mNextWordIndex;
+ *aSkipChecking = !word.mCheckableWord;
+ ::NormalizeWord(mSoftText, word.mSoftTextOffset, word.mLength, aText);
+
+#ifdef DEBUG_SPELLCHECK
+ printf("GetNextWord returning: %s (skip=%d)\n",
+ NS_ConvertUTF16toUTF8(aText).get(), *aSkipChecking);
+#endif
+
+ return NS_OK;
+}
+
+// mozInlineSpellWordUtil::MakeRange
+//
+// Convenience function for creating a range over the current document.
+
+nsresult
+mozInlineSpellWordUtil::MakeRange(NodeOffset aBegin, NodeOffset aEnd,
+ nsRange** aRange)
+{
+ NS_ENSURE_ARG_POINTER(aBegin.mNode);
+ if (!mDOMDocument)
+ return NS_ERROR_NOT_INITIALIZED;
+
+ RefPtr<nsRange> range = new nsRange(aBegin.mNode);
+ nsresult rv = range->Set(aBegin.mNode, aBegin.mOffset,
+ aEnd.mNode, aEnd.mOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ range.forget(aRange);
+
+ return NS_OK;
+}
+
+/*********** DOM text extraction ************/
+
+// IsDOMWordSeparator
+//
+// Determines if the given character should be considered as a DOM Word
+// separator. Basically, this is whitespace, although it could also have
+// certain punctuation that we know ALWAYS breaks words. This is important.
+// For example, we can't have any punctuation that could appear in a URL
+// or email address in this, because those need to always fit into a single
+// DOM word.
+
+static bool
+IsDOMWordSeparator(char16_t ch)
+{
+ // simple spaces
+ if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r')
+ return true;
+
+ // complex spaces - check only if char isn't ASCII (uncommon)
+ if (ch >= 0xA0 &&
+ (ch == 0x00A0 || // NO-BREAK SPACE
+ ch == 0x2002 || // EN SPACE
+ ch == 0x2003 || // EM SPACE
+ ch == 0x2009 || // THIN SPACE
+ ch == 0x3000)) // IDEOGRAPHIC SPACE
+ return true;
+
+ // otherwise not a space
+ return false;
+}
+
+static inline bool
+IsBRElement(nsINode* aNode)
+{
+ return aNode->IsHTMLElement(nsGkAtoms::br);
+}
+
+/**
+ * Given a TextNode, checks to see if there's a DOM word separator before
+ * aBeforeOffset within it. This function does not modify aSeparatorOffset when
+ * it returns false.
+ *
+ * @param aNode the TextNode to check.
+ * @param aBeforeOffset the offset in the TextNode before which we will search
+ * for the DOM separator. You can pass INT32_MAX to search the entire
+ * length of the string.
+ * @param aSeparatorOffset will be set to the offset of the first separator it
+ * encounters. Will not be written to if no separator is found.
+ * @returns True if it found a separator.
+ */
+static bool
+TextNodeContainsDOMWordSeparator(nsINode* aNode,
+ int32_t aBeforeOffset,
+ int32_t* aSeparatorOffset)
+{
+ // aNode is actually an nsIContent, since it's eTEXT
+ nsIContent* content = static_cast<nsIContent*>(aNode);
+ const nsTextFragment* textFragment = content->GetText();
+ NS_ASSERTION(textFragment, "Where is our text?");
+ for (int32_t i = std::min(aBeforeOffset, int32_t(textFragment->GetLength())) - 1; i >= 0; --i) {
+ if (IsDOMWordSeparator(textFragment->CharAt(i))) {
+ // Be greedy, find as many separators as we can
+ for (int32_t j = i - 1; j >= 0; --j) {
+ if (IsDOMWordSeparator(textFragment->CharAt(j))) {
+ i = j;
+ } else {
+ break;
+ }
+ }
+ *aSeparatorOffset = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Check if there's a DOM word separator before aBeforeOffset in this node.
+ * Always returns true if it's a BR element.
+ * aSeparatorOffset is set to the index of the first character in the last
+ * separator if any is found (0 for BR elements).
+ *
+ * This function does not modify aSeparatorOffset when it returns false.
+ */
+static bool
+ContainsDOMWordSeparator(nsINode* aNode, int32_t aBeforeOffset,
+ int32_t* aSeparatorOffset)
+{
+ if (IsBRElement(aNode)) {
+ *aSeparatorOffset = 0;
+ return true;
+ }
+
+ if (!IsTextNode(aNode))
+ return false;
+
+ return TextNodeContainsDOMWordSeparator(aNode, aBeforeOffset,
+ aSeparatorOffset);
+}
+
+static bool
+IsBreakElement(nsINode* aNode)
+{
+ if (!aNode->IsElement()) {
+ return false;
+ }
+
+ dom::Element *element = aNode->AsElement();
+
+ if (element->IsHTMLElement(nsGkAtoms::br))
+ return true;
+
+ // If we don't have a frame, we don't consider ourselves a break
+ // element. In particular, words can span us.
+ if (!element->GetPrimaryFrame())
+ return false;
+
+ // Anything that's not an inline element is a break element.
+ // XXXbz should replaced inlines be break elements, though?
+ return element->GetPrimaryFrame()->StyleDisplay()->mDisplay !=
+ StyleDisplay::Inline;
+}
+
+struct CheckLeavingBreakElementClosure {
+ bool mLeftBreakElement;
+};
+
+static void
+CheckLeavingBreakElement(nsINode* aNode, void* aClosure)
+{
+ CheckLeavingBreakElementClosure* cl =
+ static_cast<CheckLeavingBreakElementClosure*>(aClosure);
+ if (!cl->mLeftBreakElement && IsBreakElement(aNode)) {
+ cl->mLeftBreakElement = true;
+ }
+}
+
+void
+mozInlineSpellWordUtil::NormalizeWord(nsSubstring& aWord)
+{
+ nsAutoString result;
+ ::NormalizeWord(aWord, 0, aWord.Length(), result);
+ aWord = result;
+}
+
+void
+mozInlineSpellWordUtil::BuildSoftText()
+{
+ // First we have to work backwards from mSoftStart to find a text node
+ // containing a DOM word separator, a non-inline-element
+ // boundary, or the hard start node. That's where we'll start building the
+ // soft string from.
+ nsINode* node = mSoftBegin.mNode;
+ int32_t firstOffsetInNode = 0;
+ int32_t checkBeforeOffset = mSoftBegin.mOffset;
+ while (node) {
+ if (ContainsDOMWordSeparator(node, checkBeforeOffset, &firstOffsetInNode)) {
+ if (node == mSoftBegin.mNode) {
+ // If we find a word separator on the first node, look at the preceding
+ // word on the text node as well.
+ int32_t newOffset = 0;
+ if (firstOffsetInNode > 0) {
+ // Try to find the previous word boundary in the current node. If
+ // we can't find one, start checking previous sibling nodes (if any
+ // adjacent ones exist) to see if we can find any text nodes with
+ // DOM word separators. We bail out as soon as we see a node that is
+ // not a text node, or we run out of previous sibling nodes. In the
+ // event that we simply cannot find any preceding word separator, the
+ // offset is set to 0, and the soft text beginning node is set to the
+ // "most previous" text node before the original starting node, or
+ // kept at the original starting node if no previous text nodes exist.
+ if (!ContainsDOMWordSeparator(node, firstOffsetInNode - 1,
+ &newOffset)) {
+ nsINode* prevNode = node->GetPreviousSibling();
+ while (prevNode && IsTextNode(prevNode)) {
+ mSoftBegin.mNode = prevNode;
+ if (TextNodeContainsDOMWordSeparator(prevNode, INT32_MAX,
+ &newOffset)) {
+ break;
+ }
+ prevNode = prevNode->GetPreviousSibling();
+ }
+ }
+ }
+ firstOffsetInNode = newOffset;
+ mSoftBegin.mOffset = newOffset;
+ }
+ break;
+ }
+ checkBeforeOffset = INT32_MAX;
+ if (IsBreakElement(node)) {
+ // Since GetPreviousContent follows tree *preorder*, we're about to traverse
+ // up out of 'node'. Since node induces breaks (e.g., it's a block),
+ // don't bother trying to look outside it, just stop now.
+ break;
+ }
+ // GetPreviousContent below expects mRootNode to be an ancestor of node.
+ if (!nsContentUtils::ContentIsDescendantOf(node, mRootNode)) {
+ break;
+ }
+ node = node->GetPreviousContent(mRootNode);
+ }
+
+ // Now build up the string moving forward through the DOM until we reach
+ // the soft end and *then* see a DOM word separator, a non-inline-element
+ // boundary, or the hard end node.
+ mSoftText.Truncate();
+ mSoftTextDOMMapping.Clear();
+ bool seenSoftEnd = false;
+ // Leave this outside the loop so large heap string allocations can be reused
+ // across iterations
+ while (node) {
+ if (node == mSoftEnd.mNode) {
+ seenSoftEnd = true;
+ }
+
+ bool exit = false;
+ if (IsTextNode(node)) {
+ nsIContent* content = static_cast<nsIContent*>(node);
+ NS_ASSERTION(content, "Where is our content?");
+ const nsTextFragment* textFragment = content->GetText();
+ NS_ASSERTION(textFragment, "Where is our text?");
+ int32_t lastOffsetInNode = textFragment->GetLength();
+
+ if (seenSoftEnd) {
+ // check whether we can stop after this
+ for (int32_t i = node == mSoftEnd.mNode ? mSoftEnd.mOffset : 0;
+ i < int32_t(textFragment->GetLength()); ++i) {
+ if (IsDOMWordSeparator(textFragment->CharAt(i))) {
+ exit = true;
+ // stop at the first separator after the soft end point
+ lastOffsetInNode = i;
+ break;
+ }
+ }
+ }
+
+ if (firstOffsetInNode < lastOffsetInNode) {
+ int32_t len = lastOffsetInNode - firstOffsetInNode;
+ mSoftTextDOMMapping.AppendElement(
+ DOMTextMapping(NodeOffset(node, firstOffsetInNode), mSoftText.Length(), len));
+
+ bool ok = textFragment->AppendTo(mSoftText, firstOffsetInNode, len,
+ mozilla::fallible);
+ if (!ok) {
+ // probably out of memory, remove from mSoftTextDOMMapping
+ mSoftTextDOMMapping.RemoveElementAt(mSoftTextDOMMapping.Length() - 1);
+ exit = true;
+ }
+ }
+
+ firstOffsetInNode = 0;
+ }
+
+ if (exit)
+ break;
+
+ CheckLeavingBreakElementClosure closure = { false };
+ node = FindNextNode(node, mRootNode, CheckLeavingBreakElement, &closure);
+ if (closure.mLeftBreakElement || (node && IsBreakElement(node))) {
+ // We left, or are entering, a break element (e.g., block). Maybe we can
+ // stop now.
+ if (seenSoftEnd)
+ break;
+ // Record the break
+ mSoftText.Append(' ');
+ }
+ }
+
+#ifdef DEBUG_SPELLCHECK
+ printf("Got DOM string: %s\n", NS_ConvertUTF16toUTF8(mSoftText).get());
+#endif
+}
+
+nsresult
+mozInlineSpellWordUtil::BuildRealWords()
+{
+ // This is pretty simple. We just have to walk mSoftText, tokenizing it
+ // into "real words".
+ // We do an outer traversal of words delimited by IsDOMWordSeparator, calling
+ // SplitDOMWord on each of those DOM words
+ int32_t wordStart = -1;
+ mRealWords.Clear();
+ for (int32_t i = 0; i < int32_t(mSoftText.Length()); ++i) {
+ if (IsDOMWordSeparator(mSoftText.CharAt(i))) {
+ if (wordStart >= 0) {
+ nsresult rv = SplitDOMWord(wordStart, i);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ wordStart = -1;
+ }
+ } else {
+ if (wordStart < 0) {
+ wordStart = i;
+ }
+ }
+ }
+ if (wordStart >= 0) {
+ nsresult rv = SplitDOMWord(wordStart, mSoftText.Length());
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+/*********** DOM/realwords<->mSoftText mapping functions ************/
+
+int32_t
+mozInlineSpellWordUtil::MapDOMPositionToSoftTextOffset(NodeOffset aNodeOffset)
+{
+ if (!mSoftTextValid) {
+ NS_ERROR("Soft text must be valid if we're to map into it");
+ return -1;
+ }
+
+ for (int32_t i = 0; i < int32_t(mSoftTextDOMMapping.Length()); ++i) {
+ const DOMTextMapping& map = mSoftTextDOMMapping[i];
+ if (map.mNodeOffset.mNode == aNodeOffset.mNode) {
+ // Allow offsets at either end of the string, in particular, allow the
+ // offset that's at the end of the contributed string
+ int32_t offsetInContributedString =
+ aNodeOffset.mOffset - map.mNodeOffset.mOffset;
+ if (offsetInContributedString >= 0 &&
+ offsetInContributedString <= map.mLength)
+ return map.mSoftTextOffset + offsetInContributedString;
+ return -1;
+ }
+ }
+ return -1;
+}
+
+namespace {
+
+template<class T>
+class FirstLargerOffset
+{
+ int32_t mSoftTextOffset;
+
+public:
+ explicit FirstLargerOffset(int32_t aSoftTextOffset) : mSoftTextOffset(aSoftTextOffset) {}
+ int operator()(const T& t) const {
+ // We want the first larger offset, so never return 0 (which would
+ // short-circuit evaluation before finding the last such offset).
+ return mSoftTextOffset < t.mSoftTextOffset ? -1 : 1;
+ }
+};
+
+template<class T>
+bool
+FindLastNongreaterOffset(const nsTArray<T>& aContainer, int32_t aSoftTextOffset, size_t* aIndex)
+{
+ if (aContainer.Length() == 0) {
+ return false;
+ }
+
+ BinarySearchIf(aContainer, 0, aContainer.Length(),
+ FirstLargerOffset<T>(aSoftTextOffset), aIndex);
+ if (*aIndex > 0) {
+ // There was at least one mapping with offset <= aSoftTextOffset. Step back
+ // to find the last element with |mSoftTextOffset <= aSoftTextOffset|.
+ *aIndex -= 1;
+ } else {
+ // Every mapping had offset greater than aSoftTextOffset.
+ MOZ_ASSERT(aContainer[*aIndex].mSoftTextOffset > aSoftTextOffset);
+ }
+ return true;
+}
+
+} // namespace
+
+mozInlineSpellWordUtil::NodeOffset
+mozInlineSpellWordUtil::MapSoftTextOffsetToDOMPosition(int32_t aSoftTextOffset,
+ DOMMapHint aHint)
+{
+ NS_ASSERTION(mSoftTextValid, "Soft text must be valid if we're to map out of it");
+ if (!mSoftTextValid)
+ return NodeOffset(nullptr, -1);
+
+ // Find the last mapping, if any, such that mSoftTextOffset <= aSoftTextOffset
+ size_t index;
+ bool found = FindLastNongreaterOffset(mSoftTextDOMMapping, aSoftTextOffset, &index);
+ if (!found) {
+ return NodeOffset(nullptr, -1);
+ }
+
+ // 'index' is now the last mapping, if any, such that
+ // mSoftTextOffset <= aSoftTextOffset.
+ // If we're doing HINT_END, then we may want to return the end of the
+ // the previous mapping instead of the start of this mapping
+ if (aHint == HINT_END && index > 0) {
+ const DOMTextMapping& map = mSoftTextDOMMapping[index - 1];
+ if (map.mSoftTextOffset + map.mLength == aSoftTextOffset)
+ return NodeOffset(map.mNodeOffset.mNode, map.mNodeOffset.mOffset + map.mLength);
+ }
+
+ // We allow ourselves to return the end of this mapping even if we're
+ // doing HINT_START. This will only happen if there is no mapping which this
+ // point is the start of. I'm not 100% sure this is OK...
+ const DOMTextMapping& map = mSoftTextDOMMapping[index];
+ int32_t offset = aSoftTextOffset - map.mSoftTextOffset;
+ if (offset >= 0 && offset <= map.mLength)
+ return NodeOffset(map.mNodeOffset.mNode, map.mNodeOffset.mOffset + offset);
+
+ return NodeOffset(nullptr, -1);
+}
+
+int32_t
+mozInlineSpellWordUtil::FindRealWordContaining(int32_t aSoftTextOffset,
+ DOMMapHint aHint, bool aSearchForward)
+{
+ NS_ASSERTION(mSoftTextValid, "Soft text must be valid if we're to map out of it");
+ if (!mSoftTextValid)
+ return -1;
+
+ // Find the last word, if any, such that mSoftTextOffset <= aSoftTextOffset
+ size_t index;
+ bool found = FindLastNongreaterOffset(mRealWords, aSoftTextOffset, &index);
+ if (!found) {
+ return -1;
+ }
+
+ // 'index' is now the last word, if any, such that
+ // mSoftTextOffset <= aSoftTextOffset.
+ // If we're doing HINT_END, then we may want to return the end of the
+ // the previous word instead of the start of this word
+ if (aHint == HINT_END && index > 0) {
+ const RealWord& word = mRealWords[index - 1];
+ if (word.mSoftTextOffset + word.mLength == aSoftTextOffset)
+ return index - 1;
+ }
+
+ // We allow ourselves to return the end of this word even if we're
+ // doing HINT_START. This will only happen if there is no word which this
+ // point is the start of. I'm not 100% sure this is OK...
+ const RealWord& word = mRealWords[index];
+ int32_t offset = aSoftTextOffset - word.mSoftTextOffset;
+ if (offset >= 0 && offset <= static_cast<int32_t>(word.mLength))
+ return index;
+
+ if (aSearchForward) {
+ if (mRealWords[0].mSoftTextOffset > aSoftTextOffset) {
+ // All words have mSoftTextOffset > aSoftTextOffset
+ return 0;
+ }
+ // 'index' is the last word such that mSoftTextOffset <= aSoftTextOffset.
+ // Word index+1, if it exists, will be the first with
+ // mSoftTextOffset > aSoftTextOffset.
+ if (index + 1 < mRealWords.Length())
+ return index + 1;
+ }
+
+ return -1;
+}
+
+/*********** Word Splitting ************/
+
+// classifies a given character in the DOM word
+enum CharClass {
+ CHAR_CLASS_WORD,
+ CHAR_CLASS_SEPARATOR,
+ CHAR_CLASS_END_OF_INPUT };
+
+// Encapsulates DOM-word to real-word splitting
+struct MOZ_STACK_CLASS WordSplitState
+{
+ mozInlineSpellWordUtil* mWordUtil;
+ const nsDependentSubstring mDOMWordText;
+ int32_t mDOMWordOffset;
+ CharClass mCurCharClass;
+
+ WordSplitState(mozInlineSpellWordUtil* aWordUtil,
+ const nsString& aString, int32_t aStart, int32_t aLen)
+ : mWordUtil(aWordUtil), mDOMWordText(aString, aStart, aLen),
+ mDOMWordOffset(0), mCurCharClass(CHAR_CLASS_END_OF_INPUT) {}
+
+ CharClass ClassifyCharacter(int32_t aIndex, bool aRecurse) const;
+ void Advance();
+ void AdvanceThroughSeparators();
+ void AdvanceThroughWord();
+
+ // Finds special words like email addresses and URLs that may start at the
+ // current position, and returns their length, or 0 if not found. This allows
+ // arbitrary word breaking rules to be used for these special entities, as
+ // long as they can not contain whitespace.
+ bool IsSpecialWord();
+
+ // Similar to IsSpecialWord except that this takes a split word as
+ // input. This checks for things that do not require special word-breaking
+ // rules.
+ bool ShouldSkipWord(int32_t aStart, int32_t aLength);
+};
+
+// WordSplitState::ClassifyCharacter
+
+CharClass
+WordSplitState::ClassifyCharacter(int32_t aIndex, bool aRecurse) const
+{
+ NS_ASSERTION(aIndex >= 0 && aIndex <= int32_t(mDOMWordText.Length()),
+ "Index out of range");
+ if (aIndex == int32_t(mDOMWordText.Length()))
+ return CHAR_CLASS_SEPARATOR;
+
+ // this will classify the character, we want to treat "ignorable" characters
+ // such as soft hyphens, and also ZWJ and ZWNJ as word characters.
+ nsIUGenCategory::nsUGenCategory
+ charCategory = mozilla::unicode::GetGenCategory(mDOMWordText[aIndex]);
+ if (charCategory == nsIUGenCategory::kLetter ||
+ IsIgnorableCharacter(mDOMWordText[aIndex]) ||
+ mDOMWordText[aIndex] == 0x200C /* ZWNJ */ ||
+ mDOMWordText[aIndex] == 0x200D /* ZWJ */)
+ return CHAR_CLASS_WORD;
+
+ // If conditional punctuation is surrounded immediately on both sides by word
+ // characters it also counts as a word character.
+ if (IsConditionalPunctuation(mDOMWordText[aIndex])) {
+ if (!aRecurse) {
+ // not allowed to look around, this punctuation counts like a separator
+ return CHAR_CLASS_SEPARATOR;
+ }
+
+ // check the left-hand character
+ if (aIndex == 0)
+ return CHAR_CLASS_SEPARATOR;
+ if (ClassifyCharacter(aIndex - 1, false) != CHAR_CLASS_WORD)
+ return CHAR_CLASS_SEPARATOR;
+ // If the previous charatcer is a word-char, make sure that it's not a
+ // special dot character.
+ if (mDOMWordText[aIndex - 1] == '.')
+ return CHAR_CLASS_SEPARATOR;
+
+ // now we know left char is a word-char, check the right-hand character
+ if (aIndex == int32_t(mDOMWordText.Length()) - 1)
+ return CHAR_CLASS_SEPARATOR;
+ if (ClassifyCharacter(aIndex + 1, false) != CHAR_CLASS_WORD)
+ return CHAR_CLASS_SEPARATOR;
+ // If the next charatcer is a word-char, make sure that it's not a
+ // special dot character.
+ if (mDOMWordText[aIndex + 1] == '.')
+ return CHAR_CLASS_SEPARATOR;
+
+ // char on either side is a word, this counts as a word
+ return CHAR_CLASS_WORD;
+ }
+
+ // The dot character, if appearing at the end of a word, should
+ // be considered part of that word. Example: "etc.", or
+ // abbreviations
+ if (aIndex > 0 &&
+ mDOMWordText[aIndex] == '.' &&
+ mDOMWordText[aIndex - 1] != '.' &&
+ ClassifyCharacter(aIndex - 1, false) != CHAR_CLASS_WORD) {
+ return CHAR_CLASS_WORD;
+ }
+
+ // all other punctuation
+ if (charCategory == nsIUGenCategory::kSeparator ||
+ charCategory == nsIUGenCategory::kOther ||
+ charCategory == nsIUGenCategory::kPunctuation ||
+ charCategory == nsIUGenCategory::kSymbol) {
+ // Don't break on hyphens, as hunspell handles them on its own.
+ if (aIndex > 0 &&
+ mDOMWordText[aIndex] == '-' &&
+ mDOMWordText[aIndex - 1] != '-' &&
+ ClassifyCharacter(aIndex - 1, false) == CHAR_CLASS_WORD) {
+ // A hyphen is only meaningful as a separator inside a word
+ // if the previous and next characters are a word character.
+ if (aIndex == int32_t(mDOMWordText.Length()) - 1)
+ return CHAR_CLASS_SEPARATOR;
+ if (mDOMWordText[aIndex + 1] != '.' &&
+ ClassifyCharacter(aIndex + 1, false) == CHAR_CLASS_WORD)
+ return CHAR_CLASS_WORD;
+ }
+ return CHAR_CLASS_SEPARATOR;
+ }
+
+ // any other character counts as a word
+ return CHAR_CLASS_WORD;
+}
+
+
+// WordSplitState::Advance
+
+void
+WordSplitState::Advance()
+{
+ NS_ASSERTION(mDOMWordOffset >= 0, "Negative word index");
+ NS_ASSERTION(mDOMWordOffset < (int32_t)mDOMWordText.Length(),
+ "Length beyond end");
+
+ mDOMWordOffset ++;
+ if (mDOMWordOffset >= (int32_t)mDOMWordText.Length())
+ mCurCharClass = CHAR_CLASS_END_OF_INPUT;
+ else
+ mCurCharClass = ClassifyCharacter(mDOMWordOffset, true);
+}
+
+
+// WordSplitState::AdvanceThroughSeparators
+
+void
+WordSplitState::AdvanceThroughSeparators()
+{
+ while (mCurCharClass == CHAR_CLASS_SEPARATOR)
+ Advance();
+}
+
+// WordSplitState::AdvanceThroughWord
+
+void
+WordSplitState::AdvanceThroughWord()
+{
+ while (mCurCharClass == CHAR_CLASS_WORD)
+ Advance();
+}
+
+
+// WordSplitState::IsSpecialWord
+
+bool
+WordSplitState::IsSpecialWord()
+{
+ // Search for email addresses. We simply define these as any sequence of
+ // characters with an '@' character in the middle. The DOM word is already
+ // split on whitepace, so we know that everything to the end is the address
+ int32_t firstColon = -1;
+ for (int32_t i = mDOMWordOffset;
+ i < int32_t(mDOMWordText.Length()); i ++) {
+ if (mDOMWordText[i] == '@') {
+ // only accept this if there are unambiguous word characters (don't bother
+ // recursing to disambiguate apostrophes) on each side. This prevents
+ // classifying, e.g. "@home" as an email address
+
+ // Use this condition to only accept words with '@' in the middle of
+ // them. It works, but the inlinespellcker doesn't like this. The problem
+ // is that you type "fhsgfh@" that's a misspelled word followed by a
+ // symbol, but when you type another letter "fhsgfh@g" that first word
+ // need to be unmarked misspelled. It doesn't do this. it only checks the
+ // current position for potentially removing a spelling range.
+ if (i > 0 && ClassifyCharacter(i - 1, false) == CHAR_CLASS_WORD &&
+ i < (int32_t)mDOMWordText.Length() - 1 &&
+ ClassifyCharacter(i + 1, false) == CHAR_CLASS_WORD) {
+ return true;
+ }
+ } else if (mDOMWordText[i] == ':' && firstColon < 0) {
+ firstColon = i;
+
+ // If the first colon is followed by a slash, consider it a URL
+ // This will catch things like asdf://foo.com
+ if (firstColon < (int32_t)mDOMWordText.Length() - 1 &&
+ mDOMWordText[firstColon + 1] == '/') {
+ return true;
+ }
+ }
+ }
+
+ // Check the text before the first colon against some known protocols. It
+ // is impossible to check against all protocols, especially since you can
+ // plug in new protocols. We also don't want to waste time here checking
+ // against a lot of obscure protocols.
+ if (firstColon > mDOMWordOffset) {
+ nsString protocol(Substring(mDOMWordText, mDOMWordOffset,
+ firstColon - mDOMWordOffset));
+ if (protocol.EqualsIgnoreCase("http") ||
+ protocol.EqualsIgnoreCase("https") ||
+ protocol.EqualsIgnoreCase("news") ||
+ protocol.EqualsIgnoreCase("file") ||
+ protocol.EqualsIgnoreCase("javascript") ||
+ protocol.EqualsIgnoreCase("data") ||
+ protocol.EqualsIgnoreCase("ftp")) {
+ return true;
+ }
+ }
+
+ // not anything special
+ return false;
+}
+
+// WordSplitState::ShouldSkipWord
+
+bool
+WordSplitState::ShouldSkipWord(int32_t aStart, int32_t aLength)
+{
+ int32_t last = aStart + aLength;
+
+ // check to see if the word contains a digit
+ for (int32_t i = aStart; i < last; i ++) {
+ if (unicode::GetGenCategory(mDOMWordText[i]) == nsIUGenCategory::kNumber) {
+ return true;
+ }
+ }
+
+ // not special
+ return false;
+}
+
+// mozInlineSpellWordUtil::SplitDOMWord
+
+nsresult
+mozInlineSpellWordUtil::SplitDOMWord(int32_t aStart, int32_t aEnd)
+{
+ WordSplitState state(this, mSoftText, aStart, aEnd - aStart);
+ state.mCurCharClass = state.ClassifyCharacter(0, true);
+
+ state.AdvanceThroughSeparators();
+ if (state.mCurCharClass != CHAR_CLASS_END_OF_INPUT &&
+ state.IsSpecialWord()) {
+ int32_t specialWordLength = state.mDOMWordText.Length() - state.mDOMWordOffset;
+ if (!mRealWords.AppendElement(
+ RealWord(aStart + state.mDOMWordOffset, specialWordLength, false),
+ fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+ }
+
+ while (state.mCurCharClass != CHAR_CLASS_END_OF_INPUT) {
+ state.AdvanceThroughSeparators();
+ if (state.mCurCharClass == CHAR_CLASS_END_OF_INPUT)
+ break;
+
+ // save the beginning of the word
+ int32_t wordOffset = state.mDOMWordOffset;
+
+ // find the end of the word
+ state.AdvanceThroughWord();
+ int32_t wordLen = state.mDOMWordOffset - wordOffset;
+ if (!mRealWords.AppendElement(
+ RealWord(aStart + wordOffset, wordLen,
+ !state.ShouldSkipWord(wordOffset, wordLen)), fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ return NS_OK;
+}
diff --git a/extensions/spellcheck/src/mozInlineSpellWordUtil.h b/extensions/spellcheck/src/mozInlineSpellWordUtil.h
new file mode 100644
index 000000000..b28d24ae5
--- /dev/null
+++ b/extensions/spellcheck/src/mozInlineSpellWordUtil.h
@@ -0,0 +1,179 @@
+/* -*- Mode: C++; tab-width: 8; 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 mozInlineSpellWordUtil_h
+#define mozInlineSpellWordUtil_h
+
+#include "nsCOMPtr.h"
+#include "nsIDOMDocument.h"
+#include "nsIDocument.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+//#define DEBUG_SPELLCHECK
+
+class nsRange;
+class nsINode;
+
+/**
+ * This class extracts text from the DOM and builds it into a single string.
+ * The string includes whitespace breaks whereever non-inline elements begin
+ * and end. This string is broken into "real words", following somewhat
+ * complex rules; for example substrings that look like URLs or
+ * email addresses are treated as single words, but otherwise many kinds of
+ * punctuation are treated as word separators. GetNextWord provides a way
+ * to iterate over these "real words".
+ *
+ * The basic operation is:
+ *
+ * 1. Call Init with the weak pointer to the editor that you're using.
+ * 2. Call SetEnd to set where you want to stop spellchecking. We'll stop
+ * at the word boundary after that. If SetEnd is not called, we'll stop
+ * at the end of the document's root element.
+ * 3. Call SetPosition to initialize the current position inside the
+ * previously given range.
+ * 4. Call GetNextWord over and over until it returns false.
+ */
+
+class mozInlineSpellWordUtil
+{
+public:
+ struct NodeOffset {
+ nsINode* mNode;
+ int32_t mOffset;
+
+ NodeOffset(nsINode* aNode, int32_t aOffset) :
+ mNode(aNode), mOffset(aOffset) {}
+
+ bool operator==(const NodeOffset& aOther) const {
+ return mNode == aOther.mNode && mOffset == aOther.mOffset;
+ }
+
+ bool operator!=(const NodeOffset& aOther) const {
+ return !(*this == aOther);
+ }
+ };
+
+ mozInlineSpellWordUtil()
+ : mRootNode(nullptr),
+ mSoftBegin(nullptr, 0), mSoftEnd(nullptr, 0),
+ mNextWordIndex(-1), mSoftTextValid(false) {}
+
+ nsresult Init(nsWeakPtr aWeakEditor);
+
+ nsresult SetEnd(nsINode* aEndNode, int32_t aEndOffset);
+
+ // sets the current position, this should be inside the range. If we are in
+ // the middle of a word, we'll move to its start.
+ nsresult SetPosition(nsINode* aNode, int32_t aOffset);
+
+ // Given a point inside or immediately following a word, this returns the
+ // DOM range that exactly encloses that word's characters. The current
+ // position will be at the end of the word. This will find the previous
+ // word if the current position is space, so if you care that the point is
+ // inside the word, you should check the range.
+ //
+ // THIS CHANGES THE CURRENT POSITION AND RANGE. It is designed to be called
+ // before you actually generate the range you are interested in and iterate
+ // the words in it.
+ nsresult GetRangeForWord(nsIDOMNode* aWordNode, int32_t aWordOffset,
+ nsRange** aRange);
+
+ // Moves to the the next word in the range, and retrieves it's text and range.
+ // An empty word and a nullptr range are returned when we are done checking.
+ // aSkipChecking will be set if the word is "special" and shouldn't be
+ // checked (e.g., an email address).
+ nsresult GetNextWord(nsAString& aText, nsRange** aRange,
+ bool* aSkipChecking);
+
+ // Call to normalize some punctuation. This function takes an autostring
+ // so we can access characters directly.
+ static void NormalizeWord(nsSubstring& aWord);
+
+ nsIDOMDocument* GetDOMDocument() const { return mDOMDocument; }
+ nsIDocument* GetDocument() const { return mDocument; }
+ nsINode* GetRootNode() { return mRootNode; }
+
+private:
+
+ // cached stuff for the editor, set by Init
+ nsCOMPtr<nsIDOMDocument> mDOMDocument;
+ nsCOMPtr<nsIDocument> mDocument;
+
+ // range to check, see SetPosition and SetEnd
+ nsINode* mRootNode;
+ NodeOffset mSoftBegin;
+ NodeOffset mSoftEnd;
+
+ // DOM text covering the soft range, with newlines added at block boundaries
+ nsString mSoftText;
+ // A list of where we extracted text from, ordered by mSoftTextOffset. A given
+ // DOM node appears at most once in this list.
+ struct DOMTextMapping {
+ NodeOffset mNodeOffset;
+ int32_t mSoftTextOffset;
+ int32_t mLength;
+
+ DOMTextMapping(NodeOffset aNodeOffset, int32_t aSoftTextOffset, int32_t aLength)
+ : mNodeOffset(aNodeOffset), mSoftTextOffset(aSoftTextOffset),
+ mLength(aLength) {}
+ };
+ nsTArray<DOMTextMapping> mSoftTextDOMMapping;
+
+ // A list of the "real words" in mSoftText, ordered by mSoftTextOffset
+ struct RealWord {
+ int32_t mSoftTextOffset;
+ uint32_t mLength : 31;
+ uint32_t mCheckableWord : 1;
+
+ RealWord(int32_t aOffset, uint32_t aLength, bool aCheckable)
+ : mSoftTextOffset(aOffset), mLength(aLength), mCheckableWord(aCheckable)
+ {
+ static_assert(sizeof(RealWord) == 8, "RealWord should be limited to 8 bytes");
+ MOZ_ASSERT(aLength < INT32_MAX, "Word length is too large to fit in the bitfield");
+ }
+
+ int32_t EndOffset() const { return mSoftTextOffset + mLength; }
+ };
+ nsTArray<RealWord> mRealWords;
+ int32_t mNextWordIndex;
+
+ bool mSoftTextValid;
+
+ void InvalidateWords() { mSoftTextValid = false; }
+ nsresult EnsureWords();
+
+ int32_t MapDOMPositionToSoftTextOffset(NodeOffset aNodeOffset);
+ // Map an offset into mSoftText to a DOM position. Note that two DOM positions
+ // can map to the same mSoftText offset, e.g. given nodes A=aaaa and B=bbbb
+ // forming aaaabbbb, (A,4) and (B,0) give the same string offset. So,
+ // aHintBefore controls which position we return ... if aHint is eEnd
+ // then the position indicates the END of a range so we return (A,4). Otherwise
+ // the position indicates the START of a range so we return (B,0).
+ enum DOMMapHint { HINT_BEGIN, HINT_END };
+ NodeOffset MapSoftTextOffsetToDOMPosition(int32_t aSoftTextOffset,
+ DOMMapHint aHint);
+ // Finds the index of the real word containing aSoftTextOffset, or -1 if none
+ // If it's exactly between two words, then if aHint is HINT_BEGIN, return the
+ // later word (favouring the assumption that it's the BEGINning of a word),
+ // otherwise return the earlier word (assuming it's the END of a word).
+ // If aSearchForward is true, then if we don't find a word at the given
+ // position, search forward until we do find a word and return that (if found).
+ int32_t FindRealWordContaining(int32_t aSoftTextOffset, DOMMapHint aHint,
+ bool aSearchForward);
+
+ // build mSoftText and mSoftTextDOMMapping
+ void BuildSoftText();
+ // Build mRealWords array
+ nsresult BuildRealWords();
+
+ nsresult SplitDOMWord(int32_t aStart, int32_t aEnd);
+
+ // Convenience functions, object must be initialized
+ nsresult MakeRange(NodeOffset aBegin, NodeOffset aEnd, nsRange** aRange);
+ nsresult MakeRangeForWord(const RealWord& aWord, nsRange** aRange);
+};
+
+#endif
diff --git a/extensions/spellcheck/src/mozPersonalDictionary.cpp b/extensions/spellcheck/src/mozPersonalDictionary.cpp
new file mode 100644
index 000000000..efaf14356
--- /dev/null
+++ b/extensions/spellcheck/src/mozPersonalDictionary.cpp
@@ -0,0 +1,471 @@
+/* -*- 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 "mozPersonalDictionary.h"
+#include "nsIUnicharInputStream.h"
+#include "nsReadableUtils.h"
+#include "nsIFile.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIObserverService.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIWeakReference.h"
+#include "nsCRT.h"
+#include "nsNetUtil.h"
+#include "nsNetCID.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsISafeOutputStream.h"
+#include "nsTArray.h"
+#include "nsStringEnumerator.h"
+#include "nsUnicharInputStream.h"
+#include "nsIRunnable.h"
+#include "nsThreadUtils.h"
+#include "nsProxyRelease.h"
+#include "prio.h"
+#include "mozilla/Move.h"
+
+#define MOZ_PERSONAL_DICT_NAME "persdict.dat"
+
+/**
+ * This is the most braindead implementation of a personal dictionary possible.
+ * There is not much complexity needed, though. It could be made much faster,
+ * and probably should, but I don't see much need for more in terms of interface.
+ *
+ * Allowing personal words to be associated with only certain dictionaries maybe.
+ *
+ * TODO:
+ * Implement the suggestion record.
+ */
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(mozPersonalDictionary)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(mozPersonalDictionary)
+
+NS_INTERFACE_MAP_BEGIN(mozPersonalDictionary)
+ NS_INTERFACE_MAP_ENTRY(mozIPersonalDictionary)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, mozIPersonalDictionary)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozPersonalDictionary)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(mozPersonalDictionary, mEncoder)
+
+class mozPersonalDictionaryLoader final : public mozilla::Runnable
+{
+public:
+ explicit mozPersonalDictionaryLoader(mozPersonalDictionary *dict) : mDict(dict)
+ {
+ }
+
+ NS_IMETHOD Run() override
+ {
+ mDict->SyncLoad();
+
+ // Release the dictionary on the main thread
+ NS_ReleaseOnMainThread(mDict.forget());
+
+ return NS_OK;
+ }
+
+private:
+ RefPtr<mozPersonalDictionary> mDict;
+};
+
+class mozPersonalDictionarySave final : public mozilla::Runnable
+{
+public:
+ explicit mozPersonalDictionarySave(mozPersonalDictionary *aDict,
+ nsCOMPtr<nsIFile> aFile,
+ nsTArray<nsString> &&aDictWords)
+ : mDictWords(aDictWords),
+ mFile(aFile),
+ mDict(aDict)
+ {
+ }
+
+ NS_IMETHOD Run() override
+ {
+ nsresult res;
+
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ {
+ mozilla::MonitorAutoLock mon(mDict->mMonitorSave);
+
+ nsCOMPtr<nsIOutputStream> outStream;
+ NS_NewSafeLocalFileOutputStream(getter_AddRefs(outStream), mFile,
+ PR_CREATE_FILE | PR_WRONLY | PR_TRUNCATE,
+ 0664);
+
+ // Get a buffered output stream 4096 bytes big, to optimize writes.
+ nsCOMPtr<nsIOutputStream> bufferedOutputStream;
+ res = NS_NewBufferedOutputStream(getter_AddRefs(bufferedOutputStream),
+ outStream, 4096);
+ if (NS_FAILED(res)) {
+ return res;
+ }
+
+ uint32_t bytesWritten;
+ nsAutoCString utf8Key;
+ for (uint32_t i = 0; i < mDictWords.Length(); ++i) {
+ CopyUTF16toUTF8(mDictWords[i], utf8Key);
+
+ bufferedOutputStream->Write(utf8Key.get(), utf8Key.Length(),
+ &bytesWritten);
+ bufferedOutputStream->Write("\n", 1, &bytesWritten);
+ }
+ nsCOMPtr<nsISafeOutputStream> safeStream =
+ do_QueryInterface(bufferedOutputStream);
+ NS_ASSERTION(safeStream, "expected a safe output stream!");
+ if (safeStream) {
+ res = safeStream->Finish();
+ if (NS_FAILED(res)) {
+ NS_WARNING("failed to save personal dictionary file! possible data loss");
+ }
+ }
+
+ // Save is done, reset the state variable and notify those who are waiting.
+ mDict->mSavePending = false;
+ mon.Notify();
+
+ // Leaving the block where 'mon' was declared will call the destructor
+ // and unlock.
+ }
+
+ // Release the dictionary on the main thread.
+ NS_ReleaseOnMainThread(mDict.forget());
+
+ return NS_OK;
+ }
+
+private:
+ nsTArray<nsString> mDictWords;
+ nsCOMPtr<nsIFile> mFile;
+ RefPtr<mozPersonalDictionary> mDict;
+};
+
+mozPersonalDictionary::mozPersonalDictionary()
+ : mIsLoaded(false),
+ mSavePending(false),
+ mMonitor("mozPersonalDictionary::mMonitor"),
+ mMonitorSave("mozPersonalDictionary::mMonitorSave")
+{
+}
+
+mozPersonalDictionary::~mozPersonalDictionary()
+{
+}
+
+nsresult mozPersonalDictionary::Init()
+{
+ nsCOMPtr<nsIObserverService> svc =
+ do_GetService("@mozilla.org/observer-service;1");
+
+ NS_ENSURE_STATE(svc);
+ // we want to reload the dictionary if the profile switches
+ nsresult rv = svc->AddObserver(this, "profile-do-change", true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = svc->AddObserver(this, "profile-before-change", true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ Load();
+
+ return NS_OK;
+}
+
+void mozPersonalDictionary::WaitForLoad()
+{
+ // If the dictionary is already loaded, we return straight away.
+ if (mIsLoaded) {
+ return;
+ }
+
+ // If the dictionary hasn't been loaded, we try to lock the same monitor
+ // that the thread uses that does the load. This way the main thread will
+ // be suspended until the monitor becomes available.
+ mozilla::MonitorAutoLock mon(mMonitor);
+
+ // The monitor has become available. This can have two reasons:
+ // 1: The thread that does the load has finished.
+ // 2: The thread that does the load hasn't even started.
+ // In this case we need to wait.
+ if (!mIsLoaded) {
+ mon.Wait();
+ }
+}
+
+nsresult mozPersonalDictionary::LoadInternal()
+{
+ nsresult rv;
+ mozilla::MonitorAutoLock mon(mMonitor);
+
+ if (mIsLoaded) {
+ return NS_OK;
+ }
+
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mFile));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!mFile) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = mFile->Append(NS_LITERAL_STRING(MOZ_PERSONAL_DICT_NAME));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIEventTarget> target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIRunnable> runnable = new mozPersonalDictionaryLoader(this);
+ rv = target->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozPersonalDictionary::Load()
+{
+ nsresult rv = LoadInternal();
+
+ if (NS_FAILED(rv)) {
+ mIsLoaded = true;
+ }
+
+ return rv;
+}
+
+void mozPersonalDictionary::SyncLoad()
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ mozilla::MonitorAutoLock mon(mMonitor);
+
+ if (mIsLoaded) {
+ return;
+ }
+
+ SyncLoadInternal();
+ mIsLoaded = true;
+ mon.Notify();
+}
+
+void mozPersonalDictionary::SyncLoadInternal()
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ //FIXME Deinst -- get dictionary name from prefs;
+ nsresult rv;
+ bool dictExists;
+
+ rv = mFile->Exists(&dictExists);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ if (!dictExists) {
+ // Nothing is really wrong...
+ return;
+ }
+
+ nsCOMPtr<nsIInputStream> inStream;
+ NS_NewLocalFileInputStream(getter_AddRefs(inStream), mFile);
+
+ nsCOMPtr<nsIUnicharInputStream> convStream;
+ rv = NS_NewUnicharInputStream(inStream, getter_AddRefs(convStream));
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ // we're rereading to get rid of the old data -- we shouldn't have any, but...
+ mDictionaryTable.Clear();
+
+ char16_t c;
+ uint32_t nRead;
+ bool done = false;
+ do{ // read each line of text into the string array.
+ if( (NS_OK != convStream->Read(&c, 1, &nRead)) || (nRead != 1)) break;
+ while(!done && ((c == '\n') || (c == '\r'))){
+ if( (NS_OK != convStream->Read(&c, 1, &nRead)) || (nRead != 1)) done = true;
+ }
+ if (!done){
+ nsAutoString word;
+ while((c != '\n') && (c != '\r') && !done){
+ word.Append(c);
+ if( (NS_OK != convStream->Read(&c, 1, &nRead)) || (nRead != 1)) done = true;
+ }
+ mDictionaryTable.PutEntry(word.get());
+ }
+ } while(!done);
+}
+
+void mozPersonalDictionary::WaitForSave()
+{
+ // If no save is pending, we return straight away.
+ if (!mSavePending) {
+ return;
+ }
+
+ // If a save is pending, we try to lock the same monitor that the thread uses
+ // that does the save. This way the main thread will be suspended until the
+ // monitor becomes available.
+ mozilla::MonitorAutoLock mon(mMonitorSave);
+
+ // The monitor has become available. This can have two reasons:
+ // 1: The thread that does the save has finished.
+ // 2: The thread that does the save hasn't even started.
+ // In this case we need to wait.
+ if (mSavePending) {
+ mon.Wait();
+ }
+}
+
+NS_IMETHODIMP mozPersonalDictionary::Save()
+{
+ nsCOMPtr<nsIFile> theFile;
+ nsresult res;
+
+ WaitForSave();
+
+ mSavePending = true;
+
+ //FIXME Deinst -- get dictionary name from prefs;
+ res = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(theFile));
+ if(NS_FAILED(res)) return res;
+ if(!theFile)return NS_ERROR_FAILURE;
+ res = theFile->Append(NS_LITERAL_STRING(MOZ_PERSONAL_DICT_NAME));
+ if(NS_FAILED(res)) return res;
+
+ nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &res);
+ if (NS_WARN_IF(NS_FAILED(res))) {
+ return res;
+ }
+
+ nsTArray<nsString> array;
+ nsString* elems = array.AppendElements(mDictionaryTable.Count());
+ for (auto iter = mDictionaryTable.Iter(); !iter.Done(); iter.Next()) {
+ elems->Assign(iter.Get()->GetKey());
+ elems++;
+ }
+
+ nsCOMPtr<nsIRunnable> runnable =
+ new mozPersonalDictionarySave(this, theFile, mozilla::Move(array));
+ res = target->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(res))) {
+ return res;
+ }
+ return res;
+}
+
+NS_IMETHODIMP mozPersonalDictionary::GetWordList(nsIStringEnumerator **aWords)
+{
+ NS_ENSURE_ARG_POINTER(aWords);
+ *aWords = nullptr;
+
+ WaitForLoad();
+
+ nsTArray<nsString> *array = new nsTArray<nsString>();
+ nsString* elems = array->AppendElements(mDictionaryTable.Count());
+ for (auto iter = mDictionaryTable.Iter(); !iter.Done(); iter.Next()) {
+ elems->Assign(iter.Get()->GetKey());
+ elems++;
+ }
+
+ array->Sort();
+
+ return NS_NewAdoptingStringEnumerator(aWords, array);
+}
+
+NS_IMETHODIMP mozPersonalDictionary::Check(const char16_t *aWord, const char16_t *aLanguage, bool *aResult)
+{
+ NS_ENSURE_ARG_POINTER(aWord);
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ WaitForLoad();
+
+ *aResult = (mDictionaryTable.GetEntry(aWord) || mIgnoreTable.GetEntry(aWord));
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozPersonalDictionary::AddWord(const char16_t *aWord, const char16_t *aLang)
+{
+ nsresult res;
+ WaitForLoad();
+
+ mDictionaryTable.PutEntry(aWord);
+ res = Save();
+ return res;
+}
+
+NS_IMETHODIMP mozPersonalDictionary::RemoveWord(const char16_t *aWord, const char16_t *aLang)
+{
+ nsresult res;
+ WaitForLoad();
+
+ mDictionaryTable.RemoveEntry(aWord);
+ res = Save();
+ return res;
+}
+
+NS_IMETHODIMP mozPersonalDictionary::IgnoreWord(const char16_t *aWord)
+{
+ // avoid adding duplicate words to the ignore list
+ if (aWord && !mIgnoreTable.GetEntry(aWord))
+ mIgnoreTable.PutEntry(aWord);
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozPersonalDictionary::EndSession()
+{
+ WaitForLoad();
+
+ WaitForSave();
+ mIgnoreTable.Clear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP mozPersonalDictionary::AddCorrection(const char16_t *word, const char16_t *correction, const char16_t *lang)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP mozPersonalDictionary::RemoveCorrection(const char16_t *word, const char16_t *correction, const char16_t *lang)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP mozPersonalDictionary::GetCorrection(const char16_t *word, char16_t ***words, uint32_t *count)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP mozPersonalDictionary::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData)
+{
+ if (!nsCRT::strcmp(aTopic, "profile-do-change")) {
+ // The observer is registered in Init() which calls Load and in turn
+ // LoadInternal(); i.e. Observe() can't be called before Load().
+ WaitForLoad();
+ mIsLoaded = false;
+ Load(); // load automatically clears out the existing dictionary table
+ } else if (!nsCRT::strcmp(aTopic, "profile-before-change")) {
+ WaitForSave();
+ }
+
+ return NS_OK;
+}
diff --git a/extensions/spellcheck/src/mozPersonalDictionary.h b/extensions/spellcheck/src/mozPersonalDictionary.h
new file mode 100644
index 000000000..1a9f082d0
--- /dev/null
+++ b/extensions/spellcheck/src/mozPersonalDictionary.h
@@ -0,0 +1,83 @@
+/* -*- 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 mozPersonalDictionary_h__
+#define mozPersonalDictionary_h__
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "mozIPersonalDictionary.h"
+#include "nsIUnicodeEncoder.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+#include "nsTHashtable.h"
+#include "nsCRT.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsHashKeys.h"
+#include <mozilla/Monitor.h>
+
+#define MOZ_PERSONALDICTIONARY_CONTRACTID "@mozilla.org/spellchecker/personaldictionary;1"
+#define MOZ_PERSONALDICTIONARY_CID \
+{ /* 7EF52EAF-B7E1-462B-87E2-5D1DBACA9048 */ \
+0X7EF52EAF, 0XB7E1, 0X462B, \
+ { 0X87, 0XE2, 0X5D, 0X1D, 0XBA, 0XCA, 0X90, 0X48 } }
+
+class mozPersonalDictionaryLoader;
+class mozPersonalDictionarySave;
+
+class mozPersonalDictionary final : public mozIPersonalDictionary,
+ public nsIObserver,
+ public nsSupportsWeakReference
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_MOZIPERSONALDICTIONARY
+ NS_DECL_NSIOBSERVER
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(mozPersonalDictionary, mozIPersonalDictionary)
+
+ mozPersonalDictionary();
+
+ nsresult Init();
+
+protected:
+ virtual ~mozPersonalDictionary();
+
+ /* true if the dictionary has been loaded from disk */
+ bool mIsLoaded;
+
+ /* true if a dictionary save is pending */
+ bool mSavePending;
+
+ nsCOMPtr<nsIFile> mFile;
+ mozilla::Monitor mMonitor;
+ mozilla::Monitor mMonitorSave;
+ nsTHashtable<nsUnicharPtrHashKey> mDictionaryTable;
+ nsTHashtable<nsUnicharPtrHashKey> mIgnoreTable;
+
+ /*Encoder to use to compare with spellchecker word */
+ nsCOMPtr<nsIUnicodeEncoder> mEncoder;
+
+private:
+ /* wait for the asynchronous load of the dictionary to be completed */
+ void WaitForLoad();
+
+ /* enter the monitor before starting a synchronous load off the main-thread */
+ void SyncLoad();
+
+ /* launch an asynchrounous load of the dictionary from the main-thread
+ * after successfully initializing mFile with the path of the dictionary */
+ nsresult LoadInternal();
+
+ /* perform a synchronous load of the dictionary from disk */
+ void SyncLoadInternal();
+
+ /* wait for the asynchronous save of the dictionary to be completed */
+ void WaitForSave();
+
+ friend class mozPersonalDictionaryLoader;
+ friend class mozPersonalDictionarySave;
+};
+
+#endif
diff --git a/extensions/spellcheck/src/mozSpellChecker.cpp b/extensions/spellcheck/src/mozSpellChecker.cpp
new file mode 100644
index 000000000..771006be3
--- /dev/null
+++ b/extensions/spellcheck/src/mozSpellChecker.cpp
@@ -0,0 +1,565 @@
+/* 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 "mozSpellChecker.h"
+#include "nsIServiceManager.h"
+#include "mozISpellI18NManager.h"
+#include "nsIStringEnumerator.h"
+#include "nsICategoryManager.h"
+#include "nsISupportsPrimitives.h"
+#include "nsISimpleEnumerator.h"
+#include "mozilla/PRemoteSpellcheckEngineChild.h"
+#include "mozilla/dom/ContentChild.h"
+#include "nsXULAppAPI.h"
+
+using mozilla::dom::ContentChild;
+using mozilla::PRemoteSpellcheckEngineChild;
+using mozilla::RemoteSpellcheckEngineChild;
+
+#define DEFAULT_SPELL_CHECKER "@mozilla.org/spellchecker/engine;1"
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(mozSpellChecker)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(mozSpellChecker)
+
+NS_INTERFACE_MAP_BEGIN(mozSpellChecker)
+ NS_INTERFACE_MAP_ENTRY(nsISpellChecker)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpellChecker)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozSpellChecker)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(mozSpellChecker,
+ mTsDoc,
+ mPersonalDictionary)
+
+mozSpellChecker::mozSpellChecker()
+ : mEngine(nullptr)
+{
+}
+
+mozSpellChecker::~mozSpellChecker()
+{
+ if (mPersonalDictionary) {
+ // mPersonalDictionary->Save();
+ mPersonalDictionary->EndSession();
+ }
+ mSpellCheckingEngine = nullptr;
+ mPersonalDictionary = nullptr;
+
+ if (mEngine) {
+ MOZ_ASSERT(XRE_IsContentProcess());
+ mEngine->Send__delete__(mEngine);
+ MOZ_ASSERT(!mEngine);
+ }
+}
+
+nsresult
+mozSpellChecker::Init()
+{
+ mSpellCheckingEngine = nullptr;
+ if (XRE_IsContentProcess()) {
+ mozilla::dom::ContentChild* contentChild = mozilla::dom::ContentChild::GetSingleton();
+ MOZ_ASSERT(contentChild);
+ mEngine = new RemoteSpellcheckEngineChild(this);
+ contentChild->SendPRemoteSpellcheckEngineConstructor(mEngine);
+ } else {
+ mPersonalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1");
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+mozSpellChecker::SetDocument(nsITextServicesDocument *aDoc, bool aFromStartofDoc)
+{
+ mTsDoc = aDoc;
+ mFromStart = aFromStartofDoc;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+mozSpellChecker::NextMisspelledWord(nsAString &aWord, nsTArray<nsString> *aSuggestions)
+{
+ if(!aSuggestions||!mConverter)
+ return NS_ERROR_NULL_POINTER;
+
+ int32_t selOffset;
+ int32_t begin,end;
+ nsresult result;
+ result = SetupDoc(&selOffset);
+ bool isMisspelled,done;
+ if (NS_FAILED(result))
+ return result;
+
+ while( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done )
+ {
+ nsString str;
+ result = mTsDoc->GetCurrentTextBlock(&str);
+
+ if (NS_FAILED(result))
+ return result;
+ do{
+ result = mConverter->FindNextWord(str.get(),str.Length(),selOffset,&begin,&end);
+ if(NS_SUCCEEDED(result)&&(begin != -1)){
+ const nsAString &currWord = Substring(str, begin, end - begin);
+ result = CheckWord(currWord, &isMisspelled, aSuggestions);
+ if(isMisspelled){
+ aWord = currWord;
+ mTsDoc->SetSelection(begin, end-begin);
+ // After ScrollSelectionIntoView(), the pending notifications might
+ // be flushed and PresShell/PresContext/Frames may be dead.
+ // See bug 418470.
+ mTsDoc->ScrollSelectionIntoView();
+ return NS_OK;
+ }
+ }
+ selOffset = end;
+ }while(end != -1);
+ mTsDoc->NextBlock();
+ selOffset=0;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+mozSpellChecker::CheckWord(const nsAString &aWord, bool *aIsMisspelled, nsTArray<nsString> *aSuggestions)
+{
+ nsresult result;
+ bool correct;
+
+ if (XRE_IsContentProcess()) {
+ nsString wordwrapped = nsString(aWord);
+ bool rv;
+ if (aSuggestions) {
+ rv = mEngine->SendCheckAndSuggest(wordwrapped, aIsMisspelled, aSuggestions);
+ } else {
+ rv = mEngine->SendCheck(wordwrapped, aIsMisspelled);
+ }
+ return rv ? NS_OK : NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if(!mSpellCheckingEngine) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ *aIsMisspelled = false;
+ result = mSpellCheckingEngine->Check(PromiseFlatString(aWord).get(), &correct);
+ NS_ENSURE_SUCCESS(result, result);
+ if(!correct){
+ if(aSuggestions){
+ uint32_t count,i;
+ char16_t **words;
+
+ result = mSpellCheckingEngine->Suggest(PromiseFlatString(aWord).get(), &words, &count);
+ NS_ENSURE_SUCCESS(result, result);
+ nsString* suggestions = aSuggestions->AppendElements(count);
+ for(i=0;i<count;i++){
+ suggestions[i].Assign(words[i]);
+ }
+
+ if (count)
+ NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words);
+ }
+ *aIsMisspelled = true;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+mozSpellChecker::Replace(const nsAString &aOldWord, const nsAString &aNewWord, bool aAllOccurrences)
+{
+ if(!mConverter)
+ return NS_ERROR_NULL_POINTER;
+
+ nsAutoString newWord(aNewWord); // sigh
+
+ if(aAllOccurrences){
+ int32_t selOffset;
+ int32_t startBlock,currentBlock,currOffset;
+ int32_t begin,end;
+ bool done;
+ nsresult result;
+ nsAutoString str;
+
+ // find out where we are
+ result = SetupDoc(&selOffset);
+ if(NS_FAILED(result))
+ return result;
+ result = GetCurrentBlockIndex(mTsDoc,&startBlock);
+ if(NS_FAILED(result))
+ return result;
+
+ //start at the beginning
+ result = mTsDoc->FirstBlock();
+ currOffset=0;
+ currentBlock = 0;
+ while( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done )
+ {
+ result = mTsDoc->GetCurrentTextBlock(&str);
+ do{
+ result = mConverter->FindNextWord(str.get(),str.Length(),currOffset,&begin,&end);
+ if(NS_SUCCEEDED(result)&&(begin != -1)){
+ if (aOldWord.Equals(Substring(str, begin, end-begin))) {
+ // if we are before the current selection point but in the same block
+ // move the selection point forwards
+ if((currentBlock == startBlock)&&(begin < selOffset)){
+ selOffset +=
+ int32_t(aNewWord.Length()) - int32_t(aOldWord.Length());
+ if(selOffset < begin) selOffset=begin;
+ }
+ mTsDoc->SetSelection(begin, end-begin);
+ mTsDoc->InsertText(&newWord);
+ mTsDoc->GetCurrentTextBlock(&str);
+ end += (aNewWord.Length() - aOldWord.Length()); // recursion was cute in GEB, not here.
+ }
+ }
+ currOffset = end;
+ }while(currOffset != -1);
+ mTsDoc->NextBlock();
+ currentBlock++;
+ currOffset=0;
+ }
+
+ // We are done replacing. Put the selection point back where we found it (or equivalent);
+ result = mTsDoc->FirstBlock();
+ currentBlock = 0;
+ while(( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done ) &&(currentBlock < startBlock)){
+ mTsDoc->NextBlock();
+ }
+
+//After we have moved to the block where the first occurrence of replace was done, put the
+//selection to the next word following it. In case there is no word following it i.e if it happens
+//to be the last word in that block, then move to the next block and put the selection to the
+//first word in that block, otherwise when the Setupdoc() is called, it queries the LastSelectedBlock()
+//and the selection offset of the last occurrence of the replaced word is taken instead of the first
+//occurrence and things get messed up as reported in the bug 244969
+
+ if( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done ){
+ nsString str;
+ result = mTsDoc->GetCurrentTextBlock(&str);
+ result = mConverter->FindNextWord(str.get(),str.Length(),selOffset,&begin,&end);
+ if(end == -1)
+ {
+ mTsDoc->NextBlock();
+ selOffset=0;
+ result = mTsDoc->GetCurrentTextBlock(&str);
+ result = mConverter->FindNextWord(str.get(),str.Length(),selOffset,&begin,&end);
+ mTsDoc->SetSelection(begin, 0);
+ }
+ else
+ mTsDoc->SetSelection(begin, 0);
+ }
+ }
+ else{
+ mTsDoc->InsertText(&newWord);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+mozSpellChecker::IgnoreAll(const nsAString &aWord)
+{
+ if(mPersonalDictionary){
+ mPersonalDictionary->IgnoreWord(PromiseFlatString(aWord).get());
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+mozSpellChecker::AddWordToPersonalDictionary(const nsAString &aWord)
+{
+ nsresult res;
+ char16_t empty=0;
+ if (!mPersonalDictionary)
+ return NS_ERROR_NULL_POINTER;
+ res = mPersonalDictionary->AddWord(PromiseFlatString(aWord).get(),&empty);
+ return res;
+}
+
+NS_IMETHODIMP
+mozSpellChecker::RemoveWordFromPersonalDictionary(const nsAString &aWord)
+{
+ nsresult res;
+ char16_t empty=0;
+ if (!mPersonalDictionary)
+ return NS_ERROR_NULL_POINTER;
+ res = mPersonalDictionary->RemoveWord(PromiseFlatString(aWord).get(),&empty);
+ return res;
+}
+
+NS_IMETHODIMP
+mozSpellChecker::GetPersonalDictionary(nsTArray<nsString> *aWordList)
+{
+ if(!aWordList || !mPersonalDictionary)
+ return NS_ERROR_NULL_POINTER;
+
+ nsCOMPtr<nsIStringEnumerator> words;
+ mPersonalDictionary->GetWordList(getter_AddRefs(words));
+
+ bool hasMore;
+ nsAutoString word;
+ while (NS_SUCCEEDED(words->HasMore(&hasMore)) && hasMore) {
+ words->GetNext(word);
+ aWordList->AppendElement(word);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+mozSpellChecker::GetDictionaryList(nsTArray<nsString> *aDictionaryList)
+{
+ if (XRE_IsContentProcess()) {
+ ContentChild *child = ContentChild::GetSingleton();
+ child->GetAvailableDictionaries(*aDictionaryList);
+ return NS_OK;
+ }
+
+ nsresult rv;
+
+ // For catching duplicates
+ nsTHashtable<nsStringHashKey> dictionaries;
+
+ nsCOMArray<mozISpellCheckingEngine> spellCheckingEngines;
+ rv = GetEngineList(&spellCheckingEngines);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (int32_t i = 0; i < spellCheckingEngines.Count(); i++) {
+ nsCOMPtr<mozISpellCheckingEngine> engine = spellCheckingEngines[i];
+
+ uint32_t count = 0;
+ char16_t **words = nullptr;
+ engine->GetDictionaryList(&words, &count);
+ for (uint32_t k = 0; k < count; k++) {
+ nsAutoString dictName;
+
+ dictName.Assign(words[k]);
+
+ // Skip duplicate dictionaries. Only take the first one
+ // for each name.
+ if (dictionaries.Contains(dictName))
+ continue;
+
+ dictionaries.PutEntry(dictName);
+
+ if (!aDictionaryList->AppendElement(dictName)) {
+ NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+mozSpellChecker::GetCurrentDictionary(nsAString &aDictionary)
+{
+ if (XRE_IsContentProcess()) {
+ aDictionary = mCurrentDictionary;
+ return NS_OK;
+ }
+
+ if (!mSpellCheckingEngine) {
+ aDictionary.Truncate();
+ return NS_OK;
+ }
+
+ nsXPIDLString dictname;
+ mSpellCheckingEngine->GetDictionary(getter_Copies(dictname));
+ aDictionary = dictname;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+mozSpellChecker::SetCurrentDictionary(const nsAString &aDictionary)
+{
+ if (XRE_IsContentProcess()) {
+ nsString wrappedDict = nsString(aDictionary);
+ bool isSuccess;
+ mEngine->SendSetDictionary(wrappedDict, &isSuccess);
+ if (!isSuccess) {
+ mCurrentDictionary.Truncate();
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mCurrentDictionary = wrappedDict;
+ return NS_OK;
+ }
+
+ // Calls to mozISpellCheckingEngine::SetDictionary might destroy us
+ RefPtr<mozSpellChecker> kungFuDeathGrip = this;
+
+ mSpellCheckingEngine = nullptr;
+
+ if (aDictionary.IsEmpty()) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+ nsCOMArray<mozISpellCheckingEngine> spellCheckingEngines;
+ rv = GetEngineList(&spellCheckingEngines);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (int32_t i = 0; i < spellCheckingEngines.Count(); i++) {
+ // We must set mSpellCheckingEngine before we call SetDictionary, since
+ // SetDictionary calls back to this spell checker to check if the
+ // dictionary was set
+ mSpellCheckingEngine = spellCheckingEngines[i];
+
+ rv = mSpellCheckingEngine->SetDictionary(PromiseFlatString(aDictionary).get());
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<mozIPersonalDictionary> personalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1");
+ mSpellCheckingEngine->SetPersonalDictionary(personalDictionary.get());
+
+ nsXPIDLString language;
+ nsCOMPtr<mozISpellI18NManager> serv(do_GetService("@mozilla.org/spellchecker/i18nmanager;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return serv->GetUtil(language.get(),getter_AddRefs(mConverter));
+ }
+ }
+
+ mSpellCheckingEngine = nullptr;
+
+ // We could not find any engine with the requested dictionary
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult
+mozSpellChecker::SetupDoc(int32_t *outBlockOffset)
+{
+ nsresult rv;
+
+ nsITextServicesDocument::TSDBlockSelectionStatus blockStatus;
+ int32_t selOffset;
+ int32_t selLength;
+ *outBlockOffset = 0;
+
+ if (!mFromStart)
+ {
+ rv = mTsDoc->LastSelectedBlock(&blockStatus, &selOffset, &selLength);
+ if (NS_SUCCEEDED(rv) && (blockStatus != nsITextServicesDocument::eBlockNotFound))
+ {
+ switch (blockStatus)
+ {
+ case nsITextServicesDocument::eBlockOutside: // No TB in S, but found one before/after S.
+ case nsITextServicesDocument::eBlockPartial: // S begins or ends in TB but extends outside of TB.
+ // the TS doc points to the block we want.
+ *outBlockOffset = selOffset + selLength;
+ break;
+
+ case nsITextServicesDocument::eBlockInside: // S extends beyond the start and end of TB.
+ // we want the block after this one.
+ rv = mTsDoc->NextBlock();
+ *outBlockOffset = 0;
+ break;
+
+ case nsITextServicesDocument::eBlockContains: // TB contains entire S.
+ *outBlockOffset = selOffset + selLength;
+ break;
+
+ case nsITextServicesDocument::eBlockNotFound: // There is no text block (TB) in or before the selection (S).
+ default:
+ NS_NOTREACHED("Shouldn't ever get this status");
+ }
+ }
+ else //failed to get last sel block. Just start at beginning
+ {
+ rv = mTsDoc->FirstBlock();
+ *outBlockOffset = 0;
+ }
+
+ }
+ else // we want the first block
+ {
+ rv = mTsDoc->FirstBlock();
+ mFromStart = false;
+ }
+ return rv;
+}
+
+
+// utility method to discover which block we're in. The TSDoc interface doesn't give
+// us this, because it can't assume a read-only document.
+// shamelessly stolen from nsTextServicesDocument
+nsresult
+mozSpellChecker::GetCurrentBlockIndex(nsITextServicesDocument *aDoc, int32_t *outBlockIndex)
+{
+ int32_t blockIndex = 0;
+ bool isDone = false;
+ nsresult result = NS_OK;
+
+ do
+ {
+ aDoc->PrevBlock();
+
+ result = aDoc->IsDone(&isDone);
+
+ if (!isDone)
+ blockIndex ++;
+
+ } while (NS_SUCCEEDED(result) && !isDone);
+
+ *outBlockIndex = blockIndex;
+
+ return result;
+}
+
+nsresult
+mozSpellChecker::GetEngineList(nsCOMArray<mozISpellCheckingEngine>* aSpellCheckingEngines)
+{
+ MOZ_ASSERT(!XRE_IsContentProcess());
+
+ nsresult rv;
+ bool hasMoreEngines;
+
+ nsCOMPtr<nsICategoryManager> catMgr = do_GetService(NS_CATEGORYMANAGER_CONTRACTID);
+ if (!catMgr)
+ return NS_ERROR_NULL_POINTER;
+
+ nsCOMPtr<nsISimpleEnumerator> catEntries;
+
+ // Get contract IDs of registrated external spell-check engines and
+ // append one of HunSpell at the end.
+ rv = catMgr->EnumerateCategory("spell-check-engine", getter_AddRefs(catEntries));
+ if (NS_FAILED(rv))
+ return rv;
+
+ while (catEntries->HasMoreElements(&hasMoreEngines), hasMoreEngines){
+ nsCOMPtr<nsISupports> elem;
+ rv = catEntries->GetNext(getter_AddRefs(elem));
+
+ nsCOMPtr<nsISupportsCString> entry = do_QueryInterface(elem, &rv);
+ if (NS_FAILED(rv))
+ return rv;
+
+ nsCString contractId;
+ rv = entry->GetData(contractId);
+ if (NS_FAILED(rv))
+ return rv;
+
+ // Try to load spellchecker engine. Ignore errors silently
+ // except for the last one (HunSpell).
+ nsCOMPtr<mozISpellCheckingEngine> engine =
+ do_GetService(contractId.get(), &rv);
+ if (NS_SUCCEEDED(rv)) {
+ aSpellCheckingEngines->AppendObject(engine);
+ }
+ }
+
+ // Try to load HunSpell spellchecker engine.
+ nsCOMPtr<mozISpellCheckingEngine> engine =
+ do_GetService(DEFAULT_SPELL_CHECKER, &rv);
+ if (NS_FAILED(rv)) {
+ // Fail if not succeeded to load HunSpell. Ignore errors
+ // for external spellcheck engines.
+ return rv;
+ }
+ aSpellCheckingEngines->AppendObject(engine);
+
+ return NS_OK;
+}
diff --git a/extensions/spellcheck/src/mozSpellChecker.h b/extensions/spellcheck/src/mozSpellChecker.h
new file mode 100644
index 000000000..883dee38d
--- /dev/null
+++ b/extensions/spellcheck/src/mozSpellChecker.h
@@ -0,0 +1,76 @@
+/* -*- 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 mozSpellChecker_h__
+#define mozSpellChecker_h__
+
+#include "nsCOMPtr.h"
+#include "nsCOMArray.h"
+#include "nsISpellChecker.h"
+#include "nsString.h"
+#include "nsITextServicesDocument.h"
+#include "mozIPersonalDictionary.h"
+#include "mozISpellCheckingEngine.h"
+#include "nsClassHashtable.h"
+#include "nsTArray.h"
+#include "mozISpellI18NUtil.h"
+#include "nsCycleCollectionParticipant.h"
+#include "RemoteSpellCheckEngineChild.h"
+
+namespace mozilla {
+class PRemoteSpellcheckEngineChild;
+class RemoteSpellcheckEngineChild;
+} // namespace mozilla
+
+class mozSpellChecker : public nsISpellChecker
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(mozSpellChecker)
+
+ mozSpellChecker();
+
+ nsresult Init();
+
+ // nsISpellChecker
+ NS_IMETHOD SetDocument(nsITextServicesDocument *aDoc, bool aFromStartofDoc) override;
+ NS_IMETHOD NextMisspelledWord(nsAString &aWord, nsTArray<nsString> *aSuggestions) override;
+ NS_IMETHOD CheckWord(const nsAString &aWord, bool *aIsMisspelled, nsTArray<nsString> *aSuggestions) override;
+ NS_IMETHOD Replace(const nsAString &aOldWord, const nsAString &aNewWord, bool aAllOccurrences) override;
+ NS_IMETHOD IgnoreAll(const nsAString &aWord) override;
+
+ NS_IMETHOD AddWordToPersonalDictionary(const nsAString &aWord) override;
+ NS_IMETHOD RemoveWordFromPersonalDictionary(const nsAString &aWord) override;
+ NS_IMETHOD GetPersonalDictionary(nsTArray<nsString> *aWordList) override;
+
+ NS_IMETHOD GetDictionaryList(nsTArray<nsString> *aDictionaryList) override;
+ NS_IMETHOD GetCurrentDictionary(nsAString &aDictionary) override;
+ NS_IMETHOD SetCurrentDictionary(const nsAString &aDictionary) override;
+
+ void DeleteRemoteEngine() {
+ mEngine = nullptr;
+ }
+
+protected:
+ virtual ~mozSpellChecker();
+
+ nsCOMPtr<mozISpellI18NUtil> mConverter;
+ nsCOMPtr<nsITextServicesDocument> mTsDoc;
+ nsCOMPtr<mozIPersonalDictionary> mPersonalDictionary;
+
+ nsCOMPtr<mozISpellCheckingEngine> mSpellCheckingEngine;
+ bool mFromStart;
+
+ nsString mCurrentDictionary;
+
+ nsresult SetupDoc(int32_t *outBlockOffset);
+
+ nsresult GetCurrentBlockIndex(nsITextServicesDocument *aDoc, int32_t *outBlockIndex);
+
+ nsresult GetEngineList(nsCOMArray<mozISpellCheckingEngine> *aDictionaryList);
+
+ mozilla::PRemoteSpellcheckEngineChild *mEngine;
+};
+#endif // mozSpellChecker_h__
diff --git a/extensions/spellcheck/src/mozSpellCheckerFactory.cpp b/extensions/spellcheck/src/mozSpellCheckerFactory.cpp
new file mode 100644
index 000000000..301f5d284
--- /dev/null
+++ b/extensions/spellcheck/src/mozSpellCheckerFactory.cpp
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 "mozilla/ModuleUtils.h"
+#include "mozHunspell.h"
+#include "mozHunspellDirProvider.h"
+#include "mozSpellChecker.h"
+#include "mozInlineSpellChecker.h"
+#include "nsTextServicesCID.h"
+#include "mozPersonalDictionary.h"
+#include "mozSpellI18NManager.h"
+#include "nsIFile.h"
+
+#define NS_SPELLCHECKER_CID \
+{ /* 8227f019-afc7-461e-b030-9f185d7a0e29 */ \
+0x8227F019, 0xAFC7, 0x461e, \
+{ 0xB0, 0x30, 0x9F, 0x18, 0x5D, 0x7A, 0x0E, 0x29} }
+
+#define MOZ_INLINESPELLCHECKER_CID \
+{ /* 9FE5D975-09BD-44aa-A01A-66402EA28657 */ \
+0x9fe5d975, 0x9bd, 0x44aa, \
+{ 0xa0, 0x1a, 0x66, 0x40, 0x2e, 0xa2, 0x86, 0x57} }
+
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(mozHunspell, Init)
+NS_GENERIC_FACTORY_CONSTRUCTOR(mozHunspellDirProvider)
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(mozSpellChecker, Init)
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(mozPersonalDictionary, Init)
+NS_GENERIC_FACTORY_CONSTRUCTOR(mozSpellI18NManager)
+NS_GENERIC_FACTORY_CONSTRUCTOR(mozInlineSpellChecker)
+
+NS_DEFINE_NAMED_CID(MOZ_HUNSPELL_CID);
+NS_DEFINE_NAMED_CID(HUNSPELLDIRPROVIDER_CID);
+NS_DEFINE_NAMED_CID(NS_SPELLCHECKER_CID);
+NS_DEFINE_NAMED_CID(MOZ_PERSONALDICTIONARY_CID);
+NS_DEFINE_NAMED_CID(MOZ_SPELLI18NMANAGER_CID);
+NS_DEFINE_NAMED_CID(MOZ_INLINESPELLCHECKER_CID);
+
+static const mozilla::Module::CIDEntry kSpellcheckCIDs[] = {
+ { &kMOZ_HUNSPELL_CID, false, nullptr, mozHunspellConstructor },
+ { &kHUNSPELLDIRPROVIDER_CID, false, nullptr, mozHunspellDirProviderConstructor },
+ { &kNS_SPELLCHECKER_CID, false, nullptr, mozSpellCheckerConstructor },
+ { &kMOZ_PERSONALDICTIONARY_CID, false, nullptr, mozPersonalDictionaryConstructor },
+ { &kMOZ_SPELLI18NMANAGER_CID, false, nullptr, mozSpellI18NManagerConstructor },
+ { &kMOZ_INLINESPELLCHECKER_CID, false, nullptr, mozInlineSpellCheckerConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kSpellcheckContracts[] = {
+ { MOZ_HUNSPELL_CONTRACTID, &kMOZ_HUNSPELL_CID },
+ { mozHunspellDirProvider::kContractID, &kHUNSPELLDIRPROVIDER_CID },
+ { NS_SPELLCHECKER_CONTRACTID, &kNS_SPELLCHECKER_CID },
+ { MOZ_PERSONALDICTIONARY_CONTRACTID, &kMOZ_PERSONALDICTIONARY_CID },
+ { MOZ_SPELLI18NMANAGER_CONTRACTID, &kMOZ_SPELLI18NMANAGER_CID },
+ { MOZ_INLINESPELLCHECKER_CONTRACTID, &kMOZ_INLINESPELLCHECKER_CID },
+ { nullptr }
+};
+
+static const mozilla::Module::CategoryEntry kSpellcheckCategories[] = {
+ { XPCOM_DIRECTORY_PROVIDER_CATEGORY, "spellcheck-directory-provider", mozHunspellDirProvider::kContractID },
+ { nullptr }
+};
+
+const mozilla::Module kSpellcheckModule = {
+ mozilla::Module::kVersion,
+ kSpellcheckCIDs,
+ kSpellcheckContracts,
+ kSpellcheckCategories
+};
+
+NSMODULE_DEFN(mozSpellCheckerModule) = &kSpellcheckModule;
diff --git a/extensions/spellcheck/src/mozSpellI18NManager.cpp b/extensions/spellcheck/src/mozSpellI18NManager.cpp
new file mode 100644
index 000000000..7b77e707e
--- /dev/null
+++ b/extensions/spellcheck/src/mozSpellI18NManager.cpp
@@ -0,0 +1,32 @@
+/* -*- 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 "mozSpellI18NManager.h"
+#include "mozEnglishWordUtils.h"
+#include "nsString.h"
+#include "mozilla/RefPtr.h"
+
+NS_IMPL_ISUPPORTS(mozSpellI18NManager, mozISpellI18NManager)
+
+mozSpellI18NManager::mozSpellI18NManager()
+{
+}
+
+mozSpellI18NManager::~mozSpellI18NManager()
+{
+}
+
+NS_IMETHODIMP mozSpellI18NManager::GetUtil(const char16_t *aLanguage, mozISpellI18NUtil **_retval)
+{
+ if (!_retval) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // XXX TODO Actually handle multiple languages.
+ RefPtr<mozEnglishWordUtils> utils = new mozEnglishWordUtils;
+ utils.forget(_retval);
+
+ return NS_OK;
+}
diff --git a/extensions/spellcheck/src/mozSpellI18NManager.h b/extensions/spellcheck/src/mozSpellI18NManager.h
new file mode 100644
index 000000000..3f075f69c
--- /dev/null
+++ b/extensions/spellcheck/src/mozSpellI18NManager.h
@@ -0,0 +1,30 @@
+/* -*- 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 mozSpellI18NManager_h__
+#define mozSpellI18NManager_h__
+
+#include "nsCOMPtr.h"
+#include "mozISpellI18NManager.h"
+#include "nsCycleCollectionParticipant.h"
+
+#define MOZ_SPELLI18NMANAGER_CONTRACTID "@mozilla.org/spellchecker/i18nmanager;1"
+#define MOZ_SPELLI18NMANAGER_CID \
+{ /* {AEB8936F-219C-4D3C-8385-D9382DAA551A} */ \
+0xaeb8936f, 0x219c, 0x4d3c, \
+ { 0x83, 0x85, 0xd9, 0x38, 0x2d, 0xaa, 0x55, 0x1a } }
+
+class mozSpellI18NManager : public mozISpellI18NManager
+{
+protected:
+ virtual ~mozSpellI18NManager();
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISPELLI18NMANAGER
+
+ mozSpellI18NManager();
+};
+#endif
+