1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 sts=2 sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "EditorSpellCheck.h"
9 #include "mozilla/Attributes.h" // for final
10 #include "mozilla/EditorBase.h" // for EditorBase
11 #include "mozilla/HTMLEditor.h" // for HTMLEditor
12 #include "mozilla/dom/Element.h" // for Element
13 #include "mozilla/dom/Selection.h"
14 #include "mozilla/dom/StaticRange.h"
15 #include "mozilla/intl/LocaleService.h" // for retrieving app locale
16 #include "mozilla/intl/MozLocale.h" // for mozilla::intl::Locale
17 #include "mozilla/intl/OSPreferences.h" // for mozilla::intl::OSPreferences
18 #include "mozilla/Logging.h" // for mozilla::LazyLogModule
19 #include "mozilla/mozalloc.h" // for operator delete, etc
20 #include "mozilla/mozSpellChecker.h" // for mozSpellChecker
21 #include "mozilla/Preferences.h" // for Preferences
22 #include "mozilla/TextServicesDocument.h" // for TextServicesDocument
23 #include "nsAString.h" // for nsAString::IsEmpty, etc
24 #include "nsComponentManagerUtils.h" // for do_CreateInstance
25 #include "nsDebug.h" // for NS_ENSURE_TRUE, etc
26 #include "nsDependentSubstring.h" // for Substring
27 #include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc
28 #include "nsIContent.h" // for nsIContent
29 #include "nsIContentPrefService2.h" // for nsIContentPrefService2, etc
30 #include "mozilla/dom/Document.h" // for Document
31 #include "nsIEditor.h" // for nsIEditor
32 #include "nsILoadContext.h"
33 #include "nsISupportsBase.h" // for nsISupports
34 #include "nsISupportsUtils.h" // for NS_ADDREF
35 #include "nsIURI.h" // for nsIURI
36 #include "nsThreadUtils.h" // for GetMainThreadSerialEventTarget
37 #include "nsVariant.h" // for nsIWritableVariant, etc
38 #include "nsLiteralString.h" // for NS_LITERAL_STRING, etc
39 #include "nsMemory.h" // for nsMemory
41 #include "nsReadableUtils.h" // for ToNewUnicode, EmptyString, etc
42 #include "nsServiceManagerUtils.h" // for do_GetService
43 #include "nsString.h" // for nsAutoString, nsString, etc
44 #include "nsStringFwd.h" // for nsAFlatString
45 #include "nsStyleUtil.h" // for nsStyleUtil
46 #include "nsXULAppAPI.h" // for XRE_GetProcessType
51 using intl::LocaleService
;
52 using intl::OSPreferences
;
54 static mozilla::LazyLogModule
sEditorSpellChecker("EditorSpellChecker");
56 class UpdateDictionaryHolder
{
58 EditorSpellCheck
* mSpellCheck
;
61 explicit UpdateDictionaryHolder(EditorSpellCheck
* esc
) : mSpellCheck(esc
) {
63 mSpellCheck
->BeginUpdateDictionary();
67 ~UpdateDictionaryHolder() {
69 mSpellCheck
->EndUpdateDictionary();
74 #define CPS_PREF_NAME u"spellcheck.lang"_ns
77 * Gets the URI of aEditor's document.
79 static nsIURI
* GetDocumentURI(EditorBase
* aEditor
) {
82 Document
* doc
= aEditor
->AsEditorBase()->GetDocument();
83 if (NS_WARN_IF(!doc
)) {
87 return doc
->GetDocumentURI();
90 static nsILoadContext
* GetLoadContext(nsIEditor
* aEditor
) {
91 Document
* doc
= aEditor
->AsEditorBase()->GetDocument();
92 if (NS_WARN_IF(!doc
)) {
96 return doc
->GetLoadContext();
100 * Fetches the dictionary stored in content prefs and maintains state during the
101 * fetch, which is asynchronous.
103 class DictionaryFetcher final
: public nsIContentPrefCallback2
{
107 DictionaryFetcher(EditorSpellCheck
* aSpellCheck
,
108 nsIEditorSpellCheckCallback
* aCallback
, uint32_t aGroup
)
109 : mCallback(aCallback
), mGroup(aGroup
), mSpellCheck(aSpellCheck
) {}
111 NS_IMETHOD
Fetch(nsIEditor
* aEditor
);
113 NS_IMETHOD
HandleResult(nsIContentPref
* aPref
) override
{
114 nsCOMPtr
<nsIVariant
> value
;
115 nsresult rv
= aPref
->GetValue(getter_AddRefs(value
));
116 NS_ENSURE_SUCCESS(rv
, rv
);
117 value
->GetAsAString(mDictionary
);
121 NS_IMETHOD
HandleCompletion(uint16_t reason
) override
{
122 mSpellCheck
->DictionaryFetched(this);
126 NS_IMETHOD
HandleError(nsresult error
) override
{ return NS_OK
; }
128 nsCOMPtr
<nsIEditorSpellCheckCallback
> mCallback
;
130 nsString mRootContentLang
;
131 nsString mRootDocContentLang
;
132 nsString mDictionary
;
135 ~DictionaryFetcher() {}
137 RefPtr
<EditorSpellCheck
> mSpellCheck
;
140 NS_IMPL_ISUPPORTS(DictionaryFetcher
, nsIContentPrefCallback2
)
142 class ContentPrefInitializerRunnable final
: public Runnable
{
144 ContentPrefInitializerRunnable(nsIEditor
* aEditor
,
145 nsIContentPrefCallback2
* aCallback
)
146 : Runnable("ContentPrefInitializerRunnable"),
147 mEditorBase(aEditor
->AsEditorBase()),
148 mCallback(aCallback
) {}
150 NS_IMETHOD
Run() override
{
151 if (mEditorBase
->Destroyed()) {
152 mCallback
->HandleError(NS_ERROR_NOT_AVAILABLE
);
156 nsCOMPtr
<nsIContentPrefService2
> contentPrefService
=
157 do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID
);
158 if (NS_WARN_IF(!contentPrefService
)) {
159 mCallback
->HandleError(NS_ERROR_NOT_AVAILABLE
);
163 nsCOMPtr
<nsIURI
> docUri
= GetDocumentURI(mEditorBase
);
164 if (NS_WARN_IF(!docUri
)) {
165 mCallback
->HandleError(NS_ERROR_FAILURE
);
169 nsAutoCString docUriSpec
;
170 nsresult rv
= docUri
->GetSpec(docUriSpec
);
171 if (NS_WARN_IF(NS_FAILED(rv
))) {
172 mCallback
->HandleError(rv
);
176 rv
= contentPrefService
->GetByDomainAndName(
177 NS_ConvertUTF8toUTF16(docUriSpec
), CPS_PREF_NAME
,
178 GetLoadContext(mEditorBase
), mCallback
);
179 if (NS_WARN_IF(NS_FAILED(rv
))) {
180 mCallback
->HandleError(rv
);
187 RefPtr
<EditorBase
> mEditorBase
;
188 nsCOMPtr
<nsIContentPrefCallback2
> mCallback
;
192 DictionaryFetcher::Fetch(nsIEditor
* aEditor
) {
193 NS_ENSURE_ARG_POINTER(aEditor
);
195 nsCOMPtr
<nsIRunnable
> runnable
=
196 new ContentPrefInitializerRunnable(aEditor
, this);
197 NS_DispatchToCurrentThreadQueue(runnable
.forget(), 1000,
198 EventQueuePriority::Idle
);
204 * Stores the current dictionary for aEditor's document URL.
206 static nsresult
StoreCurrentDictionary(EditorBase
* aEditorBase
,
207 const nsACString
& aDictionary
) {
208 NS_ENSURE_ARG_POINTER(aEditorBase
);
212 nsCOMPtr
<nsIURI
> docUri
= GetDocumentURI(aEditorBase
);
213 if (NS_WARN_IF(!docUri
)) {
214 return NS_ERROR_FAILURE
;
217 nsAutoCString docUriSpec
;
218 rv
= docUri
->GetSpec(docUriSpec
);
219 NS_ENSURE_SUCCESS(rv
, rv
);
221 RefPtr
<nsVariant
> prefValue
= new nsVariant();
222 prefValue
->SetAsAString(NS_ConvertUTF8toUTF16(aDictionary
));
224 nsCOMPtr
<nsIContentPrefService2
> contentPrefService
=
225 do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID
);
226 NS_ENSURE_TRUE(contentPrefService
, NS_ERROR_NOT_INITIALIZED
);
228 return contentPrefService
->Set(NS_ConvertUTF8toUTF16(docUriSpec
),
229 CPS_PREF_NAME
, prefValue
,
230 GetLoadContext(aEditorBase
), nullptr);
234 * Forgets the current dictionary stored for aEditor's document URL.
236 static nsresult
ClearCurrentDictionary(EditorBase
* aEditorBase
) {
237 NS_ENSURE_ARG_POINTER(aEditorBase
);
241 nsCOMPtr
<nsIURI
> docUri
= GetDocumentURI(aEditorBase
);
242 if (NS_WARN_IF(!docUri
)) {
243 return NS_ERROR_FAILURE
;
246 nsAutoCString docUriSpec
;
247 rv
= docUri
->GetSpec(docUriSpec
);
248 NS_ENSURE_SUCCESS(rv
, rv
);
250 nsCOMPtr
<nsIContentPrefService2
> contentPrefService
=
251 do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID
);
252 NS_ENSURE_TRUE(contentPrefService
, NS_ERROR_NOT_INITIALIZED
);
254 return contentPrefService
->RemoveByDomainAndName(
255 NS_ConvertUTF8toUTF16(docUriSpec
), CPS_PREF_NAME
,
256 GetLoadContext(aEditorBase
), nullptr);
259 NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorSpellCheck
)
260 NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorSpellCheck
)
262 NS_INTERFACE_MAP_BEGIN(EditorSpellCheck
)
263 NS_INTERFACE_MAP_ENTRY(nsIEditorSpellCheck
)
264 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports
, nsIEditorSpellCheck
)
265 NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(EditorSpellCheck
)
268 NS_IMPL_CYCLE_COLLECTION(EditorSpellCheck
, mEditor
, mSpellChecker
)
270 EditorSpellCheck::EditorSpellCheck()
271 : mTxtSrvFilterType(0),
272 mSuggestedWordIndex(0),
274 mDictionaryFetcherGroup(0),
275 mUpdateDictionaryRunning(false) {}
277 EditorSpellCheck::~EditorSpellCheck() {
278 // Make sure we blow the spellchecker away, just in
279 // case it hasn't been destroyed already.
280 mSpellChecker
= nullptr;
283 mozSpellChecker
* EditorSpellCheck::GetSpellChecker() { return mSpellChecker
; }
285 // The problem is that if the spell checker does not exist, we can not tell
286 // which dictionaries are installed. This function works around the problem,
287 // allowing callers to ask if we can spell check without actually doing so (and
288 // enabling or disabling UI as necessary). This just creates a spellcheck
289 // object if needed and asks it for the dictionary list.
291 EditorSpellCheck::CanSpellCheck(bool* aCanSpellCheck
) {
292 RefPtr
<mozSpellChecker
> spellChecker
= mSpellChecker
;
294 spellChecker
= mozSpellChecker::Create();
295 MOZ_ASSERT(spellChecker
);
297 nsTArray
<nsCString
> dictList
;
298 nsresult rv
= spellChecker
->GetDictionaryList(&dictList
);
299 if (NS_WARN_IF(NS_FAILED(rv
))) {
303 *aCanSpellCheck
= !dictList
.IsEmpty();
307 // Instances of this class can be used as either runnables or RAII helpers.
308 class CallbackCaller final
: public Runnable
{
310 explicit CallbackCaller(nsIEditorSpellCheckCallback
* aCallback
)
311 : mozilla::Runnable("CallbackCaller"), mCallback(aCallback
) {}
313 ~CallbackCaller() { Run(); }
315 NS_IMETHOD
Run() override
{
317 mCallback
->EditorSpellCheckDone();
324 nsCOMPtr
<nsIEditorSpellCheckCallback
> mCallback
;
328 EditorSpellCheck::InitSpellChecker(nsIEditor
* aEditor
,
329 bool aEnableSelectionChecking
,
330 nsIEditorSpellCheckCallback
* aCallback
) {
331 NS_ENSURE_TRUE(aEditor
, NS_ERROR_NULL_POINTER
);
332 mEditor
= aEditor
->AsEditorBase();
334 RefPtr
<Document
> doc
= mEditor
->GetDocument();
335 if (NS_WARN_IF(!doc
)) {
336 return NS_ERROR_FAILURE
;
341 // We can spell check with any editor type
342 RefPtr
<TextServicesDocument
> textServicesDocument
=
343 new TextServicesDocument();
344 textServicesDocument
->SetFilterType(mTxtSrvFilterType
);
346 // EditorBase::AddEditActionListener() needs to access mSpellChecker and
347 // mSpellChecker->GetTextServicesDocument(). Therefore, we need to
348 // initialize them before calling TextServicesDocument::InitWithEditor()
349 // since it calls EditorBase::AddEditActionListener().
350 mSpellChecker
= mozSpellChecker::Create();
351 MOZ_ASSERT(mSpellChecker
);
352 rv
= mSpellChecker
->SetDocument(textServicesDocument
, true);
353 if (NS_WARN_IF(NS_FAILED(rv
))) {
357 // Pass the editor to the text services document
358 rv
= textServicesDocument
->InitWithEditor(aEditor
);
359 NS_ENSURE_SUCCESS(rv
, rv
);
361 if (aEnableSelectionChecking
) {
362 // Find out if the section is collapsed or not.
363 // If it isn't, we want to spellcheck just the selection.
365 RefPtr
<Selection
> selection
;
366 aEditor
->GetSelection(getter_AddRefs(selection
));
367 if (NS_WARN_IF(!selection
)) {
368 return NS_ERROR_FAILURE
;
371 if (selection
->RangeCount()) {
372 RefPtr
<const nsRange
> range
= selection
->GetRangeAt(0);
373 NS_ENSURE_STATE(range
);
375 if (!range
->Collapsed()) {
376 // We don't want to touch the range in the selection,
377 // so create a new copy of it.
378 RefPtr
<StaticRange
> staticRange
=
379 StaticRange::Create(range
, IgnoreErrors());
380 if (NS_WARN_IF(!staticRange
)) {
381 return NS_ERROR_FAILURE
;
384 // Make sure the new range spans complete words.
385 rv
= textServicesDocument
->ExpandRangeToWordBoundaries(staticRange
);
386 if (NS_WARN_IF(NS_FAILED(rv
))) {
390 // Now tell the text services that you only want
391 // to iterate over the text in this range.
392 rv
= textServicesDocument
->SetExtent(staticRange
);
393 if (NS_WARN_IF(NS_FAILED(rv
))) {
399 // do not fail if UpdateCurrentDictionary fails because this method may
401 rv
= UpdateCurrentDictionary(aCallback
);
402 if (NS_FAILED(rv
) && aCallback
) {
403 // However, if it does fail, we still need to call the callback since we
404 // discard the failure. Do it asynchronously so that the caller is always
405 // guaranteed async behavior.
406 RefPtr
<CallbackCaller
> caller
= new CallbackCaller(aCallback
);
407 rv
= doc
->Dispatch(TaskCategory::Other
, caller
.forget());
408 NS_ENSURE_SUCCESS(rv
, rv
);
415 EditorSpellCheck::GetNextMisspelledWord(nsAString
& aNextMisspelledWord
) {
416 MOZ_LOG(sEditorSpellChecker
, LogLevel::Debug
, ("%s", __FUNCTION__
));
418 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
420 DeleteSuggestedWordList();
421 // Beware! This may flush notifications via synchronous
422 // ScrollSelectionIntoView.
423 RefPtr
<mozSpellChecker
> spellChecker(mSpellChecker
);
424 return spellChecker
->NextMisspelledWord(aNextMisspelledWord
,
429 EditorSpellCheck::GetSuggestedWord(nsAString
& aSuggestedWord
) {
430 // XXX This is buggy if mSuggestedWordList.Length() is over INT32_MAX.
431 if (mSuggestedWordIndex
< static_cast<int32_t>(mSuggestedWordList
.Length())) {
432 aSuggestedWord
= mSuggestedWordList
[mSuggestedWordIndex
];
433 mSuggestedWordIndex
++;
435 // A blank string signals that there are no more strings
436 aSuggestedWord
.Truncate();
442 EditorSpellCheck::CheckCurrentWord(const nsAString
& aSuggestedWord
,
443 bool* aIsMisspelled
) {
444 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
446 DeleteSuggestedWordList();
447 return mSpellChecker
->CheckWord(aSuggestedWord
, aIsMisspelled
,
448 &mSuggestedWordList
);
451 RefPtr
<CheckWordPromise
> EditorSpellCheck::CheckCurrentWordsNoSuggest(
452 const nsTArray
<nsString
>& aSuggestedWords
) {
453 if (NS_WARN_IF(!mSpellChecker
)) {
454 return CheckWordPromise::CreateAndReject(NS_ERROR_NOT_INITIALIZED
,
458 return mSpellChecker
->CheckWords(aSuggestedWords
);
462 EditorSpellCheck::ReplaceWord(const nsAString
& aMisspelledWord
,
463 const nsAString
& aReplaceWord
,
464 bool aAllOccurrences
) {
465 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
467 RefPtr
<mozSpellChecker
> spellChecker(mSpellChecker
);
468 return spellChecker
->Replace(aMisspelledWord
, aReplaceWord
, aAllOccurrences
);
472 EditorSpellCheck::IgnoreWordAllOccurrences(const nsAString
& aWord
) {
473 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
475 return mSpellChecker
->IgnoreAll(aWord
);
479 EditorSpellCheck::GetPersonalDictionary() {
480 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
482 // We can spell check with any editor type
483 mDictionaryList
.Clear();
484 mDictionaryIndex
= 0;
485 return mSpellChecker
->GetPersonalDictionary(&mDictionaryList
);
489 EditorSpellCheck::GetPersonalDictionaryWord(nsAString
& aDictionaryWord
) {
490 // XXX This is buggy if mDictionaryList.Length() is over INT32_MAX.
491 if (mDictionaryIndex
< static_cast<int32_t>(mDictionaryList
.Length())) {
492 aDictionaryWord
= mDictionaryList
[mDictionaryIndex
];
495 // A blank string signals that there are no more strings
496 aDictionaryWord
.Truncate();
503 EditorSpellCheck::AddWordToDictionary(const nsAString
& aWord
) {
504 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
506 return mSpellChecker
->AddWordToPersonalDictionary(aWord
);
510 EditorSpellCheck::RemoveWordFromDictionary(const nsAString
& aWord
) {
511 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
513 return mSpellChecker
->RemoveWordFromPersonalDictionary(aWord
);
517 EditorSpellCheck::GetDictionaryList(nsTArray
<nsCString
>& aList
) {
518 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
520 return mSpellChecker
->GetDictionaryList(&aList
);
524 EditorSpellCheck::GetCurrentDictionary(nsACString
& aDictionary
) {
525 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
527 return mSpellChecker
->GetCurrentDictionary(aDictionary
);
531 EditorSpellCheck::SetCurrentDictionary(const nsACString
& aDictionary
) {
532 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
534 RefPtr
<EditorSpellCheck
> kungFuDeathGrip
= this;
536 // The purpose of mUpdateDictionaryRunning is to avoid doing all of this if
537 // UpdateCurrentDictionary's helper method DictionaryFetched, which calls us,
538 // is on the stack. In other words: Only do this, if the user manually
539 // selected a dictionary to use.
540 if (!mUpdateDictionaryRunning
) {
541 // Ignore pending dictionary fetchers by increasing this number.
542 mDictionaryFetcherGroup
++;
545 mEditor
->GetFlags(&flags
);
546 if (!(flags
& nsIEditor::eEditorMailMask
)) {
547 if (!aDictionary
.IsEmpty() &&
548 (mPreferredLang
.IsEmpty() ||
549 !mPreferredLang
.Equals(aDictionary
,
550 nsCaseInsensitiveCStringComparator
))) {
551 // When user sets dictionary manually, we store this value associated
552 // with editor url, if it doesn't match the document language exactly.
553 // For example on "en" sites, we need to store "en-GB", otherwise
554 // the language might jump back to en-US although the user explicitly
556 StoreCurrentDictionary(mEditor
, aDictionary
);
558 printf("***** Writing content preferences for |%s|\n",
562 // If user sets a dictionary matching the language defined by
563 // document, we consider content pref has been canceled, and we clear
565 ClearCurrentDictionary(mEditor
);
567 printf("***** Clearing content preferences for |%s|\n",
572 // Also store it in as a preference, so we can use it as a fallback.
573 // We don't want this for mail composer because it uses
574 // "spellchecker.dictionary" as a preference.
576 // XXX: Prefs can only be set in the parent process, so this condition is
577 // necessary to stop libpref from throwing errors. But this should
578 // probably be handled in a better way.
579 if (XRE_IsParentProcess()) {
580 Preferences::SetCString("spellchecker.dictionary", aDictionary
);
582 printf("***** Possibly storing spellchecker.dictionary |%s|\n",
588 return mSpellChecker
->SetCurrentDictionary(aDictionary
);
592 EditorSpellCheck::UninitSpellChecker() {
593 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
595 // Cleanup - kill the spell checker
596 DeleteSuggestedWordList();
597 mDictionaryList
.Clear();
598 mDictionaryIndex
= 0;
599 mDictionaryFetcherGroup
++;
600 mSpellChecker
= nullptr;
605 EditorSpellCheck::SetFilterType(uint32_t aFilterType
) {
606 mTxtSrvFilterType
= aFilterType
;
610 nsresult
EditorSpellCheck::DeleteSuggestedWordList() {
611 mSuggestedWordList
.Clear();
612 mSuggestedWordIndex
= 0;
617 EditorSpellCheck::UpdateCurrentDictionary(
618 nsIEditorSpellCheckCallback
* aCallback
) {
619 if (NS_WARN_IF(!mSpellChecker
)) {
620 return NS_ERROR_NOT_INITIALIZED
;
625 RefPtr
<EditorSpellCheck
> kungFuDeathGrip
= this;
627 mEditor
->GetFlags(&flags
);
629 // Get language with html5 algorithm
630 nsCOMPtr
<nsIContent
> rootContent
;
631 HTMLEditor
* htmlEditor
= mEditor
->AsHTMLEditor();
633 if (flags
& nsIEditor::eEditorMailMask
) {
634 // Always determine the root content for a mail editor,
635 // even if not focused, to enable further processing below.
636 rootContent
= htmlEditor
->GetActiveEditingHost();
638 rootContent
= htmlEditor
->GetFocusedContent();
641 rootContent
= mEditor
->GetRoot();
645 return NS_ERROR_FAILURE
;
648 // Try to get topmost document's document element for embedded mail editor.
649 if (flags
& nsIEditor::eEditorMailMask
) {
650 RefPtr
<Document
> ownerDoc
= rootContent
->OwnerDoc();
651 Document
* parentDoc
= ownerDoc
->GetInProcessParentDocument();
653 rootContent
= parentDoc
->GetDocumentElement();
655 return NS_ERROR_FAILURE
;
660 RefPtr
<DictionaryFetcher
> fetcher
=
661 new DictionaryFetcher(this, aCallback
, mDictionaryFetcherGroup
);
662 rootContent
->GetLang(fetcher
->mRootContentLang
);
663 RefPtr
<Document
> doc
= rootContent
->GetComposedDoc();
664 NS_ENSURE_STATE(doc
);
665 doc
->GetContentLanguage(fetcher
->mRootDocContentLang
);
667 rv
= fetcher
->Fetch(mEditor
);
668 NS_ENSURE_SUCCESS(rv
, rv
);
673 // Helper function that iterates over the list of dictionaries and sets the one
674 // that matches based on a given comparison type.
675 void EditorSpellCheck::BuildDictionaryList(const nsACString
& aDictName
,
676 const nsTArray
<nsCString
>& aDictList
,
677 enum dictCompare aCompareType
,
678 nsTArray
<nsCString
>& aOutList
) {
679 for (const auto& dictStr
: aDictList
) {
681 switch (aCompareType
) {
682 case DICT_NORMAL_COMPARE
:
683 equals
= aDictName
.Equals(dictStr
);
685 case DICT_COMPARE_CASE_INSENSITIVE
:
686 equals
= aDictName
.Equals(dictStr
, nsCaseInsensitiveCStringComparator
);
688 case DICT_COMPARE_DASHMATCH
:
689 equals
= nsStyleUtil::DashMatchCompare(
690 NS_ConvertUTF8toUTF16(dictStr
), NS_ConvertUTF8toUTF16(aDictName
),
691 nsCaseInsensitiveStringComparator
);
695 aOutList
.AppendElement(dictStr
);
697 if (NS_SUCCEEDED(rv
)) {
698 printf("***** Trying |%s|.\n", dictStr
.get());
701 // We always break here. We tried to set the dictionary to an existing
702 // dictionary from the list. This must work, if it doesn't, there is
703 // no point trying another one.
709 nsresult
EditorSpellCheck::DictionaryFetched(DictionaryFetcher
* aFetcher
) {
710 MOZ_ASSERT(aFetcher
);
711 RefPtr
<EditorSpellCheck
> kungFuDeathGrip
= this;
713 BeginUpdateDictionary();
715 if (aFetcher
->mGroup
< mDictionaryFetcherGroup
) {
716 // SetCurrentDictionary was called after the fetch started. Don't overwrite
717 // that dictionary with the fetched one.
718 EndUpdateDictionary();
719 if (aFetcher
->mCallback
) {
720 aFetcher
->mCallback
->EditorSpellCheckDone();
726 * We try to derive the dictionary to use based on the following priorities:
727 * 1) Content preference, so the language the user set for the site before.
728 * (Introduced in bug 678842 and corrected in bug 717433.)
729 * 2) Language set by the website, or any other dictionary that partly
730 * matches that. (Introduced in bug 338427.)
731 * Eg. if the website is "en-GB", a user who only has "en-US" will get
732 * that. If the website is generic "en", the user will get one of the
733 * "en-*" installed. If application locale or system locale is "en-*",
734 * we get it. If others, it is (almost) random.
735 * However, we prefer what is stored in "spellchecker.dictionary",
736 * so if the user chose "en-AU" before, they will get "en-AU" on a plain
737 * "en" site. (Introduced in bug 682564.)
738 * 3) The value of "spellchecker.dictionary" which reflects a previous
739 * language choice of the user (on another site).
740 * (This was the original behaviour before the aforementioned bugs
742 * 4) The user's locale.
743 * 5) Use the current dictionary that is currently set.
744 * 6) The content of the "LANG" environment variable (if set).
745 * 7) The first spell check dictionary installed.
748 // Get the language from the element or its closest parent according to:
749 // https://html.spec.whatwg.org/#attr-lang
750 // This is used in SetCurrentDictionary.
751 CopyUTF16toUTF8(aFetcher
->mRootContentLang
, mPreferredLang
);
753 printf("***** mPreferredLang (element) |%s|\n", mPreferredLang
.get());
756 // If no luck, try the "Content-Language" header.
757 if (mPreferredLang
.IsEmpty()) {
758 CopyUTF16toUTF8(aFetcher
->mRootDocContentLang
, mPreferredLang
);
760 printf("***** mPreferredLang (content-language) |%s|\n",
761 mPreferredLang
.get());
765 // We obtain a list of available dictionaries.
766 AutoTArray
<nsCString
, 8> dictList
;
767 nsresult rv
= mSpellChecker
->GetDictionaryList(&dictList
);
768 if (NS_WARN_IF(NS_FAILED(rv
))) {
769 EndUpdateDictionary();
770 if (aFetcher
->mCallback
) {
771 aFetcher
->mCallback
->EditorSpellCheckDone();
777 // If we successfully fetched a dictionary from content prefs, do not go
778 // further. Use this exact dictionary.
779 // Don't use content preferences for editor with eEditorMailMask flag.
780 nsAutoCString dictName
;
782 mEditor
->GetFlags(&flags
);
783 if (!(flags
& nsIEditor::eEditorMailMask
)) {
784 CopyUTF16toUTF8(aFetcher
->mDictionary
, dictName
);
785 if (!dictName
.IsEmpty()) {
786 AutoTArray
<nsCString
, 1> tryDictList
;
787 BuildDictionaryList(dictName
, dictList
, DICT_NORMAL_COMPARE
, tryDictList
);
789 RefPtr
<EditorSpellCheck
> self
= this;
790 RefPtr
<DictionaryFetcher
> fetcher
= aFetcher
;
791 mSpellChecker
->SetCurrentDictionaryFromList(tryDictList
)
793 GetMainThreadSerialEventTarget(), __func__
,
796 printf("***** Assigned from content preferences |%s|\n",
799 // We take an early exit here, so let's not forget to clear
801 self
->DeleteSuggestedWordList();
803 self
->EndUpdateDictionary();
804 if (fetcher
->mCallback
) {
805 fetcher
->mCallback
->EditorSpellCheckDone();
808 [self
, fetcher
](nsresult aError
) {
809 if (aError
== NS_ERROR_ABORT
) {
812 // May be dictionary was uninstalled ?
813 // Clear the content preference and continue.
814 ClearCurrentDictionary(self
->mEditor
);
816 // Priority 2 or later will handled by the following
817 self
->SetFallbackDictionary(fetcher
);
822 SetFallbackDictionary(aFetcher
);
826 void EditorSpellCheck::SetFallbackDictionary(DictionaryFetcher
* aFetcher
) {
827 MOZ_ASSERT(mUpdateDictionaryRunning
);
829 AutoTArray
<nsCString
, 6> tryDictList
;
831 // We obtain a list of available dictionaries.
832 AutoTArray
<nsCString
, 8> dictList
;
833 nsresult rv
= mSpellChecker
->GetDictionaryList(&dictList
);
834 if (NS_WARN_IF(NS_FAILED(rv
))) {
835 EndUpdateDictionary();
836 if (aFetcher
->mCallback
) {
837 aFetcher
->mCallback
->EditorSpellCheckDone();
843 // After checking the content preferences, we use the language of the element
845 nsAutoCString
dictName(mPreferredLang
);
847 printf("***** Assigned from element/doc |%s|\n", dictName
.get());
850 // Get the preference value.
851 nsAutoCString preferredDict
;
852 Preferences::GetLocalizedCString("spellchecker.dictionary", preferredDict
);
854 nsAutoCString appLocaleStr
;
855 if (!dictName
.IsEmpty()) {
856 // RFC 5646 explicitly states that matches should be case-insensitive.
857 BuildDictionaryList(dictName
, dictList
, DICT_COMPARE_CASE_INSENSITIVE
,
861 printf("***** Trying from element/doc |%s| \n", dictName
.get());
864 // Required dictionary was not available. Try to get a dictionary
865 // matching at least language part of dictName.
866 mozilla::intl::Locale loc
= mozilla::intl::Locale(dictName
);
867 nsAutoCString
langCode(loc
.GetLanguage());
869 // Try dictionary.spellchecker preference, if it starts with langCode,
870 // so we don't just get any random dictionary matching the language.
871 if (!preferredDict
.IsEmpty() &&
872 nsStyleUtil::DashMatchCompare(NS_ConvertUTF8toUTF16(preferredDict
),
873 NS_ConvertUTF8toUTF16(langCode
),
874 nsTDefaultStringComparator
)) {
877 "***** Trying preference value |%s| since it matches language code\n",
878 preferredDict
.get());
880 BuildDictionaryList(preferredDict
, dictList
,
881 DICT_COMPARE_CASE_INSENSITIVE
, tryDictList
);
884 if (tryDictList
.IsEmpty()) {
885 // Use the application locale dictionary when the required language
886 // equlas applocation locale language.
887 LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr
);
888 if (!appLocaleStr
.IsEmpty()) {
889 mozilla::intl::Locale appLoc
= mozilla::intl::Locale(appLocaleStr
);
890 if (langCode
.Equals(appLoc
.GetLanguage())) {
891 BuildDictionaryList(appLocaleStr
, dictList
,
892 DICT_COMPARE_CASE_INSENSITIVE
, tryDictList
);
896 // Use the system locale dictionary when the required language equlas
897 // system locale language.
898 nsAutoCString sysLocaleStr
;
899 OSPreferences::GetInstance()->GetSystemLocale(sysLocaleStr
);
900 if (!sysLocaleStr
.IsEmpty()) {
901 mozilla::intl::Locale sysLoc
= mozilla::intl::Locale(sysLocaleStr
);
902 if (langCode
.Equals(sysLoc
.GetLanguage())) {
903 BuildDictionaryList(sysLocaleStr
, dictList
,
904 DICT_COMPARE_CASE_INSENSITIVE
, tryDictList
);
909 // Use any dictionary with the required language.
911 printf("***** Trying to find match for language code |%s|\n",
914 BuildDictionaryList(langCode
, dictList
, DICT_COMPARE_DASHMATCH
,
919 // If the document didn't supply a dictionary or the setting failed,
920 // try the user preference next.
921 if (!preferredDict
.IsEmpty()) {
923 printf("***** Trying preference value |%s|\n", preferredDict
.get());
925 BuildDictionaryList(preferredDict
, dictList
, DICT_NORMAL_COMPARE
,
930 // As next fallback, try the current locale.
931 if (appLocaleStr
.IsEmpty()) {
932 LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr
);
935 printf("***** Trying locale |%s|\n", appLocaleStr
.get());
937 BuildDictionaryList(appLocaleStr
, dictList
, DICT_COMPARE_CASE_INSENSITIVE
,
941 // If we have a current dictionary and we don't have no item in try list,
942 // don't try anything else.
943 nsAutoCString currentDictionary
;
944 GetCurrentDictionary(currentDictionary
);
945 if (!currentDictionary
.IsEmpty() && tryDictList
.IsEmpty()) {
947 printf("***** Retrieved current dict |%s|\n", currentDictionary
.get());
949 EndUpdateDictionary();
950 if (aFetcher
->mCallback
) {
951 aFetcher
->mCallback
->EditorSpellCheckDone();
957 // Try to get current dictionary from environment variable LANG.
958 // LANG = language[_territory][.charset]
959 char* env_lang
= getenv("LANG");
961 nsAutoCString
lang(env_lang
);
962 // Strip trailing charset, if there is any.
963 int32_t dot_pos
= lang
.FindChar('.');
965 lang
= Substring(lang
, 0, dot_pos
);
968 int32_t underScore
= lang
.FindChar('_');
969 if (underScore
!= -1) {
970 lang
.Replace(underScore
, 1, '-');
972 printf("***** Trying LANG from environment |%s|\n", lang
.get());
974 BuildDictionaryList(lang
, dictList
, DICT_COMPARE_CASE_INSENSITIVE
,
980 // If it does not work, pick the first one.
981 if (!dictList
.IsEmpty()) {
982 BuildDictionaryList(dictList
[0], dictList
, DICT_NORMAL_COMPARE
,
985 printf("***** Trying first of list |%s|\n", dictList
[0].get());
989 RefPtr
<EditorSpellCheck
> self
= this;
990 RefPtr
<DictionaryFetcher
> fetcher
= aFetcher
;
991 mSpellChecker
->SetCurrentDictionaryFromList(tryDictList
)
992 ->Then(GetMainThreadSerialEventTarget(), __func__
, [self
, fetcher
]() {
993 // If an error was thrown while setting the dictionary, just
994 // fail silently so that the spellchecker dialog is allowed to come
995 // up. The user can manually reset the language to their choice on
996 // the dialog if it is wrong.
997 self
->DeleteSuggestedWordList();
998 self
->EndUpdateDictionary();
999 if (fetcher
->mCallback
) {
1000 fetcher
->mCallback
->EditorSpellCheckDone();
1005 } // namespace mozilla