File Coverage

blib/lib/WWW/Shopify/Liquid/Parser.pm
Criterion Covered Total %
statement 208 211 98.5
branch 91 102 89.2
condition 36 51 70.5
subroutine 29 29 100.0
pod 0 15 0.0
total 364 408 89.2


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3 21     21   108 use strict;
  21         31  
  21         815  
4 21     21   102 use warnings;
  21         31  
  21         832  
5              
6             package WWW::Shopify::Liquid::Parser;
7 21     21   111 use base 'WWW::Shopify::Liquid::Pipeline';
  21         35  
  21         7892  
8 21     21   129 use Module::Find;
  21         65  
  21         1438  
9 21     21   115 use List::MoreUtils qw(firstidx part);
  21         35  
  21         1169  
10 21     21   113 use List::Util qw(first);
  21         412  
  21         1301  
11 21     21   10184 use WWW::Shopify::Liquid::Exception;
  21         61  
  21         16598  
12              
13             useall WWW::Shopify::Liquid::Operator;
14             useall WWW::Shopify::Liquid::Tag;
15             useall WWW::Shopify::Liquid::Filter;
16              
17 23     23 0 330 sub new { return bless {
18             order_of_operations => [],
19             operators => {},
20             enclosing_tags => {},
21             free_tags => {},
22             filters => {},
23             inner_tags => {}
24             }, $_[0]; }
25 601     601 0 2036 sub operators { return $_[0]->{operators}; }
26 247     247 0 229 sub order_of_operations { return @{$_[0]->{order_of_operations}}; }
  247         926  
27 178     178 0 805 sub free_tags { return $_[0]->{free_tags}; }
28 856     856 0 3227 sub enclosing_tags { return $_[0]->{enclosing_tags}; }
29 141     141 0 533 sub inner_tags { return $_[0]->{inner_tags}; }
30 1311     1311 0 6575 sub filters { return $_[0]->{filters}; }
31              
32             sub register_tag {
33 322 100   322 0 1735 $_[0]->free_tags->{$_[1]->name} = $_[1] if $_[1]->is_free;
34 322 100       792 if ($_[1]->is_enclosing) {
35 230         439 $_[0]->enclosing_tags->{$_[1]->name} = $_[1];
36 230         1380 foreach my $tag ($_[1]->inner_tags) {
37 138         259 $_[0]->inner_tags->{$tag} = 1;
38             }
39             }
40             }
41              
42              
43             sub register_operator {
44 460     460 0 1031 $_[0]->operators->{$_} = $_[1] for($_[1]->symbol);
45 460         669 my $ooo = $_[0]->{order_of_operations};
46 460     1771   1872 my $element = first { $_->[0]->priority == $_[1]->priority } @$ooo;
  1771         4179  
47 460 100       1271 if ($element) {
48 230         403 push(@$element, $_[1]);
49             }
50             else {
51 230         495 push(@$ooo, [$_[1]]);
52             }
53 460         1085 $_[0]->{order_of_operations} = [sort { $b->[0]->priority <=> $a->[0]->priority } @$ooo];
  4370         8837  
54             }
55             sub register_filter {
56 1311     1311 0 1964 $_[0]->filters->{$_[1]->name} = $_[1];
57             }
58              
59              
60              
61             sub parse_filter_tokens {
62 29     29 0 75 my ($self, $initial, @tokens) = @_;
63 29         52 my $filter = shift(@tokens);
64 29         136 my $filter_name = $filter->{core}->[0]->{core};
65             # TODO God, this is stupid, but temporary patch.
66 29         42 my $filter_package;
67 29 100       115 if ($filter_name =~ m/::/) {
68 1         2 $filter_package = $filter_name;
69 1         3 eval { $filter_package->name };
  1         7  
70 1 50       4 die new WWW::Shopify::Liquid::Exception::Parser::UnknownFilter($filter) if $@;
71             } else {
72 28 50       126 die new WWW::Shopify::Liquid::Exception::Parser::UnknownFilter($filter) unless $self->{filters}->{$filter_name};
73 28         69 $filter_package = $self->{filters}->{$filter_name};
74             }
75 29 50 66     189 die new WWW::Shopify::Liquid::Exception::Parser::Arguments($filter, "In order to have arguments, filter must be followed by a colon.") if int(@tokens) > 0 && $tokens[0]->{core} ne ":";
76            
77 29         56 my @arguments = ();
78             # Get rid of our colon.
79 29 100       85 if (shift(@tokens)) {
80 16         30 my $i = 0;
81 16 100 66 18   94 @arguments = map { $self->parse_argument_tokens(grep { !$_->isa('WWW::Shopify::Liquid::Token::Separator') } @{$_}) } part { $i++ if $_->isa('WWW::Shopify::Liquid::Token::Separator') && $_->{core} eq ","; $i; } @tokens;
  17         31  
  18         133  
  17         50  
  18         136  
  18         51  
82             }
83 29         452 $filter = $filter_package->new($initial->{line}, $initial, @arguments);
84 29         183 $filter->verify;
85 28         82 return $filter;
86             }
87 21     21   144 use List::MoreUtils qw(part);
  21         34  
  21         43904  
88              
89             # Similar, but doesn't deal with tags; deals solely with order of operations.
90             sub parse_argument_tokens {
91 271     271 0 533 my ($self, @argument_tokens) = @_;
92            
93             # Preprocess all variant filters.
94 271         420 for my $variable (grep { $_->isa('WWW::Shopify::Liquid::Token::Variable') } @argument_tokens) {
  579         2491  
95 283         269 my @core = @{$variable->{core}};
  283         724  
96 283         509 ($variable->{core}->[$_]) = $self->parse_argument_tokens($core[$_]->members) for (grep { $core[$_]->isa('WWW::Shopify::Liquid::Token::Grouping') } 0..$#core);
  398         2596  
97             }
98            
99             # First, pull together filters. These are the highest priority operators, after parentheses. They also have their own weird syntax.
100 271         370 my $top = undef;
101            
102             # Don't partition if we have any pipes. Pipes and multiple arguments don't play well together.
103 271         280 my @partitions;
104 271         283 my $has_pipe = 0;
105 271 100       404 if (int(grep { $_->isa('WWW::Shopify::Liquid::Token::Operator') && $_->{core} eq "|" } @argument_tokens) == 0) {
  579 100       3458  
106 246         282 my $i = 0;
107 246         269 $has_pipe = 1;
108 246 50   442   1368 @partitions = part { $i++ if $_->isa('WWW::Shopify::Liquid::Token::Separator'); $i; } @argument_tokens;
  442         2103  
  442         866  
109             } else {
110 25         79 @partitions = (\@argument_tokens);
111             }
112            
113 271         676 my @tops;
114            
115            
116 271         421 foreach my $partition (@partitions) {
117 247         427 my @tokens = @$partition;
118             #@tokens = (grep { !$_->isa('WWW::Shopify::Liquid::Token::Separator') } @tokens) if !$has_pipe;
119            
120             # Use the order of operations to create a binary tree structure.
121 247         559 foreach my $operators ($self->order_of_operations) {
122             # If we have pipes, we deal with those, and parse their lower level arguments.
123 2457         3395 foreach my $operator (@$operators) {
124 4915         16851 my %ops = map { $_ => 1 } $operator->symbol;
  5160         11271  
125 4915 100       8356 if ($operator eq 'WWW::Shopify::Liquid::Operator::Pipe') {
126 246 100   428   880 if ((my $idx = firstidx { $_->isa('WWW::Shopify::Liquid::Token::Operator') && $_->{core} eq "|" } @tokens) != -1) {
  428 100       3228  
127 25 50       73 die new WWW::Shopify::Liquid::Exception::Parser($tokens[0]) if $idx == 0;
128 25         39 my $i = 0;
129             # Part should consist of the first token before a pipe, and then split on all pipes after this.,
130 25 100 66 117   247 my @parts = map { shift(@{$_}) if $_->[0]->{core} eq "|"; $_ } part { $i++ if $_->isa('WWW::Shopify::Liquid::Token::Operator') && $_->{core} eq "|"; $i; } splice(@tokens, $idx-1);
  54 100       191  
  29         51  
  54         120  
  117         656  
  117         196  
131 25         133 my $next = undef;
132 25         47 $top = $self->parse_filter_tokens($self->parse_argument_tokens(@{shift(@parts)}), @{shift(@parts)});
  25         126  
  25         107  
133 24         105 while (my $part = shift(@parts)) {
134 4         16 $top = $self->parse_filter_tokens($top, @$part);
135             }
136 24         146 push(@tokens, $top);
137             }
138             }
139             else {
140 4669 100   7921   14924 while ((my $idx = firstidx { $_->isa('WWW::Shopify::Liquid::Token::Operator') && exists $ops{$_->{core}} } @tokens) != -1) {
  7921         53286  
141 119         416 my ($op1, $op, $op2) = @tokens[$idx-1..$idx+1];
142             # The one exception would be if we have a - operator, and nothing before, this is unary negative operator, i.e. 0 - number.
143 119 100 33     2724 die new WWW::Shopify::Liquid::Exception::Parser::Operands($tokens[0]) unless
      33        
      33        
      100        
      33        
144             $idx > 0 && $idx < $#tokens &&
145             ($op1->isa('WWW::Shopify::Liquid::Operator') || $op1->isa('WWW::Shopify::Liquid::Token::Operand') || $op1->isa('WWW::Shopify::Liquid::Filter')) &&
146             ($op2->isa('WWW::Shopify::Liquid::Operator') || $op2->isa('WWW::Shopify::Liquid::Token::Operand') || $op2->isa('WWW::Shopify::Liquid::Filter'));
147 118 50       667 ($op1) = $self->parse_argument_tokens($op1->members) if $op1->isa('WWW::Shopify::Liquid::Token::Grouping');
148 118 100       580 ($op2) = $self->parse_argument_tokens($op2->members) if $op2->isa('WWW::Shopify::Liquid::Token::Grouping');
149 118         484 splice(@tokens, $idx-1, 3, $self->operators->{$op->{core}}->new($op->{core}, $op1, $op2));
150             }
151             }
152             }
153             }
154            
155 245 100 66     856 die new WWW::Shopify::Liquid::Exception::Parser::Operands($tokens[0]) unless int(@tokens) == 1 || int(@tokens) == 0;
156 244         639 push(@tops, $tokens[0]);
157             }
158            
159 268         1840 return @tops;
160             }
161              
162             sub parse_tokens {
163 203     203 0 1044 my ($self, @tokens) = @_;
164            
165 203 100       511 return () if int(@tokens) == 0;
166            
167 189         297 my @tags = ();
168             # First we take a look and start matching up opening and ending tags. Those which are free tags we can leave as is.
169 189         724 while (my $token = shift(@tokens)) {
170 367         572 my $line = $token->{line};
171 367 100       2517 if ($token->isa('WWW::Shopify::Liquid::Token::Tag')) {
    100          
172 124         150 my $tag = undef;
173 124 100       339 if ($self->enclosing_tags->{$token->tag}) {
    100          
174 79         145 my @internal = ();
175 79         133 my @contents = ();
176 79         173 my %allowed_internal_tags = map { $_ => 1 } $self->enclosing_tags->{$token->tag}->inner_tags;
  104         343  
177 79         165 my $level = 1;
178 79         119 my $closed = undef;
179 79         237 for (0..$#tokens) {
180 478 100       2028 if ($tokens[$_]->isa('WWW::Shopify::Liquid::Token::Tag')) {
181 240 100 100     448 if ($self->enclosing_tags->{$tokens[$_]->tag}) {
    100 100        
    100          
    100          
182 42         75 ++$level;
183             } elsif (exists $allowed_internal_tags{$tokens[$_]->tag} && $level == 1) {
184 29         46 $tokens[$_]->{arguments} = [$self->parse_argument_tokens(@{$tokens[$_]->{arguments}})];
  29         107  
185 29         81 push(@internal, $_);
186             } elsif ($tokens[$_]->tag eq "end" . $token->tag && $level == 1) {
187 77         98 --$level;
188 77         102 my $last_int = 0;
189 77         154 foreach my $int (@internal, $_) {
190 106         361 push(@contents, [splice(@tokens, 0, $int-$last_int)]);
191 106 100 66     295 shift(@{$contents[0]}) if $self->enclosing_tags->{$token->tag}->inner_ignore_whitespace && int(@contents) > 0 && int(@{$contents[0]}) > 0 && $contents[0]->[0]->isa('WWW::Shopify::Liquid::Token::Text::Whitespace');
  1   100     3  
  11   66     60  
192 143         280 @contents = map {
193 106         217 my @array = @$_;
194 143 100 100     1278 if (int(@array) > 0 && $array[0]->isa('WWW::Shopify::Liquid::Token::Tag') && $allowed_internal_tags{$array[0]->tag}) {
      100        
195 37         136 [$array[0], $self->parse_tokens(@array[1..$#array])];
196             }
197             else {
198 106         529 [$self->parse_tokens(@array)]
199             }
200             } @contents;
201 105         244 $last_int = $int;
202             }
203             # Remove the endtag.
204 76         109 shift(@tokens);
205 76         108 $closed = 1;
206 76         151 last;
207             } elsif ($tokens[$_]->tag =~ m/^end/) {
208 42         73 --$level;
209             # TODO: Fix this whole thing; right now, no close tags are being spit out for the wrong tag. We do this to avoid an {% unless %}{% if %}{% else %}{% endif %}{% endunless%} situtation.
210             }
211             }
212             }
213 78 100       257 die new WWW::Shopify::Liquid::Exception::Parser::NoClose($token) unless $closed;
214 76         208 $tag = $self->enclosing_tags->{$token->tag}->new($line, $token->tag, [$self->parse_argument_tokens(@{$token->{arguments}})], \@contents);
  76         238  
215 76         484 $tag->verify;
216             }
217             elsif ($self->free_tags->{$token->tag}) {
218 41         97 $tag = $self->free_tags->{$token->tag}->new($line, $token->tag, [$self->parse_argument_tokens(@{$token->{arguments}})]);
  41         151  
219 41         247 $tag->verify;
220             }
221             else {
222 4 100 66     14 die new WWW::Shopify::Liquid::Exception::Parser::NoOpen($token) if ($token->tag =~ m/^end(\w+)$/ && $self->enclosing_tags->{$1});
223 3 100       20 die new WWW::Shopify::Liquid::Exception::Parser::NakedInnerTag($token) if (exists $self->inner_tags->{$token->tag});
224 2         38 die new WWW::Shopify::Liquid::Exception::Parser::UnknownTag($token);
225             }
226 116         503 push(@tags, $tag);
227             }
228             elsif ($token->isa('WWW::Shopify::Liquid::Token::Output')) {
229 66         115 push(@tags, WWW::Shopify::Liquid::Tag::Output->new($line, [$self->parse_argument_tokens(@{$token->{core}})]));
  66         243  
230             }
231             else {
232 177         594 push(@tags, $token);
233             }
234             }
235            
236 178         237 my $top = undef;
237 178 100       382 if (int(@tags) > 1) {
238 61         249 $top = WWW::Shopify::Liquid::Operator::Concatenate->new;
239 61         281 $top->{operands}->[0] = shift(@tags);
240 61         90 my $next = $top;
241 61         220 while (my $tag = shift(@tags)) {
242 167 100       324 if (int(@tags) == 0) {
243 61         208 $next->{operands}->[1] = $tag;
244             }
245             else {
246 106         299 $next->{operands}->[1] = WWW::Shopify::Liquid::Operator::Concatenate->new;
247 106         163 $next = $next->{operands}->[1];
248 106         473 $next->{operands}->[0] = $tag;
249             }
250             }
251             }
252             else {
253 117         181 ($top) = @tags;
254             }
255 178         831 return $top;
256             }
257              
258             sub unparse_argument_tokens {
259 11     11 0 8 my ($self, $ast) = @_;
260 11 100       46 return $ast if $ast->isa('WWW::Shopify::Liquid::Token');
261 2         8 my @optokens = ($self->unparse_argument_tokens($ast->{operands}->[0]), WWW::Shopify::Liquid::Token::Operator->new([0,0,0], $ast->{core}), $self->unparse_argument_tokens($ast->{operands}->[1]));
262 2 100       18 return WWW::Shopify::Liquid::Token::Grouping->new([0,0,0], @optokens) if $ast->requires_grouping;
263 1         5 return @optokens;
264             }
265              
266             sub unparse_tokens {
267 14     14 0 667 my ($self, $ast) = @_;
268 14 100       65 return $ast if $ast->isa('WWW::Shopify::Liquid::Token');
269 9 100       25 if ($ast->isa('WWW::Shopify::Liquid::Tag')) {
270 5 50       10 my @arguments = $ast->{arguments} ? $self->unparse_argument_tokens(@{$ast->{arguments}}) : ();
  5         9  
271 5 100       25 if ($ast->isa('WWW::Shopify::Liquid::Tag::Enclosing')) {
    50          
272 3 100       12 if ($ast->isa('WWW::Shopify::Liquid::Tag::If')) {
273 2 50       5 return (WWW::Shopify::Liquid::Token::Tag->new([0,0,0], 'if', \@arguments), $self->unparse_tokens($ast->{true_path}), WWW::Shopify::Liquid::Token::Tag->new([0,0,0], 'endif')) if !$ast->{false_path};
274 2         7 return (WWW::Shopify::Liquid::Token::Tag->new([0,0,0], 'if', \@arguments), $self->unparse_tokens($ast->{true_path}), WWW::Shopify::Liquid::Token::Tag->new([0,0,0], 'else'), $self->unparse_tokens($ast->{false_path}), WWW::Shopify::Liquid::Token::Tag->new([0,0,0], 'endif'));
275             }
276             else {
277 1         14 return (WWW::Shopify::Liquid::Token::Tag->new([0,0,0], $ast->{core}, \@arguments), $self->unparse_tokens($ast->{contents}), WWW::Shopify::Liquid::Token::Tag->new([0,0,0], 'end' . $ast->{core}));
278             }
279             }
280             elsif ($ast->isa('WWW::Shopify::Liquid::Tag::Output')) {
281 2         4 return (WWW::Shopify::Liquid::Token::Output->new([0,0,0], [$self->unparse_argument_tokens(@{$ast->{arguments}})]));
  2         5  
282             }
283             else {
284 0         0 return (WWW::Shopify::Liquid::Token::Tag->new([0,0,0], $ast->{core}, \@arguments));
285             }
286 0         0 return $ast;
287             }
288 4 50       17 if ($ast->isa('WWW::Shopify::Liquid::Filter')) {
289 0         0 return $ast;
290             }
291 4 50       19 return ($self->unparse_tokens($ast->{operands}->[0]), $self->unparse_tokens($ast->{operands}->[1])) if ($ast->isa('WWW::Shopify::Liquid::Operator::Concatenate'));
292            
293             }
294              
295             1;