File Coverage

blib/lib/Template/Liquid/Tag/For.pm
Criterion Covered Total %
statement 74 77 96.1
branch 56 64 87.5
condition 16 24 66.6
subroutine 7 7 100.0
pod 0 2 0.0
total 153 174 87.9


line stmt bran cond sub pod time code
1             package Template::Liquid::Tag::For;
2             our $VERSION = '1.0.23';
3 25     25   174 use strict;
  25         61  
  25         745  
4 25     25   139 use warnings;
  25         67  
  25         879  
5             require Template::Liquid::Error;
6             require Template::Liquid::Utility;
7 25     25   165 use base 'Template::Liquid::Tag::If';
  25         64  
  25         36293  
8             my $Help_String = 'TODO';
9 25     25   101 sub import { Template::Liquid::register_tag('for') }
10              
11             sub new {
12 105     105 0 273 my ($class, $args) = @_;
13             raise Template::Liquid::Error {type => 'Context',
14             template => $args->{template},
15             message => 'Missing template argument',
16             fatal => 1
17             }
18 105 50       256 if !defined $args->{'template'};
19             raise Template::Liquid::Error {type => 'Context',
20             template => $args->{template},
21             message => 'Missing parent argument',
22             fatal => 1
23             }
24 105 50       230 if !defined $args->{'parent'};
25             raise Template::Liquid::Error {
26             type => 'Syntax',
27             template => $args->{template},
28             message => 'Missing argument list in ' . $args->{'markup'},
29             fatal => 1
30             }
31 105 50       260 if !defined $args->{'attrs'};
32 105 50       1027 if ($args->{'attrs'} !~ qr[^([\w\.]+)\s+in\s+(.+?)(?:\s+(.*)\s*?)?$]o) {
33             raise Template::Liquid::Error {
34             template => $args->{template},
35             type => 'Syntax',
36 0         0 message => 'Bad argument list in ' . $args->{'markup'},
37             fatal => 1
38             };
39             }
40 105   100     717 my ($var, $range, $attr) = ($1, $2, $3 || '');
41 105 100       337 my $reversed = $attr =~ s[^reversed\b][]o ? 1 : 0;
42             my %attr = map {
43              
44             #my $blah = $_;
45             #my ($k, $v)
46             # = grep { defined $_ }
47             # $_
48             # =~ m[$Template::Liquid::Utility::VariableFilterArgumentParser]g;
49             #use Data::Dump;
50             ##ddx [$k, $v];
51 72         456 my ($k, $v) = $_ =~ m[$Template::Liquid::Utility::TagAttributes]g;
52 72         128 { $k => $v };
  72         237  
53 105 50 100     631 } grep { defined && length } split qr[\s+]o, $attr || '';
  83         300  
54             my $s = bless {attributes => \%attr,
55             collection_name => $range,
56             name => $var . '-' . $range,
57             blocks => [],
58             conditional_tag => 'else',
59             reversed => $reversed,
60             tag_name => $args->{'tag_name'},
61             variable_name => $var,
62             end_tag => 'end' . $args->{'tag_name'},
63             template => $args->{'template'},
64             parent => $args->{'parent'},
65 105         1094 markup => $args->{'markup'}
66             }, $class;
67 105         347 return $s;
68             }
69              
70             sub render {
71 107     107 0 210 my ($s) = @_;
72 107         190 my $range = $s->{'collection_name'};
73 107         179 my $attr = $s->{'attributes'};
74 107         159 my $reversed = $s->{'reversed'};
75             my $sorted
76             = exists $attr->{'sorted'}
77             ? $s->{template}{context}->get($attr->{'sorted'}) ||
78 107 100 100     251 $attr->{'sorted'} ||
79             'key'
80             : ();
81 107 50 66     284 $sorted = 'key'
      66        
82             if (defined $sorted && (($sorted ne 'key') && ($sorted ne 'value')));
83             my $offset
84             = defined $attr->{'offset'}
85 107 100       263 ? $s->{template}{context}->get($attr->{'offset'})
86             : ();
87             my $limit
88             = defined $attr->{'limit'}
89 107 100       245 ? $s->{template}{context}->get($attr->{'limit'})
90             : ();
91 107         293 my $list = $s->{template}{context}->get($range);
92 107         203 my $type = 'ARRAY';
93              
94             #warn $list;
95             #
96 107         161 my $_undef_list = 0;
97 107 100       342 if (ref $list eq 'HASH') {
    100          
98 16         55 $list = [map { {key => $_, value => $list->{$_}} } keys %$list];
  46         132  
99             @$list = sort {
100 16 100       70 $a->{$sorted} =~ m[^\d+$]o &&
101             $b->{$sorted} =~ m[^\d+$]o
102             ? ($a->{$sorted} <=> $b->{$sorted})
103 31 100 66     158 : ($a->{$sorted} cmp $b->{$sorted})
104             } @$list if defined $sorted;
105 16         31 $type = 'HASH';
106             }
107             elsif (defined $sorted) {
108             @$list = sort {
109 3 50 33     13 $a =~ m[^\d+$] && $b =~ m[^\d+$] ? ($a <=> $b) : ($a cmp $b)
  24         114  
110             } @$list;
111             }
112 107 100 66     175 if (!eval { \@$list }) {
  107 100 33     763  
    50          
113              
114             # list is a string
115 1         4 $list = [$list];
116             }
117             elsif (ref($list) eq 'ARRAY' && !@$list) {
118              
119             # empty array
120 2         4 $_undef_list = 1;
121 2         6 $list = [1];
122             }
123             elsif (!defined $list || !$list) {
124 0         0 $_undef_list = 1;
125 0         0 $list = [1];
126             }
127             else { # Break it down to only the items we plan on using
128 104 100       244 my $min = (defined $offset ? $offset : 0);
129 104 100       270 my $max
    100          
130             = (defined $limit
131             ? $limit + (defined $offset ? $offset : 0) - 1
132             : $#$list);
133 104 100       223 $max = $#$list if $max > $#$list;
134 104         246 $list = [@{$list}[$min .. $max]]
  104         290  
135             ; # make a copy so we can use the list again
136 104 100       259 @$list = reverse @$list if $reversed;
137 104 100       233 $limit = defined $limit ? $limit : scalar @$list;
138 104 100       220 $offset = defined $offset ? $offset : 0;
139             }
140             return $s->{template}{context}->stack(
141             sub {
142 107     107   203 my $return = '';
143 107         183 my $steps = $#$list;
144 107 100       248 $_undef_list = 1 if $steps == -1;
145 107         216 my $nodes = $s->{'blocks'}[$_undef_list]{'nodelist'};
146 107         339 FOR: for my $index (0 .. $steps) {
147             $s->{template}{context}
148 387         1238 ->set($s->{'variable_name'}, $list->[$index]);
149             $s->{template}{context}->set(
150             'forloop',
151             {length => $steps + 1,
152             limit => $limit,
153             offset => $offset,
154 387 100       3669 name => $s->{'name'},
    100          
155             first => ($index == 0 ? !!1 : !1),
156             last => ($index == $steps ? !!1 : !1),
157             index => $index + 1,
158             index0 => $index,
159             rindex => $steps - $index + 1,
160             rindex0 => $steps - $index,
161             type => $type,
162             sorted => $sorted
163             }
164             );
165 387         1022 for my $node (@$nodes) {
166 1268 100       2746 my $rendering = ref $node ? $node->render() : $node;
167 1268 100       2529 $return .= defined $rendering ? $rendering : '';
168 1268 100       2433 if ($s->{template}{break}) {
169 4         8 $s->{template}{break} = 0;
170 4         7 last FOR;
171             }
172 1264 100       2661 if ($s->{template}{continue}) {
173 19         24 $s->{template}{continue} = 0;
174 19         37 next FOR;
175             }
176             }
177             }
178 107         280 return $return;
179             }
180 107         903 );
181             }
182             1;
183              
184             =pod
185              
186             =encoding UTF-8
187              
188             =begin stopwords
189              
190             Lütke jadedPixel iterable
191              
192             =end stopwords
193              
194             =head1 NAME
195              
196             Template::Liquid::Tag::For - Simple loop construct
197              
198             =head1 Synopsis
199              
200             {% for x in (1..10) %}
201             x = {{ x }}
202             {% endfor %}
203              
204             =head1 Description
205              
206             For loops... uh, loop over collections.
207              
208             =head2 Loop-scope Variables
209              
210             During every for loop, the following helper variables are available for extra
211             styling needs:
212              
213             =over
214              
215             =item * C<forloop.length>
216              
217             length of the entire for loop
218              
219             =item * C<forloop.index>
220              
221             index of the current iteration
222              
223             =item * C<forloop.index0>
224              
225             index of the current iteration (zero based)
226              
227             =item * C<forloop.rindex>
228              
229             how many items are still left?
230              
231             =item * C<forloop.rindex0>
232              
233             how many items are still left? (zero based)
234              
235             =item * C<forloop.first>
236              
237             is this the first iteration?
238              
239             =item * C<forloop.last>
240              
241             is this the last iteration?
242              
243             =item * C<forloop.type>
244              
245             are we looping through an C<ARRAY> or a C<HASH>?
246              
247             =back
248              
249             =head2 Attributes
250              
251             There are several attributes you can use to influence which items you receive
252             in your loop:
253              
254             =over
255              
256             =item C<limit:int>
257              
258             lets you restrict how many items you get.
259              
260             =item C<offset:int>
261              
262             lets you start the collection with the nth item.
263              
264             =back
265              
266             # array = [1,2,3,4,5,6]
267             {% for item in array limit:2 offset:2 %}
268             {{ item }}
269             {% endfor %}
270             # results in 3,4
271              
272             =head3 Reversing the Loop
273              
274             You can reverse the direction the loop works with the C<reversed> attribute. To
275             comply with the Ruby lib's functionality, C<reversed> B<must> be the first
276             attribute.
277              
278             {% for item in collection reversed %} {{item}} {% endfor %}
279              
280             =head3 Sorting
281              
282             You can sort the variable with the C<sorted> attribute. This is an extension
283             beyond the scope of Liquid's syntax and thus incompatible but it's useful.
284              
285             {% for item in collection sorted %} {{item}} {% endfor %}
286              
287             If you are sorting a hash, the values are sorted by keys by default. You may
288             decide to sort by values like so:
289              
290             {% for item in hash sorted:value %} {{item.value}} {% endfor %}
291              
292             ...or make the default obvious with...
293              
294             {% for item in hash sorted:key %} {{item.key}} {% endfor %}
295              
296             =head2 Numeric Ranges
297              
298             Instead of looping over an existing collection, you can define a range of
299             numbers to loop through. The range can be defined by both literal and variable
300             numbers:
301              
302             # if item.quantity is 4...
303             {% for i in (1..item.quantity) %}
304             {{ i }}
305             {% endfor %}
306             # results in 1,2,3,4
307              
308             =head2 Hashes
309              
310             To deal with the possibility of looping through hash references, I have chosen
311             to extend the Liquid Engine's functionality. When looping through a hash, each
312             item is made a single key/value pair. The item's actual key and value are in
313             the C<item.key> and C<item.value> variables. ...here's an example:
314              
315             # where var = {A => 1, B => 2, C => 3}
316             { {% for x in var %}
317             {{ x.key }} => {{ x.value }},
318             {% endfor %} }
319             # results in { A => 1, C => 3, B => 2, }
320              
321             The C<forloop.type> variable will contain C<HASH> if the looped variable is a
322             hashref. Also note that the keys/value pairs are left unsorted.
323              
324             =head2 C<else> tag
325              
326             The else tag allows us to do this:
327              
328             {% for item in collection %}
329             Item {{ forloop.index }}: {{ item.name }}
330             {% else %}
331             There is nothing in the collection.
332             {% endfor %}
333              
334             The C<else> branch is executed whenever the for branch will never be executed
335             (e.g. collection is blank or not an iterable or out of iteration scope).
336              
337             =for basis https://github.com/Shopify/liquid/pull/56
338              
339             =head1 TODO
340              
341             Since this is a customer facing template engine, Liquid should provide some way
342             to limit L<ranges|Template::Liquid::Tag::For/"Numeric Ranges"> and/or depth to
343             avoid (functionally) infinite loops with code like...
344              
345             {% for w in (1..10000000000) %}
346             {% for x in (1..10000000000) %}
347             {% for y in (1..10000000000) %}
348             {% for z in (1..10000000000) %}
349             {{ 'own' | replace:'o','p' }}
350             {%endfor%}
351             {%endfor%}
352             {%endfor%}
353             {%endfor%}
354              
355             =head1 See Also
356              
357             Liquid for Designers: http://wiki.github.com/tobi/liquid/liquid-for-designers
358              
359             L<Template::Liquid|Template::Liquid/"Create your own filters">'s docs on custom
360             filter creation
361              
362             L<Template::Liquid::Tag::Break|Template::Liquid::Tag::Break> and
363             L<Template::Liquid::Tag::Continue|Template::Liquid::Tag::Continue>
364              
365             =head1 Author
366              
367             Sanko Robinson <sanko@cpan.org> - http://sankorobinson.com/
368              
369             CPAN ID: SANKO
370              
371             =head1 License and Legal
372              
373             Copyright (C) 2009-2023 by Sanko Robinson E<lt>sanko@cpan.orgE<gt>
374              
375             This program is free software; you can redistribute it and/or modify it under
376             the terms of L<The Artistic License
377             2.0|http://www.perlfoundation.org/artistic_license_2_0>. See the F<LICENSE>
378             file included with this distribution or L<notes on the Artistic License
379             2.0|http://www.perlfoundation.org/artistic_2_0_notes> for clarification.
380              
381             When separated from the distribution, all original POD documentation is covered
382             by the L<Creative Commons Attribution-Share Alike 3.0
383             License|http://creativecommons.org/licenses/by-sa/3.0/us/legalcode>. See the
384             L<clarification of the
385             CCA-SA3.0|http://creativecommons.org/licenses/by-sa/3.0/us/>.
386              
387             =cut