├── Gemfile ├── performance ├── tests │ ├── vogue │ │ ├── page.liquid │ │ ├── blog.liquid │ │ ├── collection.liquid │ │ ├── index.liquid │ │ ├── article.liquid │ │ ├── product.liquid │ │ └── cart.liquid │ ├── ripen │ │ ├── page.liquid │ │ ├── blog.liquid │ │ ├── collection.liquid │ │ ├── index.liquid │ │ ├── cart.liquid │ │ ├── theme.liquid │ │ ├── article.liquid │ │ └── product.liquid │ ├── dropify │ │ ├── page.liquid │ │ ├── collection.liquid │ │ ├── blog.liquid │ │ ├── index.liquid │ │ ├── cart.liquid │ │ ├── product.liquid │ │ ├── article.liquid │ │ └── theme.liquid │ └── tribble │ │ ├── blog.liquid │ │ ├── search.liquid │ │ ├── page.liquid │ │ ├── 404.liquid │ │ ├── collection.liquid │ │ ├── theme.liquid │ │ ├── index.liquid │ │ ├── article.liquid │ │ └── product.liquid ├── shopify │ ├── json_filter.rb │ ├── weight_filter.rb │ ├── money_filter.rb │ ├── liquid.rb │ ├── tag_filter.rb │ ├── comment_form.rb │ ├── database.rb │ ├── paginate.rb │ └── shop_filter.rb ├── benchmark.rb ├── profile.rb └── theme_runner.rb ├── .gitignore ├── lib ├── liquid │ ├── version.rb │ ├── tags │ │ ├── comment.rb │ │ ├── break.rb │ │ ├── continue.rb │ │ ├── ifchanged.rb │ │ ├── raw.rb │ │ ├── unless.rb │ │ ├── assign.rb │ │ ├── increment.rb │ │ ├── capture.rb │ │ ├── decrement.rb │ │ ├── cycle.rb │ │ ├── case.rb │ │ ├── table_row.rb │ │ ├── include.rb │ │ └── if.rb │ ├── errors.rb │ ├── document.rb │ ├── interrupts.rb │ ├── extensions.rb │ ├── utils.rb │ ├── i18n.rb │ ├── tag.rb │ ├── lexer.rb │ ├── locales │ │ └── en.yml │ ├── module_ex.rb │ ├── strainer.rb │ ├── parser.rb │ ├── drop.rb │ ├── file_system.rb │ ├── condition.rb │ └── variable.rb └── liquid.rb ├── example └── server │ ├── templates │ ├── index.liquid │ └── products.liquid │ ├── server.rb │ ├── liquid_servlet.rb │ └── example_servlet.rb ├── .travis.yml ├── test ├── unit │ ├── tag_unit_test.rb │ ├── tags │ │ ├── if_tag_unit_test.rb │ │ ├── case_tag_unit_test.rb │ │ └── for_tag_unit_test.rb │ ├── template_unit_test.rb │ ├── i18n_unit_test.rb │ ├── tokenizer_unit_test.rb │ ├── file_system_unit_test.rb │ ├── lexer_unit_test.rb │ ├── regexp_unit_test.rb │ ├── block_unit_test.rb │ ├── strainer_unit_test.rb │ ├── module_ex_unit_test.rb │ ├── parser_unit_test.rb │ ├── condition_unit_test.rb │ └── variable_unit_test.rb ├── fixtures │ └── en_locale.yml ├── integration │ ├── tags │ │ ├── break_tag_test.rb │ │ ├── continue_tag_test.rb │ │ ├── increment_tag_test.rb │ │ ├── raw_tag_test.rb │ │ ├── unless_else_tag_test.rb │ │ ├── table_row_test.rb │ │ └── statements_test.rb │ ├── hash_ordering_test.rb │ ├── context_test.rb │ ├── assign_test.rb │ ├── capture_test.rb │ ├── security_test.rb │ ├── variable_test.rb │ ├── parsing_quirks_test.rb │ ├── output_test.rb │ ├── blank_test.rb │ ├── error_handling_test.rb │ └── filter_test.rb └── test_helper.rb ├── MIT-LICENSE ├── liquid.gemspec ├── CONTRIBUTING.md ├── Rakefile └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /performance/tests/vogue/page.liquid: -------------------------------------------------------------------------------- 1 |

{{ page.title }}

2 | {{ page.content }} 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.gem 3 | *.swp 4 | pkg 5 | *.rbc 6 | .rvmrc 7 | .ruby-version 8 | Gemfile.lock 9 | -------------------------------------------------------------------------------- /lib/liquid/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Liquid 3 | VERSION = "3.0.0".freeze 4 | end 5 | -------------------------------------------------------------------------------- /performance/tests/ripen/page.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{page.title}}

3 | {{ page.content }} 4 |
5 | -------------------------------------------------------------------------------- /performance/shopify/json_filter.rb: -------------------------------------------------------------------------------- 1 | module JsonFilter 2 | 3 | def json(object) 4 | object.reject {|k,v| k == "collections" }.to_json 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /performance/tests/dropify/page.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{page.title}}

3 | 4 |
5 | {{page.content}} 6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /example/server/templates/index.liquid: -------------------------------------------------------------------------------- 1 |

Hello world!

2 | 3 |

It is {{date}}

4 | 5 | 6 |

Check out the Products screen

7 | -------------------------------------------------------------------------------- /performance/shopify/weight_filter.rb: -------------------------------------------------------------------------------- 1 | module WeightFilter 2 | 3 | def weight(grams) 4 | sprintf("%.2f", grams / 1000) 5 | end 6 | 7 | def weight_with_unit(grams) 8 | "#{weight(grams)} kg" 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.0.0 4 | - 2.1.0 5 | - jruby-19mode 6 | - jruby-head 7 | - rbx-19mode 8 | matrix: 9 | allow_failures: 10 | - rvm: rbx-19mode 11 | - rvm: jruby-head 12 | 13 | script: "rake test" 14 | 15 | notifications: 16 | disable: true 17 | -------------------------------------------------------------------------------- /test/unit/tag_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TagUnitTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_tag 7 | tag = Tag.parse('tag', [], [], {}) 8 | assert_equal 'liquid::tag', tag.name 9 | assert_equal '', tag.render(Context.new) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/unit/tags/if_tag_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class IfTagUnitTest < Test::Unit::TestCase 4 | def test_if_nodelist 5 | template = Liquid::Template.parse('{% if true %}IF{% else %}ELSE{% endif %}') 6 | assert_equal ['IF', 'ELSE'], template.root.nodelist[0].nodelist 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/fixtures/en_locale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | simple: "less is more" 3 | whatever: "something %{something}" 4 | errors: 5 | i18n: 6 | undefined_interpolation: "undefined key %{key}" 7 | unknown_translation: "translation '%{name}' wasn't found" 8 | syntax: 9 | oops: "something wasn't right" 10 | -------------------------------------------------------------------------------- /lib/liquid/tags/comment.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | class Comment < Block 3 | def render(context) 4 | ''.freeze 5 | end 6 | 7 | def unknown_tag(tag, markup, tokens) 8 | end 9 | 10 | def blank? 11 | true 12 | end 13 | end 14 | 15 | Template.register_tag('comment'.freeze, Comment) 16 | end 17 | -------------------------------------------------------------------------------- /test/unit/tags/case_tag_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CaseTagUnitTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_case_nodelist 7 | template = Liquid::Template.parse('{% case var %}{% when true %}WHEN{% else %}ELSE{% endcase %}') 8 | assert_equal ['WHEN', 'ELSE'], template.root.nodelist[0].nodelist 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /performance/shopify/money_filter.rb: -------------------------------------------------------------------------------- 1 | module MoneyFilter 2 | 3 | def money_with_currency(money) 4 | return '' if money.nil? 5 | sprintf("$ %.2f USD", money/100.0) 6 | end 7 | 8 | def money(money) 9 | return '' if money.nil? 10 | sprintf("$ %.2f", money/100.0) 11 | end 12 | 13 | private 14 | 15 | def currency 16 | ShopDrop.new.currency 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /performance/benchmark.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'benchmark' 3 | require File.dirname(__FILE__) + '/theme_runner' 4 | 5 | Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first 6 | profiler = ThemeRunner.new 7 | 8 | Benchmark.bmbm do |x| 9 | x.report("parse:") { 100.times { profiler.compile } } 10 | x.report("parse & run:") { 100.times { profiler.run } } 11 | end 12 | 13 | -------------------------------------------------------------------------------- /lib/liquid/errors.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | class Error < ::StandardError; end 3 | 4 | class ArgumentError < Error; end 5 | class ContextError < Error; end 6 | class FilterNotFound < Error; end 7 | class FileSystemError < Error; end 8 | class StandardError < Error; end 9 | class SyntaxError < Error; end 10 | class StackLevelError < Error; end 11 | class MemoryError < Error; end 12 | end 13 | -------------------------------------------------------------------------------- /example/server/server.rb: -------------------------------------------------------------------------------- 1 | require 'webrick' 2 | require 'rexml/document' 3 | 4 | DIR = File.expand_path(File.dirname(__FILE__)) 5 | 6 | require DIR + '/../../lib/liquid' 7 | require DIR + '/liquid_servlet' 8 | require DIR + '/example_servlet' 9 | 10 | # Setup webrick 11 | server = WEBrick::HTTPServer.new( :Port => ARGV[1] || 3000 ) 12 | server.mount('/', Servlet) 13 | trap("INT"){ server.shutdown } 14 | server.start 15 | -------------------------------------------------------------------------------- /test/integration/tags/break_tag_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BreakTagTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | # tests that no weird errors are raised if break is called outside of a 7 | # block 8 | def test_break_with_no_block 9 | assigns = {'i' => 1} 10 | markup = '{% break %}' 11 | expected = '' 12 | 13 | assert_template_result(expected, markup, assigns) 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /test/integration/tags/continue_tag_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ContinueTagTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | # tests that no weird errors are raised if continue is called outside of a 7 | # block 8 | def test_continue_with_no_block 9 | assigns = {} 10 | markup = '{% continue %}' 11 | expected = '' 12 | 13 | assert_template_result(expected, markup, assigns) 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /lib/liquid/tags/break.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | 3 | # Break tag to be used to break out of a for loop. 4 | # 5 | # == Basic Usage: 6 | # {% for item in collection %} 7 | # {% if item.condition %} 8 | # {% break %} 9 | # {% endif %} 10 | # {% endfor %} 11 | # 12 | class Break < Tag 13 | 14 | def interrupt 15 | BreakInterrupt.new 16 | end 17 | 18 | end 19 | 20 | Template.register_tag('break'.freeze, Break) 21 | end 22 | -------------------------------------------------------------------------------- /lib/liquid/tags/continue.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | # Continue tag to be used to break out of a for loop. 3 | # 4 | # == Basic Usage: 5 | # {% for item in collection %} 6 | # {% if item.condition %} 7 | # {% continue %} 8 | # {% endif %} 9 | # {% endfor %} 10 | # 11 | class Continue < Tag 12 | def interrupt 13 | ContinueInterrupt.new 14 | end 15 | end 16 | 17 | Template.register_tag('continue'.freeze, Continue) 18 | end 19 | -------------------------------------------------------------------------------- /lib/liquid/document.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | class Document < Block 3 | def self.parse(tokens, options={}) 4 | # we don't need markup to open this block 5 | super(nil, nil, tokens, options) 6 | end 7 | 8 | # There isn't a real delimiter 9 | def block_delimiter 10 | [] 11 | end 12 | 13 | # Document blocks don't need to be terminated since they are not actually opened 14 | def assert_missing_delimitation! 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/liquid/tags/ifchanged.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | class Ifchanged < Block 3 | 4 | def render(context) 5 | context.stack do 6 | 7 | output = render_all(@nodelist, context) 8 | 9 | if output != context.registers[:ifchanged] 10 | context.registers[:ifchanged] = output 11 | output 12 | else 13 | ''.freeze 14 | end 15 | end 16 | end 17 | end 18 | 19 | Template.register_tag('ifchanged'.freeze, Ifchanged) 20 | end 21 | -------------------------------------------------------------------------------- /performance/profile.rb: -------------------------------------------------------------------------------- 1 | require 'stackprof' rescue fail("install stackprof extension/gem") 2 | require File.dirname(__FILE__) + '/theme_runner' 3 | 4 | Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first 5 | profiler = ThemeRunner.new 6 | profiler.run 7 | results = StackProf.run(mode: :cpu) do 8 | 100.times do 9 | profiler.run 10 | end 11 | end 12 | StackProf::Report.new(results).print_text(false, 20) 13 | File.write(ENV['FILENAME'], Marshal.dump(results)) if ENV['FILENAME'] 14 | -------------------------------------------------------------------------------- /performance/tests/ripen/blog.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{page.title}}

3 | {% for article in blog.articles %} 4 |

5 | {{ article.created_at | date: '%d %b' }} 6 | {{ article.title }} 7 |

8 | {{ article.content }} 9 | {% if blog.comments_enabled? %} 10 |

{{ article.comments_count }} comments

11 | {% endif %} 12 | {% endfor %} 13 |
14 | -------------------------------------------------------------------------------- /test/unit/tags/for_tag_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ForTagUnitTest < Test::Unit::TestCase 4 | def test_for_nodelist 5 | template = Liquid::Template.parse('{% for item in items %}FOR{% endfor %}') 6 | assert_equal ['FOR'], template.root.nodelist[0].nodelist 7 | end 8 | 9 | def test_for_else_nodelist 10 | template = Liquid::Template.parse('{% for item in items %}FOR{% else %}ELSE{% endfor %}') 11 | assert_equal ['FOR', 'ELSE'], template.root.nodelist[0].nodelist 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/liquid/interrupts.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | 3 | # An interrupt is any command that breaks processing of a block (ex: a for loop). 4 | class Interrupt 5 | attr_reader :message 6 | 7 | def initialize(message=nil) 8 | @message = message || "interrupt".freeze 9 | end 10 | end 11 | 12 | # Interrupt that is thrown whenever a {% break %} is called. 13 | class BreakInterrupt < Interrupt; end 14 | 15 | # Interrupt that is thrown whenever a {% continue %} is called. 16 | class ContinueInterrupt < Interrupt; end 17 | end 18 | -------------------------------------------------------------------------------- /test/integration/hash_ordering_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module MoneyFilter 4 | def money(input) 5 | sprintf(' %d$ ', input) 6 | end 7 | end 8 | 9 | module CanadianMoneyFilter 10 | def money(input) 11 | sprintf(' %d$ CAD ', input) 12 | end 13 | end 14 | 15 | class HashOrderingTest < Test::Unit::TestCase 16 | include Liquid 17 | 18 | def test_global_register_order 19 | Template.register_filter(MoneyFilter) 20 | Template.register_filter(CanadianMoneyFilter) 21 | 22 | assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil) 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /lib/liquid/tags/raw.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | class Raw < Block 3 | FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om 4 | 5 | def parse(tokens) 6 | @nodelist ||= [] 7 | @nodelist.clear 8 | while token = tokens.shift 9 | if token =~ FullTokenPossiblyInvalid 10 | @nodelist << $1 if $1 != "".freeze 11 | if block_delimiter == $2 12 | end_tag 13 | return 14 | end 15 | end 16 | @nodelist << token if not token.empty? 17 | end 18 | end 19 | end 20 | 21 | Template.register_tag('raw'.freeze, Raw) 22 | end 23 | -------------------------------------------------------------------------------- /test/unit/template_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TemplateUnitTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_sets_default_localization_in_document 7 | t = Template.new 8 | t.parse('') 9 | assert_instance_of I18n, t.root.options[:locale] 10 | end 11 | 12 | def test_sets_default_localization_in_context_with_quick_initialization 13 | t = Template.new 14 | t.parse('{{foo}}', :locale => I18n.new(fixture("en_locale.yml"))) 15 | 16 | assert_instance_of I18n, t.root.options[:locale] 17 | assert_equal fixture("en_locale.yml"), t.root.options[:locale].path 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/integration/context_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ContextTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_override_global_filter 7 | global = Module.new do 8 | def notice(output) 9 | "Global #{output}" 10 | end 11 | end 12 | 13 | local = Module.new do 14 | def notice(output) 15 | "Local #{output}" 16 | end 17 | end 18 | 19 | Template.register_filter(global) 20 | assert_equal 'Global test', Template.parse("{{'test' | notice }}").render! 21 | assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, :filters => [local]) 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /example/server/liquid_servlet.rb: -------------------------------------------------------------------------------- 1 | class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet 2 | 3 | def do_GET(req, res) 4 | handle(:get, req, res) 5 | end 6 | 7 | def do_POST(req, res) 8 | handle(:post, req, res) 9 | end 10 | 11 | private 12 | 13 | def handle(type, req, res) 14 | @request, @response = req, res 15 | 16 | @request.path_info =~ /(\w+)\z/ 17 | @action = $1 || 'index' 18 | @assigns = send(@action) if respond_to?(@action) 19 | 20 | @response['Content-Type'] = "text/html" 21 | @response.status = 200 22 | @response.body = Liquid::Template.parse(read_template).render(@assigns, :filters => [ProductsFilter]) 23 | end 24 | 25 | def read_template(filename = @action) 26 | File.read( File.dirname(__FILE__) + "/templates/#{filename}.liquid" ) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /performance/shopify/liquid.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.dirname(__FILE__) + '/../../lib' 2 | require File.dirname(__FILE__) + '/../../lib/liquid' 3 | 4 | require File.dirname(__FILE__) + '/comment_form' 5 | require File.dirname(__FILE__) + '/paginate' 6 | require File.dirname(__FILE__) + '/json_filter' 7 | require File.dirname(__FILE__) + '/money_filter' 8 | require File.dirname(__FILE__) + '/shop_filter' 9 | require File.dirname(__FILE__) + '/tag_filter' 10 | require File.dirname(__FILE__) + '/weight_filter' 11 | 12 | Liquid::Template.register_tag 'paginate', Paginate 13 | Liquid::Template.register_tag 'form', CommentForm 14 | 15 | Liquid::Template.register_filter JsonFilter 16 | Liquid::Template.register_filter MoneyFilter 17 | Liquid::Template.register_filter WeightFilter 18 | Liquid::Template.register_filter ShopFilter 19 | Liquid::Template.register_filter TagFilter 20 | -------------------------------------------------------------------------------- /performance/shopify/tag_filter.rb: -------------------------------------------------------------------------------- 1 | module TagFilter 2 | 3 | def link_to_tag(label, tag) 4 | "#{label}" 5 | end 6 | 7 | def highlight_active_tag(tag, css_class='active') 8 | if @context['current_tags'].include?(tag) 9 | "#{tag}" 10 | else 11 | tag 12 | end 13 | end 14 | 15 | def link_to_add_tag(label, tag) 16 | tags = (@context['current_tags'] + [tag]).uniq 17 | "#{label}" 18 | end 19 | 20 | def link_to_remove_tag(label, tag) 21 | tags = (@context['current_tags'] - [tag]).uniq 22 | "#{label}" 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /performance/tests/dropify/collection.liquid: -------------------------------------------------------------------------------- 1 | {% paginate collection.products by 20 %} 2 | 3 | 17 | 18 | 21 | 22 | {% endpaginate %} 23 | -------------------------------------------------------------------------------- /lib/liquid/extensions.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require 'date' 3 | 4 | class String # :nodoc: 5 | def to_liquid 6 | self 7 | end 8 | end 9 | 10 | class Array # :nodoc: 11 | def to_liquid 12 | self 13 | end 14 | end 15 | 16 | class Hash # :nodoc: 17 | def to_liquid 18 | self 19 | end 20 | end 21 | 22 | class Numeric # :nodoc: 23 | def to_liquid 24 | self 25 | end 26 | end 27 | 28 | class Time # :nodoc: 29 | def to_liquid 30 | self 31 | end 32 | end 33 | 34 | class DateTime < Date # :nodoc: 35 | def to_liquid 36 | self 37 | end 38 | end 39 | 40 | class Date # :nodoc: 41 | def to_liquid 42 | self 43 | end 44 | end 45 | 46 | class TrueClass 47 | def to_liquid # :nodoc: 48 | self 49 | end 50 | end 51 | 52 | class FalseClass 53 | def to_liquid # :nodoc: 54 | self 55 | end 56 | end 57 | 58 | class NilClass 59 | def to_liquid # :nodoc: 60 | self 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /performance/tests/vogue/blog.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{page.title}}

3 | 4 | {% paginate blog.articles by 20 %} 5 | 6 | {% for article in blog.articles %} 7 |
8 |

9 | {{ article.title }} 10 |

11 | 12 |

13 | {% if blog.comments_enabled? %} 14 | {{ article.comments_count }} comments 15 | — 16 | {% endif %} 17 | posted {{ article.created_at | date: "%Y %h" }} by {{ article.author }}

18 |
19 | {{ article.content }} 20 |
21 |
22 | {% endfor %} 23 | 24 | 27 | 28 | {% endpaginate %} 29 | 30 | 31 |
32 |
33 | -------------------------------------------------------------------------------- /lib/liquid/tags/unless.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/if' 2 | 3 | module Liquid 4 | # Unless is a conditional just like 'if' but works on the inverse logic. 5 | # 6 | # {% unless x < 0 %} x is greater than zero {% end %} 7 | # 8 | class Unless < If 9 | def render(context) 10 | context.stack do 11 | 12 | # First condition is interpreted backwards ( if not ) 13 | first_block = @blocks.first 14 | unless first_block.evaluate(context) 15 | return render_all(first_block.attachment, context) 16 | end 17 | 18 | # After the first condition unless works just like if 19 | @blocks[1..-1].each do |block| 20 | if block.evaluate(context) 21 | return render_all(block.attachment, context) 22 | end 23 | end 24 | 25 | ''.freeze 26 | end 27 | end 28 | end 29 | 30 | Template.register_tag('unless'.freeze, Unless) 31 | end 32 | -------------------------------------------------------------------------------- /lib/liquid/tags/assign.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | 3 | # Assign sets a variable in your template. 4 | # 5 | # {% assign foo = 'monkey' %} 6 | # 7 | # You can then use the variable later in the page. 8 | # 9 | # {{ foo }} 10 | # 11 | class Assign < Tag 12 | Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om 13 | 14 | def initialize(tag_name, markup, options) 15 | super 16 | if markup =~ Syntax 17 | @to = $1 18 | @from = Variable.new($2) 19 | else 20 | raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze) 21 | end 22 | end 23 | 24 | def render(context) 25 | val = @from.render(context) 26 | context.scopes.last[@to] = val 27 | context.increment_used_resources(:assign_score_current, val) 28 | ''.freeze 29 | end 30 | 31 | def blank? 32 | true 33 | end 34 | end 35 | 36 | Template.register_tag('assign'.freeze, Assign) 37 | end 38 | -------------------------------------------------------------------------------- /test/integration/assign_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AssignTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_assigned_variable 7 | assert_template_result('.foo.', 8 | '{% assign foo = values %}.{{ foo[0] }}.', 9 | 'values' => %w{foo bar baz}) 10 | 11 | assert_template_result('.bar.', 12 | '{% assign foo = values %}.{{ foo[1] }}.', 13 | 'values' => %w{foo bar baz}) 14 | end 15 | 16 | def test_assign_with_filter 17 | assert_template_result('.bar.', 18 | '{% assign foo = values | split: "," %}.{{ foo[1] }}.', 19 | 'values' => "foo,bar,baz") 20 | end 21 | 22 | def test_assign_syntax_error 23 | assert_match_syntax_error(/assign/, 24 | '{% assign foo not values %}.', 25 | 'values' => "foo,bar,baz") 26 | end 27 | end # AssignTest 28 | -------------------------------------------------------------------------------- /performance/tests/dropify/blog.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{page.title}}

3 | 4 | {% paginate blog.articles by 20 %} 5 | 6 | {% for article in blog.articles %} 7 | 8 |
9 |
10 |

11 | {{ article.title }} 12 |

13 |

Posted on {{ article.created_at | date: "%B %d, '%y" }} by {{ article.author }}.

14 |
15 | 16 |
17 | {{ article.content | strip_html | truncate: 250 }} 18 |
19 | 20 | {% if blog.comments_enabled? %} 21 |

{{ article.comments_count }} comments

22 | {% endif %} 23 |
24 | 25 | {% endfor %} 26 | 27 | 30 | 31 | {% endpaginate %} 32 | 33 |
34 | -------------------------------------------------------------------------------- /lib/liquid/utils.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | module Utils 3 | 4 | def self.slice_collection(collection, from, to) 5 | if (from != 0 || to != nil) && collection.respond_to?(:load_slice) 6 | collection.load_slice(from, to) 7 | else 8 | slice_collection_using_each(collection, from, to) 9 | end 10 | end 11 | 12 | def self.non_blank_string?(collection) 13 | collection.is_a?(String) && collection != ''.freeze 14 | end 15 | 16 | def self.slice_collection_using_each(collection, from, to) 17 | segments = [] 18 | index = 0 19 | 20 | # Maintains Ruby 1.8.7 String#each behaviour on 1.9 21 | return [collection] if non_blank_string?(collection) 22 | 23 | collection.each do |item| 24 | 25 | if to && to <= index 26 | break 27 | end 28 | 29 | if from <= index 30 | segments << item 31 | end 32 | 33 | index += 1 34 | end 35 | 36 | segments 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/liquid/tags/increment.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | # increment is used in a place where one needs to insert a counter 3 | # into a template, and needs the counter to survive across 4 | # multiple instantiations of the template. 5 | # (To achieve the survival, the application must keep the context) 6 | # 7 | # if the variable does not exist, it is created with value 0. 8 | # 9 | # Hello: {% increment variable %} 10 | # 11 | # gives you: 12 | # 13 | # Hello: 0 14 | # Hello: 1 15 | # Hello: 2 16 | # 17 | class Increment < Tag 18 | def initialize(tag_name, markup, options) 19 | super 20 | @variable = markup.strip 21 | end 22 | 23 | def render(context) 24 | value = context.environments.first[@variable] ||= 0 25 | context.environments.first[@variable] = value + 1 26 | value.to_s 27 | end 28 | 29 | def blank? 30 | false 31 | end 32 | end 33 | 34 | Template.register_tag('increment'.freeze, Increment) 35 | end 36 | -------------------------------------------------------------------------------- /lib/liquid/tags/capture.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | 3 | # Capture stores the result of a block into a variable without rendering it inplace. 4 | # 5 | # {% capture heading %} 6 | # Monkeys! 7 | # {% endcapture %} 8 | # ... 9 | #

{{ heading }}

10 | # 11 | # Capture is useful for saving content for use later in your template, such as 12 | # in a sidebar or footer. 13 | # 14 | class Capture < Block 15 | Syntax = /(\w+)/ 16 | 17 | def initialize(tag_name, markup, options) 18 | super 19 | if markup =~ Syntax 20 | @to = $1 21 | else 22 | raise SyntaxError.new(options[:locale].t("errors.syntax.capture")) 23 | end 24 | end 25 | 26 | def render(context) 27 | output = super 28 | context.scopes.last[@to] = output 29 | context.increment_used_resources(:assign_score_current, output) 30 | ''.freeze 31 | end 32 | 33 | def blank? 34 | true 35 | end 36 | end 37 | 38 | Template.register_tag('capture'.freeze, Capture) 39 | end 40 | -------------------------------------------------------------------------------- /test/integration/tags/increment_tag_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class IncrementTagTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_inc 7 | assert_template_result('0','{%increment port %}', {}) 8 | assert_template_result('0 1','{%increment port %} {%increment port%}', {}) 9 | assert_template_result('0 0 1 2 1', 10 | '{%increment port %} {%increment starboard%} ' + 11 | '{%increment port %} {%increment port%} ' + 12 | '{%increment starboard %}', {}) 13 | end 14 | 15 | def test_dec 16 | assert_template_result('9','{%decrement port %}', { 'port' => 10}) 17 | assert_template_result('-1 -2','{%decrement port %} {%decrement port%}', {}) 18 | assert_template_result('1 5 2 2 5', 19 | '{%increment port %} {%increment starboard%} ' + 20 | '{%increment port %} {%decrement port%} ' + 21 | '{%decrement starboard %}', { 'port' => 1, 'starboard' => 5 }) 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /performance/shopify/comment_form.rb: -------------------------------------------------------------------------------- 1 | class CommentForm < Liquid::Block 2 | Syntax = /(#{Liquid::VariableSignature}+)/ 3 | 4 | def initialize(tag_name, markup, options) 5 | super 6 | 7 | if markup =~ Syntax 8 | @variable_name = $1 9 | @attributes = {} 10 | else 11 | raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]") 12 | end 13 | end 14 | 15 | def render(context) 16 | article = context[@variable_name] 17 | 18 | context.stack do 19 | context['form'] = { 20 | 'posted_successfully?' => context.registers[:posted_successfully], 21 | 'errors' => context['comment.errors'], 22 | 'author' => context['comment.author'], 23 | 'email' => context['comment.email'], 24 | 'body' => context['comment.body'] 25 | } 26 | wrap_in_form(article, render_all(@nodelist, context)) 27 | end 28 | end 29 | 30 | def wrap_in_form(article, input) 31 | %Q{
\n#{input}\n
} 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/unit/i18n_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class I18nUnitTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def setup 7 | @i18n = I18n.new(fixture("en_locale.yml")) 8 | end 9 | 10 | def test_simple_translate_string 11 | assert_equal "less is more", @i18n.translate("simple") 12 | end 13 | 14 | def test_nested_translate_string 15 | assert_equal "something wasn't right", @i18n.translate("errors.syntax.oops") 16 | end 17 | 18 | def test_single_string_interpolation 19 | assert_equal "something different", @i18n.translate("whatever", :something => "different") 20 | end 21 | 22 | # def test_raises_translation_error_on_undefined_interpolation_key 23 | # assert_raise I18n::TranslationError do 24 | # @i18n.translate("whatever", :oopstypos => "yes") 25 | # end 26 | # end 27 | 28 | def test_raises_unknown_translation 29 | assert_raise I18n::TranslationError do 30 | @i18n.translate("doesnt_exist") 31 | end 32 | end 33 | 34 | def test_sets_default_path_to_en 35 | assert_equal I18n::DEFAULT_LOCALE, I18n.new.path 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/liquid/i18n.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Liquid 4 | class I18n 5 | DEFAULT_LOCALE = File.join(File.expand_path(File.dirname(__FILE__)), "locales", "en.yml") 6 | 7 | class TranslationError < StandardError 8 | end 9 | 10 | attr_reader :path 11 | 12 | def initialize(path = DEFAULT_LOCALE) 13 | @path = path 14 | end 15 | 16 | def translate(name, vars = {}) 17 | interpolate(deep_fetch_translation(name), vars) 18 | end 19 | alias_method :t, :translate 20 | 21 | def locale 22 | @locale ||= YAML.load_file(@path) 23 | end 24 | 25 | private 26 | def interpolate(name, vars) 27 | name.gsub(/%\{(\w+)\}/) { 28 | # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym] 29 | "#{vars[$1.to_sym]}" 30 | } 31 | end 32 | 33 | def deep_fetch_translation(name) 34 | name.split('.'.freeze).reduce(locale) do |level, cur| 35 | level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}" 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/liquid/tags/decrement.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | 3 | # decrement is used in a place where one needs to insert a counter 4 | # into a template, and needs the counter to survive across 5 | # multiple instantiations of the template. 6 | # NOTE: decrement is a pre-decrement, --i, 7 | # while increment is post: i++. 8 | # 9 | # (To achieve the survival, the application must keep the context) 10 | # 11 | # if the variable does not exist, it is created with value 0. 12 | 13 | # Hello: {% decrement variable %} 14 | # 15 | # gives you: 16 | # 17 | # Hello: -1 18 | # Hello: -2 19 | # Hello: -3 20 | # 21 | class Decrement < Tag 22 | def initialize(tag_name, markup, options) 23 | super 24 | @variable = markup.strip 25 | end 26 | 27 | def render(context) 28 | value = context.environments.first[@variable] ||= 0 29 | value = value - 1 30 | context.environments.first[@variable] = value 31 | value.to_s 32 | end 33 | 34 | private 35 | end 36 | 37 | Template.register_tag('decrement'.freeze, Decrement) 38 | end 39 | -------------------------------------------------------------------------------- /performance/tests/ripen/collection.liquid: -------------------------------------------------------------------------------- 1 |
2 | 3 | {% if collection.description %} 4 |
{{ collection.description }}
5 | {% endif %} 6 | 7 | {% paginate collection.products by 20 %} 8 | 9 | 23 | 24 | 27 | 28 | {% endpaginate %} 29 |
30 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005, 2006 Tobias Luetke 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /liquid.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | lib = File.expand_path('../lib/', __FILE__) 3 | $:.unshift lib unless $:.include?(lib) 4 | 5 | require "liquid/version" 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "liquid" 9 | s.version = Liquid::VERSION 10 | s.platform = Gem::Platform::RUBY 11 | s.summary = "A secure, non-evaling end user template engine with aesthetic markup." 12 | s.authors = ["Tobias Luetke"] 13 | s.email = ["tobi@leetsoft.com"] 14 | s.homepage = "http://www.liquidmarkup.org" 15 | s.license = "MIT" 16 | #s.description = "A secure, non-evaling end user template engine with aesthetic markup." 17 | 18 | s.required_rubygems_version = ">= 1.3.7" 19 | 20 | s.test_files = Dir.glob("{test}/**/*") 21 | s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md) 22 | 23 | s.extra_rdoc_files = ["History.md", "README.md"] 24 | 25 | s.require_path = "lib" 26 | 27 | s.add_development_dependency 'stackprof' if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.1.0") 28 | s.add_development_dependency 'rake' 29 | s.add_development_dependency 'activesupport' 30 | end 31 | -------------------------------------------------------------------------------- /test/unit/tokenizer_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TokenizerTest < Test::Unit::TestCase 4 | def test_tokenize_strings 5 | assert_equal [' '], tokenize(' ') 6 | assert_equal ['hello world'], tokenize('hello world') 7 | end 8 | 9 | def test_tokenize_variables 10 | assert_equal ['{{funk}}'], tokenize('{{funk}}') 11 | assert_equal [' ', '{{funk}}', ' '], tokenize(' {{funk}} ') 12 | assert_equal [' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '], tokenize(' {{funk}} {{so}} {{brother}} ') 13 | assert_equal [' ', '{{ funk }}', ' '], tokenize(' {{ funk }} ') 14 | end 15 | 16 | def test_tokenize_blocks 17 | assert_equal ['{%comment%}'], tokenize('{%comment%}') 18 | assert_equal [' ', '{%comment%}', ' '], tokenize(' {%comment%} ') 19 | 20 | assert_equal [' ', '{%comment%}', ' ', '{%endcomment%}', ' '], tokenize(' {%comment%} {%endcomment%} ') 21 | assert_equal [' ', '{% comment %}', ' ', '{% endcomment %}', ' '], tokenize(" {% comment %} {% endcomment %} ") 22 | end 23 | 24 | private 25 | 26 | def tokenize(source) 27 | Liquid::Template.new.send(:tokenize, source) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /performance/tests/vogue/collection.liquid: -------------------------------------------------------------------------------- 1 | {% paginate collection.products by 12 %}{% if collection.products.size == 0 %} 2 | No products found in this collection.{% else %} 3 |

{{ collection.title }}

4 | {{ collection.description }} 5 | 6 | {% tablerow product in collection.products cols: 3 %} 7 | 10 | 14 | {% endtablerow %} 15 | {% if paginate.pages > 1 %} 16 |
17 | {{ paginate | default_pagination }} 18 |
{% endif %}{% endif %} 19 | {% endpaginate %} 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Things we will merge 4 | 5 | * Bugfixes 6 | * Performance improvements 7 | * Features which are likely to be useful to the majority of Liquid users 8 | 9 | ## Things we won't merge 10 | 11 | * Code which introduces considerable performance degrations 12 | * Code which touches performance critical parts of Liquid and comes without benchmarks 13 | * Features which are not important for most people (we want to keep the core Liquid code small and tidy) 14 | * Features which can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem) 15 | * Code which comes without tests 16 | * Code which breaks existing tests 17 | 18 | ## Workflow 19 | 20 | * Fork the Liquid repository 21 | * Create a new branch in your fork 22 | * If it makes sense, add tests for your code and run a performance benchmark 23 | * Make sure all tests pass 24 | * Create a pull request 25 | * In the description, ping one of [@boourns](https://github.com/boourns), [@fw42](https://github.com/fw42), [@camilo](https://github.com/camilo), [@dylanahsmith](https://github.com/dylanahsmith), or [@arthurnn](https://github.com/arthurnn) and ask for a code review. 26 | 27 | -------------------------------------------------------------------------------- /example/server/example_servlet.rb: -------------------------------------------------------------------------------- 1 | module ProductsFilter 2 | def price(integer) 3 | sprintf("$%.2d USD", integer / 100.0) 4 | end 5 | 6 | def prettyprint(text) 7 | text.gsub( /\*(.*)\*/, '\1' ) 8 | end 9 | 10 | def count(array) 11 | array.size 12 | end 13 | 14 | def paragraph(p) 15 | "

#{p}

" 16 | end 17 | end 18 | 19 | class Servlet < LiquidServlet 20 | 21 | def index 22 | { 'date' => Time.now } 23 | end 24 | 25 | def products 26 | { 'products' => products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true} 27 | end 28 | 29 | private 30 | 31 | def products_list 32 | [{'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' }, 33 | {'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling'}, 34 | {'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity'}] 35 | end 36 | 37 | def description 38 | "List of Products ~ This is a list of products with price and description." 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /test/unit/file_system_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class FileSystemUnitTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_default 7 | assert_raise(FileSystemError) do 8 | BlankFileSystem.new.read_template_file("dummy", {'dummy'=>'smarty'}) 9 | end 10 | end 11 | 12 | def test_local 13 | file_system = Liquid::LocalFileSystem.new("/some/path") 14 | assert_equal "/some/path/_mypartial.liquid" , file_system.full_path("mypartial") 15 | assert_equal "/some/path/dir/_mypartial.liquid", file_system.full_path("dir/mypartial") 16 | 17 | assert_raise(FileSystemError) do 18 | file_system.full_path("../dir/mypartial") 19 | end 20 | 21 | assert_raise(FileSystemError) do 22 | file_system.full_path("/dir/../../dir/mypartial") 23 | end 24 | 25 | assert_raise(FileSystemError) do 26 | file_system.full_path("/etc/passwd") 27 | end 28 | end 29 | 30 | def test_custom_template_filename_patterns 31 | file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html") 32 | assert_equal "/some/path/mypartial.html" , file_system.full_path("mypartial") 33 | assert_equal "/some/path/dir/mypartial.html", file_system.full_path("dir/mypartial") 34 | end 35 | end # FileSystemTest 36 | -------------------------------------------------------------------------------- /test/integration/tags/raw_tag_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RawTagTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_tag_in_raw 7 | assert_template_result '{% comment %} test {% endcomment %}', 8 | '{% raw %}{% comment %} test {% endcomment %}{% endraw %}' 9 | end 10 | 11 | def test_output_in_raw 12 | assert_template_result '{{ test }}', '{% raw %}{{ test }}{% endraw %}' 13 | end 14 | 15 | def test_open_tag_in_raw 16 | assert_template_result ' Foobar {% invalid ', '{% raw %} Foobar {% invalid {% endraw %}' 17 | assert_template_result ' Foobar invalid %} ', '{% raw %} Foobar invalid %} {% endraw %}' 18 | assert_template_result ' Foobar {{ invalid ', '{% raw %} Foobar {{ invalid {% endraw %}' 19 | assert_template_result ' Foobar invalid }} ', '{% raw %} Foobar invalid }} {% endraw %}' 20 | assert_template_result ' Foobar {% invalid {% {% endraw ', '{% raw %} Foobar {% invalid {% {% endraw {% endraw %}' 21 | assert_template_result ' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}' 22 | assert_template_result ' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}' 23 | assert_template_result ' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}' 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /performance/tests/vogue/index.liquid: -------------------------------------------------------------------------------- 1 |
2 | {% assign article = pages.frontpage %} 3 | {% if article.content != "" %} 4 |

{{ article.title }}

5 | {{ article.content }} 6 | {% else %} 7 | In Admin > Blogs & Pages, create a page with the handle frontpage and it will show up here.
8 | {{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }} 9 | {% endif %} 10 |
11 | 12 | 13 | {% tablerow product in collections.frontpage.products cols: 3 limit: 12 %} 14 | 17 | 21 | {% endtablerow %} 22 | 23 | -------------------------------------------------------------------------------- /performance/tests/tribble/blog.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Post from our blog...

4 | {% paginate blog.articles by 20 %} 5 | {% for article in blog.articles %} 6 | 7 |
8 |

{{ article.title }}

9 |
10 |
{{ article.created_at | date: "%b %d" }}
11 | {{ article.content }} 12 |
13 |
14 | 15 | {% endfor %} 16 | 17 |
18 | {{ paginate | default_pagination }} 19 |
20 | 21 | {% endpaginate %} 22 |
23 | 24 |
25 |

Why Shop With Us?

26 | 40 |
41 |
42 | -------------------------------------------------------------------------------- /test/integration/capture_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CaptureTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_captures_block_content_in_variable 7 | assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {}) 8 | end 9 | 10 | def test_capture_to_variable_from_outer_scope_if_existing 11 | template_source = <<-END_TEMPLATE 12 | {% assign var = '' %} 13 | {% if true %} 14 | {% capture var %}first-block-string{% endcapture %} 15 | {% endif %} 16 | {% if true %} 17 | {% capture var %}test-string{% endcapture %} 18 | {% endif %} 19 | {{var}} 20 | END_TEMPLATE 21 | template = Template.parse(template_source) 22 | rendered = template.render! 23 | assert_equal "test-string", rendered.gsub(/\s/, '') 24 | end 25 | 26 | def test_assigning_from_capture 27 | template_source = <<-END_TEMPLATE 28 | {% assign first = '' %} 29 | {% assign second = '' %} 30 | {% for number in (1..3) %} 31 | {% capture first %}{{number}}{% endcapture %} 32 | {% assign second = first %} 33 | {% endfor %} 34 | {{ first }}-{{ second }} 35 | END_TEMPLATE 36 | template = Template.parse(template_source) 37 | rendered = template.render! 38 | assert_equal "3-3", rendered.gsub(/\s/, '') 39 | end 40 | end # CaptureTest 41 | -------------------------------------------------------------------------------- /test/integration/tags/unless_else_tag_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UnlessElseTagTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_unless 7 | assert_template_result(' ',' {% unless true %} this text should not go into the output {% endunless %} ') 8 | assert_template_result(' this text should go into the output ', 9 | ' {% unless false %} this text should go into the output {% endunless %} ') 10 | assert_template_result(' you rock ?','{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?') 11 | end 12 | 13 | def test_unless_else 14 | assert_template_result(' YES ','{% unless true %} NO {% else %} YES {% endunless %}') 15 | assert_template_result(' YES ','{% unless false %} YES {% else %} NO {% endunless %}') 16 | assert_template_result(' YES ','{% unless "foo" %} NO {% else %} YES {% endunless %}') 17 | end 18 | 19 | def test_unless_in_loop 20 | assert_template_result '23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', 'choices' => [1, nil, false] 21 | end 22 | 23 | def test_unless_else_in_loop 24 | assert_template_result ' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', 'choices' => [1, nil, false] 25 | end 26 | end # UnlessElseTest 27 | -------------------------------------------------------------------------------- /performance/tests/ripen/index.liquid: -------------------------------------------------------------------------------- 1 |
2 |

Featured products...

3 | 21 | 22 |
23 | {% assign article = pages.frontpage %} 24 | {% if article.content != "" %} 25 |

{{ article.title }}

26 | {{ article.content }} 27 | {% else %} 28 | In Admin > Blogs & Pages, create a page with the handle frontpage and it will show up here.
29 | {{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }} 30 | {% endif %} 31 |
32 |
33 | -------------------------------------------------------------------------------- /lib/liquid/tag.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | class Tag 3 | attr_accessor :options 4 | attr_reader :nodelist, :warnings 5 | 6 | class << self 7 | def parse(tag_name, markup, tokens, options) 8 | tag = new(tag_name, markup, options) 9 | tag.parse(tokens) 10 | tag 11 | end 12 | 13 | private :new 14 | end 15 | 16 | def initialize(tag_name, markup, options) 17 | @tag_name = tag_name 18 | @markup = markup 19 | @options = options 20 | end 21 | 22 | def parse(tokens) 23 | end 24 | 25 | def name 26 | self.class.name.downcase 27 | end 28 | 29 | def render(context) 30 | ''.freeze 31 | end 32 | 33 | def blank? 34 | @blank || false 35 | end 36 | 37 | def parse_with_selected_parser(markup) 38 | case @options[:error_mode] || Template.error_mode 39 | when :strict then strict_parse_with_error_context(markup) 40 | when :lax then lax_parse(markup) 41 | when :warn 42 | begin 43 | return strict_parse_with_error_context(markup) 44 | rescue SyntaxError => e 45 | @warnings ||= [] 46 | @warnings << e 47 | return lax_parse(markup) 48 | end 49 | end 50 | end 51 | 52 | private 53 | def strict_parse_with_error_context(markup) 54 | strict_parse(markup) 55 | rescue SyntaxError => e 56 | e.message << " in \"#{markup.strip}\"" 57 | raise e 58 | end 59 | end # Tag 60 | end # Liquid 61 | -------------------------------------------------------------------------------- /performance/tests/tribble/search.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |

Search Results

7 | {% if search.performed %} 8 | 9 | {% paginate search.results by 10 %} 10 | 11 | {% if search.results == empty %} 12 |
Your search for "{{search.terms | escape}}" did not yield any results
13 | {% else %} 14 | 15 | 16 | 24 | {% endif %} 25 | 26 |
27 | {{ paginate | default_pagination }} 28 |
29 | 30 | 31 |
32 |

Why Shop With Us?

33 | 47 |
48 |
49 | 50 | {% endpaginate %} 51 | {% endif %} 52 | -------------------------------------------------------------------------------- /lib/liquid/lexer.rb: -------------------------------------------------------------------------------- 1 | require "strscan" 2 | module Liquid 3 | class Lexer 4 | SPECIALS = { 5 | '|'.freeze => :pipe, 6 | '.'.freeze => :dot, 7 | ':'.freeze => :colon, 8 | ','.freeze => :comma, 9 | '['.freeze => :open_square, 10 | ']'.freeze => :close_square, 11 | '('.freeze => :open_round, 12 | ')'.freeze => :close_round 13 | } 14 | IDENTIFIER = /[\w\-?!]+/ 15 | SINGLE_STRING_LITERAL = /'[^\']*'/ 16 | DOUBLE_STRING_LITERAL = /"[^\"]*"/ 17 | NUMBER_LITERAL = /-?\d+(\.\d+)?/ 18 | DOTDOT = /\.\./ 19 | COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/ 20 | 21 | def initialize(input) 22 | @ss = StringScanner.new(input.rstrip) 23 | end 24 | 25 | def tokenize 26 | @output = [] 27 | 28 | while !@ss.eos? 29 | @ss.skip(/\s*/) 30 | tok = case 31 | when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] 32 | when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t] 33 | when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t] 34 | when t = @ss.scan(NUMBER_LITERAL) then [:number, t] 35 | when t = @ss.scan(IDENTIFIER) then [:id, t] 36 | when t = @ss.scan(DOTDOT) then [:dotdot, t] 37 | else 38 | c = @ss.getch 39 | if s = SPECIALS[c] 40 | [s,c] 41 | else 42 | raise SyntaxError, "Unexpected character #{c}" 43 | end 44 | end 45 | @output << tok 46 | end 47 | 48 | @output << [:end_of_string] 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /example/server/templates/products.liquid: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | products 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

{{ description | split: '~' | first }}

21 | 22 |

{{ description | split: '~' | last }}

23 | 24 |

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

25 | 26 | {% if cool_products %} 27 | Cool products :) 28 | {% else %} 29 | Uncool products :( 30 | {% endif %} 31 | 32 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'test/unit' 4 | require 'test/unit/assertions' 5 | 6 | $:.unshift(File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib')) 7 | require 'liquid.rb' 8 | 9 | mode = :strict 10 | if env_mode = ENV['LIQUID_PARSER_MODE'] 11 | puts "-- #{env_mode.upcase} ERROR MODE" 12 | mode = env_mode.to_sym 13 | end 14 | Liquid::Template.error_mode = mode 15 | 16 | 17 | module Test 18 | module Unit 19 | class TestCase 20 | def fixture(name) 21 | File.join(File.expand_path(File.dirname(__FILE__)), "fixtures", name) 22 | end 23 | end 24 | 25 | module Assertions 26 | include Liquid 27 | 28 | def assert_template_result(expected, template, assigns = {}, message = nil) 29 | assert_equal expected, Template.parse(template).render!(assigns) 30 | end 31 | 32 | def assert_template_result_matches(expected, template, assigns = {}, message = nil) 33 | return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp 34 | 35 | assert_match expected, Template.parse(template).render!(assigns) 36 | end 37 | 38 | def assert_match_syntax_error(match, template, registers = {}) 39 | exception = assert_raise(Liquid::SyntaxError) { 40 | Template.parse(template).render(assigns) 41 | } 42 | assert_match match, exception.message 43 | end 44 | 45 | def with_error_mode(mode) 46 | old_mode = Liquid::Template.error_mode 47 | Liquid::Template.error_mode = mode 48 | yield 49 | Liquid::Template.error_mode = old_mode 50 | end 51 | end # Assertions 52 | end # Unit 53 | end # Test 54 | -------------------------------------------------------------------------------- /test/unit/lexer_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class LexerUnitTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_strings 7 | tokens = Lexer.new(%! 'this is a test""' "wat 'lol'"!).tokenize 8 | assert_equal [[:string,%!'this is a test""'!], [:string, %!"wat 'lol'"!], [:end_of_string]], tokens 9 | end 10 | 11 | def test_integer 12 | tokens = Lexer.new('hi 50').tokenize 13 | assert_equal [[:id,'hi'], [:number, '50'], [:end_of_string]], tokens 14 | end 15 | 16 | def test_float 17 | tokens = Lexer.new('hi 5.0').tokenize 18 | assert_equal [[:id,'hi'], [:number, '5.0'], [:end_of_string]], tokens 19 | end 20 | 21 | def test_comparison 22 | tokens = Lexer.new('== <> contains').tokenize 23 | assert_equal [[:comparison,'=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens 24 | end 25 | 26 | def test_specials 27 | tokens = Lexer.new('| .:').tokenize 28 | assert_equal [[:pipe, '|'], [:dot, '.'], [:colon, ':'], [:end_of_string]], tokens 29 | tokens = Lexer.new('[,]').tokenize 30 | assert_equal [[:open_square, '['], [:comma, ','], [:close_square, ']'], [:end_of_string]], tokens 31 | end 32 | 33 | def test_fancy_identifiers 34 | tokens = Lexer.new('hi! five?').tokenize 35 | assert_equal [[:id,'hi!'], [:id, 'five?'], [:end_of_string]], tokens 36 | end 37 | 38 | def test_whitespace 39 | tokens = Lexer.new("five|\n\t ==").tokenize 40 | assert_equal [[:id,'five'], [:pipe, '|'], [:comparison, '=='], [:end_of_string]], tokens 41 | end 42 | 43 | def test_unexpected_character 44 | assert_raises(SyntaxError) do 45 | Lexer.new("%").tokenize 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/liquid/locales/en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | errors: 3 | syntax: 4 | assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]" 5 | capture: "Syntax Error in 'capture' - Valid syntax: capture [var]" 6 | case: "Syntax Error in 'case' - Valid syntax: case [condition]" 7 | case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}" 8 | case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) " 9 | cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]" 10 | for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]" 11 | for_invalid_in: "For loops require an 'in' clause" 12 | for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset" 13 | if: "Syntax Error in tag 'if' - Valid syntax: if [expression]" 14 | include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]" 15 | unknown_tag: "Unknown tag '%{tag}'" 16 | invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}" 17 | unexpected_else: "%{block_name} tag does not expect else tag" 18 | tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}" 19 | variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}" 20 | tag_never_closed: "'%{block_name}' tag was never closed" 21 | meta_syntax_error: "Liquid syntax error: #{e.message}" 22 | table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3" 23 | -------------------------------------------------------------------------------- /performance/shopify/database.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | module Database 3 | 4 | # Load the standard vision toolkit database and re-arrage it to be simply exportable 5 | # to liquid as assigns. All this is based on Shopify 6 | def self.tables 7 | @tables ||= begin 8 | db = YAML.load_file(File.dirname(__FILE__) + '/vision.database.yml') 9 | 10 | # From vision source 11 | db['products'].each do |product| 12 | collections = db['collections'].find_all do |collection| 13 | collection['products'].any? { |p| p['id'].to_i == product['id'].to_i } 14 | end 15 | product['collections'] = collections 16 | end 17 | 18 | # key the tables by handles, as this is how liquid expects it. 19 | db = db.inject({}) do |assigns, (key, values)| 20 | assigns[key] = values.inject({}) { |h, v| h[v['handle']] = v; h; } 21 | assigns 22 | end 23 | 24 | # Some standard direct accessors so that the specialized templates 25 | # render correctly 26 | db['collection'] = db['collections'].values.first 27 | db['product'] = db['products'].values.first 28 | db['blog'] = db['blogs'].values.first 29 | db['article'] = db['blog']['articles'].first 30 | 31 | db['cart'] = { 32 | 'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] }, 33 | 'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] }, 34 | 'items' => db['line_items'].values 35 | } 36 | 37 | db 38 | end 39 | end 40 | end 41 | 42 | if __FILE__ == $0 43 | p Database.tables['collections']['frontpage'].keys 44 | #p Database.tables['blog']['articles'] 45 | end 46 | -------------------------------------------------------------------------------- /test/unit/regexp_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RegexpUnitTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_empty 7 | assert_equal [], ''.scan(QuotedFragment) 8 | end 9 | 10 | def test_quote 11 | assert_equal ['"arg 1"'], '"arg 1"'.scan(QuotedFragment) 12 | end 13 | 14 | def test_words 15 | assert_equal ['arg1', 'arg2'], 'arg1 arg2'.scan(QuotedFragment) 16 | end 17 | 18 | def test_tags 19 | assert_equal ['', ''], ' '.scan(QuotedFragment) 20 | assert_equal [''], ''.scan(QuotedFragment) 21 | assert_equal ['', ''], %||.scan(QuotedFragment) 22 | end 23 | 24 | def test_double_quoted_words 25 | assert_equal ['arg1', 'arg2', '"arg 3"'], 'arg1 arg2 "arg 3"'.scan(QuotedFragment) 26 | end 27 | 28 | def test_single_quoted_words 29 | assert_equal ['arg1', 'arg2', "'arg 3'"], 'arg1 arg2 \'arg 3\''.scan(QuotedFragment) 30 | end 31 | 32 | def test_quoted_words_in_the_middle 33 | assert_equal ['arg1', 'arg2', '"arg 3"', 'arg4'], 'arg1 arg2 "arg 3" arg4 '.scan(QuotedFragment) 34 | end 35 | 36 | def test_variable_parser 37 | assert_equal ['var'], 'var'.scan(VariableParser) 38 | assert_equal ['var', 'method'], 'var.method'.scan(VariableParser) 39 | assert_equal ['var', '[method]'], 'var[method]'.scan(VariableParser) 40 | assert_equal ['var', '[method]', '[0]'], 'var[method][0]'.scan(VariableParser) 41 | assert_equal ['var', '["method"]', '[0]'], 'var["method"][0]'.scan(VariableParser) 42 | assert_equal ['var', '[method]', '[0]', 'method'], 'var[method][0].method'.scan(VariableParser) 43 | end 44 | end # RegexpTest 45 | -------------------------------------------------------------------------------- /lib/liquid/tags/cycle.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | # Cycle is usually used within a loop to alternate between values, like colors or DOM classes. 3 | # 4 | # {% for item in items %} 5 | #
{{ item }}
6 | # {% end %} 7 | # 8 | #
Item one
9 | #
Item two
10 | #
Item three
11 | #
Item four
12 | #
Item five
13 | # 14 | class Cycle < Tag 15 | SimpleSyntax = /\A#{QuotedFragment}+/o 16 | NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om 17 | 18 | def initialize(tag_name, markup, options) 19 | super 20 | case markup 21 | when NamedSyntax 22 | @variables = variables_from_string($2) 23 | @name = $1 24 | when SimpleSyntax 25 | @variables = variables_from_string(markup) 26 | @name = "'#{@variables.to_s}'" 27 | else 28 | raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze)) 29 | end 30 | end 31 | 32 | def render(context) 33 | context.registers[:cycle] ||= Hash.new(0) 34 | 35 | context.stack do 36 | key = context[@name] 37 | iteration = context.registers[:cycle][key] 38 | result = context[@variables[iteration]] 39 | iteration += 1 40 | iteration = 0 if iteration >= @variables.size 41 | context.registers[:cycle][key] = iteration 42 | result 43 | end 44 | end 45 | 46 | def blank? 47 | false 48 | end 49 | 50 | private 51 | def variables_from_string(markup) 52 | markup.split(',').collect do |var| 53 | var =~ /\s*(#{QuotedFragment})\s*/o 54 | $1 ? $1 : nil 55 | end.compact 56 | end 57 | end 58 | 59 | Template.register_tag('cycle', Cycle) 60 | end 61 | -------------------------------------------------------------------------------- /lib/liquid/module_ex.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2007 by Domizio Demichelis 2 | # This library is free software. It may be used, redistributed and/or modified 3 | # under the same terms as Ruby itself 4 | # 5 | # This extension is used in order to expose the object of the implementing class 6 | # to liquid as it were a Drop. It also limits the liquid-callable methods of the instance 7 | # to the allowed method passed with the liquid_methods call 8 | # Example: 9 | # 10 | # class SomeClass 11 | # liquid_methods :an_allowed_method 12 | # 13 | # def an_allowed_method 14 | # 'this comes from an allowed method' 15 | # end 16 | # def unallowed_method 17 | # 'this will never be an output' 18 | # end 19 | # end 20 | # 21 | # if you want to extend the drop to other methods you can defines more methods 22 | # in the class ::LiquidDropClass 23 | # 24 | # class SomeClass::LiquidDropClass 25 | # def another_allowed_method 26 | # 'and this from another allowed method' 27 | # end 28 | # end 29 | # end 30 | # 31 | # usage: 32 | # @something = SomeClass.new 33 | # 34 | # template: 35 | # {{something.an_allowed_method}}{{something.unallowed_method}} {{something.another_allowed_method}} 36 | # 37 | # output: 38 | # 'this comes from an allowed method and this from another allowed method' 39 | # 40 | # You can also chain associations, by adding the liquid_method call in the 41 | # association models. 42 | # 43 | class Module 44 | 45 | def liquid_methods(*allowed_methods) 46 | drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end" 47 | define_method :to_liquid do 48 | drop_class.new(self) 49 | end 50 | drop_class.class_eval do 51 | def initialize(object) 52 | @object = object 53 | end 54 | allowed_methods.each do |sym| 55 | define_method sym do 56 | @object.send sym 57 | end 58 | end 59 | end 60 | end 61 | 62 | end 63 | -------------------------------------------------------------------------------- /performance/tests/dropify/index.liquid: -------------------------------------------------------------------------------- 1 |
2 |

Featured Items

3 | {% for product in collections.frontpage.products limit:1 offset:0 %} 4 |
5 | {{ product.title | escape }} 6 |

{{ product.title }}

7 |
{{ product.description | strip_html | truncatewords: 18 }}
8 |

{{ product.price_min | money }}

9 |
10 | {% endfor %} 11 | {% for product in collections.frontpage.products offset:1 %} 12 |
13 | {{ product.title | escape }} 14 |

{{ product.title }}

15 |

{{ product.price_min | money }}

16 |
17 | {% endfor %} 18 |
19 | 20 |
21 | {% assign article = pages.frontpage %} 22 | 23 | {% if article.content != "" %} 24 |

{{ article.title }}

25 |
26 | {{ article.content }} 27 |
28 | {% else %} 29 |
30 | In Admin > Blogs & Pages, create a page with the handle frontpage and it will show up here.
31 | {{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }} 32 |
33 | {% endif %} 34 | 35 |
36 |
37 |
38 | {% for article in blogs.news.articles offset:1 %} 39 |
40 |

{{ article.title }}

41 |
42 | {{ article.content }} 43 |
44 |
45 | {% endfor %} 46 |
47 | 48 | -------------------------------------------------------------------------------- /test/integration/security_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module SecurityFilter 4 | def add_one(input) 5 | "#{input} + 1" 6 | end 7 | end 8 | 9 | class SecurityTest < Test::Unit::TestCase 10 | include Liquid 11 | 12 | def test_no_instance_eval 13 | text = %( {{ '1+1' | instance_eval }} ) 14 | expected = %| 1+1 | 15 | 16 | assert_equal expected, Template.parse(text).render!(@assigns) 17 | end 18 | 19 | def test_no_existing_instance_eval 20 | text = %( {{ '1+1' | __instance_eval__ }} ) 21 | expected = %| 1+1 | 22 | 23 | assert_equal expected, Template.parse(text).render!(@assigns) 24 | end 25 | 26 | 27 | def test_no_instance_eval_after_mixing_in_new_filter 28 | text = %( {{ '1+1' | instance_eval }} ) 29 | expected = %| 1+1 | 30 | 31 | assert_equal expected, Template.parse(text).render!(@assigns) 32 | end 33 | 34 | 35 | def test_no_instance_eval_later_in_chain 36 | text = %( {{ '1+1' | add_one | instance_eval }} ) 37 | expected = %| 1+1 + 1 | 38 | 39 | assert_equal expected, Template.parse(text).render!(@assigns, :filters => SecurityFilter) 40 | end 41 | 42 | def test_does_not_add_filters_to_symbol_table 43 | current_symbols = Symbol.all_symbols 44 | 45 | test = %( {{ "some_string" | a_bad_filter }} ) 46 | 47 | template = Template.parse(test) 48 | assert_equal [], (Symbol.all_symbols - current_symbols) 49 | 50 | template.render! 51 | assert_equal [], (Symbol.all_symbols - current_symbols) 52 | end 53 | 54 | def test_does_not_add_drop_methods_to_symbol_table 55 | current_symbols = Symbol.all_symbols 56 | 57 | assigns = { 'drop' => Drop.new } 58 | assert_equal "", Template.parse("{{ drop.custom_method_1 }}", assigns).render! 59 | assert_equal "", Template.parse("{{ drop.custom_method_2 }}", assigns).render! 60 | assert_equal "", Template.parse("{{ drop.custom_method_3 }}", assigns).render! 61 | 62 | assert_equal [], (Symbol.all_symbols - current_symbols) 63 | end 64 | end # SecurityTest 65 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | $LOAD_PATH.unshift File.expand_path("../lib", __FILE__) 4 | require "liquid/version" 5 | 6 | task :default => 'test' 7 | 8 | desc 'run test suite with default parser' 9 | Rake::TestTask.new(:base_test) do |t| 10 | t.libs << '.' << 'lib' << 'test' 11 | t.test_files = FileList['test/{integration,unit}/**/*_test.rb'] 12 | t.verbose = false 13 | end 14 | 15 | desc 'run test suite with warn error mode' 16 | task :warn_test do 17 | ENV['LIQUID_PARSER_MODE'] = 'warn' 18 | Rake::Task['base_test'].invoke 19 | end 20 | 21 | desc 'runs test suite with both strict and lax parsers' 22 | task :test do 23 | ENV['LIQUID_PARSER_MODE'] = 'lax' 24 | Rake::Task['base_test'].invoke 25 | ENV['LIQUID_PARSER_MODE'] = 'strict' 26 | Rake::Task['base_test'].reenable 27 | Rake::Task['base_test'].invoke 28 | end 29 | 30 | task :gem => :build 31 | task :build do 32 | system "gem build liquid.gemspec" 33 | end 34 | 35 | task :install => :build do 36 | system "gem install liquid-#{Liquid::VERSION}.gem" 37 | end 38 | 39 | task :release => :build do 40 | system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'" 41 | system "git push --tags" 42 | system "gem push liquid-#{Liquid::VERSION}.gem" 43 | system "rm liquid-#{Liquid::VERSION}.gem" 44 | end 45 | 46 | namespace :benchmark do 47 | 48 | desc "Run the liquid benchmark with lax parsing" 49 | task :run do 50 | ruby "./performance/benchmark.rb lax" 51 | end 52 | 53 | desc "Run the liquid benchmark with strict parsing" 54 | task :strict do 55 | ruby "./performance/benchmark.rb strict" 56 | end 57 | end 58 | 59 | 60 | namespace :profile do 61 | 62 | desc "Run the liquid profile/performance coverage" 63 | task :run do 64 | ruby "./performance/profile.rb" 65 | end 66 | 67 | desc "Run the liquid profile/performance coverage with strict parsing" 68 | task :strict do 69 | ruby "./performance/profile.rb strict" 70 | end 71 | 72 | end 73 | 74 | desc "Run example" 75 | task :example do 76 | ruby "-w -d -Ilib example/server/server.rb" 77 | end 78 | -------------------------------------------------------------------------------- /lib/liquid/strainer.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module Liquid 4 | 5 | # Strainer is the parent class for the filters system. 6 | # New filters are mixed into the strainer class which is then instantiated for each liquid template render run. 7 | # 8 | # The Strainer only allows method calls defined in filters given to it via Strainer.global_filter, 9 | # Context#add_filters or Template.register_filter 10 | class Strainer #:nodoc: 11 | @@filters = [] 12 | @@known_filters = Set.new 13 | @@known_methods = Set.new 14 | @@strainer_class_cache = Hash.new do |hash, filters| 15 | hash[filters] = Class.new(Strainer) do 16 | filters.each { |f| include f } 17 | end 18 | end 19 | 20 | def initialize(context) 21 | @context = context 22 | end 23 | 24 | def self.global_filter(filter) 25 | raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module) 26 | add_known_filter(filter) 27 | @@filters << filter unless @@filters.include?(filter) 28 | end 29 | 30 | def self.add_known_filter(filter) 31 | unless @@known_filters.include?(filter) 32 | @@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s)) 33 | new_methods = filter.instance_methods.map(&:to_s) 34 | new_methods.reject!{ |m| @@method_blacklist.include?(m) } 35 | @@known_methods.merge(new_methods) 36 | @@known_filters.add(filter) 37 | end 38 | end 39 | 40 | def self.strainer_class_cache 41 | @@strainer_class_cache 42 | end 43 | 44 | def self.create(context, filters = []) 45 | filters = @@filters + filters 46 | strainer_class_cache[filters].new(context) 47 | end 48 | 49 | def invoke(method, *args) 50 | if invokable?(method) 51 | send(method, *args) 52 | else 53 | args.first 54 | end 55 | rescue ::ArgumentError => e 56 | raise Liquid::ArgumentError.new(e.message) 57 | end 58 | 59 | def invokable?(method) 60 | @@known_methods.include?(method.to_s) && respond_to?(method) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/unit/block_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BlockUnitTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_blankspace 7 | template = Liquid::Template.parse(" ") 8 | assert_equal [" "], template.root.nodelist 9 | end 10 | 11 | def test_variable_beginning 12 | template = Liquid::Template.parse("{{funk}} ") 13 | assert_equal 2, template.root.nodelist.size 14 | assert_equal Variable, template.root.nodelist[0].class 15 | assert_equal String, template.root.nodelist[1].class 16 | end 17 | 18 | def test_variable_end 19 | template = Liquid::Template.parse(" {{funk}}") 20 | assert_equal 2, template.root.nodelist.size 21 | assert_equal String, template.root.nodelist[0].class 22 | assert_equal Variable, template.root.nodelist[1].class 23 | end 24 | 25 | def test_variable_middle 26 | template = Liquid::Template.parse(" {{funk}} ") 27 | assert_equal 3, template.root.nodelist.size 28 | assert_equal String, template.root.nodelist[0].class 29 | assert_equal Variable, template.root.nodelist[1].class 30 | assert_equal String, template.root.nodelist[2].class 31 | end 32 | 33 | def test_variable_many_embedded_fragments 34 | template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ") 35 | assert_equal 7, template.root.nodelist.size 36 | assert_equal [String, Variable, String, Variable, String, Variable, String], 37 | block_types(template.root.nodelist) 38 | end 39 | 40 | def test_with_block 41 | template = Liquid::Template.parse(" {% comment %} {% endcomment %} ") 42 | assert_equal [String, Comment, String], block_types(template.root.nodelist) 43 | assert_equal 3, template.root.nodelist.size 44 | end 45 | 46 | def test_with_custom_tag 47 | Liquid::Template.register_tag("testtag", Block) 48 | 49 | assert_nothing_thrown do 50 | template = Liquid::Template.parse( "{% testtag %} {% endtesttag %}") 51 | end 52 | end 53 | 54 | private 55 | def block_types(nodelist) 56 | nodelist.collect { |node| node.class } 57 | end 58 | end # VariableTest 59 | -------------------------------------------------------------------------------- /performance/tests/tribble/page.liquid: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |

{{page.title}}

6 |
7 | {{page.content}} 8 |
9 |
10 |
11 | 12 | 13 |

Featured Products

14 |
    15 | 16 | {% for product in collections.frontpage.products %} 17 |
  • 18 |
    19 |
    20 |
    21 |
    22 |

    {{product.title}}

    23 |

    {{ product.description | truncatewords: 15 }}

    24 |
    25 | {{ product.title | escape }} 26 |
    27 | 28 |
    29 | 30 | 31 |

    32 | View Details 33 | 34 | 35 | {% if product.compare_at_price %} 36 | {% if product.price_min != product.compare_at_price %} 37 | {{product.compare_at_price | money}} - 38 | {% endif %} 39 | {% endif %} 40 | 41 | {{product.price_min | money}} 42 | 43 | 44 |

    45 |
    46 |
    47 |
    48 |
  • 49 | {% endfor %} 50 | 51 |
52 |
53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /lib/liquid/tags/case.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | class Case < Block 3 | Syntax = /(#{QuotedFragment})/o 4 | WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om 5 | 6 | def initialize(tag_name, markup, options) 7 | super 8 | @blocks = [] 9 | 10 | if markup =~ Syntax 11 | @left = $1 12 | else 13 | raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze)) 14 | end 15 | end 16 | 17 | def nodelist 18 | @blocks.map(&:attachment).flatten 19 | end 20 | 21 | def unknown_tag(tag, markup, tokens) 22 | @nodelist = [] 23 | case tag 24 | when 'when'.freeze 25 | record_when_condition(markup) 26 | when 'else'.freeze 27 | record_else_condition(markup) 28 | else 29 | super 30 | end 31 | end 32 | 33 | def render(context) 34 | context.stack do 35 | execute_else_block = true 36 | 37 | output = '' 38 | @blocks.each do |block| 39 | if block.else? 40 | return render_all(block.attachment, context) if execute_else_block 41 | elsif block.evaluate(context) 42 | execute_else_block = false 43 | output << render_all(block.attachment, context) 44 | end 45 | end 46 | output 47 | end 48 | end 49 | 50 | private 51 | 52 | def record_when_condition(markup) 53 | while markup 54 | # Create a new nodelist and assign it to the new block 55 | if not markup =~ WhenSyntax 56 | raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze)) 57 | end 58 | 59 | markup = $2 60 | 61 | block = Condition.new(@left, '=='.freeze, $1) 62 | block.attach(@nodelist) 63 | @blocks.push(block) 64 | end 65 | end 66 | 67 | def record_else_condition(markup) 68 | if not markup.strip.empty? 69 | raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze)) 70 | end 71 | 72 | block = ElseCondition.new 73 | block.attach(@nodelist) 74 | @blocks << block 75 | end 76 | end 77 | 78 | Template.register_tag('case'.freeze, Case) 79 | end 80 | -------------------------------------------------------------------------------- /performance/tests/ripen/cart.liquid: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 | {% if cart.item_count == 0 %} 11 |

Your shopping cart is empty...

12 |

Continue shopping

13 | {% else %} 14 | 15 |

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for item in cart.items %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% endfor %} 36 |
ProductQtyPriceTotalRemove
{{ item.product.featured_image | product_img_url: 'thumb' | img_tag }}{{ item.title }}{{ item.price | money }}{{item.line_price | money }}Remove
37 |

38 |

39 | Subtotal: {{cart.total_price | money_with_currency }} 40 |

41 |

42 | 43 | {% if additional_checkout_buttons %} 44 |
45 |

- or -

46 | {{ content_for_additional_checkout_buttons }} 47 |
48 | {% endif %} 49 | 50 |
51 | 52 | {% endif %} 53 | 54 |
55 | -------------------------------------------------------------------------------- /performance/tests/tribble/404.liquid: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |

Oh no!

6 |
7 | Seems like you are looking for something that just isn't here. Try heading back to our main page. Or you can checkout some of our featured products below. 8 |
9 |
10 |
11 | 12 | 13 |

Featured Products

14 |
    15 | 16 | {% for product in collections.frontpage.products %} 17 |
  • 18 |
    19 |
    20 |
    21 |
    22 |

    {{product.title}}

    23 |

    {{ product.description | truncatewords: 15 }}

    24 |
    25 | {{ product.title | escape }} 26 |
    27 | 28 |
    29 | 30 | 31 |

    32 | View Details 33 | 34 | 35 | {% if product.compare_at_price %} 36 | {% if product.price_min != product.compare_at_price %} 37 | {{product.compare_at_price | money}} - 38 | {% endif %} 39 | {% endif %} 40 | 41 | {{product.price_min | money}} 42 | 43 | 44 |

    45 |
    46 |
    47 |
    48 |
  • 49 | {% endfor %} 50 | 51 |
52 |
53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /lib/liquid/parser.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | class Parser 3 | def initialize(input) 4 | l = Lexer.new(input) 5 | @tokens = l.tokenize 6 | @p = 0 # pointer to current location 7 | end 8 | 9 | def jump(point) 10 | @p = point 11 | end 12 | 13 | def consume(type = nil) 14 | token = @tokens[@p] 15 | if type && token[0] != type 16 | raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}" 17 | end 18 | @p += 1 19 | token[1] 20 | end 21 | 22 | # Only consumes the token if it matches the type 23 | # Returns the token's contents if it was consumed 24 | # or false otherwise. 25 | def consume?(type) 26 | token = @tokens[@p] 27 | return false unless token && token[0] == type 28 | @p += 1 29 | token[1] 30 | end 31 | 32 | # Like consume? Except for an :id token of a certain name 33 | def id?(str) 34 | token = @tokens[@p] 35 | return false unless token && token[0] == :id 36 | return false unless token[1] == str 37 | @p += 1 38 | token[1] 39 | end 40 | 41 | def look(type, ahead = 0) 42 | tok = @tokens[@p + ahead] 43 | return false unless tok 44 | tok[0] == type 45 | end 46 | 47 | def expression 48 | token = @tokens[@p] 49 | if token[0] == :id 50 | variable_signature 51 | elsif [:string, :number].include? token[0] 52 | consume 53 | elsif token.first == :open_round 54 | consume 55 | first = expression 56 | consume(:dotdot) 57 | last = expression 58 | consume(:close_round) 59 | "(#{first}..#{last})" 60 | else 61 | raise SyntaxError, "#{token} is not a valid expression" 62 | end 63 | end 64 | 65 | def argument 66 | str = "" 67 | # might be a keyword argument (identifier: expression) 68 | if look(:id) && look(:colon, 1) 69 | str << consume << consume << ' '.freeze 70 | end 71 | 72 | str << expression 73 | str 74 | end 75 | 76 | def variable_signature 77 | str = consume(:id) 78 | if look(:open_square) 79 | str << consume 80 | str << expression 81 | str << consume(:close_square) 82 | end 83 | if look(:dot) 84 | str << consume 85 | str << variable_signature 86 | end 87 | str 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/liquid/tags/table_row.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | class TableRow < Block 3 | Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o 4 | 5 | def initialize(tag_name, markup, options) 6 | super 7 | if markup =~ Syntax 8 | @variable_name = $1 9 | @collection_name = $2 10 | @attributes = {} 11 | markup.scan(TagAttributes) do |key, value| 12 | @attributes[key] = value 13 | end 14 | else 15 | raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze)) 16 | end 17 | end 18 | 19 | def render(context) 20 | collection = context[@collection_name] or return ''.freeze 21 | 22 | from = @attributes['offset'.freeze] ? context[@attributes['offset'.freeze]].to_i : 0 23 | to = @attributes['limit'.freeze] ? from + context[@attributes['limit'.freeze]].to_i : nil 24 | 25 | collection = Utils.slice_collection(collection, from, to) 26 | 27 | length = collection.length 28 | 29 | cols = context[@attributes['cols'.freeze]].to_i 30 | 31 | row = 1 32 | col = 0 33 | 34 | result = "\n" 35 | context.stack do 36 | 37 | collection.each_with_index do |item, index| 38 | context[@variable_name] = item 39 | context['tablerowloop'.freeze] = { 40 | 'length'.freeze => length, 41 | 'index'.freeze => index + 1, 42 | 'index0'.freeze => index, 43 | 'col'.freeze => col + 1, 44 | 'col0'.freeze => col, 45 | 'index0'.freeze => index, 46 | 'rindex'.freeze => length - index, 47 | 'rindex0'.freeze => length - index - 1, 48 | 'first'.freeze => (index == 0), 49 | 'last'.freeze => (index == length - 1), 50 | 'col_first'.freeze => (col == 0), 51 | 'col_last'.freeze => (col == cols - 1) 52 | } 53 | 54 | 55 | col += 1 56 | 57 | result << "" << render_all(@nodelist, context) << '' 58 | 59 | if col == cols and (index != length - 1) 60 | col = 0 61 | row += 1 62 | result << "\n" 63 | end 64 | 65 | end 66 | end 67 | result << "\n" 68 | result 69 | end 70 | end 71 | 72 | Template.register_tag('tablerow'.freeze, TableRow) 73 | end 74 | -------------------------------------------------------------------------------- /test/unit/strainer_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StrainerUnitTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | module AccessScopeFilters 7 | def public_filter 8 | "public" 9 | end 10 | 11 | def private_filter 12 | "private" 13 | end 14 | private :private_filter 15 | end 16 | 17 | Strainer.global_filter(AccessScopeFilters) 18 | 19 | def test_strainer 20 | strainer = Strainer.create(nil) 21 | assert_equal 5, strainer.invoke('size', 'input') 22 | assert_equal "public", strainer.invoke("public_filter") 23 | end 24 | 25 | def test_stainer_raises_argument_error 26 | strainer = Strainer.create(nil) 27 | assert_raises(Liquid::ArgumentError) do 28 | strainer.invoke("public_filter", 1) 29 | end 30 | end 31 | 32 | def test_strainer_only_invokes_public_filter_methods 33 | strainer = Strainer.create(nil) 34 | assert_equal false, strainer.invokable?('__test__') 35 | assert_equal false, strainer.invokable?('test') 36 | assert_equal false, strainer.invokable?('instance_eval') 37 | assert_equal false, strainer.invokable?('__send__') 38 | assert_equal true, strainer.invokable?('size') # from the standard lib 39 | end 40 | 41 | def test_strainer_returns_nil_if_no_filter_method_found 42 | strainer = Strainer.create(nil) 43 | assert_nil strainer.invoke("private_filter") 44 | assert_nil strainer.invoke("undef_the_filter") 45 | end 46 | 47 | def test_strainer_returns_first_argument_if_no_method_and_arguments_given 48 | strainer = Strainer.create(nil) 49 | assert_equal "password", strainer.invoke("undef_the_method", "password") 50 | end 51 | 52 | def test_strainer_only_allows_methods_defined_in_filters 53 | strainer = Strainer.create(nil) 54 | assert_equal "1 + 1", strainer.invoke("instance_eval", "1 + 1") 55 | assert_equal "puts", strainer.invoke("__send__", "puts", "Hi Mom") 56 | assert_equal "has_method?", strainer.invoke("invoke", "has_method?", "invoke") 57 | end 58 | 59 | def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation 60 | a, b = Module.new, Module.new 61 | strainer = Strainer.create(nil, [a,b]) 62 | assert_kind_of Strainer, strainer 63 | assert_kind_of a, strainer 64 | assert_kind_of b, strainer 65 | Strainer.class_variable_get(:@@filters).each do |m| 66 | assert_kind_of m, strainer 67 | end 68 | end 69 | 70 | end # StrainerTest 71 | -------------------------------------------------------------------------------- /lib/liquid/drop.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module Liquid 4 | 5 | # A drop in liquid is a class which allows you to export DOM like things to liquid. 6 | # Methods of drops are callable. 7 | # The main use for liquid drops is to implement lazy loaded objects. 8 | # If you would like to make data available to the web designers which you don't want loaded unless needed then 9 | # a drop is a great way to do that. 10 | # 11 | # Example: 12 | # 13 | # class ProductDrop < Liquid::Drop 14 | # def top_sales 15 | # Shop.current.products.find(:all, :order => 'sales', :limit => 10 ) 16 | # end 17 | # end 18 | # 19 | # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' ) 20 | # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query. 21 | # 22 | # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a 23 | # catch all. 24 | class Drop 25 | attr_writer :context 26 | 27 | EMPTY_STRING = ''.freeze 28 | 29 | # Catch all for the method 30 | def before_method(method) 31 | nil 32 | end 33 | 34 | # called by liquid to invoke a drop 35 | def invoke_drop(method_or_key) 36 | if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key) 37 | send(method_or_key) 38 | else 39 | before_method(method_or_key) 40 | end 41 | end 42 | 43 | def has_key?(name) 44 | true 45 | end 46 | 47 | def inspect 48 | self.class.to_s 49 | end 50 | 51 | def to_liquid 52 | self 53 | end 54 | 55 | def to_s 56 | self.class.name 57 | end 58 | 59 | alias :[] :invoke_drop 60 | 61 | private 62 | 63 | # Check for method existence without invoking respond_to?, which creates symbols 64 | def self.invokable?(method_name) 65 | unless @invokable_methods 66 | blacklist = Liquid::Drop.public_instance_methods + [:each] 67 | if include?(Enumerable) 68 | blacklist += Enumerable.public_instance_methods 69 | blacklist -= [:sort, :count, :first, :min, :max, :include?] 70 | end 71 | whitelist = [:to_liquid] + (public_instance_methods - blacklist) 72 | @invokable_methods = Set.new(whitelist.map(&:to_s)) 73 | end 74 | @invokable_methods.include?(method_name.to_s) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/unit/module_ex_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestClassA 4 | liquid_methods :allowedA, :chainedB 5 | def allowedA 6 | 'allowedA' 7 | end 8 | def restrictedA 9 | 'restrictedA' 10 | end 11 | def chainedB 12 | TestClassB.new 13 | end 14 | end 15 | 16 | class TestClassB 17 | liquid_methods :allowedB, :chainedC 18 | def allowedB 19 | 'allowedB' 20 | end 21 | def chainedC 22 | TestClassC.new 23 | end 24 | end 25 | 26 | class TestClassC 27 | liquid_methods :allowedC 28 | def allowedC 29 | 'allowedC' 30 | end 31 | end 32 | 33 | class TestClassC::LiquidDropClass 34 | def another_allowedC 35 | 'another_allowedC' 36 | end 37 | end 38 | 39 | class ModuleExUnitTest < Test::Unit::TestCase 40 | include Liquid 41 | 42 | def setup 43 | @a = TestClassA.new 44 | @b = TestClassB.new 45 | @c = TestClassC.new 46 | end 47 | 48 | def test_should_create_LiquidDropClass 49 | assert TestClassA::LiquidDropClass 50 | assert TestClassB::LiquidDropClass 51 | assert TestClassC::LiquidDropClass 52 | end 53 | 54 | def test_should_respond_to_liquid 55 | assert @a.respond_to?(:to_liquid) 56 | assert @b.respond_to?(:to_liquid) 57 | assert @c.respond_to?(:to_liquid) 58 | end 59 | 60 | def test_should_return_LiquidDropClass_object 61 | assert @a.to_liquid.is_a?(TestClassA::LiquidDropClass) 62 | assert @b.to_liquid.is_a?(TestClassB::LiquidDropClass) 63 | assert @c.to_liquid.is_a?(TestClassC::LiquidDropClass) 64 | end 65 | 66 | def test_should_respond_to_liquid_methods 67 | assert @a.to_liquid.respond_to?(:allowedA) 68 | assert @a.to_liquid.respond_to?(:chainedB) 69 | assert @b.to_liquid.respond_to?(:allowedB) 70 | assert @b.to_liquid.respond_to?(:chainedC) 71 | assert @c.to_liquid.respond_to?(:allowedC) 72 | assert @c.to_liquid.respond_to?(:another_allowedC) 73 | end 74 | 75 | def test_should_not_respond_to_restricted_methods 76 | assert ! @a.to_liquid.respond_to?(:restricted) 77 | end 78 | 79 | def test_should_use_regular_objects_as_drops 80 | assert_template_result 'allowedA', "{{ a.allowedA }}", 'a'=>@a 81 | assert_template_result 'allowedB', "{{ a.chainedB.allowedB }}", 'a'=>@a 82 | assert_template_result 'allowedC', "{{ a.chainedB.chainedC.allowedC }}", 'a'=>@a 83 | assert_template_result 'another_allowedC', "{{ a.chainedB.chainedC.another_allowedC }}", 'a'=>@a 84 | assert_template_result '', "{{ a.restricted }}", 'a'=>@a 85 | assert_template_result '', "{{ a.unknown }}", 'a'=>@a 86 | end 87 | end # ModuleExTest 88 | -------------------------------------------------------------------------------- /test/unit/parser_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ParserUnitTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_consume 7 | p = Parser.new("wat: 7") 8 | assert_equal 'wat', p.consume(:id) 9 | assert_equal ':', p.consume(:colon) 10 | assert_equal '7', p.consume(:number) 11 | end 12 | 13 | def test_jump 14 | p = Parser.new("wat: 7") 15 | p.jump(2) 16 | assert_equal '7', p.consume(:number) 17 | end 18 | 19 | def test_consume? 20 | p = Parser.new("wat: 7") 21 | assert_equal 'wat', p.consume?(:id) 22 | assert_equal false, p.consume?(:dot) 23 | assert_equal ':', p.consume(:colon) 24 | assert_equal '7', p.consume?(:number) 25 | end 26 | 27 | def test_id? 28 | p = Parser.new("wat 6 Peter Hegemon") 29 | assert_equal 'wat', p.id?('wat') 30 | assert_equal false, p.id?('endgame') 31 | assert_equal '6', p.consume(:number) 32 | assert_equal 'Peter', p.id?('Peter') 33 | assert_equal false, p.id?('Achilles') 34 | end 35 | 36 | def test_look 37 | p = Parser.new("wat 6 Peter Hegemon") 38 | assert_equal true, p.look(:id) 39 | assert_equal 'wat', p.consume(:id) 40 | assert_equal false, p.look(:comparison) 41 | assert_equal true, p.look(:number) 42 | assert_equal true, p.look(:id, 1) 43 | assert_equal false, p.look(:number, 1) 44 | end 45 | 46 | def test_expressions 47 | p = Parser.new("hi.there hi[5].! hi.there.bob") 48 | assert_equal 'hi.there', p.expression 49 | assert_equal 'hi[5].!', p.expression 50 | assert_equal 'hi.there.bob', p.expression 51 | 52 | p = Parser.new("567 6.0 'lol' \"wut\"") 53 | assert_equal '567', p.expression 54 | assert_equal '6.0', p.expression 55 | assert_equal "'lol'", p.expression 56 | assert_equal '"wut"', p.expression 57 | end 58 | 59 | def test_ranges 60 | p = Parser.new("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)") 61 | assert_equal '(5..7)', p.expression 62 | assert_equal '(1.5..9.6)', p.expression 63 | assert_equal '(young..old)', p.expression 64 | assert_equal '(hi[5].wat..old)', p.expression 65 | end 66 | 67 | def test_arguments 68 | p = Parser.new("filter: hi.there[5], keyarg: 7") 69 | assert_equal 'filter', p.consume(:id) 70 | assert_equal ':', p.consume(:colon) 71 | assert_equal 'hi.there[5]', p.argument 72 | assert_equal ',', p.consume(:comma) 73 | assert_equal 'keyarg: 7', p.argument 74 | end 75 | 76 | def test_invalid_expression 77 | assert_raises(SyntaxError) do 78 | p = Parser.new("==") 79 | p.expression 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /performance/tests/dropify/cart.liquid: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 | {% if cart.item_count == 0 %} 11 |

Your shopping cart is looking rather empty...

12 | {% else %} 13 |
14 | 15 |
16 | 17 |

You have {{ cart.item_count }} {{ cart.item_count | pluralize: 'product', 'products' }} in here!

18 | 19 | 48 | 49 |
50 | 51 |
52 | 53 |
54 | 55 | {% if additional_checkout_buttons %} 56 |
57 |

- or -

58 | {{ content_for_additional_checkout_buttons }} 59 |
60 | {% endif %} 61 | 62 |
63 | 64 | {% endif %} 65 | 66 |
67 | -------------------------------------------------------------------------------- /test/integration/variable_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class VariableTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_simple_variable 7 | template = Template.parse(%|{{test}}|) 8 | assert_equal 'worked', template.render!('test' => 'worked') 9 | assert_equal 'worked wonderfully', template.render!('test' => 'worked wonderfully') 10 | end 11 | 12 | def test_simple_with_whitespaces 13 | template = Template.parse(%| {{ test }} |) 14 | assert_equal ' worked ', template.render!('test' => 'worked') 15 | assert_equal ' worked wonderfully ', template.render!('test' => 'worked wonderfully') 16 | end 17 | 18 | def test_ignore_unknown 19 | template = Template.parse(%|{{ test }}|) 20 | assert_equal '', template.render! 21 | end 22 | 23 | def test_hash_scoping 24 | template = Template.parse(%|{{ test.test }}|) 25 | assert_equal 'worked', template.render!('test' => {'test' => 'worked'}) 26 | end 27 | 28 | def test_preset_assigns 29 | template = Template.parse(%|{{ test }}|) 30 | template.assigns['test'] = 'worked' 31 | assert_equal 'worked', template.render! 32 | end 33 | 34 | def test_reuse_parsed_template 35 | template = Template.parse(%|{{ greeting }} {{ name }}|) 36 | template.assigns['greeting'] = 'Goodbye' 37 | assert_equal 'Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi') 38 | assert_equal 'Hello ', template.render!('greeting' => 'Hello', 'unknown' => 'Tobi') 39 | assert_equal 'Hello Brian', template.render!('greeting' => 'Hello', 'name' => 'Brian') 40 | assert_equal 'Goodbye Brian', template.render!('name' => 'Brian') 41 | assert_equal({'greeting'=>'Goodbye'}, template.assigns) 42 | end 43 | 44 | def test_assigns_not_polluted_from_template 45 | template = Template.parse(%|{{ test }}{% assign test = 'bar' %}{{ test }}|) 46 | template.assigns['test'] = 'baz' 47 | assert_equal 'bazbar', template.render! 48 | assert_equal 'bazbar', template.render! 49 | assert_equal 'foobar', template.render!('test' => 'foo') 50 | assert_equal 'bazbar', template.render! 51 | end 52 | 53 | def test_hash_with_default_proc 54 | template = Template.parse(%|Hello {{ test }}|) 55 | assigns = Hash.new { |h,k| raise "Unknown variable '#{k}'" } 56 | assigns['test'] = 'Tobi' 57 | assert_equal 'Hello Tobi', template.render!(assigns) 58 | assigns.delete('test') 59 | e = assert_raises(RuntimeError) { 60 | template.render!(assigns) 61 | } 62 | assert_equal "Unknown variable 'test'", e.message 63 | end 64 | 65 | def test_multiline_variable 66 | assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked') 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /performance/tests/vogue/article.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{ article.title }}

3 |

posted {{ article.created_at | date: "%Y %h" }} by {{ article.author }}

4 |
5 | {{ article.content }} 6 |
7 |
8 | 9 | {% if blog.comments_enabled? %} 10 |
11 |

Comments

12 | 13 | 14 |
    15 | {% for comment in article.comments %} 16 |
  • 17 |
    18 | {{ comment.content }} 19 |
    20 | 21 |
    22 | Posted by {{ comment.author }} on {{ comment.created_at | date: "%B %d, %Y" }} 23 |
    24 |
  • 25 | {% endfor %} 26 |
27 | 28 | 29 | {% form article %} 30 |

Leave a comment

31 | 32 | 33 | {% if form.posted_successfully? %} 34 | {% if blog.moderated? %} 35 |
36 | Successfully posted your comment.
37 | It will have to be approved by the blog owner first before showing up. 38 |
39 | {% else %} 40 |
Successfully posted your comment.
41 | {% endif %} 42 | {% endif %} 43 | 44 | {% if form.errors %} 45 |
Not all the fields have been filled out correctly!
46 | {% endif %} 47 | 48 |
49 |
50 |
51 | 52 |
53 |
54 | 55 |
56 |
57 |
58 | 59 | {% if blog.moderated? %} 60 |

comments have to be approved before showing up

61 | {% endif %} 62 | 63 | 64 | {% endform %} 65 |
66 | {% endif %} 67 | -------------------------------------------------------------------------------- /test/integration/parsing_quirks_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ParsingQuirksTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_parsing_css 7 | text = " div { font-weight: bold; } " 8 | assert_equal text, Template.parse(text).render! 9 | end 10 | 11 | def test_raise_on_single_close_bracet 12 | assert_raise(SyntaxError) do 13 | Template.parse("text {{method} oh nos!") 14 | end 15 | end 16 | 17 | def test_raise_on_label_and_no_close_bracets 18 | assert_raise(SyntaxError) do 19 | Template.parse("TEST {{ ") 20 | end 21 | end 22 | 23 | def test_raise_on_label_and_no_close_bracets_percent 24 | assert_raise(SyntaxError) do 25 | Template.parse("TEST {% ") 26 | end 27 | end 28 | 29 | def test_error_on_empty_filter 30 | assert_nothing_raised do 31 | Template.parse("{{test}}") 32 | Template.parse("{{|test}}") 33 | end 34 | with_error_mode(:strict) do 35 | assert_raise(SyntaxError) do 36 | Template.parse("{{test |a|b|}}") 37 | end 38 | end 39 | end 40 | 41 | def test_meaningless_parens_error 42 | with_error_mode(:strict) do 43 | assert_raise(SyntaxError) do 44 | markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" 45 | Template.parse("{% if #{markup} %} YES {% endif %}") 46 | end 47 | end 48 | end 49 | 50 | def test_unexpected_characters_syntax_error 51 | with_error_mode(:strict) do 52 | assert_raise(SyntaxError) do 53 | markup = "true && false" 54 | Template.parse("{% if #{markup} %} YES {% endif %}") 55 | end 56 | assert_raise(SyntaxError) do 57 | markup = "false || true" 58 | Template.parse("{% if #{markup} %} YES {% endif %}") 59 | end 60 | end 61 | end 62 | 63 | def test_no_error_on_lax_empty_filter 64 | assert_nothing_raised do 65 | Template.parse("{{test |a|b|}}", :error_mode => :lax) 66 | Template.parse("{{test}}", :error_mode => :lax) 67 | Template.parse("{{|test|}}", :error_mode => :lax) 68 | end 69 | end 70 | 71 | def test_meaningless_parens_lax 72 | with_error_mode(:lax) do 73 | assigns = {'b' => 'bar', 'c' => 'baz'} 74 | markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" 75 | assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}", assigns) 76 | end 77 | end 78 | 79 | def test_unexpected_characters_silently_eat_logic_lax 80 | with_error_mode(:lax) do 81 | markup = "true && false" 82 | assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}") 83 | markup = "false || true" 84 | assert_template_result('',"{% if #{markup} %} YES {% endif %}") 85 | end 86 | end 87 | end # ParsingQuirksTest 88 | -------------------------------------------------------------------------------- /performance/tests/tribble/collection.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{ collection.title }}

3 | {% if collection.description.size > 0 %} 4 |
{{ collection.description }}
5 | {% endif %} 6 | 7 | {% paginate collection.products by 8 %} 8 | 9 |
    10 | {% for product in collection.products %} 11 |
  • 12 |
    13 |
    14 |
    15 |
    16 |

    {{product.title}}

    17 |

    {{ product.description | truncatewords: 15 }}

    18 |
    19 | {{ product.title | escape }} 20 |
    21 | 22 |
    23 | 24 | 25 |

    26 | View Details 27 | 28 | 29 | {% if product.compare_at_price %} 30 | {% if product.price_min != product.compare_at_price %} 31 | {{product.compare_at_price | money}} - 32 | {% endif %} 33 | {% endif %} 34 | 35 | {{product.price_min | money}} 36 | 37 | 38 |

    39 |
    40 |
    41 |
    42 |
  • 43 | {% endfor %} 44 |
45 | 46 |
47 | {{ paginate | default_pagination }} 48 |
49 | 50 | 51 |
52 |

Why Shop With Us?

53 |
    54 |
  • 55 |

    24 Hours

    56 |

    We're always here to help.

    57 |
  • 58 |
  • 59 |

    No Spam

    60 |

    We'll never share your info.

    61 |
  • 62 |
  • 63 |

    Secure Servers

    64 |

    Checkout is 256bit encrypted.

    65 |
  • 66 |
67 |
68 |
69 | 70 | {% endpaginate %} 71 | -------------------------------------------------------------------------------- /performance/tests/vogue/product.liquid: -------------------------------------------------------------------------------- 1 |
2 | {% for image in product.images %}{% if forloop.first %}
3 | {{ product.title | escape }} 4 |
{% else %} 5 |
6 | {{ product.title | escape }} 7 |
{% endif %}{% endfor %} 8 |
9 |
10 |

{{ product.title }}

11 | {{ product.description }} 12 | 13 | {% if product.available %} 14 |
15 | 16 |
17 |
18 | 19 | 24 |
25 | 26 | 27 |
28 | {% else %} 29 |

This product is temporarily unavailable

30 | {% endif %} 31 | 32 |
33 | Continue Shopping
34 | Browse more {{ product.type | link_to_type }} or additional {{ product.vendor | link_to_vendor }} products. 35 |
36 |
37 | 38 | 39 | 62 | 63 | -------------------------------------------------------------------------------- /performance/shopify/paginate.rb: -------------------------------------------------------------------------------- 1 | class Paginate < Liquid::Block 2 | Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/ 3 | 4 | def initialize(tag_name, markup, options) 5 | super 6 | 7 | @nodelist = [] 8 | 9 | if markup =~ Syntax 10 | @collection_name = $1 11 | @page_size = if $2 12 | $3.to_i 13 | else 14 | 20 15 | end 16 | 17 | @attributes = { 'window_size' => 3 } 18 | markup.scan(Liquid::TagAttributes) do |key, value| 19 | @attributes[key] = value 20 | end 21 | else 22 | raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number") 23 | end 24 | end 25 | 26 | def render(context) 27 | @context = context 28 | 29 | context.stack do 30 | current_page = context['current_page'].to_i 31 | 32 | pagination = { 33 | 'page_size' => @page_size, 34 | 'current_page' => 5, 35 | 'current_offset' => @page_size * 5 36 | } 37 | 38 | context['paginate'] = pagination 39 | 40 | collection_size = context[@collection_name].size 41 | 42 | raise ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection_size.nil? 43 | 44 | page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1 45 | 46 | pagination['items'] = collection_size 47 | pagination['pages'] = page_count -1 48 | pagination['previous'] = link('« Previous', current_page-1 ) unless 1 >= current_page 49 | pagination['next'] = link('Next »', current_page+1 ) unless page_count <= current_page+1 50 | pagination['parts'] = [] 51 | 52 | hellip_break = false 53 | 54 | if page_count > 2 55 | 1.upto(page_count-1) do |page| 56 | 57 | if current_page == page 58 | pagination['parts'] << no_link(page) 59 | elsif page == 1 60 | pagination['parts'] << link(page, page) 61 | elsif page == page_count -1 62 | pagination['parts'] << link(page, page) 63 | elsif page <= current_page - @attributes['window_size'] or page >= current_page + @attributes['window_size'] 64 | next if hellip_break 65 | pagination['parts'] << no_link('…') 66 | hellip_break = true 67 | next 68 | else 69 | pagination['parts'] << link(page, page) 70 | end 71 | 72 | hellip_break = false 73 | end 74 | end 75 | 76 | render_all(@nodelist, context) 77 | end 78 | end 79 | 80 | private 81 | 82 | def no_link(title) 83 | { 'title' => title, 'is_link' => false} 84 | end 85 | 86 | def link(title, page) 87 | { 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true} 88 | end 89 | 90 | def current_url 91 | "/collections/frontpage" 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /performance/tests/ripen/theme.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{shop.name}} - {{page_title}} 5 | 6 | 7 | {{ 'main.css' | asset_url | stylesheet_tag }} 8 | {{ 'shop.js' | asset_url | script_tag }} 9 | 10 | {{ 'mootools.js' | asset_url | script_tag }} 11 | {{ 'slimbox.js' | asset_url | script_tag }} 12 | {{ 'option_selection.js' | shopify_asset_url | script_tag }} 13 | {{ 'slimbox.css' | asset_url | stylesheet_tag }} 14 | 15 | {{ content_for_header }} 16 | 17 | 18 | 19 |

Skip to navigation.

20 |
21 |
22 | 25 |
26 | {{ content_for_layout }} 27 |
28 |
29 | {% if template != 'cart' %} 30 |
31 |
32 |
Shopping Cart
33 |
34 | {% if cart.item_count != 0 %} 35 | {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }} in your cart 36 | {% else %} 37 | Your cart is empty 38 | {% endif %} 39 |
40 |
41 |
42 | {% endif %} 43 | 55 | 72 |
73 | 74 |
75 |
76 |
77 | 78 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /lib/liquid.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2005 Tobias Luetke 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | module Liquid 23 | FilterSeparator = /\|/ 24 | ArgumentSeparator = ','.freeze 25 | FilterArgumentSeparator = ':'.freeze 26 | VariableAttributeSeparator = '.'.freeze 27 | TagStart = /\{\%/ 28 | TagEnd = /\%\}/ 29 | VariableSignature = /\(?[\w\-\.\[\]]\)?/ 30 | VariableSegment = /[\w\-]/ 31 | VariableStart = /\{\{/ 32 | VariableEnd = /\}\}/ 33 | VariableIncompleteEnd = /\}\}?/ 34 | QuotedString = /"[^"]*"|'[^']*'/ 35 | QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o 36 | TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o 37 | AnyStartingTag = /\{\{|\{\%/ 38 | PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om 39 | TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om 40 | VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o 41 | end 42 | 43 | require "liquid/version" 44 | require 'liquid/lexer' 45 | require 'liquid/parser' 46 | require 'liquid/i18n' 47 | require 'liquid/drop' 48 | require 'liquid/extensions' 49 | require 'liquid/errors' 50 | require 'liquid/interrupts' 51 | require 'liquid/strainer' 52 | require 'liquid/context' 53 | require 'liquid/tag' 54 | require 'liquid/block' 55 | require 'liquid/document' 56 | require 'liquid/variable' 57 | require 'liquid/file_system' 58 | require 'liquid/template' 59 | require 'liquid/standardfilters' 60 | require 'liquid/condition' 61 | require 'liquid/module_ex' 62 | require 'liquid/utils' 63 | 64 | # Load all the tags of the standard library 65 | # 66 | Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f } 67 | -------------------------------------------------------------------------------- /performance/tests/dropify/product.liquid: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | {% for image in product.images %} 5 | {% if forloop.first %} 6 | 7 | {{product.title | escape }} 8 | 9 | {% else %} 10 | 11 | {{product.title | escape }} 12 | 13 | {% endif %} 14 | {% endfor %} 15 |
16 | 17 |

{{ product.title }}

18 | 19 |
    20 |
  • Vendor: {{ product.vendor | link_to_vendor }}
  • 21 |
  • Type: {{ product.type | link_to_type }}
  • 22 |
23 | 24 | {{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %} 25 | 26 |
27 |
28 | 29 | 34 | 35 |
36 | 37 |
38 |
39 |
40 | 41 |
42 | {{ product.description }} 43 |
44 |
45 | 46 | 69 | -------------------------------------------------------------------------------- /performance/tests/ripen/article.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{ article.title }}

3 |

posted {{ article.created_at | date: "%Y %h" }} by

4 | 5 |
6 | {{ article.content }} 7 |
8 | 9 |
10 | 11 | 12 | {% if blog.comments_enabled? %} 13 |
14 |

Comments

15 | 16 | 17 |
    18 | {% for comment in article.comments %} 19 |
  • 20 |
    21 | {{ comment.content }} 22 |
    23 | 24 |
    25 | Posted by {{ comment.author }} on {{ comment.created_at | date: "%B %d, %Y" }} 26 |
    27 |
  • 28 | {% endfor %} 29 |
30 | 31 | 32 |
33 | {% form article %} 34 |

Leave a comment

35 | 36 | 37 | {% if form.posted_successfully? %} 38 | {% if blog.moderated? %} 39 |
40 | Successfully posted your comment.
41 | It will have to be approved by the blog owner first before showing up. 42 |
43 | {% else %} 44 |
Successfully posted your comment.
45 | {% endif %} 46 | {% endif %} 47 | 48 | {% if form.errors %} 49 |
Not all the fields have been filled out correctly!
50 | {% endif %} 51 | 52 |
53 |
54 |
55 | 56 |
57 |
58 | 59 |
60 |
61 |
62 | 63 | {% if blog.moderated? %} 64 |

comments have to be approved before showing up

65 | {% endif %} 66 | 67 | 68 | {% endform %} 69 |
70 | 71 | 72 |
73 | {% endif %} 74 | 75 | -------------------------------------------------------------------------------- /performance/theme_runner.rb: -------------------------------------------------------------------------------- 1 | # This profiler run simulates Shopify. 2 | # We are looking in the tests directory for liquid files and render them within the designated layout file. 3 | # We will also export a substantial database to liquid which the templates can render values of. 4 | # All this is to make the benchmark as non syntetic as possible. All templates and tests are lifted from 5 | # direct real-world usage and the profiler measures code that looks very similar to the way it looks in 6 | # Shopify which is likely the biggest user of liquid in the world which something to the tune of several 7 | # million Template#render calls a day. 8 | 9 | require 'rubygems' 10 | require 'active_support' 11 | require 'active_support/json' 12 | require 'yaml' 13 | require 'digest/md5' 14 | require File.dirname(__FILE__) + '/shopify/liquid' 15 | require File.dirname(__FILE__) + '/shopify/database.rb' 16 | 17 | class ThemeRunner 18 | class FileSystem 19 | 20 | def initialize(path) 21 | @path = path 22 | end 23 | 24 | # Called by Liquid to retrieve a template file 25 | def read_template_file(template_path, context) 26 | File.read(@path + '/' + template_path + '.liquid') 27 | end 28 | end 29 | 30 | # Load all templates into memory, do this now so that 31 | # we don't profile IO. 32 | def initialize 33 | @tests = Dir[File.dirname(__FILE__) + '/tests/**/*.liquid'].collect do |test| 34 | next if File.basename(test) == 'theme.liquid' 35 | 36 | theme_path = File.dirname(test) + '/theme.liquid' 37 | 38 | [File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test] 39 | end.compact 40 | end 41 | 42 | def compile 43 | # Dup assigns because will make some changes to them 44 | 45 | @tests.each do |liquid, layout, template_name| 46 | 47 | tmpl = Liquid::Template.new 48 | tmpl.parse(liquid) 49 | tmpl = Liquid::Template.new 50 | tmpl.parse(layout) 51 | end 52 | end 53 | 54 | def run 55 | # Dup assigns because will make some changes to them 56 | assigns = Database.tables.dup 57 | 58 | @tests.each do |liquid, layout, template_name| 59 | 60 | # Compute page_tempalte outside of profiler run, uninteresting to profiler 61 | page_template = File.basename(template_name, File.extname(template_name)) 62 | compile_and_render(liquid, layout, assigns, page_template, template_name) 63 | 64 | end 65 | end 66 | 67 | 68 | def compile_and_render(template, layout, assigns, page_template, template_file) 69 | tmpl = Liquid::Template.new 70 | tmpl.assigns['page_title'] = 'Page title' 71 | tmpl.assigns['template'] = page_template 72 | tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file)) 73 | 74 | content_for_layout = tmpl.parse(template).render!(assigns) 75 | 76 | if layout 77 | assigns['content_for_layout'] = content_for_layout 78 | tmpl.parse(layout).render!(assigns) 79 | else 80 | content_for_layout 81 | end 82 | end 83 | end 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /performance/tests/dropify/article.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{ article.title }}

3 |

posted {{ article.created_at | date: "%Y %h" }} by

4 | 5 |
6 | {{ article.content }} 7 |
8 | 9 |
10 | 11 | 12 | {% if blog.comments_enabled? %} 13 |
14 |

Comments

15 | 16 | 17 |
    18 | {% for comment in article.comments %} 19 |
  • 20 |
    21 | {{ comment.author }} said on {{ comment.created_at | date: "%B %d, %Y" }}: 22 |
    23 | 24 |
    25 | {{ comment.content }} 26 |
    27 |
  • 28 | {% endfor %} 29 |
30 | 31 | 32 |
33 | {% form article %} 34 |

Leave a comment

35 | 36 | 37 | {% if form.posted_successfully? %} 38 | {% if blog.moderated? %} 39 |
40 | Successfully posted your comment.
41 | It will have to be approved by the blog owner first before showing up. 42 |
43 | {% else %} 44 |
Successfully posted your comment.
45 | {% endif %} 46 | {% endif %} 47 | 48 | {% if form.errors %} 49 |
Not all the fields have been filled out correctly!
50 | {% endif %} 51 | 52 |
53 |
54 |
55 | 56 |
57 |
58 | 59 |
60 |
61 |
62 | 63 | {% if blog.moderated? %} 64 |

comments have to be approved before showing up

65 | {% endif %} 66 | 67 | 68 | {% endform %} 69 |
70 | 71 | 72 |
73 | {% endif %} 74 | 75 | -------------------------------------------------------------------------------- /lib/liquid/file_system.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | # A Liquid file system is a way to let your templates retrieve other templates for use with the include tag. 3 | # 4 | # You can implement subclasses that retrieve templates from the database, from the file system using a different 5 | # path structure, you can provide them as hard-coded inline strings, or any manner that you see fit. 6 | # 7 | # You can add additional instance variables, arguments, or methods as needed. 8 | # 9 | # Example: 10 | # 11 | # Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path) 12 | # liquid = Liquid::Template.parse(template) 13 | # 14 | # This will parse the template with a LocalFileSystem implementation rooted at 'template_path'. 15 | class BlankFileSystem 16 | # Called by Liquid to retrieve a template file 17 | def read_template_file(template_path, context) 18 | raise FileSystemError, "This liquid context does not allow includes." 19 | end 20 | end 21 | 22 | # This implements an abstract file system which retrieves template files named in a manner similar to Rails partials, 23 | # ie. with the template name prefixed with an underscore. The extension ".liquid" is also added. 24 | # 25 | # For security reasons, template paths are only allowed to contain letters, numbers, and underscore. 26 | # 27 | # Example: 28 | # 29 | # file_system = Liquid::LocalFileSystem.new("/some/path") 30 | # 31 | # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid" 32 | # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid" 33 | # 34 | # Optionally in the second argument you can specify a custom pattern for template filenames. 35 | # The Kernel::sprintf format specification is used. 36 | # Default pattern is "_%s.liquid". 37 | # 38 | # Example: 39 | # 40 | # file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html") 41 | # 42 | # file_system.full_path("index") # => "/some/path/index.html" 43 | # 44 | class LocalFileSystem 45 | attr_accessor :root 46 | 47 | def initialize(root, pattern = "_%s.liquid".freeze) 48 | @root = root 49 | @pattern = pattern 50 | end 51 | 52 | def read_template_file(template_path, context) 53 | full_path = full_path(template_path) 54 | raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path) 55 | 56 | File.read(full_path) 57 | end 58 | 59 | def full_path(template_path) 60 | raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/ 61 | 62 | full_path = if template_path.include?('/'.freeze) 63 | File.join(root, File.dirname(template_path), @pattern % File.basename(template_path)) 64 | else 65 | File.join(root, @pattern % template_path) 66 | end 67 | 68 | raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/ 69 | 70 | full_path 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/liquid/tags/include.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | 3 | # Include allows templates to relate with other templates 4 | # 5 | # Simply include another template: 6 | # 7 | # {% include 'product' %} 8 | # 9 | # Include a template with a local variable: 10 | # 11 | # {% include 'product' with products[0] %} 12 | # 13 | # Include a template for a collection: 14 | # 15 | # {% include 'product' for products %} 16 | # 17 | class Include < Tag 18 | Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o 19 | 20 | def initialize(tag_name, markup, options) 21 | super 22 | 23 | if markup =~ Syntax 24 | 25 | @template_name = $1 26 | @variable_name = $3 27 | @attributes = {} 28 | 29 | markup.scan(TagAttributes) do |key, value| 30 | @attributes[key] = value 31 | end 32 | 33 | else 34 | raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze)) 35 | end 36 | end 37 | 38 | def parse(tokens) 39 | end 40 | 41 | def blank? 42 | false 43 | end 44 | 45 | def render(context) 46 | partial = load_cached_partial(context) 47 | variable = context[@variable_name || @template_name[1..-2]] 48 | 49 | context.stack do 50 | @attributes.each do |key, value| 51 | context[key] = context[value] 52 | end 53 | 54 | context_variable_name = @template_name[1..-2].split('/'.freeze).last 55 | if variable.is_a?(Array) 56 | variable.collect do |var| 57 | context[context_variable_name] = var 58 | partial.render(context) 59 | end 60 | else 61 | context[context_variable_name] = variable 62 | partial.render(context) 63 | end 64 | end 65 | end 66 | 67 | private 68 | def load_cached_partial(context) 69 | cached_partials = context.registers[:cached_partials] || {} 70 | template_name = context[@template_name] 71 | 72 | if cached = cached_partials[template_name] 73 | return cached 74 | end 75 | source = read_template_from_file_system(context) 76 | partial = Liquid::Template.parse(source) 77 | cached_partials[template_name] = partial 78 | context.registers[:cached_partials] = cached_partials 79 | partial 80 | end 81 | 82 | def read_template_from_file_system(context) 83 | file_system = context.registers[:file_system] || Liquid::Template.file_system 84 | 85 | # make read_template_file call backwards-compatible. 86 | case file_system.method(:read_template_file).arity 87 | when 1 88 | file_system.read_template_file(context[@template_name]) 89 | when 2 90 | file_system.read_template_file(context[@template_name], context) 91 | else 92 | raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)" 93 | end 94 | end 95 | end 96 | 97 | Template.register_tag('include'.freeze, Include) 98 | end 99 | -------------------------------------------------------------------------------- /performance/tests/ripen/product.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{ product.title }}

3 |
4 |
5 | {% for image in product.images %} 6 | {% if forloop.first %} 7 | 8 | {{product.title | escape }} 9 | 10 | {% else %} 11 | 12 | {{product.title | escape }} 13 | 14 | {% endif %} 15 | {% endfor %} 16 |
17 | 18 |
    19 |
  • Vendor: {{ product.vendor | link_to_vendor }}
  • 20 |
  • Type: {{ product.type | link_to_type }}
  • 21 |
22 | 23 | {{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %} 24 | 25 |
26 | {% if product.available %} 27 | 28 |
29 | 30 | 35 | 36 |
37 | 38 |
39 |
40 | {% else %} 41 | Sold Out! 42 | {% endif %} 43 |
44 | 45 |
46 | {{ product.description }} 47 |
48 |
49 |
50 | 51 | 52 | 75 | 76 | -------------------------------------------------------------------------------- /performance/tests/vogue/cart.liquid: -------------------------------------------------------------------------------- 1 |

Shopping Cart

2 | {% if cart.item_count == 0 %} 3 |

Your shopping basket is empty. Perhaps a featured item below is of interest...

4 | 5 | {% tablerow product in collections.frontpage.products cols: 3 limit: 12 %} 6 | 9 | 13 | {% endtablerow %} 14 | 15 | {% else %} 16 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% for item in cart.items %} 31 | 32 | 41 | 42 | 43 | 44 | 45 | {% endfor %} 46 |
Item DescriptionPriceQtyDeleteTotal
33 |
34 | {{ item.title | escape }} 35 |
36 |
37 |

{{ item.title }}

38 | {{ item.product.description | strip_html | truncate: 120 }} 39 |
40 |
{{ item.price | money }}{% if item.variant.compare_at_price > item.price %}
{{ item.variant.compare_at_price | money }}{% endif %}
Remove{{ item.line_price | money }}
47 |
48 |

Subtotal {{ cart.total_price | money }}

49 | 50 | 51 | {% if additional_checkout_buttons %} 52 |
53 |

- or -

54 | {{ content_for_additional_checkout_buttons }} 55 |
56 | {% endif %} 57 |
58 |
{% endif %} 59 | -------------------------------------------------------------------------------- /lib/liquid/tags/if.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | # If is the conditional block 3 | # 4 | # {% if user.admin %} 5 | # Admin user! 6 | # {% else %} 7 | # Not admin user 8 | # {% endif %} 9 | # 10 | # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need. 11 | # 12 | class If < Block 13 | Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o 14 | ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o 15 | BOOLEAN_OPERATORS = %w(and or) 16 | 17 | def initialize(tag_name, markup, options) 18 | super 19 | @blocks = [] 20 | push_block('if'.freeze, markup) 21 | end 22 | 23 | def nodelist 24 | @blocks.map(&:attachment).flatten 25 | end 26 | 27 | def unknown_tag(tag, markup, tokens) 28 | if ['elsif'.freeze, 'else'.freeze].include?(tag) 29 | push_block(tag, markup) 30 | else 31 | super 32 | end 33 | end 34 | 35 | def render(context) 36 | context.stack do 37 | @blocks.each do |block| 38 | if block.evaluate(context) 39 | return render_all(block.attachment, context) 40 | end 41 | end 42 | ''.freeze 43 | end 44 | end 45 | 46 | private 47 | 48 | def push_block(tag, markup) 49 | block = if tag == 'else'.freeze 50 | ElseCondition.new 51 | else 52 | parse_with_selected_parser(markup) 53 | end 54 | 55 | @blocks.push(block) 56 | @nodelist = block.attach(Array.new) 57 | end 58 | 59 | def lax_parse(markup) 60 | expressions = markup.scan(ExpressionsAndOperators).reverse 61 | raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift =~ Syntax 62 | 63 | condition = Condition.new($1, $2, $3) 64 | 65 | while not expressions.empty? 66 | operator = (expressions.shift).to_s.strip 67 | 68 | raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift.to_s =~ Syntax 69 | 70 | new_condition = Condition.new($1, $2, $3) 71 | raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator) 72 | new_condition.send(operator, condition) 73 | condition = new_condition 74 | end 75 | 76 | condition 77 | end 78 | 79 | def strict_parse(markup) 80 | p = Parser.new(markup) 81 | 82 | condition = parse_comparison(p) 83 | 84 | while op = (p.id?('and'.freeze) || p.id?('or'.freeze)) 85 | new_cond = parse_comparison(p) 86 | new_cond.send(op, condition) 87 | condition = new_cond 88 | end 89 | p.consume(:end_of_string) 90 | 91 | condition 92 | end 93 | 94 | def parse_comparison(p) 95 | a = p.expression 96 | if op = p.consume?(:comparison) 97 | b = p.expression 98 | Condition.new(a, op, b) 99 | else 100 | Condition.new(a) 101 | end 102 | end 103 | end 104 | 105 | Template.register_tag('if'.freeze, If) 106 | end 107 | -------------------------------------------------------------------------------- /performance/tests/tribble/theme.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{shop.name}} - {{page_title}} 5 | 6 | 7 | {{ 'reset.css' | asset_url | stylesheet_tag }} 8 | {{ 'style.css' | asset_url | stylesheet_tag }} 9 | 10 | {{ 'lightbox.css' | asset_url | stylesheet_tag }} 11 | {{ 'prototype/1.6/prototype.js' | global_asset_url | script_tag }} 12 | {{ 'scriptaculous/1.8.2/scriptaculous.js' | global_asset_url | script_tag }} 13 | {{ 'lightbox.js' | asset_url | script_tag }} 14 | {{ 'option_selection.js' | shopify_asset_url | script_tag }} 15 | 16 | {{ content_for_header }} 17 | 18 | 19 | 20 |
21 | 22 |
23 |
24 |

Shopping Cart

25 |

26 | {% if cart.item_count == 0 %} 27 | Your cart is currently empty 28 | {% else %} 29 | {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }} - Total: {{cart.total_price | money_with_currency }} - View Cart 30 | {% endif %} 31 |

32 |
33 | 34 |
35 |

{{shop.name}}

36 |

Tribble: A Shopify Theme

37 | 38 |
39 |
40 | 41 | 46 | 47 | {{ content_for_layout }} 48 | 49 | 86 | 87 |
88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /performance/shopify/shop_filter.rb: -------------------------------------------------------------------------------- 1 | module ShopFilter 2 | 3 | def asset_url(input) 4 | "/files/1/[shop_id]/[shop_id]/assets/#{input}" 5 | end 6 | 7 | def global_asset_url(input) 8 | "/global/#{input}" 9 | end 10 | 11 | def shopify_asset_url(input) 12 | "/shopify/#{input}" 13 | end 14 | 15 | def script_tag(url) 16 | %() 17 | end 18 | 19 | def stylesheet_tag(url, media="all") 20 | %() 21 | end 22 | 23 | def link_to(link, url, title="") 24 | %|#{link}| 25 | end 26 | 27 | def img_tag(url, alt="") 28 | %|#{alt}| 29 | end 30 | 31 | def link_to_vendor(vendor) 32 | if vendor 33 | link_to vendor, url_for_vendor(vendor), vendor 34 | else 35 | 'Unknown Vendor' 36 | end 37 | end 38 | 39 | def link_to_type(type) 40 | if type 41 | link_to type, url_for_type(type), type 42 | else 43 | 'Unknown Vendor' 44 | end 45 | end 46 | 47 | def url_for_vendor(vendor_title) 48 | "/collections/#{to_handle(vendor_title)}" 49 | end 50 | 51 | def url_for_type(type_title) 52 | "/collections/#{to_handle(type_title)}" 53 | end 54 | 55 | def product_img_url(url, style = 'small') 56 | 57 | unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/ 58 | raise ArgumentError, 'filter "size" can only be called on product images' 59 | end 60 | 61 | case style 62 | when 'original' 63 | return '/files/shops/random_number/' + url 64 | when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon' 65 | "/files/shops/random_number/products/#{$1}_#{style}.#{$2}" 66 | else 67 | raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon ' 68 | end 69 | end 70 | 71 | def default_pagination(paginate) 72 | 73 | html = [] 74 | html << %(#{link_to(paginate['previous']['title'], paginate['previous']['url'])}) if paginate['previous'] 75 | 76 | for part in paginate['parts'] 77 | 78 | if part['is_link'] 79 | html << %(#{link_to(part['title'], part['url'])}) 80 | elsif part['title'].to_i == paginate['current_page'].to_i 81 | html << %(#{part['title']}) 82 | else 83 | html << %(#{part['title']}) 84 | end 85 | 86 | end 87 | 88 | html << %(#{link_to(paginate['next']['title'], paginate['next']['url'])}) if paginate['next'] 89 | html.join(' ') 90 | end 91 | 92 | # Accepts a number, and two words - one for singular, one for plural 93 | # Returns the singular word if input equals 1, otherwise plural 94 | def pluralize(input, singular, plural) 95 | input == 1 ? singular : plural 96 | end 97 | 98 | private 99 | 100 | def to_handle(str) 101 | result = str.dup 102 | result.downcase! 103 | result.delete!("'\"()[]") 104 | result.gsub!(/\W+/, '-') 105 | result.gsub!(/-+\z/, '') if result[-1] == '-' 106 | result.gsub!(/\A-+/, '') if result[0] == '-' 107 | result 108 | end 109 | 110 | end 111 | -------------------------------------------------------------------------------- /performance/tests/tribble/index.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Three Great Reasons You Should Shop With Us...

4 |
    5 |
  • 6 |

    Free Shipping

    7 |

    On all orders over $25

    8 |
  • 9 |
  • 10 |

    Top Quality

    11 |

    Hand made in our shop

    12 |
  • 13 |
  • 14 |

    100% Guarantee

    15 |

    Any time, any reason

    16 |
  • 17 |
18 |
19 |
20 | 21 |
22 | 23 |
{{pages.alert.content}}
24 | 25 |
    26 | 27 | {% for product in collections.frontpage.products %} 28 |
  • 29 |
    30 |
    31 |
    32 |
    33 |

    {{product.title}}

    34 | {{ product.description | truncatewords: 15 }}

    35 |
    36 | {{ product.title | escape }} 37 |
    38 | 39 |
    40 | 41 | 42 |

    43 | View Details 44 | 45 | 46 | {% if product.compare_at_price %} 47 | {% if product.price_min != product.compare_at_price %} 48 | {{product.compare_at_price | money}} - 49 | {% endif %} 50 | {% endif %} 51 | 52 | {{product.price_min | money}} 53 | 54 | 55 |

    56 |
    57 |
    58 |
    59 |
  • 60 | {% endfor %} 61 | 62 |
63 | 64 |
65 |
66 |

Why Shop With Us?

67 |
    68 |
  • 69 |

    24 Hours

    70 |

    We're always here to help.

    71 |
  • 72 |
  • 73 |

    No Spam

    74 |

    We'll never share your info.

    75 |
  • 76 |
  • 77 |

    Save Energy

    78 |

    We're green, all the way.

    79 |
  • 80 |
  • 81 |

    Secure Servers

    82 |

    Checkout is 256bits encrypted.

    83 |
  • 84 |
85 |
86 | 87 |
88 |

Our Company

89 | {{pages.about-us.content | truncatewords: 49}} read more

90 |
91 |
92 | 93 |
94 | 95 | -------------------------------------------------------------------------------- /lib/liquid/condition.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | # Container for liquid nodes which conveniently wraps decision making logic 3 | # 4 | # Example: 5 | # 6 | # c = Condition.new('1', '==', '1') 7 | # c.evaluate #=> true 8 | # 9 | class Condition #:nodoc: 10 | @@operators = { 11 | '=='.freeze => lambda { |cond, left, right| cond.send(:equal_variables, left, right) }, 12 | '!='.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) }, 13 | '<>'.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) }, 14 | '<'.freeze => :<, 15 | '>'.freeze => :>, 16 | '>='.freeze => :>=, 17 | '<='.freeze => :<=, 18 | 'contains'.freeze => lambda { |cond, left, right| left && right ? left.include?(right) : false } 19 | } 20 | 21 | def self.operators 22 | @@operators 23 | end 24 | 25 | attr_reader :attachment 26 | attr_accessor :left, :operator, :right 27 | 28 | def initialize(left = nil, operator = nil, right = nil) 29 | @left, @operator, @right = left, operator, right 30 | @child_relation = nil 31 | @child_condition = nil 32 | end 33 | 34 | def evaluate(context = Context.new) 35 | result = interpret_condition(left, right, operator, context) 36 | 37 | case @child_relation 38 | when :or 39 | result || @child_condition.evaluate(context) 40 | when :and 41 | result && @child_condition.evaluate(context) 42 | else 43 | result 44 | end 45 | end 46 | 47 | def or(condition) 48 | @child_relation, @child_condition = :or, condition 49 | end 50 | 51 | def and(condition) 52 | @child_relation, @child_condition = :and, condition 53 | end 54 | 55 | def attach(attachment) 56 | @attachment = attachment 57 | end 58 | 59 | def else? 60 | false 61 | end 62 | 63 | def inspect 64 | "#" 65 | end 66 | 67 | private 68 | 69 | def equal_variables(left, right) 70 | if left.is_a?(Symbol) 71 | if right.respond_to?(left) 72 | return right.send(left.to_s) 73 | else 74 | return nil 75 | end 76 | end 77 | 78 | if right.is_a?(Symbol) 79 | if left.respond_to?(right) 80 | return left.send(right.to_s) 81 | else 82 | return nil 83 | end 84 | end 85 | 86 | left == right 87 | end 88 | 89 | def interpret_condition(left, right, op, context) 90 | # If the operator is empty this means that the decision statement is just 91 | # a single variable. We can just poll this variable from the context and 92 | # return this as the result. 93 | return context[left] if op == nil 94 | 95 | left, right = context[left], context[right] 96 | 97 | operation = self.class.operators[op] || raise(ArgumentError.new("Unknown operator #{op}")) 98 | 99 | if operation.respond_to?(:call) 100 | operation.call(self, left, right) 101 | elsif left.respond_to?(operation) and right.respond_to?(operation) 102 | left.send(operation, right) 103 | else 104 | nil 105 | end 106 | end 107 | end 108 | 109 | 110 | class ElseCondition < Condition 111 | def else? 112 | true 113 | end 114 | 115 | def evaluate(context) 116 | true 117 | end 118 | end 119 | 120 | end 121 | -------------------------------------------------------------------------------- /test/integration/output_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module FunnyFilter 4 | def make_funny(input) 5 | 'LOL' 6 | end 7 | 8 | def cite_funny(input) 9 | "LOL: #{input}" 10 | end 11 | 12 | def add_smiley(input, smiley = ":-)") 13 | "#{input} #{smiley}" 14 | end 15 | 16 | def add_tag(input, tag = "p", id = "foo") 17 | %|<#{tag} id="#{id}">#{input}| 18 | end 19 | 20 | def paragraph(input) 21 | "

#{input}

" 22 | end 23 | 24 | def link_to(name, url) 25 | %|#{name}| 26 | end 27 | 28 | end 29 | 30 | class OutputTest < Test::Unit::TestCase 31 | include Liquid 32 | 33 | def setup 34 | @assigns = { 35 | 'best_cars' => 'bmw', 36 | 'car' => {'bmw' => 'good', 'gm' => 'bad'} 37 | } 38 | end 39 | 40 | def test_variable 41 | text = %| {{best_cars}} | 42 | 43 | expected = %| bmw | 44 | assert_equal expected, Template.parse(text).render!(@assigns) 45 | end 46 | 47 | def test_variable_traversing 48 | text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} | 49 | 50 | expected = %| good bad good | 51 | assert_equal expected, Template.parse(text).render!(@assigns) 52 | end 53 | 54 | def test_variable_piping 55 | text = %( {{ car.gm | make_funny }} ) 56 | expected = %| LOL | 57 | 58 | assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) 59 | end 60 | 61 | def test_variable_piping_with_input 62 | text = %( {{ car.gm | cite_funny }} ) 63 | expected = %| LOL: bad | 64 | 65 | assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) 66 | end 67 | 68 | def test_variable_piping_with_args 69 | text = %! {{ car.gm | add_smiley : ':-(' }} ! 70 | expected = %| bad :-( | 71 | 72 | assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) 73 | end 74 | 75 | def test_variable_piping_with_no_args 76 | text = %! {{ car.gm | add_smiley }} ! 77 | expected = %| bad :-) | 78 | 79 | assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) 80 | end 81 | 82 | def test_multiple_variable_piping_with_args 83 | text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} ! 84 | expected = %| bad :-( :-( | 85 | 86 | assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) 87 | end 88 | 89 | def test_variable_piping_with_multiple_args 90 | text = %! {{ car.gm | add_tag : 'span', 'bar'}} ! 91 | expected = %| bad | 92 | 93 | assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) 94 | end 95 | 96 | def test_variable_piping_with_variable_args 97 | text = %! {{ car.gm | add_tag : 'span', car.bmw}} ! 98 | expected = %| bad | 99 | 100 | assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) 101 | end 102 | 103 | def test_multiple_pipings 104 | text = %( {{ best_cars | cite_funny | paragraph }} ) 105 | expected = %|

LOL: bmw

| 106 | 107 | assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) 108 | end 109 | 110 | def test_link_to 111 | text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} ) 112 | expected = %| Typo | 113 | 114 | assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) 115 | end 116 | end # OutputTest 117 | -------------------------------------------------------------------------------- /lib/liquid/variable.rb: -------------------------------------------------------------------------------- 1 | module Liquid 2 | 3 | # Holds variables. Variables are only loaded "just in time" 4 | # and are not evaluated as part of the render stage 5 | # 6 | # {{ monkey }} 7 | # {{ user.name }} 8 | # 9 | # Variables can be combined with filters: 10 | # 11 | # {{ user | link }} 12 | # 13 | class Variable 14 | FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o 15 | EasyParse = /\A *(\w+(?:\.\w+)*) *\z/ 16 | attr_accessor :filters, :name, :warnings 17 | 18 | def initialize(markup, options = {}) 19 | @markup = markup 20 | @name = nil 21 | @options = options || {} 22 | 23 | case @options[:error_mode] || Template.error_mode 24 | when :strict then strict_parse(markup) 25 | when :lax then lax_parse(markup) 26 | when :warn 27 | begin 28 | strict_parse(markup) 29 | rescue SyntaxError => e 30 | @warnings ||= [] 31 | @warnings << e 32 | lax_parse(markup) 33 | end 34 | end 35 | end 36 | 37 | def lax_parse(markup) 38 | @filters = [] 39 | if match = markup.match(/\s*(#{QuotedFragment})(.*)/om) 40 | @name = match[1] 41 | if match[2].match(/#{FilterSeparator}\s*(.*)/om) 42 | filters = Regexp.last_match(1).scan(FilterParser) 43 | filters.each do |f| 44 | if matches = f.match(/\s*(\w+)/) 45 | filtername = matches[1] 46 | filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten 47 | @filters << [filtername, filterargs] 48 | end 49 | end 50 | end 51 | end 52 | end 53 | 54 | def strict_parse(markup) 55 | # Very simple valid cases 56 | if markup =~ EasyParse 57 | @name = $1 58 | @filters = [] 59 | return 60 | end 61 | 62 | @filters = [] 63 | p = Parser.new(markup) 64 | # Could be just filters with no input 65 | @name = p.look(:pipe) ? ''.freeze : p.expression 66 | while p.consume?(:pipe) 67 | filtername = p.consume(:id) 68 | filterargs = p.consume?(:colon) ? parse_filterargs(p) : [] 69 | @filters << [filtername, filterargs] 70 | end 71 | p.consume(:end_of_string) 72 | rescue SyntaxError => e 73 | e.message << " in \"{{#{markup}}}\"" 74 | raise e 75 | end 76 | 77 | def parse_filterargs(p) 78 | # first argument 79 | filterargs = [p.argument] 80 | # followed by comma separated others 81 | while p.consume?(:comma) 82 | filterargs << p.argument 83 | end 84 | filterargs 85 | end 86 | 87 | def render(context) 88 | return ''.freeze if @name.nil? 89 | @filters.inject(context[@name]) do |output, filter| 90 | filterargs = [] 91 | keyword_args = {} 92 | filter[1].to_a.each do |a| 93 | if matches = a.match(/\A#{TagAttributes}\z/o) 94 | keyword_args[matches[1]] = context[matches[2]] 95 | else 96 | filterargs << context[a] 97 | end 98 | end 99 | filterargs << keyword_args unless keyword_args.empty? 100 | begin 101 | output = context.invoke(filter[0], output, *filterargs) 102 | rescue FilterNotFound 103 | raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found." 104 | end 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /performance/tests/tribble/article.liquid: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 |
6 |

{{article.title}}

7 |
8 |
{{ article.created_at | date: "%b %d" }}
9 | {{ article.content }} 10 |
11 | 12 | 13 | {% if blog.comments_enabled? %} 14 |
15 |

Comments

16 | 17 | 18 |
    19 | {% for comment in article.comments %} 20 |
  • 21 |
    22 | {{ comment.content }} 23 |
    24 | 25 |
    26 | Posted by {{ comment.author }} on {{ comment.created_at | date: "%B %d, %Y" }} 27 |
    28 |
  • 29 | {% endfor %} 30 |
31 | 32 | 33 |
34 | {% form article %} 35 |

Leave a comment

36 | 37 | 38 | {% if form.posted_successfully? %} 39 | {% if blog.moderated? %} 40 |
41 | Successfully posted your comment.
42 | It will have to be approved by the blog owner first before showing up. 43 |
44 | {% else %} 45 |
Successfully posted your comment.
46 | {% endif %} 47 | {% endif %} 48 | 49 | {% if form.errors %} 50 |
Not all the fields have been filled out correctly!
51 | {% endif %} 52 | 53 |
54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 |
63 | 64 | {% if blog.moderated? %} 65 |

comments have to be approved before showing up

66 | {% endif %} 67 | 68 | 69 | {% endform %} 70 |
71 | 72 | 73 |
74 | {% endif %} 75 | 76 | 77 | 78 |
79 |
80 | 81 |
82 |

Why Shop With Us?

83 |
    84 |
  • 85 |

    24 Hours

    86 |

    We're always here to help.

    87 |
  • 88 |
  • 89 |

    No Spam

    90 |

    We'll never share your info.

    91 |
  • 92 |
  • 93 |

    Secure Servers

    94 |

    Checkout is 256bit encrypted.

    95 |
  • 96 |
97 |
98 |
99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/Shopify/liquid.png?branch=master)](http://travis-ci.org/Shopify/liquid) 2 | [![Inline docs](http://inch-pages.github.io/github/Shopify/liquid.png)](http://inch-pages.github.io/github/Shopify/liquid) 3 | 4 | # Liquid template engine 5 | 6 | * [Contributing guidelines](CONTRIBUTING.md) 7 | * [Version history](History.md) 8 | * [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics) 9 | * [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki) 10 | * [Website](http://liquidmarkup.org/) 11 | 12 | ## Introduction 13 | 14 | Liquid is a template engine which was written with very specific requirements: 15 | 16 | * It has to have beautiful and simple markup. Template engines which don't produce good looking markup are no fun to use. 17 | * 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. 18 | * It has to be stateless. Compile and render steps have to be separate 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. 19 | 20 | ## Why you should use Liquid 21 | 22 | * 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**. 23 | * You want to render templates directly from the database. 24 | * You like smarty (PHP) style template engines. 25 | * You need a template engine which does HTML just as well as emails. 26 | * You don't like the markup of your current templating engine. 27 | 28 | ## What does it look like? 29 | 30 | ```html 31 |
    32 | {% for product in products %} 33 |
  • 34 |

    {{ product.name }}

    35 | Only {{ product.price | price }} 36 | 37 | {{ product.description | prettyprint | paragraph }} 38 |
  • 39 | {% endfor %} 40 |
41 | ``` 42 | 43 | ## How to use Liquid 44 | 45 | Liquid supports a very simple API based around the Liquid::Template class. 46 | For standard use you can just pass it the content of a file and call render with a parameters hash. 47 | 48 | ```ruby 49 | @template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template 50 | @template.render('name' => 'tobi') # => "hi tobi" 51 | ``` 52 | 53 | ### Error Modes 54 | 55 | Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted. 56 | Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make 57 | it very hard to debug and can lead to unexpected behaviour. 58 | 59 | Liquid also comes with a stricter parser that can be used when editing templates to give better error messages 60 | when templates are invalid. You can enable this new parser like this: 61 | 62 | ```ruby 63 | Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used 64 | Liquid::Template.error_mode = :warn # Adds errors to template.errors but continues as normal 65 | Liquid::Template.error_mode = :lax # The default mode, accepts almost anything. 66 | ``` 67 | 68 | If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`: 69 | ```ruby 70 | Liquid::Template.parse(source, :error_mode => :strict) 71 | ``` 72 | This is useful for doing things like enabling strict mode only in the theme editor. 73 | 74 | It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created. 75 | It is also recommended that you use it in the template editors of existing apps to give editors better error messages. 76 | -------------------------------------------------------------------------------- /test/integration/tags/table_row_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TableRowTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | class ArrayDrop < Liquid::Drop 7 | include Enumerable 8 | 9 | def initialize(array) 10 | @array = array 11 | end 12 | 13 | def each(&block) 14 | @array.each(&block) 15 | end 16 | end 17 | 18 | def test_table_row 19 | 20 | assert_template_result("\n 1 2 3 \n 4 5 6 \n", 21 | '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', 22 | 'numbers' => [1,2,3,4,5,6]) 23 | 24 | assert_template_result("\n\n", 25 | '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', 26 | 'numbers' => []) 27 | end 28 | 29 | def test_table_row_with_different_cols 30 | assert_template_result("\n 1 2 3 4 5 \n 6 \n", 31 | '{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}', 32 | 'numbers' => [1,2,3,4,5,6]) 33 | 34 | end 35 | 36 | def test_table_col_counter 37 | assert_template_result("\n12\n12\n12\n", 38 | '{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}', 39 | 'numbers' => [1,2,3,4,5,6]) 40 | end 41 | 42 | def test_quoted_fragment 43 | assert_template_result("\n 1 2 3 \n 4 5 6 \n", 44 | "{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}", 45 | 'collections' => {'frontpage' => [1,2,3,4,5,6]}) 46 | assert_template_result("\n 1 2 3 \n 4 5 6 \n", 47 | "{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}", 48 | 'collections' => {'frontpage' => [1,2,3,4,5,6]}) 49 | 50 | end 51 | 52 | def test_enumerable_drop 53 | assert_template_result("\n 1 2 3 \n 4 5 6 \n", 54 | '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', 55 | 'numbers' => ArrayDrop.new([1,2,3,4,5,6])) 56 | end 57 | 58 | def test_offset_and_limit 59 | assert_template_result("\n 1 2 3 \n 4 5 6 \n", 60 | '{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}', 61 | 'numbers' => [0,1,2,3,4,5,6,7]) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/integration/blank_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class FoobarTag < Liquid::Tag 4 | def render(*args) 5 | " " 6 | end 7 | 8 | Liquid::Template.register_tag('foobar', FoobarTag) 9 | end 10 | 11 | class BlankTestFileSystem 12 | def read_template_file(template_path, context) 13 | template_path 14 | end 15 | end 16 | 17 | class BlankTest < Test::Unit::TestCase 18 | include Liquid 19 | N = 10 20 | 21 | def wrap_in_for(body) 22 | "{% for i in (1..#{N}) %}#{body}{% endfor %}" 23 | end 24 | 25 | def wrap_in_if(body) 26 | "{% if true %}#{body}{% endif %}" 27 | end 28 | 29 | def wrap(body) 30 | wrap_in_for(body) + wrap_in_if(body) 31 | end 32 | 33 | def test_new_tags_are_not_blank_by_default 34 | assert_template_result(" "*N, wrap_in_for("{% foobar %}")) 35 | end 36 | 37 | def test_loops_are_blank 38 | assert_template_result("", wrap_in_for(" ")) 39 | end 40 | 41 | def test_if_else_are_blank 42 | assert_template_result("", "{% if true %} {% elsif false %} {% else %} {% endif %}") 43 | end 44 | 45 | def test_unless_is_blank 46 | assert_template_result("", wrap("{% unless true %} {% endunless %}")) 47 | end 48 | 49 | def test_mark_as_blank_only_during_parsing 50 | assert_template_result(" "*(N+1), wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}")) 51 | end 52 | 53 | def test_comments_are_blank 54 | assert_template_result("", wrap(" {% comment %} whatever {% endcomment %} ")) 55 | end 56 | 57 | def test_captures_are_blank 58 | assert_template_result("", wrap(" {% capture foo %} whatever {% endcapture %} ")) 59 | end 60 | 61 | def test_nested_blocks_are_blank_but_only_if_all_children_are 62 | assert_template_result("", wrap(wrap(" "))) 63 | assert_template_result("\n but this is not "*(N+1), 64 | wrap(%q{{% if true %} {% comment %} this is blank {% endcomment %} {% endif %} 65 | {% if true %} but this is not {% endif %}})) 66 | end 67 | 68 | def test_assigns_are_blank 69 | assert_template_result("", wrap(' {% assign foo = "bar" %} ')) 70 | end 71 | 72 | def test_whitespace_is_blank 73 | assert_template_result("", wrap(" ")) 74 | assert_template_result("", wrap("\t")) 75 | end 76 | 77 | def test_whitespace_is_not_blank_if_other_stuff_is_present 78 | body = " x " 79 | assert_template_result(body*(N+1), wrap(body)) 80 | end 81 | 82 | def test_increment_is_not_blank 83 | assert_template_result(" 0"*2*(N+1), wrap("{% assign foo = 0 %} {% increment foo %} {% decrement foo %}")) 84 | end 85 | 86 | def test_cycle_is_not_blank 87 | assert_template_result(" "*((N+1)/2)+" ", wrap("{% cycle ' ', ' ' %}")) 88 | end 89 | 90 | def test_raw_is_not_blank 91 | assert_template_result(" "*(N+1), wrap(" {% raw %} {% endraw %}")) 92 | end 93 | 94 | def test_include_is_blank 95 | Liquid::Template.file_system = BlankTestFileSystem.new 96 | assert_template_result "foobar"*(N+1), wrap("{% include 'foobar' %}") 97 | assert_template_result " foobar "*(N+1), wrap("{% include ' foobar ' %}") 98 | assert_template_result " "*(N+1), wrap(" {% include ' ' %} ") 99 | end 100 | 101 | def test_case_is_blank 102 | assert_template_result("", wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} ")) 103 | assert_template_result("", wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} ")) 104 | assert_template_result(" x "*(N+1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} ")) 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /test/integration/error_handling_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ErrorDrop < Liquid::Drop 4 | def standard_error 5 | raise Liquid::StandardError, 'standard error' 6 | end 7 | 8 | def argument_error 9 | raise Liquid::ArgumentError, 'argument error' 10 | end 11 | 12 | def syntax_error 13 | raise Liquid::SyntaxError, 'syntax error' 14 | end 15 | 16 | def exception 17 | raise Exception, 'exception' 18 | end 19 | 20 | end 21 | 22 | class ErrorHandlingTest < Test::Unit::TestCase 23 | include Liquid 24 | 25 | def test_standard_error 26 | assert_nothing_raised do 27 | template = Liquid::Template.parse( ' {{ errors.standard_error }} ' ) 28 | assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new) 29 | 30 | assert_equal 1, template.errors.size 31 | assert_equal StandardError, template.errors.first.class 32 | end 33 | end 34 | 35 | def test_syntax 36 | 37 | assert_nothing_raised do 38 | 39 | template = Liquid::Template.parse( ' {{ errors.syntax_error }} ' ) 40 | assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new) 41 | 42 | assert_equal 1, template.errors.size 43 | assert_equal SyntaxError, template.errors.first.class 44 | 45 | end 46 | end 47 | 48 | def test_argument 49 | assert_nothing_raised do 50 | 51 | template = Liquid::Template.parse( ' {{ errors.argument_error }} ' ) 52 | assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new) 53 | 54 | assert_equal 1, template.errors.size 55 | assert_equal ArgumentError, template.errors.first.class 56 | end 57 | end 58 | 59 | def test_missing_endtag_parse_time_error 60 | assert_raise(Liquid::SyntaxError) do 61 | Liquid::Template.parse(' {% for a in b %} ... ') 62 | end 63 | end 64 | 65 | def test_unrecognized_operator 66 | with_error_mode(:strict) do 67 | assert_raise(SyntaxError) do 68 | Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ') 69 | end 70 | end 71 | end 72 | 73 | def test_lax_unrecognized_operator 74 | assert_nothing_raised do 75 | template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :lax) 76 | assert_equal ' Liquid error: Unknown operator =! ', template.render 77 | assert_equal 1, template.errors.size 78 | assert_equal Liquid::ArgumentError, template.errors.first.class 79 | end 80 | end 81 | 82 | def test_strict_error_messages 83 | err = assert_raise(SyntaxError) do 84 | Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict) 85 | end 86 | assert_equal 'Unexpected character = in "1 =! 2"', err.message 87 | 88 | err = assert_raise(SyntaxError) do 89 | Liquid::Template.parse('{{%%%}}', :error_mode => :strict) 90 | end 91 | assert_equal 'Unexpected character % in "{{%%%}}"', err.message 92 | end 93 | 94 | def test_warnings 95 | template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', :error_mode => :warn) 96 | assert_equal 3, template.warnings.size 97 | assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].message 98 | assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].message 99 | assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message 100 | assert_equal '', template.render 101 | end 102 | 103 | # Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError 104 | def test_exceptions_propagate 105 | assert_raise Exception do 106 | template = Liquid::Template.parse( ' {{ errors.exception }} ' ) 107 | template.render('errors' => ErrorDrop.new) 108 | end 109 | end 110 | end # ErrorHandlingTest 111 | -------------------------------------------------------------------------------- /test/integration/tags/statements_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StatementsTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_true_eql_true 7 | text = ' {% if true == true %} true {% else %} false {% endif %} ' 8 | assert_template_result ' true ', text 9 | end 10 | 11 | def test_true_not_eql_true 12 | text = ' {% if true != true %} true {% else %} false {% endif %} ' 13 | assert_template_result ' false ', text 14 | end 15 | 16 | def test_true_lq_true 17 | text = ' {% if 0 > 0 %} true {% else %} false {% endif %} ' 18 | assert_template_result ' false ', text 19 | end 20 | 21 | def test_one_lq_zero 22 | text = ' {% if 1 > 0 %} true {% else %} false {% endif %} ' 23 | assert_template_result ' true ', text 24 | end 25 | 26 | def test_zero_lq_one 27 | text = ' {% if 0 < 1 %} true {% else %} false {% endif %} ' 28 | assert_template_result ' true ', text 29 | end 30 | 31 | def test_zero_lq_or_equal_one 32 | text = ' {% if 0 <= 0 %} true {% else %} false {% endif %} ' 33 | assert_template_result ' true ', text 34 | end 35 | 36 | def test_zero_lq_or_equal_one_involving_nil 37 | text = ' {% if null <= 0 %} true {% else %} false {% endif %} ' 38 | assert_template_result ' false ', text 39 | 40 | 41 | text = ' {% if 0 <= null %} true {% else %} false {% endif %} ' 42 | assert_template_result ' false ', text 43 | end 44 | 45 | def test_zero_lqq_or_equal_one 46 | text = ' {% if 0 >= 0 %} true {% else %} false {% endif %} ' 47 | assert_template_result ' true ', text 48 | end 49 | 50 | def test_strings 51 | text = " {% if 'test' == 'test' %} true {% else %} false {% endif %} " 52 | assert_template_result ' true ', text 53 | end 54 | 55 | def test_strings_not_equal 56 | text = " {% if 'test' != 'test' %} true {% else %} false {% endif %} " 57 | assert_template_result ' false ', text 58 | end 59 | 60 | def test_var_strings_equal 61 | text = ' {% if var == "hello there!" %} true {% else %} false {% endif %} ' 62 | assert_template_result ' true ', text, 'var' => 'hello there!' 63 | end 64 | 65 | def test_var_strings_are_not_equal 66 | text = ' {% if "hello there!" == var %} true {% else %} false {% endif %} ' 67 | assert_template_result ' true ', text, 'var' => 'hello there!' 68 | end 69 | 70 | def test_var_and_long_string_are_equal 71 | text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} " 72 | assert_template_result ' true ', text, 'var' => 'hello there!' 73 | end 74 | 75 | 76 | def test_var_and_long_string_are_equal_backwards 77 | text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} " 78 | assert_template_result ' true ', text, 'var' => 'hello there!' 79 | end 80 | 81 | #def test_is_nil 82 | # text = %| {% if var != nil %} true {% else %} false {% end %} | 83 | # @template.assigns = { 'var' => 'hello there!'} 84 | # expected = %| true | 85 | # assert_equal expected, @template.parse(text) 86 | #end 87 | 88 | def test_is_collection_empty 89 | text = ' {% if array == empty %} true {% else %} false {% endif %} ' 90 | assert_template_result ' true ', text, 'array' => [] 91 | end 92 | 93 | def test_is_not_collection_empty 94 | text = ' {% if array == empty %} true {% else %} false {% endif %} ' 95 | assert_template_result ' false ', text, 'array' => [1,2,3] 96 | end 97 | 98 | def test_nil 99 | text = ' {% if var == nil %} true {% else %} false {% endif %} ' 100 | assert_template_result ' true ', text, 'var' => nil 101 | 102 | text = ' {% if var == null %} true {% else %} false {% endif %} ' 103 | assert_template_result ' true ', text, 'var' => nil 104 | end 105 | 106 | def test_not_nil 107 | text = ' {% if var != nil %} true {% else %} false {% endif %} ' 108 | assert_template_result ' true ', text, 'var' => 1 109 | 110 | text = ' {% if var != null %} true {% else %} false {% endif %} ' 111 | assert_template_result ' true ', text, 'var' => 1 112 | end 113 | end # StatementsTest 114 | -------------------------------------------------------------------------------- /test/integration/filter_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module MoneyFilter 4 | def money(input) 5 | sprintf(' %d$ ', input) 6 | end 7 | 8 | def money_with_underscore(input) 9 | sprintf(' %d$ ', input) 10 | end 11 | end 12 | 13 | module CanadianMoneyFilter 14 | def money(input) 15 | sprintf(' %d$ CAD ', input) 16 | end 17 | end 18 | 19 | module SubstituteFilter 20 | def substitute(input, params={}) 21 | input.gsub(/%\{(\w+)\}/) { |match| params[$1] } 22 | end 23 | end 24 | 25 | class FiltersTest < Test::Unit::TestCase 26 | include Liquid 27 | 28 | def setup 29 | @context = Context.new 30 | end 31 | 32 | def test_local_filter 33 | @context['var'] = 1000 34 | @context.add_filters(MoneyFilter) 35 | 36 | assert_equal ' 1000$ ', Variable.new("var | money").render(@context) 37 | end 38 | 39 | def test_underscore_in_filter_name 40 | @context['var'] = 1000 41 | @context.add_filters(MoneyFilter) 42 | assert_equal ' 1000$ ', Variable.new("var | money_with_underscore").render(@context) 43 | end 44 | 45 | def test_second_filter_overwrites_first 46 | @context['var'] = 1000 47 | @context.add_filters(MoneyFilter) 48 | @context.add_filters(CanadianMoneyFilter) 49 | 50 | assert_equal ' 1000$ CAD ', Variable.new("var | money").render(@context) 51 | end 52 | 53 | def test_size 54 | @context['var'] = 'abcd' 55 | @context.add_filters(MoneyFilter) 56 | 57 | assert_equal 4, Variable.new("var | size").render(@context) 58 | end 59 | 60 | def test_join 61 | @context['var'] = [1,2,3,4] 62 | 63 | assert_equal "1 2 3 4", Variable.new("var | join").render(@context) 64 | end 65 | 66 | def test_sort 67 | @context['value'] = 3 68 | @context['numbers'] = [2,1,4,3] 69 | @context['words'] = ['expected', 'as', 'alphabetic'] 70 | @context['arrays'] = [['flattened'], ['are']] 71 | 72 | assert_equal [1,2,3,4], Variable.new("numbers | sort").render(@context) 73 | assert_equal ['alphabetic', 'as', 'expected'], Variable.new("words | sort").render(@context) 74 | assert_equal [3], Variable.new("value | sort").render(@context) 75 | assert_equal ['are', 'flattened'], Variable.new("arrays | sort").render(@context) 76 | end 77 | 78 | def test_strip_html 79 | @context['var'] = "bla blub" 80 | 81 | assert_equal "bla blub", Variable.new("var | strip_html").render(@context) 82 | end 83 | 84 | def test_strip_html_ignore_comments_with_html 85 | @context['var'] = "bla blub" 86 | 87 | assert_equal "bla blub", Variable.new("var | strip_html").render(@context) 88 | end 89 | 90 | def test_capitalize 91 | @context['var'] = "blub" 92 | 93 | assert_equal "Blub", Variable.new("var | capitalize").render(@context) 94 | end 95 | 96 | def test_nonexistent_filter_is_ignored 97 | @context['var'] = 1000 98 | 99 | assert_equal 1000, Variable.new("var | xyzzy").render(@context) 100 | end 101 | 102 | def test_filter_with_keyword_arguments 103 | @context['surname'] = 'john' 104 | @context.add_filters(SubstituteFilter) 105 | output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context) 106 | assert_equal 'hello john, doe', output 107 | end 108 | end 109 | 110 | class FiltersInTemplate < Test::Unit::TestCase 111 | include Liquid 112 | 113 | def test_local_global 114 | Template.register_filter(MoneyFilter) 115 | 116 | assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil) 117 | assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => CanadianMoneyFilter) 118 | assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => [CanadianMoneyFilter]) 119 | end 120 | 121 | def test_local_filter_with_deprecated_syntax 122 | assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter) 123 | assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter]) 124 | end 125 | end # FiltersTest 126 | -------------------------------------------------------------------------------- /performance/tests/tribble/product.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{ collection.title }} {{ product.title }}

3 | 4 | 5 |

Product Tags: 6 | {% for tag in product.tags %} 7 | {{ tag }} | 8 | {% endfor %} 9 |

10 | 11 |
12 |
13 |

{{ product.title }}

14 |
15 |

{{ product.description }}

16 |
17 | 18 | {% if product.available %} 19 |
20 | 21 |

Product Options:

22 | 23 | 28 | 29 |
30 | 31 |
32 | 33 |
34 | 35 |
36 | {% else %} 37 |

Sold out!

38 |

Sorry, we're all out of this product. Check back often and order when it returns

39 | {% endif %} 40 |
41 | 42 |
43 | {% for image in product.images %} 44 | 45 | {% if forloop.first %} 46 |
47 | {{product.title | escape }} 48 |
49 | {% else %} 50 | {% endif %} 51 | {% endfor %} 52 | 53 |
    54 | {% for image in product.images %} 55 | {% if forloop.first %} 56 | {% else %} 57 | 58 |
  • 59 | 60 | {{product.title | escape }} 61 | 62 |
  • 63 | {% endif %} 64 | {% endfor %} 65 |
66 |
67 |
68 | 69 | 70 | 71 |
72 |

Why Shop With Us?

73 |
    74 |
  • 75 |

    24 Hours

    76 |

    We're always here to help.

    77 |
  • 78 |
  • 79 |

    No Spam

    80 |

    We'll never share your info.

    81 |
  • 82 |
  • 83 |

    Secure Servers

    84 |

    Checkout is 256bit encrypted.

    85 |
  • 86 |
87 |
88 | 89 |
90 | 91 | 92 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /performance/tests/dropify/theme.liquid: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | {{shop.name}} - {{page_title}} 8 | 9 | {{ 'textile.css' | global_asset_url | stylesheet_tag }} 10 | {{ 'lightbox/v204/lightbox.css' | global_asset_url | stylesheet_tag }} 11 | 12 | {{ 'prototype/1.6/prototype.js' | global_asset_url | script_tag }} 13 | {{ 'scriptaculous/1.8.2/scriptaculous.js' | global_asset_url | script_tag }} 14 | {{ 'lightbox/v204/lightbox.js' | global_asset_url | script_tag }} 15 | {{ 'option_selection.js' | shopify_asset_url | script_tag }} 16 | 17 | {{ 'layout.css' | asset_url | stylesheet_tag }} 18 | {{ 'shop.js' | asset_url | script_tag }} 19 | 20 | {{ content_for_header }} 21 | 22 | 23 | 24 | 25 |

Skip to navigation.

26 | 27 | {% if cart.item_count > 0 %} 28 | 39 | {% endif %} 40 | 41 |
42 | 54 |
55 |
56 | 57 |
58 |
59 | {{ content_for_layout }} 60 |
61 |
62 | 63 |
64 |
65 | 66 | 71 | 72 | {% if tags %} 73 | 78 | {% endif %} 79 | 80 | 85 | 86 |
87 | 88 |

89 | 90 | 96 | 97 |
98 |
99 | 100 |
101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /test/unit/condition_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ConditionUnitTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_basic_condition 7 | assert_equal false, Condition.new('1', '==', '2').evaluate 8 | assert_equal true, Condition.new('1', '==', '1').evaluate 9 | end 10 | 11 | def test_default_operators_evalute_true 12 | assert_evalutes_true '1', '==', '1' 13 | assert_evalutes_true '1', '!=', '2' 14 | assert_evalutes_true '1', '<>', '2' 15 | assert_evalutes_true '1', '<', '2' 16 | assert_evalutes_true '2', '>', '1' 17 | assert_evalutes_true '1', '>=', '1' 18 | assert_evalutes_true '2', '>=', '1' 19 | assert_evalutes_true '1', '<=', '2' 20 | assert_evalutes_true '1', '<=', '1' 21 | # negative numbers 22 | assert_evalutes_true '1', '>', '-1' 23 | assert_evalutes_true '-1', '<', '1' 24 | assert_evalutes_true '1.0', '>', '-1.0' 25 | assert_evalutes_true '-1.0', '<', '1.0' 26 | end 27 | 28 | def test_default_operators_evalute_false 29 | assert_evalutes_false '1', '==', '2' 30 | assert_evalutes_false '1', '!=', '1' 31 | assert_evalutes_false '1', '<>', '1' 32 | assert_evalutes_false '1', '<', '0' 33 | assert_evalutes_false '2', '>', '4' 34 | assert_evalutes_false '1', '>=', '3' 35 | assert_evalutes_false '2', '>=', '4' 36 | assert_evalutes_false '1', '<=', '0' 37 | assert_evalutes_false '1', '<=', '0' 38 | end 39 | 40 | def test_contains_works_on_strings 41 | assert_evalutes_true "'bob'", 'contains', "'o'" 42 | assert_evalutes_true "'bob'", 'contains', "'b'" 43 | assert_evalutes_true "'bob'", 'contains', "'bo'" 44 | assert_evalutes_true "'bob'", 'contains', "'ob'" 45 | assert_evalutes_true "'bob'", 'contains', "'bob'" 46 | 47 | assert_evalutes_false "'bob'", 'contains', "'bob2'" 48 | assert_evalutes_false "'bob'", 'contains', "'a'" 49 | assert_evalutes_false "'bob'", 'contains', "'---'" 50 | end 51 | 52 | def test_contains_works_on_arrays 53 | @context = Liquid::Context.new 54 | @context['array'] = [1,2,3,4,5] 55 | 56 | assert_evalutes_false "array", 'contains', '0' 57 | assert_evalutes_true "array", 'contains', '1' 58 | assert_evalutes_true "array", 'contains', '2' 59 | assert_evalutes_true "array", 'contains', '3' 60 | assert_evalutes_true "array", 'contains', '4' 61 | assert_evalutes_true "array", 'contains', '5' 62 | assert_evalutes_false "array", 'contains', '6' 63 | assert_evalutes_false "array", 'contains', '"1"' 64 | end 65 | 66 | def test_contains_returns_false_for_nil_operands 67 | @context = Liquid::Context.new 68 | assert_evalutes_false "not_assigned", 'contains', '0' 69 | assert_evalutes_false "0", 'contains', 'not_assigned' 70 | end 71 | 72 | def test_or_condition 73 | condition = Condition.new('1', '==', '2') 74 | 75 | assert_equal false, condition.evaluate 76 | 77 | condition.or Condition.new('2', '==', '1') 78 | 79 | assert_equal false, condition.evaluate 80 | 81 | condition.or Condition.new('1', '==', '1') 82 | 83 | assert_equal true, condition.evaluate 84 | end 85 | 86 | def test_and_condition 87 | condition = Condition.new('1', '==', '1') 88 | 89 | assert_equal true, condition.evaluate 90 | 91 | condition.and Condition.new('2', '==', '2') 92 | 93 | assert_equal true, condition.evaluate 94 | 95 | condition.and Condition.new('2', '==', '1') 96 | 97 | assert_equal false, condition.evaluate 98 | end 99 | 100 | def test_should_allow_custom_proc_operator 101 | Condition.operators['starts_with'] = Proc.new { |cond, left, right| left =~ %r{^#{right}} } 102 | 103 | assert_evalutes_true "'bob'", 'starts_with', "'b'" 104 | assert_evalutes_false "'bob'", 'starts_with', "'o'" 105 | 106 | ensure 107 | Condition.operators.delete 'starts_with' 108 | end 109 | 110 | def test_left_or_right_may_contain_operators 111 | @context = Liquid::Context.new 112 | @context['one'] = @context['another'] = "gnomeslab-and-or-liquid" 113 | 114 | assert_evalutes_true "one", '==', "another" 115 | end 116 | 117 | private 118 | def assert_evalutes_true(left, op, right) 119 | assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new), 120 | "Evaluated false: #{left} #{op} #{right}" 121 | end 122 | 123 | def assert_evalutes_false(left, op, right) 124 | assert !Condition.new(left, op, right).evaluate(@context || Liquid::Context.new), 125 | "Evaluated true: #{left} #{op} #{right}" 126 | end 127 | end # ConditionTest 128 | -------------------------------------------------------------------------------- /test/unit/variable_unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class VariableUnitTest < Test::Unit::TestCase 4 | include Liquid 5 | 6 | def test_variable 7 | var = Variable.new('hello') 8 | assert_equal 'hello', var.name 9 | end 10 | 11 | def test_filters 12 | var = Variable.new('hello | textileze') 13 | assert_equal 'hello', var.name 14 | assert_equal [["textileze",[]]], var.filters 15 | 16 | var = Variable.new('hello | textileze | paragraph') 17 | assert_equal 'hello', var.name 18 | assert_equal [["textileze",[]], ["paragraph",[]]], var.filters 19 | 20 | var = Variable.new(%! hello | strftime: '%Y'!) 21 | assert_equal 'hello', var.name 22 | assert_equal [["strftime",["'%Y'"]]], var.filters 23 | 24 | var = Variable.new(%! 'typo' | link_to: 'Typo', true !) 25 | assert_equal %!'typo'!, var.name 26 | assert_equal [["link_to",["'Typo'", "true"]]], var.filters 27 | 28 | var = Variable.new(%! 'typo' | link_to: 'Typo', false !) 29 | assert_equal %!'typo'!, var.name 30 | assert_equal [["link_to",["'Typo'", "false"]]], var.filters 31 | 32 | var = Variable.new(%! 'foo' | repeat: 3 !) 33 | assert_equal %!'foo'!, var.name 34 | assert_equal [["repeat",["3"]]], var.filters 35 | 36 | var = Variable.new(%! 'foo' | repeat: 3, 3 !) 37 | assert_equal %!'foo'!, var.name 38 | assert_equal [["repeat",["3","3"]]], var.filters 39 | 40 | var = Variable.new(%! 'foo' | repeat: 3, 3, 3 !) 41 | assert_equal %!'foo'!, var.name 42 | assert_equal [["repeat",["3","3","3"]]], var.filters 43 | 44 | var = Variable.new(%! hello | strftime: '%Y, okay?'!) 45 | assert_equal 'hello', var.name 46 | assert_equal [["strftime",["'%Y, okay?'"]]], var.filters 47 | 48 | var = Variable.new(%! hello | things: "%Y, okay?", 'the other one'!) 49 | assert_equal 'hello', var.name 50 | assert_equal [["things",["\"%Y, okay?\"","'the other one'"]]], var.filters 51 | end 52 | 53 | def test_filter_with_date_parameter 54 | 55 | var = Variable.new(%! '2006-06-06' | date: "%m/%d/%Y"!) 56 | assert_equal "'2006-06-06'", var.name 57 | assert_equal [["date",["\"%m/%d/%Y\""]]], var.filters 58 | 59 | end 60 | 61 | def test_filters_without_whitespace 62 | var = Variable.new('hello | textileze | paragraph') 63 | assert_equal 'hello', var.name 64 | assert_equal [["textileze",[]], ["paragraph",[]]], var.filters 65 | 66 | var = Variable.new('hello|textileze|paragraph') 67 | assert_equal 'hello', var.name 68 | assert_equal [["textileze",[]], ["paragraph",[]]], var.filters 69 | 70 | var = Variable.new("hello|replace:'foo','bar'|textileze") 71 | assert_equal 'hello', var.name 72 | assert_equal [["replace", ["'foo'", "'bar'"]], ["textileze", []]], var.filters 73 | end 74 | 75 | def test_symbol 76 | var = Variable.new("http://disney.com/logo.gif | image: 'med' ", :error_mode => :lax) 77 | assert_equal "http://disney.com/logo.gif", var.name 78 | assert_equal [["image",["'med'"]]], var.filters 79 | end 80 | 81 | def test_string_to_filter 82 | var = Variable.new("'http://disney.com/logo.gif' | image: 'med' ") 83 | assert_equal "'http://disney.com/logo.gif'", var.name 84 | assert_equal [["image",["'med'"]]], var.filters 85 | end 86 | 87 | def test_string_single_quoted 88 | var = Variable.new(%| "hello" |) 89 | assert_equal '"hello"', var.name 90 | end 91 | 92 | def test_string_double_quoted 93 | var = Variable.new(%| 'hello' |) 94 | assert_equal "'hello'", var.name 95 | end 96 | 97 | def test_integer 98 | var = Variable.new(%| 1000 |) 99 | assert_equal "1000", var.name 100 | end 101 | 102 | def test_float 103 | var = Variable.new(%| 1000.01 |) 104 | assert_equal "1000.01", var.name 105 | end 106 | 107 | def test_string_with_special_chars 108 | var = Variable.new(%| 'hello! $!@.;"ddasd" ' |) 109 | assert_equal %|'hello! $!@.;"ddasd" '|, var.name 110 | end 111 | 112 | def test_string_dot 113 | var = Variable.new(%| test.test |) 114 | assert_equal 'test.test', var.name 115 | end 116 | 117 | def test_filter_with_keyword_arguments 118 | var = Variable.new(%! hello | things: greeting: "world", farewell: 'goodbye'!) 119 | assert_equal 'hello', var.name 120 | assert_equal [['things',["greeting: \"world\"","farewell: 'goodbye'"]]], var.filters 121 | end 122 | 123 | def test_lax_filter_argument_parsing 124 | var = Variable.new(%! number_of_comments | pluralize: 'comment': 'comments' !, :error_mode => :lax) 125 | assert_equal 'number_of_comments', var.name 126 | assert_equal [['pluralize',["'comment'","'comments'"]]], var.filters 127 | end 128 | 129 | def test_strict_filter_argument_parsing 130 | with_error_mode(:strict) do 131 | assert_raises(SyntaxError) do 132 | Variable.new(%! number_of_comments | pluralize: 'comment': 'comments' !) 133 | end 134 | end 135 | end 136 | end 137 | --------------------------------------------------------------------------------