File Coverage

blib/lib/App/Greple/annotate.pm
Criterion Covered Total %
statement 41 169 24.2
branch 0 52 0.0
condition 0 16 0.0
subroutine 14 38 36.8
pod 0 8 0.0
total 55 283 19.4


line stmt bran cond sub pod time code
1             package App::Greple::annotate;
2              
3 1     1   14 use 5.024;
  1         3  
4 1     1   4 use warnings;
  1         1  
  1         44  
5 1     1   4 use utf8;
  1         1  
  1         5  
6              
7             our $VERSION = "0.9909";
8              
9             =encoding utf-8
10              
11             =head1 NAME
12              
13             App::Greple::annotate - greple module for generic annotation
14              
15             =head1 SYNOPSIS
16              
17             B B<-Mannotate> [ I ] -- [ I ] ...
18              
19             =head1 VERSION
20              
21             Version 0.9909
22              
23             =head1 DESCRIPTION
24              
25             C module is made for C
26             to display annotation for each matched text in the following style.
27              
28             $ greple -Mcharcode '\P{ASCII}' charcode.pm
29              
30             ┌─── 12 \x{fe0e} \N{VARIATION SELECTOR-15}
31             │ ┌─ 14 \x{a9} \N{COPYRIGHT SIGN}
32             │ ├─ 14 \x{fe0e} \N{VARIATION SELECTOR-15}
33             Copyright︎ ©︎ 2025 Kazumasa Utashiro.
34              
35             =for html

36            
37            

38              
39             =head1 COMMAND OPTIONS
40              
41             =over 7
42              
43             =item B<--annotate>, B<--no-annotate>
44              
45             Print annotation or not. Enabled by default, so use C<--no-annotate>
46             to disable it.
47              
48             =item B<--annotate::config>=I
49              
50             Set configuration paarameters.
51              
52             =item B<-->[B]B
53              
54             Align annotation or not.
55             Default true.
56              
57             =item B<--align-all>
58              
59             Align to the same column for all lines
60              
61             =item B<--align-side>
62              
63             Align to the longest line length, regardless of match position.
64              
65             =item B<--alignto>=I
66              
67             Align to I position.
68              
69             =back
70              
71             =head1 MODULE OPTIONS and PARAMS
72              
73             =over 7
74              
75             =item B<--config>=I
76              
77             Set configuration parameters.
78              
79             =item B<--alignto>=I
80              
81             =item B(B=I)
82              
83             Align annotation messages. Defaults to C<1>, which aligns to the
84             rightmost column; C<0> means no align; if a value of C<2> or greater
85             is given, it aligns to that numbered column.
86              
87             I can be negative; if C<-1> is specified, align to the same
88             column for all lines. If C<-2> is specified, align to the longest
89             line length, regardless of match position.
90              
91             =item B<--split>[=I<#>]
92              
93             =item config(B=[I<#>])
94              
95             Defaults to C<0>. Use C<--split> or C<--split=1> to enable it. If a
96             pattern matching multiple characters is given, annotate each character
97             individually.
98              
99             =back
100              
101             =head1 VARIABLES
102              
103             =over 7
104              
105             =item B<$App::Greple::annotate::ANNOTATE>
106              
107             Hold function reference to produce annotation text. Default function
108             is declared as this:
109              
110             our $ANNOTATE //= sub {
111             my %param = @_;
112             my($column, $str) = @param{qw(column match)};
113             sprintf("%3d %s", $column, $str);
114             };
115              
116             Parameter is passed by C and C labeled list.
117              
118             =back
119              
120             =head1 CONFIGURATION
121              
122             Configuration parameters can be set in several ways.
123              
124             =head2 MODULE START FUNCTION
125              
126             The start function of a module can be specified at the same time as
127             the module declaration.
128              
129             greple -Mannotate::config(alignto=0)
130              
131             greple -Mannotate::config=alignto=80
132              
133             =head2 PRIVATE MODULE OPTION
134              
135             Module-specific options are specified between C<-Mannotate> and C<-->.
136              
137             greple -Mannotate --config alignto=80 -- ...
138              
139             greple -Mannotate --alignto=80 -- ...
140              
141             =head2 GENERIC MODULE OPTION
142              
143             Module-specific C<---config> option can be called by normal command
144             line option C<--annotate::config>.
145              
146             greple -Mannotate --annotate::config alignto=80 ...
147              
148             =head1 INSTALL
149              
150             cpanm -n B
151              
152             =head1 SEE ALSO
153              
154             L
155              
156             L
157              
158             =head1 LICENSE
159              
160             Copyright︎ ©︎ 2025 Kazumasa Utashiro.
161              
162             This library is free software; you can redistribute it and/or modify
163             it under the same terms as Perl itself.
164              
165             =head1 AUTHOR
166              
167             Kazumasa Utashiro
168              
169             =cut
170              
171 1     1   80 use Getopt::EX::Config;
  1         2  
  1         7  
172 1     1   39 use Hash::Util qw(lock_keys);
  1         2  
  1         10  
173 1     1   46 use List::Util qw(max);
  1         1  
  1         39  
174 1     1   3 use Data::Dumper;
  1         1  
  1         380  
175              
176             our $config = Getopt::EX::Config->new(
177             annotate => \(our $opt_annotate = 1),
178             alignto => 1,
179             split => 0,
180             );
181             lock_keys %{$config};
182             my %type = ( '*' => ':1' );
183 0   0 0 0   sub optspec { $_[0] . ( $type{$_[0]} // $type{'*'} // '' ) }
      0        
184              
185             sub finalize {
186 0     0 0   our($mod, $argv) = @_;
187             $config->deal_with($argv,
188 0           map(optspec($_), keys %{$config}));
  0            
189             }
190              
191 1     1   7 use Text::ANSI::Fold::Util qw(ansi_width);
  1         1  
  1         75  
192             Text::ANSI::Fold->configure(expand => 1);
193             *vwidth = \&ansi_width;
194              
195             package # no_index
196             Local::Annon {
197 1     1   4 use strict;
  1         1  
  1         44  
198 1     1   4 use warnings;
  1         1  
  1         115  
199             sub new {
200 0     0     my $class = shift;
201 0 0         @_ == 3 or die;
202 0           bless [ @_ ], $class;
203             }
204 0     0     sub start { shift->[0] }
205 0     0     sub end { shift->[1] }
206 0     0     sub annon :lvalue { shift->[2] }
207             }
208              
209             package # no_index
210             Local::Annon::List {
211 1     1   5 use strict;
  1         1  
  1         19  
212 1     1   2 use warnings;
  1         2  
  1         30  
213 1     1   3 use List::Util;
  1         2  
  1         1140  
214             sub new {
215 0     0     my $class = shift;
216 0           bless {
217             Annotation => [],
218             Count => [],
219             }, $class;
220             }
221 0     0     sub annotation { $_[0]->{Annotation} }
222 0     0     sub count { $_[0]->{Count} }
223             sub push {
224 0     0     my $obj = CORE::shift;
225 0           push @{$obj->annotation}, @_;
  0            
226 0           push @{$obj->count}, int @_;
  0            
227             }
228             sub append {
229 0     0     my $obj = CORE::shift;
230 0           CORE::push @{$obj->annotation}, @_;
  0            
231 0           $obj->count->[-1] += int @_;
232             }
233             sub shift {
234 0     0     my $obj = CORE::shift;
235 0 0         my $count = CORE::shift @{$obj->count} or return ();
  0            
236 0           splice @{$obj->annotation}, 0, $count;
  0            
237             }
238             sub join {
239 0     0     my $obj = CORE::shift;
240 0           for (@_) {
241 0           CORE::push @{$obj->annotation}, @{$_->annotation};
  0            
  0            
242 0           CORE::push @{$obj->count}, @{$_->count};
  0            
  0            
243             }
244             }
245             sub total {
246 0     0     my $obj = CORE::shift;
247 0   0       List::Util::sum(@{$obj->count}) // 0;
  0            
248             }
249             sub last {
250 0     0     my $obj = CORE::shift;
251 0           $obj->annotation->[-1];
252             }
253             sub maxpos {
254 0     0     my $obj = CORE::shift;
255 0           List::Util::max map { $_->end - 1 } @{$obj->annotation};
  0            
  0            
256             }
257             }
258              
259             sub code {
260 0     0 0   state $format = [ qw(\x{%02x} \x{%04x}) ];
261 0           my $ord = ord($_[0]);
262 0           sprintf($format->[$ord > 0xff], $ord);
263             }
264              
265             my %cmap = (
266             "\t" => '\t',
267             "\n" => '\n',
268             "\r" => '\r',
269             "\f" => '\f',
270             "\b" => '\b',
271             "\a" => '\a',
272             "\e" => '\e',
273             );
274              
275             sub control {
276 0 0   0 0   local $_ = @_ ? shift : $_;
277 0 0         if (s/\A([\t\n\r\f\b\a\e])/$cmap{$1}/e) {
  0 0          
278 0           $_;
279 0           } elsif (s/\A([\x00-\x1f])/sprintf "\\c%c", ord($1)+0x40/e) {
280 0           $_;
281             } else {
282 0           code($_);
283             }
284             }
285              
286             sub visible {
287 0 0   0 0   local $_ = @_ ? shift : $_;
288 0           s{([^\pL\pN\pP\pS])}{control($1)}ger;
  0            
289             }
290              
291             our $ANNOTATE //= sub {
292             my %param = @_;
293             my($column, $str) = @param{qw(column match)};
294             sprintf("%3d %s", $column, visible($str));
295             };
296              
297             my $annotation;
298              
299             sub prepare {
300 0 0   0 0   config('annotate') or return;
301 0           state $target;
302 0 0 0       if (defined $target and \$_ == $target) {
303 0           return;
304             } else {
305 0           $target = \$_;
306             }
307 0           $annotation = Local::Annon::List->new;
308 0           goto &_prepare;
309             }
310             sub _prepare {
311 0     0     my $grep = shift;
312 0 0         my @result = $grep->result or return;
313 0           for my $r (@result) {
314 0           my($b, @match) = @$r;
315 0           my @slice = $grep->slice_result($r);
316 0           my $start = 0;
317 0           my $progress = '';
318 0           my $indent = '';
319 0           my $current = Local::Annon::List->new;
320 0           while (my($i, $slice) = each @slice) {
321 0 0         if ($slice eq '') {
322 0 0         $current->push() if $i % 2;
323 0           next;
324             }
325 0           my $end = vwidth($progress . $slice);
326 0           my $gap = $end - $start;
327 0           my $indent_mark = '';
328 0 0         if ($i % 2) {
329 0           my $match = $match[$i / 2];
330 0           $indent_mark = '│';
331 0           my $head = '┌';
332 0 0         if ($gap == 0) {
333 0 0 0       if ($start == 0) {
    0          
    0          
334 0           $head = '╾';
335 0           $indent_mark = '';
336             } elsif ($current->total > 0 and $current->last->end == $start) {
337 0           $head = '├';
338 0           $start = $current->last->start;
339 0           substr($indent, $start) = '';
340             } elsif ($start > 0) {
341 0           $start = vwidth($progress =~ s/\X\z//r);
342 0           substr($indent, $start) = '';
343             }
344             }
345             my $marker = sub {
346 0     0     my($head, $match) = @_;
347 1     1   6 sprintf("%s%s%s\N{NBSP}%s",
  1         1  
  1         6  
  0            
348             $indent, $head, '─',
349             $ANNOTATE->(column => $start, match => $match));
350 0           };
351 0           $current->push( do {
352 0 0         if ($config->{split}) {
353             map {
354 0           my $out = $marker->($head, $_);
  0            
355 0           $head = '├';
356 0           Local::Annon->new($start, $end, $out);
357             } $slice =~ /./sg;
358             } else {
359 0           Local::Annon->new($start, $end, $marker->($head, $slice));
360             }
361             } );
362             }
363 0           $indent .= sprintf("%-*s", $end - $start, $indent_mark);
364 0           $progress .= $slice;
365 0           $start = $end;
366             }
367 0 0         @{$current->count} == 0 and next;
  0            
368 0           my $alignto = $config->{alignto};
369 0 0 0       if ($alignto > 0 and $current->total > 0) {
370 0 0         alignto($current, $alignto > 1 ? $alignto : $current->last->[0]);
371             }
372 0           $annotation->join($current);
373             }
374 0 0         if ($config->{alignto} == -1) {
    0          
375 0           alignto($annotation, $annotation->maxpos);
376             }
377             elsif ($config->{alignto} == -2) {
378 0 0         if (my $maxlen = max map { vwidth($grep->cut($_->[0]->@*)) } @result) {
  0            
379 0           alignto($annotation, $maxlen - 1);
380             }
381             }
382             }
383              
384             sub alignto {
385 0     0 0   my($list, $pos) = @_;
386 0           for (@{$list->annotation}) {
  0            
387 0 0         if ((my $extend = $pos - $_->[0]) > 0) {
388 0           $_->annon =~ s/(?=([─]))/$1 x $extend/e;
  0            
389             }
390             }
391             }
392              
393             sub annotate {
394 0 0   0 0   config('annotate') or return;
395 0 0         if (my @annon = $annotation->shift) {
396 0           say $_->annon for @annon;
397             }
398 0           undef;
399             }
400              
401             1;
402              
403             __DATA__