File Coverage

blib/lib/WWW/Shopify/Liquid/Lexer.pm
Criterion Covered Total %
statement 405 436 92.8
branch 208 258 80.6
condition 203 246 82.5
subroutine 71 75 94.6
pod 0 16 0.0
total 887 1031 86.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3 37     37   271 use strict;
  37         114  
  37         1213  
4 37     37   239 use warnings;
  37         95  
  37         1566  
5              
6             package WWW::Shopify::Liquid::Token;
7 37     37   254 use base 'WWW::Shopify::Liquid::Element';
  37         97  
  37         7292  
8 3377     3377   16747 sub new { return bless { line => $_[1], core => $_[2] }, $_[0]; };
9 1     1   10 sub stringify { return $_[0]->{core}; }
10 94     94   413 sub tokens { return $_[0]; }
11              
12             package WWW::Shopify::Liquid::Token::Operator;
13 37     37   286 use base 'WWW::Shopify::Liquid::Token';
  37         93  
  37         11109  
14              
15             package WWW::Shopify::Liquid::Token::Operand;
16 37     37   297 use base 'WWW::Shopify::Liquid::Token';
  37         92  
  37         7448  
17              
18             package WWW::Shopify::Liquid::Token::String;
19 37     37   290 use base 'WWW::Shopify::Liquid::Token::Operand';
  37         100  
  37         12763  
20 1123     1123   2366 sub process { my ($self, $hash) = @_; return $self->{core}; }
  1123         3766  
21              
22             package WWW::Shopify::Liquid::Token::Number;
23 37     37   326 use base 'WWW::Shopify::Liquid::Token::Operand';
  37         115  
  37         9234  
24 212     212   498 sub process { my ($self, $hash) = @_; return $self->{core}; }
  212         746  
25              
26             package WWW::Shopify::Liquid::Token::NULL;
27 37     37   309 use base 'WWW::Shopify::Liquid::Token::Operand';
  37         114  
  37         7802  
28 5     5   18 sub process { return undef; }
29              
30             package WWW::Shopify::Liquid::Token::Bool;
31 37     37   292 use base 'WWW::Shopify::Liquid::Token::Operand';
  37         119  
  37         8852  
32 0     0   0 sub process { my ($self, $hash) = @_; return $self->{core}; }
  0         0  
33              
34             package WWW::Shopify::Liquid::Token::FunctionCall;
35 37     37   291 use base 'WWW::Shopify::Liquid::Token::Operand';
  37         103  
  37         7118  
36              
37 37     37   2817 use Scalar::Util qw(looks_like_number reftype blessed);
  37         109  
  37         13611  
38              
39             sub new {
40 3     3   9 my $package = shift;
41 3         26 return bless {
42             line => shift,
43             method => shift,
44             self => shift,
45             arguments => [@_]
46             }, $package;
47             }
48              
49             sub process {
50 0     0   0 my ($self, $hash, $action, $pipeline) = @_;
51 0 0       0 if ($action eq "render") {
52 0 0       0 die new WWW::Shopify::Liquid::Exception::Renderer::Forbidden($self->{line}) unless $pipeline->make_method_calls;
53            
54 0 0       0 my @arguments = map { $self->is_processed($_) ? $_ : $_->render($pipeline, $hash) } @{$self->{arguments}};
  0         0  
  0         0  
55 0 0       0 my $inner_self = $self->is_processed($self->{self}) ? $self->{self} : $self->{self}->render($pipeline, $hash);
56 0 0       0 my $method = $self->is_processed($self->{method}) ? $self->{method} : $self->{method}->render($pipeline, $hash);
57 0 0 0     0 die new WWW::Shopify::Liquid::Exception::Renderer($self->{line}, "Can't find method $method on $inner_self.") unless $inner_self && blessed($inner_self) && !ref($method);
      0        
58 0         0 return $inner_self->$method(@arguments);
59             }
60 0         0 return $self;
61            
62             }
63              
64             package WWW::Shopify::Liquid::Token::Variable;
65 37     37   306 use base 'WWW::Shopify::Liquid::Token::Operand';
  37         102  
  37         7404  
66              
67 37     37   305 use Scalar::Util qw(looks_like_number reftype blessed);
  37         103  
  37         27500  
68              
69 1289     1289   2819 sub new { my $package = shift; return bless { line => shift, core => [@_] }, $package; };
  1289         7391  
70             sub process {
71 2224     2224   6125 my ($self, $hash, $action, $pipeline) = @_;
72 2224         4425 my $place = $hash;
73            
74 2224         3988 my @inner = @{$self->{core}};
  2224         7164  
75 2224         4939 my $unprocessed = 0;
76 2224         7427 foreach my $part_idx (0..$#inner) {
77 3071         6407 my $part = $inner[$part_idx];
78 3071 100       10632 if (ref($part) eq 'WWW::Shopify::Liquid::Token::Variable::Processing') {
79 33         153 $place = $part->$action($pipeline, $hash, $place);
80             }
81             else {
82 3038 100       8706 my $key = $self->is_processed($part) ? $part : $part->$action($pipeline, $hash);
83            
84 3038 50 33     14106 return $self unless defined $key && $key ne '';
85 3038 100 100     8785 $self->{core}->[$part_idx] = $key if $self->is_processed($key) && $action eq "optimize";
86 3038 100       9705 if (defined $place) {
87 2947 100 66     24971 if (blessed($place) && $place->isa('WWW::Shopify::Liquid::Resolver')) {
    100 100        
    100 66        
    50 100        
      100        
      100        
      33        
      33        
88 2         6 $place = $place->resolver->($place, $hash, $key);
89             } elsif (reftype($place) && reftype($place) eq "HASH" && exists $place->{$key}) {
90 2747         9172 $place = $place->{$key};
91             } elsif (reftype($place) && reftype($place) eq "ARRAY" && looks_like_number($key) && defined $place->[$key]) {
92 21         60 $place = $place->[$key];
93             } elsif ($pipeline->make_method_calls && blessed($place) && $place->can($key)) {
94 0         0 $place = $place->$key;
95             } else {
96 177         347 $unprocessed = 1;
97 177         472 $place = undef;
98             }
99             }
100            
101             }
102             }
103 2224 100       6776 return $self if $unprocessed;
104 2047 100 100     7025 return $place->resolver->($place, $hash) if (blessed($place) && $place->isa('WWW::Shopify::Liquid::Resolver'));
105 2046         8361 return $place;
106             }
107 1     1   2 sub stringify { return join(".", map { $_->stringify } @{$_[0]->{core}}); }
  1         8  
  1         4  
108              
109             sub set {
110 0     0   0 my ($self, $pipeline, $hash, $value) = @_;
111 0 0       0 my @vars = map { $self->is_processed($_) ? $_ : $_->render($pipeline, $hash) } @{$self->{core}};
  0         0  
  0         0  
112 0         0 my ($reference) = $pipeline->variable_reference($hash, \@vars);
113 0         0 $$reference = $value;
114 0         0 return 1;
115             }
116              
117              
118             sub get {
119 0     0   0 my ($self, $pipeline, $hash) = @_;
120 0 0       0 my @vars = map { $self->is_processed($_) ? $_ : $_->render($pipeline, $hash) } @{$self->{core}};
  0         0  
  0         0  
121 0         0 my ($reference) = $pipeline->variable_reference($hash, \@vars, 1);
122 0 0       0 return $reference ? $$reference : undef;
123             }
124              
125             package WWW::Shopify::Liquid::Token::Variable::Processing;
126 37     37   329 use base 'WWW::Shopify::Liquid::Token::Operand';
  37         119  
  37         11764  
127             sub process {
128 33     33   95 my ($self, $hash, $argument, $action, $pipeline) = @_;
129 33 50       106 return $self if !$self->is_processed($argument);
130 33         243 my $result = $self->{core}->operate($hash, $argument);
131 33 50       103 return $self if !$self->is_processed($result);
132 33         104 return $result;
133             }
134              
135             package WWW::Shopify::Liquid::Token::Variable::Named;
136 37     37   306 use base 'WWW::Shopify::Liquid::Token::Operand';
  37         104  
  37         10467  
137              
138 18     18   42 sub new { my $package = shift; return bless { line => shift, name => shift, core => shift }, $package; };
  18         85  
139             sub process {
140 2     2   6 my ($self, $hash, $action, $pipeline) = @_;
141 2         8 return { $self->{name} => $self->{core}->$action($pipeline, $hash) };
142             }
143              
144              
145             package WWW::Shopify::Liquid::Token::Grouping;
146 37     37   293 use base 'WWW::Shopify::Liquid::Token::Operand';
  37         100  
  37         9642  
147 86     86   205 sub new { my $package = shift; return bless { line => shift, members => [@_] }, $package; };
  86         526  
148 74     74   166 sub members { return @{$_[0]->{members}}; }
  74         611  
149              
150             # Parentheses
151             package WWW::Shopify::Liquid::Token::Grouping::Parenthetical;
152 37     37   309 use base 'WWW::Shopify::Liquid::Token::Grouping';
  37         103  
  37         9026  
153              
154             # Like a grouping, but not really.
155             # Square brackets
156             package WWW::Shopify::Liquid::Token::Array;
157 37     37   306 use base 'WWW::Shopify::Liquid::Token::Operand';
  37         104  
  37         21134  
158              
159             sub new {
160 7     7   28 my ($package, $line, @members) = @_;
161             # Check to see whether or not the incoming member array is separated by commas.
162             # Should never begin with a comma, should never end with a comma, should always be
163             # 101010101 in terms of data and separators. If this is not the case, then we
164             # Should throw a lexing exception.
165             die new WWW::Shopify::Liquid::Exception::Lexer::InvalidSeparator($line) unless
166 16 100       103 int(grep { ($_ % 2) == 0 && $members[$_]->isa('WWW::Shopify::Liquid::Token::Separator') } (0..$#members)) == 0 &&
167 7 100 33     42 int(grep { ($_ % 2) == 1 && (!$members[$_]->isa('WWW::Shopify::Liquid::Token::Separator') || $members[$_]->{core} ne ",") } (0..$#members)) == 0;
  16 50 33     104  
168            
169             my $self = bless {
170             line => $line,
171 7         32 members => [grep { !$_->isa('WWW::Shopify::Liquid::Token::Separator') } @members]
  16         73  
172             }, $package;
173 7         44 return $self;
174             };
175              
176 6     6   13 sub members { return @{$_[0]->{members}}; }
  6         20  
177             sub process {
178 3     3   12 my ($self, $hash, $action, $pipeline) = @_;
179 3         9 my @members = $self->members;
180 3         12 $members[$_] = $members[$_]->$action($pipeline, $hash) for (grep { !$self->is_processed($members[$_]) } (0..$#members));
  8         30  
181 3 50       12 if ($action eq "optimize") {
182 0         0 $self->{members}->[$_] = $_ for (grep { $self->is_processed($members[$_]) } 0..$#members);
  0         0  
183             }
184 3         15 return [@members];
185             }
186              
187             # Curly brackets
188             package WWW::Shopify::Liquid::Token::Hash;
189 37     37   322 use base 'WWW::Shopify::Liquid::Token::Operand';
  37         115  
  37         23944  
190              
191 16     16   27 sub members { return @{$_[0]->{members}}; }
  16         54  
192             sub new {
193 14     14   48 my ($package, $line, @members) = @_;
194            
195 14         32 @members = map { $_->isa('WWW::Shopify::Liquid::Token::Variable::Named') ? (
196             WWW::Shopify::Liquid::Token::String->new($_->{line}, $_->{name}),
197             WWW::Shopify::Liquid::Token::Separator->new($_->{line}, ':'),
198             $_->{core}
199 32 100       139 ) : $_ } @members;
200            
201             die new WWW::Shopify::Liquid::Exception::Lexer::InvalidSeparator($line) unless
202 48 100       206 int(grep { ($_ % 2) == 0 && $members[$_]->isa('WWW::Shopify::Liquid::Token::Separator') } (0..$#members)) == 0 &&
203             int(grep {
204             ($_ % 4) == 1 && (!$members[$_]->isa('WWW::Shopify::Liquid::Token::Separator') || $members[$_]->{core} ne ":") ||
205 48 50 33     251 ($_ % 4) == 3 && (!$members[$_]->isa('WWW::Shopify::Liquid::Token::Separator') || $members[$_]->{core} ne ",")
      66        
      33        
      66        
206             } (0..$#members)) == 0 &&
207 14 50 33     91 int(grep { !$_->isa('WWW::Shopify::Liquid::Token::Separator') } @members) % 2 == 0;
  48   33     118  
208            
209             return bless {
210             line => $line,
211 14         55 members => [grep { !$_->isa('WWW::Shopify::Liquid::Token::Separator') } @members]
  48         148  
212             }, $package;
213             };
214              
215             sub process {
216 4     4   11 my ($self, $hash, $action, $pipeline) = @_;
217 4         13 my @members = $self->members;
218 4         16 $members[$_] = $members[$_]->$action($pipeline, $hash) for (grep { !$self->is_processed($members[$_]) } (0..$#members));
  10         22  
219 4 50       15 if ($action eq "optimize") {
220 0         0 $self->{members}->[$_] = $_ for (grep { $self->is_processed($members[$_]) } 0..$#members);
  0         0  
221             }
222 4         18 return { @members };
223             }
224              
225              
226             package WWW::Shopify::Liquid::Token::Text;
227 37     37   310 use base 'WWW::Shopify::Liquid::Token::Operand';
  37         100  
  37         12204  
228             sub new {
229 797     797   3173 my $self = { line => $_[1], core => $_[2] };
230 797         1621 my $package = $_[0];
231 797 100 66     5237 $package = 'WWW::Shopify::Liquid::Token::Text::Whitespace' if !defined $_[2] || $_[2] =~ m/^\s*$/;
232 797         2894 return bless $self, $package;
233             };
234 259     259   761 sub process { my ($self, $hash) = @_; return $self->{core}; }
  259         1042  
235              
236             package WWW::Shopify::Liquid::Token::Text::Whitespace;
237 37     37   309 use base 'WWW::Shopify::Liquid::Token::Text';
  37         107  
  37         9107  
238              
239             package WWW::Shopify::Liquid::Token::Tag;
240 37     37   305 use base 'WWW::Shopify::Liquid::Token';
  37         95  
  37         12148  
241 1113     1113   8537 sub new { return bless { line => $_[1], tag => $_[2], arguments => $_[3], strip_left => $_[4], strip_right => $_[5] }, $_[0] };
242 6105     6105   26982 sub tag { return $_[0]->{tag}; }
243 11     11   37 sub stringify { return $_[0]->tag; }
244              
245             package WWW::Shopify::Liquid::Token::Output;
246 37     37   314 use base 'WWW::Shopify::Liquid::Token';
  37         108  
  37         8699  
247 389     389   2360 sub new { return bless { line => $_[1], core => $_[2], strip_left => $_[3], strip_right => $_[4] }, $_[0]; };
248              
249             package WWW::Shopify::Liquid::Token::Separator;
250 37     37   300 use base 'WWW::Shopify::Liquid::Token';
  37         108  
  37         7256  
251              
252              
253             package WWW::Shopify::Liquid::Lexer;
254 37     37   314 use base 'WWW::Shopify::Liquid::Pipeline';
  37         104  
  37         4182  
255 37     37   298 use Scalar::Util qw(looks_like_number blessed);
  37         98  
  37         42849  
256              
257 85     85 0 1292 sub new { return bless { operators => {}, lexing_halters => {}, transparent_filters => {}, unparse_spaces => 0, parse_escaped_characters => 1 }, $_[0]; }
258 3776     3776 0 22202 sub operators { return $_[0]->{operators}; }
259 461 100   461 0 1123 sub unparse_spaces { $_[0]->{unparse_spaces} = $_[1] if defined $_[1]; return $_[0]->{unparse_spaces}; }
  461         1250  
260 286 100   286 0 1944 sub parse_escaped_characters { $_[0]->{parse_escaped_characters} = $_[1] if defined $_[1]; return $_[0]->{parse_escaped_characters}; }
  286         912  
261 2380     2380 0 10136 sub register_operator { $_[0]->{operators}->{$_} = $_[1] for ($_[1]->symbol); }
262             sub register_tag {
263 1801     1801 0 4207 my ($self, $package) = @_;
264 1801 100 100     6557 $self->{lexing_halters}->{$package->name} = $package if $package->is_enclosing && $package->inner_halt_lexing;
265             }
266             sub register_filter {
267 5226     5226 0 12333 my ($self, $package) = @_;
268 5226 100       47471 $self->{transparent_filters}->{$package->name} = $package if ($package->transparent);
269             }
270 1714     1714 0 8405 sub transparent_filters { return $_[0]->{transparent_filters}; }
271              
272             sub parse_token {
273 2741     2741 0 7250 my ($self, $line, $token) = @_;
274            
275             # Strip token of whitespace.
276 2741 50       6974 return undef unless defined $token;
277 2741         16712 $token =~ s/^\s*(.*?)\s*$/$1/;
278 2741 100       7595 return WWW::Shopify::Liquid::Token::Operator->new($line, $token) if $self->operators->{$token};
279 2037 100 100     9378 return WWW::Shopify::Liquid::Token::String->new($line, do {
280 285         800 my $string = $1;
281 285 100       906 if ($self->parse_escaped_characters) {
282 284         764 $string =~ s/\\r/\r/g;
283 284         565 $string =~ s/\\n/\n/g;
284 284         617 $string =~ s/\\t/\t/g;
285             }
286 285         1154 $string;
287             }) if $token =~ m/^'(.*)'$/s || $token =~ m/^"(.*)"$/s;
288 1752 100       8652 return WWW::Shopify::Liquid::Token::Number->new($line, $1) if looks_like_number($token);
289 1476 100 66     5865 return WWW::Shopify::Liquid::Token::NULL->new() if $token eq '' || $token eq 'null';
290 1465 100 100     7286 return WWW::Shopify::Liquid::Token::Separator->new($line, $token) if ($token eq ":" || $token eq "," || $token eq ".");
      66        
291 1280 100       2959 return WWW::Shopify::Liquid::Token::Array->new($line) if ($token eq "[]");
292 1277 100       2848 return WWW::Shopify::Liquid::Token::Hash->new($line) if ($token eq "{}");
293             # We're a variable. Let's see what's going on. Split along non quoted . and [ ] fields.
294 1271         3084 my ($squot, $dquot, $start, @parts) = (0,0,0);
295             # customer['test']['b']
296 1271         2137 my $open_square_bracket = 0;
297 1271         2046 my $open_curly_bracket = 0;
298 1271         8529 while ($token =~ m/(\.|\[|\]|\{|\}|(?<!\\)\"|(?<!\\)\'|\b$)/g) {
299 1863         4510 my $sym = $&;
300 1863         4563 my $begin = $-[0];
301 1863         4532 my $end = $+[0];
302 1863 50 33     7068 if (!$squot && !$dquot) {
303 1863 100 100     5397 $open_square_bracket-- if ($sym && $sym eq "]");
304 1863 100 100     4860 $open_curly_bracket-- if ($sym && $sym eq "}");
305 1863 100 100     16868 if (($sym eq "." || $sym eq "]" || $sym eq "[" || $sym eq "{" || $sym eq "}" || !$sym) && $open_square_bracket == 0 && $open_curly_bracket == 0) {
      100        
      100        
306 1773         4459 my $contents = substr($token, $start, $begin - $start);
307            
308 1773 100 66     8345 if (defined $contents && $contents !~ m/^\s*$/) {
309 1743         3518 my @variables = ();
310 1743 100 100     6475 if (!$sym || $sym eq "." || $sym eq "[") {
    100 100        
    50          
311 1677 100       4628 @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);
312             }
313             elsif ($sym eq "]") {
314 58         242 @variables = $self->parse_expression($line, $contents);
315 58 100       213 return WWW::Shopify::Liquid::Token::Array->new($line, @variables) if (int(@parts) == 0);
316             }
317             elsif ($sym eq "}") {
318 8         45 @variables = $self->parse_expression($line, $contents);
319 8 50       50 return WWW::Shopify::Liquid::Token::Hash->new($line, @variables) if (int(@parts) == 0);
320             }
321 1731 50       5058 if (int(@variables) > 0) {
322 1731 100       3785 if (int(@variables) == 1) {
323 1718         4009 push(@parts, @variables);
324             }
325             else {
326 13         48 push(@parts, WWW::Shopify::Liquid::Token::Grouping->new($line, @variables)) ;
327             }
328             }
329             }
330             }
331 1851 100 100     11581 $start = $end if $sym ne '"' && $sym ne "'" && !$open_curly_bracket && !$open_square_bracket;
      100        
      100        
332 1851 100 100     5472 $open_square_bracket++ if ($sym && $sym eq "[");
333 1851 100 100     4840 $open_curly_bracket++ if ($sym && $sym eq "{");
334            
335             }
336 1851 50       3739 $squot = !$squot if $token eq "'";
337 1851 50       10523 $dquot = !$dquot if $token eq '"';
338             }
339 1259         4621 return WWW::Shopify::Liquid::Token::Variable->new($line, @parts);
340             }
341              
342 37     37   372 use utf8;
  37         104  
  37         404  
343             # Returns a single token repsending the whole a expression.
344             sub parse_expression {
345 1458     1458 0 4665 my ($self, $line, $exp) = @_;
346 1458 100 66     7256 return () if !defined $exp || $exp eq '';
347 1035         2050 my @tokens = ();
348 1035         2945 my ($start_paren, $start_space, $level, $squot, $dquot, $start_sq, $sq_level, $start_hsh, $hsh_level) = (undef, 0, 0, 0, 0, undef, 0, undef, 0);
349             # We regex along parentheses, quotation marks (both kinds), whitespace, and non-word-operators.
350             # 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 (=, ==)
351             # There's no confusion, we always try to match the largest first.
352 1035         1956 my $non_word_operators = join("|", map { quotemeta($_) } grep { $_ =~ m/^\W+$/; } sort { length($b) <=> length($a) } keys(%{$self->operators}));
  24840         47691  
  32085         79502  
  111976         158069  
  1035         2706  
353 1035         24168 while ($exp =~ m/(?:\(|\)|\]|\[|\}|\{|(?<!\\)["”“]|(?<!\\)['‘’]|(\s+|$)|($non_word_operators|,|:))/sg) {
354 5776         28217 my ($rs, $re, $rc, $whitespace, $nword_op) = ($-[0], $+[0], $&, $1, $2);
355             # Specifically to allow variables to have a - in them, and be treated as a long literal, instead of a minus sign.
356             # This is terrible behaviour, but mimics Shopify's lexer.
357             # Only if of course the entire first half of this ISN'T a number, though. 'cause that'd be insane.
358 5776 100 100     19759 next if $nword_op && $nword_op eq "-" && $rs > 0 && substr($exp, $rs-1, 1) ne " " && substr($exp, 0, $rs) !~ m/\b\d+$/;
      66        
      100        
      100        
359 5728 100 100     19904 if (!$squot && !$dquot) {
360 4932 100 100     11951 $start_paren = $re if $rc eq "(" && $level++ == 0;
361 4932 100 100     10872 $start_sq = $re if $rc eq "[" && $sq_level++ == 0;
362 4932 100 100     10759 $start_hsh = $re if $rc eq "{" && $hsh_level++ == 0;
363             # Deal with parentheses; always the highest level of operation, except when inside a square bracket.
364 4932 100 100     13605 if ($rc eq ")" && --$level == 0 && $sq_level == 0 && $hsh_level == 0) {
      100        
      100        
365 72         153 $start_space = $re;
366 72         620 push(@tokens, WWW::Shopify::Liquid::Token::Grouping->new($line, $self->parse_expression($line, substr($exp, $start_paren, $rs - $start_paren))));
367             }
368 4932 100       10155 --$sq_level if $rc eq "]";
369 4932 100       10005 --$hsh_level if $rc eq "}";
370 4932 100 100     23606 if (($level == 0 || ($rc eq "(" && $level == 1)) && $sq_level == 0 && $hsh_level == 0) {
      100        
      100        
371             # If we're only spaces, that means we're a new a token.
372 4462 100 100     17112 if ($rc eq "(" || defined $whitespace || $nword_op) {
      100        
373 4030 50       8573 if (defined $start_space) {
374 4030         9863 my $contents = substr($exp, $start_space, $rs - $start_space);
375 4030 100       21118 push(@tokens, $self->parse_token([$line->[0], $line->[1] + $start_space, $line->[2] + $start_space, $line->[4]], $contents)) if $contents !~ m/^\s*$/;
376             }
377 4030 100       11406 push(@tokens, $self->parse_token([$line->[0], $line->[1] + $start_space, $line->[2] + $start_space, $line->[4]], $nword_op)) if $nword_op;
378 4030         7327 $start_space = $re;
379             }
380             }
381             }
382 5728 100 100     13503 $squot = !$squot if ($rc eq "'" && !$dquot);
383 5728 100 100     47477 $dquot = !$dquot if ($rc eq '"' && !$squot);
384             }
385 1035 50       3368 die WWW::Shopify::Liquid::Exception::Lexer::UnbalancedBrace->new($line) unless $level == 0;
386             # Go through and combine any -1 from OP NUM to NUM.
387             my @ids = grep {
388 1035         2982 $tokens[$_]->isa('WWW::Shopify::Liquid::Token::Number') &&
389 1778 100 66     11760 $tokens[$_-1]->isa('WWW::Shopify::Liquid::Token::Operator') && $tokens[$_-1]->{core} eq "-" &&
      66        
      100        
      100        
390             ($_ == 1 || $tokens[$_-2]->isa('WWW::Shopify::Liquid::Token::Separator') || $tokens[$_-2]->isa('WWW::Shopify::Liquid::Token::Operator'))
391             } 1..$#tokens;
392 1035         2878 for (@ids) { $tokens[$_]->{core} *= -1; $tokens[$_-1] = undef; }
  3         11  
  3         20  
393 1035         2220 @tokens = grep { defined $_ } @tokens;
  2813         6811  
394             # Go through and combine colon separated arguments into named arguments.
395             @ids = grep {
396 1035         2402 ($_ == 2 || !$tokens[$_-3]->isa('WWW::Shopify::Liquid::Token::Operator') || $tokens[$_-3]->{core} ne "|") &&
397 573         6870 $tokens[$_-2]->isa('WWW::Shopify::Liquid::Token::Variable') && int(@{$tokens[$_-2]->{core}}) == 1 && $tokens[$_-2]->{core}->[0]->isa('WWW::Shopify::Liquid::Token::String') &&
398 1195 100 100     10491 $tokens[$_-1]->isa('WWW::Shopify::Liquid::Token::Separator') && $tokens[$_-1]->{core} eq ":" &&
      100        
      100        
      66        
      100        
      100        
399             $tokens[$_]->isa('WWW::Shopify::Liquid::Token::Operand')
400             } (2..$#tokens);
401 1035         2584 for (@ids) { $tokens[$_] = WWW::Shopify::Liquid::Token::Variable::Named->new($line, $tokens[$_-2]->{core}->[0]->{core}, $tokens[$_]); $tokens[$_-2] = undef; $tokens[$_-1] = undef; }
  18         97  
  18         75  
  18         75  
402 1035         1969 @tokens = grep { defined $_ } @tokens;
  2810         6167  
403 1035         6169 return @tokens;
404             }
405              
406             sub parse_text {
407 362     362 0 225724 my ($self, $text) = @_;
408 362 50       1425 return () unless defined $text;
409 362         990 my @tokens = ();
410            
411 362         823 my $line = 1;
412 362         773 my $line_position = 0;
413            
414 362         2357 my $lexing_halter;
415             my $open_control_tag;
416 362         0 my $open_output_tag;
417 362         0 my $open_single_quote;
418 362         0 my $open_double_quote;
419 362         0 my $strip_left;
420            
421 362         728 my $end_offset = 0;
422 362         1091 my $start_position = [1, 0, 0];
423            
424             # This is a substitution instead of a simple match, because very long perl native UTF8 strings, are variable length
425             # meaning any substr, or index operations need to walk the string in order to complete. This is like an o(n^2) consequence
426             # so it's fine at smaller lengths, but at larger lengths, it's infeasible to call substr on far offsets with any significant
427             # frequency. So we eat the string as we process it. Possibly slightly slower than simple matching on byte-strings, but way
428             # faster on UTF-8, and at this point in my life, I just want consistency rather than best-possible-performance (this would because
429             # written in D -betterC if I was interested in best possible performance at this point.)
430 362         904 my $accumulator = '';
431             # Step 1. We only care about new lines (to count what line we're on), {% and {{.
432 362         722 my $count = 0;
433 37     37   52878 use Time::HiRes qw(time);
  37         47432  
  37         210  
434 362         1552 my $start = time;
435 362         8485 while ($text =~ s/^(.*?)(?:(\{\{\-?)|({%\-?))//s) {
436 1318         5055 $end_offset += $+[0];
437             # These variables are entirely boolean flags.
438 1318         5016 my ($start_output_tag, $start_control_tag) = ($2, $3);
439 1318         3627 $accumulator .= $1;
440 1318 100       3559 if ($accumulator ne '') {
441             # Count the number of lines in the accumulator, to ensure we have the approrpiate line position.
442 541         1960 $line += ($accumulator =~ tr/\r?\n//);
443 541         1611 $line_position = rindex($accumulator, "\n") + $start_position->[2] + 1;
444             }
445 1318 100       5865 $open_control_tag = [$line, $end_offset - length($start_control_tag) - $line_position, $end_offset - length($start_control_tag), $self->file_context] if $start_control_tag;
446 1318 100       4546 $open_output_tag = [$line, $end_offset - length($start_output_tag) - $line_position, $end_offset - length($start_output_tag), $self->file_context] if $start_output_tag;
447 1318   66     5015 $strip_left = index($start_control_tag || $start_output_tag, "-") != -1;
448            
449 1318 100 100     6022 $accumulator =~ s/^\s*// if int(@tokens) > 0 && $tokens[-1]->{strip_right};
450 1318 100       3311 $accumulator =~ s/\s*$// if $strip_left;
451 1318 100       4324 if ($accumulator ne '') {
452             # Count the number of lines in the accumulator.
453 511         2144 push(@tokens, WWW::Shopify::Liquid::Token::Text->new($start_position, $accumulator));
454 511         1125 $accumulator = '';
455             }
456 1318 50       3201 last if $text eq '';
457 1318         4053 $start_position = [$line, $end_offset - $line_position, $end_offset, $self->file_context];
458            
459             # Step 2. If we have an open tag, we parse out tokens from argument context inside that tag.
460 1318         18951 while ($text =~ s/(.*?)(?:(\")|(\')|(\-?%})|(\-?\}\})|(\r?\n))//s) {
461 1943         8332 my ($double_quote, $single_quote, $end_control_tag, $end_output_tag, $newline) = ($2, $3, $4, $5, $6);
462 1943         5154 $end_offset += $+[0];
463            
464 1943 100 100     12337 if (!$open_single_quote && !$open_double_quote && ($end_output_tag || $end_control_tag)) {
      100        
      100        
465 1318   66     4928 my $strip_right = index($end_control_tag || $end_output_tag, "-") != -1;
466 1318         3666 $accumulator .= $1;
467 1318 100       2940 if ($end_output_tag) {
468 367 100       919 die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedControlTag($open_control_tag) if $open_control_tag;
469 366         1320 push(@tokens, WWW::Shopify::Liquid::Token::Output->new($open_output_tag, [$self->parse_expression($start_position, $accumulator)], $strip_left, $strip_right));
470 366         923 $open_output_tag = undef;
471             } else {
472 951 50       2399 die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedOutputTag($open_output_tag) if $open_output_tag;
473 951 50       5835 die new WWW::Shopify::Liquid::Exception::Lexer::Tag($start_position, $accumulator) if $accumulator !~ m/^\s*(\w+)\s*(.*?)\s*$/s;
474 951         3392 push(@tokens, WWW::Shopify::Liquid::Token::Tag->new($open_control_tag, $1, [$self->parse_expression($start_position, $2)], $strip_left, $strip_right));
475 951 100       3659 $lexing_halter = $tokens[-1] if $self->{lexing_halters}->{$1};
476 951         1882 $open_control_tag = undef;
477             }
478 1317         2614 $accumulator = '';
479 1317         4738 $start_position = [$line, $end_offset - $line_position, $end_offset, $self->file_context];
480 1317         3159 last;
481             } else {
482 625 100       1510 if ($newline) {
483 18         33 ++$line;
484 18         36 $line_position = $end_offset;
485             }
486 625 100 100     2005 $open_single_quote = !$open_single_quote if $single_quote && !$open_double_quote;
487 625 100 100     2016 $open_double_quote = !$open_double_quote if $double_quote && !$open_single_quote;
488 625         6115 $accumulator .= $&;
489             }
490             }
491             # For lexing halters, consume everything up to the next end tag explicitly.
492 1317 100       15944 if ($lexing_halter) {
493 11         25 my $core = $lexing_halter->{tag};
494 11         200 while ($text =~ s/(.*?)(?:({%\-?\s*end$core\s*\-?%})|(\r?\n))//s) {
495 23 100       68 if ($3) {
496 12         16 ++$line;
497 12         16 $line_position = $end_offset;
498 12         75 $accumulator .= $1 . $3;
499             } else {
500 11         28 $accumulator .= $1;
501 11 100       47 push(@tokens, WWW::Shopify::Liquid::Token::Text->new($start_position, $accumulator)) if $accumulator ne '';
502 11         53 push(@tokens, WWW::Shopify::Liquid::Token::Tag->new($start_position, "end$core", []));
503 11         29 $accumulator = '';
504 11         16 $lexing_halter = undef;
505 11         49 last;
506             }
507             }
508             }
509             }
510 361 50       1010 die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedSingleQuote($open_single_quote) if $open_single_quote;
511 361 50       1042 die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedDoubleQuote($open_double_quote) if $open_double_quote;
512 361 50       926 die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedLexingHalt($lexing_halter) if $lexing_halter;
513 361 50       950 die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedControlTag($open_control_tag) if $open_control_tag;
514 361 50       982 die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedOutputTag($open_output_tag) if $open_output_tag;
515 361         999 $accumulator .= $text;
516 361 100 100     2064 $accumulator =~ s/^\s*// if int(@tokens) > 0 && $tokens[-1]->{strip_right};
517 361 100       1272 push(@tokens, WWW::Shopify::Liquid::Token::Text->new($start_position, $accumulator)) if $accumulator ne '';
518 361         4762 return @tokens;
519             }
520              
521             sub unparse_token {
522 507     507 0 1375 my ($self, $token) = @_;
523 507 50       1177 return "null" if !defined $token;
524 507 50       1588 return '' if $token eq '';
525 507 50 66     1854 return "'" . do { $token =~ s/'/\\'/g; $token } . "'" if !blessed($token) && !looks_like_number($token);
  1         5  
  1         8  
526 506 50       1767 return $token unless blessed($token);
527 506 100       2137 return "null" if $token->isa('WWW::Shopify::Liquid::Token::NULL');
528 501 100       1730 return '{}' if ($token->isa('WWW::Shopify::Liquid::Token::Hash'));
529 500 50       1658 return '[]' if ($token->isa('WWW::Shopify::Liquid::Token::Array'));
530             sub translate_variable {
531 215     215 0 571 my ($self, $token) = @_;
532             my $variable = join(".", map {
533             !$self->is_processed($_) ? (
534             $_->isa('WWW::Shopify::Liquid::Token::Variable') ? ('[' . translate_variable($self, $_) . ']') :
535             ($_->isa('WWW::Shopify::Liquid::Token::Variable::Processing') ? $_->{core}->name : $_->{core})
536 273 100       950 ) : $_
    100          
    100          
537 215         385 } @{$token->{core}});
  215         670  
538 215         740 $variable =~ s/\.\[/\[/g;
539 215         782 return $variable;
540             }
541 500 100       1738 return $token->{name} . ":" . $self->unparse_token($token->{core}) if ($token->isa('WWW::Shopify::Liquid::Token::Variable::Named'));
542 499 100       1847 return translate_variable($self, $token) if $token->isa('WWW::Shopify::Liquid::Token::Variable');
543 285 100       984 return "(" . join("", map { $self->unparse_token($_); } @{$token->{members}}) . ")" if $token->isa('WWW::Shopify::Liquid::Token::Grouping');
  3         17  
  1         3  
544 284 100       1321 return "'" . $token->{core} . "'" if $token->isa('WWW::Shopify::Liquid::Token::String');
545 214         837 return $token->{core};
546             }
547              
548             sub unparse_expression {
549 121     121 0 372 my ($self, @tokens) = @_;
550 121 100 100     314 my $a = join("", map { ($_ eq ":" || $_ eq ",") ? $_ : " " . $_; } grep { defined $_ } map { $self->unparse_token($_) } @tokens);
  503         2551  
  503         1402  
  503         1306  
551 121         662 $a =~ s/^ //;
552 121         656 return $a;
553             }
554              
555             sub unparse_text_segment {
556 454     454 0 1352 my ($self, $token) = @_;
557 454 100       1029 my $space = $self->unparse_spaces ? " " : "";
558 454 100       1526 return $token if $self->is_processed($token);
559 428 100       2200 if ($token->isa('WWW::Shopify::Liquid::Token::Tag')) {
560 151 100 66     736 return "{%" . $space . $token->{tag} . $space . "%}" if !$token->{arguments} || int(@{$token->{arguments}}) == 0;
  97         403  
561 97         366 return "{%" . $space . $token->{tag} . " " . $self->unparse_expression(@{$token->{arguments}}) . $space . "%}";
  97         382  
562             }
563 277 100       1103 return "{{" . $space . $self->unparse_expression(@{$token->{core}}) . $space . "}}" if $token->isa('WWW::Shopify::Liquid::Token::Output');
  24         116  
564 253         988 return $token->{core};
565             }
566              
567             sub unparse_text {
568 25     25 0 208 my ($self, @tokens) = @_;
569 25         90 return join('', grep { defined $_ } map { $self->unparse_text_segment($_) } @tokens);
  454         1225  
  454         1224  
570             }
571              
572             1;