Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'master' of git@github.com:tobi/liquid

Conflicts:
	lib/liquid.rb
	lib/liquid/context.rb
	lib/liquid/variable.rb
	test/standard_tag_test.rb
  • Loading branch information...
commit a65bd76e726c3a4b50a25df8289a77c3945d0b78 2 parents 8ac4d6a + 37e913f
@tobi tobi authored
Showing with 3,809 additions and 401 deletions.
  1. +10 −6 CHANGELOG
  2. +7 −0 Rakefile
  3. +27 −16 lib/extras/liquid_view.rb
  4. +15 −27 lib/liquid.rb
  5. +23 −23 lib/liquid/block.rb
  6. +30 −33 lib/liquid/condition.rb
  7. +7 −11 lib/liquid/context.rb
  8. +7 −7 lib/liquid/document.rb
  9. +17 −16 lib/liquid/drop.rb
  10. +27 −27 lib/liquid/htmltags.rb
  11. +4 −4 lib/liquid/module_ex.rb
  12. +17 −18 lib/liquid/strainer.rb
  13. +8 −8 lib/liquid/tag.rb
  14. +9 −9 lib/liquid/tags/capture.rb
  15. +54 −53 lib/liquid/template.rb
  16. +3 −6 lib/liquid/variable.rb
  17. +14 −4 liquid.gemspec
  18. +92 −0 performance/shopify.rb
  19. +33 −0 performance/shopify/comment_form.rb
  20. +45 −0 performance/shopify/database.rb
  21. +7 −0 performance/shopify/json_filter.rb
  22. +18 −0 performance/shopify/liquid.rb
  23. +18 −0 performance/shopify/money_filter.rb
  24. +93 −0 performance/shopify/paginate.rb
  25. +98 −0 performance/shopify/shop_filter.rb
  26. +25 −0 performance/shopify/tag_filter.rb
  27. +945 −0 performance/shopify/vision.database.yml
  28. +11 −0 performance/shopify/weight_filter.rb
  29. +74 −0 performance/tests/dropify/article.liquid
  30. +33 −0 performance/tests/dropify/blog.liquid
  31. +66 −0 performance/tests/dropify/cart.liquid
  32. +22 −0 performance/tests/dropify/collection.liquid
  33. +47 −0 performance/tests/dropify/index.liquid
  34. +8 −0 performance/tests/dropify/page.liquid
  35. +68 −0 performance/tests/dropify/product.liquid
  36. +105 −0 performance/tests/dropify/theme.liquid
  37. +74 −0 performance/tests/ripen/article.liquid
  38. +13 −0 performance/tests/ripen/blog.liquid
  39. +54 −0 performance/tests/ripen/cart.liquid
  40. +29 −0 performance/tests/ripen/collection.liquid
  41. +32 −0 performance/tests/ripen/index.liquid
  42. +4 −0 performance/tests/ripen/page.liquid
  43. +75 −0 performance/tests/ripen/product.liquid
  44. +85 −0 performance/tests/ripen/theme.liquid
  45. +56 −0 performance/tests/tribble/404.liquid
  46. +98 −0 performance/tests/tribble/article.liquid
  47. +41 −0 performance/tests/tribble/blog.liquid
  48. +134 −0 performance/tests/tribble/cart.liquid
  49. +70 −0 performance/tests/tribble/collection.liquid
  50. +94 −0 performance/tests/tribble/index.liquid
  51. +56 −0 performance/tests/tribble/page.liquid
  52. +116 −0 performance/tests/tribble/product.liquid
  53. +51 −0 performance/tests/tribble/search.liquid
  54. +90 −0 performance/tests/tribble/theme.liquid
  55. +66 −0 performance/tests/vogue/article.liquid
  56. +32 −0 performance/tests/vogue/blog.liquid
  57. +58 −0 performance/tests/vogue/cart.liquid
  58. +19 −0 performance/tests/vogue/collection.liquid
  59. +22 −0 performance/tests/vogue/index.liquid
  60. +3 −0  performance/tests/vogue/page.liquid
  61. +62 −0 performance/tests/vogue/product.liquid
  62. +122 −0 performance/tests/vogue/theme.liquid
  63. +13 −12 test/context_test.rb
  64. +4 −4 test/security_test.rb
  65. +112 −117 test/standard_tag_test.rb
  66. +37 −0 test/variable_test.rb
View
16 CHANGELOG
@@ -1,9 +1,13 @@
+* Ruby 1.9.1 bugfixes
+
+* Fix LiquidView for Rails 2.2. Fix local assigns for all versions of Rails
+
* Fixed gem install rake task
* Improve Error encapsulation in liquid by maintaining a own set of exceptions instead of relying on ruby build ins
* Added If with or / and expressions
-* Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods.
+* Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods.
* Added more tags to standard library
@@ -22,17 +26,17 @@
* Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond]
* Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ArgumentSeparator: , by default [Paul Hammond]
-
- {{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
-
-* Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke]
+ {{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
+
+
+* Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke]
class ProductDrop < Liquid::Drop
def top_sales
Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
end
- end
+ end
t = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {% endfor %} ' )
t.render('product' => ProductDrop.new )
View
7 Rakefile
@@ -21,4 +21,11 @@ Hoe.new(PKG_NAME, PKG_VERSION) do |p|
p.author = "Tobias Luetke"
p.email = "tobi@leetsoft.com"
p.url = "http://www.liquidmarkup.org"
+end
+
+desc "Run the liquid profile/perforamce coverage"
+task :profile do
+
+ ruby "performance/shopify.rb"
+
end
View
43 lib/extras/liquid_view.rb
@@ -5,32 +5,43 @@
#
# ActionView::Base::register_template_handler :liquid, LiquidView
class LiquidView
+ PROTECTED_ASSIGNS = %w( template_root response _session template_class action_name request_origin session template
+ _response url _request _cookies variables_added _flash params _headers request cookies
+ ignore_missing_templates flash _params logger before_filter_chain_aborted headers )
+ PROTECTED_INSTANCE_VARIABLES = %w( @_request @controller @_first_render @_memoized__pick_template @view_paths
+ @helpers @assigns_added @template @_render_stack @template_format @assigns )
+
+ def self.call(template)
+ "LiquidView.new(self).render(template, local_assigns)"
+ end
- def initialize(action_view)
- @action_view = action_view
+ def initialize(view)
+ @view = view
end
-
- def render(template, local_assigns_for_rails_less_than_2_1_0 = nil)
- @action_view.controller.headers["Content-Type"] ||= 'text/html; charset=utf-8'
- assigns = @action_view.assigns.dup
+ def render(template, local_assigns = nil)
+ @view.controller.headers["Content-Type"] ||= 'text/html; charset=utf-8'
- # template is a Template object in Rails >=2.1.0, a source string previously.
- if template.respond_to? :source
- source = template.source
- local_assigns = template.locals
+ # Rails 2.2 Template has source, but not locals
+ if template.respond_to?(:source) && !template.respond_to?(:locals)
+ assigns = (@view.instance_variables - PROTECTED_INSTANCE_VARIABLES).inject({}) do |hash, ivar|
+ hash[ivar[1..-1]] = @view.instance_variable_get(ivar)
+ hash
+ end
else
- source = template
- local_assigns = local_assigns_for_rails_less_than_2_1_0
+ assigns = @view.assigns.reject{ |k,v| PROTECTED_ASSIGNS.include?(k) }
end
-
- if content_for_layout = @action_view.instance_variable_get("@content_for_layout")
+
+ source = template.respond_to?(:source) ? template.source : template
+ local_assigns = (template.respond_to?(:locals) ? template.locals : local_assigns) || {}
+
+ if content_for_layout = @view.instance_variable_get("@content_for_layout")
assigns['content_for_layout'] = content_for_layout
end
- assigns.merge!(local_assigns)
+ assigns.merge!(local_assigns.stringify_keys)
liquid = Liquid::Template.parse(source)
- liquid.render(assigns, :filters => [@action_view.controller.master_helper_module], :registers => {:action_view => @action_view, :controller => @action_view.controller})
+ liquid.render(assigns, :filters => [@view.controller.master_helper_module], :registers => {:action_view => @view, :controller => @view.controller})
end
def compilable?
View
42 lib/liquid.rb
@@ -22,41 +22,29 @@
$LOAD_PATH.unshift(File.dirname(__FILE__))
module Liquid
-
- # Basic apperence:
-
- # Tags {% look like this %}
- TagStart = /\{%/
- TagEnd = /%\}/
-
- # Variables {{look}} like {{this}}
- VariableStart = /\{\{/
- VariableEnd = /\}\}/
-
- # Arguments are passed {% like: this %}
- FilterArgumentSeparator = ':'
-
- # Hashes are separated {{ like.this }}
- VariableAttributeSeparator = '.'
-
- # Filters are piped in {{ like | this }}
- FilterSeparator = /\|/
-
- # Muliple arguments are {% separated: like, this %}
+ FilterSeparator = /\|/
ArgumentSeparator = ','
-
-
- # Lexical parsing regexped go below.
+ FilterArgumentSeparator = ':'
+ VariableAttributeSeparator = '.'
+ TagStart = /\{\%/
+ TagEnd = /\%\}/
VariableSignature = /\(?[\w\-\.\[\]]\)?/
- VariableSegment = /[\w\-]\??/
+ VariableSegment = /[\w\-]/
+ VariableStart = /\{\{/
+ VariableEnd = /\}\}/
VariableIncompleteEnd = /\}\}?/
QuotedString = /"[^"]+"|'[^']+'/
- QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/
+ QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/
+ StrictQuotedFragment = /"[^"]+"|'[^']+'|[^\s,\|,\:,\,]+/
+ FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/
+ OtherFilterArgument = /#{ArgumentSeparator}(?:#{StrictQuotedFragment})/
+ SpacelessFilter = /#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/
+ Expression = /(?:#{QuotedFragment}(?:#{SpacelessFilter})*)/
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/
AnyStartingTag = /\{\{|\{\%/
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/
- VariableParser = /\[[^\]]+\]|#{VariableSegment}+/
+ VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/
end
require 'liquid/drop'
View
46 lib/liquid/block.rb
@@ -1,19 +1,19 @@
module Liquid
-
+
class Block < Tag
def parse(tokens)
@nodelist ||= []
@nodelist.clear
- while token = tokens.shift
+ while token = tokens.shift
case token
- when /^#{TagStart}/
+ when /^#{TagStart}/
if token =~ /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/
# if we found the proper block delimitor just end parsing here and let the outer block
- # proceed
+ # proceed
if block_delimiter == $1
end_tag
return
@@ -23,10 +23,10 @@ def parse(tokens)
if tag = Template.tags[$1]
@nodelist << tag.new($1, $2, tokens)
else
- # this tag is not registered with the system
+ # this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag($1, $2, tokens)
- end
+ end
else
raise SyntaxError, "Tag '#{token}' was not properly terminated with regexp: #{TagEnd.inspect} "
end
@@ -37,19 +37,19 @@ def parse(tokens)
else
@nodelist << token
end
- end
-
- # Make sure that its ok to end parsing in the current block.
- # Effectively this method will throw and exception unless the current block is
- # of type Document
+ end
+
+ # Make sure that its ok to end parsing in the current block.
+ # Effectively this method will throw and exception unless the current block is
+ # of type Document
assert_missing_delimitation!
- end
-
- def end_tag
+ end
+
+ def end_tag
end
def unknown_tag(tag, params, tokens)
- case tag
+ case tag
when 'else'
raise SyntaxError, "#{block_name} tag does not expect else tag"
when 'end'
@@ -61,7 +61,7 @@ def unknown_tag(tag, params, tokens)
def block_delimiter
"end#{block_name}"
- end
+ end
def block_name
@tag_name
@@ -77,7 +77,7 @@ def create_variable(token)
def render(context)
render_all(@nodelist, context)
end
-
+
protected
def assert_missing_delimitation!
@@ -86,12 +86,12 @@ def assert_missing_delimitation!
def render_all(list, context)
list.collect do |token|
- begin
+ begin
token.respond_to?(:render) ? token.render(context) : token
- rescue Exception => e
+ rescue Exception => e
context.handle_error(e)
- end
- end
+ end
+ end
end
- end
-end
+ end
+end
View
63 lib/liquid/condition.rb
@@ -3,7 +3,7 @@ module Liquid
#
# Example:
#
- # c = Condition.new('1', '==', '1')
+ # c = Condition.new('1', '==', '1')
# c.evaluate #=> true
#
class Condition #:nodoc:
@@ -17,33 +17,33 @@ class Condition #:nodoc:
'<=' => :<=,
'contains' => lambda { |cond, left, right| left.include?(right) },
}
-
+
def self.operators
@@operators
end
attr_reader :attachment
attr_accessor :left, :operator, :right
-
+
def initialize(left = nil, operator = nil, right = nil)
@left, @operator, @right = left, operator, right
@child_relation = nil
@child_condition = nil
end
-
+
def evaluate(context = Context.new)
- result = interpret_condition(left, right, operator, context)
-
+ result = interpret_condition(left, right, operator, context)
+
case @child_relation
- when :or
+ when :or
result || @child_condition.evaluate(context)
- when :and
+ when :and
result && @child_condition.evaluate(context)
else
result
- end
- end
-
+ end
+ end
+
def or(condition)
@child_relation, @child_condition = :or, condition
end
@@ -51,25 +51,25 @@ def or(condition)
def and(condition)
@child_relation, @child_condition = :and, condition
end
-
+
def attach(attachment)
@attachment = attachment
end
-
+
def else?
false
- end
-
+ end
+
def inspect
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
end
-
+
private
-
+
def equal_variables(left, right)
if left.is_a?(Symbol)
if right.respond_to?(left)
- return right.send(left.to_s)
+ return right.send(left.to_s)
else
return nil
end
@@ -77,47 +77,44 @@ def equal_variables(left, right)
if right.is_a?(Symbol)
if left.respond_to?(right)
- return left.send(right.to_s)
+ return left.send(right.to_s)
else
return nil
end
end
- left == right
- end
+ left == right
+ end
def interpret_condition(left, right, op, context)
-
- # If the operator is empty this means that the decision statement is just
- # a single variable. We can just poll this variable from the context and
+ # If the operator is empty this means that the decision statement is just
+ # a single variable. We can just poll this variable from the context and
# return this as the result.
- return context[left] if op == nil
+ return context[left] if op == nil
left, right = context[left], context[right]
-
operation = self.class.operators[op] || raise(ArgumentError.new("Unknown operator #{op}"))
if operation.respond_to?(:call)
operation.call(self, left, right)
- elsif left.respond_to?(operation) and right.respond_to?(operation)
+ elsif left.respond_to?(operation) and right.respond_to?(operation)
left.send(operation, right)
else
nil
end
- end
- end
+ end
+ end
class ElseCondition < Condition
-
- def else?
+ def else?
true
end
-
+
def evaluate(context)
true
end
end
-end
+end
View
18 lib/liquid/context.rb
@@ -56,7 +56,7 @@ def invoke(method, *args)
if strainer.respond_to?(method)
strainer.__send__(method, *args)
else
- args.first
+ raise FilterNotFound, "Filter '#{method}' not found"
end
end
@@ -156,16 +156,12 @@ def resolve(key)
# fetches an object starting at the local scope and then moving up
# the hierachy
def find_variable(key)
- @scopes.each do |scope|
- if scope.has_key?(key)
- variable = scope[key]
- variable = scope[key] = variable.call(self) if variable.is_a?(Proc)
- variable = variable.to_liquid
- variable.context = self if variable.respond_to?(:context=)
- return variable
- end
- end
- nil
+ scope = @scopes[0..-2].find { |s| s.has_key?(key) } || @scopes.last
+ variable = scope[key]
+ variable = scope[key] = variable.call(self) if variable.is_a?(Proc)
+ variable = variable.to_liquid
+ variable.context = self if variable.respond_to?(:context=)
+ return variable
end
# resolves namespaced queries gracefully.
View
14 lib/liquid/document.rb
@@ -1,17 +1,17 @@
module Liquid
- class Document < Block
+ class Document < Block
# we don't need markup to open this block
def initialize(tokens)
parse(tokens)
- end
-
- # There isn't a real delimter
+ end
+
+ # There isn't a real delimter
def block_delimiter
[]
end
-
+
# Document blocks don't need to be terminated since they are not actually opened
def assert_missing_delimitation!
- end
+ end
end
-end
+end
View
33 lib/liquid/drop.rb
@@ -1,9 +1,9 @@
module Liquid
-
+
# A drop in liquid is a class which allows you to to export DOM like things to liquid
- # Methods of drops are callable.
- # The main use for liquid drops is the implement lazy loaded objects.
- # If you would like to make data available to the web designers which you don't want loaded unless needed then
+ # Methods of drops are callable.
+ # The main use for liquid drops is the implement lazy loaded objects.
+ # If you would like to make data available to the web designers which you don't want loaded unless needed then
# a drop is a great way to do that
#
# Example:
@@ -13,38 +13,39 @@ module Liquid
# Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
# end
# end
- #
+ #
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
- # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
+ # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
#
- # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
+ # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
# catch all
class Drop
attr_writer :context
- # Catch all for the method
+ # Catch all for the method
def before_method(method)
nil
end
-
+
# called by liquid to invoke a drop
- def invoke_drop(method)
- if self.class.public_instance_methods.include?(method.to_s)
- send(method.to_sym)
- else
+ def invoke_drop(method)
+ # for backward compatibility with Ruby 1.8
+ methods = self.class.public_instance_methods.map { |m| m.to_s }
+ if methods.include?(method.to_s)
+ send(method.to_sym)
+ else
before_method(method)
end
end
-
+
def has_key?(name)
true
end
-
+
def to_liquid
self
end
alias :[] :invoke_drop
end
-
end
View
54 lib/liquid/htmltags.rb
@@ -1,7 +1,7 @@
module Liquid
- class TableRow < Block
- Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/
-
+ class TableRow < Block
+ Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/
+
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@variable_name = $1
@@ -13,62 +13,62 @@ def initialize(tag_name, markup, tokens)
else
raise SyntaxError.new("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3")
end
-
- super
+
+ super
end
-
- def render(context)
+
+ def render(context)
collection = context[@collection_name] or return ''
-
+
if @attributes['limit'] or @attributes['offset']
limit = context[@attributes['limit']] || -1
offset = context[@attributes['offset']] || 0
collection = collection[offset.to_i..(limit.to_i + offset.to_i - 1)]
end
-
+
length = collection.length
-
+
cols = context[@attributes['cols']].to_i
row = 1
col = 0
result = ["<tr class=\"row1\">\n"]
- context.stack do
+ context.stack do
collection.each_with_index do |item, index|
context[@variable_name] = item
context['tablerowloop'] = {
'length' => length,
- 'index' => index + 1,
- 'index0' => index,
- 'col' => col + 1,
- 'col0' => col,
- 'index0' => index,
+ 'index' => index + 1,
+ 'index0' => index,
+ 'col' => col + 1,
+ 'col0' => col,
+ 'index0' => index,
'rindex' => length - index,
'rindex0' => length - index -1,
'first' => (index == 0),
'last' => (index == length - 1),
'col_first' => (col == 0),
'col_last' => (col == cols - 1)
- }
-
-
+ }
+
+
col += 1
-
+
result << ["<td class=\"col#{col}\">"] + render_all(@nodelist, context) + ['</td>']
- if col == cols and not (index == length - 1)
+ if col == cols and not (index == length - 1)
col = 0
row += 1
- result << ["</tr>\n<tr class=\"row#{row}\">"]
+ result << ["</tr>\n<tr class=\"row#{row}\">"]
end
-
+
end
end
result + ["</tr>\n"]
- end
+ end
end
-
- Template.register_tag('tablerow', TableRow)
-end
+
+ Template.register_tag('tablerow', TableRow)
+end
View
8 lib/liquid/module_ex.rb
@@ -18,7 +18,7 @@
# end
# end
#
-# if you want to extend the drop to other methods you can defines more methods
+# if you want to extend the drop to other methods you can defines more methods
# in the class <YourClass>::LiquidDropClass
#
# class SomeClass::LiquidDropClass
@@ -37,11 +37,11 @@
# output:
# 'this comes from an allowed method and this from another allowed method'
#
-# You can also chain associations, by adding the liquid_method call in the
+# You can also chain associations, by adding the liquid_method call in the
# association models.
#
class Module
-
+
def liquid_methods(*allowed_methods)
drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end"
define_method :to_liquid do
@@ -58,5 +58,5 @@ def initialize(object)
end
end
end
-
+
end
View
35 lib/liquid/strainer.rb
@@ -1,52 +1,51 @@
require 'set'
module Liquid
-
-
+
parent_object = if defined? BlankObject
BlankObject
else
Object
end
- # Strainer is the parent class for the filters system.
- # New filters are mixed into the strainer class which is then instanciated for each liquid template render run.
+ # Strainer is the parent class for the filters system.
+ # New filters are mixed into the strainer class which is then instanciated for each liquid template render run.
#
- # One of the strainer's responsibilities is to keep malicious method calls out
+ # One of the strainer's responsibilities is to keep malicious method calls out
class Strainer < parent_object #:nodoc:
- INTERNAL_METHOD = /^__/
- @@required_methods = Set.new([:__send__, :__id__, :respond_to?, :extend, :methods, :class])
-
+ INTERNAL_METHOD = /^__/
+ @@required_methods = Set.new([:__id__, :__send__, :respond_to?, :extend, :methods, :class, :object_id])
+
@@filters = {}
-
+
def initialize(context)
@context = context
end
-
+
def self.global_filter(filter)
raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
@@filters[filter.name] = filter
end
-
+
def self.create(context)
strainer = Strainer.new(context)
@@filters.each { |k,m| strainer.extend(m) }
strainer
end
-
+
def respond_to?(method, include_private = false)
method_name = method.to_s
return false if method_name =~ INTERNAL_METHOD
return false if @@required_methods.include?(method_name)
super
end
-
- # remove all standard methods from the bucket so circumvent security
- # problems
- instance_methods.each do |m|
- unless @@required_methods.include?(m.to_sym)
+
+ # remove all standard methods from the bucket so circumvent security
+ # problems
+ instance_methods.each do |m|
+ unless @@required_methods.include?(m.to_sym)
undef_method m
end
- end
+ end
end
end
View
16 lib/liquid/tag.rb
@@ -1,26 +1,26 @@
module Liquid
-
+
class Tag
attr_accessor :nodelist
-
+
def initialize(tag_name, markup, tokens)
@tag_name = tag_name
@markup = markup
parse(tokens)
end
-
+
def parse(tokens)
end
-
+
def name
self.class.name.downcase
end
-
+
def render(context)
''
- end
+ end
end
-
+
end
-
+
View
18 lib/liquid/tags/capture.rb
@@ -1,5 +1,5 @@
module Liquid
-
+
# Capture stores the result of a block into a variable without rendering it inplace.
#
# {% capture heading %}
@@ -8,28 +8,28 @@ module Liquid
# ...
# <h1>{{ monkeys }}</h1>
#
- # Capture is useful for saving content for use later in your template, such as
+ # Capture is useful for saving content for use later in your template, such as
# in a sidebar or footer.
#
class Capture < Block
Syntax = /(\w+)/
- def initialize(tag_name, markup, tokens)
+ def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@to = $1
else
raise SyntaxError.new("Syntax Error in 'capture' - Valid syntax: capture [var]")
end
-
- super
+
+ super
end
def render(context)
output = super
- context[@to] = output.to_s
+ context[@to] = output.join
''
end
- end
-
+ end
+
Template.register_tag('capture', Capture)
-end
+end
View
107 lib/liquid/template.rb
@@ -1,120 +1,121 @@
module Liquid
- # Templates are central to liquid.
- # Interpretating templates is a two step process. First you compile the
- # source code you got. During compile time some extensive error checking is performed.
- # your code should expect to get some SyntaxErrors.
+ # Templates are central to liquid.
+ # Interpretating templates is a two step process. First you compile the
+ # source code you got. During compile time some extensive error checking is performed.
+ # your code should expect to get some SyntaxErrors.
#
- # After you have a compiled template you can then <tt>render</tt> it.
- # You can use a compiled template over and over again and keep it cached.
+ # After you have a compiled template you can then <tt>render</tt> it.
+ # You can use a compiled template over and over again and keep it cached.
+ #
+ # Example:
#
- # Example:
- #
# template = Liquid::Template.parse(source)
# template.render('user_name' => 'bob')
#
class Template
attr_accessor :root
@@file_system = BlankFileSystem.new
-
- class <<self
+
+ class << self
def file_system
@@file_system
end
-
+
def file_system=(obj)
@@file_system = obj
end
-
- def register_tag(name, klass)
+
+ def register_tag(name, klass)
tags[name.to_s] = klass
- end
-
+ end
+
def tags
@tags ||= {}
end
-
- # Pass a module with filter methods which should be available
+
+ # Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library
- def register_filter(mod)
+ def register_filter(mod)
Strainer.global_filter(mod)
- end
-
+ end
+
# creates a new <tt>Template</tt> object from liquid source code
def parse(source)
template = Template.new
template.parse(source)
template
- end
+ end
end
# creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
def initialize
end
-
- # Parse source code.
- # Returns self for easy chaining
+
+ # Parse source code.
+ # Returns self for easy chaining
def parse(source)
@root = Document.new(tokenize(source))
self
end
-
- def registers
+
+ def registers
@registers ||= {}
end
-
+
def assigns
@assigns ||= {}
end
-
+
def errors
@errors ||= []
end
-
+
# Render takes a hash with local variables.
#
- # if you use the same filters over and over again consider registering them globally
+ # if you use the same filters over and over again consider registering them globally
# with <tt>Template.register_filter</tt>
- #
+ #
# Following options can be passed:
- #
+ #
# * <tt>filters</tt> : array with local filters
- # * <tt>registers</tt> : hash with register variables. Those can be accessed from
- # filters and tags and might be useful to integrate liquid more with its host application
+ # * <tt>registers</tt> : hash with register variables. Those can be accessed from
+ # filters and tags and might be useful to integrate liquid more with its host application
#
def render(*args)
- return '' if @root.nil?
+ return '' if @root.nil?
context = case args.first
when Liquid::Context
args.shift
when Hash
- self.assigns.merge!(args.shift)
- Context.new(assigns, registers, @rethrow_errors)
+ a = args.shift
+ assigns.each { |k,v| a[k] = v unless a.has_key?(k) }
+ Context.new(a, registers, @rethrow_errors)
when nil
- Context.new(assigns, registers, @rethrow_errors)
+ Context.new(assigns.dup, registers, @rethrow_errors)
else
raise ArgumentError, "Expect Hash or Liquid::Context as parameter"
end
-
+
case args.last
when Hash
options = args.pop
-
+
if options[:registers].is_a?(Hash)
- self.registers.merge!(options[:registers])
+ self.registers.merge!(options[:registers])
end
if options[:filters]
context.add_filters(options[:filters])
- end
-
+ end
+
when Module
- context.add_filters(args.pop)
+ context.add_filters(args.pop)
when Array
- context.add_filters(args.pop)
+ context.add_filters(args.pop)
end
-
+
begin
# render the nodelist.
# for performance reasons we get a array back here. join will make a string out of it
@@ -123,24 +124,24 @@ def render(*args)
@errors = context.errors
end
end
-
+
def render!(*args)
@rethrow_errors = true; render(*args)
end
-
+
private
-
+
# Uses the <tt>Liquid::TemplateParser</tt> regexp to tokenize the passed source
def tokenize(source)
- source = source.source if source.respond_to?(:source)
+ source = source.source if source.respond_to?(:source)
return [] if source.to_s.empty?
tokens = source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
- tokens.shift if tokens[0] and tokens[0].empty?
+ tokens.shift if tokens[0] and tokens[0].empty?
tokens
end
-
- end
+
+ end
end
View
9 lib/liquid/variable.rb
@@ -21,8 +21,7 @@ def initialize(markup)
@name = match[1]
if markup.match(/#{FilterSeparator}\s*(.*)/)
filters = Regexp.last_match(1).split(/#{FilterSeparator}/)
-
- filters.each do |f|
+ filters.each do |f|
if matches = f.match(/\s*(\w+)/)
filtername = matches[1]
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*(#{QuotedFragment})/).flatten
@@ -35,8 +34,7 @@ def initialize(markup)
def render(context)
return '' if @name.nil?
- output = context[@name]
- @filters.inject(output) do |output, filter|
+ @filters.inject(context[@name]) do |output, filter|
filterargs = filter[1].to_a.collect do |a|
context[a]
end
@@ -46,7 +44,6 @@ def render(context)
raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found."
end
end
- output
end
end
-end
+end
View
18 liquid.gemspec
@@ -1,10 +1,10 @@
Gem::Specification.new do |s|
s.name = %q{liquid}
- s.version = "1.9.0"
- s.specification_version = 2 if s.respond_to? :specification_version=
+ s.version = "2.0.1"
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Tobias Luetke"]
- s.date = %q{2008-06-23}
+ s.date = %q{2009-04-13}
s.description = %q{A secure non evaling end user template engine with aesthetic markup.}
s.email = %q{tobi@leetsoft.com}
s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"]
@@ -14,6 +14,16 @@ Gem::Specification.new do |s|
s.rdoc_options = ["--main", "README.txt"]
s.require_paths = ["lib"]
s.rubyforge_project = %q{liquid}
- s.rubygems_version = %q{1.2.0}
+ s.rubygems_version = %q{1.3.1}
s.summary = %q{A secure non evaling end user template engine with aesthetic markup.}
+
+ if s.respond_to? :specification_version then
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
+ s.specification_version = 2
+
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
+ else
+ end
+ else
+ end
end
View
92 performance/shopify.rb
@@ -0,0 +1,92 @@
+# This profiler run simulates Shopify.
+# We are looking in the tests directory for liquid files and render them within the designated layout file.
+# We will also export a substantial database to liquid which the templates can render values of.
+# All this is to make the benchmark as non syntetic as possible. All templates and tests are lifted from
+# direct real-world usage and the profiler measures code that looks very similar to the way it looks in
+# Shopify which is likely the biggest user of liquid in the world which something to the tune of several
+# million Template#render calls a day.
+
+require 'rubygems'
+require 'active_support'
+require 'yaml'
+require 'digest/md5'
+require File.dirname(__FILE__) + '/shopify/liquid'
+require File.dirname(__FILE__) + '/shopify/database.rb'
+
+require "ruby-prof" rescue fail("install ruby-prof extension/gem")
+
+class ThemeProfiler
+
+ # Load all templates into memory, do this now so that
+ # we don't profile IO.
+ def initialize
+ @tests = Dir[File.dirname(__FILE__) + '/tests/**/*.liquid'].collect do |test|
+ next if File.basename(test) == 'theme.liquid'
+
+ theme_path = File.dirname(test) + '/theme.liquid'
+
+ [File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
+ end.compact
+ end
+
+
+ def profile
+ RubyProf.measure_mode = RubyProf::WALL_TIME
+
+ # Dup assigns because will make some changes to them
+ assigns = Database.tables.dup
+
+ @tests.each do |liquid, layout, template_name|
+
+ # Compute page_tempalte outside of profiler run, uninteresting to profiler
+ html = nil
+ page_template = File.basename(template_name, File.extname(template_name))
+
+ # Profile compiling and rendering both
+ RubyProf.resume { html = compile_and_render(liquid, layout, assigns, page_template) }
+
+ # return the result and the MD5 of the content, this can be used to detect regressions between liquid version
+ $stdout.puts "* rendered template %s, content: %s" % [template_name, Digest::MD5.hexdigest(html)]
+
+ # Uncomment to dump html files to /tmp so that you can inspect for errors
+ # File.open("/tmp/#{File.basename(template_name)}.html", "w+") { |fp| fp <<html}
+ end
+
+ RubyProf.stop
+ end
+
+ def compile_and_render(template, layout, assigns, page_template)
+ tmpl = Liquid::Template.new
+ tmpl.assigns['page_title'] = 'Page title'
+ tmpl.assigns['template'] = page_template
+
+ content_for_layout = tmpl.parse(template).render(assigns)
+
+ if layout
+ assigns['content_for_layout'] = content_for_layout
+ tmpl.parse(layout).render(assigns)
+ else
+ content_for_layout
+ end
+ end
+end
+
+
+ profiler = ThemeProfiler.new
+
+ puts 'Running profiler...'
+
+ results = profiler.profile
+
+ puts 'Success'
+ puts
+
+[RubyProf::FlatPrinter, RubyProf::GraphPrinter, RubyProf::GraphHtmlPrinter].each do |klass|
+ filename = (ENV['TMP'] || '/tmp') + (klass.name.include?('Html') ? "/liquid.#{klass.name.downcase}.html" : "/liquid.#{klass.name.downcase}.txt")
+ filename.gsub!(/:+/, '_')
+ File.open(filename, "w+") { |fp| klass.new(results).print(fp) }
+ $stderr.puts "wrote #{klass.name} output to #{filename}"
+end
+
+
+
View
33 performance/shopify/comment_form.rb
@@ -0,0 +1,33 @@
+class CommentForm < Liquid::Block
+ Syntax = /(#{Liquid::VariableSignature}+)/
+
+ def initialize(tag_name, markup, tokens)
+ if markup =~ Syntax
+ @variable_name = $1
+ @attributes = {}
+ else
+ raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]")
+ end
+
+ super
+ end
+
+ def render(context)
+ article = context[@variable_name]
+
+ context.stack do
+ context['form'] = {
+ 'posted_successfully?' => context.registers[:posted_successfully],
+ 'errors' => context['comment.errors'],
+ 'author' => context['comment.author'],
+ 'email' => context['comment.email'],
+ 'body' => context['comment.body']
+ }
+ wrap_in_form(article, render_all(@nodelist, context))
+ end
+ end
+
+ def wrap_in_form(article, input)
+ %Q{<form id="article-#{article.id}-comment-form" class="comment-form" method="post" action="">\n#{input}\n</form>}
+ end
+end
View
45 performance/shopify/database.rb
@@ -0,0 +1,45 @@
+require 'yaml'
+module Database
+
+ # Load the standard vision toolkit database and re-arrage it to be simply exportable
+ # to liquid as assigns. All this is based on Shopify
+ def self.tables
+ @tables ||= begin
+ db = YAML.load_file(File.dirname(__FILE__) + '/vision.database.yml')
+
+ # From vision source
+ db['products'].each do |product|
+ collections = db['collections'].find_all do |collection|
+ collection['products'].any? { |p| p['id'].to_i == product['id'].to_i }
+ end
+ product['collections'] = collections
+ end
+
+ # key the tables by handles, as this is how liquid expects it.
+ db = db.inject({}) do |assigns, (key, values)|
+ assigns[key] = values.inject({}) { |h, v| h[v['handle']] = v; h; }
+ assigns
+ end
+
+ # Some standard direct accessors so that the specialized templates
+ # render correctly
+ db['collection'] = db['collections'].values.first
+ db['product'] = db['products'].values.first
+ db['blog'] = db['blogs'].values.first
+ db['article'] = db['blog']['articles'].first
+
+ db['cart'] = {
+ 'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] },
+ 'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] },
+ 'items' => db['line_items'].values
+ }
+
+ db
+ end
+ end
+end
+
+if __FILE__ == $0
+ p Database.tables['collections']['frontpage'].keys
+ #p Database.tables['blog']['articles']
+end
View
7 performance/shopify/json_filter.rb
@@ -0,0 +1,7 @@
+module JsonFilter
+
+ def json(object)
+ object.reject {|k,v| k == "collections" }.to_json
+ end
+
+end
View
18 performance/shopify/liquid.rb
@@ -0,0 +1,18 @@
+require File.dirname(__FILE__) + '/../../lib/liquid'
+
+require File.dirname(__FILE__) + '/comment_form'
+require File.dirname(__FILE__) + '/paginate'
+require File.dirname(__FILE__) + '/json_filter'
+require File.dirname(__FILE__) + '/money_filter'
+require File.dirname(__FILE__) + '/shop_filter'
+require File.dirname(__FILE__) + '/tag_filter'
+require File.dirname(__FILE__) + '/weight_filter'
+
+Liquid::Template.register_tag 'paginate', Paginate
+Liquid::Template.register_tag 'form', CommentForm
+
+Liquid::Template.register_filter JsonFilter
+Liquid::Template.register_filter MoneyFilter
+Liquid::Template.register_filter WeightFilter
+Liquid::Template.register_filter ShopFilter
+Liquid::Template.register_filter TagFilter
View
18 performance/shopify/money_filter.rb
@@ -0,0 +1,18 @@
+module MoneyFilter
+
+ def money_with_currency(money)
+ return '' if money.nil?
+ sprintf("$ %.2f USD", money/100.0)
+ end
+
+ def money(money)
+ return '' if money.nil?
+ sprintf("$ %.2f", money/100.0)
+ end
+
+ private
+
+ def currency
+ ShopDrop.new.currency
+ end
+end
View
93 performance/shopify/paginate.rb
@@ -0,0 +1,93 @@
+class Paginate < Liquid::Block
+ Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
+
+ def initialize(tag_name, markup, tokens)
+ @nodelist = []
+
+ if markup =~ Syntax
+ @collection_name = $1
+ @page_size = if $2
+ $3.to_i
+ else
+ 20
+ end
+
+ @attributes = { 'window_size' => 3 }
+ markup.scan(Liquid::TagAttributes) do |key, value|
+ @attributes[key] = value
+ end
+ else
+ raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number")
+ end
+
+ super
+ end
+
+ def render(context)
+ @context = context
+
+ context.stack do
+ current_page = context['current_page'].to_i
+
+ pagination = {
+ 'page_size' => @page_size,
+ 'current_page' => 5,
+ 'current_offset' => @page_size * 5
+ }
+
+ context['paginate'] = pagination
+
+ collection_size = context[@collection_name].size
+
+ raise ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection_size.nil?
+
+ page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
+
+ pagination['items'] = collection_size
+ pagination['pages'] = page_count -1
+ pagination['previous'] = link('&laquo; Previous', current_page-1 ) unless 1 >= current_page
+ pagination['next'] = link('Next &raquo;', current_page+1 ) unless page_count <= current_page+1
+ pagination['parts'] = []
+
+ hellip_break = false
+
+ if page_count > 2
+ 1.upto(page_count-1) do |page|
+
+ if current_page == page
+ pagination['parts'] << no_link(page)
+ elsif page == 1
+ pagination['parts'] << link(page, page)
+ elsif page == page_count -1
+ pagination['parts'] << link(page, page)
+ elsif page <= current_page - @attributes['window_size'] or page >= current_page + @attributes['window_size']
+ next if hellip_break
+ pagination['parts'] << no_link('&hellip;')
+ hellip_break = true
+ next
+ else
+ pagination['parts'] << link(page, page)
+ end
+
+ hellip_break = false
+ end
+ end
+
+ render_all(@nodelist, context)
+ end
+ end
+
+ private
+
+ def no_link(title)
+ { 'title' => title, 'is_link' => false}
+ end
+
+ def link(title, page)
+ { 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true}
+ end
+
+ def current_url
+ "/collections/frontpage"
+ end
+end
View
98 performance/shopify/shop_filter.rb
@@ -0,0 +1,98 @@
+module ShopFilter
+
+ def asset_url(input)
+ "/files/1/[shop_id]/[shop_id]/assets/#{input}"
+ end
+
+ def global_asset_url(input)
+ "/global/#{input}"
+ end
+
+ def shopify_asset_url(input)
+ "/shopify/#{input}"
+ end
+
+ def script_tag(url)
+ %(<script src="#{url}" type="text/javascript"></script>)
+ end
+
+ def stylesheet_tag(url, media="all")
+ %(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />)
+ end
+
+ def link_to(link, url, title="")
+ %|<a href="#{url}" title="#{title}">#{link}</a>|
+ end
+
+ def img_tag(url, alt="")
+ %|<img src="#{url}" alt="#{alt}" />|
+ end
+
+ def link_to_vendor(vendor)
+ if vendor
+ link_to vendor, url_for_vendor(vendor), vendor
+ else
+ 'Unknown Vendor'
+ end
+ end
+
+ def link_to_type(type)
+ if type
+ link_to type, url_for_type(type), type
+ else
+ 'Unknown Vendor'
+ end
+ end
+
+ def url_for_vendor(vendor_title)
+ "/collections/#{vendor_title.to_handle}"
+ end
+
+ def url_for_type(type_title)
+ "/collections/#{type_title.to_handle}"
+ end
+
+ def product_img_url(url, style = 'small')
+
+ unless url =~ /^products\/([\w\-\_]+)\.(\w{2,4})/
+ raise ArgumentError, 'filter "size" can only be called on product images'
+ end
+
+ case style
+ when 'original'
+ return '/files/shops/random_number/' + url
+ when 'grande', 'large', 'medium', 'small', 'thumb', 'icon'
+ "/files/shops/random_number/products/#{$1}_#{style}.#{$2}"
+ else
+ raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, small, thumb and icon '
+ end
+ end
+
+ def default_pagination(paginate)
+
+ html = []
+ html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous']
+
+ for part in paginate['parts']
+
+ if part['is_link']
+ html << %(<span class="page">#{link_to(part['title'], part['url'])}</span>)
+ elsif part['title'].to_i == paginate['current_page'].to_i
+ html << %(<span class="page current">#{part['title']}</span>)
+ else
+ html << %(<span class="deco">#{part['title']}</span>)
+ end
+
+ end
+
+ html << %(<span class="next">#{link_to(paginate['next']['title'], paginate['next']['url'])}</span>) if paginate['next']
+ html.join(' ')
+ end
+
+ # Accepts a number, and two words - one for singular, one for plural
+ # Returns the singular word if input equals 1, otherwise plural
+ def pluralize(input, singular, plural)
+ input == 1 ? singular : plural
+ end
+
+end
View
25 performance/shopify/tag_filter.rb
@@ -0,0 +1,25 @@
+module TagFilter
+
+ def link_to_tag(label, tag)
+ "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>"
+ end
+
+ def highlight_active_tag(tag, css_class='active')
+ if @context['current_tags'].include?(tag)
+ "<span class=\"#{css_class}\">#{tag}</span>"
+ else
+ tag
+ end
+ end
+
+ def link_to_add_tag(label, tag)
+ tags = (@context['current_tags'] + [tag]).uniq
+ "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
+ end
+
+ def link_to_remove_tag(label, tag)
+ tags = (@context['current_tags'] - [tag]).uniq
+ "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
+ end
+
+end
View
945 performance/shopify/vision.database.yml
@@ -0,0 +1,945 @@
+# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+# Variants
+# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+
+product_variants:
+ - &product-1-var-1
+ id: 1
+ title: 151cm / Normal
+ price: 19900
+ weight: 1000
+ compare_at_price: 49900
+ available: true
+ inventory_quantity: 5
+ option1: 151cm
+ option2: Normal
+ option3:
+ - &product-1-var-2
+ id: 2
+ title: 155cm / Normal
+ price: 31900
+ weight: 1000
+ compare_at_price: 50900
+ available: true
+ inventory_quantity: 2
+ option1: 155cm
+ option2: Normal
+ option3:
+ - &product-2-var-1
+ id: 3
+ title: 162cm
+ price: 29900
+ weight: 1000
+ compare_at_price: 52900
+ available: true
+ inventory_quantity: 3
+ option1: 162cm
+ option2:
+ option3:
+ - &product-3-var-1
+ id: 4
+ title: 159cm
+ price: 19900
+ weight: 1000
+ compare_at_price:
+ available: true
+ inventory_quantity: 4
+ option1: 159cm
+ option2:
+ option3:
+ - &product-4-var-1
+ id: 5
+ title: 159cm
+ price: 19900
+ weight: 1000
+ compare_at_price: 32900
+ available: true
+ inventory_quantity: 6
+ option1: 159cm
+ option2:
+ option3:
+ - &product-1-var-3
+ id: 6
+ title: 158cm / Wide
+ price: 23900
+ weight: 1000
+ compare_at_price: 99900
+ available: false
+ inventory_quantity: 0
+ option1: 158cm
+ option2: Wide
+ option3:
+ - &product-3-var-2
+ id: 7
+ title: 162cm
+ price: 19900
+ weight: 1000
+ compare_at_price:
+ available: false
+ inventory_quantity: 0
+ option1: 162cm
+ option2:
+ option3:
+ - &product-3-var-3
+ id: 8
+ title: 165cm
+ price: 22900
+ weight: 1000
+ compare_at_price:
+ available: true
+ inventory_quantity: 4
+ option1: 165cm
+ option2:
+ option3:
+ - &product-5-var-1
+ id: 9
+ title: black / 42
+ price: 11900
+ weight: 500
+ compare_at_price: 22900
+ available: true
+ inventory_quantity: 1
+ option1: black
+ option2: 42
+ option3:
+ - &product-5-var-2
+ id: 10
+ title: beige / 42
+ price: 11900
+ weight: 500
+ compare_at_price: 22900
+ available: true
+ inventory_quantity: 3
+ option1: beige
+ option2: 42
+ option3:
+ - &product-5-var-3
+ id: 11
+ title: white / 42
+ price: 13900
+ weight: 500
+ compare_at_price: 24900
+ available: true
+ inventory_quantity: 1
+ option1: white
+ option2: 42
+ option3:
+ - &product-5-var-4
+ id: 12
+ title: black / 44
+ price: 11900
+ weight: 500
+ compare_at_price: 22900
+ available: true
+ inventory_quantity: 2
+ option1: black
+ option2: 44
+ option3:
+ - &product-5-var-5
+ id: 13
+ title: beige / 44
+ price: 11900
+ weight: 500
+ compare_at_price: 22900
+ available: false
+ inventory_quantity: 0
+ option1: beige
+ option2: 44
+ option3:
+ - &product-5-var-6
+ id: 14
+ title: white / 44
+ price: 13900
+ weight: 500
+ compare_at_price: 24900
+ available: false
+ inventory_quantity: 0
+ option1: white
+ option2: 44
+ option3:
+ - &product-6-var-1
+ id: 15
+ title: red
+ price: 2179500
+ weight: 200000
+ compare_at_price:
+ available: true
+ inventory_quantity: 0
+ option1: red
+ option2:
+ option3:
+ - &product-7-var-1
+ id: 16
+ title: black / small
+ price: 1900
+ weight: 200
+ compare_at_price:
+ available: true
+ inventory_quantity: 20
+ option1: black
+ option2: small
+ option3:
+ - &product-7-var-2
+ id: 17
+ title: black / medium
+ price: 1900
+ weight: 200
+ compare_at_price:
+ available: false
+ inventory_quantity: 0
+ option1: black
+ option2: medium
+ option3:
+ - &product-7-var-3
+ id: 18
+ title: black / large
+ price: 1900
+ weight: 200
+ compare_at_price:
+ available: true
+ inventory_quantity: 10
+ option1: black
+ option2: large
+ option3:
+ - &product-7-var-4
+ id: 19
+ title: black / extra large
+ price: 1900
+ weight: 200
+ compare_at_price:
+ available: false
+ inventory_quantity: 0
+ option1: black
+ option2: extra large
+ option3:
+ - &product-8-var-1
+ id: 20
+ title: brown / small
+ price: 5900
+ weight: 400
+ compare_at_price: 6900
+ available: true
+ inventory_quantity: 5
+ option1: brown
+ option2: small
+ option3:
+ - &product-8-var-2
+ id: 21
+ title: brown / medium
+ price: 5900
+ weight: 400
+ compare_at_price: 6900
+ available: false
+ inventory_quantity: 0
+ option1: brown
+ option2: medium
+ option3:
+ - &product-8-var-3
+ id: 22
+ title: brown / large
+ price: 5900
+ weight: 400
+ compare_at_price: 6900
+ available: true
+ inventory_quantity: 10
+ option1: brown
+ option2: large
+ option3:
+ - &product-8-var-4
+ id: 23
+ title: black / small
+ price: 5900
+ weight: 400
+ compare_at_price: 6900
+ available: true
+ inventory_quantity: 10
+ option1: black
+ option2: small
+ option3:
+ - &product-8-var-5
+ id: 24
+ title: black / medium
+ price: 5900
+ weight: 400
+ compare_at_price: 6900
+ available: true
+ inventory_quantity: 10
+ option1: black
+ option2: medium
+ option3:
+ - &product-8-var-6
+ id: 25
+ title: black / large
+ price: 5900
+ weight: 400
+ compare_at_price: 6900
+ available: false
+ inventory_quantity: 0
+ option1: black
+ option2: large
+ option3:
+ - &product-9-var-1
+ id: 26
+ title: Body Only
+ price: 499995
+ weight: 2000
+ compare_at_price:
+ available: true
+ inventory_quantity: 3
+ option1: Body Only
+ option2:
+ option3:
+ - &product-9-var-2
+ id: 27
+ title: Kit with 18-55mm VR lens
+ price: 523995
+ weight: 2000
+ compare_at_price:
+ available: true
+ inventory_quantity: 2
+ option1: Kit with 18-55mm VR lens
+ option2:
+ option3:
+ - &product-9-var-3
+ id: 28
+ title: Kit with 18-200 VR lens
+ price: 552500
+ weight: 2000
+ compare_at_price:
+ available: true
+ inventory_quantity: 3
+ option1: Kit with 18-200 VR lens
+ option2:
+ option3:
+
+# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+# Products
+# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+
+products:
+ - &product-1
+ id: 1
+ title: Arbor Draft
+ handle: arbor-draft
+ type: Snowboards
+ vendor: Arbor
+ price: 23900
+ price_max: 31900
+ price_min: 23900
+ price_varies: true
+ available: true
+ tags:
+ - season2005
+ - pro
+ - intermediate
+ - wooden
+ - freestyle
+ options:
+ - Length
+ - Style
+ compare_at_price: 49900
+ compare_at_price_max: 50900
+ compare_at_price_min: 49900
+ compare_at_price_varies: true
+ url: /products/arbor-draft
+ featured_image: products/arbor_draft.jpg
+ images:
+ - products/arbor_draft.jpg
+ description:
+ The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a new level. The board's freaky Tiki design pays homage to culture that inspired snowboarding. It's designed to spin with ease, land smoothly, lock hook-free onto rails, and take the abuse of a pavement pounding or twelve. The Draft will pop off kickers with authority and carve solidly across the pipe. The Draft features targeted Koa wood die cuts inlayed into the deck that enhance the flex pattern. Now bow down to riding's ancestors.
+ variants:
+ - *product-1-var-1
+ - *product-1-var-2
+ - *product-1-var-3
+ - &product-2
+ id: 2
+ title: Arbor Element
+ handle: arbor-element
+ type: Snowboards
+ vendor: Arbor
+ price: 29900
+ price_max: 29900
+ price_min: 29900
+ price_varies: false
+ available: true
+ tags:
+ - season2005
+ - pro
+ - wooden
+ - freestyle
+ options:
+ - Length
+ compare_at_price: 52900
+ compare_at_price_max: 52900
+ compare_at_price_min: 52900
+ compare_at_price_varies: false
+ url: /products/arbor-element
+ featured_image: products/element58.jpg
+ images:
+ - products/element58.jpg
+ description:
+ The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience. The Element is exceedingly lively, freely initiates, and holds a tight edge at speed. Its structural real-wood topsheet is made with book-matched Koa.
+ variants:
+ - *product-2-var-1
+
+ - &product-3
+ id: 3
+ title: Comic ~ Pastel
+ handle: comic-pastel
+ type: Snowboards
+ vendor: Technine
+ price: 19900
+ price_max: 22900
+ price_min: 19900
+ tags:
+ - season2006
+ - beginner
+ - intermediate
+ - freestyle
+ - purple
+ options:
+ - Length
+ price_varies: true
+ available: true
+ compare_at_price:
+ compare_at_price_max: 0
+ compare_at_price_min: 0
+ compare_at_price_varies: false
+ url: /products/comic-pastel
+ featured_image: products/technine1.jpg
+ images:
+ - products/technine1.jpg
+ - products/technine2.jpg
+ - products/technine_detail.jpg
+ description:
+ 2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all.
+ variants:
+ - *product-3-var-1
+ - *product-3-var-2
+ - *product-3-var-3
+
+ - &product-4
+ id: 4
+ title: Comic ~ Orange
+ handle: comic-orange
+ type: Snowboards
+ vendor: Technine
+ price: 19900
+ price_max: 19900
+ price_min: 19900
+ price_varies: false
+ available: true
+ tags:
+ - season2006
+ - beginner
+ - intermediate
+ - freestyle
+ - orange
+ options:
+ - Length
+ compare_at_price: 32900
+ compare_at_price_max: 32900
+ compare_at_price_min: 32900
+ compare_at_price_varies: false
+ url: /products/comic-orange
+ featured_image: products/technine3.jpg
+ images:
+ - products/technine3.jpg
+ - products/technine4.jpg
+ description:
+ 2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all.
+ variants:
+ - *product-4-var-1
+
+ - &product-5
+ id: 5
+ title: Burton Boots
+ handle: burton-boots
+ type: Boots
+ vendor: Burton
+ price: 11900
+ price_max: 11900
+ price_min: 11900
+ price_varies: false
+ available: true
+ tags:
+ - season2006
+ - beginner
+ - intermediate
+ - boots
+ options:
+ - Color
+ - Shoe Size
+ compare_at_price: 22900
+ compare_at_price_max: 22900
+ compare_at_price_min: 22900
+ compare_at_price_varies: false
+ url: /products/burton-boots
+ featured_image: products/burton.jpg
+ images:
+ - products/burton.jpg
+ description:
+ The Burton boots are particularly well on snowboards. The very best thing about them is that the according picture is cubic. This makes testing in a Vision testing environment very easy.
+ variants:
+ - *product-5-var-1
+ - *product-5-var-2
+ - *product-5-var-3
+ - *product-5-var-4
+ - *product-5-var-5
+ - *product-5-var-6
+
+ - &product-6
+ id: 6
+ title: Superbike 1198 S
+ handle: superbike
+ type: Superbike
+ vendor: Ducati
+ price: 2179500
+ price_max: 2179500
+ price_min: 2179500
+ price_varies: false
+ available: true
+ tags:
+ - ducati
+ - superbike
+ - bike
+ - street
+ - racing
+ - performance
+ options:
+ - Color
+ compare_at_price:
+ compare_at_price_max: 0
+ compare_at_price_min: 0
+ compare_at_price_varies: false
+ url: /products/superbike
+ featured_image: products/ducati.jpg
+ images:
+ - products/ducati.jpg
+ description:
+ <h3>‘S’ PERFORMANCE</h3>
+ <p>Producing 170hp (125kW) and with a dry weight of just 169kg (372.6lb), the new 1198 S now incorporates more World Superbike technology than ever before by taking the 1198 motor and adding top-of-the-range suspension, lightweight chassis components and a true racing-style traction control system designed for road use.</p>
+ <p>The high performance, fully adjustable 43mm Öhlins forks, which sport low friction titanium nitride-treated fork sliders, respond effortlessly to every imperfection in the tarmac. Beyond their advanced engineering solutions, one of the most important characteristics of Öhlins forks is their ability to communicate the condition and quality of the tyre-to-road contact patch, a feature that puts every rider in superior control. The suspension set-up at the rear is complemented with a fully adjustable Öhlins rear shock equipped with a ride enhancing top-out spring and mounted to a single-sided swingarm for outstanding drive and traction. The front-to-rear Öhlins package is completed with a control-enhancing adjustable steering damper.</p>
+ variants:
+ - *product-6-var-1
+
+ - &product-7
+ id: 7
+ title: Shopify Shirt
+ handle: shopify-shirt
+ type: Shirt
+ vendor: Shopify
+ price: 1900
+ price_max: 1900
+ price_min: 1900
+ price_varies: false
+ available: true
+ tags:
+ - shopify
+ - shirt
+ - apparel
+ - tshirt
+ - clothing
+ options:
+ - Color
+ - Size
+ compare_at_price:
+ compare_at_price_max: 0
+ compare_at_price_min: 0
+ compare_at_price_varies: false