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