From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- toolkit/modules/InlineSpellChecker.jsm | 593 +++++++++++++++++++++++++++++++++ 1 file changed, 593 insertions(+) create mode 100644 toolkit/modules/InlineSpellChecker.jsm (limited to 'toolkit/modules/InlineSpellChecker.jsm') diff --git a/toolkit/modules/InlineSpellChecker.jsm b/toolkit/modules/InlineSpellChecker.jsm new file mode 100644 index 000000000..de89f73a0 --- /dev/null +++ b/toolkit/modules/InlineSpellChecker.jsm @@ -0,0 +1,593 @@ +/* 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