]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/static/main.js
9b44dc63f383b889f1a6b095ca1d661276f90432
[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 */
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             $('.summary-column').width(function() {
46                 return contentWidth - 40 - $(this).prev().width();
47             })
48         }, 150);
49     }
50     resizeShortBlocks();
51     $(window).on('resize', resizeShortBlocks);
52
53     function highlightSourceLines() {
54         var i, from, to, match = window.location.hash.match(/^#?(\d+)(?:-(\d+))?$/);
55         if (match) {
56             from = parseInt(match[1], 10);
57             to = Math.min(50000, parseInt(match[2] || match[1], 10));
58             from = Math.min(from, to);
59             if ($('#' + from).length === 0) {
60                 return;
61             }
62             $('#' + from)[0].scrollIntoView();
63             $('.line-numbers span').removeClass('line-highlighted');
64             for (i = from; i <= to; ++i) {
65                 $('#' + i).addClass('line-highlighted');
66             }
67         }
68     }
69     highlightSourceLines();
70     $(window).on('hashchange', highlightSourceLines);
71
72     $(document).on('keyup', function(e) {
73         if (document.activeElement.tagName === 'INPUT') {
74             return;
75         }
76
77         if (e.which === 191 && $('#help').hasClass('hidden')) { // question mark
78             e.preventDefault();
79             $('#help').removeClass('hidden');
80         } else if (e.which === 27) { // esc
81             if (!$('#help').hasClass('hidden')) {
82                 e.preventDefault();
83                 $('#help').addClass('hidden');
84             } else if (!$('#search').hasClass('hidden')) {
85                 e.preventDefault();
86                 $('#search').addClass('hidden');
87                 $('#main').removeClass('hidden');
88             }
89         } else if (e.which === 83) { // S
90             e.preventDefault();
91             $('.search-input').focus();
92         }
93     }).on('click', function(e) {
94         if (!$(e.target).closest('#help').length) {
95             $('#help').addClass('hidden');
96         }
97     });
98
99     $('.version-selector').on('change', function() {
100         var i, match,
101             url = document.location.href,
102             stripped = '',
103             len = rootPath.match(/\.\.\//g).length + 1;
104
105         for (i = 0; i < len; ++i) {
106             match = url.match(/\/[^\/]*$/);
107             if (i < len - 1) {
108                 stripped = match[0] + stripped;
109             }
110             url = url.substring(0, url.length - match[0].length);
111         }
112
113         url += '/' + $('.version-selector').val() + stripped;
114
115         document.location.href = url;
116     });
117     /**
118      * A function to compute the Levenshtein distance between two strings
119      * Licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported
120      * Full License can be found at http://creativecommons.org/licenses/by-sa/3.0/legalcode
121      * This code is an unmodified version of the code written by Marco de Wit 
122      * and was found at http://stackoverflow.com/a/18514751/745719
123      */
124     var levenshtein = (function() {
125         var row2 = [];
126         return function(s1, s2) {
127             if (s1 === s2) {
128                 return 0;
129             } else {
130                 var s1_len = s1.length, s2_len = s2.length;
131                 if (s1_len && s2_len) {
132                     var i1 = 0, i2 = 0, a, b, c, c2, row = row2;
133                     while (i1 < s1_len)
134                         row[i1] = ++i1;
135                     while (i2 < s2_len) {
136                         c2 = s2.charCodeAt(i2);
137                         a = i2;
138                         ++i2;
139                         b = i2;
140                         for (i1 = 0; i1 < s1_len; ++i1) {
141                             c = a + (s1.charCodeAt(i1) !== c2 ? 1 : 0);
142                             a = row[i1];
143                             b = b < a ? (b < c ? b + 1 : c) : (a < c ? a + 1 : c);
144                             row[i1] = b;
145                         }
146                     }
147                     return b;
148                 } else {
149                     return s1_len + s2_len;
150                 }
151             }
152         };
153     })();
154
155     function initSearch(rawSearchIndex) {
156         var currentResults, index, searchIndex;
157         var MAX_LEV_DISTANCE = 3;
158         var params = getQueryStringParams();
159
160         // Populate search bar with query string search term when provided,
161         // but only if the input bar is empty. This avoid the obnoxious issue
162         // where you start trying to do a search, and the index loads, and
163         // suddenly your search is gone!
164         if ($(".search-input")[0].value === "") {
165             $(".search-input")[0].value = params.search || '';
166         }
167
168         /**
169          * Executes the query and builds an index of results
170          * @param  {[Object]} query     [The user query]
171          * @param  {[type]} max         [The maximum results returned]
172          * @param  {[type]} searchWords [The list of search words to query
173          *                               against]
174          * @return {[type]}             [A search index of results]
175          */
176         function execQuery(query, max, searchWords) {
177             var valLower = query.query.toLowerCase(),
178                 val = valLower,
179                 typeFilter = itemTypeFromName(query.type),
180                 results = [],
181                 split = valLower.split("::");
182
183             //remove empty keywords
184             for (var j = 0; j < split.length; ++j) {
185                 split[j].toLowerCase();
186                 if (split[j] === "") {
187                     split.splice(j, 1);
188                 }
189             }
190
191             // quoted values mean literal search
192             var nSearchWords = searchWords.length;
193             if ((val.charAt(0) === "\"" || val.charAt(0) === "'") &&
194                 val.charAt(val.length - 1) === val.charAt(0))
195             {
196                 val = val.substr(1, val.length - 2);
197                 for (var i = 0; i < nSearchWords; ++i) {
198                     if (searchWords[i] === val) {
199                         // filter type: ... queries
200                         if (typeFilter < 0 || typeFilter === searchIndex[i].ty) {
201                             results.push({id: i, index: -1});
202                         }
203                     }
204                     if (results.length === max) {
205                         break;
206                     }
207                 }
208             } else {
209                 // gather matching search results up to a certain maximum
210                 val = val.replace(/\_/g, "");
211                 for (var i = 0; i < split.length; ++i) {
212                     for (var j = 0; j < nSearchWords; ++j) {
213                         var lev_distance;
214                         if (searchWords[j].indexOf(split[i]) > -1 ||
215                             searchWords[j].indexOf(val) > -1 ||
216                             searchWords[j].replace(/_/g, "").indexOf(val) > -1)
217                         {
218                             // filter type: ... queries
219                             if (typeFilter < 0 || typeFilter === searchIndex[j].ty) {
220                                 results.push({
221                                     id: j,
222                                     index: searchWords[j].replace(/_/g, "").indexOf(val),
223                                     lev: 0,
224                                 });
225                             }
226                         } else if (
227                             (lev_distance = levenshtein(searchWords[j], val)) <= 
228                                 MAX_LEV_DISTANCE) {
229                             if (typeFilter < 0 || typeFilter === searchIndex[j].ty) {
230                                 results.push({
231                                     id: j,
232                                     index: 0,
233                                     // we want lev results to go lower than others
234                                     lev: lev_distance,
235                                 });
236                             }
237                         }
238                         if (results.length === max) {
239                             break;
240                         }
241                     }
242                 }
243             }
244
245             var nresults = results.length;
246             for (var i = 0; i < nresults; ++i) {
247                 results[i].word = searchWords[results[i].id];
248                 results[i].item = searchIndex[results[i].id] || {};
249             }
250             // if there are no results then return to default and fail
251             if (results.length === 0) {
252                 return [];
253             }
254
255             results.sort(function(aaa, bbb) {
256                 var a, b;
257
258                 // Sort by non levenshtein results and then levenshtein results by the distance
259                 // (less changes required to match means higher rankings)
260                 a = (aaa.lev);
261                 b = (bbb.lev);
262                 if (a !== b) return a - b;
263
264                 // sort by crate (non-current crate goes later)
265                 a = (aaa.item.crate !== window.currentCrate);
266                 b = (bbb.item.crate !== window.currentCrate);
267                 if (a !== b) return a - b;
268
269                 // sort by exact match (mismatch goes later)
270                 a = (aaa.word !== valLower);
271                 b = (bbb.word !== valLower);
272                 if (a !== b) return a - b;
273
274                 // sort by item name length (longer goes later)
275                 a = aaa.word.length;
276                 b = bbb.word.length;
277                 if (a !== b) return a - b;
278
279                 // sort by item name (lexicographically larger goes later)
280                 a = aaa.word;
281                 b = bbb.word;
282                 if (a !== b) return (a > b ? +1 : -1);
283
284                 // sort by index of keyword in item name (no literal occurrence goes later)
285                 a = (aaa.index < 0);
286                 b = (bbb.index < 0);
287                 if (a !== b) return a - b;
288                 // (later literal occurrence, if any, goes later)
289                 a = aaa.index;
290                 b = bbb.index;
291                 if (a !== b) return a - b;
292
293                 // sort by description (no description goes later)
294                 a = (aaa.item.desc === '');
295                 b = (bbb.item.desc === '');
296                 if (a !== b) return a - b;
297
298                 // sort by type (later occurrence in `itemTypes` goes later)
299                 a = aaa.item.ty;
300                 b = bbb.item.ty;
301                 if (a !== b) return a - b;
302
303                 // sort by path (lexicographically larger goes later)
304                 a = aaa.item.path;
305                 b = bbb.item.path;
306                 if (a !== b) return (a > b ? +1 : -1);
307
308                 // que sera, sera
309                 return 0;
310             });
311
312             // remove duplicates, according to the data provided
313             for (var i = results.length - 1; i > 0; i -= 1) {
314                 if (results[i].word === results[i - 1].word &&
315                     results[i].item.ty === results[i - 1].item.ty &&
316                     results[i].item.path === results[i - 1].item.path)
317                 {
318                     results[i].id = -1;
319                 }
320             }
321             for (var i = 0; i < results.length; ++i) {
322                 var result = results[i],
323                     name = result.item.name.toLowerCase(),
324                     path = result.item.path.toLowerCase(),
325                     parent = result.item.parent;
326
327                 var valid = validateResult(name, path, split, parent);
328                 if (!valid) {
329                     result.id = -1;
330                 }
331             }
332             return results;
333         }
334
335         /**
336          * Validate performs the following boolean logic. For example:
337          * "File::open" will give IF A PARENT EXISTS => ("file" && "open")
338          * exists in (name || path || parent) OR => ("file" && "open") exists in
339          * (name || path )
340          *
341          * This could be written functionally, but I wanted to minimise
342          * functions on stack.
343          *
344          * @param  {[string]} name   [The name of the result]
345          * @param  {[string]} path   [The path of the result]
346          * @param  {[string]} keys   [The keys to be used (["file", "open"])]
347          * @param  {[object]} parent [The parent of the result]
348          * @return {[boolean]}       [Whether the result is valid or not]
349          */
350         function validateResult(name, path, keys, parent) {
351             for (var i=0; i < keys.length; ++i) {
352                 // each check is for validation so we negate the conditions and invalidate
353                 if (!( 
354                     // check for an exact name match
355                     name.toLowerCase().indexOf(keys[i]) > -1 ||
356                     // then an exact path match
357                     path.toLowerCase().indexOf(keys[i]) > -1 ||
358                     // next if there is a parent, check for exact parent match
359                     (parent !== undefined && 
360                         parent.name.toLowerCase().indexOf(keys[i]) > -1) ||
361                     // lastly check to see if the name was a levenshtein match
362                     levenshtein(name.toLowerCase(), keys[i]) <= 
363                         MAX_LEV_DISTANCE)) {
364                     return false;
365                 }
366             }
367             return true;
368         }
369
370         function getQuery() {
371             var matches, type, query = $('.search-input').val();
372
373             matches = query.match(/^(fn|mod|str(uct)?|enum|trait|t(ype)?d(ef)?)\s*:\s*/i);
374             if (matches) {
375                 type = matches[1].replace(/^td$/, 'typedef')
376                                  .replace(/^str$/, 'struct')
377                                  .replace(/^tdef$/, 'typedef')
378                                  .replace(/^typed$/, 'typedef');
379                 query = query.substring(matches[0].length);
380             }
381
382             return {
383                 query: query,
384                 type: type,
385                 id: query + type,
386             };
387         }
388
389         function initSearchNav() {
390             var hoverTimeout, $results = $('.search-results .result');
391
392             $results.on('click', function() {
393                 var dst = $(this).find('a')[0];
394                 if (window.location.pathname == dst.pathname) {
395                     $('#search').addClass('hidden');
396                     $('#main').removeClass('hidden');
397                 }
398                 document.location.href = dst.href;
399             }).on('mouseover', function() {
400                 var $el = $(this);
401                 clearTimeout(hoverTimeout);
402                 hoverTimeout = setTimeout(function() {
403                     $results.removeClass('highlighted');
404                     $el.addClass('highlighted');
405                 }, 20);
406             });
407
408             $(document).off('keydown.searchnav');
409             $(document).on('keydown.searchnav', function(e) {
410                 var $active = $results.filter('.highlighted');
411
412                 if (e.which === 38) { // up
413                     e.preventDefault();
414                     if (!$active.length || !$active.prev()) {
415                         return;
416                     }
417
418                     $active.prev().addClass('highlighted');
419                     $active.removeClass('highlighted');
420                 } else if (e.which === 40) { // down
421                     e.preventDefault();
422                     if (!$active.length) {
423                         $results.first().addClass('highlighted');
424                     } else if ($active.next().length) {
425                         $active.next().addClass('highlighted');
426                         $active.removeClass('highlighted');
427                     }
428                 } else if (e.which === 13) { // return
429                     e.preventDefault();
430                     if ($active.length) {
431                         document.location.href = $active.find('a').prop('href');
432                     }
433                 }
434             });
435         }
436
437         function escape(content) {
438             return $('<h1/>').text(content).html();
439         }
440
441         function showResults(results) {
442             var output, shown, query = getQuery();
443
444             currentResults = query.id;
445             output = '<h1>Results for ' + escape(query.query) +
446                 (query.type ? ' (type: ' + escape(query.type) + ')' : '') + '</h1>';
447             output += '<table class="search-results">';
448
449             if (results.length > 0) {
450                 shown = [];
451
452                 results.forEach(function(item) {
453                     var name, type;
454
455                     if (shown.indexOf(item) !== -1) {
456                         return;
457                     }
458
459                     shown.push(item);
460                     name = item.name;
461                     type = itemTypes[item.ty];
462
463                     output += '<tr class="' + type + ' result"><td>';
464
465                     if (type === 'mod') {
466                         output += item.path +
467                             '::<a href="' + rootPath +
468                             item.path.replace(/::/g, '/') + '/' +
469                             name + '/index.html" class="' +
470                             type + '">' + name + '</a>';
471                     } else if (type === 'static' || type === 'reexport') {
472                         output += item.path +
473                             '::<a href="' + rootPath +
474                             item.path.replace(/::/g, '/') +
475                             '/index.html" class="' + type +
476                             '">' + name + '</a>';
477                     } else if (item.parent !== undefined) {
478                         var myparent = item.parent;
479                         var anchor = '#' + type + '.' + name;
480                         output += item.path + '::' + myparent.name +
481                             '::<a href="' + rootPath +
482                             item.path.replace(/::/g, '/') +
483                             '/' + itemTypes[myparent.ty] +
484                             '.' + myparent.name +
485                             '.html' + anchor +
486                             '" class="' + type +
487                             '">' + name + '</a>';
488                     } else {
489                         output += item.path +
490                             '::<a href="' + rootPath +
491                             item.path.replace(/::/g, '/') +
492                             '/' + type +
493                             '.' + name +
494                             '.html" class="' + type +
495                             '">' + name + '</a>';
496                     }
497
498                     output += '</td><td><span class="desc">' + item.desc +
499                         '</span></td></tr>';
500                 });
501             } else {
502                 output += 'No results :( <a href="https://duckduckgo.com/?q=' +
503                     encodeURIComponent('rust ' + query.query) +
504                     '">Try on DuckDuckGo?</a>';
505             }
506
507             output += "</p>";
508             $('#main.content').addClass('hidden');
509             $('#search.content').removeClass('hidden').html(output);
510             $('#search .desc').width($('#search').width() - 40 -
511                 $('#search td:first-child').first().width());
512             initSearchNav();
513         }
514
515         function search(e) {
516             var query,
517                 filterdata = [],
518                 obj, i, len,
519                 results = [],
520                 maxResults = 200,
521                 resultIndex;
522             var params = getQueryStringParams();
523
524             query = getQuery();
525             if (e) {
526                 e.preventDefault();
527             }
528
529             if (!query.query || query.id === currentResults) {
530                 return;
531             }
532
533             // Because searching is incremental by character, only the most
534             // recent search query is added to the browser history.
535             if (browserSupportsHistoryApi()) {
536                 if (!history.state && !params.search) {
537                     history.pushState(query, "", "?search=" +
538                                                 encodeURIComponent(query.query));
539                 } else {
540                     history.replaceState(query, "", "?search=" +
541                                                 encodeURIComponent(query.query));
542                 }
543             }
544
545             resultIndex = execQuery(query, 20000, index);
546             len = resultIndex.length;
547             for (i = 0; i < len; ++i) {
548                 if (resultIndex[i].id > -1) {
549                     obj = searchIndex[resultIndex[i].id];
550                     filterdata.push([obj.name, obj.ty, obj.path, obj.desc]);
551                     results.push(obj);
552                 }
553                 if (results.length >= maxResults) {
554                     break;
555                 }
556             }
557
558             showResults(results);
559         }
560
561         // This mapping table should match the discriminants of
562         // `rustdoc::html::item_type::ItemType` type in Rust.
563         var itemTypes = ["mod",
564                          "struct",
565                          "type",
566                          "fn",
567                          "type",
568                          "static",
569                          "trait",
570                          "impl",
571                          "viewitem",
572                          "tymethod",
573                          "method",
574                          "structfield",
575                          "variant",
576                          "ffi",
577                          "ffs",
578                          "macro",
579                          "primitive"];
580
581         function itemTypeFromName(typename) {
582             for (var i = 0; i < itemTypes.length; ++i) {
583                 if (itemTypes[i] === typename) return i;
584             }
585             return -1;
586         }
587
588         function buildIndex(rawSearchIndex) {
589             searchIndex = [];
590             var searchWords = [];
591             for (var crate in rawSearchIndex) {
592                 if (!rawSearchIndex.hasOwnProperty(crate)) { continue }
593
594                 // an array of [(Number) item type,
595                 //              (String) name,
596                 //              (String) full path or empty string for previous path,
597                 //              (String) description,
598                 //              (optional Number) the parent path index to `paths`]
599                 var items = rawSearchIndex[crate].items;
600                 // an array of [(Number) item type,
601                 //              (String) name]
602                 var paths = rawSearchIndex[crate].paths;
603
604                 // convert `paths` into an object form
605                 var len = paths.length;
606                 for (var i = 0; i < len; ++i) {
607                     paths[i] = {ty: paths[i][0], name: paths[i][1]};
608                 }
609
610                 // convert `items` into an object form, and construct word indices.
611                 //
612                 // before any analysis is performed lets gather the search terms to
613                 // search against apart from the rest of the data.  This is a quick
614                 // operation that is cached for the life of the page state so that
615                 // all other search operations have access to this cached data for
616                 // faster analysis operations
617                 var len = items.length;
618                 var lastPath = "";
619                 for (var i = 0; i < len; ++i) {
620                     var rawRow = items[i];
621                     var row = {crate: crate, ty: rawRow[0], name: rawRow[1],
622                                path: rawRow[2] || lastPath, desc: rawRow[3],
623                                parent: paths[rawRow[4]]};
624                     searchIndex.push(row);
625                     if (typeof row.name === "string") {
626                         var word = row.name.toLowerCase();
627                         searchWords.push(word);
628                     } else {
629                         searchWords.push("");
630                     }
631                     lastPath = row.path;
632                 }
633             }
634             return searchWords;
635         }
636
637         function startSearch() {
638             var keyUpTimeout;
639             $('.do-search').on('click', search);
640             $('.search-input').on('keyup', function() {
641                 clearTimeout(keyUpTimeout);
642                 keyUpTimeout = setTimeout(search, 100);
643             });
644
645             // Push and pop states are used to add search results to the browser
646             // history.
647             if (browserSupportsHistoryApi()) {
648                 $(window).on('popstate', function(e) {
649                     var params = getQueryStringParams();
650                     // When browsing back from search results the main page
651                     // visibility must be reset.
652                     if (!params.search) {
653                         $('#main.content').removeClass('hidden');
654                         $('#search.content').addClass('hidden');
655                     }
656                     // When browsing forward to search results the previous
657                     // search will be repeated, so the currentResults are
658                     // cleared to ensure the search is successful.
659                     currentResults = null;
660                     // Synchronize search bar with query string state and
661                     // perform the search. This will empty the bar if there's
662                     // nothing there, which lets you really go back to a
663                     // previous state with nothing in the bar.
664                     $('.search-input').val(params.search);
665                     // Some browsers fire 'onpopstate' for every page load
666                     // (Chrome), while others fire the event only when actually
667                     // popping a state (Firefox), which is why search() is
668                     // called both here and at the end of the startSearch()
669                     // function.
670                     search();
671                 });
672             }
673             search();
674         }
675
676         index = buildIndex(rawSearchIndex);
677         startSearch();
678
679         // Draw a convenient sidebar of known crates if we have a listing
680         if (rootPath == '../') {
681             var sidebar = $('.sidebar');
682             var div = $('<div>').attr('class', 'block crate');
683             div.append($('<h2>').text('Crates'));
684
685             var crates = [];
686             for (var crate in rawSearchIndex) {
687                 if (!rawSearchIndex.hasOwnProperty(crate)) { continue }
688                 crates.push(crate);
689             }
690             crates.sort();
691             for (var i = 0; i < crates.length; ++i) {
692                 var klass = 'crate';
693                 if (crates[i] == window.currentCrate) {
694                     klass += ' current';
695                 }
696                 div.append($('<a>', {'href': '../' + crates[i] + '/index.html',
697                                     'class': klass}).text(crates[i]));
698             }
699             sidebar.append(div);
700         }
701     }
702
703     window.initSearch = initSearch;
704
705     window.register_implementors = function(imp) {
706         var list = $('#implementors-list');
707         var libs = Object.getOwnPropertyNames(imp);
708         for (var i = 0; i < libs.length; ++i) {
709             if (libs[i] == currentCrate) continue;
710             var structs = imp[libs[i]];
711             for (var j = 0; j < structs.length; ++j) {
712                 var code = $('<code>').append(structs[j]);
713                 $.each(code.find('a'), function(idx, a) {
714                     var href = $(a).attr('href');
715                     if (!href.startsWith('http')) {
716                         $(a).attr('href', rootPath + $(a).attr('href'));
717                     }
718                 });
719                 var li = $('<li>').append(code);
720                 list.append(li);
721             }
722         }
723     };
724     if (window.pending_implementors) {
725         window.register_implementors(window.pending_implementors);
726     }
727
728     // See documentation in html/render.rs for what this is doing.
729     var query = getQueryStringParams();
730     if (query['gotosrc']) {
731         window.location = $('#src-' + query['gotosrc']).attr('href');
732     }
733
734     $("#expand-all").on("click", function() {
735         $(".docblock").show();
736         $(".toggle-label").hide();
737         $(".toggle-wrapper").removeClass("collapsed");
738         $(".collapse-toggle").children(".inner").html("-");
739     });
740
741     $("#collapse-all").on("click", function() {
742         $(".docblock").hide();
743         $(".toggle-label").show();
744         $(".toggle-wrapper").addClass("collapsed");
745         $(".collapse-toggle").children(".inner").html("+");
746     });
747
748     $(document).on("click", ".collapse-toggle", function() {
749         var toggle = $(this);
750         var relatedDoc = toggle.parent().next();
751         if (relatedDoc.is(".docblock")) {
752             if (relatedDoc.is(":visible")) {
753                 relatedDoc.slideUp({duration:'fast', easing:'linear'});
754                 toggle.parent(".toggle-wrapper").addClass("collapsed");
755                 toggle.children(".inner").html("+");
756                 toggle.children(".toggle-label").fadeIn();
757             } else {
758                 relatedDoc.slideDown({duration:'fast', easing:'linear'});
759                 toggle.parent(".toggle-wrapper").removeClass("collapsed");
760                 toggle.children(".inner").html("-");
761                 toggle.children(".toggle-label").hide();
762             }
763         }
764     });
765
766     $(function() {
767         var toggle = "<a href='javascript:void(0)'"
768             + "class='collapse-toggle'>[<span class='inner'>-</span>]</a>";
769
770         $(".method").each(function() {
771            if ($(this).next().is(".docblock")) {
772                $(this).children().first().after(toggle);
773            }
774         });
775
776         var mainToggle = $(toggle);
777         mainToggle.append("<span class='toggle-label' style='display:none'>"
778             + "&nbsp;Expand&nbsp;description</span></a>")
779         var wrapper =  $("<div class='toggle-wrapper'>");
780         wrapper.append(mainToggle);
781         $("#main > .docblock").before(wrapper);
782     });
783
784 }());