mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-14 00:10:39 -07:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2186981c06 | ||
|
|
41a77c68c6 | ||
|
|
3d1e2d434e | ||
|
|
1058a7768f | ||
|
|
4b01977067 |
@@ -23,4 +23,8 @@ group :test do
|
||||
gem 'rubocop', '~> 1.61.0'
|
||||
gem 'rubocop-shopify', '~> 2.12.0', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
|
||||
platform :mri, :truffleruby do
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'main'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,6 +43,8 @@ task :test do
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
|
||||
ENV['LIQUID_C'] = '1'
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
@@ -81,21 +83,10 @@ namespace :benchmark do
|
||||
end
|
||||
|
||||
desc "Run unit benchmarks"
|
||||
namespace :unit do
|
||||
desc "Run all unit benchmarks"
|
||||
task :all do
|
||||
Dir["./performance/unit/*_benchmark.rb"].each do |file|
|
||||
puts "🧪 Running #{file}"
|
||||
ruby file
|
||||
end
|
||||
end
|
||||
|
||||
%w[lexer loom].each do |benchmark|
|
||||
desc "Run the #{benchmark} benchmark"
|
||||
task benchmark.to_sym do
|
||||
puts "🧪 Running #{benchmark}"
|
||||
ruby "./performance/unit/#{benchmark}_benchmark.rb"
|
||||
end
|
||||
task :unit do
|
||||
Dir["./performance/unit/*_benchmark.rb"].each do |file|
|
||||
puts "🧪 Running #{file}"
|
||||
ruby file
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -86,4 +86,3 @@ require 'liquid/partial_cache'
|
||||
require 'liquid/usage'
|
||||
require 'liquid/registers'
|
||||
require 'liquid/template_factory'
|
||||
require 'liquid/loom'
|
||||
|
||||
@@ -31,11 +31,10 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Freeze the nodelist after optimization
|
||||
# def freeze
|
||||
# @nodelist.freeze
|
||||
# super
|
||||
# end
|
||||
def freeze
|
||||
@nodelist.freeze
|
||||
super
|
||||
end
|
||||
|
||||
private def parse_for_liquid_tag(tokenizer, parse_context)
|
||||
while (token = tokenizer.shift)
|
||||
@@ -155,12 +154,6 @@ module Liquid
|
||||
end
|
||||
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
||||
@blank &&= new_tag.blank?
|
||||
|
||||
if parse_context.eager_optimize
|
||||
next if new_tag.nodelist&.all? { !_1.is_a?(String) && _1.nodelist.empty? } # this is an empty block
|
||||
next if new_tag.is_a?(If) && new_tag.blocks.empty? # this is an empty If block
|
||||
end
|
||||
|
||||
@nodelist << new_tag
|
||||
when token.start_with?(VARSTART)
|
||||
whitespace_handler(token, parse_context)
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Loom
|
||||
MERGABLE_IF_OPERATORS = ["==", ">", "<", "!="].freeze
|
||||
EQUAL_OP = "==".freeze
|
||||
|
||||
class << self
|
||||
def optimize(template)
|
||||
new(template).optimize
|
||||
end
|
||||
end
|
||||
|
||||
def initialize template
|
||||
@root = template.root
|
||||
end
|
||||
|
||||
def optimize
|
||||
merge_if_blocks
|
||||
end
|
||||
|
||||
def merge_if_blocks
|
||||
nodelist_list = [@root.nodelist]
|
||||
|
||||
while nodelist_list.any?
|
||||
next_nodelist_list = []
|
||||
|
||||
nodelist_list.each do |nodelist|
|
||||
i = 0
|
||||
while i < nodelist.length
|
||||
node = nodelist[i]
|
||||
chain_if_blocks(nodelist, node, i) if node.is_a?(If)
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
|
||||
nodelist_list = next_nodelist_list
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mergable_if_blocks?(target_if, next_if)
|
||||
target_left = target_if.blocks.first.left
|
||||
target_right = target_if.blocks.first.right
|
||||
next_left = next_if.blocks.first.left
|
||||
next_right = next_if.blocks.first.right
|
||||
|
||||
used_variables = Hash.new { |h, k| h[k] = 0 }
|
||||
|
||||
[
|
||||
target_if.blocks.first.left,
|
||||
target_if.blocks.first.right,
|
||||
next_if.blocks.first.left,
|
||||
next_if.blocks.first.right
|
||||
].each do |var|
|
||||
if var.is_a?(VariableLookup)
|
||||
used_variables[var.name] += 1
|
||||
end
|
||||
end
|
||||
|
||||
return if used_variables.keys.count > 1
|
||||
|
||||
most_used_variable_name = used_variables.keys[0]
|
||||
|
||||
# TODO: I probably can't do this
|
||||
# It might be possible to get different result between a > b and b < a
|
||||
# Move most commonly used variable to the left side
|
||||
if (target_left.is_a?(VariableLookup) && target_left.name != most_used_variable_name) || (target_right.is_a?(VariableLookup) && target_right.name == most_used_variable_name)
|
||||
target_left, target_right = target_right, target_left
|
||||
end
|
||||
|
||||
if (next_left.is_a?(VariableLookup) && next_left.name != most_used_variable_name) || (next_right.is_a?(VariableLookup) && next_right.name == most_used_variable_name)
|
||||
next_left, next_right = next_right, next_left
|
||||
end
|
||||
|
||||
return false unless target_left.is_a?(VariableLookup) && next_left.is_a?(VariableLookup)
|
||||
return false if target_left.name != next_left.name
|
||||
|
||||
return false if target_right.nil? || next_right.nil?
|
||||
|
||||
|
||||
# we need to be conversative here and only can merge ==, >, <, and != operators
|
||||
target_operator = target_if.blocks.first.operator
|
||||
next_operator = next_if.blocks.first.operator
|
||||
|
||||
return false unless MERGABLE_IF_OPERATORS.include?(target_operator) && MERGABLE_IF_OPERATORS.include?(next_operator)
|
||||
|
||||
return false if target_operator == next_operator && target_right == next_right
|
||||
|
||||
return false if target_right.is_a?(VariableLookup) || next_right.is_a?(VariableLookup)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def chain_if_blocks(nodelist, first_if_node, first_if_index)
|
||||
used_variables = Set.new
|
||||
|
||||
# only check the top level Condition (ignore children conditions for now)
|
||||
first_if_node.blocks.each do |condition|
|
||||
used_variables << condition.left
|
||||
used_variables << condition.right if condition.right
|
||||
end
|
||||
|
||||
if_blocks = []
|
||||
|
||||
nodelist[first_if_index + 1..-1].each do |node|
|
||||
break unless node.is_a?(If)
|
||||
|
||||
# check if the variables used in the current block are used in the previous block
|
||||
break unless mergable_if_blocks?(first_if_node, node)
|
||||
|
||||
if_blocks << node
|
||||
end
|
||||
|
||||
nodelist.delete_if { |node| if_blocks.include?(node) }
|
||||
|
||||
if_blocks.each do |if_block|
|
||||
first_if_node.blocks << if_block.blocks.first
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@
|
||||
module Liquid
|
||||
class ParseContext
|
||||
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
||||
attr_reader :partial, :warnings, :error_mode, :environment, :eager_optimize
|
||||
attr_reader :partial, :warnings, :error_mode, :environment
|
||||
|
||||
def initialize(options = Const::EMPTY_HASH)
|
||||
@environment = options.fetch(:environment, Environment.default)
|
||||
@@ -11,7 +11,6 @@ module Liquid
|
||||
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@warnings = []
|
||||
@eager_optimize = options.fetch(:eager_optimize, ENV["OPTIMIZE"] == "true")
|
||||
|
||||
self.depth = 0
|
||||
self.partial = false
|
||||
|
||||
+102
-28
@@ -378,7 +378,7 @@ module Liquid
|
||||
end
|
||||
elsif ary.all? { |el| el.respond_to?(:[]) }
|
||||
begin
|
||||
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
|
||||
ary.sort { |a, b| nil_safe_compare(fetch_property(a, property), fetch_property(b, property)) }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
end
|
||||
@@ -407,7 +407,7 @@ module Liquid
|
||||
end
|
||||
elsif ary.all? { |el| el.respond_to?(:[]) }
|
||||
begin
|
||||
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
|
||||
ary.sort { |a, b| nil_safe_casecmp(fetch_property(a, property), fetch_property(b, property)) }
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
end
|
||||
@@ -424,29 +424,59 @@ module Liquid
|
||||
# @liquid_syntax array | where: string, string
|
||||
# @liquid_return [array[untyped]]
|
||||
def where(input, property, target_value = nil)
|
||||
ary = InputIterator.new(input, context)
|
||||
filter_array(input, property, target_value) { |ary, &block| ary.select(&block) }
|
||||
end
|
||||
|
||||
if ary.empty?
|
||||
[]
|
||||
elsif target_value.nil?
|
||||
ary.select do |item|
|
||||
item[property]
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
else
|
||||
ary.select do |item|
|
||||
item[property] == target_value
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
end
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Filters an array to exclude items with a specific property value.
|
||||
# @liquid_description
|
||||
# This requires you to provide both the property name and the associated value.
|
||||
# @liquid_syntax array | reject: string, string
|
||||
# @liquid_return [array[untyped]]
|
||||
def reject(input, property, target_value = nil)
|
||||
filter_array(input, property, target_value) { |ary, &block| ary.reject(&block) }
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Tests if any item in an array has a specific property value.
|
||||
# @liquid_description
|
||||
# This requires you to provide both the property name and the associated value.
|
||||
# @liquid_syntax array | some: string, string
|
||||
# @liquid_return [boolean]
|
||||
def has(input, property, target_value = nil)
|
||||
filter_array(input, property, target_value) { |ary, &block| ary.any?(&block) }
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Returns the first item in an array with a specific property value.
|
||||
# @liquid_description
|
||||
# This requires you to provide both the property name and the associated value.
|
||||
# @liquid_syntax array | find: string, string
|
||||
# @liquid_return [untyped]
|
||||
def find(input, property, target_value = nil)
|
||||
filter_array(input, property, target_value) { |ary, &block| ary.find(&block) }
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_type filter
|
||||
# @liquid_category array
|
||||
# @liquid_summary
|
||||
# Returns the index of the first item in an array with a specific property value.
|
||||
# @liquid_description
|
||||
# This requires you to provide both the property name and the associated value.
|
||||
# @liquid_syntax array | find_index: string, string
|
||||
# @liquid_return [number]
|
||||
def find_index(input, property, target_value = nil)
|
||||
filter_array(input, property, target_value) { |ary, &block| ary.find_index(&block) }
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
@@ -465,7 +495,7 @@ module Liquid
|
||||
[]
|
||||
else
|
||||
ary.uniq do |item|
|
||||
item[property]
|
||||
fetch_property(item, property)
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
@@ -501,7 +531,7 @@ module Liquid
|
||||
if property == "to_liquid"
|
||||
e
|
||||
elsif e.respond_to?(:[])
|
||||
r = e[property]
|
||||
r = fetch_property(e, property)
|
||||
r.is_a?(Proc) ? r.call : r
|
||||
end
|
||||
end
|
||||
@@ -525,7 +555,7 @@ module Liquid
|
||||
[]
|
||||
else
|
||||
ary.reject do |item|
|
||||
item[property].nil?
|
||||
fetch_property(item, property).nil?
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
@@ -899,7 +929,7 @@ module Liquid
|
||||
if property.nil?
|
||||
item
|
||||
elsif item.respond_to?(:[])
|
||||
item[property]
|
||||
fetch_property(item, property)
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -918,6 +948,50 @@ module Liquid
|
||||
|
||||
attr_reader :context
|
||||
|
||||
def filter_array(input, property, target_value, &block)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
return [] if ary.empty?
|
||||
|
||||
block.call(ary) do |item|
|
||||
if target_value.nil?
|
||||
fetch_property(item, property)
|
||||
else
|
||||
fetch_property(item, property) == target_value
|
||||
end
|
||||
rescue TypeError
|
||||
raise_property_error(property)
|
||||
rescue NoMethodError
|
||||
return nil unless item.respond_to?(:[])
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_property(drop, property_or_keys)
|
||||
##
|
||||
# This keeps backward compatibility by supporting properties containing
|
||||
# dots. This is valid in Liquid syntax and used in some runtimes, such as
|
||||
# Shopify with metafields.
|
||||
#
|
||||
# Using this approach, properties like 'price.value' can be accessed in
|
||||
# both of the following examples:
|
||||
#
|
||||
# ```
|
||||
# [
|
||||
# { 'name' => 'Item 1', 'price.price' => 40000 },
|
||||
# { 'name' => 'Item 2', 'price' => { 'value' => 39900 } }
|
||||
# ]
|
||||
# ```
|
||||
value = drop[property_or_keys]
|
||||
|
||||
return value if !value.nil? || !property_or_keys.is_a?(String)
|
||||
|
||||
keys = property_or_keys.split('.')
|
||||
keys.reduce(drop) do |drop, key|
|
||||
drop.respond_to?(:[]) ? drop[key] : drop
|
||||
end
|
||||
end
|
||||
|
||||
def raise_property_error(property)
|
||||
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
|
||||
end
|
||||
|
||||
+3
-31
@@ -23,7 +23,6 @@ module Liquid
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@blocks = []
|
||||
@has_else_block = false
|
||||
push_block('if', markup)
|
||||
end
|
||||
|
||||
@@ -34,44 +33,17 @@ module Liquid
|
||||
def parse(tokens)
|
||||
while parse_body(@blocks.last.attachment, tokens)
|
||||
end
|
||||
|
||||
if parse_context.eager_optimize && definitive_false_statement?
|
||||
@blocks.clear
|
||||
else
|
||||
@blocks.reverse_each do |block|
|
||||
block.attachment.remove_blank_strings if blank?
|
||||
block.attachment.freeze
|
||||
end
|
||||
@blocks.reverse_each do |block|
|
||||
block.attachment.remove_blank_strings if blank?
|
||||
block.attachment.freeze
|
||||
end
|
||||
end
|
||||
|
||||
def definitive_false_statement?
|
||||
# check if any blocks have variable lookups
|
||||
@blocks.each do |condition|
|
||||
return false if condition.left.is_a?(VariableLookup) || condition.right&.is_a?(VariableLookup)
|
||||
|
||||
child_condition = condition.child_condition
|
||||
|
||||
while child_condition
|
||||
return false if child_condition&.left.is_a?(VariableLookup) || child_condition&.right&.is_a?(VariableLookup)
|
||||
child_condition = child_condition.child_condition
|
||||
end
|
||||
end
|
||||
|
||||
# check if all blocks are false
|
||||
@blocks.each do |condition|
|
||||
return false if condition.evaluate
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
ELSE_TAG_NAMES = ['elsif', 'else'].freeze
|
||||
private_constant :ELSE_TAG_NAMES
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
if ELSE_TAG_NAMES.include?(tag)
|
||||
@has_else_block = true
|
||||
push_block(tag, markup)
|
||||
else
|
||||
super
|
||||
|
||||
@@ -82,11 +82,7 @@ module Liquid
|
||||
# See Liquid::Profiler for more information
|
||||
def parse(source, options = {})
|
||||
environment = options[:environment] || Environment.default
|
||||
template = new(environment: environment).parse(source, options)
|
||||
|
||||
Loom.optimize(template) if options[:eager_optimize]
|
||||
|
||||
template
|
||||
new(environment: environment).parse(source, options)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+6
-12
@@ -95,21 +95,15 @@ module Liquid
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
obj = render(context)
|
||||
render_obj_to_output(obj, output)
|
||||
output
|
||||
end
|
||||
|
||||
def render_obj_to_output(obj, output)
|
||||
case obj
|
||||
when NilClass
|
||||
# Do nothing
|
||||
when Array
|
||||
obj.each do |o|
|
||||
render_obj_to_output(o, output)
|
||||
end
|
||||
when
|
||||
if obj.is_a?(Array)
|
||||
output << obj.join
|
||||
elsif obj.nil?
|
||||
else
|
||||
output << obj.to_s
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
def disabled?(_context)
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "5.6.0.rc2"
|
||||
VERSION = "5.6.0.rc1"
|
||||
end
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
|
||||
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||
s.authors = ["Tobias Lütke"]
|
||||
s.email = ["[email protected]"]
|
||||
s.homepage = "https://shopify.github.io/liquid/"
|
||||
s.homepage = "http://www.liquidmarkup.org"
|
||||
s.license = "MIT"
|
||||
# s.description = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "benchmark/ips"
|
||||
require 'liquid'
|
||||
|
||||
RubyVM::YJIT.enable
|
||||
|
||||
TEMPLATE = <<~LIQUID
|
||||
{% if false %}
|
||||
{% for i in (1..1000000) %}
|
||||
{{ "Hello world!" }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% assign result = 1 %}
|
||||
{% if foo == 1 %}{% assign result = 1 %}{% endif %}{% if foo == 2 %}{% assign result = 2 %}{% endif %}{% if foo == 3 %}{% assign result = 3 %}{% endif %}
|
||||
Result: {{ result }}
|
||||
LIQUID
|
||||
|
||||
baseline_template = Liquid::Template.parse(TEMPLATE, eager_optimize: false)
|
||||
optimized_template = Liquid::Template.parse(TEMPLATE, eager_optimize: true)
|
||||
|
||||
[nil, 1, 2, 3].each do |foo|
|
||||
baseline_output = baseline_template.render('foo' => foo)
|
||||
optimized_output = optimized_template.render('foo' => foo)
|
||||
|
||||
if baseline_output != optimized_output
|
||||
puts "WARNING! Baseline and optimized templates render differently for foo=#{foo}"
|
||||
puts "Baseline: #{baseline_output}"
|
||||
puts "Optimized: #{optimized_output}"
|
||||
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
def render(template, foo)
|
||||
template.render('foo' => foo)
|
||||
end
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.config(time: 20, warmup: 3)
|
||||
|
||||
x.report("baseline") do
|
||||
[nil, 1, 2, 3].each do |foo|
|
||||
render(baseline_template, foo)
|
||||
end
|
||||
end
|
||||
|
||||
x.report("optimized") do
|
||||
[nil, 1, 2, 3].each do |foo|
|
||||
render(optimized_template, foo)
|
||||
end
|
||||
end
|
||||
|
||||
x.compare!
|
||||
end
|
||||
@@ -54,6 +54,30 @@ class TestEnumerable < Liquid::Drop
|
||||
end
|
||||
end
|
||||
|
||||
class TestDeepEnumerable < Liquid::Drop
|
||||
include Enumerable
|
||||
|
||||
class Product < Liquid::Drop
|
||||
attr_reader :title, :price, :premium
|
||||
|
||||
def initialize(title:, price:, premium: nil)
|
||||
@title = { "content" => title, "language" => "en" }
|
||||
@price = { "value" => price, "unit" => "USD" }
|
||||
@premium = { "category" => premium } if premium
|
||||
end
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
[
|
||||
Product.new(title: "Pro goggles", price: 1299),
|
||||
Product.new(title: "Thermal gloves", price: 1299),
|
||||
Product.new(title: "Alpine jacket", price: 3999, premium: 'Basic'),
|
||||
Product.new(title: "Mountain boots", price: 3899, premium: 'Pro'),
|
||||
Product.new(title: "Safety helmet", price: 1999)
|
||||
].each(&block)
|
||||
end
|
||||
end
|
||||
|
||||
class NumberLikeThing < Liquid::Drop
|
||||
def initialize(amount)
|
||||
@amount = amount
|
||||
@@ -392,6 +416,15 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
end
|
||||
|
||||
def test_sort_natural_with_deep_enumerables
|
||||
template = <<~LIQUID
|
||||
{{- products | sort_natural: 'title.content' | map: 'title.content' | join: ', ' -}}
|
||||
LIQUID
|
||||
expected_output = "Alpine jacket, Mountain boots, Pro goggles, Safety helmet, Thermal gloves"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
||||
end
|
||||
|
||||
def test_legacy_sort_hash
|
||||
assert_equal([{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2))
|
||||
end
|
||||
@@ -428,6 +461,15 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
end
|
||||
|
||||
def test_uniq_with_deep_enumerables
|
||||
template = <<~LIQUID
|
||||
{{- products | uniq: 'price.value' | map: "title.content" | join: ', ' -}}
|
||||
LIQUID
|
||||
expected_output = "Pro goggles, Alpine jacket, Mountain boots, Safety helmet"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
||||
end
|
||||
|
||||
def test_compact_empty_array
|
||||
assert_equal([], @filters.compact([], "a"))
|
||||
end
|
||||
@@ -444,6 +486,15 @@ class StandardFiltersTest < Minitest::Test
|
||||
end
|
||||
end
|
||||
|
||||
def test_compact_with_deep_enumerables
|
||||
template = <<~LIQUID
|
||||
{{- products | compact: 'premium.category' | map: 'title.content' | join: ', ' -}}
|
||||
LIQUID
|
||||
expected_output = "Alpine jacket, Mountain boots"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
||||
end
|
||||
|
||||
def test_reverse
|
||||
assert_equal([4, 3, 2, 1], @filters.reverse([1, 2, 3, 4]))
|
||||
end
|
||||
@@ -553,6 +604,15 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', { "foo" => TestEnumerable.new })
|
||||
end
|
||||
|
||||
def test_sort_with_deep_enumerables
|
||||
template = <<~LIQUID
|
||||
{{- products | sort: 'price.value' | map: 'title.content' | join: ', ' -}}
|
||||
LIQUID
|
||||
expected_output = "Pro goggles, Thermal gloves, Safety helmet, Mountain boots, Alpine jacket"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.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] })
|
||||
@@ -827,21 +887,219 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
|
||||
end
|
||||
|
||||
def test_where
|
||||
input = [
|
||||
def test_reject
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
expectation = [
|
||||
template = "{{ array | reject: 'ok' | map: 'handle' | join: ' ' }}"
|
||||
expected_output = "beta gamma"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_reject_with_value
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
assert_equal(expectation, @filters.where(input, "ok", true))
|
||||
assert_equal(expectation, @filters.where(input, "ok"))
|
||||
template = "{{ array | reject: 'ok', true | map: 'handle' | join: ' ' }}"
|
||||
expected_output = "beta gamma"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_reject_with_false_value
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
template = "{{ array | reject: 'ok', false | map: 'handle' | join: ' ' }}"
|
||||
expected_output = "alpha delta"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_reject_with_deep_enumerables
|
||||
template = <<~LIQUID
|
||||
{{- products | reject: 'title.content', 'Pro goggles' | map: 'price.value' | join: ', ' -}}
|
||||
LIQUID
|
||||
expected_output = "1299, 3999, 3899, 1999"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
||||
end
|
||||
|
||||
def test_has
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => false },
|
||||
]
|
||||
|
||||
expected_output = "true"
|
||||
|
||||
assert_template_result(expected_output, "{{ array | has: 'ok' }}", { "array" => array })
|
||||
assert_template_result(expected_output, "{{ array | has: 'ok', true }}", { "array" => array })
|
||||
end
|
||||
|
||||
def test_has_when_does_not_have_it
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => false },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => false },
|
||||
]
|
||||
|
||||
expected_output = "false"
|
||||
|
||||
assert_template_result(expected_output, "{{ array | has: 'ok' }}", { "array" => array })
|
||||
assert_template_result(expected_output, "{{ array | has: 'ok', true }}", { "array" => array })
|
||||
end
|
||||
|
||||
def test_has_with_false_value
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
template = "{{ array | has: 'ok', false }}"
|
||||
expected_output = "true"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_has_with_false_value_when_does_not_have_it
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => true },
|
||||
{ "handle" => "gamma", "ok" => true },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
template = "{{ array | has: 'ok', false }}"
|
||||
expected_output = "false"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_has_with_deep_enumerables
|
||||
template = <<~LIQUID
|
||||
{{- products | has: 'title.content', 'Pro goggles' -}},
|
||||
{{- products | has: 'title.content', 'foo' -}}
|
||||
LIQUID
|
||||
expected_output = "true,false"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
||||
end
|
||||
|
||||
def test_find_with_value
|
||||
products = [
|
||||
{ "title" => "Pro goggles", "price" => 1299 },
|
||||
{ "title" => "Thermal gloves", "price" => 1499 },
|
||||
{ "title" => "Alpine jacket", "price" => 3999 },
|
||||
{ "title" => "Mountain boots", "price" => 3899 },
|
||||
{ "title" => "Safety helmet", "price" => 1999 }
|
||||
]
|
||||
|
||||
template = <<~LIQUID
|
||||
{%- assign product = products | find: 'price', 3999 -%}
|
||||
{{- product.title -}}
|
||||
LIQUID
|
||||
expected_output = "Alpine jacket"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => products })
|
||||
end
|
||||
|
||||
def test_find_with_deep_enumerables
|
||||
template = <<~LIQUID
|
||||
{%- assign product = products | find: 'title.content', 'Pro goggles' -%}
|
||||
{{- product.title.content -}}
|
||||
LIQUID
|
||||
expected_output = "Pro goggles"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
||||
end
|
||||
|
||||
def test_find_index_with_value
|
||||
products = [
|
||||
{ "title" => "Pro goggles", "price" => 1299 },
|
||||
{ "title" => "Thermal gloves", "price" => 1499 },
|
||||
{ "title" => "Alpine jacket", "price" => 3999 },
|
||||
{ "title" => "Mountain boots", "price" => 3899 },
|
||||
{ "title" => "Safety helmet", "price" => 1999 }
|
||||
]
|
||||
|
||||
template = <<~LIQUID
|
||||
{%- assign index = products | find_index: 'price', 3999 -%}
|
||||
{{- index -}}
|
||||
LIQUID
|
||||
expected_output = "2"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => products })
|
||||
end
|
||||
|
||||
def test_find_index_with_deep_enumerables
|
||||
template = <<~LIQUID
|
||||
{%- assign index = products | find_index: 'title.content', 'Alpine jacket' -%}
|
||||
{{- index -}}
|
||||
LIQUID
|
||||
expected_output = "2"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
||||
end
|
||||
|
||||
def test_where
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
template = "{{ array | where: 'ok' | map: 'handle' | join: ' ' }}"
|
||||
expected_output = "alpha delta"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_where_with_value
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
template = "{{ array | where: 'ok', true | map: 'handle' | join: ' ' }}"
|
||||
expected_output = "alpha delta"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_where_with_false_value
|
||||
array = [
|
||||
{ "handle" => "alpha", "ok" => true },
|
||||
{ "handle" => "beta", "ok" => false },
|
||||
{ "handle" => "gamma", "ok" => false },
|
||||
{ "handle" => "delta", "ok" => true },
|
||||
]
|
||||
|
||||
template = "{{ array | where: 'ok', false | map: 'handle' | join: ' ' }}"
|
||||
expected_output = "beta gamma"
|
||||
|
||||
assert_template_result(expected_output, template, { "array" => array })
|
||||
end
|
||||
|
||||
def test_where_string_keys
|
||||
@@ -900,6 +1158,15 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_nil(@filters.where([nil], "ok"))
|
||||
end
|
||||
|
||||
def test_where_with_deep_enumerables
|
||||
template = <<~LIQUID
|
||||
{{- products | where: 'title.content', 'Pro goggles' | map: 'price.value' -}}
|
||||
LIQUID
|
||||
expected_output = "1299"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
||||
end
|
||||
|
||||
def test_all_filters_never_raise_non_liquid_exception
|
||||
test_drop = TestDrop.new(value: "test")
|
||||
test_drop.context = Context.new
|
||||
@@ -1051,6 +1318,15 @@ class StandardFiltersTest < Minitest::Test
|
||||
assert_template_result("0", "{{ input | sum: 'subtotal' }}", { "input" => input })
|
||||
end
|
||||
|
||||
def test_sum_with_deep_enumerables
|
||||
template = <<~LIQUID
|
||||
{{- products | sum: 'price.value' -}}
|
||||
LIQUID
|
||||
expected_output = "12495"
|
||||
|
||||
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_timezone(tz)
|
||||
|
||||
@@ -130,10 +130,6 @@ class VariableTest < Minitest::Test
|
||||
assert_template_result('bar', '{{ foo }}', { 'foo' => :bar })
|
||||
end
|
||||
|
||||
def test_nested_array
|
||||
assert_template_result('', '{{ foo }}', { 'foo' => [[nil]] })
|
||||
end
|
||||
|
||||
def test_dynamic_find_var
|
||||
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
|
||||
end
|
||||
|
||||
@@ -15,6 +15,11 @@ if (env_mode = ENV['LIQUID_PARSER_MODE'])
|
||||
end
|
||||
Liquid::Environment.default.error_mode = mode
|
||||
|
||||
if ENV['LIQUID_C'] == '1'
|
||||
puts "-- LIQUID C"
|
||||
require 'liquid/c'
|
||||
end
|
||||
|
||||
if Minitest.const_defined?('Test')
|
||||
# We're on Minitest 5+. Nothing to do here.
|
||||
else
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class EagerOptimizeTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_remove_empty_blocks
|
||||
source = <<~LIQUID.gsub(/\n/, '')
|
||||
{% for i in (1..1000000) %}
|
||||
{% endfor %}
|
||||
LIQUID
|
||||
|
||||
template = Liquid::Template.parse(source, eager_optimize: true)
|
||||
assert_equal(0, total_node_count(template))
|
||||
end
|
||||
|
||||
def test_remove_false_if_block
|
||||
source = <<~LIQUID.gsub(/\n/, '')
|
||||
{% if false %}
|
||||
{% if true %}
|
||||
{% if true %}
|
||||
{% if true %}
|
||||
{{ "Hello world!" }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
LIQUID
|
||||
|
||||
template = Liquid::Template.parse(source, eager_optimize: true)
|
||||
assert_equal(0, total_node_count(template))
|
||||
|
||||
source = <<~LIQUID.gsub(/\n/, '')
|
||||
{% if false %}
|
||||
{% for i in (1..1000000) %}
|
||||
{{ "Hello world!" }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
LIQUID
|
||||
|
||||
template = Liquid::Template.parse(source, eager_optimize: true)
|
||||
assert_equal(0, total_node_count(template))
|
||||
end
|
||||
|
||||
def test_remove_multiple_false_if_block
|
||||
source = <<~LIQUID.gsub(/\n/, '')
|
||||
{% if false %}
|
||||
{% if true %}
|
||||
{% if true %}
|
||||
{% if true %}
|
||||
{{ "Hello world!" }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
LIQUID
|
||||
|
||||
template = Liquid::Template.parse(source, eager_optimize: true)
|
||||
assert_equal(0, total_node_count(template))
|
||||
end
|
||||
|
||||
def test_merge_if_blocks
|
||||
# for now, work with consecutive if blocks without any String nodes in between
|
||||
source = <<~LIQUID.gsub(/\n/, '')
|
||||
{% if foo == 1 %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
{% if foo == 2 %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
{% if foo == 3 %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
LIQUID
|
||||
|
||||
assert_optimization([Liquid::If], source, { "foo" => nil })
|
||||
assert_optimization([Liquid::If], source, { "foo" => 1 })
|
||||
assert_optimization([Liquid::If], source, { "foo" => 2 })
|
||||
assert_optimization([Liquid::If], source, { "foo" => 5 })
|
||||
|
||||
source = <<~LIQUID.gsub(/\n/, '')
|
||||
{% assign bar = "application" %}
|
||||
{% if foo == 1 %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
{% if foo == 2 and bar contains "app" %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
{% if 3 == foo and bar == "application" %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
LIQUID
|
||||
|
||||
assert_optimization([Liquid::Assign, Liquid::If], source)
|
||||
end
|
||||
|
||||
def test_does_not_merge_if_blocks
|
||||
assert_optimization([Liquid::If, Liquid::If], <<~LIQUID.gsub(/\n/, ''))
|
||||
{% if foo == 1 %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
{% if k == 1 %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
LIQUID
|
||||
|
||||
assert_optimization([Liquid::If, Liquid::If], <<~LIQUID.gsub(/\n/, ''))
|
||||
{% if foo == 1 %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
{% if foo == 1 %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
LIQUID
|
||||
|
||||
assert_optimization([Liquid::If, Liquid::If], <<~LIQUID.gsub(/\n/, ''))
|
||||
{% if foo == 1 %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
{% if a == foo %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
LIQUID
|
||||
|
||||
assert_optimization([Liquid::If, Liquid::If], <<~LIQUID.gsub(/\n/, ''))
|
||||
{% if foo %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
{% if foo %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
LIQUID
|
||||
|
||||
assert_optimization([Liquid::If, Liquid::If], <<~LIQUID.gsub(/\n/, ''))
|
||||
{% if foo == 1 %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
{% if foo >= 1 %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
LIQUID
|
||||
|
||||
assert_optimization([Liquid::If, Liquid::If], <<~LIQUID.gsub(/\n/, ''))
|
||||
{% if foo == 1 %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
{% if 1 %}
|
||||
foo: {{ foo }}
|
||||
{% endif %}
|
||||
LIQUID
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_optimization(expected, source, context = { "foo" => 1 })
|
||||
template = Template.parse(source, eager_optimize: true)
|
||||
assert_equal(expected, template.root.nodelist.map(&:class),)
|
||||
|
||||
baseline_template = Template.parse(source, eager_optimize: false)
|
||||
|
||||
assert_equal(
|
||||
baseline_template.render(context),
|
||||
template.render(context),
|
||||
)
|
||||
end
|
||||
|
||||
def total_node_count(template)
|
||||
root = template.root
|
||||
children = root.nodelist
|
||||
count = 0
|
||||
|
||||
while children.any?
|
||||
next_children = []
|
||||
|
||||
children.each do |node|
|
||||
count += 1 unless node.is_a?(Liquid::BlockBody)
|
||||
next_children.concat(node.nodelist) if node.respond_to?(:nodelist) && node.nodelist
|
||||
end
|
||||
|
||||
children = next_children
|
||||
end
|
||||
|
||||
count
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user