• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2// For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
3
4// Coverage.py HTML report browser code.
5/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */
6/*global coverage: true, document, window, $ */
7
8coverage = {};
9
10// Find all the elements with shortkey_* class, and use them to assign a shortcut key.
11coverage.assign_shortkeys = function () {
12    $("*[class*='shortkey_']").each(function (i, e) {
13        $.each($(e).attr("class").split(" "), function (i, c) {
14            if (/^shortkey_/.test(c)) {
15                $(document).bind('keydown', c.substr(9), function () {
16                    $(e).click();
17                });
18            }
19        });
20    });
21};
22
23// Create the events for the help panel.
24coverage.wire_up_help_panel = function () {
25    $("#keyboard_icon").click(function () {
26        // Show the help panel, and position it so the keyboard icon in the
27        // panel is in the same place as the keyboard icon in the header.
28        $(".help_panel").show();
29        var koff = $("#keyboard_icon").offset();
30        var poff = $("#panel_icon").position();
31        $(".help_panel").offset({
32            top: koff.top-poff.top,
33            left: koff.left-poff.left
34        });
35    });
36    $("#panel_icon").click(function () {
37        $(".help_panel").hide();
38    });
39};
40
41// Create the events for the filter box.
42coverage.wire_up_filter = function () {
43    // Cache elements.
44    var table = $("table.index");
45    var table_rows = table.find("tbody tr");
46    var table_row_names = table_rows.find("td.name a");
47    var no_rows = $("#no_rows");
48
49    // Create a duplicate table footer that we can modify with dynamic summed values.
50    var table_footer = $("table.index tfoot tr");
51    var table_dynamic_footer = table_footer.clone();
52    table_dynamic_footer.attr('class', 'total_dynamic hidden');
53    table_footer.after(table_dynamic_footer);
54
55    // Observe filter keyevents.
56    $("#filter").on("keyup change", $.debounce(150, function (event) {
57        var filter_value = $(this).val();
58
59        if (filter_value === "") {
60            // Filter box is empty, remove all filtering.
61            table_rows.removeClass("hidden");
62
63            // Show standard footer, hide dynamic footer.
64            table_footer.removeClass("hidden");
65            table_dynamic_footer.addClass("hidden");
66
67            // Hide placeholder, show table.
68            if (no_rows.length > 0) {
69                no_rows.hide();
70            }
71            table.show();
72
73        }
74        else {
75            // Filter table items by value.
76            var hide = $([]);
77            var show = $([]);
78
79            // Compile elements to hide / show.
80            $.each(table_row_names, function () {
81                var element = $(this).parents("tr");
82
83                if ($(this).text().indexOf(filter_value) === -1) {
84                    // hide
85                    hide = hide.add(element);
86                }
87                else {
88                    // show
89                    show = show.add(element);
90                }
91            });
92
93            // Perform DOM manipulation.
94            hide.addClass("hidden");
95            show.removeClass("hidden");
96
97            // Show placeholder if no rows will be displayed.
98            if (no_rows.length > 0) {
99                if (show.length === 0) {
100                    // Show placeholder, hide table.
101                    no_rows.show();
102                    table.hide();
103                }
104                else {
105                    // Hide placeholder, show table.
106                    no_rows.hide();
107                    table.show();
108                }
109            }
110
111            // Manage dynamic header:
112            if (hide.length > 0) {
113                // Calculate new dynamic sum values based on visible rows.
114                for (var column = 2; column < 20; column++) {
115                    // Calculate summed value.
116                    var cells = table_rows.find('td:nth-child(' + column + ')');
117                    if (!cells.length) {
118                        // No more columns...!
119                        break;
120                    }
121
122                    var sum = 0, numer = 0, denom = 0;
123                    $.each(cells.filter(':visible'), function () {
124                        var ratio = $(this).data("ratio");
125                        if (ratio) {
126                            var splitted = ratio.split(" ");
127                            numer += parseInt(splitted[0], 10);
128                            denom += parseInt(splitted[1], 10);
129                        }
130                        else {
131                            sum += parseInt(this.innerHTML, 10);
132                        }
133                    });
134
135                    // Get footer cell element.
136                    var footer_cell = table_dynamic_footer.find('td:nth-child(' + column + ')');
137
138                    // Set value into dynamic footer cell element.
139                    if (cells[0].innerHTML.indexOf('%') > -1) {
140                        // Percentage columns use the numerator and denominator,
141                        // and adapt to the number of decimal places.
142                        var match = /\.([0-9]+)/.exec(cells[0].innerHTML);
143                        var places = 0;
144                        if (match) {
145                            places = match[1].length;
146                        }
147                        var pct = numer * 100 / denom;
148                        footer_cell.text(pct.toFixed(places) + '%');
149                    }
150                    else {
151                        footer_cell.text(sum);
152                    }
153                }
154
155                // Hide standard footer, show dynamic footer.
156                table_footer.addClass("hidden");
157                table_dynamic_footer.removeClass("hidden");
158            }
159            else {
160                // Show standard footer, hide dynamic footer.
161                table_footer.removeClass("hidden");
162                table_dynamic_footer.addClass("hidden");
163            }
164        }
165    }));
166
167    // Trigger change event on setup, to force filter on page refresh
168    // (filter value may still be present).
169    $("#filter").trigger("change");
170};
171
172// Loaded on index.html
173coverage.index_ready = function ($) {
174    // Look for a cookie containing previous sort settings:
175    var sort_list = [];
176    var cookie_name = "COVERAGE_INDEX_SORT";
177    var i;
178
179    // This almost makes it worth installing the jQuery cookie plugin:
180    if (document.cookie.indexOf(cookie_name) > -1) {
181        var cookies = document.cookie.split(";");
182        for (i = 0; i < cookies.length; i++) {
183            var parts = cookies[i].split("=");
184
185            if ($.trim(parts[0]) === cookie_name && parts[1]) {
186                sort_list = eval("[[" + parts[1] + "]]");
187                break;
188            }
189        }
190    }
191
192    // Create a new widget which exists only to save and restore
193    // the sort order:
194    $.tablesorter.addWidget({
195        id: "persistentSort",
196
197        // Format is called by the widget before displaying:
198        format: function (table) {
199            if (table.config.sortList.length === 0 && sort_list.length > 0) {
200                // This table hasn't been sorted before - we'll use
201                // our stored settings:
202                $(table).trigger('sorton', [sort_list]);
203            }
204            else {
205                // This is not the first load - something has
206                // already defined sorting so we'll just update
207                // our stored value to match:
208                sort_list = table.config.sortList;
209            }
210        }
211    });
212
213    // Configure our tablesorter to handle the variable number of
214    // columns produced depending on report options:
215    var headers = [];
216    var col_count = $("table.index > thead > tr > th").length;
217
218    headers[0] = { sorter: 'text' };
219    for (i = 1; i < col_count-1; i++) {
220        headers[i] = { sorter: 'digit' };
221    }
222    headers[col_count-1] = { sorter: 'percent' };
223
224    // Enable the table sorter:
225    $("table.index").tablesorter({
226        widgets: ['persistentSort'],
227        headers: headers
228    });
229
230    coverage.assign_shortkeys();
231    coverage.wire_up_help_panel();
232    coverage.wire_up_filter();
233
234    // Watch for page unload events so we can save the final sort settings:
235    $(window).unload(function () {
236        document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/";
237    });
238};
239
240// -- pyfile stuff --
241
242coverage.pyfile_ready = function ($) {
243    // If we're directed to a particular line number, highlight the line.
244    var frag = location.hash;
245    if (frag.length > 2 && frag[1] === 'n') {
246        $(frag).addClass('highlight');
247        coverage.set_sel(parseInt(frag.substr(2), 10));
248    }
249    else {
250        coverage.set_sel(0);
251    }
252
253    $(document)
254        .bind('keydown', 'j', coverage.to_next_chunk_nicely)
255        .bind('keydown', 'k', coverage.to_prev_chunk_nicely)
256        .bind('keydown', '0', coverage.to_top)
257        .bind('keydown', '1', coverage.to_first_chunk)
258        ;
259
260    $(".button_toggle_run").click(function (evt) {coverage.toggle_lines(evt.target, "run");});
261    $(".button_toggle_exc").click(function (evt) {coverage.toggle_lines(evt.target, "exc");});
262    $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");});
263    $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");});
264
265    coverage.assign_shortkeys();
266    coverage.wire_up_help_panel();
267};
268
269coverage.toggle_lines = function (btn, cls) {
270    btn = $(btn);
271    var hide = "hide_"+cls;
272    if (btn.hasClass(hide)) {
273        $("#source ."+cls).removeClass(hide);
274        btn.removeClass(hide);
275    }
276    else {
277        $("#source ."+cls).addClass(hide);
278        btn.addClass(hide);
279    }
280};
281
282// Return the nth line div.
283coverage.line_elt = function (n) {
284    return $("#t" + n);
285};
286
287// Return the nth line number div.
288coverage.num_elt = function (n) {
289    return $("#n" + n);
290};
291
292// Return the container of all the code.
293coverage.code_container = function () {
294    return $(".linenos");
295};
296
297// Set the selection.  b and e are line numbers.
298coverage.set_sel = function (b, e) {
299    // The first line selected.
300    coverage.sel_begin = b;
301    // The next line not selected.
302    coverage.sel_end = (e === undefined) ? b+1 : e;
303};
304
305coverage.to_top = function () {
306    coverage.set_sel(0, 1);
307    coverage.scroll_window(0);
308};
309
310coverage.to_first_chunk = function () {
311    coverage.set_sel(0, 1);
312    coverage.to_next_chunk();
313};
314
315coverage.is_transparent = function (color) {
316    // Different browsers return different colors for "none".
317    return color === "transparent" || color === "rgba(0, 0, 0, 0)";
318};
319
320coverage.to_next_chunk = function () {
321    var c = coverage;
322
323    // Find the start of the next colored chunk.
324    var probe = c.sel_end;
325    var color, probe_line;
326    while (true) {
327        probe_line = c.line_elt(probe);
328        if (probe_line.length === 0) {
329            return;
330        }
331        color = probe_line.css("background-color");
332        if (!c.is_transparent(color)) {
333            break;
334        }
335        probe++;
336    }
337
338    // There's a next chunk, `probe` points to it.
339    var begin = probe;
340
341    // Find the end of this chunk.
342    var next_color = color;
343    while (next_color === color) {
344        probe++;
345        probe_line = c.line_elt(probe);
346        next_color = probe_line.css("background-color");
347    }
348    c.set_sel(begin, probe);
349    c.show_selection();
350};
351
352coverage.to_prev_chunk = function () {
353    var c = coverage;
354
355    // Find the end of the prev colored chunk.
356    var probe = c.sel_begin-1;
357    var probe_line = c.line_elt(probe);
358    if (probe_line.length === 0) {
359        return;
360    }
361    var color = probe_line.css("background-color");
362    while (probe > 0 && c.is_transparent(color)) {
363        probe--;
364        probe_line = c.line_elt(probe);
365        if (probe_line.length === 0) {
366            return;
367        }
368        color = probe_line.css("background-color");
369    }
370
371    // There's a prev chunk, `probe` points to its last line.
372    var end = probe+1;
373
374    // Find the beginning of this chunk.
375    var prev_color = color;
376    while (prev_color === color) {
377        probe--;
378        probe_line = c.line_elt(probe);
379        prev_color = probe_line.css("background-color");
380    }
381    c.set_sel(probe+1, end);
382    c.show_selection();
383};
384
385// Return the line number of the line nearest pixel position pos
386coverage.line_at_pos = function (pos) {
387    var l1 = coverage.line_elt(1),
388        l2 = coverage.line_elt(2),
389        result;
390    if (l1.length && l2.length) {
391        var l1_top = l1.offset().top,
392            line_height = l2.offset().top - l1_top,
393            nlines = (pos - l1_top) / line_height;
394        if (nlines < 1) {
395            result = 1;
396        }
397        else {
398            result = Math.ceil(nlines);
399        }
400    }
401    else {
402        result = 1;
403    }
404    return result;
405};
406
407// Returns 0, 1, or 2: how many of the two ends of the selection are on
408// the screen right now?
409coverage.selection_ends_on_screen = function () {
410    if (coverage.sel_begin === 0) {
411        return 0;
412    }
413
414    var top = coverage.line_elt(coverage.sel_begin);
415    var next = coverage.line_elt(coverage.sel_end-1);
416
417    return (
418        (top.isOnScreen() ? 1 : 0) +
419        (next.isOnScreen() ? 1 : 0)
420    );
421};
422
423coverage.to_next_chunk_nicely = function () {
424    coverage.finish_scrolling();
425    if (coverage.selection_ends_on_screen() === 0) {
426        // The selection is entirely off the screen: select the top line on
427        // the screen.
428        var win = $(window);
429        coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop()));
430    }
431    coverage.to_next_chunk();
432};
433
434coverage.to_prev_chunk_nicely = function () {
435    coverage.finish_scrolling();
436    if (coverage.selection_ends_on_screen() === 0) {
437        var win = $(window);
438        coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height()));
439    }
440    coverage.to_prev_chunk();
441};
442
443// Select line number lineno, or if it is in a colored chunk, select the
444// entire chunk
445coverage.select_line_or_chunk = function (lineno) {
446    var c = coverage;
447    var probe_line = c.line_elt(lineno);
448    if (probe_line.length === 0) {
449        return;
450    }
451    var the_color = probe_line.css("background-color");
452    if (!c.is_transparent(the_color)) {
453        // The line is in a highlighted chunk.
454        // Search backward for the first line.
455        var probe = lineno;
456        var color = the_color;
457        while (probe > 0 && color === the_color) {
458            probe--;
459            probe_line = c.line_elt(probe);
460            if (probe_line.length === 0) {
461                break;
462            }
463            color = probe_line.css("background-color");
464        }
465        var begin = probe + 1;
466
467        // Search forward for the last line.
468        probe = lineno;
469        color = the_color;
470        while (color === the_color) {
471            probe++;
472            probe_line = c.line_elt(probe);
473            color = probe_line.css("background-color");
474        }
475
476        coverage.set_sel(begin, probe);
477    }
478    else {
479        coverage.set_sel(lineno);
480    }
481};
482
483coverage.show_selection = function () {
484    var c = coverage;
485
486    // Highlight the lines in the chunk
487    c.code_container().find(".highlight").removeClass("highlight");
488    for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) {
489        c.num_elt(probe).addClass("highlight");
490    }
491
492    c.scroll_to_selection();
493};
494
495coverage.scroll_to_selection = function () {
496    // Scroll the page if the chunk isn't fully visible.
497    if (coverage.selection_ends_on_screen() < 2) {
498        // Need to move the page. The html,body trick makes it scroll in all
499        // browsers, got it from http://stackoverflow.com/questions/3042651
500        var top = coverage.line_elt(coverage.sel_begin);
501        var top_pos = parseInt(top.offset().top, 10);
502        coverage.scroll_window(top_pos - 30);
503    }
504};
505
506coverage.scroll_window = function (to_pos) {
507    $("html,body").animate({scrollTop: to_pos}, 200);
508};
509
510coverage.finish_scrolling = function () {
511    $("html,body").stop(true, true);
512};
513