Wed, 31 Dec 2014 06:09:35 +0100
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 | }; |