File Coverage

blib/lib/Spreadsheet/Compare/Reporter/HTML.pm
Criterion Covered Total %
statement 35 117 29.9
branch 2 16 12.5
condition 0 10 0.0
subroutine 5 13 38.4
pod 7 8 87.5
total 49 164 29.8


line stmt bran cond sub pod time code
1             package Spreadsheet::Compare::Reporter::HTML;
2              
3             # TODO: (issue) better JAvascript event handler
4             # TODO: (issue) optional read templates/css from files
5              
6 2     2   3054 use Mojo::Base 'Spreadsheet::Compare::Reporter', -signatures;
  2         20  
  2         48  
7 2     2   363 use Spreadsheet::Compare::Common;
  2         17  
  2         12  
8 2     2   17 use Mojo::Template;
  2         11  
  2         29  
9              
10             has sheet_order => sub { [qw(Differences Missing Additional Duplicates All)] };
11              
12             my %format_defaults = (
13             fmt_head => 'font-weight: bold; text-align: left;',
14             fmt_headerr => 'background-color: yellow',
15             fmt_default => 'color: black;',
16             fmt_left_odd => 'color: blue;',
17             fmt_right_odd => 'color: red;',
18             fmt_diff_odd => 'color: green;',
19             fmt_left_even => 'color: blue; background-color: silver;',
20             fmt_right_even => 'color: red; background-color: silver;',
21             fmt_diff_even => 'color: green; background-color: silver;',
22             fmt_left_high => 'background-color: yellow;',
23             fmt_right_high => 'background-color: yellow;',
24             fmt_diff_high => 'background-color: yellow;',
25             fmt_left_low => 'background-color: lime;',
26             fmt_right_low => 'background-color: lime;',
27             fmt_diff_low => 'background-color: lime;',
28             );
29              
30             has $_, $format_defaults{$_} for keys %format_defaults;
31              
32             has report_filename => sub {
33             ( my $title = $_[0]->test_title ) =~ s/[^\w-]/_/g;
34             return "$title.html";
35             };
36              
37              
38 4     4 0 15 sub init ($self) {
  4         10  
  4         7  
39 4         12 my $css_txt = $self->css;
40 4         44 $css_txt .= ".$_ { " . $self->$_() . "}\n\n" for keys %format_defaults;
41 4         295 $self->css($css_txt);
42 4         56 return $self;
43             }
44              
45              
46 0     0 1 0 sub add_stream ( $self, $name ) {
  0         0  
  0         0  
  0         0  
47 0         0 $self->{ws}{$name} = {};
48 0         0 return $self;
49             }
50              
51              
52 0     0   0 sub _get_fmt ( $self, $name, $side ) {
  0         0  
  0         0  
  0         0  
  0         0  
53 0 0 0     0 $self->{$name}{odd} ^= 1
      0        
54             if $name eq 'Additional' and $side eq 'right'
55             or $side eq 'left';
56              
57 0 0       0 my $oe = $self->{$name}{odd} ? 'odd' : 'even';
58              
59 0         0 return ( "fmt_${side}_$oe", "fmt_${side}_high", "fmt_${side}_low" );
60             }
61              
62              
63 0     0 1 0 sub write_row ( $self, $name, $robj ) {
  0         0  
  0         0  
  0         0  
  0         0  
64              
65 0         0 my($fnorm) = $self->_get_fmt( $name, $robj->side );
66              
67 0   0     0 my $rref = $self->{ws}{$name}{rows} //= [];
68 0         0 INFO "write_row called\n";
69 0         0 push @$rref, {
70             data => $self->output_record($robj),
71             row_fmt => $fnorm,
72             };
73              
74 0         0 return $self;
75             }
76              
77              
78 0     0 1 0 sub write_fmt_row ( $self, $name, $robj ) {
  0         0  
  0         0  
  0         0  
  0         0  
79 0         0 my $data = $self->output_record($robj);
80 0         0 my $mask = $self->strip_ignore( $robj->limit_mask );
81 0         0 my $off = $self->head_offset;
82              
83 0         0 my( $fnorm, $fhigh, $flow ) = $self->_get_fmt( $name, $robj->side );
84              
85 0         0 my @fmts = map { $fnorm } 1 .. $off;
  0         0  
86 0 0       0 push @fmts, map { $_ ? ( $_ == 1 ? $fhigh : $flow ) : '' } @$mask;
  0 0       0  
87              
88 0   0     0 my $rref = $self->{ws}{$name}{rows} //= [];
89 0         0 push @$rref, {
90             data => $data,
91             row_fmt => $fnorm,
92             cell_fmt => \@fmts,
93             };
94              
95 0         0 return $self;
96             }
97              
98              
99 0     0 1 0 sub write_header ( $self, $name ) {
  0         0  
  0         0  
  0         0  
100             $self->{ws}{$name}{header} = {
101 0         0 data => $self->header,
102             row_fmt => "fmt_head",
103             };
104 0         0 return $self;
105             }
106              
107              
108 0     0 1 0 sub mark_header ( $self, $name, $mask ) {
  0         0  
  0         0  
  0         0  
  0         0  
109 0         0 my $smask = $self->strip_ignore($mask);
110 0         0 my $off = $self->head_offset;
111 0         0 my @fmts = map { '' } 1 .. $off;
  0         0  
112 0 0       0 push @fmts, map { $_ ? 'fmt_headerr' : '' } @$smask;
  0         0  
113 0         0 $self->{ws}{$name}{header}{cell_fmt} = \@fmts;
114 0         0 return $self;
115             }
116              
117              
118 2     2 1 6 sub write_summary ( $self, $summary, $filename ) {
  2         6  
  2         4  
  2         10  
  2         4  
119              
120 2 50       22 $filename .= '.html' unless $filename =~ /\.html$/i;
121 2         13 my $pout = $self->report_fullname($filename);
122 2         243 ( $self->{title} = $pout->basename ) =~ s/\.[^\.]*$//;
123 2         67 INFO "saving HTML summary to '$pout'";
124              
125 2         31 $self->{summary} = $summary;
126 2         14 $self->{header} = $self->stat_head;
127              
128 2         27 my $mt = Mojo::Template->new( vars => 1 );
129 2         22 my $tmpl = $self->summary_template;
130              
131 2         35 TRACE "Template:\n$tmpl";
132 2         41 my $res = $mt->render( $tmpl, $self );
133 2 50       23970 LOGDIE "Failed to render HTML template: $res" if ref($res) eq 'Mojo::Exception';
134              
135 2         29 $pout->parent->mkpath;
136 2         484 $pout->spew($res);
137              
138 2         1652 return $self;
139             }
140              
141              
142 0     0     sub _fill_html_template ($self) {
  0            
  0            
143 0           my @sheets = grep { $self->{ws}{$_} } @{ $self->sheet_order() };
  0            
  0            
144              
145 0           $self->{sheet_names} = \@sheets;
146 0           $self->{active} = 'Differences';
147              
148 0           my $mt = Mojo::Template->new( vars => 1 );
149 0           my $tmpl = $self->sheet_template;
150              
151 0           TRACE "Template:\n$tmpl";
152 0           my $res = $mt->render( $tmpl, $self );
153 0 0         LOGDIE "Failed to render HTML template: $res" if ref($res) eq 'Mojo::Exception';
154 0           return $res;
155             }
156              
157              
158 0     0 1   sub save_and_close ($self) {
  0            
  0            
159              
160 0           my $pout = $self->report_fullname;
161 0           INFO "saving HTML report to '$pout'";
162 0           ( $self->{title} = $pout->basename ) =~ s/\.[^\.]*$//;
163              
164 0           $pout->parent->mkpath;
165 0           $pout->spew( $self->_fill_html_template );
166              
167 0           return $self;
168             }
169              
170              
171             has sheet_template => <<'MOJO';
172             <!DOCTYPE html>
173             <html lang="en">
174             <head>
175             <meta charset="utf-8">
176             <meta http-equiv="X-UA-Compatible" content="IE=edge">
177             <meta name="viewport" content="width=device-width, initial-scale=1">
178              
179             <title><%= $title %></title>
180              
181             <style><%= $css %></style>
182              
183             <script>
184              
185             let in_scroll_timer;
186             let batch_size = 20;
187              
188             document.addEventListener('DOMContentLoaded', function () {
189             let active_top = document.getElementById('top-' + '<%= $active %>');
190             window.addEventListener('message', function (ev) {
191             if (ev.data === 'toggle') timed_scroll();
192             });
193             window.addEventListener('resize', timed_scroll);
194             window.addEventListener('scroll', timed_scroll);
195             toggle_tab(active_top);
196             });
197              
198             function timed_scroll () {
199             if ( in_scroll_timer ) return;
200             in_scroll_timer = setTimeout(function () {
201             in_scroll_timer = undefined;
202             load_rows();
203             }, 250);
204             }
205              
206             function toggle_tab (li) {
207             for (let el of document.querySelectorAll(".topbar-item") ) {
208             li === el ? el.classList.add('active') : el.classList.remove('active');
209             }
210             for (let el of document.querySelectorAll(".sheet") ) {
211             li.id === 'top-' + el.id ? el.classList.add('active') : el.classList.remove('active');
212             }
213             timed_scroll();
214             }
215              
216             function is_visible (el) {
217             let rect = el.getBoundingClientRect();
218             return ( rect.top >= 0 && rect.top <= window.innerHeight );
219             }
220              
221             function load_rows () {
222             let sheet = document.querySelector(".sheet.active");
223             let row = document.querySelector("#" + sheet.id + " .lastrow");
224             if (!row) return;
225             if (!is_visible(row)) return;
226             let rcount = 0;
227             let rsize = row.clientHeight ? window.innerHeight / row.clientHeight : batch_size;
228             do {
229             let next = row.nextElementSibling;
230             if (!next) break;
231             rcount++;
232             row.classList.remove("lastrow");
233             row = next;
234             row.classList.add("visible");
235             } while ( rcount <= rsize );
236             row.classList.add("lastrow");
237             }
238              
239             </script>
240              
241             </head>
242              
243             <body class="single-sheet" id="sheet-body">
244              
245             <div class="topbar" id="div-topbar">
246             <ul class="test">
247             <% for my $sheet ( @$sheet_names ) { =%>
248             <li class="topbar-item" id="top-<%= $sheet %>" onclick="toggle_tab(this)"><a href="#"><%= $sheet %></a></li>
249             <% } =%>
250             </ul>
251             </div> <!-- topbar -->
252              
253             <% for my $sheet ( @$sheet_names ) { =%>
254             <div class="sheet" id="<%= $sheet %>">
255             <table>
256             <thead>
257             <% my $hdr = $ws->{$sheet}{header}; =%>
258             <% my @htxt = $hdr->{data}->@*; =%>
259             <% my $trclass = $hdr->{row_fmt} // ''; =%>
260              
261             <tr class="header-row <%= $trclass %>">
262             <% for my $i ( 0 .. $#htxt ) { =%>
263             <% my $thclass = $hdr->{cell_fmt} ? $hdr->{cell_fmt}[$i] // '': ''; =%>
264             <th class="<%= $thclass %>"><%= $htxt[$i] // '' %></th>
265             <% } =%>
266             </tr>
267             </thead>
268             <tbody>
269             <% my $rows = $ws->{$sheet}{rows}; =%>
270             <% my $rowcount = 0; =%>
271             <% for my $r ( @$rows ) { =%>
272             <% my $trclass = $r->{row_fmt} // ''; =%>
273             <% $trclass .= ' visible lastrow' unless $rowcount++; =%>
274             <% my @rtxt = $r->{data}->@*; =%>
275             <tr class="<%= $trclass %>">
276             <% for my $i ( 0 .. $#rtxt ) { =%>
277             <% my $tdclass = $r->{cell_fmt} ? $r->{cell_fmt}[$i] // '': ''; =%>
278             <td class="<%= $tdclass %>"><%= $rtxt[$i] // ''%></td>
279             <% } =%>
280             </tr>
281             <% } =%>
282             </tbody>
283             </table>
284             </div> <!-- sheet -->
285             <% } =%>
286              
287             </body>
288             </html>
289             MOJO
290              
291              
292             has css => <<'CSS';
293             html {
294             margin: 0;
295             padding: 0;
296             height: 100%;
297             font-family: sans-serif;
298             font-size: medium;
299             }
300              
301             body {
302             margin: 0;
303             padding: 0;
304             /* overflow: hidden; */
305             display: flex;
306             height: 100%;
307             line-height: inherit; background-color: gray;
308             }
309              
310             body.single-sheet {
311             flex-direction: column;
312             }
313              
314             body.summary {
315             flex-direction: row;
316             }
317              
318             .topbar ul {
319             list-style: none;
320             width: 100%;
321             margin-block-start: 0.5rem;
322             margin-block-end: unset;
323             padding-inline-start: .5rem;
324             }
325              
326             .topbar li {
327             padding: .5rem .5rem .5rem;
328             display: inline-block;
329             width: 5rem;
330             text-align: center;
331             border-style: solid;
332             border-color: gray;
333             border-width: 1px 1px 0 1px;
334             border-radius: .5rem .5rem 0 0;
335             background-color: silver;
336             }
337              
338             .topbar li.active {
339             background-color: white;
340             }
341              
342             .topbar li:hover {
343             text-decoration: none;
344             background-color: white;
345             }
346              
347             .sidebar {
348             display: flex;
349             order: -1;
350             flex: 0 0 20rem;
351             height: 100%;
352             overflow: auto;
353             background-color: #262626;
354             }
355              
356             .sidebar ul {
357             list-style: none;
358             width: 100%;
359             margin-block-start: .3rem;
360             margin-block-end: unset;
361             padding-inline-start: .3rem;
362             }
363              
364             .sidebar li {
365             padding-left: .3rem;
366             padding-top: .5rem;
367             padding-bottom: .5rem;
368             display: block;
369             text-align: left;
370             }
371              
372             .sidebar li.active {
373             background-color: #404040;
374             }
375              
376             .sidebar li:hover {
377             text-decoration: none;
378             background-color: #404040;
379             }
380              
381             li.sidebar-test-item {
382             padding-left: 1rem;
383             }
384              
385             .suite-table, .test-iframe {
386             flex: 1;
387             overflow: auto;
388             }
389              
390             a {
391             color: black;
392             text-decoration: none;
393             }
394              
395             .sidebar li a {
396             color: #bfbfbf;
397             }
398              
399             .sheet {
400             display: block;
401             visibility: hidden;
402             opacity: 0;
403             transition: visibility 0s, opacity 0.1s linear;
404             position: absolute;
405             }
406              
407             .suite-table, .test-iframe {
408             display: none;
409             }
410              
411             .sheet.active {
412             opacity: 1;
413             visibility: visible;
414             position: static;
415             }
416              
417             .suite-table.active, .test-iframe.active {
418             display: flex;
419             }
420              
421             .suite-table th.left {
422             text-align: left;
423             }
424              
425             table {
426             background-color: white;
427             }
428              
429             table, th, td {
430             border-collapse:collapse;
431             }
432              
433              
434             tr {
435             display: none;
436             }
437              
438             tr.visible, .suite-row, .header-row, .suite-header {
439             display: table-row;
440             }
441              
442             th, td {
443             border:thin solid black;
444             padding:5px;
445             white-space: nowrap;
446             text-align: left;
447             }
448              
449             th {
450             position: sticky;
451             top: 0;
452             background-color: white;
453             }
454             CSS
455              
456              
457             has summary_template => <<'MOJO';
458             <!DOCTYPE html>
459             <html lang="en">
460             <head>
461             <meta charset="utf-8">
462             <meta http-equiv="X-UA-Compatible" content="IE=edge">
463             <meta name="viewport" content="width=device-width, initial-scale=1">
464              
465             <title><%= $title %></title>
466              
467             <style><%= $css %></style>
468              
469             <script>
470              
471             function toggle_test (ev, li) {
472             ev.stopPropagation();
473             for (let el of document.querySelectorAll(".sidebar-item") ) {
474             el.classList.remove('active');
475             }
476             for (let el of document.querySelectorAll(".suite-table, .test-iframe") ) {
477             if (li.id === 'side-' + el.id ) {
478             el.classList.add('active');
479             if ( el.classList.contains("test-iframe") ) {
480             el.contentWindow.postMessage('toggle', '*');
481             }
482             } else {
483             el.classList.remove('active');
484             }
485             }
486             li.classList.add('active');
487             }
488              
489             </script>
490              
491             </head>
492              
493             <body class="summary">
494              
495             <div class="sidebar" id="div-sidebar">
496             <ul class="suite">
497             <% my $i = 0; =%>
498             <% for my $suite ( sort keys %$summary ) { =%>
499             <% my $active = $i++ ? '' : 'active'; =%>
500             <li class="sidebar-item sidebar-suite-item <%= $active %>" id="side-<%= $suite %>" onclick="toggle_test(event, this)"><a href="#"><%= $suite %></a></li>
501             <% for my $test ( $summary->{$suite}->@* ) { =%>
502             <li class="sidebar-item sidebar-test-item" id="side-<%= $test->{full} %>" onclick="toggle_test(event, this)"><a href="#"><%= $test->{title} %></a></li>
503             <% } =%>
504             <% } =%>
505             </ul>
506             </div> <!-- sidebar -->
507              
508             <% $i = 0; =%>
509             <% for my $suite ( sort keys %$summary ) { =%>
510             <% my $active = $i++ ? '' : 'active'; =%>
511             <div class="suite-table <%= $active %>" id="<%= $suite %>">
512             <table>
513             <thead>
514             <tr class="suite-header">
515             <% for my $htxt ( @$header ) { =%>
516             <% next if $htxt eq 'link'; =%>
517             <th><%= $htxt =%></th>
518             <% } =%>
519             </tr>
520             </thead>
521             <tbody>
522             <% for my $test ( $summary->{$suite}->@* ) { =%>
523             <% my $result = $test->{result}; =%>
524             <% $result->{title} = $test->{title}; =%>
525             <% my @hdr = grep { $_ ne 'link' } @$header; %>
526             <% my $data = [ @$result{@hdr} ]; =%>
527             <tr class="suite-row">
528             <% for my $dtxt ( @$data ) { =%>
529             <% $dtxt //= ''; =%>
530             <% my $class = $dtxt =~ /^[a-z]/i ? 'left' : 'right'; =%>
531             <th class="<%= $class %>"><%= $dtxt %></th>
532             <% } =%>
533             </tr>
534             <% } =%>
535             </tbody>
536             </table>
537             </div>
538              
539             <% for my $test ( $summary->{$suite}->@* ) { =%>
540             <iframe class="test-iframe" id="<%= $test->{full} %>" src="<%= $test->{report} %>"></iframe>
541             <% } =%>
542             <% } =%>
543              
544             </body>
545             </html>
546             MOJO
547              
548              
549             1;
550              
551             =head1 NAME
552              
553             Spreadsheet::Compare::Reporter::HMTL - HTML Report Adapter for Spreadsheet::Compare
554              
555             =head1 DESCRIPTION
556              
557             Handles writing Spreadsheet::Compare reports in HTML format.
558              
559             =head1 ATTRIBUTES
560              
561             The format attributes have to be valid css styles
562              
563             The defaults for the attributes are:
564              
565             fmt_head => 'font-weight: bold; text-align: left;',
566             fmt_headerr => 'background-color: yellow',
567             fmt_default => 'color: black;',
568             fmt_left_odd => 'color: blue;',
569             fmt_right_odd => 'color: red;',
570             fmt_diff_odd => 'color: green;',
571             fmt_left_even => 'color: blue; background-color: silver;',
572             fmt_right_even => 'color: red; background-color: silver;',
573             fmt_diff_even => 'color: green; background-color: silver;',
574             fmt_left_high => 'background-color: yellow;',
575             fmt_right_high => 'background-color: yellow;',
576             fmt_diff_high => 'background-color: yellow;',
577             fmt_left_low => 'background-color: lime;',
578             fmt_right_low => 'background-color: lime;',
579             fmt_diff_low => 'background-color: lime;',
580              
581             =head2 css
582              
583             A scalar containing the style sheet used for all HTML output.
584              
585             =head2 sheet_order
586              
587             A reference to an array with stream names setting the order of the output worksheet tabs
588             default is: [qw(Differences Missing Additional Duplicates All)]
589              
590             =head2 sheet_template
591              
592             A scalar containing a Mojo::Template for constructing an output worksheet.
593              
594             =head2 summary_template
595              
596             A scalar containing a Mojo::Template for constructing the summary page.
597              
598             =head1 METHODS
599              
600             see L<Spreadsheet::Compare::Reporter>
601              
602             =cut