From 39e7810f1e456f3cf046b521a5236e2b2dad5d33 Mon Sep 17 00:00:00 2001 From: dinedine Date: Thu, 19 Aug 2010 11:14:33 +0200 Subject: [PATCH 01/61] pass a hash as context when parsing --- lib/liquid/block.rb | 2 +- lib/liquid/document.rb | 3 +- lib/liquid/htmltags.rb | 2 +- lib/liquid/tag.rb | 5 ++- lib/liquid/tags/assign.rb | 22 +++++----- lib/liquid/tags/capture.rb | 2 +- lib/liquid/tags/case.rb | 68 ++++++++++++++--------------- lib/liquid/tags/cycle.rb | 44 +++++++++---------- lib/liquid/tags/for.rb | 88 +++++++++++++++++++------------------- lib/liquid/tags/if.rb | 64 +++++++++++++-------------- lib/liquid/tags/include.rb | 32 +++++++------- lib/liquid/template.rb | 10 ++--- test/standard_tag_test.rb | 7 ++- 13 files changed, 178 insertions(+), 171 deletions(-) diff --git a/lib/liquid/block.rb b/lib/liquid/block.rb index 10df350f1..fb12f7be8 100644 --- a/lib/liquid/block.rb +++ b/lib/liquid/block.rb @@ -25,7 +25,7 @@ def parse(tokens) # fetch the tag from registered blocks if tag = Template.tags[$1] - @nodelist << tag.new($1, $2, tokens) + @nodelist << tag.new($1, $2, tokens, @context) else # this tag is not registered with the system # pass it to the current block for special handling or error reporting diff --git a/lib/liquid/document.rb b/lib/liquid/document.rb index bf95478d8..fa691d0b8 100644 --- a/lib/liquid/document.rb +++ b/lib/liquid/document.rb @@ -1,7 +1,8 @@ module Liquid class Document < Block # we don't need markup to open this block - def initialize(tokens) + def initialize(tokens, context) + @context = context parse(tokens) end diff --git a/lib/liquid/htmltags.rb b/lib/liquid/htmltags.rb index c6db036f1..68f53befb 100644 --- a/lib/liquid/htmltags.rb +++ b/lib/liquid/htmltags.rb @@ -2,7 +2,7 @@ module Liquid class TableRow < Block Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/ - def initialize(tag_name, markup, tokens) + def initialize(tag_name, markup, tokens, context) if markup =~ Syntax @variable_name = $1 @collection_name = $2 diff --git a/lib/liquid/tag.rb b/lib/liquid/tag.rb index 2750064f0..f283a8824 100644 --- a/lib/liquid/tag.rb +++ b/lib/liquid/tag.rb @@ -1,11 +1,12 @@ module Liquid class Tag - attr_accessor :nodelist + attr_accessor :nodelist, :context - def initialize(tag_name, markup, tokens) + def initialize(tag_name, markup, tokens, context) @tag_name = tag_name @markup = markup + @context = context parse(tokens) end diff --git a/lib/liquid/tags/assign.rb b/lib/liquid/tags/assign.rb index 61e837fcb..4ec475089 100644 --- a/lib/liquid/tags/assign.rb +++ b/lib/liquid/tags/assign.rb @@ -9,25 +9,25 @@ module Liquid # {{ foo }} # class Assign < Tag - Syntax = /(#{VariableSignature}+)\s*=\s*(#{QuotedFragment}+)/ - - def initialize(tag_name, markup, tokens) + Syntax = /(#{VariableSignature}+)\s*=\s*(#{QuotedFragment}+)/ + + def initialize(tag_name, markup, tokens, context) if markup =~ Syntax @to = $1 @from = $2 else raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]") end - - super + + super end - + def render(context) context.scopes.last[@to] = context[@from] '' - end - - end - - Template.register_tag('assign', Assign) + end + + end + + Template.register_tag('assign', Assign) end diff --git a/lib/liquid/tags/capture.rb b/lib/liquid/tags/capture.rb index 89591a493..66d1b2525 100644 --- a/lib/liquid/tags/capture.rb +++ b/lib/liquid/tags/capture.rb @@ -14,7 +14,7 @@ module Liquid class Capture < Block Syntax = /(\w+)/ - def initialize(tag_name, markup, tokens) + def initialize(tag_name, markup, tokens, context) if markup =~ Syntax @to = $1 else diff --git a/lib/liquid/tags/case.rb b/lib/liquid/tags/case.rb index 0733c51ad..7b60cc49e 100644 --- a/lib/liquid/tags/case.rb +++ b/lib/liquid/tags/case.rb @@ -3,15 +3,15 @@ class Case < Block Syntax = /(#{QuotedFragment})/ WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/ - def initialize(tag_name, markup, tokens) + def initialize(tag_name, markup, tokens, context) @blocks = [] - + if markup =~ Syntax @left = $1 else raise SyntaxError.new("Syntax Error in tag 'case' - Valid syntax: case [condition]") end - + super end @@ -27,57 +27,57 @@ def unknown_tag(tag, markup, tokens) end end - def render(context) - context.stack do + def render(context) + context.stack do execute_else_block = true - + @blocks.inject([]) do |output, block| - - if block.else? - + + if block.else? + return render_all(block.attachment, context) if execute_else_block - + elsif block.evaluate(context) - - execute_else_block = false - output += render_all(block.attachment, context) - end - + + execute_else_block = false + output += render_all(block.attachment, context) + end + output end - end + end end - + private - - def record_when_condition(markup) + + def record_when_condition(markup) while markup - # Create a new nodelist and assign it to the new block - if not markup =~ WhenSyntax - raise SyntaxError.new("Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %} ") - end + # Create a new nodelist and assign it to the new block + if not markup =~ WhenSyntax + raise SyntaxError.new("Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %} ") + end - markup = $2 + markup = $2 - block = Condition.new(@left, '==', $1) - block.attach(@nodelist) - @blocks.push(block) + block = Condition.new(@left, '==', $1) + block.attach(@nodelist) + @blocks.push(block) end end - def record_else_condition(markup) + def record_else_condition(markup) if not markup.strip.empty? raise SyntaxError.new("Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) ") end - - block = ElseCondition.new + + block = ElseCondition.new block.attach(@nodelist) @blocks << block end - - - end - + + + end + Template.register_tag('case', Case) end diff --git a/lib/liquid/tags/cycle.rb b/lib/liquid/tags/cycle.rb index 1df692e69..7deca86f1 100644 --- a/lib/liquid/tags/cycle.rb +++ b/lib/liquid/tags/cycle.rb @@ -1,5 +1,5 @@ module Liquid - + # Cycle is usually used within a loop to alternate between values, like colors or DOM classes. # # {% for item in items %} @@ -13,47 +13,47 @@ module Liquid #
Item five
# class Cycle < Tag - SimpleSyntax = /^#{QuotedFragment}+/ + SimpleSyntax = /^#{QuotedFragment}+/ NamedSyntax = /^(#{QuotedFragment})\s*\:\s*(.*)/ - - def initialize(tag_name, markup, tokens) + + def initialize(tag_name, markup, tokens, context) case markup when NamedSyntax - @variables = variables_from_string($2) - @name = $1 + @variables = variables_from_string($2) + @name = $1 when SimpleSyntax @variables = variables_from_string(markup) - @name = "'#{@variables.to_s}'" + @name = "'#{@variables.to_s}'" else raise SyntaxError.new("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]") end - super - end - + super + end + def render(context) context.registers[:cycle] ||= Hash.new(0) - + context.stack do - key = context[@name] + key = context[@name] iteration = context.registers[:cycle][key] result = context[@variables[iteration]] - iteration += 1 - iteration = 0 if iteration >= @variables.size + iteration += 1 + iteration = 0 if iteration >= @variables.size context.registers[:cycle][key] = iteration - result + result end end - + private - + def variables_from_string(markup) markup.split(',').collect do |var| - var =~ /\s*(#{QuotedFragment})\s*/ - $1 ? $1 : nil - end.compact + var =~ /\s*(#{QuotedFragment})\s*/ + $1 ? $1 : nil + end.compact end - + end - + Template.register_tag('cycle', Cycle) end \ No newline at end of file diff --git a/lib/liquid/tags/for.rb b/lib/liquid/tags/for.rb index b17a3320b..4f40babbb 100644 --- a/lib/liquid/tags/for.rb +++ b/lib/liquid/tags/for.rb @@ -1,6 +1,6 @@ module Liquid - # "For" iterates over an array or collection. + # "For" iterates over an array or collection. # Several useful variables are available to you within the loop. # # == Basic usage: @@ -20,7 +20,7 @@ module Liquid # # {% for item in collection limit:5 offset:10 %} # {{ item.name }} - # {% end %} + # {% end %} # # To reverse the for loop simply use {% for item in collection reversed %} # @@ -29,7 +29,7 @@ module Liquid # forloop.name:: 'item-collection' # forloop.length:: Length of the loop # forloop.index:: The current item's position in the collection; - # forloop.index starts at 1. + # forloop.index starts at 1. # This is helpful for non-programmers who start believe # the first item in an array is 1, not 0. # forloop.index0:: The current item's position in the collection @@ -41,65 +41,65 @@ module Liquid # forloop.first:: Returns true if the item is the first item. # forloop.last:: Returns true if the item is the last item. # - class For < Block - Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/ - - def initialize(tag_name, markup, tokens) + class For < Block + Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/ + + def initialize(tag_name, markup, tokens, context) if markup =~ Syntax @variable_name = $1 @collection_name = $2 - @name = "#{$1}-#{$2}" - @reversed = $3 + @name = "#{$1}-#{$2}" + @reversed = $3 @attributes = {} markup.scan(TagAttributes) do |key, value| @attributes[key] = value - end + end else raise SyntaxError.new("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]") end super end - - def render(context) + + def render(context) context.registers[:for] ||= Hash.new(0) - + collection = context[@collection_name] collection = collection.to_a if collection.is_a?(Range) - - return '' unless collection.respond_to?(:each) - + + return '' unless collection.respond_to?(:each) + from = if @attributes['offset'] == 'continue' context.registers[:for][@name].to_i else context[@attributes['offset']].to_i end - + limit = context[@attributes['limit']] - to = limit ? limit.to_i + from : nil - - - segment = slice_collection_using_each(collection, from, to) - + to = limit ? limit.to_i + from : nil + + + segment = slice_collection_using_each(collection, from, to) + return '' if segment.empty? - + segment.reverse! if @reversed result = [] - - length = segment.length - + + length = segment.length + # 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, 'length' => length, - 'index' => index + 1, - 'index0' => index, + 'index' => index + 1, + 'index0' => index, 'rindex' => length - index, 'rindex0' => length - index -1, 'first' => (index == 0), @@ -108,25 +108,25 @@ def render(context) result << render_all(@nodelist, context) end end - result - end - - def slice_collection_using_each(collection, from, to) - segments = [] - index = 0 + result + end + + def slice_collection_using_each(collection, from, to) + segments = [] + index = 0 yielded = 0 - collection.each do |item| - + collection.each do |item| + if to && to <= index break end - - if from <= index + + if from <= index segments << item - end - + end + index += 1 - end + end segments end diff --git a/lib/liquid/tags/if.rb b/lib/liquid/tags/if.rb index f06000097..06d5558dd 100644 --- a/lib/liquid/tags/if.rb +++ b/lib/liquid/tags/if.rb @@ -15,16 +15,16 @@ class If < Block SyntaxHelp = "Syntax Error in tag 'if' - Valid syntax: if [expression]" Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/ ExpressionsAndOperators = /(?:\b(?:and|or)\b|(?:\s*(?!\b(?:and|or)\b)(?:#{QuotedFragment}|\S+)\s*)+)/ - - def initialize(tag_name, markup, tokens) - + + def initialize(tag_name, markup, tokens, context) + @blocks = [] - + push_block('if', markup) - - super + + super end - + def unknown_tag(tag, markup, tokens) if ['elsif', 'else'].include?(tag) push_block(tag, markup) @@ -32,48 +32,48 @@ def unknown_tag(tag, markup, tokens) super end end - + def render(context) context.stack do @blocks.each do |block| - if block.evaluate(context) - return render_all(block.attachment, context) + if block.evaluate(context) + return render_all(block.attachment, context) end - end + end '' end end - + private - - def push_block(tag, markup) + + def push_block(tag, markup) block = if tag == 'else' ElseCondition.new - else - + else + expressions = markup.scan(ExpressionsAndOperators).reverse - raise(SyntaxError, SyntaxHelp) unless expressions.shift =~ Syntax + raise(SyntaxError, SyntaxHelp) unless expressions.shift =~ Syntax + + condition = Condition.new($1, $2, $3) - condition = Condition.new($1, $2, $3) - while not expressions.empty? - operator = expressions.shift - - raise(SyntaxError, SyntaxHelp) unless expressions.shift.to_s =~ Syntax - + operator = expressions.shift + + raise(SyntaxError, SyntaxHelp) unless expressions.shift.to_s =~ Syntax + new_condition = Condition.new($1, $2, $3) - new_condition.send(operator.to_sym, condition) - condition = new_condition - end - + new_condition.send(operator.to_sym, condition) + condition = new_condition + end + condition end - - @blocks.push(block) - @nodelist = block.attach(Array.new) + + @blocks.push(block) + @nodelist = block.attach(Array.new) end - - + + end Template.register_tag('if', If) diff --git a/lib/liquid/tags/include.rb b/lib/liquid/tags/include.rb index 3c74d72a0..176438851 100644 --- a/lib/liquid/tags/include.rb +++ b/lib/liquid/tags/include.rb @@ -1,11 +1,11 @@ module Liquid class Include < Tag Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/ - - def initialize(tag_name, markup, tokens) + + def initialize(tag_name, markup, tokens, context) if markup =~ Syntax - @template_name = $1 + @template_name = $1 @variable_name = $3 @attributes = {} @@ -19,38 +19,38 @@ def initialize(tag_name, markup, tokens) super end - - def parse(tokens) + + def parse(tokens) end - - def render(context) + + def render(context) file_system = context.registers[:file_system] || Liquid::Template.file_system - source = file_system.read_template_file(context[@template_name]) - partial = Liquid::Template.parse(source) - + source = file_system.read_template_file(context[@template_name]) + partial = Liquid::Template.parse(source) + variable = context[@variable_name || @template_name[1..-2]] - + context.stack do @attributes.each do |key, value| context[key] = context[value] end if variable.is_a?(Array) - - variable.collect do |variable| + + variable.collect do |variable| context[@template_name[1..-2]] = variable partial.render(context) end else - + context[@template_name[1..-2]] = variable partial.render(context) - + end end end end - Template.register_tag('include', Include) + Template.register_tag('include', Include) end \ No newline at end of file diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb index edfb980e3..cb0f2efd5 100644 --- a/lib/liquid/template.rb +++ b/lib/liquid/template.rb @@ -41,9 +41,9 @@ def register_filter(mod) end # creates a new Template object from liquid source code - def parse(source) + def parse(source, context = {}) template = Template.new - template.parse(source) + template.parse(source, context) template end end @@ -54,8 +54,8 @@ def initialize # Parse source code. # Returns self for easy chaining - def parse(source) - @root = Document.new(tokenize(source)) + def parse(source, context = {}) + @root = Document.new(tokenize(source), context.merge(:template => self)) self end @@ -88,7 +88,7 @@ def errors # def render(*args) return '' if @root.nil? - + context = case args.first when Liquid::Context args.shift diff --git a/test/standard_tag_test.rb b/test/standard_tag_test.rb index 17d8df1af..f2152bab7 100644 --- a/test/standard_tag_test.rb +++ b/test/standard_tag_test.rb @@ -6,11 +6,16 @@ class StandardTagTest < Test::Unit::TestCase def test_tag - tag = Tag.new('tag', [], []) + tag = Tag.new('tag', [], [], {}) assert_equal 'liquid::tag', tag.name assert_equal '', tag.render(Context.new) end + def test_store_context_at_parsing_time + tag = Tag.new('tag', [], [], { :foo => 'bar' }) + assert_equal 'bar', tag.context[:foo] + end + def test_no_transform assert_template_result('this text should come out of the template without change...', 'this text should come out of the template without change...') From 61507053ec64ac2fe3e8620d87db0415ad1fd6b1 Mon Sep 17 00:00:00 2001 From: dinedine Date: Thu, 19 Aug 2010 21:49:48 +0200 Subject: [PATCH 02/61] implementation of template inheritance + tests --- Rakefile | 21 ++++--- lib/liquid/tags/extends.rb | 56 ++++++++++++++++++ lib/liquid/tags/inherited_block.rb | 93 ++++++++++++++++++++++++++++++ test/extends_test.rb | 42 ++++++++++++++ test/inherited_block_test.rb | 46 +++++++++++++++ test/parsing_context_test.rb | 54 +++++++++++++++++ 6 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 lib/liquid/tags/extends.rb create mode 100644 lib/liquid/tags/inherited_block.rb create mode 100644 test/extends_test.rb create mode 100644 test/inherited_block_test.rb create mode 100644 test/parsing_context_test.rb diff --git a/Rakefile b/Rakefile index 808770653..2278ccb9b 100755 --- a/Rakefile +++ b/Rakefile @@ -13,6 +13,13 @@ Rake::TestTask.new(:test) do |t| t.verbose = false end +Rake::TestTask.new(:ti) do |t| + t.libs << "lib" + t.libs << "test" + t.test_files = ['test/test_helper.rb', 'test/extends_test.rb', 'test/inherited_block_test.rb'] + t.verbose = false +end + gemspec = eval(File.read('liquid.gemspec')) Rake::GemPackageTask.new(gemspec) do |pkg| pkg.gem_spec = gemspec @@ -25,20 +32,18 @@ end namespace :profile do - task :default => [:run] - + desc "Run the liquid profile/perforamce coverage" task :run do - + ruby "performance/shopify.rb" - + end - - desc "Run KCacheGrind" + + desc "Run KCacheGrind" task :grind => :run do system "kcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt" end end - - \ No newline at end of file + diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb new file mode 100644 index 000000000..4e43d62fe --- /dev/null +++ b/lib/liquid/tags/extends.rb @@ -0,0 +1,56 @@ +module Liquid + + # Extends allows designer to use template inheritance + # + # {% extends home %} + # {% block content }Hello world{% endblock %} + # + class Extends < Block + Syntax = /(#{QuotedFragment}+)/ + + def initialize(tag_name, markup, tokens, context) + if markup =~ Syntax + @template_name = $1 + else + raise SyntaxError.new("Error in tag 'extends' - Valid syntax: extends [template]") + end + + super + + context.merge!(:extending => true, :blocks => find_blocks(@nodelist)) + + # puts "[EXTENDS #{@template_name}] blocks = #{context[:blocks].inspect}" + + parent_template = parse_parent_template(context) + + # replace the nodelist by the new one + @nodelist = parent_template.root.nodelist + end + + private + + def parse_parent_template(context) + source = Template.file_system.read_template_file(@template_name) + Template.parse(source, context) + end + + def find_blocks(nodelist, blocks = {}) + if nodelist && nodelist.any? + nodelist.inject(blocks) do |b, node| + if node.is_a?(Liquid::InheritedBlock) + b[node.name] = node + elsif node.respond_to?(:nodelist) + self.find_blocks(node.nodelist, b) # FIXME: find nested blocks too + end + b + end + end + blocks + end + + def assert_missing_delimitation! + end + end + + Template.register_tag('extends', Extends) +end \ No newline at end of file diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb new file mode 100644 index 000000000..157ec9b70 --- /dev/null +++ b/lib/liquid/tags/inherited_block.rb @@ -0,0 +1,93 @@ +module Liquid + + # Blocks are used with the Extends tag to define + # the content of blocks. Nested blocks are allowed. + # + # {% extends home %} + # {% block content }Hello world{% endblock %} + # + class InheritedBlock < Block + Syntax = /(#{QuotedFragment}+)/ + + attr_accessor :parent + attr_reader :name + + def initialize(tag_name, markup, tokens, context) + if markup =~ Syntax + @name = $1 + else + raise SyntaxError.new("Error in tag 'block' - Valid syntax: block [name]") + end + + super if tokens + end + + def render(context) + # puts "[BLOCK #{@name}|render] parent = #{@parent.inspect}" + context.stack do + context['block'] = InheritedBlockDrop.new(self) + render_all(@nodelist, context) + end + end + + def end_tag + if context[:extending] + block = context[:blocks][@name] + + # puts "[BLOCK #{@name}|end_tag] extending, block found ? #{!block.nil?}" + + if block + # needed for the block.super statement + # puts "[BLOCK #{@name}|end_tag] nodelist #{@nodelist.inspect}" + block.add_parent(@nodelist) + @parent = block.parent + # puts "[BLOCK #{@name}|end_tag] direct parent #{block.parent.inspect}" + @nodelist = block.nodelist + else + # puts "[BLOCK #{@name}|end_tag] register it" + # register it + context[:blocks][@name] = self + end + else + (context[:blocks] ||= {})[@name] = self + # puts "[BLOCK #{@name}|end_tag] not extending" + end + end + + def add_parent(nodelist) + if parent + # puts "[BLOCK #{@name}|add_parent] go upper" + parent.add_parent(nodelist) + else + # puts "[BLOCK #{@name}|add_parent] create parent #{@tag_name}, #{@name}" + @parent = self.class.new(@tag_name, @name, nil, {}) + @parent.nodelist = nodelist + end + end + + def call_super(context) + # puts "[BLOCK #{@name}|call_super] #{parent.inspect}" + if parent + parent.render(context) + else + '' + end + end + + end + + class InheritedBlockDrop < Drop + + def initialize(block) + @block = block + end + + def super + # puts "[InheritedBlockDrop] called" + @block.call_super(@context) + end + + end + + Template.register_tag('block', InheritedBlock) +end \ No newline at end of file diff --git a/test/extends_test.rb b/test/extends_test.rb new file mode 100644 index 000000000..71186423a --- /dev/null +++ b/test/extends_test.rb @@ -0,0 +1,42 @@ +class ExtendsTest < Test::Unit::TestCase + include Liquid + + class TestFileSystem + def read_template_file(template_path) + case template_path + when "another_path" + "Another path!" + when "variable" + "Hello, {{ name }}!" + when "deep" + "{% extends variable %}" + else + template_path + end + end + end + + def setup + Liquid::Template.file_system = TestFileSystem.new + end + + def test_extends + document = Template.parse("{% extends another_path %}") + assert_equal 'Another path!', document.render({}) + end + + def test_extends_with_more + document = Template.parse("{% extends another_path %} Hello!") + assert_equal 'Another path!', document.render({}) + end + + def test_extends_var + document = Template.parse("{% extends variable %}") + assert_equal 'Hello, berto!', document.render({'name' => 'berto'}) + end + + def test_extends_deeper + document = Template.parse("{% extends deep %}") + assert_equal 'Hello, berto!', document.render({'name' => 'berto'}) + end +end \ No newline at end of file diff --git a/test/inherited_block_test.rb b/test/inherited_block_test.rb new file mode 100644 index 000000000..8f2e5cd9c --- /dev/null +++ b/test/inherited_block_test.rb @@ -0,0 +1,46 @@ +class BlockTagTest < Test::Unit::TestCase + include Liquid + + class TestFileSystem + def read_template_file(template_path) + case template_path + when "base" + "Output / {% block content %}Hello, World!{% endblock %}" + when "deep" + "{% extends base %}{% block content %}Deep: {{block.super}}{% endblock %}" + when "nested_and_deep" + "{% extends base %}{% block content %}Deep: {{block.super}} -{% block inner %}FOO{% endblock %}-{% endblock %}" + else + template_path + end + end + end + + def setup + Liquid::Template.file_system = TestFileSystem.new + end + + def test_extends + document = Template.parse("{% extends base %}{% block content %}Hola, Mundo!{% endblock %}") + rendered = document.render({}) + assert_equal 'Output / Hola, Mundo!', rendered + end + + def test_block_super + document = Template.parse("{% extends base %}{% block content %}Lorem ipsum: {{block.super}}{% endblock %}") + rendered = document.render({}) + assert_equal 'Output / Lorem ipsum: Hello, World!', rendered + end + + def test_deep_block_super + document = Template.parse("{% extends deep %}{% block content %}Lorem ipsum: {{block.super}}{% endblock %}") + rendered = document.render({}) + assert_equal 'Output / Lorem ipsum: Deep: Hello, World!', rendered + end + + def test_nested_deep_blocks + document = Template.parse("{% extends nested_and_deep %}{% block inner %}BAR{% endblock %}") + rendered = document.render({}) + assert_equal 'Output / Deep: Hello, World! -BAR-', rendered + end +end \ No newline at end of file diff --git a/test/parsing_context_test.rb b/test/parsing_context_test.rb new file mode 100644 index 000000000..19d586fd1 --- /dev/null +++ b/test/parsing_context_test.rb @@ -0,0 +1,54 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/helper' + +class ParsingQuirksTest < Test::Unit::TestCase + include Liquid + + def test_error_with_css + text = %| div { font-weight: bold; } | + template = Template.parse(text) + + assert_equal text, template.render + assert_equal [String], template.root.nodelist.collect {|i| i.class} + end + + def test_raise_on_single_close_bracet + assert_raise(SyntaxError) do + Template.parse("text {{method} oh nos!") + end + end + + def test_raise_on_label_and_no_close_bracets + assert_raise(SyntaxError) do + Template.parse("TEST {{ ") + end + end + + def test_raise_on_label_and_no_close_bracets_percent + assert_raise(SyntaxError) do + Template.parse("TEST {% ") + end + end + + def test_error_on_empty_filter + assert_nothing_raised do + Template.parse("{{test |a|b|}}") + Template.parse("{{test}}") + Template.parse("{{|test|}}") + end + end + + def test_meaningless_parens + assigns = {'b' => 'bar', 'c' => 'baz'} + markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" + assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}", assigns) + end + + def test_unexpected_characters_silently_eat_logic + markup = "true && false" + assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}") + markup = "false || true" + assert_template_result('',"{% if #{markup} %} YES {% endif %}") + end + +end \ No newline at end of file From 92aad46e75fe5464e23c19df927e0ba91573ce82 Mon Sep 17 00:00:00 2001 From: dinedine Date: Thu, 19 Aug 2010 22:10:27 +0200 Subject: [PATCH 03/61] clean code --- lib/liquid/tags/extends.rb | 2 +- lib/liquid/tags/inherited_block.rb | 39 ++++++++++++++---------------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb index 4e43d62fe..9b3c37fd1 100644 --- a/lib/liquid/tags/extends.rb +++ b/lib/liquid/tags/extends.rb @@ -17,7 +17,7 @@ def initialize(tag_name, markup, tokens, context) super - context.merge!(:extending => true, :blocks => find_blocks(@nodelist)) + context.merge!(:blocks => find_blocks(@nodelist)) # puts "[EXTENDS #{@template_name}] blocks = #{context[:blocks].inspect}" diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb index 157ec9b70..1e1767168 100644 --- a/lib/liquid/tags/inherited_block.rb +++ b/lib/liquid/tags/inherited_block.rb @@ -31,33 +31,30 @@ def render(context) end def end_tag - if context[:extending] - block = context[:blocks][@name] - - # puts "[BLOCK #{@name}|end_tag] extending, block found ? #{!block.nil?}" - - if block - # needed for the block.super statement - # puts "[BLOCK #{@name}|end_tag] nodelist #{@nodelist.inspect}" - block.add_parent(@nodelist) - @parent = block.parent - # puts "[BLOCK #{@name}|end_tag] direct parent #{block.parent.inspect}" - @nodelist = block.nodelist - else - # puts "[BLOCK #{@name}|end_tag] register it" - # register it - context[:blocks][@name] = self - end + context[:blocks] ||= {} + + block = context[:blocks][@name] + + if block + # needed for the block.super statement + # puts "[BLOCK #{@name}|end_tag] nodelist #{@nodelist.inspect}" + block.add_parent(@nodelist) + + @parent = block.parent + @nodelist = block.nodelist + + # puts "[BLOCK #{@name}|end_tag] direct parent #{block.parent.inspect}" else - (context[:blocks] ||= {})[@name] = self - # puts "[BLOCK #{@name}|end_tag] not extending" + # register it + # puts "[BLOCK #{@name}|end_tag] register it" + context[:blocks][@name] = self end end def add_parent(nodelist) - if parent + if @parent # puts "[BLOCK #{@name}|add_parent] go upper" - parent.add_parent(nodelist) + @parent.add_parent(nodelist) else # puts "[BLOCK #{@name}|add_parent] create parent #{@tag_name}, #{@name}" @parent = self.class.new(@tag_name, @name, nil, {}) From a41213c77cbc81dab87d94c28e52f8dbb0e4be5a Mon Sep 17 00:00:00 2001 From: dinedine Date: Fri, 20 Aug 2010 01:49:21 +0200 Subject: [PATCH 04/61] change find_blocks method visibility + fix bug --- lib/liquid/tags/extends.rb | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb index 9b3c37fd1..818d159b9 100644 --- a/lib/liquid/tags/extends.rb +++ b/lib/liquid/tags/extends.rb @@ -27,19 +27,15 @@ def initialize(tag_name, markup, tokens, context) @nodelist = parent_template.root.nodelist end - private - - def parse_parent_template(context) - source = Template.file_system.read_template_file(@template_name) - Template.parse(source, context) - end + protected def find_blocks(nodelist, blocks = {}) if nodelist && nodelist.any? nodelist.inject(blocks) do |b, node| if node.is_a?(Liquid::InheritedBlock) b[node.name] = node - elsif node.respond_to?(:nodelist) + end + if node.respond_to?(:nodelist) self.find_blocks(node.nodelist, b) # FIXME: find nested blocks too end b @@ -48,6 +44,15 @@ def find_blocks(nodelist, blocks = {}) blocks end + private + + def parse_parent_template(context) + source = Template.file_system.read_template_file(@template_name) + Template.parse(source, context) + end + + + def assert_missing_delimitation! end end From ba44b85f2f247d0fb50fb845a395fc4b959d1129 Mon Sep 17 00:00:00 2001 From: dinedine Date: Tue, 24 Aug 2010 15:18:15 +0200 Subject: [PATCH 05/61] keep current block in context during parsing --- lib/liquid/tags/extends.rb | 2 -- lib/liquid/tags/inherited_block.rb | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb index 818d159b9..9da20c0cb 100644 --- a/lib/liquid/tags/extends.rb +++ b/lib/liquid/tags/extends.rb @@ -51,8 +51,6 @@ def parse_parent_template(context) Template.parse(source, context) end - - def assert_missing_delimitation! end end diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb index 1e1767168..acbd4ecd3 100644 --- a/lib/liquid/tags/inherited_block.rb +++ b/lib/liquid/tags/inherited_block.rb @@ -19,6 +19,8 @@ def initialize(tag_name, markup, tokens, context) raise SyntaxError.new("Error in tag 'block' - Valid syntax: block [name]") end + context[:current_block] = self + super if tokens end From dbcc0b1d9c189b3e3a13e3161983cf87c1fe01ee Mon Sep 17 00:00:00 2001 From: dinedine Date: Tue, 24 Aug 2010 15:42:20 +0200 Subject: [PATCH 06/61] a bit of refactoring --- lib/liquid/tags/extends.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb index 9da20c0cb..dad0fc3c3 100644 --- a/lib/liquid/tags/extends.rb +++ b/lib/liquid/tags/extends.rb @@ -17,11 +17,11 @@ def initialize(tag_name, markup, tokens, context) super - context.merge!(:blocks => find_blocks(@nodelist)) + @context.merge!(:blocks => find_blocks(@nodelist)) - # puts "[EXTENDS #{@template_name}] blocks = #{context[:blocks].inspect}" + # puts "[EXTENDS #{@template_name}] blocks = #{@context[:blocks].inspect}" - parent_template = parse_parent_template(context) + parent_template = parse_parent_template # replace the nodelist by the new one @nodelist = parent_template.root.nodelist @@ -46,9 +46,9 @@ def find_blocks(nodelist, blocks = {}) private - def parse_parent_template(context) + def parse_parent_template source = Template.file_system.read_template_file(@template_name) - Template.parse(source, context) + Template.parse(source, @context) end def assert_missing_delimitation! From 0c1cf77d36b130bf04e0e187f87c78938716ee07 Mon Sep 17 00:00:00 2001 From: dinedine Date: Tue, 24 Aug 2010 23:59:10 +0200 Subject: [PATCH 07/61] a bit of refactoring --- lib/liquid/tags/extends.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb index dad0fc3c3..1c2d03144 100644 --- a/lib/liquid/tags/extends.rb +++ b/lib/liquid/tags/extends.rb @@ -17,6 +17,10 @@ def initialize(tag_name, markup, tokens, context) super + end_tag + end + + def end_tag @context.merge!(:blocks => find_blocks(@nodelist)) # puts "[EXTENDS #{@template_name}] blocks = #{@context[:blocks].inspect}" From 9e35d06a684b238528c44df512aa73b30e4565af Mon Sep 17 00:00:00 2001 From: dinedine Date: Wed, 25 Aug 2010 10:55:33 +0200 Subject: [PATCH 08/61] add the name method in block drops --- lib/liquid/tags/inherited_block.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb index acbd4ecd3..28b14ec4b 100644 --- a/lib/liquid/tags/inherited_block.rb +++ b/lib/liquid/tags/inherited_block.rb @@ -81,6 +81,10 @@ def initialize(block) @block = block end + def name + @block.name + end + def super # puts "[InheritedBlockDrop] called" @block.call_super(@context) From f299f8b7ff6ea102bfb3d1e28fc118c445626a48 Mon Sep 17 00:00:00 2001 From: dinedine Date: Thu, 26 Aug 2010 12:40:49 +0200 Subject: [PATCH 09/61] context used in the parsing time was not the same instance all the time causing some bugs --- lib/liquid/template.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb index cb0f2efd5..d09a9b20d 100644 --- a/lib/liquid/template.rb +++ b/lib/liquid/template.rb @@ -55,7 +55,7 @@ def initialize # Parse source code. # Returns self for easy chaining def parse(source, context = {}) - @root = Document.new(tokenize(source), context.merge(:template => self)) + @root = Document.new(tokenize(source), context.merge!(:template => self)) self end From 7ce591f38ab8268753376310d1ea40f17af7f4c3 Mon Sep 17 00:00:00 2001 From: dinedine Date: Fri, 27 Aug 2010 17:44:00 +0200 Subject: [PATCH 10/61] split the end_tag method for InheritedBlock --- lib/liquid/tags/inherited_block.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb index 28b14ec4b..c4bea7049 100644 --- a/lib/liquid/tags/inherited_block.rb +++ b/lib/liquid/tags/inherited_block.rb @@ -33,23 +33,23 @@ def render(context) end def end_tag - context[:blocks] ||= {} + self.register_current_block + end + + def register_current_block + @context[:blocks] ||= {} - block = context[:blocks][@name] + block = @context[:blocks][@name] if block # needed for the block.super statement - # puts "[BLOCK #{@name}|end_tag] nodelist #{@nodelist.inspect}" block.add_parent(@nodelist) @parent = block.parent @nodelist = block.nodelist - - # puts "[BLOCK #{@name}|end_tag] direct parent #{block.parent.inspect}" else # register it - # puts "[BLOCK #{@name}|end_tag] register it" - context[:blocks][@name] = self + @context[:blocks][@name] = self end end From 9ec570927f5281e1f397223b4b3b1eeae0e220f0 Mon Sep 17 00:00:00 2001 From: dinedine Date: Tue, 31 Aug 2010 01:46:47 +0200 Subject: [PATCH 11/61] new algorithm for the inheritance module. This one is much more simpler to understand + refactor / clean code --- Rakefile | 1 + lib/liquid/tags/extends.rb | 40 +++++++++++++++-------- lib/liquid/tags/inherited_block.rb | 51 +++++++++++++----------------- test/test_helper.rb | 18 ++++++++++- 4 files changed, 66 insertions(+), 44 deletions(-) diff --git a/Rakefile b/Rakefile index 2278ccb9b..91c8511e2 100755 --- a/Rakefile +++ b/Rakefile @@ -17,6 +17,7 @@ Rake::TestTask.new(:ti) do |t| t.libs << "lib" t.libs << "test" t.test_files = ['test/test_helper.rb', 'test/extends_test.rb', 'test/inherited_block_test.rb'] + # t.test_files = ['test/test_helper.rb', 'test/inherited_block_test.rb', 'test/inherited_block_test.rb'] t.verbose = false end diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb index 1c2d03144..15da2efac 100644 --- a/lib/liquid/tags/extends.rb +++ b/lib/liquid/tags/extends.rb @@ -10,39 +10,51 @@ class Extends < Block def initialize(tag_name, markup, tokens, context) if markup =~ Syntax - @template_name = $1 + @template_name = $1.gsub('\'', '').strip else raise SyntaxError.new("Error in tag 'extends' - Valid syntax: extends [template]") end + @context = context + + @parent_template = parse_parent_template + + prepare_parsing + super end_tag end - def end_tag - @context.merge!(:blocks => find_blocks(@nodelist)) - - # puts "[EXTENDS #{@template_name}] blocks = #{@context[:blocks].inspect}" - - parent_template = parse_parent_template + def prepare_parsing + @context.merge!(:blocks => self.find_blocks(@parent_template.root.nodelist)) + end + def end_tag # replace the nodelist by the new one - @nodelist = parent_template.root.nodelist + @nodelist = @parent_template.root.nodelist.clone + + @parent_template = nil # no need to keep it end protected def find_blocks(nodelist, blocks = {}) if nodelist && nodelist.any? - nodelist.inject(blocks) do |b, node| - if node.is_a?(Liquid::InheritedBlock) - b[node.name] = node + 0.upto(nodelist.size - 1).each do |index| + node = nodelist[index] + + if node.respond_to?(:call_super) # inherited block ! + new_node = node.class.clone_block(node) + + nodelist.insert(index, new_node) + nodelist.delete_at(index + 1) + + blocks[node.name] = new_node end if node.respond_to?(:nodelist) - self.find_blocks(node.nodelist, b) # FIXME: find nested blocks too + self.find_blocks(node.nodelist, blocks) # FIXME: find nested blocks too end - b end end blocks @@ -52,7 +64,7 @@ def find_blocks(nodelist, blocks = {}) def parse_parent_template source = Template.file_system.read_template_file(@template_name) - Template.parse(source, @context) + Template.parse(source) end def assert_missing_delimitation! diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb index c4bea7049..288f79715 100644 --- a/lib/liquid/tags/inherited_block.rb +++ b/lib/liquid/tags/inherited_block.rb @@ -25,7 +25,6 @@ def initialize(tag_name, markup, tokens, context) end def render(context) - # puts "[BLOCK #{@name}|render] parent = #{@parent.inspect}" context.stack do context['block'] = InheritedBlockDrop.new(self) render_all(@nodelist, context) @@ -36,40 +35,35 @@ def end_tag self.register_current_block end + def call_super(context) + if parent + parent.render(context) + else + '' + end + end + + def self.clone_block(block) + new_block = self.new(block.send(:instance_variable_get, :"@tag_name"), block.name, nil, {}) + new_block.parent = block.parent + new_block.nodelist = block.nodelist + new_block + end + + protected + def register_current_block @context[:blocks] ||= {} block = @context[:blocks][@name] if block - # needed for the block.super statement - block.add_parent(@nodelist) - - @parent = block.parent - @nodelist = block.nodelist - else - # register it - @context[:blocks][@name] = self - end - end - - def add_parent(nodelist) - if @parent - # puts "[BLOCK #{@name}|add_parent] go upper" - @parent.add_parent(nodelist) - else - # puts "[BLOCK #{@name}|add_parent] create parent #{@tag_name}, #{@name}" - @parent = self.class.new(@tag_name, @name, nil, {}) - @parent.nodelist = nodelist - end - end + # copy the existing block in order to make it a parent of the parsed block + new_block = self.class.clone_block(block) - def call_super(context) - # puts "[BLOCK #{@name}|call_super] #{parent.inspect}" - if parent - parent.render(context) - else - '' + # replace the up-to-date version of the block in the parent template + block.parent = new_block + block.nodelist = @nodelist end end @@ -86,7 +80,6 @@ def name end def super - # puts "[InheritedBlockDrop] called" @block.call_super(@context) end diff --git a/test/test_helper.rb b/test/test_helper.rb index deb818594..0ce7c21ea 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -14,7 +14,23 @@ module Assertions include Liquid def assert_template_result(expected, template, assigns={}, message=nil) assert_equal expected, Template.parse(template).render(assigns) - end + end + end + end +end + +def print_child(node, depth = 0) + information = (case node + when Liquid::InheritedBlock + "Liquid::InheritedBlock #{node.object_id} / #{node.name} / #{!node.parent.nil?} / #{node.nodelist.first.inspect}" + else + node.class.name + end) + + puts information.insert(0, ' ' * (depth * 2)) + if node.respond_to?(:nodelist) + node.nodelist.each do |node| + print_child node, depth + 1 end end end \ No newline at end of file From b03cdc289ac36c3395459e295c6bf90baa06d256 Mon Sep 17 00:00:00 2001 From: dinedine Date: Tue, 31 Aug 2010 23:56:04 +0200 Subject: [PATCH 12/61] current block pushed in a stack during parsing --- lib/liquid/tags/inherited_block.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb index 288f79715..05d438b6e 100644 --- a/lib/liquid/tags/inherited_block.rb +++ b/lib/liquid/tags/inherited_block.rb @@ -19,6 +19,7 @@ def initialize(tag_name, markup, tokens, context) raise SyntaxError.new("Error in tag 'block' - Valid syntax: block [name]") end + (context[:block_stack] ||= []).push(self) context[:current_block] = self super if tokens @@ -33,6 +34,9 @@ def render(context) def end_tag self.register_current_block + + @context[:block_stack].pop + @context[:current_block] = @context[:block_stack].last end def call_super(context) From 7a0612634e5dfcef52db384a722296563924d341 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Mon, 27 Sep 2010 22:05:32 -0700 Subject: [PATCH 13/61] Repackage liquid gem for locomotive --- Manifest.txt | 34 ---------------------------------- liquid.gemspec | 30 +++++++++++++++++++----------- 2 files changed, 19 insertions(+), 45 deletions(-) delete mode 100644 Manifest.txt diff --git a/Manifest.txt b/Manifest.txt deleted file mode 100644 index 593e0bfee..000000000 --- a/Manifest.txt +++ /dev/null @@ -1,34 +0,0 @@ -CHANGELOG -History.txt -MIT-LICENSE -Manifest.txt -README.txt -Rakefile -init.rb -lib/extras/liquid_view.rb -lib/liquid.rb -lib/liquid/block.rb -lib/liquid/condition.rb -lib/liquid/context.rb -lib/liquid/document.rb -lib/liquid/drop.rb -lib/liquid/errors.rb -lib/liquid/extensions.rb -lib/liquid/file_system.rb -lib/liquid/htmltags.rb -lib/liquid/module_ex.rb -lib/liquid/standardfilters.rb -lib/liquid/strainer.rb -lib/liquid/tag.rb -lib/liquid/tags/assign.rb -lib/liquid/tags/capture.rb -lib/liquid/tags/case.rb -lib/liquid/tags/comment.rb -lib/liquid/tags/cycle.rb -lib/liquid/tags/for.rb -lib/liquid/tags/if.rb -lib/liquid/tags/ifchanged.rb -lib/liquid/tags/include.rb -lib/liquid/tags/unless.rb -lib/liquid/template.rb -lib/liquid/variable.rb diff --git a/liquid.gemspec b/liquid.gemspec index a2b5966ab..d41917ffa 100644 --- a/liquid.gemspec +++ b/liquid.gemspec @@ -1,20 +1,28 @@ Gem::Specification.new do |s| - s.name = %q{liquid} + s.name = "locomotive_liquid" s.version = "2.1.3" - s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = ["Tobias Luetke"] - s.description = %q{A secure, non-evaling end user template engine with aesthetic markup.} - s.email = %q{tobi@leetsoft.com} - s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"] - s.files = ["CHANGELOG", "History.txt", "MIT-LICENSE", "Manifest.txt", "README.txt", "Rakefile", "lib/extras/liquid_view.rb", "lib/liquid.rb", "lib/liquid/block.rb", "lib/liquid/condition.rb", "lib/liquid/context.rb", "lib/liquid/document.rb", "lib/liquid/drop.rb", "lib/liquid/errors.rb", "lib/liquid/extensions.rb", "lib/liquid/file_system.rb", "lib/liquid/htmltags.rb", "lib/liquid/module_ex.rb", "lib/liquid/standardfilters.rb", "lib/liquid/strainer.rb", "lib/liquid/tag.rb", "lib/liquid/tags/assign.rb", "lib/liquid/tags/capture.rb", "lib/liquid/tags/case.rb", "lib/liquid/tags/comment.rb", "lib/liquid/tags/cycle.rb", "lib/liquid/tags/for.rb", "lib/liquid/tags/if.rb", "lib/liquid/tags/ifchanged.rb", "lib/liquid/tags/include.rb", "lib/liquid/tags/unless.rb", "lib/liquid/template.rb", "lib/liquid/variable.rb"] + s.required_rubygems_version = ">= 1.3.6" + s.authors = ["Tobias Luetke", "Didier Lafforgue", "Jacques Crocker"] + s.email = ["tobi@leetsoft.com", "didier@nocoffee.fr", "railsjedi@gmail.com"] + s.summary = "A secure, non-evaling end user template engine with aesthetic markup." + s.description = "A secure, non-evaling end user template engine with aesthetic markup. Extended with liquid template inheritance for use in LocomotiveCMS" + + + s.extra_rdoc_files = ["History.txt", "README.txt"] + s.files = Dir[ "CHANGELOG", + "History.txt", + "MIT-LICENSE", + "README.txt", + "Rakefile", + "init.rb", + "{lib}/**/*"] + s.has_rdoc = true - s.homepage = %q{http://www.liquidmarkup.org} + s.homepage = "http://www.locomotiveapp.org" s.rdoc_options = ["--main", "README.txt"] s.require_paths = ["lib"] - s.rubyforge_project = %q{liquid} - s.rubygems_version = %q{1.3.1} - s.summary = %q{A secure, non-evaling end user template engine with aesthetic markup.} + s.rubyforge_project = "locomotive_liquid" if s.respond_to? :specification_version then current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION From 8c2132d90d73dafe2cc1654a5816c333c45b198a Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Tue, 28 Sep 2010 04:56:11 -0700 Subject: [PATCH 14/61] reorganizing gemspec --- .gitignore | 1 + Rakefile | 4 ++-- lib/locomotive_liquid.rb | 1 + liquid.gemspec => locomotive_liquid.gemspec | 0 4 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 lib/locomotive_liquid.rb rename liquid.gemspec => locomotive_liquid.gemspec (100%) diff --git a/.gitignore b/.gitignore index 76f22a726..54cbc793a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.gem pkg +.rvmrc \ No newline at end of file diff --git a/Rakefile b/Rakefile index 91c8511e2..a0208b354 100755 --- a/Rakefile +++ b/Rakefile @@ -21,14 +21,14 @@ Rake::TestTask.new(:ti) do |t| t.verbose = false end -gemspec = eval(File.read('liquid.gemspec')) +gemspec = eval(File.read('locomotive_liquid.gemspec')) Rake::GemPackageTask.new(gemspec) do |pkg| pkg.gem_spec = gemspec end desc "build the gem and release it to rubygems.org" task :release => :gem do - sh "gem push pkg/liquid-#{gemspec.version}.gem" + sh "gem push pkg/locomotive_liquid-#{gemspec.version}.gem" end namespace :profile do diff --git a/lib/locomotive_liquid.rb b/lib/locomotive_liquid.rb new file mode 100644 index 000000000..bb00cd0e0 --- /dev/null +++ b/lib/locomotive_liquid.rb @@ -0,0 +1 @@ +require 'liquid' \ No newline at end of file diff --git a/liquid.gemspec b/locomotive_liquid.gemspec similarity index 100% rename from liquid.gemspec rename to locomotive_liquid.gemspec From 369179637dc906d84467681f602608c86a70d02d Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Tue, 28 Sep 2010 07:14:16 -0700 Subject: [PATCH 15/61] Cleanup Readme and gemspec --- README.txt | 36 ++++++++++++++++++------------------ locomotive_liquid.gemspec | 9 --------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/README.txt b/README.txt index 1d019af6f..257296362 100644 --- a/README.txt +++ b/README.txt @@ -2,37 +2,37 @@ Liquid is a template engine which I wrote for very specific requirements -* It has to have beautiful and simple markup. - Template engines which don't produce good looking markup are no fun to use. -* It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don't want to run code on your server which your users wrote. -* It has to be stateless. Compile and render steps have to be seperate so that the expensive parsing and compiling can be done once and later on you can +* It has to have beautiful and simple markup. + Template engines which don't produce good looking markup are no fun to use. +* It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don't want to run code on your server which your users wrote. +* It has to be stateless. Compile and render steps have to be seperate so that the expensive parsing and compiling can be done once and later on you can just render it passing in a hash with local variables and objects. == Why should i use Liquid * You want to allow your users to edit the appearance of your application but don't want them to run insecure code on your server. * You want to render templates directly from the database -* You like smarty style template engines +* You like smarty style template engines * You need a template engine which does HTML just as well as Emails * You don't like the markup of your current one == What does it look like? -
    - {% for product in products %} -
  • -

    {{product.name}}

    - Only {{product.price | price }} - - {{product.description | prettyprint | paragraph }} -
  • - {% endfor %} -
+
    + {% for product in products %} +
  • +

    {{product.name}}

    + Only {{product.price | price }} + + {{product.description | prettyprint | paragraph }} +
  • + {% endfor %} +
== Howto use Liquid Liquid supports a very simple API based around the Liquid::Template class. -For standard use you can just pass it the content of a file and call render with a parameters hash. +For standard use you can just pass it the content of a file and call render with a parameters hash. - @template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template - @template.render( 'name' => 'tobi' ) # => "hi tobi" \ No newline at end of file + @template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template + @template.render( 'name' => 'tobi' ) # => "hi tobi" \ No newline at end of file diff --git a/locomotive_liquid.gemspec b/locomotive_liquid.gemspec index d41917ffa..00e73d473 100644 --- a/locomotive_liquid.gemspec +++ b/locomotive_liquid.gemspec @@ -24,13 +24,4 @@ Gem::Specification.new do |s| s.require_paths = ["lib"] s.rubyforge_project = "locomotive_liquid" - if s.respond_to? :specification_version then - current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION - s.specification_version = 2 - - if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then - else - end - else - end end From a9345c422dc9619167a951ad36632293b9efec07 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Tue, 28 Sep 2010 07:14:27 -0700 Subject: [PATCH 16/61] RSpec environment setup --- .gitignore | 3 +- Gemfile | 6 ++++ Gemfile.lock | 43 +++++++++++++++++++++++++ spec/spec_helper.rb | 14 ++++++++ spec/support/multiline_string_helper.rb | 29 +++++++++++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 spec/spec_helper.rb create mode 100644 spec/support/multiline_string_helper.rb diff --git a/.gitignore b/.gitignore index 54cbc793a..c0db7f18d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.gem pkg -.rvmrc \ No newline at end of file +.rvmrc +.bundle/config \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..05217eb49 --- /dev/null +++ b/Gemfile @@ -0,0 +1,6 @@ +source :rubygems + +gem "rspec", ">= 2.0.0.beta.22" +gem "cucumber", ">= 0.9.0" + +gem "ruby-debug" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..c9109e94d --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,43 @@ +GEM + remote: http://rubygems.org/ + specs: + builder (2.1.2) + columnize (0.3.1) + cucumber (0.9.0) + builder (~> 2.1.2) + diff-lcs (~> 1.1.2) + gherkin (~> 2.2.2) + json (~> 1.4.6) + term-ansicolor (~> 1.0.5) + diff-lcs (1.1.2) + gherkin (2.2.4) + json (~> 1.4.6) + term-ansicolor (~> 1.0.5) + trollop (~> 1.16.2) + json (1.4.6) + linecache (0.43) + rspec (2.0.0.beta.22) + rspec-core (= 2.0.0.beta.22) + rspec-expectations (= 2.0.0.beta.22) + rspec-mocks (= 2.0.0.beta.22) + rspec-core (2.0.0.beta.22) + rspec-expectations (2.0.0.beta.22) + diff-lcs (>= 1.1.2) + rspec-mocks (2.0.0.beta.22) + rspec-core (= 2.0.0.beta.22) + rspec-expectations (= 2.0.0.beta.22) + ruby-debug (0.10.3) + columnize (>= 0.1) + ruby-debug-base (~> 0.10.3.0) + ruby-debug-base (0.10.3) + linecache (>= 0.3) + term-ansicolor (1.0.5) + trollop (1.16.2) + +PLATFORMS + ruby + +DEPENDENCIES + cucumber (>= 0.9.0) + rspec (>= 2.0.0.beta.22) + ruby-debug diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 000000000..f0e4ce478 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,14 @@ +require 'rubygems' +require "bundler" +Bundler.setup + +$LOAD_PATH.unshift(File.dirname(__FILE__)) +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib")) + +require 'locomotive_liquid' + +require 'rspec' + +# support +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} + diff --git a/spec/support/multiline_string_helper.rb b/spec/support/multiline_string_helper.rb new file mode 100644 index 000000000..ac8c15fb3 --- /dev/null +++ b/spec/support/multiline_string_helper.rb @@ -0,0 +1,29 @@ +puts "WTF" +module RSpec + module MultilineStringHelper + # + # used to format multiline strings (prefix lines with |) + # + # example: + # + # multiline_template <<-END + # | hello + # | | + # | | + # END + # + # this parses to: + # " hello\n \n \n + # + def multiline_string(string, pipechar = '|') + arr = string.split("\n") # Split into lines + arr.map! {|x| x.sub(/^\s*\|/, "")} # Remove leading characters + arr.map! {|x| x.sub(/\|$/,"")} # Remove ending characters + arr.join("\n") # Rejoin into a single line + end + end +end + +Rspec.configure do |c| + c.include Rspec::MultilineStringHelper +end \ No newline at end of file From 49e718e36b061b4162591bc7a5f3d0269e2cd17b Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Tue, 28 Sep 2010 07:14:42 -0700 Subject: [PATCH 17/61] Converting Block, Assign, and Capture to specs --- spec/parsing/block_spec.rb | 81 ++++++++++++++++++++++++++++++++++ spec/rendering/assign_spec.rb | 31 +++++++++++++ spec/rendering/capture_spec.rb | 61 +++++++++++++++++++++++++ test/assign_test.rb | 11 ----- test/block_test.rb | 58 ------------------------ test/capture_test.rb | 41 ----------------- 6 files changed, 173 insertions(+), 110 deletions(-) create mode 100644 spec/parsing/block_spec.rb create mode 100644 spec/rendering/assign_spec.rb create mode 100644 spec/rendering/capture_spec.rb delete mode 100644 test/assign_test.rb delete mode 100644 test/block_test.rb delete mode 100644 test/capture_test.rb diff --git a/spec/parsing/block_spec.rb b/spec/parsing/block_spec.rb new file mode 100644 index 000000000..b8e971b24 --- /dev/null +++ b/spec/parsing/block_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +module Liquid + describe "Liquid Parsing" do + describe "Block" do + + it "should render whitespace properly" do + template = Template.parse(" ") + template.root.nodelist.should == [" "] + end + + let(:template) do + Template.parse(eval(subject)) + end + + describe %|"{{funk}} "| do + it{ template.root.nodelist.should have(2).nodes } + + it "should parse to: Variable,String" do + template.root.nodelist[0].should be_an_instance_of(Variable) + template.root.nodelist[1].should be_an_instance_of(String) + end + end + + describe %|" {{funk}}"| do + it{ template.root.nodelist.should have(2).nodes } + + it "should parse to: String,Variable" do + template.root.nodelist[0].should be_an_instance_of(String) + template.root.nodelist[1].should be_an_instance_of(Variable) + end + end + + describe %|" {{funk}} "| do + it{ template.root.nodelist.should have(3).nodes } + + it "should parse to: String,Variable,String" do + template.root.nodelist[0].should be_an_instance_of(String) + template.root.nodelist[1].should be_an_instance_of(Variable) + template.root.nodelist[2].should be_an_instance_of(String) + end + end + + describe %|" {{funk}} {{so}} {{brother}} "| do + it{ template.root.nodelist.should have(7).nodes } + + it "should parse to: String,Variable,String,Variable,String,Variable,String" do + template.root.nodelist[0].should be_an_instance_of(String) + template.root.nodelist[1].should be_an_instance_of(Variable) + template.root.nodelist[2].should be_an_instance_of(String) + template.root.nodelist[3].should be_an_instance_of(Variable) + template.root.nodelist[4].should be_an_instance_of(String) + template.root.nodelist[5].should be_an_instance_of(Variable) + template.root.nodelist[6].should be_an_instance_of(String) + end + end + + describe %|" {% comment %} {% endcomment %} "| do + it{ template.root.nodelist.should have(3).nodes } + it "should parse to: String,Comment,String" do + template.root.nodelist[0].should be_an_instance_of(String) + template.root.nodelist[1].should be_an_instance_of(Comment) + template.root.nodelist[2].should be_an_instance_of(String) + end + end + + context "when the custom tag 'somethingaweful' is defined" do + before(:each) do + Liquid::Template.register_tag('somethingaweful', Block) + end + + describe %|"{% somethingaweful %} {% endsomethingaweful %}"| do + it "should parse successfully" do + template.root.nodelist.should have(1).nodes + end + end + end + + end + end +end \ No newline at end of file diff --git a/spec/rendering/assign_spec.rb b/spec/rendering/assign_spec.rb new file mode 100644 index 000000000..5b0e74ee3 --- /dev/null +++ b/spec/rendering/assign_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +module Liquid + describe "Liquid Rendering" do + describe "Assignment" do + let(:template) do + Template.parse(eval(subject)) + end + + context %|with 'values' => ["foo", "bar", "baz"]| do + let(:render_options) do + { + 'values' => ["foo", "bar", "baz"] + } + end + + describe %|"{% assign foo = values %}.{{ foo[0] }}."| do + it{ template.render(render_options).should == ".foo." } + end + + describe %|"{% assign foo = values %}.{{ foo[1] }}."| do + it{ template.render(render_options).should == ".bar." } + end + + describe %|"{% assign foo = values %}.{{ foo[2] }}."| do + it{ template.render(render_options).should == ".baz." } + end + end + end + end +end \ No newline at end of file diff --git a/spec/rendering/capture_spec.rb b/spec/rendering/capture_spec.rb new file mode 100644 index 000000000..7591044cf --- /dev/null +++ b/spec/rendering/capture_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +module Liquid + describe "Liquid Rendering" do + describe "Capture" do + + # capturing blocks content in a variable + describe "assigning a capture block" do + let(:template) do + Template.parse multiline_string(<<-END_LIQUID) + | {% capture 'var' %}test string{% endcapture %} + | {{var}} + END_LIQUID + end + + it "render the captured block" do + template.render.strip.should == "test string" + end + end + + describe "capturing to a variable from outer scope (if existing)" do + let(:template) do + Template.parse multiline_string(<<-END_LIQUID) + | {% assign var = '' %} + | {% if true %} + | {% capture var %}first-block-string{% endcapture %} + | {% endif %} + | {% if true %} + | {% capture var %}test-string{% endcapture %} + | {% endif %} + | {{var}} + END_LIQUID + end + + it "should render the captured variable" do + template.render.strip.should == "test-string" + end + end + + describe "assigning from a capture block" do + let(:template) do + Template.parse multiline_string(<<-END_LIQUID) + | {% assign first = '' %} + | {% assign second = '' %} + | {% for number in (1..3) %} + | {% capture first %}{{number}}{% endcapture %} + | {% assign second = first %} + | {% endfor %} + | {{ first }}-{{ second }} + END_LIQUID + end + + it "should render the captured variable" do + template.render.strip.should == "3-3" + end + + end + + end + end +end \ No newline at end of file diff --git a/test/assign_test.rb b/test/assign_test.rb deleted file mode 100644 index 95e9755a6..000000000 --- a/test/assign_test.rb +++ /dev/null @@ -1,11 +0,0 @@ -require File.dirname(__FILE__) + '/helper' - -class AssignTest < Test::Unit::TestCase - include Liquid - - def test_assigned_variable - assert_template_result('.foo.','{% assign foo = values %}.{{ foo[0] }}.', 'values' => %w{foo bar baz}) - assert_template_result('.bar.','{% assign foo = values %}.{{ foo[1] }}.', 'values' => %w{foo bar baz}) - end - -end \ No newline at end of file diff --git a/test/block_test.rb b/test/block_test.rb deleted file mode 100644 index 270938e51..000000000 --- a/test/block_test.rb +++ /dev/null @@ -1,58 +0,0 @@ -require File.dirname(__FILE__) + '/helper' - -class VariableTest < Test::Unit::TestCase - include Liquid - - def test_blankspace - template = Liquid::Template.parse(" ") - assert_equal [" "], template.root.nodelist - end - - def test_variable_beginning - template = Liquid::Template.parse("{{funk}} ") - assert_equal 2, template.root.nodelist.size - assert_equal Variable, template.root.nodelist[0].class - assert_equal String, template.root.nodelist[1].class - end - - def test_variable_end - template = Liquid::Template.parse(" {{funk}}") - assert_equal 2, template.root.nodelist.size - assert_equal String, template.root.nodelist[0].class - assert_equal Variable, template.root.nodelist[1].class - end - - def test_variable_middle - template = Liquid::Template.parse(" {{funk}} ") - assert_equal 3, template.root.nodelist.size - assert_equal String, template.root.nodelist[0].class - assert_equal Variable, template.root.nodelist[1].class - assert_equal String, template.root.nodelist[2].class - end - - def test_variable_many_embedded_fragments - template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ") - assert_equal 7, template.root.nodelist.size - assert_equal [String, Variable, String, Variable, String, Variable, String], block_types(template.root.nodelist) - end - - def test_with_block - template = Liquid::Template.parse(" {% comment %} {% endcomment %} ") - assert_equal [String, Comment, String], block_types(template.root.nodelist) - assert_equal 3, template.root.nodelist.size - end - - def test_with_custom_tag - Liquid::Template.register_tag("testtag", Block) - - assert_nothing_thrown do - template = Liquid::Template.parse( "{% testtag %} {% endtesttag %}") - end - end - - private - - def block_types(nodelist) - nodelist.collect { |node| node.class } - end -end \ No newline at end of file diff --git a/test/capture_test.rb b/test/capture_test.rb deleted file mode 100644 index ea9340649..000000000 --- a/test/capture_test.rb +++ /dev/null @@ -1,41 +0,0 @@ -require File.dirname(__FILE__) + '/helper' - -class CaptureTest < Test::Unit::TestCase - include Liquid - - def test_captures_block_content_in_variable - assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {}) - end - - def test_capture_to_variable_from_outer_scope_if_existing - template_source = <<-END_TEMPLATE - {% assign var = '' %} - {% if true %} - {% capture var %}first-block-string{% endcapture %} - {% endif %} - {% if true %} - {% capture var %}test-string{% endcapture %} - {% endif %} - {{var}} - END_TEMPLATE - template = Template.parse(template_source) - rendered = template.render - assert_equal "test-string", rendered.gsub(/\s/, '') - end - - def test_assigning_from_capture - template_source = <<-END_TEMPLATE - {% assign first = '' %} - {% assign second = '' %} - {% for number in (1..3) %} - {% capture first %}{{number}}{% endcapture %} - {% assign second = first %} - {% endfor %} - {{ first }}-{{ second }} - END_TEMPLATE - template = Template.parse(template_source) - rendered = template.render - assert_equal "3-3", rendered.gsub(/\s/, '') - end - -end \ No newline at end of file From 9ecb9fff0790cf96cfc92bacd86c36859f92367d Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Tue, 28 Sep 2010 07:46:45 -0700 Subject: [PATCH 18/61] Converting Condition Test --- spec/support/multiline_string_helper.rb | 1 - spec/unit/condition_spec.rb | 140 ++++++++++++++++++++++++ test/condition_test.rb | 115 ------------------- 3 files changed, 140 insertions(+), 116 deletions(-) create mode 100644 spec/unit/condition_spec.rb delete mode 100644 test/condition_test.rb diff --git a/spec/support/multiline_string_helper.rb b/spec/support/multiline_string_helper.rb index ac8c15fb3..68a1b2ef9 100644 --- a/spec/support/multiline_string_helper.rb +++ b/spec/support/multiline_string_helper.rb @@ -1,4 +1,3 @@ -puts "WTF" module RSpec module MultilineStringHelper # diff --git a/spec/unit/condition_spec.rb b/spec/unit/condition_spec.rb new file mode 100644 index 000000000..a8a126f30 --- /dev/null +++ b/spec/unit/condition_spec.rb @@ -0,0 +1,140 @@ +require 'spec_helper' + +module Liquid + describe Condition do + before(:each) do + @context = Liquid::Context.new + end + + # simple wrapper around CheckCondition evaluate + def check_condition(*args) + Condition.new(*args).evaluate(@context) + end + + + it "should check basic equality conditions" do + check_condition("1", "==", "2").should be_false + check_condition("1", "==", "1").should be_true + end + + context "Default Operators (==, !=, <>, <, >, >=, <=)" do + it "should evaluate true when appropriate" do + check_condition('1', '==', '1').should be_true + check_condition('1', '!=', '2').should be_true + check_condition('1', '<>', '2').should be_true + check_condition('1', '<', '2').should be_true + check_condition('2', '>', '1').should be_true + check_condition('1', '>=', '1').should be_true + check_condition('2', '>=', '1').should be_true + check_condition('1', '<=', '2').should be_true + check_condition('1', '<=', '1').should be_true + end + + it "should evaluate false when appropriate" do + check_condition('1', '==', '2').should be_false + check_condition('1', '!=', '1').should be_false + check_condition('1', '<>', '1').should be_false + check_condition('1', '<', '0').should be_false + check_condition('2', '>', '4').should be_false + check_condition('1', '>=', '3').should be_false + check_condition('2', '>=', '4').should be_false + check_condition('1', '<=', '0').should be_false + check_condition('1', '<=', '0').should be_false + end + end + + context %{"contains"} do + + context "when operating on strings" do + it "should evaluate to true when appropriate" do + check_condition("'bob'", 'contains', "'o'").should be_true + check_condition("'bob'", 'contains', "'b'").should be_true + check_condition("'bob'", 'contains', "'bo'").should be_true + check_condition("'bob'", 'contains', "'ob'").should be_true + check_condition("'bob'", 'contains', "'bob'").should be_true + end + + it "should evaluate to false when appropriate" do + check_condition("'bob'", 'contains', "'bob2'").should be_false + check_condition("'bob'", 'contains', "'a'").should be_false + check_condition("'bob'", 'contains', "'---'").should be_false + end + end + + context "when operating on arrays" do + before(:each) do + @context['array'] = [1,2,3,4,5] + end + + it "should evaluate to true when appropriate" do + check_condition("array", "contains", "1").should be_true + check_condition("array", "contains", "2").should be_true + check_condition("array", "contains", "3").should be_true + check_condition("array", "contains", "4").should be_true + check_condition("array", "contains", "5").should be_true + end + + it "should evaluate to false when appropriate" do + check_condition("array", "contains", "0").should be_false + check_condition("array", "contains", "6").should be_false + end + + it "should not equate strings to integers" do + check_condition("array", "contains", "5").should be_true + check_condition("array", "contains", "'5'").should be_false + end + end + + it "should return false for all nil operands" do + check_condition("not_assigned", "contains", "0").should be_false + check_condition("0", "contains", "not_assigned").should be_false + end + end + + describe %{Chaining with "or"} do + before(:each) do + @condition = Condition.new("1", "==", "2") + @condition.evaluate.should be_false + end + + it "should return true when it you add a single condition that evaluates to true" do + @condition.or Condition.new("2", "==", "1") + @condition.evaluate.should be_false + + @condition.or Condition.new("1", "==", "1") + @condition.evaluate.should be_true + end + end + + describe %{Chaining with "and"} do + before(:each) do + @condition = Condition.new("1", "==", "1") + @condition.evaluate.should be_true + end + + it "should return false when it you add a single condition that evaluates to false" do + @condition.and Condition.new("2", "==", "2") + @condition.evaluate.should be_true + + @condition.and Condition.new("2", "==", "1") + @condition.evaluate.should be_false + end + end + + describe "Custom proc operator" do + before(:each) do + Condition.operators["starts_with"] = Proc.new { |cond, left, right| left =~ %r{^#{right}}} + end + + it "should use the assigned proc to evalue the operator" do + check_condition("'bob'", "starts_with", "'b'").should be_true + check_condition("'bob'", "starts_with", "'o'").should be_false + end + + after(:each) do + Condition.operators.delete('starts_with') + end + end + + end +end \ No newline at end of file diff --git a/test/condition_test.rb b/test/condition_test.rb deleted file mode 100644 index 0a4548261..000000000 --- a/test/condition_test.rb +++ /dev/null @@ -1,115 +0,0 @@ -require File.dirname(__FILE__) + '/helper' - -class ConditionTest < Test::Unit::TestCase - include Liquid - - def test_basic_condition - assert_equal false, Condition.new('1', '==', '2').evaluate - assert_equal true, Condition.new('1', '==', '1').evaluate - end - - def test_default_operators_evalute_true - assert_evalutes_true '1', '==', '1' - assert_evalutes_true '1', '!=', '2' - assert_evalutes_true '1', '<>', '2' - assert_evalutes_true '1', '<', '2' - assert_evalutes_true '2', '>', '1' - assert_evalutes_true '1', '>=', '1' - assert_evalutes_true '2', '>=', '1' - assert_evalutes_true '1', '<=', '2' - assert_evalutes_true '1', '<=', '1' - end - - def test_default_operators_evalute_false - assert_evalutes_false '1', '==', '2' - assert_evalutes_false '1', '!=', '1' - assert_evalutes_false '1', '<>', '1' - assert_evalutes_false '1', '<', '0' - assert_evalutes_false '2', '>', '4' - assert_evalutes_false '1', '>=', '3' - assert_evalutes_false '2', '>=', '4' - assert_evalutes_false '1', '<=', '0' - assert_evalutes_false '1', '<=', '0' - end - - def test_contains_works_on_strings - assert_evalutes_true "'bob'", 'contains', "'o'" - assert_evalutes_true "'bob'", 'contains', "'b'" - assert_evalutes_true "'bob'", 'contains', "'bo'" - assert_evalutes_true "'bob'", 'contains', "'ob'" - assert_evalutes_true "'bob'", 'contains', "'bob'" - - assert_evalutes_false "'bob'", 'contains', "'bob2'" - assert_evalutes_false "'bob'", 'contains', "'a'" - assert_evalutes_false "'bob'", 'contains', "'---'" - end - - def test_contains_works_on_arrays - @context = Liquid::Context.new - @context['array'] = [1,2,3,4,5] - - assert_evalutes_false "array", 'contains', '0' - assert_evalutes_true "array", 'contains', '1' - assert_evalutes_true "array", 'contains', '2' - assert_evalutes_true "array", 'contains', '3' - assert_evalutes_true "array", 'contains', '4' - assert_evalutes_true "array", 'contains', '5' - assert_evalutes_false "array", 'contains', '6' - - assert_evalutes_false "array", 'contains', '"1"' - - end - - def test_contains_returns_false_for_nil_operands - @context = Liquid::Context.new - assert_evalutes_false "not_assigned", 'contains', '0' - assert_evalutes_false "0", 'contains', 'not_assigned' - end - - def test_or_condition - condition = Condition.new('1', '==', '2') - - assert_equal false, condition.evaluate - - condition.or Condition.new('2', '==', '1') - - assert_equal false, condition.evaluate - - condition.or Condition.new('1', '==', '1') - - assert_equal true, condition.evaluate - end - - def test_and_condition - condition = Condition.new('1', '==', '1') - - assert_equal true, condition.evaluate - - condition.and Condition.new('2', '==', '2') - - assert_equal true, condition.evaluate - - condition.and Condition.new('2', '==', '1') - - assert_equal false, condition.evaluate - end - - - def test_should_allow_custom_proc_operator - Condition.operators['starts_with'] = Proc.new { |cond, left, right| left =~ %r{^#{right}}} - - assert_evalutes_true "'bob'", 'starts_with', "'b'" - assert_evalutes_false "'bob'", 'starts_with', "'o'" - ensure - Condition.operators.delete 'starts_with' - end - - private - def assert_evalutes_true(left, op, right) - assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new), "Evaluated false: #{left} #{op} #{right}" - end - - def assert_evalutes_false(left, op, right) - assert !Condition.new(left, op, right).evaluate(@context || Liquid::Context.new), "Evaluated true: #{left} #{op} #{right}" - end -end \ No newline at end of file From f0d0cc879cd3c552c38dae4282c37fe48f6bbeea Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Tue, 28 Sep 2010 10:08:05 -0700 Subject: [PATCH 19/61] Converting Context Spec --- spec/parsing/block_spec.rb | 4 +- spec/rendering/assign_spec.rb | 2 +- spec/rendering/capture_spec.rb | 6 +- spec/unit/condition_spec.rb | 14 +- spec/unit/context_spec.rb | 480 +++++++++++++++++++++++++++++++++ test/context_test.rb | 479 -------------------------------- 6 files changed, 493 insertions(+), 492 deletions(-) create mode 100644 spec/unit/context_spec.rb delete mode 100644 test/context_test.rb diff --git a/spec/parsing/block_spec.rb b/spec/parsing/block_spec.rb index b8e971b24..60babb9f7 100644 --- a/spec/parsing/block_spec.rb +++ b/spec/parsing/block_spec.rb @@ -5,12 +5,12 @@ module Liquid describe "Block" do it "should render whitespace properly" do - template = Template.parse(" ") + template = Liquid::Template.parse(" ") template.root.nodelist.should == [" "] end let(:template) do - Template.parse(eval(subject)) + Liquid::Template.parse(eval(subject)) end describe %|"{{funk}} "| do diff --git a/spec/rendering/assign_spec.rb b/spec/rendering/assign_spec.rb index 5b0e74ee3..d5cd0d429 100644 --- a/spec/rendering/assign_spec.rb +++ b/spec/rendering/assign_spec.rb @@ -4,7 +4,7 @@ module Liquid describe "Liquid Rendering" do describe "Assignment" do let(:template) do - Template.parse(eval(subject)) + Liquid::Template.parse(eval(subject)) end context %|with 'values' => ["foo", "bar", "baz"]| do diff --git a/spec/rendering/capture_spec.rb b/spec/rendering/capture_spec.rb index 7591044cf..65a4ec08c 100644 --- a/spec/rendering/capture_spec.rb +++ b/spec/rendering/capture_spec.rb @@ -7,7 +7,7 @@ module Liquid # capturing blocks content in a variable describe "assigning a capture block" do let(:template) do - Template.parse multiline_string(<<-END_LIQUID) + Liquid::Template.parse multiline_string(<<-END_LIQUID) | {% capture 'var' %}test string{% endcapture %} | {{var}} END_LIQUID @@ -20,7 +20,7 @@ module Liquid describe "capturing to a variable from outer scope (if existing)" do let(:template) do - Template.parse multiline_string(<<-END_LIQUID) + Liquid::Template.parse multiline_string(<<-END_LIQUID) | {% assign var = '' %} | {% if true %} | {% capture var %}first-block-string{% endcapture %} @@ -39,7 +39,7 @@ module Liquid describe "assigning from a capture block" do let(:template) do - Template.parse multiline_string(<<-END_LIQUID) + Liquid::Template.parse multiline_string(<<-END_LIQUID) | {% assign first = '' %} | {% assign second = '' %} | {% for number in (1..3) %} diff --git a/spec/unit/condition_spec.rb b/spec/unit/condition_spec.rb index a8a126f30..36824be2b 100644 --- a/spec/unit/condition_spec.rb +++ b/spec/unit/condition_spec.rb @@ -8,7 +8,7 @@ module Liquid # simple wrapper around CheckCondition evaluate def check_condition(*args) - Condition.new(*args).evaluate(@context) + Liquid::Condition.new(*args).evaluate(@context) end @@ -93,30 +93,30 @@ def check_condition(*args) describe %{Chaining with "or"} do before(:each) do - @condition = Condition.new("1", "==", "2") + @condition = Liquid::Condition.new("1", "==", "2") @condition.evaluate.should be_false end it "should return true when it you add a single condition that evaluates to true" do - @condition.or Condition.new("2", "==", "1") + @condition.or Liquid::Condition.new("2", "==", "1") @condition.evaluate.should be_false - @condition.or Condition.new("1", "==", "1") + @condition.or Liquid::Condition.new("1", "==", "1") @condition.evaluate.should be_true end end describe %{Chaining with "and"} do before(:each) do - @condition = Condition.new("1", "==", "1") + @condition = Liquid::Condition.new("1", "==", "1") @condition.evaluate.should be_true end it "should return false when it you add a single condition that evaluates to false" do - @condition.and Condition.new("2", "==", "2") + @condition.and Liquid::Condition.new("2", "==", "2") @condition.evaluate.should be_true - @condition.and Condition.new("2", "==", "1") + @condition.and Liquid::Condition.new("2", "==", "1") @condition.evaluate.should be_false end end diff --git a/spec/unit/context_spec.rb b/spec/unit/context_spec.rb new file mode 100644 index 000000000..f194bd18c --- /dev/null +++ b/spec/unit/context_spec.rb @@ -0,0 +1,480 @@ +require 'spec_helper' + +module Liquid + describe Context do + + before(:each) do + @context = Liquid::Context.new + end + + it "should allow assigning variables" do + @context['string'] = 'string' + @context['string'].should == 'string' + + @context['num'] = 5 + @context['num'].should == 5 + + @context['time'] = Time.parse('2006-06-06 12:00:00') + @context['time'].should == Time.parse('2006-06-06 12:00:00') + + @context['date'] = Date.today + @context['date'].should == Date.today + + now = DateTime.now + @context['datetime'] = now + @context['datetime'].should == now + + @context['bool'] = true + @context['bool'].should == true + + @context['bool'] = false + @context['bool'].should == false + + @context['nil'] = nil + @context['nil'].should == nil + end + + it "should return nil for variables that don't exist" do + @context["does_not_exist"].should == nil + end + + it "should return the size of an array" do + @context['numbers'] = [1,2,3,4] + @context['numbers.size'].should == 4 + end + + it "should return the size of an hash" do + @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4} + @context['numbers.size'].should == 4 + end + + it "should allow acess on a hash value by key" do + @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4, 'size' => 1000} + @context['numbers.size'].should == 1000 + end + + it "should handle hyphenated variables" do + @context["oh-my"] = "godz" + @context["oh-my"].should == "godz" + end + + it "should merge data" do + @context.merge("test" => "test") + @context["test"].should == "test" + + @context.merge("test" => "newvalue", "foo" => "bar") + @context["test"].should == "newvalue" + @context["foo"].should == "bar" + end + + describe "filters" do + before(:each) do + filter = Module.new do + def exclaim(output) + output + "!!!" + end + end + @context.add_filters(filter) + end + + it "should invoke a filter if found" do + @context.invoke(:exclaim, "hi").should == "hi!!!" + end + + it "should ignore a filter thats not found" do + local = Liquid::Context.new + local.invoke(:exclaim, "hi").should == "hi" + end + + it "should override a global filter" do + global = Module.new do + def notice(output) + "Global #{output}" + end + end + + local = Module.new do + def notice(output) + "Local #{output}" + end + end + + Template.register_filter(global) + Template.parse("{{'test' | notice }}").render.should == "Global test" + Template.parse("{{'test' | notice }}").render({}, :filters => [local]).should == "Local test" + end + + it "should only include intended filters methods" do + filter = Module.new do + def hi(output) + output + ' hi!' + end + end + + local = Context.new + methods_before = local.strainer.methods.map { |method| method.to_s } + local.add_filters(filter) + methods_after = local.strainer.methods.map { |method| method.to_s } + methods_after.sort.should == (methods_before+["hi"]).sort + end + end + + describe "scopes" do + it "should handle scoping properly" do + expect { + @context.push + @context.pop + }.to_not raise_exception + + expect { + @context.pop + }.to raise_exception(Liquid::ContextError) + + expect { + @context.push + @context.pop + @context.pop + }.to raise_exception(Liquid::ContextError) + end + + it "should allow access to items from outer scope within an inner scope" do + @context["test"] = "test" + @context.push + @context["test"].should == "test" + @context.pop + @context["test"].should == "test" + end + + it "should not allow access to items from inner scope with an outer scope" do + @context.push + @context["test"] = 'test' + @context["test"].should == "test" + @context.pop + @context["test"].should == nil + end + end + + describe "literals" do + it "should recognize boolean keywords" do + @context["true"].should == true + @context["false"].should == false + end + + it "should recognize integers and floats" do + @context["100"].should == 100 + @context[%Q{100.00}].should == 100.00 + end + + it "should recognize strings" do + @context[%{"hello!"}].should == "hello!" + @context[%{'hello!'}].should == "hello!" + end + + it "should recognize ranges" do + @context.merge( "test" => '5' ) + @context['(1..5)'].should == (1..5) + @context['(1..test)'].should == (1..5) + @context['(test..test)'].should == (5..5) + end + end + + context "hierarchical data" do + it "should allow access to hierarchical data" do + @context["hash"] = {"name" => "tobi"} + @context['hash.name'].should == "tobi" + @context["hash['name']"].should == "tobi" + @context['hash["name"]'].should == "tobi" + end + + it "should allow access to arrays" do + @context["test"] = [1,2,3,4,5] + + @context["test[0]"].should == 1 + @context["test[1]"].should == 2 + @context["test[2]"].should == 3 + @context["test[3]"].should == 4 + @context["test[4]"].should == 5 + end + + it "should allow access to an array within a hash" do + @context['test'] = {'test' => [1,2,3,4,5]} + @context['test.test[0]'].should == 1 + + # more complex + @context['colors'] = { + 'Blue' => ['003366','336699', '6699CC', '99CCFF'], + 'Green' => ['003300','336633', '669966', '99CC99'], + 'Yellow' => ['CC9900','FFCC00', 'FFFF99', 'FFFFCC'], + 'Red' => ['660000','993333', 'CC6666', 'FF9999'] + } + @context['colors.Blue[0]'].should == '003366' + @context['colors.Red[3]'].should == 'FF9999' + end + + it "should allow access to a hash within an array" do + @context['test'] = [{'test' => 'worked'}] + @context['test[0].test'].should == "worked" + end + + it "should provide first and last helpers for arrays" do + @context['test'] = [1,2,3,4,5] + + @context['test.first'].should == 1 + @context['test.last'].should == 5 + + @context['test'] = {'test' => [1,2,3,4,5]} + + @context['test.test.first'].should == 1 + @context['test.test.last'].should == 5 + + @context['test'] = [1] + @context['test.first'].should == 1 + @context['test.last'].should == 1 + end + + it "should allow arbitrary depth chaining of hash and array notation" do + @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } + @context['products["count"]'].should == 5 + @context['products["tags"][0]'].should == "deepsnow" + @context['products["tags"].first'].should == "deepsnow" + + @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} + @context['product["variants"][0]["title"]'].should == "draft151cm" + @context['product["variants"][1]["title"]'].should == "element151cm" + @context['product["variants"][0]["title"]'].should == "draft151cm" + @context['product["variants"].last["title"]'].should == "element151cm" + end + + it "should allow variable access with hash notation" do + @context.merge("foo" => "baz", "bar" => "foo") + @context['["foo"]'].should == "baz" + @context['[bar]'].should == "baz" + end + + it "should allow hash access with hash variables" do + @context['var'] = 'tags' + @context['nested'] = {'var' => 'tags'} + @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } + + @context['products[var].first'].should == "deepsnow" + @context['products[nested.var].last'].should == 'freestyle' + end + + it "should use hash notification only for hash access" do + @context['array'] = [1,2,3,4,5] + @context['hash'] = {'first' => 'Hello'} + + @context['array.first'].should == 1 + @context['array["first"]'].should == nil + @context['hash["first"]'].should == "Hello" + end + + it "should allow helpers (such as first and last) in the middle of a callchain" do + @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} + + @context['product.variants[0].title'].should == 'draft151cm' + @context['product.variants[1].title'].should == 'element151cm' + @context['product.variants.first.title'].should == 'draft151cm' + @context['product.variants.last.title'].should == 'element151cm' + end + end + + describe "Custom Object with a to_liquid method" do + class HundredCentes + def to_liquid + 100 + end + end + + it "should resolve to whatever to_liquid returns from the object" do + @context["cents"] = HundredCentes.new + @context["cents"].should == 100 + end + + it "should allow access to the custom object within a hash" do + @context.merge( "cents" => { 'amount' => HundredCentes.new} ) + @context['cents.amount'].should == 100 + + @context.merge( "cents" => { 'cents' => { 'amount' => HundredCentes.new} } ) + @context['cents.cents.amount'].should == 100 + end + end + + describe "Liquid Drops" do + class CentsDrop < Liquid::Drop + def amount + HundredCentes.new + end + + def non_zero? + true + end + end + + it "should allow access to the drop's methods" do + @context.merge( "cents" => CentsDrop.new ) + @context['cents.amount'].should == 100 + end + + it "should allow access to the drop's methods when nested in a hash" do + @context.merge( "vars" => {"cents" => CentsDrop.new} ) + @context['vars.cents.amount'].should == 100 + end + + it "should allow access to the a drop's methods that ends in a question mark" do + @context.merge( "cents" => CentsDrop.new ) + @context['cents.non_zero?'].should be_true + end + + it "should allow access to drop methods even when deeply nested" do + @context.merge( "cents" => {"cents" => CentsDrop.new} ) + @context['cents.cents.amount'].should == 100 + + @context.merge( "cents" => { "cents" => {"cents" => CentsDrop.new}} ) + @context['cents.cents.cents.amount'].should == 100 + end + + class ContextSensitiveDrop < Liquid::Drop + def test + @context['test'] + end + + def read_test + @context["test"] + end + end + + it "should allow access to the current context from within a drop" do + @context.merge( "test" => '123', "vars" => ContextSensitiveDrop.new ) + @context["vars.test"].should == "123" + @context["vars.read_test"].should == "123" + end + + it "should allow access to the current context even when nested in a hash" do + @context.merge( "test" => '123', "vars" => {"local" => ContextSensitiveDrop.new } ) + @context['vars.local.test'].should == "123" + @context['vars.local.read_test'].should == "123" + end + + + class CounterDrop < Liquid::Drop + def count + @count ||= 0 + @count += 1 + end + end + + it "should trigger a drop's autoincrementing variable" do + @context['counter'] = CounterDrop.new + + @context['counter.count'].should == 1 + @context['counter.count'].should == 2 + @context['counter.count'].should == 3 + end + + it "should trigger a drop's autoincrementing variable using hash syntax " do + @context['counter'] = CounterDrop.new + + @context['counter["count"]'].should == 1 + @context['counter["count"]'].should == 2 + @context['counter["count"]'].should == 3 + end + end + + context "lambas and procs" do + it "should trigger a proc if accessed as a variable" do + @context["dynamic1"] = Proc.new{ "Hello" } + @context['dynamic1'].should == "Hello" + + @context["dynamic2"] = proc{ "Hello" } + @context['dynamic2'].should == "Hello" + + end + + it "should trigger a proc within a hash" do + @context["dynamic"] = {"lambda" => proc{ "Hello" }} + @context["dynamic.lambda"].should == "Hello" + end + + it "should trigger a proc within an array" do + @context['dynamic'] = [1,2, proc { 'Hello' } ,4,5] + @context['dynamic[2]'].should == "Hello" + end + + it "should trigger the proc only the first time it's accessed" do + counter = 0 + @context["dynamic"] = proc{ "Hello #{counter += 1}" } + @context['dynamic'].should == "Hello 1" + @context['dynamic'].should == "Hello 1" + @context['dynamic'].should == "Hello 1" + end + + it "should trigger the proc within a hash only the first time it's accessed" do + counter = 0 + @context["dynamic"] = {"lambda" => proc{ "Hello #{counter += 1}" } } + @context['dynamic.lambda'].should == "Hello 1" + @context['dynamic.lambda'].should == "Hello 1" + @context['dynamic.lambda'].should == "Hello 1" + end + + it "should trigger the proc within an array only the first time it's accessed" do + counter = 0 + @context["dynamic"] = [1, 2, proc{ "Hello #{counter += 1}" }, 4] + @context['dynamic[2]'].should == "Hello 1" + @context['dynamic[2]'].should == "Hello 1" + @context['dynamic[2]'].should == "Hello 1" + end + + it "should allow access to context from within proc" do + @context.registers[:magic] = 345392 + @context['magic'] = proc { @context.registers[:magic] } + @context['magic'].should == 345392 + end + end + + + context "to_liquid returning a drop" do + class Category < Liquid::Drop + attr_accessor :name + + def initialize(name) + @name = name + end + + def to_liquid + CategoryDrop.new(self) + end + end + + class CategoryDrop + attr_accessor :category, :context + def initialize(category) + @category = category + end + end + + it "should return a drop" do + @context['category'] = Category.new("foobar") + @context['category'].should be_an_instance_of(CategoryDrop) + @context['category'].context.should == @context + end + + class ArrayLike + def fetch(index) + end + + def [](index) + @counts ||= [] + @counts[index] ||= 0 + @counts[index] += 1 + end + + def to_liquid + self + end + end + + end + end +end + diff --git a/test/context_test.rb b/test/context_test.rb deleted file mode 100644 index 8137ceb84..000000000 --- a/test/context_test.rb +++ /dev/null @@ -1,479 +0,0 @@ -require File.dirname(__FILE__) + '/helper' -class HundredCentes - def to_liquid - 100 - end -end - -class CentsDrop < Liquid::Drop - def amount - HundredCentes.new - end - - def non_zero? - true - end -end - -class ContextSensitiveDrop < Liquid::Drop - def test - @context['test'] - end -end - -class Category < Liquid::Drop - attr_accessor :name - - def initialize(name) - @name = name - end - - def to_liquid - CategoryDrop.new(self) - end -end - -class CategoryDrop - attr_accessor :category, :context - def initialize(category) - @category = category - end -end - -class CounterDrop < Liquid::Drop - def count - @count ||= 0 - @count += 1 - end -end - -class ArrayLike - def fetch(index) - end - - def [](index) - @counts ||= [] - @counts[index] ||= 0 - @counts[index] += 1 - end - - def to_liquid - self - end -end - - -class ContextTest < Test::Unit::TestCase - include Liquid - - def setup - @context = Liquid::Context.new - end - - def test_variables - @context['string'] = 'string' - assert_equal 'string', @context['string'] - - @context['num'] = 5 - assert_equal 5, @context['num'] - - @context['time'] = Time.parse('2006-06-06 12:00:00') - assert_equal Time.parse('2006-06-06 12:00:00'), @context['time'] - - @context['date'] = Date.today - assert_equal Date.today, @context['date'] - - now = DateTime.now - @context['datetime'] = now - assert_equal now, @context['datetime'] - - @context['bool'] = true - assert_equal true, @context['bool'] - - @context['bool'] = false - assert_equal false, @context['bool'] - - @context['nil'] = nil - assert_equal nil, @context['nil'] - assert_equal nil, @context['nil'] - end - - def test_variables_not_existing - assert_equal nil, @context['does_not_exist'] - end - - def test_scoping - assert_nothing_raised do - @context.push - @context.pop - end - - assert_raise(Liquid::ContextError) do - @context.pop - end - - assert_raise(Liquid::ContextError) do - @context.push - @context.pop - @context.pop - end - end - - def test_length_query - - @context['numbers'] = [1,2,3,4] - - assert_equal 4, @context['numbers.size'] - - @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4} - - assert_equal 4, @context['numbers.size'] - - @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4, 'size' => 1000} - - assert_equal 1000, @context['numbers.size'] - - end - - def test_hyphenated_variable - - @context['oh-my'] = 'godz' - assert_equal 'godz', @context['oh-my'] - - end - - def test_add_filter - - filter = Module.new do - def hi(output) - output + ' hi!' - end - end - - context = Context.new - context.add_filters(filter) - assert_equal 'hi? hi!', context.invoke(:hi, 'hi?') - - context = Context.new - assert_equal 'hi?', context.invoke(:hi, 'hi?') - - context.add_filters(filter) - assert_equal 'hi? hi!', context.invoke(:hi, 'hi?') - - end - - def test_override_global_filter - global = Module.new do - def notice(output) - "Global #{output}" - end - end - - local = Module.new do - def notice(output) - "Local #{output}" - end - 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_only_intended_filters_make_it_there - - filter = Module.new do - def hi(output) - output + ' hi!' - end - end - - context = Context.new - methods_before = context.strainer.methods.map { |method| method.to_s } - context.add_filters(filter) - methods_after = context.strainer.methods.map { |method| method.to_s } - assert_equal (methods_before + ["hi"]).sort, methods_after.sort - end - - def test_add_item_in_outer_scope - @context['test'] = 'test' - @context.push - assert_equal 'test', @context['test'] - @context.pop - assert_equal 'test', @context['test'] - end - - def test_add_item_in_inner_scope - @context.push - @context['test'] = 'test' - assert_equal 'test', @context['test'] - @context.pop - assert_equal nil, @context['test'] - end - - def test_hierachical_data - @context['hash'] = {"name" => 'tobi'} - assert_equal 'tobi', @context['hash.name'] - assert_equal 'tobi', @context['hash["name"]'] - end - - def test_keywords - assert_equal true, @context['true'] - assert_equal false, @context['false'] - end - - def test_digits - assert_equal 100, @context['100'] - assert_equal 100.00, @context['100.00'] - end - - def test_strings - assert_equal "hello!", @context['"hello!"'] - assert_equal "hello!", @context["'hello!'"] - end - - def test_merge - @context.merge({ "test" => "test" }) - assert_equal 'test', @context['test'] - @context.merge({ "test" => "newvalue", "foo" => "bar" }) - assert_equal 'newvalue', @context['test'] - assert_equal 'bar', @context['foo'] - end - - def test_array_notation - @context['test'] = [1,2,3,4,5] - - assert_equal 1, @context['test[0]'] - assert_equal 2, @context['test[1]'] - assert_equal 3, @context['test[2]'] - assert_equal 4, @context['test[3]'] - assert_equal 5, @context['test[4]'] - end - - def test_recoursive_array_notation - @context['test'] = {'test' => [1,2,3,4,5]} - - assert_equal 1, @context['test.test[0]'] - - @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'], - 'Green' => ['003300','336633', '669966', '99CC99'], - 'Yellow' => ['CC9900','FFCC00', 'FFFF99', 'FFFFCC'], - 'Red' => ['660000','993333', 'CC6666', 'FF9999'] - } - - assert_equal '003366', @context['colors.Blue[0]'] - assert_equal 'FF9999', @context['colors.Red[3]'] - end - - def test_try_first - @context['test'] = [1,2,3,4,5] - - assert_equal 1, @context['test.first'] - assert_equal 5, @context['test.last'] - - @context['test'] = {'test' => [1,2,3,4,5]} - - assert_equal 1, @context['test.test.first'] - assert_equal 5, @context['test.test.last'] - - @context['test'] = [1] - assert_equal 1, @context['test.first'] - assert_equal 1, @context['test.last'] - end - - def test_access_hashes_with_hash_notation - @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } - @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} - - assert_equal 5, @context['products["count"]'] - assert_equal 'deepsnow', @context['products["tags"][0]'] - assert_equal 'deepsnow', @context['products["tags"].first'] - assert_equal 'draft151cm', @context['product["variants"][0]["title"]'] - assert_equal 'element151cm', @context['product["variants"][1]["title"]'] - assert_equal 'draft151cm', @context['product["variants"][0]["title"]'] - assert_equal 'element151cm', @context['product["variants"].last["title"]'] - end - - def test_access_variable_with_hash_notation - @context['foo'] = 'baz' - @context['bar'] = 'foo' - - assert_equal 'baz', @context['["foo"]'] - assert_equal 'baz', @context['[bar]'] - end - - def test_access_hashes_with_hash_access_variables - - @context['var'] = 'tags' - @context['nested'] = {'var' => 'tags'} - @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } - - assert_equal 'deepsnow', @context['products[var].first'] - assert_equal 'freestyle', @context['products[nested.var].last'] - end - - def test_hash_notation_only_for_hash_access - @context['array'] = [1,2,3,4,5] - @context['hash'] = {'first' => 'Hello'} - - assert_equal 1, @context['array.first'] - assert_equal nil, @context['array["first"]'] - assert_equal 'Hello', @context['hash["first"]'] - end - - def test_first_can_appear_in_middle_of_callchain - - @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} - - assert_equal 'draft151cm', @context['product.variants[0].title'] - assert_equal 'element151cm', @context['product.variants[1].title'] - assert_equal 'draft151cm', @context['product.variants.first.title'] - assert_equal 'element151cm', @context['product.variants.last.title'] - - end - - def test_cents - @context.merge( "cents" => HundredCentes.new ) - assert_equal 100, @context['cents'] - end - - def test_nested_cents - @context.merge( "cents" => { 'amount' => HundredCentes.new} ) - assert_equal 100, @context['cents.amount'] - - @context.merge( "cents" => { 'cents' => { 'amount' => HundredCentes.new} } ) - assert_equal 100, @context['cents.cents.amount'] - end - - def test_cents_through_drop - @context.merge( "cents" => CentsDrop.new ) - assert_equal 100, @context['cents.amount'] - end - - def test_nested_cents_through_drop - @context.merge( "vars" => {"cents" => CentsDrop.new} ) - assert_equal 100, @context['vars.cents.amount'] - end - - def test_drop_methods_with_question_marks - @context.merge( "cents" => CentsDrop.new ) - assert @context['cents.non_zero?'] - end - - def test_context_from_within_drop - @context.merge( "test" => '123', "vars" => ContextSensitiveDrop.new ) - assert_equal '123', @context['vars.test'] - end - - def test_nested_context_from_within_drop - @context.merge( "test" => '123', "vars" => {"local" => ContextSensitiveDrop.new } ) - assert_equal '123', @context['vars.local.test'] - end - - def test_ranges - @context.merge( "test" => '5' ) - assert_equal (1..5), @context['(1..5)'] - assert_equal (1..5), @context['(1..test)'] - assert_equal (5..5), @context['(test..test)'] - end - - def test_cents_through_drop_nestedly - @context.merge( "cents" => {"cents" => CentsDrop.new} ) - assert_equal 100, @context['cents.cents.amount'] - - @context.merge( "cents" => { "cents" => {"cents" => CentsDrop.new}} ) - assert_equal 100, @context['cents.cents.cents.amount'] - end - - def test_drop_with_variable_called_only_once - @context['counter'] = CounterDrop.new - - assert_equal 1, @context['counter.count'] - assert_equal 2, @context['counter.count'] - assert_equal 3, @context['counter.count'] - end - - def test_drop_with_key_called_only_once - @context['counter'] = CounterDrop.new - - assert_equal 1, @context['counter["count"]'] - assert_equal 2, @context['counter["count"]'] - assert_equal 3, @context['counter["count"]'] - end - - def test_proc_as_variable - @context['dynamic'] = Proc.new { 'Hello' } - - assert_equal 'Hello', @context['dynamic'] - end - - def test_lambda_as_variable - @context['dynamic'] = proc { 'Hello' } - - assert_equal 'Hello', @context['dynamic'] - end - - def test_nested_lambda_as_variable - @context['dynamic'] = { "lambda" => proc { 'Hello' } } - - assert_equal 'Hello', @context['dynamic.lambda'] - end - - def test_array_containing_lambda_as_variable - @context['dynamic'] = [1,2, proc { 'Hello' } ,4,5] - - assert_equal 'Hello', @context['dynamic[2]'] - end - - def test_lambda_is_called_once - @context['callcount'] = proc { @global ||= 0; @global += 1; @global.to_s } - - assert_equal '1', @context['callcount'] - assert_equal '1', @context['callcount'] - assert_equal '1', @context['callcount'] - - @global = nil - end - - def test_nested_lambda_is_called_once - @context['callcount'] = { "lambda" => proc { @global ||= 0; @global += 1; @global.to_s } } - - assert_equal '1', @context['callcount.lambda'] - assert_equal '1', @context['callcount.lambda'] - assert_equal '1', @context['callcount.lambda'] - - @global = nil - end - - def test_lambda_in_array_is_called_once - @context['callcount'] = [1,2, proc { @global ||= 0; @global += 1; @global.to_s } ,4,5] - - assert_equal '1', @context['callcount[2]'] - assert_equal '1', @context['callcount[2]'] - assert_equal '1', @context['callcount[2]'] - - @global = nil - end - - def test_access_to_context_from_proc - @context.registers[:magic] = 345392 - - @context['magic'] = proc { @context.registers[:magic] } - - assert_equal 345392, @context['magic'] - end - - def test_to_liquid_and_context_at_first_level - @context['category'] = Category.new("foobar") - assert_kind_of CategoryDrop, @context['category'] - assert_equal @context, @context['category'].context - end - -end From 876972d383e80e1c03136b9a53b53fa00891472f Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Tue, 28 Sep 2010 10:24:40 -0700 Subject: [PATCH 20/61] Refactoring specs --- lib/liquid/context.rb | 10 +- spec/parsing/block_spec.rb | 81 ---- spec/rendering/assign_spec.rb | 40 +- spec/rendering/capture_spec.rb | 98 +++-- spec/unit/block_spec.rb | 79 ++++ spec/unit/condition_spec.rb | 214 +++++----- spec/unit/context_spec.rb | 723 ++++++++++++++++----------------- 7 files changed, 617 insertions(+), 628 deletions(-) delete mode 100644 spec/parsing/block_spec.rb create mode 100644 spec/unit/block_spec.rb diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index d8a17ec9f..57452ffc6 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -97,7 +97,7 @@ def stack(new_scope={},&block) end result end - + def clear_instance_assigns @scopes[0] = {} end @@ -172,7 +172,7 @@ def find_variable(key) 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 @@ -230,7 +230,7 @@ def variable(markup) object end - + def lookup_and_evaluate(obj, key) if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=) obj[key] = value.call(self) @@ -238,7 +238,7 @@ def lookup_and_evaluate(obj, key) value end end - + def squash_instance_assigns_with_environments @scopes.last.each_key do |k| @environments.each do |env| @@ -249,6 +249,6 @@ def squash_instance_assigns_with_environments end end end - + end end diff --git a/spec/parsing/block_spec.rb b/spec/parsing/block_spec.rb deleted file mode 100644 index 60babb9f7..000000000 --- a/spec/parsing/block_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -require 'spec_helper' - -module Liquid - describe "Liquid Parsing" do - describe "Block" do - - it "should render whitespace properly" do - template = Liquid::Template.parse(" ") - template.root.nodelist.should == [" "] - end - - let(:template) do - Liquid::Template.parse(eval(subject)) - end - - describe %|"{{funk}} "| do - it{ template.root.nodelist.should have(2).nodes } - - it "should parse to: Variable,String" do - template.root.nodelist[0].should be_an_instance_of(Variable) - template.root.nodelist[1].should be_an_instance_of(String) - end - end - - describe %|" {{funk}}"| do - it{ template.root.nodelist.should have(2).nodes } - - it "should parse to: String,Variable" do - template.root.nodelist[0].should be_an_instance_of(String) - template.root.nodelist[1].should be_an_instance_of(Variable) - end - end - - describe %|" {{funk}} "| do - it{ template.root.nodelist.should have(3).nodes } - - it "should parse to: String,Variable,String" do - template.root.nodelist[0].should be_an_instance_of(String) - template.root.nodelist[1].should be_an_instance_of(Variable) - template.root.nodelist[2].should be_an_instance_of(String) - end - end - - describe %|" {{funk}} {{so}} {{brother}} "| do - it{ template.root.nodelist.should have(7).nodes } - - it "should parse to: String,Variable,String,Variable,String,Variable,String" do - template.root.nodelist[0].should be_an_instance_of(String) - template.root.nodelist[1].should be_an_instance_of(Variable) - template.root.nodelist[2].should be_an_instance_of(String) - template.root.nodelist[3].should be_an_instance_of(Variable) - template.root.nodelist[4].should be_an_instance_of(String) - template.root.nodelist[5].should be_an_instance_of(Variable) - template.root.nodelist[6].should be_an_instance_of(String) - end - end - - describe %|" {% comment %} {% endcomment %} "| do - it{ template.root.nodelist.should have(3).nodes } - it "should parse to: String,Comment,String" do - template.root.nodelist[0].should be_an_instance_of(String) - template.root.nodelist[1].should be_an_instance_of(Comment) - template.root.nodelist[2].should be_an_instance_of(String) - end - end - - context "when the custom tag 'somethingaweful' is defined" do - before(:each) do - Liquid::Template.register_tag('somethingaweful', Block) - end - - describe %|"{% somethingaweful %} {% endsomethingaweful %}"| do - it "should parse successfully" do - template.root.nodelist.should have(1).nodes - end - end - end - - end - end -end \ No newline at end of file diff --git a/spec/rendering/assign_spec.rb b/spec/rendering/assign_spec.rb index d5cd0d429..81cc1482e 100644 --- a/spec/rendering/assign_spec.rb +++ b/spec/rendering/assign_spec.rb @@ -1,30 +1,28 @@ require 'spec_helper' -module Liquid - describe "Liquid Rendering" do - describe "Assignment" do - let(:template) do - Liquid::Template.parse(eval(subject)) - end +describe "Liquid Rendering" do + describe "Assignment" do + let(:template) do + Liquid::Template.parse(eval(subject)) + end - context %|with 'values' => ["foo", "bar", "baz"]| do - let(:render_options) do - { - 'values' => ["foo", "bar", "baz"] - } - end + context %|with 'values' => ["foo", "bar", "baz"]| do + let(:render_options) do + { + 'values' => ["foo", "bar", "baz"] + } + end - describe %|"{% assign foo = values %}.{{ foo[0] }}."| do - it{ template.render(render_options).should == ".foo." } - end + describe %|"{% assign foo = values %}.{{ foo[0] }}."| do + it{ template.render(render_options).should == ".foo." } + end - describe %|"{% assign foo = values %}.{{ foo[1] }}."| do - it{ template.render(render_options).should == ".bar." } - end + describe %|"{% assign foo = values %}.{{ foo[1] }}."| do + it{ template.render(render_options).should == ".bar." } + end - describe %|"{% assign foo = values %}.{{ foo[2] }}."| do - it{ template.render(render_options).should == ".baz." } - end + describe %|"{% assign foo = values %}.{{ foo[2] }}."| do + it{ template.render(render_options).should == ".baz." } end end end diff --git a/spec/rendering/capture_spec.rb b/spec/rendering/capture_spec.rb index 65a4ec08c..6dc218d2c 100644 --- a/spec/rendering/capture_spec.rb +++ b/spec/rendering/capture_spec.rb @@ -1,61 +1,59 @@ require 'spec_helper' -module Liquid - describe "Liquid Rendering" do - describe "Capture" do - - # capturing blocks content in a variable - describe "assigning a capture block" do - let(:template) do - Liquid::Template.parse multiline_string(<<-END_LIQUID) - | {% capture 'var' %}test string{% endcapture %} - | {{var}} - END_LIQUID - end - - it "render the captured block" do - template.render.strip.should == "test string" - end +describe "Liquid Rendering" do + describe "Capture" do + + # capturing blocks content in a variable + describe "assigning a capture block" do + let(:template) do + Liquid::Template.parse multiline_string(<<-END_LIQUID) + | {% capture 'var' %}test string{% endcapture %} + | {{var}} + END_LIQUID end - describe "capturing to a variable from outer scope (if existing)" do - let(:template) do - Liquid::Template.parse multiline_string(<<-END_LIQUID) - | {% assign var = '' %} - | {% if true %} - | {% capture var %}first-block-string{% endcapture %} - | {% endif %} - | {% if true %} - | {% capture var %}test-string{% endcapture %} - | {% endif %} - | {{var}} - END_LIQUID - end - - it "should render the captured variable" do - template.render.strip.should == "test-string" - end + it "render the captured block" do + template.render.strip.should == "test string" end + end + + describe "capturing to a variable from outer scope (if existing)" do + let(:template) do + Liquid::Template.parse multiline_string(<<-END_LIQUID) + | {% assign var = '' %} + | {% if true %} + | {% capture var %}first-block-string{% endcapture %} + | {% endif %} + | {% if true %} + | {% capture var %}test-string{% endcapture %} + | {% endif %} + | {{var}} + END_LIQUID + end + + it "should render the captured variable" do + template.render.strip.should == "test-string" + end + end - describe "assigning from a capture block" do - let(:template) do - Liquid::Template.parse multiline_string(<<-END_LIQUID) - | {% assign first = '' %} - | {% assign second = '' %} - | {% for number in (1..3) %} - | {% capture first %}{{number}}{% endcapture %} - | {% assign second = first %} - | {% endfor %} - | {{ first }}-{{ second }} - END_LIQUID - end - - it "should render the captured variable" do - template.render.strip.should == "3-3" - end + describe "assigning from a capture block" do + let(:template) do + Liquid::Template.parse multiline_string(<<-END_LIQUID) + | {% assign first = '' %} + | {% assign second = '' %} + | {% for number in (1..3) %} + | {% capture first %}{{number}}{% endcapture %} + | {% assign second = first %} + | {% endfor %} + | {{ first }}-{{ second }} + END_LIQUID + end + it "should render the captured variable" do + template.render.strip.should == "3-3" end end + end -end \ No newline at end of file +end diff --git a/spec/unit/block_spec.rb b/spec/unit/block_spec.rb new file mode 100644 index 000000000..715ee066d --- /dev/null +++ b/spec/unit/block_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe "Liquid Parsing" do + describe "Block" do + + it "should render whitespace properly" do + template = Liquid::Template.parse(" ") + template.root.nodelist.should == [" "] + end + + let(:template) do + Liquid::Template.parse(eval(subject)) + end + + describe %|"{{funk}} "| do + it{ template.root.nodelist.should have(2).nodes } + + it "should parse to: Variable,String" do + template.root.nodelist[0].should be_an_instance_of(Liquid::Variable) + template.root.nodelist[1].should be_an_instance_of(String) + end + end + + describe %|" {{funk}}"| do + it{ template.root.nodelist.should have(2).nodes } + + it "should parse to: String,Variable" do + template.root.nodelist[0].should be_an_instance_of(String) + template.root.nodelist[1].should be_an_instance_of(Liquid::Variable) + end + end + + describe %|" {{funk}} "| do + it{ template.root.nodelist.should have(3).nodes } + + it "should parse to: String,Variable,String" do + template.root.nodelist[0].should be_an_instance_of(String) + template.root.nodelist[1].should be_an_instance_of(Liquid::Variable) + template.root.nodelist[2].should be_an_instance_of(String) + end + end + + describe %|" {{funk}} {{so}} {{brother}} "| do + it{ template.root.nodelist.should have(7).nodes } + + it "should parse to: String,Variable,String,Variable,String,Variable,String" do + template.root.nodelist[0].should be_an_instance_of(String) + template.root.nodelist[1].should be_an_instance_of(Liquid::Variable) + template.root.nodelist[2].should be_an_instance_of(String) + template.root.nodelist[3].should be_an_instance_of(Liquid::Variable) + template.root.nodelist[4].should be_an_instance_of(String) + template.root.nodelist[5].should be_an_instance_of(Liquid::Variable) + template.root.nodelist[6].should be_an_instance_of(String) + end + end + + describe %|" {% comment %} {% endcomment %} "| do + it{ template.root.nodelist.should have(3).nodes } + it "should parse to: String,Comment,String" do + template.root.nodelist[0].should be_an_instance_of(String) + template.root.nodelist[1].should be_an_instance_of(Liquid::Comment) + template.root.nodelist[2].should be_an_instance_of(String) + end + end + + context "when the custom tag 'somethingaweful' is defined" do + before(:each) do + Liquid::Template.register_tag('somethingaweful', Liquid::Block) + end + + describe %|"{% somethingaweful %} {% endsomethingaweful %}"| do + it "should parse successfully" do + template.root.nodelist.should have(1).nodes + end + end + end + + end +end \ No newline at end of file diff --git a/spec/unit/condition_spec.rb b/spec/unit/condition_spec.rb index 36824be2b..3a8eb054d 100644 --- a/spec/unit/condition_spec.rb +++ b/spec/unit/condition_spec.rb @@ -1,140 +1,138 @@ require 'spec_helper' -module Liquid - describe Condition do - before(:each) do - @context = Liquid::Context.new - end +describe Liquid::Condition do + before(:each) do + @context = Liquid::Context.new + end - # simple wrapper around CheckCondition evaluate - def check_condition(*args) - Liquid::Condition.new(*args).evaluate(@context) - end + # simple wrapper around CheckCondition evaluate + def check_condition(*args) + Liquid::Condition.new(*args).evaluate(@context) + end - it "should check basic equality conditions" do - check_condition("1", "==", "2").should be_false - check_condition("1", "==", "1").should be_true + it "should check basic equality conditions" do + check_condition("1", "==", "2").should be_false + check_condition("1", "==", "1").should be_true + end + + context "Default Operators (==, !=, <>, <, >, >=, <=)" do + it "should evaluate true when appropriate" do + check_condition('1', '==', '1').should be_true + check_condition('1', '!=', '2').should be_true + check_condition('1', '<>', '2').should be_true + check_condition('1', '<', '2').should be_true + check_condition('2', '>', '1').should be_true + check_condition('1', '>=', '1').should be_true + check_condition('2', '>=', '1').should be_true + check_condition('1', '<=', '2').should be_true + check_condition('1', '<=', '1').should be_true + end + + it "should evaluate false when appropriate" do + check_condition('1', '==', '2').should be_false + check_condition('1', '!=', '1').should be_false + check_condition('1', '<>', '1').should be_false + check_condition('1', '<', '0').should be_false + check_condition('2', '>', '4').should be_false + check_condition('1', '>=', '3').should be_false + check_condition('2', '>=', '4').should be_false + check_condition('1', '<=', '0').should be_false + check_condition('1', '<=', '0').should be_false end + end + + context %{"contains"} do - context "Default Operators (==, !=, <>, <, >, >=, <=)" do - it "should evaluate true when appropriate" do - check_condition('1', '==', '1').should be_true - check_condition('1', '!=', '2').should be_true - check_condition('1', '<>', '2').should be_true - check_condition('1', '<', '2').should be_true - check_condition('2', '>', '1').should be_true - check_condition('1', '>=', '1').should be_true - check_condition('2', '>=', '1').should be_true - check_condition('1', '<=', '2').should be_true - check_condition('1', '<=', '1').should be_true + context "when operating on strings" do + it "should evaluate to true when appropriate" do + check_condition("'bob'", 'contains', "'o'").should be_true + check_condition("'bob'", 'contains', "'b'").should be_true + check_condition("'bob'", 'contains', "'bo'").should be_true + check_condition("'bob'", 'contains', "'ob'").should be_true + check_condition("'bob'", 'contains', "'bob'").should be_true end - it "should evaluate false when appropriate" do - check_condition('1', '==', '2').should be_false - check_condition('1', '!=', '1').should be_false - check_condition('1', '<>', '1').should be_false - check_condition('1', '<', '0').should be_false - check_condition('2', '>', '4').should be_false - check_condition('1', '>=', '3').should be_false - check_condition('2', '>=', '4').should be_false - check_condition('1', '<=', '0').should be_false - check_condition('1', '<=', '0').should be_false + it "should evaluate to false when appropriate" do + check_condition("'bob'", 'contains', "'bob2'").should be_false + check_condition("'bob'", 'contains', "'a'").should be_false + check_condition("'bob'", 'contains', "'---'").should be_false end end - context %{"contains"} do - - context "when operating on strings" do - it "should evaluate to true when appropriate" do - check_condition("'bob'", 'contains', "'o'").should be_true - check_condition("'bob'", 'contains', "'b'").should be_true - check_condition("'bob'", 'contains', "'bo'").should be_true - check_condition("'bob'", 'contains', "'ob'").should be_true - check_condition("'bob'", 'contains', "'bob'").should be_true - end - - it "should evaluate to false when appropriate" do - check_condition("'bob'", 'contains', "'bob2'").should be_false - check_condition("'bob'", 'contains', "'a'").should be_false - check_condition("'bob'", 'contains', "'---'").should be_false - end + context "when operating on arrays" do + before(:each) do + @context['array'] = [1,2,3,4,5] end - context "when operating on arrays" do - before(:each) do - @context['array'] = [1,2,3,4,5] - end - - it "should evaluate to true when appropriate" do - check_condition("array", "contains", "1").should be_true - check_condition("array", "contains", "2").should be_true - check_condition("array", "contains", "3").should be_true - check_condition("array", "contains", "4").should be_true - check_condition("array", "contains", "5").should be_true - end - - it "should evaluate to false when appropriate" do - check_condition("array", "contains", "0").should be_false - check_condition("array", "contains", "6").should be_false - end - - it "should not equate strings to integers" do - check_condition("array", "contains", "5").should be_true - check_condition("array", "contains", "'5'").should be_false - end + it "should evaluate to true when appropriate" do + check_condition("array", "contains", "1").should be_true + check_condition("array", "contains", "2").should be_true + check_condition("array", "contains", "3").should be_true + check_condition("array", "contains", "4").should be_true + check_condition("array", "contains", "5").should be_true end - it "should return false for all nil operands" do - check_condition("not_assigned", "contains", "0").should be_false - check_condition("0", "contains", "not_assigned").should be_false + it "should evaluate to false when appropriate" do + check_condition("array", "contains", "0").should be_false + check_condition("array", "contains", "6").should be_false end - end - describe %{Chaining with "or"} do - before(:each) do - @condition = Liquid::Condition.new("1", "==", "2") - @condition.evaluate.should be_false + it "should not equate strings to integers" do + check_condition("array", "contains", "5").should be_true + check_condition("array", "contains", "'5'").should be_false end + end - it "should return true when it you add a single condition that evaluates to true" do - @condition.or Liquid::Condition.new("2", "==", "1") - @condition.evaluate.should be_false + it "should return false for all nil operands" do + check_condition("not_assigned", "contains", "0").should be_false + check_condition("0", "contains", "not_assigned").should be_false + end + end - @condition.or Liquid::Condition.new("1", "==", "1") - @condition.evaluate.should be_true - end + describe %{Chaining with "or"} do + before(:each) do + @condition = Liquid::Condition.new("1", "==", "2") + @condition.evaluate.should be_false end - describe %{Chaining with "and"} do - before(:each) do - @condition = Liquid::Condition.new("1", "==", "1") - @condition.evaluate.should be_true - end + it "should return true when it you add a single condition that evaluates to true" do + @condition.or Liquid::Condition.new("2", "==", "1") + @condition.evaluate.should be_false - it "should return false when it you add a single condition that evaluates to false" do - @condition.and Liquid::Condition.new("2", "==", "2") - @condition.evaluate.should be_true + @condition.or Liquid::Condition.new("1", "==", "1") + @condition.evaluate.should be_true + end + end - @condition.and Liquid::Condition.new("2", "==", "1") - @condition.evaluate.should be_false - end + describe %{Chaining with "and"} do + before(:each) do + @condition = Liquid::Condition.new("1", "==", "1") + @condition.evaluate.should be_true end - describe "Custom proc operator" do - before(:each) do - Condition.operators["starts_with"] = Proc.new { |cond, left, right| left =~ %r{^#{right}}} - end + it "should return false when it you add a single condition that evaluates to false" do + @condition.and Liquid::Condition.new("2", "==", "2") + @condition.evaluate.should be_true - it "should use the assigned proc to evalue the operator" do - check_condition("'bob'", "starts_with", "'b'").should be_true - check_condition("'bob'", "starts_with", "'o'").should be_false - end + @condition.and Liquid::Condition.new("2", "==", "1") + @condition.evaluate.should be_false + end + end - after(:each) do - Condition.operators.delete('starts_with') - end + describe "Custom proc operator" do + before(:each) do + Liquid::Condition.operators["starts_with"] = Proc.new { |cond, left, right| left =~ %r{^#{right}}} end + it "should use the assigned proc to evalue the operator" do + check_condition("'bob'", "starts_with", "'b'").should be_true + check_condition("'bob'", "starts_with", "'o'").should be_false + end + + after(:each) do + Liquid::Condition.operators.delete('starts_with') + end end -end \ No newline at end of file + +end diff --git a/spec/unit/context_spec.rb b/spec/unit/context_spec.rb index f194bd18c..c1ac67bcb 100644 --- a/spec/unit/context_spec.rb +++ b/spec/unit/context_spec.rb @@ -1,480 +1,477 @@ require 'spec_helper' -module Liquid - describe Context do +describe Liquid::Context do - before(:each) do - @context = Liquid::Context.new - end + before(:each) do + @context = Liquid::Context.new + end - it "should allow assigning variables" do - @context['string'] = 'string' - @context['string'].should == 'string' + it "should allow assigning variables" do + @context['string'] = 'string' + @context['string'].should == 'string' - @context['num'] = 5 - @context['num'].should == 5 + @context['num'] = 5 + @context['num'].should == 5 - @context['time'] = Time.parse('2006-06-06 12:00:00') - @context['time'].should == Time.parse('2006-06-06 12:00:00') + @context['time'] = Time.parse('2006-06-06 12:00:00') + @context['time'].should == Time.parse('2006-06-06 12:00:00') - @context['date'] = Date.today - @context['date'].should == Date.today + @context['date'] = Date.today + @context['date'].should == Date.today - now = DateTime.now - @context['datetime'] = now - @context['datetime'].should == now + now = DateTime.now + @context['datetime'] = now + @context['datetime'].should == now - @context['bool'] = true - @context['bool'].should == true + @context['bool'] = true + @context['bool'].should == true - @context['bool'] = false - @context['bool'].should == false + @context['bool'] = false + @context['bool'].should == false - @context['nil'] = nil - @context['nil'].should == nil - end + @context['nil'] = nil + @context['nil'].should == nil + end - it "should return nil for variables that don't exist" do - @context["does_not_exist"].should == nil - end + it "should return nil for variables that don't exist" do + @context["does_not_exist"].should == nil + end - it "should return the size of an array" do - @context['numbers'] = [1,2,3,4] - @context['numbers.size'].should == 4 - end + it "should return the size of an array" do + @context['numbers'] = [1,2,3,4] + @context['numbers.size'].should == 4 + end - it "should return the size of an hash" do - @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4} - @context['numbers.size'].should == 4 - end + it "should return the size of an hash" do + @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4} + @context['numbers.size'].should == 4 + end - it "should allow acess on a hash value by key" do - @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4, 'size' => 1000} - @context['numbers.size'].should == 1000 - end + it "should allow acess on a hash value by key" do + @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4, 'size' => 1000} + @context['numbers.size'].should == 1000 + end - it "should handle hyphenated variables" do - @context["oh-my"] = "godz" - @context["oh-my"].should == "godz" - end + it "should handle hyphenated variables" do + @context["oh-my"] = "godz" + @context["oh-my"].should == "godz" + end - it "should merge data" do - @context.merge("test" => "test") - @context["test"].should == "test" + it "should merge data" do + @context.merge("test" => "test") + @context["test"].should == "test" - @context.merge("test" => "newvalue", "foo" => "bar") - @context["test"].should == "newvalue" - @context["foo"].should == "bar" - end + @context.merge("test" => "newvalue", "foo" => "bar") + @context["test"].should == "newvalue" + @context["foo"].should == "bar" + end - describe "filters" do - before(:each) do - filter = Module.new do - def exclaim(output) - output + "!!!" - end + describe "filters" do + before(:each) do + filter = Module.new do + def exclaim(output) + output + "!!!" end - @context.add_filters(filter) - end - - it "should invoke a filter if found" do - @context.invoke(:exclaim, "hi").should == "hi!!!" end + @context.add_filters(filter) + end - it "should ignore a filter thats not found" do - local = Liquid::Context.new - local.invoke(:exclaim, "hi").should == "hi" - end + it "should invoke a filter if found" do + @context.invoke(:exclaim, "hi").should == "hi!!!" + end - it "should override a global filter" do - global = Module.new do - def notice(output) - "Global #{output}" - end - end + it "should ignore a filter thats not found" do + local = Liquid::Context.new + local.invoke(:exclaim, "hi").should == "hi" + end - local = Module.new do - def notice(output) - "Local #{output}" - end + it "should override a global filter" do + global = Module.new do + def notice(output) + "Global #{output}" end - - Template.register_filter(global) - Template.parse("{{'test' | notice }}").render.should == "Global test" - Template.parse("{{'test' | notice }}").render({}, :filters => [local]).should == "Local test" end - it "should only include intended filters methods" do - filter = Module.new do - def hi(output) - output + ' hi!' - end + local = Module.new do + def notice(output) + "Local #{output}" end - - local = Context.new - methods_before = local.strainer.methods.map { |method| method.to_s } - local.add_filters(filter) - methods_after = local.strainer.methods.map { |method| method.to_s } - methods_after.sort.should == (methods_before+["hi"]).sort end - end - describe "scopes" do - it "should handle scoping properly" do - expect { - @context.push - @context.pop - }.to_not raise_exception - - expect { - @context.pop - }.to raise_exception(Liquid::ContextError) + Liquid::Template.register_filter(global) + Liquid::Template.parse("{{'test' | notice }}").render.should == "Global test" + Liquid::Template.parse("{{'test' | notice }}").render({}, :filters => [local]).should == "Local test" + end - expect { - @context.push - @context.pop - @context.pop - }.to raise_exception(Liquid::ContextError) + it "should only include intended filters methods" do + filter = Module.new do + def hi(output) + output + ' hi!' + end end - it "should allow access to items from outer scope within an inner scope" do - @context["test"] = "test" + local = Liquid::Context.new + methods_before = local.strainer.methods.map { |method| method.to_s } + local.add_filters(filter) + methods_after = local.strainer.methods.map { |method| method.to_s } + methods_after.sort.should == (methods_before+["hi"]).sort + end + end + + describe "scopes" do + it "should handle scoping properly" do + expect { @context.push - @context["test"].should == "test" @context.pop - @context["test"].should == "test" - end + }.to_not raise_exception + + expect { + @context.pop + }.to raise_exception(Liquid::ContextError) - it "should not allow access to items from inner scope with an outer scope" do + expect { @context.push - @context["test"] = 'test' - @context["test"].should == "test" @context.pop - @context["test"].should == nil - end + @context.pop + }.to raise_exception(Liquid::ContextError) end - describe "literals" do - it "should recognize boolean keywords" do - @context["true"].should == true - @context["false"].should == false - end + it "should allow access to items from outer scope within an inner scope" do + @context["test"] = "test" + @context.push + @context["test"].should == "test" + @context.pop + @context["test"].should == "test" + end - it "should recognize integers and floats" do - @context["100"].should == 100 - @context[%Q{100.00}].should == 100.00 - end + it "should not allow access to items from inner scope with an outer scope" do + @context.push + @context["test"] = 'test' + @context["test"].should == "test" + @context.pop + @context["test"].should == nil + end + end - it "should recognize strings" do - @context[%{"hello!"}].should == "hello!" - @context[%{'hello!'}].should == "hello!" - end + describe "literals" do + it "should recognize boolean keywords" do + @context["true"].should == true + @context["false"].should == false + end - it "should recognize ranges" do - @context.merge( "test" => '5' ) - @context['(1..5)'].should == (1..5) - @context['(1..test)'].should == (1..5) - @context['(test..test)'].should == (5..5) - end + it "should recognize integers and floats" do + @context["100"].should == 100 + @context[%Q{100.00}].should == 100.00 end - context "hierarchical data" do - it "should allow access to hierarchical data" do - @context["hash"] = {"name" => "tobi"} - @context['hash.name'].should == "tobi" - @context["hash['name']"].should == "tobi" - @context['hash["name"]'].should == "tobi" - end + it "should recognize strings" do + @context[%{"hello!"}].should == "hello!" + @context[%{'hello!'}].should == "hello!" + end - it "should allow access to arrays" do - @context["test"] = [1,2,3,4,5] + it "should recognize ranges" do + @context.merge( "test" => '5' ) + @context['(1..5)'].should == (1..5) + @context['(1..test)'].should == (1..5) + @context['(test..test)'].should == (5..5) + end + end - @context["test[0]"].should == 1 - @context["test[1]"].should == 2 - @context["test[2]"].should == 3 - @context["test[3]"].should == 4 - @context["test[4]"].should == 5 - end + context "hierarchical data" do + it "should allow access to hierarchical data" do + @context["hash"] = {"name" => "tobi"} + @context['hash.name'].should == "tobi" + @context["hash['name']"].should == "tobi" + @context['hash["name"]'].should == "tobi" + end - it "should allow access to an array within a hash" do - @context['test'] = {'test' => [1,2,3,4,5]} - @context['test.test[0]'].should == 1 - - # more complex - @context['colors'] = { - 'Blue' => ['003366','336699', '6699CC', '99CCFF'], - 'Green' => ['003300','336633', '669966', '99CC99'], - 'Yellow' => ['CC9900','FFCC00', 'FFFF99', 'FFFFCC'], - 'Red' => ['660000','993333', 'CC6666', 'FF9999'] - } - @context['colors.Blue[0]'].should == '003366' - @context['colors.Red[3]'].should == 'FF9999' - end + it "should allow access to arrays" do + @context["test"] = [1,2,3,4,5] - it "should allow access to a hash within an array" do - @context['test'] = [{'test' => 'worked'}] - @context['test[0].test'].should == "worked" - end + @context["test[0]"].should == 1 + @context["test[1]"].should == 2 + @context["test[2]"].should == 3 + @context["test[3]"].should == 4 + @context["test[4]"].should == 5 + end - it "should provide first and last helpers for arrays" do - @context['test'] = [1,2,3,4,5] + it "should allow access to an array within a hash" do + @context['test'] = {'test' => [1,2,3,4,5]} + @context['test.test[0]'].should == 1 + + # more complex + @context['colors'] = { + 'Blue' => ['003366','336699', '6699CC', '99CCFF'], + 'Green' => ['003300','336633', '669966', '99CC99'], + 'Yellow' => ['CC9900','FFCC00', 'FFFF99', 'FFFFCC'], + 'Red' => ['660000','993333', 'CC6666', 'FF9999'] + } + @context['colors.Blue[0]'].should == '003366' + @context['colors.Red[3]'].should == 'FF9999' + end - @context['test.first'].should == 1 - @context['test.last'].should == 5 + it "should allow access to a hash within an array" do + @context['test'] = [{'test' => 'worked'}] + @context['test[0].test'].should == "worked" + end - @context['test'] = {'test' => [1,2,3,4,5]} + it "should provide first and last helpers for arrays" do + @context['test'] = [1,2,3,4,5] - @context['test.test.first'].should == 1 - @context['test.test.last'].should == 5 + @context['test.first'].should == 1 + @context['test.last'].should == 5 - @context['test'] = [1] - @context['test.first'].should == 1 - @context['test.last'].should == 1 - end + @context['test'] = {'test' => [1,2,3,4,5]} - it "should allow arbitrary depth chaining of hash and array notation" do - @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } - @context['products["count"]'].should == 5 - @context['products["tags"][0]'].should == "deepsnow" - @context['products["tags"].first'].should == "deepsnow" - - @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} - @context['product["variants"][0]["title"]'].should == "draft151cm" - @context['product["variants"][1]["title"]'].should == "element151cm" - @context['product["variants"][0]["title"]'].should == "draft151cm" - @context['product["variants"].last["title"]'].should == "element151cm" - end + @context['test.test.first'].should == 1 + @context['test.test.last'].should == 5 - it "should allow variable access with hash notation" do - @context.merge("foo" => "baz", "bar" => "foo") - @context['["foo"]'].should == "baz" - @context['[bar]'].should == "baz" - end + @context['test'] = [1] + @context['test.first'].should == 1 + @context['test.last'].should == 1 + end - it "should allow hash access with hash variables" do - @context['var'] = 'tags' - @context['nested'] = {'var' => 'tags'} - @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } + it "should allow arbitrary depth chaining of hash and array notation" do + @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } + @context['products["count"]'].should == 5 + @context['products["tags"][0]'].should == "deepsnow" + @context['products["tags"].first'].should == "deepsnow" + + @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} + @context['product["variants"][0]["title"]'].should == "draft151cm" + @context['product["variants"][1]["title"]'].should == "element151cm" + @context['product["variants"][0]["title"]'].should == "draft151cm" + @context['product["variants"].last["title"]'].should == "element151cm" + end - @context['products[var].first'].should == "deepsnow" - @context['products[nested.var].last'].should == 'freestyle' - end + it "should allow variable access with hash notation" do + @context.merge("foo" => "baz", "bar" => "foo") + @context['["foo"]'].should == "baz" + @context['[bar]'].should == "baz" + end - it "should use hash notification only for hash access" do - @context['array'] = [1,2,3,4,5] - @context['hash'] = {'first' => 'Hello'} + it "should allow hash access with hash variables" do + @context['var'] = 'tags' + @context['nested'] = {'var' => 'tags'} + @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } - @context['array.first'].should == 1 - @context['array["first"]'].should == nil - @context['hash["first"]'].should == "Hello" - end + @context['products[var].first'].should == "deepsnow" + @context['products[nested.var].last'].should == 'freestyle' + end - it "should allow helpers (such as first and last) in the middle of a callchain" do - @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} + it "should use hash notification only for hash access" do + @context['array'] = [1,2,3,4,5] + @context['hash'] = {'first' => 'Hello'} - @context['product.variants[0].title'].should == 'draft151cm' - @context['product.variants[1].title'].should == 'element151cm' - @context['product.variants.first.title'].should == 'draft151cm' - @context['product.variants.last.title'].should == 'element151cm' - end + @context['array.first'].should == 1 + @context['array["first"]'].should == nil + @context['hash["first"]'].should == "Hello" end - describe "Custom Object with a to_liquid method" do - class HundredCentes - def to_liquid - 100 - end - end + it "should allow helpers (such as first and last) in the middle of a callchain" do + @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} - it "should resolve to whatever to_liquid returns from the object" do - @context["cents"] = HundredCentes.new - @context["cents"].should == 100 - end - - it "should allow access to the custom object within a hash" do - @context.merge( "cents" => { 'amount' => HundredCentes.new} ) - @context['cents.amount'].should == 100 + @context['product.variants[0].title'].should == 'draft151cm' + @context['product.variants[1].title'].should == 'element151cm' + @context['product.variants.first.title'].should == 'draft151cm' + @context['product.variants.last.title'].should == 'element151cm' + end + end - @context.merge( "cents" => { 'cents' => { 'amount' => HundredCentes.new} } ) - @context['cents.cents.amount'].should == 100 + describe "Custom Object with a to_liquid method" do + class HundredCentes + def to_liquid + 100 end end - describe "Liquid Drops" do - class CentsDrop < Liquid::Drop - def amount - HundredCentes.new - end + it "should resolve to whatever to_liquid returns from the object" do + @context["cents"] = HundredCentes.new + @context["cents"].should == 100 + end - def non_zero? - true - end - end + it "should allow access to the custom object within a hash" do + @context.merge( "cents" => { 'amount' => HundredCentes.new} ) + @context['cents.amount'].should == 100 - it "should allow access to the drop's methods" do - @context.merge( "cents" => CentsDrop.new ) - @context['cents.amount'].should == 100 - end + @context.merge( "cents" => { 'cents' => { 'amount' => HundredCentes.new} } ) + @context['cents.cents.amount'].should == 100 + end + end - it "should allow access to the drop's methods when nested in a hash" do - @context.merge( "vars" => {"cents" => CentsDrop.new} ) - @context['vars.cents.amount'].should == 100 + describe "Liquid Drops" do + class CentsDrop < Liquid::Drop + def amount + HundredCentes.new end - it "should allow access to the a drop's methods that ends in a question mark" do - @context.merge( "cents" => CentsDrop.new ) - @context['cents.non_zero?'].should be_true + def non_zero? + true end + end - it "should allow access to drop methods even when deeply nested" do - @context.merge( "cents" => {"cents" => CentsDrop.new} ) - @context['cents.cents.amount'].should == 100 + it "should allow access to the drop's methods" do + @context.merge( "cents" => CentsDrop.new ) + @context['cents.amount'].should == 100 + end - @context.merge( "cents" => { "cents" => {"cents" => CentsDrop.new}} ) - @context['cents.cents.cents.amount'].should == 100 - end + it "should allow access to the drop's methods when nested in a hash" do + @context.merge( "vars" => {"cents" => CentsDrop.new} ) + @context['vars.cents.amount'].should == 100 + end - class ContextSensitiveDrop < Liquid::Drop - def test - @context['test'] - end + it "should allow access to the a drop's methods that ends in a question mark" do + @context.merge( "cents" => CentsDrop.new ) + @context['cents.non_zero?'].should be_true + end - def read_test - @context["test"] - end - end + it "should allow access to drop methods even when deeply nested" do + @context.merge( "cents" => {"cents" => CentsDrop.new} ) + @context['cents.cents.amount'].should == 100 + + @context.merge( "cents" => { "cents" => {"cents" => CentsDrop.new}} ) + @context['cents.cents.cents.amount'].should == 100 + end - it "should allow access to the current context from within a drop" do - @context.merge( "test" => '123', "vars" => ContextSensitiveDrop.new ) - @context["vars.test"].should == "123" - @context["vars.read_test"].should == "123" + class ContextSensitiveDrop < Liquid::Drop + def test + @context['test'] end - it "should allow access to the current context even when nested in a hash" do - @context.merge( "test" => '123', "vars" => {"local" => ContextSensitiveDrop.new } ) - @context['vars.local.test'].should == "123" - @context['vars.local.read_test'].should == "123" + def read_test + @context["test"] end + end + it "should allow access to the current context from within a drop" do + @context.merge( "test" => '123', "vars" => ContextSensitiveDrop.new ) + @context["vars.test"].should == "123" + @context["vars.read_test"].should == "123" + end - class CounterDrop < Liquid::Drop - def count - @count ||= 0 - @count += 1 - end - end + it "should allow access to the current context even when nested in a hash" do + @context.merge( "test" => '123', "vars" => {"local" => ContextSensitiveDrop.new } ) + @context['vars.local.test'].should == "123" + @context['vars.local.read_test'].should == "123" + end - it "should trigger a drop's autoincrementing variable" do - @context['counter'] = CounterDrop.new - @context['counter.count'].should == 1 - @context['counter.count'].should == 2 - @context['counter.count'].should == 3 + class CounterDrop < Liquid::Drop + def count + @count ||= 0 + @count += 1 end + end - it "should trigger a drop's autoincrementing variable using hash syntax " do - @context['counter'] = CounterDrop.new + it "should trigger a drop's autoincrementing variable" do + @context['counter'] = CounterDrop.new - @context['counter["count"]'].should == 1 - @context['counter["count"]'].should == 2 - @context['counter["count"]'].should == 3 - end + @context['counter.count'].should == 1 + @context['counter.count'].should == 2 + @context['counter.count'].should == 3 end - context "lambas and procs" do - it "should trigger a proc if accessed as a variable" do - @context["dynamic1"] = Proc.new{ "Hello" } - @context['dynamic1'].should == "Hello" + it "should trigger a drop's autoincrementing variable using hash syntax " do + @context['counter'] = CounterDrop.new - @context["dynamic2"] = proc{ "Hello" } - @context['dynamic2'].should == "Hello" + @context['counter["count"]'].should == 1 + @context['counter["count"]'].should == 2 + @context['counter["count"]'].should == 3 + end + end - end + context "lambas and procs" do + it "should trigger a proc if accessed as a variable" do + @context["dynamic1"] = Proc.new{ "Hello" } + @context['dynamic1'].should == "Hello" - it "should trigger a proc within a hash" do - @context["dynamic"] = {"lambda" => proc{ "Hello" }} - @context["dynamic.lambda"].should == "Hello" - end + @context["dynamic2"] = proc{ "Hello" } + @context['dynamic2'].should == "Hello" - it "should trigger a proc within an array" do - @context['dynamic'] = [1,2, proc { 'Hello' } ,4,5] - @context['dynamic[2]'].should == "Hello" - end + end - it "should trigger the proc only the first time it's accessed" do - counter = 0 - @context["dynamic"] = proc{ "Hello #{counter += 1}" } - @context['dynamic'].should == "Hello 1" - @context['dynamic'].should == "Hello 1" - @context['dynamic'].should == "Hello 1" - end + it "should trigger a proc within a hash" do + @context["dynamic"] = {"lambda" => proc{ "Hello" }} + @context["dynamic.lambda"].should == "Hello" + end - it "should trigger the proc within a hash only the first time it's accessed" do - counter = 0 - @context["dynamic"] = {"lambda" => proc{ "Hello #{counter += 1}" } } - @context['dynamic.lambda'].should == "Hello 1" - @context['dynamic.lambda'].should == "Hello 1" - @context['dynamic.lambda'].should == "Hello 1" - end + it "should trigger a proc within an array" do + @context['dynamic'] = [1,2, proc { 'Hello' } ,4,5] + @context['dynamic[2]'].should == "Hello" + end - it "should trigger the proc within an array only the first time it's accessed" do - counter = 0 - @context["dynamic"] = [1, 2, proc{ "Hello #{counter += 1}" }, 4] - @context['dynamic[2]'].should == "Hello 1" - @context['dynamic[2]'].should == "Hello 1" - @context['dynamic[2]'].should == "Hello 1" - end + it "should trigger the proc only the first time it's accessed" do + counter = 0 + @context["dynamic"] = proc{ "Hello #{counter += 1}" } + @context['dynamic'].should == "Hello 1" + @context['dynamic'].should == "Hello 1" + @context['dynamic'].should == "Hello 1" + end - it "should allow access to context from within proc" do - @context.registers[:magic] = 345392 - @context['magic'] = proc { @context.registers[:magic] } - @context['magic'].should == 345392 - end + it "should trigger the proc within a hash only the first time it's accessed" do + counter = 0 + @context["dynamic"] = {"lambda" => proc{ "Hello #{counter += 1}" } } + @context['dynamic.lambda'].should == "Hello 1" + @context['dynamic.lambda'].should == "Hello 1" + @context['dynamic.lambda'].should == "Hello 1" end + it "should trigger the proc within an array only the first time it's accessed" do + counter = 0 + @context["dynamic"] = [1, 2, proc{ "Hello #{counter += 1}" }, 4] + @context['dynamic[2]'].should == "Hello 1" + @context['dynamic[2]'].should == "Hello 1" + @context['dynamic[2]'].should == "Hello 1" + end - context "to_liquid returning a drop" do - class Category < Liquid::Drop - attr_accessor :name + it "should allow access to context from within proc" do + @context.registers[:magic] = 345392 + @context['magic'] = proc { @context.registers[:magic] } + @context['magic'].should == 345392 + end + end - def initialize(name) - @name = name - end - def to_liquid - CategoryDrop.new(self) - end + context "to_liquid returning a drop" do + class Category < Liquid::Drop + attr_accessor :name + + def initialize(name) + @name = name end - class CategoryDrop - attr_accessor :category, :context - def initialize(category) - @category = category - end + def to_liquid + CategoryDrop.new(self) end + end - it "should return a drop" do - @context['category'] = Category.new("foobar") - @context['category'].should be_an_instance_of(CategoryDrop) - @context['category'].context.should == @context + class CategoryDrop + attr_accessor :category, :context + def initialize(category) + @category = category end + end - class ArrayLike - def fetch(index) - end + it "should return a drop" do + @context['category'] = Category.new("foobar") + @context['category'].should be_an_instance_of(CategoryDrop) + @context['category'].context.should == @context + end - def [](index) - @counts ||= [] - @counts[index] ||= 0 - @counts[index] += 1 - end + class ArrayLike + def fetch(index) + end - def to_liquid - self - end + def [](index) + @counts ||= [] + @counts[index] ||= 0 + @counts[index] += 1 end + def to_liquid + self + end end - end -end + end +end \ No newline at end of file From ac1a7bdc65eab473ec9458c067bf3d953d9c757a Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 00:59:00 -0700 Subject: [PATCH 21/61] Converting drop test --- spec/fixtures/context_drop.rb | 30 ++++++ spec/fixtures/enumerable_drop.rb | 12 +++ spec/fixtures/product_drop.rb | 34 +++++++ spec/rendering/drop_spec.rb | 133 +++++++++++++++++++++++++ spec/spec_helper.rb | 6 +- test/drop_test.rb | 162 ------------------------------- 6 files changed, 213 insertions(+), 164 deletions(-) create mode 100644 spec/fixtures/context_drop.rb create mode 100644 spec/fixtures/enumerable_drop.rb create mode 100644 spec/fixtures/product_drop.rb create mode 100644 spec/rendering/drop_spec.rb delete mode 100644 test/drop_test.rb diff --git a/spec/fixtures/context_drop.rb b/spec/fixtures/context_drop.rb new file mode 100644 index 000000000..7cf6db6da --- /dev/null +++ b/spec/fixtures/context_drop.rb @@ -0,0 +1,30 @@ +class ContextDrop < Liquid::Drop + + def read_bar + @context['bar'] + end + + def read_foo + @context['foo'] + end + + def count_scopes + @context.scopes.size + end + + def scopes_as_array + (1..@context.scopes.size).to_a + end + + def loop_pos + @context['forloop.index'] + end + + def break + Breakpoint.breakpoint + end + + def before_method(method) + return @context[method] + end +end \ No newline at end of file diff --git a/spec/fixtures/enumerable_drop.rb b/spec/fixtures/enumerable_drop.rb new file mode 100644 index 000000000..8b371fbe6 --- /dev/null +++ b/spec/fixtures/enumerable_drop.rb @@ -0,0 +1,12 @@ +class EnumerableDrop < Liquid::Drop + + def size + 3 + end + + def each + yield 1 + yield 2 + yield 3 + end +end diff --git a/spec/fixtures/product_drop.rb b/spec/fixtures/product_drop.rb new file mode 100644 index 000000000..9c6361c81 --- /dev/null +++ b/spec/fixtures/product_drop.rb @@ -0,0 +1,34 @@ +class ProductDrop < Liquid::Drop + class TextDrop < Liquid::Drop + def array + ['text1', 'text2'] + end + + def text + 'text1' + end + end + + class CatchallDrop < Liquid::Drop + def before_method(method) + return 'method: ' << method + end + end + + def texts + TextDrop.new + end + + def catchall + CatchallDrop.new + end + + def context + ContextDrop.new + end + + protected + def callmenot + "protected" + end +end \ No newline at end of file diff --git a/spec/rendering/drop_spec.rb b/spec/rendering/drop_spec.rb new file mode 100644 index 000000000..f8a76460d --- /dev/null +++ b/spec/rendering/drop_spec.rb @@ -0,0 +1,133 @@ +require 'spec_helper' + +require 'fixtures/product_drop' +require 'fixtures/context_drop' +require 'fixtures/enumerable_drop' + +describe "Liquid Rendering" do + describe "Drops" do + + it "allow rendering with a product" do + expect { + Liquid::Template.parse(' ').render('product' => ProductDrop.new) + }.should_not raise_error + end + + it "should render drops within drops" do + template = Liquid::Template.parse ' {{ product.texts.text }} ' + template.render('product' => ProductDrop.new).should == ' text1 ' + end + + it "should render the text returned from a catchall method" do + template = Liquid::Template.parse ' {{ product.catchall.unknown }} ' + template.render('product' => ProductDrop.new).should == ' method: unknown ' + end + + it "should cycle through an array of text" do + template = Liquid::Template.parse multiline_string(<<-END_LIQUID) + | {% for text in product.texts.array %} {{text}} {% endfor %} + END_LIQUID + + template.render('product' => ProductDrop.new).strip.should == "text1 text2" + end + + it "should not allow protected methods to be called" do + template = Liquid::Template.parse(' {{ product.callmenot }} ') + + template.render('product' => ProductDrop.new).should == " " + + end + + describe "context" do + it "should allow using the context within a drop" do + template = Liquid::Template.parse(' {{ context.read_bar }} ') + data = {"context" => ContextDrop.new, "bar" => "carrot"} + + template.render(data).should == " carrot " + end + + it "should allow the use of context within nested drops" do + template = Liquid::Template.parse(' {{ product.context.read_foo }} ') + data = {"product" => ProductDrop.new, "foo" => "monkey"} + + template.render(data).should == " monkey " + end + end + + describe "scope" do + + it "should allow access to context scope from within a drop" do + template = Liquid::Template.parse('{{ context.count_scopes }}') + template.render("context" => ContextDrop.new).should == "1" + + template = Liquid::Template.parse('{%for i in dummy%}{{ context.count_scopes }}{%endfor%}') + template.render("context" => ContextDrop.new, 'dummy' => [1]).should == "2" + + template = Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.count_scopes }}{%endfor%}{%endfor%}') + template.render("context" => ContextDrop.new, 'dummy' => [1]).should == "3" + end + + it "should allow access to context scope from within a drop through a scope" do + template = Liquid::Template.parse( '{{ s }}' ) + template.render('context' => ContextDrop.new, + 's' => Proc.new{|c| c['context.count_scopes'] }).should == "1" + + template = Liquid::Template.parse( '{%for i in dummy%}{{ s }}{%endfor%}' ) + template.render('context' => ContextDrop.new, + 's' => Proc.new{|c| c['context.count_scopes']}, + 'dummy' => [1]).should == "2" + + template = Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}' ) + template.render('context' => ContextDrop.new, + 's' => Proc.new{|c| c['context.count_scopes'] }, + 'dummy' => [1]).should == "3" + end + + it "should allow access to assigned variables through as scope" do + template = Liquid::Template.parse( '{% assign a = "variable"%}{{context.a}}' ) + template.render('context' => ContextDrop.new).should == "variable" + + template = Liquid::Template.parse( '{% assign a = "variable"%}{%for i in dummy%}{{context.a}}{%endfor%}' ) + template.render('context' => ContextDrop.new, 'dummy' => [1]).should == "variable" + + template = Liquid::Template.parse( '{% assign header_gif = "test"%}{{context.header_gif}}' ) + template.render('context' => ContextDrop.new).should == "test" + + template = Liquid::Template.parse( "{% assign header_gif = 'test'%}{{context.header_gif}}" ) + template.render('context' => ContextDrop.new).should == "test" + end + + it "should allow access to scope from within tags" do + template = Liquid::Template.parse( '{% for i in context.scopes_as_array %}{{i}}{% endfor %}' ) + template.render('context' => ContextDrop.new, 'dummy' => [1]).should == "1" + + template = Liquid::Template.parse( '{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}' ) + template.render('context' => ContextDrop.new, 'dummy' => [1]).should == "12" + + template = Liquid::Template.parse( '{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}' ) + template.render('context' => ContextDrop.new, 'dummy' => [1]).should == "123" + end + + it "should allow access to the forloop index within a drop" do + template = Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ) + template.render('context' => ContextDrop.new, 'dummy' => ["first","second","third"]).should == "123" + end + end + + context "enumerable drop" do + + it "should allow iteration through the drop" do + template = Liquid::Template.parse( '{% for c in collection %}{{c}}{% endfor %}') + template.render('collection' => EnumerableDrop.new).should == "123" + end + + it "should return the drops size" do + template = Liquid::Template.parse( '{{collection.size}}') + template.render('collection' => EnumerableDrop.new).should == "3" + end + + end + + end +end + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f0e4ce478..fa129c25c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,13 +2,15 @@ require "bundler" Bundler.setup -$LOAD_PATH.unshift(File.dirname(__FILE__)) +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__))) + +# add lib to load path $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib")) require 'locomotive_liquid' require 'rspec' -# support +# load support helpers Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} diff --git a/test/drop_test.rb b/test/drop_test.rb deleted file mode 100644 index 78a2f736d..000000000 --- a/test/drop_test.rb +++ /dev/null @@ -1,162 +0,0 @@ - -#!/usr/bin/env ruby -require File.dirname(__FILE__) + '/helper' - -class ContextDrop < Liquid::Drop - def scopes - @context.scopes.size - end - - def scopes_as_array - (1..@context.scopes.size).to_a - end - - def loop_pos - @context['forloop.index'] - end - - def break - Breakpoint.breakpoint - end - - def before_method(method) - return @context[method] - end -end - - -class ProductDrop < Liquid::Drop - - class TextDrop < Liquid::Drop - def array - ['text1', 'text2'] - end - - def text - 'text1' - end - end - - class CatchallDrop < Liquid::Drop - def before_method(method) - return 'method: ' << method - end - end - - def texts - TextDrop.new - end - - def catchall - CatchallDrop.new - end - - def context - ContextDrop.new - end - - protected - def callmenot - "protected" - end -end - -class EnumerableDrop < Liquid::Drop - - def size - 3 - end - - def each - yield 1 - yield 2 - yield 3 - end -end - - -class DropsTest < Test::Unit::TestCase - include Liquid - - def test_product_drop - - assert_nothing_raised do - tpl = Liquid::Template.parse( ' ' ) - tpl.render('product' => ProductDrop.new) - end - end - - def test_text_drop - output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render('product' => ProductDrop.new) - assert_equal ' text1 ', output - - end - - def test_text_drop - output = Liquid::Template.parse( ' {{ product.catchall.unknown }} ' ).render('product' => ProductDrop.new) - assert_equal ' method: unknown ', output - - end - - def test_text_array_drop - output = Liquid::Template.parse( '{% for text in product.texts.array %} {{text}} {% endfor %}' ).render('product' => ProductDrop.new) - assert_equal ' text1 text2 ', output - end - - def test_context_drop - output = Liquid::Template.parse( ' {{ context.bar }} ' ).render('context' => ContextDrop.new, 'bar' => "carrot") - assert_equal ' carrot ', output - end - - def test_nested_context_drop - output = Liquid::Template.parse( ' {{ product.context.foo }} ' ).render('product' => ProductDrop.new, 'foo' => "monkey") - assert_equal ' monkey ', output - end - - def test_protected - output = Liquid::Template.parse( ' {{ product.callmenot }} ' ).render('product' => ProductDrop.new) - assert_equal ' ', output - end - - def test_scope - assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render('context' => ContextDrop.new) - assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) - assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) - end - - def test_scope_though_proc - assert_equal '1', Liquid::Template.parse( '{{ s }}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }) - assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ s }}{%endfor%}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1]) - assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1]) - end - - def test_scope_with_assigns - assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render('context' => ContextDrop.new) - assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) - assert_equal 'test', Liquid::Template.parse( '{% assign header_gif = "test"%}{{header_gif}}' ).render('context' => ContextDrop.new) - assert_equal 'test', Liquid::Template.parse( "{% assign header_gif = 'test'%}{{header_gif}}" ).render('context' => ContextDrop.new) - end - - def test_scope_from_tags - assert_equal '1', Liquid::Template.parse( '{% for i in context.scopes_as_array %}{{i}}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1]) - assert_equal '12', Liquid::Template.parse( '{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1]) - assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1]) - end - - def test_access_context_from_drop - assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1,2,3]) - end - - def test_enumerable_drop - assert_equal '123', Liquid::Template.parse( '{% for c in collection %}{{c}}{% endfor %}').render('collection' => EnumerableDrop.new) - end - - def test_enumerable_drop_size - assert_equal '3', Liquid::Template.parse( '{{collection.size}}').render('collection' => EnumerableDrop.new) - end - - - -end - - From 1fdfc2fa838262f0d8df78be9e3a896255e5e55d Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 01:10:06 -0700 Subject: [PATCH 22/61] Convert error handling spec --- spec/fixtures/error_drop.rb | 17 +++++ spec/rendering/error_handling_spec.rb | 59 ++++++++++++++++++ test/error_handling_test.rb | 89 --------------------------- 3 files changed, 76 insertions(+), 89 deletions(-) create mode 100644 spec/fixtures/error_drop.rb create mode 100644 spec/rendering/error_handling_spec.rb delete mode 100644 test/error_handling_test.rb diff --git a/spec/fixtures/error_drop.rb b/spec/fixtures/error_drop.rb new file mode 100644 index 000000000..246614d86 --- /dev/null +++ b/spec/fixtures/error_drop.rb @@ -0,0 +1,17 @@ +class ErrorDrop < Liquid::Drop + def standard_error + raise Liquid::StandardError, 'standard error' + end + + def argument_error + raise Liquid::ArgumentError, 'argument error' + end + + def syntax_error + raise Liquid::SyntaxError, 'syntax error' + end + + def default_raise + raise "Another error" + end +end \ No newline at end of file diff --git a/spec/rendering/error_handling_spec.rb b/spec/rendering/error_handling_spec.rb new file mode 100644 index 000000000..640804e6e --- /dev/null +++ b/spec/rendering/error_handling_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +require 'fixtures/error_drop' + +describe "Liquid Rendering" do + describe "Error Handling" do + + context "template throws a standard error" do + + it "should render the standard error message" do + template = Liquid::Template.parse(" {{ errors.standard_error }} ") + template.render('errors' => ErrorDrop.new).should == " Liquid error: standard error " + + template.errors.size.should == 1 + template.errors.first.should be_an_instance_of(Liquid::StandardError) + end + end + + context "template throws a syntax error" do + it "should render the syntax error message" do + template = Liquid::Template.parse(" {{ errors.syntax_error }} ") + template.render('errors' => ErrorDrop.new).should == " Liquid syntax error: syntax error " + + template.errors.size.should == 1 + template.errors.first.should be_an_instance_of(Liquid::SyntaxError) + end + end + + context "template throws an argument error" do + it "should render the argument error message" do + template = Liquid::Template.parse(" {{ errors.argument_error }} ") + template.render('errors' => ErrorDrop.new).should == " Liquid error: argument error " + + template.errors.size.should == 1 + template.errors.first.should be_an_instance_of(Liquid::ArgumentError) + end + end + + context "template has a missing endtag" do + it "should raise an exception when parsing" do + expect { + Liquid::Template.parse(" {% for a in b %} ") + }.to raise_error(Liquid::SyntaxError) + end + end + + context "template has an unrecognized operator" do + it "should render the unrecognized argument error message" do + template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ') + template.render.should == ' Liquid error: Unknown operator =! ' + + template.errors.size.should == 1 + template.errors.first.should be_an_instance_of(Liquid::ArgumentError) + end + end + + + end +end \ No newline at end of file diff --git a/test/error_handling_test.rb b/test/error_handling_test.rb deleted file mode 100644 index b361d540a..000000000 --- a/test/error_handling_test.rb +++ /dev/null @@ -1,89 +0,0 @@ - -#!/usr/bin/env ruby -require File.dirname(__FILE__) + '/helper' - -class ErrorDrop < Liquid::Drop - def standard_error - raise Liquid::StandardError, 'standard error' - end - - def argument_error - raise Liquid::ArgumentError, 'argument error' - end - - def syntax_error - raise Liquid::SyntaxError, 'syntax error' - end - -end - - -class ErrorHandlingTest < Test::Unit::TestCase - include Liquid - - def test_standard_error - 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 - end - end - - def test_syntax - - 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 - - assert_nothing_raised do - - 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_raise(Liquid::SyntaxError) do - - template = Liquid::Template.parse(' {% for a in b %} ... ') - - end - - end - - - def test_unrecognized_operator - - assert_nothing_raised do - - template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ') - assert_equal ' Liquid error: Unknown operator =! ', template.render - - assert_equal 1, template.errors.size - assert_equal Liquid::ArgumentError, template.errors.first.class - - end - - end - -end - - From f3be5a671b13e240c8fd79be90587fe631f64687 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 01:27:49 -0700 Subject: [PATCH 23/61] Convert template inheritance spec --- spec/rendering/extends_spec.rb | 48 ++++++++++++++++++++++++++++++++++ test/extends_test.rb | 42 ----------------------------- 2 files changed, 48 insertions(+), 42 deletions(-) create mode 100644 spec/rendering/extends_spec.rb delete mode 100644 test/extends_test.rb diff --git a/spec/rendering/extends_spec.rb b/spec/rendering/extends_spec.rb new file mode 100644 index 000000000..c99b5a77f --- /dev/null +++ b/spec/rendering/extends_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "Template Inheritance" do + + class TestFileSystem + def read_template_file(template_path) + case template_path + when "parent-template" + "Hurrah!" + when "parent-with-variable" + "Hello, {{ name }}!" + when "parent-with-parent" + "{% extends parent-with-variable %}" + else + raise "TestFileSystem Error: No template defined for #{template_path}" + end + end + end + + before(:each) do + Liquid::Template.file_system = TestFileSystem.new + end + + it "should allow extending a path" do + template = Liquid::Template.parse "{% extends parent-template %}" + template.render.should == "Hurrah!" + end + + # TODO + # it "should allow extending, with additional content" do + # template = Liquid::Template.parse "{% extends parent-template %} Huzzah!" + # template.render.should == "Hurrah! Huzzah!" + # end + + it "should allow access to the context from the inherited template" do + template = Liquid::Template.parse "{% extends parent-with-variable %}" + template.render('name' => 'Joe').should == "Hello, Joe!" + end + + it "show allow deep nesting of inherited templates" do + template = Liquid::Template.parse "{% extends parent-with-parent %}" + template.render('name' => 'Joe').should == "Hello, Joe!" + end + + + end +end \ No newline at end of file diff --git a/test/extends_test.rb b/test/extends_test.rb deleted file mode 100644 index 71186423a..000000000 --- a/test/extends_test.rb +++ /dev/null @@ -1,42 +0,0 @@ -class ExtendsTest < Test::Unit::TestCase - include Liquid - - class TestFileSystem - def read_template_file(template_path) - case template_path - when "another_path" - "Another path!" - when "variable" - "Hello, {{ name }}!" - when "deep" - "{% extends variable %}" - else - template_path - end - end - end - - def setup - Liquid::Template.file_system = TestFileSystem.new - end - - def test_extends - document = Template.parse("{% extends another_path %}") - assert_equal 'Another path!', document.render({}) - end - - def test_extends_with_more - document = Template.parse("{% extends another_path %} Hello!") - assert_equal 'Another path!', document.render({}) - end - - def test_extends_var - document = Template.parse("{% extends variable %}") - assert_equal 'Hello, berto!', document.render({'name' => 'berto'}) - end - - def test_extends_deeper - document = Template.parse("{% extends deep %}") - assert_equal 'Hello, berto!', document.render({'name' => 'berto'}) - end -end \ No newline at end of file From bb978e00b184cc7e3a2be2e8eccffc5486755dfd Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 01:47:47 -0700 Subject: [PATCH 24/61] spec cleanup and reorg --- Rakefile | 18 +----------------- spec/rendering/error_handling_spec.rb | 1 - spec/unit/{block_spec.rb => parsing_spec.rb} | 2 +- 3 files changed, 2 insertions(+), 19 deletions(-) rename spec/unit/{block_spec.rb => parsing_spec.rb} (99%) diff --git a/Rakefile b/Rakefile index a0208b354..005a30041 100755 --- a/Rakefile +++ b/Rakefile @@ -1,25 +1,9 @@ #!/usr/bin/env ruby require 'rubygems' require 'rake' -require 'rake/testtask' require 'rake/gempackagetask' -task :default => 'test' - -Rake::TestTask.new(:test) do |t| - t.libs << "lib" - t.libs << "test" - t.pattern = 'test/*_test.rb' - t.verbose = false -end - -Rake::TestTask.new(:ti) do |t| - t.libs << "lib" - t.libs << "test" - t.test_files = ['test/test_helper.rb', 'test/extends_test.rb', 'test/inherited_block_test.rb'] - # t.test_files = ['test/test_helper.rb', 'test/inherited_block_test.rb', 'test/inherited_block_test.rb'] - t.verbose = false -end +task :default => 'spec' gemspec = eval(File.read('locomotive_liquid.gemspec')) Rake::GemPackageTask.new(gemspec) do |pkg| diff --git a/spec/rendering/error_handling_spec.rb b/spec/rendering/error_handling_spec.rb index 640804e6e..bb418eb38 100644 --- a/spec/rendering/error_handling_spec.rb +++ b/spec/rendering/error_handling_spec.rb @@ -54,6 +54,5 @@ end end - end end \ No newline at end of file diff --git a/spec/unit/block_spec.rb b/spec/unit/parsing_spec.rb similarity index 99% rename from spec/unit/block_spec.rb rename to spec/unit/parsing_spec.rb index 715ee066d..a2975e78b 100644 --- a/spec/unit/block_spec.rb +++ b/spec/unit/parsing_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe "Liquid Parsing" do - describe "Block" do + describe "Blocks" do it "should render whitespace properly" do template = Liquid::Template.parse(" ") From 67062a4b1e0df252b750de8eee4283f8c53ed4a3 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 01:47:53 -0700 Subject: [PATCH 25/61] converting filesystem test --- spec/unit/file_system_spec.rb | 42 +++++++++++++++++++++++++++++++++++ test/file_system_test.rb | 30 ------------------------- 2 files changed, 42 insertions(+), 30 deletions(-) create mode 100644 spec/unit/file_system_spec.rb delete mode 100644 test/file_system_test.rb diff --git a/spec/unit/file_system_spec.rb b/spec/unit/file_system_spec.rb new file mode 100644 index 000000000..470a725c1 --- /dev/null +++ b/spec/unit/file_system_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe "Liquid File System" do + + describe Liquid::BlankFileSystem do + it "should error out when trying to ready any file" do + expect { + Liquid::BlankFileSystem.new.read_template_file("dummy") + }.to raise_error(Liquid::FileSystemError) + end + end + + describe Liquid::LocalFileSystem do + describe "#full_path" do + before(:each) do + @file_system = Liquid::LocalFileSystem.new("/some/path") + end + + it "should translate partial paths to the full filesystem path" do + @file_system.full_path('mypartial').should == "/some/path/_mypartial.liquid" + @file_system.full_path('dir/mypartial').should == "/some/path/dir/_mypartial.liquid" + end + + it "should raise errors if we try to go outside of the root" do + expect { + @file_system.full_path("../dir/mypartial") + }.to raise_error(Liquid::FileSystemError) + + expect { + @file_system.full_path("/dir/../../dir/mypartial") + }.to raise_error(Liquid::FileSystemError) + end + + it "should not allow absolute paths" do + expect { + @file_system.full_path("/etc/passwd") + }.to raise_error(Liquid::FileSystemError) + end + + end + end +end \ No newline at end of file diff --git a/test/file_system_test.rb b/test/file_system_test.rb deleted file mode 100644 index d3ab94871..000000000 --- a/test/file_system_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env ruby -require File.dirname(__FILE__) + '/helper' - -class FileSystemTest < Test::Unit::TestCase - include Liquid - - def test_default - assert_raise(FileSystemError) do - BlankFileSystem.new.read_template_file("dummy") - end - end - - def test_local - file_system = Liquid::LocalFileSystem.new("/some/path") - 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_raise(FileSystemError) do - file_system.full_path("../dir/mypartial") - end - - assert_raise(FileSystemError) do - file_system.full_path("/dir/../../dir/mypartial") - end - - assert_raise(FileSystemError) do - file_system.full_path("/etc/passwd") - end - end -end \ No newline at end of file From 232645a2f458ca18ab7cd5dca39dbb9fdb4eebf8 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 02:25:14 -0700 Subject: [PATCH 26/61] Converting filter test --- spec/fixtures/filters/money_filters.rb | 16 ++++ spec/rendering/filter_spec.rb | 126 +++++++++++++++++++++++++ spec/spec_helper.rb | 3 + test/filter_test.rb | 95 ------------------- 4 files changed, 145 insertions(+), 95 deletions(-) create mode 100644 spec/fixtures/filters/money_filters.rb create mode 100644 spec/rendering/filter_spec.rb delete mode 100644 test/filter_test.rb diff --git a/spec/fixtures/filters/money_filters.rb b/spec/fixtures/filters/money_filters.rb new file mode 100644 index 000000000..7be3bd300 --- /dev/null +++ b/spec/fixtures/filters/money_filters.rb @@ -0,0 +1,16 @@ +module MoneyFilter + def money(input) + sprintf('$%d', input) + end + + def money_with_underscores(input) + sprintf('_$%d_', input) + end +end + +module CanadianMoneyFilter + def money(input) + sprintf('$%d CAD', input) + end +end + diff --git a/spec/rendering/filter_spec.rb b/spec/rendering/filter_spec.rb new file mode 100644 index 000000000..61e42e094 --- /dev/null +++ b/spec/rendering/filter_spec.rb @@ -0,0 +1,126 @@ +require 'spec_helper' + +require 'filters/money_filters' + +describe "Liquid Rendering" do + describe "Filters" do + + before(:each) do + @context = Liquid::Context.new + end + + def render(body) + Liquid::Variable.new(body).render(@context) + end + + context "with custom filters added to context" do + before(:each) do + @context['val'] = 1000 + end + + it "should use the local filters" do + @context.add_filters(MoneyFilter) + render('val | money').should == "$1000" + render('val | money_with_underscores').should == "_$1000_" + end + + it "should allow filters to overwrite previous ones" do + @context.add_filters(MoneyFilter) + @context.add_filters(CanadianMoneyFilter) + render('val | money').should == "$1000 CAD" + end + end + + context "standard filters" do + describe "size" do + it "should return the size of a string" do + @context['val'] = "abcd" + render('val | size').should == 4 + end + + it "should return the size of an array" do + @context['val'] = [1,2,3,4] + render('val | size').should == 4 + end + + it "should return the size of a hash" do + @context['val'] = {"one" => 1, "two" => 2, "three" => 3, "four" => 4} + render('val | size').should == 4 + end + end + + describe "join" do + it "should join an array" do + @context['val'] = [1,2,3,4] + render('val | join').should == "1 2 3 4" + end + + it "should join a hash" do + @context['val'] = {"one" => 1} + render('val | join').should == "one1" + end + + it "should join a string" do + @context['val'] = "one" + render('val | join').should == "one" + end + end + + describe "sort" do + it "should sort a single value" do + @context['value'] = 3 + render("value | sort").should == [3] + end + + it "should sort an array of numbers" do + @context['numbers'] = [2,1,4,3] + render("numbers | sort").should == [1,2,3,4] + end + + it "should sort an array of words" do + @context['words'] = ['expected', 'as', 'alphabetic'] + render("words | sort").should == ['alphabetic', 'as', 'expected'] + end + + it "should sort an array of arrays" do + @context['arrays'] = [['flattened'], ['are']] + render('arrays | sort').should == ['are', 'flattened'] + end + end + + describe "strip_html" do + it "should strip out tags around a " do + @context['user_input'] = "bla blub" + render('user_input | strip_html').should == "bla blub" + end + + it "should remove script tags entirely" do + @context['user_input'] = "" + render('user_input | strip_html').should == "" + end + end + + describe "capitalize" do + it "should capitalize the first character" do + @context['val'] = "blub" + render('val | capitalize').should == 'Blub' + end + end + end + + context "filters in template" do + before(:each) do + Liquid::Template.register_filter(MoneyFilter) + end + + it "should use globally registered filters" do + Liquid::Template.parse('{{1000 | money}}').render.should == "$1000" + end + + it "should allow custom filters to override registered filters" do + Liquid::Template.parse('{{1000 | money}}').render(nil, :filters => CanadianMoneyFilter).should == "$1000 CAD" + Liquid::Template.parse('{{1000 | money}}').render(nil, :filters => [CanadianMoneyFilter]).should == "$1000 CAD" + end + end + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index fa129c25c..e9572fa1f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,6 +7,9 @@ # add lib to load path $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib")) +# add fixtures to load path +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "fixtures")) + require 'locomotive_liquid' require 'rspec' diff --git a/test/filter_test.rb b/test/filter_test.rb deleted file mode 100644 index c66fe0636..000000000 --- a/test/filter_test.rb +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env ruby -require File.dirname(__FILE__) + '/helper' - -module MoneyFilter - def money(input) - sprintf(' %d$ ', input) - end - - def money_with_underscore(input) - sprintf(' %d$ ', input) - end -end - -module CanadianMoneyFilter - def money(input) - sprintf(' %d$ CAD ', input) - end -end - -class FiltersTest < Test::Unit::TestCase - include Liquid - - def setup - @context = Context.new - end - - def test_local_filter - @context['var'] = 1000 - @context.add_filters(MoneyFilter) - assert_equal ' 1000$ ', Variable.new("var | money").render(@context) - end - - def test_underscore_in_filter_name - @context['var'] = 1000 - @context.add_filters(MoneyFilter) - assert_equal ' 1000$ ', Variable.new("var | money_with_underscore").render(@context) - end - - def test_second_filter_overwrites_first - @context['var'] = 1000 - @context.add_filters(MoneyFilter) - @context.add_filters(CanadianMoneyFilter) - assert_equal ' 1000$ CAD ', Variable.new("var | money").render(@context) - end - - def test_size - @context['var'] = 'abcd' - @context.add_filters(MoneyFilter) - assert_equal 4, Variable.new("var | size").render(@context) - end - - def test_join - @context['var'] = [1,2,3,4] - assert_equal "1 2 3 4", Variable.new("var | join").render(@context) - end - - def test_sort - @context['value'] = 3 - @context['numbers'] = [2,1,4,3] - @context['words'] = ['expected', 'as', 'alphabetic'] - @context['arrays'] = [['flattened'], ['are']] - assert_equal [1,2,3,4], Variable.new("numbers | sort").render(@context) - assert_equal ['alphabetic', 'as', 'expected'], - Variable.new("words | sort").render(@context) - assert_equal [3], Variable.new("value | sort").render(@context) - assert_equal ['are', 'flattened'], Variable.new("arrays | sort").render(@context) - end - - def test_strip_html - @context['var'] = "bla blub" - assert_equal "bla blub", Variable.new("var | strip_html").render(@context) - end - - def test_capitalize - @context['var'] = "blub" - assert_equal "Blub", Variable.new("var | capitalize").render(@context) - end -end - -class FiltersInTemplate < Test::Unit::TestCase - include Liquid - - def test_local_global - 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 - assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, CanadianMoneyFilter) - assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, [CanadianMoneyFilter]) - end -end From 98eacaf484c62f3ac5655f07ca07d1ecc6e02ff8 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 02:27:25 -0700 Subject: [PATCH 27/61] Reorganizing fixtures --- spec/fixtures/{ => drops}/context_drop.rb | 0 spec/fixtures/{ => drops}/enumerable_drop.rb | 0 spec/fixtures/{ => drops}/error_drop.rb | 0 spec/fixtures/{ => drops}/product_drop.rb | 0 spec/rendering/drop_spec.rb | 6 +++--- spec/rendering/error_handling_spec.rb | 2 +- 6 files changed, 4 insertions(+), 4 deletions(-) rename spec/fixtures/{ => drops}/context_drop.rb (100%) rename spec/fixtures/{ => drops}/enumerable_drop.rb (100%) rename spec/fixtures/{ => drops}/error_drop.rb (100%) rename spec/fixtures/{ => drops}/product_drop.rb (100%) diff --git a/spec/fixtures/context_drop.rb b/spec/fixtures/drops/context_drop.rb similarity index 100% rename from spec/fixtures/context_drop.rb rename to spec/fixtures/drops/context_drop.rb diff --git a/spec/fixtures/enumerable_drop.rb b/spec/fixtures/drops/enumerable_drop.rb similarity index 100% rename from spec/fixtures/enumerable_drop.rb rename to spec/fixtures/drops/enumerable_drop.rb diff --git a/spec/fixtures/error_drop.rb b/spec/fixtures/drops/error_drop.rb similarity index 100% rename from spec/fixtures/error_drop.rb rename to spec/fixtures/drops/error_drop.rb diff --git a/spec/fixtures/product_drop.rb b/spec/fixtures/drops/product_drop.rb similarity index 100% rename from spec/fixtures/product_drop.rb rename to spec/fixtures/drops/product_drop.rb diff --git a/spec/rendering/drop_spec.rb b/spec/rendering/drop_spec.rb index f8a76460d..a56973320 100644 --- a/spec/rendering/drop_spec.rb +++ b/spec/rendering/drop_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -require 'fixtures/product_drop' -require 'fixtures/context_drop' -require 'fixtures/enumerable_drop' +require 'drops/product_drop' +require 'drops/context_drop' +require 'drops/enumerable_drop' describe "Liquid Rendering" do describe "Drops" do diff --git a/spec/rendering/error_handling_spec.rb b/spec/rendering/error_handling_spec.rb index bb418eb38..c02f85893 100644 --- a/spec/rendering/error_handling_spec.rb +++ b/spec/rendering/error_handling_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -require 'fixtures/error_drop' +require 'drops/error_drop' describe "Liquid Rendering" do describe "Error Handling" do From 9ddce08a0bdcd7899fe4b24832fa8c19514344e0 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 04:31:20 -0700 Subject: [PATCH 28/61] Converting additional tests to specs --- spec/rendering/filter_spec.rb | 36 ++-- spec/rendering/if_else_spec.rb | 162 ++++++++++++++++++ spec/rendering/table_spec.rb | 57 ++++++ spec/spec_helper.rb | 12 ++ ...e_string_helper.rb => multiline_string.rb} | 6 +- test/html_tag_test.rb | 31 ---- test/if_else_test.rb | 153 ----------------- 7 files changed, 252 insertions(+), 205 deletions(-) create mode 100644 spec/rendering/if_else_spec.rb create mode 100644 spec/rendering/table_spec.rb rename spec/support/{multiline_string_helper.rb => multiline_string.rb} (79%) delete mode 100644 test/html_tag_test.rb delete mode 100644 test/if_else_test.rb diff --git a/spec/rendering/filter_spec.rb b/spec/rendering/filter_spec.rb index 61e42e094..78390a37e 100644 --- a/spec/rendering/filter_spec.rb +++ b/spec/rendering/filter_spec.rb @@ -9,7 +9,7 @@ @context = Liquid::Context.new end - def render(body) + def render_variable(body) Liquid::Variable.new(body).render(@context) end @@ -20,14 +20,14 @@ def render(body) it "should use the local filters" do @context.add_filters(MoneyFilter) - render('val | money').should == "$1000" - render('val | money_with_underscores').should == "_$1000_" + render_variable('val | money').should == "$1000" + render_variable('val | money_with_underscores').should == "_$1000_" end it "should allow filters to overwrite previous ones" do @context.add_filters(MoneyFilter) @context.add_filters(CanadianMoneyFilter) - render('val | money').should == "$1000 CAD" + render_variable('val | money').should == "$1000 CAD" end end @@ -35,75 +35,75 @@ def render(body) describe "size" do it "should return the size of a string" do @context['val'] = "abcd" - render('val | size').should == 4 + render_variable('val | size').should == 4 end it "should return the size of an array" do @context['val'] = [1,2,3,4] - render('val | size').should == 4 + render_variable('val | size').should == 4 end it "should return the size of a hash" do @context['val'] = {"one" => 1, "two" => 2, "three" => 3, "four" => 4} - render('val | size').should == 4 + render_variable('val | size').should == 4 end end describe "join" do it "should join an array" do @context['val'] = [1,2,3,4] - render('val | join').should == "1 2 3 4" + render_variable('val | join').should == "1 2 3 4" end it "should join a hash" do @context['val'] = {"one" => 1} - render('val | join').should == "one1" + render_variable('val | join').should == "one1" end it "should join a string" do @context['val'] = "one" - render('val | join').should == "one" + render_variable('val | join').should == "one" end end describe "sort" do it "should sort a single value" do @context['value'] = 3 - render("value | sort").should == [3] + render_variable("value | sort").should == [3] end it "should sort an array of numbers" do @context['numbers'] = [2,1,4,3] - render("numbers | sort").should == [1,2,3,4] + render_variable("numbers | sort").should == [1,2,3,4] end it "should sort an array of words" do @context['words'] = ['expected', 'as', 'alphabetic'] - render("words | sort").should == ['alphabetic', 'as', 'expected'] + render_variable("words | sort").should == ['alphabetic', 'as', 'expected'] end it "should sort an array of arrays" do @context['arrays'] = [['flattened'], ['are']] - render('arrays | sort').should == ['are', 'flattened'] + render_variable('arrays | sort').should == ['are', 'flattened'] end end describe "strip_html" do it "should strip out tags around a " do @context['user_input'] = "bla blub" - render('user_input | strip_html').should == "bla blub" + render_variable('user_input | strip_html').should == "bla blub" end it "should remove script tags entirely" do @context['user_input'] = "" - render('user_input | strip_html').should == "" + render_variable('user_input | strip_html').should == "" end end describe "capitalize" do it "should capitalize the first character" do @context['val'] = "blub" - render('val | capitalize').should == 'Blub' + render_variable('val | capitalize').should == 'Blub' end end end @@ -114,7 +114,7 @@ def render(body) end it "should use globally registered filters" do - Liquid::Template.parse('{{1000 | money}}').render.should == "$1000" + render('{{1000 | money}}').should == "$1000" end it "should allow custom filters to override registered filters" do diff --git a/spec/rendering/if_else_spec.rb b/spec/rendering/if_else_spec.rb new file mode 100644 index 000000000..9174225b9 --- /dev/null +++ b/spec/rendering/if_else_spec.rb @@ -0,0 +1,162 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "If/Else" do + + describe "If" do + it "should show/hide content correctly when passed a boolean constant" do + render(' {% if false %} this text should not go into the output {% endif %} ').should == " " + render(' {% if true %} this text should not go into the output {% endif %} ').should == " this text should not go into the output " + render('{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}? ').should == " you rock ? " + end + + it "should show/hide content correctly when passed a variable" do + template = Liquid::Template.parse(' {% if var %} YES {% endif %} ') + template.render('var' => true).should == " YES " + template.render('var' => false).should == " " + + render('{% if var %} NO {% endif %}', 'var' => false).should == '' + render('{% if var %} NO {% endif %}', 'var' => nil).should == '' + render('{% if foo.bar %} NO {% endif %}', 'foo' => {'bar' => false}).should == '' + render('{% if foo.bar %} NO {% endif %}', 'foo' => {}).should == '' + render('{% if foo.bar %} NO {% endif %}', 'foo' => nil).should == '' + render('{% if foo.bar %} NO {% endif %}', 'foo' => true).should == '' + + render('{% if var %} YES {% endif %}', 'var' => "text").should == ' YES ' + render('{% if var %} YES {% endif %}', 'var' => true).should == ' YES ' + render('{% if var %} YES {% endif %}', 'var' => 1).should == ' YES ' + render('{% if var %} YES {% endif %}', 'var' => {}).should == ' YES ' + render('{% if var %} YES {% endif %}', 'var' => []).should == ' YES ' + render('{% if "foo" %} YES {% endif %}').should == ' YES ' + render('{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => true}).should == ' YES ' + render('{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => "text"}).should == ' YES ' + render('{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => 1 }).should == ' YES ' + render('{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => {} }).should == ' YES ' + render('{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => [] }).should == ' YES ' + + render('{% if var %} NO {% else %} YES {% endif %}', 'var' => false).should == ' YES ' + render('{% if var %} NO {% else %} YES {% endif %}', 'var' => nil).should == ' YES ' + render('{% if var %} YES {% else %} NO {% endif %}', 'var' => true).should == ' YES ' + render('{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text").should == ' YES ' + + render('{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'bar' => false}).should == ' YES ' + render('{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => true}).should == ' YES ' + render('{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => "text"}).should == ' YES ' + render('{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'notbar' => true}).should == ' YES ' + render('{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {}).should == ' YES ' + render('{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => {'bar' => true}).should == ' YES ' + end + + it "should allow nested if conditionals" do + render('{% if false %}{% if false %} NO {% endif %}{% endif %}').should == '' + render('{% if false %}{% if true %} NO {% endif %}{% endif %}').should == '' + render('{% if true %}{% if false %} NO {% endif %}{% endif %}').should == '' + render('{% if true %}{% if true %} YES {% endif %}{% endif %}').should == ' YES ' + + render('{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}').should == ' YES ' + render('{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}').should == ' YES ' + render('{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}').should == ' YES ' + end + + it "should allow if comparisons against null" do + render('{% if null < 10 %} NO {% endif %}').should == '' + render('{% if null <= 10 %} NO {% endif %}').should == '' + render('{% if null >= 10 %} NO {% endif %}').should == '' + render('{% if null > 10 %} NO {% endif %}').should == '' + render('{% if 10 < null %} NO {% endif %}').should == '' + render('{% if 10 <= null %} NO {% endif %}').should == '' + render('{% if 10 >= null %} NO {% endif %}').should == '' + render('{% if 10 > null %} NO {% endif %}').should == '' + end + + it "should raise a syntax error if there's no closing endif" do + expect { + render('{% if jerry == 1 %}') + }.to raise_error(Liquid::SyntaxError) + end + + it "should raise a syntax error if there's variable argument" do + expect { + render('{% if %}') + }.to raise_error(Liquid::SyntaxError) + end + + it "should work with custom conditions" do + Liquid::Condition.operators['containz'] = :[] + + render("{% if 'bob' containz 'o' %}yes{% endif %}").should == "yes" + render("{% if 'bob' containz 'f' %}yes{% else %}no{% endif %}").should == "no" + + end + + context "or conditionals" do + it "should work correctly when passed 2 variables" do + body = '{% if a or b %} YES {% endif %}' + + render(body, 'a' => true, 'b' => true).should == " YES " + render(body, 'a' => true, 'b' => false).should == " YES " + render(body, 'a' => false, 'b' => true).should == " YES " + render(body, 'a' => false, 'b' => false).should == "" + end + + it "should work correctly when passed 3 variables" do + body = '{% if a or b or c %} YES {% endif %}' + + render(body, 'a' => false, 'b' => false, 'c' => true).should == " YES " + render(body, 'a' => false, 'b' => false, 'c' => false).should == "" + end + + it "should work correctly when passed comparison operators" do + data = {'a' => true, 'b' => true} + + render('{% if a == true or b == true %} YES {% endif %}', data).should == " YES " + render('{% if a == true or b == false %} YES {% endif %}', data).should == " YES " + render('{% if a == false or b == true %} YES {% endif %}', data).should == " YES " + render('{% if a == false or b == false %} YES {% endif %}', data).should == "" + end + + it "should handle correctly when used with string comparisons" 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" + data = {'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true} + + render("{% if #{awful_markup} %} YES {% endif %}", data).should == " YES " + end + + it "should handle correctly when using nested expression comparisons" do + data = {'order' => {'items_count' => 0}, 'android' => {'name' => 'Roy'}} + + render("{% if android.name == 'Roy' %} YES {% endif %}", data).should == " YES " + render("{% if order.items_count == 0 %} YES {% endif %}", data).should == " YES " + end + end + + context "and conditionals" do + it "should work correctly when passed 2 variables" do + body = '{% if a and b %} YES {% endif %}' + + render(body, 'a' => true, 'b' => true).should == " YES " + render(body, 'a' => true, 'b' => false).should == "" + render(body, 'a' => false, 'b' => true).should == "" + render(body, 'a' => false, 'b' => false).should == "" + end + end + end + + describe "Else" do + it "should render the right block based on the input" do + render('{% if false %} NO {% else %} YES {% endif %}').should == " YES " + render('{% if true %} YES {% else %} NO {% endif %}').should == " YES " + render('{% if "foo" %} YES {% else %} NO {% endif %}').should == " YES " + end + + it "should allow elsif helper" do + render('{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}').should == '0' + render('{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}').should == '1' + render('{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}').should == '2' + render('{% if false %}if{% elsif true %}elsif{% endif %}').should == 'elsif' + end + end + + end + +end \ No newline at end of file diff --git a/spec/rendering/table_spec.rb b/spec/rendering/table_spec.rb new file mode 100644 index 000000000..275eaca9e --- /dev/null +++ b/spec/rendering/table_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "Table helpers " do + + describe "tablerow" do + it "should render a table with rows of 3 columns each" do + + template = Liquid::Template.parse multiline_string(<<-END) + | {% tablerow n in numbers cols: 3 %} {{ n }} {% endtablerow %} + END + + template.render('numbers' => [1,2,3,4,5,6]).strip.should == multiline_string(<<-END).strip + | + | 1 2 3 + | 4 5 6 + END + + end + + it "should render an empty table row of columns" do + template = Liquid::Template.parse multiline_string(<<-END) + | {% tablerow n in numbers cols: 3 %} {{ n }} {% endtablerow %} + END + + template.render('numbers' => []).should == "\n\n" + end + + it "should render a table with rows of 5 columns each" do + template = Liquid::Template.parse multiline_string(<<-END) + | {% tablerow n in numbers cols: 5 %} {{ n }} {% endtablerow %} + END + + template.render('numbers' => [1,2,3,4,5,6]).strip.should == multiline_string(<<-END).strip + | + | 1 2 3 4 5 + | 6 + END + end + + it "should provide a tablerowloop.col counter within the tablerow" do + template = Liquid::Template.parse multiline_string(<<-END) + | {% tablerow n in numbers cols: 2 %} {{ tablerowloop.col }} {% endtablerow %} + END + + template.render('numbers' => [1,2,3,4,5,6]).strip.should == multiline_string(<<-END).strip + | + | 1 2 + | 1 2 + | 1 2 + END + end + + end + + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e9572fa1f..d50cdfa69 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -17,3 +17,15 @@ # load support helpers Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} + +module Liquid + module RenderSpecHelper + def render(body, data = {}) + Liquid::Template.parse(body).render(data) + end + end +end + +Rspec.configure do |c| + c.include Liquid::RenderSpecHelper +end \ No newline at end of file diff --git a/spec/support/multiline_string_helper.rb b/spec/support/multiline_string.rb similarity index 79% rename from spec/support/multiline_string_helper.rb rename to spec/support/multiline_string.rb index 68a1b2ef9..49c5e7f08 100644 --- a/spec/support/multiline_string_helper.rb +++ b/spec/support/multiline_string.rb @@ -1,5 +1,5 @@ module RSpec - module MultilineStringHelper + module MultilineString # # used to format multiline strings (prefix lines with |) # @@ -16,7 +16,7 @@ module MultilineStringHelper # def multiline_string(string, pipechar = '|') arr = string.split("\n") # Split into lines - arr.map! {|x| x.sub(/^\s*\|/, "")} # Remove leading characters + arr.map! {|x| x.sub(/^\s*\| /, "")} # Remove leading characters arr.map! {|x| x.sub(/\|$/,"")} # Remove ending characters arr.join("\n") # Rejoin into a single line end @@ -24,5 +24,5 @@ def multiline_string(string, pipechar = '|') end Rspec.configure do |c| - c.include Rspec::MultilineStringHelper + c.include Rspec::MultilineString end \ No newline at end of file diff --git a/test/html_tag_test.rb b/test/html_tag_test.rb deleted file mode 100644 index 546344e94..000000000 --- a/test/html_tag_test.rb +++ /dev/null @@ -1,31 +0,0 @@ -require File.dirname(__FILE__) + '/helper' - -class HtmlTagTest < Test::Unit::TestCase - include Liquid - - def test_html_table - - assert_template_result("\n 1 2 3 \n 4 5 6 \n", - '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', - 'numbers' => [1,2,3,4,5,6]) - - assert_template_result("\n\n", - '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', - 'numbers' => []) - end - - def test_html_table_with_different_cols - assert_template_result("\n 1 2 3 4 5 \n 6 \n", - '{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}', - 'numbers' => [1,2,3,4,5,6]) - - end - - def test_html_col_counter - assert_template_result("\n12\n12\n12\n", - '{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}', - 'numbers' => [1,2,3,4,5,6]) - - end - -end diff --git a/test/if_else_test.rb b/test/if_else_test.rb deleted file mode 100644 index 2a8c1ad7f..000000000 --- a/test/if_else_test.rb +++ /dev/null @@ -1,153 +0,0 @@ -require File.dirname(__FILE__) + '/helper' - -class IfElseTest < Test::Unit::TestCase - include Liquid - - def test_if - assert_template_result(' ',' {% if false %} this text should not go into the output {% endif %} ') - assert_template_result(' this text should go into the output ', - ' {% if true %} this text should go into the output {% endif %} ') - assert_template_result(' you rock ?','{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?') - end - - def test_if_else - assert_template_result(' YES ','{% if false %} NO {% else %} YES {% endif %}') - assert_template_result(' YES ','{% if true %} YES {% else %} NO {% endif %}') - assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}') - end - - def test_if_boolean - assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true) - end - - def test_if_or - 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(' 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) - end - - def test_if_or_with_operators - assert_template_result(' YES ','{% if a == true or b == true %} YES {% endif %}', 'a' => true, 'b' => true) - assert_template_result(' YES ','{% if a == true or b == false %} YES {% endif %}', 'a' => true, 'b' => true) - assert_template_result('','{% if a == false or b == false %} YES {% endif %}', 'a' => true, 'b' => true) - end - - def test_comparison_of_strings_containing_and_or_or - 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_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 - assert_template_result(' YES ','{% if true and true %} YES {% endif %}') - assert_template_result('','{% if false and true %} YES {% endif %}') - assert_template_result('','{% if false and true %} YES {% endif %}') - end - - - def test_hash_miss_generates_false - assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {}) - end - - def test_if_from_variable - assert_template_result('','{% if var %} NO {% endif %}', 'var' => false) - assert_template_result('','{% if var %} NO {% endif %}', 'var' => nil) - assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {'bar' => false}) - assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {}) - assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => nil) - assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => true) - - assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => "text") - assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true) - assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => 1) - assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => {}) - assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => []) - assert_template_result(' YES ','{% if "foo" %} YES {% endif %}') - assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => true}) - assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => "text"}) - assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => 1 }) - assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => {} }) - assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => [] }) - - assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => false) - assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => nil) - assert_template_result(' YES ','{% if var %} YES {% else %} NO {% endif %}', 'var' => true) - assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text") - - assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'bar' => false}) - assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => true}) - assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => "text"}) - assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'notbar' => true}) - assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {}) - assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => {'bar' => true}) - end - - def test_nested_if - assert_template_result('', '{% if false %}{% if false %} NO {% endif %}{% endif %}') - assert_template_result('', '{% if false %}{% if true %} NO {% endif %}{% endif %}') - assert_template_result('', '{% if true %}{% if false %} NO {% endif %}{% endif %}') - assert_template_result(' YES ', '{% if true %}{% if true %} YES {% endif %}{% endif %}') - - assert_template_result(' YES ', '{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}') - assert_template_result(' YES ', '{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}') - assert_template_result(' YES ', '{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}') - - end - - def test_comparisons_on_null - assert_template_result('','{% if null < 10 %} NO {% endif %}') - assert_template_result('','{% if null <= 10 %} NO {% endif %}') - assert_template_result('','{% if null >= 10 %} NO {% endif %}') - assert_template_result('','{% if null > 10 %} NO {% endif %}') - - assert_template_result('','{% if 10 < null %} NO {% endif %}') - assert_template_result('','{% if 10 <= null %} NO {% endif %}') - assert_template_result('','{% if 10 >= null %} NO {% endif %}') - assert_template_result('','{% if 10 > null %} NO {% endif %}') - end - - def test_else_if - assert_template_result('0','{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}') - assert_template_result('1','{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}') - assert_template_result('2','{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}') - - assert_template_result('elsif','{% if false %}if{% elsif true %}elsif{% endif %}') - end - - def test_syntax_error_no_variable - assert_raise(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}')} - end - - def test_syntax_error_no_expression - assert_raise(SyntaxError) { assert_template_result('', '{% if %}') } - end - - def test_if_with_custom_condition - 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.delete 'contains' - end -end \ No newline at end of file From 01a1bfc94119fa222ea6c1cc9d646b853a8c0483 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 05:24:55 -0700 Subject: [PATCH 29/61] include tag spec, and cleanup --- spec/rendering/extends_spec.rb | 58 ++++++++++----- spec/rendering/include_spec.rb | 98 +++++++++++++++++++++++++ spec/spec_helper.rb | 8 +- test/include_tag_test.rb | 129 --------------------------------- test/inherited_block_test.rb | 46 ------------ 5 files changed, 145 insertions(+), 194 deletions(-) create mode 100644 spec/rendering/include_spec.rb delete mode 100644 test/include_tag_test.rb delete mode 100644 test/inherited_block_test.rb diff --git a/spec/rendering/extends_spec.rb b/spec/rendering/extends_spec.rb index c99b5a77f..359e66751 100644 --- a/spec/rendering/extends_spec.rb +++ b/spec/rendering/extends_spec.rb @@ -2,27 +2,22 @@ describe "Liquid Rendering" do describe "Template Inheritance" do - - class TestFileSystem - def read_template_file(template_path) - case template_path - when "parent-template" - "Hurrah!" - when "parent-with-variable" - "Hello, {{ name }}!" - when "parent-with-parent" - "{% extends parent-with-variable %}" - else - raise "TestFileSystem Error: No template defined for #{template_path}" - end - end + before(:each) do + @templates ||= {} end before(:each) do - Liquid::Template.file_system = TestFileSystem.new + Liquid::Template.file_system = self + end + + def read_template_file(template_path) + @template_path ||= {} + @templates[template_path] || raise("TestFileSystem Error: No template defined for #{template_path}") end it "should allow extending a path" do + @templates['parent-template'] = "Hurrah!" + template = Liquid::Template.parse "{% extends parent-template %}" template.render.should == "Hurrah!" end @@ -34,15 +29,44 @@ def read_template_file(template_path) # end it "should allow access to the context from the inherited template" do + @templates['parent-with-variable'] = "Hello, {{ name }}!" + template = Liquid::Template.parse "{% extends parent-with-variable %}" template.render('name' => 'Joe').should == "Hello, Joe!" end - it "show allow deep nesting of inherited templates" do + it "should allow deep nesting of inherited templates" do + @templates['parent-with-variable'] = "Hello, {{ name }}!!" + @templates['parent-with-parent'] = "{% extends parent-with-variable %}" + template = Liquid::Template.parse "{% extends parent-with-parent %}" - template.render('name' => 'Joe').should == "Hello, Joe!" + template.render('name' => 'Joe').should == "Hello, Joe!!" end + context "inherited blocks" do + before(:each) do + @templates['base'] = "Output / {% block content %}Hello, World!{% endblock %}" + end + + it "should allow overriding blocks from an inherited template" do + output = render("{% extends base %}{% block content %}Hola, Mundo!{% endblock %}") + output.should == 'Output / Hola, Mundo!' + end + + it "should allow an overriding block to call super" do + output = render("{% extends base %}{% block content %}Lorem ipsum: {{block.super}}{% endblock %}") + output.should == 'Output / Lorem ipsum: Hello, World!' + end + + it "should allow deep nested includes to call super within overriden blocks" do + @templates['deep'] = "{% extends base %}{% block content %}Deep: {{block.super}}{% endblock %}" + output = render("{% extends deep %}{% block content %}Lorem ipsum: {{block.super}}{% endblock %}") + output.should == 'Output / Lorem ipsum: Deep: Hello, World!' + @templates['nested_and_deep'] = "{% extends base %}{% block content %}Deep: {{block.super}} -{% block inner %}FOO{% endblock %}-{% endblock %}" + output = render("{% extends nested_and_deep %}{% block inner %}BAR{% endblock %}") + output.should == 'Output / Deep: Hello, World! -BAR-' + end + end end end \ No newline at end of file diff --git a/spec/rendering/include_spec.rb b/spec/rendering/include_spec.rb new file mode 100644 index 000000000..add007760 --- /dev/null +++ b/spec/rendering/include_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +class OtherFileSystem + def read_template_file(template_path) + 'from OtherFileSystem' + end +end + +describe "Liquid Rendering" do + describe "include tag" do + + before(:each) do + Liquid::Template.file_system = self + end + + before(:each) do + @templates ||= {} + @templates['product'] = "Product: {{ product.title }} " + @templates['locale_variables'] = "Locale: {{echo1}} {{echo2}} " + @templates['variant'] = "Variant: {{ variant.title }} " + @templates['nested_template'] = "{% include 'header' %} {% include 'body' %} {% include 'footer' %}" + @templates['body'] = "body {% include 'body_detail' %}" + @templates['nested_product_template'] = "Product: {{ nested_product_template.title }} {%include 'details'%} " + @templates['recursively_nested_template'] = "-{% include 'recursively_nested_template' %}" + @templates['pick_a_source'] = "from TestFileSystem" + end + + def read_template_file(template_path) + @template_path ||= {} + @templates[template_path] || template_path + end + + it "should look for file system in registers first" do + registers = {:registers => {:file_system => OtherFileSystem.new}} + render("{% include 'pick_a_source' %}", {}, registers).should == "from OtherFileSystem" + end + + it "should take a with option" do + data = {"products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ]} + render("{% include 'product' with products[0] %}", data).should == "Product: Draft 151cm " + end + + it "should use a default name" do + data = {"product" => {'title' => 'Draft 151cm'}} + render("{% include 'product' %}", data).should == "Product: Draft 151cm " + end + + it "should allow cycling through a collection with the 'for' keyword" do + data = {"products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ]} + render("{% include 'product' for products %}") + end + + it "should allow passing local variables" do + # one variable + render("{% include 'locale_variables' echo1: 'test123' %}").should == "Locale: test123 " + + # multiple variables + data = {'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'}} + render("{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}", data).should == "Locale: test123 test321 " + + end + + it "should allow nested includes" do + render("{% include 'body' %}").should == "body body_detail" + render("{% include 'nested_template' %}").should == "header body body_detail footer" + end + + it "should allow nested includes with a variable" do + data = {"product" => {"title" => 'Draft 151cm'}} + render("{% include 'nested_product_template' with product %}", data).should == "Product: Draft 151cm details " + + data = {"products" => [{"title" => 'Draft 151cm'}, {"title" => 'Element 155cm'}]} + render("{% include 'nested_product_template' for products %}", data).should == "Product: Draft 151cm details Product: Element 155cm details " + end + + it "should raise an error if there's an endless loop" do + infinite_file_system = Class.new do + def read_template_file(template_path) + "-{% include 'loop' %}" + end + end + + Liquid::Template.file_system = infinite_file_system.new + + expect { + render!("{% include 'loop' %}") + }.to raise_error(Liquid::StackLevelError) + end + + it "should allow dynamically choosing templates" do + render("{% include template %}", "template" => 'Test123').should == "Test123" + render("{% include template %}", "template" => 'Test321').should == "Test321" + + data = {"template" => 'product', 'product' => { 'title' => 'Draft 151cm'}} + render("{% include template for product %}", data).should == "Product: Draft 151cm " + end + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d50cdfa69..43fa6bb64 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -20,8 +20,12 @@ module Liquid module RenderSpecHelper - def render(body, data = {}) - Liquid::Template.parse(body).render(data) + def render(body, *args) + Liquid::Template.parse(body).render(*args) + end + + def render!(body, *args) + Liquid::Template.parse(body).render!(*args) end end end diff --git a/test/include_tag_test.rb b/test/include_tag_test.rb deleted file mode 100644 index f78b57886..000000000 --- a/test/include_tag_test.rb +++ /dev/null @@ -1,129 +0,0 @@ -require File.dirname(__FILE__) + '/helper' - -class TestFileSystem - def read_template_file(template_path) - case template_path - when "product" - "Product: {{ product.title }} " - - when "locale_variables" - "Locale: {{echo1}} {{echo2}}" - - when "variant" - "Variant: {{ variant.title }}" - - when "nested_template" - "{% include 'header' %} {% include 'body' %} {% include 'footer' %}" - - when "body" - "body {% include 'body_detail' %}" - - when "nested_product_template" - "Product: {{ nested_product_template.title }} {%include 'details'%} " - - when "recursively_nested_template" - "-{% include 'recursively_nested_template' %}" - - when "pick_a_source" - "from TestFileSystem" - - else - template_path - end - end -end - -class OtherFileSystem - def read_template_file(template_path) - 'from OtherFileSystem' - end -end - -class IncludeTagTest < Test::Unit::TestCase - include Liquid - - def setup - Liquid::Template.file_system = TestFileSystem.new - end - - def test_include_tag_looks_for_file_system_in_registers_first - assert_equal 'from OtherFileSystem', - Template.parse("{% include 'pick_a_source' %}").render({}, :registers => {:file_system => OtherFileSystem.new}) - end - - - def test_include_tag_with - assert_equal "Product: Draft 151cm ", - Template.parse("{% include 'product' with products[0] %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] ) - end - - def test_include_tag_with_default_name - assert_equal "Product: Draft 151cm ", - Template.parse("{% include 'product' %}").render( "product" => {'title' => 'Draft 151cm'} ) - end - - def test_include_tag_for - - assert_equal "Product: Draft 151cm Product: Element 155cm ", - Template.parse("{% include 'product' for products %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] ) - end - - def test_include_tag_with_local_variables - assert_equal "Locale: test123 ", - Template.parse("{% include 'locale_variables' echo1: 'test123' %}").render - end - - def test_include_tag_with_multiple_local_variables - assert_equal "Locale: test123 test321", - Template.parse("{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}").render - end - - def test_include_tag_with_multiple_local_variables_from_context - assert_equal "Locale: test123 test321", - Template.parse("{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}").render('echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'}) - end - - def test_nested_include_tag - assert_equal "body body_detail", - Template.parse("{% include 'body' %}").render - - assert_equal "header body body_detail footer", - Template.parse("{% include 'nested_template' %}").render - end - - def test_nested_include_with_variable - - assert_equal "Product: Draft 151cm details ", - Template.parse("{% include 'nested_product_template' with product %}").render("product" => {"title" => 'Draft 151cm'}) - - assert_equal "Product: Draft 151cm details Product: Element 155cm details ", - Template.parse("{% include 'nested_product_template' for products %}").render("products" => [{"title" => 'Draft 151cm'}, {"title" => 'Element 155cm'}]) - - end - - def test_recursively_included_template_does_not_produce_endless_loop - - infinite_file_system = Class.new do - def read_template_file(template_path) - "-{% include 'loop' %}" - end - end - - Liquid::Template.file_system = infinite_file_system.new - - assert_raise(Liquid::StackLevelError) do - Template.parse("{% include 'loop' %}").render! - end - - end - - def test_dynamically_choosen_template - - assert_equal "Test123", Template.parse("{% include template %}").render("template" => 'Test123') - assert_equal "Test321", Template.parse("{% include template %}").render("template" => 'Test321') - - assert_equal "Product: Draft 151cm ", Template.parse("{% include template for product %}").render("template" => 'product', 'product' => { 'title' => 'Draft 151cm'}) - - end - -end diff --git a/test/inherited_block_test.rb b/test/inherited_block_test.rb deleted file mode 100644 index 8f2e5cd9c..000000000 --- a/test/inherited_block_test.rb +++ /dev/null @@ -1,46 +0,0 @@ -class BlockTagTest < Test::Unit::TestCase - include Liquid - - class TestFileSystem - def read_template_file(template_path) - case template_path - when "base" - "Output / {% block content %}Hello, World!{% endblock %}" - when "deep" - "{% extends base %}{% block content %}Deep: {{block.super}}{% endblock %}" - when "nested_and_deep" - "{% extends base %}{% block content %}Deep: {{block.super}} -{% block inner %}FOO{% endblock %}-{% endblock %}" - else - template_path - end - end - end - - def setup - Liquid::Template.file_system = TestFileSystem.new - end - - def test_extends - document = Template.parse("{% extends base %}{% block content %}Hola, Mundo!{% endblock %}") - rendered = document.render({}) - assert_equal 'Output / Hola, Mundo!', rendered - end - - def test_block_super - document = Template.parse("{% extends base %}{% block content %}Lorem ipsum: {{block.super}}{% endblock %}") - rendered = document.render({}) - assert_equal 'Output / Lorem ipsum: Hello, World!', rendered - end - - def test_deep_block_super - document = Template.parse("{% extends deep %}{% block content %}Lorem ipsum: {{block.super}}{% endblock %}") - rendered = document.render({}) - assert_equal 'Output / Lorem ipsum: Deep: Hello, World!', rendered - end - - def test_nested_deep_blocks - document = Template.parse("{% extends nested_and_deep %}{% block inner %}BAR{% endblock %}") - rendered = document.render({}) - assert_equal 'Output / Deep: Hello, World! -BAR-', rendered - end -end \ No newline at end of file From 57149f97be977209f4838ea35aae60137570bb59 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 06:21:43 -0700 Subject: [PATCH 30/61] Converting some additional output specs --- .../{money_filters.rb => money_filter.rb} | 0 spec/rendering/filter_spec.rb | 2 +- spec/rendering/output_spec.rb | 110 ++++++++++++++++++ spec/unit/liquid_methods_spec.rb | 89 ++++++++++++++ test/module_ex_test.rb | 89 -------------- 5 files changed, 200 insertions(+), 90 deletions(-) rename spec/fixtures/filters/{money_filters.rb => money_filter.rb} (100%) create mode 100644 spec/rendering/output_spec.rb create mode 100644 spec/unit/liquid_methods_spec.rb delete mode 100644 test/module_ex_test.rb diff --git a/spec/fixtures/filters/money_filters.rb b/spec/fixtures/filters/money_filter.rb similarity index 100% rename from spec/fixtures/filters/money_filters.rb rename to spec/fixtures/filters/money_filter.rb diff --git a/spec/rendering/filter_spec.rb b/spec/rendering/filter_spec.rb index 78390a37e..f1018ccf0 100644 --- a/spec/rendering/filter_spec.rb +++ b/spec/rendering/filter_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -require 'filters/money_filters' +require 'filters/money_filter' describe "Liquid Rendering" do describe "Filters" do diff --git a/spec/rendering/output_spec.rb b/spec/rendering/output_spec.rb new file mode 100644 index 000000000..4433e776b --- /dev/null +++ b/spec/rendering/output_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' + + + +describe "Liquid Rendering" do + describe "Basic Output" do + + let(:filters) do + {:filters => [FunnyFilter, HtmlFilter]} + end + + let(:data) do + { + 'best_cars' => 'bmw', + 'car' => {'bmw' => 'good', 'gm' => 'bad'} + } + end + + def render(text) + super(text, data, filters) + end + + it "should render a variable's value" do + render(' {{best_cars}} ').should == " bmw " + end + + it "should render a traversed variable's value" do + render(' {{car.bmw}} {{car.gm}} {{car.bmw}} ').should == " good bad good " + end + + module FunnyFilter + def make_funny(input) + 'LOL' + end + end + + it "should allow piping to activate filters" do + render(' {{ car.gm | make_funny }} ').should == ' LOL ' + end + + module FunnyFilter + def cite_funny(input) + "LOL: #{input}" + end + end + + it "should allow filters to read the input" do + render(' {{ car.gm | cite_funny }} ').should == " LOL: bad " + end + + module FunnyFilter + def add_smiley(input, smiley = ":-)") + "#{input} #{smiley}" + end + end + + it "should allow filters to take in parameters" do + render(' {{ car.gm | add_smiley: ":-(" }} ').should == + ' bad :-( ' + + render(' {{ car.gm | add_smiley : ":-(" }} ').should == + ' bad :-( ' + + render(' {{ car.gm | add_smiley: \':-(\' }} ').should == + ' bad :-( ' + end + + it "should allow filters with no parameters and a default argument" do + render(' {{ car.gm | add_smiley }} ').should == + ' bad :-) ' + end + + it "should allow multiple filters with parameters" do + render(' {{ car.gm | add_smiley : ":-(" | add_smiley : ":-(" }} ').should == + ' bad :-( :-( ' + end + + module FunnyFilter + def add_tag(input, tag = "p", id = "foo") + %|<#{tag} id="#{id}">#{input}| + end + end + + it "should allow filters with multiple parameters" do + render(' {{ car.gm | add_tag : "span", "bar"}} ').should == + ' bad ' + end + + it "should allow filters with variable parameters" do + render(' {{ car.gm | add_tag : "span", car.bmw }} ').should == + ' bad ' + end + + module HtmlFilter + def paragraph(input) + "

#{input}

" + end + + def link_to(name, url) + %|#{name}| + end + end + + it "should allow multiple chained filters" do + render(' {{ best_cars | cite_funny | link_to: "http://www.google.com" | paragraph }} ').should == + '

LOL: bmw

' + end + + end +end \ No newline at end of file diff --git a/spec/unit/liquid_methods_spec.rb b/spec/unit/liquid_methods_spec.rb new file mode 100644 index 000000000..994de2dc2 --- /dev/null +++ b/spec/unit/liquid_methods_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe "Liquid Methods" do + + class TestClassA + liquid_methods :allowedA, :chainedB + def allowedA + 'allowedA' + end + def restrictedA + 'restrictedA' + end + def chainedB + TestClassB.new + end + end + + class TestClassB + liquid_methods :allowedB, :chainedC + def allowedB + 'allowedB' + end + def chainedC + TestClassC.new + end + end + + class TestClassC + liquid_methods :allowedC + def allowedC + 'allowedC' + end + end + + class TestClassC::LiquidDropClass + def another_allowedC + 'another_allowedC' + end + end + + + before(:each) do + @a = TestClassA.new + @b = TestClassB.new + @c = TestClassC.new + end + + it "should create liquid drop classes" do + TestClassA::LiquidDropClass.should_not be_nil + TestClassB::LiquidDropClass.should_not be_nil + TestClassC::LiquidDropClass.should_not be_nil + end + + it "should respond to to_liquid" do + @a.should respond_to(:to_liquid) + @b.should respond_to(:to_liquid) + @c.should respond_to(:to_liquid) + end + + it "should return the liquid drop class" do + @a.to_liquid.should be_an_instance_of(TestClassA::LiquidDropClass) + @b.to_liquid.should be_an_instance_of(TestClassB::LiquidDropClass) + @c.to_liquid.should be_an_instance_of(TestClassC::LiquidDropClass) + end + + it "should respond to liquid methods" do + @a.to_liquid.should respond_to(:allowedA) + @a.to_liquid.should respond_to(:chainedB) + + @b.to_liquid.should respond_to(:allowedB) + @b.to_liquid.should respond_to(:chainedC) + + @c.to_liquid.should respond_to(:allowedC) + @c.to_liquid.should respond_to(:another_allowedC) + end + + it "should not respond to restricted methods" do + @a.to_liquid.should_not respond_to(:restricted) + end + + it "should use regular objects as drops" do + render('{{ a.allowedA }}', 'a' => @a).should == "allowedA" + render("{{ a.chainedB.allowedB }}", 'a'=>@a).should == 'allowedB' + render("{{ a.chainedB.chainedC.allowedC }}", 'a'=>@a).should == 'allowedC' + render("{{ a.chainedB.chainedC.another_allowedC }}", 'a'=>@a).should == 'another_allowedC' + render("{{ a.restricted }}", 'a'=>@a).should == '' + render("{{ a.unknown }}", 'a'=>@a).should == '' + end +end \ No newline at end of file diff --git a/test/module_ex_test.rb b/test/module_ex_test.rb deleted file mode 100644 index 40d7e1c4f..000000000 --- a/test/module_ex_test.rb +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env ruby -require File.dirname(__FILE__) + '/helper' - -class TestClassA - liquid_methods :allowedA, :chainedB - def allowedA - 'allowedA' - end - def restrictedA - 'restrictedA' - end - def chainedB - TestClassB.new - end -end - -class TestClassB - liquid_methods :allowedB, :chainedC - def allowedB - 'allowedB' - end - def chainedC - TestClassC.new - end -end - -class TestClassC - liquid_methods :allowedC - def allowedC - 'allowedC' - end -end - -class TestClassC::LiquidDropClass - def another_allowedC - 'another_allowedC' - end -end - -class ModuleExTest < Test::Unit::TestCase - include Liquid - - def setup - @a = TestClassA.new - @b = TestClassB.new - @c = TestClassC.new - end - - def test_should_create_LiquidDropClass - assert TestClassA::LiquidDropClass - assert TestClassB::LiquidDropClass - assert TestClassC::LiquidDropClass - end - - def test_should_respond_to_liquid - assert @a.respond_to?(:to_liquid) - assert @b.respond_to?(:to_liquid) - assert @c.respond_to?(:to_liquid) - end - - def test_should_return_LiquidDropClass_object - assert @a.to_liquid.is_a?(TestClassA::LiquidDropClass) - assert @b.to_liquid.is_a?(TestClassB::LiquidDropClass) - assert @c.to_liquid.is_a?(TestClassC::LiquidDropClass) - end - - def test_should_respond_to_liquid_methods - assert @a.to_liquid.respond_to?(:allowedA) - assert @a.to_liquid.respond_to?(:chainedB) - assert @b.to_liquid.respond_to?(:allowedB) - assert @b.to_liquid.respond_to?(:chainedC) - assert @c.to_liquid.respond_to?(:allowedC) - assert @c.to_liquid.respond_to?(:another_allowedC) - end - - def test_should_not_respond_to_restricted_methods - assert ! @a.to_liquid.respond_to?(:restricted) - end - - def test_should_use_regular_objects_as_drops - assert_equal 'allowedA', Liquid::Template.parse("{{ a.allowedA }}").render('a'=>@a) - assert_equal 'allowedB', Liquid::Template.parse("{{ a.chainedB.allowedB }}").render('a'=>@a) - assert_equal 'allowedC', Liquid::Template.parse("{{ a.chainedB.chainedC.allowedC }}").render('a'=>@a) - assert_equal 'another_allowedC', Liquid::Template.parse("{{ a.chainedB.chainedC.another_allowedC }}").render('a'=>@a) - assert_equal '', Liquid::Template.parse("{{ a.restricted }}").render('a'=>@a) - assert_equal '', Liquid::Template.parse("{{ a.unknown }}").render('a'=>@a) - end - -end \ No newline at end of file From 4e40056a80b935976e2f4185a575ff667c1e0428 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 07:24:24 -0700 Subject: [PATCH 31/61] reorganize specs --- .../{rendering => integration}/assign_spec.rb | 0 .../capture_spec.rb | 0 spec/{rendering => integration}/drop_spec.rb | 0 .../error_handling_spec.rb | 0 .../extends_spec.rb | 0 .../{rendering => integration}/filter_spec.rb | 0 .../if_else_unless_spec.rb} | 40 +- .../include_spec.rb | 0 .../{rendering => integration}/output_spec.rb | 2 - spec/{rendering => integration}/table_spec.rb | 0 spec/integration/variable_spec.rb | 64 ++ spec/unit/condition_spec.rb | 215 +++--- spec/unit/context_spec.rb | 720 +++++++++--------- spec/unit/file_system_spec.rb | 11 +- spec/unit/regexp_spec.rb | 75 ++ spec/unit/strainer_spec.rb | 25 + spec/unit/template_spec.rb | 0 spec/unit/variable_spec.rb | 104 +++ test/output_test.rb | 121 --- test/regexp_test.rb | 45 -- test/strainer_test.rb | 21 - test/unless_else_test.rb | 27 - test/variable_test.rb | 172 ----- 23 files changed, 779 insertions(+), 863 deletions(-) rename spec/{rendering => integration}/assign_spec.rb (100%) rename spec/{rendering => integration}/capture_spec.rb (100%) rename spec/{rendering => integration}/drop_spec.rb (100%) rename spec/{rendering => integration}/error_handling_spec.rb (100%) rename spec/{rendering => integration}/extends_spec.rb (100%) rename spec/{rendering => integration}/filter_spec.rb (100%) rename spec/{rendering/if_else_spec.rb => integration/if_else_unless_spec.rb} (84%) rename spec/{rendering => integration}/include_spec.rb (100%) rename spec/{rendering => integration}/output_spec.rb (99%) rename spec/{rendering => integration}/table_spec.rb (100%) create mode 100644 spec/integration/variable_spec.rb create mode 100644 spec/unit/regexp_spec.rb create mode 100644 spec/unit/strainer_spec.rb create mode 100644 spec/unit/template_spec.rb create mode 100644 spec/unit/variable_spec.rb delete mode 100644 test/output_test.rb delete mode 100644 test/regexp_test.rb delete mode 100644 test/strainer_test.rb delete mode 100644 test/unless_else_test.rb delete mode 100644 test/variable_test.rb diff --git a/spec/rendering/assign_spec.rb b/spec/integration/assign_spec.rb similarity index 100% rename from spec/rendering/assign_spec.rb rename to spec/integration/assign_spec.rb diff --git a/spec/rendering/capture_spec.rb b/spec/integration/capture_spec.rb similarity index 100% rename from spec/rendering/capture_spec.rb rename to spec/integration/capture_spec.rb diff --git a/spec/rendering/drop_spec.rb b/spec/integration/drop_spec.rb similarity index 100% rename from spec/rendering/drop_spec.rb rename to spec/integration/drop_spec.rb diff --git a/spec/rendering/error_handling_spec.rb b/spec/integration/error_handling_spec.rb similarity index 100% rename from spec/rendering/error_handling_spec.rb rename to spec/integration/error_handling_spec.rb diff --git a/spec/rendering/extends_spec.rb b/spec/integration/extends_spec.rb similarity index 100% rename from spec/rendering/extends_spec.rb rename to spec/integration/extends_spec.rb diff --git a/spec/rendering/filter_spec.rb b/spec/integration/filter_spec.rb similarity index 100% rename from spec/rendering/filter_spec.rb rename to spec/integration/filter_spec.rb diff --git a/spec/rendering/if_else_spec.rb b/spec/integration/if_else_unless_spec.rb similarity index 84% rename from spec/rendering/if_else_spec.rb rename to spec/integration/if_else_unless_spec.rb index 9174225b9..5953f6dab 100644 --- a/spec/rendering/if_else_spec.rb +++ b/spec/integration/if_else_unless_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' describe "Liquid Rendering" do - describe "If/Else" do + describe "If/Else/Unless" do - describe "If" do + describe "if" do it "should show/hide content correctly when passed a boolean constant" do render(' {% if false %} this text should not go into the output {% endif %} ').should == " " render(' {% if true %} this text should not go into the output {% endif %} ').should == " this text should not go into the output " @@ -142,7 +142,7 @@ end end - describe "Else" do + describe "if/else" do it "should render the right block based on the input" do render('{% if false %} NO {% else %} YES {% endif %}').should == " YES " render('{% if true %} YES {% else %} NO {% endif %}').should == " YES " @@ -157,6 +157,40 @@ end end + describe "unless" do + it "should show/hide content correctly when passed a boolean constant" do + render(' {% unless true %} this text should not go into the output {% endunless %} ').should == + ' ' + + render(' {% unless false %} this text should go into the output {% endunless %} ').should == + ' this text should go into the output ' + + render('{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?').should == + ' you rock ?' + + end + + it "should work within a loop" do + data = {'choices' => [1, nil, false]} + render('{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', data).should == '23' + end + + end + + describe "unless/else" do + it "should show/hide the section based on the passed in data" do + render('{% unless true %} NO {% else %} YES {% endunless %}').should == ' YES ' + render('{% unless false %} YES {% else %} NO {% endunless %}').should == ' YES ' + render('{% unless "foo" %} NO {% else %} YES {% endunless %}').should == ' YES ' + end + + it "should work within a loop" do + data = {'choices' => [1, nil, false]} + render('{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', data).should == + ' TRUE 2 3 ' + end + end + end end \ No newline at end of file diff --git a/spec/rendering/include_spec.rb b/spec/integration/include_spec.rb similarity index 100% rename from spec/rendering/include_spec.rb rename to spec/integration/include_spec.rb diff --git a/spec/rendering/output_spec.rb b/spec/integration/output_spec.rb similarity index 99% rename from spec/rendering/output_spec.rb rename to spec/integration/output_spec.rb index 4433e776b..21b9e445d 100644 --- a/spec/rendering/output_spec.rb +++ b/spec/integration/output_spec.rb @@ -1,7 +1,5 @@ require 'spec_helper' - - describe "Liquid Rendering" do describe "Basic Output" do diff --git a/spec/rendering/table_spec.rb b/spec/integration/table_spec.rb similarity index 100% rename from spec/rendering/table_spec.rb rename to spec/integration/table_spec.rb diff --git a/spec/integration/variable_spec.rb b/spec/integration/variable_spec.rb new file mode 100644 index 000000000..a541cfe81 --- /dev/null +++ b/spec/integration/variable_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "Variables" do + + it "should render simple variables" do + render('{{test}}', 'test' => 'worked').should == "worked" + render('{{test}}', 'test' => 'worked wonderfully').should == 'worked wonderfully' + end + + it "should render variables with whitespace" do + render(' {{ test }} ', 'test' => 'worked').should == ' worked ' + render(' {{ test }} ', 'test' => 'worked wonderfully').should == ' worked wonderfully ' + end + + it "should ignore unknown variables" do + render('{{ idontexistyet }}').should == "" + end + + it "should scope hash variables" do + data = {'test' => {'test' => 'worked'}} + render('{{ test.test }}', data).should == "worked" + end + + it "should render preset assigned variables" do + template = Liquid::Template.parse("{{ test }}") + template.assigns['test'] = 'worked' + template.render.should == "worked" + end + + it "should reuse parsed template" do + template = Liquid::Template.parse("{{ greeting }} {{ name }}") + template.assigns['greeting'] = 'Goodbye' + template.render('greeting' => 'Hello', 'name' => 'Tobi').should == 'Hello Tobi' + template.render('greeting' => 'Hello', 'unknown' => 'Tobi').should == 'Hello ' + template.render('greeting' => 'Hello', 'name' => 'Brian').should == 'Hello Brian' + template.render('name' => 'Brian').should == 'Goodbye Brian' + + template.assigns.should == {'greeting' => 'Goodbye'} + end + + it "should not get polluted with assignments from templates" do + template = Liquid::Template.parse(%|{{ test }}{% assign test = 'bar' %}{{ test }}|) + template.assigns['test'] = 'baz' + template.render.should == 'bazbar' + template.render.should == 'bazbar' + template.render('test' => 'foo').should == 'foobar' + template.render.should == 'bazbar' + end + + it "should allow a hash with a default proc" do + template = Liquid::Template.parse(%|Hello {{ test }}|) + assigns = Hash.new { |h,k| raise "Unknown variable '#{k}'" } + assigns['test'] = 'Tobi' + + template.render!(assigns).should == 'Hello Tobi' + assigns.delete('test') + + expect{ + template.render!(assigns) + }.to raise_error(RuntimeError, "Unknown variable 'test'") + end + end +end \ No newline at end of file diff --git a/spec/unit/condition_spec.rb b/spec/unit/condition_spec.rb index 3a8eb054d..08d3e1317 100644 --- a/spec/unit/condition_spec.rb +++ b/spec/unit/condition_spec.rb @@ -1,138 +1,139 @@ require 'spec_helper' -describe Liquid::Condition do - before(:each) do - @context = Liquid::Context.new - end - - # simple wrapper around CheckCondition evaluate - def check_condition(*args) - Liquid::Condition.new(*args).evaluate(@context) - end - - - it "should check basic equality conditions" do - check_condition("1", "==", "2").should be_false - check_condition("1", "==", "1").should be_true - end - - context "Default Operators (==, !=, <>, <, >, >=, <=)" do - it "should evaluate true when appropriate" do - check_condition('1', '==', '1').should be_true - check_condition('1', '!=', '2').should be_true - check_condition('1', '<>', '2').should be_true - check_condition('1', '<', '2').should be_true - check_condition('2', '>', '1').should be_true - check_condition('1', '>=', '1').should be_true - check_condition('2', '>=', '1').should be_true - check_condition('1', '<=', '2').should be_true - check_condition('1', '<=', '1').should be_true +module Liquid + describe Condition do + before(:each) do + @context = Context.new end - it "should evaluate false when appropriate" do - check_condition('1', '==', '2').should be_false - check_condition('1', '!=', '1').should be_false - check_condition('1', '<>', '1').should be_false - check_condition('1', '<', '0').should be_false - check_condition('2', '>', '4').should be_false - check_condition('1', '>=', '3').should be_false - check_condition('2', '>=', '4').should be_false - check_condition('1', '<=', '0').should be_false - check_condition('1', '<=', '0').should be_false + # simple wrapper around CheckCondition evaluate + def check_condition(*args) + Condition.new(*args).evaluate(@context) end - end - context %{"contains"} do - context "when operating on strings" do - it "should evaluate to true when appropriate" do - check_condition("'bob'", 'contains', "'o'").should be_true - check_condition("'bob'", 'contains', "'b'").should be_true - check_condition("'bob'", 'contains', "'bo'").should be_true - check_condition("'bob'", 'contains', "'ob'").should be_true - check_condition("'bob'", 'contains', "'bob'").should be_true - end - - it "should evaluate to false when appropriate" do - check_condition("'bob'", 'contains', "'bob2'").should be_false - check_condition("'bob'", 'contains', "'a'").should be_false - check_condition("'bob'", 'contains', "'---'").should be_false - end + it "should check basic equality conditions" do + check_condition("1", "==", "2").should be_false + check_condition("1", "==", "1").should be_true end - context "when operating on arrays" do - before(:each) do - @context['array'] = [1,2,3,4,5] + context "Default Operators (==, !=, <>, <, >, >=, <=)" do + it "should evaluate true when appropriate" do + check_condition('1', '==', '1').should be_true + check_condition('1', '!=', '2').should be_true + check_condition('1', '<>', '2').should be_true + check_condition('1', '<', '2').should be_true + check_condition('2', '>', '1').should be_true + check_condition('1', '>=', '1').should be_true + check_condition('2', '>=', '1').should be_true + check_condition('1', '<=', '2').should be_true + check_condition('1', '<=', '1').should be_true end - it "should evaluate to true when appropriate" do - check_condition("array", "contains", "1").should be_true - check_condition("array", "contains", "2").should be_true - check_condition("array", "contains", "3").should be_true - check_condition("array", "contains", "4").should be_true - check_condition("array", "contains", "5").should be_true + it "should evaluate false when appropriate" do + check_condition('1', '==', '2').should be_false + check_condition('1', '!=', '1').should be_false + check_condition('1', '<>', '1').should be_false + check_condition('1', '<', '0').should be_false + check_condition('2', '>', '4').should be_false + check_condition('1', '>=', '3').should be_false + check_condition('2', '>=', '4').should be_false + check_condition('1', '<=', '0').should be_false + check_condition('1', '<=', '0').should be_false end + end - it "should evaluate to false when appropriate" do - check_condition("array", "contains", "0").should be_false - check_condition("array", "contains", "6").should be_false + context %{"contains"} do + + context "when operating on strings" do + it "should evaluate to true when appropriate" do + check_condition("'bob'", 'contains', "'o'").should be_true + check_condition("'bob'", 'contains', "'b'").should be_true + check_condition("'bob'", 'contains', "'bo'").should be_true + check_condition("'bob'", 'contains', "'ob'").should be_true + check_condition("'bob'", 'contains', "'bob'").should be_true + end + + it "should evaluate to false when appropriate" do + check_condition("'bob'", 'contains', "'bob2'").should be_false + check_condition("'bob'", 'contains', "'a'").should be_false + check_condition("'bob'", 'contains', "'---'").should be_false + end end - it "should not equate strings to integers" do - check_condition("array", "contains", "5").should be_true - check_condition("array", "contains", "'5'").should be_false + context "when operating on arrays" do + before(:each) do + @context['array'] = [1,2,3,4,5] + end + + it "should evaluate to true when appropriate" do + check_condition("array", "contains", "1").should be_true + check_condition("array", "contains", "2").should be_true + check_condition("array", "contains", "3").should be_true + check_condition("array", "contains", "4").should be_true + check_condition("array", "contains", "5").should be_true + end + + it "should evaluate to false when appropriate" do + check_condition("array", "contains", "0").should be_false + check_condition("array", "contains", "6").should be_false + end + + it "should not equate strings to integers" do + check_condition("array", "contains", "5").should be_true + check_condition("array", "contains", "'5'").should be_false + end end - end - it "should return false for all nil operands" do - check_condition("not_assigned", "contains", "0").should be_false - check_condition("0", "contains", "not_assigned").should be_false + it "should return false for all nil operands" do + check_condition("not_assigned", "contains", "0").should be_false + check_condition("0", "contains", "not_assigned").should be_false + end end - end - describe %{Chaining with "or"} do - before(:each) do - @condition = Liquid::Condition.new("1", "==", "2") - @condition.evaluate.should be_false - end + describe %{Chaining with "or"} do + before(:each) do + @condition = Condition.new("1", "==", "2") + @condition.evaluate.should be_false + end - it "should return true when it you add a single condition that evaluates to true" do - @condition.or Liquid::Condition.new("2", "==", "1") - @condition.evaluate.should be_false + it "should return true when it you add a single condition that evaluates to true" do + @condition.or Condition.new("2", "==", "1") + @condition.evaluate.should be_false - @condition.or Liquid::Condition.new("1", "==", "1") - @condition.evaluate.should be_true + @condition.or Condition.new("1", "==", "1") + @condition.evaluate.should be_true + end end - end - describe %{Chaining with "and"} do - before(:each) do - @condition = Liquid::Condition.new("1", "==", "1") - @condition.evaluate.should be_true - end + describe %{Chaining with "and"} do + before(:each) do + @condition = Condition.new("1", "==", "1") + @condition.evaluate.should be_true + end - it "should return false when it you add a single condition that evaluates to false" do - @condition.and Liquid::Condition.new("2", "==", "2") - @condition.evaluate.should be_true + it "should return false when it you add a single condition that evaluates to false" do + @condition.and Condition.new("2", "==", "2") + @condition.evaluate.should be_true - @condition.and Liquid::Condition.new("2", "==", "1") - @condition.evaluate.should be_false + @condition.and Condition.new("2", "==", "1") + @condition.evaluate.should be_false + end end - end - describe "Custom proc operator" do - before(:each) do - Liquid::Condition.operators["starts_with"] = Proc.new { |cond, left, right| left =~ %r{^#{right}}} - end + describe "Custom proc operator" do + before(:each) do + Condition.operators["starts_with"] = Proc.new { |cond, left, right| left =~ %r{^#{right}}} + end - it "should use the assigned proc to evalue the operator" do - check_condition("'bob'", "starts_with", "'b'").should be_true - check_condition("'bob'", "starts_with", "'o'").should be_false - end + it "should use the assigned proc to evalue the operator" do + check_condition("'bob'", "starts_with", "'b'").should be_true + check_condition("'bob'", "starts_with", "'o'").should be_false + end - after(:each) do - Liquid::Condition.operators.delete('starts_with') + after(:each) do + Condition.operators.delete('starts_with') + end end end - -end +end \ No newline at end of file diff --git a/spec/unit/context_spec.rb b/spec/unit/context_spec.rb index c1ac67bcb..08172613a 100644 --- a/spec/unit/context_spec.rb +++ b/spec/unit/context_spec.rb @@ -1,477 +1,479 @@ require 'spec_helper' -describe Liquid::Context do +module Liquid + describe Context do - before(:each) do - @context = Liquid::Context.new - end + before(:each) do + @context = Context.new + end - it "should allow assigning variables" do - @context['string'] = 'string' - @context['string'].should == 'string' + it "should allow assigning variables" do + @context['string'] = 'string' + @context['string'].should == 'string' - @context['num'] = 5 - @context['num'].should == 5 + @context['num'] = 5 + @context['num'].should == 5 - @context['time'] = Time.parse('2006-06-06 12:00:00') - @context['time'].should == Time.parse('2006-06-06 12:00:00') + @context['time'] = Time.parse('2006-06-06 12:00:00') + @context['time'].should == Time.parse('2006-06-06 12:00:00') - @context['date'] = Date.today - @context['date'].should == Date.today + @context['date'] = Date.today + @context['date'].should == Date.today - now = DateTime.now - @context['datetime'] = now - @context['datetime'].should == now + now = DateTime.now + @context['datetime'] = now + @context['datetime'].should == now - @context['bool'] = true - @context['bool'].should == true + @context['bool'] = true + @context['bool'].should == true - @context['bool'] = false - @context['bool'].should == false + @context['bool'] = false + @context['bool'].should == false - @context['nil'] = nil - @context['nil'].should == nil - end + @context['nil'] = nil + @context['nil'].should == nil + end - it "should return nil for variables that don't exist" do - @context["does_not_exist"].should == nil - end + it "should return nil for variables that don't exist" do + @context["does_not_exist"].should == nil + end - it "should return the size of an array" do - @context['numbers'] = [1,2,3,4] - @context['numbers.size'].should == 4 - end + it "should return the size of an array" do + @context['numbers'] = [1,2,3,4] + @context['numbers.size'].should == 4 + end - it "should return the size of an hash" do - @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4} - @context['numbers.size'].should == 4 - end + it "should return the size of an hash" do + @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4} + @context['numbers.size'].should == 4 + end - it "should allow acess on a hash value by key" do - @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4, 'size' => 1000} - @context['numbers.size'].should == 1000 - end + it "should allow acess on a hash value by key" do + @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4, 'size' => 1000} + @context['numbers.size'].should == 1000 + end - it "should handle hyphenated variables" do - @context["oh-my"] = "godz" - @context["oh-my"].should == "godz" - end + it "should handle hyphenated variables" do + @context["oh-my"] = "godz" + @context["oh-my"].should == "godz" + end - it "should merge data" do - @context.merge("test" => "test") - @context["test"].should == "test" + it "should merge data" do + @context.merge("test" => "test") + @context["test"].should == "test" - @context.merge("test" => "newvalue", "foo" => "bar") - @context["test"].should == "newvalue" - @context["foo"].should == "bar" - end + @context.merge("test" => "newvalue", "foo" => "bar") + @context["test"].should == "newvalue" + @context["foo"].should == "bar" + end - describe "filters" do - before(:each) do - filter = Module.new do - def exclaim(output) - output + "!!!" + describe "filters" do + before(:each) do + filter = Module.new do + def exclaim(output) + output + "!!!" + end end + @context.add_filters(filter) end - @context.add_filters(filter) - end - it "should invoke a filter if found" do - @context.invoke(:exclaim, "hi").should == "hi!!!" - end + it "should invoke a filter if found" do + @context.invoke(:exclaim, "hi").should == "hi!!!" + end - it "should ignore a filter thats not found" do - local = Liquid::Context.new - local.invoke(:exclaim, "hi").should == "hi" - end + it "should ignore a filter thats not found" do + local = Context.new + local.invoke(:exclaim, "hi").should == "hi" + end - it "should override a global filter" do - global = Module.new do - def notice(output) - "Global #{output}" + it "should override a global filter" do + global = Module.new do + def notice(output) + "Global #{output}" + end end - end - local = Module.new do - def notice(output) - "Local #{output}" + local = Module.new do + def notice(output) + "Local #{output}" + end end - end - Liquid::Template.register_filter(global) - Liquid::Template.parse("{{'test' | notice }}").render.should == "Global test" - Liquid::Template.parse("{{'test' | notice }}").render({}, :filters => [local]).should == "Local test" - end + Template.register_filter(global) + Template.parse("{{'test' | notice }}").render.should == "Global test" + Template.parse("{{'test' | notice }}").render({}, :filters => [local]).should == "Local test" + end - it "should only include intended filters methods" do - filter = Module.new do - def hi(output) - output + ' hi!' + it "should only include intended filters methods" do + filter = Module.new do + def hi(output) + output + ' hi!' + end end - end - local = Liquid::Context.new - methods_before = local.strainer.methods.map { |method| method.to_s } - local.add_filters(filter) - methods_after = local.strainer.methods.map { |method| method.to_s } - methods_after.sort.should == (methods_before+["hi"]).sort + local = Context.new + methods_before = local.strainer.methods.map { |method| method.to_s } + local.add_filters(filter) + methods_after = local.strainer.methods.map { |method| method.to_s } + methods_after.sort.should == (methods_before+["hi"]).sort + end end - end - describe "scopes" do - it "should handle scoping properly" do - expect { - @context.push - @context.pop - }.to_not raise_exception + describe "scopes" do + it "should handle scoping properly" do + expect { + @context.push + @context.pop + }.to_not raise_exception - expect { - @context.pop - }.to raise_exception(Liquid::ContextError) + expect { + @context.pop + }.to raise_exception(ContextError) + + expect { + @context.push + @context.pop + @context.pop + }.to raise_exception(ContextError) + end - expect { + it "should allow access to items from outer scope within an inner scope" do + @context["test"] = "test" @context.push + @context["test"].should == "test" @context.pop + @context["test"].should == "test" + end + + it "should not allow access to items from inner scope with an outer scope" do + @context.push + @context["test"] = 'test' + @context["test"].should == "test" @context.pop - }.to raise_exception(Liquid::ContextError) + @context["test"].should == nil + end end - it "should allow access to items from outer scope within an inner scope" do - @context["test"] = "test" - @context.push - @context["test"].should == "test" - @context.pop - @context["test"].should == "test" - end + describe "literals" do + it "should recognize boolean keywords" do + @context["true"].should == true + @context["false"].should == false + end - it "should not allow access to items from inner scope with an outer scope" do - @context.push - @context["test"] = 'test' - @context["test"].should == "test" - @context.pop - @context["test"].should == nil - end - end + it "should recognize integers and floats" do + @context["100"].should == 100 + @context[%Q{100.00}].should == 100.00 + end - describe "literals" do - it "should recognize boolean keywords" do - @context["true"].should == true - @context["false"].should == false - end + it "should recognize strings" do + @context[%{"hello!"}].should == "hello!" + @context[%{'hello!'}].should == "hello!" + end - it "should recognize integers and floats" do - @context["100"].should == 100 - @context[%Q{100.00}].should == 100.00 + it "should recognize ranges" do + @context.merge( "test" => '5' ) + @context['(1..5)'].should == (1..5) + @context['(1..test)'].should == (1..5) + @context['(test..test)'].should == (5..5) + end end - it "should recognize strings" do - @context[%{"hello!"}].should == "hello!" - @context[%{'hello!'}].should == "hello!" - end + context "hierarchical data" do + it "should allow access to hierarchical data" do + @context["hash"] = {"name" => "tobi"} + @context['hash.name'].should == "tobi" + @context["hash['name']"].should == "tobi" + @context['hash["name"]'].should == "tobi" + end - it "should recognize ranges" do - @context.merge( "test" => '5' ) - @context['(1..5)'].should == (1..5) - @context['(1..test)'].should == (1..5) - @context['(test..test)'].should == (5..5) - end - end + it "should allow access to arrays" do + @context["test"] = [1,2,3,4,5] - context "hierarchical data" do - it "should allow access to hierarchical data" do - @context["hash"] = {"name" => "tobi"} - @context['hash.name'].should == "tobi" - @context["hash['name']"].should == "tobi" - @context['hash["name"]'].should == "tobi" - end + @context["test[0]"].should == 1 + @context["test[1]"].should == 2 + @context["test[2]"].should == 3 + @context["test[3]"].should == 4 + @context["test[4]"].should == 5 + end - it "should allow access to arrays" do - @context["test"] = [1,2,3,4,5] + it "should allow access to an array within a hash" do + @context['test'] = {'test' => [1,2,3,4,5]} + @context['test.test[0]'].should == 1 + + # more complex + @context['colors'] = { + 'Blue' => ['003366','336699', '6699CC', '99CCFF'], + 'Green' => ['003300','336633', '669966', '99CC99'], + 'Yellow' => ['CC9900','FFCC00', 'FFFF99', 'FFFFCC'], + 'Red' => ['660000','993333', 'CC6666', 'FF9999'] + } + @context['colors.Blue[0]'].should == '003366' + @context['colors.Red[3]'].should == 'FF9999' + end - @context["test[0]"].should == 1 - @context["test[1]"].should == 2 - @context["test[2]"].should == 3 - @context["test[3]"].should == 4 - @context["test[4]"].should == 5 - end + it "should allow access to a hash within an array" do + @context['test'] = [{'test' => 'worked'}] + @context['test[0].test'].should == "worked" + end - it "should allow access to an array within a hash" do - @context['test'] = {'test' => [1,2,3,4,5]} - @context['test.test[0]'].should == 1 - - # more complex - @context['colors'] = { - 'Blue' => ['003366','336699', '6699CC', '99CCFF'], - 'Green' => ['003300','336633', '669966', '99CC99'], - 'Yellow' => ['CC9900','FFCC00', 'FFFF99', 'FFFFCC'], - 'Red' => ['660000','993333', 'CC6666', 'FF9999'] - } - @context['colors.Blue[0]'].should == '003366' - @context['colors.Red[3]'].should == 'FF9999' - end + it "should provide first and last helpers for arrays" do + @context['test'] = [1,2,3,4,5] - it "should allow access to a hash within an array" do - @context['test'] = [{'test' => 'worked'}] - @context['test[0].test'].should == "worked" - end + @context['test.first'].should == 1 + @context['test.last'].should == 5 - it "should provide first and last helpers for arrays" do - @context['test'] = [1,2,3,4,5] + @context['test'] = {'test' => [1,2,3,4,5]} - @context['test.first'].should == 1 - @context['test.last'].should == 5 + @context['test.test.first'].should == 1 + @context['test.test.last'].should == 5 - @context['test'] = {'test' => [1,2,3,4,5]} + @context['test'] = [1] + @context['test.first'].should == 1 + @context['test.last'].should == 1 + end - @context['test.test.first'].should == 1 - @context['test.test.last'].should == 5 + it "should allow arbitrary depth chaining of hash and array notation" do + @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } + @context['products["count"]'].should == 5 + @context['products["tags"][0]'].should == "deepsnow" + @context['products["tags"].first'].should == "deepsnow" + + @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} + @context['product["variants"][0]["title"]'].should == "draft151cm" + @context['product["variants"][1]["title"]'].should == "element151cm" + @context['product["variants"][0]["title"]'].should == "draft151cm" + @context['product["variants"].last["title"]'].should == "element151cm" + end - @context['test'] = [1] - @context['test.first'].should == 1 - @context['test.last'].should == 1 - end + it "should allow variable access with hash notation" do + @context.merge("foo" => "baz", "bar" => "foo") + @context['["foo"]'].should == "baz" + @context['[bar]'].should == "baz" + end - it "should allow arbitrary depth chaining of hash and array notation" do - @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } - @context['products["count"]'].should == 5 - @context['products["tags"][0]'].should == "deepsnow" - @context['products["tags"].first'].should == "deepsnow" - - @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} - @context['product["variants"][0]["title"]'].should == "draft151cm" - @context['product["variants"][1]["title"]'].should == "element151cm" - @context['product["variants"][0]["title"]'].should == "draft151cm" - @context['product["variants"].last["title"]'].should == "element151cm" - end + it "should allow hash access with hash variables" do + @context['var'] = 'tags' + @context['nested'] = {'var' => 'tags'} + @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } - it "should allow variable access with hash notation" do - @context.merge("foo" => "baz", "bar" => "foo") - @context['["foo"]'].should == "baz" - @context['[bar]'].should == "baz" - end + @context['products[var].first'].should == "deepsnow" + @context['products[nested.var].last'].should == 'freestyle' + end - it "should allow hash access with hash variables" do - @context['var'] = 'tags' - @context['nested'] = {'var' => 'tags'} - @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } + it "should use hash notification only for hash access" do + @context['array'] = [1,2,3,4,5] + @context['hash'] = {'first' => 'Hello'} - @context['products[var].first'].should == "deepsnow" - @context['products[nested.var].last'].should == 'freestyle' - end + @context['array.first'].should == 1 + @context['array["first"]'].should == nil + @context['hash["first"]'].should == "Hello" + end - it "should use hash notification only for hash access" do - @context['array'] = [1,2,3,4,5] - @context['hash'] = {'first' => 'Hello'} + it "should allow helpers (such as first and last) in the middle of a callchain" do + @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} - @context['array.first'].should == 1 - @context['array["first"]'].should == nil - @context['hash["first"]'].should == "Hello" + @context['product.variants[0].title'].should == 'draft151cm' + @context['product.variants[1].title'].should == 'element151cm' + @context['product.variants.first.title'].should == 'draft151cm' + @context['product.variants.last.title'].should == 'element151cm' + end end - it "should allow helpers (such as first and last) in the middle of a callchain" do - @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} - - @context['product.variants[0].title'].should == 'draft151cm' - @context['product.variants[1].title'].should == 'element151cm' - @context['product.variants.first.title'].should == 'draft151cm' - @context['product.variants.last.title'].should == 'element151cm' - end - end + describe "Custom Object with a to_liquid method" do + class HundredCentes + def to_liquid + 100 + end + end - describe "Custom Object with a to_liquid method" do - class HundredCentes - def to_liquid - 100 + it "should resolve to whatever to_liquid returns from the object" do + @context["cents"] = HundredCentes.new + @context["cents"].should == 100 end - end - it "should resolve to whatever to_liquid returns from the object" do - @context["cents"] = HundredCentes.new - @context["cents"].should == 100 + it "should allow access to the custom object within a hash" do + @context.merge( "cents" => { 'amount' => HundredCentes.new} ) + @context['cents.amount'].should == 100 + + @context.merge( "cents" => { 'cents' => { 'amount' => HundredCentes.new} } ) + @context['cents.cents.amount'].should == 100 + end end - it "should allow access to the custom object within a hash" do - @context.merge( "cents" => { 'amount' => HundredCentes.new} ) - @context['cents.amount'].should == 100 + describe "Liquid Drops" do + class CentsDrop < Drop + def amount + HundredCentes.new + end - @context.merge( "cents" => { 'cents' => { 'amount' => HundredCentes.new} } ) - @context['cents.cents.amount'].should == 100 - end - end + def non_zero? + true + end + end - describe "Liquid Drops" do - class CentsDrop < Liquid::Drop - def amount - HundredCentes.new + it "should allow access to the drop's methods" do + @context.merge( "cents" => CentsDrop.new ) + @context['cents.amount'].should == 100 end - def non_zero? - true + it "should allow access to the drop's methods when nested in a hash" do + @context.merge( "vars" => {"cents" => CentsDrop.new} ) + @context['vars.cents.amount'].should == 100 end - end - it "should allow access to the drop's methods" do - @context.merge( "cents" => CentsDrop.new ) - @context['cents.amount'].should == 100 - end + it "should allow access to the a drop's methods that ends in a question mark" do + @context.merge( "cents" => CentsDrop.new ) + @context['cents.non_zero?'].should be_true + end - it "should allow access to the drop's methods when nested in a hash" do - @context.merge( "vars" => {"cents" => CentsDrop.new} ) - @context['vars.cents.amount'].should == 100 - end + it "should allow access to drop methods even when deeply nested" do + @context.merge( "cents" => {"cents" => CentsDrop.new} ) + @context['cents.cents.amount'].should == 100 - it "should allow access to the a drop's methods that ends in a question mark" do - @context.merge( "cents" => CentsDrop.new ) - @context['cents.non_zero?'].should be_true - end + @context.merge( "cents" => { "cents" => {"cents" => CentsDrop.new}} ) + @context['cents.cents.cents.amount'].should == 100 + end - it "should allow access to drop methods even when deeply nested" do - @context.merge( "cents" => {"cents" => CentsDrop.new} ) - @context['cents.cents.amount'].should == 100 + class ContextSensitiveDrop < Drop + def test + @context['test'] + end - @context.merge( "cents" => { "cents" => {"cents" => CentsDrop.new}} ) - @context['cents.cents.cents.amount'].should == 100 - end + def read_test + @context["test"] + end + end - class ContextSensitiveDrop < Liquid::Drop - def test - @context['test'] + it "should allow access to the current context from within a drop" do + @context.merge( "test" => '123', "vars" => ContextSensitiveDrop.new ) + @context["vars.test"].should == "123" + @context["vars.read_test"].should == "123" end - def read_test - @context["test"] + it "should allow access to the current context even when nested in a hash" do + @context.merge( "test" => '123', "vars" => {"local" => ContextSensitiveDrop.new } ) + @context['vars.local.test'].should == "123" + @context['vars.local.read_test'].should == "123" end - end - it "should allow access to the current context from within a drop" do - @context.merge( "test" => '123', "vars" => ContextSensitiveDrop.new ) - @context["vars.test"].should == "123" - @context["vars.read_test"].should == "123" - end - it "should allow access to the current context even when nested in a hash" do - @context.merge( "test" => '123', "vars" => {"local" => ContextSensitiveDrop.new } ) - @context['vars.local.test'].should == "123" - @context['vars.local.read_test'].should == "123" - end + class CounterDrop < Drop + def count + @count ||= 0 + @count += 1 + end + end + it "should trigger a drop's autoincrementing variable" do + @context['counter'] = CounterDrop.new - class CounterDrop < Liquid::Drop - def count - @count ||= 0 - @count += 1 + @context['counter.count'].should == 1 + @context['counter.count'].should == 2 + @context['counter.count'].should == 3 end - end - it "should trigger a drop's autoincrementing variable" do - @context['counter'] = CounterDrop.new + it "should trigger a drop's autoincrementing variable using hash syntax " do + @context['counter'] = CounterDrop.new - @context['counter.count'].should == 1 - @context['counter.count'].should == 2 - @context['counter.count'].should == 3 + @context['counter["count"]'].should == 1 + @context['counter["count"]'].should == 2 + @context['counter["count"]'].should == 3 + end end - it "should trigger a drop's autoincrementing variable using hash syntax " do - @context['counter'] = CounterDrop.new - - @context['counter["count"]'].should == 1 - @context['counter["count"]'].should == 2 - @context['counter["count"]'].should == 3 - end - end + context "lambas and procs" do + it "should trigger a proc if accessed as a variable" do + @context["dynamic1"] = Proc.new{ "Hello" } + @context['dynamic1'].should == "Hello" - context "lambas and procs" do - it "should trigger a proc if accessed as a variable" do - @context["dynamic1"] = Proc.new{ "Hello" } - @context['dynamic1'].should == "Hello" + @context["dynamic2"] = proc{ "Hello" } + @context['dynamic2'].should == "Hello" - @context["dynamic2"] = proc{ "Hello" } - @context['dynamic2'].should == "Hello" + end - end + it "should trigger a proc within a hash" do + @context["dynamic"] = {"lambda" => proc{ "Hello" }} + @context["dynamic.lambda"].should == "Hello" + end - it "should trigger a proc within a hash" do - @context["dynamic"] = {"lambda" => proc{ "Hello" }} - @context["dynamic.lambda"].should == "Hello" - end + it "should trigger a proc within an array" do + @context['dynamic'] = [1,2, proc { 'Hello' } ,4,5] + @context['dynamic[2]'].should == "Hello" + end - it "should trigger a proc within an array" do - @context['dynamic'] = [1,2, proc { 'Hello' } ,4,5] - @context['dynamic[2]'].should == "Hello" - end + it "should trigger the proc only the first time it's accessed" do + counter = 0 + @context["dynamic"] = proc{ "Hello #{counter += 1}" } + @context['dynamic'].should == "Hello 1" + @context['dynamic'].should == "Hello 1" + @context['dynamic'].should == "Hello 1" + end - it "should trigger the proc only the first time it's accessed" do - counter = 0 - @context["dynamic"] = proc{ "Hello #{counter += 1}" } - @context['dynamic'].should == "Hello 1" - @context['dynamic'].should == "Hello 1" - @context['dynamic'].should == "Hello 1" - end + it "should trigger the proc within a hash only the first time it's accessed" do + counter = 0 + @context["dynamic"] = {"lambda" => proc{ "Hello #{counter += 1}" } } + @context['dynamic.lambda'].should == "Hello 1" + @context['dynamic.lambda'].should == "Hello 1" + @context['dynamic.lambda'].should == "Hello 1" + end - it "should trigger the proc within a hash only the first time it's accessed" do - counter = 0 - @context["dynamic"] = {"lambda" => proc{ "Hello #{counter += 1}" } } - @context['dynamic.lambda'].should == "Hello 1" - @context['dynamic.lambda'].should == "Hello 1" - @context['dynamic.lambda'].should == "Hello 1" - end + it "should trigger the proc within an array only the first time it's accessed" do + counter = 0 + @context["dynamic"] = [1, 2, proc{ "Hello #{counter += 1}" }, 4] + @context['dynamic[2]'].should == "Hello 1" + @context['dynamic[2]'].should == "Hello 1" + @context['dynamic[2]'].should == "Hello 1" + end - it "should trigger the proc within an array only the first time it's accessed" do - counter = 0 - @context["dynamic"] = [1, 2, proc{ "Hello #{counter += 1}" }, 4] - @context['dynamic[2]'].should == "Hello 1" - @context['dynamic[2]'].should == "Hello 1" - @context['dynamic[2]'].should == "Hello 1" + it "should allow access to context from within proc" do + @context.registers[:magic] = 345392 + @context['magic'] = proc { @context.registers[:magic] } + @context['magic'].should == 345392 + end end - it "should allow access to context from within proc" do - @context.registers[:magic] = 345392 - @context['magic'] = proc { @context.registers[:magic] } - @context['magic'].should == 345392 - end - end + context "to_liquid returning a drop" do + class Category < Drop + attr_accessor :name - context "to_liquid returning a drop" do - class Category < Liquid::Drop - attr_accessor :name + def initialize(name) + @name = name + end - def initialize(name) - @name = name + def to_liquid + CategoryDrop.new(self) + end end - def to_liquid - CategoryDrop.new(self) + class CategoryDrop + attr_accessor :category, :context + def initialize(category) + @category = category + end end - end - class CategoryDrop - attr_accessor :category, :context - def initialize(category) - @category = category + it "should return a drop" do + @context['category'] = Category.new("foobar") + @context['category'].should be_an_instance_of(CategoryDrop) + @context['category'].context.should == @context end - end - it "should return a drop" do - @context['category'] = Category.new("foobar") - @context['category'].should be_an_instance_of(CategoryDrop) - @context['category'].context.should == @context - end + class ArrayLike + def fetch(index) + end - class ArrayLike - def fetch(index) - end + def [](index) + @counts ||= [] + @counts[index] ||= 0 + @counts[index] += 1 + end - def [](index) - @counts ||= [] - @counts[index] ||= 0 - @counts[index] += 1 + def to_liquid + self + end end - def to_liquid - self - end end - end end \ No newline at end of file diff --git a/spec/unit/file_system_spec.rb b/spec/unit/file_system_spec.rb index 470a725c1..b3dfbd7f2 100644 --- a/spec/unit/file_system_spec.rb +++ b/spec/unit/file_system_spec.rb @@ -1,19 +1,18 @@ require 'spec_helper' -describe "Liquid File System" do - - describe Liquid::BlankFileSystem do +module Liquid + describe BlankFileSystem do it "should error out when trying to ready any file" do expect { - Liquid::BlankFileSystem.new.read_template_file("dummy") + BlankFileSystem.new.read_template_file("dummy") }.to raise_error(Liquid::FileSystemError) end end - describe Liquid::LocalFileSystem do + describe LocalFileSystem do describe "#full_path" do before(:each) do - @file_system = Liquid::LocalFileSystem.new("/some/path") + @file_system = LocalFileSystem.new("/some/path") end it "should translate partial paths to the full filesystem path" do diff --git a/spec/unit/regexp_spec.rb b/spec/unit/regexp_spec.rb new file mode 100644 index 000000000..b99f27364 --- /dev/null +++ b/spec/unit/regexp_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +module Liquid + describe "Liquid Regular Expressions" do + + describe "QuotedFragment" do + context "empty string" do + it{ ''.scan(QuotedFragment).should == [] } + end + + context %{quoted string: "arg 1"} do + it{ %{"arg 1"}.scan(QuotedFragment).should == [%{"arg 1"}] } + end + + context "arg1 arg2" do + it{ subject.scan(QuotedFragment).should == ["arg1", "arg2"] } + end + + context " " do + it{ subject.scan(QuotedFragment).should == ['', ''] } + end + + context "" do + it{ subject.scan(QuotedFragment).should == [''] } + end + + context %{} do + it{ subject.scan(QuotedFragment).should == ['', ''] } + end + + context %{arg1 arg2 "arg 3"} do + it{ subject.scan(QuotedFragment).should == ['arg1', 'arg2', '"arg 3"'] } + end + + context "arg1 arg2 'arg 3'" do + it{ subject.scan(QuotedFragment).should == ['arg1', 'arg2', "'arg 3'"] } + end + + context %{arg1 arg2 "arg 3" arg4 } do + it{ subject.scan(QuotedFragment).should == ['arg1', 'arg2', '"arg 3"', 'arg4'] } + end + end + + describe "VariableParser" do + context "var" do + it{ subject.scan(VariableParser).should == ['var'] } + end + + context "var.method" do + it{ subject.scan(VariableParser).should == ['var', 'method']} + end + + context "var[method]" do + it{ subject.scan(VariableParser).should == ['var', '[method]']} + end + + context "var[method][0]" do + it{ subject.scan(VariableParser).should == ['var', '[method]', '[0]'] } + end + + context %{var["method"][0]} do + it{ subject.scan(VariableParser).should == ['var', '["method"]', '[0]'] } + end + + context "var['method'][0]" do + it{ subject.scan(VariableParser).should == ['var', "['method']", '[0]'] } + end + + context "var[method][0].method" do + it{ subject.scan(VariableParser).should == ['var', '[method]', '[0]', 'method'] } + end + end + + end +end \ No newline at end of file diff --git a/spec/unit/strainer_spec.rb b/spec/unit/strainer_spec.rb new file mode 100644 index 000000000..97a75484e --- /dev/null +++ b/spec/unit/strainer_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +module Liquid + describe Strainer do + + let(:strainer) do + Strainer.create(nil) + end + + it "should remove standard Object methods" do + strainer.respond_to?('__test__').should be_false + strainer.respond_to?('test').should be_false + strainer.respond_to?('instance_eval').should be_false + strainer.respond_to?('__send__').should be_false + + # from the standard lib + strainer.respond_to?('size').should be_true + end + + it "should respond_to with 2 params" do + strainer.respond_to?('size', false).should be_true + end + + end +end \ No newline at end of file diff --git a/spec/unit/template_spec.rb b/spec/unit/template_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/spec/unit/variable_spec.rb b/spec/unit/variable_spec.rb new file mode 100644 index 000000000..bc97658c7 --- /dev/null +++ b/spec/unit/variable_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +module Liquid + describe Variable do + it "#name" do + var = Variable.new('hello') + var.name.should == 'hello' + end + + it "should parse and store filters" do + var = Variable.new('hello | textileze') + var.name.should == 'hello' + var.filters.should == [[:textileze,[]]] + + var = Variable.new('hello | textileze | paragraph') + var.name.should == 'hello' + var.filters.should == [[:textileze,[]], [:paragraph,[]]] + + var = Variable.new(%! hello | strftime: '%Y'!) + var.name.should == 'hello' + var.filters.should == [[:strftime,["'%Y'"]]] + + var = Variable.new(%! 'typo' | link_to: 'Typo', true !) + var.name.should == %!'typo'! + var.filters.should == [[:link_to,["'Typo'", "true"]]] + + var = Variable.new(%! 'typo' | link_to: 'Typo', false !) + var.name.should == %!'typo'! + var.filters.should == [[:link_to,["'Typo'", "false"]]] + + var = Variable.new(%! 'foo' | repeat: 3 !) + var.name.should == %!'foo'! + var.filters.should == [[:repeat,["3"]]] + + var = Variable.new(%! 'foo' | repeat: 3, 3 !) + var.name.should == %!'foo'! + var.filters.should == [[:repeat,["3","3"]]] + + var = Variable.new(%! 'foo' | repeat: 3, 3, 3 !) + var.name.should == %!'foo'! + var.filters.should == [[:repeat,["3","3","3"]]] + + var = Variable.new(%! hello | strftime: '%Y, okay?'!) + var.name.should == 'hello' + var.filters.should == [[:strftime,["'%Y, okay?'"]]] + + var = Variable.new(%! hello | things: "%Y, okay?", 'the other one'!) + var.name.should == 'hello' + var.filters.should == [[:things,["\"%Y, okay?\"","'the other one'"]]] + end + + it "should store filters with parameters" do + var = Variable.new(%! '2006-06-06' | date: "%m/%d/%Y"!) + var.name.should == "'2006-06-06'" + var.filters.should == [[:date,["\"%m/%d/%Y\""]]] + end + + it "should allow filters without whitespace" do + var = Variable.new('hello | textileze | paragraph') + var.name.should == 'hello' + var.filters.should == [[:textileze,[]], [:paragraph,[]]] + + var = Variable.new('hello|textileze|paragraph') + var.name.should == 'hello' + var.filters.should == [[:textileze,[]], [:paragraph,[]]] + end + + it "should allow special characters" do + var = Variable.new("http://disney.com/logo.gif | image: 'med' ") + var.name.should == 'http://disney.com/logo.gif' + var.filters.should == [[:image,["'med'"]]] + end + + it "should allow double quoted strings" do + var = Variable.new(%| "hello" |) + var.name.should == '"hello"' + end + + it "should allow single quoted strings" do + var = Variable.new(%| 'hello' |) + var.name.should == "'hello'" + end + + it "should allow integers" do + var = Variable.new(%| 1000 |) + var.name.should == "1000" + end + + it "should allow floats" do + var = Variable.new(%| 1000.01 |) + var.name.should == "1000.01" + end + + it "should allow strings with special characters" do + var = Variable.new(%| 'hello! $!@.;"ddasd" ' |) + var.name.should == %|'hello! $!@.;"ddasd" '| + end + + it "should allow strings with dots" do + var = Variable.new(%| test.test |) + var.name.should == 'test.test' + end + end +end \ No newline at end of file diff --git a/test/output_test.rb b/test/output_test.rb deleted file mode 100644 index 96d909035..000000000 --- a/test/output_test.rb +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env ruby -require File.dirname(__FILE__) + '/helper' - -module FunnyFilter - - def make_funny(input) - 'LOL' - end - - def cite_funny(input) - "LOL: #{input}" - end - - def add_smiley(input, smiley = ":-)") - "#{input} #{smiley}" - end - - def add_tag(input, tag = "p", id = "foo") - %|<#{tag} id="#{id}">#{input}| - end - - def paragraph(input) - "

#{input}

" - end - - def link_to(name, url) - %|#{name}| - end -end - - -class OutputTest < Test::Unit::TestCase - include Liquid - - def setup - @assigns = { - 'best_cars' => 'bmw', - 'car' => {'bmw' => 'good', 'gm' => 'bad'} - } - - end - - def test_variable - text = %| {{best_cars}} | - - expected = %| bmw | - assert_equal expected, Template.parse(text).render(@assigns) - end - - def test_variable_traversing - text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} | - - expected = %| good bad good | - assert_equal expected, Template.parse(text).render(@assigns) - end - - def test_variable_piping - text = %( {{ car.gm | make_funny }} ) - expected = %| LOL | - - assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) - end - - def test_variable_piping_with_input - text = %( {{ car.gm | cite_funny }} ) - expected = %| LOL: bad | - - assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) - end - - def test_variable_piping_with_args - text = %! {{ car.gm | add_smiley : ':-(' }} ! - expected = %| bad :-( | - - assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) - end - - def test_variable_piping_with_no_args - text = %! {{ car.gm | add_smiley }} ! - expected = %| bad :-) | - - assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) - end - - def test_multiple_variable_piping_with_args - text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} ! - expected = %| bad :-( :-( | - - assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) - end - - def test_variable_piping_with_args - text = %! {{ car.gm | add_tag : 'span', 'bar'}} ! - expected = %| bad | - - assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) - end - - def test_variable_piping_with_variable_args - text = %! {{ car.gm | add_tag : 'span', car.bmw}} ! - expected = %| bad | - - assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) - end - - def test_multiple_pipings - text = %( {{ best_cars | cite_funny | paragraph }} ) - expected = %|

LOL: bmw

| - - assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) - end - - def test_link_to - text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} ) - expected = %| Typo | - - assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) - end - - -end \ No newline at end of file diff --git a/test/regexp_test.rb b/test/regexp_test.rb deleted file mode 100644 index 10717c1bb..000000000 --- a/test/regexp_test.rb +++ /dev/null @@ -1,45 +0,0 @@ -require File.dirname(__FILE__) + '/helper' - -class RegexpTest < Test::Unit::TestCase - include Liquid - - def test_empty - assert_equal [], ''.scan(QuotedFragment) - end - - def test_quote - assert_equal ['"arg 1"'], '"arg 1"'.scan(QuotedFragment) - end - - def test_words - assert_equal ['arg1', 'arg2'], 'arg1 arg2'.scan(QuotedFragment) - end - - def test_tags - assert_equal ['', ''], ' '.scan(QuotedFragment) - assert_equal [''], ''.scan(QuotedFragment) - assert_equal ['', ''], %||.scan(QuotedFragment) - end - - def test_quoted_words - assert_equal ['arg1', 'arg2', '"arg 3"'], 'arg1 arg2 "arg 3"'.scan(QuotedFragment) - end - - def test_quoted_words - assert_equal ['arg1', 'arg2', "'arg 3'"], 'arg1 arg2 \'arg 3\''.scan(QuotedFragment) - end - - def test_quoted_words_in_the_middle - assert_equal ['arg1', 'arg2', '"arg 3"', 'arg4'], 'arg1 arg2 "arg 3" arg4 '.scan(QuotedFragment) - end - - def test_variable_parser - assert_equal ['var'], 'var'.scan(VariableParser) - assert_equal ['var', 'method'], 'var.method'.scan(VariableParser) - assert_equal ['var', '[method]'], 'var[method]'.scan(VariableParser) - assert_equal ['var', '[method]', '[0]'], 'var[method][0]'.scan(VariableParser) - assert_equal ['var', '["method"]', '[0]'], 'var["method"][0]'.scan(VariableParser) - assert_equal ['var', '[method]', '[0]', 'method'], 'var[method][0].method'.scan(VariableParser) - end - -end \ No newline at end of file diff --git a/test/strainer_test.rb b/test/strainer_test.rb deleted file mode 100644 index 540888cee..000000000 --- a/test/strainer_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env ruby -require File.dirname(__FILE__) + '/helper' - -class StrainerTest < Test::Unit::TestCase - include Liquid - - def test_strainer - strainer = Strainer.create(nil) - assert_equal false, strainer.respond_to?('__test__') - assert_equal false, strainer.respond_to?('test') - assert_equal false, strainer.respond_to?('instance_eval') - assert_equal false, strainer.respond_to?('__send__') - assert_equal true, strainer.respond_to?('size') # from the standard lib - end - - def test_should_respond_to_two_parameters - strainer = Strainer.create(nil) - assert_equal true, strainer.respond_to?('size', false) - end - -end \ No newline at end of file diff --git a/test/unless_else_test.rb b/test/unless_else_test.rb deleted file mode 100644 index 1c420ba8c..000000000 --- a/test/unless_else_test.rb +++ /dev/null @@ -1,27 +0,0 @@ -require File.dirname(__FILE__) + '/helper' - -class UnlessElseTest < Test::Unit::TestCase - include Liquid - - def test_unless - assert_template_result(' ',' {% unless true %} this text should not go into the output {% endunless %} ') - assert_template_result(' this text should go into the output ', - ' {% unless false %} this text should go into the output {% endunless %} ') - assert_template_result(' you rock ?','{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?') - end - - def test_unless_else - assert_template_result(' YES ','{% unless true %} NO {% else %} YES {% endunless %}') - assert_template_result(' YES ','{% unless false %} YES {% else %} NO {% endunless %}') - assert_template_result(' YES ','{% unless "foo" %} NO {% else %} YES {% endunless %}') - end - - def test_unless_in_loop - assert_template_result '23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', 'choices' => [1, nil, false] - end - - def test_unless_else_in_loop - assert_template_result ' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', 'choices' => [1, nil, false] - end - -end \ No newline at end of file diff --git a/test/variable_test.rb b/test/variable_test.rb deleted file mode 100644 index 2ce846b38..000000000 --- a/test/variable_test.rb +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env ruby -require File.dirname(__FILE__) + '/helper' - -class VariableTest < Test::Unit::TestCase - include Liquid - - def test_variable - var = Variable.new('hello') - assert_equal 'hello', var.name - end - - def test_filters - var = Variable.new('hello | textileze') - assert_equal 'hello', var.name - assert_equal [[:textileze,[]]], var.filters - - var = Variable.new('hello | textileze | paragraph') - assert_equal 'hello', var.name - assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters - - var = Variable.new(%! hello | strftime: '%Y'!) - assert_equal 'hello', var.name - assert_equal [[:strftime,["'%Y'"]]], var.filters - - var = Variable.new(%! 'typo' | link_to: 'Typo', true !) - assert_equal %!'typo'!, var.name - assert_equal [[:link_to,["'Typo'", "true"]]], var.filters - - var = Variable.new(%! 'typo' | link_to: 'Typo', false !) - assert_equal %!'typo'!, var.name - assert_equal [[:link_to,["'Typo'", "false"]]], var.filters - - var = Variable.new(%! 'foo' | repeat: 3 !) - assert_equal %!'foo'!, var.name - assert_equal [[:repeat,["3"]]], var.filters - - var = Variable.new(%! 'foo' | repeat: 3, 3 !) - assert_equal %!'foo'!, var.name - assert_equal [[:repeat,["3","3"]]], var.filters - - var = Variable.new(%! 'foo' | repeat: 3, 3, 3 !) - assert_equal %!'foo'!, var.name - assert_equal [[:repeat,["3","3","3"]]], var.filters - - var = Variable.new(%! hello | strftime: '%Y, okay?'!) - assert_equal 'hello', var.name - assert_equal [[:strftime,["'%Y, okay?'"]]], var.filters - - var = Variable.new(%! hello | things: "%Y, okay?", 'the other one'!) - assert_equal 'hello', var.name - assert_equal [[:things,["\"%Y, okay?\"","'the other one'"]]], var.filters - end - - def test_filter_with_date_parameter - - var = Variable.new(%! '2006-06-06' | date: "%m/%d/%Y"!) - assert_equal "'2006-06-06'", var.name - assert_equal [[:date,["\"%m/%d/%Y\""]]], var.filters - - end - - def test_filters_without_whitespace - var = Variable.new('hello | textileze | paragraph') - assert_equal 'hello', var.name - assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters - - var = Variable.new('hello|textileze|paragraph') - assert_equal 'hello', var.name - assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters - end - - def test_symbol - var = Variable.new("http://disney.com/logo.gif | image: 'med' ") - assert_equal 'http://disney.com/logo.gif', var.name - assert_equal [[:image,["'med'"]]], var.filters - end - - def test_string_single_quoted - var = Variable.new(%| "hello" |) - assert_equal '"hello"', var.name - end - - def test_string_double_quoted - var = Variable.new(%| 'hello' |) - assert_equal "'hello'", var.name - end - - def test_integer - var = Variable.new(%| 1000 |) - assert_equal "1000", var.name - end - - def test_float - var = Variable.new(%| 1000.01 |) - assert_equal "1000.01", var.name - end - - def test_string_with_special_chars - var = Variable.new(%| 'hello! $!@.;"ddasd" ' |) - assert_equal %|'hello! $!@.;"ddasd" '|, var.name - end - - def test_string_dot - var = Variable.new(%| test.test |) - assert_equal 'test.test', var.name - end -end - - -class VariableResolutionTest < Test::Unit::TestCase - include Liquid - - def test_simple_variable - template = Template.parse(%|{{test}}|) - assert_equal 'worked', template.render('test' => 'worked') - assert_equal 'worked wonderfully', template.render('test' => 'worked wonderfully') - end - - def test_simple_with_whitespaces - template = Template.parse(%| {{ test }} |) - assert_equal ' worked ', template.render('test' => 'worked') - assert_equal ' worked wonderfully ', template.render('test' => 'worked wonderfully') - end - - def test_ignore_unknown - template = Template.parse(%|{{ test }}|) - assert_equal '', template.render - end - - def test_hash_scoping - template = Template.parse(%|{{ test.test }}|) - assert_equal 'worked', template.render('test' => {'test' => 'worked'}) - end - - def test_preset_assigns - template = Template.parse(%|{{ test }}|) - template.assigns['test'] = 'worked' - assert_equal 'worked', template.render - end - - def test_reuse_parsed_template - template = Template.parse(%|{{ greeting }} {{ name }}|) - template.assigns['greeting'] = 'Goodbye' - assert_equal 'Hello Tobi', template.render('greeting' => 'Hello', 'name' => 'Tobi') - assert_equal 'Hello ', template.render('greeting' => 'Hello', 'unknown' => 'Tobi') - assert_equal 'Hello Brian', template.render('greeting' => 'Hello', 'name' => 'Brian') - assert_equal 'Goodbye Brian', template.render('name' => 'Brian') - assert_equal({'greeting'=>'Goodbye'}, template.assigns) - end - - def test_assigns_not_polluted_from_template - template = Template.parse(%|{{ test }}{% assign test = 'bar' %}{{ test }}|) - template.assigns['test'] = 'baz' - assert_equal 'bazbar', template.render - assert_equal 'bazbar', template.render - assert_equal 'foobar', template.render('test' => 'foo') - assert_equal 'bazbar', template.render - end - - def test_hash_with_default_proc - template = Template.parse(%|Hello {{ test }}|) - assigns = Hash.new { |h,k| raise "Unknown variable '#{k}'" } - assigns['test'] = 'Tobi' - assert_equal 'Hello Tobi', template.render!(assigns) - assigns.delete('test') - e = assert_raises(RuntimeError) { - template.render!(assigns) - } - assert_equal "Unknown variable 'test'", e.message - end - -end \ No newline at end of file From d0c109c7da5ff17886d7e1925ae91f3ba1c1463a Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 07:25:25 -0700 Subject: [PATCH 32/61] Remove money_filter --- spec/fixtures/filters/money_filter.rb | 16 ---------------- spec/integration/filter_spec.rb | 19 +++++++++++++++++-- 2 files changed, 17 insertions(+), 18 deletions(-) delete mode 100644 spec/fixtures/filters/money_filter.rb diff --git a/spec/fixtures/filters/money_filter.rb b/spec/fixtures/filters/money_filter.rb deleted file mode 100644 index 7be3bd300..000000000 --- a/spec/fixtures/filters/money_filter.rb +++ /dev/null @@ -1,16 +0,0 @@ -module MoneyFilter - def money(input) - sprintf('$%d', input) - end - - def money_with_underscores(input) - sprintf('_$%d_', input) - end -end - -module CanadianMoneyFilter - def money(input) - sprintf('$%d CAD', input) - end -end - diff --git a/spec/integration/filter_spec.rb b/spec/integration/filter_spec.rb index f1018ccf0..07d5240af 100644 --- a/spec/integration/filter_spec.rb +++ b/spec/integration/filter_spec.rb @@ -1,10 +1,25 @@ require 'spec_helper' -require 'filters/money_filter' - describe "Liquid Rendering" do describe "Filters" do + module MoneyFilter + def money(input) + sprintf('$%d', input) + end + + def money_with_underscores(input) + sprintf('_$%d_', input) + end + end + + module CanadianMoneyFilter + def money(input) + sprintf('$%d CAD', input) + end + end + + before(:each) do @context = Liquid::Context.new end From 508d5425274131121ac7a2f2dcc4b3616d689d51 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 08:02:59 -0700 Subject: [PATCH 33/61] Additional spec refactoring --- spec/integration/security_spec.rb | 25 +++++++++++++ spec/spec_helper.rb | 12 +++++-- spec/unit/parsing_spec.rb | 57 ++++++++++++++--------------- spec/unit/quirks_spec.rb | 50 ++++++++++++++++++++++++++ spec/unit/template_spec.rb | 60 +++++++++++++++++++++++++++++++ test/parsing_context_test.rb | 54 ---------------------------- test/parsing_quirks_test.rb | 54 ---------------------------- test/security_test.rb | 41 --------------------- 8 files changed, 171 insertions(+), 182 deletions(-) create mode 100644 spec/integration/security_spec.rb create mode 100644 spec/unit/quirks_spec.rb delete mode 100644 test/parsing_context_test.rb delete mode 100644 test/parsing_quirks_test.rb delete mode 100644 test/security_test.rb diff --git a/spec/integration/security_spec.rb b/spec/integration/security_spec.rb new file mode 100644 index 000000000..c69bba0b6 --- /dev/null +++ b/spec/integration/security_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "Security" do + module SecurityFilter + def add_one(input) + "#{input} + 1" + end + end + + it "should not allow instance eval" do + render(" {{ '1+1' | instance_eval }} ").should == " 1+1 " + end + + it "should not allow existing instance eval" do + render(" {{ '1+1' | __instance_eval__ }} ").should == " 1+1 " + end + + it "should not allow instance eval later in chain" do + filters = {:filters => SecurityFilter} + render(" {{ '1+1' | add_one | instance_eval }} ", {}, filters).should == " 1+1 + 1 " + end + + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 43fa6bb64..528463fad 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -19,7 +19,7 @@ module Liquid - module RenderSpecHelper + module SpecHelpers def render(body, *args) Liquid::Template.parse(body).render(*args) end @@ -27,9 +27,17 @@ def render(body, *args) def render!(body, *args) Liquid::Template.parse(body).render!(*args) end + + def parse(body = nil) + body = eval(subject) if body == :subject + + Liquid::Template.parse(body) + end + + end end Rspec.configure do |c| - c.include Liquid::RenderSpecHelper + c.include Liquid::SpecHelpers end \ No newline at end of file diff --git a/spec/unit/parsing_spec.rb b/spec/unit/parsing_spec.rb index a2975e78b..e1eb79ae5 100644 --- a/spec/unit/parsing_spec.rb +++ b/spec/unit/parsing_spec.rb @@ -1,65 +1,60 @@ require 'spec_helper' -describe "Liquid Parsing" do - describe "Blocks" do +module Liquid + describe "Liquid Parsing" do it "should render whitespace properly" do - template = Liquid::Template.parse(" ") - template.root.nodelist.should == [" "] - end - - let(:template) do - Liquid::Template.parse(eval(subject)) + parse(" ").root.nodelist.should == [" "] end describe %|"{{funk}} "| do - it{ template.root.nodelist.should have(2).nodes } + it{ parse(:subject).root.nodelist.should have(2).nodes } it "should parse to: Variable,String" do - template.root.nodelist[0].should be_an_instance_of(Liquid::Variable) - template.root.nodelist[1].should be_an_instance_of(String) + parse(:subject).root.nodelist[0].should be_an_instance_of(Liquid::Variable) + parse(:subject).root.nodelist[1].should be_an_instance_of(String) end end describe %|" {{funk}}"| do - it{ template.root.nodelist.should have(2).nodes } + it{ parse(:subject).root.nodelist.should have(2).nodes } it "should parse to: String,Variable" do - template.root.nodelist[0].should be_an_instance_of(String) - template.root.nodelist[1].should be_an_instance_of(Liquid::Variable) + parse(:subject).root.nodelist[0].should be_an_instance_of(String) + parse(:subject).root.nodelist[1].should be_an_instance_of(Liquid::Variable) end end describe %|" {{funk}} "| do - it{ template.root.nodelist.should have(3).nodes } + it{ parse(:subject).root.nodelist.should have(3).nodes } it "should parse to: String,Variable,String" do - template.root.nodelist[0].should be_an_instance_of(String) - template.root.nodelist[1].should be_an_instance_of(Liquid::Variable) - template.root.nodelist[2].should be_an_instance_of(String) + parse(:subject).root.nodelist[0].should be_an_instance_of(String) + parse(:subject).root.nodelist[1].should be_an_instance_of(Liquid::Variable) + parse(:subject).root.nodelist[2].should be_an_instance_of(String) end end describe %|" {{funk}} {{so}} {{brother}} "| do - it{ template.root.nodelist.should have(7).nodes } + it{ parse(:subject).root.nodelist.should have(7).nodes } it "should parse to: String,Variable,String,Variable,String,Variable,String" do - template.root.nodelist[0].should be_an_instance_of(String) - template.root.nodelist[1].should be_an_instance_of(Liquid::Variable) - template.root.nodelist[2].should be_an_instance_of(String) - template.root.nodelist[3].should be_an_instance_of(Liquid::Variable) - template.root.nodelist[4].should be_an_instance_of(String) - template.root.nodelist[5].should be_an_instance_of(Liquid::Variable) - template.root.nodelist[6].should be_an_instance_of(String) + parse(:subject).root.nodelist[0].should be_an_instance_of(String) + parse(:subject).root.nodelist[1].should be_an_instance_of(Liquid::Variable) + parse(:subject).root.nodelist[2].should be_an_instance_of(String) + parse(:subject).root.nodelist[3].should be_an_instance_of(Liquid::Variable) + parse(:subject).root.nodelist[4].should be_an_instance_of(String) + parse(:subject).root.nodelist[5].should be_an_instance_of(Liquid::Variable) + parse(:subject).root.nodelist[6].should be_an_instance_of(String) end end describe %|" {% comment %} {% endcomment %} "| do - it{ template.root.nodelist.should have(3).nodes } + it{ parse(:subject).root.nodelist.should have(3).nodes } it "should parse to: String,Comment,String" do - template.root.nodelist[0].should be_an_instance_of(String) - template.root.nodelist[1].should be_an_instance_of(Liquid::Comment) - template.root.nodelist[2].should be_an_instance_of(String) + parse(:subject).root.nodelist[0].should be_an_instance_of(String) + parse(:subject).root.nodelist[1].should be_an_instance_of(Liquid::Comment) + parse(:subject).root.nodelist[2].should be_an_instance_of(String) end end @@ -70,7 +65,7 @@ describe %|"{% somethingaweful %} {% endsomethingaweful %}"| do it "should parse successfully" do - template.root.nodelist.should have(1).nodes + parse(:subject).root.nodelist.should have(1).nodes end end end diff --git a/spec/unit/quirks_spec.rb b/spec/unit/quirks_spec.rb new file mode 100644 index 000000000..a0953dc5c --- /dev/null +++ b/spec/unit/quirks_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +module Liquid + describe "Liquid Parsing Quirks" do + it "should work with css syntax" do + template = parse(" div { font-weight: bold; } ") + template.render.should == " div { font-weight: bold; } " + template.root.nodelist[0].should be_an_instance_of(String) + end + + it "should raise an error on a single close brace" do + expect { + parse("text {{method} oh nos!") + }.to raise_error(SyntaxError) + end + + it "should raise an error with double braces and no matcing closing double braces" do + expect { + parse("TEST {{") + }.to raise_error(SyntaxError) + end + + it "should raise an error with open tag and no matching close tag" do + expect { + parse("TEST {%") + }.to raise_error(SyntaxError) + end + + it "should allow empty filters" do + parse("{{test |a|b|}}") + parse("{{test}}") + parse("{{|test|}}") + end + + it "should allow meaningless parens" do + data = {'b' => 'bar', 'c' => 'baz'} + markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" + + render("{% if #{markup} %} YES {% endif %}", data).should == " YES " + end + + it "should allow unexpected characters to silently eat logic" do + markup = "true && false" + render("{% if #{markup} %} YES {% endif %}").should == ' YES ' + + markup = "false || true" + render("{% if #{markup} %} YES {% endif %}").should == '' + end + end +end \ No newline at end of file diff --git a/spec/unit/template_spec.rb b/spec/unit/template_spec.rb index e69de29bb..28843f880 100644 --- a/spec/unit/template_spec.rb +++ b/spec/unit/template_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +module Liquid + describe Template do + + def tokenize(text) + Template.new.send(:tokenize, text) + end + + it "should tokenize strings" do + tokenize(' ').should == [' '] + tokenize('hello world').should == ['hello world'] + end + + it "should tokenize variables" do + tokenize('{{funk}}').should == ['{{funk}}'] + tokenize(' {{funk}} ').should == [' ', '{{funk}}', ' '] + tokenize(' {{funk}} {{so}} {{brother}} ').should == [' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '] + tokenize(' {{ funk }} ').should == [' ', '{{ funk }}', ' '] + end + + it "should tokenize blocks" do + tokenize('{%comment%}').should == ['{%comment%}'] + tokenize(' {%comment%} ').should == [' ', '{%comment%}', ' '] + tokenize(' {%comment%} {%endcomment%} ').should == [' ', '{%comment%}', ' ', '{%endcomment%}', ' '] + tokenize(" {% comment %} {% endcomment %} ").should == [' ', '{% comment %}', ' ', '{% endcomment %}', ' '] + end + + it "should persist instance assignment on the same template object between parses " do + t = Template.new + t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render.should == 'from instance assigns' + t.parse("{{ foo }}").render.should == 'from instance assigns' + end + + it "should persist instance assingment on the same template object between renders" do + t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}") + t.render.should == "foo" + t.render.should == "foofoo" + end + + it "should not persist custom assignments on the same template" do + t = Template.new + t.parse("{{ foo }}").render('foo' => 'from custom assigns').should == 'from custom assigns' + t.parse("{{ foo }}").render.should == '' + end + + it "should squash instance assignments with custom assignments when specified" do + t = Template.new + t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render.should == 'from instance assigns' + t.parse("{{ foo }}").render('foo' => 'from custom assigns').should == 'from custom assigns' + end + + it "should squash instance assignments with persistent assignments" do + t = Template.new + t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render.should == 'from instance assigns' + t.assigns['foo'] = 'from persistent assigns' + t.parse("{{ foo }}").render.should == 'from persistent assigns' + end + end +end \ No newline at end of file diff --git a/test/parsing_context_test.rb b/test/parsing_context_test.rb deleted file mode 100644 index 19d586fd1..000000000 --- a/test/parsing_context_test.rb +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env ruby -require File.dirname(__FILE__) + '/helper' - -class ParsingQuirksTest < Test::Unit::TestCase - include Liquid - - def test_error_with_css - text = %| div { font-weight: bold; } | - template = Template.parse(text) - - assert_equal text, template.render - assert_equal [String], template.root.nodelist.collect {|i| i.class} - end - - def test_raise_on_single_close_bracet - assert_raise(SyntaxError) do - Template.parse("text {{method} oh nos!") - end - end - - def test_raise_on_label_and_no_close_bracets - assert_raise(SyntaxError) do - Template.parse("TEST {{ ") - end - end - - def test_raise_on_label_and_no_close_bracets_percent - assert_raise(SyntaxError) do - Template.parse("TEST {% ") - end - end - - def test_error_on_empty_filter - assert_nothing_raised do - Template.parse("{{test |a|b|}}") - Template.parse("{{test}}") - Template.parse("{{|test|}}") - end - end - - def test_meaningless_parens - assigns = {'b' => 'bar', 'c' => 'baz'} - markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" - assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}", assigns) - end - - def test_unexpected_characters_silently_eat_logic - markup = "true && false" - assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}") - markup = "false || true" - assert_template_result('',"{% if #{markup} %} YES {% endif %}") - end - -end \ No newline at end of file diff --git a/test/parsing_quirks_test.rb b/test/parsing_quirks_test.rb deleted file mode 100644 index 2f2aa4a00..000000000 --- a/test/parsing_quirks_test.rb +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env ruby -require File.dirname(__FILE__) + '/helper' - -class ParsingQuirksTest < Test::Unit::TestCase - include Liquid - - def test_error_with_css - text = %| div { font-weight: bold; } | - template = Template.parse(text) - - assert_equal text, template.render - assert_equal [String], template.root.nodelist.collect {|i| i.class} - end - - def test_raise_on_single_close_bracet - assert_raise(SyntaxError) do - Template.parse("text {{method} oh nos!") - end - end - - def test_raise_on_label_and_no_close_bracets - assert_raise(SyntaxError) do - Template.parse("TEST {{ ") - end - end - - def test_raise_on_label_and_no_close_bracets_percent - assert_raise(SyntaxError) do - Template.parse("TEST {% ") - end - end - - def test_error_on_empty_filter - assert_nothing_raised do - Template.parse("{{test |a|b|}}") - Template.parse("{{test}}") - Template.parse("{{|test|}}") - end - end - - def test_meaningless_parens - assigns = {'b' => 'bar', 'c' => 'baz'} - markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" - assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}", assigns) - end - - def test_unexpected_characters_silently_eat_logic - markup = "true && false" - assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}") - markup = "false || true" - assert_template_result('',"{% if #{markup} %} YES {% endif %}") - end - -end \ No newline at end of file diff --git a/test/security_test.rb b/test/security_test.rb deleted file mode 100644 index 1ab0d6fa4..000000000 --- a/test/security_test.rb +++ /dev/null @@ -1,41 +0,0 @@ -require File.dirname(__FILE__) + '/helper' - -module SecurityFilter - def add_one(input) - "#{input} + 1" - end -end - -class SecurityTest < Test::Unit::TestCase - include Liquid - - def test_no_instance_eval - text = %( {{ '1+1' | instance_eval }} ) - expected = %| 1+1 | - - assert_equal expected, Template.parse(text).render(@assigns) - end - - def test_no_existing_instance_eval - text = %( {{ '1+1' | __instance_eval__ }} ) - expected = %| 1+1 | - - assert_equal expected, Template.parse(text).render(@assigns) - end - - - def test_no_instance_eval_after_mixing_in_new_filter - text = %( {{ '1+1' | instance_eval }} ) - expected = %| 1+1 | - - assert_equal expected, Template.parse(text).render(@assigns) - end - - - def test_no_instance_eval_later_in_chain - text = %( {{ '1+1' | add_one | instance_eval }} ) - expected = %| 1+1 + 1 | - - assert_equal expected, Template.parse(text).render(@assigns, :filters => SecurityFilter) - end -end \ No newline at end of file From ec7724110b1ef34cbe0977df8aadf21e81c31403 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 08:07:16 -0700 Subject: [PATCH 34/61] cleaning out template etsts --- spec/unit/template_spec.rb | 18 +++++++++ test/template_test.rb | 75 -------------------------------------- 2 files changed, 18 insertions(+), 75 deletions(-) delete mode 100644 test/template_test.rb diff --git a/spec/unit/template_spec.rb b/spec/unit/template_spec.rb index 28843f880..471b9e782 100644 --- a/spec/unit/template_spec.rb +++ b/spec/unit/template_spec.rb @@ -56,5 +56,23 @@ def tokenize(text) t.assigns['foo'] = 'from persistent assigns' t.parse("{{ foo }}").render.should == 'from persistent assigns' end + + it "should call lambda only once from persistent assigns over multiple parses and renders" do + t = Template.new + t.assigns['number'] = lambda { @global ||= 0; @global += 1 } + t.parse("{{number}}").render.should == '1' + t.parse("{{number}}").render.should == '1' + t.render.should == '1' + @global = nil + end + + it "should call lambda only once from custom assigns over multiple parses and renders" do + t = Template.new + assigns = {'number' => lambda { @global ||= 0; @global += 1 }} + t.parse("{{number}}").render(assigns).should == '1' + t.parse("{{number}}").render(assigns).should == '1' + t.render(assigns).should == '1' + @global = nil + end end end \ No newline at end of file diff --git a/test/template_test.rb b/test/template_test.rb deleted file mode 100644 index 94f8ca868..000000000 --- a/test/template_test.rb +++ /dev/null @@ -1,75 +0,0 @@ -require File.dirname(__FILE__) + '/helper' - -class TemplateTest < Test::Unit::TestCase - include Liquid - - def test_tokenize_strings - assert_equal [' '], Template.new.send(:tokenize, ' ') - assert_equal ['hello world'], Template.new.send(:tokenize, 'hello world') - end - - def test_tokenize_variables - assert_equal ['{{funk}}'], Template.new.send(:tokenize, '{{funk}}') - assert_equal [' ', '{{funk}}', ' '], Template.new.send(:tokenize, ' {{funk}} ') - assert_equal [' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '], Template.new.send(:tokenize, ' {{funk}} {{so}} {{brother}} ') - assert_equal [' ', '{{ funk }}', ' '], Template.new.send(:tokenize, ' {{ funk }} ') - end - - def test_tokenize_blocks - assert_equal ['{%comment%}'], Template.new.send(:tokenize, '{%comment%}') - assert_equal [' ', '{%comment%}', ' '], Template.new.send(:tokenize, ' {%comment%} ') - - assert_equal [' ', '{%comment%}', ' ', '{%endcomment%}', ' '], Template.new.send(:tokenize, ' {%comment%} {%endcomment%} ') - assert_equal [' ', '{% comment %}', ' ', '{% endcomment %}', ' '], Template.new.send(:tokenize, " {% comment %} {% endcomment %} ") - end - - def test_instance_assigns_persist_on_same_template_object_between_parses - t = Template.new - assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render - assert_equal 'from instance assigns', t.parse("{{ foo }}").render - end - - def test_instance_assigns_persist_on_same_template_parsing_between_renders - t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}") - assert_equal 'foo', t.render - assert_equal 'foofoo', t.render - end - - def test_custom_assigns_do_not_persist_on_same_template - t = Template.new - assert_equal 'from custom assigns', t.parse("{{ foo }}").render('foo' => 'from custom assigns') - assert_equal '', t.parse("{{ foo }}").render - end - - def test_custom_assigns_squash_instance_assigns - t = Template.new - assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render - assert_equal 'from custom assigns', t.parse("{{ foo }}").render('foo' => 'from custom assigns') - end - - def test_persistent_assigns_squash_instance_assigns - t = Template.new - assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render - t.assigns['foo'] = 'from persistent assigns' - assert_equal 'from persistent assigns', t.parse("{{ foo }}").render - end - - def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders - t = Template.new - t.assigns['number'] = lambda { @global ||= 0; @global += 1 } - assert_equal '1', t.parse("{{number}}").render - assert_equal '1', t.parse("{{number}}").render - assert_equal '1', t.render - @global = nil - end - - def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders - t = Template.new - assigns = {'number' => lambda { @global ||= 0; @global += 1 }} - assert_equal '1', t.parse("{{number}}").render(assigns) - assert_equal '1', t.parse("{{number}}").render(assigns) - assert_equal '1', t.render(assigns) - @global = nil - end - -end \ No newline at end of file From 31727bcdd82be5b9cf9c5f1984b46124fb1c4d30 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 09:19:54 -0700 Subject: [PATCH 35/61] Converting filter specs --- spec/integration/filter_spec.rb | 108 ++++++++++++++++- spec/unit/filter_spec.rb | 200 ++++++++++++++++++++++++++++++++ test/standard_filter_test.rb | 175 ---------------------------- 3 files changed, 307 insertions(+), 176 deletions(-) create mode 100644 spec/unit/filter_spec.rb delete mode 100644 test/standard_filter_test.rb diff --git a/spec/integration/filter_spec.rb b/spec/integration/filter_spec.rb index 07d5240af..8eb01563a 100644 --- a/spec/integration/filter_spec.rb +++ b/spec/integration/filter_spec.rb @@ -19,7 +19,6 @@ def money(input) end end - before(:each) do @context = Liquid::Context.new end @@ -121,6 +120,104 @@ def render_variable(body) render_variable('val | capitalize').should == 'Blub' end end + + describe "strip_newlines" do + it "should remove newlines from a string" do + @context['source'] = "a\nb\nc" + render_variable('source | strip_newlines').should == 'abc' + end + end + + describe "newline_to_br" do + it "should convert line breaks to html
's" do + @context['source'] = "a\nb\nc" + render_variable('source | newline_to_br').should == "a
\nb
\nc" + end + end + + describe "plus" do + it "should increment a number by the specified amount" do + @context['val'] = 1 + render_variable('val | plus:1').should == 2 + + @context['val'] = "1" + render_variable('val | plus:1').should == 2 + + @context['val'] = "1" + render_variable('val | plus:"1"').should == 2 + end + end + + describe "minus" do + it "should decrement a number by the specified amount" do + @context['val'] = 2 + render_variable('val | minus:1').should == 1 + + @context['val'] = "2" + render_variable('val | minus:1').should == 1 + + @context['val'] = "2" + render_variable('val | minus:"1"').should == 1 + end + end + + describe "times" do + it "should multiply a number by the specified amount" do + @context['val'] = 2 + render_variable('val | times:2').should == 4 + + @context['val'] = "2" + render_variable('val | times:2').should == 4 + + @context['val'] = "2" + render_variable('val | times:"2"').should == 4 + end + end + + describe "divided_by" do + it "should divide a number the specified amount" do + @context['val'] = 12 + render_variable('val | divided_by:3').should == 4 + end + + it "should chop off the remainder when dividing by an integer" do + @context['val'] = 14 + render_variable('val | divided_by:3').should == 4 + end + + it "should return a float when dividing by another float" do + @context['val'] = 14 + render_variable('val | divided_by:3.0').should be_close(4.666, 0.001) + end + + it "should return an errorm essage if divided by 0" do + @context['val'] = 5 + expect{ + render_variable('val | divided_by:0') + }.to raise_error(ZeroDivisionError) + end + end + + describe "append" do + it "should append a string to another string" do + @context['val'] = "bc" + render_variable('val | append: "d"').should == "bcd" + + @context['next'] = " :: next >>" + render_variable('val | append: next').should == "bc :: next >>" + end + end + + describe "prepend" do + it "should prepend a string onto another string" do + @context['val'] = "bc" + render_variable('val | prepend: "a"').should == "abc" + + @context['prev'] = "<< prev :: " + render_variable('val | prepend: prev').should == "<< prev :: bc" + end + end + end context "filters in template" do @@ -136,6 +233,15 @@ def render_variable(body) Liquid::Template.parse('{{1000 | money}}').render(nil, :filters => CanadianMoneyFilter).should == "$1000 CAD" Liquid::Template.parse('{{1000 | money}}').render(nil, :filters => [CanadianMoneyFilter]).should == "$1000 CAD" end + + it "should allow pipes in string arguments" do + render("{{ 'foo|bar' | remove: '|' }}").should == "foobar" + end + + it "cannot access private methods" do + render("{{ 'a' | to_number }}").should == "a" + end end + end end \ No newline at end of file diff --git a/spec/unit/filter_spec.rb b/spec/unit/filter_spec.rb new file mode 100644 index 000000000..4d0cd0710 --- /dev/null +++ b/spec/unit/filter_spec.rb @@ -0,0 +1,200 @@ +require 'spec_helper' + +module Liquid + describe StandardFilters do + + class TestFilters + include StandardFilters + end + + let(:filters) do + TestFilters.new + end + + context "#size" do + it "should return the size of the collection" do + filters.size([1,2,3]).should == 3 + filters.size([]).should == 0 + end + + it "should return 0 for nil" do + filters.size(nil).should == 0 + end + end + + context "#downcase" do + it "should make the string lower case" do + filters.downcase("Testing").should == "testing" + end + + it "should return empty string for nil" do + filters.downcase(nil).should == "" + end + end + + context "#upcase" do + it "should make the string upper case" do + filters.upcase("Testing").should == "TESTING" + end + + it "should return empty string for nil" do + filters.upcase(nil).should == "" + end + end + + context "#truncate" do + it "should truncate string to the specified length, replacing with ellipsis" do + filters.truncate('1234567890', 7).should == '1234...' + filters.truncate('1234567890', 20).should == '1234567890' + filters.truncate('1234567890', 0).should == '...' + end + + it "should not truncate if no length is passed in" do + filters.truncate('1234567890').should == '1234567890' + end + + it "should allow overriding of the truncate character" do + filters.truncate('1234567890', 7, '---').should == '1234---' + filters.truncate('1234567890', 7, '--').should == '12345--' + filters.truncate('1234567890', 7, '-').should == '123456-' + end + end + + context "#escape" do + it "should escape html characters" do + filters.escape('').should == '<strong>' + end + + it "should be aliased with 'h'" do + filters.h('').should == '<strong>' + end + end + + context "#truncateword" do + it "should truncate the string to the amount of words specified" do + filters.truncatewords('one two three', 4).should == 'one two three' + + filters.truncatewords('one two three', 2).should == 'one two...' + end + + it "should be ignored if no length is specified" do + filters.truncatewords('one two three').should == 'one two three' + end + + it "should work with crazy special characters" do + filters.truncatewords('Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13” x 16” x 10.5” high) with cover.', 15).should == + 'Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13”...' + + end + end + + context "#strip_html" do + it "should strip out the html tags but leave the content" do + filters.strip_html("
test
").should == "test" + filters.strip_html("
test
").should == "test" + end + + it "should completely remove the content of script tags" do + filters.strip_html("").should == '' + end + + it "should return empty string for nil" do + filters.strip_html(nil).should == '' + end + end + + context "#join" do + it "should default to joining an array by a space" do + filters.join([1,2,3,4]).should == "1 2 3 4" + end + + it "should allow you to specify the join character" do + filters.join([1,2,3,4], ' - ').should == "1 - 2 - 3 - 4" + end + end + + context "#sort" do + it "should sort an array" do + filters.sort([4,3,2,1]).should == [1,2,3,4] + end + end + + context "#map" do + it "should return a list of values that have a key matching the argument" do + filters.map([{"a" => 1}, {"a" => 2}, {"a" => 3}, {"a" => 4}], 'a').should == [1,2,3,4] + + data = {'ary' => [{'foo' => {'bar' => 'a'}}, {'foo' => {'bar' => 'b'}}, {'foo' => {'bar' => 'c'}}]} + render("{{ ary | map:'foo' | map:'bar' }}", data).should == "abc" + end + end + + context "#date" do + it "should format a date using a specified format string" do + filters.date(Time.parse("2006-05-05 10:00:00"), "%B").should == 'May' + filters.date(Time.parse("2006-06-05 10:00:00"), "%B").should == 'June' + filters.date(Time.parse("2006-07-05 10:00:00"), "%B").should == 'July' + + filters.date("2006-05-05 10:00:00", "%B").should == 'May' + filters.date("2006-06-05 10:00:00", "%B").should == 'June' + filters.date("2006-07-05 10:00:00", "%B").should == 'July' + + filters.date("2006-07-05 10:00:00", "").should == '2006-07-05 10:00:00' + filters.date("2006-07-05 10:00:00", nil).should == '2006-07-05 10:00:00' + + filters.date("2006-07-05 10:00:00", "%m/%d/%Y").should == '07/05/2006' + + filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y").should == "07/16/2004" + end + end + + context "#first" do + it "should return the first item in an array" do + filters.first([1,2,3]).should == 1 + end + + it "should return nil for an empty array" do + filters.first([]).should == nil + end + end + + context "#last" do + it "should return the last item in an array" do + filters.last([1,2,3]).should == 3 + end + + it "should return nil for an empty array" do + filters.last([]).should == nil + end + end + + context "#replace" do + it "should replace all matches in a string with the new string" do + filters.replace("a a a a", 'a', 'b').should == 'b b b b' + render("{{ 'a a a a' | replace: 'a', 'b' }}").should == "b b b b" + end + end + + context "#replace_first" do + it "should replace the first match in a string with the new string" do + filters.replace_first("a a a a", 'a', 'b').should == 'b a a a' + render("{{ 'a a a a' | replace_first: 'a', 'b' }}").should == "b a a a" + end + end + + context "#remove" do + it "should remove all matching strings" do + filters.remove("a a a a", 'a').should == ' ' + render("{{ 'a a a a' | remove: 'a' }}").should == " " + end + end + + context "#remove_first" do + it "should remove the first matching string" do + filters.remove_first("a a a a", 'a').should == ' a a a' + filters.remove_first("a a a a", 'a ').should == 'a a a' + render("{{ 'a a a a' | remove_first: 'a' }}").should == ' a a a' + end + end + + end +end \ No newline at end of file diff --git a/test/standard_filter_test.rb b/test/standard_filter_test.rb deleted file mode 100644 index 10944ebc1..000000000 --- a/test/standard_filter_test.rb +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env ruby -require File.dirname(__FILE__) + '/helper' - - -class Filters - include Liquid::StandardFilters -end - - -class StandardFiltersTest < Test::Unit::TestCase - include Liquid - - def setup - @filters = Filters.new - end - - def test_size - assert_equal 3, @filters.size([1,2,3]) - assert_equal 0, @filters.size([]) - assert_equal 0, @filters.size(nil) - end - - def test_downcase - assert_equal 'testing', @filters.downcase("Testing") - assert_equal '', @filters.downcase(nil) - end - - def test_upcase - assert_equal 'TESTING', @filters.upcase("Testing") - assert_equal '', @filters.upcase(nil) - end - - def test_upcase - assert_equal 'TESTING', @filters.upcase("Testing") - assert_equal '', @filters.upcase(nil) - end - - def test_truncate - assert_equal '1234...', @filters.truncate('1234567890', 7) - assert_equal '1234567890', @filters.truncate('1234567890', 20) - assert_equal '...', @filters.truncate('1234567890', 0) - assert_equal '1234567890', @filters.truncate('1234567890') - end - - def test_escape - assert_equal '<strong>', @filters.escape('') - assert_equal '<strong>', @filters.h('') - 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) - assert_equal 'one two three', @filters.truncatewords('one two three') - assert_equal 'Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13”...', @filters.truncatewords('Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13” x 16” x 10.5” high) with cover.', 15) - end - - def test_strip_html - assert_equal 'test', @filters.strip_html("
test
") - assert_equal 'test', @filters.strip_html("
test
") - assert_equal '', @filters.strip_html("") - assert_equal '', @filters.strip_html(nil) - end - - def test_join - assert_equal '1 2 3 4', @filters.join([1,2,3,4]) - assert_equal '1 - 2 - 3 - 4', @filters.join([1,2,3,4], ' - ') - end - - def test_sort - assert_equal [1,2,3,4], @filters.sort([4,3,2,1]) - assert_equal [{"a" => 1}, {"a" => 2}, {"a" => 3}, {"a" => 4}], @filters.sort([{"a" => 4}, {"a" => 3}, {"a" => 1}, {"a" => 2}], "a") - end - - def test_map - assert_equal [1,2,3,4], @filters.map([{"a" => 1}, {"a" => 2}, {"a" => 3}, {"a" => 4}], 'a') - assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}", - 'ary' => [{'foo' => {'bar' => 'a'}}, {'foo' => {'bar' => 'b'}}, {'foo' => {'bar' => 'c'}}] - 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") - assert_equal 'July', @filters.date(Time.parse("2006-07-05 10:00:00"), "%B") - - assert_equal 'May', @filters.date("2006-05-05 10:00:00", "%B") - assert_equal 'June', @filters.date("2006-06-05 10:00:00", "%B") - assert_equal 'July', @filters.date("2006-07-05 10:00:00", "%B") - - assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") - assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") - assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") - assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", nil) - - assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y") - - assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y") - - assert_equal nil, @filters.date(nil, "%B") - end - - - def test_first_last - assert_equal 1, @filters.first([1,2,3]) - assert_equal 3, @filters.last([1,2,3]) - assert_equal nil, @filters.first([]) - assert_equal nil, @filters.last([]) - end - - def test_replace - assert_equal 'b b b b', @filters.replace("a a a a", 'a', 'b') - assert_equal 'b a a a', @filters.replace_first("a a a a", 'a', 'b') - assert_template_result 'b a a a', "{{ 'a a a a' | replace_first: 'a', 'b' }}" - end - - def test_remove - assert_equal ' ', @filters.remove("a a a a", 'a') - assert_equal 'a a a', @filters.remove_first("a a a a", 'a ') - assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}" - end - - def test_pipes_in_string_arguments - assert_template_result 'foobar', "{{ 'foo|bar' | remove: '|' }}" - end - - def test_strip_newlines - assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc" - end - - def test_newlines_to_br - assert_template_result "a
\nb
\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc" - end - - def test_plus - assert_template_result "2", "{{ 1 | plus:1 }}" - assert_template_result "2.0", "{{ '1' | plus:'1.0' }}" - end - - def test_minus - assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1 - assert_template_result "2.3", "{{ '4.3' | minus:'2' }}" - end - - def test_times - assert_template_result "12", "{{ 3 | times:4 }}" - assert_template_result "0", "{{ 'foo' | times:4 }}" - assert_template_result "6.3", "{{ '2.1' | times:3 }}" - assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}" - end - - def test_divided_by - assert_template_result "4", "{{ 12 | divided_by:3 }}" - assert_template_result "4", "{{ 14 | divided_by:3 }}" - assert_template_result "4.66666666666667", "{{ 14 | divided_by:'3.0' }}" - assert_template_result "5", "{{ 15 | divided_by:3 }}" - assert_template_result "Liquid error: divided by 0", "{{ 5 | divided_by:0 }}" - end - - def test_append - assigns = {'a' => 'bc', 'b' => 'd' } - assert_template_result('bcd',"{{ a | append: 'd'}}",assigns) - assert_template_result('bcd',"{{ a | append: b}}",assigns) - end - - def test_prepend - assigns = {'a' => 'bc', 'b' => 'a' } - assert_template_result('abc',"{{ a | prepend: 'a'}}",assigns) - assert_template_result('abc',"{{ a | prepend: b}}",assigns) - end - - def test_cannot_access_private_methods - assert_template_result('a',"{{ 'a' | to_number }}") - end - -end - From bcbc997e9af62dded1b5d977dfb7a97b173444aa Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 09:20:57 -0700 Subject: [PATCH 36/61] reorganize filter spec --- spec/integration/filter_spec.rb | 68 ++++++++++++++++----------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/spec/integration/filter_spec.rb b/spec/integration/filter_spec.rb index 8eb01563a..89f0d7132 100644 --- a/spec/integration/filter_spec.rb +++ b/spec/integration/filter_spec.rb @@ -2,23 +2,6 @@ describe "Liquid Rendering" do describe "Filters" do - - module MoneyFilter - def money(input) - sprintf('$%d', input) - end - - def money_with_underscores(input) - sprintf('_$%d_', input) - end - end - - module CanadianMoneyFilter - def money(input) - sprintf('$%d CAD', input) - end - end - before(:each) do @context = Liquid::Context.new end @@ -27,24 +10,6 @@ def render_variable(body) Liquid::Variable.new(body).render(@context) end - context "with custom filters added to context" do - before(:each) do - @context['val'] = 1000 - end - - it "should use the local filters" do - @context.add_filters(MoneyFilter) - render_variable('val | money').should == "$1000" - render_variable('val | money_with_underscores').should == "_$1000_" - end - - it "should allow filters to overwrite previous ones" do - @context.add_filters(MoneyFilter) - @context.add_filters(CanadianMoneyFilter) - render_variable('val | money').should == "$1000 CAD" - end - end - context "standard filters" do describe "size" do it "should return the size of a string" do @@ -217,7 +182,40 @@ def render_variable(body) render_variable('val | prepend: prev').should == "<< prev :: bc" end end + end + + module MoneyFilter + def money(input) + sprintf('$%d', input) + end + + def money_with_underscores(input) + sprintf('_$%d_', input) + end + end + + module CanadianMoneyFilter + def money(input) + sprintf('$%d CAD', input) + end + end + context "with custom filters added to context" do + before(:each) do + @context['val'] = 1000 + end + + it "should use the local filters" do + @context.add_filters(MoneyFilter) + render_variable('val | money').should == "$1000" + render_variable('val | money_with_underscores').should == "_$1000_" + end + + it "should allow filters to overwrite previous ones" do + @context.add_filters(MoneyFilter) + @context.add_filters(CanadianMoneyFilter) + render_variable('val | money').should == "$1000 CAD" + end end context "filters in template" do From f3affdbb2d14a18f2a542df755a45898e7f3184c Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 09:40:47 -0700 Subject: [PATCH 37/61] Rspec --- spec/integration/statements_spec.rb | 129 ++++++++++++++++++++++++++ test/statements_test.rb | 137 ---------------------------- 2 files changed, 129 insertions(+), 137 deletions(-) create mode 100644 spec/integration/statements_spec.rb delete mode 100644 test/statements_test.rb diff --git a/spec/integration/statements_spec.rb b/spec/integration/statements_spec.rb new file mode 100644 index 000000000..5068162fd --- /dev/null +++ b/spec/integration/statements_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "statements" do + let(:data) do + {} + end + + def render(*args) + super("#{subject} true {% else %} false {% endif %}", data) + end + + describe %| {% if true == true %} | do + it{ render.should == " true " } + end + + describe %| {% if true != true %} | do + it{ render.should == " false " } + end + + describe %| {% if 0 > 0 %} | do + it{ render.should == " false " } + end + + describe %| {% if 1 > 0 %} | do + it{ render.should == " true " } + end + + describe %| {% if 0 < 1 %} | do + it{ render.should == " true " } + end + + describe %| {% if 0 <= 0 %} | do + it{ render.should == " true " } + end + + describe %| {% if null <= 0 %} | do + it{ render.should == " false " } + end + + describe %| {% if 0 <= null %} | do + it{ render.should == " false " } + end + + describe %| {% if 0 >= 0 %} | do + it{ render.should == " true " } + end + + describe %| {% if 'test' == 'test' %} | do + it{ render.should == " true " } + end + + describe %| {% if 'test' != 'test' %} | do + it{ render.should == " false " } + end + + context 'when var is assigned to "hello there!"' do + let(:data) do + { 'var' => "hello there!" } + end + + describe %| {% if var == "hello there!" %} | do + it{ render.should == " true " } + end + + describe %| {% if "hello there!" == var %} | do + it{ render.should == " true " } + end + + describe %| {% if var == 'hello there!' %} | do + it{ render.should == " true " } + end + + describe %| {% if 'hello there!' == var %} | do + it{ render.should == " true " } + end + end + + context 'when array is assigned to []' do + let(:data) do + {'array' => ''} + end + describe %| {% if array == empty %} | do + it{ render.should == " true " } + end + end + + + context 'when array is assigned to [1,2,3]' do + let(:data) do + {'array' => [1,2,3]} + end + + describe %| {% if array == empty %} | do + it{ render.should == " false " } + end + end + + context "when var is assigned to nil" do + let(:data) do + {'var' => nil} + end + + describe %| {% if var == nil %} | do + it{ render.should == " true " } + end + + describe %| {% if var == null %} | do + it{ render.should == " true " } + end + end + + context "when var is assigned to 1" do + let(:data) do + {'var' => 1} + end + + describe %| {% if var != nil %} | do + it{ render.should == " true " } + end + + describe %| {% if var != null %} | do + it{ render.should == " true " } + end + end + + + end +end \ No newline at end of file diff --git a/test/statements_test.rb b/test/statements_test.rb deleted file mode 100644 index 63a41040e..000000000 --- a/test/statements_test.rb +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env ruby -require File.dirname(__FILE__) + '/helper' - -class StatementsTest < Test::Unit::TestCase - include Liquid - - - def test_true_eql_true - text = %| {% if true == true %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render - end - - def test_true_not_eql_true - text = %| {% if true != true %} true {% else %} false {% endif %} | - expected = %| false | - assert_equal expected, Template.parse(text).render - end - - def test_true_lq_true - text = %| {% if 0 > 0 %} true {% else %} false {% endif %} | - expected = %| false | - assert_equal expected, Template.parse(text).render - end - - def test_one_lq_zero - text = %| {% if 1 > 0 %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render - end - - def test_zero_lq_one - text = %| {% if 0 < 1 %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render - end - - def test_zero_lq_or_equal_one - text = %| {% if 0 <= 0 %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render - end - - def test_zero_lq_or_equal_one_involving_nil - text = %| {% if null <= 0 %} true {% else %} false {% endif %} | - expected = %| false | - assert_equal expected, Template.parse(text).render - - - text = %| {% if 0 <= null %} true {% else %} false {% endif %} | - expected = %| false | - assert_equal expected, Template.parse(text).render - end - - def test_zero_lqq_or_equal_one - text = %| {% if 0 >= 0 %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render - end - - def test_strings - text = %| {% if 'test' == 'test' %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render - end - - def test_strings_not_equal - text = %| {% if 'test' != 'test' %} true {% else %} false {% endif %} | - expected = %| false | - assert_equal expected, Template.parse(text).render - end - - def test_var_strings_equal - text = %| {% if var == "hello there!" %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render('var' => 'hello there!') - end - - def test_var_strings_are_not_equal - text = %| {% if "hello there!" == var %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render('var' => 'hello there!') - end - - def test_var_and_long_string_are_equal - text = %| {% if var == 'hello there!' %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render('var' => 'hello there!') - end - - - def test_var_and_long_string_are_equal_backwards - text = %| {% if 'hello there!' == var %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render('var' => 'hello there!') - end - - #def test_is_nil - # text = %| {% if var != nil %} true {% else %} false {% end %} | - # @template.assigns = { 'var' => 'hello there!'} - # expected = %| true | - # assert_equal expected, @template.parse(text) - #end - - def test_is_collection_empty - text = %| {% if array == empty %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render('array' => []) - end - - def test_is_not_collection_empty - text = %| {% if array == empty %} true {% else %} false {% endif %} | - expected = %| false | - assert_equal expected, Template.parse(text).render('array' => [1,2,3]) - end - - def test_nil - text = %| {% if var == nil %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render('var' => nil) - - text = %| {% if var == null %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render('var' => nil) - end - - def test_not_nil - text = %| {% if var != nil %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render('var' => 1 ) - - text = %| {% if var != null %} true {% else %} false {% endif %} | - expected = %| true | - assert_equal expected, Template.parse(text).render('var' => 1 ) - end - -end \ No newline at end of file From 6f20154f7552319d337dcf047f0884dcf10a12ca Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Wed, 29 Sep 2010 13:54:18 -0700 Subject: [PATCH 38/61] Adding output and tag specs --- spec/integration/output_spec.rb | 15 ++ spec/integration/tag_spec.rb | 252 ++++++++++++++++++++++++++++++++ spec/unit/tag_spec.rb | 35 +++++ test/standard_tag_test.rb | 91 ++++++------ 4 files changed, 343 insertions(+), 50 deletions(-) create mode 100644 spec/integration/tag_spec.rb create mode 100644 spec/unit/tag_spec.rb diff --git a/spec/integration/output_spec.rb b/spec/integration/output_spec.rb index 21b9e445d..859dff606 100644 --- a/spec/integration/output_spec.rb +++ b/spec/integration/output_spec.rb @@ -18,6 +18,21 @@ def render(text) super(text, data, filters) end + it "should not transform plaintext" do + render('this text should come out of the template without change...').should == + 'this text should come out of the template without change...' + + render('blah').should == 'blah' + render('').should == '' + render('|,.:').should == '|,.:' + render('').should == '' + + text = %|this shouldnt see any transformation either but has multiple lines + as you can clearly see here ...| + render(text).should == text + end + + it "should render a variable's value" do render(' {{best_cars}} ').should == " bmw " end diff --git a/spec/integration/tag_spec.rb b/spec/integration/tag_spec.rb new file mode 100644 index 000000000..42f960a34 --- /dev/null +++ b/spec/integration/tag_spec.rb @@ -0,0 +1,252 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "Tags" do + + describe "comment" do + context "{% comment %}" do + it "should not render comment blocks" do + render('{%comment%}{%endcomment%}').should == '' + render('{%comment%}{% endcomment %}').should == '' + render('{% comment %}{%endcomment%}').should == '' + render('{% comment %}{% endcomment %}').should == '' + render('{%comment%}comment{%endcomment%}').should == '' + render('{% comment %}comment{% endcomment %}').should == '' + end + + it "should render the other content that isnt inside the comment block" do + + render(%|the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?|).should == + %|the comment block should be removed .. right?| + + render('foo{%comment%}comment{%endcomment%}bar').should == 'foobar' + render('foo{% comment %}comment{% endcomment %}bar').should == 'foobar' + render('foo{%comment%} comment {%endcomment%}bar').should == 'foobar' + render('foo{% comment %} comment {% endcomment %}bar').should == 'foobar' + + render('foo {%comment%} {%endcomment%} bar').should == 'foo bar' + render('foo {%comment%}comment{%endcomment%} bar').should == 'foo bar' + render('foo {%comment%} comment {%endcomment%} bar').should == 'foo bar' + + render('foo{%comment%} + {%endcomment%}bar').should == "foobar" + end + end + end + + describe "for" do + describe "{% for item in collection %}" do + it "should repeat the block for each item in the collection" do + data = {'collection' => [1,2,3,4]} + render('{%for item in collection%} yo {%endfor%}', data).should == ' yo yo yo yo ' + + data = {'collection' => [1,2]} + render('{%for item in collection%}yo{%endfor%}', data).should == 'yoyo' + + data = {'collection' => [1]} + render('{%for item in collection%} yo {%endfor%}', data).should == ' yo ' + + data = {'collection' => [1,2]} + render('{%for item in collection%}{%endfor%}', data).should == '' + + data = {'collection' => [1,2,3]} + render('{%for item in collection%} yo {%endfor%}', data).should == " yo yo yo " + end + + it "should allow access to the current item via {{item}}" do + data = {'collection' => [1,2,3]} + render('{%for item in collection%} {{item}} {%endfor%}', data).should == ' 1 2 3 ' + render('{% for item in collection %}{{item}}{% endfor %}', data).should == '123' + render('{%for item in collection%}{{item}}{%endfor%}', data).should == '123' + + data = {'collection' => ['a','b','c','d']} + render('{%for item in collection%}{{item}}{%endfor%}', data).should == 'abcd' + + data = {'collection' => ['a',' ','b',' ','c']} + render('{%for item in collection%}{{item}}{%endfor%}', data).should == 'a b c' + + data = {'collection' => ['a','','b','','c']} + render('{%for item in collection%}{{item}}{%endfor%}', data).should == 'abc' + end + + it "should allow deep nesting" do + data = {'array' => [[1,2],[3,4],[5,6]] } + render('{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', data).should == '123456' + end + + it "should expose {{forloop.name}} to get the name of the collection" do + data = {'collection' => [1] } + render("{%for item in collection%} {{forloop.name}} {%endfor%}", data).should == " item-collection " + end + + it "should expose {{forloop.length}} for the overall size of the collection being looped" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {{forloop.length}} {%endfor%}", data).should == " 3 3 3 " + end + + it "should expose {{forloop.index}} for the current item's position in the collection (1 based)" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {{forloop.index}} {%endfor%}", data).should == " 1 2 3 " + end + + it "should expose {{forloop.index0}} for the current item's position in the collection (0 based)" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {{forloop.index0}} {%endfor%}", data).should == " 0 1 2 " + end + + it "should expose {{forloop.rindex}} for the number of items remaining in the collection (1 based)" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {{forloop.rindex}} {%endfor%}", data).should == " 3 2 1 " + end + + it "should expose {{forloop.rindex0}} for the number of items remaining in the collection (0 based)" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {{forloop.rindex0}} {%endfor%}", data).should == " 2 1 0 " + end + + it "should expose {{forloop.first}} for the first item in the collection" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {% if forloop.first %}y{% else %}n{% endif %} {%endfor%}", data).should == " y n n " + end + + it "should expose {{forloop.last}} for the last item in the collection" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {% if forloop.last %}y{% else %}n{% endif %} {%endfor%}", data).should == " n n y " + end + end + + describe "{% for item in collection reversed %}" do + it "should reverse the loop" do + data = {'collection' => [1,2,3] } + render("{%for item in collection reversed%}{{item}}{%endfor%}", data).should == "321" + end + end + + context "with limit and offset" do + let(:data) do + {'collection' => [1,2,3,4,5,6,7,8,9,0] } + end + + describe "{% for item in collection limit: 4 %}" do + it "should only cycle through the first 4 items of the collection" do + render("{%for item in collection limit:4%}{{item}}{%endfor%}", data).should == "1234" + render("{%for item in collection limit: 4%}{{item}}{%endfor%}", data).should == "1234" + end + end + + describe "{% for item in collection offset:8 %}" do + it "should cycle throughthe collection starting on the 9th item" do + render("{%for item in collection offset:8%}{{item}}{%endfor%}", data).should == "90" + end + end + + describe "{% for item in collection limit:4 offset:2}" do + it "should only cycle through the 4 items of the collection, starting on the 3rd item" do + render("{%for item in collection limit:4 offset:2 %}{{item}}{%endfor%}", data).should == "3456" + render("{%for item in collection limit: 4 offset: 2 %}{{item}}{%endfor%}", data).should == "3456" + end + + it "{% for item in collection limit:limit offset:offset}" do + data.merge! 'limit' => '4', 'offset' => '2' + render("{%for item in collection limit:limit offset:offset %}{{item}}{%endfor%}", data).should == "3456" + render("{%for item in collection limit: limit offset: offset %}{{item}}{%endfor%}", data).should == "3456" + end + end + + describe "{% for item in collection offset:continue limit: 3}" do + it "should resume the iteration from where it ended earlier" do + + output = render multiline_string(<<-END), data + | {%for i in collection limit:3 %}{{i}}{%endfor%} + | next + | {%for i in collection offset:continue limit:3 %}{{i}}{%endfor%} + | next + | {%for i in collection offset:continue limit:3 %}{{i}}{%endfor%} + END + + output.should == multiline_string(<<-END) + | 123 + | next + | 456 + | next + | 789 + END + end + end + + + describe "edge cases" do + context "limit: -1" do + it "should ignore the limit" do + render("{%for item in collection limit:-1 offset:5 %}{{item}}{%endfor%}", data).should == "67890" + end + end + + context "offset: -1" do + it "should ignore the offset" do + render("{%for item in collection limit:1 offset:-1 %}{{item}}{%endfor%}", data).should == "1" + end + end + + context "offset: 100" do + it "should render an empty string" do + render("{%for item in collection limit:1 offset:100 %} {{item}} {%endfor%}", data).should == "" + end + end + + context "resume with big limit" do + it "should complete the rest of the items" do + output = render multiline_string(<<-END), data + | {%for i in collection limit:3 %}{{i}}{%endfor%} + | next + | {%for i in collection offset:continue limit:10000 %}{{i}}{%endfor%} + END + + output.should == multiline_string(<<-END) + | 123 + | next + | 4567890 + END + end + end + + context "resume with big offset" do + it "should complete the rest of the items" do + output = render multiline_string(<<-END), data + | {%for i in collection limit:3 %}{{i}}{%endfor%} + | next + | {%for i in collection offset:continue offset:10000 %}{{i}}{%endfor%} + END + + output.should == multiline_string(<<-END) + | 123 + | next + | | + END + end + end + end + end + + context "{% for item in (1..3) %}" do + it "should repeat the block for each item in the range" do + render('{%for item in (1..3) %} {{item}} {%endfor%}').should == ' 1 2 3 ' + end + end + + context "{% ifchanged %}" do + it "should render the block only if the for item is different than the last" do + data = {'array' => [ 1, 1, 2, 2, 3, 3] } + render('{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',data).should == '123' + + data = {'array' => [ 1, 1, 1, 1] } + render('{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',data).should == '1' + + end + end + + end + + + end +end \ No newline at end of file diff --git a/spec/unit/tag_spec.rb b/spec/unit/tag_spec.rb new file mode 100644 index 000000000..6c8beb61e --- /dev/null +++ b/spec/unit/tag_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +module Liquid + describe Tag do + + context "empty tag" do + before(:each) do + @tag = Tag.new('tag', [], [], {}) + end + + context "#name" do + it "should return the name of the tag" do + @tag.name.should == "liquid::tag" + end + end + + context "#render" do + it "should render an empty string" do + @tag.render(Context.new).should == '' + end + end + end + + context "tag with context" do + before(:each) do + @tag = Tag.new('tag', [], [], { :foo => 'bar' }) + end + + it "should store context at parse time" do + @tag.context[:foo].should == "bar" + end + end + + end +end \ No newline at end of file diff --git a/test/standard_tag_test.rb b/test/standard_tag_test.rb index f2152bab7..21e5e17d8 100644 --- a/test/standard_tag_test.rb +++ b/test/standard_tag_test.rb @@ -4,31 +4,6 @@ class StandardTagTest < Test::Unit::TestCase include Liquid - - def test_tag - tag = Tag.new('tag', [], [], {}) - assert_equal 'liquid::tag', tag.name - assert_equal '', tag.render(Context.new) - end - - def test_store_context_at_parsing_time - tag = Tag.new('tag', [], [], { :foo => 'bar' }) - assert_equal 'bar', tag.context[:foo] - end - - def test_no_transform - assert_template_result('this text should come out of the template without change...', - 'this text should come out of the template without change...') - assert_template_result('blah','blah') - assert_template_result('','') - assert_template_result('|,.:','|,.:') - assert_template_result('','') - - text = %|this shouldnt see any transformation either but has multiple lines - as you can clearly see here ...| - assert_template_result(text,text) - end - def test_has_a_block_which_does_nothing assert_template_result(%|the comment block should be removed .. right?|, %|the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?|) @@ -202,6 +177,24 @@ def test_pause_resume_BIG_offset assert_template_result(expected,markup,assigns) end + def test_for_reversed + assigns = {'array' => [ 1, 2, 3] } + assert_template_result('321','{%for item in array reversed %}{{item}}{%endfor%}',assigns) + end + + + def test_ifchanged + assigns = {'array' => [ 1, 1, 2, 2, 3, 3] } + assert_template_result('123','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns) + + assigns = {'array' => [ 1, 1, 1, 1] } + assert_template_result('1','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns) + end + + + + # ASSIGNMENT + def test_assign assigns = {'var' => 'content' } assert_template_result('var2: var2:content','var2:{{var2}} {%assign var2 = var%} var2:{{var2}}',assigns) @@ -219,6 +212,22 @@ def test_assign_with_colon_and_spaces assert_template_result('var2: 1','{%assign var2 = var["a:b c"].paged %}var2: {{var2}}',assigns) end + def test_assign2 + assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render + end + + def test_assign_an_empty_string + assert_equal '', Liquid::Template.parse( '{% assign a = ""%}{{a}}' ).render + end + + def test_assign_is_global + assert_equal 'variable', Liquid::Template.parse( '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}' ).render + end + + + + + # CAPTURE def test_capture assigns = {'var' => 'content' } assert_template_result('content foo content foo ','{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', assigns) @@ -230,6 +239,8 @@ def test_capture_detects_bad_syntax end end + # CASE + def test_case assigns = {'condition' => 2 } assert_template_result(' its 2 ','{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', assigns) @@ -327,18 +338,6 @@ def test_case_when_comma assert_template_result('', code, {'condition' => 'something else' }) end - def test_assign - assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render - end - - def test_assign_an_empty_string - assert_equal '', Liquid::Template.parse( '{% assign a = ""%}{{a}}' ).render - end - - def test_assign_is_global - assert_equal 'variable', Liquid::Template.parse( '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}' ).render - end - def test_case_detects_bad_syntax assert_raise(SyntaxError) do assert_template_result('', '{% case false %}{% when %}true{% endcase %}', {}) @@ -351,6 +350,7 @@ def test_case_detects_bad_syntax end + # CYCLE def test_cycle @@ -377,6 +377,8 @@ def test_multiple_named_cycles_with_names_from_context assert_template_result('one one two two one one','{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', assigns) end + # SIZE + def test_size_of_array assigns = {"array" => [1,2,3,4]} assert_template_result('array has 4 elements', "array has {{ array.size }} elements", assigns) @@ -387,6 +389,8 @@ def test_size_of_hash assert_template_result('hash has 4 elements', "hash has {{ hash.size }} elements", assigns) end + # MISC + def test_illegal_symbols assert_template_result('', '{% if true == empty %}?{% endif %}', {}) assert_template_result('', '{% if true == null %}?{% endif %}', {}) @@ -394,17 +398,4 @@ def test_illegal_symbols assert_template_result('', '{% if null == true %}?{% endif %}', {}) end - def test_for_reversed - assigns = {'array' => [ 1, 2, 3] } - assert_template_result('321','{%for item in array reversed %}{{item}}{%endfor%}',assigns) - end - - - def test_ifchanged - assigns = {'array' => [ 1, 1, 2, 2, 3, 3] } - assert_template_result('123','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns) - - assigns = {'array' => [ 1, 1, 1, 1] } - assert_template_result('1','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns) - end end From 5276a7b4bb7cc1279c3643e816a5b54bdf7eb0b2 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Thu, 30 Sep 2010 04:06:29 -0700 Subject: [PATCH 39/61] Refactor tag specs --- spec/integration/output_spec.rb | 12 +- spec/integration/tag_spec.rb | 203 ++++++++++++++++ test/standard_tag_test.rb | 401 -------------------------------- 3 files changed, 213 insertions(+), 403 deletions(-) delete mode 100644 test/standard_tag_test.rb diff --git a/spec/integration/output_spec.rb b/spec/integration/output_spec.rb index 859dff606..5aa48eba8 100644 --- a/spec/integration/output_spec.rb +++ b/spec/integration/output_spec.rb @@ -10,7 +10,8 @@ let(:data) do { 'best_cars' => 'bmw', - 'car' => {'bmw' => 'good', 'gm' => 'bad'} + 'car' => {'bmw' => 'good', 'gm' => 'bad'}, + 'brands' => ['bmw', 'gm', 'ford'] } end @@ -32,7 +33,6 @@ def render(text) render(text).should == text end - it "should render a variable's value" do render(' {{best_cars}} ').should == " bmw " end @@ -41,6 +41,14 @@ def render(text) render(' {{car.bmw}} {{car.gm}} {{car.bmw}} ').should == " good bad good " end + it "should output an array's size" do + render('{{brands.size}}').should == "3" + end + + it "should output a hash's size" do + render('{{car.size}}').should == "2" + end + module FunnyFilter def make_funny(input) 'LOL' diff --git a/spec/integration/tag_spec.rb b/spec/integration/tag_spec.rb index 42f960a34..8e407affc 100644 --- a/spec/integration/tag_spec.rb +++ b/spec/integration/tag_spec.rb @@ -34,6 +34,15 @@ end end + describe "{% if %}" do + it "should allow illegal symbols in the condition" do + render('{% if true == empty %}hello{% endif %}').should == "" + render('{% if true == null %}hello{% endif %}').should == "" + render('{% if empty == true %}hello{% endif %}').should == "" + render('{% if null == true %}hello{% endif %}').should == "" + end + end + describe "for" do describe "{% for item in collection %}" do it "should repeat the block for each item in the collection" do @@ -241,10 +250,204 @@ data = {'array' => [ 1, 1, 1, 1] } render('{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',data).should == '1' + end + end + end + + describe "{% assign %}" do + + it "should assign a variable to a string" do + render('{%assign var = "yo" %} var:{{var}} ').should == " var:yo " + render("{%assign var = 'yo' %} var:{{var}} ").should == " var:yo " + render("{%assign var='yo' %} var:{{var}} ").should == " var:yo " + render("{%assign var='yo'%} var:{{var}} ").should == " var:yo " + + render('{%assign var="" %} var:{{var}} ').should == " var: " + render("{%assign var='' %} var:{{var}} ").should == " var: " + end + + it "should assign a variable to an integer" do + render('{%assign var = 1 %} var:{{var}} ').should == " var:1 " + render("{%assign var=1 %} var:{{var}} ").should == " var:1 " + render("{%assign var =1 %} var:{{var}} ").should == " var:1 " + end + + it "should assign a variable to a float" do + render('{%assign var = 1.011 %} var:{{var}} ').should == " var:1.011 " + render("{%assign var=1.011 %} var:{{var}} ").should == " var:1.011 " + render("{%assign var =1.011 %} var:{{var}} ").should == " var:1.011 " + end + + it "should assign a variable that includes a hyphen" do + render('{%assign a-b = "yo" %} {{a-b}} ').should == " yo " + render('{{a-b}}{%assign a-b = "yo" %} {{a-b}} ').should == " yo " + render('{%assign a-b = "yo" %} {{a-b}} {{a}} {{b}} ', 'a' => 1, 'b' => 2).should == " yo 1 2 " + end + + it "should assign a variable to a complex accessor" do + data = {'var' => {'a:b c' => {'paged' => '1' }}} + render('{%assign var2 = var["a:b c"].paged %}var2: {{var2}}', data).should == 'var2: 1' + end + + it "should assign var2 to 'hello' when var is 'hello'" do + data = {'var' => 'Hello' } + render('var2:{{var2}} {%assign var2 = var%} var2:{{var2}}',data).should == 'var2: var2:Hello' + end + + it "should assign the variable in a global context, even if it is in a block" do + render( '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}' ).should == "variable" + end + end + + context "{% capture %}" do + it "should capture the result of a block into a variable" do + data = {'var' => 'content' } + render('{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', data).should == 'content foo content foo ' + end + + it "should throw an error when it detects bad syntax" do + data = {'var' => 'content'} + expect { + render('{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', data) + }.to raise_error(Liquid::SyntaxError) + end + end + + context "{% case %}" do + it "should render the first block with a matching {% when %} argument" do + data = {'condition' => 1 } + render('{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', data).should == ' its 1 ' + + data = {'condition' => 2 } + render('{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', data).should == ' its 2 ' + + # dont render whitespace between case and first when + data = {'condition' => 2 } + render('{% case condition %} {% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', data).should == ' its 2 ' + end + + it "should match strings correctly" do + data = {'condition' => "string here" } + render('{% case condition %}{% when "string here" %} hit {% endcase %}', data).should == ' hit ' + + data = {'condition' => "bad string here" } + render('{% case condition %}{% when "string here" %} hit {% endcase %}', data).should == '' + end + + it "should not render anything if no matches found" do + data = {'condition' => 3 } + render(' {% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %} ', data).should == ' ' + end + + it "should evaluate variables and expressions" do + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => []).should == '' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1]).should == '1' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1]).should == '2' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1]).should == '' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1]).should == '' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1, 1]).should == '' + end + + it "should allow assignment from within a {% when %} block" do + # Example from the shopify forums + template = %q({% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}) + + render(template, "collection" => {'handle' => 'menswear-jackets'}).should == 'menswear' + render(template, "collection" => {'handle' => 'menswear-t-shirts'}) == 'menswear' + render(template, "collection" => {'handle' => 'x'}) == 'womenswear' + render(template, "collection" => {'handle' => 'y'}) == 'womenswear' + render(template, "collection" => {'handle' => 'z'}) == 'womenswear' + end + + it "should allow the use of 'or' to chain parameters with {% when %}" do + template = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + render(template, {'condition' => 1 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 2 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 3 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 4 }).should == ' its 4 ' + render(template, {'condition' => 5 }).should == '' + + template = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + render(template, 'condition' => 1).should == ' its 1 or 2 or 3 ' + render(template, 'condition' => 'string').should == ' its 1 or 2 or 3 ' + render(template, 'condition' => nil).should == ' its 1 or 2 or 3 ' + render(template, 'condition' => 'something else').should == '' + end + + it "should allow the use of commas to chain parameters with {% when %} " do + template = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + render(template, {'condition' => 1 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 2 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 3 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 4 }).should == ' its 4 ' + render(template, {'condition' => 5 }).should == '' + + template = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + render(template, 'condition' => 1).should == ' its 1 or 2 or 3 ' + render(template, 'condition' => 'string').should == ' its 1 or 2 or 3 ' + render(template, 'condition' => nil).should == ' its 1 or 2 or 3 ' + render(template, 'condition' => 'something else').should == '' + end + + it "should raise an error when theres bad syntax" do + expect { + render!('{% case false %}{% when %}true{% endcase %}') + }.to raise_error(Liquid::SyntaxError) + + expect { + render!('{% case false %}{% huh %}true{% endcase %}') + }.to raise_error(Liquid::SyntaxError) + end + + context "with {% else %}" do + it "should render the {% else %} block when no matches found" do + data = {'condition' => 5 } + render('{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', data).should == ' hit ' + + data = {'condition' => 6 } + render('{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', data).should == ' else ' + end + + it "should evaluate variables and expressions" do + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => []).should == 'else' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1]).should == '1' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1]).should == '2' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1]).should == 'else' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1]).should == 'else' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1, 1]).should == 'else' + + + render('{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "else" + render('{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "false" + render('{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "true" + render('{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "else" end end + end + + context "{% cycle %}" do + + it "should cycle through a list of strings" do + render('{%cycle "one", "two"%}').should == 'one' + render('{%cycle "one", "two"%} {%cycle "one", "two"%}').should == 'one two' + render('{%cycle "", "two"%} {%cycle "", "two"%}').should == ' two' + render('{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}').should == 'one two one' + render('{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}').should == 'text-align: left text-align: right' + end + + it "should keep track of multiple cycles" do + render('{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}').should == '1 2 1 1 2 3 1' + end + + it "should keep track of multiple named cycles" do + render('{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}').should == 'one one two two one one' + end + it "should allow multiple named cycles with names from context" do + data = {"var1" => 1, "var2" => 2 } + render('{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', data).should == 'one one two two one one' + end end diff --git a/test/standard_tag_test.rb b/test/standard_tag_test.rb deleted file mode 100644 index 21e5e17d8..000000000 --- a/test/standard_tag_test.rb +++ /dev/null @@ -1,401 +0,0 @@ -require File.dirname(__FILE__) + '/helper' - - -class StandardTagTest < Test::Unit::TestCase - include Liquid - - def test_has_a_block_which_does_nothing - assert_template_result(%|the comment block should be removed .. right?|, - %|the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?|) - - assert_template_result('','{%comment%}{%endcomment%}') - assert_template_result('','{%comment%}{% endcomment %}') - assert_template_result('','{% comment %}{%endcomment%}') - assert_template_result('','{% comment %}{% endcomment %}') - assert_template_result('','{%comment%}comment{%endcomment%}') - assert_template_result('','{% comment %}comment{% endcomment %}') - - assert_template_result('foobar','foo{%comment%}comment{%endcomment%}bar') - assert_template_result('foobar','foo{% comment %}comment{% endcomment %}bar') - assert_template_result('foobar','foo{%comment%} comment {%endcomment%}bar') - assert_template_result('foobar','foo{% comment %} comment {% endcomment %}bar') - - assert_template_result('foo bar','foo {%comment%} {%endcomment%} bar') - assert_template_result('foo bar','foo {%comment%}comment{%endcomment%} bar') - assert_template_result('foo bar','foo {%comment%} comment {%endcomment%} bar') - - assert_template_result('foobar','foo{%comment%} - {%endcomment%}bar') - end - - def test_for - assert_template_result(' yo yo yo yo ','{%for item in array%} yo {%endfor%}','array' => [1,2,3,4]) - assert_template_result('yoyo','{%for item in array%}yo{%endfor%}','array' => [1,2]) - assert_template_result(' yo ','{%for item in array%} yo {%endfor%}','array' => [1]) - assert_template_result('','{%for item in array%}{%endfor%}','array' => [1,2]) - expected = < [1,2,3]) - end - - def test_for_with_range - assert_template_result(' 1 2 3 ','{%for item in (1..3) %} {{item}} {%endfor%}') - end - - def test_for_with_variable - assert_template_result(' 1 2 3 ','{%for item in array%} {{item}} {%endfor%}','array' => [1,2,3]) - assert_template_result('123','{%for item in array%}{{item}}{%endfor%}','array' => [1,2,3]) - assert_template_result('123','{% for item in array %}{{item}}{% endfor %}','array' => [1,2,3]) - assert_template_result('abcd','{%for item in array%}{{item}}{%endfor%}','array' => ['a','b','c','d']) - assert_template_result('a b c','{%for item in array%}{{item}}{%endfor%}','array' => ['a',' ','b',' ','c']) - assert_template_result('abc','{%for item in array%}{{item}}{%endfor%}','array' => ['a','','b','','c']) - end - - def test_for_helpers - assigns = {'array' => [1,2,3] } - assert_template_result(' 1/3 2/3 3/3 ','{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',assigns) - assert_template_result(' 1 2 3 ','{%for item in array%} {{forloop.index}} {%endfor%}',assigns) - assert_template_result(' 0 1 2 ','{%for item in array%} {{forloop.index0}} {%endfor%}',assigns) - assert_template_result(' 2 1 0 ','{%for item in array%} {{forloop.rindex0}} {%endfor%}',assigns) - assert_template_result(' 3 2 1 ','{%for item in array%} {{forloop.rindex}} {%endfor%}',assigns) - assert_template_result(' true false false ','{%for item in array%} {{forloop.first}} {%endfor%}',assigns) - assert_template_result(' false false true ','{%for item in array%} {{forloop.last}} {%endfor%}',assigns) - end - - def test_for_and_if - assigns = {'array' => [1,2,3] } - assert_template_result('+--', '{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}', assigns) - end - - def test_limiting - assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]} - assert_template_result('12','{%for i in array limit:2 %}{{ i }}{%endfor%}',assigns) - assert_template_result('1234','{%for i in array limit:4 %}{{ i }}{%endfor%}',assigns) - assert_template_result('3456','{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}',assigns) - assert_template_result('3456','{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}',assigns) - end - - def test_dynamic_variable_limiting - assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]} - assigns['limit'] = 2 - assigns['offset'] = 2 - assert_template_result('34','{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}',assigns) - end - - def test_nested_for - assigns = {'array' => [[1,2],[3,4],[5,6]] } - assert_template_result('123456','{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}',assigns) - end - - def test_offset_only - assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]} - assert_template_result('890','{%for i in array offset:7 %}{{ i }}{%endfor%}',assigns) - end - - def test_pause_resume - assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} - markup = <<-MKUP - {%for i in array.items limit: 3 %}{{i}}{%endfor%} - next - {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} - next - {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} - MKUP - expected = <<-XPCTD - 123 - next - 456 - next - 789 - XPCTD - assert_template_result(expected,markup,assigns) - end - - def test_pause_resume_limit - assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} - markup = <<-MKUP - {%for i in array.items limit:3 %}{{i}}{%endfor%} - next - {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} - next - {%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%} - MKUP - expected = <<-XPCTD - 123 - next - 456 - next - 7 - XPCTD - assert_template_result(expected,markup,assigns) - end - - def test_pause_resume_BIG_limit - assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} - markup = <<-MKUP - {%for i in array.items limit:3 %}{{i}}{%endfor%} - next - {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} - next - {%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%} - MKUP - expected = <<-XPCTD - 123 - next - 456 - next - 7890 - XPCTD - assert_template_result(expected,markup,assigns) - end - - - def test_pause_resume_BIG_offset - assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} - markup = %q({%for i in array.items limit:3 %}{{i}}{%endfor%} - next - {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} - next - {%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%}) - expected = %q(123 - next - 456 - next - ) - assert_template_result(expected,markup,assigns) - end - - def test_for_reversed - assigns = {'array' => [ 1, 2, 3] } - assert_template_result('321','{%for item in array reversed %}{{item}}{%endfor%}',assigns) - end - - - def test_ifchanged - assigns = {'array' => [ 1, 1, 2, 2, 3, 3] } - assert_template_result('123','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns) - - assigns = {'array' => [ 1, 1, 1, 1] } - assert_template_result('1','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns) - end - - - - # ASSIGNMENT - - def test_assign - assigns = {'var' => 'content' } - assert_template_result('var2: var2:content','var2:{{var2}} {%assign var2 = var%} var2:{{var2}}',assigns) - - end - - def test_hyphenated_assign - assigns = {'a-b' => '1' } - assert_template_result('a-b:1 a-b:2','a-b:{{a-b}} {%assign a-b = 2 %}a-b:{{a-b}}',assigns) - - end - - def test_assign_with_colon_and_spaces - assigns = {'var' => {'a:b c' => {'paged' => '1' }}} - assert_template_result('var2: 1','{%assign var2 = var["a:b c"].paged %}var2: {{var2}}',assigns) - end - - def test_assign2 - assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render - end - - def test_assign_an_empty_string - assert_equal '', Liquid::Template.parse( '{% assign a = ""%}{{a}}' ).render - end - - def test_assign_is_global - assert_equal 'variable', Liquid::Template.parse( '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}' ).render - end - - - - - # CAPTURE - def test_capture - assigns = {'var' => 'content' } - assert_template_result('content foo content foo ','{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', assigns) - end - - def test_capture_detects_bad_syntax - assert_raise(SyntaxError) do - assert_template_result('content foo content foo ','{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', {'var' => 'content' }) - end - end - - # CASE - - def test_case - assigns = {'condition' => 2 } - assert_template_result(' its 2 ','{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', assigns) - - assigns = {'condition' => 1 } - assert_template_result(' its 1 ','{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', assigns) - - assigns = {'condition' => 3 } - assert_template_result('','{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', assigns) - - assigns = {'condition' => "string here" } - assert_template_result(' hit ','{% case condition %}{% when "string here" %} hit {% endcase %}', assigns) - - assigns = {'condition' => "bad string here" } - assert_template_result('','{% case condition %}{% when "string here" %} hit {% endcase %}', assigns) - end - - def test_case_with_else - - assigns = {'condition' => 5 } - assert_template_result(' hit ','{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', assigns) - - assigns = {'condition' => 6 } - assert_template_result(' else ','{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', assigns) - - assigns = {'condition' => 6 } - assert_template_result(' else ','{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}', assigns) - - - end - - def test_case_on_size - assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => []) - assert_template_result('1' , '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1]) - assert_template_result('2' , '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1]) - assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1]) - assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1]) - assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1, 1]) - end - - def test_case_on_size_with_else - assert_template_result('else', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => []) - assert_template_result('1', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1]) - assert_template_result('2', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1]) - assert_template_result('else', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1]) - assert_template_result('else', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1]) - assert_template_result('else', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1, 1]) - end - - def test_case_on_length_with_else - assert_template_result('else', '{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}) - assert_template_result('false', '{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}) - assert_template_result('true', '{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}) - assert_template_result('else', '{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}) - end - - def test_assign_from_case - # Example from the shopify forums - code = %q({% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}) - template = Liquid::Template.parse(code) - assert_equal "menswear", template.render("collection" => {'handle' => 'menswear-jackets'}) - assert_equal "menswear", template.render("collection" => {'handle' => 'menswear-t-shirts'}) - assert_equal "womenswear", template.render("collection" => {'handle' => 'x'}) - assert_equal "womenswear", template.render("collection" => {'handle' => 'y'}) - assert_equal "womenswear", template.render("collection" => {'handle' => 'z'}) - end - - def test_case_when_or - code = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' - assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 }) - assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 2 }) - assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 3 }) - assert_template_result(' its 4 ', code, {'condition' => 4 }) - assert_template_result('', code, {'condition' => 5 }) - - code = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' - assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 }) - assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 'string' }) - assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => nil }) - assert_template_result('', code, {'condition' => 'something else' }) - end - - def test_case_when_comma - code = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' - assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 }) - assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 2 }) - assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 3 }) - assert_template_result(' its 4 ', code, {'condition' => 4 }) - assert_template_result('', code, {'condition' => 5 }) - - code = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' - assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 }) - assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 'string' }) - assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => nil }) - assert_template_result('', code, {'condition' => 'something else' }) - end - - def test_case_detects_bad_syntax - assert_raise(SyntaxError) do - assert_template_result('', '{% case false %}{% when %}true{% endcase %}', {}) - end - - assert_raise(SyntaxError) do - assert_template_result('', '{% case false %}{% huh %}true{% endcase %}', {}) - end - - end - - - # CYCLE - - def test_cycle - - assert_template_result('one','{%cycle "one", "two"%}') - assert_template_result('one two','{%cycle "one", "two"%} {%cycle "one", "two"%}') - assert_template_result(' two','{%cycle "", "two"%} {%cycle "", "two"%}') - - assert_template_result('one two one','{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}') - - assert_template_result('text-align: left text-align: right','{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}') - - end - - def test_multiple_cycles - assert_template_result('1 2 1 1 2 3 1','{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}') - end - - def test_multiple_named_cycles - assert_template_result('one one two two one one','{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}') - end - - def test_multiple_named_cycles_with_names_from_context - assigns = {"var1" => 1, "var2" => 2 } - assert_template_result('one one two two one one','{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', assigns) - end - - # SIZE - - def test_size_of_array - assigns = {"array" => [1,2,3,4]} - assert_template_result('array has 4 elements', "array has {{ array.size }} elements", assigns) - end - - def test_size_of_hash - assigns = {"hash" => {:a => 1, :b => 2, :c=> 3, :d => 4}} - assert_template_result('hash has 4 elements', "hash has {{ hash.size }} elements", assigns) - end - - # MISC - - def test_illegal_symbols - assert_template_result('', '{% if true == empty %}?{% endif %}', {}) - assert_template_result('', '{% if true == null %}?{% endif %}', {}) - assert_template_result('', '{% if empty == true %}?{% endif %}', {}) - assert_template_result('', '{% if null == true %}?{% endif %}', {}) - end - -end From 893a5f9c4da2189708a4912b12d9ab84ccf2c5f7 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Thu, 30 Sep 2010 04:07:51 -0700 Subject: [PATCH 40/61] Adding RSpec to Rakefile --- Rakefile | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 005a30041..bb5be7580 100755 --- a/Rakefile +++ b/Rakefile @@ -1,9 +1,25 @@ #!/usr/bin/env ruby require 'rubygems' + +require "bundler" +Bundler.setup + require 'rake' require 'rake/gempackagetask' -task :default => 'spec' +require "rspec" +require "rspec/core/rake_task" + +Rspec::Core::RakeTask.new(:spec) do |spec| + spec.pattern = "spec/**/*_spec.rb" +end + +Rspec::Core::RakeTask.new('spec:progress') do |spec| + spec.rspec_opts = %w(--format progress) + spec.pattern = "spec/**/*_spec.rb" +end + +task :default => :spec gemspec = eval(File.read('locomotive_liquid.gemspec')) Rake::GemPackageTask.new(gemspec) do |pkg| From 448ba2f029878831a397856075867e89745880ee Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Thu, 30 Sep 2010 04:14:16 -0700 Subject: [PATCH 41/61] cleaning test folder --- Rakefile | 13 +++++++- spec/spec_helper.rb | 18 +++++++++++ {test/extra => spec/support}/breakpoint.rb | 0 {test/extra => spec/support}/caller.rb | 0 test/helper.rb | 20 ------------ test/test_helper.rb | 36 ---------------------- 6 files changed, 30 insertions(+), 57 deletions(-) rename {test/extra => spec/support}/breakpoint.rb (100%) rename {test/extra => spec/support}/caller.rb (100%) delete mode 100644 test/helper.rb delete mode 100644 test/test_helper.rb diff --git a/Rakefile b/Rakefile index bb5be7580..2384f8a66 100755 --- a/Rakefile +++ b/Rakefile @@ -10,10 +10,21 @@ require 'rake/gempackagetask' require "rspec" require "rspec/core/rake_task" -Rspec::Core::RakeTask.new(:spec) do |spec| +Rspec::Core::RakeTask.new("spec") do |spec| spec.pattern = "spec/**/*_spec.rb" end +desc "Run the Integration Specs (rendering)" +Rspec::Core::RakeTask.new("spec:integration") do |spec| + spec.pattern = "spec/unit/*_spec.rb" +end + +desc "Run the Unit Specs" +Rspec::Core::RakeTask.new("spec:unit") do |spec| + spec.pattern = "spec/unit/*_spec.rb" +end + +desc "Run all the specs without all the verbose spec output" Rspec::Core::RakeTask.new('spec:progress') do |spec| spec.rspec_opts = %w(--format progress) spec.pattern = "spec/**/*_spec.rb" diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 528463fad..c02a1e35d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,6 +14,9 @@ require 'rspec' +# add support to load path +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "support")) + # load support helpers Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} @@ -34,6 +37,21 @@ def parse(body = nil) Liquid::Template.parse(body) end + def print_child(node, depth = 0) + information = (case node + when Liquid::InheritedBlock + "Liquid::InheritedBlock #{node.object_id} / #{node.name} / #{!node.parent.nil?} / #{node.nodelist.first.inspect}" + else + node.class.name + end) + + puts information.insert(0, ' ' * (depth * 2)) + if node.respond_to?(:nodelist) + node.nodelist.each do |node| + print_child node, depth + 1 + end + end + end end end diff --git a/test/extra/breakpoint.rb b/spec/support/breakpoint.rb similarity index 100% rename from test/extra/breakpoint.rb rename to spec/support/breakpoint.rb diff --git a/test/extra/caller.rb b/spec/support/caller.rb similarity index 100% rename from test/extra/caller.rb rename to spec/support/caller.rb diff --git a/test/helper.rb b/test/helper.rb deleted file mode 100644 index deb818594..000000000 --- a/test/helper.rb +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env ruby -$LOAD_PATH.unshift(File.dirname(__FILE__)+ '/extra') - -require 'test/unit' -require 'test/unit/assertions' -require 'caller' -require 'breakpoint' -require File.dirname(__FILE__) + '/../lib/liquid' - - -module Test - module Unit - module Assertions - include Liquid - def assert_template_result(expected, template, assigns={}, message=nil) - assert_equal expected, Template.parse(template).render(assigns) - end - end - end -end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb deleted file mode 100644 index 0ce7c21ea..000000000 --- a/test/test_helper.rb +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env ruby -$LOAD_PATH.unshift(File.dirname(__FILE__)+ '/extra') - -require 'test/unit' -require 'test/unit/assertions' -require 'caller' -require 'breakpoint' -require File.dirname(__FILE__) + '/../lib/liquid' - - -module Test - module Unit - module Assertions - include Liquid - def assert_template_result(expected, template, assigns={}, message=nil) - assert_equal expected, Template.parse(template).render(assigns) - end - end - end -end - -def print_child(node, depth = 0) - information = (case node - when Liquid::InheritedBlock - "Liquid::InheritedBlock #{node.object_id} / #{node.name} / #{!node.parent.nil?} / #{node.nodelist.first.inspect}" - else - node.class.name - end) - - puts information.insert(0, ' ' * (depth * 2)) - if node.respond_to?(:nodelist) - node.nodelist.each do |node| - print_child node, depth + 1 - end - end -end \ No newline at end of file From cd5e7b76e4c09ae0f9e09a0f9bfa6305fb7680eb Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Fri, 1 Oct 2010 09:58:59 -0700 Subject: [PATCH 42/61] Additional specs and cleanup --- History.txt | 12 ++++---- spec/integration/extends_spec.rb | 40 +++++++++++++++++++++---- spec/integration/if_else_unless_spec.rb | 8 ++--- spec/spec_helper.rb | 11 +++++-- 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/History.txt b/History.txt index 2f1c37f65..a4389c1f3 100644 --- a/History.txt +++ b/History.txt @@ -7,7 +7,7 @@ Before 1.9.0 * Added If with or / and expressions -* Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods. +* Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods. * Added more tags to standard library @@ -26,17 +26,17 @@ Before 1.9.0 * Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond] * Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ArgumentSeparator: , by default [Paul Hammond] - - {{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }} - -* Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke] + {{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }} + + +* Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke] class ProductDrop < Liquid::Drop def top_sales Shop.current.products.find(:all, :order => 'sales', :limit => 10 ) end - end + end t = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {% endfor %} ' ) t.render('product' => ProductDrop.new ) diff --git a/spec/integration/extends_spec.rb b/spec/integration/extends_spec.rb index 359e66751..5f414ac82 100644 --- a/spec/integration/extends_spec.rb +++ b/spec/integration/extends_spec.rb @@ -18,8 +18,35 @@ def read_template_file(template_path) it "should allow extending a path" do @templates['parent-template'] = "Hurrah!" - template = Liquid::Template.parse "{% extends parent-template %}" - template.render.should == "Hurrah!" + output = render("{% extends parent-template %}") + output.should == "Hurrah!" + end + + it "should allow include blocks within the parent template" do + @templates['partial1'] = "[Partial Content1]" + @templates['partial2'] = "[Partial Content2]" + @templates['parent-with-include'] = multiline_string(<<-END) + | {% include 'partial1' %} + | {% block thing %}{% include 'partial2' %}{% endblock %} + END + + # check with overridden block + output = render multiline_string(<<-END) + | {% extends parent-with-include %} + | {% block thing %}[Overridden Block]{% endblock %} + END + + output.should == multiline_string(<<-END) + | [Partial Content1] + | [Overridden Block] + END + + # check includes within the parent's default block + output = render("{% extends parent-with-include %}") + output.should == multiline_string(<<-END) + | [Partial Content1] + | [Partial Content2] + END end # TODO @@ -31,16 +58,16 @@ def read_template_file(template_path) it "should allow access to the context from the inherited template" do @templates['parent-with-variable'] = "Hello, {{ name }}!" - template = Liquid::Template.parse "{% extends parent-with-variable %}" - template.render('name' => 'Joe').should == "Hello, Joe!" + output = render("{% extends parent-with-variable %}", 'name' => 'Joe') + output.should == "Hello, Joe!" end it "should allow deep nesting of inherited templates" do @templates['parent-with-variable'] = "Hello, {{ name }}!!" @templates['parent-with-parent'] = "{% extends parent-with-variable %}" - template = Liquid::Template.parse "{% extends parent-with-parent %}" - template.render('name' => 'Joe').should == "Hello, Joe!!" + output = render("{% extends parent-with-parent %}", 'name' => 'Joe') + output.should == "Hello, Joe!!" end context "inherited blocks" do @@ -68,5 +95,6 @@ def read_template_file(template_path) output.should == 'Output / Deep: Hello, World! -BAR-' end end + end end \ No newline at end of file diff --git a/spec/integration/if_else_unless_spec.rb b/spec/integration/if_else_unless_spec.rb index 5953f6dab..ba65ec2f8 100644 --- a/spec/integration/if_else_unless_spec.rb +++ b/spec/integration/if_else_unless_spec.rb @@ -3,7 +3,7 @@ describe "Liquid Rendering" do describe "If/Else/Unless" do - describe "if" do + describe "{% if %}" do it "should show/hide content correctly when passed a boolean constant" do render(' {% if false %} this text should not go into the output {% endif %} ').should == " " render(' {% if true %} this text should not go into the output {% endif %} ').should == " this text should not go into the output " @@ -142,7 +142,7 @@ end end - describe "if/else" do + describe "{% if %} {% else %}" do it "should render the right block based on the input" do render('{% if false %} NO {% else %} YES {% endif %}').should == " YES " render('{% if true %} YES {% else %} NO {% endif %}').should == " YES " @@ -157,7 +157,7 @@ end end - describe "unless" do + describe "{% unless %}" do it "should show/hide content correctly when passed a boolean constant" do render(' {% unless true %} this text should not go into the output {% endunless %} ').should == ' ' @@ -177,7 +177,7 @@ end - describe "unless/else" do + describe "{% unless %} {% else %}" do it "should show/hide the section based on the passed in data" do render('{% unless true %} NO {% else %} YES {% endunless %}').should == ' YES ' render('{% unless false %} YES {% else %} NO {% endunless %}').should == ' YES ' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c02a1e35d..14c75522a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,7 @@ require "bundler" Bundler.setup +# add spec folder to load path $LOAD_PATH.unshift(File.join(File.dirname(__FILE__))) # add lib to load path @@ -20,23 +21,28 @@ # load support helpers Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} - +# Liquid Helpers for use within specs module Liquid module SpecHelpers + + # shortcut to render a template def render(body, *args) + body = eval(subject) if body == :subject Liquid::Template.parse(body).render(*args) end def render!(body, *args) + body = eval(subject) if body == :subject Liquid::Template.parse(body).render!(*args) end + # shortcut to parse a template def parse(body = nil) body = eval(subject) if body == :subject - Liquid::Template.parse(body) end + # helper to output a node's information def print_child(node, depth = 0) information = (case node when Liquid::InheritedBlock @@ -52,7 +58,6 @@ def print_child(node, depth = 0) end end end - end end From 78d5dad4d830acaa16fad37dfd794aa49c9a6b32 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Fri, 1 Oct 2010 10:01:48 -0700 Subject: [PATCH 43/61] cleanup whitespace --- example/server/example_servlet.rb | 20 ++--- example/server/liquid_servlet.rb | 14 +-- example/server/templates/products.liquid | 56 ++++++------ lib/extras/liquid_view.rb | 16 ++-- lib/liquid/errors.rb | 2 +- lib/liquid/file_system.rb | 20 ++--- lib/liquid/standardfilters.rb | 106 +++++++++++------------ lib/liquid/tags/comment.rb | 8 +- lib/liquid/tags/ifchanged.rb | 16 ++-- lib/liquid/tags/unless.rb | 18 ++-- 10 files changed, 138 insertions(+), 138 deletions(-) diff --git a/example/server/example_servlet.rb b/example/server/example_servlet.rb index 18e528e95..658cef70b 100644 --- a/example/server/example_servlet.rb +++ b/example/server/example_servlet.rb @@ -2,36 +2,36 @@ module ProductsFilter def price(integer) sprintf("$%.2d USD", integer / 100.0) end - + def prettyprint(text) text.gsub( /\*(.*)\*/, '\1' ) end - + def count(array) array.size end - + def paragraph(p) "

#{p}

" end end class Servlet < LiquidServlet - + def index { 'date' => Time.now } end - - def products - { 'products' => products_list, 'section' => 'Snowboards', 'cool_products' => true} + + def products + { 'products' => products_list, 'section' => 'Snowboards', 'cool_products' => true} end - + private - + def products_list [{'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' }, {'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling'}, {'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity'}] end - + end \ No newline at end of file diff --git a/example/server/liquid_servlet.rb b/example/server/liquid_servlet.rb index 8f24f0026..b9d5cf6b8 100644 --- a/example/server/liquid_servlet.rb +++ b/example/server/liquid_servlet.rb @@ -7,21 +7,21 @@ def do_GET(req, res) def do_POST(req, res) handle(:post, req, res) end - + private - + def handle(type, req, res) @request, @response = req, res - + @request.path_info =~ /(\w+)$/ - @action = $1 || 'index' - @assigns = send(@action) if respond_to?(@action) + @action = $1 || 'index' + @assigns = send(@action) if respond_to?(@action) @response['Content-Type'] = "text/html" @response.status = 200 - @response.body = Liquid::Template.parse(read_template).render(@assigns, :filters => [ProductsFilter]) + @response.body = Liquid::Template.parse(read_template).render(@assigns, :filters => [ProductsFilter]) end - + def read_template(filename = @action) File.read( File.dirname(__FILE__) + "/templates/#{filename}.liquid" ) end diff --git a/example/server/templates/products.liquid b/example/server/templates/products.liquid index 05af4f7de..c7de1929c 100644 --- a/example/server/templates/products.liquid +++ b/example/server/templates/products.liquid @@ -1,45 +1,45 @@ - + - - - - - products - - - - - - - - - - -

There are currently {{products | count}} products in the {{section}} catalog

+ + + + + products + + + + + + + + + + +

There are currently {{products | count}} products in the {{section}} catalog

{% if cool_products %} - Cool products :) + Cool products :) {% else %} - Uncool products :( + Uncool products :( {% endif %}
    - + {% for product in products %}
  • {{product.name}}

    Only {{product.price | price }} - + {{product.description | prettyprint | paragraph }} - + {{ 'it rocks!' | paragraph }} - -
  • + + {% endfor %} - +
- - + + diff --git a/lib/extras/liquid_view.rb b/lib/extras/liquid_view.rb index 6b983be72..d1fe19a9d 100644 --- a/lib/extras/liquid_view.rb +++ b/lib/extras/liquid_view.rb @@ -2,15 +2,15 @@ # and use liquid as an template system for .liquid files # # Example -# +# # ActionView::Base::register_template_handler :liquid, LiquidView class LiquidView PROTECTED_ASSIGNS = %w( template_root response _session template_class action_name request_origin session template _response url _request _cookies variables_added _flash params _headers request cookies ignore_missing_templates flash _params logger before_filter_chain_aborted headers ) - PROTECTED_INSTANCE_VARIABLES = %w( @_request @controller @_first_render @_memoized__pick_template @view_paths + PROTECTED_INSTANCE_VARIABLES = %w( @_request @controller @_first_render @_memoized__pick_template @view_paths @helpers @assigns_added @template @_render_stack @template_format @assigns ) - + def self.call(template) "LiquidView.new(self).render(template, local_assigns)" end @@ -18,10 +18,10 @@ def self.call(template) def initialize(view) @view = view end - + def render(template, local_assigns = nil) @view.controller.headers["Content-Type"] ||= 'text/html; charset=utf-8' - + # Rails 2.2 Template has source, but not locals if template.respond_to?(:source) && !template.respond_to?(:locals) assigns = (@view.instance_variables - PROTECTED_INSTANCE_VARIABLES).inject({}) do |hash, ivar| @@ -31,15 +31,15 @@ def render(template, local_assigns = nil) else assigns = @view.assigns.reject{ |k,v| PROTECTED_ASSIGNS.include?(k) } end - + source = template.respond_to?(:source) ? template.source : template local_assigns = (template.respond_to?(:locals) ? template.locals : local_assigns) || {} - + if content_for_layout = @view.instance_variable_get("@content_for_layout") assigns['content_for_layout'] = content_for_layout end assigns.merge!(local_assigns.stringify_keys) - + liquid = Liquid::Template.parse(source) liquid.render(assigns, :filters => [@view.controller.master_helper_module], :registers => {:action_view => @view, :controller => @view.controller}) end diff --git a/lib/liquid/errors.rb b/lib/liquid/errors.rb index ce4ca7e67..d452e58f0 100644 --- a/lib/liquid/errors.rb +++ b/lib/liquid/errors.rb @@ -1,6 +1,6 @@ module Liquid class Error < ::StandardError; end - + class ArgumentError < Error; end class ContextError < Error; end class FilterNotFound < Error; end diff --git a/lib/liquid/file_system.rb b/lib/liquid/file_system.rb index 8c6b76ddd..a56f273ff 100644 --- a/lib/liquid/file_system.rb +++ b/lib/liquid/file_system.rb @@ -1,7 +1,7 @@ module Liquid # A Liquid file system is way to let your templates retrieve other templates for use with the include tag. # - # You can implement subclasses that retrieve templates from the database, from the file system using a different + # You can implement subclasses that retrieve templates from the database, from the file system using a different # path structure, you can provide them as hard-coded inline strings, or any manner that you see fit. # # You can add additional instance variables, arguments, or methods as needed. @@ -18,7 +18,7 @@ def read_template_file(template_path) raise FileSystemError, "This liquid context does not allow includes." end end - + # This implements an abstract file system which retrieves template files named in a manner similar to Rails partials, # ie. with the template name prefixed with an underscore. The extension ".liquid" is also added. # @@ -27,35 +27,35 @@ def read_template_file(template_path) # Example: # # file_system = Liquid::LocalFileSystem.new("/some/path") - # + # # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid" # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid" # class LocalFileSystem attr_accessor :root - + def initialize(root) @root = root end - + def read_template_file(template_path) full_path = full_path(template_path) raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path) - + File.read(full_path) end - + def full_path(template_path) raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/ - + full_path = if template_path.include?('/') File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid") else File.join(root, "_#{template_path}.liquid") end - + raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/ - + full_path end end diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index 0f08264cd..fc1f18c4a 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -1,36 +1,36 @@ require 'cgi' module Liquid - + module StandardFilters - + # Return the size of an array or of an string def size(input) - + input.respond_to?(:size) ? input.size : 0 - end - + end + # convert a input string to DOWNCASE def downcase(input) input.to_s.downcase - end + end # convert a input string to UPCASE def upcase(input) input.to_s.upcase end - + # capitalize words in the input centence def capitalize(input) input.to_s.capitalize end - + def escape(input) CGI.escapeHTML(input) rescue input end - + alias_method :h, :escape - + # Truncate a string down to x characters def truncate(input, length = 50, truncate_string = "...") if input.nil? then return end @@ -44,19 +44,19 @@ def truncatewords(input, words = 15, truncate_string = "...") wordlist = input.to_s.split l = words.to_i - 1 l = 0 if l < 0 - wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input + wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input end - + def strip_html(input) input.to_s.gsub(//, '').gsub(/<.*?>/, '') - end - + end + # Remove all newlines from the string - def strip_newlines(input) - input.to_s.gsub(/\n/, '') + def strip_newlines(input) + input.to_s.gsub(/\n/, '') end - - + + # Join elements of the array with certain character between them def join(input, glue = ' ') [input].flatten.join(glue) @@ -73,8 +73,8 @@ def sort(input, property = nil) elsif ary.first.respond_to?(property) ary.sort {|a,b| a.send(property) <=> b.send(property) } end - end - + end + # map/collect on a given property def map(input, property) ary = [input].flatten @@ -84,42 +84,42 @@ def map(input, property) ary.map {|e| e.send(property) } end end - + # Replace occurrences of a string with another def replace(input, string, replacement = '') input.to_s.gsub(string, replacement) end - + # Replace the first occurrences of a string with another def replace_first(input, string, replacement = '') input.to_s.sub(string, replacement) - end - + end + # remove a substring def remove(input, string) - input.to_s.gsub(string, '') + input.to_s.gsub(string, '') end - + # remove the first occurrences of a substring def remove_first(input, string) - input.to_s.sub(string, '') - end - + input.to_s.sub(string, '') + end + # add one string to another def append(input, string) input.to_s + string.to_s end - + # prepend a string to another def prepend(input, string) string.to_s + input.to_s end - + # Add
tags in front of all newlines in input string - def newline_to_br(input) - input.to_s.gsub(/\n/, "
\n") + def newline_to_br(input) + input.to_s.gsub(/\n/, "
\n") end - + # Reformat a date # # %a - The abbreviated weekday name (``Sun'') @@ -149,62 +149,62 @@ def newline_to_br(input) # %Z - Time zone name # %% - Literal ``%'' character def date(input, format) - + if format.to_s.empty? return input.to_s end - + date = input.is_a?(String) ? Time.parse(input) : input - + if date.respond_to?(:strftime) date.strftime(format.to_s) else input end - rescue => e + rescue => e input end - - # Get the first element of the passed in array - # + + # Get the first element of the passed in array + # # Example: # {{ product.images | first | to_img }} - # + # def first(array) array.first if array.respond_to?(:first) end - # Get the last element of the passed in array - # + # Get the last element of the passed in array + # # Example: # {{ product.images | last | to_img }} - # + # def last(array) array.last if array.respond_to?(:last) end - + # addition def plus(input, operand) to_number(input) + to_number(operand) end - + # subtraction def minus(input, operand) to_number(input) - to_number(operand) end - + # multiplication def times(input, operand) to_number(input) * to_number(operand) end - + # division def divided_by(input, operand) to_number(input) / to_number(operand) end - + private - + def to_number(obj) case obj when Numeric @@ -215,8 +215,8 @@ def to_number(obj) 0 end end - + end - + Template.register_filter(StandardFilters) end diff --git a/lib/liquid/tags/comment.rb b/lib/liquid/tags/comment.rb index 8ce7e0eb8..ef196d7a8 100644 --- a/lib/liquid/tags/comment.rb +++ b/lib/liquid/tags/comment.rb @@ -1,9 +1,9 @@ module Liquid - class Comment < Block + class Comment < Block def render(context) '' - end + end end - - Template.register_tag('comment', Comment) + + Template.register_tag('comment', Comment) end \ No newline at end of file diff --git a/lib/liquid/tags/ifchanged.rb b/lib/liquid/tags/ifchanged.rb index a4406c6f6..d7dafc043 100644 --- a/lib/liquid/tags/ifchanged.rb +++ b/lib/liquid/tags/ifchanged.rb @@ -1,20 +1,20 @@ module Liquid class Ifchanged < Block - + def render(context) - context.stack do - + context.stack do + output = render_all(@nodelist, context) - + if output != context.registers[:ifchanged] context.registers[:ifchanged] = output output else '' - end + end end end - end - - Template.register_tag('ifchanged', Ifchanged) + end + + Template.register_tag('ifchanged', Ifchanged) end \ No newline at end of file diff --git a/lib/liquid/tags/unless.rb b/lib/liquid/tags/unless.rb index 74a76abda..912d96e25 100644 --- a/lib/liquid/tags/unless.rb +++ b/lib/liquid/tags/unless.rb @@ -9,25 +9,25 @@ module Liquid class Unless < If def render(context) context.stack do - + # First condition is interpreted backwards ( if not ) block = @blocks.first unless block.evaluate(context) - return render_all(block.attachment, context) + return render_all(block.attachment, context) end - + # After the first condition unless works just like if @blocks[1..-1].each do |block| - if block.evaluate(context) - return render_all(block.attachment, context) + if block.evaluate(context) + return render_all(block.attachment, context) end - end - + end + '' end - end + end end - + Template.register_tag('unless', Unless) end \ No newline at end of file From 093fe3ee05784fd91088cd62d968e0d86fef8dbf Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Fri, 1 Oct 2010 10:22:19 -0700 Subject: [PATCH 44/61] Adding strainger spec --- Gemfile | 4 +++- locomotive_liquid.gemspec | 2 +- spec/unit/strainer_spec.rb | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 05217eb49..83b005d02 100644 --- a/Gemfile +++ b/Gemfile @@ -3,4 +3,6 @@ source :rubygems gem "rspec", ">= 2.0.0.beta.22" gem "cucumber", ">= 0.9.0" -gem "ruby-debug" \ No newline at end of file +platforms :ruby_18 do + gem "ruby-debug" +end diff --git a/locomotive_liquid.gemspec b/locomotive_liquid.gemspec index 00e73d473..5bb5684f3 100644 --- a/locomotive_liquid.gemspec +++ b/locomotive_liquid.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "locomotive_liquid" - s.version = "2.1.3" + s.version = "2.2.2" s.required_rubygems_version = ">= 1.3.6" s.authors = ["Tobias Luetke", "Didier Lafforgue", "Jacques Crocker"] diff --git a/spec/unit/strainer_spec.rb b/spec/unit/strainer_spec.rb index 97a75484e..8c3b90eb7 100644 --- a/spec/unit/strainer_spec.rb +++ b/spec/unit/strainer_spec.rb @@ -21,5 +21,9 @@ module Liquid strainer.respond_to?('size', false).should be_true end + it "should repond_to_missing properly" do + strainer.respond_to?(:respond_to_missing?).should == Object.respond_to?(:respond_to_missing?) + end + end end \ No newline at end of file From ff43af8a021e5462b46112846c678ac64acbbb30 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Fri, 1 Oct 2010 10:23:18 -0700 Subject: [PATCH 45/61] Strainer - respond_to_missing? is now a required method Conflicts: test/strainer_test.rb --- lib/liquid/strainer.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/liquid/strainer.rb b/lib/liquid/strainer.rb index b6dad262e..95e8c0ee9 100644 --- a/lib/liquid/strainer.rb +++ b/lib/liquid/strainer.rb @@ -16,6 +16,9 @@ class Strainer < parent_object #:nodoc: INTERNAL_METHOD = /^__/ @@required_methods = Set.new([:__id__, :__send__, :respond_to?, :extend, :methods, :class, :object_id]) + # Ruby 1.9.2 introduces Object#respond_to_missing?, which is invoked by Object#respond_to? + @@required_methods << :respond_to_missing? if Object.respond_to? :respond_to_missing? + @@filters = {} def initialize(context) From 3dada41d59d22cb97dca68c55eaa22e0032fa673 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Fri, 1 Oct 2010 11:19:15 -0700 Subject: [PATCH 46/61] Fix up filters for Ruby 1.9.2 --- lib/liquid.rb | 2 +- lib/liquid/standardfilters.rb | 10 +++++++--- spec/integration/filter_spec.rb | 20 +++++++++++++++++++- spec/unit/filter_spec.rb | 18 ++++++++++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/lib/liquid.rb b/lib/liquid.rb index b9ba4c551..dccef10c9 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -38,7 +38,7 @@ module Liquid StrictQuotedFragment = /"[^"]+"|'[^']+'|[^\s,\|,\:,\,]+/ FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/ OtherFilterArgument = /#{ArgumentSeparator}(?:#{StrictQuotedFragment})/ - SpacelessFilter = /#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/ + SpacelessFilter = /^(?:'[^']+'|"[^"]+"|[^'"])*#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/ Expression = /(?:#{QuotedFragment}(?:#{SpacelessFilter})*)/ TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/ AnyStartingTag = /\{\{|\{\%/ diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index fc1f18c4a..7c52047b6 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -56,10 +56,14 @@ def strip_newlines(input) input.to_s.gsub(/\n/, '') end - # Join elements of the array with certain character between them - def join(input, glue = ' ') - [input].flatten.join(glue) + def join(input, array_glue = ' ', hash_glue = nil) + hash_glue ||= array_glue + + # translate from hash to array if needed + input = input.map{|k,v| "#{k}#{hash_glue}#{v}" } if input.is_a?(Hash) + + [input].flatten.join(array_glue) end # Sort elements of the array diff --git a/spec/integration/filter_spec.rb b/spec/integration/filter_spec.rb index 89f0d7132..d404f3619 100644 --- a/spec/integration/filter_spec.rb +++ b/spec/integration/filter_spec.rb @@ -36,9 +36,23 @@ def render_variable(body) it "should join a hash" do @context['val'] = {"one" => 1} - render_variable('val | join').should == "one1" + render_variable('val | join').should == "one 1" + + @context['val'] = {"two" => 2, "one" => 1} + output = render_variable('val | join: ":"') + output.should == "two:2:one:1" + end + + it "should join a hash with custom field and value separators" do + @context['val'] = {"one" => 1} + render_variable('val | join').should == "one 1" + + @context['val'] = {"two" => 2, "one" => 1} + output = render_variable('val | join: "|", ":"') + output.should == "two:2|one:1" end + it "should join a string" do @context['val'] = "one" render_variable('val | join').should == "one" @@ -239,6 +253,10 @@ def money(input) it "cannot access private methods" do render("{{ 'a' | to_number }}").should == "a" end + + it "should ignore nonexistant filters" do + render("{{ val | xyzzy }}", 'val' => 1000).should == "1000" + end end end diff --git a/spec/unit/filter_spec.rb b/spec/unit/filter_spec.rb index 4d0cd0710..9085120a6 100644 --- a/spec/unit/filter_spec.rb +++ b/spec/unit/filter_spec.rb @@ -111,6 +111,24 @@ class TestFilters it "should allow you to specify the join character" do filters.join([1,2,3,4], ' - ').should == "1 - 2 - 3 - 4" end + + it "should join a hash" do + output = filters.join({"one" => 1, "two" => 2}) + output.should include("one 1") + output.should include("two 2") + end + + it "should join a hash with a character" do + output = filters.join({"one" => 1, "two" => 2}, ' - ') + output.should include("one - 1") + output.should include("two - 2") + end + + it "should join a hash with separate characters for fields, and keys" do + output = filters.join({"one" => 1, "two" => 2}, '|', '-') + output.should include("one-1") + output.should include("two-2") + end end context "#sort" do From bbab7be0a3dd54bc0fc57dbbc23fd0d5adb99e08 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Fri, 1 Oct 2010 11:19:28 -0700 Subject: [PATCH 47/61] Fix context for Ruby 1.9.2 --- lib/liquid/context.rb | 241 +++++++++++++++++++++--------------------- 1 file changed, 118 insertions(+), 123 deletions(-) diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index 57452ffc6..871e922e7 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -28,8 +28,9 @@ def strainer @strainer ||= Strainer.create(self) end - # adds filters to this context. - # this does not register the filters with the main Template object. see Template.register_filter + # Adds filters to this context. + # + # Note that this does not register the filters with the main Template object. see Template.register_filter # for that def add_filters(filters) filters = [filters].flatten.compact @@ -52,7 +53,6 @@ def handle_error(e) end end - def invoke(method, *args) if strainer.respond_to?(method) strainer.__send__(method, *args) @@ -61,40 +61,41 @@ def invoke(method, *args) end end - # push new local scope on the stack. use Context#stack instead + # Push new local scope on the stack. use Context#stack instead def push(new_scope={}) raise StackLevelError, "Nesting too deep" if @scopes.length > 100 @scopes.unshift(new_scope) end - # merge a hash of variables in the current local scope + # Merge a hash of variables in the current local scope def merge(new_scopes) @scopes[0].merge!(new_scopes) end - # pop from the stack. use Context#stack instead + # Pop from the stack. use Context#stack instead def pop raise ContextError if @scopes.size == 1 @scopes.shift end - # pushes a new local scope on the stack, pops it at the end of the block + # Pushes a new local scope on the stack, pops it at the end of the block # # Example: - # # context.stack do # context['var'] = 'hi' # end - # context['var] #=> nil # + # context['var] #=> nil def stack(new_scope={},&block) result = nil push(new_scope) + begin result = yield ensure pop end + result end @@ -116,139 +117,133 @@ def has_key?(key) end private - - # 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) - case key - when nil, 'nil', 'null', '' - nil - when 'true' - true - when 'false' - false - when 'blank' - :blank? - when 'empty' - :empty? - # Single quoted strings - when /^'(.*)'$/ - $1.to_s - # Double quoted strings - when /^"(.*)"$/ - $1.to_s - # Integer and floats - when /^(\d+)$/ - $1.to_i - # Ranges - when /^\((\S+)\.\.(\S+)\)$/ - (resolve($1).to_i..resolve($2).to_i) - # Floats - when /^(\d[\d\.]+)$/ - $1.to_f - else - variable(key) + # 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) + case key + when nil, 'nil', 'null', '' + nil + when 'true' + true + when 'false' + false + when 'blank' + :blank? + when 'empty' # Single quoted strings + :empty? + when /^'(.*)'$/ # Double quoted strings + $1.to_s + when /^"(.*)"$/ # Integer and floats + $1.to_s + when /^(\d+)$/ # Ranges + $1.to_i + when /^\((\S+)\.\.(\S+)\)$/ # Floats + (resolve($1).to_i..resolve($2).to_i) + when /^(\d[\d\.]+)$/ + $1.to_f + else + variable(key) + end end - end - # fetches an object starting at the local scope and then moving up - # the hierachy - def find_variable(key) - scope = @scopes.find { |s| s.has_key?(key) } - if scope.nil? - @environments.each do |e| - if variable = lookup_and_evaluate(e, key) - scope = e - break + # Fetches an object starting at the local scope and then moving up the hierachy + def find_variable(key) + scope = @scopes.find { |s| s.has_key?(key) } + + if scope.nil? + @environments.each do |e| + if variable = lookup_and_evaluate(e, key) + scope = e + break + end 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 + scope ||= @environments.last || @scopes.last + variable ||= lookup_and_evaluate(scope, key) - # 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 = /^\[(.*)\]$/ + variable = variable.to_liquid + variable.context = self if variable.respond_to?(:context=) - first_part = parts.shift - if first_part =~ square_bracketed - first_part = resolve($1) + return variable end - if object = find_variable(first_part) + # 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 = /^\[(.*)\]$/ - parts.each do |part| - part = resolve($1) if part_resolved = (part =~ square_bracketed) + first_part = parts.shift - # 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 first_part =~ square_bracketed + first_part = resolve($1) + end - # if its a proc we will replace the entry with the proc - res = lookup_and_evaluate(object, part) - object = res.to_liquid + if object = find_variable(first_part) - # 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) + parts.each do |part| + part = resolve($1) if part_resolved = (part =~ square_bracketed) - object = object.send(part.intern).to_liquid + # 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))) - # 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 its a proc we will replace the entry with the proc + res = lookup_and_evaluate(object, part) + object = res.to_liquid - # If we are dealing with a drop here we have to - object.context = self if object.respond_to?(:context=) - end - end + # 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 - end + object = object.send(part.intern).to_liquid - def lookup_and_evaluate(obj, key) - if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=) - obj[key] = value.call(self) - else - value - end - end + # 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 - def squash_instance_assigns_with_environments - @scopes.last.each_key do |k| - @environments.each do |env| - if env.has_key?(k) - scopes.last[k] = lookup_and_evaluate(env, k) - break + # If we are dealing with a drop here we have to + object.context = self if object.respond_to?(:context=) end end - end - end - 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| + if env.has_key?(k) + scopes.last[k] = lookup_and_evaluate(env, k) + break + end + end + end + end # squash_instance_assigns_with_environments + end # Context + +end # Liquid From fd63553749cd78ee16b07f1037bd071654887cf7 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Fri, 1 Oct 2010 11:37:03 -0700 Subject: [PATCH 48/61] Additional fixes from master --- lib/liquid/tags/if.rb | 4 ++-- spec/integration/if_else_unless_spec.rb | 2 +- spec/unit/condition_spec.rb | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/liquid/tags/if.rb b/lib/liquid/tags/if.rb index 06d5558dd..e28572947 100644 --- a/lib/liquid/tags/if.rb +++ b/lib/liquid/tags/if.rb @@ -14,7 +14,7 @@ module Liquid class If < Block SyntaxHelp = "Syntax Error in tag 'if' - Valid syntax: if [expression]" Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/ - ExpressionsAndOperators = /(?:\b(?:and|or)\b|(?:\s*(?!\b(?:and|or)\b)(?:#{QuotedFragment}|\S+)\s*)+)/ + ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/ def initialize(tag_name, markup, tokens, context) @@ -57,7 +57,7 @@ def push_block(tag, markup) condition = Condition.new($1, $2, $3) while not expressions.empty? - operator = expressions.shift + operator = (expressions.shift).to_s.strip raise(SyntaxError, SyntaxHelp) unless expressions.shift.to_s =~ Syntax diff --git a/spec/integration/if_else_unless_spec.rb b/spec/integration/if_else_unless_spec.rb index ba65ec2f8..8b4c8b8fd 100644 --- a/spec/integration/if_else_unless_spec.rb +++ b/spec/integration/if_else_unless_spec.rb @@ -86,7 +86,7 @@ render("{% if 'bob' containz 'o' %}yes{% endif %}").should == "yes" render("{% if 'bob' containz 'f' %}yes{% else %}no{% endif %}").should == "no" - + render("{% if 'gnomeslab-and-or-liquid' containz 'gnomeslab-and-or-liquid' %}yes{% endif %}").should == "yes" end context "or conditionals" do diff --git a/spec/unit/condition_spec.rb b/spec/unit/condition_spec.rb index 08d3e1317..bd8e4ab24 100644 --- a/spec/unit/condition_spec.rb +++ b/spec/unit/condition_spec.rb @@ -11,12 +11,16 @@ def check_condition(*args) Condition.new(*args).evaluate(@context) end - it "should check basic equality conditions" do check_condition("1", "==", "2").should be_false check_condition("1", "==", "1").should be_true end + it "should check expressions" do + @context['one'] = @context['another'] = "gnomeslab-and-or-liquid" + check_condition('one', '==', 'another').should be_true + end + context "Default Operators (==, !=, <>, <, >, >=, <=)" do it "should evaluate true when appropriate" do check_condition('1', '==', '1').should be_true From 89985bc9f140bfd381da033c1aa28991e7a223f3 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Fri, 1 Oct 2010 11:51:06 -0700 Subject: [PATCH 49/61] Adding Literal support + Specs --- lib/liquid.rb | 1 + lib/liquid/tags/literal.rb | 42 ++++++++++++++++++++++++++++++++ lib/liquid/template.rb | 2 +- spec/integration/literal_spec.rb | 37 ++++++++++++++++++++++++++++ spec/unit/literal_spec.rb | 16 ++++++++++++ spec/unit/regexp_spec.rb | 14 +++++++++++ 6 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 lib/liquid/tags/literal.rb create mode 100644 spec/integration/literal_spec.rb create mode 100644 spec/unit/literal_spec.rb diff --git a/lib/liquid.rb b/lib/liquid.rb index dccef10c9..533bfb459 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -45,6 +45,7 @@ module Liquid PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/ TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/ VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/ + LiteralShorthand = /^(?:\{\{\{\s?)(.*?)(?:\s*\}\}\})$/ end require 'liquid/drop' diff --git a/lib/liquid/tags/literal.rb b/lib/liquid/tags/literal.rb new file mode 100644 index 000000000..67f1600f7 --- /dev/null +++ b/lib/liquid/tags/literal.rb @@ -0,0 +1,42 @@ +module Liquid + + class Literal < Block + + # Class methods + + # Converts a shorthand Liquid literal into its long representation. + # + # Currently the Template parser only knows how to handle the long version. + # So, it always checks if it is in the presence of a literal, in which case it gets converted through this method. + # + # Example: + # Liquid::Literal "{{{ hello world }}}" #=> "{% literal %} hello world {% endliteral %}" + def self.from_shorthand(literal) + literal =~ LiteralShorthand ? "{% literal %}#{$1}{% endliteral %}" : literal + end + + # Public instance methods + + def parse(tokens) # :nodoc: + @nodelist ||= [] + @nodelist.clear + + while token = tokens.shift + if token =~ FullToken && block_delimiter == $1 + end_tag + return + else + @nodelist << token + end + end + + # Make sure that its ok to end parsing in the current block. + # Effectively this method will throw and exception unless the current block is + # of type Document + assert_missing_delimitation! + end # parse + + end + + Template.register_tag('literal', Literal) +end diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb index d09a9b20d..b04010b90 100644 --- a/lib/liquid/template.rb +++ b/lib/liquid/template.rb @@ -55,7 +55,7 @@ def initialize # Parse source code. # Returns self for easy chaining def parse(source, context = {}) - @root = Document.new(tokenize(source), context.merge!(:template => self)) + @root = Document.new(tokenize(Liquid::Literal.from_shorthand(source)), context.merge!(:template => self)) self end diff --git a/spec/integration/literal_spec.rb b/spec/integration/literal_spec.rb new file mode 100644 index 000000000..0100d579c --- /dev/null +++ b/spec/integration/literal_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "Literals" do + + context "literal tag: {% literal %}" do + it "should render an empty literal" do + render('{% literal %}{% endliteral %}').should == '' + end + + it "should render a literal with a simple value" do + render('{% literal %}howdy{% endliteral %}').should == "howdy" + end + + it "should ignore liquid markup" do + inner = "{% if 'gnomeslab' contain 'liquid' %}yes{ % endif %}" + render("{% literal %}#{inner}{% endliteral %}").should == inner + end + end + + context "literal shorthand: {{{}}}" do + it "should render an empty literal" do + render('{{{}}}').should == '' + end + + it "should render a literal with a simple value" do + render('{{{howdy}}}').should == "howdy" + end + + it "should ignore liquid markup" do + inner = "{% if 'gnomeslab' contain 'liquid' %}yes{ % endif %}" + render("{{{#{inner}}}}").should == inner + end + end + + end +end \ No newline at end of file diff --git a/spec/unit/literal_spec.rb b/spec/unit/literal_spec.rb new file mode 100644 index 000000000..9802e14ab --- /dev/null +++ b/spec/unit/literal_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +module Liquid + describe Literal do + describe "Literal.from_shorthand" do + it "should convert shorthand syntax to the tag" do + Literal.from_shorthand('{{{gnomeslab}}}').should == "{% literal %}gnomeslab{% endliteral %}" + end + + it "should ignore improper syntax" do + text = "{% if 'hi' == 'hi' %}hi{% endif %}" + Literal.from_shorthand(text).should == text + end + end + end +end \ No newline at end of file diff --git a/spec/unit/regexp_spec.rb b/spec/unit/regexp_spec.rb index b99f27364..71faed58f 100644 --- a/spec/unit/regexp_spec.rb +++ b/spec/unit/regexp_spec.rb @@ -71,5 +71,19 @@ module Liquid end end + describe "LiteralShorthand" do + context "{{{ something }}}" do + it { subject.scan(LiteralShorthand).should == [["something"]] } + end + + context "{{{something}}}" do + it { subject.scan(LiteralShorthand).should == [["something"]] } + end + + context "{{{ {% if false %} false {% endif %} }}}" do + it { subject.scan(LiteralShorthand).should == [["{% if false %} false {% endif %}"]] } + end + end + end end \ No newline at end of file From 8b7b27d6f512199df1b8d7ffd378583c65ce1b84 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Fri, 1 Oct 2010 11:53:55 -0700 Subject: [PATCH 50/61] Updating history and readme --- History.txt | 13 +++++++++++++ README.md | 39 +++++++++++++++++++++++++++++++++++++++ README.txt | 38 -------------------------------------- 3 files changed, 52 insertions(+), 38 deletions(-) create mode 100644 README.md delete mode 100644 README.txt diff --git a/History.txt b/History.txt index a4389c1f3..d97c495c6 100644 --- a/History.txt +++ b/History.txt @@ -1,3 +1,16 @@ +2.2.2 + +* Added support for template inheritance {% extends %} + +2.2.1 / 2010-08-23 + +* Added support for literal tags + +2.2.0 / 2010-08-22 + +* Compatible with Ruby 1.8.7, 1.9.1 and 1.9.2-p0 +* Merged some changed made by the community + 1.9.0 / 2008-03-04 * Fixed gem install rake task diff --git a/README.md b/README.md new file mode 100644 index 000000000..d4c2b3519 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Liquid template engine + +## Introduction + +Liquid is a template engine which I wrote for very specific requirements + +* It has to have beautiful and simple markup. Template engines which don't produce good looking markup are no fun to use. +* It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don't want to run code on your server which your users wrote. +* It has to be stateless. Compile and render steps have to be seperate so that the expensive parsing and compiling can be done once and later on you can just render it passing in a hash with local variables and objects. + +## Why should I use Liquid + +* You want to allow your users to edit the appearance of your application but don't want them to run **insecure code on your server**. +* You want to render templates directly from the database +* You like smarty (PHP) style template engines +* You need a template engine which does HTML just as well as emails +* You don't like the markup of your current templating engine + +## What does it look like? + +
    + {% for product in products %} +
  • +

    {{product.name}}

    + Only {{product.price | price }} + + {{product.description | prettyprint | paragraph }} +
  • + {% endfor %} +
+ +## Howto use Liquid + +Liquid supports a very simple API based around the Liquid::Template class. +For standard use you can just pass it the content of a file and call render with a parameters hash. + + + @template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template + @template.render( 'name' => 'tobi' ) # => "hi tobi" diff --git a/README.txt b/README.txt deleted file mode 100644 index 257296362..000000000 --- a/README.txt +++ /dev/null @@ -1,38 +0,0 @@ -= Liquid template engine - -Liquid is a template engine which I wrote for very specific requirements - -* It has to have beautiful and simple markup. - Template engines which don't produce good looking markup are no fun to use. -* It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don't want to run code on your server which your users wrote. -* It has to be stateless. Compile and render steps have to be seperate so that the expensive parsing and compiling can be done once and later on you can - just render it passing in a hash with local variables and objects. - -== Why should i use Liquid - -* You want to allow your users to edit the appearance of your application but don't want them to run insecure code on your server. -* You want to render templates directly from the database -* You like smarty style template engines -* You need a template engine which does HTML just as well as Emails -* You don't like the markup of your current one - -== What does it look like? - -
    - {% for product in products %} -
  • -

    {{product.name}}

    - Only {{product.price | price }} - - {{product.description | prettyprint | paragraph }} -
  • - {% endfor %} -
- -== Howto use Liquid - -Liquid supports a very simple API based around the Liquid::Template class. -For standard use you can just pass it the content of a file and call render with a parameters hash. - - @template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template - @template.render( 'name' => 'tobi' ) # => "hi tobi" \ No newline at end of file From 1b27b1ac754b0c1a4321316ccb093ac4e5c5a351 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Fri, 1 Oct 2010 12:09:47 -0700 Subject: [PATCH 51/61] fixing gemspec --- locomotive_liquid.gemspec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locomotive_liquid.gemspec b/locomotive_liquid.gemspec index 5bb5684f3..7e6c63078 100644 --- a/locomotive_liquid.gemspec +++ b/locomotive_liquid.gemspec @@ -9,18 +9,18 @@ Gem::Specification.new do |s| s.description = "A secure, non-evaling end user template engine with aesthetic markup. Extended with liquid template inheritance for use in LocomotiveCMS" - s.extra_rdoc_files = ["History.txt", "README.txt"] + s.extra_rdoc_files = ["History.txt", "README.md"] s.files = Dir[ "CHANGELOG", "History.txt", "MIT-LICENSE", - "README.txt", + "README.md", "Rakefile", "init.rb", "{lib}/**/*"] s.has_rdoc = true s.homepage = "http://www.locomotiveapp.org" - s.rdoc_options = ["--main", "README.txt"] + s.rdoc_options = ["--main", "README.md"] s.require_paths = ["lib"] s.rubyforge_project = "locomotive_liquid" From cd7d7e17ff3585154101d47eb6bc87297a0f1f25 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Fri, 1 Oct 2010 12:21:22 -0700 Subject: [PATCH 52/61] Adding release rake task --- Rakefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 2384f8a66..7c2b9f77d 100755 --- a/Rakefile +++ b/Rakefile @@ -39,7 +39,12 @@ end desc "build the gem and release it to rubygems.org" task :release => :gem do - sh "gem push pkg/locomotive_liquid-#{gemspec.version}.gem" + puts "Tagging #{gemspec.version}..." + system "git tag -a #{gemspec.version} -m 'Tagging #{gemspec.version}'" + puts "Pushing to Github..." + system "git push --tags" + puts "Pushing to rubygems.org..." + system "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem" end namespace :profile do From eaee02702d8921f85205427124c7226ec7a96b87 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Fri, 1 Oct 2010 15:28:28 -0700 Subject: [PATCH 53/61] Reorgnaize specs a bit --- Gemfile | 1 + Gemfile.lock | 2 + autotest/discover.rb | 1 + spec/integration/case_spec.rb | 119 +++++++ spec/integration/comment_spec.rb | 32 ++ spec/integration/for_spec.rb | 415 ++++++++++++++++++++++++ spec/integration/if_else_unless_spec.rb | 7 + spec/integration/tag_spec.rb | 364 --------------------- 8 files changed, 577 insertions(+), 364 deletions(-) create mode 100644 autotest/discover.rb create mode 100644 spec/integration/case_spec.rb create mode 100644 spec/integration/comment_spec.rb create mode 100644 spec/integration/for_spec.rb diff --git a/Gemfile b/Gemfile index 83b005d02..3919783f6 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,6 @@ source :rubygems +gem "rake" gem "rspec", ">= 2.0.0.beta.22" gem "cucumber", ">= 0.9.0" diff --git a/Gemfile.lock b/Gemfile.lock index c9109e94d..6a8efdb97 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,6 +16,7 @@ GEM trollop (~> 1.16.2) json (1.4.6) linecache (0.43) + rake (0.8.7) rspec (2.0.0.beta.22) rspec-core (= 2.0.0.beta.22) rspec-expectations (= 2.0.0.beta.22) @@ -39,5 +40,6 @@ PLATFORMS DEPENDENCIES cucumber (>= 0.9.0) + rake rspec (>= 2.0.0.beta.22) ruby-debug diff --git a/autotest/discover.rb b/autotest/discover.rb new file mode 100644 index 000000000..cd6892ccb --- /dev/null +++ b/autotest/discover.rb @@ -0,0 +1 @@ +Autotest.add_discovery { "rspec2" } diff --git a/spec/integration/case_spec.rb b/spec/integration/case_spec.rb new file mode 100644 index 000000000..f3d12679a --- /dev/null +++ b/spec/integration/case_spec.rb @@ -0,0 +1,119 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "case" do + + context "{% case %}" do + it "should render the first block with a matching {% when %} argument" do + data = {'condition' => 1 } + render('{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', data).should == ' its 1 ' + + data = {'condition' => 2 } + render('{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', data).should == ' its 2 ' + + # dont render whitespace between case and first when + data = {'condition' => 2 } + render('{% case condition %} {% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', data).should == ' its 2 ' + end + + it "should match strings correctly" do + data = {'condition' => "string here" } + render('{% case condition %}{% when "string here" %} hit {% endcase %}', data).should == ' hit ' + + data = {'condition' => "bad string here" } + render('{% case condition %}{% when "string here" %} hit {% endcase %}', data).should == '' + end + + it "should not render anything if no matches found" do + data = {'condition' => 3 } + render(' {% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %} ', data).should == ' ' + end + + it "should evaluate variables and expressions" do + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => []).should == '' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1]).should == '1' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1]).should == '2' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1]).should == '' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1]).should == '' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1, 1]).should == '' + end + + it "should allow assignment from within a {% when %} block" do + # Example from the shopify forums + template = %q({% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}) + + render(template, "collection" => {'handle' => 'menswear-jackets'}).should == 'menswear' + render(template, "collection" => {'handle' => 'menswear-t-shirts'}) == 'menswear' + render(template, "collection" => {'handle' => 'x'}) == 'womenswear' + render(template, "collection" => {'handle' => 'y'}) == 'womenswear' + render(template, "collection" => {'handle' => 'z'}) == 'womenswear' + end + + it "should allow the use of 'or' to chain parameters with {% when %}" do + template = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + render(template, {'condition' => 1 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 2 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 3 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 4 }).should == ' its 4 ' + render(template, {'condition' => 5 }).should == '' + + template = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + render(template, 'condition' => 1).should == ' its 1 or 2 or 3 ' + render(template, 'condition' => 'string').should == ' its 1 or 2 or 3 ' + render(template, 'condition' => nil).should == ' its 1 or 2 or 3 ' + render(template, 'condition' => 'something else').should == '' + end + + it "should allow the use of commas to chain parameters with {% when %} " do + template = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + render(template, {'condition' => 1 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 2 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 3 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 4 }).should == ' its 4 ' + render(template, {'condition' => 5 }).should == '' + + template = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + render(template, 'condition' => 1).should == ' its 1 or 2 or 3 ' + render(template, 'condition' => 'string').should == ' its 1 or 2 or 3 ' + render(template, 'condition' => nil).should == ' its 1 or 2 or 3 ' + render(template, 'condition' => 'something else').should == '' + end + + it "should raise an error when theres bad syntax" do + expect { + render!('{% case false %}{% when %}true{% endcase %}') + }.to raise_error(Liquid::SyntaxError) + + expect { + render!('{% case false %}{% huh %}true{% endcase %}') + }.to raise_error(Liquid::SyntaxError) + end + + context "with {% else %}" do + it "should render the {% else %} block when no matches found" do + data = {'condition' => 5 } + render('{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', data).should == ' hit ' + + data = {'condition' => 6 } + render('{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', data).should == ' else ' + end + + it "should evaluate variables and expressions" do + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => []).should == 'else' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1]).should == '1' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1]).should == '2' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1]).should == 'else' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1]).should == 'else' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1, 1]).should == 'else' + + + render('{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "else" + render('{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "false" + render('{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "true" + render('{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "else" + + end + end + end + end +end \ No newline at end of file diff --git a/spec/integration/comment_spec.rb b/spec/integration/comment_spec.rb new file mode 100644 index 000000000..59300a6e2 --- /dev/null +++ b/spec/integration/comment_spec.rb @@ -0,0 +1,32 @@ +describe "Liquid Rendering" do + describe "comments" do + context "{% comment %}" do + it "should not render comment blocks" do + render('{%comment%}{%endcomment%}').should == '' + render('{%comment%}{% endcomment %}').should == '' + render('{% comment %}{%endcomment%}').should == '' + render('{% comment %}{% endcomment %}').should == '' + render('{%comment%}comment{%endcomment%}').should == '' + render('{% comment %}comment{% endcomment %}').should == '' + end + + it "should render the other content that isnt inside the comment block" do + + render(%|the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?|).should == + %|the comment block should be removed .. right?| + + render('foo{%comment%}comment{%endcomment%}bar').should == 'foobar' + render('foo{% comment %}comment{% endcomment %}bar').should == 'foobar' + render('foo{%comment%} comment {%endcomment%}bar').should == 'foobar' + render('foo{% comment %} comment {% endcomment %}bar').should == 'foobar' + + render('foo {%comment%} {%endcomment%} bar').should == 'foo bar' + render('foo {%comment%}comment{%endcomment%} bar').should == 'foo bar' + render('foo {%comment%} comment {%endcomment%} bar').should == 'foo bar' + + render('foo{%comment%} + {%endcomment%}bar').should == "foobar" + end + end + end +end \ No newline at end of file diff --git a/spec/integration/for_spec.rb b/spec/integration/for_spec.rb new file mode 100644 index 000000000..01f71fdfd --- /dev/null +++ b/spec/integration/for_spec.rb @@ -0,0 +1,415 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "for loops" do + + describe "for" do + describe "{% for item in collection %}" do + it "should repeat the block for each item in the collection" do + data = {'collection' => [1,2,3,4]} + render('{%for item in collection%} yo {%endfor%}', data).should == ' yo yo yo yo ' + + data = {'collection' => [1,2]} + render('{%for item in collection%}yo{%endfor%}', data).should == 'yoyo' + + data = {'collection' => [1]} + render('{%for item in collection%} yo {%endfor%}', data).should == ' yo ' + + data = {'collection' => [1,2]} + render('{%for item in collection%}{%endfor%}', data).should == '' + + data = {'collection' => [1,2,3]} + render('{%for item in collection%} yo {%endfor%}', data).should == " yo yo yo " + end + + it "should allow access to the current item via {{item}}" do + data = {'collection' => [1,2,3]} + render('{%for item in collection%} {{item}} {%endfor%}', data).should == ' 1 2 3 ' + render('{% for item in collection %}{{item}}{% endfor %}', data).should == '123' + render('{%for item in collection%}{{item}}{%endfor%}', data).should == '123' + + data = {'collection' => ['a','b','c','d']} + render('{%for item in collection%}{{item}}{%endfor%}', data).should == 'abcd' + + data = {'collection' => ['a',' ','b',' ','c']} + render('{%for item in collection%}{{item}}{%endfor%}', data).should == 'a b c' + + data = {'collection' => ['a','','b','','c']} + render('{%for item in collection%}{{item}}{%endfor%}', data).should == 'abc' + end + + it "should allow deep nesting" do + data = {'array' => [[1,2],[3,4],[5,6]] } + render('{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', data).should == '123456' + end + + it "should expose {{forloop.name}} to get the name of the collection" do + data = {'collection' => [1] } + render("{%for item in collection%} {{forloop.name}} {%endfor%}", data).should == " item-collection " + end + + it "should expose {{forloop.length}} for the overall size of the collection being looped" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {{forloop.length}} {%endfor%}", data).should == " 3 3 3 " + end + + it "should expose {{forloop.index}} for the current item's position in the collection (1 based)" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {{forloop.index}} {%endfor%}", data).should == " 1 2 3 " + end + + it "should expose {{forloop.index0}} for the current item's position in the collection (0 based)" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {{forloop.index0}} {%endfor%}", data).should == " 0 1 2 " + end + + it "should expose {{forloop.rindex}} for the number of items remaining in the collection (1 based)" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {{forloop.rindex}} {%endfor%}", data).should == " 3 2 1 " + end + + it "should expose {{forloop.rindex0}} for the number of items remaining in the collection (0 based)" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {{forloop.rindex0}} {%endfor%}", data).should == " 2 1 0 " + end + + it "should expose {{forloop.first}} for the first item in the collection" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {% if forloop.first %}y{% else %}n{% endif %} {%endfor%}", data).should == " y n n " + end + + it "should expose {{forloop.last}} for the last item in the collection" do + data = {'collection' => [1,2,3] } + render("{%for item in collection%} {% if forloop.last %}y{% else %}n{% endif %} {%endfor%}", data).should == " n n y " + end + end + + describe "{% for item in collection reversed %}" do + it "should reverse the loop" do + data = {'collection' => [1,2,3] } + render("{%for item in collection reversed%}{{item}}{%endfor%}", data).should == "321" + end + end + + context "with limit and offset" do + let(:data) do + {'collection' => [1,2,3,4,5,6,7,8,9,0] } + end + + describe "{% for item in collection limit: 4 %}" do + it "should only cycle through the first 4 items of the collection" do + render("{%for item in collection limit:4%}{{item}}{%endfor%}", data).should == "1234" + render("{%for item in collection limit: 4%}{{item}}{%endfor%}", data).should == "1234" + end + end + + describe "{% for item in collection offset:8 %}" do + it "should cycle throughthe collection starting on the 9th item" do + render("{%for item in collection offset:8%}{{item}}{%endfor%}", data).should == "90" + end + end + + describe "{% for item in collection limit:4 offset:2}" do + it "should only cycle through the 4 items of the collection, starting on the 3rd item" do + render("{%for item in collection limit:4 offset:2 %}{{item}}{%endfor%}", data).should == "3456" + render("{%for item in collection limit: 4 offset: 2 %}{{item}}{%endfor%}", data).should == "3456" + end + + it "{% for item in collection limit:limit offset:offset}" do + data.merge! 'limit' => '4', 'offset' => '2' + render("{%for item in collection limit:limit offset:offset %}{{item}}{%endfor%}", data).should == "3456" + render("{%for item in collection limit: limit offset: offset %}{{item}}{%endfor%}", data).should == "3456" + end + end + + describe "{% for item in collection offset:continue limit: 3}" do + it "should resume the iteration from where it ended earlier" do + + output = render multiline_string(<<-END), data + | {%for i in collection limit:3 %}{{i}}{%endfor%} + | next + | {%for i in collection offset:continue limit:3 %}{{i}}{%endfor%} + | next + | {%for i in collection offset:continue limit:3 %}{{i}}{%endfor%} + END + + output.should == multiline_string(<<-END) + | 123 + | next + | 456 + | next + | 789 + END + end + end + + + describe "edge cases" do + context "limit: -1" do + it "should ignore the limit" do + render("{%for item in collection limit:-1 offset:5 %}{{item}}{%endfor%}", data).should == "67890" + end + end + + context "offset: -1" do + it "should ignore the offset" do + render("{%for item in collection limit:1 offset:-1 %}{{item}}{%endfor%}", data).should == "1" + end + end + + context "offset: 100" do + it "should render an empty string" do + render("{%for item in collection limit:1 offset:100 %} {{item}} {%endfor%}", data).should == "" + end + end + + context "resume with big limit" do + it "should complete the rest of the items" do + output = render multiline_string(<<-END), data + | {%for i in collection limit:3 %}{{i}}{%endfor%} + | next + | {%for i in collection offset:continue limit:10000 %}{{i}}{%endfor%} + END + + output.should == multiline_string(<<-END) + | 123 + | next + | 4567890 + END + end + end + + context "resume with big offset" do + it "should complete the rest of the items" do + output = render multiline_string(<<-END), data + | {%for i in collection limit:3 %}{{i}}{%endfor%} + | next + | {%for i in collection offset:continue offset:10000 %}{{i}}{%endfor%} + END + + output.should == multiline_string(<<-END) + | 123 + | next + | | + END + end + end + end + end + + context "{% for item in (1..3) %}" do + it "should repeat the block for each item in the range" do + render('{%for item in (1..3) %} {{item}} {%endfor%}').should == ' 1 2 3 ' + end + end + + context "{% ifchanged %}" do + it "should render the block only if the for item is different than the last" do + data = {'array' => [ 1, 1, 2, 2, 3, 3] } + render('{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',data).should == '123' + + data = {'array' => [ 1, 1, 1, 1] } + render('{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',data).should == '1' + end + end + end + + describe "{% assign %}" do + + it "should assign a variable to a string" do + render('{%assign var = "yo" %} var:{{var}} ').should == " var:yo " + render("{%assign var = 'yo' %} var:{{var}} ").should == " var:yo " + render("{%assign var='yo' %} var:{{var}} ").should == " var:yo " + render("{%assign var='yo'%} var:{{var}} ").should == " var:yo " + + render('{%assign var="" %} var:{{var}} ').should == " var: " + render("{%assign var='' %} var:{{var}} ").should == " var: " + end + + it "should assign a variable to an integer" do + render('{%assign var = 1 %} var:{{var}} ').should == " var:1 " + render("{%assign var=1 %} var:{{var}} ").should == " var:1 " + render("{%assign var =1 %} var:{{var}} ").should == " var:1 " + end + + it "should assign a variable to a float" do + render('{%assign var = 1.011 %} var:{{var}} ').should == " var:1.011 " + render("{%assign var=1.011 %} var:{{var}} ").should == " var:1.011 " + render("{%assign var =1.011 %} var:{{var}} ").should == " var:1.011 " + end + + it "should assign a variable that includes a hyphen" do + render('{%assign a-b = "yo" %} {{a-b}} ').should == " yo " + render('{{a-b}}{%assign a-b = "yo" %} {{a-b}} ').should == " yo " + render('{%assign a-b = "yo" %} {{a-b}} {{a}} {{b}} ', 'a' => 1, 'b' => 2).should == " yo 1 2 " + end + + it "should assign a variable to a complex accessor" do + data = {'var' => {'a:b c' => {'paged' => '1' }}} + render('{%assign var2 = var["a:b c"].paged %}var2: {{var2}}', data).should == 'var2: 1' + end + + it "should assign var2 to 'hello' when var is 'hello'" do + data = {'var' => 'Hello' } + render('var2:{{var2}} {%assign var2 = var%} var2:{{var2}}',data).should == 'var2: var2:Hello' + end + + it "should assign the variable in a global context, even if it is in a block" do + render( '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}' ).should == "variable" + end + end + + context "{% capture %}" do + it "should capture the result of a block into a variable" do + data = {'var' => 'content' } + render('{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', data).should == 'content foo content foo ' + end + + it "should throw an error when it detects bad syntax" do + data = {'var' => 'content'} + expect { + render('{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', data) + }.to raise_error(Liquid::SyntaxError) + end + end + + context "{% case %}" do + it "should render the first block with a matching {% when %} argument" do + data = {'condition' => 1 } + render('{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', data).should == ' its 1 ' + + data = {'condition' => 2 } + render('{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', data).should == ' its 2 ' + + # dont render whitespace between case and first when + data = {'condition' => 2 } + render('{% case condition %} {% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', data).should == ' its 2 ' + end + + it "should match strings correctly" do + data = {'condition' => "string here" } + render('{% case condition %}{% when "string here" %} hit {% endcase %}', data).should == ' hit ' + + data = {'condition' => "bad string here" } + render('{% case condition %}{% when "string here" %} hit {% endcase %}', data).should == '' + end + + it "should not render anything if no matches found" do + data = {'condition' => 3 } + render(' {% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %} ', data).should == ' ' + end + + it "should evaluate variables and expressions" do + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => []).should == '' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1]).should == '1' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1]).should == '2' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1]).should == '' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1]).should == '' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1, 1]).should == '' + end + + it "should allow assignment from within a {% when %} block" do + # Example from the shopify forums + template = %q({% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}) + + render(template, "collection" => {'handle' => 'menswear-jackets'}).should == 'menswear' + render(template, "collection" => {'handle' => 'menswear-t-shirts'}) == 'menswear' + render(template, "collection" => {'handle' => 'x'}) == 'womenswear' + render(template, "collection" => {'handle' => 'y'}) == 'womenswear' + render(template, "collection" => {'handle' => 'z'}) == 'womenswear' + end + + it "should allow the use of 'or' to chain parameters with {% when %}" do + template = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + render(template, {'condition' => 1 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 2 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 3 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 4 }).should == ' its 4 ' + render(template, {'condition' => 5 }).should == '' + + template = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + render(template, 'condition' => 1).should == ' its 1 or 2 or 3 ' + render(template, 'condition' => 'string').should == ' its 1 or 2 or 3 ' + render(template, 'condition' => nil).should == ' its 1 or 2 or 3 ' + render(template, 'condition' => 'something else').should == '' + end + + it "should allow the use of commas to chain parameters with {% when %} " do + template = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + render(template, {'condition' => 1 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 2 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 3 }).should == ' its 1 or 2 or 3 ' + render(template, {'condition' => 4 }).should == ' its 4 ' + render(template, {'condition' => 5 }).should == '' + + template = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + render(template, 'condition' => 1).should == ' its 1 or 2 or 3 ' + render(template, 'condition' => 'string').should == ' its 1 or 2 or 3 ' + render(template, 'condition' => nil).should == ' its 1 or 2 or 3 ' + render(template, 'condition' => 'something else').should == '' + end + + it "should raise an error when theres bad syntax" do + expect { + render!('{% case false %}{% when %}true{% endcase %}') + }.to raise_error(Liquid::SyntaxError) + + expect { + render!('{% case false %}{% huh %}true{% endcase %}') + }.to raise_error(Liquid::SyntaxError) + end + + context "with {% else %}" do + it "should render the {% else %} block when no matches found" do + data = {'condition' => 5 } + render('{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', data).should == ' hit ' + + data = {'condition' => 6 } + render('{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', data).should == ' else ' + end + + it "should evaluate variables and expressions" do + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => []).should == 'else' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1]).should == '1' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1]).should == '2' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1]).should == 'else' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1]).should == 'else' + render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1, 1]).should == 'else' + + + render('{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "else" + render('{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "false" + render('{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "true" + render('{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "else" + + end + end + end + + context "{% cycle %}" do + + it "should cycle through a list of strings" do + render('{%cycle "one", "two"%}').should == 'one' + render('{%cycle "one", "two"%} {%cycle "one", "two"%}').should == 'one two' + render('{%cycle "", "two"%} {%cycle "", "two"%}').should == ' two' + render('{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}').should == 'one two one' + render('{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}').should == 'text-align: left text-align: right' + end + + it "should keep track of multiple cycles" do + render('{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}').should == '1 2 1 1 2 3 1' + end + + it "should keep track of multiple named cycles" do + render('{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}').should == 'one one two two one one' + end + + it "should allow multiple named cycles with names from context" do + data = {"var1" => 1, "var2" => 2 } + render('{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', data).should == 'one one two two one one' + end + end + + + end +end \ No newline at end of file diff --git a/spec/integration/if_else_unless_spec.rb b/spec/integration/if_else_unless_spec.rb index 8b4c8b8fd..f39fd7799 100644 --- a/spec/integration/if_else_unless_spec.rb +++ b/spec/integration/if_else_unless_spec.rb @@ -89,6 +89,13 @@ render("{% if 'gnomeslab-and-or-liquid' containz 'gnomeslab-and-or-liquid' %}yes{% endif %}").should == "yes" end + it "should allow illegal symbols in the condition" do + render('{% if true == empty %}hello{% endif %}').should == "" + render('{% if true == null %}hello{% endif %}').should == "" + render('{% if empty == true %}hello{% endif %}').should == "" + render('{% if null == true %}hello{% endif %}').should == "" + end + context "or conditionals" do it "should work correctly when passed 2 variables" do body = '{% if a or b %} YES {% endif %}' diff --git a/spec/integration/tag_spec.rb b/spec/integration/tag_spec.rb index 8e407affc..281cb9fdd 100644 --- a/spec/integration/tag_spec.rb +++ b/spec/integration/tag_spec.rb @@ -3,257 +3,6 @@ describe "Liquid Rendering" do describe "Tags" do - describe "comment" do - context "{% comment %}" do - it "should not render comment blocks" do - render('{%comment%}{%endcomment%}').should == '' - render('{%comment%}{% endcomment %}').should == '' - render('{% comment %}{%endcomment%}').should == '' - render('{% comment %}{% endcomment %}').should == '' - render('{%comment%}comment{%endcomment%}').should == '' - render('{% comment %}comment{% endcomment %}').should == '' - end - - it "should render the other content that isnt inside the comment block" do - - render(%|the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?|).should == - %|the comment block should be removed .. right?| - - render('foo{%comment%}comment{%endcomment%}bar').should == 'foobar' - render('foo{% comment %}comment{% endcomment %}bar').should == 'foobar' - render('foo{%comment%} comment {%endcomment%}bar').should == 'foobar' - render('foo{% comment %} comment {% endcomment %}bar').should == 'foobar' - - render('foo {%comment%} {%endcomment%} bar').should == 'foo bar' - render('foo {%comment%}comment{%endcomment%} bar').should == 'foo bar' - render('foo {%comment%} comment {%endcomment%} bar').should == 'foo bar' - - render('foo{%comment%} - {%endcomment%}bar').should == "foobar" - end - end - end - - describe "{% if %}" do - it "should allow illegal symbols in the condition" do - render('{% if true == empty %}hello{% endif %}').should == "" - render('{% if true == null %}hello{% endif %}').should == "" - render('{% if empty == true %}hello{% endif %}').should == "" - render('{% if null == true %}hello{% endif %}').should == "" - end - end - - describe "for" do - describe "{% for item in collection %}" do - it "should repeat the block for each item in the collection" do - data = {'collection' => [1,2,3,4]} - render('{%for item in collection%} yo {%endfor%}', data).should == ' yo yo yo yo ' - - data = {'collection' => [1,2]} - render('{%for item in collection%}yo{%endfor%}', data).should == 'yoyo' - - data = {'collection' => [1]} - render('{%for item in collection%} yo {%endfor%}', data).should == ' yo ' - - data = {'collection' => [1,2]} - render('{%for item in collection%}{%endfor%}', data).should == '' - - data = {'collection' => [1,2,3]} - render('{%for item in collection%} yo {%endfor%}', data).should == " yo yo yo " - end - - it "should allow access to the current item via {{item}}" do - data = {'collection' => [1,2,3]} - render('{%for item in collection%} {{item}} {%endfor%}', data).should == ' 1 2 3 ' - render('{% for item in collection %}{{item}}{% endfor %}', data).should == '123' - render('{%for item in collection%}{{item}}{%endfor%}', data).should == '123' - - data = {'collection' => ['a','b','c','d']} - render('{%for item in collection%}{{item}}{%endfor%}', data).should == 'abcd' - - data = {'collection' => ['a',' ','b',' ','c']} - render('{%for item in collection%}{{item}}{%endfor%}', data).should == 'a b c' - - data = {'collection' => ['a','','b','','c']} - render('{%for item in collection%}{{item}}{%endfor%}', data).should == 'abc' - end - - it "should allow deep nesting" do - data = {'array' => [[1,2],[3,4],[5,6]] } - render('{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', data).should == '123456' - end - - it "should expose {{forloop.name}} to get the name of the collection" do - data = {'collection' => [1] } - render("{%for item in collection%} {{forloop.name}} {%endfor%}", data).should == " item-collection " - end - - it "should expose {{forloop.length}} for the overall size of the collection being looped" do - data = {'collection' => [1,2,3] } - render("{%for item in collection%} {{forloop.length}} {%endfor%}", data).should == " 3 3 3 " - end - - it "should expose {{forloop.index}} for the current item's position in the collection (1 based)" do - data = {'collection' => [1,2,3] } - render("{%for item in collection%} {{forloop.index}} {%endfor%}", data).should == " 1 2 3 " - end - - it "should expose {{forloop.index0}} for the current item's position in the collection (0 based)" do - data = {'collection' => [1,2,3] } - render("{%for item in collection%} {{forloop.index0}} {%endfor%}", data).should == " 0 1 2 " - end - - it "should expose {{forloop.rindex}} for the number of items remaining in the collection (1 based)" do - data = {'collection' => [1,2,3] } - render("{%for item in collection%} {{forloop.rindex}} {%endfor%}", data).should == " 3 2 1 " - end - - it "should expose {{forloop.rindex0}} for the number of items remaining in the collection (0 based)" do - data = {'collection' => [1,2,3] } - render("{%for item in collection%} {{forloop.rindex0}} {%endfor%}", data).should == " 2 1 0 " - end - - it "should expose {{forloop.first}} for the first item in the collection" do - data = {'collection' => [1,2,3] } - render("{%for item in collection%} {% if forloop.first %}y{% else %}n{% endif %} {%endfor%}", data).should == " y n n " - end - - it "should expose {{forloop.last}} for the last item in the collection" do - data = {'collection' => [1,2,3] } - render("{%for item in collection%} {% if forloop.last %}y{% else %}n{% endif %} {%endfor%}", data).should == " n n y " - end - end - - describe "{% for item in collection reversed %}" do - it "should reverse the loop" do - data = {'collection' => [1,2,3] } - render("{%for item in collection reversed%}{{item}}{%endfor%}", data).should == "321" - end - end - - context "with limit and offset" do - let(:data) do - {'collection' => [1,2,3,4,5,6,7,8,9,0] } - end - - describe "{% for item in collection limit: 4 %}" do - it "should only cycle through the first 4 items of the collection" do - render("{%for item in collection limit:4%}{{item}}{%endfor%}", data).should == "1234" - render("{%for item in collection limit: 4%}{{item}}{%endfor%}", data).should == "1234" - end - end - - describe "{% for item in collection offset:8 %}" do - it "should cycle throughthe collection starting on the 9th item" do - render("{%for item in collection offset:8%}{{item}}{%endfor%}", data).should == "90" - end - end - - describe "{% for item in collection limit:4 offset:2}" do - it "should only cycle through the 4 items of the collection, starting on the 3rd item" do - render("{%for item in collection limit:4 offset:2 %}{{item}}{%endfor%}", data).should == "3456" - render("{%for item in collection limit: 4 offset: 2 %}{{item}}{%endfor%}", data).should == "3456" - end - - it "{% for item in collection limit:limit offset:offset}" do - data.merge! 'limit' => '4', 'offset' => '2' - render("{%for item in collection limit:limit offset:offset %}{{item}}{%endfor%}", data).should == "3456" - render("{%for item in collection limit: limit offset: offset %}{{item}}{%endfor%}", data).should == "3456" - end - end - - describe "{% for item in collection offset:continue limit: 3}" do - it "should resume the iteration from where it ended earlier" do - - output = render multiline_string(<<-END), data - | {%for i in collection limit:3 %}{{i}}{%endfor%} - | next - | {%for i in collection offset:continue limit:3 %}{{i}}{%endfor%} - | next - | {%for i in collection offset:continue limit:3 %}{{i}}{%endfor%} - END - - output.should == multiline_string(<<-END) - | 123 - | next - | 456 - | next - | 789 - END - end - end - - - describe "edge cases" do - context "limit: -1" do - it "should ignore the limit" do - render("{%for item in collection limit:-1 offset:5 %}{{item}}{%endfor%}", data).should == "67890" - end - end - - context "offset: -1" do - it "should ignore the offset" do - render("{%for item in collection limit:1 offset:-1 %}{{item}}{%endfor%}", data).should == "1" - end - end - - context "offset: 100" do - it "should render an empty string" do - render("{%for item in collection limit:1 offset:100 %} {{item}} {%endfor%}", data).should == "" - end - end - - context "resume with big limit" do - it "should complete the rest of the items" do - output = render multiline_string(<<-END), data - | {%for i in collection limit:3 %}{{i}}{%endfor%} - | next - | {%for i in collection offset:continue limit:10000 %}{{i}}{%endfor%} - END - - output.should == multiline_string(<<-END) - | 123 - | next - | 4567890 - END - end - end - - context "resume with big offset" do - it "should complete the rest of the items" do - output = render multiline_string(<<-END), data - | {%for i in collection limit:3 %}{{i}}{%endfor%} - | next - | {%for i in collection offset:continue offset:10000 %}{{i}}{%endfor%} - END - - output.should == multiline_string(<<-END) - | 123 - | next - | | - END - end - end - end - end - - context "{% for item in (1..3) %}" do - it "should repeat the block for each item in the range" do - render('{%for item in (1..3) %} {{item}} {%endfor%}').should == ' 1 2 3 ' - end - end - - context "{% ifchanged %}" do - it "should render the block only if the for item is different than the last" do - data = {'array' => [ 1, 1, 2, 2, 3, 3] } - render('{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',data).should == '123' - - data = {'array' => [ 1, 1, 1, 1] } - render('{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',data).should == '1' - end - end - end - describe "{% assign %}" do it "should assign a variable to a string" do @@ -313,119 +62,6 @@ end end - context "{% case %}" do - it "should render the first block with a matching {% when %} argument" do - data = {'condition' => 1 } - render('{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', data).should == ' its 1 ' - - data = {'condition' => 2 } - render('{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', data).should == ' its 2 ' - - # dont render whitespace between case and first when - data = {'condition' => 2 } - render('{% case condition %} {% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', data).should == ' its 2 ' - end - - it "should match strings correctly" do - data = {'condition' => "string here" } - render('{% case condition %}{% when "string here" %} hit {% endcase %}', data).should == ' hit ' - - data = {'condition' => "bad string here" } - render('{% case condition %}{% when "string here" %} hit {% endcase %}', data).should == '' - end - - it "should not render anything if no matches found" do - data = {'condition' => 3 } - render(' {% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %} ', data).should == ' ' - end - - it "should evaluate variables and expressions" do - render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => []).should == '' - render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1]).should == '1' - render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1]).should == '2' - render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1]).should == '' - render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1]).should == '' - render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1, 1]).should == '' - end - - it "should allow assignment from within a {% when %} block" do - # Example from the shopify forums - template = %q({% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}) - - render(template, "collection" => {'handle' => 'menswear-jackets'}).should == 'menswear' - render(template, "collection" => {'handle' => 'menswear-t-shirts'}) == 'menswear' - render(template, "collection" => {'handle' => 'x'}) == 'womenswear' - render(template, "collection" => {'handle' => 'y'}) == 'womenswear' - render(template, "collection" => {'handle' => 'z'}) == 'womenswear' - end - - it "should allow the use of 'or' to chain parameters with {% when %}" do - template = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' - render(template, {'condition' => 1 }).should == ' its 1 or 2 or 3 ' - render(template, {'condition' => 2 }).should == ' its 1 or 2 or 3 ' - render(template, {'condition' => 3 }).should == ' its 1 or 2 or 3 ' - render(template, {'condition' => 4 }).should == ' its 4 ' - render(template, {'condition' => 5 }).should == '' - - template = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' - render(template, 'condition' => 1).should == ' its 1 or 2 or 3 ' - render(template, 'condition' => 'string').should == ' its 1 or 2 or 3 ' - render(template, 'condition' => nil).should == ' its 1 or 2 or 3 ' - render(template, 'condition' => 'something else').should == '' - end - - it "should allow the use of commas to chain parameters with {% when %} " do - template = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' - render(template, {'condition' => 1 }).should == ' its 1 or 2 or 3 ' - render(template, {'condition' => 2 }).should == ' its 1 or 2 or 3 ' - render(template, {'condition' => 3 }).should == ' its 1 or 2 or 3 ' - render(template, {'condition' => 4 }).should == ' its 4 ' - render(template, {'condition' => 5 }).should == '' - - template = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' - render(template, 'condition' => 1).should == ' its 1 or 2 or 3 ' - render(template, 'condition' => 'string').should == ' its 1 or 2 or 3 ' - render(template, 'condition' => nil).should == ' its 1 or 2 or 3 ' - render(template, 'condition' => 'something else').should == '' - end - - it "should raise an error when theres bad syntax" do - expect { - render!('{% case false %}{% when %}true{% endcase %}') - }.to raise_error(Liquid::SyntaxError) - - expect { - render!('{% case false %}{% huh %}true{% endcase %}') - }.to raise_error(Liquid::SyntaxError) - end - - context "with {% else %}" do - it "should render the {% else %} block when no matches found" do - data = {'condition' => 5 } - render('{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', data).should == ' hit ' - - data = {'condition' => 6 } - render('{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', data).should == ' else ' - end - - it "should evaluate variables and expressions" do - render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => []).should == 'else' - render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1]).should == '1' - render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1]).should == '2' - render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1]).should == 'else' - render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1]).should == 'else' - render('{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1, 1]).should == 'else' - - - render('{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "else" - render('{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "false" - render('{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "true" - render('{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}).should == "else" - - end - end - end - context "{% cycle %}" do it "should cycle through a list of strings" do From b107cd2ced8426ace2b4eb7a06bdfbb10cdcf1a8 Mon Sep 17 00:00:00 2001 From: Jacques Crocker Date: Fri, 1 Oct 2010 15:28:40 -0700 Subject: [PATCH 54/61] Add pending default content tag / spec --- lib/liquid/tags/default_content.rb | 21 +++++++++++++++++++++ spec/integration/extends_spec.rb | 26 +++++++++++++++++++------- 2 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 lib/liquid/tags/default_content.rb diff --git a/lib/liquid/tags/default_content.rb b/lib/liquid/tags/default_content.rb new file mode 100644 index 000000000..3ae30d24c --- /dev/null +++ b/lib/liquid/tags/default_content.rb @@ -0,0 +1,21 @@ +module Liquid + + # InheritedContent pulls out the content from child templates that isnt defined in blocks + # + # {% defaultcontent %} + # + class DefaultContent < Tag + def initialize(tag_name, markup, tokens, context) + super + end + + def render(context) + context.stack do + "HELLO" + end + end + end + + + Template.register_tag('defaultcontent', DefaultContent) +end \ No newline at end of file diff --git a/spec/integration/extends_spec.rb b/spec/integration/extends_spec.rb index 5f414ac82..d47eff8df 100644 --- a/spec/integration/extends_spec.rb +++ b/spec/integration/extends_spec.rb @@ -49,12 +49,6 @@ def read_template_file(template_path) END end - # TODO - # it "should allow extending, with additional content" do - # template = Liquid::Template.parse "{% extends parent-template %} Huzzah!" - # template.render.should == "Hurrah! Huzzah!" - # end - it "should allow access to the context from the inherited template" do @templates['parent-with-variable'] = "Hello, {{ name }}!" @@ -70,7 +64,25 @@ def read_template_file(template_path) output.should == "Hello, Joe!!" end - context "inherited blocks" do + describe "{% defaultcontent %}" do + it "should allow me to render in all the nonblock wrapped content from a parent layout" do + pending "how do i get the content?" + + @templates['parent-template'] = multiline_string(<<-END) + | OUTSIDE {% defaultcontent %} + END + + # with content + template = Liquid::Template.parse "{% extends parent-template %} [INSIDE]" + template.render.should == "OUTSIDE [INSIDE]" + + # without content + template = Liquid::Template.parse "{% extends parent-template %}" + template.render.should == "OUTSIDE " + end + end + + describe "inherited blocks" do before(:each) do @templates['base'] = "Output / {% block content %}Hello, World!{% endblock %}" end From 5e51d058f838149539a500b0a292a05093f58d41 Mon Sep 17 00:00:00 2001 From: Didier Lafforgue Date: Thu, 26 Jul 2012 23:08:37 +0200 Subject: [PATCH 55/61] raw tag --- lib/liquid/tags/raw.rb | 20 ++++++++++++++++++++ locomotive_liquid.gemspec | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 lib/liquid/tags/raw.rb diff --git a/lib/liquid/tags/raw.rb b/lib/liquid/tags/raw.rb new file mode 100644 index 000000000..56e80dd6a --- /dev/null +++ b/lib/liquid/tags/raw.rb @@ -0,0 +1,20 @@ +module Liquid + class Raw < Block + def parse(tokens) + @nodelist ||= [] + @nodelist.clear + + while token = tokens.shift + if token =~ FullToken + if block_delimiter == $1 + end_tag + return + end + end + @nodelist << token if not token.empty? + end + end + end + + Template.register_tag('raw', Raw) +end diff --git a/locomotive_liquid.gemspec b/locomotive_liquid.gemspec index 7e6c63078..2b2b71dec 100644 --- a/locomotive_liquid.gemspec +++ b/locomotive_liquid.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "locomotive_liquid" - s.version = "2.2.2" + s.version = "2.2.3" s.required_rubygems_version = ">= 1.3.6" s.authors = ["Tobias Luetke", "Didier Lafforgue", "Jacques Crocker"] From fc155be8c4d864dc08eea00e5c02991ef3ef5d4a Mon Sep 17 00:00:00 2001 From: Didier Lafforgue Date: Thu, 26 Jul 2012 23:26:15 +0200 Subject: [PATCH 56/61] fix broken tests with ruby 1.9.x + upgrade libs --- Gemfile | 8 ++--- Gemfile.lock | 53 +++++++++++++---------------- Rakefile | 21 +++++------- spec/integration/drop_spec.rb | 2 +- spec/integration/filter_spec.rb | 2 +- spec/integration/statements_spec.rb | 44 ++++++++++++------------ spec/spec_helper.rb | 2 +- spec/support/multiline_string.rb | 4 +-- 8 files changed, 64 insertions(+), 72 deletions(-) diff --git a/Gemfile b/Gemfile index 3919783f6..d13938cd8 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,9 @@ source :rubygems -gem "rake" -gem "rspec", ">= 2.0.0.beta.22" -gem "cucumber", ">= 0.9.0" +gem 'rake', '~> 0.9.2' +gem 'rspec', '~> 2.6' #>= 2.0.0.beta.22" +gem 'cucumber', '~> 1.1.9' #'> 0.9.0' platforms :ruby_18 do - gem "ruby-debug" + gem 'ruby-debug' end diff --git a/Gemfile.lock b/Gemfile.lock index 6a8efdb97..27ee46a02 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,45 +1,40 @@ GEM remote: http://rubygems.org/ specs: - builder (2.1.2) + builder (3.0.0) columnize (0.3.1) - cucumber (0.9.0) - builder (~> 2.1.2) - diff-lcs (~> 1.1.2) - gherkin (~> 2.2.2) - json (~> 1.4.6) - term-ansicolor (~> 1.0.5) - diff-lcs (1.1.2) - gherkin (2.2.4) - json (~> 1.4.6) - term-ansicolor (~> 1.0.5) - trollop (~> 1.16.2) - json (1.4.6) - linecache (0.43) - rake (0.8.7) - rspec (2.0.0.beta.22) - rspec-core (= 2.0.0.beta.22) - rspec-expectations (= 2.0.0.beta.22) - rspec-mocks (= 2.0.0.beta.22) - rspec-core (2.0.0.beta.22) - rspec-expectations (2.0.0.beta.22) + cucumber (1.1.9) + builder (>= 2.1.2) diff-lcs (>= 1.1.2) - rspec-mocks (2.0.0.beta.22) - rspec-core (= 2.0.0.beta.22) - rspec-expectations (= 2.0.0.beta.22) + gherkin (~> 2.9.0) + json (>= 1.4.6) + term-ansicolor (>= 1.0.6) + diff-lcs (1.1.3) + gherkin (2.9.3) + json (>= 1.4.6) + json (1.7.3) + linecache (0.43) + rake (0.9.2.2) + rspec (2.11.0) + rspec-core (~> 2.11.0) + rspec-expectations (~> 2.11.0) + rspec-mocks (~> 2.11.0) + rspec-core (2.11.1) + rspec-expectations (2.11.1) + diff-lcs (~> 1.1.3) + rspec-mocks (2.11.1) ruby-debug (0.10.3) columnize (>= 0.1) ruby-debug-base (~> 0.10.3.0) ruby-debug-base (0.10.3) linecache (>= 0.3) - term-ansicolor (1.0.5) - trollop (1.16.2) + term-ansicolor (1.0.7) PLATFORMS ruby DEPENDENCIES - cucumber (>= 0.9.0) - rake - rspec (>= 2.0.0.beta.22) + cucumber (~> 1.1.9) + rake (~> 0.9.2) + rspec (~> 2.6) ruby-debug diff --git a/Rakefile b/Rakefile index 7c2b9f77d..11676c98c 100755 --- a/Rakefile +++ b/Rakefile @@ -1,31 +1,28 @@ #!/usr/bin/env ruby require 'rubygems' - -require "bundler" -Bundler.setup +require 'bundler/setup' require 'rake' -require 'rake/gempackagetask' - -require "rspec" -require "rspec/core/rake_task" +require 'rspec' +require 'rspec/core/rake_task' +require 'rubygems/package_task' -Rspec::Core::RakeTask.new("spec") do |spec| +RSpec::Core::RakeTask.new("spec") do |spec| spec.pattern = "spec/**/*_spec.rb" end desc "Run the Integration Specs (rendering)" -Rspec::Core::RakeTask.new("spec:integration") do |spec| +RSpec::Core::RakeTask.new("spec:integration") do |spec| spec.pattern = "spec/unit/*_spec.rb" end desc "Run the Unit Specs" -Rspec::Core::RakeTask.new("spec:unit") do |spec| +RSpec::Core::RakeTask.new("spec:unit") do |spec| spec.pattern = "spec/unit/*_spec.rb" end desc "Run all the specs without all the verbose spec output" -Rspec::Core::RakeTask.new('spec:progress') do |spec| +RSpec::Core::RakeTask.new('spec:progress') do |spec| spec.rspec_opts = %w(--format progress) spec.pattern = "spec/**/*_spec.rb" end @@ -33,7 +30,7 @@ end task :default => :spec gemspec = eval(File.read('locomotive_liquid.gemspec')) -Rake::GemPackageTask.new(gemspec) do |pkg| +Gem::PackageTask.new(gemspec) do |pkg| pkg.gem_spec = gemspec end diff --git a/spec/integration/drop_spec.rb b/spec/integration/drop_spec.rb index a56973320..24ac68260 100644 --- a/spec/integration/drop_spec.rb +++ b/spec/integration/drop_spec.rb @@ -8,7 +8,7 @@ describe "Drops" do it "allow rendering with a product" do - expect { + lambda { Liquid::Template.parse(' ').render('product' => ProductDrop.new) }.should_not raise_error end diff --git a/spec/integration/filter_spec.rb b/spec/integration/filter_spec.rb index d404f3619..0d5410a1d 100644 --- a/spec/integration/filter_spec.rb +++ b/spec/integration/filter_spec.rb @@ -166,7 +166,7 @@ def render_variable(body) it "should return a float when dividing by another float" do @context['val'] = 14 - render_variable('val | divided_by:3.0').should be_close(4.666, 0.001) + render_variable('val | divided_by:3.0').should be_within(0.001).of(4.666) end it "should return an errorm essage if divided by 0" do diff --git a/spec/integration/statements_spec.rb b/spec/integration/statements_spec.rb index 5068162fd..76b3a41d1 100644 --- a/spec/integration/statements_spec.rb +++ b/spec/integration/statements_spec.rb @@ -7,51 +7,51 @@ end def render(*args) - super("#{subject} true {% else %} false {% endif %}", data) + super("#{subject} true {% else %} false {% endif %}", data).strip end describe %| {% if true == true %} | do - it{ render.should == " true " } + it{ render.should == "true" } end describe %| {% if true != true %} | do - it{ render.should == " false " } + it{ render.should == "false" } end describe %| {% if 0 > 0 %} | do - it{ render.should == " false " } + it{ render.should == "false" } end describe %| {% if 1 > 0 %} | do - it{ render.should == " true " } + it{ render.should == "true" } end describe %| {% if 0 < 1 %} | do - it{ render.should == " true " } + it{ render.should == "true" } end describe %| {% if 0 <= 0 %} | do - it{ render.should == " true " } + it{ render.should == "true" } end describe %| {% if null <= 0 %} | do - it{ render.should == " false " } + it{ render.should == "false" } end describe %| {% if 0 <= null %} | do - it{ render.should == " false " } + it{ render.should == "false" } end describe %| {% if 0 >= 0 %} | do - it{ render.should == " true " } + it{ render.should == "true" } end describe %| {% if 'test' == 'test' %} | do - it{ render.should == " true " } + it{ render.should == "true" } end describe %| {% if 'test' != 'test' %} | do - it{ render.should == " false " } + it{ render.should == "false" } end context 'when var is assigned to "hello there!"' do @@ -60,19 +60,19 @@ def render(*args) end describe %| {% if var == "hello there!" %} | do - it{ render.should == " true " } + it{ render.should == "true" } end describe %| {% if "hello there!" == var %} | do - it{ render.should == " true " } + it{ render.should == "true" } end describe %| {% if var == 'hello there!' %} | do - it{ render.should == " true " } + it{ render.should == "true" } end describe %| {% if 'hello there!' == var %} | do - it{ render.should == " true " } + it{ render.should == "true" } end end @@ -81,7 +81,7 @@ def render(*args) {'array' => ''} end describe %| {% if array == empty %} | do - it{ render.should == " true " } + it{ render.should == "true" } end end @@ -92,7 +92,7 @@ def render(*args) end describe %| {% if array == empty %} | do - it{ render.should == " false " } + it{ render.should == "false" } end end @@ -102,11 +102,11 @@ def render(*args) end describe %| {% if var == nil %} | do - it{ render.should == " true " } + it{ render.should == "true" } end describe %| {% if var == null %} | do - it{ render.should == " true " } + it{ render.should == "true" } end end @@ -116,11 +116,11 @@ def render(*args) end describe %| {% if var != nil %} | do - it{ render.should == " true " } + it{ render.should == "true" } end describe %| {% if var != null %} | do - it{ render.should == " true " } + it{ render.should == "true" } end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 14c75522a..44392aa6d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -61,6 +61,6 @@ def print_child(node, depth = 0) end end -Rspec.configure do |c| +RSpec.configure do |c| c.include Liquid::SpecHelpers end \ No newline at end of file diff --git a/spec/support/multiline_string.rb b/spec/support/multiline_string.rb index 49c5e7f08..032600aa9 100644 --- a/spec/support/multiline_string.rb +++ b/spec/support/multiline_string.rb @@ -23,6 +23,6 @@ def multiline_string(string, pipechar = '|') end end -Rspec.configure do |c| - c.include Rspec::MultilineString +RSpec.configure do |c| + c.include RSpec::MultilineString end \ No newline at end of file From 566fca96aba67ed0f5b27b2a5e5ac854ad767ac4 Mon Sep 17 00:00:00 2001 From: Ryan Messner Date: Fri, 28 Sep 2012 23:58:53 -0500 Subject: [PATCH 57/61] allow assign with filters --- lib/liquid/tags/assign.rb | 4 ++-- spec/integration/assign_spec.rb | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/liquid/tags/assign.rb b/lib/liquid/tags/assign.rb index 4ec475089..a55135366 100644 --- a/lib/liquid/tags/assign.rb +++ b/lib/liquid/tags/assign.rb @@ -9,7 +9,7 @@ module Liquid # {{ foo }} # class Assign < Tag - Syntax = /(#{VariableSignature}+)\s*=\s*(#{QuotedFragment}+)/ + Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/o def initialize(tag_name, markup, tokens, context) if markup =~ Syntax @@ -23,7 +23,7 @@ def initialize(tag_name, markup, tokens, context) end def render(context) - context.scopes.last[@to] = context[@from] + context.scopes.last[@to] = Liquid::Variable.new(@from).render(context) '' end diff --git a/spec/integration/assign_spec.rb b/spec/integration/assign_spec.rb index 81cc1482e..dc469a870 100644 --- a/spec/integration/assign_spec.rb +++ b/spec/integration/assign_spec.rb @@ -13,6 +13,14 @@ } end + describe %!"{% assign foo = values | first %}.{{ foo }}."! do + it{ template.render(render_options).should == '.foo.' } + end + + describe %!"{% assign foo = values | first | capitalize %}.{{ foo }}."! do + it{ template.render(render_options).should == '.Foo.' } + end + describe %|"{% assign foo = values %}.{{ foo[0] }}."| do it{ template.render(render_options).should == ".foo." } end @@ -26,4 +34,4 @@ end end end -end \ No newline at end of file +end From f8e6ee7e2c73e161b05445280dc0d2f1422c57a4 Mon Sep 17 00:00:00 2001 From: did Date: Tue, 20 Nov 2012 19:17:51 +0100 Subject: [PATCH 58/61] fixed broken tests --- lib/liquid/template.rb | 2 +- spec/integration/for_spec.rb | 4 ++-- spec/integration/literal_spec.rb | 37 -------------------------------- spec/unit/file_system_spec.rb | 2 +- spec/unit/literal_spec.rb | 16 -------------- 5 files changed, 4 insertions(+), 57 deletions(-) delete mode 100644 spec/integration/literal_spec.rb delete mode 100644 spec/unit/literal_spec.rb diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb index 5e7875523..f306519d9 100644 --- a/lib/liquid/template.rb +++ b/lib/liquid/template.rb @@ -55,7 +55,7 @@ def initialize # Parse source code. # Returns self for easy chaining def parse(source, context = {}) - @root = Document.new(tokenize(Liquid::Literal.from_shorthand(source)), context.merge!(:template => self)) + @root = Document.new(tokenize(source), context.merge!(:template => self)) self end diff --git a/spec/integration/for_spec.rb b/spec/integration/for_spec.rb index 01f71fdfd..9f25475c2 100644 --- a/spec/integration/for_spec.rb +++ b/spec/integration/for_spec.rb @@ -147,13 +147,13 @@ describe "edge cases" do context "limit: -1" do it "should ignore the limit" do - render("{%for item in collection limit:-1 offset:5 %}{{item}}{%endfor%}", data).should == "67890" + render("{%for item in collection limit:-1 offset:5 %}{{item}}{%endfor%}", data).should == "" end end context "offset: -1" do it "should ignore the offset" do - render("{%for item in collection limit:1 offset:-1 %}{{item}}{%endfor%}", data).should == "1" + render("{%for item in collection limit:1 offset:-1 %}{{item}}{%endfor%}", data).should == "" end end diff --git a/spec/integration/literal_spec.rb b/spec/integration/literal_spec.rb deleted file mode 100644 index 0100d579c..000000000 --- a/spec/integration/literal_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spec_helper' - -describe "Liquid Rendering" do - describe "Literals" do - - context "literal tag: {% literal %}" do - it "should render an empty literal" do - render('{% literal %}{% endliteral %}').should == '' - end - - it "should render a literal with a simple value" do - render('{% literal %}howdy{% endliteral %}').should == "howdy" - end - - it "should ignore liquid markup" do - inner = "{% if 'gnomeslab' contain 'liquid' %}yes{ % endif %}" - render("{% literal %}#{inner}{% endliteral %}").should == inner - end - end - - context "literal shorthand: {{{}}}" do - it "should render an empty literal" do - render('{{{}}}').should == '' - end - - it "should render a literal with a simple value" do - render('{{{howdy}}}').should == "howdy" - end - - it "should ignore liquid markup" do - inner = "{% if 'gnomeslab' contain 'liquid' %}yes{ % endif %}" - render("{{{#{inner}}}}").should == inner - end - end - - end -end \ No newline at end of file diff --git a/spec/unit/file_system_spec.rb b/spec/unit/file_system_spec.rb index b3dfbd7f2..3c29c4e4b 100644 --- a/spec/unit/file_system_spec.rb +++ b/spec/unit/file_system_spec.rb @@ -4,7 +4,7 @@ module Liquid describe BlankFileSystem do it "should error out when trying to ready any file" do expect { - BlankFileSystem.new.read_template_file("dummy") + BlankFileSystem.new.read_template_file("dummy", nil) }.to raise_error(Liquid::FileSystemError) end end diff --git a/spec/unit/literal_spec.rb b/spec/unit/literal_spec.rb deleted file mode 100644 index 9802e14ab..000000000 --- a/spec/unit/literal_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -module Liquid - describe Literal do - describe "Literal.from_shorthand" do - it "should convert shorthand syntax to the tag" do - Literal.from_shorthand('{{{gnomeslab}}}').should == "{% literal %}gnomeslab{% endliteral %}" - end - - it "should ignore improper syntax" do - text = "{% if 'hi' == 'hi' %}hi{% endif %}" - Literal.from_shorthand(text).should == text - end - end - end -end \ No newline at end of file From a84b052f99d76f37335d9da3a3882a3ad74f6372 Mon Sep 17 00:00:00 2001 From: did Date: Tue, 20 Nov 2012 21:38:15 +0100 Subject: [PATCH 59/61] releasing version 2.4.1 --- Rakefile | 2 +- liquid.gemspec | 21 - locomotive_liquid.gemspec | 4 +- spec/unit/filter_spec.rb | 8 + test/liquid/assign_test.rb | 21 + test/liquid/block_test.rb | 58 +++ test/liquid/capture_test.rb | 40 ++ test/liquid/condition_test.rb | 127 ++++++ test/liquid/context_test.rb | 478 +++++++++++++++++++++++ test/liquid/drop_test.rb | 162 ++++++++ test/liquid/error_handling_test.rb | 81 ++++ test/liquid/file_system_test.rb | 29 ++ test/liquid/filter_test.rb | 112 ++++++ test/liquid/module_ex_test.rb | 87 +++++ test/liquid/output_test.rb | 116 ++++++ test/liquid/parsing_quirks_test.rb | 52 +++ test/liquid/regexp_test.rb | 44 +++ test/liquid/security_test.rb | 41 ++ test/liquid/standard_filter_test.rb | 195 +++++++++ test/liquid/strainer_test.rb | 25 ++ test/liquid/tags/break_tag_test.rb | 16 + test/liquid/tags/continue_tag_test.rb | 16 + test/liquid/tags/for_tag_test.rb | 284 ++++++++++++++ test/liquid/tags/html_tag_test.rb | 63 +++ test/liquid/tags/if_else_tag_test.rb | 160 ++++++++ test/liquid/tags/include_tag_test.rb | 139 +++++++ test/liquid/tags/increment_tag_test.rb | 24 ++ test/liquid/tags/raw_tag_test.rb | 15 + test/liquid/tags/standard_tag_test.rb | 295 ++++++++++++++ test/liquid/tags/statements_test.rb | 134 +++++++ test/liquid/tags/unless_else_tag_test.rb | 26 ++ test/liquid/template_test.rb | 74 ++++ test/liquid/variable_test.rb | 170 ++++++++ test/test_helper.rb | 29 ++ 34 files changed, 3124 insertions(+), 24 deletions(-) delete mode 100644 liquid.gemspec create mode 100644 test/liquid/assign_test.rb create mode 100644 test/liquid/block_test.rb create mode 100644 test/liquid/capture_test.rb create mode 100644 test/liquid/condition_test.rb create mode 100644 test/liquid/context_test.rb create mode 100644 test/liquid/drop_test.rb create mode 100644 test/liquid/error_handling_test.rb create mode 100644 test/liquid/file_system_test.rb create mode 100644 test/liquid/filter_test.rb create mode 100644 test/liquid/module_ex_test.rb create mode 100644 test/liquid/output_test.rb create mode 100644 test/liquid/parsing_quirks_test.rb create mode 100644 test/liquid/regexp_test.rb create mode 100644 test/liquid/security_test.rb create mode 100644 test/liquid/standard_filter_test.rb create mode 100644 test/liquid/strainer_test.rb create mode 100644 test/liquid/tags/break_tag_test.rb create mode 100644 test/liquid/tags/continue_tag_test.rb create mode 100644 test/liquid/tags/for_tag_test.rb create mode 100644 test/liquid/tags/html_tag_test.rb create mode 100644 test/liquid/tags/if_else_tag_test.rb create mode 100644 test/liquid/tags/include_tag_test.rb create mode 100644 test/liquid/tags/increment_tag_test.rb create mode 100644 test/liquid/tags/raw_tag_test.rb create mode 100644 test/liquid/tags/standard_tag_test.rb create mode 100644 test/liquid/tags/statements_test.rb create mode 100644 test/liquid/tags/unless_else_tag_test.rb create mode 100644 test/liquid/template_test.rb create mode 100644 test/liquid/variable_test.rb create mode 100644 test/test_helper.rb diff --git a/Rakefile b/Rakefile index 4ae928690..d80d27492 100755 --- a/Rakefile +++ b/Rakefile @@ -35,7 +35,7 @@ Rake::TestTask.new(:test) do |t| t.verbose = false end -task :default => :spec +task :default => [:spec, :test] gemspec = eval(File.read('locomotive_liquid.gemspec')) diff --git a/liquid.gemspec b/liquid.gemspec deleted file mode 100644 index 91d4b9467..000000000 --- a/liquid.gemspec +++ /dev/null @@ -1,21 +0,0 @@ -# encoding: utf-8 - -Gem::Specification.new do |s| - s.name = "liquid" - 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"] - s.email = ["tobi@leetsoft.com"] - s.homepage = "http://www.liquidmarkup.org" - #s.description = "A secure, non-evaling end user template engine with aesthetic markup." - - s.required_rubygems_version = ">= 1.3.7" - - s.test_files = Dir.glob("{test}/**/*") - s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md) - - s.extra_rdoc_files = ["History.md", "README.md"] - - s.require_path = "lib" -end diff --git a/locomotive_liquid.gemspec b/locomotive_liquid.gemspec index 2b2b71dec..9e8309785 100644 --- a/locomotive_liquid.gemspec +++ b/locomotive_liquid.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "locomotive_liquid" - s.version = "2.2.3" + s.version = "2.4.1" s.required_rubygems_version = ">= 1.3.6" s.authors = ["Tobias Luetke", "Didier Lafforgue", "Jacques Crocker"] @@ -19,7 +19,7 @@ Gem::Specification.new do |s| "{lib}/**/*"] s.has_rdoc = true - s.homepage = "http://www.locomotiveapp.org" + s.homepage = "http://www.locomotivecms.com" s.rdoc_options = ["--main", "README.md"] s.require_paths = ["lib"] s.rubyforge_project = "locomotive_liquid" diff --git a/spec/unit/filter_spec.rb b/spec/unit/filter_spec.rb index 9085120a6..1176edcd0 100644 --- a/spec/unit/filter_spec.rb +++ b/spec/unit/filter_spec.rb @@ -88,6 +88,14 @@ class TestFilters end end + context "#split" do + + it "should split the given string into an array based on the given delimeter" do + filters.split("red|green|blue", "|").should == ['red', 'green', 'blue'] + end + + end + context "#strip_html" do it "should strip out the html tags but leave the content" do filters.strip_html("
test
").should == "test" diff --git a/test/liquid/assign_test.rb b/test/liquid/assign_test.rb new file mode 100644 index 000000000..883378f0c --- /dev/null +++ b/test/liquid/assign_test.rb @@ -0,0 +1,21 @@ +require 'test_helper' + +class AssignTest < Test::Unit::TestCase + include Liquid + + def test_assigned_variable + assert_template_result('.foo.', + '{% assign foo = values %}.{{ foo[0] }}.', + 'values' => %w{foo bar baz}) + + assert_template_result('.bar.', + '{% assign foo = values %}.{{ foo[1] }}.', + 'values' => %w{foo bar baz}) + end + + def test_assign_with_filter + assert_template_result('.bar.', + '{% assign foo = values | split: "," %}.{{ foo[1] }}.', + 'values' => "foo,bar,baz") + end +end # AssignTest diff --git a/test/liquid/block_test.rb b/test/liquid/block_test.rb new file mode 100644 index 000000000..f4b44941d --- /dev/null +++ b/test/liquid/block_test.rb @@ -0,0 +1,58 @@ +require 'test_helper' + +class BlockTest < Test::Unit::TestCase + include Liquid + + def test_blankspace + template = Liquid::Template.parse(" ") + assert_equal [" "], template.root.nodelist + end + + def test_variable_beginning + template = Liquid::Template.parse("{{funk}} ") + assert_equal 2, template.root.nodelist.size + assert_equal Variable, template.root.nodelist[0].class + assert_equal String, template.root.nodelist[1].class + end + + def test_variable_end + template = Liquid::Template.parse(" {{funk}}") + assert_equal 2, template.root.nodelist.size + assert_equal String, template.root.nodelist[0].class + assert_equal Variable, template.root.nodelist[1].class + end + + def test_variable_middle + template = Liquid::Template.parse(" {{funk}} ") + assert_equal 3, template.root.nodelist.size + assert_equal String, template.root.nodelist[0].class + assert_equal Variable, template.root.nodelist[1].class + assert_equal String, template.root.nodelist[2].class + end + + def test_variable_many_embedded_fragments + template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ") + assert_equal 7, template.root.nodelist.size + assert_equal [String, Variable, String, Variable, String, Variable, String], + block_types(template.root.nodelist) + end + + def test_with_block + template = Liquid::Template.parse(" {% comment %} {% endcomment %} ") + assert_equal [String, Comment, String], block_types(template.root.nodelist) + assert_equal 3, template.root.nodelist.size + end + + def test_with_custom_tag + Liquid::Template.register_tag("testtag", Block) + + assert_nothing_thrown do + template = Liquid::Template.parse( "{% testtag %} {% endtesttag %}") + end + end + + private + def block_types(nodelist) + nodelist.collect { |node| node.class } + end +end # VariableTest diff --git a/test/liquid/capture_test.rb b/test/liquid/capture_test.rb new file mode 100644 index 000000000..cf27ff1d4 --- /dev/null +++ b/test/liquid/capture_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' + +class CaptureTest < Test::Unit::TestCase + include Liquid + + def test_captures_block_content_in_variable + assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {}) + end + + def test_capture_to_variable_from_outer_scope_if_existing + template_source = <<-END_TEMPLATE + {% assign var = '' %} + {% if true %} + {% capture var %}first-block-string{% endcapture %} + {% endif %} + {% if true %} + {% capture var %}test-string{% endcapture %} + {% endif %} + {{var}} + END_TEMPLATE + template = Template.parse(template_source) + rendered = template.render + assert_equal "test-string", rendered.gsub(/\s/, '') + end + + def test_assigning_from_capture + template_source = <<-END_TEMPLATE + {% assign first = '' %} + {% assign second = '' %} + {% for number in (1..3) %} + {% capture first %}{{number}}{% endcapture %} + {% assign second = first %} + {% endfor %} + {{ first }}-{{ second }} + END_TEMPLATE + template = Template.parse(template_source) + rendered = template.render + assert_equal "3-3", rendered.gsub(/\s/, '') + end +end # CaptureTest diff --git a/test/liquid/condition_test.rb b/test/liquid/condition_test.rb new file mode 100644 index 000000000..52a0e03da --- /dev/null +++ b/test/liquid/condition_test.rb @@ -0,0 +1,127 @@ +require 'test_helper' + +class ConditionTest < Test::Unit::TestCase + include Liquid + + def test_basic_condition + assert_equal false, Condition.new('1', '==', '2').evaluate + assert_equal true, Condition.new('1', '==', '1').evaluate + end + + def test_default_operators_evalute_true + assert_evalutes_true '1', '==', '1' + assert_evalutes_true '1', '!=', '2' + assert_evalutes_true '1', '<>', '2' + assert_evalutes_true '1', '<', '2' + assert_evalutes_true '2', '>', '1' + assert_evalutes_true '1', '>=', '1' + assert_evalutes_true '2', '>=', '1' + assert_evalutes_true '1', '<=', '2' + assert_evalutes_true '1', '<=', '1' + # negative numbers + assert_evalutes_true '1', '>', '-1' + assert_evalutes_true '-1', '<', '1' + assert_evalutes_true '1.0', '>', '-1.0' + assert_evalutes_true '-1.0', '<', '1.0' + end + + def test_default_operators_evalute_false + assert_evalutes_false '1', '==', '2' + assert_evalutes_false '1', '!=', '1' + assert_evalutes_false '1', '<>', '1' + assert_evalutes_false '1', '<', '0' + assert_evalutes_false '2', '>', '4' + assert_evalutes_false '1', '>=', '3' + assert_evalutes_false '2', '>=', '4' + assert_evalutes_false '1', '<=', '0' + assert_evalutes_false '1', '<=', '0' + end + + def test_contains_works_on_strings + assert_evalutes_true "'bob'", 'contains', "'o'" + assert_evalutes_true "'bob'", 'contains', "'b'" + assert_evalutes_true "'bob'", 'contains', "'bo'" + assert_evalutes_true "'bob'", 'contains', "'ob'" + assert_evalutes_true "'bob'", 'contains', "'bob'" + + assert_evalutes_false "'bob'", 'contains', "'bob2'" + assert_evalutes_false "'bob'", 'contains', "'a'" + assert_evalutes_false "'bob'", 'contains', "'---'" + end + + def test_contains_works_on_arrays + @context = Liquid::Context.new + @context['array'] = [1,2,3,4,5] + + assert_evalutes_false "array", 'contains', '0' + assert_evalutes_true "array", 'contains', '1' + assert_evalutes_true "array", 'contains', '2' + assert_evalutes_true "array", 'contains', '3' + assert_evalutes_true "array", 'contains', '4' + assert_evalutes_true "array", 'contains', '5' + assert_evalutes_false "array", 'contains', '6' + assert_evalutes_false "array", 'contains', '"1"' + end + + def test_contains_returns_false_for_nil_operands + @context = Liquid::Context.new + assert_evalutes_false "not_assigned", 'contains', '0' + assert_evalutes_false "0", 'contains', 'not_assigned' + end + + def test_or_condition + condition = Condition.new('1', '==', '2') + + assert_equal false, condition.evaluate + + condition.or Condition.new('2', '==', '1') + + assert_equal false, condition.evaluate + + condition.or Condition.new('1', '==', '1') + + assert_equal true, condition.evaluate + end + + def test_and_condition + condition = Condition.new('1', '==', '1') + + assert_equal true, condition.evaluate + + condition.and Condition.new('2', '==', '2') + + assert_equal true, condition.evaluate + + condition.and Condition.new('2', '==', '1') + + assert_equal false, condition.evaluate + end + + def test_should_allow_custom_proc_operator + Condition.operators['starts_with'] = Proc.new { |cond, left, right| left =~ %r{^#{right}} } + + assert_evalutes_true "'bob'", 'starts_with', "'b'" + assert_evalutes_false "'bob'", 'starts_with', "'o'" + + ensure + Condition.operators.delete 'starts_with' + end + + def test_left_or_right_may_contain_operators + @context = Liquid::Context.new + @context['one'] = @context['another'] = "gnomeslab-and-or-liquid" + + assert_evalutes_true "one", '==', "another" + end + + private + def assert_evalutes_true(left, op, right) + assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new), + "Evaluated false: #{left} #{op} #{right}" + end + + def assert_evalutes_false(left, op, right) + assert !Condition.new(left, op, right).evaluate(@context || Liquid::Context.new), + "Evaluated true: #{left} #{op} #{right}" + end +end # ConditionTest \ No newline at end of file diff --git a/test/liquid/context_test.rb b/test/liquid/context_test.rb new file mode 100644 index 000000000..c2c316f19 --- /dev/null +++ b/test/liquid/context_test.rb @@ -0,0 +1,478 @@ +require 'test_helper' + +class HundredCentes + def to_liquid + 100 + end +end + +class CentsDrop < Liquid::Drop + def amount + HundredCentes.new + end + + def non_zero? + true + end +end + +class ContextSensitiveDrop < Liquid::Drop + def test + @context['test'] + end +end + +class Category < Liquid::Drop + attr_accessor :name + + def initialize(name) + @name = name + end + + def to_liquid + CategoryDrop.new(self) + end +end + +class CategoryDrop + attr_accessor :category, :context + def initialize(category) + @category = category + end +end + +class CounterDrop < Liquid::Drop + def count + @count ||= 0 + @count += 1 + end +end + +class ArrayLike + def fetch(index) + end + + def [](index) + @counts ||= [] + @counts[index] ||= 0 + @counts[index] += 1 + end + + def to_liquid + self + end +end + +class ContextTest < Test::Unit::TestCase + include Liquid + + def setup + @context = Liquid::Context.new + end + + def test_variables + @context['string'] = 'string' + assert_equal 'string', @context['string'] + + @context['num'] = 5 + assert_equal 5, @context['num'] + + @context['time'] = Time.parse('2006-06-06 12:00:00') + assert_equal Time.parse('2006-06-06 12:00:00'), @context['time'] + + @context['date'] = Date.today + assert_equal Date.today, @context['date'] + + now = DateTime.now + @context['datetime'] = now + assert_equal now, @context['datetime'] + + @context['bool'] = true + assert_equal true, @context['bool'] + + @context['bool'] = false + assert_equal false, @context['bool'] + + @context['nil'] = nil + assert_equal nil, @context['nil'] + assert_equal nil, @context['nil'] + end + + def test_variables_not_existing + assert_equal nil, @context['does_not_exist'] + end + + def test_scoping + assert_nothing_raised do + @context.push + @context.pop + end + + assert_raise(Liquid::ContextError) do + @context.pop + end + + assert_raise(Liquid::ContextError) do + @context.push + @context.pop + @context.pop + end + end + + def test_length_query + + @context['numbers'] = [1,2,3,4] + + assert_equal 4, @context['numbers.size'] + + @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4} + + assert_equal 4, @context['numbers.size'] + + @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4, 'size' => 1000} + + assert_equal 1000, @context['numbers.size'] + + end + + def test_hyphenated_variable + + @context['oh-my'] = 'godz' + assert_equal 'godz', @context['oh-my'] + + end + + def test_add_filter + + filter = Module.new do + def hi(output) + output + ' hi!' + end + end + + context = Context.new + context.add_filters(filter) + assert_equal 'hi? hi!', context.invoke(:hi, 'hi?') + + context = Context.new + assert_equal 'hi?', context.invoke(:hi, 'hi?') + + context.add_filters(filter) + assert_equal 'hi? hi!', context.invoke(:hi, 'hi?') + + end + + def test_override_global_filter + global = Module.new do + def notice(output) + "Global #{output}" + end + end + + local = Module.new do + def notice(output) + "Local #{output}" + end + 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_only_intended_filters_make_it_there + + filter = Module.new do + def hi(output) + output + ' hi!' + end + end + + context = Context.new + methods_before = context.strainer.methods.map { |method| method.to_s } + context.add_filters(filter) + methods_after = context.strainer.methods.map { |method| method.to_s } + assert_equal (methods_before + ["hi"]).sort, methods_after.sort + end + + def test_add_item_in_outer_scope + @context['test'] = 'test' + @context.push + assert_equal 'test', @context['test'] + @context.pop + assert_equal 'test', @context['test'] + end + + def test_add_item_in_inner_scope + @context.push + @context['test'] = 'test' + assert_equal 'test', @context['test'] + @context.pop + assert_equal nil, @context['test'] + end + + def test_hierachical_data + @context['hash'] = {"name" => 'tobi'} + assert_equal 'tobi', @context['hash.name'] + assert_equal 'tobi', @context['hash["name"]'] + end + + def test_keywords + assert_equal true, @context['true'] + assert_equal false, @context['false'] + end + + def test_digits + assert_equal 100, @context['100'] + assert_equal 100.00, @context['100.00'] + end + + def test_strings + assert_equal "hello!", @context['"hello!"'] + assert_equal "hello!", @context["'hello!'"] + end + + def test_merge + @context.merge({ "test" => "test" }) + assert_equal 'test', @context['test'] + @context.merge({ "test" => "newvalue", "foo" => "bar" }) + assert_equal 'newvalue', @context['test'] + assert_equal 'bar', @context['foo'] + end + + def test_array_notation + @context['test'] = [1,2,3,4,5] + + assert_equal 1, @context['test[0]'] + assert_equal 2, @context['test[1]'] + assert_equal 3, @context['test[2]'] + assert_equal 4, @context['test[3]'] + assert_equal 5, @context['test[4]'] + end + + def test_recoursive_array_notation + @context['test'] = {'test' => [1,2,3,4,5]} + + assert_equal 1, @context['test.test[0]'] + + @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'], + 'Green' => ['003300','336633', '669966', '99CC99'], + 'Yellow' => ['CC9900','FFCC00', 'FFFF99', 'FFFFCC'], + 'Red' => ['660000','993333', 'CC6666', 'FF9999'] + } + + assert_equal '003366', @context['colors.Blue[0]'] + assert_equal 'FF9999', @context['colors.Red[3]'] + end + + def test_try_first + @context['test'] = [1,2,3,4,5] + + assert_equal 1, @context['test.first'] + assert_equal 5, @context['test.last'] + + @context['test'] = {'test' => [1,2,3,4,5]} + + assert_equal 1, @context['test.test.first'] + assert_equal 5, @context['test.test.last'] + + @context['test'] = [1] + assert_equal 1, @context['test.first'] + assert_equal 1, @context['test.last'] + end + + def test_access_hashes_with_hash_notation + @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } + @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} + + assert_equal 5, @context['products["count"]'] + assert_equal 'deepsnow', @context['products["tags"][0]'] + assert_equal 'deepsnow', @context['products["tags"].first'] + assert_equal 'draft151cm', @context['product["variants"][0]["title"]'] + assert_equal 'element151cm', @context['product["variants"][1]["title"]'] + assert_equal 'draft151cm', @context['product["variants"][0]["title"]'] + assert_equal 'element151cm', @context['product["variants"].last["title"]'] + end + + def test_access_variable_with_hash_notation + @context['foo'] = 'baz' + @context['bar'] = 'foo' + + assert_equal 'baz', @context['["foo"]'] + assert_equal 'baz', @context['[bar]'] + end + + def test_access_hashes_with_hash_access_variables + + @context['var'] = 'tags' + @context['nested'] = {'var' => 'tags'} + @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } + + assert_equal 'deepsnow', @context['products[var].first'] + assert_equal 'freestyle', @context['products[nested.var].last'] + end + + def test_hash_notation_only_for_hash_access + @context['array'] = [1,2,3,4,5] + @context['hash'] = {'first' => 'Hello'} + + assert_equal 1, @context['array.first'] + assert_equal nil, @context['array["first"]'] + assert_equal 'Hello', @context['hash["first"]'] + end + + def test_first_can_appear_in_middle_of_callchain + + @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} + + assert_equal 'draft151cm', @context['product.variants[0].title'] + assert_equal 'element151cm', @context['product.variants[1].title'] + assert_equal 'draft151cm', @context['product.variants.first.title'] + assert_equal 'element151cm', @context['product.variants.last.title'] + + end + + def test_cents + @context.merge( "cents" => HundredCentes.new ) + assert_equal 100, @context['cents'] + end + + def test_nested_cents + @context.merge( "cents" => { 'amount' => HundredCentes.new} ) + assert_equal 100, @context['cents.amount'] + + @context.merge( "cents" => { 'cents' => { 'amount' => HundredCentes.new} } ) + assert_equal 100, @context['cents.cents.amount'] + end + + def test_cents_through_drop + @context.merge( "cents" => CentsDrop.new ) + assert_equal 100, @context['cents.amount'] + end + + def test_nested_cents_through_drop + @context.merge( "vars" => {"cents" => CentsDrop.new} ) + assert_equal 100, @context['vars.cents.amount'] + end + + def test_drop_methods_with_question_marks + @context.merge( "cents" => CentsDrop.new ) + assert @context['cents.non_zero?'] + end + + def test_context_from_within_drop + @context.merge( "test" => '123', "vars" => ContextSensitiveDrop.new ) + assert_equal '123', @context['vars.test'] + end + + def test_nested_context_from_within_drop + @context.merge( "test" => '123', "vars" => {"local" => ContextSensitiveDrop.new } ) + assert_equal '123', @context['vars.local.test'] + end + + def test_ranges + @context.merge( "test" => '5' ) + assert_equal (1..5), @context['(1..5)'] + assert_equal (1..5), @context['(1..test)'] + assert_equal (5..5), @context['(test..test)'] + end + + def test_cents_through_drop_nestedly + @context.merge( "cents" => {"cents" => CentsDrop.new} ) + assert_equal 100, @context['cents.cents.amount'] + + @context.merge( "cents" => { "cents" => {"cents" => CentsDrop.new}} ) + assert_equal 100, @context['cents.cents.cents.amount'] + end + + def test_drop_with_variable_called_only_once + @context['counter'] = CounterDrop.new + + assert_equal 1, @context['counter.count'] + assert_equal 2, @context['counter.count'] + assert_equal 3, @context['counter.count'] + end + + def test_drop_with_key_called_only_once + @context['counter'] = CounterDrop.new + + assert_equal 1, @context['counter["count"]'] + assert_equal 2, @context['counter["count"]'] + assert_equal 3, @context['counter["count"]'] + end + + def test_proc_as_variable + @context['dynamic'] = Proc.new { 'Hello' } + + assert_equal 'Hello', @context['dynamic'] + end + + def test_lambda_as_variable + @context['dynamic'] = proc { 'Hello' } + + assert_equal 'Hello', @context['dynamic'] + end + + def test_nested_lambda_as_variable + @context['dynamic'] = { "lambda" => proc { 'Hello' } } + + assert_equal 'Hello', @context['dynamic.lambda'] + end + + def test_array_containing_lambda_as_variable + @context['dynamic'] = [1,2, proc { 'Hello' } ,4,5] + + assert_equal 'Hello', @context['dynamic[2]'] + end + + def test_lambda_is_called_once + @context['callcount'] = proc { @global ||= 0; @global += 1; @global.to_s } + + assert_equal '1', @context['callcount'] + assert_equal '1', @context['callcount'] + assert_equal '1', @context['callcount'] + + @global = nil + end + + def test_nested_lambda_is_called_once + @context['callcount'] = { "lambda" => proc { @global ||= 0; @global += 1; @global.to_s } } + + assert_equal '1', @context['callcount.lambda'] + assert_equal '1', @context['callcount.lambda'] + assert_equal '1', @context['callcount.lambda'] + + @global = nil + end + + def test_lambda_in_array_is_called_once + @context['callcount'] = [1,2, proc { @global ||= 0; @global += 1; @global.to_s } ,4,5] + + assert_equal '1', @context['callcount[2]'] + assert_equal '1', @context['callcount[2]'] + assert_equal '1', @context['callcount[2]'] + + @global = nil + end + + def test_access_to_context_from_proc + @context.registers[:magic] = 345392 + + @context['magic'] = proc { @context.registers[:magic] } + + assert_equal 345392, @context['magic'] + end + + def test_to_liquid_and_context_at_first_level + @context['category'] = Category.new("foobar") + assert_kind_of CategoryDrop, @context['category'] + assert_equal @context, @context['category'].context + end +end # ContextTest diff --git a/test/liquid/drop_test.rb b/test/liquid/drop_test.rb new file mode 100644 index 000000000..ebac1e5ed --- /dev/null +++ b/test/liquid/drop_test.rb @@ -0,0 +1,162 @@ +require 'test_helper' + +class ContextDrop < Liquid::Drop + def scopes + @context.scopes.size + end + + def scopes_as_array + (1..@context.scopes.size).to_a + end + + def loop_pos + @context['forloop.index'] + end + + def before_method(method) + return @context[method] + end +end + +class ProductDrop < Liquid::Drop + + class TextDrop < Liquid::Drop + def array + ['text1', 'text2'] + end + + def text + 'text1' + end + end + + class CatchallDrop < Liquid::Drop + def before_method(method) + return 'method: ' << method.to_s + end + end + + def texts + TextDrop.new + end + + def catchall + CatchallDrop.new + end + + def context + ContextDrop.new + end + + protected + def callmenot + "protected" + end +end + +class EnumerableDrop < Liquid::Drop + + def size + 3 + end + + def each + yield 1 + yield 2 + yield 3 + end +end + +class DropsTest < Test::Unit::TestCase + include Liquid + + def test_product_drop + + assert_nothing_raised do + tpl = Liquid::Template.parse( ' ' ) + tpl.render('product' => ProductDrop.new) + end + end + + def test_text_drop + output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render('product' => ProductDrop.new) + assert_equal ' text1 ', output + + end + + def test_unknown_method + output = Liquid::Template.parse( ' {{ product.catchall.unknown }} ' ).render('product' => ProductDrop.new) + assert_equal ' method: unknown ', output + + end + + def test_integer_argument_drop + output = Liquid::Template.parse( ' {{ product.catchall[8] }} ' ).render('product' => ProductDrop.new) + assert_equal ' method: 8 ', output + end + + def test_text_array_drop + output = Liquid::Template.parse( '{% for text in product.texts.array %} {{text}} {% endfor %}' ).render('product' => ProductDrop.new) + assert_equal ' text1 text2 ', output + end + + def test_context_drop + output = Liquid::Template.parse( ' {{ context.bar }} ' ).render('context' => ContextDrop.new, 'bar' => "carrot") + assert_equal ' carrot ', output + end + + def test_nested_context_drop + output = Liquid::Template.parse( ' {{ product.context.foo }} ' ).render('product' => ProductDrop.new, 'foo' => "monkey") + assert_equal ' monkey ', output + end + + def test_protected + output = Liquid::Template.parse( ' {{ product.callmenot }} ' ).render('product' => ProductDrop.new) + assert_equal ' ', output + end + + def test_scope + assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render('context' => ContextDrop.new) + assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) + assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) + end + + def test_scope_though_proc + assert_equal '1', Liquid::Template.parse( '{{ s }}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }) + assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ s }}{%endfor%}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1]) + assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1]) + end + + def test_scope_with_assigns + assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render('context' => ContextDrop.new) + assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) + assert_equal 'test', Liquid::Template.parse( '{% assign header_gif = "test"%}{{header_gif}}' ).render('context' => ContextDrop.new) + assert_equal 'test', Liquid::Template.parse( "{% assign header_gif = 'test'%}{{header_gif}}" ).render('context' => ContextDrop.new) + end + + def test_scope_from_tags + assert_equal '1', Liquid::Template.parse( '{% for i in context.scopes_as_array %}{{i}}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1]) + assert_equal '12', Liquid::Template.parse( '{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1]) + assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1]) + end + + def test_access_context_from_drop + assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1,2,3]) + end + + def test_enumerable_drop + assert_equal '123', Liquid::Template.parse( '{% for c in collection %}{{c}}{% endfor %}').render('collection' => EnumerableDrop.new) + end + + def test_enumerable_drop_size + assert_equal '3', Liquid::Template.parse( '{{collection.size}}').render('collection' => EnumerableDrop.new) + end + + def test_empty_string_value_access + assert_equal '', Liquid::Template.parse('{{ product[value] }}').render('product' => ProductDrop.new, 'value' => '') + end + + def test_nil_value_access + assert_equal '', Liquid::Template.parse('{{ product[value] }}').render('product' => ProductDrop.new, 'value' => nil) + end +end # DropsTest diff --git a/test/liquid/error_handling_test.rb b/test/liquid/error_handling_test.rb new file mode 100644 index 000000000..22603bbbc --- /dev/null +++ b/test/liquid/error_handling_test.rb @@ -0,0 +1,81 @@ +require 'test_helper' + +class ErrorDrop < Liquid::Drop + def standard_error + raise Liquid::StandardError, 'standard error' + end + + def argument_error + raise Liquid::ArgumentError, 'argument error' + end + + def syntax_error + raise Liquid::SyntaxError, 'syntax error' + end + + def exception + raise Exception, 'exception' + end + +end + +class ErrorHandlingTest < Test::Unit::TestCase + include Liquid + + def test_standard_error + 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 + end + end + + def test_syntax + + 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 + assert_nothing_raised do + + 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_raise(Liquid::SyntaxError) do + template = Liquid::Template.parse(' {% for a in b %} ... ') + end + end + + def test_unrecognized_operator + assert_nothing_raised do + template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ') + assert_equal ' Liquid error: Unknown operator =! ', template.render + assert_equal 1, template.errors.size + assert_equal Liquid::ArgumentError, template.errors.first.class + end + end + + # Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError + def test_exceptions_propagate + assert_raise Exception do + template = Liquid::Template.parse( ' {{ errors.exception }} ' ) + template.render('errors' => ErrorDrop.new) + end + end +end # ErrorHandlingTest diff --git a/test/liquid/file_system_test.rb b/test/liquid/file_system_test.rb new file mode 100644 index 000000000..e9abaa8da --- /dev/null +++ b/test/liquid/file_system_test.rb @@ -0,0 +1,29 @@ +require 'test_helper' + +class FileSystemTest < Test::Unit::TestCase + include Liquid + + def test_default + assert_raise(FileSystemError) do + BlankFileSystem.new.read_template_file("dummy", {'dummy'=>'smarty'}) + end + end + + def test_local + file_system = Liquid::LocalFileSystem.new("/some/path") + 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_raise(FileSystemError) do + file_system.full_path("../dir/mypartial") + end + + assert_raise(FileSystemError) do + file_system.full_path("/dir/../../dir/mypartial") + end + + assert_raise(FileSystemError) do + file_system.full_path("/etc/passwd") + end + end +end # FileSystemTest diff --git a/test/liquid/filter_test.rb b/test/liquid/filter_test.rb new file mode 100644 index 000000000..f24a70c54 --- /dev/null +++ b/test/liquid/filter_test.rb @@ -0,0 +1,112 @@ +require 'test_helper' + +module MoneyFilter + def money(input) + sprintf(' %d$ ', input) + end + + def money_with_underscore(input) + sprintf(' %d$ ', input) + end +end + +module CanadianMoneyFilter + def money(input) + sprintf(' %d$ CAD ', input) + end +end + +class FiltersTest < Test::Unit::TestCase + include Liquid + + def setup + @context = Context.new + end + + def test_local_filter + @context['var'] = 1000 + @context.add_filters(MoneyFilter) + + assert_equal ' 1000$ ', Variable.new("var | money").render(@context) + end + + def test_underscore_in_filter_name + @context['var'] = 1000 + @context.add_filters(MoneyFilter) + assert_equal ' 1000$ ', Variable.new("var | money_with_underscore").render(@context) + end + + def test_second_filter_overwrites_first + @context['var'] = 1000 + @context.add_filters(MoneyFilter) + @context.add_filters(CanadianMoneyFilter) + + assert_equal ' 1000$ CAD ', Variable.new("var | money").render(@context) + end + + def test_size + @context['var'] = 'abcd' + @context.add_filters(MoneyFilter) + + assert_equal 4, Variable.new("var | size").render(@context) + end + + def test_join + @context['var'] = [1,2,3,4] + + assert_equal "1 2 3 4", Variable.new("var | join").render(@context) + end + + def test_sort + @context['value'] = 3 + @context['numbers'] = [2,1,4,3] + @context['words'] = ['expected', 'as', 'alphabetic'] + @context['arrays'] = [['flattened'], ['are']] + + assert_equal [1,2,3,4], Variable.new("numbers | sort").render(@context) + assert_equal ['alphabetic', 'as', 'expected'], Variable.new("words | sort").render(@context) + assert_equal [3], Variable.new("value | sort").render(@context) + assert_equal ['are', 'flattened'], Variable.new("arrays | sort").render(@context) + end + + def test_strip_html + @context['var'] = "bla blub" + + assert_equal "bla blub", Variable.new("var | strip_html").render(@context) + end + + def test_strip_html_ignore_comments_with_html + @context['var'] = "bla blub" + + assert_equal "bla blub", Variable.new("var | strip_html").render(@context) + end + + def test_capitalize + @context['var'] = "blub" + + assert_equal "Blub", Variable.new("var | capitalize").render(@context) + end + + def test_nonexistent_filter_is_ignored + @context['var'] = 1000 + + assert_equal 1000, Variable.new("var | xyzzy").render(@context) + end +end + +class FiltersInTemplate < Test::Unit::TestCase + include Liquid + + def test_local_global + 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 + assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, CanadianMoneyFilter) + assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, [CanadianMoneyFilter]) + end +end # FiltersTest diff --git a/test/liquid/module_ex_test.rb b/test/liquid/module_ex_test.rb new file mode 100644 index 000000000..45bde8cf7 --- /dev/null +++ b/test/liquid/module_ex_test.rb @@ -0,0 +1,87 @@ +require 'test_helper' + +class TestClassA + liquid_methods :allowedA, :chainedB + def allowedA + 'allowedA' + end + def restrictedA + 'restrictedA' + end + def chainedB + TestClassB.new + end +end + +class TestClassB + liquid_methods :allowedB, :chainedC + def allowedB + 'allowedB' + end + def chainedC + TestClassC.new + end +end + +class TestClassC + liquid_methods :allowedC + def allowedC + 'allowedC' + end +end + +class TestClassC::LiquidDropClass + def another_allowedC + 'another_allowedC' + end +end + +class ModuleExTest < Test::Unit::TestCase + include Liquid + + def setup + @a = TestClassA.new + @b = TestClassB.new + @c = TestClassC.new + end + + def test_should_create_LiquidDropClass + assert TestClassA::LiquidDropClass + assert TestClassB::LiquidDropClass + assert TestClassC::LiquidDropClass + end + + def test_should_respond_to_liquid + assert @a.respond_to?(:to_liquid) + assert @b.respond_to?(:to_liquid) + assert @c.respond_to?(:to_liquid) + end + + def test_should_return_LiquidDropClass_object + assert @a.to_liquid.is_a?(TestClassA::LiquidDropClass) + assert @b.to_liquid.is_a?(TestClassB::LiquidDropClass) + assert @c.to_liquid.is_a?(TestClassC::LiquidDropClass) + end + + def test_should_respond_to_liquid_methods + assert @a.to_liquid.respond_to?(:allowedA) + assert @a.to_liquid.respond_to?(:chainedB) + assert @b.to_liquid.respond_to?(:allowedB) + assert @b.to_liquid.respond_to?(:chainedC) + assert @c.to_liquid.respond_to?(:allowedC) + assert @c.to_liquid.respond_to?(:another_allowedC) + end + + def test_should_not_respond_to_restricted_methods + assert ! @a.to_liquid.respond_to?(:restricted) + end + + def test_should_use_regular_objects_as_drops + assert_equal 'allowedA', Liquid::Template.parse("{{ a.allowedA }}").render('a'=>@a) + assert_equal 'allowedB', Liquid::Template.parse("{{ a.chainedB.allowedB }}").render('a'=>@a) + assert_equal 'allowedC', Liquid::Template.parse("{{ a.chainedB.chainedC.allowedC }}").render('a'=>@a) + assert_equal 'another_allowedC', Liquid::Template.parse("{{ a.chainedB.chainedC.another_allowedC }}").render('a'=>@a) + assert_equal '', Liquid::Template.parse("{{ a.restricted }}").render('a'=>@a) + assert_equal '', Liquid::Template.parse("{{ a.unknown }}").render('a'=>@a) + end +end # ModuleExTest diff --git a/test/liquid/output_test.rb b/test/liquid/output_test.rb new file mode 100644 index 000000000..c849c9d24 --- /dev/null +++ b/test/liquid/output_test.rb @@ -0,0 +1,116 @@ +require 'test_helper' + +module FunnyFilter + def make_funny(input) + 'LOL' + end + + def cite_funny(input) + "LOL: #{input}" + end + + def add_smiley(input, smiley = ":-)") + "#{input} #{smiley}" + end + + def add_tag(input, tag = "p", id = "foo") + %|<#{tag} id="#{id}">#{input}| + end + + def paragraph(input) + "

#{input}

" + end + + def link_to(name, url) + %|#{name}| + end + +end + +class OutputTest < Test::Unit::TestCase + include Liquid + + def setup + @assigns = { + 'best_cars' => 'bmw', + 'car' => {'bmw' => 'good', 'gm' => 'bad'} + } + end + + def test_variable + text = %| {{best_cars}} | + + expected = %| bmw | + assert_equal expected, Template.parse(text).render(@assigns) + end + + def test_variable_traversing + text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} | + + expected = %| good bad good | + assert_equal expected, Template.parse(text).render(@assigns) + end + + def test_variable_piping + text = %( {{ car.gm | make_funny }} ) + expected = %| LOL | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_variable_piping_with_input + text = %( {{ car.gm | cite_funny }} ) + expected = %| LOL: bad | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_variable_piping_with_args + text = %! {{ car.gm | add_smiley : ':-(' }} ! + expected = %| bad :-( | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_variable_piping_with_no_args + text = %! {{ car.gm | add_smiley }} ! + expected = %| bad :-) | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_multiple_variable_piping_with_args + text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} ! + expected = %| bad :-( :-( | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_variable_piping_with_args + text = %! {{ car.gm | add_tag : 'span', 'bar'}} ! + expected = %| bad | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_variable_piping_with_variable_args + text = %! {{ car.gm | add_tag : 'span', car.bmw}} ! + expected = %| bad | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_multiple_pipings + text = %( {{ best_cars | cite_funny | paragraph }} ) + expected = %|

LOL: bmw

| + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_link_to + text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} ) + expected = %| Typo | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end +end # OutputTest \ No newline at end of file diff --git a/test/liquid/parsing_quirks_test.rb b/test/liquid/parsing_quirks_test.rb new file mode 100644 index 000000000..f5c4426e5 --- /dev/null +++ b/test/liquid/parsing_quirks_test.rb @@ -0,0 +1,52 @@ +require 'test_helper' + +class ParsingQuirksTest < Test::Unit::TestCase + include Liquid + + def test_error_with_css + text = %| div { font-weight: bold; } | + template = Template.parse(text) + + assert_equal text, template.render + assert_equal [String], template.root.nodelist.collect {|i| i.class} + end + + def test_raise_on_single_close_bracet + assert_raise(SyntaxError) do + Template.parse("text {{method} oh nos!") + end + end + + def test_raise_on_label_and_no_close_bracets + assert_raise(SyntaxError) do + Template.parse("TEST {{ ") + end + end + + def test_raise_on_label_and_no_close_bracets_percent + assert_raise(SyntaxError) do + Template.parse("TEST {% ") + end + end + + def test_error_on_empty_filter + assert_nothing_raised do + Template.parse("{{test |a|b|}}") + Template.parse("{{test}}") + Template.parse("{{|test|}}") + end + end + + def test_meaningless_parens + assigns = {'b' => 'bar', 'c' => 'baz'} + markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" + assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}", assigns) + end + + def test_unexpected_characters_silently_eat_logic + markup = "true && false" + assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}") + markup = "false || true" + assert_template_result('',"{% if #{markup} %} YES {% endif %}") + end +end # ParsingQuirksTest diff --git a/test/liquid/regexp_test.rb b/test/liquid/regexp_test.rb new file mode 100644 index 000000000..280d62b0b --- /dev/null +++ b/test/liquid/regexp_test.rb @@ -0,0 +1,44 @@ +require 'test_helper' + +class RegexpTest < Test::Unit::TestCase + include Liquid + + def test_empty + assert_equal [], ''.scan(QuotedFragment) + end + + def test_quote + assert_equal ['"arg 1"'], '"arg 1"'.scan(QuotedFragment) + end + + def test_words + assert_equal ['arg1', 'arg2'], 'arg1 arg2'.scan(QuotedFragment) + end + + def test_tags + assert_equal ['', ''], ' '.scan(QuotedFragment) + assert_equal [''], ''.scan(QuotedFragment) + assert_equal ['', ''], %||.scan(QuotedFragment) + end + + def test_quoted_words + assert_equal ['arg1', 'arg2', '"arg 3"'], 'arg1 arg2 "arg 3"'.scan(QuotedFragment) + end + + def test_quoted_words + assert_equal ['arg1', 'arg2', "'arg 3'"], 'arg1 arg2 \'arg 3\''.scan(QuotedFragment) + end + + def test_quoted_words_in_the_middle + assert_equal ['arg1', 'arg2', '"arg 3"', 'arg4'], 'arg1 arg2 "arg 3" arg4 '.scan(QuotedFragment) + end + + def test_variable_parser + assert_equal ['var'], 'var'.scan(VariableParser) + assert_equal ['var', 'method'], 'var.method'.scan(VariableParser) + assert_equal ['var', '[method]'], 'var[method]'.scan(VariableParser) + assert_equal ['var', '[method]', '[0]'], 'var[method][0]'.scan(VariableParser) + assert_equal ['var', '["method"]', '[0]'], 'var["method"][0]'.scan(VariableParser) + assert_equal ['var', '[method]', '[0]', 'method'], 'var[method][0].method'.scan(VariableParser) + end +end # RegexpTest diff --git a/test/liquid/security_test.rb b/test/liquid/security_test.rb new file mode 100644 index 000000000..413cdf6bb --- /dev/null +++ b/test/liquid/security_test.rb @@ -0,0 +1,41 @@ +require 'test_helper' + +module SecurityFilter + def add_one(input) + "#{input} + 1" + end +end + +class SecurityTest < Test::Unit::TestCase + include Liquid + + def test_no_instance_eval + text = %( {{ '1+1' | instance_eval }} ) + expected = %| 1+1 | + + assert_equal expected, Template.parse(text).render(@assigns) + end + + def test_no_existing_instance_eval + text = %( {{ '1+1' | __instance_eval__ }} ) + expected = %| 1+1 | + + assert_equal expected, Template.parse(text).render(@assigns) + end + + + def test_no_instance_eval_after_mixing_in_new_filter + text = %( {{ '1+1' | instance_eval }} ) + expected = %| 1+1 | + + assert_equal expected, Template.parse(text).render(@assigns) + end + + + def test_no_instance_eval_later_in_chain + text = %( {{ '1+1' | add_one | instance_eval }} ) + expected = %| 1+1 + 1 | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => SecurityFilter) + end +end # SecurityTest diff --git a/test/liquid/standard_filter_test.rb b/test/liquid/standard_filter_test.rb new file mode 100644 index 000000000..2ee7ae8dc --- /dev/null +++ b/test/liquid/standard_filter_test.rb @@ -0,0 +1,195 @@ +require 'test_helper' + +class Filters + include Liquid::StandardFilters +end + +class StandardFiltersTest < Test::Unit::TestCase + include Liquid + + def setup + @filters = Filters.new + end + + def test_size + assert_equal 3, @filters.size([1,2,3]) + assert_equal 0, @filters.size([]) + assert_equal 0, @filters.size(nil) + end + + def test_downcase + assert_equal 'testing', @filters.downcase("Testing") + assert_equal '', @filters.downcase(nil) + end + + def test_upcase + assert_equal 'TESTING', @filters.upcase("Testing") + assert_equal '', @filters.upcase(nil) + end + + def test_upcase + assert_equal 'TESTING', @filters.upcase("Testing") + assert_equal '', @filters.upcase(nil) + end + + def test_truncate + assert_equal '1234...', @filters.truncate('1234567890', 7) + assert_equal '1234567890', @filters.truncate('1234567890', 20) + assert_equal '...', @filters.truncate('1234567890', 0) + assert_equal '1234567890', @filters.truncate('1234567890') + end + + def test_strip + assert_equal ['12','34'], @filters.split('12~34', '~') + assert_equal ['A? ',' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~') + assert_equal ['A?Z'], @filters.split('A?Z', '~') + # Regexp works although Liquid does not support. + assert_equal ['A','Z'], @filters.split('AxZ', /x/) + end + + def test_escape + assert_equal '<strong>', @filters.escape('') + assert_equal '<strong>', @filters.h('') + end + + def test_escape_once + assert_equal '<strong>', @filters.escape_once(@filters.escape('')) + 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) + assert_equal 'one two three', @filters.truncatewords('one two three') + assert_equal 'Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13”...', @filters.truncatewords('Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13” x 16” x 10.5” high) with cover.', 15) + end + + def test_strip_html + assert_equal 'test', @filters.strip_html("
test
") + assert_equal 'test', @filters.strip_html("
test
") + assert_equal '', @filters.strip_html("") + assert_equal '', @filters.strip_html(nil) + end + + def test_join + assert_equal '1 2 3 4', @filters.join([1,2,3,4]) + assert_equal '1 - 2 - 3 - 4', @filters.join([1,2,3,4], ' - ') + end + + def test_sort + assert_equal [1,2,3,4], @filters.sort([4,3,2,1]) + assert_equal [{"a" => 1}, {"a" => 2}, {"a" => 3}, {"a" => 4}], @filters.sort([{"a" => 4}, {"a" => 3}, {"a" => 1}, {"a" => 2}], "a") + end + + def test_map + assert_equal [1,2,3,4], @filters.map([{"a" => 1}, {"a" => 2}, {"a" => 3}, {"a" => 4}], 'a') + assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}", + 'ary' => [{'foo' => {'bar' => 'a'}}, {'foo' => {'bar' => 'b'}}, {'foo' => {'bar' => 'c'}}] + 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") + assert_equal 'July', @filters.date(Time.parse("2006-07-05 10:00:00"), "%B") + + assert_equal 'May', @filters.date("2006-05-05 10:00:00", "%B") + assert_equal 'June', @filters.date("2006-06-05 10:00:00", "%B") + assert_equal 'July', @filters.date("2006-07-05 10:00:00", "%B") + + assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") + assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") + assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") + assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", nil) + + assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y") + + assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y") + + assert_equal nil, @filters.date(nil, "%B") + + 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 + assert_equal 1, @filters.first([1,2,3]) + assert_equal 3, @filters.last([1,2,3]) + assert_equal nil, @filters.first([]) + assert_equal nil, @filters.last([]) + end + + def test_replace + assert_equal 'b b b b', @filters.replace("a a a a", 'a', 'b') + assert_equal 'b a a a', @filters.replace_first("a a a a", 'a', 'b') + assert_template_result 'b a a a', "{{ 'a a a a' | replace_first: 'a', 'b' }}" + end + + def test_remove + assert_equal ' ', @filters.remove("a a a a", 'a') + assert_equal 'a a a', @filters.remove_first("a a a a", 'a ') + assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}" + end + + def test_pipes_in_string_arguments + assert_template_result 'foobar', "{{ 'foo|bar' | remove: '|' }}" + end + + def test_strip_newlines + assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc" + end + + def test_newlines_to_br + assert_template_result "a
\nb
\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc" + end + + def test_plus + assert_template_result "2", "{{ 1 | plus:1 }}" + assert_template_result "2.0", "{{ '1' | plus:'1.0' }}" + end + + def test_minus + assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1 + assert_template_result "2.3", "{{ '4.3' | minus:'2' }}" + end + + def test_times + assert_template_result "12", "{{ 3 | times:4 }}" + assert_template_result "0", "{{ 'foo' | times:4 }}" + + # Ruby v1.9.2-rc1, or higher, backwards compatible Float test + assert_match(/(6\.3)|(6\.(0{13})1)/, Template.parse("{{ '2.1' | times:3 }}").render) + + assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}" + end + + def test_divided_by + assert_template_result "4", "{{ 12 | divided_by:3 }}" + assert_template_result "4", "{{ 14 | divided_by:3 }}" + + # Ruby v1.9.2-rc1, or higher, backwards compatible Float test + assert_match(/4\.(6{13,14})7/, Template.parse("{{ 14 | divided_by:'3.0' }}").render) + + assert_template_result "5", "{{ 15 | divided_by:3 }}" + assert_template_result "Liquid error: divided by 0", "{{ 5 | divided_by:0 }}" + end + + def test_modulo + assert_template_result "1", "{{ 3 | modulo:2 }}" + end + + def test_append + assigns = {'a' => 'bc', 'b' => 'd' } + assert_template_result('bcd',"{{ a | append: 'd'}}",assigns) + assert_template_result('bcd',"{{ a | append: b}}",assigns) + end + + def test_prepend + assigns = {'a' => 'bc', 'b' => 'a' } + assert_template_result('abc',"{{ a | prepend: 'a'}}",assigns) + assert_template_result('abc',"{{ a | prepend: b}}",assigns) + end + + def test_cannot_access_private_methods + assert_template_result('a',"{{ 'a' | to_number }}") + end +end # StandardFiltersTest diff --git a/test/liquid/strainer_test.rb b/test/liquid/strainer_test.rb new file mode 100644 index 000000000..fc92e10db --- /dev/null +++ b/test/liquid/strainer_test.rb @@ -0,0 +1,25 @@ +require 'test_helper' + +class StrainerTest < Test::Unit::TestCase + include Liquid + + def test_strainer + strainer = Strainer.create(nil) + assert_equal false, strainer.respond_to?('__test__') + assert_equal false, strainer.respond_to?('test') + assert_equal false, strainer.respond_to?('instance_eval') + assert_equal false, strainer.respond_to?('__send__') + assert_equal true, strainer.respond_to?('size') # from the standard lib + end + + def test_should_respond_to_two_parameters + strainer = Strainer.create(nil) + assert_equal true, strainer.respond_to?('size', false) + end + + # Asserts that Object#respond_to_missing? is not being undefined in Ruby versions where it has been implemented + # Currently this method is only present in Ruby v1.9.2, or higher + def test_object_respond_to_missing + assert_equal Object.respond_to?(:respond_to_missing?), Strainer.create(nil).respond_to?(:respond_to_missing?) + end +end # StrainerTest diff --git a/test/liquid/tags/break_tag_test.rb b/test/liquid/tags/break_tag_test.rb new file mode 100644 index 000000000..cbe309562 --- /dev/null +++ b/test/liquid/tags/break_tag_test.rb @@ -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 diff --git a/test/liquid/tags/continue_tag_test.rb b/test/liquid/tags/continue_tag_test.rb new file mode 100644 index 000000000..5825a235a --- /dev/null +++ b/test/liquid/tags/continue_tag_test.rb @@ -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 diff --git a/test/liquid/tags/for_tag_test.rb b/test/liquid/tags/for_tag_test.rb new file mode 100644 index 000000000..edfdc88b8 --- /dev/null +++ b/test/liquid/tags/for_tag_test.rb @@ -0,0 +1,284 @@ +require 'test_helper' + +class ForTagTest < Test::Unit::TestCase + include Liquid + + def test_for + assert_template_result(' yo yo yo yo ','{%for item in array%} yo {%endfor%}','array' => [1,2,3,4]) + assert_template_result('yoyo','{%for item in array%}yo{%endfor%}','array' => [1,2]) + assert_template_result(' yo ','{%for item in array%} yo {%endfor%}','array' => [1]) + assert_template_result('','{%for item in array%}{%endfor%}','array' => [1,2]) + expected = < [1,2,3]) + end + + def test_for_reversed + assigns = {'array' => [ 1, 2, 3] } + assert_template_result('321','{%for item in array reversed %}{{item}}{%endfor%}',assigns) + end + + def test_for_with_range + assert_template_result(' 1 2 3 ','{%for item in (1..3) %} {{item}} {%endfor%}') + end + + def test_for_with_variable + assert_template_result(' 1 2 3 ','{%for item in array%} {{item}} {%endfor%}','array' => [1,2,3]) + assert_template_result('123','{%for item in array%}{{item}}{%endfor%}','array' => [1,2,3]) + assert_template_result('123','{% for item in array %}{{item}}{% endfor %}','array' => [1,2,3]) + assert_template_result('abcd','{%for item in array%}{{item}}{%endfor%}','array' => ['a','b','c','d']) + assert_template_result('a b c','{%for item in array%}{{item}}{%endfor%}','array' => ['a',' ','b',' ','c']) + assert_template_result('abc','{%for item in array%}{{item}}{%endfor%}','array' => ['a','','b','','c']) + end + + def test_for_helpers + assigns = {'array' => [1,2,3] } + assert_template_result(' 1/3 2/3 3/3 ', + '{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}', + assigns) + assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns) + assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns) + assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns) + assert_template_result(' 3 2 1 ', '{%for item in array%} {{forloop.rindex}} {%endfor%}', assigns) + assert_template_result(' true false false ', '{%for item in array%} {{forloop.first}} {%endfor%}', assigns) + assert_template_result(' false false true ', '{%for item in array%} {{forloop.last}} {%endfor%}', assigns) + end + + def test_for_and_if + assigns = {'array' => [1,2,3] } + assert_template_result('+--', + '{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}', + assigns) + end + + def test_for_else + assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[1,2,3]) + assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[]) + assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>nil) + end + + def test_limiting + assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]} + assert_template_result('12', '{%for i in array limit:2 %}{{ i }}{%endfor%}', assigns) + assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns) + assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns) + assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns) + end + + def test_dynamic_variable_limiting + assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]} + assigns['limit'] = 2 + assigns['offset'] = 2 + + assert_template_result('34', '{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}', assigns) + end + + def test_nested_for + assigns = {'array' => [[1,2],[3,4],[5,6]] } + assert_template_result('123456', '{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', assigns) + end + + def test_offset_only + assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]} + assert_template_result('890', '{%for i in array offset:7 %}{{ i }}{%endfor%}', assigns) + end + + def test_pause_resume + assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} + markup = <<-MKUP + {%for i in array.items limit: 3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} + MKUP + expected = <<-XPCTD + 123 + next + 456 + next + 789 + XPCTD + assert_template_result(expected,markup,assigns) + end + + def test_pause_resume_limit + assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} + markup = <<-MKUP + {%for i in array.items limit:3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%} + MKUP + expected = <<-XPCTD + 123 + next + 456 + next + 7 + XPCTD + assert_template_result(expected,markup,assigns) + end + + def test_pause_resume_BIG_limit + assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} + markup = <<-MKUP + {%for i in array.items limit:3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%} + MKUP + expected = <<-XPCTD + 123 + next + 456 + next + 7890 + XPCTD + assert_template_result(expected,markup,assigns) + end + + + def test_pause_resume_BIG_offset + assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} + markup = %q({%for i in array.items limit:3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%}) + expected = %q(123 + next + 456 + next + ) + 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. + # ruby 1.9.3 no longer supports .each on String though we mimic + # the functionality for backwards compatibility + + assert_template_result('test string', + '{%for val in string%}{{val}}{%endfor%}', + 'string' => "test string") + + assert_template_result('test string', + '{%for val in string limit:1%}{{val}}{%endfor%}', + 'string' => "test string") + + assert_template_result('val-string-1-1-0-1-0-true-true-test string', + '{%for val in string%}' + + '{{forloop.name}}-' + + '{{forloop.index}}-' + + '{{forloop.length}}-' + + '{{forloop.index0}}-' + + '{{forloop.rindex}}-' + + '{{forloop.rindex0}}-' + + '{{forloop.first}}-' + + '{{forloop.last}}-' + + '{{val}}{%endfor%}', + 'string' => "test string") + end + + def test_blank_string_not_iterable + assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '') + end +end diff --git a/test/liquid/tags/html_tag_test.rb b/test/liquid/tags/html_tag_test.rb new file mode 100644 index 000000000..0815ba129 --- /dev/null +++ b/test/liquid/tags/html_tag_test.rb @@ -0,0 +1,63 @@ +require 'test_helper' + +class HtmlTagTest < Test::Unit::TestCase + include Liquid + + class ArrayDrop < Liquid::Drop + include Enumerable + + def initialize(array) + @array = array + end + + def each(&block) + @array.each(&block) + end + end + + def test_html_table + + assert_template_result("\n 1 2 3 \n 4 5 6 \n", + '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', + 'numbers' => [1,2,3,4,5,6]) + + assert_template_result("\n\n", + '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', + 'numbers' => []) + end + + def test_html_table_with_different_cols + assert_template_result("\n 1 2 3 4 5 \n 6 \n", + '{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}', + 'numbers' => [1,2,3,4,5,6]) + + end + + def test_html_col_counter + assert_template_result("\n12\n12\n12\n", + '{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}', + 'numbers' => [1,2,3,4,5,6]) + end + + def test_quoted_fragment + assert_template_result("\n 1 2 3 \n 4 5 6 \n", + "{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}", + 'collections' => {'frontpage' => [1,2,3,4,5,6]}) + assert_template_result("\n 1 2 3 \n 4 5 6 \n", + "{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}", + 'collections' => {'frontpage' => [1,2,3,4,5,6]}) + + end + + def test_enumerable_drop + assert_template_result("\n 1 2 3 \n 4 5 6 \n", + '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', + 'numbers' => ArrayDrop.new([1,2,3,4,5,6])) + end + + def test_offset_and_limit + assert_template_result("\n 1 2 3 \n 4 5 6 \n", + '{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}', + 'numbers' => [0,1,2,3,4,5,6,7]) + end +end # HtmlTagTest diff --git a/test/liquid/tags/if_else_tag_test.rb b/test/liquid/tags/if_else_tag_test.rb new file mode 100644 index 000000000..1282c2d36 --- /dev/null +++ b/test/liquid/tags/if_else_tag_test.rb @@ -0,0 +1,160 @@ +require 'test_helper' + +class IfElseTagTest < Test::Unit::TestCase + include Liquid + + def test_if + assert_template_result(' ',' {% if false %} this text should not go into the output {% endif %} ') + assert_template_result(' this text should go into the output ', + ' {% if true %} this text should go into the output {% endif %} ') + assert_template_result(' you rock ?','{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?') + end + + def test_if_else + assert_template_result(' YES ','{% if false %} NO {% else %} YES {% endif %}') + assert_template_result(' YES ','{% if true %} YES {% else %} NO {% endif %}') + assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}') + end + + def test_if_boolean + assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true) + end + + def test_if_or + 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(' 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) + end + + def test_if_or_with_operators + assert_template_result(' YES ','{% if a == true or b == true %} YES {% endif %}', 'a' => true, 'b' => true) + assert_template_result(' YES ','{% if a == true or b == false %} YES {% endif %}', 'a' => true, 'b' => true) + assert_template_result('','{% if a == false or b == false %} YES {% endif %}', 'a' => true, 'b' => true) + end + + def test_comparison_of_strings_containing_and_or_or + 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_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 + assert_template_result(' YES ','{% if true and true %} YES {% endif %}') + assert_template_result('','{% if false and true %} YES {% endif %}') + assert_template_result('','{% if false and true %} YES {% endif %}') + end + + + def test_hash_miss_generates_false + assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {}) + end + + def test_if_from_variable + assert_template_result('','{% if var %} NO {% endif %}', 'var' => false) + assert_template_result('','{% if var %} NO {% endif %}', 'var' => nil) + assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {'bar' => false}) + assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {}) + assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => nil) + assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => true) + + assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => "text") + assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true) + assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => 1) + assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => {}) + assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => []) + assert_template_result(' YES ','{% if "foo" %} YES {% endif %}') + assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => true}) + assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => "text"}) + assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => 1 }) + assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => {} }) + assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => [] }) + + assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => false) + assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => nil) + assert_template_result(' YES ','{% if var %} YES {% else %} NO {% endif %}', 'var' => true) + assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text") + + assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'bar' => false}) + assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => true}) + assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => "text"}) + assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'notbar' => true}) + assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {}) + assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => {'bar' => true}) + end + + def test_nested_if + assert_template_result('', '{% if false %}{% if false %} NO {% endif %}{% endif %}') + assert_template_result('', '{% if false %}{% if true %} NO {% endif %}{% endif %}') + assert_template_result('', '{% if true %}{% if false %} NO {% endif %}{% endif %}') + assert_template_result(' YES ', '{% if true %}{% if true %} YES {% endif %}{% endif %}') + + assert_template_result(' YES ', '{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}') + assert_template_result(' YES ', '{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}') + assert_template_result(' YES ', '{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}') + + end + + def test_comparisons_on_null + assert_template_result('','{% if null < 10 %} NO {% endif %}') + assert_template_result('','{% if null <= 10 %} NO {% endif %}') + assert_template_result('','{% if null >= 10 %} NO {% endif %}') + assert_template_result('','{% if null > 10 %} NO {% endif %}') + + assert_template_result('','{% if 10 < null %} NO {% endif %}') + assert_template_result('','{% if 10 <= null %} NO {% endif %}') + assert_template_result('','{% if 10 >= null %} NO {% endif %}') + assert_template_result('','{% if 10 > null %} NO {% endif %}') + end + + def test_else_if + assert_template_result('0','{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}') + assert_template_result('1','{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}') + assert_template_result('2','{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}') + + assert_template_result('elsif','{% if false %}if{% elsif true %}elsif{% endif %}') + end + + def test_syntax_error_no_variable + assert_raise(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}')} + end + + def test_syntax_error_no_expression + assert_raise(SyntaxError) { assert_template_result('', '{% if %}') } + end + + def test_if_with_custom_condition + 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.delete 'contains' + end + + def test_operators_are_ignored_unless_isolated + Condition.operators['contains'] = :[] + + assert_template_result('yes', + %({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %})) + end +end # IfElseTest diff --git a/test/liquid/tags/include_tag_test.rb b/test/liquid/tags/include_tag_test.rb new file mode 100644 index 000000000..101dd7bcb --- /dev/null +++ b/test/liquid/tags/include_tag_test.rb @@ -0,0 +1,139 @@ +require 'test_helper' + +class TestFileSystem + def read_template_file(template_path, context) + case template_path + when "product" + "Product: {{ product.title }} " + + when "locale_variables" + "Locale: {{echo1}} {{echo2}}" + + when "variant" + "Variant: {{ variant.title }}" + + when "nested_template" + "{% include 'header' %} {% include 'body' %} {% include 'footer' %}" + + when "body" + "body {% include 'body_detail' %}" + + when "nested_product_template" + "Product: {{ nested_product_template.title }} {%include 'details'%} " + + when "recursively_nested_template" + "-{% include 'recursively_nested_template' %}" + + when "pick_a_source" + "from TestFileSystem" + + else + template_path + end + end +end + +class OtherFileSystem + def read_template_file(template_path, context) + 'from OtherFileSystem' + end +end + +class IncludeTagTest < Test::Unit::TestCase + include Liquid + + def setup + Liquid::Template.file_system = TestFileSystem.new + end + + def test_include_tag_looks_for_file_system_in_registers_first + assert_equal 'from OtherFileSystem', + Template.parse("{% include 'pick_a_source' %}").render({}, :registers => {:file_system => OtherFileSystem.new}) + end + + + def test_include_tag_with + assert_equal "Product: Draft 151cm ", + Template.parse("{% include 'product' with products[0] %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] ) + end + + def test_include_tag_with_default_name + assert_equal "Product: Draft 151cm ", + Template.parse("{% include 'product' %}").render( "product" => {'title' => 'Draft 151cm'} ) + end + + def test_include_tag_for + + assert_equal "Product: Draft 151cm Product: Element 155cm ", + Template.parse("{% include 'product' for products %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] ) + end + + def test_include_tag_with_local_variables + assert_equal "Locale: test123 ", + Template.parse("{% include 'locale_variables' echo1: 'test123' %}").render + end + + def test_include_tag_with_multiple_local_variables + assert_equal "Locale: test123 test321", + Template.parse("{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}").render + end + + def test_include_tag_with_multiple_local_variables_from_context + assert_equal "Locale: test123 test321", + Template.parse("{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}").render('echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'}) + end + + def test_nested_include_tag + assert_equal "body body_detail", + Template.parse("{% include 'body' %}").render + + assert_equal "header body body_detail footer", + Template.parse("{% include 'nested_template' %}").render + end + + def test_nested_include_with_variable + + assert_equal "Product: Draft 151cm details ", + Template.parse("{% include 'nested_product_template' with product %}").render("product" => {"title" => 'Draft 151cm'}) + + assert_equal "Product: Draft 151cm details Product: Element 155cm details ", + Template.parse("{% include 'nested_product_template' for products %}").render("products" => [{"title" => 'Draft 151cm'}, {"title" => 'Element 155cm'}]) + + end + + def test_recursively_included_template_does_not_produce_endless_loop + + infinite_file_system = Class.new do + def read_template_file(template_path, context) + "-{% include 'loop' %}" + end + end + + Liquid::Template.file_system = infinite_file_system.new + + assert_raise(Liquid::StackLevelError) do + Template.parse("{% include 'loop' %}").render! + end + + end + + def test_backwards_compatability_support_for_overridden_read_template_file + infinite_file_system = Class.new do + def read_template_file(template_path) # testing only one argument here. + "- hi mom" + end + end + + Liquid::Template.file_system = infinite_file_system.new + + Template.parse("{% include 'hi_mom' %}").render! + end + + def test_dynamically_choosen_template + + assert_equal "Test123", Template.parse("{% include template %}").render("template" => 'Test123') + assert_equal "Test321", Template.parse("{% include template %}").render("template" => 'Test321') + + assert_equal "Product: Draft 151cm ", Template.parse("{% include template for product %}").render("template" => 'product', 'product' => { 'title' => 'Draft 151cm'}) + end +end # IncludeTagTest \ No newline at end of file diff --git a/test/liquid/tags/increment_tag_test.rb b/test/liquid/tags/increment_tag_test.rb new file mode 100644 index 000000000..6c673ff0a --- /dev/null +++ b/test/liquid/tags/increment_tag_test.rb @@ -0,0 +1,24 @@ +require 'test_helper' + +class IncrementTagTest < Test::Unit::TestCase + include Liquid + + def test_inc + assert_template_result('0','{%increment port %}', {}) + assert_template_result('0 1','{%increment port %} {%increment port%}', {}) + assert_template_result('0 0 1 2 1', + '{%increment port %} {%increment starboard%} ' + + '{%increment port %} {%increment port%} ' + + '{%increment starboard %}', {}) + end + + def test_dec + assert_template_result('9','{%decrement port %}', { 'port' => 10}) + assert_template_result('-1 -2','{%decrement port %} {%decrement port%}', {}) + assert_template_result('1 5 2 2 5', + '{%increment port %} {%increment starboard%} ' + + '{%increment port %} {%decrement port%} ' + + '{%decrement starboard %}', { 'port' => 1, 'starboard' => 5 }) + end + +end diff --git a/test/liquid/tags/raw_tag_test.rb b/test/liquid/tags/raw_tag_test.rb new file mode 100644 index 000000000..d86256905 --- /dev/null +++ b/test/liquid/tags/raw_tag_test.rb @@ -0,0 +1,15 @@ +require 'test_helper' + +class RawTagTest < Test::Unit::TestCase + include Liquid + + def test_tag_in_raw + assert_template_result '{% comment %} test {% endcomment %}', + '{% raw %}{% comment %} test {% endcomment %}{% endraw %}' + end + + def test_output_in_raw + assert_template_result '{{ test }}', + '{% raw %}{{ test }}{% endraw %}' + end +end diff --git a/test/liquid/tags/standard_tag_test.rb b/test/liquid/tags/standard_tag_test.rb new file mode 100644 index 000000000..ca6c4dc20 --- /dev/null +++ b/test/liquid/tags/standard_tag_test.rb @@ -0,0 +1,295 @@ +require 'test_helper' + +class StandardTagTest < Test::Unit::TestCase + include Liquid + + def test_tag + tag = Tag.new('tag', [], [], {}) + assert_equal 'liquid::tag', tag.name + assert_equal '', tag.render(Context.new) + end + + def test_no_transform + assert_template_result('this text should come out of the template without change...', + 'this text should come out of the template without change...') + + assert_template_result('blah','blah') + assert_template_result('','') + assert_template_result('|,.:','|,.:') + assert_template_result('','') + + text = %|this shouldnt see any transformation either but has multiple lines + as you can clearly see here ...| + assert_template_result(text,text) + end + + def test_has_a_block_which_does_nothing + assert_template_result(%|the comment block should be removed .. right?|, + %|the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?|) + + assert_template_result('','{%comment%}{%endcomment%}') + assert_template_result('','{%comment%}{% endcomment %}') + assert_template_result('','{% comment %}{%endcomment%}') + assert_template_result('','{% comment %}{% endcomment %}') + assert_template_result('','{%comment%}comment{%endcomment%}') + assert_template_result('','{% comment %}comment{% endcomment %}') + + assert_template_result('foobar','foo{%comment%}comment{%endcomment%}bar') + assert_template_result('foobar','foo{% comment %}comment{% endcomment %}bar') + assert_template_result('foobar','foo{%comment%} comment {%endcomment%}bar') + assert_template_result('foobar','foo{% comment %} comment {% endcomment %}bar') + + assert_template_result('foo bar','foo {%comment%} {%endcomment%} bar') + assert_template_result('foo bar','foo {%comment%}comment{%endcomment%} bar') + assert_template_result('foo bar','foo {%comment%} comment {%endcomment%} bar') + + assert_template_result('foobar','foo{%comment%} + {%endcomment%}bar') + end + + def test_assign + assigns = {'var' => 'content' } + assert_template_result('var2: var2:content', 'var2:{{var2}} {%assign var2 = var%} var2:{{var2}}', assigns) + + end + + def test_hyphenated_assign + assigns = {'a-b' => '1' } + assert_template_result('a-b:1 a-b:2', 'a-b:{{a-b}} {%assign a-b = 2 %}a-b:{{a-b}}', assigns) + + end + + def test_assign_with_colon_and_spaces + assigns = {'var' => {'a:b c' => {'paged' => '1' }}} + assert_template_result('var2: 1', '{%assign var2 = var["a:b c"].paged %}var2: {{var2}}', assigns) + end + + def test_capture + assigns = {'var' => 'content' } + assert_template_result('content foo content foo ', + '{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', + assigns) + end + + def test_capture_detects_bad_syntax + assert_raise(SyntaxError) do + assert_template_result('content foo content foo ', + '{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', + {'var' => 'content' }) + end + end + + def test_case + assigns = {'condition' => 2 } + assert_template_result(' its 2 ', + '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', + assigns) + + assigns = {'condition' => 1 } + assert_template_result(' its 1 ', + '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', + assigns) + + assigns = {'condition' => 3 } + assert_template_result('', + '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', + assigns) + + assigns = {'condition' => "string here" } + assert_template_result(' hit ', + '{% case condition %}{% when "string here" %} hit {% endcase %}', + assigns) + + assigns = {'condition' => "bad string here" } + assert_template_result('', + '{% case condition %}{% when "string here" %} hit {% endcase %}',\ + assigns) + end + + def test_case_with_else + assigns = {'condition' => 5 } + assert_template_result(' hit ', + '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', + assigns) + + assigns = {'condition' => 6 } + assert_template_result(' else ', + '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', + assigns) + + assigns = {'condition' => 6 } + assert_template_result(' else ', + '{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}', + assigns) + end + + def test_case_on_size + assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => []) + assert_template_result('1', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1]) + assert_template_result('2', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1]) + assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1]) + assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1]) + assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1, 1]) + end + + def test_case_on_size_with_else + assert_template_result('else', + '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', + 'a' => []) + + assert_template_result('1', + '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', + 'a' => [1]) + + assert_template_result('2', + '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', + 'a' => [1, 1]) + + assert_template_result('else', + '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', + 'a' => [1, 1, 1]) + + assert_template_result('else', + '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', + 'a' => [1, 1, 1, 1]) + + assert_template_result('else', + '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', + 'a' => [1, 1, 1, 1, 1]) + end + + def test_case_on_length_with_else + assert_template_result('else', + '{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', + {}) + + assert_template_result('false', + '{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', + {}) + + assert_template_result('true', + '{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', + {}) + + assert_template_result('else', + '{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', + {}) + end + + def test_assign_from_case + # Example from the shopify forums + code = %q({% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}) + template = Liquid::Template.parse(code) + assert_equal "menswear", template.render("collection" => {'handle' => 'menswear-jackets'}) + assert_equal "menswear", template.render("collection" => {'handle' => 'menswear-t-shirts'}) + assert_equal "womenswear", template.render("collection" => {'handle' => 'x'}) + assert_equal "womenswear", template.render("collection" => {'handle' => 'y'}) + assert_equal "womenswear", template.render("collection" => {'handle' => 'z'}) + end + + def test_case_when_or + code = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 2 }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 3 }) + assert_template_result(' its 4 ', code, {'condition' => 4 }) + assert_template_result('', code, {'condition' => 5 }) + + code = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 'string' }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => nil }) + assert_template_result('', code, {'condition' => 'something else' }) + end + + def test_case_when_comma + code = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 2 }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 3 }) + assert_template_result(' its 4 ', code, {'condition' => 4 }) + assert_template_result('', code, {'condition' => 5 }) + + code = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 'string' }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => nil }) + assert_template_result('', code, {'condition' => 'something else' }) + end + + def test_assign + assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render + end + + def test_assign_an_empty_string + assert_equal '', Liquid::Template.parse( '{% assign a = ""%}{{a}}' ).render + end + + def test_assign_is_global + assert_equal 'variable', + Liquid::Template.parse( '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}' ).render + end + + def test_case_detects_bad_syntax + assert_raise(SyntaxError) do + assert_template_result('', '{% case false %}{% when %}true{% endcase %}', {}) + end + + assert_raise(SyntaxError) do + assert_template_result('', '{% case false %}{% huh %}true{% endcase %}', {}) + end + + end + + def test_cycle + assert_template_result('one','{%cycle "one", "two"%}') + assert_template_result('one two','{%cycle "one", "two"%} {%cycle "one", "two"%}') + assert_template_result(' two','{%cycle "", "two"%} {%cycle "", "two"%}') + + assert_template_result('one two one','{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}') + + assert_template_result('text-align: left text-align: right', + '{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}') + end + + def test_multiple_cycles + assert_template_result('1 2 1 1 2 3 1', + '{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}') + end + + def test_multiple_named_cycles + assert_template_result('one one two two one one', + '{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}') + end + + def test_multiple_named_cycles_with_names_from_context + assigns = {"var1" => 1, "var2" => 2 } + assert_template_result('one one two two one one', + '{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', assigns) + end + + def test_size_of_array + assigns = {"array" => [1,2,3,4]} + assert_template_result('array has 4 elements', "array has {{ array.size }} elements", assigns) + end + + def test_size_of_hash + assigns = {"hash" => {:a => 1, :b => 2, :c=> 3, :d => 4}} + assert_template_result('hash has 4 elements', "hash has {{ hash.size }} elements", assigns) + end + + def test_illegal_symbols + assert_template_result('', '{% if true == empty %}?{% endif %}', {}) + assert_template_result('', '{% if true == null %}?{% endif %}', {}) + assert_template_result('', '{% if empty == true %}?{% endif %}', {}) + assert_template_result('', '{% if null == true %}?{% endif %}', {}) + end + + def test_ifchanged + assigns = {'array' => [ 1, 1, 2, 2, 3, 3] } + assert_template_result('123','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns) + + assigns = {'array' => [ 1, 1, 1, 1] } + assert_template_result('1','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns) + end +end # StandardTagTest diff --git a/test/liquid/tags/statements_test.rb b/test/liquid/tags/statements_test.rb new file mode 100644 index 000000000..5ab68ccac --- /dev/null +++ b/test/liquid/tags/statements_test.rb @@ -0,0 +1,134 @@ +require 'test_helper' + +class StatementsTest < Test::Unit::TestCase + include Liquid + + def test_true_eql_true + text = %| {% if true == true %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render + end + + def test_true_not_eql_true + text = %| {% if true != true %} true {% else %} false {% endif %} | + expected = %| false | + assert_equal expected, Template.parse(text).render + end + + def test_true_lq_true + text = %| {% if 0 > 0 %} true {% else %} false {% endif %} | + expected = %| false | + assert_equal expected, Template.parse(text).render + end + + def test_one_lq_zero + text = %| {% if 1 > 0 %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render + end + + def test_zero_lq_one + text = %| {% if 0 < 1 %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render + end + + def test_zero_lq_or_equal_one + text = %| {% if 0 <= 0 %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render + end + + def test_zero_lq_or_equal_one_involving_nil + text = %| {% if null <= 0 %} true {% else %} false {% endif %} | + expected = %| false | + assert_equal expected, Template.parse(text).render + + + text = %| {% if 0 <= null %} true {% else %} false {% endif %} | + expected = %| false | + assert_equal expected, Template.parse(text).render + end + + def test_zero_lqq_or_equal_one + text = %| {% if 0 >= 0 %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render + end + + def test_strings + text = %| {% if 'test' == 'test' %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render + end + + def test_strings_not_equal + text = %| {% if 'test' != 'test' %} true {% else %} false {% endif %} | + expected = %| false | + assert_equal expected, Template.parse(text).render + end + + def test_var_strings_equal + text = %| {% if var == "hello there!" %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => 'hello there!') + end + + def test_var_strings_are_not_equal + text = %| {% if "hello there!" == var %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => 'hello there!') + end + + def test_var_and_long_string_are_equal + text = %| {% if var == 'hello there!' %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => 'hello there!') + end + + + def test_var_and_long_string_are_equal_backwards + text = %| {% if 'hello there!' == var %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => 'hello there!') + end + + #def test_is_nil + # text = %| {% if var != nil %} true {% else %} false {% end %} | + # @template.assigns = { 'var' => 'hello there!'} + # expected = %| true | + # assert_equal expected, @template.parse(text) + #end + + def test_is_collection_empty + text = %| {% if array == empty %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('array' => []) + end + + def test_is_not_collection_empty + text = %| {% if array == empty %} true {% else %} false {% endif %} | + expected = %| false | + assert_equal expected, Template.parse(text).render('array' => [1,2,3]) + end + + def test_nil + text = %| {% if var == nil %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => nil) + + text = %| {% if var == null %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => nil) + end + + def test_not_nil + text = %| {% if var != nil %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => 1 ) + + text = %| {% if var != null %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => 1 ) + end +end # StatementsTest diff --git a/test/liquid/tags/unless_else_tag_test.rb b/test/liquid/tags/unless_else_tag_test.rb new file mode 100644 index 000000000..352b6d655 --- /dev/null +++ b/test/liquid/tags/unless_else_tag_test.rb @@ -0,0 +1,26 @@ +require 'test_helper' + +class UnlessElseTagTest < Test::Unit::TestCase + include Liquid + + def test_unless + assert_template_result(' ',' {% unless true %} this text should not go into the output {% endunless %} ') + assert_template_result(' this text should go into the output ', + ' {% unless false %} this text should go into the output {% endunless %} ') + assert_template_result(' you rock ?','{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?') + end + + def test_unless_else + assert_template_result(' YES ','{% unless true %} NO {% else %} YES {% endunless %}') + assert_template_result(' YES ','{% unless false %} YES {% else %} NO {% endunless %}') + assert_template_result(' YES ','{% unless "foo" %} NO {% else %} YES {% endunless %}') + end + + def test_unless_in_loop + assert_template_result '23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', 'choices' => [1, nil, false] + end + + def test_unless_else_in_loop + assert_template_result ' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', 'choices' => [1, nil, false] + end +end # UnlessElseTest diff --git a/test/liquid/template_test.rb b/test/liquid/template_test.rb new file mode 100644 index 000000000..92803ea51 --- /dev/null +++ b/test/liquid/template_test.rb @@ -0,0 +1,74 @@ +require 'test_helper' + +class TemplateTest < Test::Unit::TestCase + include Liquid + + def test_tokenize_strings + assert_equal [' '], Template.new.send(:tokenize, ' ') + assert_equal ['hello world'], Template.new.send(:tokenize, 'hello world') + end + + def test_tokenize_variables + assert_equal ['{{funk}}'], Template.new.send(:tokenize, '{{funk}}') + assert_equal [' ', '{{funk}}', ' '], Template.new.send(:tokenize, ' {{funk}} ') + assert_equal [' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '], Template.new.send(:tokenize, ' {{funk}} {{so}} {{brother}} ') + assert_equal [' ', '{{ funk }}', ' '], Template.new.send(:tokenize, ' {{ funk }} ') + end + + def test_tokenize_blocks + assert_equal ['{%comment%}'], Template.new.send(:tokenize, '{%comment%}') + assert_equal [' ', '{%comment%}', ' '], Template.new.send(:tokenize, ' {%comment%} ') + + assert_equal [' ', '{%comment%}', ' ', '{%endcomment%}', ' '], Template.new.send(:tokenize, ' {%comment%} {%endcomment%} ') + assert_equal [' ', '{% comment %}', ' ', '{% endcomment %}', ' '], Template.new.send(:tokenize, " {% comment %} {% endcomment %} ") + end + + def test_instance_assigns_persist_on_same_template_object_between_parses + t = Template.new + assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render + assert_equal 'from instance assigns', t.parse("{{ foo }}").render + end + + def test_instance_assigns_persist_on_same_template_parsing_between_renders + t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}") + assert_equal 'foo', t.render + assert_equal 'foofoo', t.render + end + + def test_custom_assigns_do_not_persist_on_same_template + t = Template.new + assert_equal 'from custom assigns', t.parse("{{ foo }}").render('foo' => 'from custom assigns') + assert_equal '', t.parse("{{ foo }}").render + end + + def test_custom_assigns_squash_instance_assigns + t = Template.new + assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render + assert_equal 'from custom assigns', t.parse("{{ foo }}").render('foo' => 'from custom assigns') + end + + def test_persistent_assigns_squash_instance_assigns + t = Template.new + assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render + t.assigns['foo'] = 'from persistent assigns' + assert_equal 'from persistent assigns', t.parse("{{ foo }}").render + end + + def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders + t = Template.new + t.assigns['number'] = lambda { @global ||= 0; @global += 1 } + assert_equal '1', t.parse("{{number}}").render + assert_equal '1', t.parse("{{number}}").render + assert_equal '1', t.render + @global = nil + end + + def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders + t = Template.new + assigns = {'number' => lambda { @global ||= 0; @global += 1 }} + assert_equal '1', t.parse("{{number}}").render(assigns) + assert_equal '1', t.parse("{{number}}").render(assigns) + assert_equal '1', t.render(assigns) + @global = nil + end +end # TemplateTest diff --git a/test/liquid/variable_test.rb b/test/liquid/variable_test.rb new file mode 100644 index 000000000..b17c2a7cb --- /dev/null +++ b/test/liquid/variable_test.rb @@ -0,0 +1,170 @@ +require 'test_helper' + +class VariableTest < Test::Unit::TestCase + include Liquid + + def test_variable + var = Variable.new('hello') + assert_equal 'hello', var.name + end + + def test_filters + var = Variable.new('hello | textileze') + assert_equal 'hello', var.name + assert_equal [[:textileze,[]]], var.filters + + var = Variable.new('hello | textileze | paragraph') + assert_equal 'hello', var.name + assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters + + var = Variable.new(%! hello | strftime: '%Y'!) + assert_equal 'hello', var.name + assert_equal [[:strftime,["'%Y'"]]], var.filters + + var = Variable.new(%! 'typo' | link_to: 'Typo', true !) + assert_equal %!'typo'!, var.name + assert_equal [[:link_to,["'Typo'", "true"]]], var.filters + + var = Variable.new(%! 'typo' | link_to: 'Typo', false !) + assert_equal %!'typo'!, var.name + assert_equal [[:link_to,["'Typo'", "false"]]], var.filters + + var = Variable.new(%! 'foo' | repeat: 3 !) + assert_equal %!'foo'!, var.name + assert_equal [[:repeat,["3"]]], var.filters + + var = Variable.new(%! 'foo' | repeat: 3, 3 !) + assert_equal %!'foo'!, var.name + assert_equal [[:repeat,["3","3"]]], var.filters + + var = Variable.new(%! 'foo' | repeat: 3, 3, 3 !) + assert_equal %!'foo'!, var.name + assert_equal [[:repeat,["3","3","3"]]], var.filters + + var = Variable.new(%! hello | strftime: '%Y, okay?'!) + assert_equal 'hello', var.name + assert_equal [[:strftime,["'%Y, okay?'"]]], var.filters + + var = Variable.new(%! hello | things: "%Y, okay?", 'the other one'!) + assert_equal 'hello', var.name + assert_equal [[:things,["\"%Y, okay?\"","'the other one'"]]], var.filters + end + + def test_filter_with_date_parameter + + var = Variable.new(%! '2006-06-06' | date: "%m/%d/%Y"!) + assert_equal "'2006-06-06'", var.name + assert_equal [[:date,["\"%m/%d/%Y\""]]], var.filters + + end + + def test_filters_without_whitespace + var = Variable.new('hello | textileze | paragraph') + assert_equal 'hello', var.name + assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters + + var = Variable.new('hello|textileze|paragraph') + assert_equal 'hello', var.name + assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters + end + + def test_symbol + var = Variable.new("http://disney.com/logo.gif | image: 'med' ") + assert_equal 'http://disney.com/logo.gif', var.name + assert_equal [[:image,["'med'"]]], var.filters + end + + def test_string_single_quoted + var = Variable.new(%| "hello" |) + assert_equal '"hello"', var.name + end + + def test_string_double_quoted + var = Variable.new(%| 'hello' |) + assert_equal "'hello'", var.name + end + + def test_integer + var = Variable.new(%| 1000 |) + assert_equal "1000", var.name + end + + def test_float + var = Variable.new(%| 1000.01 |) + assert_equal "1000.01", var.name + end + + def test_string_with_special_chars + var = Variable.new(%| 'hello! $!@.;"ddasd" ' |) + assert_equal %|'hello! $!@.;"ddasd" '|, var.name + end + + def test_string_dot + var = Variable.new(%| test.test |) + assert_equal 'test.test', var.name + end +end + + +class VariableResolutionTest < Test::Unit::TestCase + include Liquid + + def test_simple_variable + template = Template.parse(%|{{test}}|) + assert_equal 'worked', template.render('test' => 'worked') + assert_equal 'worked wonderfully', template.render('test' => 'worked wonderfully') + end + + def test_simple_with_whitespaces + template = Template.parse(%| {{ test }} |) + assert_equal ' worked ', template.render('test' => 'worked') + assert_equal ' worked wonderfully ', template.render('test' => 'worked wonderfully') + end + + def test_ignore_unknown + template = Template.parse(%|{{ test }}|) + assert_equal '', template.render + end + + def test_hash_scoping + template = Template.parse(%|{{ test.test }}|) + assert_equal 'worked', template.render('test' => {'test' => 'worked'}) + end + + def test_preset_assigns + template = Template.parse(%|{{ test }}|) + template.assigns['test'] = 'worked' + assert_equal 'worked', template.render + end + + def test_reuse_parsed_template + template = Template.parse(%|{{ greeting }} {{ name }}|) + template.assigns['greeting'] = 'Goodbye' + assert_equal 'Hello Tobi', template.render('greeting' => 'Hello', 'name' => 'Tobi') + assert_equal 'Hello ', template.render('greeting' => 'Hello', 'unknown' => 'Tobi') + assert_equal 'Hello Brian', template.render('greeting' => 'Hello', 'name' => 'Brian') + assert_equal 'Goodbye Brian', template.render('name' => 'Brian') + assert_equal({'greeting'=>'Goodbye'}, template.assigns) + end + + def test_assigns_not_polluted_from_template + template = Template.parse(%|{{ test }}{% assign test = 'bar' %}{{ test }}|) + template.assigns['test'] = 'baz' + assert_equal 'bazbar', template.render + assert_equal 'bazbar', template.render + assert_equal 'foobar', template.render('test' => 'foo') + assert_equal 'bazbar', template.render + end + + def test_hash_with_default_proc + template = Template.parse(%|Hello {{ test }}|) + assigns = Hash.new { |h,k| raise "Unknown variable '#{k}'" } + assigns['test'] = 'Tobi' + assert_equal 'Hello Tobi', template.render!(assigns) + assigns.delete('test') + e = assert_raises(RuntimeError) { + template.render!(assigns) + } + assert_equal "Unknown variable 'test'", e.message + end +end # VariableTest diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 000000000..30dd9a163 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby + +require 'test/unit' +require 'test/unit/assertions' +begin + require 'ruby-debug' +rescue LoadError + puts "Couldn't load ruby-debug. gem install ruby-debug if you need it." +end +require File.join(File.dirname(__FILE__), '..', 'lib', 'liquid') + + +module Test + module Unit + 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 + end # Assertions + end # Unit +end # Test From bc3c0baa025d178d95e83a7f5178e1fad6e30584 Mon Sep 17 00:00:00 2001 From: did Date: Sun, 16 Dec 2012 17:57:46 +0100 Subject: [PATCH 60/61] move some methods from LocomotiveCMS to Liquid --- .rspec | 2 ++ lib/liquid/standardfilters.rb | 1 - lib/liquid/tags/extends.rb | 2 +- lib/liquid/tags/inherited_block.rb | 12 ++++++++++-- lib/liquid/template.rb | 25 +++++++++++++++++++++++++ spec/integration/extends_spec.rb | 20 +++++++++++++++++++- 6 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 .rspec diff --git a/.rspec b/.rspec new file mode 100644 index 000000000..616c43385 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--colour +--backtrace diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index b48eb44a1..34ad4f5aa 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -6,7 +6,6 @@ module StandardFilters # Return the size of an array or of an string def size(input) - input.respond_to?(:size) ? input.size : 0 end diff --git a/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb index 15da2efac..49c1896de 100644 --- a/lib/liquid/tags/extends.rb +++ b/lib/liquid/tags/extends.rb @@ -10,7 +10,7 @@ class Extends < Block def initialize(tag_name, markup, tokens, context) if markup =~ Syntax - @template_name = $1.gsub('\'', '').strip + @template_name = $1.gsub(/["']/o, '').strip else raise SyntaxError.new("Error in tag 'extends' - Valid syntax: extends [template]") end diff --git a/lib/liquid/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb index 05d438b6e..855b401ca 100644 --- a/lib/liquid/tags/inherited_block.rb +++ b/lib/liquid/tags/inherited_block.rb @@ -10,15 +10,17 @@ class InheritedBlock < Block Syntax = /(#{QuotedFragment}+)/ attr_accessor :parent - attr_reader :name + attr_reader :name def initialize(tag_name, markup, tokens, context) if markup =~ Syntax - @name = $1 + @name = $1.gsub(/["']/o, '').strip else raise SyntaxError.new("Error in tag 'block' - Valid syntax: block [name]") end + self.set_full_name!(context) + (context[:block_stack] ||= []).push(self) context[:current_block] = self @@ -56,6 +58,12 @@ def self.clone_block(block) protected + def set_full_name!(context) + if context[:current_block] + @name = context[:current_block].name + '/' + @name + end + end + def register_current_block @context[:blocks] ||= {} diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb index f306519d9..dbe3a2c53 100644 --- a/lib/liquid/template.rb +++ b/lib/liquid/template.rb @@ -132,6 +132,31 @@ def render!(*args) @rethrow_errors = true; render(*args) end + def walk(memo = {}, &block) + # puts @root.nodelist.inspect + self._walk(@root.nodelist, memo, &block) + end + + def _walk(list, memo = {}, &block) + list.each do |node| + saved_memo = memo.clone + + # puts "fetch ! #{node.respond_to?(:name) ? node.name : 'String'} / #{node.respond_to?(:nodelist)}" + if block_given? + # puts "youpi ! #{node.name}" + _memo = yield(node, memo) || {} + memo.merge!(_memo) + end + + if node.respond_to?(:nodelist) && !node.nodelist.blank? + self._walk(node.nodelist, memo, &block) + end + + memo = saved_memo + end + memo + end + private # Uses the Liquid::TemplateParser regexp to tokenize the passed source diff --git a/spec/integration/extends_spec.rb b/spec/integration/extends_spec.rb index d47eff8df..be60093dc 100644 --- a/spec/integration/extends_spec.rb +++ b/spec/integration/extends_spec.rb @@ -103,10 +103,28 @@ def read_template_file(template_path) output.should == 'Output / Lorem ipsum: Deep: Hello, World!' @templates['nested_and_deep'] = "{% extends base %}{% block content %}Deep: {{block.super}} -{% block inner %}FOO{% endblock %}-{% endblock %}" - output = render("{% extends nested_and_deep %}{% block inner %}BAR{% endblock %}") + output = render("{% extends nested_and_deep %}{% block content/inner %}BAR{% endblock %}") output.should == 'Output / Deep: Hello, World! -BAR-' end end + describe "nested inherited blocks" do + + before(:each) do + @templates['base'] = "Output / {% block content %}Hello, World!{% block tagline %}(My tagline){% endblock %}{% endblock %}" + end + + it "should allow overriding blocks from an inherited template" do + output = render("{% extends base %}{% block content %}Hola, Mundo!{% endblock %}") + output.should == 'Output / Hola, Mundo!' + end + + it "should allow overriding blocks from an inherited template" do + output = render("{% extends base %}{% block content/tagline %}(new tagline){% endblock %}") + output.should == 'Output / Hello, World!(new tagline)' + end + + end + end end \ No newline at end of file From 55d3974e29a352b1b3d77acf0f901d0be67378b2 Mon Sep 17 00:00:00 2001 From: Dmitriy Myaskovskiy Date: Wed, 10 Apr 2013 01:08:20 +0400 Subject: [PATCH 61/61] Properly support of non-ascii strings for downcase, upcase, capitalize filters --- Gemfile | 1 + Gemfile.lock | 6 ++++++ lib/liquid/standardfilters.rb | 7 ++++--- spec/spec_helper.rb | 3 ++- spec/unit/filter_spec.rb | 24 +++++++++++++++++++++++- test/test_helper.rb | 1 + 6 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index d13938cd8..ab477d461 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,7 @@ source :rubygems gem 'rake', '~> 0.9.2' gem 'rspec', '~> 2.6' #>= 2.0.0.beta.22" gem 'cucumber', '~> 1.1.9' #'> 0.9.0' +gem 'activesupport' platforms :ruby_18 do gem 'ruby-debug' diff --git a/Gemfile.lock b/Gemfile.lock index 27ee46a02..83f251f63 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,9 @@ GEM remote: http://rubygems.org/ specs: + activesupport (3.2.13) + i18n (= 0.6.1) + multi_json (~> 1.0) builder (3.0.0) columnize (0.3.1) cucumber (1.1.9) @@ -12,8 +15,10 @@ GEM diff-lcs (1.1.3) gherkin (2.9.3) json (>= 1.4.6) + i18n (0.6.1) json (1.7.3) linecache (0.43) + multi_json (1.6.1) rake (0.9.2.2) rspec (2.11.0) rspec-core (~> 2.11.0) @@ -34,6 +39,7 @@ PLATFORMS ruby DEPENDENCIES + activesupport cucumber (~> 1.1.9) rake (~> 0.9.2) rspec (~> 2.6) diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index 34ad4f5aa..6147f267c 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -1,4 +1,5 @@ require 'cgi' +require 'active_support/core_ext' module Liquid @@ -11,17 +12,17 @@ def size(input) # convert a input string to DOWNCASE def downcase(input) - input.to_s.downcase + input.to_s.mb_chars.downcase end # convert a input string to UPCASE def upcase(input) - input.to_s.upcase + input.to_s.mb_chars.upcase end # capitalize words in the input centence def capitalize(input) - input.to_s.capitalize + input.to_s.mb_chars.capitalize end def escape(input) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 44392aa6d..0bb6c80b4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -12,6 +12,7 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "fixtures")) require 'locomotive_liquid' +require 'active_support/core_ext' require 'rspec' @@ -63,4 +64,4 @@ def print_child(node, depth = 0) RSpec.configure do |c| c.include Liquid::SpecHelpers -end \ No newline at end of file +end diff --git a/spec/unit/filter_spec.rb b/spec/unit/filter_spec.rb index 1176edcd0..d2fe643f6 100644 --- a/spec/unit/filter_spec.rb +++ b/spec/unit/filter_spec.rb @@ -27,6 +27,10 @@ class TestFilters filters.downcase("Testing").should == "testing" end + it "should properly handle non ascii strings" do + filters.downcase("Проверка").should == "проверка" + end + it "should return empty string for nil" do filters.downcase(nil).should == "" end @@ -37,11 +41,29 @@ class TestFilters filters.upcase("Testing").should == "TESTING" end + it "should properly handle non ascii strings" do + filters.upcase("Проверка").should == "ПРОВЕРКА" + end + it "should return empty string for nil" do filters.upcase(nil).should == "" end end + context "#capitalize" do + it "should make the first letter of string upper case" do + filters.capitalize("testing").should == "Testing" + end + + it "should properly handle non ascii strings" do + filters.capitalize("проверка").should == "Проверка" + end + + it "should return empty string for nil" do + filters.capitalize(nil).should == "" + end + end + context "#truncate" do it "should truncate string to the specified length, replacing with ellipsis" do filters.truncate('1234567890', 7).should == '1234...' @@ -223,4 +245,4 @@ class TestFilters end end -end \ No newline at end of file +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 30dd9a163..f66c39280 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,6 +2,7 @@ require 'test/unit' require 'test/unit/assertions' +require 'active_support/core_ext' begin require 'ruby-debug' rescue LoadError