├── 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 |
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 | "
2 |
{{page.title}}
3 |
4 | {% paginate blog.articles by 20 %}
5 |
6 | {% for article in blog.articles %}
7 |
8 |
9 |
10 |
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 | #
2 |
3 | {% if collection.description %}
4 |
{{ collection.description }}
5 | {% endif %}
6 |
7 | {% paginate collection.products by 20 %}
8 |
9 |
10 | {% for product in collection.products %}
11 |
12 |
15 |
16 |
17 |
{{ product.description | strip_html | truncatewords: 35 }}
18 |
{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}
19 |
20 |
21 | {% endfor %}
22 |
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 |
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 |
2 |
3 |
Post from our blog...
4 | {% paginate blog.articles by 20 %}
5 | {% for article in blog.articles %}
6 |
7 |
8 |
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 |
27 |
28 | 24 Hours
29 | We're always here to help.
30 |
31 |
32 | No Spam
33 | We'll never share your info.
34 |
35 |
36 | Secure Servers
37 | Checkout is 256bit encrypted.
38 |
39 |
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 |
4 | {% for product in collections.frontpage.products %}
5 |
6 |
7 |
8 |
9 |
10 | {% if product.compare_at_price %}
11 | {% if product.price_min != product.compare_at_price %}
12 |
Was:{{product.compare_at_price | money}}
13 |
Now: {{product.price_min | money}}
14 | {% endif %}
15 | {% else %}
16 |
{{product.price_min | money}}
17 | {% endif %}
18 |
19 | {% endfor %}
20 |
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 |
34 |
35 | 24 Hours
36 | We're always here to help.
37 |
38 |
39 | No Spam
40 | We'll never share your info.
41 |
42 |
43 | Secure Servers
44 | Checkout is 256bit encrypted.
45 |
46 |
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 | ::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 |
6 |
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 |
14 |
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 |
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 |
13 | {% else %}
14 |
15 |
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 |
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 |
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 |
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 |
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 |
4 |
{% else %}
5 |
6 |
7 |
{% endif %}{% endfor %}
8 |
9 |
10 |
{{ product.title }}
11 | {{ product.description }}
12 |
13 | {% if product.available %}
14 |
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 |
42 | {% endif %}
43 |
44 |
45 | Search
46 |
47 |
52 |
53 |
54 |
55 |
56 |
57 | Navigation
58 | {% for link in linklists.main-menu.links %}
59 | {{ link.title | link_to: link.url }}
60 | {% endfor %}
61 |
62 |
63 | {% if tags %}
64 |
65 | Tags
66 | {% for tag in collection.tags %}
67 | {{ tag | highlight_active_tag | link_to_tag: tag }}
68 | {% endfor %}
69 |
70 | {% endif %}
71 |
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 |
8 |
9 | {% else %}
10 |
11 |
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 |
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 {{ article.author }}
4 |
5 |
6 | {{ article.content }}
7 |
8 |
9 |
10 |
11 |
12 | {% if blog.comments_enabled? %}
13 |
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 {{ article.author }}
4 |
5 |
6 | {{ article.content }}
7 |
8 |
9 |
10 |
11 |
12 | {% if blog.comments_enabled? %}
13 |
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 |
9 |
10 | {% else %}
11 |
12 |
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 |
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 |
7 |
8 |
9 |
10 |
{{ product.title | truncate: 30 }}
11 |
{{ product.price | money }}{% if product.compare_at_price_max > product.price %} {{ product.compare_at_price_max | money }}{% endif %}
12 |
13 | {% endtablerow %}
14 |
15 | {% else %}
16 |
22 | {% 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 |
36 |
Tribble: A Shopify Theme
37 |
38 |
39 |
40 |
41 |
42 | {% for link in linklists.main-menu.links %}
43 | {{ link.title | link_to: link.url }}
44 | {% endfor %}
45 |
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 | %| |
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 |
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}#{tag}>|
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 |
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 | [](http://travis-ci.org/Shopify/liquid)
2 | [](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("\n1 2 \n1 2 \n1 2 \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 |
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 |
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 |
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 |
29 |
30 |
There {{ cart.item_count | pluralize: 'is', 'are' }} {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }} in your cart ! Your subtotal is {{ cart.total_price | money }}.
31 | {% for item in cart.items %}
32 |
35 | {% endfor %}
36 |
37 |
38 |
39 | {% endif %}
40 |
41 |
42 |
54 |
55 |
56 |
57 |
58 |
59 | {{ content_for_layout }}
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | {% for link in linklists.main-menu.links %}
68 | {{ link.title | link_to: link.url }}
69 | {% endfor %}
70 |
71 |
72 | {% if tags %}
73 |
74 | {% for tag in collection.tags %}
75 | {{ '+' | link_to_add_tag: tag }} {{ tag | highlight_active_tag | link_to_tag: tag }}
76 | {% endfor %}
77 |
78 | {% endif %}
79 |
80 |
81 | {% for link in linklists.footer.links %}
82 | {{ link.title | link_to: link.url }}
83 | {% endfor %}
84 |
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 |
--------------------------------------------------------------------------------
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 |37 | It will have to be approved by the blog owner first before showing up. 38 |
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 |