File Coverage

blib/lib/Circle/GlobalRules.pm
Criterion Covered Total %
statement 40 220 18.1
branch 0 112 0.0
condition 0 3 0.0
subroutine 12 31 38.7
pod 0 20 0.0
total 52 386 13.4


line stmt bran cond sub pod time code
1             # You may distribute under the terms of the GNU General Public License
2             #
3             # (C) Paul Evans, 2008-2017 -- leonerd@leonerd.org.uk
4              
5             package Circle::GlobalRules;
6              
7 4     4   21 use strict;
  4         6  
  4         99  
8 4     4   16 use warnings;
  4         8  
  4         156  
9              
10             our $VERSION = '0.173320';
11              
12 4     4   18 use Text::Balanced qw( extract_delimited extract_quotelike );
  4         6  
  4         312  
13              
14 4     4   20 use base qw( Circle::Rule::Store ); # for the attributes
  4         6  
  4         623  
15              
16 4     4   24 use Circle::TaggedString;
  4         7  
  4         1515  
17              
18             sub unquote_qr
19             {
20 0     0 0 0 my $re = shift;
21              
22 0         0 $re = "$re";
23              
24             # Perl tries to put (?-xism:RE) around our pattern. Lets attempt to remove
25             # it if we can
26             # Recent perls use (?^:RE) instead
27 0         0 $re =~ s/^\(\?-xism:(.*)\)$/$1/;
28 0         0 $re =~ s/^\(\?\^:(.*)\)$/$1/;
29              
30 0 0       0 return ( $2, $1 ) if $re =~ m/^\(\?([ixsm]*)-?[xism]*:(.*)\)$/;
31 0 0       0 return ( $2, $1 ) if $re =~ m/^\(\?\^([ixsm]*):(.*)\)$/;
32              
33             # Failed. Lets just be safe then
34 0         0 return ( $re, "" );
35             }
36              
37             # Not an object class. Instead, just a store of rule subs
38              
39             sub register
40             {
41 3     3 0 9 my ( $rulestore ) = @_;
42              
43 3         11 $rulestore->register_cond( matches => __PACKAGE__ );
44              
45 3         16 $rulestore->register_action( rewrite => __PACKAGE__ );
46 3         16 $rulestore->register_action( format => __PACKAGE__ );
47 3         11 $rulestore->register_action( unformat => __PACKAGE__ );
48 3         11 $rulestore->register_action( level => __PACKAGE__ );
49 3         10 $rulestore->register_action( highlight => __PACKAGE__ );
50             }
51              
52             ###### CONDITIONS
53              
54             ### MATCHES
55              
56             sub parse_cond_matches
57             : Rule_description("Look for regexp or substring matches in the text")
58             : Rule_format('/regexp/ or "literal"')
59             {
60 0     0 0 0 shift; # class
61 0         0 my ( $spec ) = @_;
62              
63 0 0       0 if( $spec =~ m{^/} ) {
    0          
64             # Try to pull the flags
65 0 0       0 my ( $content, $flags ) = $spec =~ m{^/(.*)/([i]*)$} or die "Unrecognised regexp string $spec\n";
66              
67 0 0       0 return qr/$content/i if $flags eq "i";
68 0         0 return qr/$content/;
69             }
70             elsif( $spec =~ m{^"} ) {
71 0 0       0 my ( $content ) = $spec =~ m{^"(.*)"$} or die "Unrecognised literal string $spec\n";
72              
73 0         0 return qr/\Q$content/;
74             }
75             else {
76 0         0 die "Unrecognised string type $spec";
77             }
78 4     4   38 }
  4         7  
  4         24  
79              
80             sub deparse_cond_matches
81             {
82 0     0 0   shift; # class
83 0           my ( $re ) = @_;
84              
85 0           my ( $pattern, $flags ) = unquote_qr( $re );
86 0           return "/$pattern/$flags";
87             }
88              
89             sub eval_cond_matches
90             {
91 0     0 0   shift; # class
92 0           my ( $event, $results, $re ) = @_;
93              
94 0 0         defined( my $text = $event->{text} ) or return 0;
95 0           $text = "$text"; # stringify a String::Tagged
96              
97 0           pos( $text ) = 0;
98              
99 0           my $matched;
100              
101 0           while( $text =~ m/$re/g ) {
102 0           my @matchgroups;
103 0           for ( 0 .. $#+ ) {
104 0           my ( $start, $end ) = ( $-[$_], $+[$_] );
105 0           my $len = $end - $start;
106              
107 0           push @matchgroups, [ $start, $len ];
108             }
109              
110 0           $results->push_result( "matchgroups", \@matchgroups );
111 0           $matched = 1;
112             }
113              
114 0           return $matched;
115             }
116              
117             ###### ACTIONS
118              
119             ### REWRITE
120              
121             sub parse_action_rewrite
122             : Rule_description("Rewrite text of the line or matched parts")
123             : Rule_format('line|matches|match(number) "string"|s/pattern/replacement/')
124             {
125 0     0 0 0 shift; # class
126 0         0 my ( $spec ) = @_;
127              
128 0 0       0 $spec =~ s/^(\w+)\s*// or die "Expected type as first argument\n";
129 0         0 my $type = $1;
130              
131 0         0 my $groupnum;
132              
133 0 0       0 if( $type eq "line" ) {
    0          
    0          
134 0         0 $groupnum = -1;
135             }
136             elsif( $type eq "matches" ) {
137 0         0 $groupnum = 0;
138             }
139             elsif( $type eq "match" ) {
140 0 0       0 $spec =~ s/^\((\d+)\)\s*// or die "Expected match group number\n";
141 0         0 $groupnum = $1;
142             }
143             else {
144 0         0 die "Unrecognised format type $type\n";
145             }
146              
147 0 0       0 my ( undef, $remains, undef, $op, $delim, $lhs, undef, undef, $rhs, undef, $mods ) = extract_quotelike( $spec )
148             or die 'Expected "string" or s/pattern/replacement/';
149 0         0 $spec = $remains;
150 0 0       0 $op = $delim if $op eq "";
151              
152 0 0       0 if( $op eq '"' ) {
    0          
153             # Literal
154 0         0 return ( $groupnum, literal => $lhs );
155             }
156             elsif( $op eq "s" ) {
157             # s/foo/bar/
158 0         0 my $global = $mods =~ s/g//;
159             # TODO: Range check that mods contains only /ism
160 0         0 return ( $groupnum, subst => qr/(?$mods:$lhs)/, $rhs, $global );
161             }
162             else {
163 0         0 die 'Expected "string" or s/pattern/replacement/';
164             }
165 4     4   2332 }
  4         16  
  4         16  
166              
167             sub deparse_action_rewrite
168             {
169 0     0 0   shift; # class
170 0           my ( $groupnum, $kind, $lhs, $rhs, $global ) = @_;
171              
172 0 0         my $type = $groupnum == -1 ? "line" :
    0          
173             $groupnum == 0 ? "matches" :
174             "match($groupnum)";
175              
176 0 0         if( $kind eq "literal" ) {
    0          
177 0           return "$type \"$lhs\"";
178             }
179             elsif( $kind eq "subst" ) {
180 0           my ( $pattern, $flags ) = unquote_qr( $lhs );
181 0 0         return "$type s/$pattern/$rhs/$flags" . ( $global ? "g" : "" );
182             }
183             }
184              
185             sub eval_action_rewrite
186             {
187 0     0 0   shift; # class
188 0           my ( $event, $results, $groupnum, $kind, $lhs, $rhs, $global ) = @_;
189              
190 0           my @location;
191 0 0         if( $groupnum == -1 ) {
192 0           @location = ( 0, -1 );
193             }
194             else {
195 0           foreach my $groups ( @{ $results->get_result( "matchgroups" ) } ) {
  0            
196 0 0         my $group = $groups->[$groupnum] or next;
197 0           @location = @$group;
198 0           last; # can only do the first one
199             }
200             }
201              
202 0 0         ref $event->{text} or $event->{text} = Circle::TaggedString->new( $event->{text} );
203 0           my $text = $event->{text}->substr( $location[0], $location[1] );
204              
205 0 0         if( $kind eq "literal" ) {
    0          
206 0           $text = $lhs;
207             }
208             elsif( $kind eq "subst" ) {
209 0 0         $text =~ s/$lhs/$rhs/ if !$global;
210 0 0         $text =~ s/$lhs/$rhs/g if $global;
211             }
212              
213 0           $event->{text}->set_substr( $location[0], $location[1], $text );
214             }
215              
216             ### FORMAT
217              
218             sub parse_action_format
219             : Rule_description("Apply formatting to the line or matched parts")
220             : Rule_format('line|matches|match(number) key="value" [key="value" ...]')
221             {
222 0     0 0 0 shift; # class
223 0         0 my ( $spec ) = @_;
224              
225 0 0       0 $spec =~ s/^(\w+)\s*// or die "Expected type as first argument\n";
226 0         0 my $type = $1;
227              
228 0         0 my $groupnum;
229              
230 0 0       0 if( $type eq "line" ) {
    0          
    0          
231 0         0 $groupnum = -1;
232             }
233             elsif( $type eq "matches" ) {
234 0         0 $groupnum = 0;
235             }
236             elsif( $type eq "match" ) {
237 0 0       0 $spec =~ s/^\((\d+)\)\s*// or die "Expected match group number\n";
238 0         0 $groupnum = $1;
239             }
240             else {
241 0         0 die "Unrecognised format type $type\n";
242             }
243              
244 0         0 my %format;
245 0         0 while( $spec =~ s/^(\w+)=// ) {
246 0         0 my $name = $1;
247              
248 0         0 my $value = extract_delimited( $spec, q{"'} );
249 0         0 s/^["']//, s/["']$// for $value;
250              
251 0         0 $format{$name} = $value;
252              
253 0         0 $spec =~ s/^\s+//;
254             }
255              
256 0 0       0 if( length $spec ) {
257 0         0 die "Unrecognised format spec $spec\n";
258             }
259              
260 0         0 return ( $groupnum, \%format );
261 4     4   2902 }
  4         26  
  4         13  
262              
263             sub deparse_action_format
264             {
265 0     0 0   shift; # class
266 0           my ( $groupnum, $formathash ) = @_;
267              
268 0 0         return unless %$formathash;
269              
270 0 0         my $type = $groupnum == -1 ? "line" :
    0          
271             $groupnum == 0 ? "matches" :
272             "match($groupnum)";
273              
274 0           return "$type ".join( " ", map { qq($_="$formathash->{$_}") } sort keys %$formathash );
  0            
275             }
276              
277             sub eval_action_format
278             {
279 0     0 0   shift; # class
280 0           my ( $event, $results, $groupnum, $formathash ) = @_;
281              
282 0           my $str = $event->{text};
283 0 0         ref $str or $str = Circle::TaggedString->new( $str );
284              
285 0 0         if( $groupnum == -1 ) {
286 0           $str->apply_tag( 0, -1, $_, $formathash->{$_} ) for keys %$formathash;
287             }
288             else {
289 0           foreach my $groups ( @{ $results->get_result( "matchgroups" ) } ) {
  0            
290 0 0         my $group = $groups->[$groupnum] or next;
291 0           my ( $start, $len ) = @$group;
292              
293 0           $str->apply_tag( $start, $len, $_, $formathash->{$_} ) for keys %$formathash;
294             }
295             }
296             }
297              
298             ### UNFORMAT
299              
300             sub parse_action_unformat
301             : Rule_description("Remove formatting from the line or matched parts")
302             : Rule_format('line|matches|match(number) key [key ...]')
303             {
304 0     0 0 0 shift; # class
305 0         0 my ( $spec ) = @_;
306              
307 0 0       0 $spec =~ s/^(\w+)\s*// or die "Expected type as first argument\n";
308 0         0 my $type = $1;
309              
310 0         0 my $groupnum;
311              
312 0 0       0 if( $type eq "line" ) {
    0          
    0          
313 0         0 $groupnum = -1;
314             }
315             elsif( $type eq "matches" ) {
316 0         0 $groupnum = 0;
317             }
318             elsif( $type eq "match" ) {
319 0 0       0 $spec =~ s/^\((\d+)\)\s*// or die "Expected match group number\n";
320 0         0 $groupnum = $1;
321             }
322             else {
323 0         0 die "Unrecognised format type $type\n";
324             }
325              
326 0         0 my @tags;
327 0         0 while( $spec =~ s/^(\w+)// ) {
328 0         0 my $name = $1;
329              
330 0         0 push @tags, $name;
331              
332 0         0 $spec =~ s/^\s+//;
333             }
334              
335 0 0       0 if( length $spec ) {
336 0         0 die "Unrecognised format spec $spec\n";
337             }
338              
339 0         0 return ( $groupnum, \@tags );
340 4     4   2265 }
  4         8  
  4         14  
341              
342             sub deparse_action_unformat
343             {
344 0     0 0   shift; # class
345 0           my ( $groupnum, $taglist ) = @_;
346              
347 0 0         my $type = $groupnum == -1 ? "line" :
    0          
348             $groupnum == 0 ? "matches" :
349             "match($groupnum)";
350              
351 0           my $ret = $type;
352 0           $ret .= " $_" for @$taglist;
353              
354 0           return $ret;
355             }
356              
357             my @alltags = qw( fg bg b u i );
358              
359             sub eval_action_unformat
360             {
361 0     0 0   shift; # class
362 0           my ( $event, $results, $groupnum, $taglist ) = @_;
363              
364 0 0         $taglist = \@alltags unless @$taglist;
365              
366 0           my $str = $event->{text};
367 0 0         ref $str or $str = Circle::TaggedString->new( $str );
368              
369 0 0         if( $groupnum == -1 ) {
370 0           $str->unapply_tag( 0, -1, $_ ) for @$taglist;
371             }
372             else {
373 0           foreach my $groups ( @{ $results->get_result( "matchgroups" ) } ) {
  0            
374 0 0         my $group = $groups->[$groupnum] or next;
375 0           my ( $start, $len ) = @$group;
376              
377 0           $str->unapply_tag( $start, $len, $_ ) for @$taglist;
378             }
379             }
380             }
381              
382             ### LEVEL
383              
384             sub parse_action_level
385             : Rule_description("Set the activity level for the targetted item")
386             : Rule_format('$level')
387             {
388 0     0 0 0 shift; # class
389 0         0 my ( $spec ) = @_;
390              
391 0 0       0 $spec =~ s/^(\d)// or die "Expected level number as first argument\n";
392 0         0 my $level = $1;
393              
394 0 0 0     0 $level >= 0 and $level <= 3 or die "Expected 'level' between 0 and 3\n";
395              
396 0         0 return ( $level );
397 4     4   1607 }
  4         7  
  4         15  
398              
399             sub deparse_action_level
400             {
401 0     0 0   shift; # class
402 0           my ( $level ) = @_;
403              
404 0           return "$level";
405             }
406              
407             sub eval_action_level
408             {
409 0     0 0   shift; # class
410 0           my ( $event, $results, $level ) = @_;
411              
412 0           $event->{level} = $level;
413             }
414              
415             ## HIGHLIGHT
416              
417             sub parse_action_highlight
418             : Rule_description("Highlight matched regions and set activity level to 3")
419             : Rule_format('')
420             {
421 0     0 0 0 my $self = shift;
422 0         0 return;
423 4     4   824 }
  4         13  
  4         14  
424              
425             sub deparse_action_highlight
426             {
427 0     0 0   my $self = shift;
428 0           return;
429             }
430              
431             sub eval_action_highlight
432             {
433 0     0 0   my $self = shift;
434 0           my ( $event, $results ) = @_;
435              
436 0           my $str = $event->{text};
437 0 0         ref $str or $str = Circle::TaggedString->new( $str );
438              
439 0           foreach my $matchgroup ( @{ $results->get_result( "matchgroups" ) } ) {
  0            
440 0           my ( $start, $len ) = @{$matchgroup->[0]}[0,1];
  0            
441              
442 0           $str->apply_tag( $start, $len, b => 1 );
443 0           $str->apply_tag( $start, $len, fg => "highlight" );
444             }
445              
446 0           $event->{level} = 3;
447             }
448              
449             0x55AA;