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());
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 $.ajax({url: DOCUMENTATION_OPTIONS.URL_ROOT + '_sources/' + item[5] + (item[5].slice(-suffix.length) === suffix ? '' : suffix),
545 complete: function(jqxhr, textstatus) {
546 var data = jqxhr.responseText;
547 if (data !== '' && data !== undefined) {
548 listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));
550 Search.output.append(listItem);
551 listItem.slideDown(5, function() {
556 // no source available, just display title
557 Search.output.append(listItem);
558 listItem.slideDown(5, function() {
563 // search finished, update title and status message
566 Search.title.text(_('Search Results'));
568 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.'));
570 Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));
571 Search.status.fadeIn(500);
578 * search for object names
580 performObjectSearch : function(object, otherterms) {
581 var filenames = this._index.filenames;
582 var docnames = this._index.docnames;
583 var objects = this._index.objects;
584 var objnames = this._index.objnames;
585 var titles = this._index.titles;
590 for (var prefix in objects) {
591 for (var name in objects[prefix]) {
592 var fullname = (prefix ? prefix + '.' : '') + name;
593 if (fullname.toLowerCase().indexOf(object) > -1) {
595 var parts = fullname.split('.');
596 // check for different match types: exact matches of full name or
597 // "last name" (i.e. last dotted part)
598 if (fullname == object || parts[parts.length - 1] == object) {
599 score += Scorer.objNameMatch;
600 // matches in last name
601 } else if (parts[parts.length - 1].indexOf(object) > -1) {
602 score += Scorer.objPartialMatch;
604 var match = objects[prefix][name];
605 var objname = objnames[match[1]][2];
606 var title = titles[match[0]];
607 // If more than one term searched for, we require other words to be
608 // found in the name/title/description
609 if (otherterms.length > 0) {
610 var haystack = (prefix + ' ' + name + ' ' +
611 objname + ' ' + title).toLowerCase();
613 for (i = 0; i < otherterms.length; i++) {
614 if (haystack.indexOf(otherterms[i]) == -1) {
623 var descr = objname + _(', in ') + title;
625 var anchor = match[3];
628 else if (anchor == '-')
629 anchor = objnames[match[1]][1] + '-' + fullname;
630 // add custom score for some objects according to scorer
631 if (Scorer.objPrio.hasOwnProperty(match[2])) {
632 score += Scorer.objPrio[match[2]];
634 score += Scorer.objPrioDefault;
636 results.push([docnames[match[0]], fullname, '#'+anchor, descr, score, filenames[match[0]]]);
645 * search for full-text terms in the index
647 performTermsSearch : function(searchterms, excluded, terms, titleterms) {
648 var docnames = this._index.docnames;
649 var filenames = this._index.filenames;
650 var titles = this._index.titles;
657 // perform the search on the required terms
658 for (i = 0; i < searchterms.length; i++) {
659 var word = searchterms[i];
662 {files: terms[word], score: Scorer.term},
663 {files: titleterms[word], score: Scorer.title}
666 // no match but word was a required one
667 if ($u.every(_o, function(o){return o.files === undefined;})) {
670 // found search word in contents
671 $u.each(_o, function(o) {
672 var _files = o.files;
673 if (_files === undefined)
676 if (_files.length === undefined)
678 files = files.concat(_files);
680 // set score for the word in each file to Scorer.term
681 for (j = 0; j < _files.length; j++) {
683 if (!(file in scoreMap))
685 scoreMap[file][word] = o.score;
689 // create the mapping
690 for (j = 0; j < files.length; j++) {
693 fileMap[file].push(word);
695 fileMap[file] = [word];
699 // now check if the files don't contain excluded terms
700 for (file in fileMap) {
703 // check if all requirements are matched
704 if (fileMap[file].length != searchterms.length)
707 // ensure that none of the excluded terms is in the search result
708 for (i = 0; i < excluded.length; i++) {
709 if (terms[excluded[i]] == file ||
710 titleterms[excluded[i]] == file ||
711 $u.contains(terms[excluded[i]] || [], file) ||
712 $u.contains(titleterms[excluded[i]] || [], file)) {
718 // if we have still a valid result we can add it to the result list
720 // select one (max) score for the file.
721 // for better ranking, we should calculate ranking by using words statistics like basic tf-idf...
722 var score = $u.max($u.map(fileMap[file], function(w){return scoreMap[file][w]}));
723 results.push([docnames[file], titles[file], '', null, score, filenames[file]]);
730 * helper function to return a node containing the
731 * search summary for a given text. keywords is a list
732 * of stemmed words, hlwords is the list of normal, unstemmed
733 * words. the first one is used to find the occurrence, the
734 * latter for highlighting it.
736 makeSearchSummary : function(text, keywords, hlwords) {
737 var textLower = text.toLowerCase();
739 $.each(keywords, function() {
740 var i = textLower.indexOf(this.toLowerCase());
744 start = Math.max(start - 120, 0);
745 var excerpt = ((start > 0) ? '...' : '') +
746 $.trim(text.substr(start, 240)) +
747 ((start + 240 - text.length) ? '...' : '');
748 var rv = $('<div class="context"></div>').text(excerpt);
749 $.each(hlwords, function() {
750 rv = rv.highlightText(this, 'highlighted');
756 $(document).ready(function() {