toolkit/modules/InlineSpellChecker.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 this.EXPORTED_SYMBOLS = [ "InlineSpellChecker" ];
michael@0 6 var gLanguageBundle;
michael@0 7 var gRegionBundle;
michael@0 8 const MAX_UNDO_STACK_DEPTH = 1;
michael@0 9
michael@0 10 this.InlineSpellChecker = function InlineSpellChecker(aEditor) {
michael@0 11 this.init(aEditor);
michael@0 12 this.mAddedWordStack = []; // We init this here to preserve it between init/uninit calls
michael@0 13 }
michael@0 14
michael@0 15 InlineSpellChecker.prototype = {
michael@0 16 // Call this function to initialize for a given editor
michael@0 17 init: function(aEditor)
michael@0 18 {
michael@0 19 this.uninit();
michael@0 20 this.mEditor = aEditor;
michael@0 21 try {
michael@0 22 this.mInlineSpellChecker = this.mEditor.getInlineSpellChecker(true);
michael@0 23 // note: this might have been NULL if there is no chance we can spellcheck
michael@0 24 } catch(e) {
michael@0 25 this.mInlineSpellChecker = null;
michael@0 26 }
michael@0 27 },
michael@0 28
michael@0 29 // call this to clear state
michael@0 30 uninit: function()
michael@0 31 {
michael@0 32 this.mEditor = null;
michael@0 33 this.mInlineSpellChecker = null;
michael@0 34 this.mOverMisspelling = false;
michael@0 35 this.mMisspelling = "";
michael@0 36 this.mMenu = null;
michael@0 37 this.mSpellSuggestions = [];
michael@0 38 this.mSuggestionItems = [];
michael@0 39 this.mDictionaryMenu = null;
michael@0 40 this.mDictionaryNames = [];
michael@0 41 this.mDictionaryItems = [];
michael@0 42 this.mWordNode = null;
michael@0 43 },
michael@0 44
michael@0 45 // for each UI event, you must call this function, it will compute the
michael@0 46 // word the cursor is over
michael@0 47 initFromEvent: function(rangeParent, rangeOffset)
michael@0 48 {
michael@0 49 this.mOverMisspelling = false;
michael@0 50
michael@0 51 if (!rangeParent || !this.mInlineSpellChecker)
michael@0 52 return;
michael@0 53
michael@0 54 var selcon = this.mEditor.selectionController;
michael@0 55 var spellsel = selcon.getSelection(selcon.SELECTION_SPELLCHECK);
michael@0 56 if (spellsel.rangeCount == 0)
michael@0 57 return; // easy case - no misspellings
michael@0 58
michael@0 59 var range = this.mInlineSpellChecker.getMisspelledWord(rangeParent,
michael@0 60 rangeOffset);
michael@0 61 if (! range)
michael@0 62 return; // not over a misspelled word
michael@0 63
michael@0 64 this.mMisspelling = range.toString();
michael@0 65 this.mOverMisspelling = true;
michael@0 66 this.mWordNode = rangeParent;
michael@0 67 this.mWordOffset = rangeOffset;
michael@0 68 },
michael@0 69
michael@0 70 // returns false if there should be no spellchecking UI enabled at all, true
michael@0 71 // means that you can at least give the user the ability to turn it on.
michael@0 72 get canSpellCheck()
michael@0 73 {
michael@0 74 // inline spell checker objects will be created only if there are actual
michael@0 75 // dictionaries available
michael@0 76 return (this.mInlineSpellChecker != null);
michael@0 77 },
michael@0 78
michael@0 79 // Whether spellchecking is enabled in the current box
michael@0 80 get enabled()
michael@0 81 {
michael@0 82 return (this.mInlineSpellChecker &&
michael@0 83 this.mInlineSpellChecker.enableRealTimeSpell);
michael@0 84 },
michael@0 85 set enabled(isEnabled)
michael@0 86 {
michael@0 87 if (this.mInlineSpellChecker)
michael@0 88 this.mEditor.setSpellcheckUserOverride(isEnabled);
michael@0 89 },
michael@0 90
michael@0 91 // returns true if the given event is over a misspelled word
michael@0 92 get overMisspelling()
michael@0 93 {
michael@0 94 return this.mOverMisspelling;
michael@0 95 },
michael@0 96
michael@0 97 // this prepends up to "maxNumber" suggestions at the given menu position
michael@0 98 // for the word under the cursor. Returns the number of suggestions inserted.
michael@0 99 addSuggestionsToMenu: function(menu, insertBefore, maxNumber)
michael@0 100 {
michael@0 101 if (! this.mInlineSpellChecker || ! this.mOverMisspelling)
michael@0 102 return 0; // nothing to do
michael@0 103
michael@0 104 var spellchecker = this.mInlineSpellChecker.spellChecker;
michael@0 105 try {
michael@0 106 if (! spellchecker.CheckCurrentWord(this.mMisspelling))
michael@0 107 return 0; // word seems not misspelled after all (?)
michael@0 108 } catch(e) {
michael@0 109 return 0;
michael@0 110 }
michael@0 111
michael@0 112 this.mMenu = menu;
michael@0 113 this.mSpellSuggestions = [];
michael@0 114 this.mSuggestionItems = [];
michael@0 115 for (var i = 0; i < maxNumber; i ++) {
michael@0 116 var suggestion = spellchecker.GetSuggestedWord();
michael@0 117 if (! suggestion.length)
michael@0 118 break;
michael@0 119 this.mSpellSuggestions.push(suggestion);
michael@0 120
michael@0 121 var item = menu.ownerDocument.createElement("menuitem");
michael@0 122 this.mSuggestionItems.push(item);
michael@0 123 item.setAttribute("label", suggestion);
michael@0 124 item.setAttribute("value", suggestion);
michael@0 125 // this function thing is necessary to generate a callback with the
michael@0 126 // correct binding of "val" (the index in this loop).
michael@0 127 var callback = function(me, val) { return function(evt) { me.replaceMisspelling(val); } };
michael@0 128 item.addEventListener("command", callback(this, i), true);
michael@0 129 item.setAttribute("class", "spell-suggestion");
michael@0 130 menu.insertBefore(item, insertBefore);
michael@0 131 }
michael@0 132 return this.mSpellSuggestions.length;
michael@0 133 },
michael@0 134
michael@0 135 // undoes the work of addSuggestionsToMenu for the same menu
michael@0 136 // (call from popup hiding)
michael@0 137 clearSuggestionsFromMenu: function()
michael@0 138 {
michael@0 139 for (var i = 0; i < this.mSuggestionItems.length; i ++) {
michael@0 140 this.mMenu.removeChild(this.mSuggestionItems[i]);
michael@0 141 }
michael@0 142 this.mSuggestionItems = [];
michael@0 143 },
michael@0 144
michael@0 145 // returns the number of dictionary languages. If insertBefore is NULL, this
michael@0 146 // does an append to the given menu
michael@0 147 addDictionaryListToMenu: function(menu, insertBefore)
michael@0 148 {
michael@0 149 this.mDictionaryMenu = menu;
michael@0 150 this.mDictionaryNames = [];
michael@0 151 this.mDictionaryItems = [];
michael@0 152
michael@0 153 if (! this.mInlineSpellChecker || ! this.enabled)
michael@0 154 return 0;
michael@0 155 var spellchecker = this.mInlineSpellChecker.spellChecker;
michael@0 156 var o1 = {}, o2 = {};
michael@0 157 spellchecker.GetDictionaryList(o1, o2);
michael@0 158 var list = o1.value;
michael@0 159 var listcount = o2.value;
michael@0 160 var curlang = "";
michael@0 161 try {
michael@0 162 curlang = spellchecker.GetCurrentDictionary();
michael@0 163 } catch(e) {}
michael@0 164
michael@0 165 var sortedList = [];
michael@0 166 for (var i = 0; i < list.length; i ++) {
michael@0 167 sortedList.push({"id": list[i],
michael@0 168 "label": this.getDictionaryDisplayName(list[i])});
michael@0 169 }
michael@0 170 sortedList.sort(function(a, b) {
michael@0 171 if (a.label < b.label)
michael@0 172 return -1;
michael@0 173 if (a.label > b.label)
michael@0 174 return 1;
michael@0 175 return 0;
michael@0 176 });
michael@0 177
michael@0 178 for (var i = 0; i < sortedList.length; i ++) {
michael@0 179 this.mDictionaryNames.push(sortedList[i].id);
michael@0 180 var item = menu.ownerDocument.createElement("menuitem");
michael@0 181 item.setAttribute("id", "spell-check-dictionary-" + sortedList[i].id);
michael@0 182 item.setAttribute("label", sortedList[i].label);
michael@0 183 item.setAttribute("type", "radio");
michael@0 184 this.mDictionaryItems.push(item);
michael@0 185 if (curlang == sortedList[i].id) {
michael@0 186 item.setAttribute("checked", "true");
michael@0 187 } else {
michael@0 188 var callback = function(me, val) { return function(evt) { me.selectDictionary(val); } };
michael@0 189 item.addEventListener("command", callback(this, i), true);
michael@0 190 }
michael@0 191 if (insertBefore)
michael@0 192 menu.insertBefore(item, insertBefore);
michael@0 193 else
michael@0 194 menu.appendChild(item);
michael@0 195 }
michael@0 196 return list.length;
michael@0 197 },
michael@0 198
michael@0 199 // Formats a valid BCP 47 language tag based on available localized names.
michael@0 200 getDictionaryDisplayName: function(dictionaryName) {
michael@0 201 try {
michael@0 202 // Get the display name for this dictionary.
michael@0 203 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;
michael@0 204 var [languageTag, languageSubtag, scriptSubtag, regionSubtag, variantSubtags] = dictionaryName.match(languageTagMatch);
michael@0 205 } catch(e) {
michael@0 206 // If we weren't given a valid language tag, just use the raw dictionary name.
michael@0 207 return dictionaryName;
michael@0 208 }
michael@0 209
michael@0 210 if (!gLanguageBundle) {
michael@0 211 // Create the bundles for language and region names.
michael@0 212 var bundleService = Components.classes["@mozilla.org/intl/stringbundle;1"]
michael@0 213 .getService(Components.interfaces.nsIStringBundleService);
michael@0 214 gLanguageBundle = bundleService.createBundle(
michael@0 215 "chrome://global/locale/languageNames.properties");
michael@0 216 gRegionBundle = bundleService.createBundle(
michael@0 217 "chrome://global/locale/regionNames.properties");
michael@0 218 }
michael@0 219
michael@0 220 var displayName = "";
michael@0 221
michael@0 222 // Language subtag will normally be 2 or 3 letters, but could be up to 8.
michael@0 223 try {
michael@0 224 displayName += gLanguageBundle.GetStringFromName(languageSubtag.toLowerCase());
michael@0 225 } catch(e) {
michael@0 226 displayName += languageSubtag.toLowerCase(); // Fall back to raw language subtag.
michael@0 227 }
michael@0 228
michael@0 229 // Region subtag will be 2 letters or 3 digits.
michael@0 230 if (regionSubtag) {
michael@0 231 displayName += " (";
michael@0 232
michael@0 233 try {
michael@0 234 displayName += gRegionBundle.GetStringFromName(regionSubtag.toLowerCase());
michael@0 235 } catch(e) {
michael@0 236 displayName += regionSubtag.toUpperCase(); // Fall back to raw region subtag.
michael@0 237 }
michael@0 238
michael@0 239 displayName += ")";
michael@0 240 }
michael@0 241
michael@0 242 // Script subtag will be 4 letters.
michael@0 243 if (scriptSubtag) {
michael@0 244 displayName += " / ";
michael@0 245
michael@0 246 // XXX: See bug 666662 and bug 666731 for full implementation.
michael@0 247 displayName += scriptSubtag; // Fall back to raw script subtag.
michael@0 248 }
michael@0 249
michael@0 250 // Each variant subtag will be 4 to 8 chars.
michael@0 251 if (variantSubtags)
michael@0 252 // XXX: See bug 666662 and bug 666731 for full implementation.
michael@0 253 displayName += " (" + variantSubtags.substr(1).split(/[-_]/).join(" / ") + ")"; // Collapse multiple variants.
michael@0 254
michael@0 255 return displayName;
michael@0 256 },
michael@0 257
michael@0 258 // undoes the work of addDictionaryListToMenu for the menu
michael@0 259 // (call on popup hiding)
michael@0 260 clearDictionaryListFromMenu: function()
michael@0 261 {
michael@0 262 for (var i = 0; i < this.mDictionaryItems.length; i ++) {
michael@0 263 this.mDictionaryMenu.removeChild(this.mDictionaryItems[i]);
michael@0 264 }
michael@0 265 this.mDictionaryItems = [];
michael@0 266 },
michael@0 267
michael@0 268 // callback for selecting a dictionary
michael@0 269 selectDictionary: function(index)
michael@0 270 {
michael@0 271 if (! this.mInlineSpellChecker || index < 0 || index >= this.mDictionaryNames.length)
michael@0 272 return;
michael@0 273 var spellchecker = this.mInlineSpellChecker.spellChecker;
michael@0 274 spellchecker.SetCurrentDictionary(this.mDictionaryNames[index]);
michael@0 275 this.mInlineSpellChecker.spellCheckRange(null); // causes recheck
michael@0 276 },
michael@0 277
michael@0 278 // callback for selecting a suggesteed replacement
michael@0 279 replaceMisspelling: function(index)
michael@0 280 {
michael@0 281 if (! this.mInlineSpellChecker || ! this.mOverMisspelling)
michael@0 282 return;
michael@0 283 if (index < 0 || index >= this.mSpellSuggestions.length)
michael@0 284 return;
michael@0 285 this.mInlineSpellChecker.replaceWord(this.mWordNode, this.mWordOffset,
michael@0 286 this.mSpellSuggestions[index]);
michael@0 287 },
michael@0 288
michael@0 289 // callback for enabling or disabling spellchecking
michael@0 290 toggleEnabled: function()
michael@0 291 {
michael@0 292 this.mEditor.setSpellcheckUserOverride(!this.mInlineSpellChecker.enableRealTimeSpell);
michael@0 293 },
michael@0 294
michael@0 295 // callback for adding the current misspelling to the user-defined dictionary
michael@0 296 addToDictionary: function()
michael@0 297 {
michael@0 298 // Prevent the undo stack from growing over the max depth
michael@0 299 if (this.mAddedWordStack.length == MAX_UNDO_STACK_DEPTH)
michael@0 300 this.mAddedWordStack.shift();
michael@0 301
michael@0 302 this.mAddedWordStack.push(this.mMisspelling);
michael@0 303 this.mInlineSpellChecker.addWordToDictionary(this.mMisspelling);
michael@0 304 },
michael@0 305 // callback for removing the last added word to the dictionary LIFO fashion
michael@0 306 undoAddToDictionary: function()
michael@0 307 {
michael@0 308 if (this.mAddedWordStack.length > 0)
michael@0 309 {
michael@0 310 var word = this.mAddedWordStack.pop();
michael@0 311 this.mInlineSpellChecker.removeWordFromDictionary(word);
michael@0 312 }
michael@0 313 },
michael@0 314 canUndo : function()
michael@0 315 {
michael@0 316 // Return true if we have words on the stack
michael@0 317 return (this.mAddedWordStack.length > 0);
michael@0 318 },
michael@0 319 ignoreWord: function()
michael@0 320 {
michael@0 321 this.mInlineSpellChecker.ignoreWord(this.mMisspelling);
michael@0 322 }
michael@0 323 };

mercurial