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.

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

mercurial