File Coverage

blib/lib/List/Prefixed.pm
Criterion Covered Total %
statement 117 118 99.1
branch 60 66 90.9
condition 14 15 93.3
subroutine 13 13 100.0
pod 4 4 100.0
total 208 216 96.3


line stmt bran cond sub pod time code
1             package List::Prefixed;
2 1     1   37545 use 5.014002;
  1         5  
  1         42  
3 1     1   6 use strict;
  1         2  
  1         43  
4 1     1   6 use warnings;
  1         7  
  1         39  
5 1     1   6 use Carp;
  1         1  
  1         376  
6              
7             # -- globals --
8              
9             our $VERSION = '0.01';
10              
11             # Public global variables.
12             our (
13             $UC_ESCAPE_STYLE, # "PCRE" | "Java" | undef
14             $REGEX_ESCAPE, # CODE: Str -> Str
15             $REGEX_UNESCAPE # CODE: Str -> Str
16             );
17              
18             # The default unicode escape format is the PCRE style \x{FF}.
19             # - To output regular expressions in \uFFFF (Java, etc.) style, set $UC_ESCAPE_STYLE to 'Java'.
20             # - To avoid replacement of non-ASCII characters at all, set $UC_ESCAPE_STYLE to undef.
21             $UC_ESCAPE_STYLE = 'PCRE';
22              
23             # Unicode escape styles
24             my %UC_escape = (
25             PCRE => [ '\\x{%X}' => qr/\\x\{([0-9A-F]{2,6})}/i ],
26             Java => [ '\\u%04X' => qr/\\u([0-9A-F]{4,6})/i ],
27             );
28              
29             # Get the escape form (0) / unescape regex (1) depending on $UC_ESCAPE_STYLE.
30             my $UC_ESCAPE = sub {
31             return undef unless defined $UC_ESCAPE_STYLE;
32             return $UC_escape{$UC_ESCAPE_STYLE}[$_[0]] if exists $UC_escape{$UC_ESCAPE_STYLE};
33             croak "Unsupported UC_ESCAPE_STYLE: '$UC_ESCAPE_STYLE'"; # RTFM
34             };
35              
36             # The default escape procedure.
37             $REGEX_ESCAPE = sub {
38              
39             # quotemeta EXPR
40             # Returns the value of EXPR with all non-"word" characters backslashed.
41             # (That is, all characters not matching "/[A-Za-z_0-9]/" will be preceded by
42             # a backslash in the returned string, regardless of any locale settings.)
43             local $_ = quotemeta $_[0];
44              
45             # additionally apply $UC_ESCAPE_STYLE
46             if ( defined(my $uc_form = &$UC_ESCAPE(0)) ) {
47 1     1   829 s/\\?(\P{ASCII})/ sprintf $uc_form => ord $1 /eg;
  1         10  
  1         18  
48             }
49            
50             $_;
51             };
52              
53             # The default unescape procedure.
54             $REGEX_UNESCAPE = sub {
55             local $_ = $_[0];
56              
57             # apply $UC_ESCAPE_STYLE first
58             if ( defined(my $uc_re = &$UC_ESCAPE(1)) ) {
59             s/$uc_re/ chr hex $1 /eg;
60             }
61              
62             # replace backslash escapes as inserted by quotemeta
63             s/\\([^A-Z0-9_])/$1/gi;
64            
65             $_;
66             };
67              
68             # Globals may passed as module arguments, e.g.:
69             #
70             # use List::Prefixed uc_escape_style => 'Java';
71             #
72             sub import {
73 1     1   14 shift; # __PACKAGE__;
74 1         2 my %args = @_;
75              
76 1 50       5 $UC_ESCAPE_STYLE = $args{uc_escape_style} if exists $args{uc_escape_style};
77 1 50       4 $REGEX_ESCAPE = $args{regex_escape} if exists $args{regex_escape};
78 1 50       17 $REGEX_UNESCAPE = $args{regex_unescape} if exists $args{regex_unescape};
79             }
80              
81             # -- public --
82              
83             # Construct a Prefixed object from a string list
84             sub fold {
85              
86             local *reduce = sub {
87 54     54   61 my ($prefix, $nlist, $opt) = @{$_[0]};
  54         117  
88              
89 54 100 66     357 return $_[0] unless ref $nlist && @$nlist > 1;
90              
91             # 1st char of the prefix of 1st node in list
92 43         53 my ($c);
93              
94             # check whether 2nd prefix starts with same letter as the 1st
95 43 50       111 if ( length $nlist->[0][0] ) {
96 43         106 $c = substr $nlist->[0][0], 0, 1; # first char
97 43 100       147 undef $c unless $c eq substr $nlist->[1][0], 0, 1;
98             }
99              
100 43 100       106 unless ( defined $c )
101             {
102 16 100       85 return $_[0] unless @$nlist > 2;
103              
104             # try to reduce next list part
105 8         18 my $first = shift @$nlist;
106 8         60 my $next = reduce(bless ['', $nlist, 0]);
107 8 100       37 return bless [ $prefix, [ $first, $next ], $opt] if length $next->[0];
108              
109             # couldn't be reduced
110 5         10 return bless [ $prefix, [ $first, @{$next->[1]} ], $opt ];
  5         118  
111             }
112              
113             # reduce any ensuing node whose prefix starts with $c
114 27         29 my @new;
115 27         43 my $newopt = undef;
116 27         58 my $qc = quotemeta $c;
117 27         69 while ( @$nlist )
118             {
119 87 100       674 last unless $nlist->[0][0] =~ s/^$qc//;
120              
121             # reduce node or detect new optional node
122 80         127 my $n = shift @$nlist;
123 80 100       175 if ( length $n->[0] )
124             {
125 65         82 push @new, $n;
126 65         148 next;
127             }
128 15         44 $newopt = 1;
129             }
130              
131 27 100 100     124 if ( @$nlist || $opt )
132             {
133 9         113 my $new = reduce(bless [ $c, [ @new ], $newopt ]);
134 9 100       38 if ( @$nlist )
135             {
136             # reduce remaining nlist
137 7         29 my $next = reduce(bless ['', $nlist, 0]);
138 7 100       86 return bless [ $prefix, [ $new, $next ], $opt] if length $next->[0];
139              
140             # couldn't be reduced
141 4         6 return bless [ $prefix, [ $new, @{$next->[1]} ], $opt ];
  4         88  
142             }
143              
144             # current node is optional
145 2         16 return bless [ $prefix, [ $new, @$nlist ], $opt ];
146             }
147              
148             # nothing left to reduce
149 18         146 reduce(bless [ $prefix.$c, [ @new ], $newopt ]);
150 14     14 1 23844 };
151              
152 14         32 shift; # __PACKAGE__
153 14         25 my @s = sort keys %{ { map{ $_ => 1 } @_ } }; # unique sorted
  14         36  
  52         204  
154 14 100       114 return bless [$_[0],[],undef] if @s == 1; # singleton
155 12         33 reduce(bless [ '', [( map { [$_] } @s )],undef]);
  49         149  
156             };
157              
158             my $RE_PREFIX = qr/(?:\\(?:.|\n)|[^\|\(\)])+/;
159              
160             # Construct a Prefixed object from a regular expression.
161             #
162             # Solution based on basic regular expression evaluation.
163             sub unfold {
164 7     7 1 305247 shift; # __PACKAGE__
165 7         25 my ($regex) = @_;
166 7         18 my ($nn,$cn,@st);
167 7         35 while ( length $regex ) {
168              
169             # prefix string
170 87 100       756 if ( $regex =~ s/^\|?($RE_PREFIX)// ) {
171 43         96 my $p = &$REGEX_UNESCAPE($1);
172 43 100       91 if ( $cn ) {
173 40 100       135 $cn->[1] = [] unless ref $cn->[1];
174 40         45 push @{$cn->[1]}, ($nn = bless [$p]);
  40         139  
175             }
176             else {
177 3         10 $cn = $nn = bless [$p];
178             }
179 43         163 next;
180             }
181              
182             # node start
183 44 100       189 if ( $regex =~ s/^\(\?:// ) {
184 22 100       49 if ( $nn ) {
185 18         37 $cn = $nn;
186             }
187             else {
188 4         17 $cn = $nn = bless [''];
189             }
190 22         34 push @st, $nn;
191 22         58 next;
192             }
193              
194             # node end
195 22 50       142 if ( $regex =~ s/^\)(\?)?// ) {
196 22         38 $cn = pop @st;
197 22 100       75 $cn->[2] = defined $1 ? 1 : undef;
198 22 100       61 $cn = $st[$#st] if @st;
199 22         65 next;
200             }
201              
202 0         0 die "invalid: '$regex'";
203             }
204 7         27 $cn;
205             };
206              
207              
208             sub regex {
209              
210             local *to_regex_rec = sub {
211 63     63   71 my ($prefix, $nlist, $opt) = @{$_[0]};
  63         127  
212 63         126 my $rv = &$REGEX_ESCAPE($prefix);
213 63 100 100     238 if ( $nlist && @$nlist )
214             {
215 26         52 $rv .= '(?:'.(join '|', map { to_regex_rec($_) } @$nlist).')';
  49         126  
216 26 100       79 $rv .= '?' if $opt;
217             }
218 63         301 $rv;
219 14     14 1 1580 };
220              
221 14         27 my $node = shift;
222 14         42 to_regex_rec($node);
223             }
224              
225             sub list {
226 59     59 1 30258 my $node = shift;
227 59         133 my ($s) = @_;
228 59 100       141 $s = '' unless defined $s;
229 59         104 my $qs = quotemeta $s;
230              
231             local *list_rec = sub {
232 241     241   331 my ($p,$n,$list) = @_;
233 241         360 my ($prefix, $nlist, $opt) = @$n;
234 241         342 my $p2 = $p.$prefix;
235 241         269 my $qp2 = quotemeta $p2;
236 241         204 my ($push,$continue);
237 241 100       2215 if ( $p2 =~ m/^$qs/ ) {
    100          
238             # current prefix starts with search string
239 137         188 $push = $continue = 1;
240             }
241             elsif ( $s =~ m/^$qp2/ ) {
242             # search string starts with current prefix
243 37         65 $continue = 1;
244             }
245              
246 241 100 100     954 if ($nlist && @$nlist) {
    100          
247 107 100 100     354 push @$list, $p2 if $push && $opt;
248 107 100       245 if ( $continue ) {
249 87         396 list_rec($p2,$_,$list) foreach @$nlist;
250             }
251             }
252             elsif ( $push ) {
253 87         366 push @$list, $p2;
254             }
255 59         387 };
256              
257 59         71 my @list;
258 59         146 list_rec('',$node,\@list);
259 59 50       915 wantarray ? @list : \@list;
260             }
261              
262              
263             # Standard new constructor is an alias for fold.
264             *new = \&fold;
265              
266             1;
267             __END__