File Coverage

blib/lib/App/diff2vba.pm
Criterion Covered Total %
statement 50 173 28.9
branch 0 48 0.0
condition 0 12 0.0
subroutine 17 34 50.0
pod 1 14 7.1
total 68 281 24.2


line stmt bran cond sub pod time code
1             package App::diff2vba;
2 1     1   393608 use 5.014;
  1         4  
3 1     1   7 use warnings;
  1         3  
  1         109  
4              
5             our $VERSION = "1.00";
6              
7 1     1   657 use utf8;
  1         350  
  1         7  
8 1     1   38 use Encode;
  1         2  
  1         115  
9 1     1   694 use Data::Dumper;
  1         9472  
  1         92  
10             {
11 1     1   7 no warnings 'redefine';
  1         2  
  1         137  
12 0     0     *Data::Dumper::qquote = sub { qq["${\(shift)}"] };
  0            
13             $Data::Dumper::Useperl = 1;
14             }
15 1     1   1154 use open IO => 'utf8', ':std';
  1         1491  
  1         6  
16 1     1   738 use Pod::Usage;
  1         61455  
  1         190  
17 1     1   660 use Data::Section::Simple qw(get_data_section);
  1         801  
  1         84  
18 1     1   7 use List::Util qw(max);
  1         3  
  1         75  
19 1     1   759 use List::MoreUtils qw(pairwise);
  1         19418  
  1         9  
20 1     1   2167 use App::diff2vba::Util qw(split_string);
  1         3  
  1         86  
21 1     1   630 use App::sdif::Util qw(read_unified_2);
  1         8454  
  1         103  
22              
23 1     1   1771 use Getopt::EX::Hashed 1.03; {
  1         9857  
  1         7  
24              
25             Getopt::EX::Hashed->configure( DEFAULT => [ is => 'ro' ] );
26              
27             has debug => " " ;
28             has verbose => " v ! " , default => 1;
29             has format => " =s " , default => 'dumb';
30             has subname => " =s " , default => 'Patch';
31             has maxlen => " =i " , default => 250;
32             has adjust => " =i " , default => 2;
33             has identical => " ! " ;
34             has reverse => " ! " ;
35             has help => " " ;
36             has version => " " ;
37              
38             has '+help' => sub {
39             pod2usage
40             -verbose => 99,
41             -sections => [ qw(SYNOPSIS VERSION) ];
42             };
43              
44             has '+version' => sub {
45             print "Version: $VERSION\n";
46             exit;
47             };
48              
49             Getopt::EX::Hashed->configure( DEFAULT => [ is => 'rw' ] );
50              
51             has SCRIPT => ;
52             has TABLE => ;
53             has QUOTES => default => { '“' => "Chr(&H8167)", '”' => "Chr(&H8168)" };
54             has QUOTES_RE => ;
55              
56 1     1   389 } no Getopt::EX::Hashed;
  1         2  
  1         5  
57              
58             sub run {
59 0     0 0   my $app = shift;
60 0 0         local @ARGV = map { utf8::is_utf8($_) ? $_ : decode('utf8', $_) } @_;
  0            
61              
62 1     1   859 use Getopt::EX::Long qw(GetOptions Configure ExConfigure);
  1         128131  
  1         1264  
63 0           ExConfigure BASECLASS => [ __PACKAGE__, "Getopt::EX" ];
64 0           Configure qw(bundling no_getopt_compat);
65 0 0         $app->getopt || pod2usage();
66              
67 0           $app->initialize;
68              
69 0 0         for my $file (@ARGV ? @ARGV : '-') {
70 0           print $app->reset->load($file)->vba->script;
71             }
72              
73 0           return 0;
74             }
75              
76             sub load {
77 0     0 0   my $app = shift;
78 0           my $file = shift;
79              
80 0 0         open my $fh, $file or die "$file: $!\n";
81              
82 0           $app->TABLE(my $fromto = []);
83 0           while (<$fh>) {
84             #
85             # diff --combined (generic)
86             #
87 0 0         if (m{^
88             (?<command>
89             (?<mark> \@{2,} ) [ ]
90             (?<lines> (?: [-+]\d+(?:,\d+)? [ ] ){2,} )
91             \g{mark}
92             (?s:.*)
93             )
94             }x) {
95 0           my($command, $lines) = @+{qw(command lines)};
96 0           my $column = length $+{mark};
97             my @lines = map {
98 0 0         $_ eq ' ' ? 1 : int $_
  0            
99             } $lines =~ /\d+(?|,(\d+)|( ))/g;
100              
101 0 0         warn $_ if $app->debug;
102              
103 0 0         next if @lines != $column;
104 0 0         next if $column != 2;
105              
106 0           push @$fromto, $app->read_diff($fh, @lines);
107             }
108             }
109 0           $app;
110             }
111              
112             sub vba {
113 0     0 0   (my $app = shift)
114             ->prologue()
115             ->substitute()
116             ->epilogue();
117             }
118              
119             sub prologue {
120 0     0 0   my $app = shift;
121 0 0         if (my $name = $app->subname) {
122 0           $app->append(text => "Sub $name()\n");
123             }
124 0           $app->append(section => "setup.vba");
125 0           $app;
126             }
127              
128             sub epilogue {
129 0     0 0   my $app = shift;
130 0 0         if (my $name = $app->subname) {
131 0           $app->append(text => "End Sub\n");
132             }
133 0           $app;
134             }
135              
136             sub substitute {
137 0     0 0   my $app = shift;
138 0           my $template = sprintf "subst_%s.vba", $app->format;
139 0           my $max = $app->maxlen;
140              
141 0           my @fromto = do {
142 0 0         if ($app->reverse) {
143 0           map { [ $_->[1], $_->[0] ] } @{$app->TABLE};
  0            
  0            
144             } else {
145 0           @{$app->TABLE};
  0            
146             }
147             };
148 0           while (my($i, $fromto) = each @fromto) {
149 0           my $fromto = $fromto[$i];
150 1     1   12 use integer;
  1         3  
  1         10  
151 0           chomp @$fromto;
152 0           my($from, $to) = @$fromto;
153 0           my $longer = max map { length } $from, $to;
  0            
154 0           my $count = ($longer + $max - 1) / $max;
155 0           my @from = split_string($from, $count);
156 0           my @to = split_string($to, $count);
157 0 0         adjust_border(\@from, \@to, $app->adjust) if $app->adjust;
158 0           for my $j (keys @from) {
159 0 0 0       next if !$app->identical and $from[$j] eq $to[$j];
160 0           $app->append(text => sprintf "' # %d-%d\n", $i + 1, $j + 1);
161 0           $app->append(section => $template,
162             { TARGET => $app->string_literal($from[$j]),
163             REPLACEMENT => $app->string_literal($to[$j]) });
164             }
165             }
166 0           $app;
167             }
168              
169             sub initialize {
170 0     0 0   my $app = shift;
171 0           my $chrs = join '', keys %{$app->QUOTES};
  0            
172 0           $app->QUOTES_RE(qr/[\Q$chrs\E]/);
173 0           $app;
174             }
175              
176             sub append {
177 0     0 0   my $app = shift;
178 0   0       my $what = shift // 'text';
179 0 0         if ($what eq 'text') {
    0          
180 0           push @{$app->SCRIPT}, @_;
  0            
181             } elsif ($what eq 'section') {
182 0           push @{$app->SCRIPT}, $app->section(@_);
  0            
183 0           } else { die }
184 0           $app;
185             }
186              
187             sub script {
188 0     0 0   my $app = shift;
189 0           join "\n", @{$app->SCRIPT};
  0            
190             }
191              
192             sub reset {
193 0     0 1   (my $app = shift)->SCRIPT([]);
194 0           $app;
195             }
196              
197             sub section {
198 0     0 0   my $app = shift;
199 0           my $section = shift;
200 0   0       my $replace = shift // {};
201 0           local $_ = get_data_section($section);
202 0           do { s/\A\n*//; s/\n\K\n*\z// };
  0            
  0            
203 0           for my $name (keys %$replace) {
204 0           s/\b(\Q$name\E)\b/$replace->{$1}/ge;
  0            
205             }
206 0           $_;
207             }
208              
209             sub read_diff {
210 0     0 0   my $app = shift;
211 0           my($fh, @lines) = @_;
212 0           my @diff = read_unified_2 $fh, @lines;
213 0           my @out;
214 0           while (my($c, $o, $n) = splice(@diff, 0, 3)) {
215 0 0 0       @$o > 0 and @$o == @$n or next;
216 0           s/^[\t +-]// for @$c, @$o, @$n;
217 0     0     push @out, pairwise { [ $a, $b ] } @$o, @$n;
  0            
218             }
219 0           @out;
220             }
221              
222             sub string_literal {
223 0     0 0   my $app = shift;
224 0           my $quotes = $app->QUOTES;
225 0           my $chrs_re = $app->QUOTES_RE;
226             join(' & ',
227 0 0         map { $quotes->{$_} || sprintf('"%s"', s/\"/\"\"/gr) }
228 0           map { split /($chrs_re)/ } @_);
  0            
229             }
230              
231             ######################################################################
232              
233             sub adjust_border {
234 0     0 0   my($a, $b, $max) = @_;
235 0   0       $max //= 2;
236 0 0         return if @$a < 1;
237 0 0         return if $max == 0;
238 0           for my $i (1 .. $#{$a}) {
  0            
239 0 0         next if substr($a->[$i-1], -($max+1)) eq substr($b->[$i-1], -($max+1));
240 0           for my $shift (reverse 1 .. $max) {
241 0 0         _adjust($a, $b, $i, $shift) and last;
242 0 0         _adjust($b, $a, $i, $shift) and last;
243             }
244             }
245             }
246              
247             sub _adjust {
248 0     0     my($a, $b, $i, $len) = @_;
249 0 0         if (substr($a->[$i-1], -($len + 1), $len + 1) eq
250             substr($b->[$i-1], -1, 1) . substr($b->[$i], 0, $len)) {
251 0           $b->[$i-1] .= substr($b->[$i], 0, $len, '');
252 0           return 1;
253             }
254 0           return 0;
255             }
256              
257             1;
258              
259             =encoding utf-8
260              
261             =head1 NAME
262              
263             App::diff2vba - generate VBA patch script from diff output
264              
265             =head1 VERSION
266              
267             Version 1.00
268              
269             =head1 SYNOPSIS
270              
271             greple -Mmsdoc -Msubst \
272             --all-sample-dict --diff some.docx | diff2vba > patch.vba
273              
274             =head1 DESCRIPTION
275              
276             B<diff2vba> is a command to generate VBA patch script from diff output.
277              
278             Read document in script file for detail.
279              
280             =head1 AUTHOR
281              
282             Kazumasa Utashiro
283              
284             =head1 LICENSE
285              
286             Copyright 2021 Kazumasa Utashiro.
287              
288             This library is free software; you can redistribute it and/or modify
289             it under the same terms as Perl itself.
290              
291             =cut
292              
293             __DATA__
294              
295             @@ setup.vba
296              
297             With Selection.Find
298             .MatchCase = True
299             .MatchByte = True
300             .IgnoreSpace = False
301             .IgnorePunct = False
302             End With
303              
304             Options.AutoFormatAsYouTypeReplaceQuotes = False
305              
306             @@ subst_dumb.vba
307              
308             With Selection.Find
309             .Text = TARGET
310             .Replacement.Text = REPLACEMENT
311             .Execute Replace:=wdReplaceOne
312             End With
313             Selection.Collapse Direction:=wdCollapseEnd
314              
315             @@ subst_dumb2.vba
316              
317             With Selection.Find
318             .Text = TARGET
319             if .Execute Then
320             Selection.Range.Text = REPLACEMENT
321             End If
322             End With
323