Compare commits

..

15 Commits

Author SHA1 Message Date
Tobias Lutke
cd040dabd8 Implement naive recusrive descent
Ragel doesn't allow us to recurse so we simply
reinvoke the parser for each step.
2012-10-28 21:55:20 -04:00
Tobias Lütke
18b83a58bd Replace regexpes with Ragel grammer
context parsing was handrolled and pretty ad-hoc
this branch exists to explore parsing the context
through a defined fsm as produced by Ragel
2012-10-28 21:50:18 -04:00
Tobias Lutke
6b64bfb53e fix benchmarks 2012-10-28 21:37:07 -04:00
Tom Burns
6c2fde5eea Instantiate blank string once instead of at every comparison 2012-10-25 11:54:19 -04:00
Tobias Lütke
ce76dbf8d9 fixed the performance suite 2012-10-20 10:53:53 -04:00
Steven Soroka
661ff2ccdf Merge pull request #140 from binarycleric/feature/break_for_loop
Added break and continue statements
2012-08-21 13:22:24 -07:00
Jon Daniel
9c183bea83 added interrupt class for continue/break statements
When a continue or break statement is executed it pushes an interrupt to a
stack in context. If any non-handled interrupts are present blocks will cease
to execute. The for loop can handle the most recent interrupt in the stack.
2012-08-21 13:14:27 -04:00
Jon Daniel
484fd18612 added break and continue tags 2012-08-21 00:00:02 -04:00
Jonathan Rudenberg
bf86459456 Merge pull request #139 from pjb3/fix_block_test_name
Class name does not match file name
2012-08-19 15:07:59 -07:00
Paul Barry
d2827c561b Class name does not match file name 2012-08-19 07:44:35 -04:00
Tobias Lütke
16c34595a4 fix mergeconflict 2012-08-07 13:21:31 -04:00
Tobias Lütke
6e091909ee Merge branch 'master' of github.com:Shopify/liquid 2012-08-07 13:20:37 -04:00
Tobias Lütke
d7cb39ccb3 release 2.4.0 2012-08-07 13:20:23 -04:00
Jonathan Rudenberg
f8d46804fd Fix rake test for broken version of rake on travis 2012-08-07 09:49:55 -04:00
Jonathan Rudenberg
5c6de2d919 Fix typo 2012-08-07 09:37:19 -04:00
24 changed files with 1902 additions and 124 deletions

View File

@@ -8,7 +8,7 @@
* Ruby 1.8, 1.9, and Rubinius compatibility fixes
* Add support for `quoted['references']` in `tablerow`
* Add support for Enumerable to `tablerow`
* `strip\_html` filter removes html comments
* `strip_html` filter removes html comments
## 2.3.0 / 2011-10-16

View File

@@ -7,9 +7,14 @@ require 'rubygems/package_task'
task :default => 'test'
task :ragel do
sh "find . -name '*.rl' | xargs ragel -R -F1"
end
Rake::TestTask.new(:test) do |t|
t.libs << '.' << 'lib' << 'test'
t.pattern = 'test/liquid/**/*_test.rb'
t.test_files = FileList['test/liquid/**/*_test.rb']
t.verbose = false
end
@@ -27,7 +32,7 @@ namespace :benchmark do
desc "Run the liquid benchmark"
task :run do
ruby "performance/benchmark.rb"
ruby "./performance/benchmark.rb"
end
end
@@ -37,12 +42,12 @@ namespace :profile do
desc "Run the liquid profile/performance coverage"
task :run do
ruby "performance/profile.rb"
ruby "./performance/profile.rb"
end
desc "Run KCacheGrind"
task :grind => :run do
system "kcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt"
system "qcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt"
end
end

View File

@@ -48,6 +48,7 @@ end
require 'liquid/drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer'
require 'liquid/context'
require 'liquid/tag'
@@ -61,6 +62,7 @@ require 'liquid/standardfilters'
require 'liquid/condition'
require 'liquid/module_ex'
require 'liquid/utils'
require 'liquid/parser'
# Load all the tags of the standard library
#

View File

@@ -89,13 +89,27 @@ module Liquid
end
def render_all(list, context)
list.collect do |token|
output = []
list.each do |token|
# Break out if we have any unhanded interrupts.
break if context.has_interrupt?
begin
token.respond_to?(:render) ? token.render(context) : token
# If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}
if token.is_a? Continue or token.is_a? Break
context.push_interrupt(token.interrupt)
break
end
output << (token.respond_to?(:render) ? token.render(context) : token)
rescue ::StandardError => e
context.handle_error(e)
output << (context.handle_error(e))
end
end.join
end
output.join
end
end
end

View File

@@ -1,5 +1,6 @@
module Liquid
# Context keeps the variable stack and resolves variables, as well as keywords
#
# context['variable'] = 'testing'
@@ -22,6 +23,8 @@ module Liquid
@errors = []
@rethrow_errors = rethrow_errors
squash_instance_assigns_with_environments
@interrupts = []
end
def strainer
@@ -41,6 +44,21 @@ module Liquid
end
end
# are there any not handled interrupts?
def has_interrupt?
!@interrupts.empty?
end
# push an interrupt to the stack. this interrupt is considered not handled.
def push_interrupt(e)
@interrupts.push(e)
end
# pop an interrupt from the stack
def pop_interrupt
@interrupts.pop
end
def handle_error(e)
errors.push(e)
raise if @rethrow_errors
@@ -111,13 +129,6 @@ module Liquid
end
private
LITERALS = {
nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
'true' => true,
'false' => false,
'blank' => :blank?,
'empty' => :empty?
}
# Look up variable, either resolve directly after considering the name. We can directly handle
# Strings, digits, floats and booleans (true,false).
@@ -127,25 +138,67 @@ module Liquid
#
# Example:
# products == empty #=> products.empty?
def resolve(key)
if LITERALS.key?(key)
LITERALS[key]
else
case key
when /^'(.*)'$/ # Single quoted strings
$1
when /^"(.*)"$/ # Double quoted strings
$1
when /^(-?\d+)$/ # Integer and floats
$1.to_i
when /^\((\S+)\.\.(\S+)\)$/ # Ranges
(resolve($1).to_i..resolve($2).to_i)
when /^(-?\d[\d\.]+)$/ # Floats
$1.to_f
else
variable(key)
def resolve(key)
case key
when nil, ""
return nil
when "blank"
return :blank?
when "empty"
return :empty?
end
result = Parser.parse(key)
stack = []
result.each do |(sym, value)|
case sym
when :id
stack.push value
when :lookup
left = stack.pop
value = find_variable(left)
stack.push(harden(value))
when :range
right = stack.pop.to_i
left = stack.pop.to_i
stack.push (left..right)
when :buildin
left = stack.pop
value = invoke_buildin(left, value)
stack.push(harden(value))
when :call
left = stack.pop
right = stack.pop
value = lookup_and_evaluate(right, left)
stack.push(harden(value))
else
raise "unknown #{sym}"
end
end
return stack.first
end
def invoke_buildin(obj, key)
# as weird as this is, liquid unit tests demand that we prioritize hash lookups
# to buildins. So if we got a hash and it has a :first element we need to call that
# instead of sending the first message...
if obj.respond_to?(:has_key?) && obj.has_key?(key)
return lookup_and_evaluate(obj, key)
end
if obj.respond_to?(key)
return obj.send(key)
else
return nil
end
end
# Fetches an object starting at the local scope and then moving up the hierachy
@@ -164,71 +217,35 @@ module Liquid
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
# 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 = markup.scan(VariableParser)
square_bracketed = /^\[(.*)\]$/
first_part = parts.shift
if first_part =~ square_bracketed
first_part = resolve($1)
end
if object = find_variable(first_part)
parts.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', 'first', 'last'].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
return nil unless obj.respond_to?(:[])
if obj.is_a?(Array)
return nil unless key.is_a?(Integer)
end
end # lookup_and_evaluate
value = obj[key]
case value
when Proc
# call the proc
value = (value.arity == 0) ? value.call : value.call(self)
# memozie if possible
obj[key] = value if obj.respond_to?(:[]=)
end
value
end
def harden(value)
value = value.to_liquid
value.context = self if value.respond_to?(:context=)
return value
end
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@@ -240,6 +257,7 @@ module Liquid
end
end
end # squash_instance_assigns_with_environments
end # Context
end # Liquid

View File

@@ -22,6 +22,8 @@ module Liquid
class Drop
attr_writer :context
EMPTY_STRING = ''.freeze
# Catch all for the method
def before_method(method)
nil
@@ -29,7 +31,7 @@ module Liquid
# called by liquid to invoke a drop
def invoke_drop(method_or_key)
if method_or_key && method_or_key != '' && self.class.public_method_defined?(method_or_key.to_s.to_sym)
if method_or_key && method_or_key != EMPTY_STRING && self.class.public_method_defined?(method_or_key.to_s.to_sym)
send(method_or_key.to_s.to_sym)
else
before_method(method_or_key)

View File

@@ -8,4 +8,4 @@ module Liquid
class StandardError < Error; end
class SyntaxError < Error; end
class StackLevelError < Error; end
end
end

17
lib/liquid/interrupts.rb Normal file
View File

@@ -0,0 +1,17 @@
module Liquid
# An interrupt is any command that breaks processing of a block (ex: a for loop).
class Interrupt
attr_reader :message
def initialize(message=nil)
@message = message || "interrupt"
end
end
# Interrupt that is thrown whenever a {% break %} is called.
class BreakInterrupt < Interrupt; end
# Interrupt that is thrown whenever a {% continue %} is called.
class ContinueInterrupt < Interrupt; end
end

1313
lib/liquid/parser.rb Normal file

File diff suppressed because it is too large Load Diff

126
lib/liquid/parser.rl Normal file
View File

@@ -0,0 +1,126 @@
# Parser for context#[] method. Generated through ragel from parser.rl
# Only modify parser.rl. Run rake ragel afterwards to generate this file.
#
#VERBOSE=true
%%{
machine fsm;
action mark {
mark = p
}
action lookup {
emit(:lookup, :instruction, nil, tokens)
}
action call {
emit(:call, :instruction, nil, tokens)
}
action range {
emit(:range, :instruction, nil, tokens)
}
constants = ( "true" | "false" | "nil" | "null" );
# strings
string = "\"" any* "\"" | "'" any* "'";
# nothingness
nil = "nil" | "null" ;
# numbers
integer = ('+'|'-')? digit+;
float = ('+'|'-')? digit+ '.' digit+;
# simple values
primitive = (
integer >mark %{ emit(:id, :integer, Integer(data[mark..p-1]), tokens) } |
float >mark %{ emit(:id, :float, Float(data[mark..p-1]), tokens) } |
nil %{ emit(:id, :nil, nil, tokens) } |
"true" %{ emit(:id, :bool, true, tokens) } |
"false" %{ emit(:id, :bool, false, tokens)} |
string >mark %{ emit(:id, :string, data[mark+1..p-2], tokens) }
);
entity = (
((alpha [A-Za-z0-9_\-]*) - (constants)) >mark %{
emit(:id, :label, data[mark..p-1], tokens)
emit(:lookup, :variable, nil, tokens)
}
);
# Because of recursion we cannot immediatly resolve the content of this in
# the current grammar. We simply re-invoke the parser here to descend into
# the substring
recur = (
(any+ - ']') >mark %{
self.parse(data[mark..p-1], tokens)
}
);
expr = (
entity |
primitive |
"(" (primitive | entity) ".." (primitive | entity) <: ")" %range |
"[" recur "]" %lookup
);
hash_accessors = (
"[" recur "]" %call |
".first" %{
emit(:buildin, :symbol, "first", tokens)
} |
".last" %{
emit(:buildin, :symbol, "last", tokens)
} |
".size" %{
emit(:buildin, :symbol, "size", tokens)
} |
"." ((alpha [A-Za-z0-9_\-]*) - ("first"|"last"|"size")) >mark %{
emit(:id, :label, data[mark..p-1], tokens)
emit(:call, :variable, nil, tokens)
}
);
main := (
expr <: (hash_accessors)*
);
}%%
# % fix syntax highlighting
module Liquid
module Parser
%% write data;
def self.emit(sym, type, data, tokens)
puts "emitting: #{type} #{sym} -> #{data.inspect}" if $VERBOSE
tokens.push [sym, data]
end
def self.parse(data, tokens = [])
puts "--> self.parse with #{data.inspect}, #{tokens.inspect}" if $VERBOSE
eof = data.length
%% write init;
%% write exec;
puts "<-- #{tokens.inspect}" if $VERBOSE
return tokens
end
end
end

21
lib/liquid/tags/break.rb Normal file
View File

@@ -0,0 +1,21 @@
module Liquid
# Break tag to be used to break out of a for loop.
#
# == Basic Usage:
# {% for item in collection %}
# {% if item.condition %}
# {% break %}
# {% endif %}
# {% endfor %}
#
class Break < Tag
def interrupt
BreakInterrupt.new
end
end
Template.register_tag('break', Break)
end

View File

@@ -0,0 +1,21 @@
module Liquid
# Continue tag to be used to break out of a for loop.
#
# == Basic Usage:
# {% for item in collection %}
# {% if item.condition %}
# {% continue %}
# {% endif %}
# {% endfor %}
#
class Continue < Tag
def interrupt
ContinueInterrupt.new
end
end
Template.register_tag('continue', Continue)
end

View File

@@ -69,7 +69,7 @@ module Liquid
@nodelist = @else_block = []
end
def render(context)
def render(context)
context.registers[:for] ||= Hash.new(0)
collection = context[@collection_name]
@@ -101,8 +101,8 @@ module Liquid
# Store our progress through the collection for the continue flag
context.registers[:for][@name] = from + segment.length
context.stack do
segment.each_with_index do |item, index|
context.stack do
segment.each_with_index do |item, index|
context[@variable_name] = item
context['forloop'] = {
'name' => @name,
@@ -115,6 +115,13 @@ module Liquid
'last' => (index == length - 1) }
result << render_all(@for_block, context)
# Handle any interrupts if they exist.
if context.has_interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
end
end
result

View File

@@ -2,7 +2,7 @@
Gem::Specification.new do |s|
s.name = "liquid"
s.version = "2.4.0"
s.version = "2.4.1"
s.platform = Gem::Platform::RUBY
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
s.authors = ["Tobias Luetke"]

View File

@@ -5,6 +5,7 @@ require File.dirname(__FILE__) + '/theme_runner'
profiler = ThemeRunner.new
Benchmark.bmbm do |x|
x.report("parse & run:") { 10.times { profiler.run(false) } }
x.report("parse:") { 100.times { profiler.compile } }
x.report("parse & run:") { 100.times { profiler.run } }
end

View File

@@ -6,14 +6,12 @@ profiler = ThemeRunner.new
puts 'Running profiler...'
results = profiler.run(true)
results = profiler.run_profile
puts 'Success'
puts
[RubyProf::FlatPrinter, RubyProf::GraphPrinter, RubyProf::GraphHtmlPrinter, RubyProf::CallTreePrinter].each do |klass|
filename = (ENV['TMP'] || '/tmp') + (klass.name.include?('Html') ? "/liquid.#{klass.name.downcase}.html" : "/liquid.#{klass.name.downcase}.txt")
filename.gsub!(/:+/, '_')
File.open(filename, "w+") { |fp| klass.new(results).print(fp) }
$stderr.puts "wrote #{klass.name} output to #{filename}"
filename = (ENV['TMP'] || '/tmp') + "/callgrind.liquid.txt"
File.open(filename, "w+") do |fp|
RubyProf::CallTreePrinter.new(results).print(fp, :print_file => true)
end
$stderr.puts "wrote RubyProf::CallTreePrinter output to #{filename}"

View File

@@ -27,9 +27,34 @@ class ThemeRunner
end.compact
end
def compile
# Dup assigns because will make some changes to them
def run(profile = false)
RubyProf.measure_mode = RubyProf::WALL_TIME if profile
@tests.each do |liquid, layout, template_name|
tmpl = Liquid::Template.new
tmpl.parse(liquid)
tmpl = Liquid::Template.new
tmpl.parse(layout)
end
end
def run
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
@tests.each do |liquid, layout, template_name|
# Compute page_tempalte outside of profiler run, uninteresting to profiler
page_template = File.basename(template_name, File.extname(template_name))
compile_and_render(liquid, layout, assigns, page_template)
end
end
def run_profile
RubyProf.measure_mode = RubyProf::WALL_TIME
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
@@ -40,26 +65,27 @@ class ThemeRunner
html = nil
page_template = File.basename(template_name, File.extname(template_name))
# Profile compiling and rendering both
if profile
RubyProf.resume do
html = compile_and_render(liquid, layout, assigns, page_template)
end
else
html = compile_and_render(liquid, layout, assigns, page_template)
unless @started
RubyProf.start
RubyProf.pause
@started = true
end
html = nil
RubyProf.resume
html = compile_and_render(liquid, layout, assigns, page_template)
RubyProf.pause
# return the result and the MD5 of the content, this can be used to detect regressions between liquid version
$stdout.puts "* rendered template %s, content: %s" % [template_name, Digest::MD5.hexdigest(html)] if profile
$stdout.puts "* rendered template %s, content: %s" % [template_name, Digest::MD5.hexdigest(html)]
# Uncomment to dump html files to /tmp so that you can inspect for errors
# File.open("/tmp/#{File.basename(template_name)}.html", "w+") { |fp| fp <<html}
end
RubyProf.stop if profile
RubyProf.stop
end
def compile_and_render(template, layout, assigns, page_template)

View File

@@ -1,6 +1,6 @@
require 'test_helper'
class VariableTest < Test::Unit::TestCase
class BlockTest < Test::Unit::TestCase
include Liquid
def test_blankspace

View File

@@ -254,12 +254,16 @@ class ContextTest < Test::Unit::TestCase
@context['test'] = {'test' => [1,2,3,4,5]}
assert_equal 1, @context['test.test[0]']
end
def test_recoursive_array_notation_for_hash
@context['test'] = [{'test' => 'worked'}]
assert_equal 'worked', @context['test[0].test']
end
def test_hash_to_array_transition
@context['colors'] = {
'Blue' => ['003366','336699', '6699CC', '99CCFF'],
@@ -315,7 +319,7 @@ class ContextTest < Test::Unit::TestCase
@context['nested'] = {'var' => 'tags'}
@context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
assert_equal 'deepsnow', @context['products[var].first']
#assert_equal 'deepsnow', @context['products[var].first']
assert_equal 'freestyle', @context['products[nested.var].last']
end

View File

@@ -0,0 +1,89 @@
require 'test_helper'
class ParserTest < Test::Unit::TestCase
include Liquid
def test_strings
assert_equal [[:id, "string"]], Parser.parse('"string"')
assert_equal [[:id, "string"]], Parser.parse('\'string\'')
end
def test_integer
assert_equal [[:id, 1]], Parser.parse('1')
assert_equal [[:id, 100001]], Parser.parse('100001')
end
def test_float
assert_equal [[:id, 1.1]], Parser.parse('1.1')
assert_equal [[:id, 1.55435]], Parser.parse('1.55435')
end
def test_null
assert_equal [[:id, nil]], Parser.parse('null')
assert_equal [[:id, nil]], Parser.parse('nil')
end
def test_bool
assert_equal [[:id, true]], Parser.parse('true')
assert_equal [[:id, false]], Parser.parse('false')
end
def test_ranges
assert_equal [[:id, 1], [:id, 5], [:range, nil]], Parser.parse('(1..5)')
assert_equal [[:id, 100], [:id, 500], [:range, nil]], Parser.parse('(100..500)')
end
def test_ranges_with_lookups
assert_equal [[:id, 1], [:id, "test"], [:lookup, nil], [:range, nil]], Parser.parse('(1..test)')
end
def test_lookups
assert_equal [[:id, "variable"], [:lookup, nil]], Parser.parse('variable')
assert_equal [[:id, "underscored_variable"], [:lookup, nil]], Parser.parse('underscored_variable')
end
def test_global_hash
assert_equal [[:id, true], [:lookup, nil]], Parser.parse('[true]')
assert_equal [[:id, "string"], [:lookup, nil]], Parser.parse('["string"]')
assert_equal [[:id, 5.55], [:lookup, nil]], Parser.parse('[5.55]')
assert_equal [[:id, 0], [:lookup, nil]], Parser.parse('[0]')
assert_equal [[:id, "variable"], [:lookup, nil], [:lookup, nil]], Parser.parse('[variable]')
end
def test_descent
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "variable2"], [:call, nil]], Parser.parse('variable1.variable2')
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "variable2"], [:call, nil], [:id, "variable3"], [:call, nil]], Parser.parse('variable1.variable2.variable3')
end
def test_descent_hash
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "variable2"], [:call, nil]], Parser.parse('variable1["variable2"]')
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "variable2"], [:lookup, nil], [:call, nil]], Parser.parse('variable1[variable2]')
end
def test_buildin
assert_equal [[:id, "first"], [:lookup, nil]], Parser.parse('first')
assert_equal [[:id, "var"], [:lookup, nil], [:buildin, "first"]], Parser.parse('var.first')
assert_equal [[:id, "var"], [:lookup, nil], [:buildin, "last"]], Parser.parse('var.last')
assert_equal [[:id, "var"], [:lookup, nil], [:buildin, "size"]], Parser.parse('var.size')
end
def test_descent_hash_descent
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "test1"], [:lookup, nil], [:id, "test2"], [:call, nil], [:call, nil]],
Parser.parse('variable1[test1.test2]'), "resolove: variable1[test1.test2]"
# assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "test1"], [:lookup, nil], [:id, "test2"], [:call, nil], [:call, nil]],
# Parser.parse('variable1[test1["test2"]]'), 'resolove: variable1[test1["test2"]]'
# assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "test1"], [:lookup, nil], [:id, "test2"], [:lookup, nil], [:call, nil], [:call, nil]],
# Parser.parse('variable1[test1[test2]]'), "resolove: variable1[test1[test2]]"
end
end

View File

@@ -39,7 +39,7 @@ class ParsingQuirksTest < Test::Unit::TestCase
def test_meaningless_parens
assigns = {'b' => 'bar', 'c' => 'baz'}
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
markup = "a == 'foo' or b == 'bar' and c == 'baz' or false"
assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}", assigns)
end

View File

@@ -0,0 +1,16 @@
require 'test_helper'
class BreakTagTest < Test::Unit::TestCase
include Liquid
# tests that no weird errors are raised if break is called outside of a
# block
def test_break_with_no_block
assigns = {'i' => 1}
markup = '{% break %}'
expected = ''
assert_template_result(expected, markup, assigns)
end
end

View File

@@ -0,0 +1,16 @@
require 'test_helper'
class ContinueTagTest < Test::Unit::TestCase
include Liquid
# tests that no weird errors are raised if continue is called outside of a
# block
def test_continue_with_no_block
assigns = {}
markup = '{% continue %}'
expected = ''
assert_template_result(expected, markup, assigns)
end
end

View File

@@ -168,6 +168,88 @@ HERE
assert_template_result(expected,markup,assigns)
end
def test_for_with_break
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,10]}}
markup = '{% for i in array.items %}{% break %}{% endfor %}'
expected = ""
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
expected = "1"
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
expected = ""
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
expected = "1234"
assert_template_result(expected,markup,assigns)
# tests to ensure it only breaks out of the local for loop
# and not all of them.
assigns = {'array' => [[1,2],[3,4],[5,6]] }
markup = '{% for item in array %}' +
'{% for i in item %}' +
'{% if i == 1 %}' +
'{% break %}' +
'{% endif %}' +
'{{ i }}' +
'{% endfor %}' +
'{% endfor %}'
expected = '3456'
assert_template_result(expected, markup, assigns)
# test break does nothing when unreached
assigns = {'array' => {'items' => [1,2,3,4,5]}}
markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}'
expected = '12345'
assert_template_result(expected, markup, assigns)
end
def test_for_with_continue
assigns = {'array' => {'items' => [1,2,3,4,5]}}
markup = '{% for i in array.items %}{% continue %}{% endfor %}'
expected = ""
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
expected = "12345"
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
expected = ""
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = "123"
assert_template_result(expected,markup,assigns)
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
expected = "1245"
assert_template_result(expected,markup,assigns)
# tests to ensure it only continues the local for loop and not all of them.
assigns = {'array' => [[1,2],[3,4],[5,6]] }
markup = '{% for item in array %}' +
'{% for i in item %}' +
'{% if i == 1 %}' +
'{% continue %}' +
'{% endif %}' +
'{{ i }}' +
'{% endfor %}' +
'{% endfor %}'
expected = '23456'
assert_template_result(expected, markup, assigns)
# test continue does nothing when unreached
assigns = {'array' => {'items' => [1,2,3,4,5]}}
markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = '12345'
assert_template_result(expected, markup, assigns)
end
def test_for_tag_string
# ruby 1.8.7 "String".each => Enumerator with single "String" element.