diff --git a/.gitignore b/.gitignore index 76f22a726..c0db7f18d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.gem pkg +.rvmrc +.bundle/config \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..3919783f6 --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +source :rubygems + +gem "rake" +gem "rspec", ">= 2.0.0.beta.22" +gem "cucumber", ">= 0.9.0" + +platforms :ruby_18 do + gem "ruby-debug" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..6a8efdb97 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,45 @@ +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) + 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) + 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) + rake + rspec (>= 2.0.0.beta.22) + ruby-debug diff --git a/History.txt b/History.txt index 2f1c37f65..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 @@ -7,7 +20,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 +39,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/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/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? + + + +## 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 1d019af6f..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? - - - -== 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 diff --git a/Rakefile b/Rakefile index 808770653..7c2b9f77d 100755 --- a/Rakefile +++ b/Rakefile @@ -1,44 +1,66 @@ #!/usr/bin/env ruby require 'rubygems' + +require "bundler" +Bundler.setup + require 'rake' -require 'rake/testtask' require 'rake/gempackagetask' -task :default => 'test' +require "rspec" +require "rspec/core/rake_task" + +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 -Rake::TestTask.new(:test) do |t| - t.libs << "lib" - t.libs << "test" - t.pattern = 'test/*_test.rb' - t.verbose = false +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" end -gemspec = eval(File.read('liquid.gemspec')) +task :default => :spec + +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" + 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 - 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/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/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 %} - - + + 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.rb b/lib/liquid.rb index b9ba4c551..533bfb459 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -38,13 +38,14 @@ 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 = /\{\{|\{\%/ PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/ TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/ VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/ + LiteralShorthand = /^(?:\{\{\{\s?)(.*?)(?:\s*\}\}\})$/ end require 'liquid/drop' 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/context.rb b/lib/liquid/context.rb index d8a17ec9f..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,43 +61,44 @@ 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 - + def clear_instance_assigns @scopes[0] = {} 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 - # 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 = /^\[(.*)\]$/ + scope ||= @environments.last || @scopes.last + variable ||= lookup_and_evaluate(scope, key) + + 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=) + # Some special cases. If the part wasn't in square brackets and + # no key with the same name was found we interpret following calls + # as commands and call them on the current object + elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part) + + object = object.send(part.intern).to_liquid + + # No key was present with the desired value and it wasn't one of the directly supported + # keywords either. The only thing we got left is to return nil + else + return nil + end + + # If we are dealing with a drop here we have to + object.context = self if object.respond_to?(:context=) + end end - end - object - end - - 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 - - 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 + 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 - end - - end -end + end # squash_instance_assigns_with_environments + end # Context + +end # Liquid 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/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/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/standardfilters.rb b/lib/liquid/standardfilters.rb index 0f08264cd..7c52047b6 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,22 +44,26 @@ 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) + 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 @@ -73,8 +77,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 +88,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 +153,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 +219,8 @@ def to_number(obj) 0 end end - + end - + Template.register_filter(StandardFilters) end 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) 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/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/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/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/lib/liquid/tags/extends.rb b/lib/liquid/tags/extends.rb new file mode 100644 index 000000000..15da2efac --- /dev/null +++ b/lib/liquid/tags/extends.rb @@ -0,0 +1,75 @@ +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.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 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.clone + + @parent_template = nil # no need to keep it + end + + protected + + def find_blocks(nodelist, blocks = {}) + if nodelist && nodelist.any? + 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, blocks) # FIXME: find nested blocks too + end + end + end + blocks + end + + private + + def parse_parent_template + source = Template.file_system.read_template_file(@template_name) + Template.parse(source) + 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/for.rb b/lib/liquid/tags/for.rb index b17a3320b..0d01b1de8 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,19 @@ module Liquid # # {% for item in collection limit:5 offset:10 %} # {{ item.name }} - # {% end %} + # {% end %} + # + # You can also specify an order for the collection items + # + # {% for item in collection order:ascending %} + # {{ item.name }} + # {% end %} + # + # You can also specify which attribute to sort by. + # + # {% for item in collection sort_by:name order:descending %} + # {{ item.name }} + # {% end %} # # To reverse the for loop simply use {% for item in collection reversed %} # @@ -29,7 +41,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 +53,81 @@ 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 + @reversed = 'reversed' if @attributes['order'] == 'descending' 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) + + sort_property = @attributes['sort_by'] + order_property = @attributes['order'] + + if sort_property || order_property + collection = if sort_property.nil? && (@attributes['order'] == 'ascending' || @attributes['order'] == 'descending') + collection.sort + elsif collection.first.respond_to?('[]') and !collection.first[sort_property].nil? + collection.sort {|a,b| a[sort_property] <=> b[sort_property] } + elsif collection.first.respond_to?(sort_property) + collection.sort {|a,b| a.send(sort_property) <=> b.send(sort_property) } + else + collection + end + end + + collection.reverse! if @reversed + 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 +136,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..e28572947 100644 --- a/lib/liquid/tags/if.rb +++ b/lib/liquid/tags/if.rb @@ -14,17 +14,17 @@ 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*)+)/ - - def initialize(tag_name, markup, tokens) - + 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) + @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).to_s.strip + + 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/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/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/tags/inherited_block.rb b/lib/liquid/tags/inherited_block.rb new file mode 100644 index 000000000..05d438b6e --- /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 + + (context[:block_stack] ||= []).push(self) + context[:current_block] = self + + super if tokens + end + + def render(context) + context.stack do + context['block'] = InheritedBlockDrop.new(self) + render_all(@nodelist, context) + end + end + + def end_tag + self.register_current_block + + @context[:block_stack].pop + @context[:current_block] = @context[:block_stack].last + 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 + # copy the existing block in order to make it a parent of the parsed block + new_block = self.class.clone_block(block) + + # replace the up-to-date version of the block in the parent template + block.parent = new_block + block.nodelist = @nodelist + end + end + + end + + class InheritedBlockDrop < Drop + + def initialize(block) + @block = block + end + + def name + @block.name + end + + def super + @block.call_super(@context) + end + + end + + Template.register_tag('block', InheritedBlock) +end \ No newline at end of file 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/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 diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb index edfb980e3..b04010b90 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(Liquid::Literal.from_shorthand(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/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/liquid.gemspec deleted file mode 100644 index a2b5966ab..000000000 --- a/liquid.gemspec +++ /dev/null @@ -1,28 +0,0 @@ -Gem::Specification.new do |s| - s.name = %q{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.has_rdoc = true - s.homepage = %q{http://www.liquidmarkup.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.} - - 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 diff --git a/locomotive_liquid.gemspec b/locomotive_liquid.gemspec new file mode 100644 index 000000000..7e6c63078 --- /dev/null +++ b/locomotive_liquid.gemspec @@ -0,0 +1,27 @@ +Gem::Specification.new do |s| + s.name = "locomotive_liquid" + s.version = "2.2.2" + + 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.md"] + s.files = Dir[ "CHANGELOG", + "History.txt", + "MIT-LICENSE", + "README.md", + "Rakefile", + "init.rb", + "{lib}/**/*"] + + s.has_rdoc = true + s.homepage = "http://www.locomotiveapp.org" + s.rdoc_options = ["--main", "README.md"] + s.require_paths = ["lib"] + s.rubyforge_project = "locomotive_liquid" + +end diff --git a/spec/fixtures/drops/context_drop.rb b/spec/fixtures/drops/context_drop.rb new file mode 100644 index 000000000..7cf6db6da --- /dev/null +++ b/spec/fixtures/drops/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/drops/enumerable_drop.rb b/spec/fixtures/drops/enumerable_drop.rb new file mode 100644 index 000000000..8b371fbe6 --- /dev/null +++ b/spec/fixtures/drops/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/drops/error_drop.rb b/spec/fixtures/drops/error_drop.rb new file mode 100644 index 000000000..246614d86 --- /dev/null +++ b/spec/fixtures/drops/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/fixtures/drops/product_drop.rb b/spec/fixtures/drops/product_drop.rb new file mode 100644 index 000000000..9c6361c81 --- /dev/null +++ b/spec/fixtures/drops/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/integration/assign_spec.rb b/spec/integration/assign_spec.rb new file mode 100644 index 000000000..81cc1482e --- /dev/null +++ b/spec/integration/assign_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +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 + + 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 \ No newline at end of file diff --git a/spec/integration/capture_spec.rb b/spec/integration/capture_spec.rb new file mode 100644 index 000000000..6dc218d2c --- /dev/null +++ b/spec/integration/capture_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +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 + 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 + + end + + end +end 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/drop_spec.rb b/spec/integration/drop_spec.rb new file mode 100644 index 000000000..a56973320 --- /dev/null +++ b/spec/integration/drop_spec.rb @@ -0,0 +1,133 @@ +require 'spec_helper' + +require 'drops/product_drop' +require 'drops/context_drop' +require 'drops/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/integration/error_handling_spec.rb b/spec/integration/error_handling_spec.rb new file mode 100644 index 000000000..c02f85893 --- /dev/null +++ b/spec/integration/error_handling_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +require 'drops/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/spec/integration/extends_spec.rb b/spec/integration/extends_spec.rb new file mode 100644 index 000000000..d47eff8df --- /dev/null +++ b/spec/integration/extends_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "Template Inheritance" do + before(:each) do + @templates ||= {} + end + + before(:each) do + 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!" + + 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 + + it "should allow access to the context from the inherited template" do + @templates['parent-with-variable'] = "Hello, {{ name }}!" + + 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 %}" + + output = render("{% extends parent-with-parent %}", 'name' => 'Joe') + output.should == "Hello, Joe!!" + end + + 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 + + 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/integration/filter_spec.rb b/spec/integration/filter_spec.rb new file mode 100644 index 000000000..d404f3619 --- /dev/null +++ b/spec/integration/filter_spec.rb @@ -0,0 +1,263 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "Filters" do + before(:each) do + @context = Liquid::Context.new + end + + def render_variable(body) + Liquid::Variable.new(body).render(@context) + end + + context "standard filters" do + describe "size" do + it "should return the size of a string" do + @context['val'] = "abcd" + render_variable('val | size').should == 4 + end + + it "should return the size of an array" do + @context['val'] = [1,2,3,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_variable('val | size').should == 4 + end + end + + describe "join" do + it "should join an array" do + @context['val'] = [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_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" + end + end + + describe "sort" do + it "should sort a single value" do + @context['value'] = 3 + render_variable("value | sort").should == [3] + end + + it "should sort an array of numbers" do + @context['numbers'] = [2,1,4,3] + render_variable("numbers | sort").should == [1,2,3,4] + end + + it "should sort an array of words" do + @context['words'] = ['expected', 'as', 'alphabetic'] + render_variable("words | sort").should == ['alphabetic', 'as', 'expected'] + end + + it "should sort an array of arrays" do + @context['arrays'] = [['flattened'], ['are']] + 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_variable('user_input | strip_html').should == "bla blub" + end + + it "should remove script tags entirely" do + @context['user_input'] = "" + render_variable('user_input | strip_html').should == "" + end + end + + describe "capitalize" do + it "should capitalize the first character" do + @context['val'] = "blub" + 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 + + 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 + before(:each) do + Liquid::Template.register_filter(MoneyFilter) + end + + it "should use globally registered filters" do + render('{{1000 | money}}').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 + + 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 + + it "should ignore nonexistant filters" do + render("{{ val | xyzzy }}", 'val' => 1000).should == "1000" + 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 new file mode 100644 index 000000000..f39fd7799 --- /dev/null +++ b/spec/integration/if_else_unless_spec.rb @@ -0,0 +1,203 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "If/Else/Unless" 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" + 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 %}' + + 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 "{% 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 " + 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 + + 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/integration/include_spec.rb b/spec/integration/include_spec.rb new file mode 100644 index 000000000..add007760 --- /dev/null +++ b/spec/integration/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/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/integration/output_spec.rb b/spec/integration/output_spec.rb new file mode 100644 index 000000000..5aa48eba8 --- /dev/null +++ b/spec/integration/output_spec.rb @@ -0,0 +1,131 @@ +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'}, + 'brands' => ['bmw', 'gm', 'ford'] + } + end + + 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 + + it "should render a traversed variable's value" do + 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' + 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/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/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/spec/integration/table_spec.rb b/spec/integration/table_spec.rb new file mode 100644 index 000000000..275eaca9e --- /dev/null +++ b/spec/integration/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/integration/tag_spec.rb b/spec/integration/tag_spec.rb new file mode 100644 index 000000000..281cb9fdd --- /dev/null +++ b/spec/integration/tag_spec.rb @@ -0,0 +1,91 @@ +require 'spec_helper' + +describe "Liquid Rendering" do + describe "Tags" do + + 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 "{% 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/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/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 000000000..14c75522a --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,66 @@ +require 'rubygems' +require "bundler" +Bundler.setup + +# add spec folder to load path +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__))) + +# 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' + +# 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} + +# 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 + "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 + +Rspec.configure do |c| + c.include Liquid::SpecHelpers +end \ No newline at end of file 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/spec/support/multiline_string.rb b/spec/support/multiline_string.rb new file mode 100644 index 000000000..49c5e7f08 --- /dev/null +++ b/spec/support/multiline_string.rb @@ -0,0 +1,28 @@ +module RSpec + module MultilineString + # + # 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::MultilineString +end \ No newline at end of file diff --git a/spec/unit/condition_spec.rb b/spec/unit/condition_spec.rb new file mode 100644 index 000000000..bd8e4ab24 --- /dev/null +++ b/spec/unit/condition_spec.rb @@ -0,0 +1,143 @@ +require 'spec_helper' + +module Liquid + describe Condition do + before(:each) do + @context = 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 + + 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 + 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/spec/unit/context_spec.rb b/spec/unit/context_spec.rb new file mode 100644 index 000000000..08172613a --- /dev/null +++ b/spec/unit/context_spec.rb @@ -0,0 +1,479 @@ +require 'spec_helper' + +module Liquid + describe Context do + + before(:each) do + @context = 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 = 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(ContextError) + + expect { + @context.push + @context.pop + @context.pop + }.to raise_exception(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 < 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 < 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 < 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 < 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 \ No newline at end of file diff --git a/spec/unit/file_system_spec.rb b/spec/unit/file_system_spec.rb new file mode 100644 index 000000000..b3dfbd7f2 --- /dev/null +++ b/spec/unit/file_system_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +module Liquid + describe BlankFileSystem do + it "should error out when trying to ready any file" do + expect { + BlankFileSystem.new.read_template_file("dummy") + }.to raise_error(Liquid::FileSystemError) + end + end + + describe LocalFileSystem do + describe "#full_path" do + before(:each) do + @file_system = 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/spec/unit/filter_spec.rb b/spec/unit/filter_spec.rb new file mode 100644 index 000000000..9085120a6 --- /dev/null +++ b/spec/unit/filter_spec.rb @@ -0,0 +1,218 @@ +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 + + 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 + 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/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/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/parsing_spec.rb b/spec/unit/parsing_spec.rb new file mode 100644 index 000000000..e1eb79ae5 --- /dev/null +++ b/spec/unit/parsing_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +module Liquid + describe "Liquid Parsing" do + + it "should render whitespace properly" do + parse(" ").root.nodelist.should == [" "] + end + + describe %|"{{funk}} "| do + it{ parse(:subject).root.nodelist.should have(2).nodes } + + it "should parse to: Variable,String" do + 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{ parse(:subject).root.nodelist.should have(2).nodes } + + it "should parse to: String,Variable" do + 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{ parse(:subject).root.nodelist.should have(3).nodes } + + it "should parse to: String,Variable,String" do + 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{ parse(:subject).root.nodelist.should have(7).nodes } + + it "should parse to: String,Variable,String,Variable,String,Variable,String" do + 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{ parse(:subject).root.nodelist.should have(3).nodes } + it "should parse to: String,Comment,String" do + 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 + + 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 + parse(:subject).root.nodelist.should have(1).nodes + end + end + end + + end +end \ No newline at end of file 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/regexp_spec.rb b/spec/unit/regexp_spec.rb new file mode 100644 index 000000000..71faed58f --- /dev/null +++ b/spec/unit/regexp_spec.rb @@ -0,0 +1,89 @@ +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 + + 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 diff --git a/spec/unit/strainer_spec.rb b/spec/unit/strainer_spec.rb new file mode 100644 index 000000000..8c3b90eb7 --- /dev/null +++ b/spec/unit/strainer_spec.rb @@ -0,0 +1,29 @@ +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 + + 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 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/spec/unit/template_spec.rb b/spec/unit/template_spec.rb new file mode 100644 index 000000000..471b9e782 --- /dev/null +++ b/spec/unit/template_spec.rb @@ -0,0 +1,78 @@ +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 + + 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/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/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 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 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 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 - - 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 - - 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 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 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/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 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/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 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/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/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/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 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 - diff --git a/test/standard_tag_test.rb b/test/standard_tag_test.rb deleted file mode 100644 index 17d8df1af..000000000 --- a/test/standard_tag_test.rb +++ /dev/null @@ -1,405 +0,0 @@ -require File.dirname(__FILE__) + '/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_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_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_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 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 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/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 diff --git a/test/test_helper.rb b/test/test_helper.rb deleted file mode 100644 index deb818594..000000000 --- a/test/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/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