File Coverage

lib/Petal/Tiny.pm
Criterion Covered Total %
statement 267 271 98.5
branch 131 150 87.3
condition 34 43 79.0
subroutine 22 22 100.0
pod 0 15 0.0
total 454 501 90.6


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