File Coverage

blib/lib/WWW/Shopify/Liquid/Parser.pm
Criterion Covered Total %
statement 24 239 10.0
branch 0 122 0.0
condition 0 60 0.0
subroutine 8 32 25.0
pod n/a
total 32 453 7.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3 30     30   151 use strict;
  30         48  
  30         906  
4 30     30   129 use warnings;
  30         43  
  30         1235  
5              
6             package WWW::Shopify::Liquid::Parser;
7 30     30   132 use base 'WWW::Shopify::Liquid::Pipeline';
  30         50  
  30         11253  
8 30     30   177 use Module::Find;
  30         48  
  30         2026  
9 30     30   191 use List::MoreUtils qw(firstidx part);
  30         51  
  30         212  
10 30     30   11872 use List::Util qw(first);
  30         49  
  30         2600  
11 30     30   13946 use WWW::Shopify::Liquid::Exception;
  30         92  
  30         26421  
12              
13             useall WWW::Shopify::Liquid::Operator;
14             useall WWW::Shopify::Liquid::Tag;
15             useall WWW::Shopify::Liquid::Filter;
16              
17 0     0     sub new { return bless {
18             order_of_operations => [],
19             operators => {},
20             enclosing_tags => {},
21             free_tags => {},
22             filters => {},
23             inner_tags => {},
24             security => WWW::Shopify::Liquid::Security->new,
25             accept_unknown_filters => 0
26             }, $_[0]; }
27 0 0   0     sub accept_unknown_filters { $_[0]->{accept_unknown_filters} = $_[1] if defined $_[1]; return $_[0]->{accept_unknown_filters}; }
  0            
28 0     0     sub operator { return $_[0]->{operators}->{$_[1]}; }
29 0     0     sub operators { return $_[0]->{operators}; }
30 0     0     sub order_of_operations { return @{$_[0]->{order_of_operations}}; }
  0            
31 0     0     sub free_tags { return $_[0]->{free_tags}; }
32 0     0     sub enclosing_tags { return $_[0]->{enclosing_tags}; }
33 0     0     sub inner_tags { return $_[0]->{inner_tags}; }
34 0     0     sub filters { return $_[0]->{filters}; }
35              
36             sub register_tag {
37 0 0   0     $_[0]->free_tags->{$_[1]->name} = $_[1] if $_[1]->is_free;
38 0 0         if ($_[1]->is_enclosing) {
39 0           $_[0]->enclosing_tags->{$_[1]->name} = $_[1];
40 0           foreach my $tag ($_[1]->inner_tags) {
41 0           $_[0]->inner_tags->{$tag} = 1;
42             }
43             }
44             }
45              
46              
47             sub register_operator {
48 0     0     $_[0]->SUPER::register_operator($_[1]);
49 0           $_[0]->operators->{$_} = $_[1] for($_[1]->symbol);
50 0           my $ooo = $_[0]->{order_of_operations};
51 0     0     my $element = first { $_->[0]->priority == $_[1]->priority } @$ooo;
  0            
52 0 0         if ($element) {
53 0           push(@$element, $_[1]);
54             }
55             else {
56 0           push(@$ooo, [$_[1]]);
57             }
58 0           $_[0]->{order_of_operations} = [sort { $b->[0]->priority <=> $a->[0]->priority } @$ooo];
  0            
59             }
60             sub register_filter {
61 0     0     $_[0]->filters->{$_[1]->name} = $_[1];
62             }
63              
64              
65              
66             sub parse_filter_tokens {
67 0     0     my ($self, $initial, @tokens) = @_;
68 0           my $filter = shift(@tokens);
69 0           my $filter_name = $filter->{core}->[0]->{core};
70             # TODO God, this is stupid, but temporary patch.
71 0           my $filter_package;
72 0 0         if ($filter_name =~ m/::/) {
73 0           $filter_package = $filter_name;
74 0           eval { $filter_package->name };
  0            
75 0 0         if ($@) {
76 0 0         die new WWW::Shopify::Liquid::Exception::Parser::UnknownFilter($filter) if !$self->accept_unknown_filters;
77 0           $filter_package = 'WWW::Shopify::Liquid::Filter::Unknown';
78             }
79             } else {
80 0 0         if (!$self->{filters}->{$filter_name}) {
81 0 0         die new WWW::Shopify::Liquid::Exception::Parser::UnknownFilter($filter) if !$self->accept_unknown_filters;
82 0           $filter_package = 'WWW::Shopify::Liquid::Filter::Unknown';
83             } else {
84 0           $filter_package = $self->{filters}->{$filter_name};
85             }
86             }
87 0 0 0       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 ":";
88            
89 0           my @arguments = ();
90             # Get rid of our colon.
91 0 0         if (shift(@tokens)) {
92 0           my $i = 0;
93 0 0 0 0     @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;
  0            
  0            
  0            
  0            
  0            
94             }
95 0           $filter = $filter_package->new($initial->{line}, $filter_name, $initial, @arguments);
96 0           $filter->verify($self);
97 0           return $filter;
98             }
99 30     30   195 use List::MoreUtils qw(part);
  30         61  
  30         251  
100              
101             # Similar, but doesn't deal with tags; deals solely with order of operations.
102             sub parse_argument_tokens {
103 0     0     my ($self, @argument_tokens) = @_;
104            
105             # Process all groupings.
106 0           ($argument_tokens[$_]) = $self->parse_argument_tokens($argument_tokens[$_]->members) for (grep { $argument_tokens[$_]->isa('WWW::Shopify::Liquid::Token::Grouping') } 0..$#argument_tokens);
  0            
107            
108            
109            
110             # Process all groupings inside named variables.
111 0 0         ($_->{core}) = $self->parse_argument_tokens($_->{core}->members) for (grep { $_->isa('WWW::Shopify::Liquid::Token::Variable::Named') && $_->{core}->isa('WWW::Shopify::Liquid::Token::Grouping') } @argument_tokens);
  0            
112             # Preprocess all variant filters.
113 0           for my $variable (grep { $_->isa('WWW::Shopify::Liquid::Token::Variable') } @argument_tokens) {
  0            
114 0           my @core = @{$variable->{core}};
  0            
115 0           ($variable->{core}->[$_]) = $self->parse_argument_tokens($core[$_]->members) for (grep { $core[$_]->isa('WWW::Shopify::Liquid::Token::Grouping') } 0..$#core);
  0            
116             }
117            
118            
119            
120             # Process unary operators first; these have highest priority, regardless of what the priority field says.
121 0 0 0 0     while ((my $idx = firstidx { $_->isa('WWW::Shopify::Liquid::Token::Operator') && defined $_->{core} && $self->operator($_->{core}) && $self->operator($_->{core})->arity eq "unary" } @argument_tokens) != -1) {
  0   0        
122 0           my $op = $argument_tokens[$idx];
123 0           my $fixness = $self->operator($argument_tokens[$idx]->{core})->fixness;
124 0 0         my $op1 = $fixness eq "postfix" ? $argument_tokens[$idx-1] : $argument_tokens[$idx+1];
125 0 0         my $start = $fixness eq "potsfix" ? $idx-1 : $idx;
126 0           splice(@argument_tokens, $start, 2, $self->operator($argument_tokens[$idx]->{core})->new($op->{line}, $op->{core}, $op1));
127             }
128            
129             # First, pull together filters. These are the highest priority operators, after parentheses. They also have their own weird syntax.
130 0           my $top = undef;
131            
132             # Don't partition if we have any pipes. Pipes and multiple arguments don't play well together.
133 0           my @partitions;
134 0           my $has_pipe = 0;
135 0 0         if (int(grep { $_->isa('WWW::Shopify::Liquid::Token::Operator') && $_->{core} eq "|" } @argument_tokens) == 0) {
  0 0          
136 0           my $i = 0;
137 0           $has_pipe = 1;
138 0 0   0     @partitions = part { $i++ if $_->isa('WWW::Shopify::Liquid::Token::Separator'); $i; } @argument_tokens;
  0            
  0            
139 0           @partitions = map { my @n = grep { !$_->isa('WWW::Shopify::Liquid::Token::Separator') } @$_; \@n } @partitions;
  0            
  0            
  0            
140             } else {
141 0           @partitions = (\@argument_tokens);
142             }
143            
144 0           my @tops;
145            
146            
147            
148 0           foreach my $partition (@partitions) {
149 0           my @tokens = @$partition;
150             #@tokens = (grep { !$_->isa('WWW::Shopify::Liquid::Token::Separator') } @tokens) if !$has_pipe;
151            
152             # Use the order of operations to create a binary tree structure.
153 0           foreach my $operators ($self->order_of_operations) {
154 0           my %ops = map { $_ => 1 } map { $_->symbol } @$operators;
  0            
  0            
155             # If we have pipes, we deal with those, and parse their lower level arguments first; this is an exception. Rewrite?
156 0 0         if ($operators->[0] eq 'WWW::Shopify::Liquid::Operator::Pipe') {
157 0 0   0     if ((my $idx = firstidx { $_->isa('WWW::Shopify::Liquid::Token::Operator') && $_->{core} eq "|" } @tokens) != -1) {
  0 0          
158 0 0         die new WWW::Shopify::Liquid::Exception::Parser($tokens[0]) if $idx == 0;
159 0           my $i = 0;
160             # Part should consist of the first token before a pipe, and then split on all pipes after this.,
161 0 0 0 0     my @parts = map { shift(@{$_}) if $_->[0]->{core} eq "|"; $_ } part { $i++ if $_->isa('WWW::Shopify::Liquid::Token::Operator') && $_->{core} eq "|"; $i; } splice(@tokens, $idx-1);
  0 0          
  0            
  0            
  0            
  0            
162 0           my $next = undef;
163 0           $top = $self->parse_filter_tokens($self->parse_argument_tokens(@{shift(@parts)}), @{shift(@parts)});
  0            
  0            
164 0           while (my $part = shift(@parts)) {
165 0           $top = $self->parse_filter_tokens($top, @$part);
166             }
167 0           push(@tokens, $top);
168             }
169             } else {
170 0 0   0     while ((my $idx = firstidx { $_->isa('WWW::Shopify::Liquid::Token::Operator') && exists $ops{$_->{core}} } @tokens) != -1) {
  0            
171 0           my ($op1, $op, $op2) = @tokens[$idx-1..$idx+1];
172             # The one exception would be if we have a - operator, and nothing before, this is unary negative operator, i.e. 0 - number.
173 0 0 0       die new WWW::Shopify::Liquid::Exception::Parser::Operands($tokens[0], $op1, $op, $op2) unless
      0        
      0        
      0        
      0        
174             $idx > 0 && $idx < $#tokens &&
175             ($op1->isa('WWW::Shopify::Liquid::Operator') || $op1->isa('WWW::Shopify::Liquid::Token::Operand') || $op1->isa('WWW::Shopify::Liquid::Filter')) &&
176             ($op2->isa('WWW::Shopify::Liquid::Operator') || $op2->isa('WWW::Shopify::Liquid::Token::Operand') || $op2->isa('WWW::Shopify::Liquid::Filter'));
177 0 0         ($op1) = $self->parse_argument_tokens($op1->members) if $op1->isa('WWW::Shopify::Liquid::Token::Grouping');
178 0 0         ($op2) = $self->parse_argument_tokens($op2->members) if $op2->isa('WWW::Shopify::Liquid::Token::Grouping');
179 0           splice(@tokens, $idx-1, 3, $self->operators->{$op->{core}}->new($op->{line}, $op->{core}, $op1, $op2));
180             }
181             }
182             }
183            
184             # Only named variables can be without commas, for whatever reason. Goddammit shopify.
185 0 0 0       die new WWW::Shopify::Liquid::Exception::Parser::Operands(@tokens) unless int(grep { !$_->isa('WWW::Shopify::Liquid::Token::Variable::Named') } @tokens) == 1 || int(grep { !$_->isa('WWW::Shopify::Liquid::Token::Variable::Named') } @tokens) == 0;
  0            
  0            
186 0           push(@tops, @tokens);
187             }
188            
189 0           return @tops;
190             }
191              
192             sub parse_tokens {
193 0     0     my ($self, @tokens) = @_;
194            
195 0 0         return () if int(@tokens) == 0;
196            
197 0           my @tags = ();
198             # First we take a look and start matching up opening and ending tags. Those which are free tags we can leave as is.
199 0           while (my $token = shift(@tokens)) {
200 0           my $line = $token->{line};
201 0 0         if ($token->isa('WWW::Shopify::Liquid::Token::Tag')) {
    0          
202 0           my $tag = undef;
203 0 0         if ($self->enclosing_tags->{$token->tag}) {
    0          
204 0           my @internal = ();
205 0           my @contents = ();
206 0           my %allowed_internal_tags = map { $_ => 1 } $self->enclosing_tags->{$token->tag}->inner_tags;
  0            
207 0           my $level = 1;
208 0           my $closed = undef;
209 0           for (0..$#tokens) {
210 0 0         if ($tokens[$_]->isa('WWW::Shopify::Liquid::Token::Tag')) {
211 0 0 0       if ($self->enclosing_tags->{$tokens[$_]->tag}) {
    0 0        
    0          
    0          
212 0           ++$level;
213             } elsif (exists $allowed_internal_tags{$tokens[$_]->tag} && $level == 1) {
214 0           $tokens[$_]->{arguments} = [$self->parse_argument_tokens(@{$tokens[$_]->{arguments}})];
  0            
215 0           push(@internal, $_);
216             } elsif ($tokens[$_]->tag eq "end" . $token->tag && $level == 1) {
217 0           --$level;
218 0           my $last_int = 0;
219 0           foreach my $int (@internal, $_) {
220 0           push(@contents, [splice(@tokens, 0, $int-$last_int)]);
221 0 0 0       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');
  0   0        
  0   0        
222             @contents = map {
223 0           my @array = @$_;
  0            
224 0 0 0       if (int(@array) > 0 && $array[0]->isa('WWW::Shopify::Liquid::Token::Tag') && $allowed_internal_tags{$array[0]->tag}) {
      0        
225 0           [$array[0], $self->parse_tokens(@array[1..$#array])];
226             }
227             else {
228 0           [$self->parse_tokens(@array)]
229             }
230             } @contents;
231 0           $last_int = $int;
232             }
233             # Remove the endtag.
234 0           shift(@tokens);
235 0           $closed = 1;
236 0           last;
237             } elsif ($tokens[$_]->tag =~ m/^end/) {
238 0           --$level;
239             # 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.
240             }
241             }
242             }
243 0 0         die new WWW::Shopify::Liquid::Exception::Parser::NoClose($token) unless $closed;
244 0           $tag = $self->enclosing_tags->{$token->tag}->new($line, $token->tag, [$self->parse_argument_tokens(@{$token->{arguments}})], \@contents);
  0            
245 0           $tag->verify($self);
246             }
247             elsif ($self->free_tags->{$token->tag}) {
248 0           $tag = $self->free_tags->{$token->tag}->new($line, $token->tag, [$self->parse_argument_tokens(@{$token->{arguments}})]);
  0            
249 0           $tag->verify($self);
250             }
251             else {
252 0 0 0       die new WWW::Shopify::Liquid::Exception::Parser::NoOpen($token) if ($token->tag =~ m/^end(\w+)$/ && $self->enclosing_tags->{$1});
253 0 0         die new WWW::Shopify::Liquid::Exception::Parser::NakedInnerTag($token) if (exists $self->inner_tags->{$token->tag});
254 0           die new WWW::Shopify::Liquid::Exception::Parser::UnknownTag($token);
255             }
256 0           push(@tags, $tag);
257             }
258             elsif ($token->isa('WWW::Shopify::Liquid::Token::Output')) {
259 0           push(@tags, WWW::Shopify::Liquid::Tag::Output->new($line, [$self->parse_argument_tokens(@{$token->{core}})]));
  0            
260             }
261             else {
262 0           push(@tags, $token);
263             }
264             }
265            
266 0           my $top = undef;
267 0 0         if (int(@tags) > 1) {
268 0           $top = WWW::Shopify::Liquid::Operator::Concatenate->new($tags[0]->{line}, '', @tags);
269             }
270             else {
271 0           ($top) = @tags;
272             }
273 0           return $top;
274             }
275              
276             sub unparse_argument_tokens {
277 0     0     my ($self, $ast) = @_;
278 0 0         return $ast if $self->is_processed($ast);
279 0 0         if ($ast->isa('WWW::Shopify::Liquid::Filter')) {
    0          
280             my @optokens = ($self->unparse_argument_tokens($ast->{operand}),
281             WWW::Shopify::Liquid::Token::Operator->new([0,0,0], '|'),
282             WWW::Shopify::Liquid::Token::Variable->new([0,0,0], WWW::Shopify::Liquid::Token::String->new([0,0,0], $ast->{core})),
283 0 0         (int(@{$ast->{arguments}}) > 0 ? (do {
  0            
284 0           my @args = @{$ast->{arguments}};
  0            
285             (
286             WWW::Shopify::Liquid::Token::Separator->new([0,0,0], ':'),
287 0 0         (map { (($_ > 0 ? (WWW::Shopify::Liquid::Token::Separator->new([0,0,0], ',')) : ()), $self->unparse_argument_tokens($args[$_])) } 0..$#args)
  0            
288             )
289             }) : ())
290             );
291 0           return @optokens;
292             } elsif ($ast->isa('WWW::Shopify::Liquid::Operator')) {
293 0           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]));
294 0 0         return WWW::Shopify::Liquid::Token::Grouping->new([0,0,0], @optokens) if $ast->requires_grouping;
295 0           return @optokens;
296             } else {
297 0           return $ast;
298             }
299             }
300              
301             sub unparse_tokens {
302 0     0     my ($self, $ast) = @_;
303 0 0 0       return $ast if $self->is_processed($ast) || $ast->isa('WWW::Shopify::Liquid::Token');
304 0 0         if ($ast->isa('WWW::Shopify::Liquid::Tag')) {
305 0 0         my @arguments = $ast->{arguments} ? $self->unparse_argument_tokens(@{$ast->{arguments}}) : ();
  0            
306 0 0         if ($ast->isa('WWW::Shopify::Liquid::Tag::Enclosing')) {
    0          
307 0 0         if ($ast->isa('WWW::Shopify::Liquid::Tag::If')) {
308 0 0         return (WWW::Shopify::Liquid::Token::Tag->new([0,0,0], $ast->{core}, \@arguments), $self->unparse_tokens($ast->{true_path}), WWW::Shopify::Liquid::Token::Tag->new([0,0,0], 'end' . $ast->{core})) if !$ast->{false_path};
309 0           return (WWW::Shopify::Liquid::Token::Tag->new([0,0,0], $ast->{core}, \@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], 'end'. $ast->{core}));
310             }
311             else {
312 0           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}));
313             }
314             }
315             elsif ($ast->isa('WWW::Shopify::Liquid::Tag::Output')) {
316 0           return (WWW::Shopify::Liquid::Token::Output->new([0,0,0], [$self->unparse_argument_tokens(@{$ast->{arguments}})]));
  0            
317             }
318             else {
319 0           return (WWW::Shopify::Liquid::Token::Tag->new([0,0,0], $ast->{core}, \@arguments));
320             }
321 0           return $ast;
322             }
323 0 0         if ($ast->isa('WWW::Shopify::Liquid::Filter')) {
324 0           return $ast;
325             }
326 0 0         return (map { $self->unparse_tokens($_) } @{$ast->{operands}}) if ($ast->isa('WWW::Shopify::Liquid::Operator::Concatenate'));
  0            
  0            
327            
328             }
329              
330             1;