File Coverage

blib/lib/Template/Liquid/Tag/For.pm
Criterion Covered Total %
statement 72 73 98.6
branch 53 60 88.3
condition 15 21 71.4
subroutine 7 7 100.0
pod 0 2 0.0
total 147 163 90.1


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