1/** 2 * helper function to return a node containing the 3 * search summary for a given text. keywords is a list 4 * of stemmed words, hlwords is the list of normal, unstemmed 5 * words. the first one is used to find the occurance, the 6 * latter for highlighting it. 7 */ 8 9jQuery.makeSearchSummary = function(text, keywords, hlwords) { 10 var textLower = text.toLowerCase(); 11 var start = 0; 12 $.each(keywords, function() { 13 var i = textLower.indexOf(this.toLowerCase()); 14 if (i > -1) 15 start = i; 16 }); 17 start = Math.max(start - 120, 0); 18 var excerpt = ((start > 0) ? '...' : '') + 19 $.trim(text.substr(start, 240)) + 20 ((start + 240 - text.length) ? '...' : ''); 21 var rv = $('<div class="context"></div>').text(excerpt); 22 $.each(hlwords, function() { 23 rv = rv.highlightText(this, 'highlight'); 24 }); 25 return rv; 26} 27 28/** 29 * Porter Stemmer 30 */ 31var PorterStemmer = function() { 32 33 var step2list = { 34 ational: 'ate', 35 tional: 'tion', 36 enci: 'ence', 37 anci: 'ance', 38 izer: 'ize', 39 bli: 'ble', 40 alli: 'al', 41 entli: 'ent', 42 eli: 'e', 43 ousli: 'ous', 44 ization: 'ize', 45 ation: 'ate', 46 ator: 'ate', 47 alism: 'al', 48 iveness: 'ive', 49 fulness: 'ful', 50 ousness: 'ous', 51 aliti: 'al', 52 iviti: 'ive', 53 biliti: 'ble', 54 logi: 'log' 55 }; 56 57 var step3list = { 58 icate: 'ic', 59 ative: '', 60 alize: 'al', 61 iciti: 'ic', 62 ical: 'ic', 63 ful: '', 64 ness: '' 65 }; 66 67 var c = "[^aeiou]"; // consonant 68 var v = "[aeiouy]"; // vowel 69 var C = c + "[^aeiouy]*"; // consonant sequence 70 var V = v + "[aeiou]*"; // vowel sequence 71 72 var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 73 var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 74 var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 75 var s_v = "^(" + C + ")?" + v; // vowel in stem 76 77 this.stemWord = function (w) { 78 var stem; 79 var suffix; 80 var firstch; 81 var origword = w; 82 83 if (w.length < 3) 84 return w; 85 86 var re; 87 var re2; 88 var re3; 89 var re4; 90 91 firstch = w.substr(0,1); 92 if (firstch == "y") 93 w = firstch.toUpperCase() + w.substr(1); 94 95 // Step 1a 96 re = /^(.+?)(ss|i)es$/; 97 re2 = /^(.+?)([^s])s$/; 98 99 if (re.test(w)) 100 w = w.replace(re,"$1$2"); 101 else if (re2.test(w)) 102 w = w.replace(re2,"$1$2"); 103 104 // Step 1b 105 re = /^(.+?)eed$/; 106 re2 = /^(.+?)(ed|ing)$/; 107 if (re.test(w)) { 108 var fp = re.exec(w); 109 re = new RegExp(mgr0); 110 if (re.test(fp[1])) { 111 re = /.$/; 112 w = w.replace(re,""); 113 } 114 } 115 else if (re2.test(w)) { 116 var fp = re2.exec(w); 117 stem = fp[1]; 118 re2 = new RegExp(s_v); 119 if (re2.test(stem)) { 120 w = stem; 121 re2 = /(at|bl|iz)$/; 122 re3 = new RegExp("([^aeiouylsz])\\1$"); 123 re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 124 if (re2.test(w)) 125 w = w + "e"; 126 else if (re3.test(w)) { 127 re = /.$/; 128 w = w.replace(re,""); 129 } 130 else if (re4.test(w)) 131 w = w + "e"; 132 } 133 } 134 135 // Step 1c 136 re = /^(.+?)y$/; 137 if (re.test(w)) { 138 var fp = re.exec(w); 139 stem = fp[1]; 140 re = new RegExp(s_v); 141 if (re.test(stem)) 142 w = stem + "i"; 143 } 144 145 // Step 2 146 re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; 147 if (re.test(w)) { 148 var fp = re.exec(w); 149 stem = fp[1]; 150 suffix = fp[2]; 151 re = new RegExp(mgr0); 152 if (re.test(stem)) 153 w = stem + step2list[suffix]; 154 } 155 156 // Step 3 157 re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; 158 if (re.test(w)) { 159 var fp = re.exec(w); 160 stem = fp[1]; 161 suffix = fp[2]; 162 re = new RegExp(mgr0); 163 if (re.test(stem)) 164 w = stem + step3list[suffix]; 165 } 166 167 // Step 4 168 re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; 169 re2 = /^(.+?)(s|t)(ion)$/; 170 if (re.test(w)) { 171 var fp = re.exec(w); 172 stem = fp[1]; 173 re = new RegExp(mgr1); 174 if (re.test(stem)) 175 w = stem; 176 } 177 else if (re2.test(w)) { 178 var fp = re2.exec(w); 179 stem = fp[1] + fp[2]; 180 re2 = new RegExp(mgr1); 181 if (re2.test(stem)) 182 w = stem; 183 } 184 185 // Step 5 186 re = /^(.+?)e$/; 187 if (re.test(w)) { 188 var fp = re.exec(w); 189 stem = fp[1]; 190 re = new RegExp(mgr1); 191 re2 = new RegExp(meq1); 192 re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 193 if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) 194 w = stem; 195 } 196 re = /ll$/; 197 re2 = new RegExp(mgr1); 198 if (re.test(w) && re2.test(w)) { 199 re = /.$/; 200 w = w.replace(re,""); 201 } 202 203 // and turn initial Y back to y 204 if (firstch == "y") 205 w = firstch.toLowerCase() + w.substr(1); 206 return w; 207 } 208} 209 210 211/** 212 * Search Module 213 */ 214var Search = { 215 216 _index : null, 217 _queued_query : null, 218 _pulse_status : -1, 219 220 init : function() { 221 var params = $.getQueryParameters(); 222 if (params.q) { 223 var query = params.q[0]; 224 $('input[name="q"]')[0].value = query; 225 this.performSearch(query); 226 } 227 }, 228 229 /** 230 * Sets the index 231 */ 232 setIndex : function(index) { 233 var q; 234 this._index = index; 235 if ((q = this._queued_query) !== null) { 236 this._queued_query = null; 237 Search.query(q); 238 } 239 }, 240 241 hasIndex : function() { 242 return this._index !== null; 243 }, 244 245 deferQuery : function(query) { 246 this._queued_query = query; 247 }, 248 249 stopPulse : function() { 250 this._pulse_status = 0; 251 }, 252 253 startPulse : function() { 254 if (this._pulse_status >= 0) 255 return; 256 function pulse() { 257 Search._pulse_status = (Search._pulse_status + 1) % 4; 258 var dotString = ''; 259 for (var i = 0; i < Search._pulse_status; i++) 260 dotString += '.'; 261 Search.dots.text(dotString); 262 if (Search._pulse_status > -1) 263 window.setTimeout(pulse, 500); 264 }; 265 pulse(); 266 }, 267 268 /** 269 * perform a search for something 270 */ 271 performSearch : function(query) { 272 // create the required interface elements 273 this.out = $('#search-results'); 274 this.title = $('<h2>' + _('Searching') + '</h2>').appendTo(this.out); 275 this.dots = $('<span></span>').appendTo(this.title); 276 this.status = $('<p style="display: none"></p>').appendTo(this.out); 277 this.output = $('<ul class="search"/>').appendTo(this.out); 278 279 $('#search-progress').text(_('Preparing search...')); 280 this.startPulse(); 281 282 // index already loaded, the browser was quick! 283 if (this.hasIndex()) 284 this.query(query); 285 else 286 this.deferQuery(query); 287 }, 288 289 query : function(query) { 290 // stem the searchterms and add them to the 291 // correct list 292 var stemmer = new PorterStemmer(); 293 var searchterms = []; 294 var excluded = []; 295 var hlterms = []; 296 var tmp = query.split(/\s+/); 297 var object = (tmp.length == 1) ? tmp[0].toLowerCase() : null; 298 for (var i = 0; i < tmp.length; i++) { 299 // stem the word 300 var word = stemmer.stemWord(tmp[i]).toLowerCase(); 301 // select the correct list 302 if (word[0] == '-') { 303 var toAppend = excluded; 304 word = word.substr(1); 305 } 306 else { 307 var toAppend = searchterms; 308 hlterms.push(tmp[i].toLowerCase()); 309 } 310 // only add if not already in the list 311 if (!$.contains(toAppend, word)) 312 toAppend.push(word); 313 }; 314 var highlightstring = '?highlight=' + $.urlencode(hlterms.join(" ")); 315 316 console.debug('SEARCH: searching for:'); 317 console.info('required: ', searchterms); 318 console.info('excluded: ', excluded); 319 320 // prepare search 321 var filenames = this._index.filenames; 322 var titles = this._index.titles; 323 var terms = this._index.terms; 324 var descrefs = this._index.descrefs; 325 var modules = this._index.modules; 326 var desctypes = this._index.desctypes; 327 var fileMap = {}; 328 var files = null; 329 var objectResults = []; 330 var regularResults = []; 331 $('#search-progress').empty(); 332 333 // lookup as object 334 if (object != null) { 335 for (var module in modules) { 336 if (module.indexOf(object) > -1) { 337 fn = modules[module]; 338 descr = _('module, in ') + titles[fn]; 339 objectResults.push([filenames[fn], module, '#module-'+module, descr]); 340 } 341 } 342 for (var prefix in descrefs) { 343 for (var name in descrefs[prefix]) { 344 var fullname = (prefix ? prefix + '.' : '') + name; 345 if (fullname.toLowerCase().indexOf(object) > -1) { 346 match = descrefs[prefix][name]; 347 descr = desctypes[match[1]] + _(', in ') + titles[match[0]]; 348 objectResults.push([filenames[match[0]], fullname, '#'+fullname, descr]); 349 } 350 } 351 } 352 } 353 354 // sort results descending 355 objectResults.sort(function(a, b) { 356 return (a[1] > b[1]) ? -1 : ((a[1] < b[1]) ? 1 : 0); 357 }); 358 359 360 // perform the search on the required terms 361 for (var i = 0; i < searchterms.length; i++) { 362 var word = searchterms[i]; 363 // no match but word was a required one 364 if ((files = terms[word]) == null) 365 break; 366 if (files.length == undefined) { 367 files = [files]; 368 } 369 // create the mapping 370 for (var j = 0; j < files.length; j++) { 371 var file = files[j]; 372 if (file in fileMap) 373 fileMap[file].push(word); 374 else 375 fileMap[file] = [word]; 376 } 377 } 378 379 // now check if the files don't contain excluded terms 380 for (var file in fileMap) { 381 var valid = true; 382 383 // check if all requirements are matched 384 if (fileMap[file].length != searchterms.length) 385 continue; 386 387 // ensure that none of the excluded terms is in the 388 // search result. 389 for (var i = 0; i < excluded.length; i++) { 390 if (terms[excluded[i]] == file || 391 $.contains(terms[excluded[i]] || [], file)) { 392 valid = false; 393 break; 394 } 395 } 396 397 // if we have still a valid result we can add it 398 // to the result list 399 if (valid) 400 regularResults.push([filenames[file], titles[file], '', null]); 401 } 402 403 // delete unused variables in order to not waste 404 // memory until list is retrieved completely 405 delete filenames, titles, terms; 406 407 // now sort the regular results descending by title 408 regularResults.sort(function(a, b) { 409 var left = a[1].toLowerCase(); 410 var right = b[1].toLowerCase(); 411 return (left > right) ? -1 : ((left < right) ? 1 : 0); 412 }); 413 414 // combine both 415 var results = regularResults.concat(objectResults); 416 417 // print the results 418 var resultCount = results.length; 419 function displayNextItem() { 420 // results left, load the summary and display it 421 if (results.length) { 422 var item = results.pop(); 423 var listItem = $('<li style="display:none"></li>'); 424 listItem.append($('<a/>').attr( 425 'href', 426 item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX + 427 highlightstring + item[2]).html(item[1])); 428 if (item[3]) { 429 listItem.append($('<span> (' + item[3] + ')</span>')); 430 Search.output.append(listItem); 431 listItem.slideDown(5, function() { 432 displayNextItem(); 433 }); 434 } else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) { 435 $.get('_sources/' + item[0] + '.txt', function(data) { 436 listItem.append($.makeSearchSummary(data, searchterms, hlterms)); 437 Search.output.append(listItem); 438 listItem.slideDown(5, function() { 439 displayNextItem(); 440 }); 441 }); 442 } else { 443 // no source available, just display title 444 Search.output.append(listItem); 445 listItem.slideDown(5, function() { 446 displayNextItem(); 447 }); 448 } 449 } 450 // search finished, update title and status message 451 else { 452 Search.stopPulse(); 453 Search.title.text(_('Search Results')); 454 if (!resultCount) 455 Search.status.text(_('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.')); 456 else 457 Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount)); 458 Search.status.fadeIn(500); 459 } 460 } 461 displayNextItem(); 462 } 463} 464 465$(document).ready(function() { 466 Search.init(); 467}); 468