A semantic search engine for source code https://bitshift.benkurtovic.com/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

10 年之前
10 年之前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. /*
  2. * @file Manages all library initialization, jQuery callbacks, query entry
  3. * callbacks, server querying, and results diplay for `index.html`.
  4. */
  5. var advancedSearchDiv = $("div#advanced-search");
  6. var advancedSearchButton = $("button#advanced-search");
  7. FINISH_TYPING_INTERVAL = 650;
  8. var searchBar = $("form#search-bar input[type='text']")[0];
  9. var resultsDiv = $("div#results")[0];
  10. var typingTimer, scrollTimer, lastValue;
  11. var searchResultsPage = 1;
  12. /*
  13. * Set all page callbacks.
  14. */
  15. (function setHomePageCallbabacks(){
  16. var results = $('#results').get(0);
  17. // Enable infinite scrolling down the results page.
  18. $(window).scroll(function(){
  19. if($(window).scrollTop() + $(window).height() == $(document).height() &&
  20. resultsDiv.querySelectorAll(".result").length > 0)
  21. loadMoreResults();
  22. clearTimeout(scrollTimer);
  23. if (!results.classList.contains('disable-hover'))
  24. results.classList.add('disable-hover')
  25. scrollTimer = setTimeout(function(){
  26. if (results.classList.contains('disable-hover'))
  27. results.classList.remove('disable-hover');
  28. }, 200);
  29. });
  30. // Toggle the advanced-search form's visibility.
  31. advancedSearchButton.click(function(){
  32. var searchField = $("div#search-field");
  33. if(!advancedSearchDiv.hasClass("visible")){
  34. searchField.addClass("partly-visible");
  35. advancedSearchDiv.fadeIn(500).addClass("visible");
  36. advancedSearchButton.addClass("clicked");
  37. }
  38. else {
  39. advancedSearchDiv.fadeOut(300).removeClass("visible");
  40. advancedSearchButton.removeClass("clicked");
  41. if($("div#results .result").length == 0)
  42. searchField.removeClass("partly-visible");
  43. }
  44. });
  45. // Enable capturing the `enter` key.
  46. $("form#search-bar").submit(function(event){
  47. event.preventDefault();
  48. return false;
  49. });
  50. searchBar.onkeyup = typingTimer;
  51. }());
  52. /*
  53. * Set keyboard shortcut mappings.
  54. */
  55. (function resultsHotkeys(){
  56. /*
  57. * If the currently viewed result is not the first, scroll to the previous
  58. * result.
  59. */
  60. var previousResult = function(){
  61. var currResult = $(".display-all");
  62. if(currResult.length) {
  63. currResult.removeClass("display-all");
  64. currResult = currResult.closest(".result").prev(".result");
  65. } else {
  66. currResult = $(document.querySelectorAll(".result")[0]);
  67. }
  68. currResult.addClass("display-all");
  69. currResult.each(function(){
  70. $('html,body').stop().animate({
  71. scrollTop: $(this).offset().top - (
  72. $(window).height() - $(this).outerHeight(true)) / 2
  73. }, 140);
  74. });
  75. };
  76. /*
  77. * If the currently viewed result is not the last, scroll to the next
  78. * result.
  79. */
  80. var nextResult = function(){
  81. var currResult = $(".display-all");
  82. if(currResult.length) {
  83. currResult.removeClass("display-all");
  84. currResult = currResult.closest(".result").next(".result");
  85. } else {
  86. currResult = $(document.querySelectorAll(".result")[0]);
  87. }
  88. currResult.addClass('display-all');
  89. currResult.each(function(){
  90. $('html,body').stop().animate({
  91. scrollTop: $(this).offset().top - (
  92. $(window).height() - $(this).outerHeight(true)) / 2
  93. }, 140);
  94. });
  95. };
  96. var displayHotkeyHelp = function(){
  97. var help = $("div#hotkey-help");
  98. console.log("H");
  99. if(help.hasClass("hidden"))
  100. help.fadeIn(420);
  101. else
  102. help.fadeOut(420);
  103. $("div#body").toggleClass("faded");
  104. help.toggleClass("hidden");
  105. }
  106. var hotkeyActions = {
  107. "k" : previousResult,
  108. "j" : nextResult,
  109. "h" : previousSymbolMatch,
  110. "l" : nextSymbolMatch,
  111. "?" : displayHotkeyHelp
  112. };
  113. $(window).keypress(function(key){
  114. for(var hotkey in hotkeyActions){
  115. var keyChar = String.fromCharCode(key.keyCode);
  116. if(keyChar == hotkey &&
  117. !($(key.target).is("textarea") || $(key.target).is("input")))
  118. hotkeyActions[keyChar]();
  119. }
  120. });
  121. }());
  122. //Obtained by parsing python file with pygments
  123. var codeExample = '<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre> 1\n 2\n 3\n 4\n 5\n 6\n 7\n 8\n 9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40</pre></div></td><td class="code"><div class="highlight"><pre><span class="sd">&quot;&quot;&quot;</span>\n<span class="sd">Module to contain all the project&#39;s Flask server plumbing.</span>\n<span class="sd">&quot;&quot;&quot;</span>\n\n<span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span>\n<span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">render_template</span><span class="p">,</span> <span class="n">session</span>\n\n<span class="kn">from</span> <span class="nn">bitshift</span> <span class="kn">import</span> <span class="n">assets</span>\n<span class="c"># from bitshift.database import Database</span>\n<span class="c"># from bitshift.query import parse_query</span>\n\n<span class="hll"><span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>\n</span><span class="hll"><span class="n">app</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">from_object</span><span class="p">(</span><span class="s">&quot;bitshift.config&quot;</span><span class="p">)</span>\n</span>\n<span class="hll"><span class="n">app_env</span> <span class="o">=</span> <span class="n">app</span><span class="o">.</span><span class="n">jinja_env</span>\n</span><span class="hll"><span class="n">app_env</span><span class="o">.</span><span class="n">line_statement_prefix</span> <span class="o">=</span> <span class="s">&quot;=&quot;</span>\n</span><span class="hll"><span class="n">app_env</span><span class="o">.</span><span class="n">globals</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">assets</span><span class="o">=</span><span class="n">assets</span><span class="p">)</span>\n</span>\n<span class="c"># database = Database()</span>\n\n<span class="hll"><span class="nd">@app.route</span><span class="p">(</span><span class="s">&quot;/&quot;</span><span class="p">)</span>\n</span><span class="k">def</span> <span class="nf">index</span><span class="p">():</span>\n <span class="k">return</span> <span class="n">render_template</span><span class="p">(</span><span class="s">&quot;index.html&quot;</span><span class="p">)</span>\n\n<span class="hll"><span class="nd">@app.route</span><span class="p">(</span><span class="s">&quot;/search/&lt;query&gt;&quot;</span><span class="p">)</span>\n</span><span class="k">def</span> <span class="nf">search</span><span class="p">(</span><span class="n">query</span><span class="p">):</span>\n <span class="c"># tree = parse_query(query)</span>\n <span class="c"># database.search(tree)</span>\n <span class="k">pass</span>\n\n<span class="hll"><span class="nd">@app.route</span><span class="p">(</span><span class="s">&quot;/about&quot;</span><span class="p">)</span>\n</span><span class="k">def</span> <span class="nf">about</span><span class="p">():</span>\n <span class="k">return</span> <span class="n">render_template</span><span class="p">(</span><span class="s">&quot;about.html&quot;</span><span class="p">)</span>\n\n<span class="hll"><span class="nd">@app.route</span><span class="p">(</span><span class="s">&quot;/developers&quot;</span><span class="p">)</span>\n</span><span class="k">def</span> <span class="nf">developers</span><span class="p">():</span>\n <span class="k">return</span> <span class="n">render_template</span><span class="p">(</span><span class="s">&quot;developers.html&quot;</span><span class="p">)</span>\n\n<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">&quot;__main__&quot;</span><span class="p">:</span>\n<span class="hll"> <span class="n">app</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">debug</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>\n</span></pre></div>\n</td></tr></table>'
  124. var testCodelet = {
  125. 'url': 'https://github.com/earwig/bitshift/blob/develop/app.py',
  126. 'filename': 'app.py',
  127. 'language': 'python',
  128. 'date_created': 'May 10, 2014',
  129. 'date_modified': '2 days ago',
  130. 'origin': ['GitHub', 'https://github.com', ''],
  131. 'authors': ['sevko', 'earwig'],
  132. 'html_code': codeExample
  133. };
  134. // Enable infinite scrolling down the results page.
  135. $(window).scroll(function() {
  136. var searchField = $("div#search-field");
  137. if($(window).scrollTop() + $(window).height() == $(document).height() &&
  138. searchField.hasClass('partly-visible')){
  139. loadMoreResults();
  140. }
  141. });
  142. /*
  143. * Clear the existing timer and set a new one the the user types text into the
  144. * search bar.
  145. */
  146. function typingTimer(event){
  147. clearTimeout(typingTimer);
  148. var enterKeyCode = 13;
  149. if(event.keyCode != enterKeyCode){
  150. if(lastValue != searchBar.value)
  151. typingTimer = setTimeout(finishedTyping, FINISH_TYPING_INTERVAL);
  152. }
  153. else {
  154. event.preventDefault();
  155. finishedTyping();
  156. return false;
  157. }
  158. };
  159. /*
  160. * Callback which queries the server whenver the user stops typing.
  161. *
  162. * Whenever the user doesn't type for a `FINISH_TYPING_INTERVAL` after having
  163. * entered new text in the search bar, send the current query request to the
  164. * server.
  165. */
  166. function finishedTyping(){
  167. lastValue = searchBar.value;
  168. var searchField = $("div#search-field");
  169. clearResults();
  170. if(searchBar.value){
  171. searchField.addClass("partly-visible");
  172. populateResults();
  173. }
  174. else {
  175. searchField.removeClass("partly-visible");
  176. $("div#advanced-search").fadeOut(50);
  177. advancedSearchButton.removeClass("clicked");
  178. }
  179. }
  180. /*
  181. * Removes any child elements of `div#results`.
  182. */
  183. function clearResults(){
  184. while(resultsDiv.firstChild)
  185. resultsDiv.removeChild(resultsDiv.firstChild);
  186. }
  187. /*
  188. * Query the server with the current search string, and populate `div#results`
  189. * with its response.
  190. */
  191. function populateResults(){
  192. searchResultsPage = 1;
  193. var results = queryServer();
  194. for(var result = 0; result < results.length; result++){
  195. var newDiv = results[result];
  196. resultsDiv.appendChild(newDiv);
  197. setTimeout(
  198. (function(divReference){
  199. return function(){
  200. divReference.classList.add("cascade");
  201. };
  202. }(newDiv)), result * 20);
  203. }
  204. }
  205. /*
  206. * Create a result element based upon a codelet instance.
  207. *
  208. * @return {Element} The result element.
  209. */
  210. function createResult(codelet) {
  211. //Level 1
  212. var newDiv = document.createElement("div"),
  213. table = document.createElement("table"),
  214. row = document.createElement("tr");
  215. //Level 2
  216. var displayInfo = document.createElement("div"),
  217. codeElt = document.createElement("td"),
  218. hiddenInfoContainer = document.createElement("td"),
  219. hiddenInfo = document.createElement("div"),
  220. cycle = document.createElement("div");
  221. //Level 3
  222. var title = document.createElement("span"),
  223. site = document.createElement("span"),
  224. nextMatch = document.createElement("a"),
  225. prevMatch = document.createElement("a"),
  226. dateModified = document.createElement("div"),
  227. language = document.createElement("div"),
  228. dateCreated = document.createElement("div"),
  229. authors = document.createElement("div");
  230. //Classes and ID's
  231. newDiv.classList.add('result');
  232. displayInfo.id = 'display-info';
  233. codeElt.id = 'code';
  234. hiddenInfo.id = 'hidden-info';
  235. cycle.id = 'cycle-matches'
  236. title.id = 'title';
  237. site.id = 'site';
  238. nextMatch.id = 'next-match';
  239. nextMatch.href = '#';
  240. prevMatch.id = 'prev-match';
  241. prevMatch.href = '#';
  242. dateModified.id = 'date-modified';
  243. language.id = 'language';
  244. dateCreated.id = 'date-created';
  245. authors.id = 'authors';
  246. //Add the bulk of the html
  247. title.innerHTML = ' &raquo; <a href="' + codelet.url + '">'
  248. + codelet.filename + '</a>';
  249. site.innerHTML = '<a href="' + codelet.origin[1] + '">' + codelet.origin[0] +'</a>';
  250. nextMatch.innerHTML = 'next match';
  251. prevMatch.innerHTML = 'prev match';
  252. language.innerHTML = 'Language: <span>' + codelet.language + '</span>';
  253. dateModified.innerHTML = 'Last modified: <span>' + codelet.date_modified + '</span>';
  254. // Needs to be changed from int to string on the server
  255. dateCreated.innerHTML = 'Created: <span>' + codelet.date_created + '</span>';
  256. var authorsHtml = 'Authors: <span>';
  257. codelet.authors.forEach(function(a, i) {
  258. if (i == codelet.authors.length - 1)
  259. authorsHtml += '<a href=#>' + a + ' </a>';
  260. else
  261. authorsHtml += '<a href=#>' + a + ' </a>, ';
  262. });
  263. authors.innerHTML = authorsHtml;
  264. // Needs to be processed on the server
  265. codeElt.innerHTML = '<div id=tablecontainer>' + codelet.html_code + '</div>';
  266. //Event binding
  267. $(newDiv).on('mousemove', function(e) {
  268. var holdCondition = $('.disable-hover');
  269. if(holdCondition.length == 0) {
  270. $(this).siblings().removeClass('display-all');
  271. $(this).addClass('display-all');
  272. }
  273. });
  274. $(newDiv).on('mouseleave', function(e) {
  275. var holdCondition = $('.disable-hover');
  276. if(holdCondition.length == 0)
  277. $(this).removeClass('display-all');
  278. });
  279. $(nextMatch).click(function(e) {
  280. e.stopPropagation();
  281. e.preventDefault();
  282. nextSymbolMatch();
  283. });
  284. $(prevMatch).click(function(e) {
  285. e.stopPropagation();
  286. e.preventDefault();
  287. previousSymbolMatch();
  288. });
  289. //Finish and append elements to parent elements
  290. hiddenInfo.appendChild(dateCreated);
  291. hiddenInfo.appendChild(dateModified);
  292. hiddenInfo.appendChild(language);
  293. hiddenInfo.appendChild(authors);
  294. hiddenInfoContainer.appendChild(hiddenInfo);
  295. row.appendChild(codeElt);
  296. row.appendChild(hiddenInfoContainer);
  297. table.appendChild(row);
  298. displayInfo.appendChild(site);
  299. displayInfo.appendChild(title);
  300. cycle.appendChild(prevMatch);
  301. cycle.appendChild(nextMatch);
  302. newDiv.appendChild(displayInfo);
  303. newDiv.appendChild(table);
  304. return newDiv;
  305. }
  306. function previousSymbolMatch() {
  307. var currResult = $(".display-all"),
  308. currMatch = currResult.find(".hll.current"),
  309. matches = currResult.find(".hll"),
  310. scrollDiv = currResult.find("#tablecontainer");
  311. if (currMatch.length == 0)
  312. currMatch = matches[0];
  313. else
  314. currMatch.removeClass('current');
  315. var index = matches.index(currMatch.get(0)) - 1;
  316. index = index <= 0 ? matches.length - 1 : index;
  317. var newMatch = $(matches[index]);
  318. scrollDiv.scrollTop(scrollDiv.scrollTop()
  319. - scrollDiv.height() / 2
  320. + newMatch.position().top + newMatch.height() / 2);
  321. newMatch.effect("highlight", {color: '#FFF'}, 750)
  322. newMatch.addClass('current');
  323. };
  324. function nextSymbolMatch() {
  325. var currResult = $(".display-all"),
  326. currMatch = currResult.find(".hll.current"),
  327. matches = currResult.find(".hll"),
  328. scrollDiv = currResult.find("#tablecontainer");
  329. if (currMatch.length == 0)
  330. currMatch = $(matches[0]);
  331. else
  332. currMatch.removeClass("current");
  333. var index = matches.index(currMatch.get(0)) + 1;
  334. index = index >= matches.length ? 0 : index;
  335. var newMatch = $(matches[index]);
  336. scrollDiv.scrollTop(scrollDiv.scrollTop()
  337. - scrollDiv.height() / 2
  338. + newMatch.position().top + newMatch.height() / 2);
  339. newMatch.effect("highlight", {color: "#FFF"}, 750)
  340. newMatch.addClass("current");
  341. };
  342. /*
  343. * AJAX the current query string to the server, and return its response.
  344. *
  345. * @return {Array} The server's response in the form of `div.result` DOM
  346. * elements, to fill `div#results`.
  347. */
  348. function queryServer(){
  349. var queryUrl = document.URL + "search.json?" + $.param({
  350. "q" : searchBar.value,
  351. "p" : searchResultsPage++
  352. });
  353. var resultDivs = [];
  354. $.getJSON(queryUrl, function(result){
  355. if("error" in result)
  356. insertErrorMessage(result["error"]);
  357. else
  358. for(var codelet = 0; codelet < result["results"].length; codelet++)
  359. resultDivs.push(result["results"][codelet]);
  360. });
  361. for(var result = 0; result < 20; result++){
  362. var newDiv = createResult(testCodelet);
  363. resultDivs.push(newDiv)
  364. }
  365. return resultDivs;
  366. }
  367. /*
  368. * Adds more results to `div#results`.
  369. */
  370. function loadMoreResults(){
  371. var results = queryServer();
  372. for(var result = 0; result < results.length; result++){
  373. var newDiv = results[result];
  374. resultsDiv.appendChild(newDiv);
  375. setTimeout(
  376. (function(divReference){
  377. return function(){
  378. divReference.classList.add("cascade");
  379. };
  380. }(newDiv)),
  381. result * 20);
  382. }
  383. }
  384. /*
  385. * Displays a warning message in the UI.
  386. *
  387. * @param msg (str) The message string.
  388. */
  389. function insertErrorMessage(msg){
  390. var error = $("<div id='error'><span>Error: </span></div>");
  391. error.append(msg);
  392. resultsDiv.appendChild(error[0]);
  393. }
  394. // loadMoreResults();