File Coverage

blib/lib/WWW/Shopify/Liquid/Lexer.pm
Criterion Covered Total %
statement 230 244 94.2
branch 99 120 82.5
condition 85 111 76.5
subroutine 47 52 90.3
pod 0 13 0.0
total 461 540 85.3


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3 21     21   94 use strict;
  21         34  
  21         745  
4 21     21   97 use warnings;
  21         32  
  21         720  
5              
6             package WWW::Shopify::Liquid::Token;
7 21     21   97 use base 'WWW::Shopify::Liquid::Element';
  21         34  
  21         8095  
8 748     748   3844 sub new { return bless { line => $_[1], core => $_[2] }, $_[0]; };
9 0     0   0 sub stringify { return $_[0]->{core}; }
10 0     0   0 sub tokens { return $_[0]; }
11              
12             package WWW::Shopify::Liquid::Token::Operator;
13 21     21   118 use base 'WWW::Shopify::Liquid::Token';
  21         37  
  21         6511  
14              
15             package WWW::Shopify::Liquid::Token::Operand;
16 21     21   111 use base 'WWW::Shopify::Liquid::Token';
  21         38  
  21         5511  
17              
18             package WWW::Shopify::Liquid::Token::String;
19 21     21   115 use base 'WWW::Shopify::Liquid::Token::Operand';
  21         31  
  21         6921  
20 74     74   82 sub process { my ($self, $hash) = @_; return $self->{core}; }
  74         217  
21              
22             package WWW::Shopify::Liquid::Token::Number;
23 21     21   116 use base 'WWW::Shopify::Liquid::Token::Operand';
  21         39  
  21         6413  
24 10     10   18 sub process { my ($self, $hash) = @_; return $self->{core}; }
  10         38  
25              
26             package WWW::Shopify::Liquid::Token::Bool;
27 21     21   116 use base 'WWW::Shopify::Liquid::Token::Operand';
  21         31  
  21         6108  
28 0     0   0 sub process { my ($self, $hash) = @_; return $self->{core}; }
  0         0  
29              
30             package WWW::Shopify::Liquid::Token::Variable;
31 21     21   117 use base 'WWW::Shopify::Liquid::Token::Operand';
  21         32  
  21         5469  
32              
33 21     21   13556 use Data::Dumper;
  21         123908  
  21         1750  
34 21     21   163 use Scalar::Util qw(looks_like_number reftype);
  21         39  
  21         6246  
35              
36 314     314   391 sub new { my $package = shift; return bless { line => shift, core => [@_] }, $package; };
  314         1769  
37             sub process {
38 36     36   44 my ($self, $hash, $action) = @_;
39 36         40 my $place = $hash;
40            
41 36 50       96 return $self->{core} if $self->is_processed($self->{core});
42 36         44 foreach my $part (@{$self->{core}}) {
  36         75  
43 47 50       110 if (ref($part) eq 'WWW::Shopify::Liquid::Token::Variable::Processing') {
44 0         0 $place = $part->$action($hash, $place);
45             }
46             else {
47 47 50       105 my $key = $self->is_processed($part) ? $part : $part->$action($hash);
48 47 50 33     207 return $self unless defined $key && $key ne '';
49 47 100 66     344 if (reftype($place) && reftype($place) eq "HASH" && exists $place->{$key}) {
    50 66        
      33        
      33        
      33        
50 45         112 $place = $place->{$key};
51             } elsif (reftype($place) && reftype($place) eq "ARRAY" && looks_like_number($key) && defined $place->[$key]) {
52 2         7 $place = $place->[$key];
53             } else {
54 0         0 return $self;
55             }
56            
57             }
58             }
59 36         91 return $place;
60             }
61 0     0   0 sub stringify { return join(".", map { $_->stringify } @{$_[0]->{core}}); }
  0         0  
  0         0  
62              
63             package WWW::Shopify::Liquid::Token::Variable::Processing;
64 21     21   121 use base 'WWW::Shopify::Liquid::Token::Operand';
  21         39  
  21         6545  
65 21     21   127 use Data::Dumper;
  21         50  
  21         2617  
66             sub process {
67 0     0   0 my ($self, $hash, $argument, $action) = @_;
68 0 0       0 return $self if !$self->is_processed($argument);
69 0         0 my $result = $self->{core}->operate($hash, $argument);
70 0 0       0 return $self if !$self->is_processed($result);
71 0         0 return $result;
72             }
73              
74              
75             package WWW::Shopify::Liquid::Token::Grouping;
76 21     21   111 use base 'WWW::Shopify::Liquid::Token::Operand';
  21         38  
  21         6910  
77 26     26   52 sub new { my $package = shift; return bless { line => shift, members => [@_] }, $package; };
  26         151  
78 15     15   21 sub members { return @{$_[0]->{members}}; }
  15         186  
79              
80             package WWW::Shopify::Liquid::Token::Text;
81 21     21   119 use base 'WWW::Shopify::Liquid::Token::Operand';
  21         36  
  21         8183  
82             sub new {
83 194     194   694 my $self = { line => $_[1], core => $_[2] };
84 194         271 my $package = $_[0];
85 194 100 66     1288 $package = 'WWW::Shopify::Liquid::Token::Text::Whitespace' if !defined $_[2] || $_[2] =~ m/^\s*$/;
86 194         724 return bless $self, $package;
87             };
88 4     4   7 sub process { my ($self, $hash) = @_; return $self->{core}; }
  4         14  
89              
90             package WWW::Shopify::Liquid::Token::Text::Whitespace;
91 21     21   124 use base 'WWW::Shopify::Liquid::Token::Text';
  21         37  
  21         6563  
92              
93             package WWW::Shopify::Liquid::Token::Tag;
94 21     21   130 use base 'WWW::Shopify::Liquid::Token';
  21         47  
  21         8113  
95 279     279   1383 sub new { return bless { line => $_[1], tag => $_[2], arguments => $_[3] }, $_[0] };
96 1592     1592   6780 sub tag { return $_[0]->{tag}; }
97 6     6   18 sub stringify { return $_[0]->tag; }
98              
99             package WWW::Shopify::Liquid::Token::Output;
100 21     21   124 use base 'WWW::Shopify::Liquid::Token';
  21         40  
  21         6541  
101 93     93   398 sub new { return bless { line => $_[1], core => $_[2] }, $_[0]; };
102              
103             package WWW::Shopify::Liquid::Token::Separator;
104 21     21   114 use base 'WWW::Shopify::Liquid::Token';
  21         39  
  21         5643  
105              
106              
107             package WWW::Shopify::Liquid::Lexer;
108 21     21   116 use base 'WWW::Shopify::Liquid::Pipeline';
  21         38  
  21         6402  
109 21     21   152 use Scalar::Util qw(looks_like_number);
  21         43  
  21         40110  
110              
111 23     23 0 283 sub new { return bless { operators => {}, lexing_halters => {}, transparent_filters => {} }, $_[0]; }
112 909     909 0 4440 sub operators { return $_[0]->{operators}; }
113 460     460 0 3546 sub register_operator { $_[0]->{operators}->{$_} = $_[1] for ($_[1]->symbol); }
114             sub register_tag {
115 322     322 0 442 my ($self, $package) = @_;
116 322 100 100     1667 $self->{lexing_halters}->{$package->name} = $package if $package->is_enclosing && $package->inner_halt_lexing;
117             }
118             sub register_filter {
119 1311     1311 0 1423 my ($self, $package) = @_;
120 1311 100       11021 $self->{transparent_filters}->{$package->name} = $package if ($package->transparent);
121             }
122 429     429 0 1927 sub transparent_filters { return $_[0]->{transparent_filters}; }
123              
124             sub parse_token {
125 636     636 0 809 my ($self, $line, $token) = @_;
126             # Strip token of whitespace.
127 636 50       1081 return undef unless defined $token;
128 636         2706 $token =~ s/^\s*(.*?)\s*$/$1/;
129 636 100       1209 return WWW::Shopify::Liquid::Token::Operator->new($line, $token) if $self->operators->{$token};
130 462 100 100     2104 return WWW::Shopify::Liquid::Token::String->new($line, $1) if $token =~ m/^'(.*)'$/ || $token =~ m/^"(.*)"$/;
131 415 100       1513 return WWW::Shopify::Liquid::Token::Number->new($line, $1) if looks_like_number($token);
132 333 50       615 return WWW::Shopify::Liquid::Token::NULL->new() if $token eq '';
133 333 100 100     1256 return WWW::Shopify::Liquid::Token::Separator->new($line, $token) if ($token eq ":" || $token eq ",");
134             # We're a variable. Let's see what's going on. Split along non quoted . and [ ] fields.
135 314         520 my ($squot, $dquot, $start, @parts) = (0,0,0);
136             # customer['test']['b']
137 314         351 my $open_bracket = 0;
138 314         1849 while ($token =~ m/(\.|\[|\]|(?
139 460         654 my $sym = $&;
140 460 50 33     1286 if (!$squot && !$dquot) {
141 460 100 100     1191 $open_bracket-- if ($sym && $sym eq "]");
142 460 100 100     3394 if (($sym eq "." || $sym eq "]" || $sym eq "[" || !$sym) && $open_bracket == 0) {
      100        
143 442         1031 my $contents = substr($token, $start, $-[0] - $start);
144 442 100 66     1633 if (defined $contents && $contents ne "") {
145 440         617 my @variables = ();
146 440 100 100     1456 if (!$sym || $sym eq "." || $sym eq "[") {
    50 100        
147 424 100       786 @variables = $self->transparent_filters->{$contents} ? WWW::Shopify::Liquid::Token::Variable::Processing->new($line, $self->transparent_filters->{$contents}) : WWW::Shopify::Liquid::Token::String->new($line, $contents);
148             }
149             elsif ($sym eq "]") {
150 16         55 @variables = $self->parse_expression($line, $contents);
151             }
152 440 50       1069 if (int(@variables) > 0) {
153 440 100       698 if (int(@variables) == 1) {
154 436         696 push(@parts, @variables);
155             }
156             else {
157 4         20 push(@parts, WWW::Shopify::Liquid::Token::Grouping->new($line, @variables)) ;
158             }
159             }
160            
161             }
162             }
163 460 100 66     2714 $start = $+[0] if $sym ne '"' && $sym ne "'" && !$open_bracket;
      100        
164 460 100 100     1484 $open_bracket++ if ($sym && $sym eq "[");
165            
166             }
167 460 50       775 $squot = !$squot if $token eq "'";
168 460 50       2340 $dquot = !$dquot if $token eq '"';
169             }
170 314         941 return WWW::Shopify::Liquid::Token::Variable->new($line, @parts);
171             }
172              
173             # Returns a single token repsending the whole a expression.
174             sub parse_expression {
175 401     401 0 649 my ($self, $line, $exp) = @_;
176 401 100 66     10865 return () if !defined $exp || $exp eq '';
177 273         375 my @tokens = ();
178 273         470 my ($start_paren, $start_space, $level, $squot, $dquot, $start_sq, $sq_level) = (undef, 0, 0, 0, 0, undef, 0);
179             # We regex along parentheses, quotation marks (both kinds), whitespace, and non-word-operators.
180             # We sort along length, so that we make sure to get all the largest operators first, so that way if a larger operator is made from a smaller one (=, ==)
181             # There's no confusion, we always try to match the largest first.
182 273         279 my $non_word_operators = join("|", map { quotemeta($_) } grep { $_ =~ m/^\W+$/; } sort { length($b) <=> length($a) } keys(%{$self->operators}));
  4368         5278  
  5733         8893  
  18769         15187  
  273         548  
183 273         4893 while ($exp =~ m/(?:\(|\)|\]|\[|(?
184 1002         3283 my ($rs, $re, $rc, $whitespace, $nword_op) = ($-[0], $+[0], $&, $1, $2);
185 1002 100 66     3123 if (!$squot && !$dquot) {
186 924 100 66     1891 $start_paren = $re if $rc eq "(" && $level++ == 0;
187 924 100 66     1712 $start_sq = $re if $rc eq "[" && $sq_level++ == 0;
188             # Deal with parentheses; always the highest level of operation.
189 924 100 66     1623 if ($rc eq ")" && --$level == 0) {
190 21         43 $start_space = $re;
191 21         148 push(@tokens, WWW::Shopify::Liquid::Token::Grouping->new($line, $self->parse_expression($line, substr($exp, $start_paren, $rs - $start_paren))));
192             }
193 924 100       1305 --$sq_level if $rc eq "]";
194 924 100 100     2996 if ($level == 0 && $sq_level == 0) {
195             # If we're only spaces, that means we're a new a token.
196 835 100 100     1909 if (defined $whitespace || $nword_op) {
197 751 50       1125 if (defined $start_space) {
198 751         1223 my $contents = substr($exp, $start_space, $rs - $start_space);
199 751 100       2851 push(@tokens, $self->parse_token($line, $contents)) if $contents !~ m/^\s*$/;
200             }
201 751 100       1429 push(@tokens, $self->parse_token($line, $nword_op)) if $nword_op;
202 751         859 $start_space = $re;
203             }
204             }
205             }
206 1002 100 66     2106 $squot = !$squot if ($rc eq "'" && !$dquot);
207 1002 100 66     7347 $dquot = !$dquot if ($rc eq '"' && !$squot);
208             }
209 273 50       539 die WWW::Shopify::Liquid::Exception::Lexer::UnbalancedBrace->new($line) unless $level == 0;
210             # Go through and combine any -1 from OP NUM to NUM.
211 384 100 33     3003 my @ids = grep {
      33        
      100        
      100        
212 273         610 $tokens[$_]->isa('WWW::Shopify::Liquid::Token::Number') &&
213             $tokens[$_-1]->isa('WWW::Shopify::Liquid::Token::Operator') && $tokens[$_-1]->{core} eq "-" &&
214             ($_ == 1 || $tokens[$_-2]->isa('WWW::Shopify::Liquid::Token::Separator') || $tokens[$_-2]->isa('WWW::Shopify::Liquid::Token::Operator'))
215             } 1..$#tokens;
216 273         557 for (@ids) { $tokens[$_]->{core} *= -1; $tokens[$_-1] = undef; }
  1         11  
  1         13  
217 273         345 return grep { defined $_ } @tokens;
  657         1959  
218             }
219              
220             sub parse_text {
221 78     78 0 77300 my ($self, $text) = @_;
222 78 50       271 return () unless defined $text;
223 78         143 my @tokens = ();
224 78         127 my $start = 0;
225 78         117 my $line = 1;
226 78         106 my $column = 0;
227 78         106 my $lexing_halter = undef;
228            
229 78         101 my $start_line = 1;
230 78         107 my $start_column = 0;
231 78         101 my $line_position = 0;
232            
233 78 50       235 return (WWW::Shopify::Liquid::Token::Text->new(1, '')) if $text eq '';
234            
235             # No need to worry about quotations; liquid overrides everything.
236 78         861 while ($text =~ m/(?:(?:{%\s*(\w+)\s*(.*?)\s*%})|(?:{{\s*(.*?)\s*}})|(\n)|$)/g) {
237 581         1270 my ($tag, $arguments, $output) = ($1, $2, $3);
238 581 100       1127 if (defined $4) {
239 134         124 ++$line;
240 134         232 $line_position = $+[0];
241 134         751 next;
242             };
243 447         918 my $column = $+[0] - $line_position;
244 447         1115 my $position = [$start_line, $start_column, $-[0]];
245 447 100 100     3768 if ($tag && !$lexing_halter && exists $self->{lexing_halters}->{$tag}) {
    100 100        
      100        
      100        
246 4         8 $lexing_halter = $tag;
247 4         17 push(@tokens, WWW::Shopify::Liquid::Token::Tag->new($position, $tag, [$self->parse_expression($position, $arguments)]));
248 4         14 $start = $+[0];
249 4         9 $start_line = $line;
250 4         7 $start_column = $column;
251             }
252             elsif ($tag && $lexing_halter && $tag eq "end" . $lexing_halter) {
253 4         8 $lexing_halter = undef;
254             }
255 447 100       793 if (!$lexing_halter) {
256 436 100       1175 if ($start < $-[0]) {
257 192         750 push(@tokens, WWW::Shopify::Liquid::Token::Text->new($position, substr($text, $start, $-[0] - $start)));
258 192         295 $start_line = $line;
259 192         332 $start_column = $-[0] - $line_position;
260 192         478 $position = [$start_line, $start_column, $-[0]];
261             }
262 436 100       1246 push(@tokens, WWW::Shopify::Liquid::Token::Tag->new($position, $tag, [$self->parse_expression($position, $arguments)])) if $tag;
263 436 100       931 push(@tokens, WWW::Shopify::Liquid::Token::Output->new($position, [$self->parse_expression($position, $output)])) if $output;
264 436         767 $start = $+[0];
265 436         495 $start_line = $line;
266 436         2135 $start_column = $column;
267             }
268             }
269 78         857 return @tokens;
270             }
271              
272             sub unparse_token {
273 10     10 0 10 my ($self, $token) = @_;
274 10 50       17 return '' unless $token;
275 10 100       30 return join(".", map { $_->{core} } @{$token->{core}}) if $token->isa('WWW::Shopify::Liquid::Token::Variable');
  6         26  
  5         6  
276 5 100       16 return "(" . join("", map { $self->unparse_token($_); } @{$token->{members}}) . ")" if $token->isa('WWW::Shopify::Liquid::Token::Grouping');
  3         9  
  1         2  
277 4         10 return $token->{core};
278             }
279              
280             sub unparse_expression {
281 5     5 0 6 my ($self, @tokens) = @_;
282 5         5 return join(' ', map { $self->unparse_token($_) } @tokens);
  7         10  
283             }
284              
285             sub unparse_text_segment {
286 15     15 0 12 my ($self, $token) = @_;
287 15 100       38 if ($token->isa('WWW::Shopify::Liquid::Token::Tag')) {
288 8 100       22 return "{%" . $token->{tag} . "%}" if !$token->{arguments};
289 3         5 return "{%" . $token->{tag} . " " . $self->unparse_expression(@{$token->{arguments}}) . "%}";
  3         6  
290             }
291 7 100       22 return "{{" . $self->unparse_expression(@{$token->{core}}) . "}}" if $token->isa('WWW::Shopify::Liquid::Token::Output');
  2         6  
292 5         9 return $token->{core};
293             }
294              
295             sub unparse_text {
296 1     1 0 7 my ($self, @tokens) = @_;
297 1         2 return join('', map { $self->unparse_text_segment($_) } @tokens);
  15         18  
298             }
299              
300             1;