File Coverage

blib/lib/Text/Diff/FormattedHTML.pm
Criterion Covered Total %
statement 20 107 18.6
branch 0 46 0.0
condition 0 6 0.0
subroutine 7 21 33.3
pod 3 3 100.0
total 30 183 16.3


\n", @_); \n"; \n", @_); \n", \n", \n",
line stmt bran cond sub pod time code
1             package Text::Diff::FormattedHTML;
2              
3 1     1   12783 use 5.006;
  1         2  
4 1     1   3 use strict;
  1         1  
  1         13  
5 1     1   7 use warnings;
  1         6  
  1         20  
6              
7 1     1   453 use File::Slurp;
  1         10146  
  1         53  
8 1     1   528 use Algorithm::Diff 'traverse_balanced';
  1         3414  
  1         45  
9 1     1   423 use String::Diff 'diff';
  1         1568  
  1         65  
10              
11 1     1   6 use base 'Exporter';
  1         1  
  1         889  
12              
13             our @EXPORT = (qw'diff_files diff_strings diff_css');
14              
15             =head1 NAME
16              
17             Text::Diff::FormattedHTML - Generate a colorful HTML diff of strings/files.
18              
19             =head1 VERSION
20              
21             Version 0.08
22              
23             =cut
24              
25             our $VERSION = '0.08';
26              
27              
28             =head1 SYNOPSIS
29              
30             use Text::Diff::FormattedHTML;
31              
32             my $output = diff_files($file1, $file2);
33              
34             # for strings
35              
36             my $output = diff_strings( { vertical => 1 }, $file1, $file2);
37              
38              
39             # as you might want some CSS:
40             open OUT, ">diff.html";
41             print OUT "\n";
42             print OUT diff_files('fileA', 'fileB');
43             close OUT;
44              
45              
46             =head1 DESCRIPTION
47              
48             Presents in a (nice?) HTML table the difference between two files or strings.
49             Inspired on GitHub diff view.
50              
51             =head1 SUBROUTINES
52              
53             =head2 diff_files
54              
55             my $html = diff_files("filename1", "filename2");
56              
57             C and C support a first optional argument
58             (an hash reference) where options can be set.
59              
60             Valid options are:
61              
62             =over 4
63              
64             =item C
65              
66             Can be set to a true value, for a more compact table.
67              
68             =item C
69              
70             Makes tables look nicer when there is a side with too many new lines.
71              
72             =back
73              
74             =cut
75              
76             sub diff_files {
77 0     0 1   my $settings = {};
78 0 0         $settings = shift if ref($_[0]) eq "HASH";
79              
80 0           my ($f1, $f2) = @_;
81              
82 0 0         die "$f1 not available" unless -f $f1;
83 0 0         die "$f2 not available" unless -f $f2;
84              
85 0           my @f1 = read_file $f1;
86 0           my @f2 = read_file $f2;
87              
88 0           _internal_diff($settings, \@f1, \@f2);
89             }
90              
91             =head2 diff_strings
92              
93             my $html = diff_strings("string1", "string2");
94              
95             Compare strings. First split by newline, and then treat them as file
96             content (see function above).
97              
98             =cut
99              
100             sub diff_strings {
101 0     0 1   my $settings = {};
102 0 0         $settings = shift if ref($_[0]) eq "HASH";
103              
104 0           my ($s1, $s2) = @_;
105 0           my @f1 = split /\n/, $s1;
106 0           my @f2 = split /\n/, $s2;
107 0           _internal_diff($settings, \@f1, \@f2);
108             }
109              
110             =head2 diff_css
111              
112             my $css = diff_css;
113              
114             Return the default css. You are invited to override it.
115              
116             =cut
117              
118             sub diff_css {
119 0     0 1   return <<'EOCSS';
120             table.diff {
121             border-collapse: collapse;
122             border-top: solid 1px #999999;
123             border-left: solid 1px #999999;
124             }
125              
126             table.diff td {
127             padding: 2px;
128             padding-left: 5px;
129             padding-right: 5px;
130             border-right: solid 1px #999999;
131             border-bottom: solid 1px #999999;
132             }
133              
134             table.diff td:nth-child(1),
135             table.diff td:nth-child(2) {
136             background-color: #deedff;
137             }
138              
139             table.diff tr.change,
140             table.diff tr.disc_a,
141             table.diff tr.disc_b {
142             background-color: #ffffdd;
143             }
144              
145             table.diff tr.del {
146             background-color: #ffeeee;
147             }
148              
149             table.diff tr.ins {
150             background-color: #eeffee;
151             }
152              
153              
154             table.diff td:nth-child(3),
155             table.diff td:nth-child(4) {
156             font-family: monospace;
157             white-space: pre;
158             }
159              
160             table.diff td ins {
161             padding: 2px;
162             color: #009900;
163             background-color: #ccffcc;
164             text-decoration: none;
165             font-weight: bold;
166             }
167              
168             table.diff td del {
169             padding: 2px;
170             color: #990000;
171             background-color: #ffcccc;
172             text-decoration: none;
173             font-weight: bold;
174             }
175              
176             EOCSS
177             }
178              
179             sub _protect {
180 0     0     my $x = shift;
181 0 0         if ($x) {
182 0           $x =~ s/&/&/g;
183 0           $x =~ s/
184 0           $x =~ s/>/>/g;
185             }
186 0           return $x;
187             }
188              
189             sub _internal_diff {
190 0     0     my ($settings, $sq1, $sq2) = @_;
191              
192             my $get = sub {
193 0     0     my ($l, $r) = @_;
194 0           $l = $sq1->[$l];
195 0           $r = $sq2->[$r];
196 0 0         chomp($l) if $l;
197 0 0         chomp($r) if $r;
198 0           return ($l,$r);
199 0           };
200              
201 0           my ($ll, $rl);
202              
203             my $line = sub {
204 0     0     sprintf("
%s%s%s%s
205 0           };
206              
207 0 0         if ($settings->{limit_onesided}) {
208             # Prevent really long lists where we just go on showing
209             # all of the values that one side does not have
210 0 0         if($settings->{vertical}){
211 0           die "Option: [vertical] is incompatible with [limit_empty]";
212             }
213 0           my ($am_skipping, $num_since_lc, $num_since_rc) = (0, 0, 0);
214             $line = sub {
215 0     0     my ($class, $ln, $rn, $l, $r) = @_;
216            
217 0           my $out = '';
218 0 0 0       if(
    0          
    0          
219             ($class ne 'disc_a') &&
220             ($class ne 'disc_b')
221             ){
222 0 0         if($am_skipping){
223 0           $out .= "($num_since_lc, $num_since_rc)
224             }
225 0           ($am_skipping, $num_since_lc, $num_since_rc) = (0, 0, 0);
226             }elsif($class ne 'disc_a'){
227 0           $num_since_lc++;
228             }elsif($class ne 'disc_b'){
229 0           $num_since_rc++;
230             }
231 0 0 0       if(
232             ($num_since_lc > $settings->{limit_onesided}) ||
233             ($num_since_rc > $settings->{limit_onesided})
234             ){
235 0 0         if(!$am_skipping){
236 0           $out = '
';
237 0           $am_skipping = 1;
238             }
239 0           $out .= '. ';
240 0           return $out;
241             }
242            
243 0           $out .= sprintf("
%s%s%s%s
244 0           return $out;
245 0           };
246             }
247              
248            
249 0 0         if ($settings->{vertical}) {
250             $line = sub {
251 0     0     my $out = "";
252 0           my ($class, $ln, $rn, $l, $r) = @_;
253 0 0         if ($l eq $r) {
254 0           $out .= sprintf("
%s%s%s
255             $class, $ln, $rn, $l);
256             } else {
257 0 0         $class eq "disc_a" && ($class = "disc_a del");
258 0 0         $class eq "disc_b" && ($class = "disc_b ins");
259              
260 0 0         $class eq "change" && ($class = "change del");
261 0 0         $l and $out .= sprintf("
%s%s
262             $class, $ln, $l);
263 0 0         $class eq "change del" && ($class = "change ins");
264 0 0         $r and $out .= sprintf("
%s%s
265             $class, $rn, $r);
266             }
267 0           $out
268             }
269 0           }
270              
271 0           my $out = "\n";
272              
273             traverse_balanced $sq1, $sq2,
274             {
275             MATCH => sub {
276 0     0     my ($l, $r) = $get->(@_);
277 0           ++$ll; ++$rl;
  0            
278 0           $out .= $line->('match', $ll, $rl, _protect($l), _protect($r));
279             },
280             DISCARD_A => sub {
281 0     0     my ($l, $r) = $get->(@_);
282 0           ++$ll;
283 0           $out .= $line->('disc_a', $ll, '', _protect($l), '');
284             },
285             DISCARD_B => sub {
286 0     0     my ($l, $r) = $get->(@_);
287 0           ++$rl;
288 0           $out .= $line->('disc_b', '', $rl, '', _protect($r));
289             },
290             CHANGE => sub {
291 0     0     my ($l, $r) = $get->(@_);
292 0           my $diff = diff($l, $r,
293             remove_open => '#del#',
294             remove_close => '#/del#',
295             append_open => '#ins#',
296             append_close => '#/ins#',
297             );
298 0           ++$ll; ++$rl;
  0            
299 0           $out .= $line->('change', $ll, $rl,
300             _retag(_protect($diff->[0])), _retag(_protect($diff->[1])));
301             },
302 0           };
303 0           $out .= "
\n";
304             }
305              
306             sub _retag {
307 0     0     my $x = shift;
308 0           $x =~ s/#(.?(?:del|ins))#/<$1>/g;
309 0           return $x;
310             }
311              
312             =head1 AUTHOR
313              
314             Alberto Simoes, C<< >>
315              
316             =head1 BUGS
317              
318             Please report any bugs or feature requests to
319             C, or through the web
320             interface at
321             L.
322             I will be notified, and then you'll automatically be notified of
323             progress on your bug as I make changes.
324              
325              
326              
327              
328             =head1 SUPPORT
329              
330             You can find documentation for this module with the perldoc command.
331              
332             perldoc Text::Diff::FormattedHTML
333              
334              
335             You can also look for information at:
336              
337             =over 4
338              
339             =item * RT: CPAN's request tracker (report bugs here)
340              
341             L
342              
343             =item * AnnoCPAN: Annotated CPAN documentation
344              
345             L
346              
347             =item * CPAN Ratings
348              
349             L
350              
351             =item * Search CPAN
352              
353             L
354              
355             =back
356              
357              
358             =head1 ACKNOWLEDGEMENTS
359              
360              
361             =head1 LICENSE AND COPYRIGHT
362              
363             Copyright 2011 Alberto Simoes.
364              
365             This program is free software; you can redistribute it and/or modify it
366             under the terms of either: the GNU General Public License as published
367             by the Free Software Foundation; or the Artistic License.
368              
369             See http://dev.perl.org/licenses/ for more information.
370              
371              
372             =cut
373              
374             1; # End of Text::Diff::FormattedHTML