Compare commits

..

2 Commits

Author SHA1 Message Date
Dylan Thacker-Smith
6a9a7ae8d2 Use tokenizer-quoted-strings liquid-c branch. 2015-07-13 22:03:16 -04:00
Dylan Thacker-Smith
4a12fee1f5 Allow variable and tag end characters to be quoted. 2015-07-13 13:19:56 -04:00
37 changed files with 197 additions and 635 deletions

View File

@@ -118,8 +118,6 @@ Style/ClassVars:
Style/PerlBackrefs:
Enabled: false
Style/TrivialAccessors:
AllowPredicates: true
Style/WordArray:
Enabled: false

View File

@@ -10,11 +10,6 @@ rvm:
sudo: false
addons:
apt:
packages:
- libgmp3-dev
matrix:
allow_failures:
- rvm: jruby-head

View File

@@ -4,22 +4,23 @@
* Bugfixes
* Performance improvements
* Features that are likely to be useful to the majority of Liquid users
* Features which are likely to be useful to the majority of Liquid users
## Things we won't merge
* Code that introduces considerable performance degrations
* Code that touches performance-critical parts of Liquid and comes without benchmarks
* Features that are not important for most people (we want to keep the core Liquid code small and tidy)
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code that does not include tests
* Code that breaks existing tests
* Code which introduces considerable performance degrations
* Code which touches performance critical parts of Liquid and comes without benchmarks
* Features which are not important for most people (we want to keep the core Liquid code small and tidy)
* Features which can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code which comes without tests
* Code which breaks existing tests
## Workflow
* Fork the Liquid repository
* Create a new branch in your fork
* If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass (`bundle exec rake`)
* If it makes sense, add tests for your code and run a performance benchmark
* Make sure all tests pass
* Create a pull request
* In the description, ping one of [@boourns](https://github.com/boourns), [@fw42](https://github.com/fw42), [@camilo](https://github.com/camilo), [@dylanahsmith](https://github.com/dylanahsmith), or [@arthurnn](https://github.com/arthurnn) and ask for a code review.

View File

@@ -6,9 +6,9 @@ gem 'stackprof', platforms: :mri_21
group :test do
gem 'spy', '0.4.1'
gem 'benchmark-ips'
gem 'rubocop', '0.34.2'
gem 'rubocop', '>=0.32.0'
platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '2570693d8d03faa0df9160ec74348a7149436df3'
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '11d38237d9f491588a58c83dc3d364a7d0d1d55b'
end
end

View File

@@ -3,11 +3,6 @@
## 4.0.0 / not yet released / branch "master"
### Changed
* Add strict_variables and strict_filters options to detect undefined references (#691)
* Improve loop performance (#681) [Florian Weingarten]
* Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal]
* Add url_decode filter to invert url_encode (#645) [Larry Archer]
* Add global_filter to apply a filter to all output (#610) [Loren Hale]
* Add compact filter (#600) [Carson Reinke]
* Rename deprecated "has_key?" and "has_interrupt?" methods (#593) [Florian Weingarten]
* Include template name with line numbers in render errors (574) [Dylan Thacker-Smith]
@@ -18,14 +13,8 @@
* Ruby 1.9 support dropped (#491) [Justin Li]
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
* Remove support for `liquid_methods`
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
### Fixed
* Fix map filter when value is a Proc (#672) [Guillaume Malette]
* Fix truncate filter when value is not a string (#672) [Guillaume Malette]
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
* Fix sort filter behaviour with empty array input (#652) [Marcel Cary]
* Fix test failure under certain timezones (#631) [Dylan Thacker-Smith]
* Fix bug in uniq filter (#595) [Florian Weingarten]
* Fix bug when "blank" and "empty" are used as variable names (#592) [Florian Weingarten]
* Fix condition parse order in strict mode (#569) [Justin Li]
@@ -37,15 +26,7 @@
* Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li]
* Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li]
## 3.0.5 / 2015-07-23 / branch "3-0-stable"
* Fix test failure under certain timezones [Dylan Thacker-Smith]
## 3.0.4 / 2015-07-17
* Fix chained access to multi-dimensional hashes [Florian Weingarten]
## 3.0.3 / 2015-05-28
## 3.0.3 / 2015-05-28 / branch "3-0-stable"
* Fix condition parse order in strict mode (#569) [Justin Li]
@@ -93,15 +74,7 @@
* Make map filter work on enumerable drops (#233) [Florian Weingarten]
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten]
## 2.6.3 / 2015-07-23 / branch "2-6-stable"
* Fix test failure under certain timezones [Dylan Thacker-Smith]
## 2.6.2 / 2015-01-23
* Remove duplicate hash key [Parker Moore]
## 2.6.1 / 2014-01-10
## 2.6.1 / 2014-01-10 / branch "2-6-stable"
Security fix, cherry-picked from master (4e14a65):
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]

View File

@@ -73,34 +73,3 @@ This is useful for doing things like enabling strict mode only in the theme edit
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
### Undefined variables and filters
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance.
Here are some examples:
```ruby
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
#=> '1 2 ' # when a variable is undefined, it's rendered as nil
template.errors
#=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
```
```ruby
template = Liquid::Template.parse("{{x | filter1 | upcase}}")
template.render({ 'x' => 'foo' }, { strict_filters: true })
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
template.errors
#=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
```
If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method:
```ruby
template = Liquid::Template.parse("{{x}} {{y}}")
template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
```

View File

@@ -35,7 +35,8 @@ module Liquid
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /\{\{|\{\%/
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
tag_contents = /(?:#{QuotedString}|.)*?/m
PartialTemplateParser = /#{TagStart}#{tag_contents}#{TagEnd}|#{VariableStart}#{tag_contents}#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
@@ -48,8 +49,6 @@ require 'liquid/lexer'
require 'liquid/parser'
require 'liquid/i18n'
require 'liquid/drop'
require 'liquid/tablerowloop_drop'
require 'liquid/forloop_drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'

View File

@@ -76,9 +76,6 @@ module Liquid
end
rescue MemoryError => e
raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, token.line_number)
output << nil
rescue ::StandardError => e
output << context.handle_error(e, token.line_number)
end

View File

@@ -13,7 +13,7 @@ module Liquid
# context['bob'] #=> nil class Context
class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_handler, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
attr_accessor :exception_handler, :template_name, :partial
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten
@@ -32,7 +32,6 @@ module Liquid
@interrupts = []
@filters = []
@global_filter = nil
end
def warnings
@@ -53,10 +52,6 @@ module Liquid
@strainer = nil
end
def apply_global_filter(obj)
global_filter.nil? ? obj : global_filter.call(obj)
end
# are there any not handled interrupts?
def interrupt?
!@interrupts.empty?
@@ -205,13 +200,7 @@ module Liquid
end
def lookup_and_evaluate(obj, key)
if @strict_variables && obj.respond_to?(:key?) && !obj.key?(key)
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
value = obj[key]
if value.is_a?(Proc) && obj.respond_to?(:[]=)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
else
value

View File

@@ -18,23 +18,24 @@ module Liquid
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
#
# Your drop can either implement the methods sans any parameters
# or implement the liquid_method_missing(name) method which is a catch all.
# Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
# catch all.
class Drop
attr_writer :context
EMPTY_STRING = ''.freeze
# Catch all for the method
def liquid_method_missing(method)
return nil unless @context.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
def before_method(_method)
nil
end
# called by liquid to invoke a drop
def invoke_drop(method_or_key)
if self.class.invokable?(method_or_key)
if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
send(method_or_key)
else
liquid_method_missing(method_or_key)
before_method(method_or_key)
end
end

View File

@@ -56,8 +56,4 @@ module Liquid
MemoryError = Class.new(Error)
ZeroDivisionError = Class.new(Error)
FloatDomainError = Class.new(Error)
UndefinedVariable = Class.new(Error)
UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error)
end

View File

@@ -7,44 +7,44 @@ class String # :nodoc:
end
end
class Array # :nodoc:
class Array # :nodoc:
def to_liquid
self
end
end
class Hash # :nodoc:
class Hash # :nodoc:
def to_liquid
self
end
end
class Numeric # :nodoc:
class Numeric # :nodoc:
def to_liquid
self
end
end
class Time # :nodoc:
class Time # :nodoc:
def to_liquid
self
end
end
class DateTime < Date # :nodoc:
class DateTime < Date # :nodoc:
def to_liquid
self
end
end
class Date # :nodoc:
class Date # :nodoc:
def to_liquid
self
end
end
class TrueClass
def to_liquid # :nodoc:
def to_liquid # :nodoc:
self
end
end

View File

@@ -1,42 +0,0 @@
module Liquid
class ForloopDrop < Drop
def initialize(name, length, parentloop)
@name = name
@length = length
@parentloop = parentloop
@index = 0
end
attr_reader :name, :length, :parentloop
def index
@index + 1
end
def index0
@index
end
def rindex
@length - @index
end
def rindex0
@length - @index - 1
end
def first
@index == 0
end
def last
@index == @length - 1
end
protected
def increment!
@index += 1
end
end
end

View File

@@ -75,7 +75,7 @@ module Liquid
def variable_signature
str = consume(:id)
while look(:open_square)
if look(:open_square)
str << consume
str << expression
str << consume(:close_square)

View File

@@ -33,7 +33,7 @@ module Liquid
end
def escape(input)
CGI.escapeHTML(input).untaint unless input.nil?
CGI.escapeHTML(input).untaint
end
alias_method :h, :escape
@@ -42,11 +42,7 @@ module Liquid
end
def url_encode(input)
CGI.escape(input) unless input.nil?
end
def url_decode(input)
CGI.unescape(input) unless input.nil?
CGI.escape(input) rescue input
end
def slice(input, offset, length = nil)
@@ -63,11 +59,10 @@ module Liquid
# Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...".freeze)
return if input.nil?
input_str = input.to_s
length = Utils.to_integer(length)
l = length - truncate_string.length
l = 0 if l < 0
input_str.length > length ? input_str[0...l] + truncate_string : input_str
input.length > length ? input[0...l] + truncate_string : input
end
def truncatewords(input, words = 15, truncate_string = "...".freeze)
@@ -121,10 +116,10 @@ module Liquid
ary = InputIterator.new(input)
if property.nil?
ary.sort
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort { |a, b| a[property] <=> b[property] }
elsif ary.first.respond_to?(property)
ary.sort { |a, b| a.send(property) <=> b.send(property) }
end
end
@@ -135,10 +130,10 @@ module Liquid
if property.nil?
ary.sort { |a, b| a.casecmp(b) }
elsif ary.empty? # The next two cases assume a non-empty array.
[]
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
@@ -149,8 +144,6 @@ module Liquid
if property.nil?
ary.uniq
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
ary.uniq{ |a| a[property] }
end
@@ -170,8 +163,7 @@ module Liquid
if property == "to_liquid".freeze
e
elsif e.respond_to?(:[])
r = e[property]
r.is_a?(Proc) ? r.call : r
e[property]
end
end
end
@@ -183,10 +175,10 @@ module Liquid
if property.nil?
ary.compact
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
ary.reject{ |a| a[property].nil? }
elsif ary.first.respond_to?(property)
ary.reject { |a| a.send(property).nil? }
end
end
@@ -335,12 +327,9 @@ module Liquid
raise Liquid::FloatDomainError, e.message
end
def default(input, default_value = ''.freeze)
if !input || input.respond_to?(:empty?) && input.empty?
default_value
else
input
end
def default(input, default_value = "".freeze)
is_blank = input.respond_to?(:empty?) ? input.empty? : !input
is_blank ? default_value : input
end
private
@@ -385,11 +374,6 @@ module Liquid
to_a.compact
end
def empty?
@input.each { return false }
true
end
def each
@input.each do |e|
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)

View File

@@ -26,15 +26,10 @@ module Liquid
end
def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
raise ArgumentError, "Expected module but got: #{f.class}" unless filter.is_a?(Module)
unless self.class.include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
else
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
end
@@ -53,13 +48,11 @@ module Liquid
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
elsif @context && @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
end
rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message, e.backtrace
raise Liquid::ArgumentError.new(e.message)
end
end
end

View File

@@ -1,62 +0,0 @@
module Liquid
class TablerowloopDrop < Drop
def initialize(length, cols)
@length = length
@row = 1
@col = 1
@cols = cols
@index = 0
end
attr_reader :length, :col, :row
def index
@index + 1
end
def index0
@index
end
def col0
@col - 1
end
def rindex
@length - @index
end
def rindex0
@length - @index - 1
end
def first
@index == 0
end
def last
@index == @length - 1
end
def col_first
@col == 1
end
def col_last
@col == @cols
end
protected
def increment!
@index += 1
if @col == @cols
@col = 1
@row += 1
else
@col += 1
end
end
end
end

View File

@@ -23,28 +23,16 @@ module Liquid
def render(context)
val = @from.render(context)
context.scopes.last[@to] = val
context.resource_limits.assign_score += assign_score_of(val)
inc = val.instance_of?(String) || val.instance_of?(Array) || val.instance_of?(Hash) ? val.length : 1
context.resource_limits.assign_score += inc
''.freeze
end
def blank?
true
end
private
def assign_score_of(val)
if val.instance_of?(String)
val.length
elsif val.instance_of?(Array) || val.instance_of?(Hash)
sum = 1
# Uses #each to avoid extra allocations.
val.each { |child| sum += assign_score_of(child) }
sum
else
1
end
end
end
Template.register_tag('assign'.freeze, Assign)

View File

@@ -37,7 +37,7 @@ module Liquid
iteration = context.registers[:cycle][key]
result = context.evaluate(@variables[iteration])
iteration += 1
iteration = 0 if iteration >= @variables.size
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
result
end

View File

@@ -67,13 +67,69 @@ module Liquid
end
def render(context)
segment = collection_segment(context)
for_offsets = context.registers[:for] ||= Hash.new(0)
for_stack = context.registers[:for_stack] ||= []
if segment.empty?
render_else(context)
parent_loop = for_stack.last
for_stack.push(nil)
collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range)
from = if @from == :continue
for_offsets[@name].to_i
else
render_segment(context, segment)
context.evaluate(@from).to_i
end
limit = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to)
return render_else(context) if segment.empty?
segment.reverse! if @reversed
result = ''
length = segment.length
# Store our progress through the collection for the continue flag
for_offsets[@name] = from + segment.length
context.stack do
segment.each_with_index do |item, index|
context[@variable_name] = item
loop_vars = {
'name'.freeze => @name,
'length'.freeze => length,
'index'.freeze => index + 1,
'index0'.freeze => index,
'rindex'.freeze => length - index,
'rindex0'.freeze => length - index - 1,
'first'.freeze => (index == 0),
'last'.freeze => (index == length - 1),
'parentloop'.freeze => parent_loop
}
context['forloop'.freeze] = loop_vars
for_stack[-1] = loop_vars
result << @for_block.render(context)
# Handle any interrupts if they exist.
if context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
end
end
result
ensure
for_stack.pop
end
protected
@@ -96,7 +152,7 @@ module Liquid
def strict_parse(markup)
p = Parser.new(markup)
@variable_name = p.consume(:id)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
collection_name = p.expression
@name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name)
@@ -114,63 +170,6 @@ module Liquid
private
def collection_segment(context)
offsets = context.registers[:for] ||= Hash.new(0)
from = if @from == :continue
offsets[@name].to_i
else
context.evaluate(@from).to_i
end
collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range)
limit = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed
offsets[@name] = from + segment.length
segment
end
def render_segment(context, segment)
for_stack = context.registers[:for_stack] ||= []
length = segment.length
result = ''
context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
for_stack.push(loop_vars)
begin
context['forloop'.freeze] = loop_vars
segment.each_with_index do |item, index|
context[@variable_name] = item
result << @for_block.render(context)
loop_vars.send(:increment!)
# Handle any interrupts if they exist.
if context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
end
ensure
for_stack.pop
end
end
result
end
def set_attribute(key, expr)
case key
when 'offset'.freeze

View File

@@ -28,21 +28,36 @@ module Liquid
cols = context.evaluate(@attributes['cols'.freeze]).to_i
row = 1
col = 0
result = "<tr class=\"row1\">\n"
context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'.freeze] = tablerowloop
collection.each_with_index do |item, index|
context[@variable_name] = item
context['tablerowloop'.freeze] = {
'length'.freeze => length,
'index'.freeze => index + 1,
'index0'.freeze => index,
'col'.freeze => col + 1,
'col0'.freeze => col,
'rindex'.freeze => length - index,
'rindex0'.freeze => length - index - 1,
'first'.freeze => (index == 0),
'last'.freeze => (index == length - 1),
'col_first'.freeze => (col == 0),
'col_last'.freeze => (col == cols - 1)
}
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
col += 1
if tablerowloop.col_last && !tablerowloop.last
result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
result << "<td class=\"col#{col}\">" << super << '</td>'
if col == cols && (index != length - 1)
col = 0
row += 1
result << "</tr>\n<tr class=\"row#{row}\">"
end
tablerowloop.send(:increment!)
end
end
result << "</tr>\n"

View File

@@ -179,10 +179,20 @@ module Liquid
when Hash
options = args.pop
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
if options[:registers].is_a?(Hash)
registers.merge!(options[:registers])
end
apply_options_to_context(context, options)
when Module, Array
if options[:filters]
context.add_filters(options[:filters])
end
if options[:exception_handler]
context.exception_handler = options[:exception_handler]
end
when Module
context.add_filters(args.pop)
when Array
context.add_filters(args.pop)
end
@@ -230,13 +240,5 @@ module Liquid
yield
end
end
def apply_options_to_context(context, options)
context.add_filters(options[:filters]) if options[:filters]
context.global_filter = options[:global_filter] if options[:global_filter]
context.exception_handler = options[:exception_handler] if options[:exception_handler]
context.strict_variables = options[:strict_variables] if options[:strict_variables]
context.strict_filters = options[:strict_filters] if options[:strict_filters]
end
end
end

View File

@@ -72,7 +72,7 @@ module Liquid
when String
Time.parse(obj)
end
rescue ::ArgumentError
rescue ArgumentError
nil
end
end

View File

@@ -73,16 +73,10 @@ module Liquid
end
def render(context)
obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
@filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
context.invoke(filter_name, output, *filter_args)
end
obj = context.apply_global_filter(obj)
taint_check(context, obj)
obj
end.tap{ |obj| taint_check(context, obj) }
end
private

View File

@@ -55,11 +55,9 @@ module Liquid
object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil or
# raise an exception if `strict_variables` option is set to true
# keywords either. The only thing we got left is to return nil
else
return nil unless context.strict_variables
raise Liquid::UndefinedVariable, "undefined variable #{key}"
return nil
end
# If we are dealing with a drop here we have to

View File

@@ -1,4 +1,4 @@
# encoding: utf-8
module Liquid
VERSION = "4.0.0.rc1"
VERSION = "4.0.0.alpha"
end

View File

@@ -18,9 +18,9 @@ Gem::Specification.new do |s|
s.required_rubygems_version = ">= 1.3.7"
s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md)
s.extra_rdoc_files = ["History.md", "README.md"]
s.extra_rdoc_files = ["History.md", "README.md"]
s.require_path = "lib"

View File

@@ -13,7 +13,7 @@ class ContextDrop < Liquid::Drop
@context['forloop.index']
end
def liquid_method_missing(method)
def before_method(method)
@context[method]
end
end
@@ -30,8 +30,8 @@ class ProductDrop < Liquid::Drop
end
class CatchallDrop < Liquid::Drop
def liquid_method_missing(method)
'catchall_method: ' << method.to_s
def before_method(method)
'method: ' << method.to_s
end
end
@@ -59,7 +59,7 @@ class ProductDrop < Liquid::Drop
end
class EnumerableDrop < Liquid::Drop
def liquid_method_missing(method)
def before_method(method)
method
end
@@ -93,7 +93,7 @@ end
class RealEnumerableDrop < Liquid::Drop
include Enumerable
def liquid_method_missing(method)
def before_method(method)
method
end
@@ -157,14 +157,14 @@ class DropsTest < Minitest::Test
assert_equal ' text1 ', output
end
def test_catchall_unknown_method
def test_unknown_method
output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
assert_equal ' catchall_method: unknown ', output
assert_equal ' method: unknown ', output
end
def test_catchall_integer_argument_drop
def test_integer_argument_drop
output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
assert_equal ' catchall_method: 8 ', output
assert_equal ' method: 8 ', output
end
def test_text_array_drop
@@ -231,7 +231,7 @@ class DropsTest < Minitest::Test
assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new)
end
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
def test_enumerable_drop_will_invoke_before_method_for_clashing_method_names
["select", "each", "map", "cycle"].each do |method|
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)

View File

@@ -43,14 +43,6 @@ class OutputTest < Minitest::Test
assert_equal expected, Template.parse(text).render!(@assigns)
end
def test_variable_traversing_with_two_brackets
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" }
)
end
def test_variable_traversing
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )

View File

@@ -118,7 +118,6 @@ class StandardFiltersTest < Minitest::Test
def test_escape
assert_equal '&lt;strong&gt;', @filters.escape('<strong>')
assert_equal nil, @filters.escape(nil)
assert_equal '&lt;strong&gt;', @filters.h('<strong>')
end
@@ -131,13 +130,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal nil, @filters.url_encode(nil)
end
def test_url_decode
assert_equal 'foo bar', @filters.url_decode('foo+bar')
assert_equal 'foo bar', @filters.url_decode('foo%20bar')
assert_equal 'foo+1@example.com', @filters.url_decode('foo%2B1%40example.com')
assert_equal nil, @filters.url_decode(nil)
end
def test_truncatewords
assert_equal 'one two three', @filters.truncatewords('one two three', 4)
assert_equal 'one two...', @filters.truncatewords('one two three', 2)
@@ -166,14 +158,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
end
def test_sort_empty_array
assert_equal [], @filters.sort([], "a")
end
def test_sort_natural_empty_array
assert_equal [], @filters.sort_natural([], "a")
end
def test_legacy_sort_hash
assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 })
end
@@ -193,14 +177,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test')
end
def test_uniq_empty_array
assert_equal [], @filters.uniq([], "a")
end
def test_compact_empty_array
assert_equal [], @filters.compact([], "a")
end
def test_reverse
assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4])
end
@@ -249,19 +225,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result "testfoo", templ, "procs" => [p]
end
def test_map_over_drops_returning_procs
drops = [
{
"proc" => ->{ "foo" },
},
{
"proc" => ->{ "bar" },
},
]
templ = '{{ drops | map: "proc" }}'
assert_template_result "foobar", templ, "drops" => drops
end
def test_map_works_on_enumerables
assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new
end
@@ -275,10 +238,6 @@ class StandardFiltersTest < Minitest::Test
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
end
def test_date
assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B")
assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B")
@@ -304,10 +263,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal '', @filters.date('', "%B")
with_timezone("UTC") do
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
end
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
end
def test_first_last
@@ -460,19 +417,4 @@ class StandardFiltersTest < Minitest::Test
def test_cannot_access_private_methods
assert_template_result('a', "{{ 'a' | to_number }}")
end
def test_date_raises_nothing
assert_template_result('', "{{ '' | date: '%D' }}")
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
end
private
def with_timezone(tz)
old_tz = ENV['TZ']
ENV['TZ'] = tz
yield
ensure
ENV['TZ'] = old_tz
end
end # StandardFiltersTest

View File

@@ -0,0 +1,13 @@
require 'test_helper'
class AssignTagTest < Minitest::Test
include Liquid
def test_assign
assert_template_result('monkey', "{% assign foo = 'monkey' %}{{ foo }}")
end
def test_string_with_end_tag
assert_template_result("{% quoted %}", "{% assign string = '{% quoted %}' %}{{ string }}")
end
end

View File

@@ -29,10 +29,10 @@ class IfElseTagTest < Minitest::Test
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('', '{% 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('', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => false)
end
def test_if_or_with_operators

View File

@@ -2,7 +2,7 @@ require 'test_helper'
require 'timeout'
class TemplateContextDrop < Liquid::Drop
def liquid_method_missing(method)
def before_method(method)
method
end
@@ -27,12 +27,6 @@ class ErroneousDrop < Liquid::Drop
end
end
class DropWithUndefinedMethod < Liquid::Drop
def foo
'foo'
end
end
class TemplateTest < Minitest::Test
include Liquid
@@ -139,17 +133,6 @@ class TemplateTest < Minitest::Test
refute_nil t.resource_limits.assign_score
end
def test_resource_limits_assign_score_nested
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
t.resource_limits.assign_score_limit = 3
assert_equal "Liquid error: Memory limits exceeded", t.render
assert t.resource_limits.reached?
t.resource_limits.assign_score_limit = 5
assert_equal "", t.render!
end
def test_resource_limits_aborts_rendering_after_first_error
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
t.resource_limits.render_score_limit = 50
@@ -228,82 +211,4 @@ class TemplateTest < Minitest::Test
end
assert exception.is_a?(Liquid::ZeroDivisionError)
end
def test_global_filter_option_on_render
global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse("{{name}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal 'bob filtered', rendered_template
end
def test_global_filter_option_when_native_filters_exist
global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse("{{name | upcase}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal 'BOB filtered', rendered_template
end
def test_undefined_variables
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
assert_equal '33 32 ', result
assert_equal 3, t.errors.count
assert_instance_of Liquid::UndefinedVariable, t.errors[0]
assert_equal 'Liquid error: undefined variable y', t.errors[0].message
assert_instance_of Liquid::UndefinedVariable, t.errors[1]
assert_equal 'Liquid error: undefined variable b', t.errors[1].message
assert_instance_of Liquid::UndefinedVariable, t.errors[2]
assert_equal 'Liquid error: undefined variable d', t.errors[2].message
end
def test_undefined_variables_raise
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
assert_raises UndefinedVariable do
t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
end
end
def test_undefined_drop_methods
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
result = t.render(d, { strict_variables: true })
assert_equal 'foo ', result
assert_equal 1, t.errors.count
assert_instance_of Liquid::UndefinedDropMethod, t.errors[0]
end
def test_undefined_drop_methods_raise
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
assert_raises UndefinedDropMethod do
t.render!(d, { strict_variables: true })
end
end
def test_undefined_filters
t = Template.parse("{{a}} {{x | upcase | somefilter1 | somefilter2 | somefilter3}}")
filters = Module.new do
def somefilter3(v)
"-#{v}-"
end
end
result = t.render({ 'a' => 123, 'x' => 'foo' }, { filters: [filters], strict_filters: true })
assert_equal '123 ', result
assert_equal 1, t.errors.count
assert_instance_of Liquid::UndefinedFilter, t.errors[0]
assert_equal 'Liquid error: undefined filter somefilter1', t.errors[0].message
end
def test_undefined_filters_raise
t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
assert_raises UndefinedFilter do
t.render!({ 'x' => 'foo' }, { strict_filters: true })
end
end
end

View File

@@ -89,4 +89,9 @@ class VariableTest < Minitest::Test
def test_multiline_variable
assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')
end
def test_string_with_curly_brackets
json = '{ "key": { "nested": "value" }}'
assert_template_result(json, "{{ '#{json}' }}")
end
end

View File

@@ -267,7 +267,7 @@ class ContextUnitTest < Minitest::Test
def test_access_hashes_with_hash_notation
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
@context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
@context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
assert_equal 5, @context['products["count"]']
assert_equal 'deepsnow', @context['products["tags"][0]']
@@ -305,7 +305,7 @@ class ContextUnitTest < Minitest::Test
end
def test_first_can_appear_in_middle_of_callchain
@context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
@context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
assert_equal 'draft151cm', @context['product.variants[0].title']
assert_equal 'element151cm', @context['product.variants[1].title']
@@ -466,18 +466,4 @@ class ContextUnitTest < Minitest::Test
assert contx
assert_nil contx['poutine']
end
def test_apply_global_filter
global_filter_proc = ->(output) { "#{output} filtered" }
context = Context.new
context.global_filter = global_filter_proc
assert_equal 'hi filtered', context.apply_global_filter('hi')
end
def test_apply_global_filter_when_no_global_filter_exist
context = Context.new
assert_equal 'hi', context.apply_global_filter('hi')
end
end # ContextTest

View File

@@ -29,18 +29,6 @@ class StrainerUnitTest < Minitest::Test
end
end
def test_stainer_argument_error_contains_backtrace
strainer = Strainer.create(nil)
begin
strainer.invoke("public_filter", 1)
rescue Liquid::ArgumentError => e
assert_match(
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
e.message)
assert_equal e.backtrace[0].split(':')[0], __FILE__
end
end
def test_strainer_only_invokes_public_filter_methods
strainer = Strainer.create(nil)
assert_equal false, strainer.class.invokable?('__test__')
@@ -77,60 +65,4 @@ class StrainerUnitTest < Minitest::Test
assert_kind_of b, strainer
assert_kind_of Liquid::StandardFilters, strainer
end
def test_add_filter_when_wrong_filter_class
c = Context.new
s = c.strainer
wrong_filter = ->(v) { v.reverse }
assert_raises ArgumentError do
s.class.add_filter(wrong_filter)
end
end
module PrivateMethodOverrideFilter
private
def public_filter
"overriden as private"
end
end
def test_add_filter_raises_when_module_privately_overrides_registered_public_methods
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(PrivateMethodOverrideFilter)
end
assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
end
module ProtectedMethodOverrideFilter
protected
def public_filter
"overriden as protected"
end
end
def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(ProtectedMethodOverrideFilter)
end
assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
end
module PublicMethodOverrideFilter
def public_filter
"public"
end
end
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
strainer = Context.new.strainer
strainer.class.add_filter(PublicMethodOverrideFilter)
assert strainer.class.filter_methods.include?('public_filter')
end
end # StrainerTest