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.
 
 
 
 
 
 

425 lines
16 KiB

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