// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 // For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt // Coverage.py HTML report browser code. /*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ /*global coverage: true, document, window, $ */ coverage = {}; // Find all the elements with shortkey_* class, and use them to assign a shortcut key. coverage.assign_shortkeys = function () { $("*[class*='shortkey_']").each(function (i, e) { $.each($(e).attr("class").split(" "), function (i, c) { if (/^shortkey_/.test(c)) { $(document).bind('keydown', c.substr(9), function () { $(e).click(); }); } }); }); }; // Create the events for the help panel. coverage.wire_up_help_panel = function () { $("#keyboard_icon").click(function () { // Show the help panel, and position it so the keyboard icon in the // panel is in the same place as the keyboard icon in the header. $(".help_panel").show(); var koff = $("#keyboard_icon").offset(); var poff = $("#panel_icon").position(); $(".help_panel").offset({ top: koff.top-poff.top, left: koff.left-poff.left }); }); $("#panel_icon").click(function () { $(".help_panel").hide(); }); }; // Create the events for the filter box. coverage.wire_up_filter = function () { // Cache elements. var table = $("table.index"); var table_rows = table.find("tbody tr"); var table_row_names = table_rows.find("td.name a"); var no_rows = $("#no_rows"); // Create a duplicate table footer that we can modify with dynamic summed values. var table_footer = $("table.index tfoot tr"); var table_dynamic_footer = table_footer.clone(); table_dynamic_footer.attr('class', 'total_dynamic hidden'); table_footer.after(table_dynamic_footer); // Observe filter keyevents. $("#filter").on("keyup change", $.debounce(150, function (event) { var filter_value = $(this).val(); if (filter_value === "") { // Filter box is empty, remove all filtering. table_rows.removeClass("hidden"); // Show standard footer, hide dynamic footer. table_footer.removeClass("hidden"); table_dynamic_footer.addClass("hidden"); // Hide placeholder, show table. if (no_rows.length > 0) { no_rows.hide(); } table.show(); } else { // Filter table items by value. var hidden = 0; var shown = 0; // Hide / show elements. $.each(table_row_names, function () { var element = $(this).parents("tr"); if ($(this).text().indexOf(filter_value) === -1) { // hide element.addClass("hidden"); hidden++; } else { // show element.removeClass("hidden"); shown++; } }); // Show placeholder if no rows will be displayed. if (no_rows.length > 0) { if (shown === 0) { // Show placeholder, hide table. no_rows.show(); table.hide(); } else { // Hide placeholder, show table. no_rows.hide(); table.show(); } } // Manage dynamic header: if (hidden > 0) { // Calculate new dynamic sum values based on visible rows. for (var column = 2; column < 20; column++) { // Calculate summed value. var cells = table_rows.find('td:nth-child(' + column + ')'); if (!cells.length) { // No more columns...! break; } var sum = 0, numer = 0, denom = 0; $.each(cells.filter(':visible'), function () { var ratio = $(this).data("ratio"); if (ratio) { var splitted = ratio.split(" "); numer += parseInt(splitted[0], 10); denom += parseInt(splitted[1], 10); } else { sum += parseInt(this.innerHTML, 10); } }); // Get footer cell element. var footer_cell = table_dynamic_footer.find('td:nth-child(' + column + ')'); // Set value into dynamic footer cell element. if (cells[0].innerHTML.indexOf('%') > -1) { // Percentage columns use the numerator and denominator, // and adapt to the number of decimal places. var match = /\.([0-9]+)/.exec(cells[0].innerHTML); var places = 0; if (match) { places = match[1].length; } var pct = numer * 100 / denom; footer_cell.text(pct.toFixed(places) + '%'); } else { footer_cell.text(sum); } } // Hide standard footer, show dynamic footer. table_footer.addClass("hidden"); table_dynamic_footer.removeClass("hidden"); } else { // Show standard footer, hide dynamic footer. table_footer.removeClass("hidden"); table_dynamic_footer.addClass("hidden"); } } })); // Trigger change event on setup, to force filter on page refresh // (filter value may still be present). $("#filter").trigger("change"); }; // Loaded on index.html coverage.index_ready = function ($) { // Look for a localStorage item containing previous sort settings: var sort_list = []; var storage_name = "COVERAGE_INDEX_SORT"; var stored_list = undefined; try { stored_list = localStorage.getItem(storage_name); } catch(err) {} if (stored_list) { sort_list = JSON.parse('[[' + stored_list + ']]'); } // Create a new widget which exists only to save and restore // the sort order: $.tablesorter.addWidget({ id: "persistentSort", // Format is called by the widget before displaying: format: function (table) { if (table.config.sortList.length === 0 && sort_list.length > 0) { // This table hasn't been sorted before - we'll use // our stored settings: $(table).trigger('sorton', [sort_list]); } else { // This is not the first load - something has // already defined sorting so we'll just update // our stored value to match: sort_list = table.config.sortList; } } }); // Configure our tablesorter to handle the variable number of // columns produced depending on report options: var headers = []; var col_count = $("table.index > thead > tr > th").length; headers[0] = { sorter: 'text' }; for (i = 1; i < col_count-1; i++) { headers[i] = { sorter: 'digit' }; } headers[col_count-1] = { sorter: 'percent' }; // Enable the table sorter: $("table.index").tablesorter({ widgets: ['persistentSort'], headers: headers }); coverage.assign_shortkeys(); coverage.wire_up_help_panel(); coverage.wire_up_filter(); // Watch for page unload events so we can save the final sort settings: $(window).unload(function () { try { localStorage.setItem(storage_name, sort_list.toString()) } catch(err) {} }); }; // -- pyfile stuff -- coverage.pyfile_ready = function ($) { // If we're directed to a particular line number, highlight the line. var frag = location.hash; if (frag.length > 2 && frag[1] === 't') { $(frag).addClass('highlight'); coverage.set_sel(parseInt(frag.substr(2), 10)); } else { coverage.set_sel(0); } $(document) .bind('keydown', 'j', coverage.to_next_chunk_nicely) .bind('keydown', 'k', coverage.to_prev_chunk_nicely) .bind('keydown', '0', coverage.to_top) .bind('keydown', '1', coverage.to_first_chunk) ; $(".button_toggle_run").click(function (evt) {coverage.toggle_lines(evt.target, "run");}); $(".button_toggle_exc").click(function (evt) {coverage.toggle_lines(evt.target, "exc");}); $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");}); $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");}); coverage.assign_shortkeys(); coverage.wire_up_help_panel(); coverage.init_scroll_markers(); // Rebuild scroll markers when the window height changes. $(window).resize(coverage.build_scroll_markers); }; coverage.toggle_lines = function (btn, cls) { btn = $(btn); var show = "show_"+cls; if (btn.hasClass(show)) { $("#source ." + cls).removeClass(show); btn.removeClass(show); } else { $("#source ." + cls).addClass(show); btn.addClass(show); } coverage.build_scroll_markers(); }; // Return the nth line div. coverage.line_elt = function (n) { return $("#t" + n); }; // Return the nth line number div. coverage.num_elt = function (n) { return $("#n" + n); }; // Set the selection. b and e are line numbers. coverage.set_sel = function (b, e) { // The first line selected. coverage.sel_begin = b; // The next line not selected. coverage.sel_end = (e === undefined) ? b+1 : e; }; coverage.to_top = function () { coverage.set_sel(0, 1); coverage.scroll_window(0); }; coverage.to_first_chunk = function () { coverage.set_sel(0, 1); coverage.to_next_chunk(); }; // Return a string indicating what kind of chunk this line belongs to, // or null if not a chunk. coverage.chunk_indicator = function (line_elt) { var klass = line_elt.attr('class'); if (klass) { var m = klass.match(/\bshow_\w+\b/); if (m) { return m[0]; } } return null; }; coverage.to_next_chunk = function () { var c = coverage; // Find the start of the next colored chunk. var probe = c.sel_end; var chunk_indicator, probe_line; while (true) { probe_line = c.line_elt(probe); if (probe_line.length === 0) { return; } chunk_indicator = c.chunk_indicator(probe_line); if (chunk_indicator) { break; } probe++; } // There's a next chunk, `probe` points to it. var begin = probe; // Find the end of this chunk. var next_indicator = chunk_indicator; while (next_indicator === chunk_indicator) { probe++; probe_line = c.line_elt(probe); next_indicator = c.chunk_indicator(probe_line); } c.set_sel(begin, probe); c.show_selection(); }; coverage.to_prev_chunk = function () { var c = coverage; // Find the end of the prev colored chunk. var probe = c.sel_begin-1; var probe_line = c.line_elt(probe); if (probe_line.length === 0) { return; } var chunk_indicator = c.chunk_indicator(probe_line); while (probe > 0 && !chunk_indicator) { probe--; probe_line = c.line_elt(probe); if (probe_line.length === 0) { return; } chunk_indicator = c.chunk_indicator(probe_line); } // There's a prev chunk, `probe` points to its last line. var end = probe+1; // Find the beginning of this chunk. var prev_indicator = chunk_indicator; while (prev_indicator === chunk_indicator) { probe--; probe_line = c.line_elt(probe); prev_indicator = c.chunk_indicator(probe_line); } c.set_sel(probe+1, end); c.show_selection(); }; // Return the line number of the line nearest pixel position pos coverage.line_at_pos = function (pos) { var l1 = coverage.line_elt(1), l2 = coverage.line_elt(2), result; if (l1.length && l2.length) { var l1_top = l1.offset().top, line_height = l2.offset().top - l1_top, nlines = (pos - l1_top) / line_height; if (nlines < 1) { result = 1; } else { result = Math.ceil(nlines); } } else { result = 1; } return result; }; // Returns 0, 1, or 2: how many of the two ends of the selection are on // the screen right now? coverage.selection_ends_on_screen = function () { if (coverage.sel_begin === 0) { return 0; } var top = coverage.line_elt(coverage.sel_begin); var next = coverage.line_elt(coverage.sel_end-1); return ( (top.isOnScreen() ? 1 : 0) + (next.isOnScreen() ? 1 : 0) ); }; coverage.to_next_chunk_nicely = function () { coverage.finish_scrolling(); if (coverage.selection_ends_on_screen() === 0) { // The selection is entirely off the screen: select the top line on // the screen. var win = $(window); coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop())); } coverage.to_next_chunk(); }; coverage.to_prev_chunk_nicely = function () { coverage.finish_scrolling(); if (coverage.selection_ends_on_screen() === 0) { var win = $(window); coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height())); } coverage.to_prev_chunk(); }; // Select line number lineno, or if it is in a colored chunk, select the // entire chunk coverage.select_line_or_chunk = function (lineno) { var c = coverage; var probe_line = c.line_elt(lineno); if (probe_line.length === 0) { return; } var the_indicator = c.chunk_indicator(probe_line); if (the_indicator) { // The line is in a highlighted chunk. // Search backward for the first line. var probe = lineno; var indicator = the_indicator; while (probe > 0 && indicator === the_indicator) { probe--; probe_line = c.line_elt(probe); if (probe_line.length === 0) { break; } indicator = c.chunk_indicator(probe_line); } var begin = probe + 1; // Search forward for the last line. probe = lineno; indicator = the_indicator; while (indicator === the_indicator) { probe++; probe_line = c.line_elt(probe); indicator = c.chunk_indicator(probe_line); } coverage.set_sel(begin, probe); } else { coverage.set_sel(lineno); } }; coverage.show_selection = function () { var c = coverage; // Highlight the lines in the chunk $(".linenos .highlight").removeClass("highlight"); for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { c.num_elt(probe).addClass("highlight"); } c.scroll_to_selection(); }; coverage.scroll_to_selection = function () { // Scroll the page if the chunk isn't fully visible. if (coverage.selection_ends_on_screen() < 2) { // Need to move the page. The html,body trick makes it scroll in all // browsers, got it from http://stackoverflow.com/questions/3042651 var top = coverage.line_elt(coverage.sel_begin); var top_pos = parseInt(top.offset().top, 10); coverage.scroll_window(top_pos - 30); } }; coverage.scroll_window = function (to_pos) { $("html,body").animate({scrollTop: to_pos}, 200); }; coverage.finish_scrolling = function () { $("html,body").stop(true, true); }; coverage.init_scroll_markers = function () { var c = coverage; // Init some variables c.lines_len = $('#source p').length; c.body_h = $('body').height(); c.header_h = $('div#header').height(); // Build html c.build_scroll_markers(); }; coverage.build_scroll_markers = function () { var c = coverage, min_line_height = 3, max_line_height = 10, visible_window_h = $(window).height(); c.lines_to_mark = $('#source').find('p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par'); $('#scroll_marker').remove(); // Don't build markers if the window has no scroll bar. if (c.body_h <= visible_window_h) { return; } $("body").append("