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   202805 use strict;
  4         21  
  4         109  
4 4     4   17 use vars qw(@ISA $VERSION);
  4         8  
  4         186  
5 4     4   2168 use Algorithm::Diff ();
  4         17049  
  4         77  
6 4     4   391 use IO::File;
  4         6783  
  4         403  
7 4     4   23 use Carp;
  4         7  
  4         334  
8              
9             $VERSION = '0.09';
10              
11             # _Mastering Regular Expressions_, p. 132.
12             my $BEGIN_WORD = $] >= 5.006
13 4     4   2007 ? qr/(?:(?
  4         49  
  4         45  
14             : qr/(?:(?
15              
16             my %styles = (
17             ANSIColor => undef,
18             HTML => undef,
19             HTMLTwoLines => undef,
20             );
21              
22             sub import {
23 4     4   42 my $caller = caller;
24 4     4   75813 no strict 'refs';
  4         8  
  4         3449  
25 4         9 *{"$caller\::word_diff"} = \&word_diff;
  4         67  
26             }
27              
28             sub word_diff ($$;$) {
29 35     35 0 3341 my @seqs = ( shift, shift );
30 35 100       93 my $opts = $_[0] ? { %{ +shift } } : {};
  28         219  
31 35   50     201 $opts->{FILENAME_PREFIX_A} ||= '---';
32 35   50     146 $opts->{FILENAME_PREFIX_B} ||= '+++';
33 35         47 my $AorB = 'A';
34              
35 35         59 for my $seq (@seqs) {
36 70         114 my $type = ref $seq;
37              
38 70         138 while ( $type eq 'CODE' ) {
39 6         15 $seq = $seq->( $opts );
40 6         23 $type = ref $seq;
41             }
42              
43             # Get a handle on options.
44 70         196 my $filename = \$opts->{"FILENAME_$AorB"};
45 70         137 my $mtime = \$opts->{"MTIME_$AorB"};
46              
47 70 100 66     190 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         717 $seq = [ split $BEGIN_WORD, $$seq ];
54             }
55              
56             elsif ( !$type ) {
57             # Assume that it's a raw file name.
58 6 50       18 $$filename = $seq unless defined $$filename;
59 6 50       83 $$mtime = (stat $seq)[9] unless defined $$mtime;
60              
61             # Parse the words from the file.
62 6         49 my $seq_fh = IO::File->new($seq, '<');
63 6         600 $seq = do { local $/; [ split $BEGIN_WORD, <$seq_fh> ] };
  6         25  
  6         666  
64 6         44 $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         33  
  12         1329  
70             }
71              
72             else {
73             # Damn.
74 0         0 confess "Can't handle input of type $type";
75             }
76 70         373 $AorB++;
77             }
78              
79             # Set up the output handler.
80 35         45 my $output;
81 35         57 my $out_handler = delete $opts->{OUTPUT};
82 35         54 my $type = ref $out_handler ;
83              
84 35 100 33     115 if ( ! defined $out_handler ) {
    100          
    100          
    100          
    50          
85             # Default to concatenating a string.
86 23         33 $output = '';
87 23     549   83 $out_handler = sub { $output .= shift };
  549         3553  
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         17 my $out_ref = $out_handler;
95 3     27   15 $out_handler = sub { $$out_ref .= shift };
  27         180  
96             }
97             elsif ( $type eq 'ARRAY' ) {
98             # Push each item onto the array.
99 3         6 my $out_ref = $out_handler;
100 3     27   15 $out_handler = sub { push @$out_ref, shift };
  27         162  
101             }
102             elsif ( $type eq 'GLOB' || UNIVERSAL::isa( $out_handler, 'IO::Handle' )) {
103             # print to the file handle.
104 3         5 my $output_handle = $out_handler;
105 3     27   15 $out_handler = sub { print $output_handle shift };
  27         179  
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         164 my $diff = Algorithm::Diff->new(@seqs, delete $opts->{DIFF_OPTS});
114              
115             # Load the style class and instantiate an instance.
116 35   100     21640 my $style = delete $opts->{STYLE} || 'ANSIColor';
117 35 100       118 $style = __PACKAGE__ . "::$style" if exists $styles{$style};
118 35 50 0     277 eval "require $style" or die $@ unless $style->can('new');
119 35 50       121 $style = $style->new($opts) if !ref $style;
120              
121             # Run the diff.
122 35         57 my $hunks = 0;
123 35         85 $out_handler->($style->file_header());
124 35         108 while ($diff->Next) {
125 169         3352 $hunks++;
126 169         274 $out_handler->( $style->hunk_header() );
127              
128             # Output unchanged items.
129 169 100       290 if (my @same = $diff->Same) {
130 89         2482 $out_handler->( $style->same_items(@same) );
131             }
132              
133             # Output deleted and inserted items.
134             else {
135 80 50       770 if (my @del = $diff->Items(1)) {
136 80         1249 $out_handler->( $style->delete_items(@del) );
137             }
138 80 50       220 if (my @ins = $diff->Items(2)) {
139 80         1233 $out_handler->( $style->insert_items(@ins) );
140             }
141             }
142 169         477 $out_handler->( $style->hunk_footer() );
143             }
144 35         862 $out_handler->( $style->file_footer() );
145              
146 35 100       430 return defined $output ? $output : $hunks;
147             }
148              
149             package Text::WordDiff::Base;
150              
151             sub new {
152 35     35   64 my ($class, $opts) = @_;
153 35         41 return bless { %{$opts} } => $class;
  35         206  
154             }
155              
156              
157             sub file_header {
158 23     23   114 my $self = shift;
159 23         51 my $fn1 = $self->filename_a;
160 23         55 my $fn2 = $self->filename_b;
161 23 100 66     89 return '' unless defined $fn1 && defined $fn2;
162              
163 3         323 my $p1 = $self->filename_prefix_a;
164 3         15 my $t1 = $self->mtime_a;
165 3         12 my $p2 = $self->filename_prefix_b;
166 3         22 my $t2 = $self->mtime_b;
167              
168 3 50       455 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   95 sub hunk_header { return '' }
174 1     1   3 sub same_items { return '' }
175 1     1   2 sub insert_items { return '' }
176 1     1   3 sub delete_items { return '' }
177 57     57   87 sub hunk_footer { return '' }
178 12     12   24 sub file_footer { return '' }
179 34     34   67 sub filename_a { return shift->{FILENAME_A} }
180 34     34   50 sub filename_b { return shift->{FILENAME_B} }
181 4     4   9 sub mtime_a { return shift->{MTIME_A} }
182 4     4   10 sub mtime_b { return shift->{MTIME_B} }
183 4     4   14 sub filename_prefix_a { return shift->{FILENAME_PREFIX_A} }
184 4     4   9 sub filename_prefix_b { return shift->{FILENAME_PREFIX_B} }
185              
186             1;
187             __END__