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