5 * Sphinx JavaScript utilities for the full-text search.
7 * :copyright: Copyright 2007-2018 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());
441 // prevent stemmer from cutting word smaller than two chars
442 if(word.length < 3 && tmp[i].length >= 3) {
446 // select the correct list
447 if (word[0] == '-') {
449 word = word.substr(1);
452 toAppend = searchterms;
453 hlterms.push(tmp[i].toLowerCase());
455 // only add if not already in the list
456 if (!$u.contains(toAppend, word))
459 var highlightstring = '?highlight=' + $.urlencode(hlterms.join(" "));
461 // console.debug('SEARCH: searching for:');
462 // console.info('required: ', searchterms);
463 // console.info('excluded: ', excluded);
466 var terms = this._index.terms;
467 var titleterms = this._index.titleterms;
469 // array of [filename, title, anchor, descr, score]
471 $('#search-progress').empty();
474 for (i = 0; i < objectterms.length; i++) {
475 var others = [].concat(objectterms.slice(0, i),
476 objectterms.slice(i+1, objectterms.length));
477 results = results.concat(this.performObjectSearch(objectterms[i], others));
480 // lookup as search terms in fulltext
481 results = results.concat(this.performTermsSearch(searchterms, excluded, terms, titleterms));
483 // let the scorer override scores with a custom scoring function
485 for (i = 0; i < results.length; i++)
486 results[i][4] = Scorer.score(results[i]);
489 // now sort the results by score (in opposite order of appearance, since the
490 // display function below uses pop() to retrieve items) and then
492 results.sort(function(a, b) {
497 } else if (left < right) {
500 // same score: sort alphabetically
501 left = a[1].toLowerCase();
502 right = b[1].toLowerCase();
503 return (left > right) ? -1 : ((left < right) ? 1 : 0);
508 //Search.lastresults = results.slice(); // a copy
509 //console.info('search results:', Search.lastresults);
512 var resultCount = results.length;
513 function displayNextItem() {
514 // results left, load the summary and display it
515 if (results.length) {
516 var item = results.pop();
517 var listItem = $('<li style="display:none"></li>');
518 if (DOCUMENTATION_OPTIONS.FILE_SUFFIX === '') {
520 var dirname = item[0] + '/';
521 if (dirname.match(/\/index\/$/)) {
522 dirname = dirname.substring(0, dirname.length-6);
523 } else if (dirname == 'index/') {
526 listItem.append($('<a/>').attr('href',
527 DOCUMENTATION_OPTIONS.URL_ROOT + dirname +
528 highlightstring + item[2]).html(item[1]));
530 // normal html builders
531 listItem.append($('<a/>').attr('href',
532 item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX +
533 highlightstring + item[2]).html(item[1]));
536 listItem.append($('<span> (' + item[3] + ')</span>'));
537 Search.output.append(listItem);
538 listItem.slideDown(5, function() {
541 } else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {
542 var suffix = DOCUMENTATION_OPTIONS.SOURCELINK_SUFFIX;
543 if (suffix === undefined) {
546 $.ajax({url: DOCUMENTATION_OPTIONS.URL_ROOT + '_sources/' + item[5] + (item[5].slice(-suffix.length) === suffix ? '' : suffix),
548 complete: function(jqxhr, textstatus) {
549 var data = jqxhr.responseText;
550 if (data !== '' && data !== undefined) {
551 listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));
553 Search.output.append(listItem);
554 listItem.slideDown(5, function() {
559 // no source available, just display title
560 Search.output.append(listItem);
561 listItem.slideDown(5, function() {
566 // search finished, update title and status message
569 Search.title.text(_('Search Results'));
571 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.'));
573 Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));
574 Search.status.fadeIn(500);
581 * search for object names
583 performObjectSearch : function(object, otherterms) {
584 var filenames = this._index.filenames;
585 var docnames = this._index.docnames;
586 var objects = this._index.objects;
587 var objnames = this._index.objnames;
588 var titles = this._index.titles;
593 for (var prefix in objects) {
594 for (var name in objects[prefix]) {
595 var fullname = (prefix ? prefix + '.' : '') + name;
596 if (fullname.toLowerCase().indexOf(object) > -1) {
598 var parts = fullname.split('.');
599 // check for different match types: exact matches of full name or
600 // "last name" (i.e. last dotted part)
601 if (fullname == object || parts[parts.length - 1] == object) {
602 score += Scorer.objNameMatch;
603 // matches in last name
604 } else if (parts[parts.length - 1].indexOf(object) > -1) {
605 score += Scorer.objPartialMatch;
607 var match = objects[prefix][name];
608 var objname = objnames[match[1]][2];
609 var title = titles[match[0]];
610 // If more than one term searched for, we require other words to be
611 // found in the name/title/description
612 if (otherterms.length > 0) {
613 var haystack = (prefix + ' ' + name + ' ' +
614 objname + ' ' + title).toLowerCase();
616 for (i = 0; i < otherterms.length; i++) {
617 if (haystack.indexOf(otherterms[i]) == -1) {
626 var descr = objname + _(', in ') + title;
628 var anchor = match[3];
631 else if (anchor == '-')
632 anchor = objnames[match[1]][1] + '-' + fullname;
633 // add custom score for some objects according to scorer
634 if (Scorer.objPrio.hasOwnProperty(match[2])) {
635 score += Scorer.objPrio[match[2]];
637 score += Scorer.objPrioDefault;
639 results.push([docnames[match[0]], fullname, '#'+anchor, descr, score, filenames[match[0]]]);
648 * search for full-text terms in the index
650 performTermsSearch : function(searchterms, excluded, terms, titleterms) {
651 var docnames = this._index.docnames;
652 var filenames = this._index.filenames;
653 var titles = this._index.titles;
660 // perform the search on the required terms
661 for (i = 0; i < searchterms.length; i++) {
662 var word = searchterms[i];
665 {files: terms[word], score: Scorer.term},
666 {files: titleterms[word], score: Scorer.title}
669 // no match but word was a required one
670 if ($u.every(_o, function(o){return o.files === undefined;})) {
673 // found search word in contents
674 $u.each(_o, function(o) {
675 var _files = o.files;
676 if (_files === undefined)
679 if (_files.length === undefined)
681 files = files.concat(_files);
683 // set score for the word in each file to Scorer.term
684 for (j = 0; j < _files.length; j++) {
686 if (!(file in scoreMap))
688 scoreMap[file][word] = o.score;
692 // create the mapping
693 for (j = 0; j < files.length; j++) {
696 fileMap[file].push(word);
698 fileMap[file] = [word];
702 // now check if the files don't contain excluded terms
703 for (file in fileMap) {
706 // check if all requirements are matched
707 if (fileMap[file].length != searchterms.length)
710 // ensure that none of the excluded terms is in the search result
711 for (i = 0; i < excluded.length; i++) {
712 if (terms[excluded[i]] == file ||
713 titleterms[excluded[i]] == file ||
714 $u.contains(terms[excluded[i]] || [], file) ||
715 $u.contains(titleterms[excluded[i]] || [], file)) {
721 // if we have still a valid result we can add it to the result list
723 // select one (max) score for the file.
724 // for better ranking, we should calculate ranking by using words statistics like basic tf-idf...
725 var score = $u.max($u.map(fileMap[file], function(w){return scoreMap[file][w]}));
726 results.push([docnames[file], titles[file], '', null, score, filenames[file]]);
733 * helper function to return a node containing the
734 * search summary for a given text. keywords is a list
735 * of stemmed words, hlwords is the list of normal, unstemmed
736 * words. the first one is used to find the occurrence, the
737 * latter for highlighting it.
739 makeSearchSummary : function(text, keywords, hlwords) {
740 var textLower = text.toLowerCase();
742 $.each(keywords, function() {
743 var i = textLower.indexOf(this.toLowerCase());
747 start = Math.max(start - 120, 0);
748 var excerpt = ((start > 0) ? '...' : '') +
749 $.trim(text.substr(start, 240)) +
750 ((start + 240 - text.length) ? '...' : '');
751 var rv = $('<div class="context"></div>').text(excerpt);
752 $.each(hlwords, function() {
753 rv = rv.highlightText(this, 'highlighted');
759 $(document).ready(function() {