File Coverage

blib/lib/WWW/Shopify/Liquid/Tag/For.pm
Criterion Covered Total %
statement 123 135 91.1
branch 54 80 67.5
condition 37 75 49.3
subroutine 15 17 88.2
pod 0 11 0.0
total 229 318 72.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2 37     37   15893 use strict;
  37         108  
  37         1228  
3 37     37   240 use warnings;
  37         101  
  37         1476  
4              
5              
6             # The special-ist tag in the world.
7             package WWW::Shopify::Liquid::Tag::For;
8 37     37   244 use base 'WWW::Shopify::Liquid::Tag::Enclosing';
  37         95  
  37         10939  
9              
10 0     0 0 0 sub min_arguments { return 1; }
11 0     0 0 0 sub max_arguments { return 1; }
12              
13             sub verify {
14 94     94 0 258 my ($self) = @_;
15             die new WWW::Shopify::Liquid::Exception::Parser::Arguments($self, "Requires in operator to be part of loop.") unless
16 94 100       522 $self->{arguments}->[0]->isa('WWW::Shopify::Liquid::Operator::In');
17             die new WWW::Shopify::Liquid::Exception::Parser::Arguments($self, "Requires the opening variable of a loop to be a simple variable.") unless
18             $self->{arguments}->[0]->{operands}->[0] && $self->{arguments}->[0]->{operands}->[0]->isa('WWW::Shopify::Liquid::Token::Variable') &&
19 93 50 33     838 int(@{$self->{arguments}->[0]->{operands}->[0]->{core}}) == 1 && $self->{arguments}->[0]->{operands}->[0]->{core}->[0]->isa('WWW::Shopify::Liquid::Token::String');
  93   33     992  
      33        
20             }
21 179     179 0 689 sub inner_tags { return qw(else) }
22              
23 37     37   289 use List::Util qw(min);
  37         105  
  37         2802  
24 37     37   287 use Scalar::Util qw(looks_like_number blessed reftype);
  37         113  
  37         2374  
25 37     37   11102 use Clone qw(clone);
  37         85721  
  37         58198  
26              
27              
28              
29             sub new {
30 94     94 0 291 my $package = shift;
31 94         706 my $self = bless {
32             line => shift,
33             core => shift,
34             arguments => shift,
35             contents => undef,
36             false_path => undef
37             }, $package;
38 94         239 $self->interpret_inner_tokens(@{$_[0]});
  94         501  
39 94         284 return $self;
40             }
41              
42             sub interpret_inner_tokens {
43 94     94 0 305 my ($self, @tokens) = @_;
44             # Comes in [for_path], [tag, other_path]...
45 94         220 my $token = shift(@tokens);
46 94 50       324 return undef unless $token;
47 94         338 $self->{contents} = $token->[0];
48 94 100       397 if (int(@tokens) > 0) {
49 3 50 33     22 die new WWW::Shopify::Liquid::Exception::Parser($self, "else cannot be anywhere, except the end tag of the for statement.") if int(@tokens) > 1 || $tokens[0]->[0]->tag ne "else";
50 3         8 $self->{false_path} = $tokens[0]->[1];
51             }
52             }
53              
54             sub render_loop {
55 47     47 0 282 my ($self, $renderer, $hash, $op1, $op2, $start, $end, @array) = @_;
56            
57 47         116 my @texts = ();
58 47         98 my $all_processed = 1;
59 47         146 my $var = $op1->{core}->[0]->{core};
60 47         98 my $content;
61            
62 47 100 100     150 if ($renderer->state && exists $renderer->state->{values}->{$self . "-1"}) {
63 3         13 $start = $renderer->state->{values}->{$self . "-1"}->{index};
64 3         8 @texts = @{$renderer->state->{values}->{$self . "-1"}->{texts}};
  3         12  
65 3         12 delete $renderer->state->{values}->{$self . "-1"};
66              
67             }
68 47         171 for ($start..$end) {
69 435         1242 $hash->{$var} = $array[$_];
70             $hash->{forloop} = {
71 435         3821 index => ($_+1), index0 => $_, first => $_ == 0, last => $_ == $#array,
72             length => int(@array), rindex0 => (($#array - $_) + 1), rindex => (($#array - $_)),
73             };
74 435         1120 eval {
75 435         1774 $content = $self->{contents}->render($renderer, $hash);
76             };
77 435 100       1391 if (my $exp = $@) {
78 7 50 33     65 if (defined $exp && blessed($exp) && $exp->isa('WWW::Shopify::Liquid::Exception::Control')) {
      33        
79 7 100       28 if ($exp->isa('WWW::Shopify::Liquid::Exception::Control::Pause')) {
80 3         27 $exp->register_value($self . "-1", {
81             index => $_,
82             texts => \@texts
83             });
84 3         34 die $exp;
85             } else {
86 4 100 66     22 push(@texts, @{$exp->initial_render}) if $exp->initial_render && int(@{$exp->initial_render}) > 0;
  2         7  
  2         6  
87 4 100       18 if ($exp->isa('WWW::Shopify::Liquid::Exception::Control::Break')) {
    50          
88 2         4 last;
89             } elsif ($exp->isa('WWW::Shopify::Liquid::Exception::Control::Continue')) {
90 2         9 next;
91             }
92             }
93             } else {
94 0         0 die $exp;
95             }
96             }
97 428 50       1283 $all_processed = 0 if !$self->is_processed($content);
98 428         1402 push(@texts, $content);
99             }
100            
101 44 50       169 return join('', grep { defined $_ } @texts) if $all_processed;
  430         1343  
102 0         0 return $self;
103             }
104              
105             sub expand_concatenations {
106 71     71 0 149 my ($self, $result) = @_;
107 71 100 100     430 return blessed($result) && $result->isa('WWW::Shopify::Liquid::Operator::Concatenate') ? (map { $self->expand_concatenations($_) } @{$result->{operands}}) : ($result);
  22         54  
  10         26  
108             }
109              
110             sub optimize_loop {
111 7     7 0 42 my ($self, $optimizer, $hash, $op1, $op2, $start, $end, @array) = @_;
112            
113            
114             # First step, we replace everything by a big concatenate.
115 7         19 my @texts = ();
116            
117 7         25 my $var = $op1->{core}->[0]->{core};
118 7 50       28 return '' if $start > $end;
119            
120             my @parts = map {
121 7         24 $hash->{$var} = $array[$_];
  49         118  
122             $hash->{forloop} = {
123 49         328 index => ($_+1), index0 => $_, first => $_ == 0, last => $_ == $#array,
124             length => int(@array), rindex0 => (($#array - $_) + 1), rindex => (($#array - $_)),
125             };
126 49         105 my $result;
127 49         80 eval {
128 49 50       151 $result = $self->is_processed($self->{contents}) ? $self->{contents} : clone($self->{contents})->optimize($optimizer, $hash);
129             };
130 49 50       190 if (my $exp = $@) {
131 0 0 0     0 if (defined $exp && blessed($exp) && $exp->isa('WWW::Shopify::Liquid::Exception::Control')) {
      0        
132 0 0 0     0 push(@texts, @{$exp->initial_render}) if $exp->initial_render && int(@{$exp->initial_render}) > 0;
  0         0  
  0         0  
133 0 0       0 if ($exp->isa('WWW::Shopify::Liquid::Exception::Control::Break')) {
    0          
134 0         0 last;
135             } elsif ($exp->isa('WWW::Shopify::Liquid::Exception::Control::Continue')) {
136 0         0 next;
137             }
138             } else {
139 0         0 die $exp;
140             }
141             }
142            
143 49         140 $self->expand_concatenations($result);
144             } ($start..$end);
145            
146 7 50       31 return $parts[0] if int(@parts) == 1;
147 7         41 return WWW::Shopify::Liquid::Operator::Concatenate->new($self->{line}, "", @parts);
148             }
149              
150             # Called to expand the second operand in "in" argument list.
151             sub apply {
152 66     66 0 189 my ($self, $hash, $operand) = @_;
153 66 100       217 return [keys(%$operand)] if ref($operand) eq 'HASH';
154 65         166 return $operand;
155             }
156              
157             # Should eventually support loop unrolling.
158             sub process {
159 67     67 0 233 my ($self, $hash, $action, $pipeline) = @_;
160 67         134 my @args = @{$self->{arguments}};
  67         212  
161            
162            
163 67         184 my ($op1, $op2) = @{$args[0]->{operands}};
  67         213  
164 67 50       297 $op2 = $op2->$action($pipeline, $hash) if !$self->is_processed($op2);
165            
166 66 100 100     237 $self->{arguments}->[0]->{operands}->[1] = $op2 if $self->is_processed($op2) && $action eq 'optimize';
167 66 100 66     298 if ($action eq "optimize" && !$self->is_processed($self->{contents})) {
168             # Ensure that the loop variable is unset for at least the duration of the loop while we do an initial optimization.
169 13         48 my $var = $op1->{core}->[0]->{core};
170 13         37 delete $hash->{$var};
171             # And, in fact, make sure that ALL variables that get set in the loop get unset here.
172 13         102 for (grep { ref($_) eq 'WWW::Shopify::Liquid::Tag::Assign' } $self->{contents}->tokens) {
  115         242  
173 1         23 next if int(@{$_->{arguments}->[0]->{operands}->[0]->{core}}) == 1 &&
174             blessed($_->{arguments}->[0]->{operands}->[0]->{core}->[0]->{core}) &&
175 1 50 33     2 $_->{arguments}->[0]->{operands}->[0]->{core}->[0]->{core}->isa('WWW::Shopify::Liquid::Token::String');
      33        
176 1         5 delete $hash->{$_->{arguments}->[0]->{operands}->[0]->{core}->[0]->{core}};
177             }
178 13         72 $pipeline->push_conditional_state;
179 13         64 $self->{contents} = $self->{contents}->optimize($pipeline, $hash);
180 13         58 $pipeline->pop_conditional_state;
181             }
182 66         284 $op2 = $self->apply($hash, $op2);
183 66 100 66     202 return $self if (!$self->is_processed($op2) && $action eq "optimize");
184 61 50 33     208 return '' if (!$self->is_processed($op2) && $action eq "render");
185 61 100 100     386 return (defined $self->{false_path} ? $self->{false_path}->$action($pipeline, $hash) : '') if ref($op2) ne "ARRAY" || int(@$op2) == 0;
    100          
186 54 50       195 die new WWW::Shopify::Liquid::Exception::Renderer::Arguments($self, "Requires an array in for loop.") unless ref($op2) eq "ARRAY";
187            
188 54         187 my @array = @$op2;
189 54         129 my $limit = int(@array);
190 54         130 my $offset = 0;
191            
192            
193 54 100       126 my ($limit_arg) = grep { $_->isa('WWW::Shopify::Liquid::Token::Variable::Named') && $_->{name} eq "limit" } @args;
  56         378  
194 54 100       173 my $limit_result = $limit_arg->$action($pipeline, $hash) if $limit_arg;
195 54 50       178 return $self if !$self->is_processed($limit_result);
196 54 50 66     208 $limit = $limit_result->{limit} if $limit_result && ref($limit_result) && ref($limit_result) eq 'HASH' && looks_like_number($limit_result->{limit});
      66        
      33        
197            
198            
199 54 100       124 my ($offset_arg) = grep { $_->isa('WWW::Shopify::Liquid::Token::Variable::Named') && $_->{name} eq "offset" } @args;
  56         304  
200 54 100       211 my $offset_result = $offset_arg->$action($pipeline, $hash) if $offset_arg;
201 54 50 66     183 $offset = $offset_result->{offset} if $offset_result && ref($offset_result) && ref($offset_result) eq 'HASH' && looks_like_number($offset_result->{offset});
      66        
      33        
202            
203 54         158 my $dispatch = $action . "_loop";
204            
205 54         517 return $self->$dispatch($pipeline, $hash, $op1, $op2, $offset, min($#array, $limit+$offset-1), @array);
206             }
207              
208              
209              
210             1;