]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/static/main.js
auto merge of #11760 : dmac/rust/addressable-search, r=alexcrichton
[rust.git] / src / librustdoc / html / static / main.js
1 // Copyright 2014 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
4 //
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
10
11 /*jslint browser: true, es5: true */
12 /*globals $: true, searchIndex: true, rootPath: true, allPaths: true */
13
14 (function() {
15     "use strict";
16     var resizeTimeout, interval;
17
18     $('.js-only').removeClass('js-only');
19
20     function getQueryStringParams() {
21         var params = {};
22         window.location.search.substring(1).split("&").
23             map(function(s) {
24                 var pair = s.split("=");
25                 params[decodeURIComponent(pair[0])] =
26                     typeof pair[1] === "undefined" ? null : decodeURIComponent(pair[1]);
27             });
28         return params;
29     }
30
31     function browserSupportsHistoryApi() {
32         return window.history && typeof window.history.pushState === "function";
33     }
34
35     function resizeShortBlocks() {
36         if (resizeTimeout) {
37             clearTimeout(resizeTimeout);
38         }
39         resizeTimeout = setTimeout(function() {
40             var contentWidth = $('.content').width();
41             $('.docblock.short').width(function() {
42                 return contentWidth - 40 - $(this).prev().width();
43             }).addClass('nowrap');
44         }, 150);
45     }
46     resizeShortBlocks();
47     $(window).on('resize', resizeShortBlocks);
48
49     function highlightSourceLines() {
50         var i, from, to, match = window.location.hash.match(/^#?(\d+)(?:-(\d+))?$/);
51         if (match) {
52             from = parseInt(match[1], 10);
53             to = Math.min(50000, parseInt(match[2] || match[1], 10));
54             from = Math.min(from, to);
55             if ($('#' + from).length === 0) {
56                 return;
57             }
58             $('#' + from)[0].scrollIntoView();
59             $('.line-numbers span').removeClass('line-highlighted');
60             for (i = from; i <= to; i += 1) {
61                 $('#' + i).addClass('line-highlighted');
62             }
63         }
64     }
65     highlightSourceLines();
66     $(window).on('hashchange', highlightSourceLines);
67
68     $(document).on('keyup', function(e) {
69         if (document.activeElement.tagName === 'INPUT') {
70             return;
71         }
72
73         if (e.keyCode === 188 && $('#help').hasClass('hidden')) { // question mark
74             e.preventDefault();
75             $('#help').removeClass('hidden');
76         } else if (e.keyCode === 27) { // esc
77             if (!$('#help').hasClass('hidden')) {
78                 e.preventDefault();
79                 $('#help').addClass('hidden');
80             } else if (!$('#search').hasClass('hidden')) {
81                 e.preventDefault();
82                 $('#search').addClass('hidden');
83                 $('#main').removeClass('hidden');
84             }
85         } else if (e.keyCode === 83) { // S
86             e.preventDefault();
87             $('.search-input').focus();
88         }
89     }).on('click', function(e) {
90         if (!$(e.target).closest('#help').length) {
91             $('#help').addClass('hidden');
92         }
93     });
94
95     $('.version-selector').on('change', function() {
96         var i, match,
97             url = document.location.href,
98             stripped = '',
99             len = rootPath.match(/\.\.\//g).length + 1;
100
101         for (i = 0; i < len; i += 1) {
102             match = url.match(/\/[^\/]*$/);
103             if (i < len - 1) {
104                 stripped = match[0] + stripped;
105             }
106             url = url.substring(0, url.length - match[0].length);
107         }
108
109         url += '/' + $('.version-selector').val() + stripped;
110
111         document.location.href = url;
112     });
113
114     function initSearch(searchIndex) {
115         var currentResults, index, params = getQueryStringParams();
116
117         // Populate search bar with query string search term when provided.
118         $(".search-input")[0].value = params.search || '';
119
120         /**
121          * Executes the query and builds an index of results
122          * @param  {[Object]} query     [The user query]
123          * @param  {[type]} max         [The maximum results returned]
124          * @param  {[type]} searchWords [The list of search words to query against]
125          * @return {[type]}             [A search index of results]
126          */
127         function execQuery(query, max, searchWords) {
128             var valLower = query.query.toLowerCase(),
129                 val = valLower,
130                 typeFilter = query.type,
131                 results = [],
132                 aa = 0,
133                 bb = 0,
134                 split = valLower.split("::");
135
136             //remove empty keywords
137             for (var j = 0; j < split.length; j++) {
138                 split[j].toLowerCase();
139                 if (split[j] === "") {
140                     split.splice(j, 1);
141                 }
142             }
143
144             // quoted values mean literal search
145             bb = searchWords.length;
146             if ((val.charAt(0) === "\"" || val.charAt(0) === "'") && val.charAt(val.length - 1) === val.charAt(0)) {
147                 val = val.substr(1, val.length - 2);
148                 for (aa = 0; aa < bb; aa += 1) {
149                     if (searchWords[aa] === val) {
150                         // filter type: ... queries
151                         if (!typeFilter || typeFilter === searchIndex[aa].ty) {
152                             results.push([aa, -1]);
153                         }
154                     }
155                     if (results.length === max) {
156                         break;
157                     }
158                 }
159             } else {
160                 // gather matching search results up to a certain maximum
161                 val = val.replace(/\_/g, "");
162                 for (var i = 0; i < split.length; i++) {
163                     for (aa = 0; aa < bb; aa += 1) {
164                         if (searchWords[aa].indexOf(split[i]) > -1 || searchWords[aa].indexOf(val) > -1 || searchWords[aa].replace(/_/g, "").indexOf(val) > -1) {
165                             // filter type: ... queries
166                             if (!typeFilter || typeFilter === searchIndex[aa].ty) {
167                                 results.push([aa, searchWords[aa].replace(/_/g, "").indexOf(val)]);
168                             }
169                         }
170                         if (results.length === max) {
171                             break;
172                         }
173                     }
174                 }
175             }
176
177             bb = results.length;
178             for (aa = 0; aa < bb; aa += 1) {
179                 results[aa].push(searchIndex[results[aa][0]].ty);
180                 results[aa].push(searchIndex[results[aa][0]].path);
181                 results[aa].push(searchIndex[results[aa][0]].name);
182                 results[aa].push(searchIndex[results[aa][0]].parent);
183             }
184             // if there are no results then return to default and fail
185             if (results.length === 0) {
186                 return [];
187             }
188
189             // sort by exact match
190             results.sort(function search_complete_sort0(aaa, bbb) {
191                 if (searchWords[aaa[0]] === valLower && searchWords[bbb[0]] !== valLower) {
192                     return 1;
193                 }
194             });
195             // first sorting attempt
196             // sort by item name length
197             results.sort(function search_complete_sort1(aaa, bbb) {
198                 if (searchWords[aaa[0]].length > searchWords[bbb[0]].length) {
199                     return 1;
200                 }
201             });
202             // second sorting attempt
203             // sort by item name
204             results.sort(function search_complete_sort1(aaa, bbb) {
205                 if (searchWords[aaa[0]].length === searchWords[bbb[0]].length && searchWords[aaa[0]] > searchWords[bbb[0]]) {
206                     return 1;
207                 }
208             });
209             // third sorting attempt
210             // sort by index of keyword in item name
211             if (results[0][1] !== -1) {
212                 results.sort(function search_complete_sort1(aaa, bbb) {
213                     if (aaa[1] > bbb[1] && bbb[1] === 0) {
214                         return 1;
215                     }
216                 });
217             }
218             // fourth sorting attempt
219             // sort by type
220             results.sort(function search_complete_sort3(aaa, bbb) {
221                 if (searchWords[aaa[0]] === searchWords[bbb[0]] && aaa[2] > bbb[2]) {
222                     return 1;
223                 }
224             });
225             // fifth sorting attempt
226             // sort by path
227             results.sort(function search_complete_sort4(aaa, bbb) {
228                 if (searchWords[aaa[0]] === searchWords[bbb[0]] && aaa[2] === bbb[2] && aaa[3] > bbb[3]) {
229                     return 1;
230                 }
231             });
232             // sixth sorting attempt
233             // remove duplicates, according to the data provided
234             for (aa = results.length - 1; aa > 0; aa -= 1) {
235                 if (searchWords[results[aa][0]] === searchWords[results[aa - 1][0]] && results[aa][2] === results[aa - 1][2] && results[aa][3] === results[aa - 1][3]) {
236                     results[aa][0] = -1;
237                 }
238             }
239             for (var i = 0; i < results.length; i++) {
240                 var result = results[i],
241                     name = result[4].toLowerCase(),
242                     path = result[3].toLowerCase(),
243                     parent = allPaths[result[5]];
244
245                 var valid = validateResult(name, path, split, parent);
246                 if (!valid) {
247                     result[0] = -1;
248                 }
249             }
250             return results;
251         }
252
253         /**
254          * Validate performs the following boolean logic. For example: "File::open" will give
255          * IF A PARENT EXISTS => ("file" && "open") exists in (name || path || parent)
256          * OR => ("file" && "open") exists in (name || path )
257          *
258          * This could be written functionally, but I wanted to minimise functions on stack.
259          * @param  {[string]} name   [The name of the result]
260          * @param  {[string]} path   [The path of the result]
261          * @param  {[string]} keys   [The keys to be used (["file", "open"])]
262          * @param  {[object]} parent [The parent of the result]
263          * @return {[boolean]}       [Whether the result is valid or not]
264          */
265         function validateResult(name, path, keys, parent) {
266             //initially valid
267             var validate = true;
268             //if there is a parent, then validate against parent
269             if (parent !== undefined) {
270                 for (var i = 0; i < keys.length; i++) {
271                     // if previous keys are valid and current key is in the path, name or parent
272                     if ((validate) && (name.toLowerCase().indexOf(keys[i]) > -1 || path.toLowerCase().indexOf(keys[i]) > -1 || parent.name.toLowerCase().indexOf(keys[i]) > -1)) {
273                         validate = true;
274                     } else {
275                         validate = false;
276                     }
277                 }
278             } else {
279                 for (var i = 0; i < keys.length; i++) {
280                     // if previous keys are valid and current key is in the path, name
281                     if ((validate) && (name.toLowerCase().indexOf(keys[i]) > -1 || path.toLowerCase().indexOf(keys[i]) > -1)) {
282                         validate = true;
283                     } else {
284                         validate = false;
285                     }
286                 }
287             }
288             return validate;
289         }
290
291         function getQuery() {
292             var matches, type, query = $('.search-input').val();
293
294             matches = query.match(/^(fn|mod|str(uct)?|enum|trait|t(ype)?d(ef)?)\s*:\s*/i);
295             if (matches) {
296                 type = matches[1].replace(/^td$/, 'typedef').replace(/^str$/, 'struct').replace(/^tdef$/, 'typedef').replace(/^typed$/, 'typedef');
297                 query = query.substring(matches[0].length);
298             }
299
300             return {
301                 query: query,
302                 type: type,
303                 id: query + type,
304             };
305         }
306
307         function initSearchNav() {
308             var hoverTimeout, $results = $('.search-results .result');
309
310             $results.on('click', function() {
311                 var dst = $(this).find('a')[0];
312                 console.log(window.location.pathname, dst.pathname);
313                 if (window.location.pathname == dst.pathname) {
314                     $('#search').addClass('hidden');
315                     $('#main').removeClass('hidden');
316                 }
317                 document.location.href = dst.href;
318             }).on('mouseover', function() {
319                 var $el = $(this);
320                 clearTimeout(hoverTimeout);
321                 hoverTimeout = setTimeout(function() {
322                     $results.removeClass('highlighted');
323                     $el.addClass('highlighted');
324                 }, 20);
325             });
326
327             $(document).off('keypress.searchnav');
328             $(document).on('keypress.searchnav', function(e) {
329                 var $active = $results.filter('.highlighted');
330
331                 if (e.keyCode === 38) { // up
332                     e.preventDefault();
333                     if (!$active.length || !$active.prev()) {
334                         return;
335                     }
336
337                     $active.prev().addClass('highlighted');
338                     $active.removeClass('highlighted');
339                 } else if (e.keyCode === 40) { // down
340                     e.preventDefault();
341                     if (!$active.length) {
342                         $results.first().addClass('highlighted');
343                     } else if ($active.next().length) {
344                         $active.next().addClass('highlighted');
345                         $active.removeClass('highlighted');
346                     }
347                 } else if (e.keyCode === 13) { // return
348                     e.preventDefault();
349                     if ($active.length) {
350                         document.location.href = $active.find('a').prop('href');
351                     }
352                 }
353             });
354         }
355
356         function showResults(results) {
357             var output, shown, query = getQuery();
358
359             currentResults = query.id;
360             output = '<h1>Results for ' + query.query + (query.type ? ' (type: ' + query.type + ')' : '') + '</h1>';
361             output += '<table class="search-results">';
362
363             if (results.length > 0) {
364                 shown = [];
365
366                 results.forEach(function(item) {
367                     var name, type;
368
369                     if (shown.indexOf(item) !== -1) {
370                         return;
371                     }
372
373                     shown.push(item);
374                     name = item.name;
375                     type = item.ty;
376
377                     output += '<tr class="' + type + ' result"><td>';
378
379                     if (type === 'mod') {
380                         output += item.path +
381                             '::<a href="' + rootPath +
382                             item.path.replace(/::/g, '/') + '/' +
383                             name + '/index.html" class="' +
384                             type + '">' + name + '</a>';
385                     } else if (type === 'static' || type === 'reexport') {
386                         output += item.path +
387                             '::<a href="' + rootPath +
388                             item.path.replace(/::/g, '/') +
389                             '/index.html" class="' + type +
390                             '">' + name + '</a>';
391                     } else if (item.parent !== undefined) {
392                         var myparent = allPaths[item.parent];
393                         var anchor = '#' + type + '.' + name;
394                         output += item.path + '::' + myparent.name +
395                             '::<a href="' + rootPath +
396                             item.path.replace(/::/g, '/') +
397                             '/' + myparent.type +
398                             '.' + myparent.name +
399                             '.html' + anchor +
400                             '" class="' + type +
401                             '">' + name + '</a>';
402                     } else {
403                         output += item.path +
404                             '::<a href="' + rootPath +
405                             item.path.replace(/::/g, '/') +
406                             '/' + type +
407                             '.' + name +
408                             '.html" class="' + type +
409                             '">' + name + '</a>';
410                     }
411
412                     output += '</td><td><span class="desc">' + item.desc +
413                         '</span></td></tr>';
414                 });
415             } else {
416                 output += 'No results :( <a href="https://duckduckgo.com/?q=' +
417                     encodeURIComponent('rust ' + query.query) +
418                     '">Try on DuckDuckGo?</a>';
419             }
420
421             output += "</p>";
422             $('#main.content').addClass('hidden');
423             $('#search.content').removeClass('hidden').html(output);
424             $('#search .desc').width($('#search').width() - 40 -
425                 $('#search td:first-child').first().width());
426             initSearchNav();
427         }
428
429         function search(e) {
430             var query,
431                 filterdata = [],
432                 obj, i, len,
433                 results = [],
434                 maxResults = 200,
435                 resultIndex;
436             var params = getQueryStringParams();
437
438             query = getQuery();
439             if (e) {
440                 e.preventDefault();
441             }
442
443             if (!query.query || query.id === currentResults) {
444                 return;
445             }
446
447             // Because searching is incremental by character, only the most recent search query
448             // is added to the browser history.
449             if (browserSupportsHistoryApi()) {
450                 if (!history.state && !params.search) {
451                     history.pushState(query, "", "?search=" + encodeURIComponent(query.query));
452                 } else {
453                     history.replaceState(query, "", "?search=" + encodeURIComponent(query.query));
454                 }
455             }
456
457             resultIndex = execQuery(query, 20000, index);
458             len = resultIndex.length;
459             for (i = 0; i < len; i += 1) {
460                 if (resultIndex[i][0] > -1) {
461                     obj = searchIndex[resultIndex[i][0]];
462                     filterdata.push([obj.name, obj.ty, obj.path, obj.desc]);
463                     results.push(obj);
464                 }
465                 if (results.length >= maxResults) {
466                     break;
467                 }
468             }
469
470             // TODO add sorting capability through this function?
471             //
472             //            // the handler for the table heading filtering
473             //            filterdraw = function search_complete_filterdraw(node) {
474             //                var name = "",
475             //                    arrow = "",
476             //                    op = 0,
477             //                    tbody = node.parentNode.parentNode.nextSibling,
478             //                    anchora = {},
479             //                    tra = {},
480             //                    tha = {},
481             //                    td1a = {},
482             //                    td2a = {},
483             //                    td3a = {},
484             //                    aaa = 0,
485             //                    bbb = 0;
486             //
487             //                // the 4 following conditions set the rules for each
488             //                // table heading
489             //                if (node === ths[0]) {
490             //                    op = 0;
491             //                    name = "name";
492             //                    ths[1].innerHTML = ths[1].innerHTML.split(" ")[0];
493             //                    ths[2].innerHTML = ths[2].innerHTML.split(" ")[0];
494             //                    ths[3].innerHTML = ths[3].innerHTML.split(" ")[0];
495             //                }
496             //                if (node === ths[1]) {
497             //                    op = 1;
498             //                    name = "type";
499             //                    ths[0].innerHTML = ths[0].innerHTML.split(" ")[0];
500             //                    ths[2].innerHTML = ths[2].innerHTML.split(" ")[0];
501             //                    ths[3].innerHTML = ths[3].innerHTML.split(" ")[0];
502             //                }
503             //                if (node === ths[2]) {
504             //                    op = 2;
505             //                    name = "path";
506             //                    ths[0].innerHTML = ths[0].innerHTML.split(" ")[0];
507             //                    ths[1].innerHTML = ths[1].innerHTML.split(" ")[0];
508             //                    ths[3].innerHTML = ths[3].innerHTML.split(" ")[0];
509             //                }
510             //                if (node === ths[3]) {
511             //                    op = 3;
512             //                    name = "description";
513             //                    ths[0].innerHTML = ths[0].innerHTML.split(" ")[0];
514             //                    ths[1].innerHTML = ths[1].innerHTML.split(" ")[0];
515             //                    ths[2].innerHTML = ths[2].innerHTML.split(" ")[0];
516             //                }
517             //
518             //                // ascending or descending search
519             //                arrow = node.innerHTML.split(" ")[1];
520             //                if (arrow === undefined || arrow === "\u25b2") {
521             //                    arrow = "\u25bc";
522             //                } else {
523             //                    arrow = "\u25b2";
524             //                }
525             //
526             //                // filter the data
527             //                filterdata.sort(function search_complete_filterDraw_sort(xx, yy) {
528             //                    if ((arrow === "\u25b2" && xx[op].toLowerCase() < yy[op].toLowerCase()) || (arrow === "\u25bc" && xx[op].toLowerCase() > yy[op].toLowerCase())) {
529             //                        return 1;
530             //                    }
531             //                });
532             //            };
533
534             showResults(results);
535         }
536
537         function buildIndex(searchIndex) {
538             var len = searchIndex.length,
539                 i = 0,
540                 searchWords = [];
541
542             // before any analysis is performed lets gather the search terms to
543             // search against apart from the rest of the data.  This is a quick
544             // operation that is cached for the life of the page state so that
545             // all other search operations have access to this cached data for
546             // faster analysis operations
547             for (i = 0; i < len; i += 1) {
548                 if (typeof searchIndex[i].name === "string") {
549                     searchWords.push(searchIndex[i].name.toLowerCase());
550                 } else {
551                     searchWords.push("");
552                 }
553             }
554
555             return searchWords;
556         }
557
558         function startSearch() {
559             var keyUpTimeout;
560             $('.do-search').on('click', search);
561             $('.search-input').on('keyup', function() {
562                 clearTimeout(keyUpTimeout);
563                 keyUpTimeout = setTimeout(search, 100);
564             });
565             // Push and pop states are used to add search results to the browser history.
566             if (browserSupportsHistoryApi()) {
567                 $(window).on('popstate', function(e) {
568                     var params = getQueryStringParams();
569                     // When browsing back from search results the main page visibility must be reset.
570                     if (!params.search) {
571                         $('#main.content').removeClass('hidden');
572                         $('#search.content').addClass('hidden');
573                     }
574                     // When browsing forward to search results the previous search will be repeated,
575                     // so the currentResults are cleared to ensure the search is successful.
576                     currentResults = null;
577                     // Synchronize search bar with query string state and perform the search.
578                     $('.search-input').val(params.search);
579                     // Some browsers fire 'onpopstate' for every page load (Chrome), while others fire the
580                     // event only when actually popping a state (Firefox), which is why search() is called
581                     // both here and at the end of the startSearch() function.
582                     search();
583                 });
584             }
585             search();
586         }
587
588         index = buildIndex(searchIndex);
589         startSearch();
590     }
591
592     initSearch(searchIndex);
593 }());