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
99 changed files with 2944 additions and 1991 deletions
+2
View File
@@ -0,0 +1,2 @@
enabled:
- cla
-22
View File
@@ -1,22 +0,0 @@
name: Contributor License Agreement (CLA)
on:
pull_request_target:
types: [opened, synchronize]
issue_comment:
types: [created]
jobs:
cla:
runs-on: ubuntu-latest
if: |
(github.event.issue.pull_request
&& !github.event.issue.pull_request.merged_at
&& contains(github.event.comment.body, 'signed')
)
|| (github.event.pull_request && !github.event.pull_request.merged)
steps:
- uses: Shopify/shopify-cla-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
cla-token: ${{ secrets.CLA_TOKEN }}
+3 -3
View File
@@ -6,12 +6,12 @@ jobs:
strategy:
matrix:
entry:
- { ruby: 2.7, allowed-failure: false } # minimum supported
- { ruby: 2.5, allowed-failure: false } # minimum supported
- { ruby: 3.1, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: true }
name: test (${{ matrix.entry.ruby }})
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.entry.ruby }}
@@ -26,7 +26,7 @@ jobs:
memory_profile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
+1 -1
View File
@@ -10,7 +10,7 @@ Performance:
Enabled: true
AllCops:
TargetRubyVersion: 2.7
TargetRubyVersion: 2.5
NewCops: disable
Exclude:
- 'vendor/bundle/**/*'
+14 -137
View File
@@ -1,74 +1,27 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2022-05-18 19:25:47 UTC using RuboCop version 1.29.1.
# on 2020-12-11 18:53:41 UTC using RuboCop version 1.6.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 1
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
# Include: **/*.gemspec
Gemspec/OrderedDependencies:
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: runtime_error, standard_error
Lint/InheritException:
Exclude:
- 'liquid.gemspec'
- 'lib/liquid/interrupts.rb'
# Offense count: 6
# This cop supports safe auto-correction (--auto-correct).
Layout/ClosingHeredocIndentation:
Exclude:
- 'test/integration/tags/for_tag_test.rb'
# Offense count: 34
# This cop supports safe auto-correction (--auto-correct).
Layout/EmptyLineAfterGuardClause:
Exclude:
- 'lib/liquid/block.rb'
- 'lib/liquid/block_body.rb'
- 'lib/liquid/context.rb'
- 'lib/liquid/drop.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/parser.rb'
- 'lib/liquid/profiler/hooks.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/utils.rb'
- 'lib/liquid/variable.rb'
- 'lib/liquid/variable_lookup.rb'
- 'performance/shopify/money_filter.rb'
- 'performance/shopify/paginate.rb'
# Offense count: 113
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Layout/LineLength:
Max: 260
# Offense count: 8
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: AllowAliasSyntax, AllowedMethods.
# AllowedMethods: alias_method, public, protected, private
Layout/EmptyLinesAroundAttributeAccessor:
Exclude:
- 'lib/liquid/template.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/unit/strainer_template_unit_test.rb'
# Offense count: 17
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: aligned, indented
Layout/LineEndStringConcatenationIndentation:
Exclude:
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/tags/increment_tag_test.rb'
# Offense count: 1
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: aligned, indented
Layout/MultilineOperationIndentation:
Exclude:
- 'lib/liquid/expression.rb'
# Offense count: 9
Lint/MissingSuper:
Exclude:
- 'lib/liquid/forloop_drop.rb'
@@ -80,7 +33,7 @@ Lint/MissingSuper:
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/tags/table_row_test.rb'
# Offense count: 44
# Offense count: 43
Naming/ConstantName:
Exclude:
- 'lib/liquid.rb'
@@ -98,83 +51,7 @@ Naming/ConstantName:
- 'performance/shopify/paginate.rb'
- 'test/integration/tags/include_tag_test.rb'
# Offense count: 9
# Configuration parameters: CheckIdentifiers, CheckConstants, CheckVariables, CheckStrings, CheckSymbols, CheckComments, CheckFilepaths, FlaggedTerms.
Naming/InclusiveLanguage:
Exclude:
- 'lib/liquid/drop.rb'
- 'lib/liquid/parse_context.rb'
- 'test/integration/drop_test.rb'
- 'test/integration/tags/if_else_tag_test.rb'
# Offense count: 2
Style/ClassVars:
Exclude:
- 'lib/liquid/condition.rb'
# Offense count: 3
# This cop supports safe auto-correction (--auto-correct).
Style/ExplicitBlockArgument:
Exclude:
- 'test/integration/context_test.rb'
- 'test/integration/tag/disableable_test.rb'
- 'test/integration/tags/for_tag_test.rb'
# Offense count: 2982
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiterals:
Enabled: false
# Offense count: 20
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/strainer_template.rb'
- 'lib/liquid/tag/disableable.rb'
- 'performance/shopify/shop_filter.rb'
- 'performance/shopify/tag_filter.rb'
# Offense count: 6
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInArrayLiteral:
Exclude:
- 'example/server/example_servlet.rb'
- 'lib/liquid/condition.rb'
- 'test/integration/context_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/unit/parse_tree_visitor_test.rb'
# Offense count: 1
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInHashLiteral:
Exclude:
- 'lib/liquid/expression.rb'
# Offense count: 19
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle, MinSize, WordRegex.
# SupportedStyles: percent, brackets
Style/WordArray:
Exclude:
- 'lib/liquid/tags/if.rb'
- 'liquid.gemspec'
- 'test/integration/assign_test.rb'
- 'test/integration/context_test.rb'
- 'test/integration/drop_test.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 117
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
# URISchemes: http, https
Layout/LineLength:
Max: 260
+7 -1
View File
@@ -11,14 +11,20 @@ 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-shopify', '~> 2.7.0', require: false
gem 'rubocop', '~> 1.4', require: false
gem 'rubocop-shopify', '~> 1.0.7', require: false
gem 'rubocop-performance', require: false
platform :mri, :truffleruby do
-16
View File
@@ -1,21 +1,5 @@
# Liquid Change Log
## 5.4.0 2022-07-29
### Breaking Changes
* Drop support for end-of-life Ruby versions (2.5 and 2.6) (#1578) [Andy Waite]
### Features
* Allow `#` to be used as an inline comment tag (#1498) [CP Clermont]
### Fixes
* `PartialCache` now shares snippet cache with subcontexts by default (#1553) [Chris AtLee]
* Hash registers no longer leak into subcontexts as static registers (#1564) [Chris AtLee]
* Fix `ParseTreeVisitor` for `with` variable expressions in `Render` tag (#1596) [CP Clermont]
### Changed
* Liquid::Context#registers now always returns a Liquid::Registers object, though supports the most used Hash functions for compatibility (#1553)
## 5.3.0 2022-03-22
### Fixes
+2 -2
View File
@@ -63,13 +63,13 @@ when templates are invalid. You can enable this new parser like this:
```ruby
Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
Liquid::Template.error_mode = :warn # Adds strict errors to template.errors but continues as normal
Liquid::Template.error_mode = :warn # Adds errors to template.errors but continues as normal
Liquid::Template.error_mode = :lax # The default mode, accepts almost anything.
```
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
```ruby
Liquid::Template.parse(source, error_mode: :strict)
Liquid::Template.parse(source, :error_mode => :strict)
```
This is useful for doing things like enabling strict mode only in the theme editor.
+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
@@ -29,7 +29,6 @@ module Liquid
WhitespaceControl = '-'
TagStart = /\{\%/
TagEnd = /\%\}/
TagName = /#|\w+/
VariableSignature = /\(?[\w\-\.\[\]]\)?/
VariableSegment = /[\w\-]/
VariableStart = /\{\{/
@@ -84,7 +83,8 @@ require 'liquid/tokenizer'
require 'liquid/parse_context'
require 'liquid/partial_cache'
require 'liquid/usage'
require 'liquid/registers'
require 'liquid/register'
require 'liquid/static_registers'
require 'liquid/template_factory'
# Load all the tags of the standard library
+4 -12
View File
@@ -4,8 +4,8 @@ require 'English'
module Liquid
class BlockBody
LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%"
@@ -109,22 +109,14 @@ module Liquid
end
end
private def handle_invalid_tag_token(token, parse_context)
if token.end_with?('%}')
yield token, token
else
BlockBody.raise_missing_tag_terminator(token, parse_context)
end
end
private def parse_for_document(tokenizer, parse_context, &block)
private def parse_for_document(tokenizer, parse_context)
while (token = tokenizer.shift)
next if token.empty?
case
when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context)
unless token =~ FullToken
return handle_invalid_tag_token(token, parse_context, &block)
BlockBody.raise_missing_tag_terminator(token, parse_context)
end
tag_name = Regexp.last_match(2)
markup = Regexp.last_match(4)
+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
+3 -7
View File
@@ -26,9 +26,9 @@ module Liquid
@environments = [environments]
@environments.flatten!
@static_environments = [static_environments].flatten(1).freeze
@static_environments = [static_environments].flat_map(&:freeze).freeze
@scopes = [(outer_scope || {})]
@registers = registers.is_a?(Registers) ? registers : Registers.new(registers)
@registers = registers
@errors = []
@partial = false
@strict_variables = false
@@ -39,10 +39,6 @@ module Liquid
@global_filter = nil
@disabled_tags = {}
@registers.static[:cached_partials] ||= {}
@registers.static[:file_system] ||= Liquid::Template.file_system
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
self.exception_renderer = Template.default_exception_renderer
if rethrow_errors
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
@@ -144,7 +140,7 @@ module Liquid
self.class.build(
resource_limits: resource_limits,
static_environments: static_environments,
registers: Registers.new(registers)
registers: StaticRegisters.new(registers)
).tap do |subcontext|
subcontext.base_scope_depth = base_scope_depth + 1
subcontext.exception_renderer = exception_renderer
+5 -45
View File
@@ -1,11 +1,6 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name forloop
# @liquid_summary
# Information about a parent [`for` loop](/api/liquid/tags/for).
class ForloopDrop < Drop
def initialize(name, length, parentloop)
@name = name
@@ -14,68 +9,33 @@ module Liquid
@index = 0
end
# @liquid_public_docs
# @liquid_name length
# @liquid_summary
# The total number of iterations in the loop.
# @liquid_return [number]
attr_reader :length
attr_reader :length, :parentloop
# @liquid_public_docs
# @liquid_name parentloop
# @liquid_summary
# The parent `forloop` object.
# @liquid_description
# If the current `for` loop isn't nested inside another `for` loop, then `nil` is returned.
# @liquid_return [forloop]
attr_reader :parentloop
def name
Usage.increment('forloop_drop_name')
@name
end
attr_reader :name
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration.
# @liquid_return [number]
def index
@index + 1
end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration.
# @liquid_return [number]
def index0
@index
end
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex
@length - @index
end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex0
@length - @index - 1
end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the first. Returns `false` if not.
# @liquid_return [boolean]
def first
@index == 0
end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the last. Returns `false` if not.
# @liquid_return [boolean]
def last
@index == @length - 1
end
+3 -2
View File
@@ -18,7 +18,6 @@ module Liquid
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/
STRING_LITERAL = Regexp.union(SINGLE_STRING_LITERAL, DOUBLE_STRING_LITERAL)
NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
@@ -36,7 +35,9 @@ module Liquid
break if @ss.eos?
tok = if (t = @ss.scan(COMPARISON_OPERATOR))
[:comparison, t]
elsif (t = @ss.scan(STRING_LITERAL))
elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
[:string, t]
elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
[:string, t]
elsif (t = @ss.scan(NUMBER_LITERAL))
[:number, t]
+5 -6
View File
@@ -13,16 +13,15 @@
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character"
unknown_tag: "Unknown tag '%{tag}'"
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
tag_never_closed: "'%{block_name}' tag was never closed"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
unexpected_else: "%{block_name} tag does not expect 'else' tag"
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
unknown_tag: "Unknown tag '%{tag}'"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
tag_never_closed: "'%{block_name}' tag was never closed"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
argument:
include: "Argument error in tag 'include' - Illegal template name"
disabled:
+3 -3
View File
@@ -3,16 +3,16 @@
module Liquid
class PartialCache
def self.load(template_name, context:, parse_context:)
cached_partials = context.registers[:cached_partials]
cached_partials = (context.registers[:cached_partials] ||= {})
cached = cached_partials[template_name]
return cached if cached
file_system = context.registers[:file_system]
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
source = file_system.read_template_file(template_name)
parse_context.partial = true
template_factory = context.registers[:template_factory]
template_factory = (context.registers[:template_factory] ||= Liquid::TemplateFactory.new)
template = template_factory.for(template_name)
partial = template.parse(source, parse_context)
+5 -11
View File
@@ -8,17 +8,7 @@ module Liquid
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj)
else
begin
start_obj.to_i..end_obj.to_i
rescue NoMethodError
invalid_expr = start_markup unless start_obj.respond_to?(:to_i)
invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
if invalid_expr
raise Liquid::SyntaxError, "Invalid expression type '#{invalid_expr}' in range expression"
end
raise
end
start_obj.to_i..end_obj.to_i
end
end
@@ -35,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)
+6
View File
@@ -0,0 +1,6 @@
# frozen_string_literal: true
module Liquid
class Register
end
end
+65 -430
View File
@@ -6,14 +6,7 @@ require 'bigdecimal'
module Liquid
module StandardFilters
MAX_I32 = (1 << 31) - 1
private_constant :MAX_I32
MIN_I64 = -(1 << 63)
MAX_I64 = (1 << 63) - 1
I64_RANGE = MIN_I64..MAX_I64
private_constant :MIN_I64, :MAX_I64, :I64_RANGE
MAX_INT = (1 << 31) - 1
HTML_ESCAPE = {
'&' => '&amp;',
'>' => '&gt;',
@@ -29,99 +22,39 @@ module Liquid
)
STRIP_HTML_TAGS = /<.*?>/m
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the size of a string or array.
# @liquid_description
# The size of a string is the number of characters that the string includes. The size of an array is the number of items
# in the array.
# @liquid_syntax variable | size
# @liquid_return [number]
# Return the size of an array or of an string
def size(input)
input.respond_to?(:size) ? input.size : 0
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts a string to all lowercase characters.
# @liquid_syntax string | downcase
# @liquid_return [string]
# convert an input string to DOWNCASE
def downcase(input)
input.to_s.downcase
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts a string to all uppercase characters.
# @liquid_syntax string | upcase
# @liquid_return [string]
# convert an input string to UPCASE
def upcase(input)
input.to_s.upcase
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Capitalizes the first word in a string.
# @liquid_syntax string | capitalize
# @liquid_return [string]
# capitalize words in the input centence
def capitalize(input)
input.to_s.capitalize
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Escapes special characters in HTML, such as `<>`, `'`, and `&`, and converts characters into escape sequences. The filter doesn't effect characters within the string that dont have a corresponding escape sequence.".
# @liquid_syntax string | escape
# @liquid_return [string]
def escape(input)
CGI.escapeHTML(input.to_s) unless input.nil?
end
alias_method :h, :escape
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Escapes a string without changing characters that have already been escaped.
# @liquid_syntax string | escape_once
# @liquid_return [string]
def escape_once(input)
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts any URL-unsafe characters in a string to the
# [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) equivalent.
# @liquid_description
# > Note:
# > Spaces are converted to a `+` character, instead of a percent-encoded character.
# @liquid_syntax string | url_encode
# @liquid_return [string]
def url_encode(input)
CGI.escape(input.to_s) unless input.nil?
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Decodes any [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) characters
# in a string.
# @liquid_syntax string | url_decode
# @liquid_return [string]
def url_decode(input)
return if input.nil?
@@ -131,94 +64,38 @@ module Liquid
result
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Encodes a string to [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_encode
# @liquid_return [string]
def base64_encode(input)
Base64.strict_encode64(input.to_s)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Decodes a string in [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_decode
# @liquid_return [string]
def base64_decode(input)
Base64.strict_decode64(input.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Encodes a string to URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_url_safe_encode
# @liquid_return [string]
def base64_url_safe_encode(input)
Base64.urlsafe_encode64(input.to_s)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Decodes a string in URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_url_safe_decode
# @liquid_return [string]
def base64_url_safe_decode(input)
Base64.urlsafe_decode64(input.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Returns a substring or series of array items, starting at a given 0-based index.
# @liquid_description
# By default, the substring has a length of one character, and the array series has one array item. However, you can
# provide a second parameter to specify the number of characters or array items.
# @liquid_syntax string | slice
# @liquid_return [string]
def slice(input, offset, length = nil)
offset = Utils.to_integer(offset)
length = length ? Utils.to_integer(length) : 1
begin
if input.is_a?(Array)
input.slice(offset, length) || []
else
input.to_s.slice(offset, length) || ''
end
rescue RangeError
if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
raise # unexpected error
end
offset = offset.clamp(I64_RANGE)
length = length.clamp(I64_RANGE)
retry
if input.is_a?(Array)
input.slice(offset, length) || []
else
input.to_s.slice(offset, length) || ''
end
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Truncates a string down to a given number of characters.
# @liquid_description
# If the specified number of characters is less than the length of the string, then an ellipsis (`...`) is appended to
# the truncated string. The ellipsis is included in the character count of the truncated string.
# @liquid_syntax string | truncate: number
# @liquid_return [string]
# Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...")
return if input.nil?
input_str = input.to_s
@@ -232,20 +109,6 @@ module Liquid
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Truncates a string down to a given number of words.
# @liquid_description
# If the specified number of words is less than the number of words in the string, then an ellipsis (`...`) is appended to
# the truncated string.
#
# > Caution:
# > HTML tags are treated as words, so you should strip any HTML from truncated content. If you don't strip HTML, then
# > closing HTML tags can be removed, which can result in unexpected behavior.
# @liquid_syntax string | truncatewords: number
# @liquid_return [string]
def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil?
input = input.to_s
@@ -255,9 +118,9 @@ module Liquid
wordlist = begin
input.split(" ", words + 1)
rescue RangeError
# integer too big for String#split, but we can semantically assume no truncation is needed
return input if words + 1 > MAX_I32
raise # unexpected error
raise if words + 1 < MAX_INT
# e.g. integer #{words} too big to convert to `int'
raise Liquid::ArgumentError, "integer #{words} too big for truncatewords"
end
return input if wordlist.length <= words
@@ -265,57 +128,27 @@ module Liquid
wordlist.join(" ").concat(truncate_string.to_s)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Splits a string into an array of substrings based on a given separator.
# @liquid_syntax string | split: string
# @liquid_return [array[string]]
# Split input string into an array of substrings separated by given pattern.
#
# Example:
# <div class="summary">{{ post | split '//' | first }}</div>
#
def split(input, pattern)
input.to_s.split(pattern.to_s)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all whitespace from the left and right of a string.
# @liquid_syntax string | strip
# @liquid_return [string]
def strip(input)
input.to_s.strip
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all whitespace from the left of a string.
# @liquid_syntax string | lstrip
# @liquid_return [string]
def lstrip(input)
input.to_s.lstrip
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all whitespace from the right of a string.
# @liquid_syntax string | rstrip
# @liquid_return [string]
def rstrip(input)
input.to_s.rstrip
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all HTML tags from a string.
# @liquid_syntax string | strip_html
# @liquid_return [string]
def strip_html(input)
empty = ''
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
@@ -323,35 +156,18 @@ module Liquid
result
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all newline characters (line breaks) from a string.
# @liquid_syntax string | strip_newlines
# @liquid_return [string]
# Remove all newlines from the string
def strip_newlines(input)
input.to_s.gsub(/\r?\n/, '')
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Combines all of the items in an array into a single string, separated by a space.
# @liquid_syntax array | join
# @liquid_return [string]
# Join elements of the array with certain character between them
def join(input, glue = ' ')
InputIterator.new(input, context).join(glue)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Sorts the items in an array in case-sensitive alphabetical, or numerical, order.
# @liquid_syntax array | sort
# @liquid_return [array[untyped]]
# Sort elements of the array
# provide optional property with which to sort an array of hashes or drops
def sort(input, property = nil)
ary = InputIterator.new(input, context)
@@ -370,17 +186,8 @@ module Liquid
end
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Sorts the items in an array in case-insensitive alphabetical order.
# @liquid_description
# > Caution:
# > You shouldn't use the `sort_natural` filter to sort numerical values. When comparing items an array, each item is converted to a
# > string, so sorting on numerical values can lead to unexpected results.
# @liquid_syntax array | sort_natural
# @liquid_return [array[untyped]]
# Sort elements of an array ignoring case if strings
# provide optional property with which to sort an array of hashes or drops
def sort_natural(input, property = nil)
ary = InputIterator.new(input, context)
@@ -399,15 +206,8 @@ module Liquid
end
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Filters an array to include only items with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | where: string, string
# @liquid_return [array[untyped]]
# Filter the elements of an array to those with a certain property value.
# By default the target is any truthy value.
def where(input, property, target_value = nil)
ary = InputIterator.new(input, context)
@@ -434,13 +234,8 @@ module Liquid
end
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Removes any duplicate items in an array.
# @liquid_syntax array | uniq
# @liquid_return [array[untyped]]
# Remove duplicate elements from an array
# provide optional property with which to determine uniqueness
def uniq(input, property = nil)
ary = InputIterator.new(input, context)
@@ -460,25 +255,13 @@ module Liquid
end
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Reverses the order of the items in an array.
# @liquid_syntax array | reverse
# @liquid_return [array[untyped]]
# Reverse the elements of an array
def reverse(input)
ary = InputIterator.new(input, context)
ary.reverse
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Creates an array of values from a specific property of the items in an array.
# @liquid_syntax array | map: string
# @liquid_return [array[untyped]]
# map/collect on a given property
def map(input, property)
InputIterator.new(input, context).map do |e|
e = e.call if e.is_a?(Proc)
@@ -494,13 +277,8 @@ module Liquid
raise_property_error(property)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Removes any `nil` items from an array.
# @liquid_syntax array | compact
# @liquid_return [array[untyped]]
# Remove nils within an array
# provide optional property with which to check for nil
def compact(input, property = nil)
ary = InputIterator.new(input, context)
@@ -520,35 +298,17 @@ module Liquid
end
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Replaces any instance of a substring inside a string with a given string.
# @liquid_syntax string | replace: string, string
# @liquid_return [string]
# Replace occurrences of a string with another
def replace(input, string, replacement = '')
input.to_s.gsub(string.to_s, replacement.to_s)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Replaces the first instance of a substring inside a string with a given string.
# @liquid_syntax string | replace_first: string, string
# @liquid_return [string]
# Replace the first occurrences of a string with another
def replace_first(input, string, replacement = '')
input.to_s.sub(string.to_s, replacement.to_s)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Replaces the last instance of a substring inside a string with a given string.
# @liquid_syntax string | replace_last: string, string
# @liquid_return [string]
# Replace the last occurrences of a string with another
def replace_last(input, string, replacement)
input = input.to_s
string = string.to_s
@@ -563,61 +323,26 @@ module Liquid
output
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes any instance of a substring inside a string.
# @liquid_syntax string | remove: string
# @liquid_return [string]
# remove a substring
def remove(input, string)
replace(input, string, '')
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes the first instance of a substring inside a string.
# @liquid_syntax string | remove_first: string
# @liquid_return [string]
# remove the first occurrences of a substring
def remove_first(input, string)
replace_first(input, string, '')
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes the last instance of a substring inside a string.
# @liquid_syntax string | remove_last: string
# @liquid_return [string]
# remove the last occurences of a substring
def remove_last(input, string)
replace_last(input, string, '')
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Adds a given string to the end of a string.
# @liquid_syntax string | append: string
# @liquid_return [string]
# add one string to another
def append(input, string)
input.to_s + string.to_s
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Concatenates (combines) two arrays.
# @liquid_description
# > Note:
# > The `concat` filter won't filter out duplicates. If you want to remove duplicates, then you need to use the
# > [`uniq` filter](/api/liquid/filters/uniq).
# @liquid_syntax array | concat: array
# @liquid_return [array[untyped]]
def concat(input, array)
unless array.respond_to?(:to_ary)
raise ArgumentError, "concat filter requires an array argument"
@@ -625,24 +350,12 @@ module Liquid
InputIterator.new(input, context).concat(array)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Adds a given string to the beginning of a string.
# @liquid_syntax string | prepend: string
# @liquid_return [string]
# prepend a string to another
def prepend(input, string)
string.to_s + input.to_s
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts newlines (`\n`) in a string to HTML line breaks (`<br>`).
# @liquid_syntax string | newline_to_br
# @liquid_return [string]
# Add <br /> tags in front of all newlines in input string
def newline_to_br(input)
input.to_s.gsub(/\r?\n/, "<br />\n")
end
@@ -686,106 +399,58 @@ module Liquid
date.strftime(format.to_s)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the first item in an array.
# @liquid_syntax array | first
# @liquid_return [untyped]
# Get the first element of the passed in array
#
# Example:
# {{ product.images | first | to_img }}
#
def first(array)
array.first if array.respond_to?(:first)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the last item in an array.
# @liquid_syntax array | last
# @liquid_return [untyped]
# Get the last element of the passed in array
#
# Example:
# {{ product.images | last | to_img }}
#
def last(array)
array.last if array.respond_to?(:last)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Returns the absolute value of a number.
# @liquid_syntax number | abs
# @liquid_return [number]
# absolute value
def abs(input)
result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Adds two numbers.
# @liquid_syntax number | plus: number
# @liquid_return [number]
# addition
def plus(input, operand)
apply_operation(input, operand, :+)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Subtracts a given number from another number.
# @liquid_syntax number | minus: number
# @liquid_return [number]
# subtraction
def minus(input, operand)
apply_operation(input, operand, :-)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Multiplies a number by a given number.
# @liquid_syntax number | times: number
# @liquid_return [number]
# multiplication
def times(input, operand)
apply_operation(input, operand, :*)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Divides a number by a given number.
# @liquid_syntax number | divided_by: number
# @liquid_return [number]
# division
def divided_by(input, operand)
apply_operation(input, operand, :/)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Returns the remainder of dividing a number by a given number.
# @liquid_syntax number | modulo: number
# @liquid_return [number]
def modulo(input, operand)
apply_operation(input, operand, :%)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Rounds a number to the nearest integer.
# @liquid_syntax number | round
# @liquid_return [number]
def round(input, n = 0)
result = Utils.to_number(input).round(Utils.to_number(n))
result = result.to_f if result.is_a?(BigDecimal)
@@ -795,39 +460,18 @@ module Liquid
raise Liquid::FloatDomainError, e.message
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Rounds a number up to the nearest integer.
# @liquid_syntax number | ceil
# @liquid_return [number]
def ceil(input)
Utils.to_number(input).ceil.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Rounds a number down to the nearest integer.
# @liquid_syntax number | floor
# @liquid_return [number]
def floor(input)
Utils.to_number(input).floor.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Limits a number to a minimum value.
# @liquid_syntax number | at_least
# @liquid_return [number]
def at_least(input, n)
min_value = Utils.to_number(n)
@@ -836,13 +480,6 @@ module Liquid
result.is_a?(BigDecimal) ? result.to_f : result
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Limits a number to a maximum value.
# @liquid_syntax number | at_most
# @liquid_return [number]
def at_most(input, n)
max_value = Utils.to_number(n)
@@ -851,18 +488,16 @@ module Liquid
result.is_a?(BigDecimal) ? result.to_f : result
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category default
# @liquid_summary
# Sets a default value for any variable whose value is one of the following:
# Set a default value when the input is nil, false or empty
#
# Example:
# {{ product.title | default: "No Title" }}
#
# Use `allow_false` when an input should only be tested against nil or empty and not false.
#
# Example:
# {{ product.title | default: "No Title", allow_false: true }}
#
# - [`empty`](/api/liquid/basics#empty)
# - [`false`](/api/liquid/basics#truthy-and-falsy)
# - [`nil`](/api/liquid/basics#nil)
# @liquid_syntax variable | default: variable
# @liquid_return [untyped]
# @liquid_optional_param allow_false [boolean] Whether to use false values instead of the default.
def default(input, default_value = '', options = {})
options = {} unless options.is_a?(Hash)
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
@@ -1,35 +1,35 @@
# frozen_string_literal: true
module Liquid
class Registers
class StaticRegisters
attr_reader :static
def initialize(registers = {})
@static = registers.is_a?(Registers) ? registers.static : registers
@changes = {}
@static = registers.is_a?(StaticRegisters) ? registers.static : registers
@registers = {}
end
def []=(key, value)
@changes[key] = value
@registers[key] = value
end
def [](key)
if @changes.key?(key)
@changes[key]
if @registers.key?(key)
@registers[key]
else
@static[key]
end
end
def delete(key)
@changes.delete(key)
@registers.delete(key)
end
UNDEFINED = Object.new
def fetch(key, default = UNDEFINED, &block)
if @changes.key?(key)
@changes.fetch(key)
if @registers.key?(key)
@registers.fetch(key)
elsif default != UNDEFINED
if block_given?
@static.fetch(key, &block)
@@ -42,10 +42,7 @@ module Liquid
end
def key?(key)
@changes.key?(key) || @static.key?(key)
@registers.key?(key) || @static.key?(key)
end
end
# Alias for backwards compatibility
StaticRegisters = Registers
end
-4
View File
@@ -14,10 +14,6 @@ module Liquid
strainer_from_cache(filters).new(context)
end
def global_filter_names
GlobalCache.filter_method_names
end
GlobalCache = Class.new(StrainerTemplate)
private
-4
View File
@@ -36,10 +36,6 @@ module Liquid
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
end
def filter_method_names
filter_methods.map(&:to_s).to_a
end
private
def filter_methods
+1 -58
View File
@@ -1,11 +1,6 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name tablerowloop
# @liquid_summary
# Information about a parent [`tablerow` loop](/api/liquid/tags/tablerow).
class TablerowloopDrop < Drop
def initialize(length, cols)
@length = length
@@ -15,92 +10,40 @@ module Liquid
@index = 0
end
# @liquid_public_docs
# @liquid_summary
# The total number of iterations in the loop.
# @liquid_return [number]
attr_reader :length
attr_reader :length, :col, :row
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current column.
# @liquid_return [number]
attr_reader :col
# @liquid_public_docs
# @liquid_summary
# The 1-based index of current row.
# @liquid_return [number]
attr_reader :row
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration.
# @liquid_return [number]
def index
@index + 1
end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration.
# @liquid_return [number]
def index0
@index
end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current column.
# @liquid_return [number]
def col0
@col - 1
end
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex
@length - @index
end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex0
@length - @index - 1
end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the first. Returns `false` if not.
# @liquid_return [boolean]
def first
@index == 0
end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the last. Returns `false` if not.
# @liquid_return [boolean]
def last
@index == @length - 1
end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current column is the first in the row. Returns `false` if not.
# @liquid_return [boolean]
def col_first
@col == 1
end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current column is the last in the row. Returns `false` if not.
# @liquid_return [boolean]
def col_last
@col == @cols
end
+2 -8
View File
@@ -14,18 +14,12 @@ module Liquid
end
def disable_tags(*tag_names)
tag_names += disabled_tags
define_singleton_method(:disabled_tags) { tag_names }
@disabled_tags ||= []
@disabled_tags.concat(tag_names)
prepend(Disabler)
end
private :new
protected
def disabled_tags
[]
end
end
def initialize(tag_name, markup, parse_context)
+8
View File
@@ -3,6 +3,14 @@
module Liquid
class Tag
module Disabler
module ClassMethods
attr_reader :disabled_tags
end
def self.prepended(base)
base.extend(ClassMethods)
end
def render_to_output_buffer(context, output)
context.with_disabled_tags(self.class.disabled_tags) do
super
+8 -12
View File
@@ -1,18 +1,14 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category variable
# @liquid_name assign
# @liquid_summary
# Creates a new variable.
# @liquid_description
# You can create variables of any [basic type](/api/liquid/basics#types), [object](/api/liquid/objects), or object property.
# @liquid_syntax
# {% assign variable_name = value %}
# @liquid_syntax_keyword variable_name The name of the variable being created.
# @liquid_syntax_keyword value The value you want to assign to the variable.
# Assign sets a variable in your template.
#
# {% assign foo = 'monkey' %}
#
# You can then use the variable later in the page.
#
# {{ foo }}
#
class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
-8
View File
@@ -10,14 +10,6 @@ module Liquid
# {% endif %}
# {% endfor %}
#
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name break
# @liquid_summary
# Stops a [`for` loop](/api/liquid/tags/for) from iterating.
# @liquid_syntax
# {% break %}
class Break < Tag
INTERRUPT = BreakInterrupt.new.freeze
+10 -13
View File
@@ -1,20 +1,17 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category variable
# @liquid_name capture
# @liquid_summary
# Creates a new variable with a string value.
# @liquid_description
# You can create complex strings with Liquid logic and variables.
# @liquid_syntax
# {% capture variable %}
# value
# Capture stores the result of a block into a variable without rendering it inplace.
#
# {% capture heading %}
# Monkeys!
# {% endcapture %}
# @liquid_syntax_keyword variable The name of the variable being created.
# @liquid_syntax_keyword value The value you want to assign to the variable.
# ...
# <h1>{{ heading }}</h1>
#
# Capture is useful for saving content for use later in your template, such as
# in a sidebar or footer.
#
class Capture < Block
Syntax = /(#{VariableSignature}+)/o
-21
View File
@@ -1,27 +1,6 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category conditional
# @liquid_name case
# @liquid_summary
# Renders a specific expression depending on the value of a specific variable.
# @liquid_syntax
# {% case variable %}
# {% when first_value %}
# first_expression
# {% when second_value %}
# second_expression
# {% else %}
# third_expression
# {% endcase %}
# @liquid_syntax_keyword variable The name of the variable you want to base your case statement on.
# @liquid_syntax_keyword first_value A specific value to check for.
# @liquid_syntax_keyword second_value A specific value to check for.
# @liquid_syntax_keyword first_expression An expression to be rendered when the variable's value matches `first_value`.
# @liquid_syntax_keyword second_expression An expression to be rendered when the variable's value matches `second_value`.
# @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
class Case < Block
Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
-13
View File
@@ -1,19 +1,6 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name comment
# @liquid_summary
# Prevents an expression from being rendered or output.
# @liquid_description
# Any text inside `comment` tags won't be output, and any Liquid code will be parsed, but not executed.
# @liquid_syntax
# {% comment %}
# content
# {% endcomment %}
# @liquid_syntax_keyword content The content of the comment.
class Comment < Block
def render_to_output_buffer(_context, output)
output
+9 -8
View File
@@ -1,14 +1,15 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name continue
# @liquid_summary
# Causes a [`for` loop](/api/liquid/tags/for) to skip to the next iteration.
# @liquid_syntax
# {% continue %}
# Continue tag to be used to break out of a for loop.
#
# == Basic Usage:
# {% for item in collection %}
# {% if item.condition %}
# {% continue %}
# {% endif %}
# {% endfor %}
#
class Continue < Tag
INTERRUPT = ContinueInterrupt.new.freeze
+11 -12
View File
@@ -1,19 +1,18 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name cycle
# @liquid_summary
# Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/api/liquid/tags/for).
# @liquid_description
# The `cycle` tag must be used inside a `for` loop.
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
#
# {% for item in items %}
# <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
# {% end %}
#
# <div class="red"> Item one </div>
# <div class="green"> Item two </div>
# <div class="blue"> Item three </div>
# <div class="red"> Item four </div>
# <div class="green"> Item five</div>
#
# > Tip:
# > Use the `cycle` tag to output text in a predictable pattern. For example, to apply odd/even classes to rows in a table.
# @liquid_syntax
# {% cycle string, string, ... %}
class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
+20 -22
View File
@@ -1,36 +1,34 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category variable
# @liquid_name decrement
# @liquid_summary
# Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
# @liquid_description
# Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# [snippets](/themes/architecture#snippets) included in the file.
# decrement is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across
# multiple instantiations of the template.
# NOTE: decrement is a pre-decrement, --i,
# while increment is post: i++.
#
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/api/liquid/tags/assign)
# and [`capture`](/api/liquid/tags/capture). However, `decrement` and [`increment`](/api/liquid/tags/increment) share
# variables.
# @liquid_syntax
# {% decrement variable_name %}
# @liquid_syntax_keyword variable_name The name of the variable being decremented.
class Decrement < Tag
attr_reader :variable_name
# (To achieve the survival, the application must keep the context)
#
# if the variable does not exist, it is created with value 0.
# Hello: {% decrement variable %}
#
# gives you:
#
# Hello: -1
# Hello: -2
# Hello: -3
#
class Decrement < Tag
def initialize(tag_name, markup, options)
super
@variable_name = markup.strip
@variable = markup.strip
end
def render_to_output_buffer(context, output)
counter_environment = context.environments.first
value = counter_environment[@variable_name] || 0
value = context.environments.first[@variable] ||= 0
value -= 1
counter_environment[@variable_name] = value
context.environments.first[@variable] = value
output << value.to_s
output
end
+9 -16
View File
@@ -1,23 +1,16 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name echo
# @liquid_summary
# Outputs an expression.
# @liquid_description
# Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly
# bracket method, you can use the `echo` tag inside [`liquid` tags](/api/liquid/tags/liquid).
# Echo outputs an expression
#
# {% echo monkey %}
# {% echo user.name %}
#
# This is identical to variable output syntax, like {{ foo }}, but works
# inside {% liquid %} tags. The full syntax is supported, including filters:
#
# {% echo user | link %}
#
# > Tip:
# > You can use [filters](/api/liquid/filters) on expressions inside `echo` tags.
# @liquid_syntax
# {% liquid
# echo expression
# %}
# @liquid_syntax_keyword expression The expression to be output.
class Echo < Tag
attr_reader :variable
+44 -22
View File
@@ -1,29 +1,50 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name for
# @liquid_summary
# Renders an expression for every item in an array.
# @liquid_description
# You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the
# [`paginate` tag](/api/liquid/tags/paginate) to split the items over multiple pages.
# "For" iterates over an array or collection.
# Several useful variables are available to you within the loop.
#
# == Basic usage:
# {% for item in collection %}
# {{ forloop.index }}: {{ item.name }}
# {% endfor %}
#
# == Advanced usage:
# {% for item in collection %}
# <div {% if forloop.first %}class="first"{% endif %}>
# Item {{ forloop.index }}: {{ item.name }}
# </div>
# {% else %}
# There is nothing in the collection.
# {% endfor %}
#
# You can also define a limit and offset much like SQL. Remember
# that offset starts at 0 for the first item.
#
# {% for item in collection limit:5 offset:10 %}
# {{ item.name }}
# {% end %}
#
# To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
#
# == Available variables:
#
# forloop.name:: 'item-collection'
# forloop.length:: Length of the loop
# forloop.index:: The current item's position in the collection;
# forloop.index starts at 1.
# This is helpful for non-programmers who start believe
# the first item in an array is 1, not 0.
# forloop.index0:: The current item's position in the collection
# where the first item is 0
# forloop.rindex:: Number of items remaining in the loop
# (length - index) where 1 is the last item.
# forloop.rindex0:: Number of items remaining in the loop
# where 0 is the last item.
# forloop.first:: Returns true if the item is the first item.
# forloop.last:: Returns true if the item is the last item.
# forloop.parentloop:: Provides access to the parent loop, if present.
#
# > Tip:
# > Every `for` loop has an associated [`forloop` object](/api/liquid/objects/forloop) with information about the loop.
# @liquid_syntax
# {% for variable in array %}
# expression
# {% endfor %}
# @liquid_syntax_keyword variable The current item in the array.
# @liquid_syntax_keyword array The array to iterate over.
# @liquid_syntax_keyword expression The expression to render for each iteration.
# @liquid_optional_param limit [number] The number of iterations to perform.
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
# @liquid_optional_param reversed [untyped] Iterate in reverse order.
class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
@@ -177,6 +198,7 @@ module Liquid
case key
when 'offset'
@from = if expr == 'continue'
Usage.increment('for_offset_continue')
:continue
else
parse_expression(expr)
+9 -11
View File
@@ -1,18 +1,16 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category conditional
# @liquid_name if
# @liquid_summary
# Renders an expression if a specific condition is `true`.
# @liquid_syntax
# {% if condition %}
# expression
# If is the conditional block
#
# {% if user.admin %}
# Admin user!
# {% else %}
# Not admin user
# {% endif %}
# @liquid_syntax_keyword condition The condition to evaluate.
# @liquid_syntax_keyword expression The expression to render if the condition is met.
#
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
#
class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
+14 -16
View File
@@ -1,22 +1,20 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category theme
# @liquid_name include
# @liquid_summary
# Renders a [snippet](/themes/architecture#snippets).
# @liquid_description
# Inside the snippet, you can access and alter variables that are [created](/api/liquid/tags/variable-tags) outside of the
# snippet.
# @liquid_syntax
# {% include 'filename' %}
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
# @liquid_deprecated
# Deprecated because the way that variables are handled reduces performance and makes code harder to both read and maintain.
# Include allows templates to relate with other templates
#
# Simply include another template:
#
# {% include 'product' %}
#
# Include a template with a local variable:
#
# {% include 'product' with products[0] %}
#
# Include a template for a collection:
#
# {% include 'product' for products %}
#
# The `include` tag has been replaced by [`render`](/api/liquid/tags/render).
class Include < Tag
prepend Tag::Disableable
@@ -52,7 +50,7 @@ module Liquid
def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name.is_a?(String)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
partial = PartialCache.load(
template_name,
+17 -22
View File
@@ -1,35 +1,30 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category variable
# @liquid_name increment
# @liquid_summary
# Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call.
# @liquid_description
# Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# [snippets](/themes/architecture#snippets) included in the file.
# increment is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across
# multiple instantiations of the template.
# (To achieve the survival, the application must keep the context)
#
# if the variable does not exist, it is created with value 0.
#
# Hello: {% increment variable %}
#
# gives you:
#
# Hello: 0
# Hello: 1
# Hello: 2
#
# Similarly, variables that are created with `increment` are independent from those created with [`assign`](/api/liquid/tags/assign)
# and [`capture`](/api/liquid/tags/capture). However, `increment` and [`decrement`](/api/liquid/tags/decrement) share
# variables.
# @liquid_syntax
# {% increment variable_name %}
# @liquid_syntax_keyword variable_name The name of the variable being incremented.
class Increment < Tag
attr_reader :variable_name
def initialize(tag_name, markup, options)
super
@variable_name = markup.strip
@variable = markup.strip
end
def render_to_output_buffer(context, output)
counter_environment = context.environments.first
value = counter_environment[@variable_name] || 0
counter_environment[@variable_name] = value + 1
value = context.environments.first[@variable] ||= 0
context.environments.first[@variable] = value + 1
output << value.to_s
output
-30
View File
@@ -1,30 +0,0 @@
# frozen_string_literal: true
module Liquid
class InlineComment < Tag
def initialize(tag_name, markup, options)
super
# Semantically, a comment should only ignore everything after it on the line.
# Currently, this implementation doesn't support mixing a comment with another tag
# but we need to reserve future support for this and prevent the introduction
# of inline comments from being backward incompatible change.
#
# As such, we're forcing users to put a # symbol on every line otherwise this
# tag will throw an error.
if markup.match?(/\n\s*[^#\s]/)
raise SyntaxError, options[:locale].t("errors.syntax.inline_comment_invalid")
end
end
def render_to_output_buffer(_context, output)
output
end
def blank?
true
end
end
Template.register_tag('#', InlineComment)
end
-11
View File
@@ -1,17 +1,6 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name raw
# @liquid_summary
# Outputs any Liquid code as text instead of rendering it.
# @liquid_syntax
# {% raw %}
# expression
# {% endraw %}
# @liquid_syntax_keyword expression The expression to be output without being rendered.
class Raw < Block
Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
+6 -35
View File
@@ -1,37 +1,13 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category theme
# @liquid_name render
# @liquid_summary
# Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
# @liquid_description
# Inside snippets and app blocks, you can't directly access variables that are [created](/api/liquid/tags/variable-tags) outside
# of the snippet or app block. However, you can [specify variables as parameters](/api/liquid/tags/render#render-passing-variables-to-a-snippet)
# to pass outside variables to snippets.
#
# While you can't directly access created variables, you can access global objects, as well as any objects that are
# directly accessible outside the snippet or app block. For example, a snippet or app block inside the [product template](/themes/architecture/templates/product)
# can access the [`product` object](/api/liquid/objects/product), and a snippet or app block inside a [section](/themes/architecture/sections)
# can access the [`section` object](/api/liquid/objects/section).
#
# Outside a snippet or app block, you can't access variables created inside the snippet or app block.
#
# > Note:
# > When you render a snippet using the `render` tag, you can't use the [`include` tag](/api/liquid/tags/include)
# > inside the snippet.
# @liquid_syntax
# {% render 'filename' %}
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
class Render < Tag
FOR = 'for'
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
disable_tags "include"
attr_reader :template_name_expr, :variable_name_expr, :attributes, :alias_name
attr_reader :template_name_expr, :attributes
def initialize(tag_name, markup, options)
super
@@ -45,7 +21,7 @@ module Liquid
@alias_name = Regexp.last_match(6)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name)
@is_for_loop = (with_or_for == FOR)
@for = (with_or_for == FOR)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@@ -53,18 +29,14 @@ module Liquid
end
end
def for_loop?
@is_for_loop
end
def render_to_output_buffer(context, output)
render_tag(context, output)
end
def render_tag(context, output)
# The expression should be a String literal, which parses to a String object
template_name = @template_name_expr
raise ::ArgumentError unless template_name.is_a?(String)
# Though we evaluate this here we will only ever parse it as a string literal.
template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
partial = PartialCache.load(
template_name,
@@ -89,7 +61,7 @@ module Liquid
}
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
if @for && variable.respond_to?(:each) && variable.respond_to?(:count)
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
variable.each { |var| render_partial_func.call(var, forloop) }
else
@@ -103,7 +75,6 @@ module Liquid
def children
[
@node.template_name_expr,
@node.variable_name_expr,
] + @node.attributes.values
end
end
+1 -23
View File
@@ -1,28 +1,6 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name tablerow
# @liquid_summary
# Generates HTML table rows for every item in an array.
# @liquid_description
# The `tablerow` tag must be wrapped in HTML `<table>` and `</table>` tags.
#
# > Tip:
# > Every `tablerow` loop has an associated [`tablerowloop` object](/api/liquid/objects/tablerowloop) with information about the loop.
# @liquid_syntax
# {% tablerow variable in array %}
# expression
# {% endtablerow %}
# @liquid_syntax_keyword variable The current item in the array.
# @liquid_syntax_keyword array The array to iterate over.
# @liquid_syntax_keyword expression The expression to render.
# @liquid_optional_param cols [number] The number of columns that the table should have.
# @liquid_optional_param limit [number] The number of iterations to perform.
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
@@ -51,7 +29,7 @@ module Liquid
collection = Utils.slice_collection(collection, from, to)
length = collection.length
cols = @attributes.key?('cols') ? context.evaluate(@attributes['cols']).to_i : length
cols = context.evaluate(@attributes['cols']).to_i
output << "<tr class=\"row1\">\n"
context.stack do
+4 -15
View File
@@ -3,21 +3,10 @@
require_relative 'if'
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category conditional
# @liquid_name unless
# @liquid_summary
# Renders an expression unless a specific condition is `true`.
# @liquid_description
# > Tip:
# > Similar to the [`if` tag](/api/liquid/tags/if), you can use `elsif` to add more conditions to an `unless` tag.
# @liquid_syntax
# {% unless condition %}
# expression
# {% endunless %}
# @liquid_syntax_keyword condition The condition to evaluate.
# @liquid_syntax_keyword expression The expression to render unless the condition is met.
# Unless is a conditional just like 'if' but works on the inverse logic.
#
# {% unless x < 0 %} x is greater than zero {% endunless %}
#
class Unless < If
def render_to_output_buffer(context, output)
# First condition is interpreted backwards ( if not )
+3 -2
View File
@@ -167,14 +167,15 @@ module Liquid
output = nil
context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
case args.last
when Hash
options = args.pop
output = options[:output] if options[:output]
static_registers = context.registers.static
options[:registers]&.each do |key, register|
static_registers[key] = register
context_register[key] = register
end
apply_options_to_context(context, options)
+2 -8
View File
@@ -8,15 +8,11 @@ module Liquid
@source = source.to_s.to_str
@line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag
@offset = 0
@tokens = tokenize
end
def shift
token = @tokens[@offset]
return nil unless token
@offset += 1
(token = @tokens.shift) || return
if @line_number
@line_number += @for_liquid_tag ? 1 : token.count("\n")
@@ -35,9 +31,7 @@ module Liquid
tokens = @source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
if tokens[0]&.empty?
@offset += 1
end
tokens.shift if tokens[0]&.empty?
tokens
end
+8 -4
View File
@@ -47,7 +47,7 @@ module Liquid
name_markup = Regexp.last_match(1)
filter_markup = Regexp.last_match(2)
@name = parse_context.parse_expression(name_markup)
@name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex
filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f|
@@ -65,7 +65,7 @@ module Liquid
return if p.look(:end_of_string)
@name = parse_context.parse_expression(p.expression)
@name = Expression.parse(p.expression)
while p.consume?(:pipe)
filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
@@ -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)
@@ -122,9 +126,9 @@ module Liquid
unparsed_args.each do |a|
if (matches = a.match(JustTagAttributes))
keyword_args ||= {}
keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
keyword_args[matches[1]] = Expression.parse(matches[2])
else
filter_args << parse_context.parse_expression(a)
filter_args << Expression.parse(a)
end
end
result = [filter_name, filter_args]
+1 -5
View File
@@ -32,10 +32,6 @@ module Liquid
end
end
def lookup_command?(lookup_index)
@command_flags & (1 << lookup_index) != 0
end
def evaluate(context)
name = context.evaluate(@name)
object = context.find_variable(name)
@@ -59,7 +55,7 @@ module Liquid
# Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls
# as commands and call them on the current object
elsif lookup_command?(i) && object.respond_to?(key)
elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "5.4.0"
VERSION = "5.3.0"
end
+3 -2
View File
@@ -17,12 +17,13 @@ Gem::Specification.new do |s|
s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.7.0"
s.required_ruby_version = ">= 2.5.0"
s.required_rubygems_version = ">= 1.3.7"
s.metadata['allowed_push_host'] = 'https://rubygems.org'
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
s.extra_rdoc_files = ["History.md", "README.md"]
+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'
+1 -1
View File
@@ -3,7 +3,7 @@
# 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 synthetic as possible. All templates and tests are lifted from
# 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.
+21 -12
View File
@@ -6,42 +6,51 @@ class AssignTest < Minitest::Test
include Liquid
def test_assign_with_hyphen_in_variable_name
template_source = <<~END_TEMPLATE
{% assign this-thing = 'Print this-thing' -%}
{{ this-thing -}}
template_source = <<-END_TEMPLATE
{% assign this-thing = 'Print this-thing' %}
{{ this-thing }}
END_TEMPLATE
assert_template_result("Print this-thing", template_source)
template = Template.parse(template_source)
rendered = template.render!
assert_equal("Print this-thing", rendered.strip)
end
def test_assigned_variable
assert_template_result('.foo.',
'{% assign foo = values %}.{{ foo[0] }}.',
{ 'values' => %w(foo bar baz) })
'values' => %w(foo bar baz))
assert_template_result('.bar.',
'{% assign foo = values %}.{{ foo[1] }}.',
{ 'values' => %w(foo bar baz) })
'values' => %w(foo bar baz))
end
def test_assign_with_filter
assert_template_result('.bar.',
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
{ 'values' => "foo,bar,baz" })
'values' => "foo,bar,baz")
end
def test_assign_syntax_error
assert_match_syntax_error(/assign/, '{% assign foo not values %}.')
assert_match_syntax_error(/assign/,
'{% assign foo not values %}.',
'values' => "foo,bar,baz")
end
def test_assign_uses_error_mode
assert_match_syntax_error("Expected dotdot but found pipe in ",
"{% assign foo = ('X' | downcase) %}", error_mode: :strict)
assert_template_result("", "{% assign foo = ('X' | downcase) %}", error_mode: :lax)
with_error_mode(:strict) do
assert_raises(SyntaxError) do
Template.parse("{% assign foo = ('X' | downcase) %}")
end
end
with_error_mode(:lax) do
assert(Template.parse("{% assign foo = ('X' | downcase) %}"))
end
end
def test_expression_with_whitespace_in_square_brackets
source = "{% assign r = a[ 'b' ] %}{{ r }}"
assert_template_result('result', source, { 'a' => { 'b' => 'result' } })
assert_template_result('result', source, 'a' => { 'b' => 'result' })
end
def test_assign_score_exceeding_resource_limit
+11 -7
View File
@@ -9,6 +9,12 @@ class FoobarTag < Liquid::Tag
end
end
class BlankTestFileSystem
def read_template_file(template_path)
template_path
end
end
class BlankTest < Minitest::Test
include Liquid
N = 10
@@ -27,7 +33,7 @@ class BlankTest < Minitest::Test
def test_new_tags_are_not_blank_by_default
with_custom_tag('foobar', FoobarTag) do
assert_equal(" " * N, Liquid::Template.parse(wrap_in_for("{% foobar %}")).render!)
assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
end
end
@@ -89,12 +95,10 @@ class BlankTest < Minitest::Test
end
def test_include_is_blank
assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}"),
partials: { 'foobar' => 'foobar' })
assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}"),
partials: { ' foobar ' => ' foobar ' })
assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} "),
partials: { ' ' => ' ' })
Liquid::Template.file_system = BlankTestFileSystem.new
assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}"))
assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}"))
assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} "))
end
def test_case_is_blank
+4 -2
View File
@@ -6,8 +6,10 @@ class BlockTest < Minitest::Test
include Liquid
def test_unexpected_end_tag
source = '{% if true %}{% endunless %}'
assert_match_syntax_error("Liquid syntax error (line 1): 'endunless' is not a valid delimiter for if tags. use endif", source)
exc = assert_raises(SyntaxError) do
Template.parse("{% if true %}{% endunless %}")
end
assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif")
end
def test_with_custom_tag
+29 -23
View File
@@ -10,38 +10,44 @@ class CaptureTest < Minitest::Test
end
def test_capture_with_hyphen_in_variable_name
template_source = <<~END_TEMPLATE
{% capture this-thing %}Print this-thing{% endcapture -%}
{{ this-thing -}}
template_source = <<-END_TEMPLATE
{% capture this-thing %}Print this-thing{% endcapture %}
{{ this-thing }}
END_TEMPLATE
assert_template_result("Print this-thing", template_source)
template = Template.parse(template_source)
rendered = template.render!
assert_equal("Print this-thing", rendered.strip)
end
def test_capture_to_variable_from_outer_scope_if_existing
template_source = <<~END_TEMPLATE
{% assign var = '' -%}
{% if true -%}
{% capture var %}first-block-string{% endcapture -%}
{% endif -%}
{% if true -%}
{% capture var %}test-string{% endcapture -%}
{% endif -%}
{{var-}}
template_source = <<-END_TEMPLATE
{% assign var = '' %}
{% if true %}
{% capture var %}first-block-string{% endcapture %}
{% endif %}
{% if true %}
{% capture var %}test-string{% endcapture %}
{% endif %}
{{var}}
END_TEMPLATE
assert_template_result("test-string", template_source)
template = Template.parse(template_source)
rendered = template.render!
assert_equal("test-string", rendered.gsub(/\s/, ''))
end
def test_assigning_from_capture
template_source = <<~END_TEMPLATE
{% assign first = '' -%}
{% assign second = '' -%}
{% for number in (1..3) -%}
{% capture first %}{{number}}{% endcapture -%}
{% assign second = first -%}
{% endfor -%}
{{ first }}-{{ second -}}
template_source = <<-END_TEMPLATE
{% assign first = '' %}
{% assign second = '' %}
{% for number in (1..3) %}
{% capture first %}{{number}}{% endcapture %}
{% assign second = first %}
{% endfor %}
{{ first }}-{{ second }}
END_TEMPLATE
assert_template_result("3-3", template_source)
template = Template.parse(template_source)
rendered = template.render!
assert_equal("3-3", rendered.gsub(/\s/, ''))
end
def test_increment_assign_score_by_bytes_not_characters
+83 -86
View File
@@ -102,7 +102,7 @@ class ContextTest < Minitest::Test
end
def test_variables_not_existing
assert_template_result("true", "{% if does_not_exist == nil %}true{% endif %}")
assert_nil(@context['does_not_exist'])
end
def test_scoping
@@ -121,18 +121,22 @@ class ContextTest < Minitest::Test
end
def test_length_query
assert_template_result("true", "{% if numbers.size == 4 %}true{% endif %}",
{ "numbers" => [1, 2, 3, 4] })
@context['numbers'] = [1, 2, 3, 4]
assert_template_result("true", "{% if numbers.size == 4 %}true{% endif %}",
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } })
assert_equal(4, @context['numbers.size'])
assert_template_result("true", "{% if numbers.size == 1000 %}true{% endif %}",
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 } })
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4 }
assert_equal(4, @context['numbers.size'])
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 }
assert_equal(1000, @context['numbers.size'])
end
def test_hyphenated_variable
assert_template_result("godz", "{{ oh-my }}", { "oh-my" => 'godz' })
@context['oh-my'] = 'godz'
assert_equal('godz', @context['oh-my'])
end
def test_add_filter
@@ -184,24 +188,24 @@ class ContextTest < Minitest::Test
end
def test_hierachical_data
assigns = { 'hash' => { "name" => 'tobi' } }
assert_template_result("tobi", "{{ hash.name }}", assigns)
assert_template_result("tobi", '{{ hash["name"] }}', assigns)
@context['hash'] = { "name" => 'tobi' }
assert_equal('tobi', @context['hash.name'])
assert_equal('tobi', @context['hash["name"]'])
end
def test_keywords
assert_template_result("pass", "{% if true == expect %}pass{% endif %}", { "expect" => true })
assert_template_result("pass", "{% if false == expect %}pass{% endif %}", { "expect" => false })
assert_equal(true, @context['true'])
assert_equal(false, @context['false'])
end
def test_digits
assert_template_result("pass", "{% if 100 == expect %}pass{% endif %}", { "expect" => 100 })
assert_template_result("pass", "{% if 100.00 == expect %}pass{% endif %}", { "expect" => 100.00 })
assert_equal(100, @context['100'])
assert_equal(100.00, @context['100.00'])
end
def test_strings
assert_template_result("hello!", '{{ "hello!" }}')
assert_template_result("hello!", "{{ 'hello!' }}")
assert_equal("hello!", @context['"hello!"'])
assert_equal("hello!", @context["'hello!'"])
end
def test_merge
@@ -213,90 +217,99 @@ class ContextTest < Minitest::Test
end
def test_array_notation
assigns = { "test" => ["a", "b"] }
assert_template_result("a", "{{ test[0] }}", assigns)
assert_template_result("b", "{{ test[1] }}", assigns)
assert_template_result("pass", "{% if test[2] == nil %}pass{% endif %}", assigns)
@context['test'] = [1, 2, 3, 4, 5]
assert_equal(1, @context['test[0]'])
assert_equal(2, @context['test[1]'])
assert_equal(3, @context['test[2]'])
assert_equal(4, @context['test[3]'])
assert_equal(5, @context['test[4]'])
end
def test_recoursive_array_notation
assigns = { "test" => { 'test' => [1, 2, 3, 4, 5] } }
assert_template_result("1", "{{ test.test[0] }}", assigns)
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
assigns = { "test" => [{ 'test' => 'worked' }] }
assert_template_result("worked", "{{ test[0].test }}", assigns)
assert_equal(1, @context['test.test[0]'])
@context['test'] = [{ 'test' => 'worked' }]
assert_equal('worked', @context['test[0].test'])
end
def test_hash_to_array_transition
assigns = { 'colors' => {
@context['colors'] = {
'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
'Green' => ['003300', '336633', '669966', '99CC99'],
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
} }
}
assert_template_result("003366", "{{ colors.Blue[0] }}", assigns)
assert_template_result("FF9999", "{{ colors.Red[3] }}", assigns)
assert_equal('003366', @context['colors.Blue[0]'])
assert_equal('FF9999', @context['colors.Red[3]'])
end
def test_try_first
assigns = { 'test' => [1, 2, 3, 4, 5] }
assert_template_result("1", "{{ test.first }}", assigns)
assert_template_result("pass", "{% if test.last == 5 %}pass{% endif %}", assigns)
@context['test'] = [1, 2, 3, 4, 5]
assigns = { "test" => { "test" => [1, 2, 3, 4, 5] } }
assert_template_result("1", "{{ test.test.first }}", assigns)
assert_template_result("5", "{{ test.test.last }}", assigns)
assert_equal(1, @context['test.first'])
assert_equal(5, @context['test.last'])
assigns = { "test" => [1] }
assert_template_result("1", "{{ test.first }}", assigns)
assert_template_result("1", "{{ test.last }}", assigns)
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
assert_equal(1, @context['test.test.first'])
assert_equal(5, @context['test.test.last'])
@context['test'] = [1]
assert_equal(1, @context['test.first'])
assert_equal(1, @context['test.last'])
end
def test_access_hashes_with_hash_notation
assigns = { 'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } }
assert_template_result("5", '{{ products["count"] }}', assigns)
assert_template_result("deepsnow", '{{ products["tags"][0] }}', assigns)
assert_template_result("deepsnow", '{{ products["tags"].first }}', assigns)
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
assigns = { 'product' => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } }
assert_template_result("draft151cm", '{{ product["variants"][0]["title"] }}', assigns)
assert_template_result("element151cm", '{{ product["variants"][1]["title"] }}', assigns)
assert_template_result("draft151cm", '{{ product["variants"][0]["title"] }}', assigns)
assert_template_result("element151cm", '{{ product["variants"].last["title"] }}', assigns)
assert_equal(5, @context['products["count"]'])
assert_equal('deepsnow', @context['products["tags"][0]'])
assert_equal('deepsnow', @context['products["tags"].first'])
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
assert_equal('element151cm', @context['product["variants"][1]["title"]'])
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
assert_equal('element151cm', @context['product["variants"].last["title"]'])
end
def test_access_variable_with_hash_notation
assert_template_result('baz', '{{ ["foo"] }}', { "foo" => "baz" })
assert_template_result('baz', '{{ [bar] }}', { 'foo' => 'baz', 'bar' => 'foo' })
@context['foo'] = 'baz'
@context['bar'] = 'foo'
assert_equal('baz', @context['["foo"]'])
assert_equal('baz', @context['[bar]'])
end
def test_access_hashes_with_hash_access_variables
assigns = {
'var' => 'tags',
'nested' => { 'var' => 'tags' },
'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] },
}
@context['var'] = 'tags'
@context['nested'] = { 'var' => 'tags' }
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
assert_template_result('deepsnow', '{{ products[var].first }}', assigns)
assert_template_result('freestyle', '{{ products[nested.var].last }}', assigns)
assert_equal('deepsnow', @context['products[var].first'])
assert_equal('freestyle', @context['products[nested.var].last'])
end
def test_hash_notation_only_for_hash_access
assigns = { "array" => [1, 2, 3, 4, 5] }
assert_template_result("1", "{{ array.first }}", assigns)
assert_template_result("pass", '{% if array["first"] == nil %}pass{% endif %}', assigns)
@context['array'] = [1, 2, 3, 4, 5]
@context['hash'] = { 'first' => 'Hello' }
assert_template_result("Hello", '{{ hash["first"] }}', { "hash" => { "first" => "Hello" } })
assert_equal(1, @context['array.first'])
assert_nil(@context['array["first"]'])
assert_equal('Hello', @context['hash["first"]'])
end
def test_first_can_appear_in_middle_of_callchain
assigns = { "product" => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } }
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
assert_template_result('draft151cm', '{{ product.variants[0].title }}', assigns)
assert_template_result('element151cm', '{{ product.variants[1].title }}', assigns)
assert_template_result('draft151cm', '{{ product.variants.first.title }}', assigns)
assert_template_result('element151cm', '{{ product.variants.last.title }}', assigns)
assert_equal('draft151cm', @context['product.variants[0].title'])
assert_equal('element151cm', @context['product.variants[1].title'])
assert_equal('draft151cm', @context['product.variants.first.title'])
assert_equal('element151cm', @context['product.variants.last.title'])
end
def test_cents
@@ -338,12 +351,10 @@ class ContextTest < Minitest::Test
end
def test_ranges
assert_template_result("1..5", '{{ (1..5) }}')
assert_template_result("pass", '{% if (1..5) == expect %}pass{% endif %}', { "expect" => (1..5) })
assigns = { "test" => '5' }
assert_template_result("1..5", "{{ (1..test) }}", assigns)
assert_template_result("5..5", "{{ (test..test) }}", assigns)
@context.merge("test" => '5')
assert_equal((1..5), @context['(1..5)'])
assert_equal((1..5), @context['(1..test)'])
assert_equal((5..5), @context['(test..test)'])
end
def test_cents_through_drop_nestedly
@@ -518,7 +529,7 @@ class ContextTest < Minitest::Test
registers = {
my_register: :my_value,
}
super_context = Context.new({}, {}, Registers.new(registers))
super_context = Context.new({}, {}, StaticRegisters.new(registers))
super_context.registers[:my_register] = :my_alt_value
subcontext = super_context.new_isolated_subcontext
assert_equal(:my_value, subcontext.registers[:my_register])
@@ -607,20 +618,6 @@ class ContextTest < Minitest::Test
end
end
def test_context_always_uses_static_registers
registers = {
my_register: :my_value,
}
c = Context.new({}, {}, registers)
assert_instance_of(Registers, c.registers)
assert_equal(:my_value, c.registers[:my_register])
r = Registers.new(registers)
c = Context.new({}, {}, r)
assert_instance_of(Registers, c.registers)
assert_equal(:my_value, c.registers[:my_register])
end
private
def assert_no_object_allocations
+8 -4
View File
@@ -6,12 +6,16 @@ class DocumentTest < Minitest::Test
include Liquid
def test_unexpected_outer_tag
source = "{% else %}"
assert_match_syntax_error("Liquid syntax error (line 1): Unexpected outer 'else' tag", source)
exc = assert_raises(SyntaxError) do
Template.parse("{% else %}")
end
assert_equal(exc.message, "Liquid syntax error: Unexpected outer 'else' tag")
end
def test_unknown_tag
source = "{% foo %}"
assert_match_syntax_error("Liquid syntax error (line 1): Unknown tag 'foo'", source)
exc = assert_raises(SyntaxError) do
Template.parse("{% foo %}")
end
assert_equal(exc.message, "Liquid syntax error: Unknown tag 'foo'")
end
end
+33 -21
View File
@@ -63,7 +63,9 @@ class ErrorHandlingTest < Minitest::Test
end
def test_missing_endtag_parse_time_error
assert_match_syntax_error(/: 'for' tag was never closed\z/, ' {% for a in b %} ... ')
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse(' {% for a in b %} ... ')
end
end
def test_unrecognized_operator
@@ -82,26 +84,33 @@ class ErrorHandlingTest < Minitest::Test
end
def test_with_line_numbers_adds_numbers_to_parser_errors
source = <<~LIQUID
foobar
err = assert_raises(SyntaxError) do
Liquid::Template.parse('
foobar
{% "cat" | foobar %}
{% "cat" | foobar %}
bla
LIQUID
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source)
bla
',
line_numbers: true)
end
assert_match(/Liquid syntax error \(line 4\)/, err.message)
end
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
source = <<~LIQUID
foobar
err = assert_raises(SyntaxError) do
Liquid::Template.parse('
foobar
{%- "cat" | foobar -%}
{%- "cat" | foobar -%}
bla
LIQUID
bla
',
line_numbers: true)
end
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source)
assert_match(/Liquid syntax error \(line 4\)/, err.message)
end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
@@ -136,17 +145,20 @@ class ErrorHandlingTest < Minitest::Test
end
def test_syntax_errors_in_nested_blocks_have_correct_line_number
source = <<~LIQUID
foobar
err = assert_raises(SyntaxError) do
Liquid::Template.parse('
foobar
{% if 1 != 2 %}
{% foo %}
{% endif %}
{% if 1 != 2 %}
{% foo %}
{% endif %}
bla
LIQUID
bla
',
line_numbers: true)
end
assert_match_syntax_error("Liquid syntax error (line 4): Unknown tag 'foo'", source)
assert_equal("Liquid syntax error (line 5): Unknown tag 'foo'", err.message)
end
def test_strict_error_messages
+22 -25
View File
@@ -4,46 +4,43 @@ require 'test_helper'
class ExpressionTest < Minitest::Test
def test_keyword_literals
assert_template_result("true", "{{ true }}")
assert_expression_result(true, "true")
assert_equal(true, parse_and_eval("true"))
assert_equal(true, parse_and_eval(" true "))
end
def test_string
assert_template_result("single quoted", "{{'single quoted'}}")
assert_template_result("double quoted", '{{"double quoted"}}')
assert_template_result("spaced", "{{ 'spaced' }}")
assert_template_result("spaced2", "{{ 'spaced2' }}")
assert_equal("single quoted", parse_and_eval("'single quoted'"))
assert_equal("double quoted", parse_and_eval('"double quoted"'))
assert_equal("spaced", parse_and_eval(" 'spaced' "))
assert_equal("spaced2", parse_and_eval(' "spaced2" '))
end
def test_int
assert_template_result("456", "{{ 456 }}")
assert_expression_result(123, "123")
assert_expression_result(12, "012")
assert_equal(123, parse_and_eval("123"))
assert_equal(456, parse_and_eval(" 456 "))
assert_equal(12, parse_and_eval("012"))
end
def test_float
assert_template_result("2.5", "{{ 2.5 }}")
assert_expression_result(1.5, "1.5")
assert_equal(1.5, parse_and_eval("1.5"))
assert_equal(2.5, parse_and_eval(" 2.5 "))
end
def test_range
assert_template_result("3..4", "{{ ( 3 .. 4 ) }}")
assert_expression_result(1..2, "(1..2)")
assert_match_syntax_error(
"Liquid syntax error (line 1): Invalid expression type 'false' in range expression",
"{{ (false..true) }}"
)
assert_match_syntax_error(
"Liquid syntax error (line 1): Invalid expression type '(1..2)' in range expression",
"{{ ((1..2)..3) }}"
)
assert_equal(1..2, parse_and_eval("(1..2)"))
assert_equal(3..4, parse_and_eval(" ( 3 .. 4 ) "))
end
private
def assert_expression_result(expect, markup, **assigns)
liquid = "{% if expect == #{markup} %}pass{% else %}got {{ #{markup} }}{% endif %}"
assert_template_result("pass", liquid, { "expect" => expect, **assigns })
def parse_and_eval(markup, **assigns)
if Liquid::Template.error_mode == :strict
p = Liquid::Parser.new(markup)
markup = p.expression
p.consume(:end_of_string)
end
expression = Liquid::Expression.parse(markup)
context = Liquid::Context.new(assigns)
context.evaluate(expression)
end
end
+42 -24
View File
@@ -59,66 +59,84 @@ class FiltersTest < Minitest::Test
end
def test_size
assert_template_result("4", "{{var | size}}", { "var" => 'abcd' })
@context['var'] = 'abcd'
@context.add_filters(MoneyFilter)
assert_equal('4', Template.parse("{{var | size}}").render(@context))
end
def test_join
assert_template_result("1 2 3 4", "{{var | join}}", { "var" => [1, 2, 3, 4] })
@context['var'] = [1, 2, 3, 4]
assert_equal("1 2 3 4", Template.parse("{{var | join}}").render(@context))
end
def test_sort
assert_template_result("1 2 3 4", "{{numbers | sort | join}}", { "numbers" => [2, 1, 4, 3] })
assert_template_result("alphabetic as expected", "{{words | sort | join}}",
{ "words" => ['expected', 'as', 'alphabetic'] })
assert_template_result("3", "{{value | sort}}", { "value" => 3 })
assert_template_result('are flower', "{{arrays | sort | join}}", { 'arrays' => ['flower', 'are'] })
assert_template_result("Expected case sensitive", "{{case_sensitive | sort | join}}",
{ "case_sensitive" => ["sensitive", "Expected", "case"] })
@context['value'] = 3
@context['numbers'] = [2, 1, 4, 3]
@context['words'] = ['expected', 'as', 'alphabetic']
@context['arrays'] = ['flower', 'are']
@context['case_sensitive'] = ['sensitive', 'Expected', 'case']
assert_equal('1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context))
assert_equal('alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context))
assert_equal('3', Template.parse("{{value | sort}}").render(@context))
assert_equal('are flower', Template.parse("{{arrays | sort | join}}").render(@context))
assert_equal('Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context))
end
def test_sort_natural
@context['words'] = ['case', 'Assert', 'Insensitive']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
# Test strings
assert_template_result("Assert case Insensitive", "{{words | sort_natural | join}}",
{ "words" => ["case", "Assert", "Insensitive"] })
assert_equal('Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context))
# Test hashes
assert_template_result("A b C", "{{hashes | sort_natural: 'a' | map: 'a' | join}}",
{ "hashes" => [{ "a" => "A" }, { "a" => "b" }, { "a" => "C" }] })
assert_equal('A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context))
# Test objects
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context))
end
def test_compact
@context['words'] = ['a', nil, 'b', nil, 'c']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
# Test strings
assert_template_result("a b c", "{{words | compact | join}}",
{ "words" => ['a', nil, 'b', nil, 'c'] })
assert_equal('a b c', Template.parse("{{words | compact | join}}").render(@context))
# Test hashes
assert_template_result("A C", "{{hashes | compact: 'a' | map: 'a' | join}}",
{ "hashes" => [{ "a" => "A" }, { "a" => nil }, { "a" => "C" }] })
assert_equal('A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context))
# Test objects
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context))
end
def test_strip_html
assert_template_result("bla blub", "{{ var | strip_html }}", { "var" => "<b>bla blub</a>" })
@context['var'] = "<b>bla blub</a>"
assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
end
def test_strip_html_ignore_comments_with_html
assert_template_result("bla blub", "{{ var | strip_html }}",
{ "var" => "<!-- split and some <ul> tag --><b>bla blub</a>" })
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
end
def test_capitalize
assert_template_result("Blub", "{{ var | capitalize }}", { "var" => "blub" })
@context['var'] = "blub"
assert_equal("Blub", Template.parse("{{ var | capitalize }}").render(@context))
end
def test_nonexistent_filter_is_ignored
assert_template_result("1000", "{{ var | xyzzy }}", { "var" => 1000 })
@context['var'] = 1000
assert_equal('1000', Template.parse("{{ var | xyzzy }}").render(@context))
end
def test_filter_with_keyword_arguments
+14 -9
View File
@@ -33,25 +33,31 @@ class OutputTest < Minitest::Test
def setup
@assigns = {
'best_cars' => 'bmw',
'car' => { 'bmw' => 'good', 'gm' => 'bad' },
}
end
def test_variable
assert_template_result(" bmw ", " {{best_cars}} ", { "best_cars" => "bmw" })
text = %( {{best_cars}} )
expected = %( bmw )
assert_equal(expected, Template.parse(text).render!(@assigns))
end
def test_variable_traversing_with_two_brackets
source = "{{ site.data.menu[include.menu][include.locale] }}"
assert_template_result("it works!", source, {
text = %({{ site.data.menu[include.menu][include.locale] }})
assert_equal("it works!", Template.parse(text).render!(
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
"include" => { "menu" => "foo", "locale" => "bar" },
})
"include" => { "menu" => "foo", "locale" => "bar" }
))
end
def test_variable_traversing
source = " {{car.bmw}} {{car.gm}} {{car.bmw}} "
assert_template_result(" good bad good ", source, @assigns)
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )
expected = %( good bad good )
assert_equal(expected, Template.parse(text).render!(@assigns))
end
def test_variable_piping
@@ -104,11 +110,10 @@ class OutputTest < Minitest::Test
end
def test_multiple_pipings
assigns = { 'best_cars' => 'bmw' }
text = %( {{ best_cars | cite_funny | paragraph }} )
expected = %( <p>LOL: bmw</p> )
assert_equal(expected, Template.parse(text).render!(assigns, filters: [FunnyFilter]))
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end
def test_link_to
+1 -1
View File
@@ -129,6 +129,6 @@ class ParsingQuirksTest < Minitest::Test
end
def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', { 'containsallshipments' => true })
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
end
end # ParsingQuirksTest
+53 -62
View File
@@ -32,7 +32,7 @@ class TestDrop < Liquid::Drop
attr_reader :value
def registers
{ @value => @context.registers[@value] }
@context.registers
end
end
@@ -109,10 +109,6 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::ArgumentError) do
@filters.slice('foobar', 0, "")
end
assert_equal("", @filters.slice("foobar", 0, -(1 << 64)))
assert_equal("foobar", @filters.slice("foobar", 0, 1 << 63))
assert_equal("", @filters.slice("foobar", 1 << 63, 6))
assert_equal("", @filters.slice("foobar", -(1 << 63), 6))
end
def test_slice_on_arrays
@@ -127,10 +123,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal(%w(r), @filters.slice(input, -1))
assert_equal(%w(), @filters.slice(input, 100, 10))
assert_equal(%w(), @filters.slice(input, -100, 10))
assert_equal([], @filters.slice(input, 0, -(1 << 64)))
assert_equal(input, @filters.slice(input, 0, 1 << 63))
assert_equal([], @filters.slice(input, 1 << 63, 6))
assert_equal([], @filters.slice(input, -(1 << 63), 6))
end
def test_truncate
@@ -140,8 +132,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal('1234567890', @filters.truncate('1234567890'))
assert_equal("测试...", @filters.truncate("测试测试测试测试", 5))
assert_equal('12341', @filters.truncate("1234567890", 5, 1))
assert_equal("foobar", @filters.truncate("foobar", 1 << 63))
assert_equal("...", @filters.truncate("foobar", -(1 << 63)))
end
def test_split
@@ -237,8 +227,10 @@ class StandardFiltersTest < Minitest::Test
assert_equal('one two three...', @filters.truncatewords("one two\tthree\nfour", 3))
assert_equal('one two...', @filters.truncatewords("one two three four", 2))
assert_equal('one...', @filters.truncatewords("one two three four", 0))
assert_equal('one two three four', @filters.truncatewords("one two three four", 1 << 31))
assert_equal('one...', @filters.truncatewords("one two three four", -(1 << 32)))
exception = assert_raises(Liquid::ArgumentError) do
@filters.truncatewords("one two three four", 1 << 31)
end
assert_equal("Liquid error: integer #{1 << 31} too big for truncatewords", exception.message)
end
def test_strip_html
@@ -434,7 +426,7 @@ class StandardFiltersTest < Minitest::Test
def test_map
assert_equal([1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a'))
assert_template_result('abc', "{{ ary | map:'foo' | map:'bar' }}",
{ 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }] })
'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }])
end
def test_map_doesnt_call_arbitrary_stuff
@@ -444,11 +436,11 @@ class StandardFiltersTest < Minitest::Test
def test_map_calls_to_liquid
t = TestThing.new
assert_template_result("woot: 1", '{{ foo | map: "whatever" }}', { "foo" => [t] })
assert_template_result("woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t])
end
def test_map_calls_context=
model = TestModel.new(value: :test)
model = TestModel.new(value: "test")
template = Template.parse('{{ foo | map: "registers" }}')
template.registers[:test] = 1234
@@ -459,13 +451,13 @@ class StandardFiltersTest < Minitest::Test
def test_map_on_hashes
assert_template_result("4217", '{{ thing | map: "foo" | map: "bar" }}',
{ "thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] } })
"thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] })
end
def test_legacy_map_on_hashes_with_dynamic_key
template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}"
hash = { "foo" => { "bar" => 42 } }
assert_template_result("42", template, { "thing" => hash })
assert_template_result("42", template, "thing" => hash)
end
def test_sort_calls_to_liquid
@@ -477,8 +469,8 @@ class StandardFiltersTest < Minitest::Test
def test_map_over_proc
drop = TestDrop.new(value: "testfoo")
p = proc { drop }
output = Liquid::Template.parse('{{ procs | map: "value" }}').render!({ "procs" => [p] })
assert_equal("testfoo", output)
templ = '{{ procs | map: "value" }}'
assert_template_result("testfoo", templ, "procs" => [p])
end
def test_map_over_drops_returning_procs
@@ -490,13 +482,12 @@ class StandardFiltersTest < Minitest::Test
"proc" => -> { "bar" },
},
]
output = Liquid::Template.parse('{{ drops | map: "proc" }}').render!({ "drops" => drops })
assert_equal("foobar", output)
templ = '{{ drops | map: "proc" }}'
assert_template_result("foobar", templ, "drops" => drops)
end
def test_map_works_on_enumerables
output = Liquid::Template.parse('{{ foo | map: "foo" }}').render!({ "foo" => TestEnumerable.new })
assert_equal("123", output)
assert_template_result("123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new)
end
def test_map_returns_empty_on_2d_input_array
@@ -523,16 +514,16 @@ class StandardFiltersTest < Minitest::Test
end
def test_sort_works_on_enumerables
assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', { "foo" => TestEnumerable.new })
assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new)
end
def test_first_and_last_call_to_liquid
assert_template_result('foobar', '{{ foo | first }}', { 'foo' => [ThingWithToLiquid.new] })
assert_template_result('foobar', '{{ foo | last }}', { 'foo' => [ThingWithToLiquid.new] })
assert_template_result('foobar', '{{ foo | first }}', 'foo' => [ThingWithToLiquid.new])
assert_template_result('foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new])
end
def test_truncate_calls_to_liquid
assert_template_result("wo...", '{{ foo | truncate: 5 }}', { "foo" => TestThing.new })
assert_template_result("wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new)
end
def test_date
@@ -606,42 +597,42 @@ class StandardFiltersTest < Minitest::Test
end
def test_strip
assert_template_result('ab c', "{{ source | strip }}", { 'source' => " ab c " })
assert_template_result('ab c', "{{ source | strip }}", { 'source' => " \tab c \n \t" })
assert_template_result('ab c', "{{ source | strip }}", 'source' => " ab c ")
assert_template_result('ab c', "{{ source | strip }}", 'source' => " \tab c \n \t")
end
def test_lstrip
assert_template_result('ab c ', "{{ source | lstrip }}", { 'source' => " ab c " })
assert_template_result("ab c \n \t", "{{ source | lstrip }}", { 'source' => " \tab c \n \t" })
assert_template_result('ab c ', "{{ source | lstrip }}", 'source' => " ab c ")
assert_template_result("ab c \n \t", "{{ source | lstrip }}", 'source' => " \tab c \n \t")
end
def test_rstrip
assert_template_result(" ab c", "{{ source | rstrip }}", { 'source' => " ab c " })
assert_template_result(" \tab c", "{{ source | rstrip }}", { 'source' => " \tab c \n \t" })
assert_template_result(" ab c", "{{ source | rstrip }}", 'source' => " ab c ")
assert_template_result(" \tab c", "{{ source | rstrip }}", 'source' => " \tab c \n \t")
end
def test_strip_newlines
assert_template_result('abc', "{{ source | strip_newlines }}", { 'source' => "a\nb\nc" })
assert_template_result('abc', "{{ source | strip_newlines }}", { 'source' => "a\r\nb\nc" })
assert_template_result('abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc")
assert_template_result('abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc")
end
def test_newlines_to_br
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", { 'source' => "a\nb\nc" })
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", { 'source' => "a\r\nb\nc" })
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc")
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\r\nb\nc")
end
def test_plus
assert_template_result("2", "{{ 1 | plus:1 }}")
assert_template_result("2.0", "{{ '1' | plus:'1.0' }}")
assert_template_result("5", "{{ price | plus:'2' }}", { 'price' => NumberLikeThing.new(3) })
assert_template_result("5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3))
end
def test_minus
assert_template_result("4", "{{ input | minus:operand }}", { 'input' => 5, 'operand' => 1 })
assert_template_result("4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1)
assert_template_result("2.3", "{{ '4.3' | minus:'2' }}")
assert_template_result("5", "{{ price | minus:'2' }}", { 'price' => NumberLikeThing.new(7) })
assert_template_result("5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7))
end
def test_abs
@@ -664,7 +655,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("7.25", "{{ 0.0725 | times:100 }}")
assert_template_result("-7.25", '{{ "-0.0725" | times:100 }}')
assert_template_result("7.25", '{{ "-0.0725" | times: -100 }}')
assert_template_result("4", "{{ price | times:2 }}", { 'price' => NumberLikeThing.new(2) })
assert_template_result("4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2))
end
def test_divided_by
@@ -679,7 +670,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("4", "{{ 1 | modulo: 0 }}")
end
assert_template_result("5", "{{ price | divided_by:2 }}", { 'price' => NumberLikeThing.new(10) })
assert_template_result("5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10))
end
def test_modulo
@@ -688,39 +679,39 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("4", "{{ 1 | modulo: 0 }}")
end
assert_template_result("1", "{{ price | modulo:2 }}", { 'price' => NumberLikeThing.new(3) })
assert_template_result("1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3))
end
def test_round
assert_template_result("5", "{{ input | round }}", { 'input' => 4.6 })
assert_template_result("5", "{{ input | round }}", 'input' => 4.6)
assert_template_result("4", "{{ '4.3' | round }}")
assert_template_result("4.56", "{{ input | round: 2 }}", { 'input' => 4.5612 })
assert_template_result("4.56", "{{ input | round: 2 }}", 'input' => 4.5612)
assert_raises(Liquid::FloatDomainError) do
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | round }}")
end
assert_template_result("5", "{{ price | round }}", { 'price' => NumberLikeThing.new(4.6) })
assert_template_result("4", "{{ price | round }}", { 'price' => NumberLikeThing.new(4.3) })
assert_template_result("5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6))
assert_template_result("4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3))
end
def test_ceil
assert_template_result("5", "{{ input | ceil }}", { 'input' => 4.6 })
assert_template_result("5", "{{ input | ceil }}", 'input' => 4.6)
assert_template_result("5", "{{ '4.3' | ceil }}")
assert_raises(Liquid::FloatDomainError) do
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | ceil }}")
end
assert_template_result("5", "{{ price | ceil }}", { 'price' => NumberLikeThing.new(4.6) })
assert_template_result("5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6))
end
def test_floor
assert_template_result("4", "{{ input | floor }}", { 'input' => 4.6 })
assert_template_result("4", "{{ input | floor }}", 'input' => 4.6)
assert_template_result("4", "{{ '4.3' | floor }}")
assert_raises(Liquid::FloatDomainError) do
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | floor }}")
end
assert_template_result("5", "{{ price | floor }}", { 'price' => NumberLikeThing.new(5.4) })
assert_template_result("5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4))
end
def test_at_most
@@ -729,9 +720,9 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("5", "{{ 5 | at_most:6 }}")
assert_template_result("4.5", "{{ 4.5 | at_most:5 }}")
assert_template_result("5", "{{ width | at_most:5 }}", { 'width' => NumberLikeThing.new(6) })
assert_template_result("4", "{{ width | at_most:5 }}", { 'width' => NumberLikeThing.new(4) })
assert_template_result("4", "{{ 5 | at_most: width }}", { 'width' => NumberLikeThing.new(4) })
assert_template_result("5", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(6))
assert_template_result("4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4))
assert_template_result("4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4))
end
def test_at_least
@@ -740,9 +731,9 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("6", "{{ 5 | at_least:6 }}")
assert_template_result("5", "{{ 4.5 | at_least:5 }}")
assert_template_result("6", "{{ width | at_least:5 }}", { 'width' => NumberLikeThing.new(6) })
assert_template_result("5", "{{ width | at_least:5 }}", { 'width' => NumberLikeThing.new(4) })
assert_template_result("6", "{{ 5 | at_least: width }}", { 'width' => NumberLikeThing.new(6) })
assert_template_result("6", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(6))
assert_template_result("5", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(4))
assert_template_result("6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6))
end
def test_append
@@ -775,8 +766,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal("bar", @filters.default([], "bar"))
assert_equal("bar", @filters.default({}, "bar"))
assert_template_result('bar', "{{ false | default: 'bar' }}")
assert_template_result('bar', "{{ drop | default: 'bar' }}", { 'drop' => BooleanDrop.new(false) })
assert_template_result('Yay', "{{ drop | default: 'bar' }}", { 'drop' => BooleanDrop.new(true) })
assert_template_result('bar', "{{ drop | default: 'bar' }}", 'drop' => BooleanDrop.new(false))
assert_template_result('Yay', "{{ drop | default: 'bar' }}", 'drop' => BooleanDrop.new(true))
end
def test_default_handle_false
@@ -787,8 +778,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal("bar", @filters.default([], "bar", "allow_false" => true))
assert_equal("bar", @filters.default({}, "bar", "allow_false" => true))
assert_template_result('false', "{{ false | default: 'bar', allow_false: true }}")
assert_template_result('Nay', "{{ drop | default: 'bar', allow_false: true }}", { 'drop' => BooleanDrop.new(false) })
assert_template_result('Yay', "{{ drop | default: 'bar', allow_false: true }}", { 'drop' => BooleanDrop.new(true) })
assert_template_result('Nay', "{{ drop | default: 'bar', allow_false: true }}", 'drop' => BooleanDrop.new(false))
assert_template_result('Yay', "{{ drop | default: 'bar', allow_false: true }}", 'drop' => BooleanDrop.new(true))
end
def test_cannot_access_private_methods
+2 -2
View File
@@ -9,8 +9,8 @@ class BreakTagTest < Minitest::Test
# block
def test_break_with_no_block
assigns = { 'i' => 1 }
markup = 'before{% break %}after'
expected = 'before'
markup = '{% break %}'
expected = ''
assert_template_result(expected, markup, assigns)
end
+1 -1
View File
@@ -6,7 +6,7 @@ class EchoTest < Minitest::Test
include Liquid
def test_echo_outputs_its_input
assert_template_result('BAR', <<~LIQUID, { 'variable-name' => 'bar' })
assert_template_result('BAR', <<~LIQUID, 'variable-name' => 'bar')
{%- echo variable-name | upcase -%}
LIQUID
end
+50 -37
View File
@@ -12,10 +12,10 @@ class ForTagTest < Minitest::Test
include Liquid
def test_for
assert_template_result(' yo yo yo yo ', '{%for item in array%} yo {%endfor%}', { 'array' => [1, 2, 3, 4] })
assert_template_result('yoyo', '{%for item in array%}yo{%endfor%}', { 'array' => [1, 2] })
assert_template_result(' yo ', '{%for item in array%} yo {%endfor%}', { 'array' => [1] })
assert_template_result('', '{%for item in array%}{%endfor%}', { 'array' => [1, 2] })
assert_template_result(' yo yo yo yo ', '{%for item in array%} yo {%endfor%}', 'array' => [1, 2, 3, 4])
assert_template_result('yoyo', '{%for item in array%}yo{%endfor%}', 'array' => [1, 2])
assert_template_result(' yo ', '{%for item in array%} yo {%endfor%}', 'array' => [1])
assert_template_result('', '{%for item in array%}{%endfor%}', 'array' => [1, 2])
expected = <<HERE
yo
@@ -30,7 +30,7 @@ HERE
yo
{%endfor%}
HERE
assert_template_result(expected, template, { 'array' => [1, 2, 3] })
assert_template_result(expected, template, 'array' => [1, 2, 3])
end
def test_for_reversed
@@ -45,30 +45,30 @@ HERE
Template.parse('{% for i in (a..2) %}{% endfor %}').render!("a" => [1, 2])
end
assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', { "a" => "invalid integer" })
assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', "a" => "invalid integer")
end
def test_for_with_variable_range
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', { "foobar" => 3 })
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3)
end
def test_for_with_hash_value_range
foobar = { "value" => 3 }
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', { "foobar" => foobar })
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
end
def test_for_with_drop_value_range
foobar = ThingWithValue.new
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', { "foobar" => foobar })
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
end
def test_for_with_variable
assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', { 'array' => [1, 2, 3] })
assert_template_result('123', '{%for item in array%}{{item}}{%endfor%}', { 'array' => [1, 2, 3] })
assert_template_result('123', '{% for item in array %}{{item}}{% endfor %}', { 'array' => [1, 2, 3] })
assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', { 'array' => ['a', 'b', 'c', 'd'] })
assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', { 'array' => ['a', ' ', 'b', ' ', 'c'] })
assert_template_result('abc', '{%for item in array%}{{item}}{%endfor%}', { 'array' => ['a', '', 'b', '', 'c'] })
assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', 'array' => [1, 2, 3])
assert_template_result('123', '{%for item in array%}{{item}}{%endfor%}', 'array' => [1, 2, 3])
assert_template_result('123', '{% for item in array %}{{item}}{% endfor %}', 'array' => [1, 2, 3])
assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', 'b', 'c', 'd'])
assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', ' ', 'b', ' ', 'c'])
assert_template_result('abc', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', '', 'b', '', 'c'])
end
def test_for_helpers
@@ -92,9 +92,9 @@ HERE
end
def test_for_else
assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', { 'array' => [1, 2, 3] })
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', { 'array' => [] })
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', { 'array' => nil })
assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => [1, 2, 3])
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => [])
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => nil)
end
def test_limiting
@@ -263,19 +263,6 @@ HERE
assert_template_result(expected, markup, assigns)
end
def test_for_with_break_after_nested_loop
source = <<~LIQUID.chomp
{% for i in (1..2) -%}
{% for j in (1..2) -%}
{{ i }}-{{ j }},
{%- endfor -%}
{% break -%}
{% endfor -%}
after
LIQUID
assert_template_result("1-1,1-2,after", source)
end
def test_for_with_continue
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
@@ -326,11 +313,11 @@ HERE
assert_template_result('test string',
'{%for val in string%}{{val}}{%endfor%}',
{ 'string' => "test string" })
'string' => "test string")
assert_template_result('test string',
'{%for val in string limit:1%}{{val}}{%endfor%}',
{ 'string' => "test string" })
'string' => "test string")
assert_template_result('val-string-1-1-0-1-0-true-true-test string',
'{%for val in string%}' \
@@ -343,7 +330,7 @@ HERE
'{{forloop.first}}-' \
'{{forloop.last}}-' \
'{{val}}{%endfor%}',
{ 'string' => "test string" })
'string' => "test string")
end
def test_for_parentloop_references_parent_loop
@@ -351,7 +338,7 @@ HERE
'{% for inner in outer %}{% for k in inner %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}{% endfor %}',
{ 'outer' => [[1, 1, 1], [1, 1, 1]] })
'outer' => [[1, 1, 1], [1, 1, 1]])
end
def test_for_parentloop_nil_when_not_present
@@ -359,7 +346,7 @@ HERE
'{% for inner in outer %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}',
{ 'outer' => [[1, 1, 1], [1, 1, 1]] })
'outer' => [[1, 1, 1], [1, 1, 1]])
end
def test_inner_for_over_empty_input
@@ -367,7 +354,7 @@ HERE
end
def test_blank_string_not_iterable
assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", { 'characters' => '' })
assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '')
end
def test_bad_variable_naming_in_for_loop
@@ -450,4 +437,30 @@ HERE
assert(context.registers[:for_stack].empty?)
end
def test_instrument_for_offset_continue
assert_usage_increment('for_offset_continue') do
Template.parse('{% for item in items offset:continue %}{{item}}{% endfor %}')
end
assert_usage_increment('for_offset_continue', times: 0) do
Template.parse('{% for item in items offset:2 %}{{item}}{% endfor %}')
end
end
def test_instrument_forloop_drop_name
assigns = { 'items' => [1, 2, 3, 4, 5] }
assert_usage_increment('forloop_drop_name', times: 5) do
Template.parse('{% for item in items %}{{forloop.name}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{forloop.index}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{item}}{% endfor %}').render!(assigns)
end
end
end
+38 -38
View File
@@ -24,23 +24,23 @@ class IfElseTagTest < Minitest::Test
end
def test_if_boolean
assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => true })
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true)
end
def test_if_or
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', { 'a' => true, 'b' => true })
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', { 'a' => true, 'b' => false })
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', { 'a' => false, 'b' => true })
assert_template_result('', '{% if a or b %} YES {% endif %}', { 'a' => false, 'b' => false })
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => true)
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => false)
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => true)
assert_template_result('', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => false)
assert_template_result(' YES ', '{% if a or b or c %} YES {% endif %}', { 'a' => false, 'b' => false, 'c' => true })
assert_template_result('', '{% if a or b or c %} YES {% endif %}', { 'a' => false, 'b' => false, 'c' => false })
assert_template_result(' YES ', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => true)
assert_template_result('', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => false)
end
def test_if_or_with_operators
assert_template_result(' YES ', '{% if a == true or b == true %} YES {% endif %}', { 'a' => true, 'b' => true })
assert_template_result(' YES ', '{% if a == true or b == false %} YES {% endif %}', { 'a' => true, 'b' => true })
assert_template_result('', '{% if a == false or b == false %} YES {% endif %}', { 'a' => true, 'b' => true })
assert_template_result(' YES ', '{% if a == true or b == true %} YES {% endif %}', 'a' => true, 'b' => true)
assert_template_result(' YES ', '{% if a == true or b == false %} YES {% endif %}', 'a' => true, 'b' => true)
assert_template_result('', '{% if a == false or b == false %} YES {% endif %}', 'a' => true, 'b' => true)
end
def test_comparison_of_strings_containing_and_or_or
@@ -66,40 +66,40 @@ class IfElseTagTest < Minitest::Test
end
def test_hash_miss_generates_false
assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => {} })
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => {})
end
def test_if_from_variable
assert_template_result('', '{% if var %} NO {% endif %}', { 'var' => false })
assert_template_result('', '{% if var %} NO {% endif %}', { 'var' => nil })
assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => { 'bar' => false } })
assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => {} })
assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => nil })
assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => true })
assert_template_result('', '{% if var %} NO {% endif %}', 'var' => false)
assert_template_result('', '{% if var %} NO {% endif %}', 'var' => nil)
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => { 'bar' => false })
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => {})
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => nil)
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => true)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => "text" })
assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => true })
assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => 1 })
assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => {} })
assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => [] })
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => "text")
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => 1)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => {})
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => [])
assert_template_result(' YES ', '{% if "foo" %} YES {% endif %}')
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => true } })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => "text" } })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => 1 } })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => {} } })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => [] } })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => true })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => "text" })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => 1 })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => {} })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => [] })
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', { 'var' => false })
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', { 'var' => nil })
assert_template_result(' YES ', '{% if var %} YES {% else %} NO {% endif %}', { 'var' => true })
assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', { 'var' => "text" })
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => false)
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => nil)
assert_template_result(' YES ', '{% if var %} YES {% else %} NO {% endif %}', 'var' => true)
assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text")
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', { 'foo' => { 'bar' => false } })
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', { 'foo' => { 'bar' => true } })
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', { 'foo' => { 'bar' => "text" } })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', { 'foo' => { 'notbar' => true } })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', { 'foo' => {} })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', { 'notfoo' => { 'bar' => true } })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'bar' => false })
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => true })
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => "text" })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'notbar' => true })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {})
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => { 'bar' => true })
end
def test_nested_if
@@ -184,7 +184,7 @@ class IfElseTagTest < Minitest::Test
tests.each do |vals, expected|
a, b, c = vals
assigns = { 'a' => a, 'b' => b, 'c' => c }
assert_template_result(expected.to_s, tpl, assigns, message: assigns.to_s)
assert_template_result(expected.to_s, tpl, assigns, assigns.to_s)
end
end
end
+67 -83
View File
@@ -3,13 +3,44 @@
require 'test_helper'
class TestFileSystem
PARTIALS = {
"nested_template" => "{% include 'header' %} {% include 'body' %} {% include 'footer' %}",
"body" => "body {% include 'body_detail' %}",
}
def read_template_file(template_path)
PARTIALS[template_path] || template_path
case template_path
when "product"
"Product: {{ product.title }} "
when "product_alias"
"Product: {{ product.title }} "
when "locale_variables"
"Locale: {{echo1}} {{echo2}}"
when "variant"
"Variant: {{ variant.title }}"
when "nested_template"
"{% include 'header' %} {% include 'body' %} {% include 'footer' %}"
when "body"
"body {% include 'body_detail' %}"
when "nested_product_template"
"Product: {{ nested_product_template.title }} {%include 'details'%} "
when "recursively_nested_template"
"-{% include 'recursively_nested_template' %}"
when "pick_a_source"
"from TestFileSystem"
when 'assignments'
"{% assign foo = 'bar' %}"
when 'break'
"{% break %}"
else
template_path
end
end
end
@@ -50,11 +81,7 @@ class IncludeTagTest < Minitest::Test
include Liquid
def setup
@default_file_system = Liquid::Template.file_system
end
def teardown
Liquid::Template.file_system = @default_file_system
Liquid::Template.file_system = TestFileSystem.new
end
def test_include_tag_looks_for_file_system_in_registers_first
@@ -64,86 +91,60 @@ class IncludeTagTest < Minitest::Test
def test_include_tag_with
assert_template_result("Product: Draft 151cm ",
"{% include 'product' with products[0] %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: { "product" => "Product: {{ product.title }} " })
"{% include 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_include_tag_with_alias
assert_template_result("Product: Draft 151cm ",
"{% include 'product_alias' with products[0] as product %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: { "product_alias" => "Product: {{ product.title }} " })
"{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_include_tag_for_alias
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% include 'product_alias' for products as product %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: { "product_alias" => "Product: {{ product.title }} " })
"{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_include_tag_with_default_name
assert_template_result("Product: Draft 151cm ",
"{% include 'product' %}", { "product" => { 'title' => 'Draft 151cm' } },
partials: { "product" => "Product: {{ product.title }} " })
"{% include 'product' %}", "product" => { 'title' => 'Draft 151cm' })
end
def test_include_tag_for
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% include 'product' for products %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: { "product" => "Product: {{ product.title }} " })
"{% include 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_include_tag_with_local_variables
assert_template_result("Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}",
partials: { "locale_variables" => "Locale: {{echo1}} {{echo2}}" })
assert_template_result("Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}")
end
def test_include_tag_with_multiple_local_variables
assert_template_result("Locale: test123 test321",
"{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}",
partials: { "locale_variables" => "Locale: {{echo1}} {{echo2}}" })
"{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}")
end
def test_include_tag_with_multiple_local_variables_from_context
assert_template_result("Locale: test123 test321",
"{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}",
{ 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' } },
partials: { "locale_variables" => "Locale: {{echo1}} {{echo2}}" })
'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' })
end
def test_included_templates_assigns_variables
assert_template_result("bar", "{% include 'assignments' %}{{ foo }}",
partials: { 'assignments' => "{% assign foo = 'bar' %}" })
assert_template_result("bar", "{% include 'assignments' %}{{ foo }}")
end
def test_nested_include_tag
partials = { "body" => "body {% include 'body_detail' %}", "body_detail" => "body_detail" }
assert_template_result("body body_detail", "{% include 'body' %}", partials: partials)
assert_template_result("body body_detail", "{% include 'body' %}")
partials = partials.merge({
"nested_template" => "{% include 'header' %} {% include 'body' %} {% include 'footer' %}",
"header" => "header",
"footer" => "footer",
})
assert_template_result("header body body_detail footer", "{% include 'nested_template' %}", partials: partials)
assert_template_result("header body body_detail footer", "{% include 'nested_template' %}")
end
def test_nested_include_with_variable
partials = {
"nested_product_template" => "Product: {{ nested_product_template.title }} {%include 'details'%} ",
"details" => "details",
}
assert_template_result("Product: Draft 151cm details ",
"{% include 'nested_product_template' with product %}", { "product" => { "title" => 'Draft 151cm' } },
partials: partials)
"{% include 'nested_product_template' with product %}", "product" => { "title" => 'Draft 151cm' })
assert_template_result("Product: Draft 151cm details Product: Element 155cm details ",
"{% include 'nested_product_template' for products %}", { "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }] },
partials: partials)
"{% include 'nested_product_template' for products %}", "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }])
end
def test_recursively_included_template_does_not_produce_endless_loop
@@ -161,15 +162,11 @@ class IncludeTagTest < Minitest::Test
end
def test_dynamically_choosen_template
assert_template_result("Test123", "{% include template %}", { "template" => 'Test123' },
partials: { "Test123" => "Test123" })
assert_template_result("Test321", "{% include template %}", { "template" => 'Test321' },
partials: { "Test321" => "Test321" })
assert_template_result("Test123", "{% include template %}", "template" => 'Test123')
assert_template_result("Test321", "{% include template %}", "template" => 'Test321')
assert_template_result("Product: Draft 151cm ", "{% include template for product %}",
{ "template" => 'product', 'product' => { 'title' => 'Draft 151cm' } },
partials: { "product" => "Product: {{ product.title }} " })
"template" => 'product', 'product' => { 'title' => 'Draft 151cm' })
end
def test_include_tag_caches_second_read_of_same_partial
@@ -191,8 +188,7 @@ class IncludeTagTest < Minitest::Test
end
def test_include_tag_within_if_statement
assert_template_result("foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}",
partials: { "foo_if_true" => "foo_if_true" })
assert_template_result("foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}")
end
def test_custom_include_tag
@@ -226,7 +222,6 @@ class IncludeTagTest < Minitest::Test
end
def test_passing_options_to_included_templates
Liquid::Template.file_system = TestFileSystem.new
assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}')
end
@@ -242,35 +237,25 @@ class IncludeTagTest < Minitest::Test
end
def test_render_raise_argument_error_when_template_is_undefined
assert_template_result("Liquid error (line 1): Argument error in tag 'include' - Illegal template name",
"{% include undefined_variable %}", render_errors: true)
assert_template_result("Liquid error (line 1): Argument error in tag 'include' - Illegal template name",
"{% include nil %}", render_errors: true)
end
def test_render_raise_argument_error_when_template_is_not_a_string
assert_template_result("Liquid error (line 1): Argument error in tag 'include' - Illegal template name",
"{% include 123 %}", render_errors: true)
assert_raises(Liquid::ArgumentError) do
template = Liquid::Template.parse('{% include undefined_variable %}')
template.render!
end
assert_raises(Liquid::ArgumentError) do
template = Liquid::Template.parse('{% include nil %}')
template.render!
end
end
def test_including_via_variable_value
assert_template_result("from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}",
partials: { "pick_a_source" => "from TestFileSystem" })
assert_template_result("from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}")
partials = { "product" => "Product: {{ product.title }} " }
assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page %}", "product" => { 'title' => 'Draft 151cm' })
assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page %}",
{ "product" => { 'title' => 'Draft 151cm' } },
partials: partials)
assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}",
{ "foo" => { 'title' => 'Draft 151cm' } },
partials: partials)
assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' })
end
def test_including_with_strict_variables
Liquid::Template.file_system = StubFileSystem.new({ "simple" => "simple" })
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
template.render(nil, strict_variables: true)
@@ -279,7 +264,6 @@ class IncludeTagTest < Minitest::Test
def test_break_through_include
assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}")
assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}",
partials: { 'break' => "{% break %}" })
assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}")
end
end # IncludeTagTest
+7 -8
View File
@@ -6,21 +6,20 @@ class IncrementTagTest < Minitest::Test
include Liquid
def test_inc
assert_template_result('0 1', '{%increment port %} {{ port }}')
assert_template_result(' 0 1 2', '{{port}} {%increment port %} {%increment port%} {{port}}')
assert_template_result('0', '{%increment port %}', {})
assert_template_result('0 1', '{%increment port %} {%increment port%}', {})
assert_template_result('0 0 1 2 1',
'{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%increment port%} ' \
'{%increment starboard %}')
'{%increment starboard %}', {})
end
def test_dec
assert_template_result('-1 -1', '{%decrement port %} {{ port }}', { 'port' => 10 })
assert_template_result(' -1 -2 -2', '{{port}} {%decrement port %} {%decrement port%} {{port}}')
assert_template_result('0 1 2 0 3 1 1 3',
'{%increment starboard %} {%increment starboard%} {%increment starboard%} ' \
assert_template_result('9', '{%decrement port %}', 'port' => 10)
assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {})
assert_template_result('1 5 2 2 5',
'{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%decrement port%} ' \
'{%decrement starboard %}')
'{%decrement starboard %}', 'port' => 1, 'starboard' => 5)
end
end
@@ -1,69 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class InlineCommentTest < Minitest::Test
include Liquid
def test_inline_comment_returns_nothing
assert_template_result('', '{%- # this is an inline comment -%}')
assert_template_result('', '{%-# this is an inline comment -%}')
assert_template_result('', '{% # this is an inline comment %}')
assert_template_result('', '{%# this is an inline comment %}')
end
def test_inline_comment_does_not_require_a_space_after_the_pound_sign
assert_template_result('', '{%#this is an inline comment%}')
end
def test_liquid_inline_comment_returns_nothing
assert_template_result('Hey there, how are you doing today?', <<~LIQUID)
{%- liquid
# This is how you'd write a block comment in a liquid tag.
# It looks a lot like what you'd have in ruby.
# You can use it as inline documentation in your
# liquid blocks to explain why you're doing something.
echo "Hey there, "
# It won't affect the output.
echo "how are you doing today?"
-%}
LIQUID
end
def test_inline_comment_can_be_written_on_multiple_lines
assert_template_result('', <<~LIQUID)
{%-
# That kind of block comment is also allowed.
# It would only be a stylistic difference.
# Much like JavaScript's /* */ comments and their
# leading * on new lines.
-%}
LIQUID
end
def test_inline_comment_multiple_pound_signs
assert_template_result('', <<~LIQUID)
{%- liquid
######################################
# We support comments like this too. #
######################################
-%}
LIQUID
end
def test_inline_comments_require_the_pound_sign_on_every_new_line
assert_match_syntax_error("Each line of comments must be prefixed by the '#' character", <<~LIQUID)
{%-
# some comment
echo 'hello world'
-%}
LIQUID
end
def test_inline_comment_does_not_support_nested_tags
assert_template_result(' -%}', "{%- # {% echo 'hello world' %} -%}")
end
end
+3 -3
View File
@@ -6,13 +6,13 @@ class LiquidTagTest < Minitest::Test
include Liquid
def test_liquid_tag
assert_template_result('1 2 3', <<~LIQUID, { 'array' => [1, 2, 3] })
assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3])
{%- liquid
echo array | join: " "
-%}
LIQUID
assert_template_result('1 2 3', <<~LIQUID, { 'array' => [1, 2, 3] })
assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3])
{%- liquid
for value in array
echo value
@@ -23,7 +23,7 @@ class LiquidTagTest < Minitest::Test
-%}
LIQUID
assert_template_result('4 8 12 6', <<~LIQUID, { 'array' => [1, 2, 3] })
assert_template_result('4 8 12 6', <<~LIQUID, 'array' => [1, 2, 3])
{%- liquid
for value in array
assign double_value = value | times: 2
+90 -86
View File
@@ -6,52 +6,53 @@ class RenderTagTest < Minitest::Test
include Liquid
def test_render_with_no_arguments
assert_template_result('rendered content', '{% render "source" %}',
partials: { 'source' => 'rendered content' })
Liquid::Template.file_system = StubFileSystem.new('source' => 'rendered content')
assert_template_result('rendered content', '{% render "source" %}')
end
def test_render_tag_looks_for_file_system_in_registers_first
assert_template_result('from register file system', '{% render "pick_a_source" %}',
partials: { 'pick_a_source' => 'from register file system' })
file_system = StubFileSystem.new('pick_a_source' => 'from register file system')
assert_equal('from register file system',
Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system }))
end
def test_render_passes_named_arguments_into_inner_scope
Liquid::Template.file_system = StubFileSystem.new('product' => '{{ inner_product.title }}')
assert_template_result('My Product', '{% render "product", inner_product: outer_product %}',
{ 'outer_product' => { 'title' => 'My Product' } },
partials: { 'product' => '{{ inner_product.title }}' })
'outer_product' => { 'title' => 'My Product' })
end
def test_render_accepts_literals_as_arguments
assert_template_result('123', '{% render "snippet", price: 123 %}',
partials: { 'snippet' => '{{ price }}' })
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ price }}')
assert_template_result('123', '{% render "snippet", price: 123 %}')
end
def test_render_accepts_multiple_named_arguments
assert_template_result('1 2', '{% render "snippet", one: 1, two: 2 %}',
partials: { 'snippet' => '{{ one }} {{ two }}' })
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ one }} {{ two }}')
assert_template_result('1 2', '{% render "snippet", one: 1, two: 2 %}')
end
def test_render_does_not_inherit_parent_scope_variables
assert_template_result('', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}',
partials: { 'snippet' => '{{ outer_variable }}' })
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ outer_variable }}')
assert_template_result('', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}')
end
def test_render_does_not_inherit_variable_with_same_name_as_snippet
assert_template_result('', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}",
partials: { 'snippet' => '{{ snippet }}' })
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ snippet }}')
assert_template_result('', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}")
end
def test_render_does_not_mutate_parent_scope
assert_template_result('', "{% render 'snippet' %}{{ inner }}",
partials: { 'snippet' => '{% assign inner = 1 %}' })
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}')
assert_template_result('', "{% render 'snippet' %}{{ inner }}")
end
def test_nested_render_tag
assert_template_result('one two', "{% render 'one' %}",
partials: {
'one' => "one {% render 'two' %}",
'two' => 'two',
})
Liquid::Template.file_system = StubFileSystem.new(
'one' => "one {% render 'two' %}",
'two' => 'two'
)
assert_template_result('one two', "{% render 'one' %}")
end
def test_recursively_rendered_template_does_not_produce_endless_loop
@@ -72,7 +73,11 @@ class RenderTagTest < Minitest::Test
end
def test_dynamically_choosen_templates_are_not_allowed
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}")
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered')
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}")
end
end
def test_include_tag_caches_second_read_of_same_partial
@@ -96,114 +101,113 @@ class RenderTagTest < Minitest::Test
end
def test_render_tag_within_if_statement
assert_template_result('my message', '{% if true %}{% render "snippet" %}{% endif %}',
partials: { 'snippet' => 'my message' })
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'my message')
assert_template_result('my message', '{% if true %}{% render "snippet" %}{% endif %}')
end
def test_break_through_render
options = { partials: { 'break' => '{% break %}' } }
assert_template_result('1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}', **options)
assert_template_result('112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}', **options)
Liquid::Template.file_system = StubFileSystem.new('break' => '{% break %}')
assert_template_result('1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}')
assert_template_result('112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}')
end
def test_increment_is_isolated_between_renders
assert_template_result('010', '{% increment %}{% increment %}{% render "incr" %}',
partials: { 'incr' => '{% increment %}' })
Liquid::Template.file_system = StubFileSystem.new('incr' => '{% increment %}')
assert_template_result('010', '{% increment %}{% increment %}{% render "incr" %}')
end
def test_decrement_is_isolated_between_renders
assert_template_result('-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}',
partials: { 'decr' => '{% decrement %}' })
Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}')
assert_template_result('-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}')
end
def test_includes_will_not_render_inside_render_tag
assert_template_result(
'Liquid error (test_include line 1): include usage is not allowed in this context',
'{% render "test_include" %}',
render_errors: true,
partials: {
'foo' => 'bar',
'test_include' => '{% include "foo" %}',
}
Liquid::Template.file_system = StubFileSystem.new(
'foo' => 'bar',
'test_include' => '{% include "foo" %}'
)
exc = assert_raises(Liquid::DisabledError) do
Liquid::Template.parse('{% render "test_include" %}').render!
end
assert_equal('Liquid error: include usage is not allowed in this context', exc.message)
end
def test_includes_will_not_render_inside_nested_sibling_tags
assert_template_result(
"Liquid error (test_include line 1): include usage is not allowed in this context" \
"Liquid error (nested_render_with_sibling_include line 1): include usage is not allowed in this context",
'{% render "nested_render_with_sibling_include" %}',
partials: {
'foo' => 'bar',
'nested_render_with_sibling_include' => '{% render "test_include" %}{% include "foo" %}',
'test_include' => '{% include "foo" %}',
},
render_errors: true
Liquid::Template.file_system = StubFileSystem.new(
'foo' => 'bar',
'nested_render_with_sibling_include' => '{% render "test_include" %}{% include "foo" %}',
'test_include' => '{% include "foo" %}'
)
output = Liquid::Template.parse('{% render "nested_render_with_sibling_include" %}').render
assert_equal('Liquid error: include usage is not allowed in this contextLiquid error: include usage is not allowed in this context', output)
end
def test_render_tag_with
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm ",
"{% render 'product' with products[0] %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
})
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_with_alias
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm ",
"{% render 'product_alias' with products[0] as product %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
})
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for_alias
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product_alias' for products as product %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
})
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product' for products %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
})
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_forloop
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} {% if forloop.first %}first{% endif %} {% if forloop.last %}last{% endif %} index:{{ forloop.index }} ",
)
assert_template_result("Product: Draft 151cm first index:1 Product: Element 155cm last index:2 ",
"{% render 'product' for products %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: {
'product' => "Product: {{ product.title }} {% if forloop.first %}first{% endif %} {% if forloop.last %}last{% endif %} index:{{ forloop.index }} ",
})
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for_drop
Liquid::Template.file_system = StubFileSystem.new(
'loop' => "{{ value.foo }}",
)
assert_template_result("123",
"{% render 'loop' for loop as value %}", { "loop" => TestEnumerable.new },
partials: {
'loop' => "{{ value.foo }}",
})
"{% render 'loop' for loop as value %}", "loop" => TestEnumerable.new)
end
def test_render_tag_with_drop
Liquid::Template.file_system = StubFileSystem.new(
'loop' => "{{ value }}",
)
assert_template_result("TestEnumerable",
"{% render 'loop' with loop as value %}", { "loop" => TestEnumerable.new },
partials: {
'loop' => "{{ value }}",
})
"{% render 'loop' with loop as value %}", "loop" => TestEnumerable.new)
end
end
+39 -40
View File
@@ -36,8 +36,6 @@ class StandardTagTest < Minitest::Test
assert_template_result('', '{%comment%}{% endif %}{%endcomment%}')
assert_template_result('', '{% comment %}{% endwhatever %}{% endcomment %}')
assert_template_result('', '{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}')
assert_template_result('', '{% comment %}{% " %}{% endcomment %}')
assert_template_result('', '{% comment %}{%%}{% endcomment %}')
assert_template_result('foobar', 'foo{%comment%}comment{%endcomment%}bar')
assert_template_result('foobar', 'foo{% comment %}comment{% endcomment %}bar')
@@ -73,7 +71,7 @@ class StandardTagTest < Minitest::Test
assert_raises(SyntaxError) do
assert_template_result('content foo content foo ',
'{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
{ 'var' => 'content' })
'var' => 'content')
end
end
@@ -122,38 +120,38 @@ class StandardTagTest < Minitest::Test
end
def test_case_on_size
assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', { 'a' => [] })
assert_template_result('1', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', { 'a' => [1] })
assert_template_result('2', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', { 'a' => [1, 1] })
assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', { 'a' => [1, 1, 1] })
assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', { 'a' => [1, 1, 1, 1] })
assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', { 'a' => [1, 1, 1, 1, 1] })
assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [])
assert_template_result('1', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1])
assert_template_result('2', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1])
assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1])
assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1])
assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1, 1])
end
def test_case_on_size_with_else
assert_template_result('else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [] })
'a' => [])
assert_template_result('1',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1] })
'a' => [1])
assert_template_result('2',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1, 1] })
'a' => [1, 1])
assert_template_result('else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1, 1, 1] })
'a' => [1, 1, 1])
assert_template_result('else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1, 1, 1, 1] })
'a' => [1, 1, 1, 1])
assert_template_result('else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
{ 'a' => [1, 1, 1, 1, 1] })
'a' => [1, 1, 1, 1, 1])
end
def test_case_on_length_with_else
@@ -176,47 +174,48 @@ class StandardTagTest < Minitest::Test
def test_assign_from_case
# Example from the shopify forums
code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}"
assert_template_result("menswear", code, { "collection" => { 'handle' => 'menswear-jackets' } })
assert_template_result("menswear", code, { "collection" => { 'handle' => 'menswear-t-shirts' } })
assert_template_result("womenswear", code, { "collection" => { 'handle' => 'x' } })
assert_template_result("womenswear", code, { "collection" => { 'handle' => 'y' } })
assert_template_result("womenswear", code, { "collection" => { 'handle' => 'z' } })
code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}"
template = Liquid::Template.parse(code)
assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-jackets' }))
assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-t-shirts' }))
assert_equal("womenswear", template.render!("collection" => { 'handle' => 'x' }))
assert_equal("womenswear", template.render!("collection" => { 'handle' => 'y' }))
assert_equal("womenswear", template.render!("collection" => { 'handle' => 'z' }))
end
def test_case_when_or
code = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 2 })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 3 })
assert_template_result(' its 4 ', code, { 'condition' => 4 })
assert_template_result('', code, { 'condition' => 5 })
assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1)
assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 2)
assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 3)
assert_template_result(' its 4 ', code, 'condition' => 4)
assert_template_result('', code, 'condition' => 5)
code = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 'string' })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => nil })
assert_template_result('', code, { 'condition' => 'something else' })
assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1)
assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 'string')
assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => nil)
assert_template_result('', code, 'condition' => 'something else')
end
def test_case_when_comma
code = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 2 })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 3 })
assert_template_result(' its 4 ', code, { 'condition' => 4 })
assert_template_result('', code, { 'condition' => 5 })
assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1)
assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 2)
assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 3)
assert_template_result(' its 4 ', code, 'condition' => 4)
assert_template_result('', code, 'condition' => 5)
code = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 'string' })
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => nil })
assert_template_result('', code, { 'condition' => 'something else' })
assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1)
assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 'string')
assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => nil)
assert_template_result('', code, 'condition' => 'something else')
end
def test_case_when_comma_and_blank_body
code = '{% case condition %}{% when 1, 2 %} {% assign r = "result" %} {% endcase %}{{ r }}'
assert_template_result('result', code, { 'condition' => 2 })
assert_template_result('result', code, 'condition' => 2)
end
def test_assign
+10 -10
View File
@@ -60,22 +60,22 @@ class StatementsTest < Minitest::Test
def test_var_strings_equal
text = ' {% if var == "hello there!" %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'var' => 'hello there!' })
assert_template_result(' true ', text, 'var' => 'hello there!')
end
def test_var_strings_are_not_equal
text = ' {% if "hello there!" == var %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'var' => 'hello there!' })
assert_template_result(' true ', text, 'var' => 'hello there!')
end
def test_var_and_long_string_are_equal
text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} "
assert_template_result(' true ', text, { 'var' => 'hello there!' })
assert_template_result(' true ', text, 'var' => 'hello there!')
end
def test_var_and_long_string_are_equal_backwards
text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} "
assert_template_result(' true ', text, { 'var' => 'hello there!' })
assert_template_result(' true ', text, 'var' => 'hello there!')
end
# def test_is_nil
@@ -87,27 +87,27 @@ class StatementsTest < Minitest::Test
def test_is_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'array' => [] })
assert_template_result(' true ', text, 'array' => [])
end
def test_is_not_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} '
assert_template_result(' false ', text, { 'array' => [1, 2, 3] })
assert_template_result(' false ', text, 'array' => [1, 2, 3])
end
def test_nil
text = ' {% if var == nil %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'var' => nil })
assert_template_result(' true ', text, 'var' => nil)
text = ' {% if var == null %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'var' => nil })
assert_template_result(' true ', text, 'var' => nil)
end
def test_not_nil
text = ' {% if var != nil %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'var' => 1 })
assert_template_result(' true ', text, 'var' => 1)
text = ' {% if var != null %} true {% else %} false {% endif %} '
assert_template_result(' true ', text, { 'var' => 1 })
assert_template_result(' true ', text, 'var' => 1)
end
end # StatementsTest
+9 -77
View File
@@ -20,115 +20,47 @@ class TableRowTest < Minitest::Test
def test_table_row
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
{ 'numbers' => [1, 2, 3, 4, 5, 6] })
'numbers' => [1, 2, 3, 4, 5, 6])
assert_template_result("<tr class=\"row1\">\n</tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
{ 'numbers' => [] })
'numbers' => [])
end
def test_table_row_with_different_cols
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}',
{ 'numbers' => [1, 2, 3, 4, 5, 6] })
'numbers' => [1, 2, 3, 4, 5, 6])
end
def test_table_col_counter
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n",
'{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}',
{ 'numbers' => [1, 2, 3, 4, 5, 6] })
'numbers' => [1, 2, 3, 4, 5, 6])
end
def test_quoted_fragment
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}",
{ 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] } })
'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] })
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}",
{ 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] } })
'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] })
end
def test_enumerable_drop
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
{ 'numbers' => ArrayDrop.new([1, 2, 3, 4, 5, 6]) })
'numbers' => ArrayDrop.new([1, 2, 3, 4, 5, 6]))
end
def test_offset_and_limit
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}',
{ 'numbers' => [0, 1, 2, 3, 4, 5, 6, 7] })
'numbers' => [0, 1, 2, 3, 4, 5, 6, 7])
end
def test_blank_string_not_iterable
assert_template_result("<tr class=\"row1\">\n</tr>\n",
"{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}",
{ 'characters' => '' })
end
def test_cols_nil_constant_same_as_evaluated_nil_expression
expect = "<tr class=\"row1\">\n" \
"<td class=\"col1\">false</td>" \
"<td class=\"col2\">false</td>" \
"</tr>\n"
assert_template_result(expect,
"{% tablerow i in (1..2) cols:nil %}{{ tablerowloop.col_last }}{% endtablerow %}")
assert_template_result(expect,
"{% tablerow i in (1..2) cols:var %}{{ tablerowloop.col_last }}{% endtablerow %}",
{ "var" => nil })
end
def test_tablerow_loop_drop_attributes
template = <<~LIQUID.chomp
{% tablerow i in (1...2) %}
col: {{ tablerowloop.col }}
col0: {{ tablerowloop.col0 }}
col_first: {{ tablerowloop.col_first }}
col_last: {{ tablerowloop.col_last }}
first: {{ tablerowloop.first }}
index: {{ tablerowloop.index }}
index0: {{ tablerowloop.index0 }}
last: {{ tablerowloop.last }}
length: {{ tablerowloop.length }}
rindex: {{ tablerowloop.rindex }}
rindex0: {{ tablerowloop.rindex0 }}
row: {{ tablerowloop.row }}
{% endtablerow %}
LIQUID
expected_output = <<~OUTPUT
<tr class="row1">
<td class="col1">
col: 1
col0: 0
col_first: true
col_last: false
first: true
index: 1
index0: 0
last: false
length: 2
rindex: 2
rindex0: 1
row: 1
</td><td class="col2">
col: 2
col0: 1
col_first: false
col_last: true
first: false
index: 2
index0: 1
last: true
length: 2
rindex: 1
rindex0: 0
row: 1
</td></tr>
OUTPUT
assert_template_result(expected_output, template)
assert_template_result("<tr class=\"row1\">\n</tr>\n", "{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}", 'characters' => '')
end
end
@@ -19,10 +19,10 @@ class UnlessElseTagTest < Minitest::Test
end
def test_unless_in_loop
assert_template_result('23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', { 'choices' => [1, nil, false] })
assert_template_result('23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', 'choices' => [1, nil, false])
end
def test_unless_else_in_loop
assert_template_result(' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', { 'choices' => [1, nil, false] })
assert_template_result(' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', 'choices' => [1, nil, false])
end
end # UnlessElseTest
+6 -4
View File
@@ -315,11 +315,13 @@ class TemplateTest < Minitest::Test
end
def test_using_range_literal_works_as_expected
source = "{% assign foo = (x..y) %}{{ foo }}"
assert_template_result("1..5", source, { "x" => 1, "y" => 5 })
t = Template.parse("{% assign foo = (x..y) %}{{ foo }}")
result = t.render('x' => 1, 'y' => 5)
assert_equal('1..5', result)
source = "{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}"
assert_template_result("12345", source, { "x" => 1, "y" => 5 })
t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}")
result = t.render('x' => 1, 'y' => 5)
assert_equal('12345', result)
end
def test_source_string_subclass
+8 -3
View File
@@ -530,9 +530,14 @@ class TrimModeTest < Minitest::Test
end
def test_pre_trim_blank_preceding_text
assert_template_result("", "\n{%- raw %}{% endraw %}")
assert_template_result("", "\n{%- if true %}{% endif %}")
assert_template_result("BC", "{{ 'B' }} \n{%- if true %}C{% endif %}")
template = Liquid::Template.parse("\n{%- raw %}{% endraw %}")
assert_equal("", template.render)
template = Liquid::Template.parse("\n{%- if true %}{% endif %}")
assert_equal("", template.render)
template = Liquid::Template.parse("{{ 'B' }} \n{%- if true %}C{% endif %}")
assert_equal("BC", template.render)
end
def test_bug_compatible_pre_trim
+39 -34
View File
@@ -6,76 +6,81 @@ class VariableTest < Minitest::Test
include Liquid
def test_simple_variable
assert_template_result('worked', "{{test}}", { 'test' => 'worked' })
assert_template_result('worked wonderfully', "{{test}}", { 'test' => 'worked wonderfully' })
template = Template.parse(%({{test}}))
assert_equal('worked', template.render!('test' => 'worked'))
assert_equal('worked wonderfully', template.render!('test' => 'worked wonderfully'))
end
def test_variable_render_calls_to_liquid
assert_template_result('foobar', '{{ foo }}', { 'foo' => ThingWithToLiquid.new })
assert_template_result('foobar', '{{ foo }}', 'foo' => ThingWithToLiquid.new)
end
def test_variable_lookup_calls_to_liquid_value
assert_template_result('1', '{{ foo }}', { 'foo' => IntegerDrop.new('1') })
assert_template_result('2', '{{ list[foo] }}', { 'foo' => IntegerDrop.new('1'), 'list' => [1, 2, 3] })
assert_template_result('one', '{{ list[foo] }}', { 'foo' => IntegerDrop.new('1'), 'list' => { 1 => 'one' } })
assert_template_result('Yay', '{{ foo }}', { 'foo' => BooleanDrop.new(true) })
assert_template_result('YAY', '{{ foo | upcase }}', { 'foo' => BooleanDrop.new(true) })
assert_template_result('1', '{{ foo }}', 'foo' => IntegerDrop.new('1'))
assert_template_result('2', '{{ list[foo] }}', 'foo' => IntegerDrop.new('1'), 'list' => [1, 2, 3])
assert_template_result('one', '{{ list[foo] }}', 'foo' => IntegerDrop.new('1'), 'list' => { 1 => 'one' })
assert_template_result('Yay', '{{ foo }}', 'foo' => BooleanDrop.new(true))
assert_template_result('YAY', '{{ foo | upcase }}', 'foo' => BooleanDrop.new(true))
end
def test_if_tag_calls_to_liquid_value
assert_template_result('one', '{% if foo == 1 %}one{% endif %}', { 'foo' => IntegerDrop.new('1') })
assert_template_result('one', '{% if 0 < foo %}one{% endif %}', { 'foo' => IntegerDrop.new('1') })
assert_template_result('one', '{% if foo > 0 %}one{% endif %}', { 'foo' => IntegerDrop.new('1') })
assert_template_result('true', '{% if foo == true %}true{% endif %}', { 'foo' => BooleanDrop.new(true) })
assert_template_result('true', '{% if foo %}true{% endif %}', { 'foo' => BooleanDrop.new(true) })
assert_template_result('one', '{% if foo == 1 %}one{% endif %}', 'foo' => IntegerDrop.new('1'))
assert_template_result('one', '{% if 0 < foo %}one{% endif %}', 'foo' => IntegerDrop.new('1'))
assert_template_result('one', '{% if foo > 0 %}one{% endif %}', 'foo' => IntegerDrop.new('1'))
assert_template_result('true', '{% if foo == true %}true{% endif %}', 'foo' => BooleanDrop.new(true))
assert_template_result('true', '{% if foo %}true{% endif %}', 'foo' => BooleanDrop.new(true))
assert_template_result('', '{% if foo %}true{% endif %}', { 'foo' => BooleanDrop.new(false) })
assert_template_result('', '{% if foo == true %}True{% endif %}', { 'foo' => BooleanDrop.new(false) })
assert_template_result('', '{% if foo %}true{% endif %}', 'foo' => BooleanDrop.new(false))
assert_template_result('', '{% if foo == true %}True{% endif %}', 'foo' => BooleanDrop.new(false))
end
def test_unless_tag_calls_to_liquid_value
assert_template_result('', '{% unless foo %}true{% endunless %}', { 'foo' => BooleanDrop.new(true) })
assert_template_result('', '{% unless foo %}true{% endunless %}', 'foo' => BooleanDrop.new(true))
end
def test_case_tag_calls_to_liquid_value
assert_template_result('One', '{% case foo %}{% when 1 %}One{% endcase %}', { 'foo' => IntegerDrop.new('1') })
assert_template_result('One', '{% case foo %}{% when 1 %}One{% endcase %}', 'foo' => IntegerDrop.new('1'))
end
def test_simple_with_whitespaces
assert_template_result(" worked ", " {{ test }} ", { "test" => "worked" })
assert_template_result(" worked wonderfully ", " {{ test }} ", { "test" => "worked wonderfully" })
template = Template.parse(%( {{ test }} ))
assert_equal(' worked ', template.render!('test' => 'worked'))
assert_equal(' worked wonderfully ', template.render!('test' => 'worked wonderfully'))
end
def test_expression_with_whitespace_in_square_brackets
assert_template_result('result', "{{ a[ 'b' ] }}", { 'a' => { 'b' => 'result' } })
assert_template_result('result', "{{ a[ [ 'b' ] ] }}", { 'b' => 'c', 'a' => { 'c' => 'result' } })
assert_template_result('result', "{{ a[ 'b' ] }}", 'a' => { 'b' => 'result' })
assert_template_result('result', "{{ a[ [ 'b' ] ] }}", 'b' => 'c', 'a' => { 'c' => 'result' })
end
def test_ignore_unknown
assert_template_result("", "{{ test }}")
template = Template.parse(%({{ test }}))
assert_equal('', template.render!)
end
def test_using_blank_as_variable_name
assert_template_result("", "{% assign foo = blank %}{{ foo }}")
template = Template.parse("{% assign foo = blank %}{{ foo }}")
assert_equal('', template.render!)
end
def test_using_empty_as_variable_name
assert_template_result("", "{% assign foo = empty %}{{ foo }}")
template = Template.parse("{% assign foo = empty %}{{ foo }}")
assert_equal('', template.render!)
end
def test_hash_scoping
assert_template_result('worked', "{{ test.test }}", { 'test' => { 'test' => 'worked' } })
assert_template_result('worked', "{{ test . test }}", { 'test' => { 'test' => 'worked' } })
assert_template_result('worked', "{{ test.test }}", 'test' => { 'test' => 'worked' })
assert_template_result('worked', "{{ test . test }}", 'test' => { 'test' => 'worked' })
end
def test_false_renders_as_false
assert_template_result("false", "{{ foo }}", { 'foo' => false })
assert_template_result("false", "{{ false }}")
assert_equal('false', Template.parse("{{ foo }}").render!('foo' => false))
assert_equal('false', Template.parse("{{ false }}").render!)
end
def test_nil_renders_as_empty_string
assert_template_result("", "{{ nil }}")
assert_template_result("cat", "{{ nil | append: 'cat' }}")
assert_equal('', Template.parse("{{ nil }}").render!)
assert_equal('cat', Template.parse("{{ nil | append: 'cat' }}").render!)
end
def test_preset_assigns
@@ -116,18 +121,18 @@ class VariableTest < Minitest::Test
end
def test_multiline_variable
assert_template_result("worked", "{{\ntest\n}}", { "test" => "worked" })
assert_equal('worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked'))
end
def test_render_symbol
assert_template_result('bar', '{{ foo }}', { 'foo' => :bar })
assert_template_result('bar', '{{ foo }}', 'foo' => :bar)
end
def test_dynamic_find_var
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
assert_template_result('bar', '{{ [key] }}', 'key' => 'foo', 'foo' => 'bar')
end
def test_raw_value_variable
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
assert_template_result('bar', '{{ [key] }}', 'key' => 'foo', 'foo' => 'bar')
end
end
+14 -17
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
@@ -37,29 +40,23 @@ module Minitest
module Assertions
include Liquid
def assert_template_result(
expected, template, assigns = {},
message: nil, partials: nil, error_mode: nil, render_errors: false
)
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym)
file_system = StubFileSystem.new(partials || {})
registers = Liquid::Registers.new(file_system: file_system)
context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers)
output = template.render(context)
assert_equal(expected, output, message)
def assert_template_result(expected, template, assigns = {}, message = nil)
assert_equal(expected, Template.parse(template, line_numbers: true).render!(assigns), message)
end
def assert_match_syntax_error(match, template, error_mode: nil)
def assert_template_result_matches(expected, template, assigns = {}, message = nil)
return assert_template_result(expected, template, assigns, message) unless expected.is_a?(Regexp)
assert_match(expected, Template.parse(template, line_numbers: true).render!(assigns), message)
end
def assert_match_syntax_error(match, template, assigns = {})
exception = assert_raises(Liquid::SyntaxError) do
Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym).render
Template.parse(template, line_numbers: true).render(assigns)
end
assert_match(match, exception.message)
end
def assert_syntax_error(template, error_mode: nil)
assert_match_syntax_error("", template, error_mode: error_mode)
end
def assert_usage_increment(name, times: 1)
old_method = Liquid::Usage.method(:increment)
calls = 0
-14
View File
@@ -236,20 +236,6 @@ class ParseTreeVisitorTest < Minitest::Test
)
end
def test_render_with
assert_equal(
["test"],
visit(%({% render "hai" with test %}))
)
end
def test_render_for
assert_equal(
["test"],
visit(%({% render "hai" for test %}))
)
end
def test_preserve_tree_structure
assert_equal(
[[nil, [
-31
View File
@@ -125,35 +125,4 @@ class PartialCacheUnitTest < Minitest::Test
assert_equal('my partial body', partial.render)
assert_equal(1, template_factory.count)
end
def test_cache_state_is_shared_for_subcontexts
parse_context = Liquid::ParseContext.new
shared_file_system = StubFileSystem.new(
'my_partial' => 'my shared value'
)
context = Liquid::Context.build(
registers: Liquid::Registers.new(
file_system: shared_file_system,
)
)
subcontext = context.new_isolated_subcontext
assert_equal(subcontext.registers[:cached_partials].object_id, context.registers[:cached_partials].object_id)
2.times do
Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: parse_context
)
Liquid::PartialCache.load(
'my_partial',
context: subcontext,
parse_context: parse_context
)
end
assert_equal(1, shared_file_system.file_read_count)
end
end
@@ -2,11 +2,11 @@
require 'test_helper'
class RegistersUnitTest < Minitest::Test
class StaticRegistersUnitTest < Minitest::Test
include Liquid
def test_set
static_register = Registers.new(a: 1, b: 2)
static_register = StaticRegisters.new(a: 1, b: 2)
static_register[:b] = 22
static_register[:c] = 33
@@ -16,13 +16,13 @@ class RegistersUnitTest < Minitest::Test
end
def test_get_missing_key
static_register = Registers.new
static_register = StaticRegisters.new
assert_nil(static_register[:missing])
end
def test_delete
static_register = Registers.new(a: 1, b: 2)
static_register = StaticRegisters.new(a: 1, b: 2)
static_register[:b] = 22
static_register[:c] = 33
@@ -37,7 +37,7 @@ class RegistersUnitTest < Minitest::Test
end
def test_fetch
static_register = Registers.new(a: 1, b: 2)
static_register = StaticRegisters.new(a: 1, b: 2)
static_register[:b] = 22
static_register[:c] = 33
@@ -61,7 +61,7 @@ class RegistersUnitTest < Minitest::Test
end
def test_key
static_register = Registers.new(a: 1, b: 2)
static_register = StaticRegisters.new(a: 1, b: 2)
static_register[:b] = 22
static_register[:c] = 33
@@ -72,7 +72,7 @@ class RegistersUnitTest < Minitest::Test
end
def test_static_register_can_be_frozen
static_register = Registers.new(a: 1)
static_register = StaticRegisters.new(a: 1)
static_register.static.freeze
@@ -94,14 +94,14 @@ class RegistersUnitTest < Minitest::Test
end
def test_new_static_retains_static
static_register = Registers.new(a: 1, b: 2)
static_register = StaticRegisters.new(a: 1, b: 2)
static_register[:b] = 22
static_register[:c] = 33
new_static_register = Registers.new(static_register)
new_static_register = StaticRegisters.new(static_register)
new_static_register[:b] = 222
newest_static_register = Registers.new(new_static_register)
newest_static_register = StaticRegisters.new(new_static_register)
newest_static_register[:c] = 333
assert_equal(1, static_register[:a])
@@ -118,11 +118,11 @@ class RegistersUnitTest < Minitest::Test
end
def test_multiple_instances_are_unique
static_register_1 = Registers.new(a: 1, b: 2)
static_register_1 = StaticRegisters.new(a: 1, b: 2)
static_register_1[:b] = 22
static_register_1[:c] = 33
static_register_2 = Registers.new(a: 10, b: 20)
static_register_2 = StaticRegisters.new(a: 10, b: 20)
static_register_2[:b] = 220
static_register_2[:c] = 330
@@ -138,11 +138,11 @@ class RegistersUnitTest < Minitest::Test
end
def test_initialization_reused_static_same_memory_object
static_register_1 = Registers.new(a: 1, b: 2)
static_register_1 = StaticRegisters.new(a: 1, b: 2)
static_register_1[:b] = 22
static_register_1[:c] = 33
static_register_2 = Registers.new(static_register_1)
static_register_2 = StaticRegisters.new(static_register_1)
assert_equal(1, static_register_2[:a])
assert_equal(2, static_register_2[:b])