/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ this.EXPORTED_SYMBOLS = [ "InlineSpellChecker", "SpellCheckHelper" ]; var gLanguageBundle; var gRegionBundle; const MAX_UNDO_STACK_DEPTH = 1; const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; this.InlineSpellChecker = function InlineSpellChecker(aEditor) { this.init(aEditor); this.mAddedWordStack = []; // We init this here to preserve it between init/uninit calls } InlineSpellChecker.prototype = { // Call this function to initialize for a given editor init: function(aEditor) { this.uninit(); this.mEditor = aEditor; try { this.mInlineSpellChecker = this.mEditor.getInlineSpellChecker(true); // note: this might have been NULL if there is no chance we can spellcheck } catch(e) { this.mInlineSpellChecker = null; } }, initFromRemote: function(aSpellInfo) { if (this.mRemote) throw new Error("Unexpected state"); this.uninit(); if (!aSpellInfo) return; this.mInlineSpellChecker = this.mRemote = new RemoteSpellChecker(aSpellInfo); this.mOverMisspelling = aSpellInfo.overMisspelling; this.mMisspelling = aSpellInfo.misspelling; }, // call this to clear state uninit: function() { if (this.mRemote) { this.mRemote.uninit(); this.mRemote = null; } this.mEditor = null; this.mInlineSpellChecker = null; this.mOverMisspelling = false; this.mMisspelling = ""; this.mMenu = null; this.mSpellSuggestions = []; this.mSuggestionItems = []; this.mDictionaryMenu = null; this.mDictionaryNames = []; this.mDictionaryItems = []; this.mWordNode = null; }, // for each UI event, you must call this function, it will compute the // word the cursor is over initFromEvent: function(rangeParent, rangeOffset) { this.mOverMisspelling = false; if (!rangeParent || !this.mInlineSpellChecker) return; var selcon = this.mEditor.selectionController; var spellsel = selcon.getSelection(selcon.SELECTION_SPELLCHECK); if (spellsel.rangeCount == 0) return; // easy case - no misspellings var range = this.mInlineSpellChecker.getMisspelledWord(rangeParent, rangeOffset); if (! range) return; // not over a misspelled word this.mMisspelling = range.toString(); this.mOverMisspelling = true; this.mWordNode = rangeParent; this.mWordOffset = rangeOffset; }, // returns false if there should be no spellchecking UI enabled at all, true // means that you can at least give the user the ability to turn it on. get canSpellCheck() { // inline spell checker objects will be created only if there are actual // dictionaries available if (this.mRemote) return this.mRemote.canSpellCheck; return this.mInlineSpellChecker != null; }, get initialSpellCheckPending() { if (this.mRemote) { return this.mRemote.spellCheckPending; } return !!(this.mInlineSpellChecker && !this.mInlineSpellChecker.spellChecker && this.mInlineSpellChecker.spellCheckPending); }, // Whether spellchecking is enabled in the current box get enabled() { if (this.mRemote) return this.mRemote.enableRealTimeSpell; return (this.mInlineSpellChecker && this.mInlineSpellChecker.enableRealTimeSpell); }, set enabled(isEnabled) { if (this.mRemote) this.mRemote.setSpellcheckUserOverride(isEnabled); else if (this.mInlineSpellChecker) this.mEditor.setSpellcheckUserOverride(isEnabled); }, // returns true if the given event is over a misspelled word get overMisspelling() { return this.mOverMisspelling; }, // this prepends up to "maxNumber" suggestions at the given menu position // for the word under the cursor. Returns the number of suggestions inserted. addSuggestionsToMenu: function(menu, insertBefore, maxNumber) { if (!this.mRemote && (!this.mInlineSpellChecker || !this.mOverMisspelling)) return 0; // nothing to do var spellchecker = this.mRemote || this.mInlineSpellChecker.spellChecker; try { if (!this.mRemote && !spellchecker.CheckCurrentWord(this.mMisspelling)) return 0; // word seems not misspelled after all (?) } catch(e) { return 0; } this.mMenu = menu; this.mSpellSuggestions = []; this.mSuggestionItems = []; for (var i = 0; i < maxNumber; i ++) { var suggestion = spellchecker.GetSuggestedWord(); if (! suggestion.length) break; this.mSpellSuggestions.push(suggestion); var item = menu.ownerDocument.createElement("menuitem"); this.mSuggestionItems.push(item); item.setAttribute("label", suggestion); item.setAttribute("value", suggestion); // this function thing is necessary to generate a callback with the // correct binding of "val" (the index in this loop). var callback = function(me, val) { return function(evt) { me.replaceMisspelling(val); } }; item.addEventListener("command", callback(this, i), true); item.setAttribute("class", "spell-suggestion"); menu.insertBefore(item, insertBefore); } return this.mSpellSuggestions.length; }, // undoes the work of addSuggestionsToMenu for the same menu // (call from popup hiding) clearSuggestionsFromMenu: function() { for (var i = 0; i < this.mSuggestionItems.length; i ++) { this.mMenu.removeChild(this.mSuggestionItems[i]); } this.mSuggestionItems = []; }, sortDictionaryList: function(list) { var sortedList = []; for (var i = 0; i < list.length; i ++) { sortedList.push({"id": list[i], "label": this.getDictionaryDisplayName(list[i])}); } sortedList.sort(function(a, b) { if (a.label < b.label) return -1; if (a.label > b.label) return 1; return 0; }); return sortedList; }, // returns the number of dictionary languages. If insertBefore is NULL, this // does an append to the given menu addDictionaryListToMenu: function(menu, insertBefore) { this.mDictionaryMenu = menu; this.mDictionaryNames = []; this.mDictionaryItems = []; if (!this.enabled) return 0; var list; var curlang = ""; if (this.mRemote) { list = this.mRemote.dictionaryList; curlang = this.mRemote.currentDictionary; } else if (this.mInlineSpellChecker) { var spellchecker = this.mInlineSpellChecker.spellChecker; var o1 = {}, o2 = {}; spellchecker.GetDictionaryList(o1, o2); list = o1.value; var listcount = o2.value; try { curlang = spellchecker.GetCurrentDictionary(); } catch(e) {} } var sortedList = this.sortDictionaryList(list); for (var i = 0; i < sortedList.length; i ++) { this.mDictionaryNames.push(sortedList[i].id); var item = menu.ownerDocument.createElement("menuitem"); item.setAttribute("id", "spell-check-dictionary-" + sortedList[i].id); item.setAttribute("label", sortedList[i].label); item.setAttribute("type", "radio"); this.mDictionaryItems.push(item); if (curlang == sortedList[i].id) { item.setAttribute("checked", "true"); } else { var callback = function(me, val, dictName) { return function(evt) { me.selectDictionary(val); // Notify change of dictionary, especially for Thunderbird, // which is otherwise not notified any more. var view = menu.ownerDocument.defaultView; var spellcheckChangeEvent = new view.CustomEvent( "spellcheck-changed", {detail: { dictionary: dictName}}); menu.ownerDocument.dispatchEvent(spellcheckChangeEvent); } }; item.addEventListener ("command", callback(this, i, sortedList[i].id), true); } if (insertBefore) menu.insertBefore(item, insertBefore); else menu.appendChild(item); } return list.length; }, // Formats a valid BCP 47 language tag based on available localized names. getDictionaryDisplayName: function(dictionaryName) { try { // Get the display name for this dictionary. let languageTagMatch = /^([a-z]{2,3}|[a-z]{4}|[a-z]{5,8})(?:[-_]([a-z]{4}))?(?:[-_]([A-Z]{2}|[0-9]{3}))?((?:[-_](?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*)(?:[-_][a-wy-z0-9](?:[-_][a-z0-9]{2,8})+)*(?:[-_]x(?:[-_][a-z0-9]{1,8})+)?$/i; var [languageTag, languageSubtag, scriptSubtag, regionSubtag, variantSubtags] = dictionaryName.match(languageTagMatch); } catch(e) { // If we weren't given a valid language tag, just use the raw dictionary name. return dictionaryName; } if (!gLanguageBundle) { // Create the bundles for language and region names. var bundleService = Components.classes["@mozilla.org/intl/stringbundle;1"] .getService(Components.interfaces.nsIStringBundleService); gLanguageBundle = bundleService.createBundle( "chrome://global/locale/languageNames.properties"); gRegionBundle = bundleService.createBundle( "chrome://global/locale/regionNames.properties"); } var displayName = ""; // Language subtag will normally be 2 or 3 letters, but could be up to 8. try { displayName += gLanguageBundle.GetStringFromName(languageSubtag.toLowerCase()); } catch(e) { displayName += languageSubtag.toLowerCase(); // Fall back to raw language subtag. } // Region subtag will be 2 letters or 3 digits. if (regionSubtag) { displayName += " ("; try { displayName += gRegionBundle.GetStringFromName(regionSubtag.toLowerCase()); } catch(e) { displayName += regionSubtag.toUpperCase(); // Fall back to raw region subtag. } displayName += ")"; } // Script subtag will be 4 letters. if (scriptSubtag) { displayName += " / "; // XXX: See bug 666662 and bug 666731 for full implementation. displayName += scriptSubtag; // Fall back to raw script subtag. } // Each variant subtag will be 4 to 8 chars. if (variantSubtags) // XXX: See bug 666662 and bug 666731 for full implementation. displayName += " (" + variantSubtags.substr(1).split(/[-_]/).join(" / ") + ")"; // Collapse multiple variants. return displayName; }, // undoes the work of addDictionaryListToMenu for the menu // (call on popup hiding) clearDictionaryListFromMenu: function() { for (var i = 0; i < this.mDictionaryItems.length; i ++) { this.mDictionaryMenu.removeChild(this.mDictionaryItems[i]); } this.mDictionaryItems = []; }, // callback for selecting a dictionary selectDictionary: function(index) { if (this.mRemote) { this.mRemote.selectDictionary(index); return; } if (! this.mInlineSpellChecker || index < 0 || index >= this.mDictionaryNames.length) return; var spellchecker = this.mInlineSpellChecker.spellChecker; spellchecker.SetCurrentDictionary(this.mDictionaryNames[index]); this.mInlineSpellChecker.spellCheckRange(null); // causes recheck }, // callback for selecting a suggested replacement replaceMisspelling: function(index) { if (this.mRemote) { this.mRemote.replaceMisspelling(index); return; } if (! this.mInlineSpellChecker || ! this.mOverMisspelling) return; if (index < 0 || index >= this.mSpellSuggestions.length) return; this.mInlineSpellChecker.replaceWord(this.mWordNode, this.mWordOffset, this.mSpellSuggestions[index]); }, // callback for enabling or disabling spellchecking toggleEnabled: function() { if (this.mRemote) this.mRemote.toggleEnabled(); else this.mEditor.setSpellcheckUserOverride(!this.mInlineSpellChecker.enableRealTimeSpell); }, // callback for adding the current misspelling to the user-defined dictionary addToDictionary: function() { // Prevent the undo stack from growing over the max depth if (this.mAddedWordStack.length == MAX_UNDO_STACK_DEPTH) this.mAddedWordStack.shift(); this.mAddedWordStack.push(this.mMisspelling); if (this.mRemote) this.mRemote.addToDictionary(); else { this.mInlineSpellChecker.addWordToDictionary(this.mMisspelling); } }, // callback for removing the last added word to the dictionary LIFO fashion undoAddToDictionary: function() { if (this.mAddedWordStack.length > 0) { var word = this.mAddedWordStack.pop(); if (this.mRemote) this.mRemote.undoAddToDictionary(word); else this.mInlineSpellChecker.removeWordFromDictionary(word); } }, canUndo : function() { // Return true if we have words on the stack return (this.mAddedWordStack.length > 0); }, ignoreWord: function() { if (this.mRemote) this.mRemote.ignoreWord(); else this.mInlineSpellChecker.ignoreWord(this.mMisspelling); } }; var SpellCheckHelper = { // Set when over a non-read-only