Compare commits

..

16 Commits

Author SHA1 Message Date
Justin Li
f2d760b0e9 Add to_hash for ResourceLimits for serialization 2015-05-12 14:27:26 -04:00
Justin Li
8a8996387b Update history to reflect merge of #554
[ci skip]
2015-05-12 13:20:06 -04:00
Justin Li
9310640bdd Merge pull request #554 from arthanzel/529-sort_natural
Merge pull request 554
2015-05-12 13:19:24 -04:00
Justin Li
4c3381a523 Update history to reflect merge of #559
[ci skip]
2015-05-12 10:59:58 -04:00
Justin Li
261aa2e726 Merge pull request #559 from Shopify/fix-include-var
Merge pull request 559
2015-05-12 10:50:13 -04:00
Justin Li
247c51ac70 Call Context#find_variable directly 2015-05-11 18:22:15 -04:00
Justin Li
37dbec3610 Remove unnecessary parse 2015-05-11 18:10:38 -04:00
Justin Li
ff253a04c6 Lazily evaluate template name for context variable injection 2015-05-11 18:01:24 -04:00
Justin Li
25ef0df671 Add tests for #461 2015-05-11 17:59:05 -04:00
Martin Hanzel
32460c255b Removed a few superfluous comments 2015-05-08 11:48:33 -04:00
Justin Li
724d625f47 Update history to reflect merge of #555 [ci skip] 2015-05-07 14:03:38 -04:00
Justin Li
f658dcee8b Merge pull request #555 from boobooninja/date_filter
Merge pull request 555
2015-05-07 13:59:22 -04:00
Loren Hale
fa6cd6287e date filter gracefully accepts empty string 2015-05-07 17:04:21 +08:00
Martin Hanzel
068791d698 Added method parens 2015-05-05 11:49:14 -04:00
Martin Hanzel
3a082ddbbd Changed sort_natural filter to use casecmp. Strings only. 2015-05-04 11:55:14 -04:00
Martin Hanzel
03b3446119 Resolves #529. Resolves #404. Added natural sorting filter and tests. 2015-05-03 20:55:28 -04:00
17 changed files with 111 additions and 68 deletions

View File

@@ -2,7 +2,6 @@ source 'https://rubygems.org'
gemspec
gem 'stackprof', platforms: :mri_21
gem 'allocation_tracer', platforms: :mri_21
group :test do
gem 'spy', '0.4.1'

View File

@@ -2,6 +2,7 @@
## 4.0.0 / not yet released / branch "master"
### Changed
* Add sort_natural filter (#554) [Martin Hanzel, arthanzel]
* Add forloop.parentloop as a reference to the parent loop (#520) [Justin Li, pushrax]
* Block parsing moved to BlockBody class (#458) [Dylan Thacker-Smith, dylanahsmith]
* Add concat filter to concatenate arrays (#429) [Diogo Beato, dvbeato]
@@ -9,6 +10,8 @@
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith, sunblaze]
### Fixed
* Fix naming of the "context variable" when dynamically including a template (#559) [Justin Li, pushrax]
* Gracefully accept empty strings in the date filter (#555) [Loren Hale, boobooninja]
* Fix capturing into variables with a hyphen in the name (#505) [Florian Weingarten, fw42]
* Fix case sensitivity regression in date standard filter (#499) [Kelley Reynolds, kreynolds]
* Disallow filters with no variable in strict mode (#475) [Justin Li, pushrax]

View File

@@ -71,20 +71,6 @@ namespace :profile do
end
namespace :memory do
desc "Run the liquid memory tracer"
task :run do
ruby "./performance/memory.rb"
end
desc "Run the liquid memory tracer with strict parsing"
task :strict do
ruby "./performance/memory.rb strict"
end
end
desc "Run example"
task :example do
ruby "-w -d -Ilib example/server/server.rb"

View File

@@ -68,8 +68,7 @@ module Liquid
end
def render(context)
@output ||= []
@output.clear
output = []
context.resource_limits.render_score += @nodelist.length
@nodelist.each do |token|
@@ -88,16 +87,16 @@ module Liquid
token_output = render_token(token, context)
unless token.is_a?(Block) && token.blank?
@output << token_output
output << token_output
end
rescue MemoryError => e
raise e
rescue ::StandardError => e
@output << context.handle_error(e, token)
output << context.handle_error(e, token)
end
end
@output.join
output.join
end
private

View File

@@ -13,13 +13,14 @@ module Liquid
#
# context['bob'] #=> nil class Context
class Context
attr_reader :scopes, :registers, :environments, :resource_limits
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_handler
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
squash_instance_assigns_with_environments
@@ -29,14 +30,10 @@ module Liquid
self.exception_handler = ->(e) { true }
end
@interrupts = nil
@interrupts = []
@filters = []
end
def errors
@errors ||= []
end
def strainer
@strainer ||= Strainer.create(self, @filters)
end
@@ -53,17 +50,17 @@ module Liquid
# are there any not handled interrupts?
def has_interrupt?
@interrupts && !@interrupts.empty?
!@interrupts.empty?
end
# push an interrupt to the stack. this interrupt is considered not handled.
def push_interrupt(e)
(@interrupts ||= []).push(e)
@interrupts.push(e)
end
# pop an interrupt from the stack
def pop_interrupt
@interrupts.pop if @interrupts
@interrupts.pop
end

View File

@@ -12,7 +12,7 @@ module Liquid
class Include < Tag
def render_with_profiling(context)
Profiler.profile_children(context.evaluate(@template_name).to_s) do
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
render_without_profiling(context)
end
end

View File

@@ -19,5 +19,11 @@ module Liquid
def reset
@render_length = @render_score = @assign_score = 0
end
def to_hash
instance_variables.each_with_object({}) do |key, hash|
hash[key.to_s[1..-1].to_sym] = instance_variable_get(key)
end
end
end
end

View File

@@ -122,6 +122,20 @@ module Liquid
end
end
# 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)
if property.nil?
ary.sort {|a,b| a.casecmp(b) }
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort {|a,b| a[property].casecmp(b[property]) }
elsif ary.first.respond_to?(property)
ary.sort {|a,b| a.send(property).casecmp(b.send(property)) }
end
end
# Remove duplicate elements from an array
# provide optional property with which to determine uniqueness
def uniq(input, property = nil)
@@ -310,7 +324,10 @@ module Liquid
def to_date(obj)
return obj if obj.respond_to?(:strftime)
obj = obj.downcase if obj.is_a?(String)
if obj.is_a?(String)
return nil if obj.empty?
obj = obj.downcase
end
case obj
when 'now'.freeze, 'today'.freeze

View File

@@ -25,10 +25,9 @@ module Liquid
template_name = $1
variable_name = $3
@variable_name = Expression.parse(variable_name || template_name[1..-2])
@context_variable_name = template_name[1..-2].split('/'.freeze).last
@template_name = Expression.parse(template_name)
@attributes = {}
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = Expression.parse(template_name)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value)
@@ -44,7 +43,15 @@ module Liquid
def render(context)
partial = load_cached_partial(context)
variable = context.evaluate(@variable_name)
template_name = context.evaluate(@template_name_expr)
context_variable_name = template_name.split('/'.freeze).last
variable = if @variable_name_expr
context.evaluate(@variable_name_expr)
else
context.find_variable(template_name)
end
context.stack do
@attributes.each do |key, value|
@@ -53,11 +60,11 @@ module Liquid
if variable.is_a?(Array)
variable.collect do |var|
context[@context_variable_name] = var
context[context_variable_name] = var
partial.render(context)
end
else
context[@context_variable_name] = variable
context[context_variable_name] = variable
partial.render(context)
end
end
@@ -66,7 +73,7 @@ module Liquid
private
def load_cached_partial(context)
cached_partials = context.registers[:cached_partials] || {}
template_name = context.evaluate(@template_name)
template_name = context.evaluate(@template_name_expr)
if cached = cached_partials[template_name]
return cached
@@ -81,7 +88,7 @@ module Liquid
def read_template_from_file_system(context)
file_system = context.registers[:file_system] || Liquid::Template.file_system
file_system.read_template_file(context.evaluate(@template_name))
file_system.read_template_file(context.evaluate(@template_name_expr))
end
def pass_options

View File

@@ -33,8 +33,7 @@ module Liquid
end
def lax_parse(markup)
@filters ||= []
@filters.clear
@filters = []
if markup =~ /(#{QuotedFragment})(.*)/om
name_markup = $1
filter_markup = $2
@@ -53,8 +52,7 @@ module Liquid
end
def strict_parse(markup)
@filters ||= []
@filters.clear
@filters = []
p = Parser.new(markup)
@name = Expression.parse(p.expression)

View File

@@ -10,20 +10,21 @@ module Liquid
end
def initialize(markup)
@lookups = markup.scan(VariableParser)
lookups = markup.scan(VariableParser)
name = @lookups.shift
name = lookups.shift
if name =~ SQUARE_BRACKETED
name = Expression.parse($1)
end
@name = name
@lookups = lookups
@command_flags = 0
@lookups.each_index do |i|
lookup = @lookups[i]
lookup = lookups[i]
if lookup =~ SQUARE_BRACKETED
@lookups[i] = Expression.parse($1)
lookups[i] = Expression.parse($1)
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i
end

View File

@@ -1,19 +0,0 @@
at_exit do
p 'Objects distribution:'
require 'pp'
pp ObjectSpace.count_objects
end
require 'allocation_tracer' rescue fail("install allocation_tracer extension/gem")
require File.dirname(__FILE__) + '/theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
require 'allocation_tracer/trace'
puts "Profiling memory usage..."
200.times do
profiler.run
end

View File

@@ -74,11 +74,34 @@ class FiltersTest < Minitest::Test
@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], Variable.new("numbers | sort").render(@context)
assert_equal ['alphabetic', 'as', 'expected'], Variable.new("words | sort").render(@context)
assert_equal [3], Variable.new("value | sort").render(@context)
assert_equal ['are', 'flower'], Variable.new("arrays | sort").render(@context)
assert_equal ['Expected', 'case', 'sensitive'], Variable.new("case_sensitive | sort").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_equal ['Assert', 'case', 'Insensitive'], Variable.new("words | sort_natural").render(@context)
# Test hashes
sorted = Variable.new("hashes | sort_natural: 'a'").render(@context)
assert_equal sorted[0]['a'], 'A'
assert_equal sorted[1]['a'], 'b'
assert_equal sorted[2]['a'], 'C'
# Test objects
sorted = Variable.new("objects | sort_natural: 'a'").render(@context)
assert_equal sorted[0].a, 'A'
assert_equal sorted[1].a, 'b'
assert_equal sorted[2].a, 'C'
end
def test_strip_html
@@ -136,3 +159,10 @@ class FiltersInTemplate < Minitest::Test
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter])
end
end # FiltersTest
class TestObject
attr_accessor :a
def initialize(a)
@a = a
end
end

View File

@@ -253,6 +253,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal nil, @filters.date(nil, "%B")
assert_equal '', @filters.date('', "%B")
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
end

View File

@@ -219,4 +219,12 @@ class IncludeTagTest < Minitest::Test
assert_equal 'x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}')
end
end
def test_including_via_variable_value
assert_template_result "from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}"
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 for foo %}", "foo" => {'title' => 'Draft 151cm'}
end
end # IncludeTagTest

View File

@@ -181,6 +181,16 @@ class TemplateTest < Minitest::Test
assert context.resource_limits.render_length > 0
end
def test_resource_limits_serialization
t = Template.parse("{% if true %}aaaa{% endif %}")
t.resource_limits.render_score_limit = 50
t.resource_limits.render_length_limit = 50
t.resource_limits.assign_score_limit = 50
expected = {render_score: 0, render_length: 0, assign_score: 0, render_score_limit: 50, render_length_limit: 50, assign_score_limit: 50}
assert_equal expected, t.resource_limits.to_hash
end
def test_can_use_drop_as_context
t = Template.new
t.registers['lulz'] = 'haha'

View File

@@ -465,7 +465,6 @@ class ContextUnitTest < Minitest::Test
mock_empty = Spy.on_instance_method(Array, :empty?)
mock_has_interrupt = Spy.on(@context, :has_interrupt?).and_call_through
@context.push_interrupt(StandardError.new)
@context.has_interrupt?
refute mock_any.has_been_called?