source: gs3-extensions/seaweed-debug/trunk/src/Spell.js@ 25160

Last change on this file since 25160 was 25160, checked in by sjm84, 12 years ago

Initial cut at a version of seaweed for debugging purposes. Check it out live into the web/ext folder

File size: 16.9 KB
Line 
1/*
2 * file: Spell.js
3 *
4 * @BEGINLICENSE
5 * Copyright 2010 Brook Novak (email : [email protected])
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 * @ENDLICENSE
18 */
19
20bootstrap.provides("Spell");
21
22$enqueueInit("Spell", function() {
23
24 // Setup auto-selection of marked errors
25 _addHandler(_engine == _Platform.GECKO ? window : document, "mouseup", function() {
26
27 var sel = de.selection.getRange();
28 if (sel && !sel.endNode) {
29
30 // The user clicked in an editable section... i.e. did not range a selection
31 var spellNode = de.spell.getMarkedAncestor(sel.startNode);
32 if (spellNode) {
33
34 // The user click inside inline content marked as a spelling error
35 // so select the whole word
36 de.selection.setSelection({
37 startNode:spellNode,
38 startIndex:0,
39 endNode:spellNode,
40 endIndex:1
41 });
42 }
43 }
44
45 });
46
47}, "Selection"); // add mouseup event after selection's event registration
48
49(function() {
50
51 var
52 /**
53 * The classname of the wrappers used for marking spelling mistakes
54 * @final
55 * @type String
56 */
57 SPELL_MARK_CLASS_NAME = "sw-spell-error",
58 SPELL_MARK_CLASS_NAME_RE = /^sw-spell-error$/;
59
60 /**
61 * @namespace
62 */
63 de.spell = {
64
65 /**
66 * @param {[Element]} editableSections (Optional) The editable sections to get words from.
67 * If not provided then all editable sections will be queried.
68 *
69 * @return {[String]} The <em>set</em> of words found in all the given editable sections.
70 */
71 getWords : function(editableSections) {
72
73 if (!editableSections)
74 editableSections = de.doc.getAllEditSections();
75
76 var words = [], wmap = {};
77
78 for (var i in editableSections) {
79 visitWords(editableSections[i], function(word) {
80 if (!wmap[word]) { // Unique word?
81 words.push(word);
82 wmap[word] = 1; // Track words to build a set
83 }
84 });
85 }
86
87 return words;
88
89 },
90
91 /**
92 * Scans a given set of editable sections for spelling errors, for each spelling error found
93 * it wraps them with a span with the spelling error class.
94 *
95 * Calling this may result in a undoable action executed
96 * via the undo manager (where the users can undo themselves).
97 *
98 * @param {[String]|String} errors An array of miss-spelt words - or a string of whitespace seporated words
99 *
100 * @param {[Element]} editableSections (Optional) An array of editable sections to mark errors for.
101 * If not provided then all editable sections will be queried.
102 */
103 markWords : function(errors, editableSections) {
104
105 if (!editableSections)
106 editableSections = de.doc.getAllEditSections();
107
108 // Convert error array/string into a lookup map
109 errors = $createLookupMap(typeof errors == "string" ? errors.replace(/\s/g,',') : errors.join(','));
110
111 var foundError = 0;
112
113 for (var i in editableSections) {
114 visitWords(editableSections[i], function(word, startNode, startIndex, endNode, endIndex) {
115
116 // Is this word considered an error?
117 if (errors[word]) {
118 // Wrap the word and get the fragment of the selected word.
119 // Group all the actions together as one unit.
120 var frag = de.UndoMan.execute(
121 foundError ? de.UndoMan.ExecFlag.GROUP : 0,
122 'SpellMark',
123 startNode,
124 startIndex,
125 endNode,
126 endIndex + 1);
127
128 foundError = 1;
129 return frag.children[frag.children.length-1].node; // Return the (exclusive) resume node as the last text node that made up the word
130 }
131
132
133 });
134 }
135
136 },
137
138 /**
139 * Clears all words marked with spelling errors in a given set of editable sections.
140 * This operation will add a single undoable action to the undo manager if any
141 * marked spelling errors are found.
142 *
143 * @param {[Element]} editableSections (Optional) An array of editable sections to clear marked errors from.
144 * If not provided then all editable sections will be queried.
145 *
146 * @return {Boolean} Evaluates true if some errors were unmarked (i.e. an undoable action was added
147 * to the undo managaer).
148 */
149 clearAllMarks : function(editableSections) {
150
151 if (!editableSections)
152 editableSections = de.doc.getAllEditSections();
153
154 var errorWrappers = [], i;
155
156 for (i in editableSections) {
157
158 // Find all spelling error wrappers in each editable section
159 _visitAllNodes(editableSections[i], editableSections[i], true, function(domNode) {
160 if (de.spell.isSpellErrorWrapper(domNode))
161 errorWrappers.push(domNode);
162 });
163 }
164
165 // Remove all the wrappers
166 for (i in errorWrappers) {
167 de.UndoMan.execute(i == '0' ? 0 : de.UndoMan.ExecFlag.GROUP, "SpellUnmark", errorWrappers[i]);
168 }
169
170 // If nothing was found then zero will be returned.
171 return errorWrappers.length;
172
173 },
174
175 /**
176 * Clears a specific marked spelling error.
177 * This will add an action to the undo manager.
178 *
179 * @param {Element} markNode The spelling error wrapper to "ignore" (i.e. remove)
180 */
181 ignoreError : function(markNode, execFlags) {
182 de.UndoMan.execute(execFlags ? execFlags : 0, 'SpellUnmark', markNode);
183 },
184
185 /**
186 * Replaces an error with a correction.
187 * This will add an action to the undo manager.
188 *
189 * @param {Element} markNode The spelling error wrapper to "correct" (i.e. replace)
190 * @param {String} correction The word to replace the spelling error with
191 */
192 correctError : function(markNode, correction) {
193 de.UndoMan.execute('SpellCorrect', markNode, correction);
194 },
195
196 /**
197 * @param {Node} node A dom node
198 * @return {Element} The spelling error wrapper which is either the given dom node or a ancestor of the given dom node.
199 */
200 getMarkedAncestor : function(node) {
201 return this.isSpellErrorWrapper(node) ? node : _findAncestor(node, this.isSpellErrorWrapper);
202 },
203
204 /**
205 * @param {Node} node The node to test
206 * @return {Boolean} True iff the node is a spelling error wrapper element.
207 */
208 isSpellErrorWrapper : function(node) {
209 return _findClassName(node, SPELL_MARK_CLASS_NAME_RE) ? true : false;
210 },
211
212 /**
213 * Strips spelling wrapper tags from HTML
214 *
215 * @param {String} html HTML Markup.
216 *
217 * @return {String} The given HTML with all spelling error wrappers removed.
218 */
219 stripSpellWrapperHTML : function(html) {
220
221 var re = /<span class\s*=\s*(?:"|')sw-spell-error(?:"|')\s*>([^<]+)<\/span>/i;
222
223 // TODO: Can I just use replace rather than dealing with loops all the time?
224
225 // Remove spelling wrappers via RE
226 while (match = re.exec(html)) {
227 html = html.substr(0, match.index) + match[1] + html.substr(match.index + match[0].length);
228 }
229
230 return html;
231 }
232
233
234 };
235
236 /**
237 * Scans for words in an editable section.
238 *
239 * @param {Element} editableSection The editable section to scan words for
240 *
241 * @param {Function} callback The callback function to send words to. Takes 5 args:
242 * word (string) - The word that was found
243 * startNode (Node) - The start text node of the word
244 * startIndex (Number) - The start index of the word within the start text node
245 * endNode (Node) - The end text node of the word
246 * endIndex (Number) - The end index of the word within the end text node
247 *
248 * Return the node to exclusively resume the in-order traversal from
249 */
250 function visitWords(editableSection, callback) {
251
252 var wordBreakerChars = /^[^\w']$/,
253 resumeNode = editableSection,
254 startNode,
255 startIndex,
256 endNode,
257 endIndex,
258 curWord;
259
260 while (resumeNode) {
261
262 // Continue visiting nodes within the editable section...
263 _visitAllNodes(editableSection, resumeNode, true, function(domNode) {
264
265 // Skip resume node (exclusive start point)
266 if (resumeNode == domNode)
267 resumeNode = 0; // Clear current resume node
268
269 else {
270
271 // For all text nodes within an element (e.g. not within comments, styles or scripts)...
272 if (domNode.nodeType == Node.TEXT_NODE && domNode.parentNode.nodeType == Node.ELEMENT_NODE) {
273
274 // Only allow words to extend between adjacent text nodes .. otherwise
275 // wrapping the words will become over complex and bloat the code.
276 if (endNode && domNode.previousSibling != endNode) {
277 if (checkWord())
278 return false; // NB: Will revisit this node since will exclusively resume at the previous sibling
279 }
280
281 var str = domNode.nodeValue, i;
282
283 // For each charactor in this text node's string
284 for (i = 0; i < str.length; i++) {
285
286 var c = str.charAt(i);
287
288 // Is this charactor a part of a word or a word breaker?
289 if (wordBreakerChars.test(c)) {
290
291 // If so then check for a pending word...
292 if (checkWord())
293 return false;
294
295 } else { // Found word symbol
296
297 // Set start node/index for start of new word
298 if (!startNode) {
299 startNode = domNode;
300 startIndex = i;
301 curWord = "";
302 }
303
304 // Build word
305 curWord += c;
306
307 // Track end point
308 endNode = domNode;
309 endIndex = i;
310 }
311
312 } // End loop: parsing words in text node
313
314 }
315
316 }
317
318 });
319
320 } // End loop: visiting nodes within the given editable section
321
322 // Check any pending word....
323 checkWord();
324
325 /**
326 * Checks if there is a pending word, if there is then the callback is invoked.
327 * @return {Node} The node to exclusively resume from if the callback changed the DOM.
328 */
329 function checkWord() {
330
331 // Is there a word pending? Exclude words in a package or within a protected heirarchy
332 if (startNode
333 && endNode
334 && !de.doc.isProtectedNode(startNode)
335 && !de.doc.isNodePackaged(startNode)) {
336
337 // Invoke callback
338 resumeNode = callback(curWord, startNode, startIndex, endNode, endIndex);
339
340 // Reset start/end point.
341 startNode = endNode = 0;
342
343 // Return result
344 return resumeNode;
345 }
346
347 // Reset start/end point.
348 startNode = endNode = 0;
349 }
350
351 } // End visitWords function
352
353
354 _registerAction("SpellMark", {
355
356 /**
357 * An undoable action: wraps a adjacent group of text nodes with a spelling error wrapper
358 *
359 * @param {Node} startNode
360 * @param {Number} startIndex
361 * @param {Node} endNode
362 * @param {Number} endIndex
363 */
364 exec : function(startNode, startIndex, endNode, endIndex) {
365
366 // Build a fragment - isolating the word within the text node
367 var frag = _buildFragment(null, startNode, startIndex, endNode, endIndex),
368 wrapper = $createElement("span");
369
370 // Wrap the text with a span
371 _setClassName(wrapper, SPELL_MARK_CLASS_NAME);
372 _execOp(_Operation.INSERT_NODE, wrapper, frag.node, frag.children[0].pos);
373
374 for (var i = 0; i < frag.children.length; i++) {
375 var migrant = frag.children[i].node;
376 _execOp(_Operation.REMOVE_NODE, migrant);
377 _execOp(_Operation.INSERT_NODE, migrant, wrapper);
378 }
379
380 return frag;
381 }
382
383 });
384
385 _registerAction("SpellUnmark", {
386
387 /**
388 * Removes a wrapper
389 *
390 * @param {Element} markedNode A mark wrapper element to remove...
391 */
392 exec : function(markedNode) {
393
394 debug.assert(markedNode && de.spell.isSpellErrorWrapper(markedNode));
395
396 // Decide whether to remove the wrapper
397 /*var shouldRemove = _getClassName(node) != SPELL_MARK_CLASS_NAME;
398 if (!shouldRemove)
399 shouldRemove = _doesHaveElementStyle(node);
400 */
401
402 // TODO: REFACTOR THIS OPERATION... IT IS USED EVERYWHERE
403 while(markedNode.firstChild) {
404 var migrant = markedNode.firstChild;
405 _execOp(_Operation.REMOVE_NODE, migrant);
406 _execOp(_Operation.INSERT_NODE, migrant, markedNode.parentNode, _indexInParent(markedNode));
407 }
408 _execOp(_Operation.REMOVE_NODE, markedNode);
409
410 }
411
412 });
413
414 _registerAction("SpellCorrect", {
415
416 /**
417 * Replaces an error with a correction.
418 *
419 * @param {Element} markNode The spelling error wrapper to "correct" (i.e. replace)
420 * @param {String} correction The word to replace the spelling error with
421 */
422 exec : function(markedNode, correction) {
423 _execOp(_Operation.INSERT_NODE, document.createTextNode(correction), markedNode.parentNode, _indexInParent(markedNode));
424 _execOp(_Operation.REMOVE_NODE, markedNode);
425 }
426
427 });
428
429
430})();
431
Note: See TracBrowser for help on using the repository browser.