mirror of
https://github.com/kemko/liquid.git
synced 2026-01-02 16:25:42 +03:00
Compare commits
6 Commits
resource-l
...
objects-op
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e853bf5b84 | ||
|
|
27248f1eb1 | ||
|
|
174839fbef | ||
|
|
01a86728f2 | ||
|
|
0e38f88b58 | ||
|
|
5a48edae6a |
1
Gemfile
1
Gemfile
@@ -2,6 +2,7 @@ source 'https://rubygems.org'
|
||||
|
||||
gemspec
|
||||
gem 'stackprof', platforms: :mri_21
|
||||
gem 'allocation_tracer', platforms: :mri_21
|
||||
|
||||
group :test do
|
||||
gem 'spy', '0.4.1'
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
## 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]
|
||||
@@ -10,8 +9,6 @@
|
||||
* 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]
|
||||
|
||||
14
Rakefile
14
Rakefile
@@ -71,6 +71,20 @@ 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"
|
||||
|
||||
@@ -68,7 +68,8 @@ module Liquid
|
||||
end
|
||||
|
||||
def render(context)
|
||||
output = []
|
||||
@output ||= []
|
||||
@output.clear
|
||||
context.resource_limits.render_score += @nodelist.length
|
||||
|
||||
@nodelist.each do |token|
|
||||
@@ -87,16 +88,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
|
||||
|
||||
@@ -13,14 +13,13 @@ module Liquid
|
||||
#
|
||||
# context['bob'] #=> nil class Context
|
||||
class Context
|
||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
||||
attr_reader :scopes, :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
|
||||
|
||||
@@ -30,10 +29,14 @@ module Liquid
|
||||
self.exception_handler = ->(e) { true }
|
||||
end
|
||||
|
||||
@interrupts = []
|
||||
@interrupts = nil
|
||||
@filters = []
|
||||
end
|
||||
|
||||
def errors
|
||||
@errors ||= []
|
||||
end
|
||||
|
||||
def strainer
|
||||
@strainer ||= Strainer.create(self, @filters)
|
||||
end
|
||||
@@ -50,17 +53,17 @@ module Liquid
|
||||
|
||||
# are there any not handled interrupts?
|
||||
def has_interrupt?
|
||||
!@interrupts.empty?
|
||||
@interrupts && !@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
|
||||
@interrupts.pop if @interrupts
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ module Liquid
|
||||
|
||||
class Include < Tag
|
||||
def render_with_profiling(context)
|
||||
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
|
||||
Profiler.profile_children(context.evaluate(@template_name).to_s) do
|
||||
render_without_profiling(context)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,11 +19,5 @@ 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
|
||||
|
||||
@@ -122,20 +122,6 @@ 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)
|
||||
@@ -324,10 +310,7 @@ module Liquid
|
||||
def to_date(obj)
|
||||
return obj if obj.respond_to?(:strftime)
|
||||
|
||||
if obj.is_a?(String)
|
||||
return nil if obj.empty?
|
||||
obj = obj.downcase
|
||||
end
|
||||
obj = obj.downcase if obj.is_a?(String)
|
||||
|
||||
case obj
|
||||
when 'now'.freeze, 'today'.freeze
|
||||
|
||||
@@ -25,9 +25,10 @@ module Liquid
|
||||
template_name = $1
|
||||
variable_name = $3
|
||||
|
||||
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
|
||||
@template_name_expr = Expression.parse(template_name)
|
||||
@attributes = {}
|
||||
@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 = {}
|
||||
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
@@ -43,15 +44,7 @@ module Liquid
|
||||
|
||||
def render(context)
|
||||
partial = load_cached_partial(context)
|
||||
|
||||
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
|
||||
variable = context.evaluate(@variable_name)
|
||||
|
||||
context.stack do
|
||||
@attributes.each do |key, value|
|
||||
@@ -60,11 +53,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
|
||||
@@ -73,7 +66,7 @@ module Liquid
|
||||
private
|
||||
def load_cached_partial(context)
|
||||
cached_partials = context.registers[:cached_partials] || {}
|
||||
template_name = context.evaluate(@template_name_expr)
|
||||
template_name = context.evaluate(@template_name)
|
||||
|
||||
if cached = cached_partials[template_name]
|
||||
return cached
|
||||
@@ -88,7 +81,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_expr))
|
||||
file_system.read_template_file(context.evaluate(@template_name))
|
||||
end
|
||||
|
||||
def pass_options
|
||||
|
||||
@@ -33,7 +33,8 @@ module Liquid
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
@filters = []
|
||||
@filters ||= []
|
||||
@filters.clear
|
||||
if markup =~ /(#{QuotedFragment})(.*)/om
|
||||
name_markup = $1
|
||||
filter_markup = $2
|
||||
@@ -52,7 +53,8 @@ module Liquid
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
@filters = []
|
||||
@filters ||= []
|
||||
@filters.clear
|
||||
p = Parser.new(markup)
|
||||
|
||||
@name = Expression.parse(p.expression)
|
||||
|
||||
@@ -10,21 +10,20 @@ 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
|
||||
|
||||
19
performance/memory.rb
Normal file
19
performance/memory.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
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
|
||||
@@ -74,34 +74,11 @@ 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
|
||||
@@ -159,10 +136,3 @@ 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
|
||||
|
||||
@@ -253,8 +253,6 @@ 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
|
||||
|
||||
@@ -219,12 +219,4 @@ 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
|
||||
|
||||
@@ -181,16 +181,6 @@ 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'
|
||||
|
||||
@@ -465,6 +465,7 @@ 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?
|
||||
|
||||
Reference in New Issue
Block a user