File Coverage

blib/lib/Template/Pure/ParseUtils.pm
Criterion Covered Total %
statement 98 98 100.0
branch 28 32 87.5
condition 13 21 61.9
subroutine 10 10 100.0
pod 5 5 100.0
total 154 166 92.7


line stmt bran cond sub pod time code
1             package Template::Pure::ParseUtils;
2            
3 28     28   173324 use strict;
  28         37  
  28         664  
4 28     28   95 use warnings;
  28         32  
  28         557  
5 28     28   86 use Scalar::Util ();
  28         31  
  28         7437  
6              
7             sub parse_processing_instruction {
8 6     6 1 9 my ($pi) = @_;
9 6         28 my ($target, $body) = ($pi =~m/^\s*([^\s]+)(.+)$/s);
10             my %attrs = map {
11 14         27 my ($key, $val) = split '=', $_;
12 14         24 $key=~s/^\s+//g;
13 14 100       26 if($val=~s/^\\//) {
14 3         36 $val=~s/^['"]|['"]$//g;
15 3         4 my $val2 = \$val;
16 3         10 $key, $val2;
17             } else {
18 11         29 $val=~s/^['"]|['"]$//g;
19 11         25 $key, $val;
20             }
21 6         27 } grep { $_ }
  14         17  
22             split(/['"]\s+/, $body);
23 6         46 return $target => %attrs;
24             }
25              
26             sub parse_itr_spec {
27 11     11 1 18 my ($spec) = @_;
28 11         34 my ($key, $data_spec) = split('<-', $spec);
29 11         30 return $key => +{ parse_data_spec($data_spec) };
30             }
31              
32             {
33             package Template::Pure::Literal;
34             use overload
35 28     28   123 '""' => sub { my $self = shift; return ${$self} };
  28     51   31  
  28         215  
  51         3238  
  51         41  
  51         336  
36             }
37              
38             sub parse_data_template {
39 10     10 1 16 my ($spec) = @_;
40 10         40 $spec=~s/\r|\n//gs; # cleanup newlines.
41              
42 10         33 my $opentag = qr/=\{/;
43 10         23 my $closetag = qr/}/;
44 10         158 my $placeholder = qr{(
45             (?:
46             $opentag (
47             (?:
48             (?> [^={}]+ )
49             |
50             (?2)
51             )*
52             ) $closetag
53             )
54             )}x;
55              
56 10         14 my @parts;
57              
58             # TODO Regexp hack info. Ok so maybe my regexp Foo is not as good as it
59             # could be... The problem I have is that ( [^={]+ ) capture = and { not both
60             # grouped together. I can't seem to get it working right with things like
61             # (?!\=\{)+ that just never seems to pass tests. Since '=' is very common in
62             # HTML tags (like for setting attributes) matching on = is probably not my best
63             # idea. For now there's a hack here to change m/=./ into !\1! and then I revert
64             # it. That obviously sucks and wastes performance as well. I'm leaving it like
65             # this for now but someone with awesome regexp foo I hope can help me out :)
66              
67 10         42 $spec=~s/\=([^{])/\!$1\!/g; #TODO Hack step1
68              
69 10         195 while($spec =~/( [^={]+ ) | $placeholder /gx) {
70 34   66     127 my $part = $1||$2;
71              
72 34         43 $part=~s/\!(.)+\!/\=$1/g; #TODO Hack step2
73              
74 34 100       195 if(my ($is_data_spec) = ($part=~/^$opentag(.+?)$closetag$/)) {
75 17         31 push @parts, +{ parse_data_spec($is_data_spec) };
76             } else {
77 17         127 push @parts, bless \$part, 'Template::Pure::Literal';
78             }
79             }
80 10         104 return @parts;
81             }
82              
83             sub parse_data_spec {
84 155     155 1 160 my $spec = shift;
85              
86             # Is this a literal?
87 155 100       506 if(my ($value) = ($spec =~m/^[\'\"](.+)[\'\"]$/)) {
88             return (
89 4         29 literal => $value,
90             absolute => '',
91             path => [],
92             filters => [],
93             );
94             }
95              
96 151         357 $spec=~s/\r|\n//gs; # cleanup newlines.
97 151         174 my $absolute = ($spec=~s[^\/][]);
98              
99 151         141 my @parts;
100 151         1583 push @parts, $1 while $spec =~ /
101             ((?:
102             [^()\|]+ |
103             ( \(
104             (?: [^()]+ | (?2) )*
105             \) )
106             )*)
107             (?: \|\s* | $)
108             /xg;
109              
110             my ($path_proto, @filters_proto) =
111 326         522 grep { length($_) > 0 }
112 151         195 map { $_=~s/^\s+|\s+$//g; $_ } @parts;
  326         567  
  326         441  
113              
114             #Special case: If you have a part that is " |filter" we need to
115             #munge a bit.
116 151 100       315 if($parts[0] eq '') {
117 1         4 push @filters_proto, $path_proto;
118 1         2 $path_proto = '';
119             }
120              
121 151         534 my @path_proto = split(/\.|\//, $path_proto);
122              
123             my @path = map {
124             # Not ideal regexp here but safe enough I think since ':' is never in a method...
125             # Just would croak on a hash key that is meant to mean this. I'll doc that as
126             # 'don't do that...
127 151         165 my $maybe = ($_=~s/(maybe:)//);
  226         244  
128 226         210 my $optional = ($_=~s/(optional:)//);
129 226         673 +{ key => $_, maybe => $maybe, optional => $optional };
130             } @path_proto;
131              
132             my @filters = map {
133 151         187 my ($filter, $arg_proto) = ($_=~m/^(.*?)(?:\(\s*(.+?)?\s*\))?$/); # Borrowed from Catalyst::Controller
  24         129  
134 24         27 my @arg_parts;
135              
136 24 100       44 if($arg_proto) {
137 12         111 push @arg_parts, $1 while $arg_proto =~ /
138             ((?:
139             [^(),]+ |
140             ( \(
141             (?: [^()]+ | (?2) )*
142             \) )
143             )*)
144             (?: ,\s* | $)
145             /xg;
146             }
147              
148             my @args = map {
149 19         41 my ($data_spec) = ($_=~/^\=\{(.+?)\}$/);
150 19 100       557 $data_spec ? +{ parse_data_spec($data_spec) } : eval $_;
151             } grep {
152 24         33 length($_) > 0;
  31         39  
153             } @arg_parts;
154              
155 24         71 +[ $filter, @args ];
156             } @filters_proto;
157              
158             return (
159 151         1374 absolute => $absolute,
160             path => \@path,
161             filters => \@filters,
162             );
163             }
164              
165             sub parse_match_spec {
166 193     193 1 261 my $spec = shift;
167              
168             # Look for all the possibilities, try to leave $spec in a useful state
169 193         326 my $maybe_target_node = ($spec=~s/^\^//);
170 193         211 my $maybe_filter = ($spec=~s/\|$//);
171 193         218 my $maybe_prepend = ($spec=~s/^(\+)//);
172 193         230 my $maybe_append = ($spec=~s/(\+)$//);
173 193         170 my $maybe_absolute = ($spec=~s[^\/][]);
174              
175 193         383 my ($css, $maybe_attr) = split('@', $spec);
176 193 100 100     427 $css = '.' if $maybe_attr && !$css; # $css unlikely to be 0
177              
178             # All the error conditions I can think of.
179 193 50       273 die "You need a CSS style match: '$spec'"
180             unless $css;
181              
182 193 50 33     299 die "Can't add a filter when appending or prepending: '$spec'"
      66        
183             if $maybe_filter && ($maybe_append || $maybe_prepend);
184              
185 193 50 33     275 die "Can't set a target when filtering: '$spec'"
      66        
186             if $maybe_filter && ($maybe_target_node || $maybe_attr);
187              
188 193 50 66     389 die "Can't set a target attribute and target node: '$spec'"
189             if $maybe_target_node && $maybe_attr;
190              
191 193         186 my $target = 'content';
192 193 100       376 if($maybe_target_node) {
    100          
193 34         35 $target = 'node';
194             } elsif($maybe_attr) {
195 21         24 $target = \$maybe_attr;
196             }
197              
198 193         150 my $mode = 'replace';
199 193 100       425 if($maybe_append) {
    100          
    100          
200 25         29 $mode = 'append';
201             } elsif($maybe_prepend) {
202 11         10 $mode = 'prepend';
203             } elsif($maybe_filter) {
204 2         3 $mode = 'filter';
205 2         4 $target = '';
206             }
207              
208             return (
209 193         1571 absolute => $maybe_absolute,
210             css => $css,
211             target => $target,
212             mode => $mode,
213             );
214             }
215              
216             1;
217              
218             =head1 NAME
219              
220             Template::Pure::ParseUtils - Utility Functions
221              
222             =head1 SYNOPSIS
223              
224             For internal use
225              
226             =head1 DESCRIPTION
227              
228             Contains utility functions for L<Template::Pure>
229              
230             =head1 FUNCTIONS
231              
232             This package contains the following functions:
233              
234             =head2 parse_processing_instruction ($pi)
235              
236             Given a processing instruction, parse it into a $target and %attrs such that:
237              
238             <?pure-include id='ddd'
239             pure:mode='append|prepend|replace'
240             pure:target='node|content'
241             pure:src='include' ?>
242              
243             Is parsed into:
244              
245             "pure-include" => {
246             id => "ddd",
247             "pure:mode" => "append|prepend|replace",
248             "pure:src" => "include",
249             "pure:target" => "node|content"
250             }
251              
252             and returned.
253              
254             =head2 parse_itr_spec ($spec)
255              
256             Used to parse a string when we are specifying an iterator. For example
257              
258             "user<-users"
259              
260             or:
261              
262             "friend<-user.friends"
263              
264             Returns a hashref when the key is the new data label and the value is a reference to the
265             indicated path from the current data context.
266              
267             {
268             user => {
269             absolute => '';
270             filters => [],
271             path => [
272             key => 'users',
273             maybe => '',
274             optional => ''
275             ],
276             },
277             }
278              
279             B<NOTE> you cannot use a filter on an iterator specification.
280              
281             B<NOTE> Indicated data context path must be something that can be coerced into an iterator
282             (an arrayref, a hashref, or an Object that provides the iterator interface).
283              
284             =head2 parse_data_template ($spec)
285              
286             Used to parse a string that is the target action of a match, when the string contains
287             template placeholders, for example:
288              
289             "Hello ={meta.first_name} ={meta.last_name}!"
290              
291             Which is intended to be parsed as containing a string with two placeholders, each pointing
292             to a different path on the current data context.
293              
294             When parsed returns an array, where each element is either a string (for a literal string
295             value) or a hash reference (indicates a patch to a value on the current data context).
296              
297             For example the shown string would parse in this way:
298              
299             (
300             'Hello ',
301             {
302             absolute => '',
303             filters => [],
304             path => [
305             { key => 'meta', optional => undef, maybe => undef },
306             { key => 'first_name', optional => undef, maybe => undef },
307             },
308             ' ',
309             {
310             absolute => '',
311             filters => [],
312             path => [
313             { key => 'meta', optional => undef, maybe => undef },
314             { key => 'first_name', optional => undef, maybe => undef },
315             },
316             '!',
317             );
318              
319             Information inside the placeholder may contain filters and prefixes and other markers:
320              
321             "Year of Birth: ={/maybe:meta.optional:dob | strftime(%Y)}"
322              
323             Would parse as:
324              
325             (
326             'Year of Birth: ',
327             {
328             absolute => '1',
329             filters => [
330             ['strftime', '%Y'],
331             ],
332             path => [
333             { key => 'meta', optional => undef, maybe => 1 },
334             { key => 'dob', optional => 1, maybe => undef },
335             },
336             );
337              
338              
339             =head2 parse_data_spec ($spec)
340              
341             When the action target is a string we need to inspect it to figure out what do do with it.
342             Returns a hash with keys as follows:
343              
344             =over 4
345              
346             =item absolute
347              
348             Boolean. Defaults to False. When true this means the described path should be absolute
349             from the top of the data context. Otherwise the described path is relative to the current
350             point selected in the data context.
351              
352              
353             =item path
354              
355             Example:
356              
357             path => [
358             {
359             key => 'meta',
360             optional => 0,
361             maybe => 0
362             },
363             {
364             key => 'title',
365             optional => 0,
366             maybe => 0
367             }
368             ];
369              
370             An array hashrefs that indicate path parts from the current data context to the value we
371             wish to use. Each hashref contains three keys:
372              
373             =over 4
374              
375             =item key
376              
377             The name that is a 'key' point on the path. Likely to be a key in a hash or a method on an
378             object.
379              
380             =item optional
381              
382             Boolean. Defaults to false. Generally if the key does not match a real path on the current
383             data context, this should return an error. If this value is false, that means instead of
384             throwing an error we return an 'undef'.
385              
386             Value is derived from the prefix 'optional:'. Presence of this prefix sets this to true.
387              
388             B<NOTE> since 'optional:' has special meaning here, this means that if your data context is
389             a hash, you should not have any keys that match 'optional:' for your own purposes... If you
390             really run into this you'll have to write an anonymous subroutine type action.
391              
392             =item maybe
393              
394             Boolean. Defaults to false. Generally if you have several path parts and a midpoint part
395             returns undefined, that mean we throw an exception on later parts (can't find a next path on
396             an undefined value). In some cases (like when you are chaining resultset methods in L<DBIx::Class>)
397             we might not prefer tothrow an error but just return 'undef'. When a path is 'maybe' we
398             wrap in in an object such that the next path is always found (but returns undef).
399              
400             Value is derived from the prefix 'maybe:'. Presence of this prefix sets this to true.
401              
402             B<NOTE> since 'maybe:' has special meaning here, this means that if your data context is
403             a hash, you should not have any keys that match 'maybe:' for your own purposes... If you
404             really run into this you'll have to write an anonymous subroutine type action.
405              
406             =back
407              
408             =item filters
409              
410             An Arrayref of Arrayrefs which are any filters added to the value and their arguments.
411              
412             Example:
413              
414             filters => [
415             [ 'repeat', '3'],
416             [ 'escape_html' ],
417             ];
418              
419             In some cases the arguments for a filter might itself point to a resolved data spec (which itself
420             could include filters... In this case the argument value will be a hashref that is itself the
421             result of a call to L</parse_data_spec>, example:
422              
423             (
424             path => [
425             {
426             key => 'meta', maybe => 0, optional => 0,
427             },
428             {
429             key => 'title', maybe => 0, optional => 0,
430             },
431             ],
432             filters => [
433             ['title_case'],
434             ['truncate',
435             {
436             path => [
437             { key => 'settings', maybe => 0, optional => 0 },
438             { key => 'title_length', maybe => 0, optional => 0 },
439             ],
440             filters => [],
441             },
442             '...',
443             ],
444             ],
445             );
446              
447             You'd have a string to parse like "meta.title | title_case | truncate(={settings.title_length},'...')"
448             which has a filter 'truncate' that has two args, the first being 'whatever the value is at
449             'settings.title_length'' and the second is a literal '...'.
450              
451             You could go wild here with nested values and filters but I recommend if you have such complex
452             needs it would be better to do it in Perl with an anonymous subroutine rather than over cleverness
453             in the string based DSL, which will never be as good as Perl itself. Use it for straight and simple
454             things and for when you want to let non Perl programmers work with the directives.
455              
456             B<NOTE> we run C<eval> on each argument to convert it to a Perl data value, so you could in
457             theory do fancy stuff here like "filter(1+2+3)" and get an arg of '3'. I highly recommend constraint
458             in this. Since its C<eval>'d you should be certain these values are properly cleaned and untainted.
459             For example beware of something like "filter($a)", where $a comes from uncontrolled source such as the
460             input of a HTML Form post, or from external sources like a database or file. This could be considered
461             a possible injection attack location. Because of this we might someday switch this to a non eval
462             parser such as L<Data::Pond> or similar, and if you did crazy expression stuff that don't work with
463             a more restrictive and safe expression parser, its possible your code will break. Buyer beware.
464              
465             B<NOTE> The values for the boolean keys 'maybe' and 'optional' are only specificed to return a
466             Perl value to be evaluated as a boolean. We don't specify the exact value. For example, under Perl
467             both 0 and undef are considered false in boolean context.
468              
469             =back
470              
471             =head2 parse_match_spec ($spec)
472              
473             Given a directive match specification (such as '#head', 'title', 'p.links@href', ...) parse
474             it into a hash that defines how the match is to be performed. Returns a hash with keys are
475             follows.
476              
477             =over 4
478              
479             =item css
480              
481             This is the actual CSS match component ('p', '#id', '.class') or the special match indicator of
482             '.' for the current node.
483              
484             =item target
485              
486             This is the indicator of the replacement target for the match. Can be: 'node', 'content', \'$attribute':
487              
488             =over 4
489              
490             =item content
491              
492             Example Match Specifiction: 'p.headline', 'title', '#id'
493              
494             This is the default value for target. Indicates we will update the matched nodes' content. For example the
495             content of node '<p>content</p>' is 'content'. No special symbols are needed to indicate this target type.
496              
497             =item \$attribute
498              
499             Example Match Specifiction: 'a#homepage@href', 'ul.links@class'
500              
501             When the value of 'target' is a scalar reference, this indicates the update type to be an attribute on the current
502             matched node. The dereferenced scalar is the name of the attribute. If the attribute does not exist in the current
503             node this does not raise an exception, but rather we automatically add it.
504              
505             It is an error to indicate both node and attribute targets.
506              
507             B<NOTE> Should a match specification consist only of an attribute, we presume a 'css' value of '.'
508              
509             =item node
510              
511             Example Match Specifiction: '^p.headline', '^#id'
512              
513             Indicated a target of 'node', which means we will replace the entire matched node. Indicated by a '^' appearing
514             as the first character of the match specification.
515              
516             It is an error to indicate both node and attribute targets.
517              
518             =back
519              
520             =item mode
521              
522             Defines the relationship, if any, between a new value from the data context and any existing information
523             in the template and the match location. One of 'append', 'prepend', or 'replace', with 'replace' being the default.
524              
525             =over 4
526              
527             =item replace
528              
529             Example Match Specifiction: 'title', '#id', 'p.content@class'
530              
531             The default behavior. Needs no special indicators in the match specification. Means the new value
532             completely replaces the match target.
533              
534             =item append
535              
536             Example Match Specifiction: 'title+', '#id+', 'p.content@class+'
537              
538             Match specifications that end with '+' will append to the indicated match (that is we place the
539             new value after the old value, preserving th old value.
540              
541             It is an error to try to set both append and prepend mode on the same target. It is also an error
542             to use append and prepend along with a filter indicator (see below).
543              
544             When appending to a target of attribute where the attribute is 'class', we automatically add a ' ' (space)
545             between the appending value and any existing value. This is a special case since generally a space
546             is required between classes in order for them to work as expected.
547              
548             =item prepend
549              
550             Example Match Specifiction: '+body', '+p.content@class'
551              
552             Match specifications that begin with a '+' (or '^+') indicate we expect to add the data context to the
553             front of the existing value, preserving the existing value.
554              
555             It is an error to try to set both append and prepend mode on the same target. It is also an error
556             to use append and prepend along with a filter indicator (see below).
557              
558             When prepending to a target of attribute where the attribute is 'class', we automatically add a ' ' (space)
559             between the appending value and any existing value. This is a special case since generally a space
560             is required between classes in order for them to work as expected.
561              
562             =item filter
563              
564             Example Match Specification: 'html|', 'body|'
565              
566             Means that we expect to run a filter callback on the matched node. Useful when you want to make global
567             changes across the entire template. Indicated by a '|' or pipe symbol. Cannot be used with append,
568             prepend or any special target indicators (attributes or node).
569              
570             We expect the action the be an anonymous subroutine.
571              
572             =back
573              
574             =back
575              
576             =head1 SEE ALSO
577            
578             L<Template::Pure>.
579              
580             =head1 AUTHOR
581            
582             John Napiorkowski L<email:jjnapiork@cpan.org>
583              
584             But lots of this code was copied from L<Template::Filters> and other prior art on CPAN. Thanks!
585            
586             =head1 COPYRIGHT & LICENSE
587            
588             Please see L<Template::Pure> for copyright and license information.
589              
590             =cut