File Coverage

blib/lib/WWW/Shopify/Liquid/Lexer.pm
Criterion Covered Total %
statement 75 428 17.5
branch 0 250 0.0
condition 0 282 0.0
subroutine 25 70 35.7
pod 0 16 0.0
total 100 1046 9.5


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3 24     24   80478 use strict;
  24         55  
  24         836  
4 24     24   108 use warnings;
  24         42  
  24         1004  
5              
6             package WWW::Shopify::Liquid::Token;
7 24     24   106 use base 'WWW::Shopify::Liquid::Element';
  24         49  
  24         9778  
8 0     0     sub new { return bless { line => $_[1], core => $_[2] }, $_[0]; };
9 0     0     sub stringify { return $_[0]->{core}; }
10 0     0     sub tokens { return $_[0]; }
11              
12             package WWW::Shopify::Liquid::Token::Operator;
13 24     24   170 use base 'WWW::Shopify::Liquid::Token';
  24         39  
  24         8066  
14              
15             package WWW::Shopify::Liquid::Token::Operand;
16 24     24   181 use base 'WWW::Shopify::Liquid::Token';
  24         50  
  24         6679  
17              
18             package WWW::Shopify::Liquid::Token::String;
19 24     24   138 use base 'WWW::Shopify::Liquid::Token::Operand';
  24         52  
  24         8307  
20 0     0     sub process { my ($self, $hash) = @_; return $self->{core}; }
  0            
21              
22             package WWW::Shopify::Liquid::Token::Number;
23 24     24   153 use base 'WWW::Shopify::Liquid::Token::Operand';
  24         40  
  24         7496  
24 0     0     sub process { my ($self, $hash) = @_; return $self->{core}; }
  0            
25              
26             package WWW::Shopify::Liquid::Token::NULL;
27 24     24   140 use base 'WWW::Shopify::Liquid::Token::Operand';
  24         37  
  24         6854  
28 0     0     sub process { return undef; }
29              
30             package WWW::Shopify::Liquid::Token::Bool;
31 24     24   131 use base 'WWW::Shopify::Liquid::Token::Operand';
  24         40  
  24         6963  
32 0     0     sub process { my ($self, $hash) = @_; return $self->{core}; }
  0            
33              
34             package WWW::Shopify::Liquid::Token::Variable;
35 24     24   149 use base 'WWW::Shopify::Liquid::Token::Operand';
  24         52  
  24         6271  
36              
37 24     24   140 use Scalar::Util qw(looks_like_number reftype blessed);
  24         46  
  24         15318  
38              
39 0     0     sub new { my $package = shift; return bless { line => shift, core => [@_] }, $package; };
  0            
40             sub process {
41 0     0     my ($self, $hash, $action, $pipeline) = @_;
42 0           my $place = $hash;
43            
44 0           my @inner = @{$self->{core}};
  0            
45 0           my $unprocessed = 0;
46 0           foreach my $part_idx (0..$#inner) {
47 0           my $part = $inner[$part_idx];
48 0 0         if (ref($part) eq 'WWW::Shopify::Liquid::Token::Variable::Processing') {
49 0           $place = $part->$action($pipeline, $hash, $place);
50             }
51             else {
52 0 0         my $key = $self->is_processed($part) ? $part : $part->$action($pipeline, $hash);
53            
54 0 0 0       return $self unless defined $key && $key ne '';
55 0 0 0       $self->{core}->[$part_idx] = $key if $self->is_processed($key) && $action eq "optimize";
56 0 0         if (defined $place) {
57 0 0 0       if (reftype($place) && reftype($place) eq "HASH" && exists $place->{$key}) {
    0 0        
    0 0        
      0        
      0        
      0        
      0        
58 0           $place = $place->{$key};
59             } elsif (reftype($place) && reftype($place) eq "ARRAY" && looks_like_number($key) && defined $place->[$key]) {
60 0           $place = $place->[$key];
61             } elsif ($pipeline->make_method_calls && blessed($place) && $place->can($key)) {
62 0           $place = $place->$key;
63             } else {
64 0           $unprocessed = 1;
65 0           $place = undef;
66             }
67             }
68            
69             }
70             }
71 0 0         return $unprocessed ? $self : $place;
72             }
73 0     0     sub stringify { return join(".", map { $_->stringify } @{$_[0]->{core}}); }
  0            
  0            
74              
75             sub set {
76 0     0     my ($self, $pipeline, $hash, $value) = @_;
77 0 0         my @vars = map { $self->is_processed($_) ? $_ : $_->render($pipeline, $hash) } @{$self->{core}};
  0            
  0            
78 0           my ($reference) = $pipeline->variable_reference($hash, \@vars);
79 0           $$reference = $value;
80 0           return 1;
81             }
82              
83              
84             sub get {
85 0     0     my ($self, $pipeline, $hash) = @_;
86 0 0         my @vars = map { $self->is_processed($_) ? $_ : $_->render($pipeline, $hash) } @{$self->{core}};
  0            
  0            
87 0           my ($reference) = $pipeline->variable_reference($hash, \@vars, 1);
88 0 0         return $reference ? $$reference : undef;
89             }
90              
91             package WWW::Shopify::Liquid::Token::Variable::Processing;
92 24     24   170 use base 'WWW::Shopify::Liquid::Token::Operand';
  24         51  
  24         8389  
93             sub process {
94 0     0     my ($self, $hash, $argument, $action, $pipeline) = @_;
95 0 0         return $self if !$self->is_processed($argument);
96 0           my $result = $self->{core}->operate($hash, $argument);
97 0 0         return $self if !$self->is_processed($result);
98 0           return $result;
99             }
100              
101             package WWW::Shopify::Liquid::Token::Variable::Named;
102 24     24   143 use base 'WWW::Shopify::Liquid::Token::Operand';
  24         43  
  24         8431  
103              
104 0     0     sub new { my $package = shift; return bless { line => shift, name => shift, core => shift }, $package; };
  0            
105             sub process {
106 0     0     my ($self, $hash, $action, $pipeline) = @_;
107 0           return { $self->{name} => $self->{core}->$action($pipeline, $hash) };
108             }
109              
110              
111             package WWW::Shopify::Liquid::Token::Grouping;
112 24     24   189 use base 'WWW::Shopify::Liquid::Token::Operand';
  24         41  
  24         7800  
113 0     0     sub new { my $package = shift; return bless { line => shift, members => [@_] }, $package; };
  0            
114 0     0     sub members { return @{$_[0]->{members}}; }
  0            
115              
116             # Parentheses
117             package WWW::Shopify::Liquid::Token::Grouping::Parenthetical;
118 24     24   135 use base 'WWW::Shopify::Liquid::Token::Grouping';
  24         34  
  24         7867  
119              
120             # Like a grouping, but not really.
121             # Square brackets
122             package WWW::Shopify::Liquid::Token::Array;
123 24     24   148 use base 'WWW::Shopify::Liquid::Token::Operand';
  24         40  
  24         15374  
124              
125             sub new {
126 0     0     my ($package, $line, @members) = @_;
127             # Check to see whether or not the incoming member array is separated by commas.
128             # Should never begin with a comma, should never end with a comma, should always be
129             # 101010101 in terms of data and separators. If this is not the case, then we
130             # Should throw a lexing exception.
131             die new WWW::Shopify::Liquid::Exception::Lexer::InvalidSeparator($line) unless
132 0 0         int(grep { ($_ % 2) == 0 && $members[$_]->isa('WWW::Shopify::Liquid::Token::Separator') } (0..$#members)) == 0 &&
133 0 0 0       int(grep { ($_ % 2) == 1 && (!$members[$_]->isa('WWW::Shopify::Liquid::Token::Separator') || $members[$_]->{core} ne ",") } (0..$#members)) == 0;
  0 0 0        
134            
135             my $self = bless {
136             line => $line,
137 0           members => [grep { !$_->isa('WWW::Shopify::Liquid::Token::Separator') } @members]
  0            
138             }, $package;
139 0           return $self;
140             };
141              
142 0     0     sub members { return @{$_[0]->{members}}; }
  0            
143             sub process {
144 0     0     my ($self, $hash, $action, $pipeline) = @_;
145 0           my @members = $self->members;
146 0           $members[$_] = $members[$_]->$action($pipeline, $hash) for (grep { !$self->is_processed($members[$_]) } (0..$#members));
  0            
147 0 0         if ($action eq "optimize") {
148 0           $self->{members}->[$_] = $_ for (grep { $self->is_processed($members[$_]) } 0..$#members);
  0            
149             }
150 0           return [@members];
151             }
152              
153             # Curly brackets
154             package WWW::Shopify::Liquid::Token::Hash;
155 24     24   143 use base 'WWW::Shopify::Liquid::Token::Operand';
  24         43  
  24         14793  
156              
157 0     0     sub members { return @{$_[0]->{members}}; }
  0            
158             sub new {
159 0     0     my ($package, $line, @members) = @_;
160            
161             die new WWW::Shopify::Liquid::Exception::Lexer::InvalidSeparator($line) unless
162 0 0         int(grep { ($_ % 2) == 0 && $members[$_]->isa('WWW::Shopify::Liquid::Token::Separator') } (0..$#members)) == 0 &&
163             int(grep {
164             ($_ % 4) == 1 && (!$members[$_]->isa('WWW::Shopify::Liquid::Token::Separator') || $members[$_]->{core} ne ":") ||
165 0 0 0       ($_ % 4) == 3 && (!$members[$_]->isa('WWW::Shopify::Liquid::Token::Separator') || $members[$_]->{core} ne ",")
      0        
      0        
      0        
166             } (0..$#members)) == 0 &&
167 0 0 0       int(grep { !$_->isa('WWW::Shopify::Liquid::Token::Separator') } @members) % 2 == 0;
  0   0        
168            
169             return bless {
170             line => $line,
171 0           members => [grep { !$_->isa('WWW::Shopify::Liquid::Token::Separator') } @members]
  0            
172             }, $package;
173             };
174              
175             sub process {
176 0     0     my ($self, $hash, $action, $pipeline) = @_;
177 0           my @members = $self->members;
178 0           $members[$_] = $members[$_]->$action($pipeline, $hash) for (grep { !$self->is_processed($members[$_]) } (0..$#members));
  0            
179 0 0         if ($action eq "optimize") {
180 0           $self->{members}->[$_] = $_ for (grep { $self->is_processed($members[$_]) } 0..$#members);
  0            
181             }
182 0           return { @members };
183             }
184              
185              
186             package WWW::Shopify::Liquid::Token::Text;
187 24     24   173 use base 'WWW::Shopify::Liquid::Token::Operand';
  24         63  
  24         9529  
188             sub new {
189 0     0     my $self = { line => $_[1], core => $_[2] };
190 0           my $package = $_[0];
191 0 0 0       $package = 'WWW::Shopify::Liquid::Token::Text::Whitespace' if !defined $_[2] || $_[2] =~ m/^\s*$/;
192 0           return bless $self, $package;
193             };
194 0     0     sub process { my ($self, $hash) = @_; return $self->{core}; }
  0            
195              
196             package WWW::Shopify::Liquid::Token::Text::Whitespace;
197 24     24   152 use base 'WWW::Shopify::Liquid::Token::Text';
  24         42  
  24         7554  
198              
199             package WWW::Shopify::Liquid::Token::Tag;
200 24     24   134 use base 'WWW::Shopify::Liquid::Token';
  24         57  
  24         9703  
201 0     0     sub new { return bless { line => $_[1], tag => $_[2], arguments => $_[3], strip_left => $_[4], strip_right => $_[5] }, $_[0] };
202 0     0     sub tag { return $_[0]->{tag}; }
203 0     0     sub stringify { return $_[0]->tag; }
204              
205             package WWW::Shopify::Liquid::Token::Output;
206 24     24   148 use base 'WWW::Shopify::Liquid::Token';
  24         38  
  24         7723  
207 0     0     sub new { return bless { line => $_[1], core => $_[2], strip_left => $_[3], strip_right => $_[4] }, $_[0]; };
208              
209             package WWW::Shopify::Liquid::Token::Separator;
210 24     24   155 use base 'WWW::Shopify::Liquid::Token';
  24         53  
  24         6916  
211              
212              
213             package WWW::Shopify::Liquid::Lexer;
214 24     24   142 use base 'WWW::Shopify::Liquid::Pipeline';
  24         34  
  24         7752  
215 24     24   154 use Scalar::Util qw(looks_like_number blessed);
  24         43  
  24         24594  
216              
217 0     0 0   sub new { return bless { operators => {}, lexing_halters => {}, transparent_filters => {}, unparse_spaces => 0, parse_escaped_characters => 1 }, $_[0]; }
218 0     0 0   sub operators { return $_[0]->{operators}; }
219 0 0   0 0   sub unparse_spaces { $_[0]->{unparse_spaces} = $_[1] if defined $_[1]; return $_[0]->{unparse_spaces}; }
  0            
220 0 0   0 0   sub parse_escaped_characters { $_[0]->{parse_escaped_characters} = $_[1] if defined $_[1]; return $_[0]->{parse_escaped_characters}; }
  0            
221 0     0 0   sub register_operator { $_[0]->{operators}->{$_} = $_[1] for ($_[1]->symbol); }
222             sub register_tag {
223 0     0 0   my ($self, $package) = @_;
224 0 0 0       $self->{lexing_halters}->{$package->name} = $package if $package->is_enclosing && $package->inner_halt_lexing;
225             }
226             sub register_filter {
227 0     0 0   my ($self, $package) = @_;
228 0 0         $self->{transparent_filters}->{$package->name} = $package if ($package->transparent);
229             }
230 0     0 0   sub transparent_filters { return $_[0]->{transparent_filters}; }
231              
232             sub parse_token {
233 0     0 0   my ($self, $line, $token) = @_;
234            
235             # Strip token of whitespace.
236 0 0         return undef unless defined $token;
237 0           $token =~ s/^\s*(.*?)\s*$/$1/;
238 0 0         return WWW::Shopify::Liquid::Token::Operator->new($line, $token) if $self->operators->{$token};
239 0 0 0       return WWW::Shopify::Liquid::Token::String->new($line, do {
240 0           my $string = $1;
241 0 0         if ($self->parse_escaped_characters) {
242 0           $string =~ s/\\r/\r/g;
243 0           $string =~ s/\\n/\n/g;
244 0           $string =~ s/\\t/\t/g;
245             }
246 0           $string;
247             }) if $token =~ m/^'(.*)'$/s || $token =~ m/^"(.*)"$/s;
248 0 0         return WWW::Shopify::Liquid::Token::Number->new($line, $1) if looks_like_number($token);
249 0 0 0       return WWW::Shopify::Liquid::Token::NULL->new() if $token eq '' || $token eq 'null';
250 0 0 0       return WWW::Shopify::Liquid::Token::Separator->new($line, $token) if ($token eq ":" || $token eq ",");
251 0 0         return WWW::Shopify::Liquid::Token::Array->new($line) if ($token eq "[]");
252 0 0         return WWW::Shopify::Liquid::Token::Hash->new($line) if ($token eq "{}");
253             # We're a variable. Let's see what's going on. Split along non quoted . and [ ] fields.
254 0           my ($squot, $dquot, $start, @parts) = (0,0,0);
255             # customer['test']['b']
256 0           my $open_square_bracket = 0;
257 0           my $open_curly_bracket = 0;
258 0           while ($token =~ m/(\.|\[|\]|\{|\}|(?<!\\)\"|(?<!\\)\'|\b$)/g) {
259 0           my $sym = $&;
260 0           my $begin = $-[0];
261 0           my $end = $+[0];
262 0 0 0       if (!$squot && !$dquot) {
263 0 0 0       $open_square_bracket-- if ($sym && $sym eq "]");
264 0 0 0       $open_curly_bracket-- if ($sym && $sym eq "}");
265 0 0 0       if (($sym eq "." || $sym eq "]" || $sym eq "[" || $sym eq "{" || $sym eq "}" || !$sym) && $open_square_bracket == 0 && $open_curly_bracket == 0) {
      0        
      0        
266 0           my $contents = substr($token, $start, $begin - $start);
267            
268 0 0 0       if (defined $contents && $contents !~ m/^\s*$/) {
269 0           my @variables = ();
270 0 0 0       if (!$sym || $sym eq "." || $sym eq "[") {
    0 0        
    0          
271 0 0         @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);
272             }
273             elsif ($sym eq "]") {
274 0           @variables = $self->parse_expression($line, $contents);
275 0 0         return WWW::Shopify::Liquid::Token::Array->new($line, @variables) if (int(@parts) == 0);
276             }
277             elsif ($sym eq "}") {
278 0           @variables = $self->parse_expression($line, $contents);
279 0 0         return WWW::Shopify::Liquid::Token::Hash->new($line, @variables) if (int(@parts) == 0);
280             }
281 0 0         if (int(@variables) > 0) {
282 0 0         if (int(@variables) == 1) {
283 0           push(@parts, @variables);
284             }
285             else {
286 0           push(@parts, WWW::Shopify::Liquid::Token::Grouping->new($line, @variables)) ;
287             }
288             }
289             }
290             }
291 0 0 0       $start = $end if $sym ne '"' && $sym ne "'" && !$open_curly_bracket && !$open_square_bracket;
      0        
      0        
292 0 0 0       $open_square_bracket++ if ($sym && $sym eq "[");
293 0 0 0       $open_curly_bracket++ if ($sym && $sym eq "{");
294            
295             }
296 0 0         $squot = !$squot if $token eq "'";
297 0 0         $dquot = !$dquot if $token eq '"';
298             }
299 0           return WWW::Shopify::Liquid::Token::Variable->new($line, @parts);
300             }
301              
302 24     24   11662 use utf8;
  24         221  
  24         185  
303             # Returns a single token repsending the whole a expression.
304             sub parse_expression {
305 0     0 0   my ($self, $line, $exp) = @_;
306 0 0 0       return () if !defined $exp || $exp eq '';
307 0           my @tokens = ();
308 0           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);
309             # We regex along parentheses, quotation marks (both kinds), whitespace, and non-word-operators.
310             # 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 (=, ==)
311             # There's no confusion, we always try to match the largest first.
312 0           my $non_word_operators = join("|", map { quotemeta($_) } grep { $_ =~ m/^\W+$/; } sort { length($b) <=> length($a) } keys(%{$self->operators}));
  0            
  0            
  0            
  0            
313 0           while ($exp =~ m/(?:\(|\)|\]|\[|\}|\{|(?<!\\)["”“]|(?<!\\)['‘’]|(\s+|$)|($non_word_operators|,|:))/sg) {
314 0           my ($rs, $re, $rc, $whitespace, $nword_op) = ($-[0], $+[0], $&, $1, $2);
315             # Specifically to allow variables to have a - in them, and be treated as a long literal, instead of a minus sign.
316             # This is terrible behaviour, but mimics Shopify's lexer.
317             # Only if of course the entire first half of this ISN'T a number, though. 'cause that'd be insane.
318 0 0 0       next if $nword_op && $nword_op eq "-" && $rs > 0 && substr($exp, $rs-1, 1) ne " " && substr($exp, 0, $rs) !~ m/\b\d+$/;
      0        
      0        
      0        
319 0 0 0       if (!$squot && !$dquot) {
320 0 0 0       $start_paren = $re if $rc eq "(" && $level++ == 0;
321 0 0 0       $start_sq = $re if $rc eq "[" && $sq_level++ == 0;
322 0 0 0       $start_hsh = $re if $rc eq "{" && $hsh_level++ == 0;
323             # Deal with parentheses; always the highest level of operation, except when inside a square bracket.
324 0 0 0       if ($rc eq ")" && --$level == 0 && $sq_level == 0) {
      0        
325 0           $start_space = $re;
326 0           push(@tokens, WWW::Shopify::Liquid::Token::Grouping->new($line, $self->parse_expression($line, substr($exp, $start_paren, $rs - $start_paren))));
327             }
328 0 0         --$sq_level if $rc eq "]";
329 0 0         --$hsh_level if $rc eq "}";
330 0 0 0       if ($level == 0 && $sq_level == 0 && $hsh_level == 0) {
      0        
331             # If we're only spaces, that means we're a new a token.
332 0 0 0       if (defined $whitespace || $nword_op) {
333 0 0         if (defined $start_space) {
334 0           my $contents = substr($exp, $start_space, $rs - $start_space);
335 0 0         push(@tokens, $self->parse_token($line, $contents)) if $contents !~ m/^\s*$/;
336             }
337 0 0         push(@tokens, $self->parse_token($line, $nword_op)) if $nword_op;
338 0           $start_space = $re;
339             }
340             }
341             }
342 0 0 0       $squot = !$squot if ($rc eq "'" && !$dquot);
343 0 0 0       $dquot = !$dquot if ($rc eq '"' && !$squot);
344             }
345 0 0         die WWW::Shopify::Liquid::Exception::Lexer::UnbalancedBrace->new($line) unless $level == 0;
346             # Go through and combine any -1 from OP NUM to NUM.
347             my @ids = grep {
348 0           $tokens[$_]->isa('WWW::Shopify::Liquid::Token::Number') &&
349 0 0 0       $tokens[$_-1]->isa('WWW::Shopify::Liquid::Token::Operator') && $tokens[$_-1]->{core} eq "-" &&
      0        
      0        
      0        
350             ($_ == 1 || $tokens[$_-2]->isa('WWW::Shopify::Liquid::Token::Separator') || $tokens[$_-2]->isa('WWW::Shopify::Liquid::Token::Operator'))
351             } 1..$#tokens;
352 0           for (@ids) { $tokens[$_]->{core} *= -1; $tokens[$_-1] = undef; }
  0            
  0            
353 0           @tokens = grep { defined $_ } @tokens;
  0            
354             # Go through and combine colon separated arguments into named arguments.
355             @ids = grep {
356 0           ($_ == 2 || !$tokens[$_-3]->isa('WWW::Shopify::Liquid::Token::Operator') || $tokens[$_-3]->{core} ne "|") &&
357 0           $tokens[$_-2]->isa('WWW::Shopify::Liquid::Token::Variable') && int(@{$tokens[$_-2]->{core}}) == 1 && $tokens[$_-2]->{core}->[0]->isa('WWW::Shopify::Liquid::Token::String') &&
358 0 0 0       $tokens[$_-1]->isa('WWW::Shopify::Liquid::Token::Separator') && $tokens[$_-1]->{core} eq ":" &&
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
359             ($tokens[$_]->isa('WWW::Shopify::Liquid::Token::Grouping') || $tokens[$_]->isa('WWW::Shopify::Liquid::Token::Variable') || $tokens[$_]->isa('WWW::Shopify::Liquid::Token::Number') || $tokens[$_]->isa('WWW::Shopify::Liquid::Token::String'))
360             } (2..$#tokens);
361 0           for (@ids) { $tokens[$_] = WWW::Shopify::Liquid::Token::Variable::Named->new($line, $tokens[$_-2]->{core}->[0]->{core}, $tokens[$_]); $tokens[$_-2] = undef; $tokens[$_-1] = undef; }
  0            
  0            
  0            
362 0           @tokens = grep { defined $_ } @tokens;
  0            
363 0           return @tokens;
364             }
365              
366             sub parse_text {
367 0     0 0   my ($self, $text) = @_;
368 0 0         return () unless defined $text;
369 0           my @tokens = ();
370 0           my $start = 0;
371 0           my $line = 1;
372 0           my $column = 0;
373 0           my $lexing_halter = undef;
374            
375 0           my $start_line = 1;
376 0           my $start_column = 0;
377 0           my $line_position = 0;
378            
379 0 0         return (WWW::Shopify::Liquid::Token::Text->new(1, '')) if $text eq '';
380            
381             # No need to worry about quotations; liquid overrides everything.
382             # We specifically look for tags within tags due to lexing halters abilities to be placed pretty much anywhere.
383             # Oh god no, we have to rewrite this so nested output braces inside strings inside output braces are parsed properly.
384             #while ($text =~ m/(?:(?:{%\s*(\w+)\s*(.*?)(?:\s*%}|\s*(?={%)))|(?:{{\s*(.*?)\s*}})|(\n)|$)/sg) {
385 0           my $open_tag = undef;
386 0           my $open_output_tag = undef;
387 0           my $open_single_quote = undef;
388 0           my $open_double_quote = undef;
389 0           my $start_tag_strip = undef;
390 0           my $end_tag_strip = undef;
391 0           my @lines = split(/\n/, $text);
392 0           while ($text =~ m/(?:(?:({%\-?)|(\-?%}))|(?:(\{\{\-?)|(\-?\}\}))|(\n)|(")|(')|$)/sg) {
393 0           my ($start_tag, $end_tag, $start_output_tag, $end_output_tag, $newline, $double_quote, $single_quote) = ($1, $2, $3, $4, $5, $6, $7);
394 0 0 0       next if ($double_quote || $single_quote) && !$open_tag && !$open_output_tag;
      0        
      0        
395 0           my $column = $+[0] - $line_position;
396 0           my $position = [$start_line, $start_column, $-[0], $self->file_context];
397 0           my $end_position = [$start_line, $start_column, $+[0], $self->file_context];
398            
399 0 0 0       if ($start_output_tag || $start_tag) {
    0 0        
400 0   0       $start_tag_strip = index(($start_output_tag || $start_tag), "-") != -1;
401 0           $end_tag_strip = undef;
402             } elsif ($end_output_tag || $end_tag) {
403 0   0       $end_tag_strip = index(($end_output_tag || $end_tag), "-") != -1;
404             }
405            
406 0 0         if (!$lexing_halter) {
407 0 0 0       if ($double_quote || $single_quote) {
408 0 0 0       if (!$open_double_quote && $single_quote) {
    0 0        
409 0 0         $open_single_quote = $open_single_quote ? undef : $position;
410             } elsif (!$open_single_quote && $double_quote) {
411 0 0         $open_double_quote = $open_double_quote ? undef : $position;
412             }
413 0           next;
414             }
415 0 0 0       next if $open_single_quote || $open_double_quote;
416            
417 0 0 0       die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedTag([$line, ($-[0] - $line_position), $-[0]]) if ($end_tag && !defined $open_tag);
418             # Unbalanced outputs tags shouldn't throw a lexer exception, due to CSS rules. We just treat this as part of text.
419             # die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedOutputTag($position) if $end_output_tag && !defined $open_output_tag;
420 0 0 0       next if $end_output_tag && !defined $open_output_tag;
421             }
422 0 0         if ($newline) {
423 0           ++$line;
424 0           $line_position = $+[0];
425 0           next;
426             }
427 0 0         if ($start_tag) {
428 0           $open_tag = $position;
429 0           next;
430             }
431 0 0         if ($start_output_tag) {
432 0           $open_output_tag = $position;
433 0           next;
434             }
435            
436 0           my ($tag, $arguments, $output);
437 0           my $start_element = undef;
438 0           my $end_element = undef;
439 0 0 0       if ($end_tag && $open_tag) {
    0 0        
440 0           my $tag_line = substr($text, $open_tag->[2]+2, $-[0] - $open_tag->[2] - 2);
441 0           $start_element = $open_tag;
442 0           $end_element = $end_position;
443 0           $open_tag = undef;
444 0 0         die new WWW::Shopify::Liquid::Exception::Lexer::Tag($start_element, $tag_line) unless $tag_line =~ m/\s*(\w+)\s*(.*?)\s*$/s;
445 0           ($tag, $arguments) = ($1, $2);
446             } elsif ($end_output_tag && $open_output_tag) {
447 0           $start_element = $open_output_tag;
448 0           $end_element = $end_position;
449 0           $output = substr($text, $open_output_tag->[2]+2, $-[0] - $open_output_tag->[2] - 2);
450 0           $output =~ s/(^\s*|\s*$)//g;
451 0           $open_output_tag = undef;
452             } else {
453 0           $start_element = $position;
454 0           $end_element = $end_position;
455             }
456            
457 0 0 0       if ($tag && !$lexing_halter && exists $self->{lexing_halters}->{$tag}) {
    0 0        
      0        
      0        
458 0           $lexing_halter = $tag;
459 0 0         push(@tokens, WWW::Shopify::Liquid::Token::Text->new($position, substr($text, $start, $start_element->[2] - $start), $start_tag_strip, $end_tag_strip)) if ($start < $start_element->[2]);
460 0           push(@tokens, WWW::Shopify::Liquid::Token::Tag->new($position, $tag, [$self->parse_expression($position, $arguments)], $start_tag_strip, $end_tag_strip));
461 0           $start = $end_element->[2];
462 0           $start_line = $line;
463 0           $start_column = $start_element->[2] - $line_position;
464 0           $position = [$start_line, $start_column, $start_element->[2], $self->file_context];
465 0           $start_element = $position;
466             }
467             elsif ($tag && $lexing_halter && $tag eq "end" . $lexing_halter) {
468 0           $lexing_halter = undef;
469             }
470 0 0         if (!$lexing_halter) {
471 0 0         if ($start < $start_element->[2]) {
472 0           push(@tokens, WWW::Shopify::Liquid::Token::Text->new([$start_line, $start_column, $start, $self->file_context], substr($text, $start, $start_element->[2] - $start), $start_tag_strip, $end_tag_strip));
473             # Strip whitespace.
474 0 0 0       if (int(@tokens) > 1 && $tokens[-1]->isa('WWW::Shopify::Liquid::Token::Text') && ($tokens[-2]->isa('WWW::Shopify::Liquid::Token::Tag') || $tokens[-2]->isa('WWW::Shopify::Liquid::Token::Output')) && $tokens[-2]->{strip_right}) {
      0        
      0        
      0        
475 0 0         if ($tokens[-1]->isa('WWW::Shopify::Liquid::Token::Text::Whitespace')) {
476 0           splice(@tokens, -1, 1);
477             } else {
478 0           $tokens[-1]->{core} =~ s/^\s+//ms;
479             }
480             }
481            
482 0           $start_line = $line;
483 0           $start_column = $start_element->[2] - $line_position;
484 0           $position = [$start_line, $start_column, $start_element->[2], $self->file_context];
485 0           $start_element = $position;
486             }
487 0 0         push(@tokens, WWW::Shopify::Liquid::Token::Tag->new($start_element, $tag, [$self->parse_expression($position, $arguments)], $start_tag_strip, $end_tag_strip)) if $tag;
488 0 0         push(@tokens, WWW::Shopify::Liquid::Token::Output->new($start_element, [$self->parse_expression($position, $output)], $start_tag_strip, $end_tag_strip)) if $output;
489 0           $start = $end_element->[2];
490 0           $start_line = $line;
491 0           $start_column = $column;
492             }
493             # Strip whitespace.
494 0 0 0       if ($tag && $start_tag_strip && int(@tokens) > 1 && $tokens[-2]->isa('WWW::Shopify::Liquid::Token::Text')) {
      0        
      0        
495 0 0         if ($tokens[-2]->isa('WWW::Shopify::Liquid::Token::Text::Whitespace')) {
496 0           splice(@tokens, -2, 1);
497             } else {
498 0           $tokens[-2]->{core} =~ s/\s+$//ms;
499             }
500             }
501            
502             }
503 0 0         die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedSingleQuote($open_single_quote) if $open_single_quote;
504 0 0         die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedDoubleQuote($open_double_quote) if $open_double_quote;
505 0 0         die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedTag($open_tag) if $open_tag;
506 0 0         die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedLexingHalt() if $lexing_halter;
507 0 0         die new WWW::Shopify::Liquid::Exception::Lexer::UnbalancedOutputTag($open_output_tag) if $open_output_tag;
508 0           return @tokens;
509             }
510              
511             sub unparse_token {
512 0     0 0   my ($self, $token) = @_;
513 0 0         return "null" if !defined $token;
514 0 0         return '' if $token eq '';
515 0 0 0       return "'" . do { $token =~ s/'/\\'/g; $token } . "'" if !blessed($token) && !looks_like_number($token);
  0            
  0            
516 0 0         return $token unless blessed($token);
517 0 0         return "null" if $token->isa('WWW::Shopify::Liquid::Token::NULL');
518             sub translate_variable {
519 0     0 0   my ($self, $token) = @_;
520             my $variable = join(".", map {
521             !$self->is_processed($_) ? (
522             $_->isa('WWW::Shopify::Liquid::Token::Variable') ? ('[' . translate_variable($self, $_) . ']') : $_->{core}
523 0 0         ) : $_
    0          
524 0           } @{$token->{core}});
  0            
525 0           $variable =~ s/\.\[/\[/g;
526 0           return $variable;
527             }
528            
529 0 0         return translate_variable($self, $token) if $token->isa('WWW::Shopify::Liquid::Token::Variable');
530 0 0         return "(" . join("", map { $self->unparse_token($_); } @{$token->{members}}) . ")" if $token->isa('WWW::Shopify::Liquid::Token::Grouping');
  0            
  0            
531 0 0         return "'" . $token->{core} . "'" if $token->isa('WWW::Shopify::Liquid::Token::String');
532 0           return $token->{core};
533             }
534              
535             sub unparse_expression {
536 0     0 0   my ($self, @tokens) = @_;
537 0 0 0       my $a = join("", map { ($_ eq ":" || $_ eq ",") ? $_ : " " . $_; } grep { defined $_ } map { $self->unparse_token($_) } @tokens);
  0            
  0            
  0            
538 0           $a =~ s/^ //;
539 0           return $a;
540             }
541              
542             sub unparse_text_segment {
543 0     0 0   my ($self, $token) = @_;
544 0 0         my $space = $self->unparse_spaces ? " " : "";
545 0 0         return $token if $self->is_processed($token);
546 0 0         if ($token->isa('WWW::Shopify::Liquid::Token::Tag')) {
547 0 0 0       return "{%" . $space . $token->{tag} . $space . "%}" if !$token->{arguments} || int(@{$token->{arguments}}) == 0;
  0            
548 0           return "{%" . $space . $token->{tag} . " " . $self->unparse_expression(@{$token->{arguments}}) . $space . "%}";
  0            
549             }
550 0 0         return "{{" . $space . $self->unparse_expression(@{$token->{core}}) . $space . "}}" if $token->isa('WWW::Shopify::Liquid::Token::Output');
  0            
551 0           return $token->{core};
552             }
553              
554             sub unparse_text {
555 0     0 0   my ($self, @tokens) = @_;
556 0           return join('', grep { defined $_ } map { $self->unparse_text_segment($_) } @tokens);
  0            
  0            
557             }
558              
559             1;