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.
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 };