Compare commits

..
Author SHA1 Message Date
Sam Doiron ba82f8d05e Perf: Remove unneeded to_s and freeze strings
- No longer call to_s on string literals passed to undefined filters
- Freeze all string literals at the top of the generated code

This shows a ~9% performance improvement on the fluid benchmark when run without YJIT
2022-04-20 11:03:36 -03:00
Sam Doiron 7c70b56002 Fix implementation of escape and money filters 2022-04-18 11:22:19 -03:00
Sam Doiron 8a71d29bfb Add + tailor fluid benchmark 2022-04-18 10:32:44 -03:00
Sam Doiron f6d1b56b4e Add single-benchmark execute, simplify compilation 2022-04-13 11:14:23 -03:00
Sam Doiron 45ddc00710 Add benchmark viewer 2022-04-07 18:15:18 -03:00
Sam Doiron c2b7b3df50 Almost all tests passing, except profiler 2022-04-06 17:35:00 -03:00
Dylan Thacker-SmithandGitHub 6e07f73f68 History.md: Remove non-fix from fixes section of recent release. (#1556) 2022-03-23 11:59:50 -04:00
Marc-André CournoyerandGitHub f64af57b7b Merge pull request #1540 from Watson1978/remove-redundant-regexp
Remove redundant regexp
2022-03-22 14:52:49 -04:00
Marc-André CournoyerandGitHub c60c3c7802 Merge pull request #1554 from Shopify/bump-5.3.0
Update changelog & bump version for 5.3.0 release
2022-03-22 13:30:28 -04:00
Marc-André Cournoyer 11625b1bc9 Update release date 2022-03-22 13:24:02 -04:00
Marc-André Cournoyer ec6fb4d5fa Update changelog & bump version for 5.3.0 release 2022-03-17 15:39:04 -04:00
Watson fad58ef436 Use String#match? instead of String#=~ to reduce allocation for backreferecne
## Test code
```ruby
require 'benchmark/ips'

WhitespaceOrNothing = /\A\s*\z/
token = " " * 20
token =~ WhitespaceOrNothing

Benchmark.ips do |x|
  x.report("=~") {
    token =~ WhitespaceOrNothing
  }
  x.report("match?") {
    token.match?(WhitespaceOrNothing)
  }

  x.compare!
end
```

## Result
```
Warming up --------------------------------------
                  =~   271.356k i/100ms
              match?   579.655k i/100ms
Calculating -------------------------------------
                  =~      2.717M (± 0.4%) i/s -     13.839M in   5.092947s
              match?      5.695M (± 1.6%) i/s -     28.983M in   5.090640s

Comparison:
              match?:  5694747.3 i/s
                  =~:  2717370.9 i/s - 2.10x  (± 0.00) slower
```
2022-03-16 12:44:27 +09:00
Watson 22568080b1 Revert "Use strip & empty? to detect Whitespaces"
This reverts commit dd7ed00ec4.
2022-03-16 12:33:45 +09:00
Watson dd7ed00ec4 Use strip & empty? to detect Whitespaces
## Test code
```ruby
require 'benchmark/ips'

WhitespaceOrNothing = /\A\s*\z/
token = " " * 20

Benchmark.ips do |x|
  x.report("WhitespaceOrNothing") {
    token =~ WhitespaceOrNothing
  }
  x.report("strip & empty?") {
    token.strip.empty?
  }

  x.compare!
end
```

## Result
```
Warming up --------------------------------------
 WhitespaceOrNothing   266.391k i/100ms
      strip & empty?     1.044M i/100ms
Calculating -------------------------------------
 WhitespaceOrNothing      2.705M (± 0.4%) i/s -     13.586M in   5.023453s
      strip & empty?     10.400M (± 1.1%) i/s -     52.182M in   5.017990s

Comparison:
      strip & empty?: 10400286.2 i/s
 WhitespaceOrNothing:  2704552.3 i/s - 3.85x  (± 0.00) slower
```
2022-03-12 19:24:56 +09:00
Watson 1667c1180e Use start_with? and end_with? to detect SQUARE_BRAKET
## Test code
```ruby
require 'benchmark/ips'

SQUARE_BRACKETED = /\A\[(.*)\]\z/m
markup = "[product.catchall]"

Benchmark.ips do |x|
  x.report("SQUARE_BRACKETED") {
    if markup =~ SQUARE_BRACKETED
      Regexp.last_match(1)
    end
  }
  x.report("start/end_with?") {
    if markup&.start_with?('[') && markup&.end_with?(']')
      markup[1..-2]
    end
  }

  x.compare!
end
```

## Result
```
Warming up --------------------------------------
    SQUARE_BRACKETED   261.300k i/100ms
     start/end_with?   548.813k i/100ms
Calculating -------------------------------------
    SQUARE_BRACKETED      2.632M (± 0.6%) i/s -     13.326M in   5.064085s
     start/end_with?      5.471M (± 0.5%) i/s -     27.441M in   5.015770s

Comparison:
     start/end_with?:  5470994.1 i/s
    SQUARE_BRACKETED:  2631642.3 i/s - 2.08x  (± 0.00) slower
```
2022-03-12 18:14:16 +09:00
Thierry JoyalandGitHub 7357dcf185 Merge pull request #1536 from Shopify/flaky-profiler-test-v2
Add artificial execution time in profiler tests
2022-03-07 10:06:13 -05:00
Thierry JoyalandGitHub 68c3827ef2 Merge pull request #1525 from Shopify/standardfilter/fix-missing-context-on-iterations
[StandardFilter] Fix missing @context on iterations
2022-03-07 09:19:18 -05:00
Thierry Joyal 4af38bc549 [StandardFilter] Fix missing @context on iterations 2022-03-07 09:17:07 -05:00
Thierry Joyal df241abf70 Add artificial execution time in profiler tests 2022-03-07 08:50:03 -05:00
10f8337209 Test Ruby 3.1 in CI (#1533)
Co-authored-by: Dylan Thacker-Smith <[email protected]>
2022-03-04 13:23:04 -05:00
Thierry JoyalandGitHub 5ed0410a8b Merge pull request #1534 from Shopify/context-test-cleanup
Context test cleanup
2022-03-04 11:31:00 -05:00
Thierry Joyal 0f5220c391 ContextTest: Classes to use appropriate ancestor 2022-03-04 09:14:57 -05:00
Thierry Joyal 7a23f46fab ContextTest: Cleanup global variable assignments 2022-03-04 09:10:38 -05:00
Peter ZhuandGitHub 3f7edf00b9 Merge pull request #1531 from Shopify/pz-array-fetch-warning
Fix warning about block and default value
2022-03-02 16:25:23 -05:00
Thierry JoyalandGitHub b4a2a79e26 Merge pull request #1527 from Shopify/condition/receive-mandatory-context-argument
Condition#evaluate to receive mandatory context argument
2022-03-02 15:04:24 -05:00
Thierry Joyal 1d2bee1f60 Condition#evaluate to receive mandatory context argument 2022-03-02 14:35:31 -05:00
Peter Zhu 01e6eec97a Fix warning about block and default value
Ruby's Array#fetch accepts either a default value or a block, but not
both. If both are passed in, then it uses the block and outputs this
warning:

```
lib/liquid/static_registers.rb:34: warning: block supersedes default value argument
```
2022-03-02 14:33:12 -05:00
Jean Boussier fbdab19358 We're in 2022... 2022-03-02 18:27:25 +01:00
Thierry JoyalandGitHub ce85ac5d3d Merge pull request #1529 from Shopify/tests/standard-filters-with-context
StandardFiltersTest: Initialize following production code paths with context
2022-03-02 08:44:16 -05:00
Thierry Joyal c0ffee16a3 StandardFiltersTest: Initialize following production code paths with context 2022-03-01 16:01:00 +00:00
Jean Boussier a7eb33fa39 Release 5.2.0 2022-03-01 16:18:49 +01:00
Jean byroot BoussierandGitHub 1a85e98793 Merge pull request #1524 from Shopify/global-constant-cache
Eagerly cache global filters
2022-03-01 16:14:31 +01:00
Jean Boussier c588337aac Eagerly cache global filters
Including a module can cause Ruby's global constant cache to be busted
if the included module contain constants. So that's something you don't
want to happen at "runtime", otherwise it will severely degrade performance
and if you are using YJIT or MJIT most of the compiled code will be invalidated.

To limit the impact of this, we can pre-include the global filters,
as they're generally registered during boot, that limits the problem
to non-global filters.
2022-03-01 13:40:40 +01:00
Dylan Thacker-SmithandGitHub 97f7922457 Add missing changelog entry for PR #1518 (#1521) 2022-02-24 14:46:26 -05:00
0d83e64cfe Add replace_last and remove_last filters (#1422)
Co-authored-by: ADTC <[email protected]>
Co-authored-by: Dylan Thacker-Smith <[email protected]>
2022-02-24 14:02:15 -05:00
Dylan Thacker-SmithandGitHub 0d5e01ae98 Fix some internal errors in filters from invalid input. (#1476)
These fixes came from improving the corresponding test, so these might not
actually be causing problems in practice.
2022-02-24 09:17:37 -05:00
Charles-Philippe ClermontandGitHub 15eaa49e48 Merge pull request #1518 from Shopify/fix/kwarg-key-name-liquid-c-inconsistency
Fix kwarg parsing inconsistency with Liquid::C
2022-02-14 13:22:12 -05:00
Tobias LütkeandGitHub 91c54c579d Merge pull request #1477 from Watson1978/performance
Increase parsing performance
2022-02-14 12:25:19 -05:00
Charles-P. Clermont 1310c4978d Fix kwarg parsing inconsistency with Liquid::C
Liquid::C parses liquid filter arguments with dashes in them, Liquid does not.

For tags that accept kwargs and dumps them on the HTML tag, this is an important feature.

e.g. {{ ... | image_tag: loading: 'lazy', data-something: 'value!' }}

Without this change, Liquid would incorrectly parse the
`data-something` kwarg as a single argument and would skip over the
invalid characters.

See https://github.com/Shopify/theme-check/issues/539 for more context
2022-02-11 15:10:07 -05:00
shainaraskasandGitHub 3de1db3c3a Merge pull request #1509 from Shopify/1508-shopify-docs-link
Fix Shopify documentation link
2022-01-20 12:06:16 -05:00
Shaina Raskas 03522caaf8 fix Shopify documentation link 2022-01-20 09:40:48 -05:00
Shaina Raskas 7acea2a9c9 Revert "fix Shopify documentation link"
This reverts commit d8ef698539.
2022-01-20 09:37:26 -05:00
Shaina Raskas d8ef698539 fix Shopify documentation link 2022-01-20 09:27:00 -05:00
Watson ebdfdb80e5 Detect quoted string using String#{start_with?, end_with?} to reduce Regexp#=== calling 2021-09-26 04:30:49 +09:00
Watson 95e9fa5010 Use String#=~ and Regexp.last_match instead to retrieve the markup content
If the first value is only used obtained with String#scan,
it will increase the performance if replace with `String#=~` and `Regexp.last_match`.

### Environment
- MacBook Air (M1, 2020)
- macOS 12.0 beta 7
- Apple M1
- Ruby 3.0.2

### Test code
```ruby
require 'benchmark/ips'

WhitespaceControl           = '-'
VariableStart               = /\{\{/
VariableEnd                 = /\}\}/

ContentOfVariable   = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
token = "{{item.product.featured_image | product_img_url: 'thumb' }}"

Benchmark.ips do |x|
  x.report("String#scan")  { token.scan(ContentOfVariable) {|content| break }  }
  x.report("String#match") { m = token.match(ContentOfVariable); m[1] }
  x.report("String#=~")    { token =~ ContentOfVariable; Regexp.last_match(1) }

  x.compare!
end
```

### Result
```
Warming up --------------------------------------
         String#scan   135.724k i/100ms
        String#match   117.397k i/100ms
           String#=~   151.637k i/100ms
Calculating -------------------------------------
         String#scan      1.351M (± 0.8%) i/s -      6.786M in   5.021955s
        String#match      1.169M (± 1.3%) i/s -      5.870M in   5.020429s
           String#=~      1.520M (± 0.9%) i/s -      7.733M in   5.087427s

Comparison:
           String#=~:  1520250.9 i/s
         String#scan:  1351399.0 i/s - 1.12x  (± 0.00) slower
        String#match:  1169384.1 i/s - 1.30x  (± 0.00) slower
```
2021-09-26 04:12:53 +09:00
45 changed files with 2213 additions and 199 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
matrix:
entry:
- { ruby: 2.5, allowed-failure: false } # minimum supported
- { ruby: 3.0, allowed-failure: false } # latest
- { ruby: 3.1, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: true }
name: test (${{ matrix.entry.ruby }})
steps:
+5
View File
@@ -11,12 +11,17 @@ group :benchmark, :test do
gem 'benchmark-ips'
gem 'memory_profiler'
gem 'terminal-table'
gem 'unicode_plot'
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
gem 'stackprof'
end
end
group :development do
gem 'pry-byebug'
end
group :test do
gem 'rubocop', '~> 1.4', require: false
gem 'rubocop-shopify', '~> 1.0.7', require: false
+19
View File
@@ -1,5 +1,24 @@
# Liquid Change Log
## 5.3.0 2022-03-22
### Fixes
* StandardFilter: Fix missing @context on iterations (#1525) [Thierry Joyal]
* Fix warning about block and default value in `static_registers.rb` (#1531) [Peter Zhu]
### Deprecation
* Condition#evaluate to require mandatory context argument in Liquid 6.0.0 (#1527) [Thierry Joyal]
## 5.2.0 2022-03-01
### Features
* Add `remove_last`, and `replace_last` filters (#1422) [Anders Hagbard]
* Eagerly cache global filters (#1524) [Jean Boussier]
### Fixes
* Fix some internal errors in filters from invalid input (#1476) [Dylan Thacker-Smith]
* Allow dash in filter kwarg name for consistency with Liquid::C (#1518) [CP Clermont]
## 5.1.0 / 2021-09-09
### Features
+2 -2
View File
@@ -5,7 +5,7 @@
* [Contributing guidelines](CONTRIBUTING.md)
* [Version history](History.md)
* [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
* [Liquid documentation from Shopify](https://shopify.dev/api/liquid)
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
* [Website](http://liquidmarkup.org/)
@@ -56,7 +56,7 @@ For standard use you can just pass it the content of a file and call render with
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
it very hard to debug and can lead to unexpected behaviour.
it very hard to debug and can lead to unexpected behaviour.
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
when templates are invalid. You can enable this new parser like this:
+31
View File
@@ -53,6 +53,21 @@ task :test do
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
end
if RUBY_ENGINE == 'ruby'
Rake::Task['test_compile'].invoke
end
end
task :test_compile do
ENV['LIQUID_COMPILE'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
end
task(gem: :build)
@@ -77,6 +92,22 @@ namespace :benchmark do
ruby "./performance/benchmark.rb lax"
end
desc "Compare the render performance of compiled Liquid to standard Liquid and Liquid-C"
task :compare_render do
ENV.delete("LIQUID_C")
ENV.delete("RENDER_ONLY")
ENV["LIQUID_COMPILE"] = "1"
ruby "./performance/benchmark.rb strict"
ENV.delete("LIQUID_COMPILE")
ruby "./performance/benchmark.rb strict"
ENV["LIQUID_C"] = "1"
ruby "./performance/benchmark.rb strict"
end
desc "Run the liquid benchmark with strict parsing"
task :strict do
ruby "./performance/benchmark.rb strict"
+26
View File
@@ -0,0 +1,26 @@
# https://github.com/evanphx/benchmark-ips
require 'liquid'
#require 'liquid/c'
require 'benchmark/ips'
require_relative '../lib/liquid/compile'
require_relative 'shop_filter'
require_relative 'money_filter'
# Each database table is a hash
require_relative 'database'
tables = Database.tables
Liquid::Template.register_filter(MoneyFilter)
Liquid::Template.register_filter(ShopFilter)
@template = Liquid::Template.parse(File.read("product.liquid"))
context = Liquid::Context.new([tables, {}], {}, {}, false, Liquid::ResourceLimits.new(Liquid::Template.default_resource_limits))
Benchmark.ips do |x|
x.report("render") { @template.render(context) }
# Compare the iterations per second of the various reports
x.compare!
end
+57
View File
@@ -0,0 +1,57 @@
# frozen_string_literal: true
require 'yaml'
require 'stackprof'
module Database
DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml"
# 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 =
if YAML.respond_to?(:unsafe_load_file) # Only Psych 4+ can use unsafe_load_file
# unsafe_load_file is needed for YAML references
YAML.unsafe_load_file(DATABASE_FILE_PATH, symbolize_names: true)
else
YAML.load_file(DATABASE_FILE_PATH, symbolize_names: true)
end
# 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.each_with_object({}) do |(key, values), assigns|
assigns[key] = values.each_with_object({}) do |v, h|
h[v[:handle]] = v
end
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__ == $PROGRAM_NAME
p(Database.tables[:collections][:frontpage].keys)
# p Database.tables[:blog][:articles]
end
+19
View File
@@ -0,0 +1,19 @@
# frozen_string_literal: true
module MoneyFilter
def money_with_currency(money)
return '' if money.nil?
format("$ %.2f USD", money / 100.0)
end
def money(money)
return '' if money.nil?
format("$ %.2f", money / 100.0)
end
private
def currency
ShopDrop.new.currency
end
end
+36
View File
@@ -0,0 +1,36 @@
<div id="product-left">
{% for image in product.images %}{% if forloop.first %}<div id="product-image">
<a href="{{ image | product_img_url: 'large' }}" rel="lightbox[images]" title="{{ product.title | escape }}"><img src="{{ image | product_img_url: 'medium' }}" alt="{{ product.title | escape }}" /></a>
</div>{% else %}
<div class="product-images">
<a href="{{ image | product_img_url: 'large' }}" rel="lightbox[images]" title="{{ product.title | escape }}"><img src="{{ image | product_img_url: 'small' }}" alt="{{ product.title | escape }}" /></a>
</div>{% endif %}{% endfor %}
</div>
<div id="product-right">
<h1>{{ product.title }}</h1>
{{ product.description }}
{% if product.available %}
<form action="/cart/add" method="post">
<div id="product-variants">
<div id="price-field"></div>
<select id="product-select" name='id'>
{% for variant in product.variants %}
<option value="{{ variant.id }}">{{ variant.title }} - {{ variant.price | money }}</option>
{% endfor %}
</select>
</div>
<input type="image" src="{{ 'purchase.png' | asset_url }}" name="add" value="Purchase" id="purchase" />
</form>
{% else %}
<p class="bold-red">This product is temporarily unavailable</p>
{% endif %}
<div id="product-details">
<strong>Continue Shopping</strong><br />
Browse more {{ product.type | link_to_type }} or additional {{ product.vendor | link_to_vendor }} products.
</div>
</div>
+106
View File
@@ -0,0 +1,106 @@
# frozen_string_literal: true
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/#{to_handle(vendor_title)}"
end
def url_for_type(type_title)
"/collections/#{to_handle(type_title)}"
end
def product_img_url(url, style = 'small')
unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})}
raise ArgumentError, 'filter "size" can only be called on product images'
end
case style
when 'original'
'/files/shops/random_number/' + url
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
else
raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, 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']
paginate['parts'].each do |part|
html << if part['is_link']
%(<span class="page">#{link_to(part['title'], part['url'])}</span>)
elsif part['title'].to_i == paginate['current_page'].to_i
%(<span class="page current">#{part['title']}</span>)
else
%(<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
private
def to_handle(str)
result = str.dup
result.downcase!
result.delete!("'\"()[]")
result.gsub!(/\W+/, '-')
result.gsub!(/-+\z/, '') if result[-1] == '-'
result.gsub!(/\A-+/, '') if result[0] == '-'
result
end
end
Binary file not shown.
+945
View File
@@ -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
url: /products/shopify-shirt
featured_image: products/shopify_shirt.png
images:
- products/shopify_shirt.png
description:
<p>High Quality Shopify Shirt. Wear your e-commerce solution with pride and attract attention anywhere you go.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
variants:
- *product-7-var-1
- *product-7-var-2
- *product-7-var-3
- *product-7-var-4
- &product-8
id: 8
title: Hooded Sweater
handle: hooded-sweater
type: Sweater
vendor: Stormtech
price: 5900
price_max: 5900
price_min: 5900
price_varies: false
available: true
tags:
- sweater
- hooded
- apparel
- clothing
options:
- Color
- Size
compare_at_price: 6900
compare_at_price_max: 6900
compare_at_price_min: 6900
compare_at_price_varies: false
url: /products/hooded-sweater
featured_image: products/hooded-sweater.jpg
images:
- products/hooded-sweater.jpg
- products/hooded-sweater-b.jpg
description:
<p>Extra comfortable zip up sweater. Durable quality, ideal for any outdoor activities.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
variants:
- *product-8-var-1
- *product-8-var-2
- *product-8-var-3
- *product-8-var-4
- *product-8-var-5
- *product-8-var-6
- &product-9
id: 9
title: D3 Digital SLR Camera
handle: d3
type: SLR
vendor: Nikon
price: 499995
price_max: 552500
price_min: 499995
price_varies: true
available: true
tags:
- camera
- slr
- nikon
- professional
options:
- Bundle
compare_at_price:
compare_at_price_max: 0
compare_at_price_min: 0
compare_at_price_varies: false
url: /products/d3
featured_image: products/d3.jpg
images:
- products/d3.jpg
- products/d3_2.jpg
- products/d3_3.jpg
description:
<p>Flagship pro D-SLR with a 12.1-MP FX-format CMOS sensor, blazing 9 fps shooting at full FX resolution and low-noise performance up to 6400 ISO.</p>
<p><strong>Nikon's original 12.1-megapixel FX-format (23.9 x 36mm) CMOS sensor:</strong> Couple Nikon's exclusive digital image processing system with the 12.1-megapixel FX-format and you'll get breathtakingly rich images while also reducing noise to unprecedented levels with even higher ISOs.</p>
<p><strong>Continuous shooting at up to 9 frames per second:</strong> At full FX resolution and up to 11fps in the DX crop mode, the D3 offers uncompromised shooting speeds for fast-action and sports photography.</p>
variants:
- *product-9-var-1
- *product-9-var-2
- *product-9-var-3
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Line Items
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
line_items:
- &line_item-1
id: 1
title: 'Arbor Draft'
subtitle: '151cm'
price: 29900
line_price: 29900
quantity: 1
variant: *product-1-var-1
product: *product-1
- &line_item-2
id: 2
title: 'Comic ~ Orange'
subtitle: '159cm'
price: 19900
line_price: 39800
quantity: 2
variant: *product-4-var-1
product: *product-4
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Link Lists
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
links:
- &link-1
id: 1
title: Our Sale
url: /collections/sale
- &link-2
id: 2
title: Arbor Stuff
url: /collections/arbor
- &link-3
id: 3
title: All our Snowboards
url: /collections/snowboards
- &link-4
id: 4
title: Powered by Shopify
url: 'http://shopify.com'
- &link-5
id: 5
title: About Us
url: /pages/about-us
- &link-6
id: 6
title: Policies
url: /pages/shipping
- &link-7
id: 7
title: Contact Us
url: /pages/contact
- &link-8
id: 8
title: Our blog
url: /blogs/bigcheese-blog
- &link-9
id: 9
title: New Boots
url: /products/burton-boots
- &link-10
id: 10
title: Paginated Sale
url: /collections/paginated-sale
- &link-11
id: 11
title: Our Paginated blog
url: /blogs/paginated-blog
- &link-12
id: 12
title: Catalog
url: /collections/all
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Link Lists
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
link_lists:
- &link-list-1
id: 1
title: 'Main Menu'
handle: 'main-menu'
links:
- *link-12
- *link-5
- *link-7
- *link-8
- &link-list-2
id: 1
title: 'Footer Menu'
handle: 'footer'
links:
- *link-5
- *link-6
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Collections
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
collections:
- &collection-1
id: 1
title: Frontpage
handle: frontpage
url: /collections/frontpage
products:
- *product-7
- *product-8
- *product-9
- &collection-2
id: 2
title: Arbor
handle: arbor
url: /collections/arbor
products:
- *product-1
- *product-2
- &collection-3
id: 3
title: Snowboards
handle: snowboards
url: /collections/snowboards
description:
<p>This is a description for my <strong>Snowboards</strong> collection.</p>
products:
- *product-1
- *product-2
- *product-3
- *product-4
- &collection-4
id: 4
title: Items On Sale
handle: sale
url: /collections/sale
products:
- *product-1
- &collection-5
id: 5
title: Paginated Sale
handle: 'paginated-sale'
url: '/collections/paginated-sale'
products:
- *product-1
- *product-2
- *product-3
- *product-4
products_count: 210
- &collection-6
id: 6
title: All products
handle: 'all'
url: '/collections/all'
products:
- *product-7
- *product-8
- *product-9
- *product-6
- *product-1
- *product-2
- *product-3
- *product-4
- *product-5
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Pages and Blogs
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
pages:
- &page-2
id: 1
title: Contact Us
handle: contact
url: /pages/contact
author: Tobi
content:
"<p>You can contact us via phone under (555) 567-2222.</p>
<p>Our retail store is located at <em>Rue d'Avignon 32, Avignon (Provence)</em>.</p>
<p><strong>Opening Hours:</strong><br />Monday through Friday: 9am - 6pm<br />Saturday: 10am - 3pm<br />Sunday: closed</p>"
created_at: 2005-04-04 12:00
- &page-3
id: 2
title: About Us
handle: about-us
url: /pages/about-us
author: Tobi
content:
"<p>Our company was founded in 1894 and we are since operating out of Avignon from the beautiful Provence.</p>
<p>We offer the highest quality products and are proud to serve our customers to their heart's content.</p>"
created_at: 2005-04-04 12:00
- &page-4
id: 3
title: Shopping Cart
handle: shopping-cart
url: /pages/shopping-cart
author: Tobi
content: "<ul><li>Your order is safe with us. Our checkout uses industry standard security to protect your information.</li><li>Your order will be billed immediately upon checkout.</li><li><b>ALL SALES ARE FINAL:</b> Defective or damaged product will be exchanged</li><li>All orders are processed expediently: usually in under 24 hours.</li></ul>"
created_at: 2005-04-04 12:00
- &page-5
id: 4
title: Shipping and Handling
handle: shipping
url: /pages/shipping
author: Tobi
content: <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
created_at: 2005-04-04 12:00
- &page-6
id: 5
title: Frontpage
handle: frontpage
url: /pages/frontpage
author: Tobi
content: <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
created_at: 2005-04-04 12:00
blogs:
- id: 1
handle: news
title: News
url: /blogs/news
articles:
- id: 3
title: 'Welcome to the new Foo Shop'
author: Daniel
content: <p><strong>Welcome to your Shopify store! The jaded Pixel crew is really glad you decided to take Shopify for a spin.</strong></p><p>To help you get you started with Shopify, here are a couple of tips regarding what you see on this page.</p><p>The text you see here is an article. To edit this article, create new articles or create new pages you can go to the <a href="/admin/pages">Blogs &amp; Pages</a> tab of the administration menu.</p><p>The Shopify t-shirt above is a product and selling products is what Shopify is all about. To edit this product, or create new products you can go to the <a href="/admin/products">Products Tab</a> in of the administration menu.</p><p>While you're looking around be sure to check out the <a href="/admin/collections">Collections</a> and <a href="/admin/links">Navigations</a> tabs and soon you will be well on your way to populating your site.</p><p>And of course don't forget to browse the <a href="admin/design/appearance/themes">theme gallery</a> to pick a new look for your shop!</p><p><strong>Shopify is in beta</strong><br />If you would like to make comments or suggestions please visit us in the <a href="http://forums.shopify.com/community">Shopify Forums</a> or drop us an <a href="mailto:[email protected]">email</a>.</p>
created_at: 2005-04-04 16:00
- id: 4
title: 'Breaking News: Restock on all sales products'
author: Tobi
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-04 12:00
articles_count: 2
- id: 2
handle: bigcheese-blog
title: Bigcheese blog
url: /blogs/bigcheese-blog
articles:
- id: 1
title: 'One thing you probably did not know yet...'
author: Justin
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-04 16:00
comments:
-
id: 1
author: John Smith
email: [email protected]
content: Wow...great article man.
status: published
created_at: 2009-01-01 12:00
updated_at: 2009-02-01 12:00
url: ""
-
id: 2
author: John Jones
email: [email protected]
content: I really enjoyed this article. And I love your shop! It's awesome. Shopify rocks!
status: published
created_at: 2009-03-01 12:00
updated_at: 2009-02-01 12:00
url: "http://somesite.com/"
- id: 2
title: Fascinating
author: Tobi
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-06 12:00
comments:
articles_count: 2
comments_enabled?: true
comment_post_url: ""
comments_count: 2
moderated?: true
- id: 3
handle: paginated-blog
title: Paginated blog
url: /blogs/paginated-blog
articles:
- id: 6
title: 'One thing you probably did not know yet...'
author: Justin
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-04 16:00
- id: 7
title: Fascinating
author: Tobi
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-06 12:00
articles_count: 200
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -36,7 +36,7 @@ module Liquid
VariableIncompleteEnd = /\}\}?/
QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
@@ -59,8 +59,8 @@ require 'liquid/forloop_drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer_factory'
require 'liquid/strainer_template'
require 'liquid/strainer_factory'
require 'liquid/expression'
require 'liquid/context'
require 'liquid/parser_switching'
+4 -4
View File
@@ -37,7 +37,7 @@ module Liquid
private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift)
unless token.empty? || token =~ WhitespaceOrNothing
unless token.empty? || token.match?(WhitespaceOrNothing)
unless token =~ LiquidTagToken
# line isn't empty but didn't match tag syntax, yield and let the
# caller raise a syntax error
@@ -150,7 +150,7 @@ module Liquid
end
parse_context.trim_whitespace = false
@nodelist << token
@blank &&= !!(token =~ WhitespaceOrNothing)
@blank &&= token.match?(WhitespaceOrNothing)
end
parse_context.line_number = tokenizer.line_number
end
@@ -231,8 +231,8 @@ module Liquid
end
def create_variable(token, parse_context)
token.scan(ContentOfVariable) do |content|
markup = content.first
if token =~ ContentOfVariable
markup = Regexp.last_match(1)
return Variable.new(markup, parse_context)
end
BlockBody.raise_missing_variable_terminator(token, parse_context)
+330
View File
@@ -0,0 +1,330 @@
# frozen_string_literal: true
require 'pry-byebug'
require 'set'
module Liquid
class Compiler
def initialize
@ruby = +""
@nodes = {}
@declared = Set.new(['__l_product'])
end
def <<(line)
@ruby << line
end
def var_name(name)
"__l_#{name}"
end
def declare(name)
@declared << name
end
def declared?(name)
@declared.member?(name)
end
def to_proc
show_ruby = ENV["SHOW_RUBY"] && ENV["SHOW_RUBY"].to_i || 0
STDERR.puts @ruby if show_ruby >= 1
res = RubyVM::InstructionSequence.compile(<<~RUBY).eval.call(@nodes)
# frozen_string_literal: true
->(__nodes) {
->(__context, __output, __l_product) {
__assigns = __context.scopes.last
#{@ruby}
__output
}
}
RUBY
STDERR.puts RubyVM::InstructionSequence.disasm(res) if show_ruby >= 2
res
end
def compile_expr(node)
if node.respond_to?(:compile_expr)
node.compile_expr(self)
else
case node
when Range # returned by RangeLookup when range contains only literals
node.inspect
when Integer, Float, nil, true, false, String
node.inspect
else
raise ArgumentError, "cannot compile node #{node.inspect}"
end
end
end
def output(node)
compiled = compile_expr(node)
self << if compiled.is_a?(String)
"__output << #{compiled}\n"
else
"__output << #{compiled}.to_s\n"
end
end
def fallback_evaluate_expr(node)
"__nodes[#{node.object_id}].evaluate(__context)"
end
def to_integer_expr(var_name)
<<~RUBY.strip
(begin
if #{var_name}.is_a?(Integer)
#{var_name}
else
begin
Integer(#{var_name}.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid integer"
end
end
end)
RUBY
end
def compile(node)
if node.instance_of?(String)
self << "__output << #{node.inspect}\n"
else
@nodes[node.object_id] = node
if node.respond_to?(:compile)
node.compile(self)
else
line_number = if node.respond_to?(:line_number) && node.line_number.is_a?(Integer)
node.line_number
else
nil
end
catch_errors(line_number, show_message: !node.blank?) do
self << "__nodes[#{node.object_id}].render_to_output_buffer(__context, __output) # #{node.inspect} \n"
end
end
end
end
def catch_errors(line_number, show_message: true)
self << "begin\n"
yield
self << <<~RUBY
rescue => __exc
case __exc
when Liquid::MemoryError
raise
when Liquid::UndefinedVariable, Liquid::UndefinedDropMethod, Liquid::UndefinedFilter
__context.handle_error(__exc, #{line_number.inspect})
else
__error_message = __context.handle_error(__exc, #{line_number.inspect})
RUBY
self << "__output << __error_message\n" if show_message
self << "end\nend\n"
end
end
class BlockBody
def render_to_output_buffer(context, output)
raise "Tried to render uncompiled block" unless @compiled
@compiled.call(context, output, context.environments[0][:product])
end
def compile_top_level
compiler = Compiler.new
compile(compiler)
@compiled = compiler.to_proc
end
def compile(compiler)
nodelist.each { |node| compiler.compile(node) }
end
end
class Document
def parse(tokenizer, parse_context)
while parse_body(tokenizer)
end
@body.compile_top_level
@body.freeze
rescue SyntaxError => e
e.line_number ||= parse_context.line_number
raise
end
end
class VariableLookup
def compile_expr(compiler)
# HACK
if @name == "forloop" && @lookups == ["first"]
return "forloop_first"
end
var_name = compiler.var_name(@name)
root = if compiler.declared?(var_name)
var_name
else
"__scope[#{@name.inspect}]"
end
@lookups.reduce(root) do |prev, lookup|
"#{prev}[:#{lookup}]"
end
end
end
class Variable
FILTERS = {
"modulo" => ->(compiler, expr, args, kwargs) {
"(#{expr} % #{args[0]})"
},
"product_img_url" => ->(compiler, expr, args, kwargs) {
style = args.fetch(0, 'small')
rest = case style
when 'original'
"\"/files/shops/random_number/\#{url}\""
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
"\"/files/shops/random_number/products/\#{$1}_#{style}.\#{$2}\""
else
"raise ArgumentError, 'valid parameters for filter \"size\" are: original, grande, large, medium, compact, small, thumb and icon '"
end
<<~RUBY.strip
(begin
if #{expr} =~ %r{\\Aproducts/([\\w\\-\\_]+)\\.(\\w{2,4})}
#{rest}
else
raise ArgumentError, 'filter \"size\" can only be called on product images'
end
end)
RUBY
},
"escape" => ->(compiler, expr, args, kwargs) {
"(_t = #{expr}; CGI.escapeHTML(_t) if _t)"
},
"money" => ->(compiler, expr, args, kwargs) {
"(_m = #{expr}; _m.nil? ? '' : \"$ \#{(_m / 100.0).round(2)}\")"
},
}
def compile(compiler)
if const?
compiler.output(name)
else
compiler.catch_errors(@line_number, show_message: true) do
compiler.output(self)
end
end
end
def const?
@filters.empty? && !name.respond_to?(:compile_expr)
end
def compile_expr(compiler)
@filters.reduce(compiler.compile_expr(name)) do |expr, (name, args, kwargs)|
if FILTERS.key?(name)
FILTERS[name].call(compiler, expr, args, kwargs)
else
expr
end
end
end
end
class Echo
def compile(compiler)
compiler.compile(variable)
end
end
class For
def compile(compiler)
compiler << "collection = #{compiler.compile_expr(@collection_name)}\n"
unless @from.nil?
compiler << <<~RUBY
from_value = #{compiler.compile_expr(@from)}
from = if from_value.nil?
0
else
#{compiler.to_integer_expr(:from_value)}
end
limit_value = #{compiler.compile_expr(@limit)}
to = if limit_value.nil?
nil
else
#{compiler.to_integer_expr(:limit_value)} + from
end
collection = #{compiler.slice_collection_expr(:collection, :from, :to)}
#{@reversed ? "segment.reverse!" : "" }
RUBY
end
item_var = compiler.var_name(variable_name)
compiler << <<~RUBY
forloop_first = true
for #{item_var} in collection
RUBY
compiler.declare(item_var)
compiler.compile(@for_block)
compiler << <<~RUBY
forloop_first = false
end
RUBY
end
end
class If
def compile(compiler)
compiler << "if #{compiler.compile_expr(blocks[0])}\n"
compiler.compile(blocks[0].attachment)
blocks.drop(1).each do |block|
if block.else?
compiler << "else\n"
else
compiler << "elsif #{block.compile_expr(compiler)}\n"
end
compiler.compile(block.attachment)
end
compiler << "end\n"
end
end
class Comment
def compile(_compiler); end
end
class Condition
def compile_expr(compiler)
if operator
left_expr = compiler.compile_expr(left)
right_expr = compiler.compile_expr(right)
if child_relation
child_expr = compiler.compile_expr(child_condition)
expr = "((#{left_expr} #{operator} #{right_expr}) #{child_relation} #{child_expr})"
else
expr = "(#{left_expr} #{operator} #{right_expr})"
end
expr
else
left_expr = compiler.compile_expr(left)
expr = "#{left_expr}"
end
end
end
class Assign
def compile(compiler)
compiler.declare(compiler.var_name(@to))
compiler << "#{compiler.var_name(@to)} = #{compiler.compile_expr(@from)}\n"
end
end
end
+7 -1
View File
@@ -61,7 +61,7 @@ module Liquid
@child_condition = nil
end
def evaluate(context = Context.new)
def evaluate(context = deprecated_default_context)
condition = self
result = nil
loop do
@@ -150,6 +150,12 @@ module Liquid
end
end
def deprecated_default_context
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
" and will be removed from Liquid 6.0.0.")
Context.new
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
+11 -10
View File
@@ -10,21 +10,23 @@ module Liquid
'empty' => ''
}.freeze
SINGLE_QUOTED_STRING = /\A\s*'(.*)'\s*\z/m
DOUBLE_QUOTED_STRING = /\A\s*"(.*)"\s*\z/m
INTEGERS_REGEX = /\A\s*(-?\d+)\s*\z/
FLOATS_REGEX = /\A\s*(-?\d[\d\.]+)\s*\z/
INTEGERS_REGEX = /\A(-?\d+)\z/
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
# Use an atomic group (?>...) to avoid pathological backtracing from
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
RANGES_REGEX = /\A\s*\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\s*\z/
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
def self.parse(markup)
return nil unless markup
markup = markup.strip
if (markup.start_with?('"') && markup.end_with?('"')) ||
(markup.start_with?("'") && markup.end_with?("'"))
return markup[1..-2]
end
case markup
when nil
nil
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
Regexp.last_match(1)
when INTEGERS_REGEX
Regexp.last_match(1).to_i
when RANGES_REGEX
@@ -32,7 +34,6 @@ module Liquid
when FLOATS_REGEX
Regexp.last_match(1).to_f
else
markup = markup.strip
if LITERALS.key?(markup)
LITERALS[markup]
else
+4
View File
@@ -25,6 +25,10 @@ module Liquid
start_int..end_int
end
def compile_expr(compiler)
compiler.fallback_evaluate_expr(self)
end
private
def to_integer(input)
+57 -18
View File
@@ -213,17 +213,23 @@ module Liquid
if ary.empty?
[]
elsif ary.first.respond_to?(:[]) && target_value.nil?
begin
ary.select { |item| item[property] }
elsif target_value.nil?
ary.select do |item|
item[property]
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
elsif ary.first.respond_to?(:[])
begin
ary.select { |item| item[property] == target_value }
else
ary.select do |item|
item[property] == target_value
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
end
@@ -237,11 +243,14 @@ module Liquid
ary.uniq
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
begin
ary.uniq { |a| a[property] }
else
ary.uniq do |item|
item[property]
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
end
@@ -277,11 +286,14 @@ module Liquid
ary.compact
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
begin
ary.reject { |a| a[property].nil? }
else
ary.reject do |item|
item[property].nil?
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
end
@@ -296,14 +308,34 @@ module Liquid
input.to_s.sub(string.to_s, replacement.to_s)
end
# Replace the last occurrences of a string with another
def replace_last(input, string, replacement)
input = input.to_s
string = string.to_s
replacement = replacement.to_s
start_index = input.rindex(string)
return input unless start_index
output = input.dup
output[start_index, string.length] = replacement
output
end
# remove a substring
def remove(input, string)
input.to_s.gsub(string.to_s, '')
replace(input, string, '')
end
# remove the first occurrences of a substring
def remove_first(input, string)
input.to_s.sub(string.to_s, '')
replace_first(input, string, '')
end
# remove the last occurences of a substring
def remove_last(input, string)
replace_last(input, string, '')
end
# add one string to another
@@ -486,10 +518,16 @@ module Liquid
end
def nil_safe_compare(a, b)
if !a.nil? && !b.nil?
a <=> b
result = a <=> b
if result
result
elsif a.nil?
1
elsif b.nil?
-1
else
a.nil? ? 1 : -1
raise Liquid::ArgumentError, "cannot sort values of incompatible types"
end
end
@@ -544,8 +582,9 @@ module Liquid
def each
@input.each do |e|
e = e.respond_to?(:to_liquid) ? e.to_liquid : e
e.context = @context if e.respond_to?(:context=)
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
yield(e)
end
end
end
+5 -1
View File
@@ -31,7 +31,11 @@ module Liquid
if @registers.key?(key)
@registers.fetch(key)
elsif default != UNDEFINED
@static.fetch(key, default, &block)
if block_given?
@static.fetch(key, &block)
else
@static.fetch(key, default)
end
else
@static.fetch(key, &block)
end
+11 -10
View File
@@ -7,25 +7,26 @@ module Liquid
def add_global_filter(filter)
strainer_class_cache.clear
global_filters << filter
GlobalCache.add_filter(filter)
end
def create(context, filters = [])
strainer_from_cache(filters).new(context)
end
GlobalCache = Class.new(StrainerTemplate)
private
def global_filters
@global_filters ||= []
end
def strainer_from_cache(filters)
strainer_class_cache[filters] ||= begin
klass = Class.new(StrainerTemplate)
global_filters.each { |f| klass.add_filter(f) }
filters.each { |f| klass.add_filter(f) }
klass
if filters.empty?
GlobalCache
else
strainer_class_cache[filters] ||= begin
klass = Class.new(GlobalCache)
filters.each { |f| klass.add_filter(f) }
klass
end
end
end
+5
View File
@@ -31,6 +31,11 @@ module Liquid
filter_methods.include?(method.to_s)
end
def inherited(subclass)
super
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
end
private
def filter_methods
-13
View File
@@ -2,19 +2,6 @@
module Liquid
class Comment < Block
# Potential fix
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om
def parse(tokens)
while (token = tokens.shift)
if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
return
end
end
raise_tag_never_closed(block_name)
end
def render_to_output_buffer(_context, output)
output
end
-4
View File
@@ -15,19 +15,15 @@ module Liquid
attr_reader :variable
def initialize(tag_name, markup, parse_context)
puts "Initializing Echo tag"
super
@variable = Variable.new(markup, parse_context)
end
def render(context)
puts "Render Echo tag"
@variable.render_to_output_buffer(context, +'')
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
puts "ParseTreeVisitor Echo tag"
def children
[@node.variable]
end
+4
View File
@@ -93,6 +93,10 @@ module Liquid
context.apply_global_filter(obj)
end
def compile
"__output << #{@name}\n"
end
def render_to_output_buffer(context, output)
obj = render(context)
+5 -6
View File
@@ -2,8 +2,7 @@
module Liquid
class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size', 'first', 'last'].freeze
COMMAND_METHODS = ['size', 'first', 'last'].freeze
attr_reader :name, :lookups
@@ -15,8 +14,8 @@ module Liquid
lookups = markup.scan(VariableParser)
name = lookups.shift
if name =~ SQUARE_BRACKETED
name = Expression.parse(Regexp.last_match(1))
if name&.start_with?('[') && name&.end_with?(']')
name = Expression.parse(name[1..-2])
end
@name = name
@@ -25,8 +24,8 @@ module Liquid
@lookups.each_index do |i|
lookup = lookups[i]
if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse(Regexp.last_match(1))
if lookup&.start_with?('[') && lookup&.end_with?(']')
lookups[i] = Expression.parse(lookup[1..-2])
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i
end
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "5.1.0"
VERSION = "5.3.0"
end
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
unless ENV.key?("BUNDLE_BIN_PATH")
exec("bundle", "exec", "ruby", __FILE__, *ARGV)
end
require "pry"
require "liquid"
require "unicode_plot"
require "optparse"
require "open3"
require "csv"
require "io/console"
TERM_ROWS, TERM_COLS = IO.console.winsize
def record
if ARGV.count != 2
STDERR.puts "Usage: benchmark.rb record [output_path]"
exit(1)
end
output_path = ARGV[1]
out, status = Open3.capture2("ruby", "#{__dir__}/benchmark_child.rb")
File.write(output_path, out)
end
def calc_stats(nums)
mean = nums.reduce(:+) / nums.length
variance = nums.map { |n| (n - mean).pow(2) }.reduce(:+) / nums.length
stddev = Math.sqrt(variance)
{
mean: mean,
variance: variance,
stddev: stddev,
normalized: normalize_outliers(mean, stddev, nums),
raw: nums
}
end
def normalize_outliers(mean, stddev, nums)
cutoff = stddev * 3
nums.map do |n|
if (n - mean).abs < cutoff
n
else
mean
end
end
end
def show
if ARGV.count < 2
STDERR.puts "Usage: benchmark.rb show [path1] [path2]? ..."
exit(1)
end
recordings = ARGV.drop(1).to_h do |path|
[File.basename(path), CSV.parse(
File.open(path),
col_sep: "\t",
headers: true,
converters: :integer
)]
end
runs = (1..1000).to_a
recordings.values.first.headers.each do |benchmark|
colors = [:green, :blue, :red]
10.times { puts }
title = "Benchmark: #{benchmark} (times in µs)"
print " " * (TERM_COLS / 2 - title.length)
puts title
puts
all_stats = recordings.transform_values do |csv|
stats = calc_stats(csv.map { |row| row[benchmark] })
stats[:color] = colors.shift
stats
end
line_plots = []
distributions = []
shared_line_plot = nil
max_mean_stats = all_stats.values.max_by { |stats| stats[:mean] }
max_y = (max_mean_stats[:mean] + max_mean_stats[:stddev] * 3).to_i
all_stats.each do |name, stats|
if shared_line_plot == nil
shared_line_plot = UnicodePlot.lineplot(
runs,
stats[:normalized],
name: name,
width: TERM_COLS - 25,
ylim: [0, max_y],
color: stats[:color]
)
else
UnicodePlot.lineplot!(shared_line_plot, stats[:normalized], name: name, color: stats[:color])
end
line_plots << render_to_s(UnicodePlot.lineplot(
runs,
stats[:normalized],
name: name,
color: stats[:color],
width: 40
))
distributions << render_to_s(UnicodePlot.histogram(
stats[:normalized],
title: name,
color: stats[:color]
))
end
shared_line_plot.render
print_columns(line_plots)
print_columns(distributions)
all_times = all_stats.transform_values { |stats| stats[:normalized] }
UnicodePlot.boxplot(data: all_times, title: "Comparison", width: TERM_COLS - 25).render
end
end
def render_to_s(plot)
io = StringIO.new
plot.render(io, color: true)
io.string
end
def visual_length(line)
line.gsub(/\e\[(\d+)m/, '').length
end
def print_columns(cols)
col_lines = cols.map { |col| col.split("\n") }
col_width = col_lines.map do |lines|
lines.map { |line| visual_length(line) }.max
end.max
col_height = col_lines.map { |lines| lines.length }.max
(0...col_height).each do |i|
col_lines.each do |lines|
line = lines[i] || ""
vis_length = visual_length(line)
print line
print(" " * (col_width - vis_length))
end
puts
end
end
ITERS = 1
Benchmarks = Class.new do
def define(_name, benchmark)
@benchmark = benchmark
end
def run
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
@benchmark.compile
parsed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
# warmup
ITERS.times { @benchmark.render}
warm = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
res = nil
ITERS.times { res = @benchmark.render }
ran = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
if res.is_a?(String)
puts res
puts "Output digest: #{Digest::SHA2.hexdigest(res)}"
end
if ENV['SHOW_RUBY'] == '1'
STDERR.puts "note: SHOW_RUBY prevents gathering parse metrics"
STDERR.puts "render: #{(ran - warm) / ITERS}µs"
else
STDERR.puts "parse: #{parsed - start}µs"
STDERR.puts "render: #{(ran - warm) / ITERS}µs"
STDERR.puts "total: #{(ran - warm) / ITERS + (parsed - start)}µs"
end
end
end.new
def execute
require 'digest'
if ARGV.count != 2
STDERR.puts "Usage: benchmark.rb record [output_path]"
exit(1)
end
case ENV['ENGINE']
when 'LIQUID_COMPILE'
require_relative '../lib/liquid/compile'
when 'LIQUID_C'
require 'liquid/c'
when 'LIQUID_RUBY'
else
raise "Invalid ENGINE: #{ENV['ENGINE'].inspect}, expected ENGINE=(LIQUID_RUBY|LIQUID_COMPILE)"
end
require_relative "./benchmarks/#{ARGV[1]}.rb"
Benchmarks.run
end
case ARGV.first
when "record", "r"
record
when "show", "s"
show
when "execute", "x"
execute
else
puts "Invalid command. Expected benchmark [record|show|execute] ..."
exit 1
end
-20
View File
@@ -1,20 +0,0 @@
# frozen_string_literal: true
require 'benchmark/ips'
require_relative 'theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.ips do |x|
x.time = 10
x.warmup = 5
puts
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
puts
x.report("parse:") { profiler.compile }
x.report("render:") { profiler.render }
x.report("parse & render:") { profiler.run }
end
+62
View File
@@ -0,0 +1,62 @@
# frozen_string_literal: true
require 'liquid'
Liquid::Template.error_mode = :strict
case ENV['ENGINE']
when 'LIQUID_COMPILE'
require_relative '../lib/liquid/compile'
when 'LIQUID_C'
require 'liquid/c'
when 'LIQUID_RUBY'
else
raise "Invalid engine: #{ENV['ENGINE'].inspect}"
end
OPTIONS = {
render_iters: 1000
}
def get_time_us
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
end
Benchmarks = Class.new do
def initialize
@by_name = {}
end
def define(name, benchmark)
@by_name[name] = benchmark
end
def run
times = {}
@by_name.each do |name, benchmark|
benchmark.compile
times[name] = OPTIONS[:render_iters].times.map do
before = get_time_us
benchmark.render
get_time_us - before
end
end
puts times.keys.join("\t")
cols = times.values
OPTIONS[:render_iters].times do |i|
cols.each do |values|
print values[i]
print "\t" unless values == cols.last
end
puts
end
end
end.new
Dir[__dir__ + "/benchmarks/*.rb", base: __dir__].each do |path|
require_relative path
end
Benchmarks.run
+28
View File
@@ -0,0 +1,28 @@
# frozen_string_literal: true
Benchmarks.define('fizzbuzz_10000', Class.new do
TEMPLATE = <<~LIQUID
{% for i in (1..#{ENV['COUNT'].to_i}) %}
{% liquid
assign rem_3 = i | modulo: 3
assign rem_5 = i | modulo: 5
if rem_3 == 0 and rem_5 == 0
echo "Fizzbuzz"
elsif rem_3 == 0
echo "Fizz"
elsif rem_5 == 0
echo "Buzz"
else
echo i
endif
%}{% endfor %}
LIQUID
def compile
@parsed = Liquid::Template.parse(TEMPLATE)
end
def render
@parsed.render
end
end.new)
+13
View File
@@ -0,0 +1,13 @@
# frozen_string_literal: true
Benchmarks.define('simple_loop', Class.new do
TEMPLATE = "{% for i in (1..1000) %}{{ i }}{% endfor %}"
def compile
@parsed = Liquid::Template.parse(TEMPLATE)
end
def render
@parsed.render
end
end.new)
+5
View File
@@ -0,0 +1,5 @@
# frozen_string_literal: true
require_relative '../theme_runner'
#Benchmarks.define('theme_runner', ThemeRunner.new)
+10
View File
@@ -3,6 +3,16 @@
require 'stackprof'
require_relative 'theme_runner'
if ENV['LIQUID_C'] == '1'
puts "-- LIQUID C"
require 'liquid/c'
end
if ENV['LIQUID_COMPILE'] == '1'
puts "-- COMPILED"
require 'liquid/compile'
end
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
profiler.run
-2
View File
@@ -1,8 +1,6 @@
# frozen_string_literal: true
$LOAD_PATH.unshift(__dir__ + '/../../lib')
require_relative '../../lib/liquid'
require_relative 'comment_form'
require_relative 'paginate'
require_relative 'json_filter'
+12 -14
View File
@@ -24,7 +24,7 @@ class ContextSensitiveDrop < Liquid::Drop
end
end
class Category < Liquid::Drop
class Category
attr_accessor :name
def initialize(name)
@@ -36,8 +36,9 @@ class Category < Liquid::Drop
end
end
class CategoryDrop
class CategoryDrop < Liquid::Drop
attr_accessor :category, :context
def initialize(category)
@category = category
end
@@ -405,45 +406,42 @@ class ContextTest < Minitest::Test
end
def test_lambda_is_called_once
@global = 0
@context['callcount'] = proc {
@global ||= 0
@global += 1
@global += 1
@global.to_s
}
assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount'])
@global = nil
end
def test_nested_lambda_is_called_once
@global = 0
@context['callcount'] = { "lambda" => proc {
@global ||= 0
@global += 1
@global += 1
@global.to_s
} }
assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda'])
@global = nil
end
def test_lambda_in_array_is_called_once
@global = 0
@context['callcount'] = [1, 2, proc {
@global ||= 0
@global += 1
@global += 1
@global.to_s
}, 4, 5]
assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]'])
@global = nil
end
def test_access_to_context_from_proc
+24
View File
@@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'test_helper'
class FilterKwargTest < Minitest::Test
module KwargFilter
def html_tag(_tag, attributes)
attributes
.map { |key, value| "#{key}='#{value}'" }
.join(' ')
end
end
include Liquid
def test_can_parse_data_kwargs
with_global_filter(KwargFilter) do
assert_equal(
"data-src='src' data-widths='100, 200'",
Template.parse("{{ 'img' | html_tag: data-src: 'src', data-widths: '100, 200' }}").render(nil, nil)
)
end
end
end
+33 -6
View File
@@ -3,6 +3,27 @@
require 'test_helper'
class ProfilerTest < Minitest::Test
class TestDrop < Liquid::Drop
def initialize(value)
super()
@value = value
end
def to_s
artificial_execution_time
@value
end
private
# Monotonic clock precision fluctuate based on the operating system
# By introducing a small sleep we ensure ourselves to register a non zero unit of time
def artificial_execution_time
sleep(Process.clock_getres(Process::CLOCK_MONOTONIC))
end
end
include Liquid
class ProfilingFileSystem
@@ -198,16 +219,22 @@ class ProfilerTest < Minitest::Test
def test_profiling_supports_self_time
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
t.render!("collection" => ["one", "two"])
leaf = t.profiler[0].children[0]
collection = [
TestDrop.new("one"),
TestDrop.new("two"),
]
output = t.render!("collection" => collection)
assert_equal(" one two ", output)
assert_operator(leaf.self_time, :>, 0)
leaf = t.profiler[0].children[0]
assert_operator(leaf.self_time, :>, 0.0)
end
def test_profiling_supports_total_time
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
t.render!
t = Template.parse("{% if true %} {{ test }} {% endif %}", profile: true)
output = t.render!("test" => TestDrop.new("one"))
assert_equal(" one ", output)
assert_operator(t.profiler[0].total_time, :>, 0)
assert_operator(t.profiler[0].total_time, :>, 0.0)
end
end
+65 -32
View File
@@ -3,10 +3,6 @@
require 'test_helper'
class Filters
include Liquid::StandardFilters
end
class TestThing
attr_reader :foo
@@ -29,8 +25,24 @@ class TestThing
end
class TestDrop < Liquid::Drop
def test
"testfoo"
def initialize(value:)
@value = value
end
attr_reader :value
def registers
@context.registers
end
end
class TestModel
def initialize(value:)
@value = value
end
def to_liquid
TestDrop.new(value: @value)
end
end
@@ -53,10 +65,13 @@ class NumberLikeThing < Liquid::Drop
end
class StandardFiltersTest < Minitest::Test
Filters = Class.new(Liquid::StrainerTemplate)
Filters.add_filter(Liquid::StandardFilters)
include Liquid
def setup
@filters = Filters.new
@filters = Filters.new(Context.new)
end
def test_size
@@ -259,8 +274,8 @@ class StandardFiltersTest < Minitest::Test
{ "price" => 1, "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" },
{ "price" => 4, "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" },
{ "handle" => "delta" },
]
assert_equal(expectation, @filters.sort(input, "price"))
end
@@ -363,8 +378,9 @@ class StandardFiltersTest < Minitest::Test
assert_equal(["foo"], @filters.uniq("foo"))
assert_equal([1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1]))
assert_equal([{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
testdrop = TestDrop.new
assert_equal([testdrop], @filters.uniq([testdrop, TestDrop.new], 'test'))
test_drop = TestDrop.new(value: "test")
test_drop_alternate = TestDrop.new(value: "test")
assert_equal([test_drop], @filters.uniq([test_drop, test_drop_alternate], 'value'))
end
def test_uniq_empty_array
@@ -423,6 +439,16 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t])
end
def test_map_calls_context=
model = TestModel.new(value: "test")
template = Template.parse('{{ foo | map: "registers" }}')
template.registers[:test] = 1234
template.assigns['foo'] = [model]
assert_template_result("{:test=>1234}", template.render!)
end
def test_map_on_hashes
assert_template_result("4217", '{{ thing | map: "foo" | map: "bar" }}',
"thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] })
@@ -441,9 +467,9 @@ class StandardFiltersTest < Minitest::Test
end
def test_map_over_proc
drop = TestDrop.new
drop = TestDrop.new(value: "testfoo")
p = proc { drop }
templ = '{{ procs | map: "test" }}'
templ = '{{ procs | map: "value" }}'
assert_template_result("testfoo", templ, "procs" => [p])
end
@@ -539,19 +565,31 @@ class StandardFiltersTest < Minitest::Test
end
def test_replace
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', '1', 2))
assert_equal('b b b b', @filters.replace('a a a a', 'a', 'b'))
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2))
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2))
assert_equal('1 1 1 1', @filters.replace('1 1 1 1', 2, 3))
assert_template_result('2 2 2 2', "{{ '1 1 1 1' | replace: '1', 2 }}")
assert_equal('b a a a', @filters.replace_first('a a a a', 'a', 'b'))
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2))
assert_equal('1 1 1 1', @filters.replace_first('1 1 1 1', 2, 3))
assert_template_result('2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}")
assert_equal('a a a b', @filters.replace_last('a a a a', 'a', 'b'))
assert_equal('1 1 1 2', @filters.replace_last('1 1 1 1', 1, 2))
assert_equal('1 1 1 1', @filters.replace_last('1 1 1 1', 2, 3))
assert_template_result('1 1 1 2', "{{ '1 1 1 1' | replace_last: '1', 2 }}")
end
def test_remove
assert_equal(' ', @filters.remove("a a a a", 'a'))
assert_equal(' ', @filters.remove("1 1 1 1", 1))
assert_equal('a a a', @filters.remove_first("a a a a", 'a '))
assert_equal(' 1 1 1', @filters.remove_first("1 1 1 1", 1))
assert_template_result('a a a', "{{ 'a a a a' | remove_first: 'a ' }}")
assert_template_result(' ', "{{ '1 1 1 1' | remove: 1 }}")
assert_equal('b a a', @filters.remove_first("a b a a", 'a '))
assert_template_result(' 1 1 1', "{{ '1 1 1 1' | remove_first: 1 }}")
assert_equal('a a b', @filters.remove_last("a a b a", ' a'))
assert_template_result('1 1 1 ', "{{ '1 1 1 1' | remove_last: 1 }}")
end
def test_pipes_in_string_arguments
@@ -827,7 +865,7 @@ class StandardFiltersTest < Minitest::Test
end
def test_all_filters_never_raise_non_liquid_exception
test_drop = TestDrop.new
test_drop = TestDrop.new(value: "test")
test_drop.context = Context.new
test_enum = TestEnumerable.new
test_enum.context = Context.new
@@ -852,19 +890,14 @@ class StandardFiltersTest < Minitest::Test
{ 1 => "bar" },
["foo", 123, nil, true, false, Drop, ["foo"], { foo: "bar" }],
]
test_types.each do |first|
test_types.each do |other|
(@filters.methods - Object.methods).each do |method|
arg_count = @filters.method(method).arity
arg_count *= -1 if arg_count < 0
inputs = [first]
inputs << ([other] * (arg_count - 1)) if arg_count > 1
begin
@filters.send(method, *inputs)
rescue Liquid::ArgumentError, Liquid::ZeroDivisionError
nil
end
end
StandardFilters.public_instance_methods(false).each do |method|
arg_count = @filters.method(method).arity
arg_count *= -1 if arg_count < 0
test_types.repeated_permutation(arg_count) do |args|
@filters.send(method, *args)
rescue Liquid::Error
nil
end
end
end
-25
View File
@@ -1,25 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class CommentTagTest < Minitest::Test
include Liquid
def test_single_line_comments_parse
assert_template_result('Before comment', <<~LIQUID)
Before comment
{%- comment -%}
Regular text comment
Liquid in comment: {% echo 'Hi from comment' %}
{%- endcomment -%}
LIQUID
end
def test_multi_line_comments_parse
assert_template_result('Before comment', <<~LIQUID)
Before comment
{%- comment -%} Regular text comment {%- endcomment -%}
{%- comment -%} Liquid in comment: {% echo 'Hi from comment' %} {%- endcomment -%}
LIQUID
end
end # CommentTagTest
+14 -11
View File
@@ -15,7 +15,10 @@ if (env_mode = ENV['LIQUID_PARSER_MODE'])
end
Liquid::Template.error_mode = mode
if ENV['LIQUID_C'] == '1'
if ENV['LIQUID_COMPILE'] == '1'
puts "-- COMPILED"
require 'liquid/compile'
elsif ENV['LIQUID_C'] == '1'
puts "-- LIQUID C"
require 'liquid/c'
end
@@ -72,21 +75,21 @@ module Minitest
end
def with_global_filter(*globals)
original_global_filters = Liquid::StrainerFactory.instance_variable_get(:@global_filters)
Liquid::StrainerFactory.instance_variable_set(:@global_filters, [])
globals.each do |global|
Liquid::StrainerFactory.add_global_filter(global)
end
Liquid::StrainerFactory.send(:strainer_class_cache).clear
original_global_cache = Liquid::StrainerFactory::GlobalCache
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
Liquid::StrainerFactory.const_set(:GlobalCache, Class.new(Liquid::StrainerTemplate))
globals.each do |global|
Liquid::Template.register_filter(global)
end
yield
ensure
Liquid::StrainerFactory.send(:strainer_class_cache).clear
Liquid::StrainerFactory.instance_variable_set(:@global_filters, original_global_filters)
begin
yield
ensure
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
Liquid::StrainerFactory.const_set(:GlobalCache, original_global_cache)
Liquid::StrainerFactory.send(:strainer_class_cache).clear
end
end
def with_error_mode(mode)
+27 -14
View File
@@ -10,8 +10,8 @@ class ConditionUnitTest < Minitest::Test
end
def test_basic_condition
assert_equal(false, Condition.new(1, '==', 2).evaluate)
assert_equal(true, Condition.new(1, '==', 1).evaluate)
assert_equal(false, Condition.new(1, '==', 2).evaluate(Context.new))
assert_equal(true, Condition.new(1, '==', 1).evaluate(Context.new))
end
def test_default_operators_evalute_true
@@ -67,11 +67,11 @@ class ConditionUnitTest < Minitest::Test
end
def test_hash_compare_backwards_compatibility
assert_nil(Condition.new({}, '>', 2).evaluate)
assert_nil(Condition.new(2, '>', {}).evaluate)
assert_equal(false, Condition.new({}, '==', 2).evaluate)
assert_equal(true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate)
assert_equal(true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate)
assert_nil(Condition.new({}, '>', 2).evaluate(Context.new))
assert_nil(Condition.new(2, '>', {}).evaluate(Context.new))
assert_equal(false, Condition.new({}, '==', 2).evaluate(Context.new))
assert_equal(true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate(Context.new))
assert_equal(true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate(Context.new))
end
def test_contains_works_on_arrays
@@ -106,30 +106,29 @@ class ConditionUnitTest < Minitest::Test
def test_or_condition
condition = Condition.new(1, '==', 2)
assert_equal(false, condition.evaluate)
assert_equal(false, condition.evaluate(Context.new))
condition.or(Condition.new(2, '==', 1))
assert_equal(false, condition.evaluate)
assert_equal(false, condition.evaluate(Context.new))
condition.or(Condition.new(1, '==', 1))
assert_equal(true, condition.evaluate)
assert_equal(true, condition.evaluate(Context.new))
end
def test_and_condition
condition = Condition.new(1, '==', 1)
assert_equal(true, condition.evaluate)
assert_equal(true, condition.evaluate(Context.new))
condition.and(Condition.new(2, '==', 2))
assert_equal(true, condition.evaluate)
assert_equal(true, condition.evaluate(Context.new))
condition.and(Condition.new(2, '==', 1))
assert_equal(false, condition.evaluate)
assert_equal(false, condition.evaluate(Context.new))
end
def test_should_allow_custom_proc_operator
@@ -148,6 +147,20 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another"))
end
def test_default_context_is_deprecated
if Gem::Version.new(Liquid::VERSION) >= Gem::Version.new('6.0.0')
flunk("Condition#evaluate without a context argument is to be removed")
end
_out, err = capture_io do
assert_equal(true, Condition.new(1, '==', 1).evaluate)
end
expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
" and will be removed from Liquid 6.0.0."
assert_includes(err.lines.map(&:strip), expected)
end
private
def assert_evaluates_true(left, op, right)
+2 -1
View File
@@ -52,7 +52,8 @@ class StrainerFactoryUnitTest < Minitest::Test
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
exception.message
)
assert_equal(exception.backtrace[0].split(':')[0], __FILE__)
source = AccessScopeFilters.instance_method(:public_filter).source_location
assert_equal(source.map(&:to_s), exception.backtrace[0].split(':')[0..1])
end
def test_strainer_only_invokes_public_filter_methods
+1 -1
View File
@@ -57,8 +57,8 @@ class StrainerTemplateUnitTest < Minitest::Test
end
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
strainer = Context.new.strainer
with_global_filter do
strainer = Context.new.strainer
strainer.class.add_filter(PublicMethodOverrideFilter)
assert(strainer.class.send(:filter_methods).include?('public_filter'))
end