File Coverage

blib/lib/WWW/Shopify/Liquid.pm
Criterion Covered Total %
statement 105 178 58.9
branch 12 42 28.5
condition 0 12 0.0
subroutine 37 58 63.7
pod 0 42 0.0
total 154 332 46.3


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3 37     37   1116173 use strict;
  37         169  
  37         1210  
4 37     37   232 use warnings;
  37         89  
  37         1852  
5              
6              
7             package WWW::Shopify::Liquid;
8 37     37   13439 use File::Slurp;
  37         231508  
  37         3182  
9 37     37   17241 use List::MoreUtils qw(firstidx part);
  37         487830  
  37         388  
10 37     37   59275 use Module::Find;
  37         50906  
  37         3782  
11              
12             our $VERSION = '0.07';
13              
14             =head1 NAME
15              
16             WWW::Shopify::Liquid - Fully featured liquid preprocessor with shopify tags & filters added in.
17              
18             =cut
19              
20             =head1 DESCRIPTION
21              
22             A concise and clear liquid processor. Runs a superset of what Shopify can do. For a strict shopify implementation
23             see L<Template::Liquid> for one that emulates all the quirks and ridiculousness of the real thing, but without the tags.
24             (Meaning no actual arithemtic is literal tags without filters, insanity on acutal number processing and conversion,
25             insane array handling, no real optimization, or partial AST reduction, etc.., etc..).
26              
27             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,
28             liquid_render_file.
29              
30             use WWW::Shopify::Liquid qw/liquid_render_file/;
31            
32             $contents = liquid_render_file({ collection => { handle => "test" } }, "myfile.liquid");
33             print $contents;
34            
35             This is the simplest method. There are auxiliary methods which provide a lot of flexibility over how you render your
36             liquid (see below), and an OO interface.
37              
38             This method represents the whole pipeline, so to get an overview of this module, we'll describe it here.
39             Fundamentally, what liquid_render_file does, is it slurps the whole file into a string, and then passes that string to
40             the lexer. This then generates a stream of tokens. These tokens are then transformed into an abstract syntax tree, by the
41             the parser if the syntax is valid. This AST represents the canonical form of the file, and can, from here, either
42             transformed back into almost the same file, statically optimized to remove any unecessary calls, or partially optimized to
43             remove branches of the tree for which you have variables to fill at this time, though both these steps are optional.
44              
45             Finally, these tokens are passed to the renderer, which interprets the tokens and then produces a string representing the
46             final content that can be printed.
47              
48             Has better error handling than Shopify's liquid processor, so if you use this to validate your liquid, you should get better
49             errors than if you're simply submitting them. This module is integrated into the L<Padre::Plugin::Shopify> module, so if you
50             use Padre as your Shopify IDE, you can automatically check the liquid of the file you're currently looking at with the click
51             of a button.
52              
53             You can invoke each stage individually if you like.
54              
55             use WWW::Shopify::Liquid;
56             my $text = ...
57             my $liquid = WWW::Shopify::Liquid->new;
58             my @tokens = $liquid->lexer->parse_text($text);
59             my $ast = $liquid->parser->parse_tokens(@tokens);
60            
61             # Here we have a partial tree optimization. Meaning, if you have some of your
62             # variables, but not all of them, you can simplify the template.
63             $ast = $liquid->optimizer->optimize({ a => 2 }, $ast);
64            
65             # Finally, you can render.
66             $result = $liquid->renderer->render({ b => 3 }, $ast);
67            
68             If you're simply looking to check whether a liquid file is valid, you can do the following:
69              
70             use WWW::Shopify::Liquid qw/liquid_verify_file/;
71             liquid_verify_file("my-snippet.liquid");
72            
73             If sucessful, it'll return nothing, if it fails, it'll throw an exception, detailing the fault's location and description.
74              
75             =cut
76              
77             =head1 STATUS
78              
79             This module is currently in beta. That means that while it is able to parse and validate liquid documents from Shopify, it may
80             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
81             unrolling. However, it does do partial tree rendering. Essentially what's missing is the ability to generate liquid from syntax trees.
82              
83             This is close to complete, but not quite there yet. When done, this will be extremely beneficial to application proxies, as it will allow
84             the use of custom liquid syntax, with partial evaluation, before passing the remaining liquid back to Shopify for full evaluation. This
85             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
86             evaluate stuff like asset_urls, includes, and whatnot.
87              
88             =cut
89              
90 37     37   15349 use WWW::Shopify::Liquid::Parser;
  37         158  
  37         1535  
91 37     37   11508 use WWW::Shopify::Liquid::Optimizer;
  37         135  
  37         1291  
92 37     37   14751 use WWW::Shopify::Liquid::Lexer;
  37         192  
  37         1736  
93 37     37   13587 use WWW::Shopify::Liquid::Renderer;
  37         134  
  37         1347  
94 37     37   11948 use WWW::Shopify::Liquid::Debugger;
  37         139  
  37         1243  
95 37     37   286 use WWW::Shopify::Liquid::Operator;
  37         92  
  37         1277  
96 37     37   243 use WWW::Shopify::Liquid::Tag;
  37         104  
  37         1102  
97 37     37   229 use Scalar::Util qw(blessed);
  37         93  
  37         45981  
98              
99             sub new {
100 85     85 0 113132 my $package = shift;
101 85         862 my $self = bless {
102             filters => [],
103             operators => [],
104             tags => [],
105            
106             lexer => WWW::Shopify::Liquid::Lexer->new,
107             parser => WWW::Shopify::Liquid::Parser->new,
108             optimizer => WWW::Shopify::Liquid::Optimizer->new,
109             renderer => WWW::Shopify::Liquid::Renderer->new,
110             debugger => WWW::Shopify::Liquid::Debugger->new,
111            
112             dialects => [],
113            
114             @_
115             }, $package;
116            
117 85 50       520 $self->lexer->parent($self) if $self->lexer;
118 85 50       368 $self->parser->parent($self) if $self->parser;
119 85 50       354 $self->optimizer->parent($self) if $self->optimizer;
120 85 50       349 $self->renderer->parent($self) if $self->renderer;
121            
122 85         417 $self->load_modules;
123            
124 85         586 return $self;
125             }
126              
127             sub load_modules {
128 85     85 0 281 my ($self) = @_;
129 85         579 $self->register_operator($_) for (findallmod WWW::Shopify::Liquid::Operator);
130 85         901 $self->register_filter($_) for (findallmod WWW::Shopify::Liquid::Filter);
131 85         1074 $self->register_tag($_) for (findallmod WWW::Shopify::Liquid::Tag);
132 85         1545 $_->apply($self) for (@{$self->{dialects}});
  85         460  
133             }
134              
135 435     435 0 6438 sub lexer { return $_[0]->{lexer}; }
136 422     422 0 15288 sub parser { return $_[0]->{parser}; }
137 189     189 0 2014 sub optimizer { return $_[0]->{optimizer}; }
138 382     382 0 5687 sub renderer { return $_[0]->{renderer}; }
139 0     0 0 0 sub debugger { return $_[0]->{debugger}; }
140              
141             sub register_tag {
142 1971 100   1971 0 752383 if (!$_[1]->abstract) {
143 1801         3750 push(@{$_[0]->tags}, $_[1]);
  1801         4083  
144 1801 100       3362 $_->register_tag($_[1]) for (grep { blessed($_) && $_->can('register_tag') } values(%{$_[0]}));
  16230         65585  
  1801         5143  
145             }
146             }
147             sub register_filter {
148 5226     5226 0 1772113 push(@{$_[0]->filters}, $_[1]);
  5226         10535  
149 5226 100       8903 $_->register_filter($_[1]) for (grep { blessed($_) && $_->can('register_filter') } values(%{$_[0]}));
  47095         184415  
  5226         13842  
150             }
151             sub register_operator {
152 2380     2380 0 878938 WWW::Shopify::Liquid::Pipeline->register_operator($_[1]);
153 2380         6996 push(@{$_[0]->operators}, $_[1]);
  2380         5348  
154 2380 100       4094 $_->register_operator($_[1]) for (grep { blessed($_) && $_->can('register_operator') } values(%{$_[0]}));
  21448         95081  
  2380         6817  
155             }
156 1801     1801 0 4837 sub tags { return $_[0]->{tags}; }
157 5226     5226 0 13015 sub filters { return $_[0]->{filters}; }
158 2380     2380 0 8274 sub operators { return $_[0]->{operators}; }
159 0     0 0 0 sub order_of_operations { return $_[0]->{order_of_operations}; }
160 0     0 0 0 sub free_tags { return $_[0]->{free_tags}; }
161 0     0 0 0 sub enclosing_tags { return $_[0]->{enclosing_tags}; }
162 0     0 0 0 sub processing_variables { return $_[0]->{processing_variables}; }
163 0     0 0 0 sub money_format { return $_[0]->{money_format}; }
164 0     0 0 0 sub money_with_currency_format { return $_[0]->{money_with_currency_format}; }
165 0     0 0 0 sub tag_list { return (keys(%{$_[0]->free_tags}), keys(%{$_[0]->enclosing_tags})); }
  0         0  
  0         0  
166             sub file_context {
167 0     0 0 0 my ($self, $file) = @_;
168 0         0 $self->lexer->file_context($file);
169 0         0 $self->parser->file_context($file);
170 0         0 $self->renderer->file_context($file);
171             }
172              
173 0     0 0 0 sub operate { return $_[0]->operators->{$_[3]}->($_[0], $_[1], $_[2], $_[4]); }
174              
175 174     174 0 11642 sub render_ast { my ($self, $hash, $ast) = @_; return $self->renderer->render($hash, $ast); }
  174         895  
176 5     5 0 7677 sub render_resume { my ($self, $state, $ast) = @_; return $self->renderer->resume($state, $ast); }
  5         25  
177 0     0 0 0 sub unpack_ast { my ($self, $ast) = @_; return $self->parser->unparse_tokens($ast); }
  0         0  
178 0     0 0 0 sub unparse_text { my ($self, @tokens) = @_; return $self->lexer->unparse_text(@tokens); }
  0         0  
179 3     3 0 519 sub optimize_ast { my ($self, $hash, $ast) = @_; return $self->optimizer->optimize($hash, $ast); }
  3         13  
180 189     189 0 503 sub tokenize_text { my ($self, $text) = @_; return $self->lexer->parse_text($text); }
  189         673  
181 197     197 0 2434 sub parse_tokens { my ($self, @tokens) = @_; return $self->parser->parse_tokens(@tokens); }
  197         675  
182 38     38 0 20111 sub parse_text { my ($self, $text) = @_; return $self->parse_tokens($self->tokenize_text($text)); }
  38         156  
183 0     0 0 0 sub parse_file { my ($self, $file) = @_; return $self->parse_tokens($self->tokenize_file($file)); }
  0         0  
184              
185 37     37   383 use Encode;
  37         115  
  37         14413  
186             sub tokenize_file {
187 0     0 0 0 my ($self, $file) = @_;
188 0         0 $self->file_context($file);
189 0         0 my $text = decode("UTF-8", scalar(read_file($file)));
190 0         0 my @tokens = $self->tokenize_text($text);
191 0         0 $self->file_context(undef);
192 0         0 return @tokens;
193             }
194              
195              
196 1     1 0 12 sub verify_text { my ($self, $text) = @_; $self->parse_tokens($self->parse_text($text)); }
  1         4  
197             sub verify_file {
198 0     0 0 0 my ($self, $file) = @_;
199 0         0 $self->file_context($file);
200 0         0 $self->verify_text(decode("UTF-8", scalar(read_file($file))));
201 0         0 $self->file_context(undef);
202             }
203 151     151 0 71421 sub render_text { my ($self, $hash, $text) = @_; return $self->render_ast($hash, $self->parse_tokens($self->tokenize_text($text))); }
  151         613  
204             sub render_file {
205 0     0 0 0 my ($self, $hash, $file) = @_;
206 0         0 $self->file_context($file);
207 0         0 my @results = $self->render_text($hash, decode("UTF-8", scalar(read_file($file))));
208 0         0 $self->file_context(undef);
209 0 0       0 return $results[0] unless wantarray;
210 0         0 return @results;
211             }
212              
213 37     37   324 use Exporter;
  37         103  
  37         1751  
214 37     37   288 use base 'Exporter';
  37         108  
  37         25286  
215             our @EXPORT_OK = qw(liquid_render_file liquid_render_text liquid_verify_file liquid_verify_text liquid_parse_file);
216 46     46 0 55340 sub liquid_render_text { my ($hash, $text) = @_; my $self = WWW::Shopify::Liquid->new; return $self->render_text($hash, $text); }
  46         384  
  46         269  
217 0     0 0   sub liquid_verify_text { my ($text) = @_; my $self = WWW::Shopify::Liquid->new; $self->verify_text($text); }
  0            
  0            
218 0     0 0   sub liquid_parse_file { my ($file) = @_; my $self = WWW::Shopify::Liquid->new; return $self->parse_file($file); }
  0            
  0            
219 0     0 0   sub liquid_render_file { my ($hash, $file) = @_; my $self = WWW::Shopify::Liquid->new; return $self->render_file($hash, $file); }
  0            
  0            
220 0     0 0   sub liquid_verify_file { my ($file) = @_; my $self = WWW::Shopify::Liquid->new; $self->verify_file($file); }
  0            
  0            
221              
222             sub liquify_item {
223 0     0 0   my ($self, $item) = @_;
224 0 0 0       die new WWW::Shopify::Liquid::Exception("Can only liquify shopify objects.") unless ref($item) && $item->isa('WWW::Shopify::Model::Item');
225            
226 0           my $fields = $item->fields();
227 0           my $final = {};
228 0           foreach my $key (keys(%$item)) {
229 0 0         next unless exists $fields->{$key};
230 0 0         if ($fields->{$key}->is_relation()) {
    0          
231 0 0         if ($fields->{$key}->is_many()) {
232             # Since metafields don't come prepackaged, we don't get them. Unless we've already got them.
233 0 0 0       next if $key eq "metafields" && !$item->{metafields};
234 0           my @results = $item->$key();
235 0 0         if (int(@results)) {
236 0           $final->{$key} = [map { $self->liquify_item($_) } @results];
  0            
237             }
238             else {
239 0           $final->{$key} = [];
240             }
241             }
242 0 0 0       if ($fields->{$key}->is_one() && $fields->{$key}->is_reference()) {
243 0 0         if (defined $item->$key()) {
244             # This is inconsistent; this if is a stop-gap measure.
245             # Getting directly from teh database seems to make this automatically an id.
246 0 0         if (ref($item->$key())) {
247 0           $final->{$key} = $item->$key()->id();
248             }
249             else {
250 0           $final->{$key} = $item->$key();
251             }
252             }
253             else {
254 0           $final->{$key} = undef;
255             }
256             }
257 0 0 0       $final->{$key} = ($item->$key ? $item->$key->to_json() : undef) if ($fields->{$key}->is_one() && $fields->{$key}->is_own());
    0          
258             } elsif (ref($fields->{$key}) !~ m/Date$/) {
259 0           $final->{$key} = $fields->{$key}->to_shopify($item->$key);
260             } else {
261 0           $final->{$key} = $item->$key;
262             }
263             }
264 0           return $final;
265             }
266              
267              
268             =head1 SEE ALSO
269              
270             L<WWW::Shopify>, L<WWW::Shoipfy::Tools::Themer>, L<Padre::Plugin::Shopify>
271              
272             =head1 AUTHOR
273              
274             Adam Harrison (adamdharrison@gmail.com)
275              
276             =head1 LICENSE
277              
278             Copyright (C) 2016 Adam Harrison
279              
280             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:
281              
282             The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
283              
284             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.
285              
286             =cut
287              
288             1;