searchtools.js 16 KB

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