toolkit/modules/InlineSpellChecker.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/modules/InlineSpellChecker.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,323 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +this.EXPORTED_SYMBOLS = [ "InlineSpellChecker" ];
     1.9 +var gLanguageBundle;
    1.10 +var gRegionBundle;
    1.11 +const MAX_UNDO_STACK_DEPTH = 1;
    1.12 +
    1.13 +this.InlineSpellChecker = function InlineSpellChecker(aEditor) {
    1.14 +  this.init(aEditor);
    1.15 +  this.mAddedWordStack = []; // We init this here to preserve it between init/uninit calls
    1.16 +}
    1.17 +
    1.18 +InlineSpellChecker.prototype = {
    1.19 +  // Call this function to initialize for a given editor
    1.20 +  init: function(aEditor)
    1.21 +  {
    1.22 +    this.uninit();
    1.23 +    this.mEditor = aEditor;
    1.24 +    try {
    1.25 +      this.mInlineSpellChecker = this.mEditor.getInlineSpellChecker(true);
    1.26 +      // note: this might have been NULL if there is no chance we can spellcheck
    1.27 +    } catch(e) {
    1.28 +      this.mInlineSpellChecker = null;
    1.29 +    }
    1.30 +  },
    1.31 +
    1.32 +  // call this to clear state
    1.33 +  uninit: function()
    1.34 +  {
    1.35 +    this.mEditor = null;
    1.36 +    this.mInlineSpellChecker = null;
    1.37 +    this.mOverMisspelling = false;
    1.38 +    this.mMisspelling = "";
    1.39 +    this.mMenu = null;
    1.40 +    this.mSpellSuggestions = [];
    1.41 +    this.mSuggestionItems = [];
    1.42 +    this.mDictionaryMenu = null;
    1.43 +    this.mDictionaryNames = [];
    1.44 +    this.mDictionaryItems = [];
    1.45 +    this.mWordNode = null;
    1.46 +  },
    1.47 +
    1.48 +  // for each UI event, you must call this function, it will compute the
    1.49 +  // word the cursor is over
    1.50 +  initFromEvent: function(rangeParent, rangeOffset)
    1.51 +  {
    1.52 +    this.mOverMisspelling = false;
    1.53 +
    1.54 +    if (!rangeParent || !this.mInlineSpellChecker)
    1.55 +      return;
    1.56 +
    1.57 +    var selcon = this.mEditor.selectionController;
    1.58 +    var spellsel = selcon.getSelection(selcon.SELECTION_SPELLCHECK);
    1.59 +    if (spellsel.rangeCount == 0)
    1.60 +      return; // easy case - no misspellings
    1.61 +
    1.62 +    var range = this.mInlineSpellChecker.getMisspelledWord(rangeParent,
    1.63 +                                                          rangeOffset);
    1.64 +    if (! range)
    1.65 +      return; // not over a misspelled word
    1.66 +
    1.67 +    this.mMisspelling = range.toString();
    1.68 +    this.mOverMisspelling = true;
    1.69 +    this.mWordNode = rangeParent;
    1.70 +    this.mWordOffset = rangeOffset;
    1.71 +  },
    1.72 +
    1.73 +  // returns false if there should be no spellchecking UI enabled at all, true
    1.74 +  // means that you can at least give the user the ability to turn it on.
    1.75 +  get canSpellCheck()
    1.76 +  {
    1.77 +    // inline spell checker objects will be created only if there are actual
    1.78 +    // dictionaries available
    1.79 +    return (this.mInlineSpellChecker != null);
    1.80 +  },
    1.81 +
    1.82 +  // Whether spellchecking is enabled in the current box
    1.83 +  get enabled()
    1.84 +  {
    1.85 +    return (this.mInlineSpellChecker &&
    1.86 +            this.mInlineSpellChecker.enableRealTimeSpell);
    1.87 +  },
    1.88 +  set enabled(isEnabled)
    1.89 +  {
    1.90 +    if (this.mInlineSpellChecker)
    1.91 +      this.mEditor.setSpellcheckUserOverride(isEnabled);
    1.92 +  },
    1.93 +
    1.94 +  // returns true if the given event is over a misspelled word
    1.95 +  get overMisspelling()
    1.96 +  {
    1.97 +    return this.mOverMisspelling;
    1.98 +  },
    1.99 +
   1.100 +  // this prepends up to "maxNumber" suggestions at the given menu position
   1.101 +  // for the word under the cursor. Returns the number of suggestions inserted.
   1.102 +  addSuggestionsToMenu: function(menu, insertBefore, maxNumber)
   1.103 +  {
   1.104 +    if (! this.mInlineSpellChecker || ! this.mOverMisspelling)
   1.105 +      return 0; // nothing to do
   1.106 +
   1.107 +    var spellchecker = this.mInlineSpellChecker.spellChecker;
   1.108 +    try {
   1.109 +      if (! spellchecker.CheckCurrentWord(this.mMisspelling))
   1.110 +        return 0;  // word seems not misspelled after all (?)
   1.111 +    } catch(e) {
   1.112 +        return 0;
   1.113 +    }
   1.114 +
   1.115 +    this.mMenu = menu;
   1.116 +    this.mSpellSuggestions = [];
   1.117 +    this.mSuggestionItems = [];
   1.118 +    for (var i = 0; i < maxNumber; i ++) {
   1.119 +      var suggestion = spellchecker.GetSuggestedWord();
   1.120 +      if (! suggestion.length)
   1.121 +        break;
   1.122 +      this.mSpellSuggestions.push(suggestion);
   1.123 +
   1.124 +      var item = menu.ownerDocument.createElement("menuitem");
   1.125 +      this.mSuggestionItems.push(item);
   1.126 +      item.setAttribute("label", suggestion);
   1.127 +      item.setAttribute("value", suggestion);
   1.128 +      // this function thing is necessary to generate a callback with the
   1.129 +      // correct binding of "val" (the index in this loop).
   1.130 +      var callback = function(me, val) { return function(evt) { me.replaceMisspelling(val); } };
   1.131 +      item.addEventListener("command", callback(this, i), true);
   1.132 +      item.setAttribute("class", "spell-suggestion");
   1.133 +      menu.insertBefore(item, insertBefore);
   1.134 +    }
   1.135 +    return this.mSpellSuggestions.length;
   1.136 +  },
   1.137 +
   1.138 +  // undoes the work of addSuggestionsToMenu for the same menu
   1.139 +  // (call from popup hiding)
   1.140 +  clearSuggestionsFromMenu: function()
   1.141 +  {
   1.142 +    for (var i = 0; i < this.mSuggestionItems.length; i ++) {
   1.143 +      this.mMenu.removeChild(this.mSuggestionItems[i]);
   1.144 +    }
   1.145 +    this.mSuggestionItems = [];
   1.146 +  },
   1.147 +
   1.148 +  // returns the number of dictionary languages. If insertBefore is NULL, this
   1.149 +  // does an append to the given menu
   1.150 +  addDictionaryListToMenu: function(menu, insertBefore)
   1.151 +  {
   1.152 +    this.mDictionaryMenu = menu;
   1.153 +    this.mDictionaryNames = [];
   1.154 +    this.mDictionaryItems = [];
   1.155 +
   1.156 +    if (! this.mInlineSpellChecker || ! this.enabled)
   1.157 +      return 0;
   1.158 +    var spellchecker = this.mInlineSpellChecker.spellChecker;
   1.159 +    var o1 = {}, o2 = {};
   1.160 +    spellchecker.GetDictionaryList(o1, o2);
   1.161 +    var list = o1.value;
   1.162 +    var listcount = o2.value;
   1.163 +    var curlang = "";
   1.164 +    try {
   1.165 +        curlang = spellchecker.GetCurrentDictionary();
   1.166 +    } catch(e) {}
   1.167 +
   1.168 +    var sortedList = [];
   1.169 +    for (var i = 0; i < list.length; i ++) {
   1.170 +      sortedList.push({"id": list[i],
   1.171 +                       "label": this.getDictionaryDisplayName(list[i])});
   1.172 +    }
   1.173 +    sortedList.sort(function(a, b) {
   1.174 +      if (a.label < b.label)
   1.175 +        return -1;
   1.176 +      if (a.label > b.label)
   1.177 +        return 1;
   1.178 +      return 0;
   1.179 +    });
   1.180 +
   1.181 +    for (var i = 0; i < sortedList.length; i ++) {
   1.182 +      this.mDictionaryNames.push(sortedList[i].id);
   1.183 +      var item = menu.ownerDocument.createElement("menuitem");
   1.184 +      item.setAttribute("id", "spell-check-dictionary-" + sortedList[i].id);
   1.185 +      item.setAttribute("label", sortedList[i].label);
   1.186 +      item.setAttribute("type", "radio");
   1.187 +      this.mDictionaryItems.push(item);
   1.188 +      if (curlang == sortedList[i].id) {
   1.189 +        item.setAttribute("checked", "true");
   1.190 +      } else {
   1.191 +        var callback = function(me, val) { return function(evt) { me.selectDictionary(val); } };
   1.192 +        item.addEventListener("command", callback(this, i), true);
   1.193 +      }
   1.194 +      if (insertBefore)
   1.195 +        menu.insertBefore(item, insertBefore);
   1.196 +      else
   1.197 +        menu.appendChild(item);
   1.198 +    }
   1.199 +    return list.length;
   1.200 +  },
   1.201 +
   1.202 +  // Formats a valid BCP 47 language tag based on available localized names.
   1.203 +  getDictionaryDisplayName: function(dictionaryName) {
   1.204 +    try {
   1.205 +      // Get the display name for this dictionary.
   1.206 +      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;
   1.207 +      var [languageTag, languageSubtag, scriptSubtag, regionSubtag, variantSubtags] = dictionaryName.match(languageTagMatch);
   1.208 +    } catch(e) {
   1.209 +      // If we weren't given a valid language tag, just use the raw dictionary name.
   1.210 +      return dictionaryName;
   1.211 +    }
   1.212 +
   1.213 +    if (!gLanguageBundle) {
   1.214 +      // Create the bundles for language and region names.
   1.215 +      var bundleService = Components.classes["@mozilla.org/intl/stringbundle;1"]
   1.216 +                                    .getService(Components.interfaces.nsIStringBundleService);
   1.217 +      gLanguageBundle = bundleService.createBundle(
   1.218 +          "chrome://global/locale/languageNames.properties");
   1.219 +      gRegionBundle = bundleService.createBundle(
   1.220 +          "chrome://global/locale/regionNames.properties");
   1.221 +    }
   1.222 +
   1.223 +    var displayName = "";
   1.224 +
   1.225 +    // Language subtag will normally be 2 or 3 letters, but could be up to 8.
   1.226 +    try {
   1.227 +      displayName += gLanguageBundle.GetStringFromName(languageSubtag.toLowerCase());
   1.228 +    } catch(e) {
   1.229 +      displayName += languageSubtag.toLowerCase(); // Fall back to raw language subtag.
   1.230 +    }
   1.231 +
   1.232 +    // Region subtag will be 2 letters or 3 digits.
   1.233 +    if (regionSubtag) {
   1.234 +      displayName += " (";
   1.235 +
   1.236 +      try {
   1.237 +        displayName += gRegionBundle.GetStringFromName(regionSubtag.toLowerCase());
   1.238 +      } catch(e) {
   1.239 +        displayName += regionSubtag.toUpperCase(); // Fall back to raw region subtag.
   1.240 +      }
   1.241 +
   1.242 +      displayName += ")";
   1.243 +    }
   1.244 +
   1.245 +    // Script subtag will be 4 letters.
   1.246 +    if (scriptSubtag) {
   1.247 +      displayName += " / ";
   1.248 +
   1.249 +      // XXX: See bug 666662 and bug 666731 for full implementation.
   1.250 +      displayName += scriptSubtag; // Fall back to raw script subtag.
   1.251 +    }
   1.252 +
   1.253 +    // Each variant subtag will be 4 to 8 chars.
   1.254 +    if (variantSubtags)
   1.255 +      // XXX: See bug 666662 and bug 666731 for full implementation.
   1.256 +      displayName += " (" + variantSubtags.substr(1).split(/[-_]/).join(" / ") + ")"; // Collapse multiple variants.
   1.257 +
   1.258 +    return displayName;
   1.259 +  },
   1.260 +
   1.261 +  // undoes the work of addDictionaryListToMenu for the menu
   1.262 +  // (call on popup hiding)
   1.263 +  clearDictionaryListFromMenu: function()
   1.264 +  {
   1.265 +    for (var i = 0; i < this.mDictionaryItems.length; i ++) {
   1.266 +      this.mDictionaryMenu.removeChild(this.mDictionaryItems[i]);
   1.267 +    }
   1.268 +    this.mDictionaryItems = [];
   1.269 +  },
   1.270 +
   1.271 +  // callback for selecting a dictionary
   1.272 +  selectDictionary: function(index)
   1.273 +  {
   1.274 +    if (! this.mInlineSpellChecker || index < 0 || index >= this.mDictionaryNames.length)
   1.275 +      return;
   1.276 +    var spellchecker = this.mInlineSpellChecker.spellChecker;
   1.277 +    spellchecker.SetCurrentDictionary(this.mDictionaryNames[index]);
   1.278 +    this.mInlineSpellChecker.spellCheckRange(null); // causes recheck
   1.279 +  },
   1.280 +
   1.281 +  // callback for selecting a suggesteed replacement
   1.282 +  replaceMisspelling: function(index)
   1.283 +  {
   1.284 +    if (! this.mInlineSpellChecker || ! this.mOverMisspelling)
   1.285 +      return;
   1.286 +    if (index < 0 || index >= this.mSpellSuggestions.length)
   1.287 +      return;
   1.288 +    this.mInlineSpellChecker.replaceWord(this.mWordNode, this.mWordOffset,
   1.289 +                                         this.mSpellSuggestions[index]);
   1.290 +  },
   1.291 +
   1.292 +  // callback for enabling or disabling spellchecking
   1.293 +  toggleEnabled: function()
   1.294 +  {
   1.295 +    this.mEditor.setSpellcheckUserOverride(!this.mInlineSpellChecker.enableRealTimeSpell);
   1.296 +  },
   1.297 +
   1.298 +  // callback for adding the current misspelling to the user-defined dictionary
   1.299 +  addToDictionary: function()
   1.300 +  {
   1.301 +    // Prevent the undo stack from growing over the max depth
   1.302 +    if (this.mAddedWordStack.length == MAX_UNDO_STACK_DEPTH)
   1.303 +      this.mAddedWordStack.shift();
   1.304 +
   1.305 +    this.mAddedWordStack.push(this.mMisspelling);
   1.306 +    this.mInlineSpellChecker.addWordToDictionary(this.mMisspelling);
   1.307 +  },
   1.308 +  // callback for removing the last added word to the dictionary LIFO fashion
   1.309 +  undoAddToDictionary: function()
   1.310 +  {
   1.311 +    if (this.mAddedWordStack.length > 0)
   1.312 +    {
   1.313 +      var word = this.mAddedWordStack.pop();
   1.314 +      this.mInlineSpellChecker.removeWordFromDictionary(word);
   1.315 +    }
   1.316 +  },
   1.317 +  canUndo : function()
   1.318 +  {
   1.319 +    // Return true if we have words on the stack
   1.320 +    return (this.mAddedWordStack.length > 0);
   1.321 +  },
   1.322 +  ignoreWord: function()
   1.323 +  {
   1.324 +    this.mInlineSpellChecker.ignoreWord(this.mMisspelling);
   1.325 +  }
   1.326 +};

mercurial