Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sts=2 sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "EditorSpellCheck.h"
#include "EditorBase.h" // for EditorBase
#include "HTMLEditor.h" // for HTMLEditor
#include "TextServicesDocument.h" // for TextServicesDocument
#include "mozilla/Attributes.h" // for final
#include "mozilla/dom/Element.h" // for Element
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/StaticRange.h"
#include "mozilla/intl/Locale.h" // for mozilla::intl::Locale
#include "mozilla/intl/LocaleService.h" // for retrieving app locale
#include "mozilla/intl/OSPreferences.h" // for mozilla::intl::OSPreferences
#include "mozilla/Logging.h" // for mozilla::LazyLogModule
#include "mozilla/mozalloc.h" // for operator delete, etc
#include "mozilla/mozSpellChecker.h" // for mozSpellChecker
#include "mozilla/Preferences.h" // for Preferences
#include "nsAString.h" // for nsAString::IsEmpty, etc
#include "nsComponentManagerUtils.h" // for do_CreateInstance
#include "nsDebug.h" // for NS_ENSURE_TRUE, etc
#include "nsDependentSubstring.h" // for Substring
#include "nsError.h" // for NS_ERROR_NOT_INITIALIZED, etc
#include "nsIContent.h" // for nsIContent
#include "nsIContentPrefService2.h" // for nsIContentPrefService2, etc
#include "mozilla/dom/Document.h" // for Document
#include "nsIEditor.h" // for nsIEditor
#include "nsILoadContext.h"
#include "nsISupports.h" // for nsISupports
#include "nsISupportsUtils.h" // for NS_ADDREF
#include "nsIURI.h" // for nsIURI
#include "nsThreadUtils.h" // for GetMainThreadSerialEventTarget
#include "nsVariant.h" // for nsIWritableVariant, etc
#include "nsLiteralString.h" // for NS_LITERAL_STRING, etc
#include "nsRange.h"
#include "nsReadableUtils.h" // for ToNewUnicode, EmptyString, etc
#include "nsServiceManagerUtils.h" // for do_GetService
#include "nsString.h" // for nsAutoString, nsString, etc
#include "nsStringFwd.h" // for nsAFlatString
#include "nsStyleUtil.h" // for nsStyleUtil
#include "nsXULAppAPI.h" // for XRE_GetProcessType
namespace mozilla {
using namespace dom;
using intl::LocaleService;
using intl::OSPreferences;
static mozilla::LazyLogModule sEditorSpellChecker("EditorSpellChecker");
class UpdateDictionaryHolder {
private:
EditorSpellCheck* mSpellCheck;
public:
explicit UpdateDictionaryHolder(EditorSpellCheck* esc) : mSpellCheck(esc) {
if (mSpellCheck) {
mSpellCheck->BeginUpdateDictionary();
}
}
~UpdateDictionaryHolder() {
if (mSpellCheck) {
mSpellCheck->EndUpdateDictionary();
}
}
};
#define CPS_PREF_NAME u"spellcheck.lang"_ns
/**
* Gets the URI of aEditor's document.
*/
static nsIURI* GetDocumentURI(EditorBase* aEditor) {
MOZ_ASSERT(aEditor);
Document* doc = aEditor->AsEditorBase()->GetDocument();
if (NS_WARN_IF(!doc)) {
return nullptr;
}
return doc->GetDocumentURI();
}
static nsILoadContext* GetLoadContext(nsIEditor* aEditor) {
Document* doc = aEditor->AsEditorBase()->GetDocument();
if (NS_WARN_IF(!doc)) {
return nullptr;
}
return doc->GetLoadContext();
}
static nsCString DictionariesToString(
const nsTArray<nsCString>& aDictionaries) {
nsCString asString;
for (const auto& dictionary : aDictionaries) {
asString.Append(dictionary);
asString.Append(',');
}
return asString;
}
static void StringToDictionaries(const nsCString& aString,
nsTArray<nsCString>& aDictionaries) {
nsTArray<nsCString> asDictionaries;
for (const nsACString& token :
nsCCharSeparatedTokenizer(aString, ',').ToRange()) {
if (token.IsEmpty()) {
continue;
}
aDictionaries.AppendElement(token);
}
}
/**
* Fetches the dictionary stored in content prefs and maintains state during the
* fetch, which is asynchronous.
*/
class DictionaryFetcher final : public nsIContentPrefCallback2 {
public:
NS_DECL_ISUPPORTS
DictionaryFetcher(EditorSpellCheck* aSpellCheck,
nsIEditorSpellCheckCallback* aCallback, uint32_t aGroup)
: mCallback(aCallback), mGroup(aGroup), mSpellCheck(aSpellCheck) {}
NS_IMETHOD Fetch(nsIEditor* aEditor);
NS_IMETHOD HandleResult(nsIContentPref* aPref) override {
nsCOMPtr<nsIVariant> value;
nsresult rv = aPref->GetValue(getter_AddRefs(value));
NS_ENSURE_SUCCESS(rv, rv);
nsCString asString;
value->GetAsACString(asString);
StringToDictionaries(asString, mDictionaries);
return NS_OK;
}
NS_IMETHOD HandleCompletion(uint16_t reason) override {
mSpellCheck->DictionaryFetched(this);
return NS_OK;
}
NS_IMETHOD HandleError(nsresult error) override { return NS_OK; }
nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
uint32_t mGroup;
RefPtr<nsAtom> mRootContentLang;
RefPtr<nsAtom> mRootDocContentLang;
nsTArray<nsCString> mDictionaries;
private:
~DictionaryFetcher() {}
RefPtr<EditorSpellCheck> mSpellCheck;
};
NS_IMPL_ISUPPORTS(DictionaryFetcher, nsIContentPrefCallback2)
class ContentPrefInitializerRunnable final : public Runnable {
public:
ContentPrefInitializerRunnable(nsIEditor* aEditor,
nsIContentPrefCallback2* aCallback)
: Runnable("ContentPrefInitializerRunnable"),
mEditorBase(aEditor->AsEditorBase()),
mCallback(aCallback) {}
NS_IMETHOD Run() override {
if (mEditorBase->Destroyed()) {
mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
return NS_OK;
}
nsCOMPtr<nsIContentPrefService2> contentPrefService =
do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
if (NS_WARN_IF(!contentPrefService)) {
mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
return NS_OK;
}
nsCOMPtr<nsIURI> docUri = GetDocumentURI(mEditorBase);
if (NS_WARN_IF(!docUri)) {
mCallback->HandleError(NS_ERROR_FAILURE);
return NS_OK;
}
nsAutoCString docUriSpec;
nsresult rv = docUri->GetSpec(docUriSpec);
if (NS_WARN_IF(NS_FAILED(rv))) {
mCallback->HandleError(rv);
return NS_OK;
}
rv = contentPrefService->GetByDomainAndName(
NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME,
GetLoadContext(mEditorBase), mCallback);
if (NS_WARN_IF(NS_FAILED(rv))) {
mCallback->HandleError(rv);
return NS_OK;
}
return NS_OK;
}
private:
RefPtr<EditorBase> mEditorBase;
nsCOMPtr<nsIContentPrefCallback2> mCallback;
};
NS_IMETHODIMP
DictionaryFetcher::Fetch(nsIEditor* aEditor) {
NS_ENSURE_ARG_POINTER(aEditor);
nsCOMPtr<nsIRunnable> runnable =
new ContentPrefInitializerRunnable(aEditor, this);
NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000,
EventQueuePriority::Idle);
return NS_OK;
}
/**
* Stores the current dictionary for aEditor's document URL.
*/
static nsresult StoreCurrentDictionaries(
EditorBase* aEditorBase, const nsTArray<nsCString>& aDictionaries) {
NS_ENSURE_ARG_POINTER(aEditorBase);
nsresult rv;
nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
if (NS_WARN_IF(!docUri)) {
return NS_ERROR_FAILURE;
}
nsAutoCString docUriSpec;
rv = docUri->GetSpec(docUriSpec);
NS_ENSURE_SUCCESS(rv, rv);
RefPtr<nsVariant> prefValue = new nsVariant();
nsCString asString = DictionariesToString(aDictionaries);
prefValue->SetAsAString(NS_ConvertUTF8toUTF16(asString));
nsCOMPtr<nsIContentPrefService2> contentPrefService =
do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
return contentPrefService->Set(NS_ConvertUTF8toUTF16(docUriSpec),
CPS_PREF_NAME, prefValue,
GetLoadContext(aEditorBase), nullptr);
}
/**
* Forgets the current dictionary stored for aEditor's document URL.
*/
static nsresult ClearCurrentDictionaries(EditorBase* aEditorBase) {
NS_ENSURE_ARG_POINTER(aEditorBase);
nsresult rv;
nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
if (NS_WARN_IF(!docUri)) {
return NS_ERROR_FAILURE;
}
nsAutoCString docUriSpec;
rv = docUri->GetSpec(docUriSpec);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIContentPrefService2> contentPrefService =
do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
return contentPrefService->RemoveByDomainAndName(
NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME,
GetLoadContext(aEditorBase), nullptr);
}
NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorSpellCheck)
NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorSpellCheck)
NS_INTERFACE_MAP_BEGIN(EditorSpellCheck)
NS_INTERFACE_MAP_ENTRY(nsIEditorSpellCheck)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditorSpellCheck)
NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(EditorSpellCheck)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTION(EditorSpellCheck, mEditor, mSpellChecker)
EditorSpellCheck::EditorSpellCheck()
: mTxtSrvFilterType(0),
mSuggestedWordIndex(0),
mDictionaryIndex(0),
mDictionaryFetcherGroup(0),
mUpdateDictionaryRunning(false) {}
EditorSpellCheck::~EditorSpellCheck() {
// Make sure we blow the spellchecker away, just in
// case it hasn't been destroyed already.
mSpellChecker = nullptr;
}
mozSpellChecker* EditorSpellCheck::GetSpellChecker() { return mSpellChecker; }
// The problem is that if the spell checker does not exist, we can not tell
// which dictionaries are installed. This function works around the problem,
// allowing callers to ask if we can spell check without actually doing so (and
// enabling or disabling UI as necessary). This just creates a spellcheck
// object if needed and asks it for the dictionary list.
NS_IMETHODIMP
EditorSpellCheck::CanSpellCheck(bool* aCanSpellCheck) {
RefPtr<mozSpellChecker> spellChecker = mSpellChecker;
if (!spellChecker) {
spellChecker = mozSpellChecker::Create();
MOZ_ASSERT(spellChecker);
}
nsTArray<nsCString> dictList;
nsresult rv = spellChecker->GetDictionaryList(&dictList);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
*aCanSpellCheck = !dictList.IsEmpty();
return NS_OK;
}
// Instances of this class can be used as either runnables or RAII helpers.
class CallbackCaller final : public Runnable {
public:
explicit CallbackCaller(nsIEditorSpellCheckCallback* aCallback)
: mozilla::Runnable("CallbackCaller"), mCallback(aCallback) {}
~CallbackCaller() { Run(); }
NS_IMETHOD Run() override {
if (mCallback) {
mCallback->EditorSpellCheckDone();
mCallback = nullptr;
}
return NS_OK;
}
private:
nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
};
NS_IMETHODIMP
EditorSpellCheck::InitSpellChecker(nsIEditor* aEditor,
bool aEnableSelectionChecking,
nsIEditorSpellCheckCallback* aCallback) {
NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER);
mEditor = aEditor->AsEditorBase();
RefPtr<Document> doc = mEditor->GetDocument();
if (NS_WARN_IF(!doc)) {
return NS_ERROR_FAILURE;
}
nsresult rv;
// We can spell check with any editor type
RefPtr<TextServicesDocument> textServicesDocument =
new TextServicesDocument();
textServicesDocument->SetFilterType(mTxtSrvFilterType);
// EditorBase::AddEditActionListener() needs to access mSpellChecker and
// mSpellChecker->GetTextServicesDocument(). Therefore, we need to
// initialize them before calling TextServicesDocument::InitWithEditor()
// since it calls EditorBase::AddEditActionListener().
mSpellChecker = mozSpellChecker::Create();
MOZ_ASSERT(mSpellChecker);
rv = mSpellChecker->SetDocument(textServicesDocument, true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Pass the editor to the text services document
rv = textServicesDocument->InitWithEditor(aEditor);
NS_ENSURE_SUCCESS(rv, rv);
if (aEnableSelectionChecking) {
// Find out if the section is collapsed or not.
// If it isn't, we want to spellcheck just the selection.
RefPtr<Selection> selection;
aEditor->GetSelection(getter_AddRefs(selection));
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
if (selection->RangeCount()) {
RefPtr<const nsRange> range = selection->GetRangeAt(0);
NS_ENSURE_STATE(range);
if (!range->Collapsed()) {
// We don't want to touch the range in the selection,
// so create a new copy of it.
RefPtr<StaticRange> staticRange =
StaticRange::Create(range, IgnoreErrors());
if (NS_WARN_IF(!staticRange)) {
return NS_ERROR_FAILURE;
}
// Make sure the new range spans complete words.
rv = textServicesDocument->ExpandRangeToWordBoundaries(staticRange);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Now tell the text services that you only want
// to iterate over the text in this range.
rv = textServicesDocument->SetExtent(staticRange);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
}
// do not fail if UpdateCurrentDictionary fails because this method may
// succeed later.
rv = UpdateCurrentDictionary(aCallback);
if (NS_FAILED(rv) && aCallback) {
// However, if it does fail, we still need to call the callback since we
// discard the failure. Do it asynchronously so that the caller is always
// guaranteed async behavior.
RefPtr<CallbackCaller> caller = new CallbackCaller(aCallback);
rv = doc->Dispatch(caller.forget());
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
NS_IMETHODIMP
EditorSpellCheck::GetNextMisspelledWord(nsAString& aNextMisspelledWord) {
MOZ_LOG(sEditorSpellChecker, LogLevel::Debug, ("%s", __FUNCTION__));
NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
DeleteSuggestedWordList();
// Beware! This may flush notifications via synchronous
// ScrollSelectionIntoView.
RefPtr<mozSpellChecker> spellChecker(mSpellChecker);
return spellChecker->NextMisspelledWord(aNextMisspelledWord,
mSuggestedWordList);
}
NS_IMETHODIMP
EditorSpellCheck::GetSuggestedWord(nsAString& aSuggestedWord) {
// XXX This is buggy if mSuggestedWordList.Length() is over INT32_MAX.
if (mSuggestedWordIndex < static_cast<int32_t>(mSuggestedWordList.Length())) {
aSuggestedWord = mSuggestedWordList[mSuggestedWordIndex];
mSuggestedWordIndex++;
} else {
// A blank string signals that there are no more strings
aSuggestedWord.Truncate();
}
return NS_OK;
}
NS_IMETHODIMP
EditorSpellCheck::CheckCurrentWord(const nsAString& aSuggestedWord,
bool* aIsMisspelled) {
NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
DeleteSuggestedWordList();
return mSpellChecker->CheckWord(aSuggestedWord, aIsMisspelled,
&mSuggestedWordList);
}
NS_IMETHODIMP
EditorSpellCheck::Suggest(const nsAString& aSuggestedWord, uint32_t aCount,
JSContext* aCx, Promise** aPromise) {
NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
if (NS_WARN_IF(!globalObject)) {
return NS_ERROR_UNEXPECTED;
}
ErrorResult result;
RefPtr<Promise> promise = Promise::Create(globalObject, result);
if (NS_WARN_IF(result.Failed())) {
return result.StealNSResult();
}
mSpellChecker->Suggest(aSuggestedWord, aCount)
->Then(
GetMainThreadSerialEventTarget(), __func__,
[promise](const CopyableTArray<nsString>& aSuggestions) {
promise->MaybeResolve(aSuggestions);
},
[promise](nsresult aError) {
promise->MaybeReject(NS_ERROR_FAILURE);
});
promise.forget(aPromise);
return NS_OK;
}
RefPtr<CheckWordPromise> EditorSpellCheck::CheckCurrentWordsNoSuggest(
const nsTArray<nsString>& aSuggestedWords) {
if (NS_WARN_IF(!mSpellChecker)) {
return CheckWordPromise::CreateAndReject(NS_ERROR_NOT_INITIALIZED,
__func__);
}
return mSpellChecker->CheckWords(aSuggestedWords);
}
NS_IMETHODIMP
EditorSpellCheck::ReplaceWord(const nsAString& aMisspelledWord,
const nsAString& aReplaceWord,
bool aAllOccurrences) {
NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
RefPtr<mozSpellChecker> spellChecker(mSpellChecker);
return spellChecker->Replace(aMisspelledWord, aReplaceWord, aAllOccurrences);
}
NS_IMETHODIMP
EditorSpellCheck::IgnoreWordAllOccurrences(const nsAString& aWord) {
NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
return mSpellChecker->IgnoreAll(aWord);
}
NS_IMETHODIMP
EditorSpellCheck::GetPersonalDictionary() {
NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
// We can spell check with any editor type
mDictionaryList.Clear();
mDictionaryIndex = 0;
return mSpellChecker->GetPersonalDictionary(&mDictionaryList);
}
NS_IMETHODIMP
EditorSpellCheck::GetPersonalDictionaryWord(nsAString& aDictionaryWord) {
// XXX This is buggy if mDictionaryList.Length() is over INT32_MAX.
if (mDictionaryIndex < static_cast<int32_t>(mDictionaryList.Length())) {
aDictionaryWord = mDictionaryList[mDictionaryIndex];
mDictionaryIndex++;
} else {
// A blank string signals that there are no more strings
aDictionaryWord.Truncate();
}
return NS_OK;
}
NS_IMETHODIMP
EditorSpellCheck::AddWordToDictionary(const nsAString& aWord) {
NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
return mSpellChecker->AddWordToPersonalDictionary(aWord);
}
NS_IMETHODIMP
EditorSpellCheck::RemoveWordFromDictionary(const nsAString& aWord) {
NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
return mSpellChecker->RemoveWordFromPersonalDictionary(aWord);
}
NS_IMETHODIMP
EditorSpellCheck::GetDictionaryList(nsTArray<nsCString>& aList) {
NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
return mSpellChecker->GetDictionaryList(&aList);
}
NS_IMETHODIMP
EditorSpellCheck::GetCurrentDictionaries(nsTArray<nsCString>& aDictionaries) {
NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
return mSpellChecker->GetCurrentDictionaries(aDictionaries);
}
NS_IMETHODIMP
EditorSpellCheck::SetCurrentDictionaries(
const nsTArray<nsCString>& aDictionaries, JSContext* aCx,
Promise** aPromise) {
NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
// The purpose of mUpdateDictionaryRunning is to avoid doing all of this if
// UpdateCurrentDictionary's helper method DictionaryFetched, which calls us,
// is on the stack. In other words: Only do this, if the user manually
// selected a dictionary to use.
if (!mUpdateDictionaryRunning) {
// Ignore pending dictionary fetchers by increasing this number.
mDictionaryFetcherGroup++;
uint32_t flags = 0;
mEditor->GetFlags(&flags);
if (!(flags & nsIEditor::eEditorMailMask)) {
bool contentPrefMatchesUserPref = true;
// Check if aDictionaries has the same languages as mPreferredLangs.
if (!aDictionaries.IsEmpty()) {
if (aDictionaries.Length() != mPreferredLangs.Length()) {
contentPrefMatchesUserPref = false;
} else {
for (const auto& dictName : aDictionaries) {
if (mPreferredLangs.IndexOf(dictName) ==
nsTArray<nsCString>::NoIndex) {
contentPrefMatchesUserPref = false;
break;
}
}
}
}
if (!contentPrefMatchesUserPref) {
// When user sets dictionary manually, we store this value associated
// with editor url, if it doesn't match the document language exactly.
// For example on "en" sites, we need to store "en-GB", otherwise
// the language might jump back to en-US although the user explicitly
// chose otherwise.
StoreCurrentDictionaries(mEditor, aDictionaries);
#ifdef DEBUG_DICT
printf("***** Writing content preferences for |%s|\n",
DictionariesToString(aDictionaries).Data());
#endif
} else {
// If user sets a dictionary matching the language defined by
// document, we consider content pref has been canceled, and we clear
// it.
ClearCurrentDictionaries(mEditor);
#ifdef DEBUG_DICT
printf("***** Clearing content preferences for |%s|\n",
DictionariesToString(aDictionaries).Data());
#endif
}
// Also store it in as a preference, so we can use it as a fallback.
// We don't want this for mail composer because it uses
// "spellchecker.dictionary" as a preference.
//
// XXX: Prefs can only be set in the parent process, so this condition is
// necessary to stop libpref from throwing errors. But this should
// probably be handled in a better way.
if (XRE_IsParentProcess()) {
nsCString asString = DictionariesToString(aDictionaries);
Preferences::SetCString("spellchecker.dictionary", asString);
#ifdef DEBUG_DICT
printf("***** Possibly storing spellchecker.dictionary |%s|\n",
asString.Data());
#endif
}
} else {
MOZ_ASSERT(flags & nsIEditor::eEditorMailMask);
// Since the mail editor can only influence the language selection by the
// html lang attribute, set the content-language document to persist
// multi language selections.
// XXX Why doesn't here use the document of the editor directly?
nsCOMPtr<nsIContent> anonymousDivOrEditingHost;
if (HTMLEditor* htmlEditor = mEditor->GetAsHTMLEditor()) {
anonymousDivOrEditingHost = htmlEditor->ComputeEditingHost();
} else {
anonymousDivOrEditingHost = mEditor->GetRoot();
}
RefPtr<Document> ownerDoc = anonymousDivOrEditingHost->OwnerDoc();
Document* parentDoc = ownerDoc->GetInProcessParentDocument();
if (parentDoc) {
parentDoc->SetHeaderData(
nsGkAtoms::headerContentLanguage,
NS_ConvertUTF8toUTF16(DictionariesToString(aDictionaries)));
}
}
}
nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
if (NS_WARN_IF(!globalObject)) {
return NS_ERROR_UNEXPECTED;
}
ErrorResult result;
RefPtr<Promise> promise = Promise::Create(globalObject, result);
if (NS_WARN_IF(result.Failed())) {
return result.StealNSResult();
}
mSpellChecker->SetCurrentDictionaries(aDictionaries)
->Then(
GetMainThreadSerialEventTarget(), __func__,
[promise]() { promise->MaybeResolveWithUndefined(); },
[promise](nsresult aError) {
promise->MaybeReject(NS_ERROR_FAILURE);
});
promise.forget(aPromise);
return NS_OK;
}
NS_IMETHODIMP
EditorSpellCheck::UninitSpellChecker() {
NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
// Cleanup - kill the spell checker
DeleteSuggestedWordList();
mDictionaryList.Clear();
mDictionaryIndex = 0;
mDictionaryFetcherGroup++;
mSpellChecker = nullptr;
return NS_OK;
}
NS_IMETHODIMP
EditorSpellCheck::SetFilterType(uint32_t aFilterType) {
mTxtSrvFilterType = aFilterType;
return NS_OK;
}
nsresult EditorSpellCheck::DeleteSuggestedWordList() {
mSuggestedWordList.Clear();
mSuggestedWordIndex = 0;
return NS_OK;
}
NS_IMETHODIMP
EditorSpellCheck::UpdateCurrentDictionary(
nsIEditorSpellCheckCallback* aCallback) {
if (NS_WARN_IF(!mSpellChecker)) {
return NS_ERROR_NOT_INITIALIZED;
}
nsresult rv;
RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
// Get language with html5 algorithm
const RefPtr<Element> rootEditableElement =
[](const EditorBase& aEditorBase) -> Element* {
if (!aEditorBase.IsHTMLEditor()) {
return aEditorBase.GetRoot();
}
if (aEditorBase.IsMailEditor()) {
// Shouldn't run spellcheck in a mail editor without focus
// XXX Why doesn't here use the document of the editor directly?
Element* const editingHost =
aEditorBase.AsHTMLEditor()->ComputeEditingHost();
if (!editingHost) {
return nullptr;
}
// Try to get topmost document's document element for embedded mail
// editor (bug 967494)
Document* parentDoc =
editingHost->OwnerDoc()->GetInProcessParentDocument();
if (!parentDoc) {
return editingHost;
}
return parentDoc->GetDocumentElement();
}
return aEditorBase.AsHTMLEditor()->GetFocusedElement();
}(*mEditor);
if (!rootEditableElement) {
return NS_ERROR_FAILURE;
}
RefPtr<DictionaryFetcher> fetcher =
new DictionaryFetcher(this, aCallback, mDictionaryFetcherGroup);
fetcher->mRootContentLang = rootEditableElement->GetLang();
RefPtr<Document> doc = rootEditableElement->GetComposedDoc();
NS_ENSURE_STATE(doc);
fetcher->mRootDocContentLang = doc->GetContentLanguage();
rv = fetcher->Fetch(mEditor);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
// Helper function that iterates over the list of dictionaries and sets the one
// that matches based on a given comparison type.
bool EditorSpellCheck::BuildDictionaryList(const nsACString& aDictName,
const nsTArray<nsCString>& aDictList,
enum dictCompare aCompareType,
nsTArray<nsCString>& aOutList) {
for (const auto& dictStr : aDictList) {
bool equals = false;
switch (aCompareType) {
case DICT_NORMAL_COMPARE:
equals = aDictName.Equals(dictStr);
break;
case DICT_COMPARE_CASE_INSENSITIVE:
equals = aDictName.Equals(dictStr, nsCaseInsensitiveCStringComparator);
break;
case DICT_COMPARE_DASHMATCH:
equals = nsStyleUtil::DashMatchCompare(
NS_ConvertUTF8toUTF16(dictStr), NS_ConvertUTF8toUTF16(aDictName),
nsCaseInsensitiveStringComparator);
break;
}
if (equals) {
// Avoid adding duplicates to aOutList.
if (aOutList.IndexOf(dictStr) == nsTArray<nsCString>::NoIndex) {
aOutList.AppendElement(dictStr);
}
#ifdef DEBUG_DICT
printf("***** Trying |%s|.\n", dictStr.get());
#endif
// We always break here. We tried to set the dictionary to an existing
// dictionary from the list. This must work, if it doesn't, there is
// no point trying another one.
return true;
}
}
return false;
}
nsresult EditorSpellCheck::DictionaryFetched(DictionaryFetcher* aFetcher) {
MOZ_ASSERT(aFetcher);
RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
BeginUpdateDictionary();
if (aFetcher->mGroup < mDictionaryFetcherGroup) {
// SetCurrentDictionary was called after the fetch started. Don't overwrite
// that dictionary with the fetched one.
EndUpdateDictionary();
if (aFetcher->mCallback) {
aFetcher->mCallback->EditorSpellCheckDone();
}
return NS_OK;
}
/*
* We try to derive the dictionary to use based on the following priorities:
* 1) Content preference, so the language the user set for the site before.
* (Introduced in bug 678842 and corrected in bug 717433.)
* 2) Language set by the website, or any other dictionary that partly
* matches that. (Introduced in bug 338427.)
* Eg. if the website is "en-GB", a user who only has "en-US" will get
* that. If the website is generic "en", the user will get one of the
* "en-*" installed. If application locale or system locale is "en-*",
* we get it. If others, it is (almost) random.
* However, we prefer what is stored in "spellchecker.dictionary",
* so if the user chose "en-AU" before, they will get "en-AU" on a plain
* "en" site. (Introduced in bug 682564.)
* If the site has multiple languages declared in its Content-Language
* header and there is no more specific lang tag in HTML, we try to
* enable a dictionary for every content language.
* 3) The value of "spellchecker.dictionary" which reflects a previous
* language choice of the user (on another site).
* (This was the original behaviour before the aforementioned bugs
* landed).
* 4) The user's locale.
* 5) Use the current dictionary that is currently set.
* 6) The content of the "LANG" environment variable (if set).
* 7) The first spell check dictionary installed.
*/
// Get the language from the element or its closest parent according to:
// This is used in SetCurrentDictionaries.
nsCString contentLangs;
// Reset mPreferredLangs so we only get the current state.
mPreferredLangs.Clear();
if (aFetcher->mRootContentLang) {
aFetcher->mRootContentLang->ToUTF8String(contentLangs);
}
#ifdef DEBUG_DICT
printf("***** mPreferredLangs (element) |%s|\n", contentLangs.get());
#endif
if (!contentLangs.IsEmpty()) {
mPreferredLangs.AppendElement(contentLangs);
} else {
// If no luck, try the "Content-Language" header.
if (aFetcher->mRootDocContentLang) {
aFetcher->mRootDocContentLang->ToUTF8String(contentLangs);
}
#ifdef DEBUG_DICT
printf("***** mPreferredLangs (content-language) |%s|\n",
contentLangs.get());
#endif
StringToDictionaries(contentLangs, mPreferredLangs);
}
// We obtain a list of available dictionaries.
AutoTArray<nsCString, 8> dictList;
nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
if (NS_WARN_IF(NS_FAILED(rv))) {
EndUpdateDictionary();
if (aFetcher->mCallback) {
aFetcher->mCallback->EditorSpellCheckDone();
}
return rv;
}
// Priority 1:
// If we successfully fetched a dictionary from content prefs, do not go
// further. Use this exact dictionary.
// Don't use content preferences for editor with eEditorMailMask flag.
nsAutoCString dictName;
uint32_t flags;
mEditor->GetFlags(&flags);
if (!(flags & nsIEditor::eEditorMailMask)) {
if (!aFetcher->mDictionaries.IsEmpty()) {
RefPtr<EditorSpellCheck> self = this;
RefPtr<DictionaryFetcher> fetcher = aFetcher;
mSpellChecker->SetCurrentDictionaries(aFetcher->mDictionaries)
->Then(
GetMainThreadSerialEventTarget(), __func__,
[self, fetcher]() {
#ifdef DEBUG_DICT
printf("***** Assigned from content preferences |%s|\n",
DictionariesToString(fetcher->mDictionaries).Data());
#endif
// We take an early exit here, so let's not forget to clear
// the word list.
self->DeleteSuggestedWordList();
self->EndUpdateDictionary();
if (fetcher->mCallback) {
fetcher->mCallback->EditorSpellCheckDone();
}
},
[self, fetcher](nsresult aError) {
if (aError == NS_ERROR_ABORT) {
return;
}
// May be dictionary was uninstalled ?
// Clear the content preference and continue.
ClearCurrentDictionaries(self->mEditor);
// Priority 2 or later will handled by the following
self->SetFallbackDictionary(fetcher);
});
return NS_OK;
}
}
SetFallbackDictionary(aFetcher);
return NS_OK;
}
void EditorSpellCheck::SetDictionarySucceeded(DictionaryFetcher* aFetcher) {
DeleteSuggestedWordList();
EndUpdateDictionary();
if (aFetcher->mCallback) {
aFetcher->mCallback->EditorSpellCheckDone();
}
}
void EditorSpellCheck::SetFallbackDictionary(DictionaryFetcher* aFetcher) {
MOZ_ASSERT(mUpdateDictionaryRunning);
AutoTArray<nsCString, 6> tryDictList;
// We obtain a list of available dictionaries.
AutoTArray<nsCString, 8> dictList;
nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
if (NS_WARN_IF(NS_FAILED(rv))) {
EndUpdateDictionary();
if (aFetcher->mCallback) {
aFetcher->mCallback->EditorSpellCheckDone();
}
return;
}
// Priority 2:
// After checking the content preferences, we use the languages of the element
// or document.
// Get the preference value.
nsAutoCString prefDictionariesAsString;
Preferences::GetLocalizedCString("spellchecker.dictionary",
prefDictionariesAsString);
nsTArray<nsCString> prefDictionaries;
StringToDictionaries(prefDictionariesAsString, prefDictionaries);
nsAutoCString appLocaleStr;
// We pick one dictionary for every language that the element or document
// indicates it contains.
for (const auto& dictName : mPreferredLangs) {
// RFC 5646 explicitly states that matches should be case-insensitive.
if (BuildDictionaryList(dictName, dictList, DICT_COMPARE_CASE_INSENSITIVE,
tryDictList)) {
#ifdef DEBUG_DICT
printf("***** Trying from element/doc |%s| \n", dictName.get());
#endif
continue;
}
// Required dictionary was not available. Try to get a dictionary
// matching at least language part of dictName.
mozilla::intl::Locale loc;
if (mozilla::intl::LocaleParser::TryParse(dictName, loc).isOk() &&
loc.Canonicalize().isOk()) {
Span<const char> language = loc.Language().Span();
nsAutoCString langCode(language.data(), language.size());
// Try dictionary.spellchecker preference, if it starts with langCode,
// so we don't just get any random dictionary matching the language.
bool didAppend = false;
for (const auto& dictionary : prefDictionaries) {
if (nsStyleUtil::DashMatchCompare(NS_ConvertUTF8toUTF16(dictionary),
NS_ConvertUTF8toUTF16(langCode),
nsTDefaultStringComparator)) {
#ifdef DEBUG_DICT
printf(
"***** Trying preference value |%s| since it matches language "
"code\n",
dictionary.Data());
#endif
if (BuildDictionaryList(dictionary, dictList,
DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
didAppend = true;
break;
}
}
}
if (didAppend) {
continue;
}
// Use the application locale dictionary when the required language
// equals applocation locale language.
LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
if (!appLocaleStr.IsEmpty()) {
mozilla::intl::Locale appLoc;
auto result =
mozilla::intl::LocaleParser::TryParse(appLocaleStr, appLoc);
if (result.isOk() && appLoc.Canonicalize().isOk() &&
loc.Language().Span() == appLoc.Language().Span()) {
if (BuildDictionaryList(appLocaleStr, dictList,
DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
continue;
}
}
}
// Use the system locale dictionary when the required language equlas
// system locale language.
nsAutoCString sysLocaleStr;
OSPreferences::GetInstance()->GetSystemLocale(sysLocaleStr);
if (!sysLocaleStr.IsEmpty()) {
mozilla::intl::Locale sysLoc;
auto result =
mozilla::intl::LocaleParser::TryParse(sysLocaleStr, sysLoc);
if (result.isOk() && sysLoc.Canonicalize().isOk() &&
loc.Language().Span() == sysLoc.Language().Span()) {
if (BuildDictionaryList(sysLocaleStr, dictList,
DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
continue;
}
}
}
// Use any dictionary with the required language.
#ifdef DEBUG_DICT
printf("***** Trying to find match for language code |%s|\n",
langCode.get());
#endif
BuildDictionaryList(langCode, dictList, DICT_COMPARE_DASHMATCH,
tryDictList);
}
}
RefPtr<EditorSpellCheck> self = this;
RefPtr<DictionaryFetcher> fetcher = aFetcher;
RefPtr<GenericPromise> promise;
if (tryDictList.IsEmpty()) {
// Proceed to priority 3 if the list of dictionaries is empty.
promise = GenericPromise::CreateAndReject(NS_ERROR_INVALID_ARG, __func__);
} else {
promise = mSpellChecker->SetCurrentDictionaries(tryDictList);
}
// If an error was thrown while setting the dictionary, just
// fail silently so that the spellchecker dialog is allowed to come
// up. The user can manually reset the language to their choice on
// the dialog if it is wrong.
promise->Then(
GetMainThreadSerialEventTarget(), __func__,
[self, fetcher]() { self->SetDictionarySucceeded(fetcher); },
[prefDictionaries = prefDictionaries.Clone(), dictList = dictList.Clone(),
self, fetcher]() {
// Build tryDictList with dictionaries for priorities 4 through 7.
// We'll use this list if there is no user preference or trying
// the user preference fails.
AutoTArray<nsCString, 6> tryDictList;
// Priority 4:
// As next fallback, try the current locale.
nsAutoCString appLocaleStr;
LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
#ifdef DEBUG_DICT
printf("***** Trying locale |%s|\n", appLocaleStr.get());
#endif
self->BuildDictionaryList(appLocaleStr, dictList,
DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
// Priority 5:
// If we have a current dictionary and we don't have no item in try
// list, don't try anything else.
nsTArray<nsCString> currentDictionaries;
self->GetCurrentDictionaries(currentDictionaries);
if (!currentDictionaries.IsEmpty() && tryDictList.IsEmpty()) {
#ifdef DEBUG_DICT
printf("***** Retrieved current dict |%s|\n",
DictionariesToString(currentDictionaries).Data());
#endif
self->EndUpdateDictionary();
if (fetcher->mCallback) {
fetcher->mCallback->EditorSpellCheckDone();
}
return;
}
// Priority 6:
// Try to get current dictionary from environment variable LANG.
// LANG = language[_territory][.charset]
char* env_lang = getenv("LANG");
if (env_lang) {
nsAutoCString lang(env_lang);
// Strip trailing charset, if there is any.
int32_t dot_pos = lang.FindChar('.');
if (dot_pos != -1) {
lang = Substring(lang, 0, dot_pos);
}
int32_t underScore = lang.FindChar('_');
if (underScore != -1) {
lang.Replace(underScore, 1, '-');
#ifdef DEBUG_DICT
printf("***** Trying LANG from environment |%s|\n", lang.get());
#endif
self->BuildDictionaryList(
lang, dictList, DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
}
}
// Priority 7:
// If it does not work, pick the first one.
if (!dictList.IsEmpty()) {
self->BuildDictionaryList(dictList[0], dictList, DICT_NORMAL_COMPARE,
tryDictList);
#ifdef DEBUG_DICT
printf("***** Trying first of list |%s|\n", dictList[0].get());
#endif
}
// Priority 3:
// If the document didn't supply a dictionary or the setting
// failed, try the user preference next.
if (!prefDictionaries.IsEmpty()) {
self->mSpellChecker->SetCurrentDictionaries(prefDictionaries)
->Then(
GetMainThreadSerialEventTarget(), __func__,
[self, fetcher]() { self->SetDictionarySucceeded(fetcher); },
// Priority 3 failed, we'll use the list we built of
// priorities 4 to 7.
[tryDictList = tryDictList.Clone(), self, fetcher]() {
self->mSpellChecker
->SetCurrentDictionaryFromList(tryDictList)
->Then(GetMainThreadSerialEventTarget(), __func__,
[self, fetcher]() {
self->SetDictionarySucceeded(fetcher);
});
});
} else {
// We don't have a user preference, so we'll try the list we
// built of priorities 4 to 7.
self->mSpellChecker->SetCurrentDictionaryFromList(tryDictList)
->Then(
GetMainThreadSerialEventTarget(), __func__,
[self, fetcher]() { self->SetDictionarySucceeded(fetcher); });
}
});
}
} // namespace mozilla