Compare commits

..

1 Commits

Author SHA1 Message Date
Florian Weingarten
17db93cf6f bump version to v3.0.0.rc1 2014-07-24 15:04:39 +00:00
65 changed files with 410 additions and 1050 deletions

View File

@@ -5,5 +5,4 @@ gem 'stackprof', platforms: :mri_21
group :test do
gem 'spy', '0.4.1'
gem 'benchmark-ips'
end

View File

@@ -3,7 +3,6 @@
## 3.0.0 / not yet released / branch "master"
* ...
* Add uniq to standard filters [Florian Weingarten, fw42]
* Add exception_handler feature, see #397 and #254 [Bogdan Gusiev, bogdan and Florian Weingarten, fw42]
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge, jasonhl]
* Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge, jasonhl]

View File

@@ -52,26 +52,18 @@ require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer'
require 'liquid/expression'
require 'liquid/context'
require 'liquid/parser_switching'
require 'liquid/tag'
require 'liquid/block'
require 'liquid/document'
require 'liquid/variable'
require 'liquid/variable_lookup'
require 'liquid/range_lookup'
require 'liquid/file_system'
require 'liquid/template'
require 'liquid/standardfilters'
require 'liquid/condition'
require 'liquid/module_ex'
require 'liquid/utils'
require 'liquid/token'
# Load all the tags of the standard library
#
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
require 'liquid/profiler'
require 'liquid/profiler/hooks'

View File

@@ -32,9 +32,7 @@ module Liquid
# fetch the tag from registered blocks
if tag = Template.tags[$1]
markup = token.is_a?(Token) ? token.child($2) : $2
new_tag = tag.parse($1, markup, tokens, @options)
new_tag.line_number = token.line_number if token.is_a?(Token)
new_tag = tag.parse($1, $2, tokens, @options)
@blank &&= new_tag.blank?
@nodelist << new_tag
@children << new_tag
@@ -48,7 +46,6 @@ module Liquid
end
when token.start_with?(VARSTART)
new_var = create_variable(token)
new_var.line_number = token.line_number if token.is_a?(Token)
@nodelist << new_var
@children << new_var
@blank = false
@@ -104,8 +101,7 @@ module Liquid
def create_variable(token)
token.scan(ContentOfVariable) do |content|
markup = token.is_a?(Token) ? token.child(content.first) : content.first
return Variable.new(markup, @options)
return Variable.new(content.first, @options)
end
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
end
@@ -138,29 +134,23 @@ module Liquid
break
end
token_output = render_token(token, context)
token_output = (token.respond_to?(:render) ? token.render(context) : token)
context.increment_used_resources(:render_length_current, token_output)
if context.resource_limits_reached?
context.resource_limits[:reached] = true
raise MemoryError.new("Memory limits exceeded".freeze)
end
unless token.is_a?(Block) && token.blank?
output << token_output
end
rescue MemoryError => e
raise e
rescue ::StandardError => e
output << (context.handle_error(e, token))
output << (context.handle_error(e))
end
end
output.join
end
def render_token(token, context)
token_output = (token.respond_to?(:render) ? token.render(context) : token)
context.increment_used_resources(:render_length_current, token_output)
if context.resource_limits_reached?
context.resource_limits[:reached] = true
raise MemoryError.new("Memory limits exceeded".freeze)
end
token_output
end
end
end

View File

@@ -16,16 +16,15 @@ module Liquid
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_handler
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
@environments = [environments].flatten
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
@parsed_expression = Hash.new{ |cache, markup| cache[markup] = Expression.parse(markup) }
squash_instance_assigns_with_environments
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
@this_stack_used = false
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
@environments = [environments].flatten
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
squash_instance_assigns_with_environments
if rethrow_errors
self.exception_handler = ->(e) { true }
@@ -33,6 +32,7 @@ module Liquid
@interrupts = []
@filters = []
@parsed_variables = Hash.new{ |cache, markup| cache[markup] = variable_parse(markup) }
end
def increment_used_resources(key, obj)
@@ -91,16 +91,21 @@ module Liquid
@interrupts.pop
end
def handle_error(e, token)
e = Liquid::Error.error_with_line_number(e, token)
def handle_error(e)
errors.push(e)
raise if exception_handler && exception_handler.call(e)
Liquid::Error.render(e)
case e
when SyntaxError
"Liquid syntax error: #{e.message}"
else
"Liquid error: #{e.message}"
end
end
def invoke(method, *args)
strainer.invoke(method, *args).to_liquid
strainer.invoke(method, *args)
end
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
@@ -128,19 +133,11 @@ module Liquid
# end
#
# context['var] #=> nil
def stack(new_scope=nil)
old_stack_used = @this_stack_used
if new_scope
push(new_scope)
@this_stack_used = true
else
@this_stack_used = false
end
def stack(new_scope={})
push(new_scope)
yield
ensure
pop if @this_stack_used
@this_stack_used = old_stack_used
pop
end
def clear_instance_assigns
@@ -149,71 +146,152 @@ module Liquid
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
def []=(key, value)
unless @this_stack_used
@this_stack_used = true
push({})
end
@scopes[0][key] = value
end
# Look up variable, either resolve directly after considering the name. We can directly handle
# Strings, digits, floats and booleans (true,false).
# If no match is made we lookup the variable in the current scope and
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
#
# Example:
# products == empty #=> products.empty?
def [](expression)
evaluate(@parsed_expression[expression])
def [](key)
resolve(key)
end
def has_key?(key)
self[key] != nil
resolve(key) != nil
end
def evaluate(object)
object.respond_to?(:evaluate) ? object.evaluate(self) : object
end
private
LITERALS = {
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
'true'.freeze => true,
'false'.freeze => false,
'blank'.freeze => :blank?,
'empty'.freeze => :empty?
}
# Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key)
# This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.has_key?(key) }
scope = @scopes[index] if index
variable = nil
if scope.nil?
@environments.each do |e|
variable = lookup_and_evaluate(e, key)
unless variable.nil?
scope = e
break
# Look up variable, either resolve directly after considering the name. We can directly handle
# Strings, digits, floats and booleans (true,false).
# If no match is made we lookup the variable in the current scope and
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
#
# Example:
# products == empty #=> products.empty?
def resolve(key)
if LITERALS.key?(key)
LITERALS[key]
else
case key
when /\A'(.*)'\z/m # Single quoted strings
$1
when /\A"(.*)"\z/m # Double quoted strings
$1
when /\A(-?\d+)\z/ # Integer and floats
$1.to_i
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
(resolve($1).to_i..resolve($2).to_i)
when /\A(-?\d[\d\.]+)\z/ # Floats
$1.to_f
else
variable(key)
end
end
end
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key)
# Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key)
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
# This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.has_key?(key) }
scope = @scopes[index] if index
return variable
end
variable = nil
def lookup_and_evaluate(obj, key)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
else
value
if scope.nil?
@environments.each do |e|
variable = lookup_and_evaluate(e, key)
unless variable.nil?
scope = e
break
end
end
end
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key)
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
return variable
end
end
private
def variable_parse(markup)
parts = markup.scan(VariableParser)
needs_resolution = false
if parts.first =~ SQUARE_BRACKETED
needs_resolution = true
parts[0] = $1
end
{:first => parts.shift, :needs_resolution => needs_resolution, :rest => parts}
end
# Resolves namespaced queries gracefully.
#
# Example
# @context['hash'] = {"name" => 'tobi'}
# assert_equal 'tobi', @context['hash.name']
# assert_equal 'tobi', @context['hash["name"]']
def variable(markup)
parts = @parsed_variables[markup]
first_part = parts[:first]
if parts[:needs_resolution]
first_part = resolve(parts[:first])
end
if object = find_variable(first_part)
parts[:rest].each do |part|
part = resolve($1) if part_resolved = (part =~ SQUARE_BRACKETED)
# If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it
if object.respond_to?(:[]) and
((object.respond_to?(:has_key?) and object.has_key?(part)) or
(object.respond_to?(:fetch) and part.is_a?(Integer)))
# if its a proc we will replace the entry with the proc
res = lookup_and_evaluate(object, part)
object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls
# as commands and call them on the current object
elsif !part_resolved and object.respond_to?(part) and ['size'.freeze, 'first'.freeze, 'last'.freeze].include?(part)
object = object.send(part.intern).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
else
return nil
end
# If we are dealing with a drop here we have to
object.context = self if object.respond_to?(:context=)
end
end
object
end # variable
def lookup_and_evaluate(obj, key)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
else
value
end
end # lookup_and_evaluate
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@environments.each do |env|

View File

@@ -1,35 +1,5 @@
module Liquid
class Error < ::StandardError
attr_accessor :line_number
def self.render(e)
msg = if e.is_a?(Liquid::Error) && e.line_number
"#{e.line_number}: #{e.message}"
else
e.message
end
case e
when SyntaxError
"Liquid syntax error: #{msg}"
else
"Liquid error: #{msg}"
end
end
def self.error_with_line_number(e, token)
if e.is_a?(Liquid::Error)
e.set_line_number_from_token(token)
end
e
end
def set_line_number_from_token(token)
return unless token.respond_to?(:line_number)
self.line_number = token.line_number
end
end
class Error < ::StandardError; end
class ArgumentError < Error; end
class ContextError < Error; end

View File

@@ -1,33 +0,0 @@
module Liquid
class Expression
LITERALS = {
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
'true'.freeze => true,
'false'.freeze => false,
'blank'.freeze => :blank?,
'empty'.freeze => :empty?
}
def self.parse(markup)
if LITERALS.key?(markup)
LITERALS[markup]
else
case markup
when /\A'(.*)'\z/m # Single quoted strings
$1
when /\A"(.*)"\z/m # Double quoted strings
$1
when /\A(-?\d+)\z/ # Integer and floats
$1.to_i
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
RangeLookup.parse($1, $2)
when /\A(-?\d[\d\.]+)\z/ # Floats
$1.to_f
else
VariableLookup.parse(markup)
end
end
end
end
end

View File

@@ -1,31 +0,0 @@
module Liquid
module ParserSwitching
def parse_with_selected_parser(markup)
case @options[:error_mode] || Template.error_mode
when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup)
when :warn
begin
return strict_parse_with_error_context(markup)
rescue SyntaxError => e
e.line_number = markup.line_number if markup.is_a?(Token)
@warnings ||= []
@warnings << e
return lax_parse(markup)
end
end
end
private
def strict_parse_with_error_context(markup)
strict_parse(markup)
rescue SyntaxError => e
e.message << markup_context(markup)
raise e
end
def markup_context(markup)
" in \"#{markup.strip}\""
end
end
end

View File

@@ -1,159 +0,0 @@
module Liquid
# Profiler enables support for profiling template rendering to help track down performance issues.
#
# To enable profiling, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>. Then, after
# <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
# class via the <tt>Liquid::Template#profiler</tt> method.
#
# template = Liquid::Template.parse(template_content, profile: true)
# output = template.render
# profile = template.profiler
#
# This object contains all profiling information, containing information on what tags were rendered,
# where in the templates these tags live, and how long each tag took to render.
#
# This is a tree structure that is Enumerable all the way down, and keeps track of tags and rendering times
# inside of <tt>{% include %}</tt> tags.
#
# profile.each do |node|
# # Access to the token itself
# node.code
#
# # Which template and line number of this node.
# # If top level, this will be "<root>".
# node.partial
# node.line_number
#
# # Render time in seconds of this node
# node.render_time
#
# # If the template used {% include %}, this node will also have children.
# node.children.each do |child2|
# # ...
# end
# end
#
# Profiler also exposes the total time of the template's render in <tt>Liquid::Profiler#total_render_time</tt>.
#
# All render times are in seconds. There is a small performance hit when profiling is enabled.
#
class Profiler
include Enumerable
class Timing
attr_reader :code, :partial, :line_number, :children
def initialize(token, partial)
@code = token.respond_to?(:raw) ? token.raw : token
@partial = partial
@line_number = token.respond_to?(:line_number) ? token.line_number : nil
@children = []
end
def self.start(token, partial)
new(token, partial).tap do |t|
t.start
end
end
def start
@start_time = Time.now
end
def finish
@end_time = Time.now
end
def render_time
@end_time - @start_time
end
end
def self.profile_token_render(token)
if Profiler.current_profile && token.respond_to?(:render)
Profiler.current_profile.start_token(token)
output = yield
Profiler.current_profile.end_token(token)
output
else
yield
end
end
def self.profile_children(template_name)
if Profiler.current_profile
Profiler.current_profile.push_partial(template_name)
output = yield
Profiler.current_profile.pop_partial
output
else
yield
end
end
def self.current_profile
Thread.current[:liquid_profiler]
end
def initialize
@partial_stack = ["<root>"]
@root_timing = Timing.new("", current_partial)
@timing_stack = [@root_timing]
@render_start_at = Time.now
@render_end_at = @render_start_at
end
def start
Thread.current[:liquid_profiler] = self
@render_start_at = Time.now
end
def stop
Thread.current[:liquid_profiler] = nil
@render_end_at = Time.now
end
def total_render_time
@render_end_at - @render_start_at
end
def each(&block)
@root_timing.children.each(&block)
end
def [](idx)
@root_timing.children[idx]
end
def length
@root_timing.children.length
end
def start_token(token)
@timing_stack.push(Timing.start(token, current_partial))
end
def end_token(token)
timing = @timing_stack.pop
timing.finish
@timing_stack.last.children << timing
end
def current_partial
@partial_stack.last
end
def push_partial(partial_name)
@partial_stack.push(partial_name)
end
def pop_partial
@partial_stack.pop
end
end
end

View File

@@ -1,23 +0,0 @@
module Liquid
class Block < Tag
def render_token_with_profiling(token, context)
Profiler.profile_token_render(token) do
render_token_without_profiling(token, context)
end
end
alias_method :render_token_without_profiling, :render_token
alias_method :render_token, :render_token_with_profiling
end
class Include < Tag
def render_with_profiling(context)
Profiler.profile_children(@template_name) do
render_without_profiling(context)
end
end
alias_method :render_without_profiling, :render
alias_method :render, :render_with_profiling
end
end

View File

@@ -1,22 +0,0 @@
module Liquid
class RangeLookup
def self.parse(start_markup, end_markup)
start_obj = Expression.parse(start_markup)
end_obj = Expression.parse(end_markup)
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj)
else
start_obj.to_i..end_obj.to_i
end
end
def initialize(start_obj, end_obj)
@start_obj = start_obj
@end_obj = end_obj
end
def evaluate(context)
context.evaluate(@start_obj).to_i..context.evaluate(@end_obj).to_i
end
end
end

View File

@@ -36,22 +36,12 @@ module Liquid
def escape(input)
CGI.escapeHTML(input) rescue input
end
alias_method :h, :escape
def escape_once(input)
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
end
def slice(input, offset, length=nil)
offset = Integer(offset)
length = length ? Integer(length) : 1
if input.is_a?(Array)
input.slice(offset, length) || []
else
input.to_s.slice(offset, length) || ''
end
end
alias_method :h, :escape
# Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...".freeze)
@@ -75,7 +65,7 @@ module Liquid
# <div class="summary">{{ post | split '//' | first }}</div>
#
def split(input, pattern)
input.to_s.split(pattern)
input.split(pattern)
end
def strip(input)
@@ -111,24 +101,13 @@ module Liquid
ary = InputIterator.new(input)
if property.nil?
ary.sort
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
elsif ary.first.respond_to?('[]'.freeze) && !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
# Remove duplicate elements from an array
# provide optional property with which to determine uniqueness
def uniq(input, property = nil)
ary = InputIterator.new(input)
if property.nil?
input.uniq
elsif input.first.respond_to?(:[])
input.uniq{ |a| a[property] }
end
end
# Reverse the elements of an array
def reverse(input)
ary = InputIterator.new(input)

View File

@@ -1,8 +1,7 @@
module Liquid
class Tag
attr_accessor :options, :line_number
attr_accessor :options
attr_reader :nodelist, :warnings
include ParserSwitching
class << self
def parse(tag_name, markup, tokens, options)
@@ -23,10 +22,6 @@ module Liquid
def parse(tokens)
end
def raw
"#{@tag_name} #{@markup}"
end
def name
self.class.name.downcase
end
@@ -38,5 +33,29 @@ module Liquid
def blank?
false
end
def parse_with_selected_parser(markup)
case @options[:error_mode] || Template.error_mode
when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup)
when :warn
begin
return strict_parse_with_error_context(markup)
rescue SyntaxError => e
@warnings ||= []
@warnings << e
return lax_parse(markup)
end
end
end
private
def strict_parse_with_error_context(markup)
strict_parse(markup)
rescue SyntaxError => e
e.message << " in \"#{markup.strip}\""
raise e
end
end
end

View File

@@ -15,8 +15,7 @@ module Liquid
super
if markup =~ Syntax
@to = $1
@from = Variable.new($2,options)
@from.line_number = line_number
@from = Variable.new($2)
else
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
end

View File

@@ -51,8 +51,6 @@ module Liquid
end
end
attr_reader :profiler
class << self
# Sets how strict the parser should be.
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
@@ -87,8 +85,6 @@ module Liquid
end
# creates a new <tt>Template</tt> object from liquid source code
# To enable profiling, pass in <tt>profile: true</tt> as an option.
# See Liquid::Profiler for more information
def parse(source, options = {})
template = Template.new
template.parse(source, options)
@@ -103,8 +99,6 @@ module Liquid
# Parse source code.
# Returns self for easy chaining
def parse(source, options = {})
@profiling = options.delete(:profile)
@line_numbers = options.delete(:line_numbers) || @profiling
@root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
@warnings = nil
self
@@ -136,9 +130,6 @@ module Liquid
# if you use the same filters over and over again consider registering them globally
# with <tt>Template.register_filter</tt>
#
# if profiling was enabled in <tt>Template#parse</tt> then the resulting profiling information
# will be available via <tt>Template#profiler</tt>
#
# Following options can be passed:
#
# * <tt>filters</tt> : array with local filters
@@ -192,12 +183,10 @@ module Liquid
begin
# render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it.
result = with_profiling do
@root.render(context)
end
result = @root.render(context)
result.respond_to?(:join) ? result.join : result
rescue Liquid::MemoryError => e
context.handle_error(e, nil)
context.handle_error(e)
ensure
@errors = context.errors
end
@@ -214,8 +203,7 @@ module Liquid
def tokenize(source)
source = source.source if source.respond_to?(:source)
return [] if source.to_s.empty?
tokens = calculate_line_numbers(source.split(TemplateParser))
tokens = source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] and tokens[0].empty?
@@ -223,30 +211,5 @@ module Liquid
tokens
end
def calculate_line_numbers(raw_tokens)
return raw_tokens unless @line_numbers
current_line = 1
raw_tokens.map do |token|
Token.new(token, current_line).tap do
current_line += token.count("\n")
end
end
end
def with_profiling
if @profiling
@profiler = Profiler.new
@profiler.start
begin
yield
ensure
@profiler.stop
end
else
yield
end
end
end
end

View File

@@ -1,18 +0,0 @@
module Liquid
class Token < String
attr_reader :line_number
def initialize(content, line_number)
super(content)
@line_number = line_number
end
def raw
"<raw>"
end
def child(string)
Token.new(string, @line_number)
end
end
end

View File

@@ -11,26 +11,27 @@ module Liquid
# {{ user | link }}
#
class Variable
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
attr_accessor :filters, :name, :warnings
attr_accessor :line_number
include ParserSwitching
def initialize(markup, options = {})
@markup = markup
@name = nil
@options = options || {}
parse_with_selected_parser(markup)
end
def raw
@markup
end
def markup_context(markup)
" in \"{{#{markup}}}\""
case @options[:error_mode] || Template.error_mode
when :strict then strict_parse(markup)
when :lax then lax_parse(markup)
when :warn
begin
strict_parse(markup)
rescue SyntaxError => e
@warnings ||= []
@warnings << e
lax_parse(markup)
end
end
end
def lax_parse(markup)
@@ -40,8 +41,8 @@ module Liquid
if Regexp.last_match(2) =~ /#{FilterSeparator}\s*(.*)/om
filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f|
if f =~ /\w+/
filtername = Regexp.last_match(0)
if f =~ /\s*(\w+)/
filtername = Regexp.last_match(1)
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
@filters << [filtername, filterargs]
end
@@ -68,6 +69,9 @@ module Liquid
@filters << [filtername, filterargs]
end
p.consume(:end_of_string)
rescue SyntaxError => e
e.message << " in \"{{#{markup}}}\""
raise e
end
def parse_filterargs(p)

View File

@@ -1,68 +0,0 @@
module Liquid
class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
def self.parse(markup)
new(markup)
end
def initialize(markup)
lookups = markup.scan(VariableParser)
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]
if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse($1)
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i
end
end
end
def evaluate(context)
name = context.evaluate(@name)
object = context.find_variable(name)
@lookups.each_index do |i|
key = context.evaluate(@lookups[i])
# If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it
if object.respond_to?(:[]) &&
((object.respond_to?(:has_key?) && object.has_key?(key)) ||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
# if its a proc we will replace the entry with the proc
res = context.lookup_and_evaluate(object, key)
object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls
# as commands and call them on the current object
elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil
else
return nil
end
# If we are dealing with a drop here we have to
object.context = context if object.respond_to?(:context=)
end
object
end
end
end

View File

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

View File

@@ -25,5 +25,4 @@ Gem::Specification.new do |s|
s.require_path = "lib"
s.add_development_dependency 'rake'
s.add_development_dependency 'minitest'
end

View File

@@ -1,17 +1,11 @@
require 'benchmark/ips'
require 'benchmark'
require File.dirname(__FILE__) + '/theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.ips do |x|
x.time = 60
x.warmup = 5
puts
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
puts
x.report("parse:") { profiler.compile }
x.report("parse & run:") { profiler.run }
Benchmark.bmbm do |x|
x.report("parse:") { 100.times { profiler.compile } }
x.report("parse & run:") { 100.times { profiler.run } }
end

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class AssignTest < Minitest::Test
class AssignTest < Test::Unit::TestCase
include Liquid
def test_assigned_variable
@@ -24,15 +24,4 @@ class AssignTest < Minitest::Test
'{% assign foo not values %}.',
'values' => "foo,bar,baz")
end
def test_assign_uses_error_mode
with_error_mode(:strict) do
assert_raises(SyntaxError) do
Template.parse("{% assign foo = ('X' | downcase) %}")
end
end
with_error_mode(:lax) do
assert Template.parse("{% assign foo = ('X' | downcase) %}")
end
end
end # AssignTest

View File

@@ -14,7 +14,7 @@ class BlankTestFileSystem
end
end
class BlankTest < Minitest::Test
class BlankTest < Test::Unit::TestCase
include Liquid
N = 10

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class CaptureTest < Minitest::Test
class CaptureTest < Test::Unit::TestCase
include Liquid
def test_captures_block_content_in_variable

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class ContextTest < Minitest::Test
class ContextTest < Test::Unit::TestCase
include Liquid
def test_override_global_filter
@@ -16,10 +16,9 @@ class ContextTest < Minitest::Test
end
end
with_global_filter(global) do
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, :filters => [local])
end
Template.register_filter(global)
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, :filters => [local])
end
def test_has_key_will_not_add_an_error_for_missing_keys

View File

@@ -100,12 +100,14 @@ class RealEnumerableDrop < Liquid::Drop
end
end
class DropsTest < Minitest::Test
class DropsTest < Test::Unit::TestCase
include Liquid
def test_product_drop
tpl = Liquid::Template.parse(' ')
assert_equal ' ', tpl.render!('product' => ProductDrop.new)
assert_nothing_raised do
tpl = Liquid::Template.parse( ' ' )
tpl.render!('product' => ProductDrop.new)
end
end
def test_drop_does_only_respond_to_whitelisted_methods

View File

@@ -19,94 +19,73 @@ class ErrorDrop < Liquid::Drop
end
class ErrorHandlingTest < Minitest::Test
class ErrorHandlingTest < Test::Unit::TestCase
include Liquid
def test_templates_parsed_with_line_numbers_renders_them_in_errors
template = <<-LIQUID
Hello,
{{ errors.standard_error }} will raise a standard error.
Bla bla test.
{{ errors.syntax_error }} will raise a syntax error.
This is an argument error: {{ errors.argument_error }}
Bla.
LIQUID
expected = <<-TEXT
Hello,
Liquid error: 3: standard error will raise a standard error.
Bla bla test.
Liquid syntax error: 7: syntax error will raise a syntax error.
This is an argument error: Liquid error: 9: argument error
Bla.
TEXT
output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new)
assert_equal expected, output
end
def test_standard_error
template = Liquid::Template.parse( ' {{ errors.standard_error }} ' )
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
assert_nothing_raised do
template = Liquid::Template.parse( ' {{ errors.standard_error }} ' )
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size
assert_equal StandardError, template.errors.first.class
assert_equal 1, template.errors.size
assert_equal StandardError, template.errors.first.class
end
end
def test_syntax
template = Liquid::Template.parse( ' {{ errors.syntax_error }} ' )
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size
assert_equal SyntaxError, template.errors.first.class
assert_nothing_raised do
template = Liquid::Template.parse( ' {{ errors.syntax_error }} ' )
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size
assert_equal SyntaxError, template.errors.first.class
end
end
def test_argument
template = Liquid::Template.parse( ' {{ errors.argument_error }} ' )
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
assert_nothing_raised do
assert_equal 1, template.errors.size
assert_equal ArgumentError, template.errors.first.class
template = Liquid::Template.parse( ' {{ errors.argument_error }} ' )
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
assert_equal 1, template.errors.size
assert_equal ArgumentError, template.errors.first.class
end
end
def test_missing_endtag_parse_time_error
assert_raises(Liquid::SyntaxError) do
assert_raise(Liquid::SyntaxError) do
Liquid::Template.parse(' {% for a in b %} ... ')
end
end
def test_unrecognized_operator
with_error_mode(:strict) do
assert_raises(SyntaxError) do
assert_raise(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
end
end
end
def test_lax_unrecognized_operator
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :lax)
assert_equal ' Liquid error: Unknown operator =! ', template.render
assert_equal 1, template.errors.size
assert_equal Liquid::ArgumentError, template.errors.first.class
assert_nothing_raised do
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :lax)
assert_equal ' Liquid error: Unknown operator =! ', template.render
assert_equal 1, template.errors.size
assert_equal Liquid::ArgumentError, template.errors.first.class
end
end
def test_strict_error_messages
err = assert_raises(SyntaxError) do
err = assert_raise(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict)
end
assert_equal 'Unexpected character = in "1 =! 2"', err.message
err = assert_raises(SyntaxError) do
err = assert_raise(SyntaxError) do
Liquid::Template.parse('{{%%%}}', :error_mode => :strict)
end
assert_equal 'Unexpected character % in "{{%%%}}"', err.message
@@ -121,17 +100,11 @@ class ErrorHandlingTest < Minitest::Test
assert_equal '', template.render
end
def test_warning_line_numbers
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", :error_mode => :warn, :line_numbers => true)
assert_equal 3, template.warnings.size
assert_equal [1,2,3], template.warnings.map(&:line_number)
end
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
def test_exceptions_propagate
assert_raises Exception do
template = Liquid::Template.parse('{{ errors.exception }}')
assert_raise Exception do
template = Liquid::Template.parse( ' {{ errors.exception }} ' )
template.render('errors' => ErrorDrop.new)
end
end
end
end # ErrorHandlingTest

View File

@@ -22,7 +22,7 @@ module SubstituteFilter
end
end
class FiltersTest < Minitest::Test
class FiltersTest < Test::Unit::TestCase
include Liquid
def setup
@@ -107,15 +107,15 @@ class FiltersTest < Minitest::Test
end
end
class FiltersInTemplate < Minitest::Test
class FiltersInTemplate < Test::Unit::TestCase
include Liquid
def test_local_global
with_global_filter(MoneyFilter) do
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => [CanadianMoneyFilter])
end
Template.register_filter(MoneyFilter)
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => [CanadianMoneyFilter])
end
def test_local_filter_with_deprecated_syntax

View File

@@ -12,12 +12,14 @@ module CanadianMoneyFilter
end
end
class HashOrderingTest < Minitest::Test
class HashOrderingTest < Test::Unit::TestCase
include Liquid
def test_global_register_order
with_global_filter(MoneyFilter, CanadianMoneyFilter) do
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil)
end
end
def test_global_register_order
Template.register_filter(MoneyFilter)
Template.register_filter(CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil)
end
end

View File

@@ -27,7 +27,7 @@ module FunnyFilter
end
class OutputTest < Minitest::Test
class OutputTest < Test::Unit::TestCase
include Liquid
def setup

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class ParsingQuirksTest < Minitest::Test
class ParsingQuirksTest < Test::Unit::TestCase
include Liquid
def test_parsing_css
@@ -9,28 +9,30 @@ class ParsingQuirksTest < Minitest::Test
end
def test_raise_on_single_close_bracet
assert_raises(SyntaxError) do
assert_raise(SyntaxError) do
Template.parse("text {{method} oh nos!")
end
end
def test_raise_on_label_and_no_close_bracets
assert_raises(SyntaxError) do
assert_raise(SyntaxError) do
Template.parse("TEST {{ ")
end
end
def test_raise_on_label_and_no_close_bracets_percent
assert_raises(SyntaxError) do
assert_raise(SyntaxError) do
Template.parse("TEST {% ")
end
end
def test_error_on_empty_filter
assert Template.parse("{{test}}")
assert Template.parse("{{|test}}")
assert_nothing_raised do
Template.parse("{{test}}")
Template.parse("{{|test}}")
end
with_error_mode(:strict) do
assert_raises(SyntaxError) do
assert_raise(SyntaxError) do
Template.parse("{{test |a|b|}}")
end
end
@@ -38,7 +40,7 @@ class ParsingQuirksTest < Minitest::Test
def test_meaningless_parens_error
with_error_mode(:strict) do
assert_raises(SyntaxError) do
assert_raise(SyntaxError) do
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
Template.parse("{% if #{markup} %} YES {% endif %}")
end
@@ -47,11 +49,11 @@ class ParsingQuirksTest < Minitest::Test
def test_unexpected_characters_syntax_error
with_error_mode(:strict) do
assert_raises(SyntaxError) do
assert_raise(SyntaxError) do
markup = "true && false"
Template.parse("{% if #{markup} %} YES {% endif %}")
end
assert_raises(SyntaxError) do
assert_raise(SyntaxError) do
markup = "false || true"
Template.parse("{% if #{markup} %} YES {% endif %}")
end
@@ -59,9 +61,11 @@ class ParsingQuirksTest < Minitest::Test
end
def test_no_error_on_lax_empty_filter
assert Template.parse("{{test |a|b|}}", :error_mode => :lax)
assert Template.parse("{{test}}", :error_mode => :lax)
assert Template.parse("{{|test|}}", :error_mode => :lax)
assert_nothing_raised do
Template.parse("{{test |a|b|}}", :error_mode => :lax)
Template.parse("{{test}}", :error_mode => :lax)
Template.parse("{{|test|}}", :error_mode => :lax)
end
end
def test_meaningless_parens_lax
@@ -82,22 +86,9 @@ class ParsingQuirksTest < Minitest::Test
end
def test_raise_on_invalid_tag_delimiter
assert_raises(Liquid::SyntaxError) do
assert_raise(Liquid::SyntaxError) do
Template.new.parse('{% end %}')
end
end
def test_unanchored_filter_arguments
with_error_mode(:lax) do
assert_template_result('hi',"{{ 'hi there' | split$$$:' ' | first }}")
assert_template_result('x', "{{ 'X' | downcase) }}")
# After the messed up quotes a filter without parameters (reverse) should work
# but one with parameters (remove) shouldn't be detected.
assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}")
assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
end
end
end # ParsingQuirksTest

View File

@@ -1,142 +0,0 @@
require 'test_helper'
class RenderProfilingTest < Minitest::Test
include Liquid
class ProfilingFileSystem
def read_template_file(template_path, context)
"Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}"
end
end
def setup
Liquid::Template.file_system = ProfilingFileSystem.new
end
def test_template_allows_flagging_profiling
t = Template.parse("{{ 'a string' | upcase }}")
t.render!
assert_nil t.profiler
end
def test_parse_makes_available_simple_profiling
t = Template.parse("{{ 'a string' | upcase }}", :profile => true)
t.render!
assert_equal 1, t.profiler.length
node = t.profiler[0]
assert_equal " 'a string' | upcase ", node.code
end
def test_render_ignores_raw_strings_when_profiling
t = Template.parse("This is raw string\nstuff\nNewline", :profile => true)
t.render!
assert_equal 0, t.profiler.length
end
def test_profiling_includes_line_numbers_of_liquid_nodes
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", :profile => true)
t.render!
assert_equal 2, t.profiler.length
# {{ 'a string' | upcase }}
assert_equal 1, t.profiler[0].line_number
# {{ increment test }}
assert_equal 2, t.profiler[1].line_number
end
def test_profiling_times_the_rendering_of_tokens
t = Template.parse("{% include 'a_template' %}", :profile => true)
t.render!
node = t.profiler[0]
refute_nil node.render_time
end
def test_profiling_times_the_entire_render
t = Template.parse("{% include 'a_template' %}", :profile => true)
t.render!
assert t.profiler.total_render_time > 0, "Total render time was not calculated"
end
def test_profiling_uses_include_to_mark_children
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", :profile => true)
t.render!
include_node = t.profiler[1]
assert_equal 2, include_node.children.length
end
def test_profiling_marks_children_with_the_name_of_included_partial
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", :profile => true)
t.render!
include_node = t.profiler[1]
include_node.children.each do |child|
assert_equal "'a_template'", child.partial
end
end
def test_profiling_supports_multiple_templates
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'b_template' %}", :profile => true)
t.render!
a_template = t.profiler[1]
a_template.children.each do |child|
assert_equal "'a_template'", child.partial
end
b_template = t.profiler[2]
b_template.children.each do |child|
assert_equal "'b_template'", child.partial
end
end
def test_profiling_supports_rendering_the_same_partial_multiple_times
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'a_template' %}", :profile => true)
t.render!
a_template1 = t.profiler[1]
a_template1.children.each do |child|
assert_equal "'a_template'", child.partial
end
a_template2 = t.profiler[2]
a_template2.children.each do |child|
assert_equal "'a_template'", child.partial
end
end
def test_can_iterate_over_each_profiling_entry
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", :profile => true)
t.render!
timing_count = 0
t.profiler.each do |timing|
timing_count += 1
end
assert_equal 2, timing_count
end
def test_profiling_marks_children_of_if_blocks
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", :profile => true)
t.render!
assert_equal 1, t.profiler.length
assert_equal 2, t.profiler[0].children.length
end
def test_profiling_marks_children_of_for_blocks
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", :profile => true)
t.render!({"collection" => ["one", "two"]})
assert_equal 1, t.profiler.length
# Will profile each invocation of the for block
assert_equal 2, t.profiler[0].children.length
end
end

View File

@@ -6,7 +6,7 @@ module SecurityFilter
end
end
class SecurityTest < Minitest::Test
class SecurityTest < Test::Unit::TestCase
include Liquid
def test_no_instance_eval

View File

@@ -41,7 +41,7 @@ class TestEnumerable < Liquid::Drop
end
end
class StandardFiltersTest < Minitest::Test
class StandardFiltersTest < Test::Unit::TestCase
include Liquid
def setup
@@ -64,34 +64,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal '', @filters.upcase(nil)
end
def test_slice
assert_equal 'oob', @filters.slice('foobar', 1, 3)
assert_equal 'oobar', @filters.slice('foobar', 1, 1000)
assert_equal '', @filters.slice('foobar', 1, 0)
assert_equal 'o', @filters.slice('foobar', 1, 1)
assert_equal 'bar', @filters.slice('foobar', 3, 3)
assert_equal 'ar', @filters.slice('foobar', -2, 2)
assert_equal 'ar', @filters.slice('foobar', -2, 1000)
assert_equal 'r', @filters.slice('foobar', -1)
assert_equal '', @filters.slice(nil, 0)
assert_equal '', @filters.slice('foobar', 100, 10)
assert_equal '', @filters.slice('foobar', -100, 10)
end
def test_slice_on_arrays
input = 'foobar'.split(//)
assert_equal %w{o o b}, @filters.slice(input, 1, 3)
assert_equal %w{o o b a r}, @filters.slice(input, 1, 1000)
assert_equal %w{}, @filters.slice(input, 1, 0)
assert_equal %w{o}, @filters.slice(input, 1, 1)
assert_equal %w{b a r}, @filters.slice(input, 3, 3)
assert_equal %w{a r}, @filters.slice(input, -2, 2)
assert_equal %w{a r}, @filters.slice(input, -2, 1000)
assert_equal %w{r}, @filters.slice(input, -1)
assert_equal %w{}, @filters.slice(input, 100, 10)
assert_equal %w{}, @filters.slice(input, -100, 10)
end
def test_truncate
assert_equal '1234...', @filters.truncate('1234567890', 7)
assert_equal '1234567890', @filters.truncate('1234567890', 20)
@@ -106,7 +78,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal ['A?Z'], @filters.split('A?Z', '~')
# Regexp works although Liquid does not support.
assert_equal ['A','Z'], @filters.split('AxZ', /x/)
assert_equal [], @filters.split(nil, ' ')
end
def test_escape
@@ -157,13 +128,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal [{"a" => "10"}, {"a" => "2"}], @filters.sort([{"a" => "10"}, {"a" => "2"}], "a")
end
def test_uniq
assert_equal [1,3,2,4], @filters.uniq([1,1,3,2,3,1,4,3,2,1])
assert_equal [{"a" => 1}, {"a" => 3}, {"a" => 2}], @filters.uniq([{"a" => 1}, {"a" => 3}, {"a" => 1}, {"a" => 2}], "a")
testdrop = TestDrop.new
assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test')
end
def test_reverse
assert_equal [4,3,2,1], @filters.reverse([1,2,3,4])
end
@@ -220,11 +184,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new
end
def test_first_and_last_call_to_liquid
assert_template_result 'foobar', '{{ foo | first }}', 'foo' => [ThingWithToLiquid.new]
assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new]
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")

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class BreakTagTest < Minitest::Test
class BreakTagTest < Test::Unit::TestCase
include Liquid
# tests that no weird errors are raised if break is called outside of a

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class ContinueTagTest < Minitest::Test
class ContinueTagTest < Test::Unit::TestCase
include Liquid
# tests that no weird errors are raised if continue is called outside of a

View File

@@ -6,7 +6,7 @@ class ThingWithValue < Liquid::Drop
end
end
class ForTagTest < Minitest::Test
class ForTagTest < Test::Unit::TestCase
include Liquid
def test_for
@@ -303,7 +303,7 @@ HERE
end
def test_bad_variable_naming_in_for_loop
assert_raises(Liquid::SyntaxError) do
assert_raise(Liquid::SyntaxError) do
Liquid::Template.parse('{% for a/b in x %}{% endfor %}')
end
end

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class IfElseTagTest < Minitest::Test
class IfElseTagTest < Test::Unit::TestCase
include Liquid
def test_if
@@ -37,19 +37,25 @@ class IfElseTagTest < Minitest::Test
end
def test_comparison_of_strings_containing_and_or_or
awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar"
assigns = {'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true}
assert_template_result(' YES ',"{% if #{awful_markup} %} YES {% endif %}", assigns)
assert_nothing_raised do
awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar"
assigns = {'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true}
assert_template_result(' YES ',"{% if #{awful_markup} %} YES {% endif %}", assigns)
end
end
def test_comparison_of_expressions_starting_with_and_or_or
assigns = {'order' => {'items_count' => 0}, 'android' => {'name' => 'Roy'}}
assert_template_result( "YES",
"{% if android.name == 'Roy' %}YES{% endif %}",
assigns)
assert_template_result( "YES",
"{% if order.items_count == 0 %}YES{% endif %}",
assigns)
assert_nothing_raised do
assert_template_result( "YES",
"{% if android.name == 'Roy' %}YES{% endif %}",
assigns)
end
assert_nothing_raised do
assert_template_result( "YES",
"{% if order.items_count == 0 %}YES{% endif %}",
assigns)
end
end
def test_if_and
@@ -129,35 +135,31 @@ class IfElseTagTest < Minitest::Test
end
def test_syntax_error_no_variable
assert_raises(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}')}
assert_raise(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}')}
end
def test_syntax_error_no_expression
assert_raises(SyntaxError) { assert_template_result('', '{% if %}') }
assert_raise(SyntaxError) { assert_template_result('', '{% if %}') }
end
def test_if_with_custom_condition
original_op = Condition.operators['contains']
Condition.operators['contains'] = :[]
assert_template_result('yes', %({% if 'bob' contains 'o' %}yes{% endif %}))
assert_template_result('no', %({% if 'bob' contains 'f' %}yes{% else %}no{% endif %}))
ensure
Condition.operators['contains'] = original_op
Condition.operators.delete 'contains'
end
def test_operators_are_ignored_unless_isolated
original_op = Condition.operators['contains']
Condition.operators['contains'] = :[]
assert_template_result('yes',
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}))
ensure
Condition.operators['contains'] = original_op
end
def test_operators_are_whitelisted
assert_raises(SyntaxError) do
assert_raise(SyntaxError) do
assert_template_result('', %({% if 1 or throw or or 1 %}yes{% endif %}))
end
end

View File

@@ -65,7 +65,7 @@ class CustomInclude < Liquid::Tag
end
end
class IncludeTagTest < Minitest::Test
class IncludeTagTest < Test::Unit::TestCase
include Liquid
def setup
@@ -132,7 +132,7 @@ class IncludeTagTest < Minitest::Test
Liquid::Template.file_system = infinite_file_system.new
assert_raises(Liquid::StackLevelError) do
assert_raise(Liquid::StackLevelError) do
Template.parse("{% include 'loop' %}").render!
end

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class IncrementTagTest < Minitest::Test
class IncrementTagTest < Test::Unit::TestCase
include Liquid
def test_inc

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class RawTagTest < Minitest::Test
class RawTagTest < Test::Unit::TestCase
include Liquid
def test_tag_in_raw

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class StandardTagTest < Minitest::Test
class StandardTagTest < Test::Unit::TestCase
include Liquid
def test_no_transform
@@ -66,7 +66,7 @@ class StandardTagTest < Minitest::Test
end
def test_capture_detects_bad_syntax
assert_raises(SyntaxError) do
assert_raise(SyntaxError) do
assert_template_result('content foo content foo ',
'{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
{'var' => 'content' })
@@ -229,11 +229,11 @@ class StandardTagTest < Minitest::Test
end
def test_case_detects_bad_syntax
assert_raises(SyntaxError) do
assert_raise(SyntaxError) do
assert_template_result('', '{% case false %}{% when %}true{% endcase %}', {})
end
assert_raises(SyntaxError) do
assert_raise(SyntaxError) do
assert_template_result('', '{% case false %}{% huh %}true{% endcase %}', {})
end

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class StatementsTest < Minitest::Test
class StatementsTest < Test::Unit::TestCase
include Liquid
def test_true_eql_true

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class TableRowTest < Minitest::Test
class TableRowTest < Test::Unit::TestCase
include Liquid
class ArrayDrop < Liquid::Drop

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class UnlessElseTagTest < Minitest::Test
class UnlessElseTagTest < Test::Unit::TestCase
include Liquid
def test_unless

View File

@@ -28,7 +28,7 @@ class ErroneousDrop < Liquid::Drop
end
end
class TemplateTest < Minitest::Test
class TemplateTest < Test::Unit::TestCase
include Liquid
def test_instance_assigns_persist_on_same_template_object_between_parses
@@ -93,7 +93,7 @@ class TemplateTest < Minitest::Test
assert t.resource_limits[:reached]
t.resource_limits = { :render_length_limit => 10 }
assert_equal "0123456789", t.render!()
refute_nil t.resource_limits[:render_length_current]
assert_not_nil t.resource_limits[:render_length_current]
end
def test_resource_limits_render_score
@@ -107,7 +107,7 @@ class TemplateTest < Minitest::Test
assert t.resource_limits[:reached]
t.resource_limits = { :render_score_limit => 200 }
assert_equal (" foo " * 100), t.render!()
refute_nil t.resource_limits[:render_score_current]
assert_not_nil t.resource_limits[:render_score_current]
end
def test_resource_limits_assign_score
@@ -117,7 +117,7 @@ class TemplateTest < Minitest::Test
assert t.resource_limits[:reached]
t.resource_limits = { :assign_score_limit => 2 }
assert_equal "", t.render!()
refute_nil t.resource_limits[:assign_score_current]
assert_not_nil t.resource_limits[:assign_score_current]
end
def test_resource_limits_aborts_rendering_after_first_error

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class VariableTest < Minitest::Test
class VariableTest < Test::Unit::TestCase
include Liquid
def test_simple_variable
@@ -9,10 +9,6 @@ class VariableTest < Minitest::Test
assert_equal 'worked wonderfully', template.render!('test' => 'worked wonderfully')
end
def test_variable_render_calls_to_liquid
assert_template_result 'foobar', '{{ foo }}', 'foo' => ThingWithToLiquid.new
end
def test_simple_with_whitespaces
template = Template.parse(%| {{ test }} |)
assert_equal ' worked ', template.render!('test' => 'worked')

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env ruby
require 'minitest/autorun'
require 'test/unit'
require 'test/unit/assertions'
require 'spy/integration'
$:.unshift(File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib'))
@@ -13,62 +14,41 @@ if env_mode = ENV['LIQUID_PARSER_MODE']
end
Liquid::Template.error_mode = mode
if Minitest.const_defined?('Test')
# We're on Minitest 5+. Nothing to do here.
else
# Minitest 4 doesn't have Minitest::Test yet.
Minitest::Test = MiniTest::Unit::TestCase
end
module Minitest
class Test
def fixture(name)
File.join(File.expand_path(File.dirname(__FILE__)), "fixtures", name)
end
end
module Assertions
include Liquid
def assert_template_result(expected, template, assigns = {}, message = nil)
assert_equal expected, Template.parse(template).render!(assigns)
end
def assert_template_result_matches(expected, template, assigns = {}, message = nil)
return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp
assert_match expected, Template.parse(template).render!(assigns)
end
def assert_match_syntax_error(match, template, registers = {})
exception = assert_raises(Liquid::SyntaxError) {
Template.parse(template).render(assigns)
}
assert_match match, exception.message
end
def with_global_filter(*globals)
original_filters = Array.new(Liquid::Strainer.class_variable_get(:@@filters))
globals.each do |global|
Liquid::Template.register_filter(global)
module Test
module Unit
class TestCase
def fixture(name)
File.join(File.expand_path(File.dirname(__FILE__)), "fixtures", name)
end
yield
ensure
Liquid::Strainer.class_variable_set(:@@filters, original_filters)
end
def with_error_mode(mode)
old_mode = Liquid::Template.error_mode
Liquid::Template.error_mode = mode
yield
ensure
Liquid::Template.error_mode = old_mode
end
end
end
module Assertions
include Liquid
class ThingWithToLiquid
def to_liquid
'foobar'
end
end
def assert_template_result(expected, template, assigns = {}, message = nil)
assert_equal expected, Template.parse(template).render!(assigns)
end
def assert_template_result_matches(expected, template, assigns = {}, message = nil)
return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp
assert_match expected, Template.parse(template).render!(assigns)
end
def assert_match_syntax_error(match, template, registers = {})
exception = assert_raise(Liquid::SyntaxError) {
Template.parse(template).render(assigns)
}
assert_match match, exception.message
end
def with_error_mode(mode)
old_mode = Liquid::Template.error_mode
Liquid::Template.error_mode = mode
yield
Liquid::Template.error_mode = old_mode
end
end # Assertions
end # Unit
end # Test

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class BlockUnitTest < Minitest::Test
class BlockUnitTest < Test::Unit::TestCase
include Liquid
def test_blankspace
@@ -45,7 +45,10 @@ class BlockUnitTest < Minitest::Test
def test_with_custom_tag
Liquid::Template.register_tag("testtag", Block)
assert Liquid::Template.parse( "{% testtag %} {% endtesttag %}")
assert_nothing_thrown do
template = Liquid::Template.parse( "{% testtag %} {% endtesttag %}")
end
end
private

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class ConditionUnitTest < Minitest::Test
class ConditionUnitTest < Test::Unit::TestCase
include Liquid
def test_basic_condition

View File

@@ -63,7 +63,7 @@ class ArrayLike
end
end
class ContextUnitTest < Minitest::Test
class ContextUnitTest < Test::Unit::TestCase
include Liquid
def setup
@@ -107,14 +107,16 @@ class ContextUnitTest < Minitest::Test
end
def test_scoping
@context.push
@context.pop
assert_raises(Liquid::ContextError) do
assert_nothing_raised do
@context.push
@context.pop
end
assert_raises(Liquid::ContextError) do
assert_raise(Liquid::ContextError) do
@context.pop
end
assert_raise(Liquid::ContextError) do
@context.push
@context.pop
@context.pop
@@ -481,12 +483,4 @@ class ContextUnitTest < Minitest::Test
assert_equal 1, mock_scan.calls.size
end
def test_context_initialization_with_a_proc_in_environment
contx = Context.new([:test => lambda { |c| c['poutine']}], {:test => :foo})
assert contx
assert_nil contx['poutine']
end
end # ContextTest

View File

@@ -1,10 +1,10 @@
require 'test_helper'
class FileSystemUnitTest < Minitest::Test
class FileSystemUnitTest < Test::Unit::TestCase
include Liquid
def test_default
assert_raises(FileSystemError) do
assert_raise(FileSystemError) do
BlankFileSystem.new.read_template_file("dummy", {'dummy'=>'smarty'})
end
end
@@ -14,15 +14,15 @@ class FileSystemUnitTest < Minitest::Test
assert_equal "/some/path/_mypartial.liquid" , file_system.full_path("mypartial")
assert_equal "/some/path/dir/_mypartial.liquid", file_system.full_path("dir/mypartial")
assert_raises(FileSystemError) do
assert_raise(FileSystemError) do
file_system.full_path("../dir/mypartial")
end
assert_raises(FileSystemError) do
assert_raise(FileSystemError) do
file_system.full_path("/dir/../../dir/mypartial")
end
assert_raises(FileSystemError) do
assert_raise(FileSystemError) do
file_system.full_path("/etc/passwd")
end
end

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class I18nUnitTest < Minitest::Test
class I18nUnitTest < Test::Unit::TestCase
include Liquid
def setup
@@ -20,13 +20,13 @@ class I18nUnitTest < Minitest::Test
end
# def test_raises_translation_error_on_undefined_interpolation_key
# assert_raises I18n::TranslationError do
# assert_raise I18n::TranslationError do
# @i18n.translate("whatever", :oopstypos => "yes")
# end
# end
def test_raises_unknown_translation
assert_raises I18n::TranslationError do
assert_raise I18n::TranslationError do
@i18n.translate("doesnt_exist")
end
end

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class LexerUnitTest < Minitest::Test
class LexerUnitTest < Test::Unit::TestCase
include Liquid
def test_strings

View File

@@ -36,7 +36,7 @@ class TestClassC::LiquidDropClass
end
end
class ModuleExUnitTest < Minitest::Test
class ModuleExUnitTest < Test::Unit::TestCase
include Liquid
def setup

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class ParserUnitTest < Minitest::Test
class ParserUnitTest < Test::Unit::TestCase
include Liquid
def test_consume

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class RegexpUnitTest < Minitest::Test
class RegexpUnitTest < Test::Unit::TestCase
include Liquid
def test_empty

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class StrainerUnitTest < Minitest::Test
class StrainerUnitTest < Test::Unit::TestCase
include Liquid
module AccessScopeFilters

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class TagUnitTest < Minitest::Test
class TagUnitTest < Test::Unit::TestCase
include Liquid
def test_tag
@@ -8,9 +8,4 @@ class TagUnitTest < Minitest::Test
assert_equal 'liquid::tag', tag.name
assert_equal '', tag.render(Context.new)
end
def test_return_raw_text_of_tag
tag = Tag.parse("long_tag", "param1, param2, param3", [], {})
assert_equal("long_tag param1, param2, param3", tag.raw)
end
end

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class CaseTagUnitTest < Minitest::Test
class CaseTagUnitTest < Test::Unit::TestCase
include Liquid
def test_case_nodelist

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class ForTagUnitTest < Minitest::Test
class ForTagUnitTest < Test::Unit::TestCase
def test_for_nodelist
template = Liquid::Template.parse('{% for item in items %}FOR{% endfor %}')
assert_equal ['FOR'], template.root.nodelist[0].nodelist

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class IfTagUnitTest < Minitest::Test
class IfTagUnitTest < Test::Unit::TestCase
def test_if_nodelist
template = Liquid::Template.parse('{% if true %}IF{% else %}ELSE{% endif %}')
assert_equal ['IF', 'ELSE'], template.root.nodelist[0].nodelist

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class TemplateUnitTest < Minitest::Test
class TemplateUnitTest < Test::Unit::TestCase
include Liquid
def test_sets_default_localization_in_document

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class TokenizerTest < Minitest::Test
class TokenizerTest < Test::Unit::TestCase
def test_tokenize_strings
assert_equal [' '], tokenize(' ')
assert_equal ['hello world'], tokenize('hello world')
@@ -21,15 +21,6 @@ class TokenizerTest < Minitest::Test
assert_equal [' ', '{% comment %}', ' ', '{% endcomment %}', ' '], tokenize(" {% comment %} {% endcomment %} ")
end
def test_calculate_line_numbers_per_token_with_profiling
template = Liquid::Template.parse("", :profile => true)
assert_equal [1], template.send(:tokenize, "{{funk}}").map(&:line_number)
assert_equal [1, 1, 1], template.send(:tokenize, " {{funk}} ").map(&:line_number)
assert_equal [1, 2, 2], template.send(:tokenize, "\n{{funk}}\n").map(&:line_number)
assert_equal [1, 1, 3], template.send(:tokenize, " {{\n funk \n}} ").map(&:line_number)
end
private
def tokenize(source)

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class VariableUnitTest < Minitest::Test
class VariableUnitTest < Test::Unit::TestCase
include Liquid
def test_variable
@@ -133,9 +133,4 @@ class VariableUnitTest < Minitest::Test
end
end
end
def test_output_raw_source_of_variable
var = Variable.new(%! name_of_variable | upcase !)
assert_equal " name_of_variable | upcase ", var.raw
end
end