/src/pyechonest/doc/build/html/_static/searchtools.js

http://echo-nest-remix.googlecode.com/ · JavaScript · 518 lines · 397 code · 52 blank · 69 comment · 89 complexity · 2d2d54448ebb4ce62716b0a8fbb11dbe MD5 · raw file

  1. /*
  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. * 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 PorterStemmer = 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',
  270. 'if', 'for', 'no', 'there', 'their', 'was', 'is',
  271. 'be', 'to', 'that', 'but', 'they', 'not', 'such',
  272. 'with', 'by', 'a', 'on', 'these', 'of', 'will',
  273. 'this', 'near', 'the', 'or', 'at'];
  274. // stem the searchterms and add them to the correct list
  275. var stemmer = new PorterStemmer();
  276. var searchterms = [];
  277. var excluded = [];
  278. var hlterms = [];
  279. var tmp = query.split(/\s+/);
  280. var object = (tmp.length == 1) ? tmp[0].toLowerCase() : null;
  281. for (var i = 0; i < tmp.length; i++) {
  282. if ($u.indexOf(stopwords, tmp[i]) != -1 || tmp[i].match(/^\d+$/) ||
  283. tmp[i] == "") {
  284. // skip this "word"
  285. continue;
  286. }
  287. // stem the word
  288. var word = stemmer.stemWord(tmp[i]).toLowerCase();
  289. // select the correct list
  290. if (word[0] == '-') {
  291. var toAppend = excluded;
  292. word = word.substr(1);
  293. }
  294. else {
  295. var toAppend = searchterms;
  296. hlterms.push(tmp[i].toLowerCase());
  297. }
  298. // only add if not already in the list
  299. if (!$.contains(toAppend, word))
  300. toAppend.push(word);
  301. };
  302. var highlightstring = '?highlight=' + $.urlencode(hlterms.join(" "));
  303. // console.debug('SEARCH: searching for:');
  304. // console.info('required: ', searchterms);
  305. // console.info('excluded: ', excluded);
  306. // prepare search
  307. var filenames = this._index.filenames;
  308. var titles = this._index.titles;
  309. var terms = this._index.terms;
  310. var objects = this._index.objects;
  311. var objtypes = this._index.objtypes;
  312. var objnames = this._index.objnames;
  313. var fileMap = {};
  314. var files = null;
  315. // different result priorities
  316. var importantResults = [];
  317. var objectResults = [];
  318. var regularResults = [];
  319. var unimportantResults = [];
  320. $('#search-progress').empty();
  321. // lookup as object
  322. if (object != null) {
  323. for (var prefix in objects) {
  324. for (var name in objects[prefix]) {
  325. var fullname = (prefix ? prefix + '.' : '') + name;
  326. if (fullname.toLowerCase().indexOf(object) > -1) {
  327. match = objects[prefix][name];
  328. descr = objnames[match[1]] + _(', in ') + titles[match[0]];
  329. // XXX the generated anchors are not generally correct
  330. // XXX there may be custom prefixes
  331. result = [filenames[match[0]], fullname, '#'+fullname, descr];
  332. switch (match[2]) {
  333. case 1: objectResults.push(result); break;
  334. case 0: importantResults.push(result); break;
  335. case 2: unimportantResults.push(result); break;
  336. }
  337. }
  338. }
  339. }
  340. }
  341. // sort results descending
  342. objectResults.sort(function(a, b) {
  343. return (a[1] > b[1]) ? -1 : ((a[1] < b[1]) ? 1 : 0);
  344. });
  345. importantResults.sort(function(a, b) {
  346. return (a[1] > b[1]) ? -1 : ((a[1] < b[1]) ? 1 : 0);
  347. });
  348. unimportantResults.sort(function(a, b) {
  349. return (a[1] > b[1]) ? -1 : ((a[1] < b[1]) ? 1 : 0);
  350. });
  351. // perform the search on the required terms
  352. for (var i = 0; i < searchterms.length; i++) {
  353. var word = searchterms[i];
  354. // no match but word was a required one
  355. if ((files = terms[word]) == null)
  356. break;
  357. if (files.length == undefined) {
  358. files = [files];
  359. }
  360. // create the mapping
  361. for (var j = 0; j < files.length; j++) {
  362. var file = files[j];
  363. if (file in fileMap)
  364. fileMap[file].push(word);
  365. else
  366. fileMap[file] = [word];
  367. }
  368. }
  369. // now check if the files don't contain excluded terms
  370. for (var file in fileMap) {
  371. var valid = true;
  372. // check if all requirements are matched
  373. if (fileMap[file].length != searchterms.length)
  374. continue;
  375. // ensure that none of the excluded terms is in the
  376. // search result.
  377. for (var i = 0; i < excluded.length; i++) {
  378. if (terms[excluded[i]] == file ||
  379. $.contains(terms[excluded[i]] || [], file)) {
  380. valid = false;
  381. break;
  382. }
  383. }
  384. // if we have still a valid result we can add it
  385. // to the result list
  386. if (valid)
  387. regularResults.push([filenames[file], titles[file], '', null]);
  388. }
  389. // delete unused variables in order to not waste
  390. // memory until list is retrieved completely
  391. delete filenames, titles, terms;
  392. // now sort the regular results descending by title
  393. regularResults.sort(function(a, b) {
  394. var left = a[1].toLowerCase();
  395. var right = b[1].toLowerCase();
  396. return (left > right) ? -1 : ((left < right) ? 1 : 0);
  397. });
  398. // combine all results
  399. var results = unimportantResults.concat(regularResults)
  400. .concat(objectResults).concat(importantResults);
  401. // print the results
  402. var resultCount = results.length;
  403. function displayNextItem() {
  404. // results left, load the summary and display it
  405. if (results.length) {
  406. var item = results.pop();
  407. var listItem = $('<li style="display:none"></li>');
  408. if (DOCUMENTATION_OPTIONS.FILE_SUFFIX == '') {
  409. // dirhtml builder
  410. var dirname = item[0] + '/';
  411. if (dirname.match(/\/index\/$/)) {
  412. dirname = dirname.substring(0, dirname.length-6);
  413. } else if (dirname == 'index/') {
  414. dirname = '';
  415. }
  416. listItem.append($('<a/>').attr('href',
  417. DOCUMENTATION_OPTIONS.URL_ROOT + dirname +
  418. highlightstring + item[2]).html(item[1]));
  419. } else {
  420. // normal html builders
  421. listItem.append($('<a/>').attr('href',
  422. item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX +
  423. highlightstring + item[2]).html(item[1]));
  424. }
  425. if (item[3]) {
  426. listItem.append($('<span> (' + item[3] + ')</span>'));
  427. Search.output.append(listItem);
  428. listItem.slideDown(5, function() {
  429. displayNextItem();
  430. });
  431. } else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {
  432. $.get(DOCUMENTATION_OPTIONS.URL_ROOT + '_sources/' +
  433. item[0] + '.txt', function(data) {
  434. if (data != '') {
  435. listItem.append($.makeSearchSummary(data, searchterms, hlterms));
  436. Search.output.append(listItem);
  437. }
  438. listItem.slideDown(5, function() {
  439. displayNextItem();
  440. });
  441. });
  442. } else {
  443. // no source available, just display title
  444. Search.output.append(listItem);
  445. listItem.slideDown(5, function() {
  446. displayNextItem();
  447. });
  448. }
  449. }
  450. // search finished, update title and status message
  451. else {
  452. Search.stopPulse();
  453. Search.title.text(_('Search Results'));
  454. if (!resultCount)
  455. 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.'));
  456. else
  457. Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));
  458. Search.status.fadeIn(500);
  459. }
  460. }
  461. displayNextItem();
  462. }
  463. }
  464. $(document).ready(function() {
  465. Search.init();
  466. });