1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/trace",
  5     "firebug/lib/options",
  6 ],
  7 function(FBTrace, Options) {
  8 
  9 // ********************************************************************************************* //
 10 // Constants
 11 
 12 var Ci = Components.interfaces;
 13 var Cc = Components.classes;
 14 var Cu = Components.utils;
 15 
 16 var Search = {};
 17 
 18 var finder = Search.finder = Cc["@mozilla.org/embedcomp/rangefind;1"].createInstance(Ci.nsIFind);
 19 
 20 // ********************************************************************************************* //
 21 
 22 /**
 23  * @class Searches for text in a given node.
 24  *
 25  * @constructor
 26  * @param {Node} rootNode Node to search
 27  * @param {function} rowFinder Results filter. On find this method will be called
 28  *      with the node containing the matched text as the first parameter. This may
 29  *      be undefined to return the node as is.
 30  */
 31 Search.TextSearch = function(rootNode, rowFinder)
 32 {
 33     var doc = rootNode.ownerDocument;
 34     var count, searchRange, startPt;
 35 
 36     /**
 37      * Find the first result in the node.
 38      *
 39      * @param {String} text Text to search for
 40      * @param {boolean} reverse True to perform a reverse search
 41      * @param {boolean} caseSensitive True to perform a case sensitive search
 42      */
 43     this.find = function(text, reverse, caseSensitive)
 44     {
 45         this.text = text;
 46 
 47         finder.findBackwards = !!reverse;
 48         finder.caseSensitive = !!caseSensitive;
 49 
 50         var range = this.range = finder.Find(
 51             text, searchRange,
 52             startPt || searchRange,
 53             searchRange);
 54 
 55         var match = range ?  range.startContainer : null;
 56         return this.currentNode = (rowFinder && match ? rowFinder(match) : match);
 57     };
 58 
 59     /**
 60      * Find the next search result
 61      *
 62      * @param {boolean} wrapAround True to wrap the search if the end of range is reached
 63      * @param {boolean} sameNode True to return multiple results from the same text node
 64      * @param {boolean} reverse True to search in reverse
 65      * @param {boolean} caseSensitive True to perform a case sensitive search
 66      */
 67     this.findNext = function(wrapAround, sameNode, reverse, caseSensitive)
 68     {
 69         this.wrapped = false;
 70         startPt = undefined;
 71 
 72         if (sameNode && this.range)
 73         {
 74             startPt = this.range.cloneRange();
 75             if (reverse)
 76                 startPt.setEnd(startPt.startContainer, startPt.startOffset);
 77             else
 78                 startPt.setStart(startPt.startContainer, startPt.startOffset+1);
 79         }
 80 
 81         if (!startPt)
 82         {
 83             var curNode = this.currentNode ? this.currentNode : rootNode;
 84             startPt = doc.createRange();
 85             try
 86             {
 87                 if (reverse)
 88                 {
 89                     startPt.setStartBefore(curNode);
 90                 }
 91                 else
 92                 {
 93                     startPt.setStartAfter(curNode);
 94                 }
 95             }
 96             catch (e)
 97             {
 98                 if (FBTrace.DBG_ERRORS)
 99                     FBTrace.sysout("lib.TextSearch.findNext setStartAfter fails for nodeType:"+
100                         (this.currentNode?this.currentNode.nodeType:rootNode.nodeType),e);
101 
102                 try
103                 {
104                     FBTrace.sysout("setStart try\n");
105                     startPt.setStart(curNode);
106                     FBTrace.sysout("setStart success\n");
107                 }
108                 catch (exc)
109                 {
110                     return;
111                 }
112             }
113         }
114 
115         var match = startPt && this.find(this.text, reverse, caseSensitive);
116         if (!match && wrapAround)
117         {
118             this.wrapped = true;
119             this.reset();
120             return this.find(this.text, reverse, caseSensitive);
121         }
122 
123         return match;
124     };
125 
126     /**
127      * Resets the instance state to the initial state.
128      */
129     this.reset = function()
130     {
131         searchRange = doc.createRange();
132         searchRange.selectNode(rootNode);
133 
134         startPt = searchRange;
135     };
136 
137     this.reset();
138 };
139 
140 Search.SourceBoxTextSearch = function(sourceBox)
141 {
142     this.tryToContinueSearch = function(sBox, text)
143     {
144         if (sBox != sourceBox)
145             return false;
146 
147         var isSubstring = text.indexOf(this.text) !=-1 || this.text.indexOf(text) !=-1;
148 
149         if (isSubstring && this.mark && Math.abs(sourceBox.centralLine - this.mark) < 10)
150             this.mark--;
151         else
152             this.reset();
153 
154         return true;
155     };
156     this.find = function(text, reverse, caseSensitive)
157     {
158         this.text = text;
159 
160         this.re = new Search.ReversibleRegExp(text);
161 
162         return this.findNext(false, reverse, caseSensitive);
163     };
164 
165     this.findNext = function(wrapAround, reverse, caseSensitive)
166     {
167         this.wrapped = false;
168         var lines = sourceBox.lines;
169         var match = null;
170         for (var iter = new Search.ReversibleIterator(lines.length, this.mark, reverse); iter.next();)
171         {
172             match = this.re.exec(lines[iter.index], false, caseSensitive);
173             if (match)
174             {
175                 this.mark = iter.index;
176                 return iter.index;
177             }
178         }
179 
180         if (!match && wrapAround)
181         {
182             this.reset();
183             this.wrapped = true;
184             return this.findNext(false, reverse, caseSensitive);
185         }
186 
187         return match;
188     };
189 
190     this.reset = function()
191     {
192         delete this.mark;
193     };
194 
195     this.reset();
196 };
197 
198 Search.ReversibleIterator = function(length, start, reverse)
199 {
200     this.length = length;
201     this.index = start;
202     this.reversed = !!reverse;
203 
204     this.next = function()
205     {
206         if (this.index === undefined || this.index === null)
207         {
208             this.index = this.reversed ? length : -1;
209         }
210         this.index += this.reversed ? -1 : 1;
211 
212         return 0 <= this.index && this.index < length;
213     };
214 
215     this.reverse = function()
216     {
217         this.reversed = !this.reversed;
218     };
219 };
220 
221 
222 /**
223  * @class Implements a RegExp-like object that will search for the literal value
224  * of a given string, rather than the regular expression. This allows for
225  * iterative literal searches without having to escape user input strings
226  * to prevent invalid regular expressions from being used.
227  *
228  * @constructor
229  * @param {String} literal Text to search for
230  * @param {Boolean} reverse Truthy to preform a reverse search, falsy to perform a forward seach
231  * @param {Boolean} caseSensitive Truthy to perform a case sensitive search, falsy to perform a case insensitive search.
232  */
233 Search.LiteralRegExp = function(literal, reverse, caseSensitive)
234 {
235     var searchToken = (!caseSensitive) ? literal.toLowerCase() : literal;
236 
237     this.__defineGetter__("global", function() { return true; });
238     this.__defineGetter__("multiline", function() { return true; });
239     this.__defineGetter__("reverse", function() { return reverse; });
240     this.__defineGetter__("ignoreCase", function() { return !caseSensitive; });
241     this.lastIndex = 0;
242 
243     this.exec = function(text)
244     {
245         if (!text)
246             return null;
247 
248         var searchText = (!caseSensitive) ? text.toLowerCase() : text,
249             startIndex = (reverse ? text.length-1 : 0) + this.lastIndex,
250             index;
251 
252         if (0 <= startIndex && startIndex < text.length)
253             index = searchText[reverse ? "lastIndexOf" : "indexOf"](searchToken, startIndex);
254         else
255             index = -1;
256 
257         if (index >= 0)
258         {
259             var ret = [ text.substr(index, searchToken.length) ];
260             ret.index = index;
261             ret.input = text;
262             this.lastIndex = index + (reverse ? -1*text.length : searchToken.length);
263             return ret;
264         }
265         else
266             this.lastIndex = 0;
267 
268         return null;
269     };
270 
271     this.test = function(text)
272     {
273         if (!text)
274             return false;
275 
276         var searchText = (!caseSensitive) ? text.toLowerCase() : text;
277         return searchText.indexOf(searchToken) >= 0;
278     };
279 };
280 
281 Search.ReversibleRegExp = function(regex, flags)
282 {
283     var re = {};
284 
285     function expression(text, reverse)
286     {
287         return text + (reverse ? "(?![\\s\\S]*" + text + ")" : "");
288     }
289 
290     function flag(flags, caseSensitive)
291     {
292         return (flags || "") + (caseSensitive ? "" : "i");
293     }
294 
295     this.exec = function(text, reverse, caseSensitive, lastMatch)
296     {
297         // Ensure we have a regex
298         var key = (reverse ? "r" : "n") + (caseSensitive ? "n" : "i") 
299                                 + (Firebug.searchUseRegularExpression ? "r" : "n");
300         if (!re[key])
301         {
302             try
303             {
304                 if (Firebug.searchUseRegularExpression)
305                     re[key] = new RegExp(expression(regex, reverse), flag(flags, caseSensitive));
306                 else
307                     re[key] = new Search.LiteralRegExp(regex, reverse, caseSensitive);
308             }
309             catch (ex)
310             {
311                 // The user likely entered an invalid regular expression or is in the
312                 // process of entering a valid one. Treat this as a plain text search
313                 re[key] = new Search.LiteralRegExp(regex, reverse, caseSensitive);
314             }
315         }
316 
317         // Modify as needed to all for iterative searches
318         var indexOffset = 0;
319         var searchText = text;
320         if (lastMatch)
321         {
322             if (reverse)
323             {
324                 searchText = text.substr(0, lastMatch.index);
325             }
326             else
327             {
328                 indexOffset = lastMatch.index+lastMatch[0].length;
329                 searchText = text.substr(indexOffset);
330             }
331         }
332 
333         var curRe = re[key];
334         curRe.lastIndex = 0;
335         var ret = curRe.exec(searchText);
336         if (ret)
337         {
338             ret.input = text;
339             ret.index = ret.index + indexOffset;
340             ret.reverse = reverse;
341             ret.caseSensitive = caseSensitive;
342         }
343         return ret;
344     };
345 };
346 
347 return Search;
348 
349 // ********************************************************************************************* //
350 });
351