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.
 
 
 
 
 
 

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