File Coverage

blib/lib/WWW/Shopify/Liquid.pm
Criterion Covered Total %
statement 118 173 68.2
branch 5 32 15.6
condition 6 27 22.2
subroutine 46 65 70.7
pod 0 34 0.0
total 175 331 52.8


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3 21     21   98259 use strict;
  21         39  
  21         862  
4 21     21   105 use warnings;
  21         24  
  21         3976  
5              
6             package WWW::Shopify::Liquid::Pipeline;
7 0     0   0 sub register_tag { }
8 0     0   0 sub register_operator { }
9 0     0   0 sub register_filter { }
10              
11             package WWW::Shopify::Liquid::Element;
12              
13 51     51   201 sub verify { return 1; }
14             sub render {
15 170     170   185 my $self = shift;
16 170         159 my $return = eval { $self->process(@_, "render"); };
  170         453  
17 170 100 33     487 return '' if $@ || !$self->is_processed($return) || !defined $return;
      66        
18 162         326 return $return;
19             }
20 2     2   12 sub optimize { return shift->process(@_, "optimize"); }
21 0     0   0 sub process { return $_[0]; }
22              
23 21     21   115 use List::Util qw(first);
  21         37  
  21         2477  
24 21     21   120 use Scalar::Util qw(looks_like_number blessed);
  21         32  
  21         4844  
25              
26 587   66 587   4782 sub is_processed { return !ref($_[1]) || (ref($_[1]) eq "ARRAY" && int(grep { !$_[0]->is_processed($_) } @{$_[1]}) == 0) || ref($_[1]) eq "HASH" || (blessed($_[1]) && ref($_[1]) !~ m/^WWW::Shopify::Liquid/); }
27             sub ensure_numerical {
28 10 50 33 10   99 return $_[1] if defined $_[1] && looks_like_number($_[1]);
29 0 0 0     0 return $_[1] if ref($_[1]) && ref($_[1]) eq "DateTime";
30 0         0 return 0;
31             }
32              
33             package WWW::Shopify::Liquid;
34 21     21   10728 use Clone qw(clone);
  21         53168  
  21         1333  
35 21     21   10550 use URI::Escape;
  21         26908  
  21         1440  
36 21     21   13850 use File::Slurp;
  21         251350  
  21         1836  
37 21     21   4204 use JSON qw(encode_json);
  21         70029  
  21         188  
38 21     21   16457 use HTML::Strip;
  21         181676  
  21         1223  
39 21     21   244 use List::Util qw(first);
  21         37  
  21         1810  
40 21     21   120 use Digest::MD5 qw(md5_hex);
  21         37  
  21         1194  
41 21     21   13450 use List::MoreUtils qw(firstidx part);
  21         21428  
  21         1935  
42 21     21   133 use List::Util qw(first);
  21         36  
  21         1068  
43 21     21   11709 use Module::Find;
  21         25395  
  21         2154  
44              
45             our $VERSION = '0.05';
46              
47             =head1 NAME
48              
49             WWW::Shopify::Liquid - Fully featured liquid preprocessor with shopify tags & filters added in.
50              
51             =cut
52              
53             =head1 DESCRIPTION
54              
55             A concise and clear liquid processor. Runs a superset of what Shopify can do. For a strict shopify implementation
56             see L for one that emulates all the quirks and ridiculousness of the real thing, but without the tags.
57             (Meaning no actual arithemtic is literal tags without filters, insanity on acutal number processing and conversion,
58             insane array handling, no real optimization, or partial AST reduction, etc.., etc..).
59              
60             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,
61             liquid_render_file.
62              
63             use WWW::Shopify::Liquid qw/liquid_render_file/;
64            
65             $contents = liquid_render_file({ collection => { handle => "test" } }, "myfile.liquid");
66             print $contents;
67            
68             This is the simplest method. There are auxiliary methods which provide a lot of flexibility over how you render your
69             liquid (see below), and an OO interface.
70              
71             This method represents the whole pipeline, so to get an overview of this module, we'll describe it here.
72             Fundamentally, what liquid_render_file does, is it slurps the whole file into a string, and then passes that string to
73             the lexer. This then generates a stream of tokens. These tokens are then transformed into an abstract syntax tree, by the
74             the parser if the syntax is valid. This AST represents the canonical form of the file, and can, from here, either
75             transformed back into almost the same file, statically optimized to remove any unecessary calls, or partially optimized to
76             remove branches of the tree for which you have variables to fill at this time, though both these steps are optional.
77              
78             Finally, these tokens are passed to the renderer, which interprets the tokens and then produces a string representing the
79             final content that can be printed.
80              
81             Has better error handling than Shopify's liquid processor, so if you use this to validate your liquid, you should get better
82             errors than if you're simply submitting them. This module is integrated into the L module, so if you
83             use Padre as your Shopify IDE, you can automatically check the liquid of the file you're currently looking at with the click
84             of a button.
85              
86             You can invoke each stage individually if you like.
87              
88             use WWW::Shopify::Liquid;
89             my $text = ...
90             my $liquid = WWW::Shopify::Liquid->new;
91             my @tokens = $liquid->lexer->parse_text($text);
92             my $ast = $liquid->parser->parse_tokens(@tokens);
93            
94             # Here we have a partial tree optimization. Meaning, if you have some of your
95             # variables, but not all of them, you can simplify the template.
96             $ast = $liquid->optimizer->optimize({ a => 2 }, $ast);
97            
98             # Finally, you can render.
99             $result = $liquid->renderer->render({ b => 3 }, $ast);
100            
101             If you're simply looking to check whether a liquid file is valid, you can do the following:
102              
103             use WWW::Shopify::Liquid qw/liquid_verify_file/;
104             liquid_verify_file("my-snippet.liquid");
105            
106             If sucessful, it'll return nothing, if it fails, it'll throw an exception, detailing the fault's location and description.
107              
108             =cut
109              
110             =head1 STATUS
111              
112             This module is currently in beta. That means that while it is able to parse and validate liquid documents from Shopify, it may
113             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
114             unrolling. However, it does do partial tree rendering. Essentially what's missing is the ability to generate liquid from syntax trees.
115              
116             This is close to complete, but not quite there yet. When done, this will be extremely beneficial to application proxies, as it will allow
117             the use of custom liquid syntax, with partial evaluation, before passing the remaining liquid back to Shopify for full evaluation. This
118             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
119             evaluate stuff like asset_urls, includes, and whatnot.
120              
121             =cut
122              
123 21     21   11061 use WWW::Shopify::Liquid::Parser;
  21         67  
  21         799  
124 21     21   8115 use WWW::Shopify::Liquid::Optimizer;
  21         55  
  21         599  
125 21     21   9460 use WWW::Shopify::Liquid::Lexer;
  21         68  
  21         720  
126 21     21   9960 use WWW::Shopify::Liquid::Renderer;
  21         48  
  21         604  
127 21     21   129 use WWW::Shopify::Liquid::Operator;
  21         36  
  21         509  
128 21     21   99 use WWW::Shopify::Liquid::Tag;
  21         40  
  21         19151  
129              
130             sub new {
131 23     23 0 46296 my $package = shift;
132 23         182 my $self = bless {
133             filters => [],
134             operators => [],
135             tags => [],
136            
137             lexer => WWW::Shopify::Liquid::Lexer->new,
138             parser => WWW::Shopify::Liquid::Parser->new,
139             optimizer => WWW::Shopify::Liquid::Optimizer->new,
140             renderer => WWW::Shopify::Liquid::Renderer->new,
141            
142             %_
143             }, $package;
144            
145 23         135 $self->register_operator($_) for (findallmod WWW::Shopify::Liquid::Operator);
146 23         148 $self->register_filter($_) for (findallmod WWW::Shopify::Liquid::Filter);
147 23         1117 $self->register_tag($_) for (findallmod WWW::Shopify::Liquid::Tag);
148            
149 23         130 return $self;
150             }
151 2122     2122 0 5433 sub lexer { return $_[0]->{lexer}; }
152 2120     2120 0 7255 sub parser { return $_[0]->{parser}; }
153 8     8 0 50 sub optimizer { return $_[0]->{optimizer}; }
154 12     12 0 80 sub renderer { return $_[0]->{renderer}; }
155              
156             sub register_tag {
157 368 100   368 0 132072 if (!$_[1]->abstract) {
158 322         293 push(@{$_[0]->tags}, $_[1]);
  322         618  
159 322         572 $_[0]->lexer->register_tag($_[1]);
160 322         652 $_[0]->parser->register_tag($_[1]);
161             }
162             }
163             sub register_filter {
164 1311     1311 0 348703 push(@{$_[0]->filters}, $_[1]);
  1311         1981  
165 1311         2006 $_[0]->lexer->register_filter($_[1]);
166 1311         2247 $_[0]->parser->register_filter($_[1]);
167             }
168             sub register_operator {
169 460     460 0 139546 push(@{$_[0]->operators}, $_[1]);
  460         726  
170 460         734 $_[0]->lexer->register_operator($_[1]);
171 460         864 $_[0]->parser->register_operator($_[1]);
172             }
173 322     322 0 732 sub tags { return $_[0]->{tags}; }
174 1311     1311 0 2387 sub filters { return $_[0]->{filters}; }
175 460     460 0 993 sub operators { return $_[0]->{operators}; }
176 0     0 0 0 sub order_of_operations { return $_[0]->{order_of_operations}; }
177 0     0 0 0 sub free_tags { return $_[0]->{free_tags}; }
178 0     0 0 0 sub enclosing_tags { return $_[0]->{enclosing_tags}; }
179 0     0 0 0 sub processing_variables { return $_[0]->{processing_variables}; }
180 0     0 0 0 sub money_format { return $_[0]->{money_format}; }
181 0     0 0 0 sub money_with_currency_format { return $_[0]->{money_with_currency_format}; }
182 0     0 0 0 sub tag_list { return (keys(%{$_[0]->free_tags}), keys(%{$_[0]->enclosing_tags})); }
  0         0  
  0         0  
183              
184 0     0 0 0 sub operate { return $_[0]->operators->{$_[3]}->($_[0], $_[1], $_[2], $_[4]); }
185              
186 7     7 0 463 sub render_ast { my ($self, $hash, $ast) = @_; return $self->renderer->render($hash, $ast); }
  7         29  
187 0     0 0 0 sub unpack_ast { my ($self, $ast) = @_; return $self->parser->unparse_tokens($ast); }
  0         0  
188 1     1 0 2 sub optimize_ast { my ($self, $hash, $ast) = @_; return $self->optimizer->optimize($hash, $ast); }
  1         5  
189 8     8 0 12 sub tokenize_text { my ($self, $text) = @_; return $self->lexer->parse_text($text); }
  8         27  
190 10     10 0 23 sub parse_tokens { my ($self, @tokens) = @_; return $self->parser->parse_tokens(@tokens); }
  10         29  
191 2     2 0 1581 sub parse_text { my ($self, $text) = @_; return $self->parse_tokens($self->tokenize_text($text)); }
  2         11  
192              
193 1     1 0 38 sub verify_text { my ($self, $text) = @_; $self->parse_tokens($self->parse_text($text)); }
  1         3  
194 0     0 0 0 sub verify_file { my ($self, $file) = @_; $self->verify_text(scalar(read_file($file))); }
  0         0  
195 6     6 0 1521 sub render_text { my ($self, $hash, $text) = @_; return $self->render_ast($hash, $self->parse_tokens($self->tokenize_text($text))); }
  6         26  
196 0     0 0 0 sub render_file { my ($self, $hash, $file) = @_; return $self->render_text($hash, scalar(read_file($file))); }
  0         0  
197              
198 21     21   133 use Exporter;
  21         36  
  21         920  
199 21     21   97 use base 'Exporter';
  21         37  
  21         11441  
200             our @EXPORT_OK = qw(liquid_render_file liquid_render_text liquid_verify_file liquid_verify_text);
201 4     4 0 1701 sub liquid_render_text { my ($hash, $text) = @_; my $self = WWW::Shopify::Liquid->new; return $self->render_text($hash, $text); }
  4         22  
  4         18  
202 0     0 0   sub liquid_verify_text { my ($text) = @_; my $self = WWW::Shopify::Liquid->new; $self->verify_text($text); }
  0            
  0            
203 0     0 0   sub liquid_render_file { my ($hash, $file) = @_; my $self = WWW::Shopify::Liquid->new; return $self->render_file($hash, $file); }
  0            
  0            
204 0     0 0   sub liquid_verify_file { my ($file) = @_; my $self = WWW::Shopify::Liquid->new; $self->verify_file($file); }
  0            
  0            
205              
206             sub liquify_item {
207 0     0 0   my ($self, $item) = @_;
208 0 0 0       die new WWW::Shopify::Liquid::Exception("Can only liquify shopify objects.") unless ref($item) && $item->isa('WWW::Shopify::Model::Item');
209            
210 0           my $fields = $item->fields();
211 0           my $final = {};
212 0           foreach my $key (keys(%$item)) {
213 0 0         next unless exists $fields->{$key};
214 0 0         if ($fields->{$key}->is_relation()) {
    0          
215 0 0         if ($fields->{$key}->is_many()) {
216             # Since metafields don't come prepackaged, we don't get them. Unless we've already got them.
217 0 0 0       next if $key eq "metafields" && !$item->{metafields};
218 0           my @results = $item->$key();
219 0 0         if (int(@results)) {
220 0           $final->{$key} = [map { $_->to_json() } @results];
  0            
221             }
222             else {
223 0           $final->{$key} = [];
224             }
225             }
226 0 0 0       if ($fields->{$key}->is_one() && $fields->{$key}->is_reference()) {
227 0 0         if (defined $item->$key()) {
228             # This is inconsistent; this if is a stop-gap measure.
229             # Getting directly from teh database seems to make this automatically an id.
230 0 0         if (ref($item->$key())) {
231 0           $final->{$key} = $item->$key()->id();
232             }
233             else {
234 0           $final->{$key} = $item->$key();
235             }
236             }
237             else {
238 0           $final->{$key} = undef;
239             }
240             }
241 0 0 0       $final->{$key} = ($item->$key ? $item->$key->to_json() : undef) if ($fields->{$key}->is_one() && $fields->{$key}->is_own());
    0          
242             } elsif (ref($fields->{$key}) !~ m/Date$/) {
243 0           $final->{$key} = $fields->{$key}->to_shopify($item->$key);
244             } else {
245 0           $final->{$key} = $item->$key;
246             }
247             }
248 0           return $final;
249             }
250              
251              
252             =head1 SEE ALSO
253              
254             L, L, L
255              
256             =head1 AUTHOR
257              
258             Adam Harrison (adamdharrison@gmail.com)
259              
260             =head1 LICENSE
261              
262             Copyright (C) 2014 Adam Harrison
263              
264             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:
265              
266             The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
267              
268             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.
269              
270             =cut
271              
272             1;