File Coverage

blib/lib/Pinwheel/View/Data.pm
Criterion Covered Total %
statement 280 280 100.0
branch 122 122 100.0
condition 17 17 100.0
subroutine 36 36 100.0
pod 0 3 0.0
total 455 458 99.3


line stmt bran cond sub pod time code
1             package Pinwheel::View::Data;
2              
3 5     5   46564 use strict;
  5         10  
  5         357  
4 5     5   28 use warnings;
  5         10  
  5         135  
5              
6 5     5   26 use Carp;
  5         11  
  5         397  
7 5     5   13170 use PPI;
  5         1043277  
  5         4794  
8              
9              
10             sub parse_template
11             {
12 125     124 0 175866 my ($s, $name) = @_;
13 125         298 my ($pkgname, $vars, $perlvars, $ctxvars);
14              
15 125         355 $pkgname = $name;
16 125         510 $pkgname =~ s!\..*!!;
17 125         890 $pkgname =~ s!(^|/)([^a-zA-Z])!$1_$2!g;
18 125         731 $pkgname =~ s![^a-z0-9/]+!_!g;
19 125         780 $pkgname =~ s!/!::!;
20 125         523 $pkgname = 'Template::' . $pkgname;
21              
22 125         2969 $vars = find_parameters($s);
23             # Can't override the $h helpers variable
24 125         61104 delete $vars->{'$h'};
25 125         974 $vars->{'$dummy'} = 1;
26 125         850 $vars = [keys %$vars];
27 125         554 $perlvars = join(', ', @$vars);
28 125         604 $ctxvars = join(', ', map { "'" . substr($_, 1) . "'" } @$vars);
  127         750  
29              
30 2     2   21 eval qq{
  2     1   3  
  2         86  
  2         12  
  2         5  
  2         288  
  2         21  
  2         5  
  2         90  
  2         14  
  2         5  
  2         697  
  125         12695  
31             package Pinwheel::View::Data::$pkgname;
32             use strict;
33             use warnings;
34             our \$h;
35             *AUTOLOAD = *Pinwheel::View::Data::Builder::AUTOLOAD;
36             *TAG = *Pinwheel::View::Data::Builder::TAG;
37             sub _render_
38             {
39             my ($perlvars) = \@_;
40             #line 1 "$name"
41             $s;
42             }
43             };
44 125 100       988 croak $@ if $@;
45              
46 124         29496 return eval qq{
47             sub {
48             my (\$locals, \$globals, \$fn) = \@_;
49             my (\$vars, \@values);
50              
51             \$vars = \{dummy => undef, \%\$globals, \%\$locals\};
52             foreach (($ctxvars)) \{
53             croak("Missing parameter '\$_'") if !exists(\$vars->\{\$_\});
54             \}
55             \$Pinwheel::View::Data::$pkgname\::h = \$fn;
56             \@values = \@\$vars\{($ctxvars)\};
57             Pinwheel::View::Data::Wrapper->new(Pinwheel::View::Data::$pkgname\::_render_(\@values));
58             }
59             };
60             }
61              
62             sub find_parameters
63             {
64 140     139 0 34207 my ($s) = @_;
65 140         309 my ($d, $global, $subs, $declared, $undeclared);
66              
67 140         1440 $d = PPI::Document->new(\$s);
68 140         393707 $global = $d->clone;
69 140         59078 $global->prune('PPI::Statement::Sub');
70 140   100     116165 $subs = $d->find('PPI::Statement::Sub') || [];
71              
72 140         143257 $undeclared = {};
73 140         590 $declared = find_undeclared($global, {}, $undeclared);
74 140         628 find_undeclared($_, $declared, $undeclared) foreach (@$subs);
75              
76 140         1035 return $undeclared;
77             }
78              
79             sub find_undeclared
80             {
81 148     147 0 317 my ($d, $declared, $undeclared) = @_;
82 148         586 my ($nodes, $n, $var);
83              
84             $nodes = $d->find(sub {
85 1985 100   1984   66433 $_[1]->isa('PPI::Token::Symbol') ||
86             $_[1]->isa('PPI::Statement::Variable')
87 148         1517 });
88 148 100       2929 $nodes = [] if !$nodes;
89              
90 148         542 $declared = {%$declared};
91 148         1376 foreach $n (@$nodes) {
92 45 100       685 if ($n->isa('PPI::Statement::Variable')) {
    100          
93 11         70 foreach (@{$n->find('PPI::Token')}) {
  11         45  
94 61 100 100     5487 if ($_->isa('PPI::Token::Operator') && $_->content eq '=') {
    100          
95 7         58 last;
96             } elsif ($_->isa('PPI::Token::Symbol')) {
97 14         54 $declared->{$_->content} = 1;
98             }
99             }
100             } elsif (!$n->isa('PPI::Token::Magic')) {
101 33         243 $var = $n->content;
102 33 100 100     384 $undeclared->{$var} = 1 if ($var =~ /^\$/ && !$declared->{$var});
103             }
104             }
105              
106 148         709 return $declared;
107             }
108              
109             sub _clear_templates
110             {
111 5     4   609 my ($pkg, $dir, $name);
112              
113 5         19 $pkg = \%::;
114 5         33 $pkg = $pkg->{'Pinwheel::'}{'View::'}{'Data::'}{'Template::'};
115 5         143 foreach $dir (keys %$pkg) {
116 7         22 foreach $name (keys %{$pkg->{$dir}}) {
  7         80  
117 125         184 foreach (keys %{$pkg->{$dir}{$name}}) {
  125         567  
118 800         3650 delete $pkg->{$dir}{$name}{$_};
119             }
120 125         1034 delete $pkg->{$dir}{$name};
121             }
122 7         215 delete $pkg->{$dir};
123             }
124             }
125              
126              
127              
128             package Pinwheel::View::Data::Builder;
129              
130 5     5   62 use strict;
  5         12  
  5         237  
131 5     5   29 use warnings;
  5         13  
  5         711  
132              
133             our $AUTOLOAD;
134              
135             my @stack;
136              
137              
138             sub AUTOLOAD
139             {
140 149     148   1392 my ($name, $fn);
141              
142 149         513 $name = $AUTOLOAD;
143 149         1457 $name =~ s/.*://;
144              
145 149     170   756 $fn = sub { TAG($name, @_) };
  171         776  
146              
147 5     5   30 no strict 'refs';
  5         12  
  5         1122  
148 149         1060 *$AUTOLOAD = $fn;
149 149         572 goto &$fn;
150             }
151              
152             sub TAG
153             {
154 184     183   413 my ($name, $content, $attrs, $data);
155              
156 184         358 $name = shift @_;
157 184 100       682 $content = pop @_ if (@_ & 1);
158 184 100       592 $attrs = [@_] if @_;
159              
160 184 100       562 push @stack, [] if (scalar(@stack) == 0);
161 184 100       1031 if (ref($content)) {
162 39         151 push @stack, [];
163 39         125 &$content;
164 39         92 $content = pop @stack;
165             }
166 184         638 $data = [$name, $attrs, $content];
167 184         339 push @{$stack[-1]}, $data;
  184         471  
168              
169 184         1587 return $data;
170              
171             }
172              
173              
174              
175             package Pinwheel::View::Data::Wrapper;
176              
177 5     5   287 use strict;
  5         14  
  5         628  
178 5     5   30 use warnings;
  5         18  
  5         151  
179              
180 5     5   51 use Carp;
  5         76  
  5         424  
181 5     5   9846 use Data::Dumper qw();
  5         52187  
  5         14071  
182              
183              
184             sub new
185             {
186 125     124   627 my ($class, $raw) = @_;
187 125         1687 return bless({raw => $raw}, $class);
188             }
189              
190             sub to_string
191             {
192 10     9   144 my ($self, $format) = @_;
193              
194 10 100       84 if ($format =~ /^(xml|atom|rss)$/) {
    100          
    100          
    100          
195 5         19 return $self->to_xml();
196             } elsif ($format eq 'json') {
197 3         52 return $self->to_json();
198             } elsif ($format eq 'yaml') {
199 2         10 return $self->to_yaml();
200             } elsif ($format eq 'html') {
201 2         8 return $self->to_html();
202             } else {
203 2         256 croak "Unsupported format";
204             }
205             }
206              
207             sub to_json
208             {
209 46     45   217 my ($self) = @_;
210              
211 46         347 return '{' . _to_json(@{$self->{raw}}) . '}';
  46         292  
212             }
213              
214             sub to_yaml
215             {
216 32     31   406 my ($self) = @_;
217              
218 32         68 return _to_yaml(@{$self->{raw}}, 0) . "\n";
  32         312  
219             }
220              
221             sub to_xml
222             {
223 33     32   114 my ($self) = @_;
224              
225 33         86 return "\n" . _to_xml(@{$self->{raw}});
  33         571  
226             }
227              
228             ## JSON with HTML syntax highlighting
229             sub to_html
230             {
231 38     37   163 my ($self) = @_;
232              
233 38         442 return "\n".
234             "".
235             "".
242             "\n".
243             "
{" .
244 38         101 _to_html(@{$self->{raw}}) .
245             "}\n".
246             "";
247             }
248              
249             sub _to_json
250             {
251 86     85   208 my ($tag, $attrs, $content, $ignore_tag) = @_;
252 86         122 my ($is_list, $s, $i, $n, @values);
253              
254 86         222 $tag =~ s/:/\$/;
255 86         328 $is_list = ($tag =~ s/_$//);
256 86 100       463 $s = '"' . $tag . '":' unless $ignore_tag;
257              
258 86 100       432 if ($attrs) {
259 18         49 $n = @$attrs;
260 18         71 for ($i = 0; $i < $n; $i += 2) {
261 20         139 push @values, [$attrs->[$i], undef, $attrs->[$i + 1]];
262             }
263 18 100       94 if (!defined($content)) {
    100          
264 10         21 $content = [];
265             } elsif (!ref($content)) {
266 6         115 $content = [['$t', undef, $content]];
267             }
268 18         70 $content = [@values, @$content];
269             }
270              
271 86 100       8050 if (!defined($content)) {
    100          
    100          
272 12         67 $s .= 'null';
273             } elsif (ref($content)) {
274 25 100       71 $s .= $is_list ? '[' : '{';
275 25         42 $i = -1;
276 25         167 foreach (@$content) {
277 41 100       110 $s .= ',' if (++$i);
278 41         119 $s .= _to_json(@$_, $is_list);
279             }
280 25 100       121 $s .= $is_list ? ']' : '}';
281             } elsif ($content =~ /^-?[0-9]+(?:\.[0-9]+)?$/) {
282 22         56 $s .= $content;
283             } else {
284 30 100       113 $content = _json_escape($content) if $content =~ /[\\"\x00-\x1f]/;
285 30         170 $s .= '"' . $content . '"';
286             }
287              
288 86         892 return $s;
289             }
290              
291             sub _json_escape
292             {
293 7     6   18 my ($s) = @_;
294              
295 7         48 $s =~ s/\\/\\\\/g;
296 7         20 $s =~ s/\n/\\n/g;
297 7         18 $s =~ s/"/\\"/g;
298 7 100       129 return $s unless $s =~ /[\x00-\x1f]/;
299              
300 5         28 $s =~ s/([\x00-\x1f])/sprintf('\u%04x', ord($1))/ge;
  5         32  
301 5         49 return $s;
302             }
303              
304             sub _to_yaml
305             {
306 69     68   186 my ($tag, $attrs, $content, $depth, $ignore_tag) = @_;
307 69         108 my ($is_list, $s, $i, $n, @values, $indent);
308              
309 69         320 $tag =~ s/:/\$/;
310 69         164 $is_list = ($tag =~ s/_$//);
311              
312 69 100       186 if ($attrs) {
313 13         61 $n = @$attrs;
314 13         62 for ($i = 0; $i < $n; $i += 2) {
315 13         71 push @values, [$attrs->[$i], undef, $attrs->[$i + 1]];
316             }
317 13 100       180 if (!defined($content)) {
    100          
318 7         26 $content = [];
319             } elsif (!ref($content)) {
320 5         18 $content = [['$t', undef, $content]];
321             }
322 13         72 $content = [@values, @$content];
323             }
324              
325 69 100       362 if (!$ignore_tag) {
326 59         100 $s = $tag . ':';
327 59 100 100     583 $s .= ' ' unless (ref($content) && @$content > 0);
328             }
329              
330 69 100 100     847 if (!defined($content)) {
    100          
    100          
    100          
    100          
331 9         18 $s .= '~';
332             } elsif (ref($content) && @$content == 0) {
333 3 100       43 $s .= $is_list ? '[]' : '{}';
334             } elsif (ref($content)) {
335 23         98 $depth += 1;
336 23 100       249 $indent = "\n" . (' ' x $depth) . ($is_list ? '- ' : '');
337 23         121 $i = -1;
338 23         73 foreach (@$content) {
339 38 100 100     175 $s .= $indent if (++$i || !$ignore_tag);
340 38         145 $s .= _to_yaml(@$_, $depth, $is_list);
341             }
342             } elsif ($content =~ /^-?[0-9]+(?:\.[0-9]+)?$/) {
343             # Could check /^[\x20-\x22\x24-\x39\x3b-\x7e]+$/ instead, but for
344             # visual consistency with JSON just omit quotes from data that looks
345             # numeric.
346 9         29 $s .= $content;
347             } elsif ($content =~ /[\x00-\x08\x0a-\x1f"\\\x7f\xe2\xed]/) {
348 4         12 $s .= '"' . _yaml_escape($content) . '"';
349             } else {
350 26         159 $s .= '"' . $content . '"';
351             }
352              
353 69         990 return $s;
354             }
355              
356             sub _yaml_escape
357             {
358 4     3   12 my ($s) = @_;
359              
360 4         58 $s =~ s/([\\"])/\\$1/g;
361 4 100       27 return $s unless $s =~ /[\x00-\x08\x0a-\x1f\x7f\xe2\xed]/;
362              
363 3         10 $s =~ s/([\x00-\x08\x0a-\x1f\x7f])/sprintf('\x%02x', ord($1))/ge;
  2         142  
364 3         17 $s =~ s/\xe2\x80([\xa8\xa9])/sprintf('\u20%02x', ord($1) - 128)/ge;
  3         345  
365 3         46 $s =~ s/\xed([\xa0-\xbf])([\x80-\xbf])/
366 5         33 sprintf('\ud%03x', ((ord($1) & 63) << 6) | (ord($2) & 63))/ge;
367 3         19 return $s;
368             }
369              
370             sub _to_xml
371             {
372 44     43   212 my ($tag, $attrs, $content) = @_;
373 44         82 my ($s, $i, $n, $value);
374              
375 44         116 $tag =~ s/_$//;
376 44         152 $s = '<' . $tag;
377              
378 44 100       146 $n = $attrs ? @$attrs : 0;
379 44         160 for ($i = 0; $i < $n; $i += 2) {
380 18         193 $value = $attrs->[$i + 1];
381 18 100       57 $value = '' if !defined($value);
382 18 100       72 $value = _xml_escape($value) if $value =~ /[&<>'"]/;
383 18         145 $s .= ' ' . $attrs->[$i] . '="' . $value . '"';
384             }
385              
386 44 100       162 if (!defined($content)) {
    100          
387 16         34 $s .= '/>';
388             } elsif (ref($content)) {
389 8         133 $s .= '>';
390 8         51 $s .= _to_xml(@$_) foreach (@$content);
391 8         26 $s .= '';
392             } else {
393 22 100       190 $content = _xml_escape($content) if $content =~ /[&<>'"]/;
394 22         70 $s .= '>' . $content . '';
395             }
396              
397 44         276 return $s;
398             }
399              
400             sub _xml_escape
401             {
402 3     2   122 my ($s) = @_;
403              
404 3         20 $s =~ s/&/&/g;
405 3         9 $s =~ s/
406 3         46 $s =~ s/>/>/g;
407 3         11 $s =~ s/'/'/g;
408 3         11 $s =~ s/\"/"/g;
409              
410 3         105 return $s;
411             }
412              
413             sub _to_html
414             {
415 76     75   168 my ($tag, $attrs, $content, $ignore_tag) = @_;
416 76         121 my ($is_list, $s);
417              
418 76         190 $tag =~ s/:/\$/;
419 76         157 $is_list = ($tag =~ s/_$//);
420              
421 76         140 $s = "
";
422 76 100       403 $s .= "\"" . $tag . "\": " unless ($ignore_tag);
423              
424 76 100       205 if ($attrs) {
425 17         44 my $n = @$attrs;
426 17         70 my @values = ();
427 17         78 for (my $i = 0; $i < $n; $i += 2) {
428 19         105 push @values, [$attrs->[$i], undef, $attrs->[$i + 1]];
429             }
430 17 100       169 if (!defined($content)) {
    100          
431 10         31 $content = [];
432             } elsif (!ref($content)) {
433 5         16 $content = [['$t', undef, $content]];
434             }
435 17         87 $content = [@values, @$content];
436             }
437              
438 76 100       551 if (!defined($content)) {
    100          
439 12         30 $s .= 'null';
440             } elsif (ref($content)) {
441 24         160 my $i = 0;
442 24 100       73 $s .= $is_list ? '[' : "{";
443 24         67 foreach (@$content) {
444 39         142 $s .= _to_html(@$_, $is_list);
445 39 100       127 $s .= "," unless (++$i == @$content);
446 39         107 $s .= "";
447             }
448 24 100       175 $s .= $is_list ? ']' : "}";
449             } else {
450 42 100       255 unless ($content =~ /^-?[0-9]+(?:\.[0-9]+)?$/) {
451 27 100       114 $content = _json_escape($content) if $content =~ /[\\"\x00-\x1f]/;
452 27         101 $content = "\"$content\"";
453             }
454 42         208 $s .= '' . _html_escape($content) . "";
455             }
456              
457 76         687 return $s;
458             }
459              
460             sub _html_escape
461             {
462 42     41   188 my ($s) = @_;
463 42 100       1522 return $s unless ($s =~ /[&<>'"\x80-\xff]/);
464 27         53 $s =~ s/&/&/g;
465 27         82 $s =~ s/
466 27         46 $s =~ s/>/>/g;
467 27         65 $s =~ s/'/'/g;
468 27         240 $s =~ s/\"/"/g;
469 27         98 $s =~ s/([\xc0-\xef][\x80-\xbf]+)/_make_utf8_entity($1)/ge;
  4         12  
470 27         181 return $s;
471             }
472              
473             sub _make_utf8_entity
474             {
475 4     3   23 my ($i, @bytes) = split(//, shift());
476 4 100       15 $i = ord($i) & ((ord($i) < 0xe0) ? 0x1f : 0x0f);
477 4         106 $i = ($i << 6) + (ord($_) & 0x3f) foreach @bytes;
478 4         24 return "&#$i;";
479             }
480              
481              
482             1;