/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla Spellchecker Component. * * The Initial Developer of the Original Code is David Einstein. * Portions created by the Initial Developer are Copyright (C) 2001 * the Initial Developer. All Rights Reserved. * * Contributor(s): David Einstein Deinst@world.std.com * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include "mozSpellChecker.h" #include "nsIServiceManager.h" #include "mozISpellI18NManager.h" #include "nsIStringEnumerator.h" #include "nsICategoryManager.h" #include "nsISupportsPrimitives.h" #define UNREASONABLE_WORD_LENGTH 64 #define DEFAULT_SPELL_CHECKER "@mozilla.org/spellchecker/engine;1" NS_IMPL_ISUPPORTS1(mozSpellChecker, nsISpellChecker) mozSpellChecker::mozSpellChecker() { } mozSpellChecker::~mozSpellChecker() { if(mPersonalDictionary){ // mPersonalDictionary->Save(); mPersonalDictionary->EndSession(); } mSpellCheckingEngine = nsnull; mPersonalDictionary = nsnull; } nsresult mozSpellChecker::Init() { mPersonalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1"); mSpellCheckingEngine = nsnull; mCurrentEngineContractId = nsnull; mDictionariesMap.Init(); InitSpellCheckDictionaryMap(); return NS_OK; } NS_IMETHODIMP mozSpellChecker::SetDocument(nsITextServicesDocument *aDoc, PRBool aFromStartofDoc) { mTsDoc = aDoc; mFromStart = aFromStartofDoc; return NS_OK; } NS_IMETHODIMP mozSpellChecker::NextMisspelledWord(nsAString &aWord, nsStringArray *aSuggestions) { if(!aSuggestions||!mConverter) return NS_ERROR_NULL_POINTER; PRUint32 selOffset; PRInt32 begin,end; nsresult result; result = SetupDoc(&selOffset); PRBool isMisspelled,done; if (NS_FAILED(result)) return result; while( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done ) { nsString str; result = mTsDoc->GetCurrentTextBlock(&str); if (NS_FAILED(result)) return result; do{ result = mConverter->FindNextWord(str.get(),str.Length(),selOffset,&begin,&end); if(NS_SUCCEEDED(result)&&(begin != -1)){ const nsAString &currWord = Substring(str, begin, end - begin); result = CheckWord(currWord, &isMisspelled, aSuggestions); if(isMisspelled){ aWord = currWord; mTsDoc->SetSelection(begin, end-begin); // After ScrollSelectionIntoView(), the pending notifications might // be flushed and PresShell/PresContext/Frames may be dead. // See bug 418470. mTsDoc->ScrollSelectionIntoView(); return NS_OK; } } selOffset = end; }while(end != -1); mTsDoc->NextBlock(); selOffset=0; } return NS_OK; } NS_IMETHODIMP mozSpellChecker::CheckWord(const nsAString &aWord, PRBool *aIsMisspelled, nsStringArray *aSuggestions) { nsresult result; PRBool correct; if(!mSpellCheckingEngine) return NS_ERROR_NULL_POINTER; // don't bother to check crazy words if (aWord.Length() > UNREASONABLE_WORD_LENGTH) { *aIsMisspelled = PR_TRUE; return NS_OK; } *aIsMisspelled = PR_FALSE; result = mSpellCheckingEngine->Check(PromiseFlatString(aWord).get(), &correct); NS_ENSURE_SUCCESS(result, result); if(!correct){ if(aSuggestions){ PRUint32 count,i; PRUnichar **words; result = mSpellCheckingEngine->Suggest(PromiseFlatString(aWord).get(), &words, &count); NS_ENSURE_SUCCESS(result, result); for(i=0;iAppendString(nsDependentString(words[i])); } if (count) NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, words); } if(aIsMisspelled){ *aIsMisspelled = PR_TRUE; } } return NS_OK; } NS_IMETHODIMP mozSpellChecker::Replace(const nsAString &aOldWord, const nsAString &aNewWord, PRBool aAllOccurrences) { if(!mConverter) return NS_ERROR_NULL_POINTER; nsAutoString newWord(aNewWord); // sigh if(aAllOccurrences){ PRUint32 selOffset; PRInt32 startBlock,currentBlock,currOffset; PRInt32 begin,end; PRBool done; nsresult result; nsAutoString str; // find out where we are result = SetupDoc(&selOffset); if(NS_FAILED(result)) return result; result = GetCurrentBlockIndex(mTsDoc,&startBlock); if(NS_FAILED(result)) return result; //start at the beginning result = mTsDoc->FirstBlock(); currOffset=0; currentBlock = 0; while( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done ) { result = mTsDoc->GetCurrentTextBlock(&str); do{ result = mConverter->FindNextWord(str.get(),str.Length(),currOffset,&begin,&end); if(NS_SUCCEEDED(result)&&(begin != -1)){ if (aOldWord.Equals(Substring(str, begin, end-begin))) { // if we are before the current selection point but in the same block // move the selection point forwards if((currentBlock == startBlock)&&(begin < (PRInt32) selOffset)){ selOffset += (aNewWord.Length() - aOldWord.Length()); if(selOffset < 0) selOffset=0; } mTsDoc->SetSelection(begin, end-begin); mTsDoc->InsertText(&newWord); mTsDoc->GetCurrentTextBlock(&str); end += (aNewWord.Length() - aOldWord.Length()); // recursion was cute in GEB, not here. } } currOffset = end; }while(currOffset != -1); mTsDoc->NextBlock(); currentBlock++; currOffset=0; } // We are done replacing. Put the selection point back where we found it (or equivalent); result = mTsDoc->FirstBlock(); currentBlock = 0; while(( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done ) &&(currentBlock < startBlock)){ mTsDoc->NextBlock(); } //After we have moved to the block where the first occurrence of replace was done, put the //selection to the next word following it. In case there is no word following it i.e if it happens //to be the last word in that block, then move to the next block and put the selection to the //first word in that block, otherwise when the Setupdoc() is called, it queries the LastSelectedBlock() //and the selection offset of the last occurrence of the replaced word is taken instead of the first //occurrence and things get messed up as reported in the bug 244969 if( NS_SUCCEEDED(mTsDoc->IsDone(&done)) && !done ){ nsString str; result = mTsDoc->GetCurrentTextBlock(&str); result = mConverter->FindNextWord(str.get(),str.Length(),selOffset,&begin,&end); if(end == -1) { mTsDoc->NextBlock(); selOffset=0; result = mTsDoc->GetCurrentTextBlock(&str); result = mConverter->FindNextWord(str.get(),str.Length(),selOffset,&begin,&end); mTsDoc->SetSelection(begin, 0); } else mTsDoc->SetSelection(begin, 0); } } else{ mTsDoc->InsertText(&newWord); } return NS_OK; } NS_IMETHODIMP mozSpellChecker::IgnoreAll(const nsAString &aWord) { if(mPersonalDictionary){ mPersonalDictionary->IgnoreWord(PromiseFlatString(aWord).get()); } return NS_OK; } NS_IMETHODIMP mozSpellChecker::AddWordToPersonalDictionary(const nsAString &aWord) { nsresult res; PRUnichar empty=0; if (!mPersonalDictionary) return NS_ERROR_NULL_POINTER; res = mPersonalDictionary->AddWord(PromiseFlatString(aWord).get(),&empty); return res; } NS_IMETHODIMP mozSpellChecker::RemoveWordFromPersonalDictionary(const nsAString &aWord) { nsresult res; PRUnichar empty=0; if (!mPersonalDictionary) return NS_ERROR_NULL_POINTER; res = mPersonalDictionary->RemoveWord(PromiseFlatString(aWord).get(),&empty); return res; } NS_IMETHODIMP mozSpellChecker::GetPersonalDictionary(nsStringArray *aWordList) { if(!aWordList || !mPersonalDictionary) return NS_ERROR_NULL_POINTER; nsCOMPtr words; mPersonalDictionary->GetWordList(getter_AddRefs(words)); PRBool hasMore; nsAutoString word; while (NS_SUCCEEDED(words->HasMore(&hasMore)) && hasMore) { words->GetNext(word); aWordList->AppendString(word); } return NS_OK; } struct AppendNewStruct { nsStringArray *dictionaryList; PRBool failed; }; static PLDHashOperator AppendNewString(const nsAString& aString, nsCString*, void* aClosure) { AppendNewStruct *ans = (AppendNewStruct*) aClosure; if (!ans->dictionaryList->AppendString(aString)) { ans->failed = PR_TRUE; return PL_DHASH_STOP; } return PL_DHASH_NEXT; } NS_IMETHODIMP mozSpellChecker::GetDictionaryList(nsStringArray *aDictionaryList) { AppendNewStruct ans = {aDictionaryList, PR_FALSE}; mDictionariesMap.EnumerateRead(AppendNewString, &ans); if (ans.failed) return NS_ERROR_OUT_OF_MEMORY; return NS_OK; } NS_IMETHODIMP mozSpellChecker::GetCurrentDictionary(nsAString &aDictionary) { nsXPIDLString dictname; if (!mSpellCheckingEngine) return NS_ERROR_NOT_INITIALIZED; mSpellCheckingEngine->GetDictionary(getter_Copies(dictname)); aDictionary = dictname; return NS_OK; } NS_IMETHODIMP mozSpellChecker::SetCurrentDictionary(const nsAString &aDictionary) { nsresult rv; nsCString *contractId; if (!mDictionariesMap.Get(aDictionary, &contractId)){ NS_WARNING("Dictionary not found"); return NS_ERROR_NOT_AVAILABLE; } if (!mCurrentEngineContractId || !mCurrentEngineContractId->Equals(*contractId)){ mSpellCheckingEngine = do_GetService(contractId->get(), &rv); if (NS_FAILED(rv)) return rv; mCurrentEngineContractId = contractId; } nsresult res; res = mSpellCheckingEngine->SetDictionary(PromiseFlatString(aDictionary).get()); if(NS_FAILED(res)){ NS_WARNING("Dictionary load failed"); return res; } mSpellCheckingEngine->SetPersonalDictionary(mPersonalDictionary); nsXPIDLString language; nsCOMPtr serv(do_GetService("@mozilla.org/spellchecker/i18nmanager;1", &res)); if(serv && NS_SUCCEEDED(res)){ res = serv->GetUtil(language.get(),getter_AddRefs(mConverter)); } return res; } nsresult mozSpellChecker::SetupDoc(PRUint32 *outBlockOffset) { nsresult rv; nsITextServicesDocument::TSDBlockSelectionStatus blockStatus; PRInt32 selOffset; PRInt32 selLength; *outBlockOffset = 0; if (!mFromStart) { rv = mTsDoc->LastSelectedBlock(&blockStatus, &selOffset, &selLength); if (NS_SUCCEEDED(rv) && (blockStatus != nsITextServicesDocument::eBlockNotFound)) { switch (blockStatus) { case nsITextServicesDocument::eBlockOutside: // No TB in S, but found one before/after S. case nsITextServicesDocument::eBlockPartial: // S begins or ends in TB but extends outside of TB. // the TS doc points to the block we want. *outBlockOffset = selOffset + selLength; break; case nsITextServicesDocument::eBlockInside: // S extends beyond the start and end of TB. // we want the block after this one. rv = mTsDoc->NextBlock(); *outBlockOffset = 0; break; case nsITextServicesDocument::eBlockContains: // TB contains entire S. *outBlockOffset = selOffset + selLength; break; case nsITextServicesDocument::eBlockNotFound: // There is no text block (TB) in or before the selection (S). default: NS_NOTREACHED("Shouldn't ever get this status"); } } else //failed to get last sel block. Just start at beginning { rv = mTsDoc->FirstBlock(); *outBlockOffset = 0; } } else // we want the first block { rv = mTsDoc->FirstBlock(); mFromStart = PR_FALSE; } return rv; } // utility method to discover which block we're in. The TSDoc interface doesn't give // us this, because it can't assume a read-only document. // shamelessly stolen from nsTextServicesDocument nsresult mozSpellChecker::GetCurrentBlockIndex(nsITextServicesDocument *aDoc, PRInt32 *outBlockIndex) { PRInt32 blockIndex = 0; PRBool isDone = PR_FALSE; nsresult result = NS_OK; do { aDoc->PrevBlock(); result = aDoc->IsDone(&isDone); if (!isDone) blockIndex ++; } while (NS_SUCCEEDED(result) && !isDone); *outBlockIndex = blockIndex; return result; } nsresult mozSpellChecker::InitSpellCheckDictionaryMap() { nsresult rv; PRBool hasMoreEngines; PRInt32 i; nsCStringArray contractIds; nsCOMPtr catMgr = do_GetService(NS_CATEGORYMANAGER_CONTRACTID); if (!catMgr) return NS_ERROR_NULL_POINTER; nsCOMPtr catEntries; // Get contract IDs of registrated external spell-check engines and // append one of HunSpell at the end. rv = catMgr->EnumerateCategory("spell-check-engine", getter_AddRefs(catEntries)); if (NS_FAILED(rv)) return rv; while (catEntries->HasMoreElements(&hasMoreEngines), hasMoreEngines){ nsCOMPtr elem; rv = catEntries->GetNext(getter_AddRefs(elem)); nsCOMPtr entry = do_QueryInterface(elem, &rv); if (NS_FAILED(rv)) return rv; nsCString contractId; rv = entry->GetData(contractId); if (NS_FAILED(rv)) return rv; contractIds.AppendCString(contractId); } contractIds.AppendCString(NS_LITERAL_CSTRING(DEFAULT_SPELL_CHECKER)); // Retrieve dictionaries from all available spellcheckers and // fill mDictionariesMap hash (only the first dictionary with the // each name is used). for (i=0;i engine = do_GetService(contractId->get(), &rv); if (NS_FAILED(rv)){ // Fail if not succeeded to load HunSpell. Ignore errors // for external spellcheck engines. if (i==contractIds.Count()-1){ return rv; } continue; } engine->GetDictionaryList(&words,&count); for(k=0;k