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