1Searcher = function(data) { 2 this.data = data; 3 this.handlers = []; 4} 5 6Searcher.prototype = new function() { 7 // search is performed in chunks of 1000 for non-blocking user input 8 var CHUNK_SIZE = 1000; 9 // do not try to find more than 100 results 10 var MAX_RESULTS = 100; 11 var huid = 1; 12 var suid = 1; 13 var runs = 0; 14 15 this.find = function(query) { 16 var queries = splitQuery(query); 17 var regexps = buildRegexps(queries); 18 var highlighters = buildHilighters(queries); 19 var state = { from: 0, pass: 0, limit: MAX_RESULTS, n: suid++}; 20 var _this = this; 21 22 this.currentSuid = state.n; 23 24 if (!query) return; 25 26 var run = function() { 27 // stop current search thread if new search started 28 if (state.n != _this.currentSuid) return; 29 30 var results = 31 performSearch(_this.data, regexps, queries, highlighters, state); 32 var hasMore = (state.limit > 0 && state.pass < 4); 33 34 triggerResults.call(_this, results, !hasMore); 35 if (hasMore) { 36 setTimeout(run, 2); 37 } 38 runs++; 39 }; 40 runs = 0; 41 42 // start search thread 43 run(); 44 } 45 46 /* ----- Events ------ */ 47 this.ready = function(fn) { 48 fn.huid = huid; 49 this.handlers.push(fn); 50 } 51 52 /* ----- Utilities ------ */ 53 function splitQuery(query) { 54 return jQuery.grep(query.split(/(\s+|::?|\(\)?)/), function(string) { 55 return string.match(/\S/) 56 }); 57 } 58 59 function buildRegexps(queries) { 60 return jQuery.map(queries, function(query) { 61 return new RegExp(query.replace(/(.)/g, '([$1])([^$1]*?)'), 'i') 62 }); 63 } 64 65 function buildHilighters(queries) { 66 return jQuery.map(queries, function(query) { 67 return jQuery.map(query.split(''), function(l, i) { 68 return '\u0001$' + (i*2+1) + '\u0002$' + (i*2+2); 69 }).join(''); 70 }); 71 } 72 73 // function longMatchRegexp(index, longIndex, regexps) { 74 // for (var i = regexps.length - 1; i >= 0; i--){ 75 // if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) return false; 76 // }; 77 // return true; 78 // } 79 80 81 /* ----- Mathchers ------ */ 82 83 /* 84 * This record matches if the index starts with queries[0] and the record 85 * matches all of the regexps 86 */ 87 function matchPassBeginning(index, longIndex, queries, regexps) { 88 if (index.indexOf(queries[0]) != 0) return false; 89 for (var i=1, l = regexps.length; i < l; i++) { 90 if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) 91 return false; 92 }; 93 return true; 94 } 95 96 /* 97 * This record matches if the longIndex starts with queries[0] and the 98 * longIndex matches all of the regexps 99 */ 100 function matchPassLongIndex(index, longIndex, queries, regexps) { 101 if (longIndex.indexOf(queries[0]) != 0) return false; 102 for (var i=1, l = regexps.length; i < l; i++) { 103 if (!longIndex.match(regexps[i])) 104 return false; 105 }; 106 return true; 107 } 108 109 /* 110 * This record matches if the index contains queries[0] and the record 111 * matches all of the regexps 112 */ 113 function matchPassContains(index, longIndex, queries, regexps) { 114 if (index.indexOf(queries[0]) == -1) return false; 115 for (var i=1, l = regexps.length; i < l; i++) { 116 if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) 117 return false; 118 }; 119 return true; 120 } 121 122 /* 123 * This record matches if regexps[0] matches the index and the record 124 * matches all of the regexps 125 */ 126 function matchPassRegexp(index, longIndex, queries, regexps) { 127 if (!index.match(regexps[0])) return false; 128 for (var i=1, l = regexps.length; i < l; i++) { 129 if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) 130 return false; 131 }; 132 return true; 133 } 134 135 136 /* ----- Highlighters ------ */ 137 function highlightRegexp(info, queries, regexps, highlighters) { 138 var result = createResult(info); 139 for (var i=0, l = regexps.length; i < l; i++) { 140 result.title = result.title.replace(regexps[i], highlighters[i]); 141 result.namespace = result.namespace.replace(regexps[i], highlighters[i]); 142 }; 143 return result; 144 } 145 146 function hltSubstring(string, pos, length) { 147 return string.substring(0, pos) + '\u0001' + string.substring(pos, pos + length) + '\u0002' + string.substring(pos + length); 148 } 149 150 function highlightQuery(info, queries, regexps, highlighters) { 151 var result = createResult(info); 152 var pos = 0; 153 var lcTitle = result.title.toLowerCase(); 154 155 pos = lcTitle.indexOf(queries[0]); 156 if (pos != -1) { 157 result.title = hltSubstring(result.title, pos, queries[0].length); 158 } 159 160 result.namespace = result.namespace.replace(regexps[0], highlighters[0]); 161 for (var i=1, l = regexps.length; i < l; i++) { 162 result.title = result.title.replace(regexps[i], highlighters[i]); 163 result.namespace = result.namespace.replace(regexps[i], highlighters[i]); 164 }; 165 return result; 166 } 167 168 function createResult(info) { 169 var result = {}; 170 result.title = info[0]; 171 result.namespace = info[1]; 172 result.path = info[2]; 173 result.params = info[3]; 174 result.snippet = info[4]; 175 return result; 176 } 177 178 /* ----- Searching ------ */ 179 function performSearch(data, regexps, queries, highlighters, state) { 180 var searchIndex = data.searchIndex; 181 var longSearchIndex = data.longSearchIndex; 182 var info = data.info; 183 var result = []; 184 var i = state.from; 185 var l = searchIndex.length; 186 var togo = CHUNK_SIZE; 187 var matchFunc, hltFunc; 188 189 while (state.pass < 4 && state.limit > 0 && togo > 0) { 190 if (state.pass == 0) { 191 matchFunc = matchPassBeginning; 192 hltFunc = highlightQuery; 193 } else if (state.pass == 1) { 194 matchFunc = matchPassLongIndex; 195 hltFunc = highlightQuery; 196 } else if (state.pass == 2) { 197 matchFunc = matchPassContains; 198 hltFunc = highlightQuery; 199 } else if (state.pass == 3) { 200 matchFunc = matchPassRegexp; 201 hltFunc = highlightRegexp; 202 } 203 204 for (; togo > 0 && i < l && state.limit > 0; i++, togo--) { 205 if (info[i].n == state.n) continue; 206 if (matchFunc(searchIndex[i], longSearchIndex[i], queries, regexps)) { 207 info[i].n = state.n; 208 result.push(hltFunc(info[i], queries, regexps, highlighters)); 209 state.limit--; 210 } 211 }; 212 if (searchIndex.length <= i) { 213 state.pass++; 214 i = state.from = 0; 215 } else { 216 state.from = i; 217 } 218 } 219 return result; 220 } 221 222 function triggerResults(results, isLast) { 223 jQuery.each(this.handlers, function(i, fn) { 224 fn.call(this, results, isLast) 225 }) 226 } 227} 228 229