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 "EditorBase.h" // for EditorBase
10 #include "HTMLEditor.h" // for HTMLEditor
11 #include "TextServicesDocument.h" // for TextServicesDocument
13 #include "mozilla/Attributes.h" // for final
14 #include "mozilla/dom/Element.h" // for Element
15 #include "mozilla/dom/Promise.h"
16 #include "mozilla/dom/Selection.h"
17 #include "mozilla/dom/StaticRange.h"
18 #include "mozilla/intl/Locale.h" // for mozilla::intl::Locale
19 #include "mozilla/intl/LocaleService.h" // for retrieving app locale
20 #include "mozilla/intl/OSPreferences.h" // for mozilla::intl::OSPreferences
21 #include "mozilla/Logging.h" // for mozilla::LazyLogModule
22 #include "mozilla/mozalloc.h" // for operator delete, etc
23 #include "mozilla/mozSpellChecker.h" // for mozSpellChecker
24 #include "mozilla/Preferences.h" // for Preferences
26 #include "nsAString.h" // for nsAString::IsEmpty, etc
27 #include "nsComponentManagerUtils.h" // for do_CreateInstance
28 #include "nsDebug.h" // for NS_ENSURE_TRUE, etc
29 #include "nsDependentSubstring.h" // for Substring
30 #include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc
31 #include "nsIContent.h" // for nsIContent
32 #include "nsIContentPrefService2.h" // for nsIContentPrefService2, etc
33 #include "mozilla/dom/Document.h" // for Document
34 #include "nsIEditor.h" // for nsIEditor
35 #include "nsILoadContext.h"
36 #include "nsISupports.h" // for nsISupports
37 #include "nsISupportsUtils.h" // for NS_ADDREF
38 #include "nsIURI.h" // for nsIURI
39 #include "nsThreadUtils.h" // for GetMainThreadSerialEventTarget
40 #include "nsVariant.h" // for nsIWritableVariant, etc
41 #include "nsLiteralString.h" // for NS_LITERAL_STRING, etc
43 #include "nsReadableUtils.h" // for ToNewUnicode, EmptyString, etc
44 #include "nsServiceManagerUtils.h" // for do_GetService
45 #include "nsString.h" // for nsAutoString, nsString, etc
46 #include "nsStringFwd.h" // for nsAFlatString
47 #include "nsStyleUtil.h" // for nsStyleUtil
48 #include "nsXULAppAPI.h" // for XRE_GetProcessType
53 using intl::LocaleService
;
54 using intl::OSPreferences
;
56 static mozilla::LazyLogModule
sEditorSpellChecker("EditorSpellChecker");
58 class UpdateDictionaryHolder
{
60 EditorSpellCheck
* mSpellCheck
;
63 explicit UpdateDictionaryHolder(EditorSpellCheck
* esc
) : mSpellCheck(esc
) {
65 mSpellCheck
->BeginUpdateDictionary();
69 ~UpdateDictionaryHolder() {
71 mSpellCheck
->EndUpdateDictionary();
76 #define CPS_PREF_NAME u"spellcheck.lang"_ns
79 * Gets the URI of aEditor's document.
81 static nsIURI
* GetDocumentURI(EditorBase
* aEditor
) {
84 Document
* doc
= aEditor
->AsEditorBase()->GetDocument();
85 if (NS_WARN_IF(!doc
)) {
89 return doc
->GetDocumentURI();
92 static nsILoadContext
* GetLoadContext(nsIEditor
* aEditor
) {
93 Document
* doc
= aEditor
->AsEditorBase()->GetDocument();
94 if (NS_WARN_IF(!doc
)) {
98 return doc
->GetLoadContext();
101 static nsCString
DictionariesToString(
102 const nsTArray
<nsCString
>& aDictionaries
) {
104 for (const auto& dictionary
: aDictionaries
) {
105 asString
.Append(dictionary
);
106 asString
.Append(',');
111 static void StringToDictionaries(const nsCString
& aString
,
112 nsTArray
<nsCString
>& aDictionaries
) {
113 nsTArray
<nsCString
> asDictionaries
;
114 for (const nsACString
& token
:
115 nsCCharSeparatedTokenizer(aString
, ',').ToRange()) {
116 if (token
.IsEmpty()) {
119 aDictionaries
.AppendElement(token
);
124 * Fetches the dictionary stored in content prefs and maintains state during the
125 * fetch, which is asynchronous.
127 class DictionaryFetcher final
: public nsIContentPrefCallback2
{
131 DictionaryFetcher(EditorSpellCheck
* aSpellCheck
,
132 nsIEditorSpellCheckCallback
* aCallback
, uint32_t aGroup
)
133 : mCallback(aCallback
), mGroup(aGroup
), mSpellCheck(aSpellCheck
) {}
135 NS_IMETHOD
Fetch(nsIEditor
* aEditor
);
137 NS_IMETHOD
HandleResult(nsIContentPref
* aPref
) override
{
138 nsCOMPtr
<nsIVariant
> value
;
139 nsresult rv
= aPref
->GetValue(getter_AddRefs(value
));
140 NS_ENSURE_SUCCESS(rv
, rv
);
142 value
->GetAsACString(asString
);
143 StringToDictionaries(asString
, mDictionaries
);
147 NS_IMETHOD
HandleCompletion(uint16_t reason
) override
{
148 mSpellCheck
->DictionaryFetched(this);
152 NS_IMETHOD
HandleError(nsresult error
) override
{ return NS_OK
; }
154 nsCOMPtr
<nsIEditorSpellCheckCallback
> mCallback
;
156 RefPtr
<nsAtom
> mRootContentLang
;
157 RefPtr
<nsAtom
> mRootDocContentLang
;
158 nsTArray
<nsCString
> mDictionaries
;
161 ~DictionaryFetcher() {}
163 RefPtr
<EditorSpellCheck
> mSpellCheck
;
166 NS_IMPL_ISUPPORTS(DictionaryFetcher
, nsIContentPrefCallback2
)
168 class ContentPrefInitializerRunnable final
: public Runnable
{
170 ContentPrefInitializerRunnable(nsIEditor
* aEditor
,
171 nsIContentPrefCallback2
* aCallback
)
172 : Runnable("ContentPrefInitializerRunnable"),
173 mEditorBase(aEditor
->AsEditorBase()),
174 mCallback(aCallback
) {}
176 NS_IMETHOD
Run() override
{
177 if (mEditorBase
->Destroyed()) {
178 mCallback
->HandleError(NS_ERROR_NOT_AVAILABLE
);
182 nsCOMPtr
<nsIContentPrefService2
> contentPrefService
=
183 do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID
);
184 if (NS_WARN_IF(!contentPrefService
)) {
185 mCallback
->HandleError(NS_ERROR_NOT_AVAILABLE
);
189 nsCOMPtr
<nsIURI
> docUri
= GetDocumentURI(mEditorBase
);
190 if (NS_WARN_IF(!docUri
)) {
191 mCallback
->HandleError(NS_ERROR_FAILURE
);
195 nsAutoCString docUriSpec
;
196 nsresult rv
= docUri
->GetSpec(docUriSpec
);
197 if (NS_WARN_IF(NS_FAILED(rv
))) {
198 mCallback
->HandleError(rv
);
202 rv
= contentPrefService
->GetByDomainAndName(
203 NS_ConvertUTF8toUTF16(docUriSpec
), CPS_PREF_NAME
,
204 GetLoadContext(mEditorBase
), mCallback
);
205 if (NS_WARN_IF(NS_FAILED(rv
))) {
206 mCallback
->HandleError(rv
);
213 RefPtr
<EditorBase
> mEditorBase
;
214 nsCOMPtr
<nsIContentPrefCallback2
> mCallback
;
218 DictionaryFetcher::Fetch(nsIEditor
* aEditor
) {
219 NS_ENSURE_ARG_POINTER(aEditor
);
221 nsCOMPtr
<nsIRunnable
> runnable
=
222 new ContentPrefInitializerRunnable(aEditor
, this);
223 NS_DispatchToCurrentThreadQueue(runnable
.forget(), 1000,
224 EventQueuePriority::Idle
);
230 * Stores the current dictionary for aEditor's document URL.
232 static nsresult
StoreCurrentDictionaries(
233 EditorBase
* aEditorBase
, const nsTArray
<nsCString
>& aDictionaries
) {
234 NS_ENSURE_ARG_POINTER(aEditorBase
);
238 nsCOMPtr
<nsIURI
> docUri
= GetDocumentURI(aEditorBase
);
239 if (NS_WARN_IF(!docUri
)) {
240 return NS_ERROR_FAILURE
;
243 nsAutoCString docUriSpec
;
244 rv
= docUri
->GetSpec(docUriSpec
);
245 NS_ENSURE_SUCCESS(rv
, rv
);
247 RefPtr
<nsVariant
> prefValue
= new nsVariant();
249 nsCString asString
= DictionariesToString(aDictionaries
);
250 prefValue
->SetAsAString(NS_ConvertUTF8toUTF16(asString
));
252 nsCOMPtr
<nsIContentPrefService2
> contentPrefService
=
253 do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID
);
254 NS_ENSURE_TRUE(contentPrefService
, NS_ERROR_NOT_INITIALIZED
);
256 return contentPrefService
->Set(NS_ConvertUTF8toUTF16(docUriSpec
),
257 CPS_PREF_NAME
, prefValue
,
258 GetLoadContext(aEditorBase
), nullptr);
262 * Forgets the current dictionary stored for aEditor's document URL.
264 static nsresult
ClearCurrentDictionaries(EditorBase
* aEditorBase
) {
265 NS_ENSURE_ARG_POINTER(aEditorBase
);
269 nsCOMPtr
<nsIURI
> docUri
= GetDocumentURI(aEditorBase
);
270 if (NS_WARN_IF(!docUri
)) {
271 return NS_ERROR_FAILURE
;
274 nsAutoCString docUriSpec
;
275 rv
= docUri
->GetSpec(docUriSpec
);
276 NS_ENSURE_SUCCESS(rv
, rv
);
278 nsCOMPtr
<nsIContentPrefService2
> contentPrefService
=
279 do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID
);
280 NS_ENSURE_TRUE(contentPrefService
, NS_ERROR_NOT_INITIALIZED
);
282 return contentPrefService
->RemoveByDomainAndName(
283 NS_ConvertUTF8toUTF16(docUriSpec
), CPS_PREF_NAME
,
284 GetLoadContext(aEditorBase
), nullptr);
287 NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorSpellCheck
)
288 NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorSpellCheck
)
290 NS_INTERFACE_MAP_BEGIN(EditorSpellCheck
)
291 NS_INTERFACE_MAP_ENTRY(nsIEditorSpellCheck
)
292 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports
, nsIEditorSpellCheck
)
293 NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(EditorSpellCheck
)
296 NS_IMPL_CYCLE_COLLECTION(EditorSpellCheck
, mEditor
, mSpellChecker
)
298 EditorSpellCheck::EditorSpellCheck()
299 : mTxtSrvFilterType(0),
300 mSuggestedWordIndex(0),
301 mDictionaryFetcherGroup(0),
302 mUpdateDictionaryRunning(false) {}
304 EditorSpellCheck::~EditorSpellCheck() {
305 // Make sure we blow the spellchecker away, just in
306 // case it hasn't been destroyed already.
307 mSpellChecker
= nullptr;
310 mozSpellChecker
* EditorSpellCheck::GetSpellChecker() { return mSpellChecker
; }
312 // The problem is that if the spell checker does not exist, we can not tell
313 // which dictionaries are installed. This function works around the problem,
314 // allowing callers to ask if we can spell check without actually doing so (and
315 // enabling or disabling UI as necessary). This just creates a spellcheck
316 // object if needed and asks it for the dictionary list.
318 EditorSpellCheck::CanSpellCheck(bool* aCanSpellCheck
) {
319 RefPtr
<mozSpellChecker
> spellChecker
= mSpellChecker
;
321 spellChecker
= mozSpellChecker::Create();
322 MOZ_ASSERT(spellChecker
);
324 nsTArray
<nsCString
> dictList
;
325 nsresult rv
= spellChecker
->GetDictionaryList(&dictList
);
326 if (NS_WARN_IF(NS_FAILED(rv
))) {
330 *aCanSpellCheck
= !dictList
.IsEmpty();
334 // Instances of this class can be used as either runnables or RAII helpers.
335 class CallbackCaller final
: public Runnable
{
337 explicit CallbackCaller(nsIEditorSpellCheckCallback
* aCallback
)
338 : mozilla::Runnable("CallbackCaller"), mCallback(aCallback
) {}
340 ~CallbackCaller() { Run(); }
342 NS_IMETHOD
Run() override
{
344 mCallback
->EditorSpellCheckDone();
351 nsCOMPtr
<nsIEditorSpellCheckCallback
> mCallback
;
355 EditorSpellCheck::InitSpellChecker(nsIEditor
* aEditor
,
356 bool aEnableSelectionChecking
,
357 nsIEditorSpellCheckCallback
* aCallback
) {
358 NS_ENSURE_TRUE(aEditor
, NS_ERROR_NULL_POINTER
);
359 mEditor
= aEditor
->AsEditorBase();
361 RefPtr
<Document
> doc
= mEditor
->GetDocument();
362 if (NS_WARN_IF(!doc
)) {
363 return NS_ERROR_FAILURE
;
368 // We can spell check with any editor type
369 RefPtr
<TextServicesDocument
> textServicesDocument
=
370 new TextServicesDocument();
371 textServicesDocument
->SetFilterType(mTxtSrvFilterType
);
373 // EditorBase::AddEditActionListener() needs to access mSpellChecker and
374 // mSpellChecker->GetTextServicesDocument(). Therefore, we need to
375 // initialize them before calling TextServicesDocument::InitWithEditor()
376 // since it calls EditorBase::AddEditActionListener().
377 mSpellChecker
= mozSpellChecker::Create();
378 MOZ_ASSERT(mSpellChecker
);
379 rv
= mSpellChecker
->SetDocument(textServicesDocument
, true);
380 if (NS_WARN_IF(NS_FAILED(rv
))) {
384 // Pass the editor to the text services document
385 rv
= textServicesDocument
->InitWithEditor(aEditor
);
386 NS_ENSURE_SUCCESS(rv
, rv
);
388 if (aEnableSelectionChecking
) {
389 // Find out if the section is collapsed or not.
390 // If it isn't, we want to spellcheck just the selection.
392 RefPtr
<Selection
> selection
;
393 aEditor
->GetSelection(getter_AddRefs(selection
));
394 if (NS_WARN_IF(!selection
)) {
395 return NS_ERROR_FAILURE
;
398 if (selection
->RangeCount()) {
399 RefPtr
<const nsRange
> range
= selection
->GetRangeAt(0);
400 NS_ENSURE_STATE(range
);
402 if (!range
->Collapsed()) {
403 // We don't want to touch the range in the selection,
404 // so create a new copy of it.
405 RefPtr
<StaticRange
> staticRange
=
406 StaticRange::Create(range
, IgnoreErrors());
407 if (NS_WARN_IF(!staticRange
)) {
408 return NS_ERROR_FAILURE
;
411 // Make sure the new range spans complete words.
412 rv
= textServicesDocument
->ExpandRangeToWordBoundaries(staticRange
);
413 if (NS_WARN_IF(NS_FAILED(rv
))) {
417 // Now tell the text services that you only want
418 // to iterate over the text in this range.
419 rv
= textServicesDocument
->SetExtent(staticRange
);
420 if (NS_WARN_IF(NS_FAILED(rv
))) {
426 // do not fail if UpdateCurrentDictionary fails because this method may
428 rv
= UpdateCurrentDictionary(aCallback
);
429 if (NS_FAILED(rv
) && aCallback
) {
430 // However, if it does fail, we still need to call the callback since we
431 // discard the failure. Do it asynchronously so that the caller is always
432 // guaranteed async behavior.
433 RefPtr
<CallbackCaller
> caller
= new CallbackCaller(aCallback
);
434 rv
= doc
->Dispatch(caller
.forget());
435 NS_ENSURE_SUCCESS(rv
, rv
);
442 EditorSpellCheck::GetNextMisspelledWord(nsAString
& aNextMisspelledWord
) {
443 MOZ_LOG(sEditorSpellChecker
, LogLevel::Debug
, ("%s", __FUNCTION__
));
445 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
447 DeleteSuggestedWordList();
448 // Beware! This may flush notifications via synchronous
449 // ScrollSelectionIntoView.
450 RefPtr
<mozSpellChecker
> spellChecker(mSpellChecker
);
451 return spellChecker
->NextMisspelledWord(aNextMisspelledWord
,
456 EditorSpellCheck::GetSuggestedWord(nsAString
& aSuggestedWord
) {
457 // XXX This is buggy if mSuggestedWordList.Length() is over INT32_MAX.
458 if (mSuggestedWordIndex
< static_cast<int32_t>(mSuggestedWordList
.Length())) {
459 aSuggestedWord
= mSuggestedWordList
[mSuggestedWordIndex
];
460 mSuggestedWordIndex
++;
462 // A blank string signals that there are no more strings
463 aSuggestedWord
.Truncate();
469 EditorSpellCheck::CheckCurrentWord(const nsAString
& aSuggestedWord
,
470 bool* aIsMisspelled
) {
471 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
473 DeleteSuggestedWordList();
474 return mSpellChecker
->CheckWord(aSuggestedWord
, aIsMisspelled
,
475 &mSuggestedWordList
);
479 EditorSpellCheck::Suggest(const nsAString
& aSuggestedWord
, uint32_t aCount
,
480 JSContext
* aCx
, Promise
** aPromise
) {
481 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
483 nsIGlobalObject
* globalObject
= xpc::CurrentNativeGlobal(aCx
);
484 if (NS_WARN_IF(!globalObject
)) {
485 return NS_ERROR_UNEXPECTED
;
489 RefPtr
<Promise
> promise
= Promise::Create(globalObject
, result
);
490 if (NS_WARN_IF(result
.Failed())) {
491 return result
.StealNSResult();
494 mSpellChecker
->Suggest(aSuggestedWord
, aCount
)
496 GetMainThreadSerialEventTarget(), __func__
,
497 [promise
](const CopyableTArray
<nsString
>& aSuggestions
) {
498 promise
->MaybeResolve(aSuggestions
);
500 [promise
](nsresult aError
) {
501 promise
->MaybeReject(NS_ERROR_FAILURE
);
504 promise
.forget(aPromise
);
508 RefPtr
<CheckWordPromise
> EditorSpellCheck::CheckCurrentWordsNoSuggest(
509 const nsTArray
<nsString
>& aSuggestedWords
) {
510 if (NS_WARN_IF(!mSpellChecker
)) {
511 return CheckWordPromise::CreateAndReject(NS_ERROR_NOT_INITIALIZED
,
515 return mSpellChecker
->CheckWords(aSuggestedWords
);
519 EditorSpellCheck::ReplaceWord(const nsAString
& aMisspelledWord
,
520 const nsAString
& aReplaceWord
,
521 bool aAllOccurrences
) {
522 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
524 RefPtr
<mozSpellChecker
> spellChecker(mSpellChecker
);
525 return spellChecker
->Replace(aMisspelledWord
, aReplaceWord
, aAllOccurrences
);
529 EditorSpellCheck::IgnoreWordAllOccurrences(const nsAString
& aWord
) {
530 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
532 return mSpellChecker
->IgnoreAll(aWord
);
536 EditorSpellCheck::AddWordToDictionary(const nsAString
& aWord
) {
537 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
539 return mSpellChecker
->AddWordToPersonalDictionary(aWord
);
543 EditorSpellCheck::RemoveWordFromDictionary(const nsAString
& aWord
) {
544 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
546 return mSpellChecker
->RemoveWordFromPersonalDictionary(aWord
);
550 EditorSpellCheck::GetDictionaryList(nsTArray
<nsCString
>& aList
) {
551 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
553 return mSpellChecker
->GetDictionaryList(&aList
);
557 EditorSpellCheck::GetCurrentDictionaries(nsTArray
<nsCString
>& aDictionaries
) {
558 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
559 return mSpellChecker
->GetCurrentDictionaries(aDictionaries
);
563 EditorSpellCheck::SetCurrentDictionaries(
564 const nsTArray
<nsCString
>& aDictionaries
, JSContext
* aCx
,
565 Promise
** aPromise
) {
566 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
568 RefPtr
<EditorSpellCheck
> kungFuDeathGrip
= this;
570 // The purpose of mUpdateDictionaryRunning is to avoid doing all of this if
571 // UpdateCurrentDictionary's helper method DictionaryFetched, which calls us,
572 // is on the stack. In other words: Only do this, if the user manually
573 // selected a dictionary to use.
574 if (!mUpdateDictionaryRunning
) {
575 // Ignore pending dictionary fetchers by increasing this number.
576 mDictionaryFetcherGroup
++;
579 mEditor
->GetFlags(&flags
);
580 if (!(flags
& nsIEditor::eEditorMailMask
)) {
581 bool contentPrefMatchesUserPref
= true;
582 // Check if aDictionaries has the same languages as mPreferredLangs.
583 if (!aDictionaries
.IsEmpty()) {
584 if (aDictionaries
.Length() != mPreferredLangs
.Length()) {
585 contentPrefMatchesUserPref
= false;
587 for (const auto& dictName
: aDictionaries
) {
588 if (mPreferredLangs
.IndexOf(dictName
) ==
589 nsTArray
<nsCString
>::NoIndex
) {
590 contentPrefMatchesUserPref
= false;
596 if (!contentPrefMatchesUserPref
) {
597 // When user sets dictionary manually, we store this value associated
598 // with editor url, if it doesn't match the document language exactly.
599 // For example on "en" sites, we need to store "en-GB", otherwise
600 // the language might jump back to en-US although the user explicitly
602 StoreCurrentDictionaries(mEditor
, aDictionaries
);
604 printf("***** Writing content preferences for |%s|\n",
605 DictionariesToString(aDictionaries
).Data());
608 // If user sets a dictionary matching the language defined by
609 // document, we consider content pref has been canceled, and we clear
611 ClearCurrentDictionaries(mEditor
);
613 printf("***** Clearing content preferences for |%s|\n",
614 DictionariesToString(aDictionaries
).Data());
618 // Also store it in as a preference, so we can use it as a fallback.
619 // We don't want this for mail composer because it uses
620 // "spellchecker.dictionary" as a preference.
622 // XXX: Prefs can only be set in the parent process, so this condition is
623 // necessary to stop libpref from throwing errors. But this should
624 // probably be handled in a better way.
625 if (XRE_IsParentProcess()) {
626 nsCString asString
= DictionariesToString(aDictionaries
);
627 Preferences::SetCString("spellchecker.dictionary", asString
);
629 printf("***** Possibly storing spellchecker.dictionary |%s|\n",
634 MOZ_ASSERT(flags
& nsIEditor::eEditorMailMask
);
635 // Since the mail editor can only influence the language selection by the
636 // html lang attribute, set the content-language document to persist
637 // multi language selections.
638 // XXX Why doesn't here use the document of the editor directly?
639 nsCOMPtr
<nsIContent
> anonymousDivOrEditingHost
;
640 if (HTMLEditor
* htmlEditor
= mEditor
->GetAsHTMLEditor()) {
641 anonymousDivOrEditingHost
= htmlEditor
->ComputeEditingHost();
643 anonymousDivOrEditingHost
= mEditor
->GetRoot();
645 RefPtr
<Document
> ownerDoc
= anonymousDivOrEditingHost
->OwnerDoc();
646 Document
* parentDoc
= ownerDoc
->GetInProcessParentDocument();
648 parentDoc
->SetHeaderData(
649 nsGkAtoms::headerContentLanguage
,
650 NS_ConvertUTF8toUTF16(DictionariesToString(aDictionaries
)));
655 nsIGlobalObject
* globalObject
= xpc::CurrentNativeGlobal(aCx
);
656 if (NS_WARN_IF(!globalObject
)) {
657 return NS_ERROR_UNEXPECTED
;
661 RefPtr
<Promise
> promise
= Promise::Create(globalObject
, result
);
662 if (NS_WARN_IF(result
.Failed())) {
663 return result
.StealNSResult();
666 mSpellChecker
->SetCurrentDictionaries(aDictionaries
)
668 GetMainThreadSerialEventTarget(), __func__
,
669 [promise
]() { promise
->MaybeResolveWithUndefined(); },
670 [promise
](nsresult aError
) {
671 promise
->MaybeReject(NS_ERROR_FAILURE
);
674 promise
.forget(aPromise
);
679 EditorSpellCheck::UninitSpellChecker() {
680 NS_ENSURE_TRUE(mSpellChecker
, NS_ERROR_NOT_INITIALIZED
);
682 // Cleanup - kill the spell checker
683 DeleteSuggestedWordList();
684 mDictionaryFetcherGroup
++;
685 mSpellChecker
= nullptr;
690 EditorSpellCheck::SetFilterType(uint32_t aFilterType
) {
691 mTxtSrvFilterType
= aFilterType
;
695 nsresult
EditorSpellCheck::DeleteSuggestedWordList() {
696 mSuggestedWordList
.Clear();
697 mSuggestedWordIndex
= 0;
702 EditorSpellCheck::UpdateCurrentDictionary(
703 nsIEditorSpellCheckCallback
* aCallback
) {
704 if (NS_WARN_IF(!mSpellChecker
)) {
705 return NS_ERROR_NOT_INITIALIZED
;
710 RefPtr
<EditorSpellCheck
> kungFuDeathGrip
= this;
712 // Get language with html5 algorithm
713 const RefPtr
<Element
> rootEditableElement
=
714 [](const EditorBase
& aEditorBase
) -> Element
* {
715 if (!aEditorBase
.IsHTMLEditor()) {
716 return aEditorBase
.GetRoot();
718 if (aEditorBase
.IsMailEditor()) {
719 // Shouldn't run spellcheck in a mail editor without focus
721 // XXX Why doesn't here use the document of the editor directly?
722 Element
* const editingHost
=
723 aEditorBase
.AsHTMLEditor()->ComputeEditingHost();
727 // Try to get topmost document's document element for embedded mail
728 // editor (bug 967494)
729 Document
* parentDoc
=
730 editingHost
->OwnerDoc()->GetInProcessParentDocument();
734 return parentDoc
->GetDocumentElement();
736 return aEditorBase
.AsHTMLEditor()->GetFocusedElement();
739 if (!rootEditableElement
) {
740 return NS_ERROR_FAILURE
;
743 RefPtr
<DictionaryFetcher
> fetcher
=
744 new DictionaryFetcher(this, aCallback
, mDictionaryFetcherGroup
);
745 fetcher
->mRootContentLang
= rootEditableElement
->GetLang();
746 RefPtr
<Document
> doc
= rootEditableElement
->GetComposedDoc();
747 NS_ENSURE_STATE(doc
);
748 fetcher
->mRootDocContentLang
= doc
->GetContentLanguage();
750 rv
= fetcher
->Fetch(mEditor
);
751 NS_ENSURE_SUCCESS(rv
, rv
);
756 // Helper function that iterates over the list of dictionaries and sets the one
757 // that matches based on a given comparison type.
758 bool EditorSpellCheck::BuildDictionaryList(const nsACString
& aDictName
,
759 const nsTArray
<nsCString
>& aDictList
,
760 enum dictCompare aCompareType
,
761 nsTArray
<nsCString
>& aOutList
) {
762 for (const auto& dictStr
: aDictList
) {
764 switch (aCompareType
) {
765 case DICT_NORMAL_COMPARE
:
766 equals
= aDictName
.Equals(dictStr
);
768 case DICT_COMPARE_CASE_INSENSITIVE
:
769 equals
= aDictName
.Equals(dictStr
, nsCaseInsensitiveCStringComparator
);
771 case DICT_COMPARE_DASHMATCH
:
772 equals
= nsStyleUtil::DashMatchCompare(
773 NS_ConvertUTF8toUTF16(dictStr
), NS_ConvertUTF8toUTF16(aDictName
),
774 nsCaseInsensitiveStringComparator
);
778 // Avoid adding duplicates to aOutList.
779 if (aOutList
.IndexOf(dictStr
) == nsTArray
<nsCString
>::NoIndex
) {
780 aOutList
.AppendElement(dictStr
);
783 printf("***** Trying |%s|.\n", dictStr
.get());
785 // We always break here. We tried to set the dictionary to an existing
786 // dictionary from the list. This must work, if it doesn't, there is
787 // no point trying another one.
794 nsresult
EditorSpellCheck::DictionaryFetched(DictionaryFetcher
* aFetcher
) {
795 MOZ_ASSERT(aFetcher
);
796 RefPtr
<EditorSpellCheck
> kungFuDeathGrip
= this;
798 BeginUpdateDictionary();
800 if (aFetcher
->mGroup
< mDictionaryFetcherGroup
) {
801 // SetCurrentDictionary was called after the fetch started. Don't overwrite
802 // that dictionary with the fetched one.
803 EndUpdateDictionary();
804 if (aFetcher
->mCallback
) {
805 aFetcher
->mCallback
->EditorSpellCheckDone();
811 * We try to derive the dictionary to use based on the following priorities:
812 * 1) Content preference, so the language the user set for the site before.
813 * (Introduced in bug 678842 and corrected in bug 717433.)
814 * 2) Language set by the website, or any other dictionary that partly
815 * matches that. (Introduced in bug 338427.)
816 * Eg. if the website is "en-GB", a user who only has "en-US" will get
817 * that. If the website is generic "en", the user will get one of the
818 * "en-*" installed. If application locale or system locale is "en-*",
819 * we get it. If others, it is (almost) random.
820 * However, we prefer what is stored in "spellchecker.dictionary",
821 * so if the user chose "en-AU" before, they will get "en-AU" on a plain
822 * "en" site. (Introduced in bug 682564.)
823 * If the site has multiple languages declared in its Content-Language
824 * header and there is no more specific lang tag in HTML, we try to
825 * enable a dictionary for every content language.
826 * 3) The value of "spellchecker.dictionary" which reflects a previous
827 * language choice of the user (on another site).
828 * (This was the original behaviour before the aforementioned bugs
830 * 4) The user's locale.
831 * 5) Use the current dictionary that is currently set.
832 * 6) The content of the "LANG" environment variable (if set).
833 * 7) The first spell check dictionary installed.
836 // Get the language from the element or its closest parent according to:
837 // https://html.spec.whatwg.org/#attr-lang
838 // This is used in SetCurrentDictionaries.
839 nsCString contentLangs
;
840 // Reset mPreferredLangs so we only get the current state.
841 mPreferredLangs
.Clear();
842 if (aFetcher
->mRootContentLang
) {
843 aFetcher
->mRootContentLang
->ToUTF8String(contentLangs
);
846 printf("***** mPreferredLangs (element) |%s|\n", contentLangs
.get());
848 if (!contentLangs
.IsEmpty()) {
849 mPreferredLangs
.AppendElement(contentLangs
);
851 // If no luck, try the "Content-Language" header.
852 if (aFetcher
->mRootDocContentLang
) {
853 aFetcher
->mRootDocContentLang
->ToUTF8String(contentLangs
);
856 printf("***** mPreferredLangs (content-language) |%s|\n",
859 StringToDictionaries(contentLangs
, mPreferredLangs
);
862 // We obtain a list of available dictionaries.
863 AutoTArray
<nsCString
, 8> dictList
;
864 nsresult rv
= mSpellChecker
->GetDictionaryList(&dictList
);
865 if (NS_WARN_IF(NS_FAILED(rv
))) {
866 EndUpdateDictionary();
867 if (aFetcher
->mCallback
) {
868 aFetcher
->mCallback
->EditorSpellCheckDone();
874 // If we successfully fetched a dictionary from content prefs, do not go
875 // further. Use this exact dictionary.
876 // Don't use content preferences for editor with eEditorMailMask flag.
877 nsAutoCString dictName
;
879 mEditor
->GetFlags(&flags
);
880 if (!(flags
& nsIEditor::eEditorMailMask
)) {
881 if (!aFetcher
->mDictionaries
.IsEmpty()) {
882 RefPtr
<EditorSpellCheck
> self
= this;
883 RefPtr
<DictionaryFetcher
> fetcher
= aFetcher
;
884 mSpellChecker
->SetCurrentDictionaries(aFetcher
->mDictionaries
)
886 GetMainThreadSerialEventTarget(), __func__
,
889 printf("***** Assigned from content preferences |%s|\n",
890 DictionariesToString(fetcher
->mDictionaries
).Data());
892 // We take an early exit here, so let's not forget to clear
894 self
->DeleteSuggestedWordList();
896 self
->EndUpdateDictionary();
897 if (fetcher
->mCallback
) {
898 fetcher
->mCallback
->EditorSpellCheckDone();
901 [self
, fetcher
](nsresult aError
) {
902 if (aError
== NS_ERROR_ABORT
) {
905 // May be dictionary was uninstalled ?
906 // Clear the content preference and continue.
907 ClearCurrentDictionaries(self
->mEditor
);
909 // Priority 2 or later will handled by the following
910 self
->SetFallbackDictionary(fetcher
);
915 SetFallbackDictionary(aFetcher
);
919 void EditorSpellCheck::SetDictionarySucceeded(DictionaryFetcher
* aFetcher
) {
920 DeleteSuggestedWordList();
921 EndUpdateDictionary();
922 if (aFetcher
->mCallback
) {
923 aFetcher
->mCallback
->EditorSpellCheckDone();
927 void EditorSpellCheck::SetFallbackDictionary(DictionaryFetcher
* aFetcher
) {
928 MOZ_ASSERT(mUpdateDictionaryRunning
);
930 AutoTArray
<nsCString
, 6> tryDictList
;
932 // We obtain a list of available dictionaries.
933 AutoTArray
<nsCString
, 8> dictList
;
934 nsresult rv
= mSpellChecker
->GetDictionaryList(&dictList
);
935 if (NS_WARN_IF(NS_FAILED(rv
))) {
936 EndUpdateDictionary();
937 if (aFetcher
->mCallback
) {
938 aFetcher
->mCallback
->EditorSpellCheckDone();
944 // After checking the content preferences, we use the languages of the element
947 // Get the preference value.
948 nsAutoCString prefDictionariesAsString
;
949 Preferences::GetLocalizedCString("spellchecker.dictionary",
950 prefDictionariesAsString
);
951 nsTArray
<nsCString
> prefDictionaries
;
952 StringToDictionaries(prefDictionariesAsString
, prefDictionaries
);
954 nsAutoCString appLocaleStr
;
955 // We pick one dictionary for every language that the element or document
956 // indicates it contains.
957 for (const auto& dictName
: mPreferredLangs
) {
958 // RFC 5646 explicitly states that matches should be case-insensitive.
959 if (BuildDictionaryList(dictName
, dictList
, DICT_COMPARE_CASE_INSENSITIVE
,
962 printf("***** Trying from element/doc |%s| \n", dictName
.get());
967 // Required dictionary was not available. Try to get a dictionary
968 // matching at least language part of dictName.
969 mozilla::intl::Locale loc
;
970 if (mozilla::intl::LocaleParser::TryParse(dictName
, loc
).isOk() &&
971 loc
.Canonicalize().isOk()) {
972 Span
<const char> language
= loc
.Language().Span();
973 nsAutoCString
langCode(language
.data(), language
.size());
975 // Try dictionary.spellchecker preference, if it starts with langCode,
976 // so we don't just get any random dictionary matching the language.
977 bool didAppend
= false;
978 for (const auto& dictionary
: prefDictionaries
) {
979 if (nsStyleUtil::DashMatchCompare(NS_ConvertUTF8toUTF16(dictionary
),
980 NS_ConvertUTF8toUTF16(langCode
),
981 nsTDefaultStringComparator
)) {
984 "***** Trying preference value |%s| since it matches language "
988 if (BuildDictionaryList(dictionary
, dictList
,
989 DICT_COMPARE_CASE_INSENSITIVE
, tryDictList
)) {
999 // Use the application locale dictionary when the required language
1000 // equals applocation locale language.
1001 LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr
);
1002 if (!appLocaleStr
.IsEmpty()) {
1003 mozilla::intl::Locale appLoc
;
1005 mozilla::intl::LocaleParser::TryParse(appLocaleStr
, appLoc
);
1006 if (result
.isOk() && appLoc
.Canonicalize().isOk() &&
1007 loc
.Language().Span() == appLoc
.Language().Span()) {
1008 if (BuildDictionaryList(appLocaleStr
, dictList
,
1009 DICT_COMPARE_CASE_INSENSITIVE
, tryDictList
)) {
1015 // Use the system locale dictionary when the required language equlas
1016 // system locale language.
1017 nsAutoCString sysLocaleStr
;
1018 OSPreferences::GetInstance()->GetSystemLocale(sysLocaleStr
);
1019 if (!sysLocaleStr
.IsEmpty()) {
1020 mozilla::intl::Locale sysLoc
;
1022 mozilla::intl::LocaleParser::TryParse(sysLocaleStr
, sysLoc
);
1023 if (result
.isOk() && sysLoc
.Canonicalize().isOk() &&
1024 loc
.Language().Span() == sysLoc
.Language().Span()) {
1025 if (BuildDictionaryList(sysLocaleStr
, dictList
,
1026 DICT_COMPARE_CASE_INSENSITIVE
, tryDictList
)) {
1032 // Use any dictionary with the required language.
1034 printf("***** Trying to find match for language code |%s|\n",
1037 BuildDictionaryList(langCode
, dictList
, DICT_COMPARE_DASHMATCH
,
1042 RefPtr
<EditorSpellCheck
> self
= this;
1043 RefPtr
<DictionaryFetcher
> fetcher
= aFetcher
;
1044 RefPtr
<GenericPromise
> promise
;
1046 if (tryDictList
.IsEmpty()) {
1047 // Proceed to priority 3 if the list of dictionaries is empty.
1048 promise
= GenericPromise::CreateAndReject(NS_ERROR_INVALID_ARG
, __func__
);
1050 promise
= mSpellChecker
->SetCurrentDictionaries(tryDictList
);
1053 // If an error was thrown while setting the dictionary, just
1054 // fail silently so that the spellchecker dialog is allowed to come
1055 // up. The user can manually reset the language to their choice on
1056 // the dialog if it is wrong.
1058 GetMainThreadSerialEventTarget(), __func__
,
1059 [self
, fetcher
]() { self
->SetDictionarySucceeded(fetcher
); },
1060 [prefDictionaries
= prefDictionaries
.Clone(), dictList
= dictList
.Clone(),
1062 // Build tryDictList with dictionaries for priorities 4 through 7.
1063 // We'll use this list if there is no user preference or trying
1064 // the user preference fails.
1065 AutoTArray
<nsCString
, 6> tryDictList
;
1068 // As next fallback, try the current locale.
1069 nsAutoCString appLocaleStr
;
1070 LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr
);
1072 printf("***** Trying locale |%s|\n", appLocaleStr
.get());
1074 self
->BuildDictionaryList(appLocaleStr
, dictList
,
1075 DICT_COMPARE_CASE_INSENSITIVE
, tryDictList
);
1078 // If we have a current dictionary and we don't have no item in try
1079 // list, don't try anything else.
1080 nsTArray
<nsCString
> currentDictionaries
;
1081 self
->GetCurrentDictionaries(currentDictionaries
);
1082 if (!currentDictionaries
.IsEmpty() && tryDictList
.IsEmpty()) {
1084 printf("***** Retrieved current dict |%s|\n",
1085 DictionariesToString(currentDictionaries
).Data());
1087 self
->EndUpdateDictionary();
1088 if (fetcher
->mCallback
) {
1089 fetcher
->mCallback
->EditorSpellCheckDone();
1095 // Try to get current dictionary from environment variable LANG.
1096 // LANG = language[_territory][.charset]
1097 char* env_lang
= getenv("LANG");
1099 nsAutoCString
lang(env_lang
);
1100 // Strip trailing charset, if there is any.
1101 int32_t dot_pos
= lang
.FindChar('.');
1102 if (dot_pos
!= -1) {
1103 lang
= Substring(lang
, 0, dot_pos
);
1106 int32_t underScore
= lang
.FindChar('_');
1107 if (underScore
!= -1) {
1108 lang
.Replace(underScore
, 1, '-');
1110 printf("***** Trying LANG from environment |%s|\n", lang
.get());
1112 self
->BuildDictionaryList(
1113 lang
, dictList
, DICT_COMPARE_CASE_INSENSITIVE
, tryDictList
);
1118 // If it does not work, pick the first one.
1119 if (!dictList
.IsEmpty()) {
1120 self
->BuildDictionaryList(dictList
[0], dictList
, DICT_NORMAL_COMPARE
,
1123 printf("***** Trying first of list |%s|\n", dictList
[0].get());
1128 // If the document didn't supply a dictionary or the setting
1129 // failed, try the user preference next.
1130 if (!prefDictionaries
.IsEmpty()) {
1131 self
->mSpellChecker
->SetCurrentDictionaries(prefDictionaries
)
1133 GetMainThreadSerialEventTarget(), __func__
,
1134 [self
, fetcher
]() { self
->SetDictionarySucceeded(fetcher
); },
1135 // Priority 3 failed, we'll use the list we built of
1136 // priorities 4 to 7.
1137 [tryDictList
= tryDictList
.Clone(), self
, fetcher
]() {
1139 ->SetCurrentDictionaryFromList(tryDictList
)
1140 ->Then(GetMainThreadSerialEventTarget(), __func__
,
1142 self
->SetDictionarySucceeded(fetcher
);
1146 // We don't have a user preference, so we'll try the list we
1147 // built of priorities 4 to 7.
1148 self
->mSpellChecker
->SetCurrentDictionaryFromList(tryDictList
)
1150 GetMainThreadSerialEventTarget(), __func__
,
1151 [self
, fetcher
]() { self
->SetDictionarySucceeded(fetcher
); });
1156 } // namespace mozilla