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