File Coverage

lib/Petal/Tiny.pm
Criterion Covered Total %
statement 266 274 97.0
branch 132 150 88.0
condition 36 46 78.2
subroutine 22 22 100.0
pod 0 15 0.0
total 456 507 89.9


line stmt bran cond sub pod time code
1             package Petal::Tiny;
2             $Petal::Tiny::VERSION = '1.16';
3 14     14   237847 use warnings;
  14         27  
  14         470  
4 14     14   62 use strict;
  14         24  
  14         461  
5 14     14   55 use Carp;
  14         21  
  14         47710  
6              
7             # REX/Perl 1.0
8             # Robert D. Cameron "REX: XML Shallow Parsing with Regular Expressions",
9             # Technical Report TR 1998-17, School of Computing Science, Simon Fraser
10             # University, November, 1998.
11             # Copyright (c) 1998, Robert D. Cameron.
12             # The following code may be freely used and distributed provided that
13             # this copyright and citation notice remains intact and that modifications
14             # or additions are clearly identified.
15             my $TextSE = "[^<]+";
16             my $UntilHyphen = "[^-]*-";
17             my $Until2Hyphens = "$UntilHyphen(?:[^-]$UntilHyphen)*-";
18             my $CommentCE = "$Until2Hyphens>?";
19             my $UntilRSBs = "[^\\]]*](?:[^\\]]+])*]+";
20             my $CDATA_CE = "$UntilRSBs(?:[^\\]>]$UntilRSBs)*>";
21             my $S = "[ \\n\\t\\r]+";
22             my $NameStrt = "[A-Za-z_:]|[^\\x00-\\x7F]";
23             my $NameChar = "[A-Za-z0-9_:.-]|[^\\x00-\\x7F]";
24             my $Name = "(?:$NameStrt)(?:$NameChar)*";
25             my $QuoteSE = "\"[^\"]*\"|'[^']*'";
26             my $DT_IdentSE = "$S$Name(?:$S(?:$Name|$QuoteSE))*";
27             my $MarkupDeclCE = "(?:[^\\]\"'><]+|$QuoteSE)*>";
28             my $S1 = "[\\n\\r\\t ]";
29             my $UntilQMs = "[^?]*\\?+";
30             my $PI_Tail = "\\?>|$S1$UntilQMs(?:[^>?]$UntilQMs)*>";
31             my $DT_ItemSE = "<(?:!(?:--$Until2Hyphens>|[^-]$MarkupDeclCE)|\\?$Name(?:$PI_Tail))|%$Name;|$S";
32             my $DocTypeCE = "$DT_IdentSE(?:$S)?(?:\\[(?:$DT_ItemSE)*](?:$S)?)?>?";
33             my $DeclCE = "--(?:$CommentCE)?|\\[CDATA\\[(?:$CDATA_CE)?|DOCTYPE(?:$DocTypeCE)?";
34             my $PI_CE = "$Name(?:$PI_Tail)?";
35             my $EndTagCE = "$Name(?:$S)?>?";
36             my $AttValSE = "\"[^<\"]*\"|'[^<']*'";
37             my $ElemTagCE = "$Name(?:$S$Name(?:$S)?=(?:$S)?(?:$AttValSE))*(?:$S)?/?>?";
38             my $ElemTagCE_Mod = "$S($Name)(?:$S)?=(?:$S)?($AttValSE)";
39             my $MarkupSPE = "<(?:!(?:$DeclCE)?|\\?(?:$PI_CE)?|/(?:$EndTagCE)?|(?:$ElemTagCE)?)";
40             my $XML_SPE = "$TextSE|$MarkupSPE";
41             # REX END - thank you Robert for this 26 line XML parser - awesome ...
42              
43             my $ATTR_RE = qr /$ElemTagCE_Mod/;
44              
45             my $DEFAULT_NS = 'petal';
46              
47             sub new {
48 14     14 0 2888 my $class = shift;
49 14   33     80 $class = ref $class || $class;
50 14         19 my $thing = shift;
51 14         40 my $self = bless {}, $class;
52 14 100 66     171 if (defined $thing and $thing =~ /(\<|\n|\>)/) {
    50          
53 13         70 $self->{xmldata} = $thing;
54             }
55             elsif (defined $thing) {
56 1 50       27 open my $xmldatafile, "<", $thing or die "cannot read open $thing";
57 1         15 $self->{xmldata} = join '', <$xmldatafile>;
58 1         6 close $xmldatafile;
59             }
60 14         58 return $self;
61             }
62              
63              
64             sub process {
65 14     14 0 555 my $self = shift;
66 14         107 my $context = { @_ };
67 14         32 my $data = $self->{xmldata};
68 14 50       59 defined $data or return; # empty data, empty result.
69 14         55 return $self->makeitso($self->xml2nodes($data), $context); # earl grey. hot.
70             }
71              
72             sub xml2nodes {
73 14     14 0 23 my ($self, $xml) = @_;
74              
75 14         3367 my @flat = ( $xml =~ /$XML_SPE/og );
76 14         104 my $top = { _kids => [], _ns => $DEFAULT_NS };
77 14         30 my @nest = ( $top );
78 14         36 for my $tag (@flat) {
79 370         511 my $node = tag2node($tag, $nest[-1]{_ns}); # if ns is not explicitly set, inherit parent ns
80              
81 370 100       510 if ($node->{_close}) {
82 93         68 my $open = pop @nest;
83 93 50       180 confess "Too many close-tags! Last {_tag}>." if $open == $top;
84 93 50       253 if (lc($node->{_tag}) ne lc($open->{_tag})) {
85 0         0 my $in = "";
86 0         0 $in .= $nest[$_]{_elem} for 1..$#nest;
87 0         0 $in .= $open->{_elem};
88 0         0 confess "Wrong close-tag '{_tag}>' following '$in'";
89             }
90             }
91             else {
92 277         165 push @{ $nest[-1]{_kids} }, $node;
  277         342  
93 277 100 100     697 push @nest, $node unless ($node->{_simple} or $node->{_selfclose});
94             }
95             }
96 14 50       39 confess "Unbalanced tree, more open than close nodes" if @nest > 1;
97              
98 14         15 my @nodes = @{ $top->{_kids} };
  14         33  
99              
100 14         81 return \@nodes;
101             }
102              
103             sub makeitso {
104 162     162 0 139 my ($self, $nodes, $context) = @_;
105              
106 162 100       216 return "" unless @$nodes;
107              
108 153         109 my @res;
109 153         147 for my $node (@$nodes) {
110 588 100       684 if ($node->{_simple}) {
111 313         448 push @res, $self->_interpolate_dollar($context, $node->{_elem}, 'resolve_expression');
112             }
113             else {
114 275         421 push @res, $self->makeitso_node($node, $context);
115             }
116             }
117              
118 152         472 return join "", @res;
119             }
120              
121             sub _interpolate_dollar {
122 332     332   351 my ($self, $context, $string, $method) = @_;
123              
124 332 100       525 if ($string =~ /\$/) {
125             my $subst = sub {
126 10     10   19 my $what = shift;
127 10         26 my $res = $self->$method($what, $context);
128 10 50       54 return $res if defined $res;
129 0         0 carp "'$what' in \$-interpolation resolved to undef";
130 0         0 return "";
131 4         21 };
132              
133 4         21 $string =~ s/(?($1) /xegi;
  3         8  
134 4         19 $string =~ s/(?($1) /xegi;
  7         14  
135 4         21 $string =~ s/\$\$/\$/g;
136             }
137 332         488 return $string;
138             }
139              
140             sub _deep_copy {
141 590     590   385 my $node = shift;
142 590         1324 my %copy = %$node;
143 590         465 my @kids;
144 590         316 for my $kid (@{ $node->{_kids} }) {
  590         607  
145 567         515 push @kids, _deep_copy($kid);
146             }
147 590         532 $copy{_kids} = \@kids;
148 590         816 return \%copy;
149             }
150              
151             sub makeitso_node {
152 301     301 0 251 my ($self, $node, $context) = @_;
153              
154 301         249 my $TAL = $node->{_ns};
155              
156 301         217 my $STOP_RECURSE = 0;
157            
158 301 100       402 if ($node->{_has_tal}) {
159 188         182 $node->{_change} = 1;
160              
161 188 100       352 if (defined( my $stuff = delete $node->{"$TAL:on-error"} )) {
162 3         14 my $nodeCopy = { %$node };
163 3         6 my $res = eval { $self->makeitso_node($node, $context); };
  3         16  
164 3 100       1007 if ($@) {
165 2 100       6 for my $k (keys %$nodeCopy) { delete $nodeCopy->{$k} if $k =~ /^$TAL:/ }
  17         46  
166 2         5 delete $nodeCopy->{_selfclose};
167 2         6 $nodeCopy->{_contents} = $self->resolve_expression($stuff, $context);
168 2         5 return node2txt($nodeCopy);
169             }
170 1         4 return $res;
171             }
172              
173 185         616 $context = { %$context };
174              
175 185 100       381 if (defined( my $stuff = delete $node->{"$TAL:define"} )) {
176 7         28 for my $def (split /;(?!;)/, $stuff) {
177 13         19 my ($symbol, $expression) = split ' ', $def, 2;
178 13         22 $context->{$symbol} = $self->resolve_expression($expression, $context);
179             }
180             }
181              
182 185 100       312 if (defined( my $stuff = delete $node->{"$TAL:condition"} )) {
183 13         24 for my $cond (split /;(?!;)/, $stuff) {
184 15 100       22 return '' unless $self->resolve_expression($cond, $context);
185             }
186             }
187              
188 180 100       293 if (defined( my $stuff = delete $node->{"$TAL:repeat"} )) {
189 7         19 my @loops = split /;(?!;)/, $stuff;
190 7         6 my $count = 0;
191 7         17 return join "", $self->_do_repeat(\$count, 1, \@loops, $node, $context);
192             }
193              
194 173 100       279 if (defined( my $stuff = delete $node->{"$TAL:content"} )) {
195 22         41 my $res = $self->resolve_expression($stuff, $context);
196 20 100       47 $node->{_contents} = defined $res ? $res : "";
197 20         30 delete $node->{_selfclose};
198              
199             # set the stop recurse flag so that if content contains $foo and $bar,
200             # those aren't interpolated as variables.
201 20         22 $STOP_RECURSE = 1;
202             }
203              
204 171 100       350 if (defined( my $stuff = delete $node->{"$TAL:replace"} )) {
205 116         139 my $res = $self->resolve_expression($stuff, $context);
206 116 100       408 return defined $res ? $res : '';
207             }
208              
209 55 100       117 if (defined( my $stuff = delete $node->{"$TAL:attributes"} )) {
210 21         70 for my $att (split /;(?!;)/, $stuff) {
211 35         65 my ($symbol, $expression) = split ' ', $att, 2;
212 35         55 my $add = ($symbol =~ s/^\+//);
213 35         58 my $new = $self->resolve_expression($expression, $context);
214 35 100       49 if (defined $new) {
215 34 100       65 if ($add) {
216 2         4 my $old = $node->{$symbol};
217 2 100       6 $old = "" unless defined $old;
218 2         3 $new = $old . $new;
219             }
220 34         70 $node->{$symbol} = $new;
221             }
222             else {
223 1 50       4 delete $node->{$symbol} unless $add;
224             }
225             }
226             }
227              
228 55 100       141 if (defined(my $stuff = delete $node->{"$TAL:omit-tag"})) {
229 3 100 100     9 if ($stuff eq '' or $self->resolve_expression($stuff, $context)) {
230 2 50       3 return $node->{_contents} if $STOP_RECURSE;
231 2         5 return $self->makeitso($node->{_kids}, $context);
232             }
233             }
234             }
235              
236 166 100       261 unless ($STOP_RECURSE) {
237 146         225 $node->{_contents} = $self->makeitso($node->{_kids}, $context);
238             }
239 165         223 return node2txt($node);
240             }
241              
242             sub _do_repeat {
243 10     10   11 my ($self, $count, $last, $loops_ref, $node, $context) = @_;
244 10         15 my @loops = @$loops_ref;
245 10         11 my $stuff = shift @loops;
246 10         15 my ($symbol, $expression) = split ' ', $stuff, 2;
247 10         17 my $array = $self->resolve_expression($expression, $context);
248 10 100       19 $array = [ $array ] unless ref $array; # we don't judge
249 10         9 my @result;
250 10         17 foreach my $idx (0 .. $#$array) {
251 26         32 my $item = $array->[$idx];
252 26         29 $context->{$symbol} = $item;
253 26 100       29 if (@loops) {
254 3   66     16 push @result, $self->_do_repeat($count, $last && $idx == $#$array, \@loops, $node, $context);
255             }
256             else {
257 23         19 $$count++;
258 23         28 $context->{repeat} = {};
259 23         41 $context->{repeat}->{index} = $$count;
260 23         23 $context->{repeat}->{number} = $$count;
261 23 100       45 $context->{repeat}->{even} = $$count%2 ? 0 : 1;
262 23 100       31 $context->{repeat}->{odd} = $$count%2 ? 1 : 0;
263 23 100       36 $context->{repeat}->{start} = $$count == 1 ? 1 : 0;
264 23 100 100     71 $context->{repeat}->{end} = $last && $idx == $#$array ? 1 : 0;
265 23 100 100     78 $context->{repeat}->{inner} = $context->{repeat}->{start} || $context->{repeat}->{end} ? 0 : 1;
266              
267 23         34 push @result, $self->makeitso_node(_deep_copy($node), $context);
268             }
269             }
270 10         68 return @result;
271             }
272              
273              
274             sub resolve_expression {
275 236     236 0 828 my ($self, $expr, $context) = @_;
276              
277 236 50       324 $expr = "" unless defined $expr;
278 236         295 $expr =~ s/[\n\r]/ /g;
279 236         329 $expr =~ s/^\s+//;
280 236         278 $expr =~ s/\s+$//;
281              
282 236         207 $expr =~ s/([;\$])\1/$1/g;
283 236 50       342 $expr eq 'nothing' and return undef;
284 236         184 $expr =~ s/^fresh\s+//;
285 236         204 my $structure = ($expr =~ s/^structure\s+//);
286 236         736 my $resolved = $self->resolve($expr, $context);
287 234 100       417 return $structure ? $resolved : xmlencode($resolved);
288             }
289              
290             sub reftype {
291 692     692 0 509 my ($self, $obj) = @_;
292 692         861 return ref $obj;
293             }
294              
295             sub resolve {
296 282     282 0 264 my ($self, $expr, $context) = @_;
297 282 100       481 $expr =~ /:(?!pattern)/ and do { # XXX what is :pattern?
298 36         97 my ($mod, $expr) = split /:(?!pattern)\s*/, $expr, 2;
299 36         151 my $meth = $self->can("modifier_$mod");
300 36 50       107 return $self->$meth($expr, $context) if $meth;
301 0         0 confess "unknown modifier $mod";
302             };
303 246 100       460 return $expr if $expr =~ s/^--//;
304              
305 223         369 my ($what, @args) = split ' ', $expr;
306 223 100       305 defined $what or return;
307              
308 221         313 my (@path) = split /\//, $what;
309 221         168 my @resolved;
310 221         154 my $obj = $context;
311 221         198 @args = map { $self->resolve($_, $context) } @args;
  15         40  
312 221         307 while (@path) {
313 344         339 my $attribute_or_method = shift @path;
314 344         320 push @resolved, $attribute_or_method;
315 344         350 my $resolved = join '/', @resolved;
316 344 100       627 $obj or confess "cannot fetch $what, because $resolved is undefined";
317 343         408 my $reftype = $self->reftype($obj);
318 343 100       609 $reftype or confess "cannot fetch $what, because $resolved is not a reference";
319              
320 342 100       599 if ($reftype eq 'ARRAY') {
    100          
    50          
321 2         6 $obj = $obj->[$attribute_or_method];
322             }
323             elsif ($reftype eq 'HASH') {
324 321         344 $obj = $obj->{$attribute_or_method};
325             }
326             elsif ($obj->can($attribute_or_method)) {
327 19 100       26 if (@path) {
328 11         24 $obj = $obj->$attribute_or_method();
329             }
330             else {
331 8         22 $obj = $obj->$attribute_or_method(@args);
332 8         30 @args = ();
333             }
334             }
335              
336             # now, check if what we found was a code-ref
337 342         385 $reftype = $self->reftype($obj);
338 342 100       451 if ($reftype eq 'CODE') {
339 1 50       3 if (@path) {
340 0         0 $obj = $obj->();
341             }
342             else {
343 1         4 $obj = $obj->(@args);
344 1         6 @args = ();
345             }
346             }
347              
348             # if we're done with @path and there's a single arg, use it to look up in array/hash
349 342 100 100     1156 if (not @path and @args == 1) {
350 7         11 $reftype = $self->reftype($obj);
351              
352 7 100       17 if ($reftype eq 'ARRAY') {
    100          
353 2         3 $obj = $obj->[ $args[0] ];
354 2         2 last;
355             }
356             elsif ($reftype eq 'HASH') {
357 4         5 $obj = $obj->{ $args[0] };
358 4         6 last;
359             }
360             }
361              
362 336 100 100     1254 not @path and @args and confess "cannot resolve expression $expr";
363             }
364 218         320 return $obj;
365             }
366              
367              
368             sub modifier_true {
369 20     20 0 30 my ($self, $expr, $context) = @_;
370 20         60 my $arg = $self->resolve($expr, $context);
371 20 50 33     31 ref $arg and $self->reftype($arg) eq 'ARRAY' and return scalar @$arg;
372 20 100       49 return $arg ? 1 : 0;
373             }
374              
375              
376             sub modifier_false {
377 9     9 0 10 my $self = shift;
378 9         17 return not $self->modifier_true(@_);
379             }
380              
381              
382             sub modifier_string {
383 19     19 0 24 my ($self, $string, $context) = @_;
384 19         44 $string = $self->_interpolate_dollar($context, $string, 'resolve');
385 19         90 return $string;
386             }
387              
388              
389             sub node2txt {
390 167     167 0 125 my $node = shift;
391              
392 167 50       257 return $node unless ref $node eq 'HASH'; # handle textnodes introduced in makeitso_node
393 167 50       238 return $node->{_elem} if $node->{_simple};
394              
395 167         186 delete $node->{_ns};
396 167         148 delete $node->{_has_tal};
397 167         563 delete $node->{_kids};
398              
399 167         138 my $change = delete $node->{_change};
400 167         196 my $elem = delete $node->{_elem};
401 167         154 my $tag = delete $node->{_tag};
402 167         165 my $close = delete $node->{_selfclose};
403 167         140 my $quotes = delete $node->{_quotes};
404 167         167 my $contents = delete $node->{_contents};
405 167   100     301 my $att = join ' ', map { my $q = $quotes->{$_} || '"'; qq|$_=$q$node->{$_}$q| } keys %$node;
  42         104  
  42         113  
406              
407 167 100       247 if ($close) {
408 8 100       35 return $change ? ($att ? "<$tag $att />" : "<$tag />") : $elem;
    100          
409             }
410              
411 159 100       254 my $start = $change ? ($att ? "<$tag $att>" : "<$tag>") : $elem;
    100          
412 159         326 my $end = "";
413              
414 159 50       219 $contents = "" unless defined $contents;
415              
416 159         478 return $start . $contents . $end;
417             }
418              
419             sub tag2node {
420 370     370 0 890 my ($elem, $ns) = @_;
421              
422 370 100 66     1597 if (my ($has_close, $tag) = $elem =~ m, \A < ( /? ) ( [A-Za-z0-9] [A-Za-z0-9_:-]* ) ,x and
423             my ($has_self_close ) = $elem =~ m, ( /? ) > \z ,x)
424             {
425 197 100       1040 return { _tag => $tag, _close => 1 } if $has_close; # don't waste any time on nodes, they're just for book-keeping
426              
427 104         138 my %node = extract_attributes($elem);
428 104   66     303 $node{_ns} ||= $ns;
429              
430 104         159 $node{_has_tal} = exists $node{_ns_prefix}{ $node{_ns} };
431 104         102 $node{_tag} = $tag;
432 104         99 $node{_selfclose} = $has_self_close;
433 104         113 $node{_elem} = $elem;
434 104         124 $node{_kids} = [];
435              
436 104         143 delete $node{_ns_prefix};
437              
438 104         149 return \%node;
439             }
440              
441             return {
442 173         307 _elem => $elem,
443             _simple => 1,
444             };
445             }
446              
447             sub extract_attributes {
448 104     104 0 868 my $tag = shift;
449              
450 104         1529 my %attr = $tag =~ /$ATTR_RE/og;
451              
452 104         101 my (%quotes, %prefix);
453              
454 104         584 foreach my $key (keys %attr) {
455 101         414 $attr{$key} =~ s/^(['"])(.*?)\1$/$2/;
456 101   50     231 my $q = $1 || '"';
457              
458 101 100       244 if ($key =~ /^(.*?):/) {
459 92 100 66     214 if ($1 eq 'xmlns' && $attr{$key} eq 'http://purl.org/petal/1.0/') {
460 10         24 delete $attr{$key};
461 10         30 $key =~ s/^xmlns\://;
462 10         18 $attr{_ns} = $key;
463 10         16 $attr{_change} = 1;
464 10         27 next;
465             }
466 82         130 $prefix{$1} = 1;
467             }
468 91         156 $quotes{$key} = $q;
469             }
470              
471 104         147 $attr{_quotes} = \%quotes;
472 104         103 $attr{_ns_prefix} = \%prefix;
473              
474 104         342 %attr;
475             }
476              
477             my %_encode_dict = (
478             '&' => '&',
479             '<' => '<',
480             '>' => '>',
481             '"' => '"',
482             "'" => ''',
483             );
484              
485             sub xmlencode {
486 230     230 0 181 my $string = shift;
487 230 100 100     631 return $string if !$string or ref $string;
488 169         248 $string =~ s/([&<>"'])/$_encode_dict{$1}/g;
489 169         313 return $string;
490             }
491              
492              
493             1;
494              
495              
496             __END__