5 * Sphinx JavaScript utilities for the full-text search.
7 * :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
8 * :license: BSD, see LICENSE for details.
13 /* Non-minified version JS is _stemmer.js if file is provided */
17 var Stemmer = function() {
53 var c = "[^aeiou]"; // consonant
54 var v = "[aeiouy]"; // vowel
55 var C = c + "[^aeiouy]*"; // consonant sequence
56 var V = v + "[aeiou]*"; // vowel sequence
58 var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0
59 var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1
60 var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1
61 var s_v = "^(" + C + ")?" + v; // vowel in stem
63 this.stemWord = function (w) {
77 firstch = w.substr(0,1);
79 w = firstch.toUpperCase() + w.substr(1);
82 re = /^(.+?)(ss|i)es$/;
83 re2 = /^(.+?)([^s])s$/;
86 w = w.replace(re,"$1$2");
88 w = w.replace(re2,"$1$2");
92 re2 = /^(.+?)(ed|ing)$/;
95 re = new RegExp(mgr0);
101 else if (re2.test(w)) {
102 var fp = re2.exec(w);
104 re2 = new RegExp(s_v);
105 if (re2.test(stem)) {
108 re3 = new RegExp("([^aeiouylsz])\\1$");
109 re4 = new RegExp("^" + C + v + "[^aeiouwxy]$");
112 else if (re3.test(w)) {
114 w = w.replace(re,"");
116 else if (re4.test(w))
126 re = new RegExp(s_v);
132 re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
137 re = new RegExp(mgr0);
139 w = stem + step2list[suffix];
143 re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
148 re = new RegExp(mgr0);
150 w = stem + step3list[suffix];
154 re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
155 re2 = /^(.+?)(s|t)(ion)$/;
159 re = new RegExp(mgr1);
163 else if (re2.test(w)) {
164 var fp = re2.exec(w);
165 stem = fp[1] + fp[2];
166 re2 = new RegExp(mgr1);
176 re = new RegExp(mgr1);
177 re2 = new RegExp(meq1);
178 re3 = new RegExp("^" + C + v + "[^aeiouwxy]$");
179 if (re.test(stem) || (re2.test(stem) && !(re3.test(stem))))
183 re2 = new RegExp(mgr1);
184 if (re.test(w) && re2.test(w)) {
186 w = w.replace(re,"");
189 // and turn initial Y back to y
191 w = firstch.toLowerCase() + w.substr(1);
199 * Simple result scoring code.
202 // Implement the following function to further tweak the score for each result
203 // The function takes a result array [filename, title, anchor, descr, score]
204 // and returns the new score.
206 score: function(result) {
211 // query matches the full name of an object
213 // or matches in the last dotted part of the object name
215 // Additive scores depending on the priority of the object
216 objPrio: {0: 15, // used to be importantResults
217 1: 5, // used to be objectResults
218 2: -5}, // used to be unimportantResults
219 // Used when the priority is not in the mapping.
222 // query found in title
224 // query found in terms
232 var splitChars = (function() {
234 var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648,
235 1748, 1809, 2416, 2473, 2481, 2526, 2601, 2609, 2612, 2615, 2653, 2702,
236 2706, 2729, 2737, 2740, 2857, 2865, 2868, 2910, 2928, 2948, 2961, 2971,
237 2973, 3085, 3089, 3113, 3124, 3213, 3217, 3241, 3252, 3295, 3341, 3345,
238 3369, 3506, 3516, 3633, 3715, 3721, 3736, 3744, 3748, 3750, 3756, 3761,
239 3781, 3912, 4239, 4347, 4681, 4695, 4697, 4745, 4785, 4799, 4801, 4823,
240 4881, 5760, 5901, 5997, 6313, 7405, 8024, 8026, 8028, 8030, 8117, 8125,
241 8133, 8181, 8468, 8485, 8487, 8489, 8494, 8527, 11311, 11359, 11687, 11695,
242 11703, 11711, 11719, 11727, 11735, 12448, 12539, 43010, 43014, 43019, 43587,
243 43696, 43713, 64286, 64297, 64311, 64317, 64319, 64322, 64325, 65141];
244 var i, j, start, end;
245 for (i = 0; i < singles.length; i++) {
246 result[singles[i]] = true;
248 var ranges = [[0, 47], [58, 64], [91, 94], [123, 169], [171, 177], [182, 184], [706, 709],
249 [722, 735], [741, 747], [751, 879], [888, 889], [894, 901], [1154, 1161],
250 [1318, 1328], [1367, 1368], [1370, 1376], [1416, 1487], [1515, 1519], [1523, 1568],
251 [1611, 1631], [1642, 1645], [1750, 1764], [1767, 1773], [1789, 1790], [1792, 1807],
252 [1840, 1868], [1958, 1968], [1970, 1983], [2027, 2035], [2038, 2041], [2043, 2047],
253 [2070, 2073], [2075, 2083], [2085, 2087], [2089, 2307], [2362, 2364], [2366, 2383],
254 [2385, 2391], [2402, 2405], [2419, 2424], [2432, 2436], [2445, 2446], [2449, 2450],
255 [2483, 2485], [2490, 2492], [2494, 2509], [2511, 2523], [2530, 2533], [2546, 2547],
256 [2554, 2564], [2571, 2574], [2577, 2578], [2618, 2648], [2655, 2661], [2672, 2673],
257 [2677, 2692], [2746, 2748], [2750, 2767], [2769, 2783], [2786, 2789], [2800, 2820],
258 [2829, 2830], [2833, 2834], [2874, 2876], [2878, 2907], [2914, 2917], [2930, 2946],
259 [2955, 2957], [2966, 2968], [2976, 2978], [2981, 2983], [2987, 2989], [3002, 3023],
260 [3025, 3045], [3059, 3076], [3130, 3132], [3134, 3159], [3162, 3167], [3170, 3173],
261 [3184, 3191], [3199, 3204], [3258, 3260], [3262, 3293], [3298, 3301], [3312, 3332],
262 [3386, 3388], [3390, 3423], [3426, 3429], [3446, 3449], [3456, 3460], [3479, 3481],
263 [3518, 3519], [3527, 3584], [3636, 3647], [3655, 3663], [3674, 3712], [3717, 3718],
264 [3723, 3724], [3726, 3731], [3752, 3753], [3764, 3772], [3774, 3775], [3783, 3791],
265 [3802, 3803], [3806, 3839], [3841, 3871], [3892, 3903], [3949, 3975], [3980, 4095],
266 [4139, 4158], [4170, 4175], [4182, 4185], [4190, 4192], [4194, 4196], [4199, 4205],
267 [4209, 4212], [4226, 4237], [4250, 4255], [4294, 4303], [4349, 4351], [4686, 4687],
268 [4702, 4703], [4750, 4751], [4790, 4791], [4806, 4807], [4886, 4887], [4955, 4968],
269 [4989, 4991], [5008, 5023], [5109, 5120], [5741, 5742], [5787, 5791], [5867, 5869],
270 [5873, 5887], [5906, 5919], [5938, 5951], [5970, 5983], [6001, 6015], [6068, 6102],
271 [6104, 6107], [6109, 6111], [6122, 6127], [6138, 6159], [6170, 6175], [6264, 6271],
272 [6315, 6319], [6390, 6399], [6429, 6469], [6510, 6511], [6517, 6527], [6572, 6592],
273 [6600, 6607], [6619, 6655], [6679, 6687], [6741, 6783], [6794, 6799], [6810, 6822],
274 [6824, 6916], [6964, 6980], [6988, 6991], [7002, 7042], [7073, 7085], [7098, 7167],
275 [7204, 7231], [7242, 7244], [7294, 7400], [7410, 7423], [7616, 7679], [7958, 7959],
276 [7966, 7967], [8006, 8007], [8014, 8015], [8062, 8063], [8127, 8129], [8141, 8143],
277 [8148, 8149], [8156, 8159], [8173, 8177], [8189, 8303], [8306, 8307], [8314, 8318],
278 [8330, 8335], [8341, 8449], [8451, 8454], [8456, 8457], [8470, 8472], [8478, 8483],
279 [8506, 8507], [8512, 8516], [8522, 8525], [8586, 9311], [9372, 9449], [9472, 10101],
280 [10132, 11263], [11493, 11498], [11503, 11516], [11518, 11519], [11558, 11567],
281 [11622, 11630], [11632, 11647], [11671, 11679], [11743, 11822], [11824, 12292],
282 [12296, 12320], [12330, 12336], [12342, 12343], [12349, 12352], [12439, 12444],
283 [12544, 12548], [12590, 12592], [12687, 12689], [12694, 12703], [12728, 12783],
284 [12800, 12831], [12842, 12880], [12896, 12927], [12938, 12976], [12992, 13311],
285 [19894, 19967], [40908, 40959], [42125, 42191], [42238, 42239], [42509, 42511],
286 [42540, 42559], [42592, 42593], [42607, 42622], [42648, 42655], [42736, 42774],
287 [42784, 42785], [42889, 42890], [42893, 43002], [43043, 43055], [43062, 43071],
288 [43124, 43137], [43188, 43215], [43226, 43249], [43256, 43258], [43260, 43263],
289 [43302, 43311], [43335, 43359], [43389, 43395], [43443, 43470], [43482, 43519],
290 [43561, 43583], [43596, 43599], [43610, 43615], [43639, 43641], [43643, 43647],
291 [43698, 43700], [43703, 43704], [43710, 43711], [43715, 43738], [43742, 43967],
292 [44003, 44015], [44026, 44031], [55204, 55215], [55239, 55242], [55292, 55295],
293 [57344, 63743], [64046, 64047], [64110, 64111], [64218, 64255], [64263, 64274],
294 [64280, 64284], [64434, 64466], [64830, 64847], [64912, 64913], [64968, 65007],
295 [65020, 65135], [65277, 65295], [65306, 65312], [65339, 65344], [65371, 65381],
296 [65471, 65473], [65480, 65481], [65488, 65489], [65496, 65497]];
297 for (i = 0; i < ranges.length; i++) {
298 start = ranges[i][0];
300 for (j = start; j <= end; j++) {
307 function splitQuery(query) {
310 for (var i = 0; i < query.length; i++) {
311 if (splitChars[query.charCodeAt(i)]) {
313 result.push(query.slice(start, i));
316 } else if (start === -1) {
321 result.push(query.slice(start));
335 _queued_query : null,
339 var params = $.getQueryParameters();
341 var query = params.q[0];
342 $('input[name="q"]')[0].value = query;
343 this.performSearch(query);
347 loadIndex : function(url) {
348 $.ajax({type: "GET", url: url, data: null,
349 dataType: "script", cache: true,
350 complete: function(jqxhr, textstatus) {
351 if (textstatus != "success") {
352 document.getElementById("searchindexloader").src = url;
357 setIndex : function(index) {
360 if ((q = this._queued_query) !== null) {
361 this._queued_query = null;
366 hasIndex : function() {
367 return this._index !== null;
370 deferQuery : function(query) {
371 this._queued_query = query;
374 stopPulse : function() {
375 this._pulse_status = 0;
378 startPulse : function() {
379 if (this._pulse_status >= 0)
383 Search._pulse_status = (Search._pulse_status + 1) % 4;
385 for (i = 0; i < Search._pulse_status; i++)
387 Search.dots.text(dotString);
388 if (Search._pulse_status > -1)
389 window.setTimeout(pulse, 500);
395 * perform a search for something (or wait until index is loaded)
397 performSearch : function(query) {
398 // create the required interface elements
399 this.out = $('#search-results');
400 this.title = $('<h2>' + _('Searching') + '</h2>').appendTo(this.out);
401 this.dots = $('<span></span>').appendTo(this.title);
402 this.status = $('<p style="display: none"></p>').appendTo(this.out);
403 this.output = $('<ul class="search"/>').appendTo(this.out);
405 $('#search-progress').text(_('Preparing search...'));
408 // index already loaded, the browser was quick!
412 this.deferQuery(query);
416 * execute search (requires search index to be loaded)
418 query : function(query) {
420 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"];
422 // stem the searchterms and add them to the correct list
423 var stemmer = new Stemmer();
424 var searchterms = [];
427 var tmp = splitQuery(query);
428 var objectterms = [];
429 for (i = 0; i < tmp.length; i++) {
431 objectterms.push(tmp[i].toLowerCase());
434 if ($u.indexOf(stopwords, tmp[i].toLowerCase()) != -1 || tmp[i].match(/^\d+$/) ||
440 var word = stemmer.stemWord(tmp[i].toLowerCase());
442 // select the correct list
443 if (word[0] == '-') {
445 word = word.substr(1);
448 toAppend = searchterms;
449 hlterms.push(tmp[i].toLowerCase());
451 // only add if not already in the list
452 if (!$u.contains(toAppend, word))
455 var highlightstring = '?highlight=' + $.urlencode(hlterms.join(" "));
457 // console.debug('SEARCH: searching for:');
458 // console.info('required: ', searchterms);
459 // console.info('excluded: ', excluded);
462 var terms = this._index.terms;
463 var titleterms = this._index.titleterms;
465 // array of [filename, title, anchor, descr, score]
467 $('#search-progress').empty();
470 for (i = 0; i < objectterms.length; i++) {
471 var others = [].concat(objectterms.slice(0, i),
472 objectterms.slice(i+1, objectterms.length));
473 results = results.concat(this.performObjectSearch(objectterms[i], others));
476 // lookup as search terms in fulltext
477 results = results.concat(this.performTermsSearch(searchterms, excluded, terms, titleterms));
479 // let the scorer override scores with a custom scoring function
481 for (i = 0; i < results.length; i++)
482 results[i][4] = Scorer.score(results[i]);
485 // now sort the results by score (in opposite order of appearance, since the
486 // display function below uses pop() to retrieve items) and then
488 results.sort(function(a, b) {
493 } else if (left < right) {
496 // same score: sort alphabetically
497 left = a[1].toLowerCase();
498 right = b[1].toLowerCase();
499 return (left > right) ? -1 : ((left < right) ? 1 : 0);
504 //Search.lastresults = results.slice(); // a copy
505 //console.info('search results:', Search.lastresults);
508 var resultCount = results.length;
509 function displayNextItem() {
510 // results left, load the summary and display it
511 if (results.length) {
512 var item = results.pop();
513 var listItem = $('<li style="display:none"></li>');
514 if (DOCUMENTATION_OPTIONS.FILE_SUFFIX === '') {
516 var dirname = item[0] + '/';
517 if (dirname.match(/\/index\/$/)) {
518 dirname = dirname.substring(0, dirname.length-6);
519 } else if (dirname == 'index/') {
522 listItem.append($('<a/>').attr('href',
523 DOCUMENTATION_OPTIONS.URL_ROOT + dirname +
524 highlightstring + item[2]).html(item[1]));
526 // normal html builders
527 listItem.append($('<a/>').attr('href',
528 item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX +
529 highlightstring + item[2]).html(item[1]));
532 listItem.append($('<span> (' + item[3] + ')</span>'));
533 Search.output.append(listItem);
534 listItem.slideDown(5, function() {
537 } else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {
538 $.ajax({url: DOCUMENTATION_OPTIONS.URL_ROOT + '_sources/' + item[0] + '.txt',
540 complete: function(jqxhr, textstatus) {
541 var data = jqxhr.responseText;
542 if (data !== '' && data !== undefined) {
543 listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));
545 Search.output.append(listItem);
546 listItem.slideDown(5, function() {
551 // no source available, just display title
552 Search.output.append(listItem);
553 listItem.slideDown(5, function() {
558 // search finished, update title and status message
561 Search.title.text(_('Search Results'));
563 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.'));
565 Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));
566 Search.status.fadeIn(500);
573 * search for object names
575 performObjectSearch : function(object, otherterms) {
576 var filenames = this._index.filenames;
577 var objects = this._index.objects;
578 var objnames = this._index.objnames;
579 var titles = this._index.titles;
584 for (var prefix in objects) {
585 for (var name in objects[prefix]) {
586 var fullname = (prefix ? prefix + '.' : '') + name;
587 if (fullname.toLowerCase().indexOf(object) > -1) {
589 var parts = fullname.split('.');
590 // check for different match types: exact matches of full name or
591 // "last name" (i.e. last dotted part)
592 if (fullname == object || parts[parts.length - 1] == object) {
593 score += Scorer.objNameMatch;
594 // matches in last name
595 } else if (parts[parts.length - 1].indexOf(object) > -1) {
596 score += Scorer.objPartialMatch;
598 var match = objects[prefix][name];
599 var objname = objnames[match[1]][2];
600 var title = titles[match[0]];
601 // If more than one term searched for, we require other words to be
602 // found in the name/title/description
603 if (otherterms.length > 0) {
604 var haystack = (prefix + ' ' + name + ' ' +
605 objname + ' ' + title).toLowerCase();
607 for (i = 0; i < otherterms.length; i++) {
608 if (haystack.indexOf(otherterms[i]) == -1) {
617 var descr = objname + _(', in ') + title;
619 var anchor = match[3];
622 else if (anchor == '-')
623 anchor = objnames[match[1]][1] + '-' + fullname;
624 // add custom score for some objects according to scorer
625 if (Scorer.objPrio.hasOwnProperty(match[2])) {
626 score += Scorer.objPrio[match[2]];
628 score += Scorer.objPrioDefault;
630 results.push([filenames[match[0]], fullname, '#'+anchor, descr, score]);
639 * search for full-text terms in the index
641 performTermsSearch : function(searchterms, excluded, terms, titleterms) {
642 var filenames = this._index.filenames;
643 var titles = this._index.titles;
650 // perform the search on the required terms
651 for (i = 0; i < searchterms.length; i++) {
652 var word = searchterms[i];
655 {files: terms[word], score: Scorer.term},
656 {files: titleterms[word], score: Scorer.title}
659 // no match but word was a required one
660 if ($u.every(_o, function(o){return o.files === undefined;})) {
663 // found search word in contents
664 $u.each(_o, function(o) {
665 var _files = o.files;
666 if (_files === undefined)
669 if (_files.length === undefined)
671 files = files.concat(_files);
673 // set score for the word in each file to Scorer.term
674 for (j = 0; j < _files.length; j++) {
676 if (!(file in scoreMap))
678 scoreMap[file][word] = o.score;
682 // create the mapping
683 for (j = 0; j < files.length; j++) {
686 fileMap[file].push(word);
688 fileMap[file] = [word];
692 // now check if the files don't contain excluded terms
693 for (file in fileMap) {
696 // check if all requirements are matched
697 if (fileMap[file].length != searchterms.length)
700 // ensure that none of the excluded terms is in the search result
701 for (i = 0; i < excluded.length; i++) {
702 if (terms[excluded[i]] == file ||
703 titleterms[excluded[i]] == file ||
704 $u.contains(terms[excluded[i]] || [], file) ||
705 $u.contains(titleterms[excluded[i]] || [], file)) {
711 // if we have still a valid result we can add it to the result list
713 // select one (max) score for the file.
714 // for better ranking, we should calculate ranking by using words statistics like basic tf-idf...
715 var score = $u.max($u.map(fileMap[file], function(w){return scoreMap[file][w]}));
716 results.push([filenames[file], titles[file], '', null, score]);
723 * helper function to return a node containing the
724 * search summary for a given text. keywords is a list
725 * of stemmed words, hlwords is the list of normal, unstemmed
726 * words. the first one is used to find the occurrence, the
727 * latter for highlighting it.
729 makeSearchSummary : function(text, keywords, hlwords) {
730 var textLower = text.toLowerCase();
732 $.each(keywords, function() {
733 var i = textLower.indexOf(this.toLowerCase());
737 start = Math.max(start - 120, 0);
738 var excerpt = ((start > 0) ? '...' : '') +
739 $.trim(text.substr(start, 240)) +
740 ((start + 240 - text.length) ? '...' : '');
741 var rv = $('<div class="context"></div>').text(excerpt);
742 $.each(hlwords, function() {
743 rv = rv.highlightText(this, 'highlighted');
749 $(document).ready(function() {