File Coverage

blib/lib/Unicode/Regex/Set.pm
Criterion Covered Total %
statement 103 112 91.9
branch 57 66 86.3
condition 35 41 85.3
subroutine 8 8 100.0
pod 1 3 33.3
total 204 230 88.7


line stmt bran cond sub pod time code
1             package Unicode::Regex::Set;
2              
3             require 5.008;
4              
5 2     2   1757 use strict;
  2         3  
  2         61  
6 2     2   9 use warnings;
  2         3  
  2         43  
7 2     2   10 use Carp;
  2         7  
  2         327  
8              
9             require Exporter;
10             our @ISA = qw(Exporter);
11              
12             our @EXPORT_OK = qw(parse maketree tostring);
13             our @EXPORT = ();
14              
15             our $VERSION = '0.04';
16             our $PACKAGE = __PACKAGE__;
17              
18 2     2   11 use constant TRUE => 1;
  2         2  
  2         150  
19 2     2   11 use constant FALSE => '';
  2         2  
  2         2980  
20              
21             my %Meaning = (
22             '[' => 'group beginning',
23             ']' => 'group end',
24             '&' => 'intersection',
25             '|' => 'union',
26             '' => 'union',
27             '-' => 'subtraction',
28             );
29              
30             # Token combination table: e.g '[' followed by '&' is NG.
31             #
32             # 1\2 '[' ']' '&' '|' '-' Lit
33             # '[' OK NG NG NG NG OK
34             # ']' OK OK OK OK OK OK
35             # '&' OK NG NG NG NG OK
36             # '|' OK NG NG NG NG OK
37             # '-' OK NG NG NG NG OK
38             # Lit OK OK OK OK OK OK
39             #
40             # Lit, literal, includes A-Z, \[, \|, \-, '\ ' (escaped space), \n, \r,
41             # \t, \f, \cA, \ooo, \xhh, \x{hhhh}, \p{Prop}, \N{NAME}, [:posix:].
42             # They are retained as they are.
43             # [=oops=] are not considered.
44              
45 103     103 1 32707 sub parse { tostring(maketree(@_)) }
46              
47             # $node = {
48             # parent => $node_or_undef, # undef for root
49             # neg => $boolean, # true if group begins with '[^'
50             # follow => $boolean, # true if requires literal
51             # op => $char, # '&', '-', '|'
52             # childs => $arrayref_of_nodes,
53             # }
54              
55             sub maketree {
56 103     103 0 102 my $cur;
57 103         156 my $arg = shift;
58              
59 103 100       268 foreach (ref $arg ? $$arg : $arg) # store in $_
60             {
61 103 50       467 if (!s/^\[//) {
62 0         0 croak "a character class not beginning at [";
63             }
64 103         336 $cur = { parent => undef, op => FALSE, childs => [] };
65 103 100       302 s/^\^// and $cur->{neg} = TRUE;
66              
67 103         125 while (1) {
68              
69             # skip whitespaces
70 721 100       2218 if (s/^\s+//) {
71 231         425 next;
72             }
73              
74             # beginning of a group
75 490 100       916 if (s/^\[ (?! \: [^\[\]]+ \:\] )//x) {
76 34 100 100     96 if ($cur->{op} eq '&' && !$cur->{follow}) {
77 1         3 $cur = $cur->{parent};
78             }
79              
80 34         35 push @{ $cur->{childs} },
  34         153  
81             +{ parent => $cur, op => FALSE, childs => [] };
82              
83 34         57 $cur = $cur->{childs}->[-1];
84 34 100       77 s/^\^// and $cur->{neg} = TRUE;
85 34         37 next;
86             }
87              
88             # end of a group
89 456 100       910 if (s/^\]//) {
90 137 50 33     316 if (! $cur->{childs} || ! @{ $cur->{childs} }) {
  137         466  
91 0         0 croak "empty (sub)group in a character class";
92             }
93              
94 137 100 66     332 if ($cur->{op} eq '&' && !$cur->{follow}) {
95 15         18 $cur = $cur->{parent};
96             }
97              
98             # LAST:
99 137 100       376 last if ! $cur->{parent};
100              
101 34 50       62 if ($cur->{follow}) {
102 0         0 my $op = $cur->{op};
103 0         0 croak "no operand after '$op' ($Meaning{$op})";
104             }
105              
106 34         45 $cur = $cur->{parent};
107              
108 34 100       70 $cur->{follow} and $cur->{follow} = FALSE;
109 34         37 next;
110             }
111              
112             # operators
113 319 100       951 if (s/^([\&\|\-])(?=[\s\[\]])//) {
114 77         136 my $o = $1;
115              
116 77 50 33     186 if (! $cur->{childs} || ! @{ $cur->{childs} }) {
  77         160  
117 0         0 croak "no operand before '$o' ($Meaning{$o})";
118             }
119              
120 77 50       145 if ($cur->{follow}) {
121 0         0 my $p = $cur->{op};
122 0         0 croak "no operand between '$p' ($Meaning{$p}) "
123             . "and '$o' ($Meaning{$o})";
124             }
125              
126 77 100       132 if ($cur->{op} eq $o)
127             {
128 10         27 $cur->{follow} = TRUE;
129 10         15 next;
130             }
131              
132 67 100 66     2432 if ($cur->{op} eq '&' && !$cur->{follow})
133             # in this case $op must not be '&' (see the prev block)
134             # '&' has high precedence: [A & B - C] as [[A & B] - C]
135             {
136 4         7 $cur = $cur->{parent};
137             }
138              
139 67 100       298 if ($o eq '&')
140             # '&' has high precedence: [A B & C D] as [A [B & C] D]
141             {
142 21         24 my $last = pop @{ $cur->{childs} };
  21         35  
143              
144 21         26 push @{ $cur->{childs} },
  21         89  
145             { parent => $cur, op => $o, childs => [ $last ] };
146              
147 21         35 $cur = $cur->{childs}->[-1];
148 21         33 $cur->{follow} = TRUE;
149 21         28 next;
150             }
151              
152 46 100       80 if ($o eq '-') {
153 41 100       47 if (@{ $cur->{childs} } > 1)
  41         85  
154             # '-' has low precedence: [A B - C] as [[A B] - C]
155             {
156 3         7 my @kids = @{ $cur->{childs} };
  3         9  
157 3         12 @{ $cur->{childs} } =
  3         17  
158             { parent => $cur, op => FALSE, childs => \@kids };
159             }
160              
161 41         74 $cur->{op} = $o;
162 41         54 next;
163             }
164              
165 5 50       12 if ($o eq '|') { # simple union
166 5         9 $cur->{op} = $o;
167 5         7 next;
168             }
169             }
170              
171              
172 242 50       1041 if (s/^((?:
173             \\[pPN]\{ [^{}]* \}
174             | \\c?(?s:.)
175             | [^\s\[\]]
176             | \[\: [^\[\]]+ \:\]
177             )+)//x)
178             {
179 242         378 my $lit = $1;
180              
181 242 50       403 if ($lit eq '^') {
182 0         0 croak "A bare '^', that has nothing to be negated.";
183             }
184              
185 242 100 100     736 if ($cur->{op} eq '&' && !$cur->{follow})
186             # '&' has high precedence: [A & B C] as [[A & B] C]
187             {
188 1         2 $cur = $cur->{parent};
189             }
190              
191 242 100       421 $cur->{follow} and $cur->{follow} = FALSE;
192 242         256 my $kid = $cur->{childs};
193              
194 242 100 100     2180 if (@$kid
      100        
      100        
      100        
      100        
      100        
      100        
195             && ! ref($kid->[-1])
196             && $lit !~ /^[\-\^]/
197             && $kid->[-1] !~ /^\[\^/
198             && $kid->[-1] !~ /\-\]\z/
199             && $cur->{op} ne '&'
200             && !($cur->{op} eq '-' && @$kid == 1))
201             # this is only simplification, so avoids uncertain cases
202             {
203 45         82 substr($kid->[-1], -1, 0, $lit);
204             }
205             else {
206 197         417 push @$kid, "[$lit]";
207             }
208 242         326 next;
209             }
210              
211 0         0 croak "panic or incomplete character class (missing last ']'?);";
212             }
213             }
214              
215 103         431 return $cur;
216             }
217              
218             sub tostring {
219 161     161 0 170 my $list = shift;
220              
221 161         140 for (@{ $list->{childs} }) {
  161         294  
222 255 100       530 next if !ref($_);
223 58 50       100 croak "panic" if ref($_) ne 'HASH';
224 58         101 $_ = tostring($_); # recursive
225             }
226 161         189 my $ret;
227 161   100     1057 my $op = $list->{op} || FALSE;
228 161         190 my $kids = $list->{childs};
229              
230 161 100       538 if ($op eq '&') {
    100          
231 21         31 my $base = shift @$kids;
232 21         85 my $pre = join '', map "(?=$_)", @$kids;
233 21         43 $ret = "(?:$pre$base)";
234             }
235             elsif ($op eq '-') {
236 41         58 my $base = shift @$kids;
237 41         270 my $pre = join('|', @$kids);
238 41         85 $ret = "(?:(?!$pre)$base)";
239             }
240             else {
241 99 100       249 $ret = @$kids > 1 ? "(?:".join('|', @$kids).")" : $kids->[0];
242             }
243 161 100       674 return $list->{neg} ? "(?:(?!$ret)(?s:.))" : $ret;
244             }
245              
246             1;
247             __END__