File Coverage

blib/lib/WWW/Shopify/Liquid.pm
Criterion Covered Total %
statement 22 80 27.5
branch 0 44 0.0
condition 0 57 0.0
subroutine 8 25 32.0
pod n/a
total 30 206 14.5


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3 30     30   834020 use strict;
  30         45  
  30         919  
4 30     30   116 use warnings;
  30         45  
  30         1683  
5              
6             package WWW::Shopify::Liquid::Pipeline;
7 30     30   139 use Scalar::Util qw(weaken blessed looks_like_number);
  30         47  
  30         29323  
8       0     sub register_tag { }
9             sub register_operator {
10 0 0 0 0     die new WWW::Shopify::Liquid::Exception("Cannot have a unary operator, that has infix notation.") if $_[1]->arity eq "unary" && $_[1]->fixness eq "infix";
11             }
12       0     sub register_filter { }
13 0 0   0     sub strict { $_[0]->{strict} = $_[1] if defined $_[1]; return $_[0]->{strict}; }
  0            
14 0 0   0     sub file_context { $_[0]->{file_context} = $_[1] if @_ > 1; return $_[0]->{file_context}; }
  0            
15             sub parent {
16 0 0   0     if (defined $_[1]) {
17 0           $_[0]->{parent} = $_[1];
18 0           weaken($_[0]->{parent});
19             }
20 0           return $_[0]->{parent};
21             }
22              
23             sub is_processed {
24             return !ref($_[1]) ||
25             (ref($_[1]) eq "ARRAY" && int(grep { !$_[0]->is_processed($_) } @{$_[1]}) == 0) ||
26 0   0 0     (ref($_[1]) eq "HASH" && int(grep { !$_[0]->is_processed($_[1]->{$_}) } keys(%{$_[1]})) == 0) ||
27             (blessed($_[1]) && ref($_[1]) !~ m/^WWW::Shopify::Liquid/ && !$_[1]->isa('WWW::Shopify::Liquid::Element'));
28             }
29              
30             # If static is true, we do not create new indices, we return null.
31             sub variable_reference {
32 0     0     my ($self, $hash, $indices, $static) = @_;
33 0           my @vars = @$indices;
34 0           my $inner_hash = $hash;
35 0           for (0..$#vars-1) {
36 0 0 0       if (looks_like_number($vars[$_]) && ref($inner_hash) && ref($inner_hash) eq "ARRAY") {
      0        
37 0 0         if (!defined $inner_hash->[$vars[$_]]) {
38 0 0         return () if $static;
39 0           $inner_hash->[$vars[$_]] = {};
40             }
41 0           $inner_hash = $inner_hash->[$vars[$_]];
42             } else {
43 0 0         if (!exists $inner_hash->{$vars[$_]}) {
44 0 0         return () if $static;
45 0           $inner_hash->{$vars[$_]} = {};
46             }
47 0           $inner_hash = $inner_hash->{$vars[$_]};
48             }
49             }
50 0 0 0       if (looks_like_number($vars[-1]) && ref($inner_hash) && ref($inner_hash) eq "ARRAY") {
      0        
51 0 0 0       return (\$inner_hash->[$vars[-1]], $inner_hash) if int(@$inner_hash) > $vars[-1] || !$static;
52             } else {
53 0 0 0       return (\$inner_hash->{$vars[-1]}, $inner_hash) if exists $inner_hash->{$vars[-1]} || !$static;
54             }
55 0           return ();
56             }
57              
58 0 0   0     sub make_method_calls { $_[0]->{make_method_calls} = $_[1] if @_ > 1; return $_[0]->{make_method_calls}; }
  0            
59              
60             package WWW::Shopify::Liquid::Element;
61              
62 0     0     sub verify { return 1; }
63             sub render {
64 0     0     my $self = shift;
65 0           my $renderer = shift;
66 0 0 0       die new WWW::Shopify::Liquid::Exception(undef, "Cannot render without a valid renderer.")
      0        
67             unless $renderer && blessed($renderer) && $renderer->isa('WWW::Shopify::Liquid::Renderer');
68 0           my $return;
69             my $exp;
70 0 0         if ($renderer->{silence_exceptions}) {
71 0           $return = eval { $self->process(@_, "render", $renderer); };
  0            
72 0           $exp = $@;
73 0 0 0       die $exp if (blessed($exp) && $exp->isa('WWW::Shopify::Liquid::Exception::Control'));
74             } else {
75 0           $return = $self->process(@_, "render", $renderer);
76             }
77 0 0 0       return undef if $exp || !$self->is_processed($return) || !defined $return;
      0        
78 0           return $return;
79             }
80              
81             sub get_parameter {
82 0     0     my ($self, $name, @arguments) = @_;
83 0 0 0       my ($arg) = grep { ref($_) && ref($_) eq 'HASH' && int(keys(%$_)) == 1 && exists $_->{$name} } @arguments;
  0   0        
84 0 0         return $arg ? $arg->{$name} : undef;
85             }
86              
87             sub optimize {
88 0     0     my $self = shift;
89 0           my $optimizer = shift;
90 0 0 0       die new WWW::Shopify::Liquid::Exception("Cannot optimize without a valid optimizer.")
      0        
91             unless $optimizer && blessed($optimizer) && $optimizer->isa('WWW::Shopify::Liquid::Optimizer');
92 0           return $self->process(@_, "optimize", $optimizer);
93             }
94 0     0     sub process { return $_[0]; }
95             # Determines whether or not the element is part of the strict subset of liquid that Shopify uses.
96 0     0     sub is_strict { return 0; }
97              
98 30     30   181 use Scalar::Util qw(looks_like_number blessed);
  30         43  
  30         4003  
99              
100 0     0     sub is_processed { return WWW::Shopify::Liquid::Pipeline->is_processed($_[1]); }
101             sub ensure_numerical {
102 0 0 0 0     return $_[1] if defined $_[1] && looks_like_number($_[1]);
103 0 0 0       return $_[1] if ref($_[1]) && ref($_[1]) eq "DateTime";
104 0           return 0;
105             }
106              
107             package WWW::Shopify::Liquid;
108 30     30   18995 use File::Slurp;
  30         286171  
  30         2334  
109 30     30   19121 use List::MoreUtils qw(firstidx part);
  30         270627  
  30         214  
110 30     30   34331 use Module::Find;
  30         32314  
  30         2887  
111              
112             our $VERSION = '0.06';
113              
114             =head1 NAME
115              
116             WWW::Shopify::Liquid - Fully featured liquid preprocessor with shopify tags & filters added in.
117              
118             =cut
119              
120             =head1 DESCRIPTION
121              
122             A concise and clear liquid processor. Runs a superset of what Shopify can do. For a strict shopify implementation
123             see L<Template::Liquid> for one that emulates all the quirks and ridiculousness of the real thing, but without the tags.
124             (Meaning no actual arithemtic is literal tags without filters, insanity on acutal number processing and conversion,
125             insane array handling, no real optimization, or partial AST reduction, etc.., etc..).
126              
127             Combines a lexer, parser, optimizer and a renderer. Can be invoked in any number of ways. Simplest is to use the sub this module exports,
128             liquid_render_file.
129              
130             use WWW::Shopify::Liquid qw/liquid_render_file/;
131            
132             $contents = liquid_render_file({ collection => { handle => "test" } }, "myfile.liquid");
133             print $contents;
134            
135             This is the simplest method. There are auxiliary methods which provide a lot of flexibility over how you render your
136             liquid (see below), and an OO interface.
137              
138             This method represents the whole pipeline, so to get an overview of this module, we'll describe it here.
139             Fundamentally, what liquid_render_file does, is it slurps the whole file into a string, and then passes that string to
140             the lexer. This then generates a stream of tokens. These tokens are then transformed into an abstract syntax tree, by the
141             the parser if the syntax is valid. This AST represents the canonical form of the file, and can, from here, either
142             transformed back into almost the same file, statically optimized to remove any unecessary calls, or partially optimized to
143             remove branches of the tree for which you have variables to fill at this time, though both these steps are optional.
144              
145             Finally, these tokens are passed to the renderer, which interprets the tokens and then produces a string representing the
146             final content that can be printed.
147              
148             Has better error handling than Shopify's liquid processor, so if you use this to validate your liquid, you should get better
149             errors than if you're simply submitting them. This module is integrated into the L<Padre::Plugin::Shopify> module, so if you
150             use Padre as your Shopify IDE, you can automatically check the liquid of the file you're currently looking at with the click
151             of a button.
152              
153             You can invoke each stage individually if you like.
154              
155             use WWW::Shopify::Liquid;
156             my $text = ...
157             my $liquid = WWW::Shopify::Liquid->new;
158             my @tokens = $liquid->lexer->parse_text($text);
159             my $ast = $liquid->parser->parse_tokens(@tokens);
160            
161             # Here we have a partial tree optimization. Meaning, if you have some of your
162             # variables, but not all of them, you can simplify the template.
163             $ast = $liquid->optimizer->optimize({ a => 2 }, $ast);
164            
165             # Finally, you can render.
166             $result = $liquid->renderer->render({ b => 3 }, $ast);
167            
168             If you're simply looking to check whether a liquid file is valid, you can do the following:
169              
170             use WWW::Shopify::Liquid qw/liquid_verify_file/;
171             liquid_verify_file("my-snippet.liquid");
172            
173             If sucessful, it'll return nothing, if it fails, it'll throw an exception, detailing the fault's location and description.
174              
175             =cut
176              
177             =head1 STATUS
178              
179             This module is currently in beta. That means that while it is able to parse and validate liquid documents from Shopify, it may
180             be missing a few tags. In addition to this, the optimizer is not yet fully complete; it does not do advanced optimizations such as loop
181             unrolling. However, it does do partial tree rendering. Essentially what's missing is the ability to generate liquid from syntax trees.
182              
183             This is close to complete, but not quite there yet. When done, this will be extremely beneficial to application proxies, as it will allow
184             the use of custom liquid syntax, with partial evaluation, before passing the remaining liquid back to Shopify for full evaluation. This
185             will allow you to do things like have custom tags that a user can customize which will be filled with your data, yet still allow Shopify to
186             evaluate stuff like asset_urls, includes, and whatnot.
187              
188             =cut
189              
190 30     30   15909 use WWW::Shopify::Liquid::Parser;
  0            
  0            
191             use WWW::Shopify::Liquid::Optimizer;
192             use WWW::Shopify::Liquid::Lexer;
193             use WWW::Shopify::Liquid::Renderer;
194             use WWW::Shopify::Liquid::Operator;
195             use WWW::Shopify::Liquid::Tag;
196             use Scalar::Util qw(blessed);
197              
198             sub new {
199             my $package = shift;
200             my $self = bless {
201             filters => [],
202             operators => [],
203             tags => [],
204            
205             lexer => WWW::Shopify::Liquid::Lexer->new,
206             parser => WWW::Shopify::Liquid::Parser->new,
207             optimizer => WWW::Shopify::Liquid::Optimizer->new,
208             renderer => WWW::Shopify::Liquid::Renderer->new,
209            
210             @_
211             }, $package;
212            
213             $self->lexer->parent($self) if $self->lexer;
214             $self->parser->parent($self) if $self->parser;
215             $self->optimizer->parent($self) if $self->optimizer;
216             $self->renderer->parent($self) if $self->renderer;
217            
218             $self->load_modules;
219            
220             return $self;
221             }
222              
223             sub load_modules {
224             my ($self) = @_;
225             $self->register_operator($_) for (findallmod WWW::Shopify::Liquid::Operator);
226             $self->register_filter($_) for (findallmod WWW::Shopify::Liquid::Filter);
227             $self->register_tag($_) for (findallmod WWW::Shopify::Liquid::Tag);
228             }
229              
230             sub lexer { return $_[0]->{lexer}; }
231             sub parser { return $_[0]->{parser}; }
232             sub optimizer { return $_[0]->{optimizer}; }
233             sub renderer { return $_[0]->{renderer}; }
234              
235             sub register_tag {
236             if (!$_[1]->abstract) {
237             push(@{$_[0]->tags}, $_[1]);
238             $_->register_tag($_[1]) for (grep { blessed($_) && $_->can('register_tag') } values(%{$_[0]}));
239             }
240             }
241             sub register_filter {
242             push(@{$_[0]->filters}, $_[1]);
243             $_->register_filter($_[1]) for (grep { blessed($_) && $_->can('register_filter') } values(%{$_[0]}));
244             }
245             sub register_operator {
246             WWW::Shopify::Liquid::Pipeline->register_operator($_[1]);
247             push(@{$_[0]->operators}, $_[1]);
248             $_->register_operator($_[1]) for (grep { blessed($_) && $_->can('register_operator') } values(%{$_[0]}));
249             }
250             sub tags { return $_[0]->{tags}; }
251             sub filters { return $_[0]->{filters}; }
252             sub operators { return $_[0]->{operators}; }
253             sub order_of_operations { return $_[0]->{order_of_operations}; }
254             sub free_tags { return $_[0]->{free_tags}; }
255             sub enclosing_tags { return $_[0]->{enclosing_tags}; }
256             sub processing_variables { return $_[0]->{processing_variables}; }
257             sub money_format { return $_[0]->{money_format}; }
258             sub money_with_currency_format { return $_[0]->{money_with_currency_format}; }
259             sub tag_list { return (keys(%{$_[0]->free_tags}), keys(%{$_[0]->enclosing_tags})); }
260              
261             sub operate { return $_[0]->operators->{$_[3]}->($_[0], $_[1], $_[2], $_[4]); }
262              
263             sub render_ast { my ($self, $hash, $ast) = @_; return $self->renderer->render($hash, $ast); }
264             sub unpack_ast { my ($self, $ast) = @_; return $self->parser->unparse_tokens($ast); }
265             sub unparse_text { my ($self, @tokens) = @_; return $self->lexer->unparse_text(@tokens); }
266             sub optimize_ast { my ($self, $hash, $ast) = @_; return $self->optimizer->optimize($hash, $ast); }
267             sub tokenize_text { my ($self, $text) = @_; return $self->lexer->parse_text($text); }
268             sub parse_tokens { my ($self, @tokens) = @_; return $self->parser->parse_tokens(@tokens); }
269             sub parse_text { my ($self, $text) = @_; return $self->parse_tokens($self->tokenize_text($text)); }
270              
271             sub verify_text { my ($self, $text) = @_; $self->parse_tokens($self->parse_text($text)); }
272             sub verify_file { my ($self, $file) = @_; $self->verify_text(scalar(read_file($file))); }
273             sub render_text { my ($self, $hash, $text) = @_; return $self->render_ast($hash, $self->parse_tokens($self->tokenize_text($text))); }
274             sub render_file {
275             my ($self, $hash, $file) = @_;
276             $self->lexer->file_context($file);
277             $self->parser->file_context($file);
278             $self->renderer->file_context($file);
279             my @results = $self->render_text($hash, scalar(read_file($file)));
280             $self->lexer->file_context(undef);
281             $self->parser->file_context(undef);
282             $self->renderer->file_context(undef);
283             return $results[0] unless wantarray;
284             return @results;
285             }
286              
287             use Exporter;
288             use base 'Exporter';
289             our @EXPORT_OK = qw(liquid_render_file liquid_render_text liquid_verify_file liquid_verify_text);
290             sub liquid_render_text { my ($hash, $text) = @_; my $self = WWW::Shopify::Liquid->new; return $self->render_text($hash, $text); }
291             sub liquid_verify_text { my ($text) = @_; my $self = WWW::Shopify::Liquid->new; $self->verify_text($text); }
292             sub liquid_render_file { my ($hash, $file) = @_; my $self = WWW::Shopify::Liquid->new; return $self->render_file($hash, $file); }
293             sub liquid_verify_file { my ($file) = @_; my $self = WWW::Shopify::Liquid->new; $self->verify_file($file); }
294              
295             sub liquify_item {
296             my ($self, $item) = @_;
297             die new WWW::Shopify::Liquid::Exception("Can only liquify shopify objects.") unless ref($item) && $item->isa('WWW::Shopify::Model::Item');
298            
299             my $fields = $item->fields();
300             my $final = {};
301             foreach my $key (keys(%$item)) {
302             next unless exists $fields->{$key};
303             if ($fields->{$key}->is_relation()) {
304             if ($fields->{$key}->is_many()) {
305             # Since metafields don't come prepackaged, we don't get them. Unless we've already got them.
306             next if $key eq "metafields" && !$item->{metafields};
307             my @results = $item->$key();
308             if (int(@results)) {
309             $final->{$key} = [map { $self->liquify_item($_) } @results];
310             }
311             else {
312             $final->{$key} = [];
313             }
314             }
315             if ($fields->{$key}->is_one() && $fields->{$key}->is_reference()) {
316             if (defined $item->$key()) {
317             # This is inconsistent; this if is a stop-gap measure.
318             # Getting directly from teh database seems to make this automatically an id.
319             if (ref($item->$key())) {
320             $final->{$key} = $item->$key()->id();
321             }
322             else {
323             $final->{$key} = $item->$key();
324             }
325             }
326             else {
327             $final->{$key} = undef;
328             }
329             }
330             $final->{$key} = ($item->$key ? $item->$key->to_json() : undef) if ($fields->{$key}->is_one() && $fields->{$key}->is_own());
331             } elsif (ref($fields->{$key}) !~ m/Date$/) {
332             $final->{$key} = $fields->{$key}->to_shopify($item->$key);
333             } else {
334             $final->{$key} = $item->$key;
335             }
336             }
337             return $final;
338             }
339              
340              
341             =head1 SEE ALSO
342              
343             L<WWW::Shopify>, L<WWW::Shoipfy::Tools::Themer>, L<Padre::Plugin::Shopify>
344              
345             =head1 AUTHOR
346              
347             Adam Harrison (adamdharrison@gmail.com)
348              
349             =head1 LICENSE
350              
351             Copyright (C) 2016 Adam Harrison
352              
353             Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
354              
355             The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
356              
357             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
358              
359             =cut
360              
361             1;