File Coverage

blib/lib/Text/WordDiff.pm
Criterion Covered Total %
statement 109 111 98.2
branch 34 44 77.2
condition 9 17 52.9
subroutine 27 27 100.0
pod 0 1 0.0
total 179 200 89.5


line stmt bran cond sub pod time code
1             package Text::WordDiff;
2              
3 4     4   164941 use strict;
  4         10  
  4         184  
4 4     4   27 use vars qw(@ISA $VERSION);
  4         6  
  4         259  
5 4     4   5143 use Algorithm::Diff ();
  4         25561  
  4         103  
6 4     4   1371 use IO::File;
  4         14581  
  4         924  
7 4     4   31 use Carp;
  4         12  
  4         630  
8              
9             $VERSION = '0.08';
10              
11             # _Mastering Regular Expressions_, p. 132.
12             my $BEGIN_WORD = $] >= 5.006
13 4     4   4650 ? qr/(?:(?
  4         44  
  4         92  
14             : qr/(?:(?
15              
16             my %styles = (
17             ANSIColor => undef,
18             HTML => undef,
19             HTMLTwoLines => undef,
20             );
21              
22             sub import {
23 4     4   57 my $caller = caller;
24 4     4   159266 no strict 'refs';
  4         10  
  4         5371  
25 4         12 *{"$caller\::word_diff"} = \&word_diff;
  4         109  
26             }
27              
28             sub word_diff ($$;$) {
29 35     35 0 4640 my @seqs = ( shift, shift );
30 35 100       112 my $opts = $_[0] ? { %{ +shift } } : {};
  28         173  
31 35   50     239 $opts->{FILENAME_PREFIX_A} ||= '---';
32 35   50     167 $opts->{FILENAME_PREFIX_B} ||= '+++';
33 35         55 my $AorB = 'A';
34              
35 35         74 for my $seq (@seqs) {
36 70         125 my $type = ref $seq;
37              
38 70         208 while ( $type eq 'CODE' ) {
39 6         19 $seq = $seq->( $opts );
40 6         31 $type = ref $seq;
41             }
42              
43             # Get a handle on options.
44 70         186 my $filename = \$opts->{"FILENAME_$AorB"};
45 70         175 my $mtime = \$opts->{"MTIME_$AorB"};
46              
47 70 100 66     289 if ( $type eq 'ARRAY' ) {
    100          
    100          
    50          
48             # The work has already been done for us.
49             }
50              
51             elsif ( $type eq 'SCALAR' ) {
52             # Parse the words from the string.
53 43         738 $seq = [ split $BEGIN_WORD, $$seq ];
54             }
55              
56             elsif ( !$type ) {
57             # Assume that it's a raw file name.
58 6 50       21 $$filename = $seq unless defined $$filename;
59 6 50       103 $$mtime = (stat $seq)[9] unless defined $$mtime;
60              
61             # Parse the words from the file.
62 6         53 my $seq_fh = IO::File->new($seq, '<');
63 6         610 $seq = do { local $/; [ split $BEGIN_WORD, <$seq_fh> ] };
  6         26  
  6         944  
64 6         70 $seq_fh->close;
65             }
66              
67             elsif ( $type eq "GLOB" || UNIVERSAL::isa( $seq, "IO::Handle" ) ) {
68             # Parse the words from the file.
69 12         17 $seq = do { local $/; [ split $BEGIN_WORD, <$seq> ] };
  12         43  
  12         2349  
70             }
71              
72             else {
73             # Damn.
74 0         0 confess "Can't handle input of type $type";
75             }
76 70         357 $AorB++;
77             }
78              
79             # Set up the output handler.
80 35         60 my $output;
81 35         75 my $out_handler = delete $opts->{OUTPUT};
82 35         74 my $type = ref $out_handler ;
83              
84 35 100 33     180 if ( ! defined $out_handler ) {
    100          
    100          
    100          
    50          
85             # Default to concatenating a string.
86 23         80 $output = '';
87 23     549   109 $out_handler = sub { $output .= shift };
  549         4971  
88             }
89             elsif ( $type eq 'CODE' ) {
90             # We'll just use the handler.
91             }
92             elsif ( $type eq 'SCALAR' ) {
93             # Append to the scalar reference.
94 3         7 my $out_ref = $out_handler;
95 3     27   19 $out_handler = sub { $$out_ref .= shift };
  27         217  
96             }
97             elsif ( $type eq 'ARRAY' ) {
98             # Push each item onto the array.
99 3         6 my $out_ref = $out_handler;
100 3     27   20 $out_handler = sub { push @$out_ref, shift };
  27         243  
101             }
102             elsif ( $type eq 'GLOB' || UNIVERSAL::isa( $out_handler, 'IO::Handle' )) {
103             # print to the file handle.
104 3         7 my $output_handle = $out_handler;
105 3     27   18 $out_handler = sub { print $output_handle shift };
  27         266  
106             }
107             else {
108             # D'oh!
109 0         0 croak "Unrecognized output type: $type";
110             }
111              
112             # Instantiate the diff object, along with any options.
113 35         232 my $diff = Algorithm::Diff->new(@seqs, delete $opts->{DIFF_OPTS});
114              
115             # Load the style class and instantiate an instance.
116 35   100     23376 my $style = delete $opts->{STYLE} || 'ANSIColor';
117 35 100       167 $style = __PACKAGE__ . "::$style" if exists $styles{$style};
118 35 50 0     349 eval "require $style" or die $@ unless $style->can('new');
119 35 50       151 $style = $style->new($opts) if !ref $style;
120              
121             # Run the diff.
122 35         101 my $hunks = 0;
123 35         143 $out_handler->($style->file_header());
124 35         156 while ($diff->Next) {
125 169         2654 $hunks++;
126 169         398 $out_handler->( $style->hunk_header() );
127              
128             # Output unchanged items.
129 169 100       442 if (my @same = $diff->Same) {
130 89         2153 $out_handler->( $style->same_items(@same) );
131             }
132              
133             # Output deleted and inserted items.
134             else {
135 80 50       790 if (my @del = $diff->Items(1)) {
136 80         1276 $out_handler->( $style->delete_items(@del) );
137             }
138 80 50       506 if (my @ins = $diff->Items(2)) {
139 80         1773 $out_handler->( $style->insert_items(@ins) );
140             }
141             }
142 169         604 $out_handler->( $style->hunk_footer() );
143             }
144 35         647 $out_handler->( $style->file_footer() );
145              
146 35 100       745 return defined $output ? $output : $hunks;
147             }
148              
149             package Text::WordDiff::Base;
150              
151             sub new {
152 35     35   100 my ($class, $opts) = @_;
153 35         51 return bless { %{$opts} } => $class;
  35         273  
154             }
155              
156              
157             sub file_header {
158 23     23   61 my $self = shift;
159 23         74 my $fn1 = $self->filename_a;
160 23         82 my $fn2 = $self->filename_b;
161 23 100 66     143 return '' unless defined $fn1 && defined $fn2;
162              
163 3         28 my $p1 = $self->filename_prefix_a;
164 3         27 my $t1 = $self->mtime_a;
165 3         37 my $p2 = $self->filename_prefix_b;
166 3         19 my $t2 = $self->mtime_b;
167              
168 3 50       228 return "$p1 $fn1" . (defined $t1 ? "\t" . localtime $t1 : '') . "\n"
    50          
169             . "$p2 $fn2" . (defined $t2 ? "\t" . localtime $t2 : '') . "\n"
170             ;
171             }
172              
173 57     57   114 sub hunk_header { return '' }
174 1     1   3 sub same_items { return '' }
175 1     1   3 sub insert_items { return '' }
176 1     1   3 sub delete_items { return '' }
177 57     57   110 sub hunk_footer { return '' }
178 12     12   26 sub file_footer { return '' }
179 34     34   95 sub filename_a { return shift->{FILENAME_A} }
180 34     34   77 sub filename_b { return shift->{FILENAME_B} }
181 4     4   18 sub mtime_a { return shift->{MTIME_A} }
182 4     4   12 sub mtime_b { return shift->{MTIME_B} }
183 4     4   15 sub filename_prefix_a { return shift->{FILENAME_PREFIX_A} }
184 4     4   13 sub filename_prefix_b { return shift->{FILENAME_PREFIX_B} }
185              
186             1;
187             __END__