├── .github ├── probots.yml └── workflows │ └── build_tests.yml ├── 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 │ └── tribble │ │ ├── blog.liquid │ │ ├── search.liquid │ │ ├── page.liquid │ │ ├── 404.liquid │ │ ├── collection.liquid │ │ ├── theme.liquid │ │ ├── index.liquid │ │ └── article.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 └── memory_profile.rb ├── lib ├── liquid │ ├── register.rb │ ├── version.rb │ ├── usage.rb │ ├── template_factory.rb │ ├── tags │ │ ├── comment.rb │ │ ├── break.rb │ │ ├── continue.rb │ │ ├── ifchanged.rb │ │ ├── echo.rb │ │ ├── unless.rb │ │ ├── increment.rb │ │ ├── decrement.rb │ │ ├── capture.rb │ │ ├── raw.rb │ │ ├── assign.rb │ │ ├── table_row.rb │ │ ├── cycle.rb │ │ ├── case.rb │ │ ├── render.rb │ │ └── include.rb │ ├── interrupts.rb │ ├── registers │ │ └── disabled_tags.rb │ ├── forloop_drop.rb │ ├── resource_limits.rb │ ├── document.rb │ ├── partial_cache.rb │ ├── profiler │ │ └── hooks.rb │ ├── parser_switching.rb │ ├── strainer_factory.rb │ ├── tokenizer.rb │ ├── range_lookup.rb │ ├── static_registers.rb │ ├── tablerowloop_drop.rb │ ├── extensions.rb │ ├── i18n.rb │ ├── parse_context.rb │ ├── parse_tree_visitor.rb │ ├── expression.rb │ ├── errors.rb │ ├── strainer_template.rb │ ├── lexer.rb │ ├── tag.rb │ ├── locales │ │ └── en.yml │ ├── utils.rb │ ├── block.rb │ ├── drop.rb │ ├── parser.rb │ ├── variable_lookup.rb │ └── file_system.rb └── liquid.rb ├── .gitignore ├── example └── server │ ├── templates │ ├── index.liquid │ └── products.liquid │ ├── server.rb │ ├── liquid_servlet.rb │ └── example_servlet.rb ├── test ├── fixtures │ └── en_locale.yml ├── integration │ ├── tags │ │ ├── echo_test.rb │ │ ├── break_tag_test.rb │ │ ├── continue_tag_test.rb │ │ ├── increment_tag_test.rb │ │ ├── unless_else_tag_test.rb │ │ ├── raw_tag_test.rb │ │ ├── liquid_tag_test.rb │ │ └── table_row_test.rb │ ├── block_test.rb │ ├── document_test.rb │ ├── hash_ordering_test.rb │ ├── registers │ │ └── disabled_tags_test.rb │ ├── context_test.rb │ ├── assign_test.rb │ ├── capture_test.rb │ ├── security_test.rb │ └── variable_test.rb └── unit │ ├── template_factory_unit_test.rb │ ├── tags │ ├── if_tag_unit_test.rb │ ├── case_tag_unit_test.rb │ └── for_tag_unit_test.rb │ ├── i18n_unit_test.rb │ ├── registers │ └── disabled_tags_unit_test.rb │ ├── file_system_unit_test.rb │ ├── regexp_unit_test.rb │ ├── lexer_unit_test.rb │ ├── tag_unit_test.rb │ ├── tokenizer_unit_test.rb │ ├── strainer_template_unit_test.rb │ ├── parser_unit_test.rb │ ├── template_unit_test.rb │ ├── block_unit_test.rb │ └── strainer_factory_unit_test.rb ├── .rubocop.yml ├── .travis.yml ├── Gemfile ├── CONTRIBUTING.md ├── LICENSE ├── liquid.gemspec ├── .rubocop_todo.yml └── Rakefile /.github/probots.yml: -------------------------------------------------------------------------------- 1 | enabled: 2 | - cla 3 | -------------------------------------------------------------------------------- /performance/tests/vogue/page.liquid: -------------------------------------------------------------------------------- 1 |

{{ page.title }}

2 | {{ page.content }} 3 | 4 | -------------------------------------------------------------------------------- /lib/liquid/register.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class Register 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/liquid/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | module Liquid 5 | VERSION = "4.0.3" 6 | end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.gem 3 | *.swp 4 | pkg 5 | *.rbc 6 | .rvmrc 7 | .ruby-version 8 | Gemfile.lock 9 | .bundle 10 | .byebug_history 11 | -------------------------------------------------------------------------------- /performance/tests/ripen/page.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{page.title}}

3 | {{ page.content }} 4 |
5 | -------------------------------------------------------------------------------- /lib/liquid/usage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | module Usage 5 | def self.increment(name) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /example/server/templates/index.liquid: -------------------------------------------------------------------------------- 1 |

Hello world!

2 | 3 |

It is {{date}}

4 | 5 | 6 |

Check out the Products screen

7 | -------------------------------------------------------------------------------- /performance/tests/dropify/page.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{page.title}}

3 | 4 |
5 | {{page.content}} 6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /lib/liquid/template_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class TemplateFactory 5 | def for(_template_name) 6 | Liquid::Template.new 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /performance/shopify/json_filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | 5 | module JsonFilter 6 | def json(object) 7 | JSON.dump(object.reject { |k, _v| k == "collections" }) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /performance/shopify/weight_filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module WeightFilter 4 | def weight(grams) 5 | format("%.2f", grams / 1000) 6 | end 7 | 8 | def weight_with_unit(grams) 9 | "#{weight(grams)} kg" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/fixtures/en_locale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | simple: "less is more" 3 | whatever: "something %{something}" 4 | errors: 5 | i18n: 6 | undefined_interpolation: "undefined key %{key}" 7 | unknown_translation: "translation '%{name}' wasn't found" 8 | syntax: 9 | oops: "something wasn't right" 10 | -------------------------------------------------------------------------------- /test/integration/tags/echo_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class EchoTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_echo_outputs_its_input 9 | assert_template_result('BAR', <<~LIQUID, 'variable-name' => 'bar') 10 | {%- echo variable-name | upcase -%} 11 | LIQUID 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/unit/template_factory_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class TemplateFactoryUnitTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_for_returns_liquid_template_instance 9 | template = TemplateFactory.new.for("anything") 10 | assert_instance_of(Liquid::Template, template) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/unit/tags/if_tag_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class IfTagUnitTest < Minitest::Test 6 | def test_if_nodelist 7 | template = Liquid::Template.parse('{% if true %}IF{% else %}ELSE{% endif %}') 8 | assert_equal(['IF', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - 'https://shopify.github.io/ruby-style-guide/rubocop.yml' 3 | - .rubocop_todo.yml 4 | 5 | require: rubocop-performance 6 | 7 | Performance: 8 | Enabled: true 9 | 10 | AllCops: 11 | TargetRubyVersion: 2.4 12 | Exclude: 13 | - 'vendor/bundle/**/*' 14 | 15 | Naming/MethodName: 16 | Exclude: 17 | - 'example/server/liquid_servlet.rb' 18 | -------------------------------------------------------------------------------- /lib/liquid/tags/comment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class Comment < Block 5 | def render_to_output_buffer(_context, output) 6 | output 7 | end 8 | 9 | def unknown_tag(_tag, _markup, _tokens) 10 | end 11 | 12 | def blank? 13 | true 14 | end 15 | end 16 | 17 | Template.register_tag('comment', Comment) 18 | end 19 | -------------------------------------------------------------------------------- /example/server/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'webrick' 4 | require 'rexml/document' 5 | 6 | require_relative '../../lib/liquid' 7 | require_relative 'liquid_servlet' 8 | require_relative 'example_servlet' 9 | 10 | # Setup webrick 11 | server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000) 12 | server.mount('/', Servlet) 13 | trap("INT") { server.shutdown } 14 | server.start 15 | -------------------------------------------------------------------------------- /performance/shopify/money_filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MoneyFilter 4 | def money_with_currency(money) 5 | return '' if money.nil? 6 | format("$ %.2f USD", money / 100.0) 7 | end 8 | 9 | def money(money) 10 | return '' if money.nil? 11 | format("$ %.2f", money / 100.0) 12 | end 13 | 14 | private 15 | 16 | def currency 17 | ShopDrop.new.currency 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/unit/tags/case_tag_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class CaseTagUnitTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_case_nodelist 9 | template = Liquid::Template.parse('{% case var %}{% when true %}WHEN{% else %}ELSE{% endcase %}') 10 | assert_equal(['WHEN', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/integration/block_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class BlockTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_unexpected_end_tag 9 | exc = assert_raises(SyntaxError) do 10 | Template.parse("{% if true %}{% endunless %}") 11 | end 12 | assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/integration/tags/break_tag_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class BreakTagTest < Minitest::Test 6 | include Liquid 7 | 8 | # tests that no weird errors are raised if break is called outside of a 9 | # block 10 | def test_break_with_no_block 11 | assigns = { 'i' => 1 } 12 | markup = '{% break %}' 13 | expected = '' 14 | 15 | assert_template_result(expected, markup, assigns) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/integration/tags/continue_tag_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class ContinueTagTest < Minitest::Test 6 | include Liquid 7 | 8 | # tests that no weird errors are raised if continue is called outside of a 9 | # block 10 | def test_continue_with_no_block 11 | assigns = {} 12 | markup = '{% continue %}' 13 | expected = '' 14 | 15 | assert_template_result(expected, markup, assigns) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | 4 | rvm: 5 | - 2.4 6 | - 2.5 7 | - 2.6 8 | - &latest_ruby 2.7 9 | - ruby-head 10 | 11 | matrix: 12 | include: 13 | - rvm: *latest_ruby 14 | script: bundle exec rake memory_profile:run 15 | name: Profiling Memory Usage 16 | allow_failures: 17 | - rvm: ruby-head 18 | 19 | branches: 20 | only: 21 | - master 22 | - gh-pages 23 | - /.*-stable/ 24 | 25 | notifications: 26 | disable: true 27 | -------------------------------------------------------------------------------- /lib/liquid/tags/break.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | # Break tag to be used to break out of a for loop. 5 | # 6 | # == Basic Usage: 7 | # {% for item in collection %} 8 | # {% if item.condition %} 9 | # {% break %} 10 | # {% endif %} 11 | # {% endfor %} 12 | # 13 | class Break < Tag 14 | def interrupt 15 | BreakInterrupt.new 16 | end 17 | end 18 | 19 | Template.register_tag('break', Break) 20 | end 21 | -------------------------------------------------------------------------------- /lib/liquid/tags/continue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | # Continue tag to be used to break out of a for loop. 5 | # 6 | # == Basic Usage: 7 | # {% for item in collection %} 8 | # {% if item.condition %} 9 | # {% continue %} 10 | # {% endif %} 11 | # {% endfor %} 12 | # 13 | class Continue < Tag 14 | def interrupt 15 | ContinueInterrupt.new 16 | end 17 | end 18 | 19 | Template.register_tag('continue', Continue) 20 | end 21 | -------------------------------------------------------------------------------- /lib/liquid/tags/ifchanged.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class Ifchanged < Block 5 | def render_to_output_buffer(context, output) 6 | block_output = +'' 7 | super(context, block_output) 8 | 9 | if block_output != context.registers[:ifchanged] 10 | context.registers[:ifchanged] = block_output 11 | output << block_output 12 | end 13 | 14 | output 15 | end 16 | end 17 | 18 | Template.register_tag('ifchanged', Ifchanged) 19 | end 20 | -------------------------------------------------------------------------------- /performance/tests/ripen/blog.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{page.title}}

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

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

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

{{ article.comments_count }} comments

11 | {% endif %} 12 | {% endfor %} 13 |
14 | -------------------------------------------------------------------------------- /performance/benchmark.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'benchmark/ips' 4 | require_relative 'theme_runner' 5 | 6 | Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first 7 | profiler = ThemeRunner.new 8 | 9 | Benchmark.ips do |x| 10 | x.time = 10 11 | x.warmup = 5 12 | 13 | puts 14 | puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)." 15 | puts 16 | 17 | x.report("parse:") { profiler.compile } 18 | x.report("render:") { profiler.render } 19 | x.report("parse & render:") { profiler.run } 20 | end 21 | -------------------------------------------------------------------------------- /lib/liquid/interrupts.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | # An interrupt is any command that breaks processing of a block (ex: a for loop). 5 | class Interrupt 6 | attr_reader :message 7 | 8 | def initialize(message = nil) 9 | @message = message || "interrupt" 10 | end 11 | end 12 | 13 | # Interrupt that is thrown whenever a {% break %} is called. 14 | class BreakInterrupt < Interrupt; end 15 | 16 | # Interrupt that is thrown whenever a {% continue %} is called. 17 | class ContinueInterrupt < Interrupt; end 18 | end 19 | -------------------------------------------------------------------------------- /test/unit/tags/for_tag_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class ForTagUnitTest < Minitest::Test 6 | def test_for_nodelist 7 | template = Liquid::Template.parse('{% for item in items %}FOR{% endfor %}') 8 | assert_equal(['FOR'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten) 9 | end 10 | 11 | def test_for_else_nodelist 12 | template = Liquid::Template.parse('{% for item in items %}FOR{% else %}ELSE{% endfor %}') 13 | assert_equal(['FOR', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/integration/document_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class DocumentTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_unexpected_outer_tag 9 | exc = assert_raises(SyntaxError) do 10 | Template.parse("{% else %}") 11 | end 12 | assert_equal(exc.message, "Liquid syntax error: Unexpected outer 'else' tag") 13 | end 14 | 15 | def test_unknown_tag 16 | exc = assert_raises(SyntaxError) do 17 | Template.parse("{% foo %}") 18 | end 19 | assert_equal(exc.message, "Liquid syntax error: Unknown tag 'foo'") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/integration/hash_ordering_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class HashOrderingTest < Minitest::Test 6 | module MoneyFilter 7 | def money(input) 8 | format(' %d$ ', input) 9 | end 10 | end 11 | 12 | module CanadianMoneyFilter 13 | def money(input) 14 | format(' %d$ CAD ', input) 15 | end 16 | end 17 | 18 | include Liquid 19 | 20 | def test_global_register_order 21 | with_global_filter(MoneyFilter, CanadianMoneyFilter) do 22 | assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | git_source(:github) do |repo_name| 5 | "https://github.com/#{repo_name}.git" 6 | end 7 | 8 | gemspec 9 | 10 | group :benchmark, :test do 11 | gem 'benchmark-ips' 12 | gem 'memory_profiler' 13 | gem 'terminal-table' 14 | 15 | install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do 16 | gem 'stackprof' 17 | end 18 | end 19 | 20 | group :test do 21 | gem 'rubocop', '~> 0.78.0', require: false 22 | gem 'rubocop-performance', require: false 23 | 24 | platform :mri, :truffleruby do 25 | gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master' 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/liquid/tags/echo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | # Echo outputs an expression 5 | # 6 | # {% echo monkey %} 7 | # {% echo user.name %} 8 | # 9 | # This is identical to variable output syntax, like {{ foo }}, but works 10 | # inside {% liquid %} tags. The full syntax is supported, including filters: 11 | # 12 | # {% echo user | link %} 13 | # 14 | class Echo < Tag 15 | def initialize(tag_name, markup, parse_context) 16 | super 17 | @variable = Variable.new(markup, parse_context) 18 | end 19 | 20 | def render(context) 21 | @variable.render_to_output_buffer(context, +'') 22 | end 23 | end 24 | 25 | Template.register_tag('echo', Echo) 26 | end 27 | -------------------------------------------------------------------------------- /lib/liquid/registers/disabled_tags.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Liquid 3 | class DisabledTags < Register 4 | def initialize 5 | @disabled_tags = {} 6 | end 7 | 8 | def disabled?(tag) 9 | @disabled_tags.key?(tag) && @disabled_tags[tag] > 0 10 | end 11 | 12 | def disable(tags) 13 | tags.each(&method(:increment)) 14 | yield 15 | ensure 16 | tags.each(&method(:decrement)) 17 | end 18 | 19 | private 20 | 21 | def increment(tag) 22 | @disabled_tags[tag] ||= 0 23 | @disabled_tags[tag] += 1 24 | end 25 | 26 | def decrement(tag) 27 | @disabled_tags[tag] -= 1 28 | end 29 | end 30 | 31 | Template.add_register(:disabled_tags, DisabledTags.new) 32 | end 33 | -------------------------------------------------------------------------------- /performance/shopify/liquid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(__dir__ + '/../../lib') 4 | require_relative '../../lib/liquid' 5 | 6 | require_relative 'comment_form' 7 | require_relative 'paginate' 8 | require_relative 'json_filter' 9 | require_relative 'money_filter' 10 | require_relative 'shop_filter' 11 | require_relative 'tag_filter' 12 | require_relative 'weight_filter' 13 | 14 | Liquid::Template.register_tag('paginate', Paginate) 15 | Liquid::Template.register_tag('form', CommentForm) 16 | 17 | Liquid::Template.register_filter(JsonFilter) 18 | Liquid::Template.register_filter(MoneyFilter) 19 | Liquid::Template.register_filter(WeightFilter) 20 | Liquid::Template.register_filter(ShopFilter) 21 | Liquid::Template.register_filter(TagFilter) 22 | -------------------------------------------------------------------------------- /.github/workflows/build_tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: build tests 9 | on: [push, pull_request] 10 | 11 | jobs: 12 | test: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | bundler-cache: true 21 | ruby-version: '2.6' 22 | - run: bundle install 23 | - run: bundle exec rake 24 | -------------------------------------------------------------------------------- /lib/liquid/forloop_drop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class ForloopDrop < Drop 5 | def initialize(name, length, parentloop) 6 | @name = name 7 | @length = length 8 | @parentloop = parentloop 9 | @index = 0 10 | end 11 | 12 | attr_reader :name, :length, :parentloop 13 | 14 | def index 15 | @index + 1 16 | end 17 | 18 | def index0 19 | @index 20 | end 21 | 22 | def rindex 23 | @length - @index 24 | end 25 | 26 | def rindex0 27 | @length - @index - 1 28 | end 29 | 30 | def first 31 | @index == 0 32 | end 33 | 34 | def last 35 | @index == @length - 1 36 | end 37 | 38 | protected 39 | 40 | def increment! 41 | @index += 1 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /performance/profile.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'stackprof' 4 | require_relative 'theme_runner' 5 | 6 | Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first 7 | profiler = ThemeRunner.new 8 | profiler.run 9 | 10 | [:cpu, :object].each do |profile_type| 11 | puts "Profiling in #{profile_type} mode..." 12 | results = StackProf.run(mode: profile_type) do 13 | 200.times do 14 | profiler.run 15 | end 16 | end 17 | 18 | if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME']) 19 | File.open(graph_filename, 'w') do |f| 20 | StackProf::Report.new(results).print_graphviz(nil, f) 21 | end 22 | end 23 | 24 | StackProf::Report.new(results).print_text(false, 20) 25 | File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME'] 26 | end 27 | -------------------------------------------------------------------------------- /example/server/liquid_servlet.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet 4 | def do_GET(req, res) 5 | handle(:get, req, res) 6 | end 7 | 8 | def do_POST(req, res) 9 | handle(:post, req, res) 10 | end 11 | 12 | private 13 | 14 | def handle(_type, req, res) 15 | @request = req 16 | @response = res 17 | 18 | @request.path_info =~ /(\w+)\z/ 19 | @action = Regexp.last_match(1) || 'index' 20 | @assigns = send(@action) if respond_to?(@action) 21 | 22 | @response['Content-Type'] = "text/html" 23 | @response.status = 200 24 | @response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter]) 25 | end 26 | 27 | def read_template(filename = @action) 28 | File.read("#{__dir__}/templates/#{filename}.liquid") 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/liquid/resource_limits.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class ResourceLimits 5 | attr_accessor :render_length, :render_score, :assign_score, 6 | :render_length_limit, :render_score_limit, :assign_score_limit 7 | 8 | def initialize(limits) 9 | @render_length_limit = limits[:render_length_limit] 10 | @render_score_limit = limits[:render_score_limit] 11 | @assign_score_limit = limits[:assign_score_limit] 12 | reset 13 | end 14 | 15 | def reached? 16 | (@render_length_limit && @render_length > @render_length_limit) || 17 | (@render_score_limit && @render_score > @render_score_limit) || 18 | (@assign_score_limit && @assign_score > @assign_score_limit) 19 | end 20 | 21 | def reset 22 | @render_length = @render_score = @assign_score = 0 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/liquid/document.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class Document < BlockBody 5 | def self.parse(tokens, parse_context) 6 | doc = new 7 | doc.parse(tokens, parse_context) 8 | doc 9 | end 10 | 11 | def parse(tokens, parse_context) 12 | super do |end_tag_name, _end_tag_params| 13 | unknown_tag(end_tag_name, parse_context) if end_tag_name 14 | end 15 | rescue SyntaxError => e 16 | e.line_number ||= parse_context.line_number 17 | raise 18 | end 19 | 20 | def unknown_tag(tag, parse_context) 21 | case tag 22 | when 'else', 'end' 23 | raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag) 24 | else 25 | raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /performance/tests/dropify/collection.liquid: -------------------------------------------------------------------------------- 1 | {% paginate collection.products by 20 %} 2 | 3 | 17 | 18 | 21 | 22 | {% endpaginate %} 23 | -------------------------------------------------------------------------------- /lib/liquid/partial_cache.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class PartialCache 5 | def self.load(template_name, context:, parse_context:) 6 | cached_partials = (context.registers[:cached_partials] ||= {}) 7 | cached = cached_partials[template_name] 8 | return cached if cached 9 | 10 | file_system = (context.registers[:file_system] ||= Liquid::Template.file_system) 11 | source = file_system.read_template_file(template_name) 12 | 13 | parse_context.partial = true 14 | 15 | template_factory = (context.registers[:template_factory] ||= Liquid::TemplateFactory.new) 16 | template = template_factory.for(template_name) 17 | 18 | partial = template.parse(source, parse_context) 19 | cached_partials[template_name] = partial 20 | ensure 21 | parse_context.partial = false 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /performance/shopify/tag_filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TagFilter 4 | def link_to_tag(label, tag) 5 | "#{label}" 6 | end 7 | 8 | def highlight_active_tag(tag, css_class = 'active') 9 | if @context['current_tags'].include?(tag) 10 | "#{tag}" 11 | else 12 | tag 13 | end 14 | end 15 | 16 | def link_to_add_tag(label, tag) 17 | tags = (@context['current_tags'] + [tag]).uniq 18 | "#{label}" 19 | end 20 | 21 | def link_to_remove_tag(label, tag) 22 | tags = (@context['current_tags'] - [tag]).uniq 23 | "#{label}" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/integration/registers/disabled_tags_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class DisabledTagsTest < Minitest::Test 6 | include Liquid 7 | 8 | class DisableRaw < Block 9 | disable_tags "raw" 10 | end 11 | 12 | class DisableRawEcho < Block 13 | disable_tags "raw", "echo" 14 | end 15 | 16 | def test_disables_raw 17 | with_custom_tag('disable', DisableRaw) do 18 | assert_template_result 'raw usage is not allowed in this contextfoo', '{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}' 19 | end 20 | end 21 | 22 | def test_disables_echo_and_raw 23 | with_custom_tag('disable', DisableRawEcho) do 24 | assert_template_result 'raw usage is not allowed in this contextecho usage is not allowed in this context', '{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}' 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/liquid/profiler/hooks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class BlockBody 5 | def render_node_with_profiling(context, output, node) 6 | Profiler.profile_node_render(node) do 7 | render_node_without_profiling(context, output, node) 8 | end 9 | end 10 | 11 | alias_method :render_node_without_profiling, :render_node 12 | alias_method :render_node, :render_node_with_profiling 13 | end 14 | 15 | class Include < Tag 16 | def render_to_output_buffer_with_profiling(context, output) 17 | Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do 18 | render_to_output_buffer_without_profiling(context, output) 19 | end 20 | end 21 | 22 | alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer 23 | alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/liquid/parser_switching.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | module ParserSwitching 5 | def parse_with_selected_parser(markup) 6 | case parse_context.error_mode 7 | when :strict then strict_parse_with_error_context(markup) 8 | when :lax then lax_parse(markup) 9 | when :warn 10 | begin 11 | strict_parse_with_error_context(markup) 12 | rescue SyntaxError => e 13 | parse_context.warnings << e 14 | lax_parse(markup) 15 | end 16 | end 17 | end 18 | 19 | private 20 | 21 | def strict_parse_with_error_context(markup) 22 | strict_parse(markup) 23 | rescue SyntaxError => e 24 | e.line_number = line_number 25 | e.markup_context = markup_context(markup) 26 | raise e 27 | end 28 | 29 | def markup_context(markup) 30 | "in \"#{markup.strip}\"" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/integration/context_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class ContextTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_override_global_filter 9 | global = Module.new do 10 | def notice(output) 11 | "Global #{output}" 12 | end 13 | end 14 | 15 | local = Module.new do 16 | def notice(output) 17 | "Local #{output}" 18 | end 19 | end 20 | 21 | with_global_filter(global) do 22 | assert_equal 'Global test', Template.parse("{{'test' | notice }}").render! 23 | assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local]) 24 | end 25 | end 26 | 27 | def test_has_key_will_not_add_an_error_for_missing_keys 28 | with_error_mode :strict do 29 | context = Context.new 30 | context.key?('unknown') 31 | assert_empty context.errors 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /performance/tests/vogue/blog.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{page.title}}

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

9 | {{ article.title }} 10 |

11 | 12 |

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

18 |
19 | {{ article.content }} 20 |
21 |
22 | {% endfor %} 23 | 24 | 27 | 28 | {% endpaginate %} 29 | 30 | 31 |
32 |
33 | -------------------------------------------------------------------------------- /lib/liquid/strainer_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | # StrainerFactory is the factory for the filters system. 5 | module StrainerFactory 6 | extend self 7 | 8 | def add_global_filter(filter) 9 | strainer_class_cache.clear 10 | global_filters << filter 11 | end 12 | 13 | def create(context, filters = []) 14 | strainer_from_cache(filters).new(context) 15 | end 16 | 17 | private 18 | 19 | def global_filters 20 | @global_filters ||= [] 21 | end 22 | 23 | def strainer_from_cache(filters) 24 | strainer_class_cache[filters] ||= begin 25 | klass = Class.new(StrainerTemplate) 26 | global_filters.each { |f| klass.add_filter(f) } 27 | filters.each { |f| klass.add_filter(f) } 28 | klass 29 | end 30 | end 31 | 32 | def strainer_class_cache 33 | @strainer_class_cache ||= {} 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Things we will merge 4 | 5 | * Bugfixes 6 | * Performance improvements 7 | * Features that are likely to be useful to the majority of Liquid users 8 | 9 | ## Things we won't merge 10 | 11 | * Code that introduces considerable performance degrations 12 | * Code that touches performance-critical parts of Liquid and comes without benchmarks 13 | * Features that are not important for most people (we want to keep the core Liquid code small and tidy) 14 | * Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem) 15 | * Code that does not include tests 16 | * Code that breaks existing tests 17 | 18 | ## Workflow 19 | 20 | * Fork the Liquid repository 21 | * Create a new branch in your fork 22 | * If it makes sense, add tests for your code and/or run a performance benchmark 23 | * Make sure all tests pass (`bundle exec rake`) 24 | * Create a pull request 25 | 26 | -------------------------------------------------------------------------------- /lib/liquid/tags/unless.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'if' 4 | 5 | module Liquid 6 | # Unless is a conditional just like 'if' but works on the inverse logic. 7 | # 8 | # {% unless x < 0 %} x is greater than zero {% endunless %} 9 | # 10 | class Unless < If 11 | def render_to_output_buffer(context, output) 12 | # First condition is interpreted backwards ( if not ) 13 | first_block = @blocks.first 14 | unless first_block.evaluate(context) 15 | return first_block.attachment.render_to_output_buffer(context, output) 16 | end 17 | 18 | # After the first condition unless works just like if 19 | @blocks[1..-1].each do |block| 20 | if block.evaluate(context) 21 | return block.attachment.render_to_output_buffer(context, output) 22 | end 23 | end 24 | 25 | output 26 | end 27 | end 28 | 29 | Template.register_tag('unless', Unless) 30 | end 31 | -------------------------------------------------------------------------------- /test/integration/tags/increment_tag_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class IncrementTagTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_inc 9 | assert_template_result('0', '{%increment port %}', {}) 10 | assert_template_result('0 1', '{%increment port %} {%increment port%}', {}) 11 | assert_template_result('0 0 1 2 1', 12 | '{%increment port %} {%increment starboard%} ' \ 13 | '{%increment port %} {%increment port%} ' \ 14 | '{%increment starboard %}', {}) 15 | end 16 | 17 | def test_dec 18 | assert_template_result('9', '{%decrement port %}', 'port' => 10) 19 | assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {}) 20 | assert_template_result('1 5 2 2 5', 21 | '{%increment port %} {%increment starboard%} ' \ 22 | '{%increment port %} {%decrement port%} ' \ 23 | '{%decrement starboard %}', 'port' => 1, 'starboard' => 5) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /performance/tests/dropify/blog.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{page.title}}

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

11 | {{ article.title }} 12 |

13 |

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

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

{{ article.comments_count }} comments

22 | {% endif %} 23 |
24 | 25 | {% endfor %} 26 | 27 | 30 | 31 | {% endpaginate %} 32 | 33 |
34 | -------------------------------------------------------------------------------- /lib/liquid/tokenizer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class Tokenizer 5 | attr_reader :line_number, :for_liquid_tag 6 | 7 | def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false) 8 | @source = source 9 | @line_number = line_number || (line_numbers ? 1 : nil) 10 | @for_liquid_tag = for_liquid_tag 11 | @tokens = tokenize 12 | end 13 | 14 | def shift 15 | (token = @tokens.shift) || return 16 | 17 | if @line_number 18 | @line_number += @for_liquid_tag ? 1 : token.count("\n") 19 | end 20 | 21 | token 22 | end 23 | 24 | private 25 | 26 | def tokenize 27 | return [] if @source.to_s.empty? 28 | 29 | return @source.split("\n") if @for_liquid_tag 30 | 31 | tokens = @source.split(TemplateParser) 32 | 33 | # removes the rogue empty element at the beginning of the array 34 | tokens.shift if tokens[0]&.empty? 35 | 36 | tokens 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/liquid/tags/increment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | # increment is used in a place where one needs to insert a counter 5 | # into a template, and needs the counter to survive across 6 | # multiple instantiations of the template. 7 | # (To achieve the survival, the application must keep the context) 8 | # 9 | # if the variable does not exist, it is created with value 0. 10 | # 11 | # Hello: {% increment variable %} 12 | # 13 | # gives you: 14 | # 15 | # Hello: 0 16 | # Hello: 1 17 | # Hello: 2 18 | # 19 | class Increment < Tag 20 | def initialize(tag_name, markup, options) 21 | super 22 | @variable = markup.strip 23 | end 24 | 25 | def render_to_output_buffer(context, output) 26 | value = context.environments.first[@variable] ||= 0 27 | context.environments.first[@variable] = value + 1 28 | 29 | output << value.to_s 30 | output 31 | end 32 | end 33 | 34 | Template.register_tag('increment', Increment) 35 | end 36 | -------------------------------------------------------------------------------- /lib/liquid/range_lookup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class RangeLookup 5 | def self.parse(start_markup, end_markup) 6 | start_obj = Expression.parse(start_markup) 7 | end_obj = Expression.parse(end_markup) 8 | if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate) 9 | new(start_obj, end_obj) 10 | else 11 | start_obj.to_i..end_obj.to_i 12 | end 13 | end 14 | 15 | def initialize(start_obj, end_obj) 16 | @start_obj = start_obj 17 | @end_obj = end_obj 18 | end 19 | 20 | def evaluate(context) 21 | start_int = to_integer(context.evaluate(@start_obj)) 22 | end_int = to_integer(context.evaluate(@end_obj)) 23 | start_int..end_int 24 | end 25 | 26 | private 27 | 28 | def to_integer(input) 29 | case input 30 | when Integer 31 | input 32 | when NilClass, String 33 | input.to_i 34 | else 35 | Utils.to_integer(input) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/liquid/static_registers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class StaticRegisters 5 | attr_reader :static 6 | 7 | def initialize(registers = {}) 8 | @static = registers.is_a?(StaticRegisters) ? registers.static : registers 9 | @registers = {} 10 | end 11 | 12 | def []=(key, value) 13 | @registers[key] = value 14 | end 15 | 16 | def [](key) 17 | if @registers.key?(key) 18 | @registers[key] 19 | else 20 | @static[key] 21 | end 22 | end 23 | 24 | def delete(key) 25 | @registers.delete(key) 26 | end 27 | 28 | UNDEFINED = Object.new 29 | 30 | def fetch(key, default = UNDEFINED, &block) 31 | if @registers.key?(key) 32 | @registers.fetch(key) 33 | elsif default != UNDEFINED 34 | @static.fetch(key, default, &block) 35 | else 36 | @static.fetch(key, &block) 37 | end 38 | end 39 | 40 | def key?(key) 41 | @registers.key?(key) || @static.key?(key) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/liquid/tablerowloop_drop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class TablerowloopDrop < Drop 5 | def initialize(length, cols) 6 | @length = length 7 | @row = 1 8 | @col = 1 9 | @cols = cols 10 | @index = 0 11 | end 12 | 13 | attr_reader :length, :col, :row 14 | 15 | def index 16 | @index + 1 17 | end 18 | 19 | def index0 20 | @index 21 | end 22 | 23 | def col0 24 | @col - 1 25 | end 26 | 27 | def rindex 28 | @length - @index 29 | end 30 | 31 | def rindex0 32 | @length - @index - 1 33 | end 34 | 35 | def first 36 | @index == 0 37 | end 38 | 39 | def last 40 | @index == @length - 1 41 | end 42 | 43 | def col_first 44 | @col == 1 45 | end 46 | 47 | def col_last 48 | @col == @cols 49 | end 50 | 51 | protected 52 | 53 | def increment! 54 | @index += 1 55 | 56 | if @col == @cols 57 | @col = 1 58 | @row += 1 59 | else 60 | @col += 1 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /performance/tests/ripen/collection.liquid: -------------------------------------------------------------------------------- 1 |
2 | 3 | {% if collection.description %} 4 |
{{ collection.description }}
5 | {% endif %} 6 | 7 | {% paginate collection.products by 20 %} 8 | 9 | 23 | 24 | 27 | 28 | {% endpaginate %} 29 |
30 | -------------------------------------------------------------------------------- /liquid.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | lib = File.expand_path('../lib/', __FILE__) 5 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 6 | 7 | require "liquid/version" 8 | 9 | Gem::Specification.new do |s| 10 | s.name = "liquid" 11 | s.version = Liquid::VERSION 12 | s.platform = Gem::Platform::RUBY 13 | s.summary = "A secure, non-evaling end user template engine with aesthetic markup." 14 | s.authors = ["Tobias Lütke"] 15 | s.email = ["tobi@leetsoft.com"] 16 | s.homepage = "http://www.liquidmarkup.org" 17 | s.license = "MIT" 18 | # s.description = "A secure, non-evaling end user template engine with aesthetic markup." 19 | 20 | s.required_ruby_version = ">= 2.4.0" 21 | s.required_rubygems_version = ">= 1.3.7" 22 | 23 | s.test_files = Dir.glob("{test}/**/*") 24 | s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md) 25 | 26 | s.extra_rdoc_files = ["History.md", "README.md"] 27 | 28 | s.require_path = "lib" 29 | 30 | s.add_development_dependency('rake', '~> 11.3') 31 | s.add_development_dependency('minitest') 32 | end 33 | -------------------------------------------------------------------------------- /lib/liquid/extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'time' 4 | require 'date' 5 | 6 | class String # :nodoc: 7 | def to_liquid 8 | self 9 | end 10 | end 11 | 12 | class Symbol # :nodoc: 13 | def to_liquid 14 | to_s 15 | end 16 | end 17 | 18 | class Array # :nodoc: 19 | def to_liquid 20 | self 21 | end 22 | end 23 | 24 | class Hash # :nodoc: 25 | def to_liquid 26 | self 27 | end 28 | end 29 | 30 | class Numeric # :nodoc: 31 | def to_liquid 32 | self 33 | end 34 | end 35 | 36 | class Range # :nodoc: 37 | def to_liquid 38 | self 39 | end 40 | end 41 | 42 | class Time # :nodoc: 43 | def to_liquid 44 | self 45 | end 46 | end 47 | 48 | class DateTime < Date # :nodoc: 49 | def to_liquid 50 | self 51 | end 52 | end 53 | 54 | class Date # :nodoc: 55 | def to_liquid 56 | self 57 | end 58 | end 59 | 60 | class TrueClass 61 | def to_liquid # :nodoc: 62 | self 63 | end 64 | end 65 | 66 | class FalseClass 67 | def to_liquid # :nodoc: 68 | self 69 | end 70 | end 71 | 72 | class NilClass 73 | def to_liquid # :nodoc: 74 | self 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/unit/i18n_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class I18nUnitTest < Minitest::Test 6 | include Liquid 7 | 8 | def setup 9 | @i18n = I18n.new(fixture("en_locale.yml")) 10 | end 11 | 12 | def test_simple_translate_string 13 | assert_equal("less is more", @i18n.translate("simple")) 14 | end 15 | 16 | def test_nested_translate_string 17 | assert_equal("something wasn't right", @i18n.translate("errors.syntax.oops")) 18 | end 19 | 20 | def test_single_string_interpolation 21 | assert_equal("something different", @i18n.translate("whatever", something: "different")) 22 | end 23 | 24 | # def test_raises_translation_error_on_undefined_interpolation_key 25 | # assert_raises I18n::TranslationError do 26 | # @i18n.translate("whatever", :oopstypos => "yes") 27 | # end 28 | # end 29 | 30 | def test_raises_unknown_translation 31 | assert_raises I18n::TranslationError do 32 | @i18n.translate("doesnt_exist") 33 | end 34 | end 35 | 36 | def test_sets_default_path_to_en 37 | assert_equal(I18n::DEFAULT_LOCALE, I18n.new.path) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/liquid/i18n.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | module Liquid 6 | class I18n 7 | DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml") 8 | 9 | TranslationError = Class.new(StandardError) 10 | 11 | attr_reader :path 12 | 13 | def initialize(path = DEFAULT_LOCALE) 14 | @path = path 15 | end 16 | 17 | def translate(name, vars = {}) 18 | interpolate(deep_fetch_translation(name), vars) 19 | end 20 | alias_method :t, :translate 21 | 22 | def locale 23 | @locale ||= YAML.load_file(@path) 24 | end 25 | 26 | private 27 | 28 | def interpolate(name, vars) 29 | name.gsub(/%\{(\w+)\}/) do 30 | # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym] 31 | (vars[Regexp.last_match(1).to_sym]).to_s 32 | end 33 | end 34 | 35 | def deep_fetch_translation(name) 36 | name.split('.').reduce(locale) do |level, cur| 37 | level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}") 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/liquid/tags/decrement.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | # decrement is used in a place where one needs to insert a counter 5 | # into a template, and needs the counter to survive across 6 | # multiple instantiations of the template. 7 | # NOTE: decrement is a pre-decrement, --i, 8 | # while increment is post: i++. 9 | # 10 | # (To achieve the survival, the application must keep the context) 11 | # 12 | # if the variable does not exist, it is created with value 0. 13 | 14 | # Hello: {% decrement variable %} 15 | # 16 | # gives you: 17 | # 18 | # Hello: -1 19 | # Hello: -2 20 | # Hello: -3 21 | # 22 | class Decrement < Tag 23 | def initialize(tag_name, markup, options) 24 | super 25 | @variable = markup.strip 26 | end 27 | 28 | def render_to_output_buffer(context, output) 29 | value = context.environments.first[@variable] ||= 0 30 | value -= 1 31 | context.environments.first[@variable] = value 32 | output << value.to_s 33 | output 34 | end 35 | end 36 | 37 | Template.register_tag('decrement', Decrement) 38 | end 39 | -------------------------------------------------------------------------------- /performance/tests/vogue/collection.liquid: -------------------------------------------------------------------------------- 1 | {% paginate collection.products by 12 %}{% if collection.products.size == 0 %} 2 | No products found in this collection.{% else %} 3 |

{{ collection.title }}

4 | {{ collection.description }} 5 | 6 | {% tablerow product in collection.products cols: 3 %} 7 | 10 | 14 | {% endtablerow %} 15 | {% if paginate.pages > 1 %} 16 |
17 | {{ paginate | default_pagination }} 18 |
{% endif %}{% endif %} 19 | {% endpaginate %} 20 | -------------------------------------------------------------------------------- /lib/liquid/tags/capture.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | # Capture stores the result of a block into a variable without rendering it inplace. 5 | # 6 | # {% capture heading %} 7 | # Monkeys! 8 | # {% endcapture %} 9 | # ... 10 | #

{{ heading }}

11 | # 12 | # Capture is useful for saving content for use later in your template, such as 13 | # in a sidebar or footer. 14 | # 15 | class Capture < Block 16 | Syntax = /(#{VariableSignature}+)/o 17 | 18 | def initialize(tag_name, markup, options) 19 | super 20 | if markup =~ Syntax 21 | @to = Regexp.last_match(1) 22 | else 23 | raise SyntaxError, options[:locale].t("errors.syntax.capture") 24 | end 25 | end 26 | 27 | def render_to_output_buffer(context, output) 28 | previous_output_size = output.bytesize 29 | super 30 | context.scopes.last[@to] = output 31 | context.resource_limits.assign_score += (output.bytesize - previous_output_size) 32 | output 33 | end 34 | 35 | def blank? 36 | true 37 | end 38 | end 39 | 40 | Template.register_tag('capture', Capture) 41 | end 42 | -------------------------------------------------------------------------------- /lib/liquid/parse_context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class ParseContext 5 | attr_accessor :locale, :line_number, :trim_whitespace, :depth 6 | attr_reader :partial, :warnings, :error_mode 7 | 8 | def initialize(options = {}) 9 | @template_options = options ? options.dup : {} 10 | 11 | @locale = @template_options[:locale] ||= I18n.new 12 | @warnings = [] 13 | 14 | self.depth = 0 15 | self.partial = false 16 | end 17 | 18 | def [](option_key) 19 | @options[option_key] 20 | end 21 | 22 | def partial=(value) 23 | @partial = value 24 | @options = value ? partial_options : @template_options 25 | 26 | @error_mode = @options[:error_mode] || Template.error_mode 27 | end 28 | 29 | def partial_options 30 | @partial_options ||= begin 31 | dont_pass = @template_options[:include_options_blacklist] 32 | if dont_pass == true 33 | { locale: locale } 34 | elsif dont_pass.is_a?(Array) 35 | @template_options.reject { |k, _v| dont_pass.include?(k) } 36 | else 37 | @template_options 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/liquid/parse_tree_visitor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class ParseTreeVisitor 5 | def self.for(node, callbacks = Hash.new(proc {})) 6 | if defined?(node.class::ParseTreeVisitor) 7 | node.class::ParseTreeVisitor 8 | else 9 | self 10 | end.new(node, callbacks) 11 | end 12 | 13 | def initialize(node, callbacks) 14 | @node = node 15 | @callbacks = callbacks 16 | end 17 | 18 | def add_callback_for(*classes, &block) 19 | callback = block 20 | callback = ->(node, _) { yield node } if block.arity.abs == 1 21 | callback = ->(_, _) { yield } if block.arity.zero? 22 | classes.each { |klass| @callbacks[klass] = callback } 23 | self 24 | end 25 | 26 | def visit(context = nil) 27 | children.map do |node| 28 | item, new_context = @callbacks[node.class].call(node, context) 29 | [ 30 | item, 31 | ParseTreeVisitor.for(node, @callbacks).visit(new_context || context), 32 | ] 33 | end 34 | end 35 | 36 | protected 37 | 38 | def children 39 | @node.respond_to?(:nodelist) ? Array(@node.nodelist) : [] 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /performance/shopify/comment_form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CommentForm < Liquid::Block 4 | Syntax = /(#{Liquid::VariableSignature}+)/ 5 | 6 | def initialize(tag_name, markup, options) 7 | super 8 | 9 | if markup =~ Syntax 10 | @variable_name = Regexp.last_match(1) 11 | @attributes = {} 12 | else 13 | raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]" 14 | end 15 | end 16 | 17 | def render_to_output_buffer(context, output) 18 | article = context[@variable_name] 19 | 20 | context.stack do 21 | context['form'] = { 22 | 'posted_successfully?' => context.registers[:posted_successfully], 23 | 'errors' => context['comment.errors'], 24 | 'author' => context['comment.author'], 25 | 'email' => context['comment.email'], 26 | 'body' => context['comment.body'], 27 | } 28 | 29 | output << wrap_in_form(article, render_all(@nodelist, context, output)) 30 | output 31 | end 32 | end 33 | 34 | def wrap_in_form(article, input) 35 | %(
\n#{input}\n
) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/unit/registers/disabled_tags_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class DisabledTagsUnitTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_disables_tag_specified 9 | register = DisabledTags.new 10 | register.disable(%w(foo bar)) do 11 | assert_equal true, register.disabled?("foo") 12 | assert_equal true, register.disabled?("bar") 13 | assert_equal false, register.disabled?("unknown") 14 | end 15 | end 16 | 17 | def test_disables_nested_tags 18 | register = DisabledTags.new 19 | register.disable(["foo"]) do 20 | register.disable(["foo"]) do 21 | assert_equal true, register.disabled?("foo") 22 | assert_equal false, register.disabled?("bar") 23 | end 24 | register.disable(["bar"]) do 25 | assert_equal true, register.disabled?("foo") 26 | assert_equal true, register.disabled?("bar") 27 | register.disable(["foo"]) do 28 | assert_equal true, register.disabled?("foo") 29 | assert_equal true, register.disabled?("bar") 30 | end 31 | end 32 | assert_equal true, register.disabled?("foo") 33 | assert_equal false, register.disabled?("bar") 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/unit/file_system_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class FileSystemUnitTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_default 9 | assert_raises(FileSystemError) do 10 | BlankFileSystem.new.read_template_file("dummy") 11 | end 12 | end 13 | 14 | def test_local 15 | file_system = Liquid::LocalFileSystem.new("/some/path") 16 | assert_equal("/some/path/_mypartial.liquid", file_system.full_path("mypartial")) 17 | assert_equal("/some/path/dir/_mypartial.liquid", file_system.full_path("dir/mypartial")) 18 | 19 | assert_raises(FileSystemError) do 20 | file_system.full_path("../dir/mypartial") 21 | end 22 | 23 | assert_raises(FileSystemError) do 24 | file_system.full_path("/dir/../../dir/mypartial") 25 | end 26 | 27 | assert_raises(FileSystemError) do 28 | file_system.full_path("/etc/passwd") 29 | end 30 | end 31 | 32 | def test_custom_template_filename_patterns 33 | file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html") 34 | assert_equal("/some/path/mypartial.html", file_system.full_path("mypartial")) 35 | assert_equal("/some/path/dir/mypartial.html", file_system.full_path("dir/mypartial")) 36 | end 37 | end # FileSystemTest 38 | -------------------------------------------------------------------------------- /performance/tests/vogue/index.liquid: -------------------------------------------------------------------------------- 1 |
2 | {% assign article = pages.frontpage %} 3 | {% if article.content != "" %} 4 |

{{ article.title }}

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

Post from our blog...

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

{{ article.title }}

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

Why Shop With Us?

26 | 40 |
41 |
42 | -------------------------------------------------------------------------------- /test/integration/tags/unless_else_tag_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class UnlessElseTagTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_unless 9 | assert_template_result(' ', ' {% unless true %} this text should not go into the output {% endunless %} ') 10 | assert_template_result(' this text should go into the output ', 11 | ' {% unless false %} this text should go into the output {% endunless %} ') 12 | assert_template_result(' you rock ?', '{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?') 13 | end 14 | 15 | def test_unless_else 16 | assert_template_result(' YES ', '{% unless true %} NO {% else %} YES {% endunless %}') 17 | assert_template_result(' YES ', '{% unless false %} YES {% else %} NO {% endunless %}') 18 | assert_template_result(' YES ', '{% unless "foo" %} NO {% else %} YES {% endunless %}') 19 | end 20 | 21 | def test_unless_in_loop 22 | assert_template_result('23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', 'choices' => [1, nil, false]) 23 | end 24 | 25 | def test_unless_else_in_loop 26 | assert_template_result(' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', 'choices' => [1, nil, false]) 27 | end 28 | end # UnlessElseTest 29 | -------------------------------------------------------------------------------- /lib/liquid/tags/raw.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class Raw < Block 5 | Syntax = /\A\s*\z/ 6 | FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om 7 | 8 | def initialize(tag_name, markup, parse_context) 9 | super 10 | 11 | ensure_valid_markup(tag_name, markup, parse_context) 12 | end 13 | 14 | def parse(tokens) 15 | @body = +'' 16 | while (token = tokens.shift) 17 | if token =~ FullTokenPossiblyInvalid 18 | @body << Regexp.last_match(1) if Regexp.last_match(1) != "" 19 | return if block_delimiter == Regexp.last_match(2) 20 | end 21 | @body << token unless token.empty? 22 | end 23 | 24 | raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name) 25 | end 26 | 27 | def render_to_output_buffer(_context, output) 28 | output << @body 29 | output 30 | end 31 | 32 | def nodelist 33 | [@body] 34 | end 35 | 36 | def blank? 37 | @body.empty? 38 | end 39 | 40 | protected 41 | 42 | def ensure_valid_markup(tag_name, markup, parse_context) 43 | unless Syntax.match?(markup) 44 | raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name) 45 | end 46 | end 47 | end 48 | 49 | Template.register_tag('raw', Raw) 50 | end 51 | -------------------------------------------------------------------------------- /performance/tests/ripen/index.liquid: -------------------------------------------------------------------------------- 1 |
2 |

Featured products...

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

{{ article.title }}

26 | {{ article.content }} 27 | {% else %} 28 | In Admin > Blogs & Pages, create a page with the handle frontpage and it will show up here.
29 | {{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }} 30 | {% endif %} 31 |
32 |
33 | -------------------------------------------------------------------------------- /lib/liquid/expression.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class Expression 5 | class MethodLiteral 6 | attr_reader :method_name, :to_s 7 | 8 | def initialize(method_name, to_s) 9 | @method_name = method_name 10 | @to_s = to_s 11 | end 12 | 13 | def to_liquid 14 | to_s 15 | end 16 | end 17 | 18 | LITERALS = { 19 | nil => nil, 'nil' => nil, 'null' => nil, '' => nil, 20 | 'true' => true, 21 | 'false' => false, 22 | 'blank' => MethodLiteral.new(:blank?, '').freeze, 23 | 'empty' => MethodLiteral.new(:empty?, '').freeze 24 | }.freeze 25 | 26 | SINGLE_QUOTED_STRING = /\A'(.*)'\z/m 27 | DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m 28 | INTEGERS_REGEX = /\A(-?\d+)\z/ 29 | FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/ 30 | RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/ 31 | 32 | def self.parse(markup) 33 | if LITERALS.key?(markup) 34 | LITERALS[markup] 35 | else 36 | case markup 37 | when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING 38 | Regexp.last_match(1) 39 | when INTEGERS_REGEX 40 | Regexp.last_match(1).to_i 41 | when RANGES_REGEX 42 | RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2)) 43 | when FLOATS_REGEX 44 | Regexp.last_match(1).to_f 45 | else 46 | VariableLookup.parse(markup) 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /performance/tests/tribble/search.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |

Search Results

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

Why Shop With Us?

33 | 47 |
48 |
49 | 50 | {% endpaginate %} 51 | {% endif %} 52 | -------------------------------------------------------------------------------- /test/integration/assign_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class AssignTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_assign_with_hyphen_in_variable_name 9 | template_source = <<-END_TEMPLATE 10 | {% assign this-thing = 'Print this-thing' %} 11 | {{ this-thing }} 12 | END_TEMPLATE 13 | template = Template.parse(template_source) 14 | rendered = template.render! 15 | assert_equal("Print this-thing", rendered.strip) 16 | end 17 | 18 | def test_assigned_variable 19 | assert_template_result('.foo.', 20 | '{% assign foo = values %}.{{ foo[0] }}.', 21 | 'values' => %w(foo bar baz)) 22 | 23 | assert_template_result('.bar.', 24 | '{% assign foo = values %}.{{ foo[1] }}.', 25 | 'values' => %w(foo bar baz)) 26 | end 27 | 28 | def test_assign_with_filter 29 | assert_template_result('.bar.', 30 | '{% assign foo = values | split: "," %}.{{ foo[1] }}.', 31 | 'values' => "foo,bar,baz") 32 | end 33 | 34 | def test_assign_syntax_error 35 | assert_match_syntax_error(/assign/, 36 | '{% assign foo not values %}.', 37 | 'values' => "foo,bar,baz") 38 | end 39 | 40 | def test_assign_uses_error_mode 41 | with_error_mode(:strict) do 42 | assert_raises(SyntaxError) do 43 | Template.parse("{% assign foo = ('X' | downcase) %}") 44 | end 45 | end 46 | with_error_mode(:lax) do 47 | assert Template.parse("{% assign foo = ('X' | downcase) %}") 48 | end 49 | end 50 | end # AssignTest 51 | -------------------------------------------------------------------------------- /test/integration/tags/raw_tag_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class RawTagTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_tag_in_raw 9 | assert_template_result('{% comment %} test {% endcomment %}', 10 | '{% raw %}{% comment %} test {% endcomment %}{% endraw %}') 11 | end 12 | 13 | def test_output_in_raw 14 | assert_template_result('{{ test }}', '{% raw %}{{ test }}{% endraw %}') 15 | end 16 | 17 | def test_open_tag_in_raw 18 | assert_template_result(' Foobar {% invalid ', '{% raw %} Foobar {% invalid {% endraw %}') 19 | assert_template_result(' Foobar invalid %} ', '{% raw %} Foobar invalid %} {% endraw %}') 20 | assert_template_result(' Foobar {{ invalid ', '{% raw %} Foobar {{ invalid {% endraw %}') 21 | assert_template_result(' Foobar invalid }} ', '{% raw %} Foobar invalid }} {% endraw %}') 22 | assert_template_result(' Foobar {% invalid {% {% endraw ', '{% raw %} Foobar {% invalid {% {% endraw {% endraw %}') 23 | assert_template_result(' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}') 24 | assert_template_result(' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}') 25 | assert_template_result(' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}') 26 | end 27 | 28 | def test_invalid_raw 29 | assert_match_syntax_error(/tag was never closed/, '{% raw %} foo') 30 | assert_match_syntax_error(/Valid syntax/, '{% raw } foo {% endraw %}') 31 | assert_match_syntax_error(/Valid syntax/, '{% raw } foo %}{% endraw %}') 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/liquid/tags/assign.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | # Assign sets a variable in your template. 5 | # 6 | # {% assign foo = 'monkey' %} 7 | # 8 | # You can then use the variable later in the page. 9 | # 10 | # {{ foo }} 11 | # 12 | class Assign < Tag 13 | Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om 14 | 15 | attr_reader :to, :from 16 | 17 | def initialize(tag_name, markup, options) 18 | super 19 | if markup =~ Syntax 20 | @to = Regexp.last_match(1) 21 | @from = Variable.new(Regexp.last_match(2), options) 22 | else 23 | raise SyntaxError, options[:locale].t('errors.syntax.assign') 24 | end 25 | end 26 | 27 | def render_to_output_buffer(context, output) 28 | val = @from.render(context) 29 | context.scopes.last[@to] = val 30 | context.resource_limits.assign_score += assign_score_of(val) 31 | output 32 | end 33 | 34 | def blank? 35 | true 36 | end 37 | 38 | private 39 | 40 | def assign_score_of(val) 41 | if val.instance_of?(String) 42 | val.bytesize 43 | elsif val.instance_of?(Array) || val.instance_of?(Hash) 44 | sum = 1 45 | # Uses #each to avoid extra allocations. 46 | val.each { |child| sum += assign_score_of(child) } 47 | sum 48 | else 49 | 1 50 | end 51 | end 52 | 53 | class ParseTreeVisitor < Liquid::ParseTreeVisitor 54 | def children 55 | [@node.from] 56 | end 57 | end 58 | end 59 | 60 | Template.register_tag('assign', Assign) 61 | end 62 | -------------------------------------------------------------------------------- /lib/liquid/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class Error < ::StandardError 5 | attr_accessor :line_number 6 | attr_accessor :template_name 7 | attr_accessor :markup_context 8 | 9 | def to_s(with_prefix = true) 10 | str = +"" 11 | str << message_prefix if with_prefix 12 | str << super() 13 | 14 | if markup_context 15 | str << " " 16 | str << markup_context 17 | end 18 | 19 | str 20 | end 21 | 22 | private 23 | 24 | def message_prefix 25 | str = +"" 26 | str << if is_a?(SyntaxError) 27 | "Liquid syntax error" 28 | else 29 | "Liquid error" 30 | end 31 | 32 | if line_number 33 | str << " (" 34 | str << template_name << " " if template_name 35 | str << "line " << line_number.to_s << ")" 36 | end 37 | 38 | str << ": " 39 | str 40 | end 41 | end 42 | 43 | ArgumentError = Class.new(Error) 44 | ContextError = Class.new(Error) 45 | FileSystemError = Class.new(Error) 46 | StandardError = Class.new(Error) 47 | SyntaxError = Class.new(Error) 48 | StackLevelError = Class.new(Error) 49 | TaintedError = Class.new(Error) 50 | MemoryError = Class.new(Error) 51 | ZeroDivisionError = Class.new(Error) 52 | FloatDomainError = Class.new(Error) 53 | UndefinedVariable = Class.new(Error) 54 | UndefinedDropMethod = Class.new(Error) 55 | UndefinedFilter = Class.new(Error) 56 | MethodOverrideError = Class.new(Error) 57 | InternalError = Class.new(Error) 58 | end 59 | -------------------------------------------------------------------------------- /example/server/templates/products.liquid: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | products 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% assign all_products = products | concat: more_products %} 20 |

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

21 | 22 |

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

23 | 24 |

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

25 | 26 | {% if cool_products %} 27 | Cool products :) 28 | {% else %} 29 | Uncool products :( 30 | {% endif %} 31 | 32 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/server/example_servlet.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProductsFilter 4 | def price(integer) 5 | format("$%.2d USD", integer / 100.0) 6 | end 7 | 8 | def prettyprint(text) 9 | text.gsub(/\*(.*)\*/, '\1') 10 | end 11 | 12 | def count(array) 13 | array.size 14 | end 15 | 16 | def paragraph(p) 17 | "

#{p}

" 18 | end 19 | end 20 | 21 | class Servlet < LiquidServlet 22 | def index 23 | { 'date' => Time.now } 24 | end 25 | 26 | def products 27 | { 'products' => products_list, 'more_products' => more_products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true } 28 | end 29 | 30 | private 31 | 32 | def products_list 33 | [{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' }, 34 | { 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' }, 35 | { 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }] 36 | end 37 | 38 | def more_products_list 39 | [{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' }, 40 | { 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }] 41 | end 42 | 43 | def description 44 | "List of Products ~ This is a list of products with price and description." 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2019-09-11 06:34:25 +1000 using RuboCop version 0.74.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 2 10 | # Cop supports --auto-correct. 11 | # Configuration parameters: EnforcedStyle. 12 | # SupportedStyles: runtime_error, standard_error 13 | Lint/InheritException: 14 | Exclude: 15 | - 'lib/liquid/interrupts.rb' 16 | 17 | # Offense count: 98 18 | # Cop supports --auto-correct. 19 | # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 20 | # URISchemes: http, https 21 | Layout/LineLength: 22 | Max: 294 23 | 24 | # Offense count: 44 25 | Naming/ConstantName: 26 | Exclude: 27 | - 'lib/liquid.rb' 28 | - 'lib/liquid/block_body.rb' 29 | - 'lib/liquid/tags/assign.rb' 30 | - 'lib/liquid/tags/capture.rb' 31 | - 'lib/liquid/tags/case.rb' 32 | - 'lib/liquid/tags/cycle.rb' 33 | - 'lib/liquid/tags/for.rb' 34 | - 'lib/liquid/tags/if.rb' 35 | - 'lib/liquid/tags/include.rb' 36 | - 'lib/liquid/tags/raw.rb' 37 | - 'lib/liquid/tags/table_row.rb' 38 | - 'lib/liquid/variable.rb' 39 | - 'performance/shopify/comment_form.rb' 40 | - 'performance/shopify/paginate.rb' 41 | - 'test/integration/tags/include_tag_test.rb' 42 | 43 | # Offense count: 5 44 | Style/ClassVars: 45 | Exclude: 46 | - 'lib/liquid/condition.rb' 47 | - 'lib/liquid/strainer.rb' 48 | - 'lib/liquid/template.rb' -------------------------------------------------------------------------------- /test/integration/capture_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class CaptureTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_captures_block_content_in_variable 9 | assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {}) 10 | end 11 | 12 | def test_capture_with_hyphen_in_variable_name 13 | template_source = <<-END_TEMPLATE 14 | {% capture this-thing %}Print this-thing{% endcapture %} 15 | {{ this-thing }} 16 | END_TEMPLATE 17 | template = Template.parse(template_source) 18 | rendered = template.render! 19 | assert_equal("Print this-thing", rendered.strip) 20 | end 21 | 22 | def test_capture_to_variable_from_outer_scope_if_existing 23 | template_source = <<-END_TEMPLATE 24 | {% assign var = '' %} 25 | {% if true %} 26 | {% capture var %}first-block-string{% endcapture %} 27 | {% endif %} 28 | {% if true %} 29 | {% capture var %}test-string{% endcapture %} 30 | {% endif %} 31 | {{var}} 32 | END_TEMPLATE 33 | template = Template.parse(template_source) 34 | rendered = template.render! 35 | assert_equal("test-string", rendered.gsub(/\s/, '')) 36 | end 37 | 38 | def test_assigning_from_capture 39 | template_source = <<-END_TEMPLATE 40 | {% assign first = '' %} 41 | {% assign second = '' %} 42 | {% for number in (1..3) %} 43 | {% capture first %}{{number}}{% endcapture %} 44 | {% assign second = first %} 45 | {% endfor %} 46 | {{ first }}-{{ second }} 47 | END_TEMPLATE 48 | template = Template.parse(template_source) 49 | rendered = template.render! 50 | assert_equal("3-3", rendered.gsub(/\s/, '')) 51 | end 52 | end # CaptureTest 53 | -------------------------------------------------------------------------------- /lib/liquid/strainer_template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'set' 4 | 5 | module Liquid 6 | # StrainerTemplate is the computed class for the filters system. 7 | # New filters are mixed into the strainer class which is then instantiated for each liquid template render run. 8 | # 9 | # The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter, 10 | # Context#add_filters or Template.register_filter 11 | class StrainerTemplate 12 | def initialize(context) 13 | @context = context 14 | end 15 | 16 | class << self 17 | def add_filter(filter) 18 | return if include?(filter) 19 | 20 | invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) } 21 | if invokable_non_public_methods.any? 22 | raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}" 23 | end 24 | 25 | include(filter) 26 | 27 | filter_methods.merge(filter.public_instance_methods.map(&:to_s)) 28 | end 29 | 30 | def invokable?(method) 31 | filter_methods.include?(method.to_s) 32 | end 33 | 34 | private 35 | 36 | def filter_methods 37 | @filter_methods ||= Set.new 38 | end 39 | end 40 | 41 | def invoke(method, *args) 42 | if self.class.invokable?(method) 43 | send(method, *args) 44 | elsif @context.strict_filters 45 | raise Liquid::UndefinedFilter, "undefined filter #{method}" 46 | else 47 | args.first 48 | end 49 | rescue ::ArgumentError => e 50 | raise Liquid::ArgumentError, e.message, e.backtrace 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/liquid/lexer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "strscan" 4 | module Liquid 5 | class Lexer 6 | SPECIALS = { 7 | '|' => :pipe, 8 | '.' => :dot, 9 | ':' => :colon, 10 | ',' => :comma, 11 | '[' => :open_square, 12 | ']' => :close_square, 13 | '(' => :open_round, 14 | ')' => :close_round, 15 | '?' => :question, 16 | '-' => :dash, 17 | }.freeze 18 | IDENTIFIER = /[a-zA-Z_][\w-]*\??/ 19 | SINGLE_STRING_LITERAL = /'[^\']*'/ 20 | DOUBLE_STRING_LITERAL = /"[^\"]*"/ 21 | NUMBER_LITERAL = /-?\d+(\.\d+)?/ 22 | DOTDOT = /\.\./ 23 | COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/ 24 | WHITESPACE_OR_NOTHING = /\s*/ 25 | 26 | def initialize(input) 27 | @ss = StringScanner.new(input) 28 | end 29 | 30 | def tokenize 31 | @output = [] 32 | 33 | until @ss.eos? 34 | @ss.skip(WHITESPACE_OR_NOTHING) 35 | break if @ss.eos? 36 | tok = if (t = @ss.scan(COMPARISON_OPERATOR)) 37 | [:comparison, t] 38 | elsif (t = @ss.scan(SINGLE_STRING_LITERAL)) 39 | [:string, t] 40 | elsif (t = @ss.scan(DOUBLE_STRING_LITERAL)) 41 | [:string, t] 42 | elsif (t = @ss.scan(NUMBER_LITERAL)) 43 | [:number, t] 44 | elsif (t = @ss.scan(IDENTIFIER)) 45 | [:id, t] 46 | elsif (t = @ss.scan(DOTDOT)) 47 | [:dotdot, t] 48 | else 49 | c = @ss.getch 50 | if (s = SPECIALS[c]) 51 | [s, c] 52 | else 53 | raise SyntaxError, "Unexpected character #{c}" 54 | end 55 | end 56 | @output << tok 57 | end 58 | 59 | @output << [:end_of_string] 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /performance/shopify/database.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | module Database 6 | # Load the standard vision toolkit database and re-arrage it to be simply exportable 7 | # to liquid as assigns. All this is based on Shopify 8 | def self.tables 9 | @tables ||= begin 10 | db = YAML.load_file("#{__dir__}/vision.database.yml") 11 | 12 | # From vision source 13 | db['products'].each do |product| 14 | collections = db['collections'].find_all do |collection| 15 | collection['products'].any? { |p| p['id'].to_i == product['id'].to_i } 16 | end 17 | product['collections'] = collections 18 | end 19 | 20 | # key the tables by handles, as this is how liquid expects it. 21 | db = db.each_with_object({}) do |(key, values), assigns| 22 | assigns[key] = values.each_with_object({}) do |v, h| 23 | h[v['handle']] = v 24 | end 25 | end 26 | 27 | # Some standard direct accessors so that the specialized templates 28 | # render correctly 29 | db['collection'] = db['collections'].values.first 30 | db['product'] = db['products'].values.first 31 | db['blog'] = db['blogs'].values.first 32 | db['article'] = db['blog']['articles'].first 33 | 34 | db['cart'] = { 35 | 'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] }, 36 | 'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] }, 37 | 'items' => db['line_items'].values, 38 | } 39 | 40 | db 41 | end 42 | end 43 | end 44 | 45 | if __FILE__ == $PROGRAM_NAME 46 | p(Database.tables['collections']['frontpage'].keys) 47 | # p Database.tables['blog']['articles'] 48 | end 49 | -------------------------------------------------------------------------------- /test/unit/regexp_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class RegexpUnitTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_empty 9 | assert_equal([], ''.scan(QuotedFragment)) 10 | end 11 | 12 | def test_quote 13 | assert_equal(['"arg 1"'], '"arg 1"'.scan(QuotedFragment)) 14 | end 15 | 16 | def test_words 17 | assert_equal(['arg1', 'arg2'], 'arg1 arg2'.scan(QuotedFragment)) 18 | end 19 | 20 | def test_tags 21 | assert_equal(['', ''], ' '.scan(QuotedFragment)) 22 | assert_equal([''], ''.scan(QuotedFragment)) 23 | assert_equal(['', ''], %().scan(QuotedFragment)) 24 | end 25 | 26 | def test_double_quoted_words 27 | assert_equal(['arg1', 'arg2', '"arg 3"'], 'arg1 arg2 "arg 3"'.scan(QuotedFragment)) 28 | end 29 | 30 | def test_single_quoted_words 31 | assert_equal(['arg1', 'arg2', "'arg 3'"], 'arg1 arg2 \'arg 3\''.scan(QuotedFragment)) 32 | end 33 | 34 | def test_quoted_words_in_the_middle 35 | assert_equal(['arg1', 'arg2', '"arg 3"', 'arg4'], 'arg1 arg2 "arg 3" arg4 '.scan(QuotedFragment)) 36 | end 37 | 38 | def test_variable_parser 39 | assert_equal(['var'], 'var'.scan(VariableParser)) 40 | assert_equal(['var', 'method'], 'var.method'.scan(VariableParser)) 41 | assert_equal(['var', '[method]'], 'var[method]'.scan(VariableParser)) 42 | assert_equal(['var', '[method]', '[0]'], 'var[method][0]'.scan(VariableParser)) 43 | assert_equal(['var', '["method"]', '[0]'], 'var["method"][0]'.scan(VariableParser)) 44 | assert_equal(['var', '[method]', '[0]', 'method'], 'var[method][0].method'.scan(VariableParser)) 45 | end 46 | end # RegexpTest 47 | -------------------------------------------------------------------------------- /lib/liquid/tag.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class Tag 5 | attr_reader :nodelist, :tag_name, :line_number, :parse_context 6 | alias_method :options, :parse_context 7 | include ParserSwitching 8 | 9 | class << self 10 | def parse(tag_name, markup, tokenizer, parse_context) 11 | tag = new(tag_name, markup, parse_context) 12 | tag.parse(tokenizer) 13 | tag 14 | end 15 | 16 | def disable_tags(*tags) 17 | disabled_tags.push(*tags) 18 | end 19 | 20 | private :new 21 | 22 | def disabled_tags 23 | @disabled_tags ||= [] 24 | end 25 | end 26 | 27 | def initialize(tag_name, markup, parse_context) 28 | @tag_name = tag_name 29 | @markup = markup 30 | @parse_context = parse_context 31 | @line_number = parse_context.line_number 32 | end 33 | 34 | def parse(_tokens) 35 | end 36 | 37 | def raw 38 | "#{@tag_name} #{@markup}" 39 | end 40 | 41 | def name 42 | self.class.name.downcase 43 | end 44 | 45 | def render(_context) 46 | '' 47 | end 48 | 49 | def disabled?(context) 50 | context.registers[:disabled_tags].disabled?(tag_name) 51 | end 52 | 53 | def disabled_error_message 54 | "#{tag_name} #{options[:locale].t('errors.disabled.tag')}" 55 | end 56 | 57 | # For backwards compatibility with custom tags. In a future release, the semantics 58 | # of the `render_to_output_buffer` method will become the default and the `render` 59 | # method will be removed. 60 | def render_to_output_buffer(context, output) 61 | output << render(context) 62 | output 63 | end 64 | 65 | def blank? 66 | false 67 | end 68 | 69 | def disabled_tags 70 | self.class.disabled_tags 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/unit/lexer_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class LexerUnitTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_strings 9 | tokens = Lexer.new(%( 'this is a test""' "wat 'lol'")).tokenize 10 | assert_equal([[:string, %('this is a test""')], [:string, %("wat 'lol'")], [:end_of_string]], tokens) 11 | end 12 | 13 | def test_integer 14 | tokens = Lexer.new('hi 50').tokenize 15 | assert_equal([[:id, 'hi'], [:number, '50'], [:end_of_string]], tokens) 16 | end 17 | 18 | def test_float 19 | tokens = Lexer.new('hi 5.0').tokenize 20 | assert_equal([[:id, 'hi'], [:number, '5.0'], [:end_of_string]], tokens) 21 | end 22 | 23 | def test_comparison 24 | tokens = Lexer.new('== <> contains ').tokenize 25 | assert_equal([[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens) 26 | end 27 | 28 | def test_specials 29 | tokens = Lexer.new('| .:').tokenize 30 | assert_equal([[:pipe, '|'], [:dot, '.'], [:colon, ':'], [:end_of_string]], tokens) 31 | tokens = Lexer.new('[,]').tokenize 32 | assert_equal([[:open_square, '['], [:comma, ','], [:close_square, ']'], [:end_of_string]], tokens) 33 | end 34 | 35 | def test_fancy_identifiers 36 | tokens = Lexer.new('hi five?').tokenize 37 | assert_equal([[:id, 'hi'], [:id, 'five?'], [:end_of_string]], tokens) 38 | 39 | tokens = Lexer.new('2foo').tokenize 40 | assert_equal([[:number, '2'], [:id, 'foo'], [:end_of_string]], tokens) 41 | end 42 | 43 | def test_whitespace 44 | tokens = Lexer.new("five|\n\t ==").tokenize 45 | assert_equal([[:id, 'five'], [:pipe, '|'], [:comparison, '=='], [:end_of_string]], tokens) 46 | end 47 | 48 | def test_unexpected_character 49 | assert_raises(SyntaxError) do 50 | Lexer.new("%").tokenize 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /performance/memory_profile.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'benchmark/ips' 4 | require 'memory_profiler' 5 | require 'terminal-table' 6 | require_relative 'theme_runner' 7 | 8 | class Profiler 9 | LOG_LABEL = "Profiling: ".rjust(14).freeze 10 | REPORTS_DIR = File.expand_path('.memprof', __dir__).freeze 11 | 12 | def self.run 13 | puts 14 | yield new 15 | end 16 | 17 | def initialize 18 | @allocated = [] 19 | @retained = [] 20 | @headings = [] 21 | end 22 | 23 | def profile(phase, &block) 24 | print(LOG_LABEL) 25 | print("#{phase}.. ".ljust(10)) 26 | report = MemoryProfiler.report(&block) 27 | puts 'Done.' 28 | @headings << phase.capitalize 29 | @allocated << "#{report.scale_bytes(report.total_allocated_memsize)} (#{report.total_allocated} objects)" 30 | @retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)" 31 | 32 | return if ENV['CI'] 33 | 34 | require 'fileutils' 35 | report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt") 36 | FileUtils.mkdir_p(REPORTS_DIR) 37 | report.pretty_print(to_file: report_file, scale_bytes: true) 38 | end 39 | 40 | def tabulate 41 | table = Terminal::Table.new(headings: @headings.unshift('Phase')) do |t| 42 | t << @allocated.unshift('Total allocated') 43 | t << @retained.unshift('Total retained') 44 | end 45 | 46 | puts 47 | puts table 48 | puts "\nDetailed report(s) saved to #{REPORTS_DIR}/" unless ENV['CI'] 49 | end 50 | 51 | def sanitize(string) 52 | string.downcase.gsub(/[\W]/, '-').squeeze('-') 53 | end 54 | end 55 | 56 | Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first 57 | 58 | runner = ThemeRunner.new 59 | Profiler.run do |x| 60 | x.profile('parse') { runner.compile } 61 | x.profile('render') { runner.render } 62 | x.tabulate 63 | end 64 | -------------------------------------------------------------------------------- /test/unit/tag_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class TagUnitTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_tag 9 | tag = Tag.parse('tag', "", Tokenizer.new(""), ParseContext.new) 10 | assert_equal('liquid::tag', tag.name) 11 | assert_equal('', tag.render(Context.new)) 12 | end 13 | 14 | def test_return_raw_text_of_tag 15 | tag = Tag.parse("long_tag", "param1, param2, param3", Tokenizer.new(""), ParseContext.new) 16 | assert_equal("long_tag param1, param2, param3", tag.raw) 17 | end 18 | 19 | def test_tag_name_should_return_name_of_the_tag 20 | tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new) 21 | assert_equal('some_tag', tag.tag_name) 22 | end 23 | 24 | def test_custom_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility 25 | klass1 = Class.new(Tag) do 26 | def render(*) 27 | 'hello' 28 | end 29 | end 30 | 31 | with_custom_tag('blabla', klass1) do 32 | template = Liquid::Template.parse("{% blabla %}") 33 | 34 | assert_equal 'hello', template.render 35 | 36 | buf = +'' 37 | output = template.render({}, output: buf) 38 | assert_equal 'hello', output 39 | assert_equal 'hello', buf 40 | assert_equal buf.object_id, output.object_id 41 | end 42 | 43 | klass2 = Class.new(klass1) do 44 | def render(*) 45 | 'foo' + super + 'bar' 46 | end 47 | end 48 | 49 | with_custom_tag('blabla', klass2) do 50 | template = Liquid::Template.parse("{% blabla %}") 51 | 52 | assert_equal 'foohellobar', template.render 53 | 54 | buf = +'' 55 | output = template.render({}, output: buf) 56 | assert_equal 'foohellobar', output 57 | assert_equal 'foohellobar', buf 58 | assert_equal buf.object_id, output.object_id 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /performance/tests/dropify/index.liquid: -------------------------------------------------------------------------------- 1 |
2 |

Featured Items

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

{{ product.title }}

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

{{ product.price_min | money }}

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

{{ product.title }}

15 |

{{ product.price_min | money }}

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

{{ article.title }}

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

{{ article.title }}

41 |
42 | {{ article.content }} 43 |
44 |
45 | {% endfor %} 46 |
47 | 48 | -------------------------------------------------------------------------------- /test/unit/tokenizer_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class TokenizerTest < Minitest::Test 6 | def test_tokenize_strings 7 | assert_equal([' '], tokenize(' ')) 8 | assert_equal(['hello world'], tokenize('hello world')) 9 | end 10 | 11 | def test_tokenize_variables 12 | assert_equal(['{{funk}}'], tokenize('{{funk}}')) 13 | assert_equal([' ', '{{funk}}', ' '], tokenize(' {{funk}} ')) 14 | assert_equal([' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '], tokenize(' {{funk}} {{so}} {{brother}} ')) 15 | assert_equal([' ', '{{ funk }}', ' '], tokenize(' {{ funk }} ')) 16 | end 17 | 18 | def test_tokenize_blocks 19 | assert_equal(['{%comment%}'], tokenize('{%comment%}')) 20 | assert_equal([' ', '{%comment%}', ' '], tokenize(' {%comment%} ')) 21 | 22 | assert_equal([' ', '{%comment%}', ' ', '{%endcomment%}', ' '], tokenize(' {%comment%} {%endcomment%} ')) 23 | assert_equal([' ', '{% comment %}', ' ', '{% endcomment %}', ' '], tokenize(" {% comment %} {% endcomment %} ")) 24 | end 25 | 26 | def test_calculate_line_numbers_per_token_with_profiling 27 | assert_equal([1], tokenize_line_numbers("{{funk}}")) 28 | assert_equal([1, 1, 1], tokenize_line_numbers(" {{funk}} ")) 29 | assert_equal([1, 2, 2], tokenize_line_numbers("\n{{funk}}\n")) 30 | assert_equal([1, 1, 3], tokenize_line_numbers(" {{\n funk \n}} ")) 31 | end 32 | 33 | private 34 | 35 | def tokenize(source) 36 | tokenizer = Liquid::Tokenizer.new(source) 37 | tokens = [] 38 | while (t = tokenizer.shift) 39 | tokens << t 40 | end 41 | tokens 42 | end 43 | 44 | def tokenize_line_numbers(source) 45 | tokenizer = Liquid::Tokenizer.new(source, true) 46 | line_numbers = [] 47 | loop do 48 | line_number = tokenizer.line_number 49 | if tokenizer.shift 50 | line_numbers << line_number 51 | else 52 | break 53 | end 54 | end 55 | line_numbers 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/liquid/locales/en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | errors: 3 | syntax: 4 | tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}" 5 | assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]" 6 | capture: "Syntax Error in 'capture' - Valid syntax: capture [var]" 7 | case: "Syntax Error in 'case' - Valid syntax: case [condition]" 8 | case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}" 9 | case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) " 10 | cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]" 11 | for: "Syntax Error in 'for loop' - Valid syntax: for [item[, item_2, ...]] in [collection]" 12 | for_invalid_in: "For loops require an 'in' clause" 13 | for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset" 14 | if: "Syntax Error in tag 'if' - Valid syntax: if [expression]" 15 | include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]" 16 | unknown_tag: "Unknown tag '%{tag}'" 17 | invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}" 18 | unexpected_else: "%{block_name} tag does not expect 'else' tag" 19 | unexpected_outer_tag: "Unexpected outer '%{tag}' tag" 20 | tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}" 21 | variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}" 22 | tag_never_closed: "'%{block_name}' tag was never closed" 23 | meta_syntax_error: "Liquid syntax error: #{e.message}" 24 | table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3" 25 | render: "Syntax error in tag 'render' - Template name must be a quoted string" 26 | argument: 27 | include: "Argument error in tag 'include' - Illegal template name" 28 | disabled: 29 | tag: "usage is not allowed in this context" 30 | -------------------------------------------------------------------------------- /lib/liquid/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | module Utils 5 | def self.slice_collection(collection, from, to) 6 | if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice) 7 | collection.load_slice(from, to) 8 | else 9 | slice_collection_using_each(collection, from, to) 10 | end 11 | end 12 | 13 | def self.slice_collection_using_each(collection, from, to) 14 | segments = [] 15 | index = 0 16 | 17 | # Maintains Ruby 1.8.7 String#each behaviour on 1.9 18 | if collection.is_a?(String) 19 | return collection.empty? ? [] : [collection] 20 | end 21 | return [] unless collection.respond_to?(:each) 22 | 23 | collection.each do |item| 24 | if to && to <= index 25 | break 26 | end 27 | 28 | if from <= index 29 | segments << item 30 | end 31 | 32 | index += 1 33 | end 34 | 35 | segments 36 | end 37 | 38 | def self.to_integer(num) 39 | return num if num.is_a?(Integer) 40 | num = num.to_s 41 | begin 42 | Integer(num) 43 | rescue ::ArgumentError 44 | raise Liquid::ArgumentError, "invalid integer" 45 | end 46 | end 47 | 48 | def self.to_number(obj) 49 | case obj 50 | when Float 51 | BigDecimal(obj.to_s) 52 | when Numeric 53 | obj 54 | when String 55 | /\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i 56 | else 57 | if obj.respond_to?(:to_number) 58 | obj.to_number 59 | else 60 | 0 61 | end 62 | end 63 | end 64 | 65 | def self.to_date(obj) 66 | return obj if obj.respond_to?(:strftime) 67 | 68 | if obj.is_a?(String) 69 | return nil if obj.empty? 70 | obj = obj.downcase 71 | end 72 | 73 | case obj 74 | when 'now', 'today' 75 | Time.now 76 | when /\A\d+\z/, Integer 77 | Time.at(obj.to_i) 78 | when String 79 | Time.parse(obj) 80 | end 81 | rescue ::ArgumentError 82 | nil 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/liquid/tags/table_row.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class TableRow < Block 5 | Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o 6 | 7 | attr_reader :variable_name, :collection_name, :attributes 8 | 9 | def initialize(tag_name, markup, options) 10 | super 11 | if markup =~ Syntax 12 | @variable_name = Regexp.last_match(1) 13 | @collection_name = Expression.parse(Regexp.last_match(2)) 14 | @attributes = {} 15 | markup.scan(TagAttributes) do |key, value| 16 | @attributes[key] = Expression.parse(value) 17 | end 18 | else 19 | raise SyntaxError, options[:locale].t("errors.syntax.table_row") 20 | end 21 | end 22 | 23 | def render_to_output_buffer(context, output) 24 | (collection = context.evaluate(@collection_name)) || (return '') 25 | 26 | from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0 27 | to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil 28 | 29 | collection = Utils.slice_collection(collection, from, to) 30 | length = collection.length 31 | 32 | cols = context.evaluate(@attributes['cols']).to_i 33 | 34 | output << "\n" 35 | context.stack do 36 | tablerowloop = Liquid::TablerowloopDrop.new(length, cols) 37 | context['tablerowloop'] = tablerowloop 38 | 39 | collection.each do |item| 40 | context[@variable_name] = item 41 | 42 | output << "" 43 | super 44 | output << '' 45 | 46 | if tablerowloop.col_last && !tablerowloop.last 47 | output << "\n" 48 | end 49 | 50 | tablerowloop.send(:increment!) 51 | end 52 | end 53 | 54 | output << "\n" 55 | output 56 | end 57 | 58 | class ParseTreeVisitor < Liquid::ParseTreeVisitor 59 | def children 60 | super + @node.attributes.values + [@node.collection_name] 61 | end 62 | end 63 | end 64 | 65 | Template.register_tag('tablerow', TableRow) 66 | end 67 | -------------------------------------------------------------------------------- /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 | 52 |
53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /lib/liquid/block.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class Block < Tag 5 | MAX_DEPTH = 100 6 | 7 | def initialize(tag_name, markup, options) 8 | super 9 | @blank = true 10 | end 11 | 12 | def parse(tokens) 13 | @body = BlockBody.new 14 | while parse_body(@body, tokens) 15 | end 16 | end 17 | 18 | # For backwards compatibility 19 | def render(context) 20 | @body.render(context) 21 | end 22 | 23 | def blank? 24 | @blank 25 | end 26 | 27 | def nodelist 28 | @body.nodelist 29 | end 30 | 31 | def unknown_tag(tag, _params, _tokens) 32 | if tag == 'else' 33 | raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else", 34 | block_name: block_name) 35 | elsif tag.start_with?('end') 36 | raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter", 37 | tag: tag, 38 | block_name: block_name, 39 | block_delimiter: block_delimiter) 40 | else 41 | raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag) 42 | end 43 | end 44 | 45 | def block_name 46 | @tag_name 47 | end 48 | 49 | def block_delimiter 50 | @block_delimiter ||= "end#{block_name}" 51 | end 52 | 53 | protected 54 | 55 | def parse_body(body, tokens) 56 | if parse_context.depth >= MAX_DEPTH 57 | raise StackLevelError, "Nesting too deep" 58 | end 59 | parse_context.depth += 1 60 | begin 61 | body.parse(tokens, parse_context) do |end_tag_name, end_tag_params| 62 | @blank &&= body.blank? 63 | 64 | return false if end_tag_name == block_delimiter 65 | unless end_tag_name 66 | raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name) 67 | end 68 | 69 | # this tag is not registered with the system 70 | # pass it to the current block for special handling or error reporting 71 | unknown_tag(end_tag_name, end_tag_params, tokens) 72 | end 73 | ensure 74 | parse_context.depth -= 1 75 | end 76 | 77 | true 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /performance/tests/ripen/cart.liquid: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 | {% if cart.item_count == 0 %} 11 |

Your shopping cart is empty...

12 |

Continue shopping

13 | {% else %} 14 | 15 |

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

38 |

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

41 |

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

- or -

46 | {{ content_for_additional_checkout_buttons }} 47 |
48 | {% endif %} 49 | 50 |
51 | 52 | {% endif %} 53 | 54 |
55 | -------------------------------------------------------------------------------- /lib/liquid/tags/cycle.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | # Cycle is usually used within a loop to alternate between values, like colors or DOM classes. 5 | # 6 | # {% for item in items %} 7 | #
{{ item }}
8 | # {% end %} 9 | # 10 | #
Item one
11 | #
Item two
12 | #
Item three
13 | #
Item four
14 | #
Item five
15 | # 16 | class Cycle < Tag 17 | SimpleSyntax = /\A#{QuotedFragment}+/o 18 | NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om 19 | 20 | attr_reader :variables 21 | 22 | def initialize(tag_name, markup, options) 23 | super 24 | case markup 25 | when NamedSyntax 26 | @variables = variables_from_string(Regexp.last_match(2)) 27 | @name = Expression.parse(Regexp.last_match(1)) 28 | when SimpleSyntax 29 | @variables = variables_from_string(markup) 30 | @name = @variables.to_s 31 | else 32 | raise SyntaxError, options[:locale].t("errors.syntax.cycle") 33 | end 34 | end 35 | 36 | def render_to_output_buffer(context, output) 37 | context.registers[:cycle] ||= {} 38 | 39 | key = context.evaluate(@name) 40 | iteration = context.registers[:cycle][key].to_i 41 | 42 | val = context.evaluate(@variables[iteration]) 43 | 44 | if val.is_a?(Array) 45 | val = val.join 46 | elsif !val.is_a?(String) 47 | val = val.to_s 48 | end 49 | 50 | output << val 51 | 52 | iteration += 1 53 | iteration = 0 if iteration >= @variables.size 54 | 55 | context.registers[:cycle][key] = iteration 56 | output 57 | end 58 | 59 | private 60 | 61 | def variables_from_string(markup) 62 | markup.split(',').collect do |var| 63 | var =~ /\s*(#{QuotedFragment})\s*/o 64 | Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil 65 | end.compact 66 | end 67 | 68 | class ParseTreeVisitor < Liquid::ParseTreeVisitor 69 | def children 70 | Array(@node.variables) 71 | end 72 | end 73 | end 74 | 75 | Template.register_tag('cycle', Cycle) 76 | end 77 | -------------------------------------------------------------------------------- /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 | 52 |
53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /lib/liquid/drop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'set' 4 | 5 | module Liquid 6 | # A drop in liquid is a class which allows you to export DOM like things to liquid. 7 | # Methods of drops are callable. 8 | # The main use for liquid drops is to implement lazy loaded objects. 9 | # If you would like to make data available to the web designers which you don't want loaded unless needed then 10 | # a drop is a great way to do that. 11 | # 12 | # Example: 13 | # 14 | # class ProductDrop < Liquid::Drop 15 | # def top_sales 16 | # Shop.current.products.find(:all, :order => 'sales', :limit => 10 ) 17 | # end 18 | # end 19 | # 20 | # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' ) 21 | # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query. 22 | # 23 | # Your drop can either implement the methods sans any parameters 24 | # or implement the liquid_method_missing(name) method which is a catch all. 25 | class Drop 26 | attr_writer :context 27 | 28 | # Catch all for the method 29 | def liquid_method_missing(method) 30 | return nil unless @context&.strict_variables 31 | raise Liquid::UndefinedDropMethod, "undefined method #{method}" 32 | end 33 | 34 | # called by liquid to invoke a drop 35 | def invoke_drop(method_or_key) 36 | if self.class.invokable?(method_or_key) 37 | send(method_or_key) 38 | else 39 | liquid_method_missing(method_or_key) 40 | end 41 | end 42 | 43 | def 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_method :[], :invoke_drop 60 | 61 | # Check for method existence without invoking respond_to?, which creates symbols 62 | def self.invokable?(method_name) 63 | invokable_methods.include?(method_name.to_s) 64 | end 65 | 66 | def self.invokable_methods 67 | @invokable_methods ||= begin 68 | blacklist = Liquid::Drop.public_instance_methods + [:each] 69 | 70 | if include?(Enumerable) 71 | blacklist += Enumerable.public_instance_methods 72 | blacklist -= [:sort, :count, :first, :min, :max] 73 | end 74 | 75 | whitelist = [:to_liquid] + (public_instance_methods - blacklist) 76 | Set.new(whitelist.map(&:to_s)) 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/liquid/tags/case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class Case < Block 5 | Syntax = /(#{QuotedFragment})/o 6 | WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om 7 | 8 | attr_reader :blocks, :left 9 | 10 | def initialize(tag_name, markup, options) 11 | super 12 | @blocks = [] 13 | 14 | if markup =~ Syntax 15 | @left = Expression.parse(Regexp.last_match(1)) 16 | else 17 | raise SyntaxError, options[:locale].t("errors.syntax.case") 18 | end 19 | end 20 | 21 | def parse(tokens) 22 | body = BlockBody.new 23 | body = @blocks.last.attachment while parse_body(body, tokens) 24 | end 25 | 26 | def nodelist 27 | @blocks.map(&:attachment) 28 | end 29 | 30 | def unknown_tag(tag, markup, tokens) 31 | case tag 32 | when 'when' 33 | record_when_condition(markup) 34 | when 'else' 35 | record_else_condition(markup) 36 | else 37 | super 38 | end 39 | end 40 | 41 | def render_to_output_buffer(context, output) 42 | execute_else_block = true 43 | 44 | @blocks.each do |block| 45 | if block.else? 46 | block.attachment.render_to_output_buffer(context, output) if execute_else_block 47 | elsif block.evaluate(context) 48 | execute_else_block = false 49 | block.attachment.render_to_output_buffer(context, output) 50 | end 51 | end 52 | 53 | output 54 | end 55 | 56 | private 57 | 58 | def record_when_condition(markup) 59 | body = BlockBody.new 60 | 61 | while markup 62 | unless markup =~ WhenSyntax 63 | raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when") 64 | end 65 | 66 | markup = Regexp.last_match(2) 67 | 68 | block = Condition.new(@left, '==', Expression.parse(Regexp.last_match(1))) 69 | block.attach(body) 70 | @blocks << block 71 | end 72 | end 73 | 74 | def record_else_condition(markup) 75 | unless markup.strip.empty? 76 | raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else") 77 | end 78 | 79 | block = ElseCondition.new 80 | block.attach(BlockBody.new) 81 | @blocks << block 82 | end 83 | 84 | class ParseTreeVisitor < Liquid::ParseTreeVisitor 85 | def children 86 | [@node.left] + @node.blocks 87 | end 88 | end 89 | end 90 | 91 | Template.register_tag('case', Case) 92 | end 93 | -------------------------------------------------------------------------------- /lib/liquid/parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class Parser 5 | def initialize(input) 6 | l = Lexer.new(input) 7 | @tokens = l.tokenize 8 | @p = 0 # pointer to current location 9 | end 10 | 11 | def jump(point) 12 | @p = point 13 | end 14 | 15 | def consume(type = nil) 16 | token = @tokens[@p] 17 | if type && token[0] != type 18 | raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}" 19 | end 20 | @p += 1 21 | token[1] 22 | end 23 | 24 | # Only consumes the token if it matches the type 25 | # Returns the token's contents if it was consumed 26 | # or false otherwise. 27 | def consume?(type) 28 | token = @tokens[@p] 29 | return false unless token && token[0] == type 30 | @p += 1 31 | token[1] 32 | end 33 | 34 | # Like consume? Except for an :id token of a certain name 35 | def id?(str) 36 | token = @tokens[@p] 37 | return false unless token && token[0] == :id 38 | return false unless token[1] == str 39 | @p += 1 40 | token[1] 41 | end 42 | 43 | def look(type, ahead = 0) 44 | tok = @tokens[@p + ahead] 45 | return false unless tok 46 | tok[0] == type 47 | end 48 | 49 | SINGLE_TOKEN_EXPRESSION_TYPES = [:string, :number].freeze 50 | private_constant :SINGLE_TOKEN_EXPRESSION_TYPES 51 | 52 | def expression 53 | token = @tokens[@p] 54 | if token[0] == :id 55 | variable_signature 56 | elsif SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0]) 57 | consume 58 | elsif token.first == :open_round 59 | consume 60 | first = expression 61 | consume(:dotdot) 62 | last = expression 63 | consume(:close_round) 64 | "(#{first}..#{last})" 65 | else 66 | raise SyntaxError, "#{token} is not a valid expression" 67 | end 68 | end 69 | 70 | def argument 71 | str = +"" 72 | # might be a keyword argument (identifier: expression) 73 | if look(:id) && look(:colon, 1) 74 | str << consume << consume << ' ' 75 | end 76 | 77 | str << expression 78 | str 79 | end 80 | 81 | def variable_signature 82 | str = consume(:id) 83 | while look(:open_square) 84 | str << consume 85 | str << expression 86 | str << consume(:close_square) 87 | end 88 | if look(:dot) 89 | str << consume 90 | str << variable_signature 91 | end 92 | str 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/unit/strainer_template_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class StrainerTemplateUnitTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_add_filter_when_wrong_filter_class 9 | c = Context.new 10 | s = c.strainer 11 | wrong_filter = ->(v) { v.reverse } 12 | 13 | exception = assert_raises(TypeError) do 14 | s.class.add_filter(wrong_filter) 15 | end 16 | assert_equal(exception.message, "wrong argument type Proc (expected Module)") 17 | end 18 | 19 | module PrivateMethodOverrideFilter 20 | private 21 | 22 | def public_filter 23 | "overriden as private" 24 | end 25 | end 26 | 27 | def test_add_filter_raises_when_module_privately_overrides_registered_public_methods 28 | strainer = Context.new.strainer 29 | 30 | error = assert_raises(Liquid::MethodOverrideError) do 31 | strainer.class.add_filter(PrivateMethodOverrideFilter) 32 | end 33 | assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message) 34 | end 35 | 36 | module ProtectedMethodOverrideFilter 37 | protected 38 | 39 | def public_filter 40 | "overriden as protected" 41 | end 42 | end 43 | 44 | def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected 45 | strainer = Context.new.strainer 46 | 47 | error = assert_raises(Liquid::MethodOverrideError) do 48 | strainer.class.add_filter(ProtectedMethodOverrideFilter) 49 | end 50 | assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message) 51 | end 52 | 53 | module PublicMethodOverrideFilter 54 | def public_filter 55 | "public" 56 | end 57 | end 58 | 59 | def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method 60 | strainer = Context.new.strainer 61 | with_global_filter do 62 | strainer.class.add_filter(PublicMethodOverrideFilter) 63 | assert(strainer.class.send(:filter_methods).include?('public_filter')) 64 | end 65 | end 66 | 67 | def test_add_filter_does_not_include_already_included_module 68 | mod = Module.new do 69 | class << self 70 | attr_accessor :include_count 71 | def included(_mod) 72 | self.include_count += 1 73 | end 74 | end 75 | self.include_count = 0 76 | end 77 | strainer = Context.new.strainer 78 | strainer.class.add_filter(mod) 79 | strainer.class.add_filter(mod) 80 | assert_equal(1, mod.include_count) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /performance/tests/dropify/cart.liquid: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 | {% if cart.item_count == 0 %} 11 |

Your shopping cart is looking rather empty...

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

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

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

- or -

58 | {{ content_for_additional_checkout_buttons }} 59 |
60 | {% endif %} 61 | 62 |
63 | 64 | {% endif %} 65 | 66 |
67 | -------------------------------------------------------------------------------- /test/integration/security_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module SecurityFilter 6 | def add_one(input) 7 | "#{input} + 1" 8 | end 9 | end 10 | 11 | class SecurityTest < Minitest::Test 12 | include Liquid 13 | 14 | def setup 15 | @assigns = {} 16 | end 17 | 18 | def test_no_instance_eval 19 | text = %( {{ '1+1' | instance_eval }} ) 20 | expected = %( 1+1 ) 21 | 22 | assert_equal(expected, Template.parse(text).render!(@assigns)) 23 | end 24 | 25 | def test_no_existing_instance_eval 26 | text = %( {{ '1+1' | __instance_eval__ }} ) 27 | expected = %( 1+1 ) 28 | 29 | assert_equal(expected, Template.parse(text).render!(@assigns)) 30 | end 31 | 32 | def test_no_instance_eval_after_mixing_in_new_filter 33 | text = %( {{ '1+1' | instance_eval }} ) 34 | expected = %( 1+1 ) 35 | 36 | assert_equal(expected, Template.parse(text).render!(@assigns)) 37 | end 38 | 39 | def test_no_instance_eval_later_in_chain 40 | text = %( {{ '1+1' | add_one | instance_eval }} ) 41 | expected = %( 1+1 + 1 ) 42 | 43 | assert_equal(expected, Template.parse(text).render!(@assigns, filters: SecurityFilter)) 44 | end 45 | 46 | def test_does_not_add_filters_to_symbol_table 47 | current_symbols = Symbol.all_symbols 48 | 49 | test = %( {{ "some_string" | a_bad_filter }} ) 50 | 51 | template = Template.parse(test) 52 | assert_equal([], (Symbol.all_symbols - current_symbols)) 53 | 54 | template.render! 55 | assert_equal([], (Symbol.all_symbols - current_symbols)) 56 | end 57 | 58 | def test_does_not_add_drop_methods_to_symbol_table 59 | current_symbols = Symbol.all_symbols 60 | 61 | assigns = { 'drop' => Drop.new } 62 | assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!) 63 | assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!) 64 | assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!) 65 | 66 | assert_equal([], (Symbol.all_symbols - current_symbols)) 67 | end 68 | 69 | def test_max_depth_nested_blocks_does_not_raise_exception 70 | depth = Liquid::Block::MAX_DEPTH 71 | code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth 72 | assert_equal("rendered", Template.parse(code).render!) 73 | end 74 | 75 | def test_more_than_max_depth_nested_blocks_raises_exception 76 | depth = Liquid::Block::MAX_DEPTH + 1 77 | code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth 78 | assert_raises(Liquid::StackLevelError) do 79 | Template.parse(code).render! 80 | end 81 | end 82 | end # SecurityTest 83 | -------------------------------------------------------------------------------- /test/unit/parser_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class ParserUnitTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_consume 9 | p = Parser.new("wat: 7") 10 | assert_equal('wat', p.consume(:id)) 11 | assert_equal(':', p.consume(:colon)) 12 | assert_equal('7', p.consume(:number)) 13 | end 14 | 15 | def test_jump 16 | p = Parser.new("wat: 7") 17 | p.jump(2) 18 | assert_equal('7', p.consume(:number)) 19 | end 20 | 21 | def test_consume? 22 | p = Parser.new("wat: 7") 23 | assert_equal('wat', p.consume?(:id)) 24 | assert_equal(false, p.consume?(:dot)) 25 | assert_equal(':', p.consume(:colon)) 26 | assert_equal('7', p.consume?(:number)) 27 | end 28 | 29 | def test_id? 30 | p = Parser.new("wat 6 Peter Hegemon") 31 | assert_equal('wat', p.id?('wat')) 32 | assert_equal(false, p.id?('endgame')) 33 | assert_equal('6', p.consume(:number)) 34 | assert_equal('Peter', p.id?('Peter')) 35 | assert_equal(false, p.id?('Achilles')) 36 | end 37 | 38 | def test_look 39 | p = Parser.new("wat 6 Peter Hegemon") 40 | assert_equal(true, p.look(:id)) 41 | assert_equal('wat', p.consume(:id)) 42 | assert_equal(false, p.look(:comparison)) 43 | assert_equal(true, p.look(:number)) 44 | assert_equal(true, p.look(:id, 1)) 45 | assert_equal(false, p.look(:number, 1)) 46 | end 47 | 48 | def test_expressions 49 | p = Parser.new("hi.there hi?[5].there? hi.there.bob") 50 | assert_equal('hi.there', p.expression) 51 | assert_equal('hi?[5].there?', p.expression) 52 | assert_equal('hi.there.bob', p.expression) 53 | 54 | p = Parser.new("567 6.0 'lol' \"wut\"") 55 | assert_equal('567', p.expression) 56 | assert_equal('6.0', p.expression) 57 | assert_equal("'lol'", p.expression) 58 | assert_equal('"wut"', p.expression) 59 | end 60 | 61 | def test_ranges 62 | p = Parser.new("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)") 63 | assert_equal('(5..7)', p.expression) 64 | assert_equal('(1.5..9.6)', p.expression) 65 | assert_equal('(young..old)', p.expression) 66 | assert_equal('(hi[5].wat..old)', p.expression) 67 | end 68 | 69 | def test_arguments 70 | p = Parser.new("filter: hi.there[5], keyarg: 7") 71 | assert_equal('filter', p.consume(:id)) 72 | assert_equal(':', p.consume(:colon)) 73 | assert_equal('hi.there[5]', p.argument) 74 | assert_equal(',', p.consume(:comma)) 75 | assert_equal('keyarg: 7', p.argument) 76 | end 77 | 78 | def test_invalid_expression 79 | assert_raises(SyntaxError) do 80 | p = Parser.new("==") 81 | p.expression 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /performance/tests/vogue/article.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{ article.title }}

3 |

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

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

Comments

12 | 13 | 14 | 27 | 28 | 29 | {% form article %} 30 |

Leave a comment

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

comments have to be approved before showing up

61 | {% endif %} 62 | 63 | 64 | {% endform %} 65 |
66 | {% endif %} 67 | -------------------------------------------------------------------------------- /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 | 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 | -------------------------------------------------------------------------------- /test/unit/template_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class TemplateUnitTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_sets_default_localization_in_document 9 | t = Template.new 10 | t.parse('{%comment%}{%endcomment%}') 11 | assert_instance_of(I18n, t.root.nodelist[0].options[:locale]) 12 | end 13 | 14 | def test_sets_default_localization_in_context_with_quick_initialization 15 | t = Template.new 16 | t.parse('{%comment%}{%endcomment%}', locale: I18n.new(fixture("en_locale.yml"))) 17 | 18 | locale = t.root.nodelist[0].options[:locale] 19 | assert_instance_of(I18n, locale) 20 | assert_equal(fixture("en_locale.yml"), locale.path) 21 | end 22 | 23 | def test_with_cache_classes_tags_returns_the_same_class 24 | original_cache_setting = Liquid.cache_classes 25 | Liquid.cache_classes = true 26 | 27 | original_klass = Class.new 28 | Object.send(:const_set, :CustomTag, original_klass) 29 | Template.register_tag('custom', CustomTag) 30 | 31 | Object.send(:remove_const, :CustomTag) 32 | 33 | new_klass = Class.new 34 | Object.send(:const_set, :CustomTag, new_klass) 35 | 36 | assert(Template.tags['custom'].equal?(original_klass)) 37 | ensure 38 | Object.send(:remove_const, :CustomTag) 39 | Template.tags.delete('custom') 40 | Liquid.cache_classes = original_cache_setting 41 | end 42 | 43 | def test_without_cache_classes_tags_reloads_the_class 44 | original_cache_setting = Liquid.cache_classes 45 | Liquid.cache_classes = false 46 | 47 | original_klass = Class.new 48 | Object.send(:const_set, :CustomTag, original_klass) 49 | Template.register_tag('custom', CustomTag) 50 | 51 | Object.send(:remove_const, :CustomTag) 52 | 53 | new_klass = Class.new 54 | Object.send(:const_set, :CustomTag, new_klass) 55 | 56 | assert(Template.tags['custom'].equal?(new_klass)) 57 | ensure 58 | Object.send(:remove_const, :CustomTag) 59 | Template.tags.delete('custom') 60 | Liquid.cache_classes = original_cache_setting 61 | end 62 | 63 | class FakeTag; end 64 | 65 | def test_tags_delete 66 | Template.register_tag('fake', FakeTag) 67 | assert_equal(FakeTag, Template.tags['fake']) 68 | 69 | Template.tags.delete('fake') 70 | assert_nil(Template.tags['fake']) 71 | end 72 | 73 | def test_tags_can_be_looped_over 74 | Template.register_tag('fake', FakeTag) 75 | result = Template.tags.map { |name, klass| [name, klass] } 76 | assert(result.include?(["fake", "TemplateUnitTest::FakeTag"])) 77 | ensure 78 | Template.tags.delete('fake') 79 | end 80 | 81 | class TemplateSubclass < Liquid::Template 82 | end 83 | 84 | def test_template_inheritance 85 | assert_equal("foo", TemplateSubclass.parse("foo").render) 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /performance/tests/vogue/product.liquid: -------------------------------------------------------------------------------- 1 |
2 | {% for image in product.images %}{% if forloop.first %}
3 | {{ product.title | escape }} 4 |
{% else %} 5 |
6 | {{ product.title | escape }} 7 |
{% endif %}{% endfor %} 8 |
9 |
10 |

{{ product.title }}

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

This product is temporarily unavailable

30 | {% endif %} 31 | 32 |
33 | Continue Shopping
34 | Browse more {{ product.type | link_to_type }} or additional {{ product.vendor | link_to_vendor }} products. 35 |
36 |
37 | 38 | 39 | 62 | 63 | -------------------------------------------------------------------------------- /performance/tests/ripen/theme.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{shop.name}} - {{page_title}} 5 | 6 | 7 | {{ 'main.css' | asset_url | stylesheet_tag }} 8 | {{ 'shop.js' | asset_url | script_tag }} 9 | 10 | {{ 'mootools.js' | asset_url | script_tag }} 11 | {{ 'slimbox.js' | asset_url | script_tag }} 12 | {{ 'option_selection.js' | shopify_asset_url | script_tag }} 13 | {{ 'slimbox.css' | asset_url | stylesheet_tag }} 14 | 15 | {{ content_for_header }} 16 | 17 | 18 | 19 |

Skip to navigation.

20 |
21 |
22 | 25 |
26 | {{ content_for_layout }} 27 |
28 |
29 | {% if template != 'cart' %} 30 |
31 |
32 |
Shopping Cart
33 |
34 | {% if cart.item_count != 0 %} 35 | {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }} in your cart 36 | {% else %} 37 | Your cart is empty 38 | {% endif %} 39 |
40 |
41 |
42 | {% endif %} 43 | 55 | 72 |
73 | 74 |
75 |
76 |
77 | 78 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rake' 4 | require 'rake/testtask' 5 | $LOAD_PATH.unshift(File.expand_path("../lib", __FILE__)) 6 | require "liquid/version" 7 | 8 | task(default: [:test, :rubocop]) 9 | 10 | desc('run test suite with default parser') 11 | Rake::TestTask.new(:base_test) do |t| 12 | t.libs << '.' << 'lib' << 'test' 13 | t.test_files = FileList['test/{integration,unit}/**/*_test.rb'] 14 | t.verbose = false 15 | end 16 | 17 | desc('run test suite with warn error mode') 18 | task :warn_test do 19 | ENV['LIQUID_PARSER_MODE'] = 'warn' 20 | Rake::Task['base_test'].invoke 21 | end 22 | 23 | task :rubocop do 24 | if RUBY_ENGINE == 'ruby' 25 | require 'rubocop/rake_task' 26 | RuboCop::RakeTask.new 27 | end 28 | end 29 | 30 | desc('runs test suite with both strict and lax parsers') 31 | task :test do 32 | ENV['LIQUID_PARSER_MODE'] = 'lax' 33 | Rake::Task['base_test'].invoke 34 | 35 | ENV['LIQUID_PARSER_MODE'] = 'strict' 36 | Rake::Task['base_test'].reenable 37 | Rake::Task['base_test'].invoke 38 | 39 | if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby' 40 | ENV['LIQUID_C'] = '1' 41 | 42 | ENV['LIQUID_PARSER_MODE'] = 'lax' 43 | Rake::Task['base_test'].reenable 44 | Rake::Task['base_test'].invoke 45 | 46 | ENV['LIQUID_PARSER_MODE'] = 'strict' 47 | Rake::Task['base_test'].reenable 48 | Rake::Task['base_test'].invoke 49 | end 50 | end 51 | 52 | task(gem: :build) 53 | task :build do 54 | system "gem build liquid.gemspec" 55 | end 56 | 57 | task install: :build do 58 | system "gem install liquid-#{Liquid::VERSION}.gem" 59 | end 60 | 61 | task release: :build do 62 | system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'" 63 | system "git push --tags" 64 | system "gem push liquid-#{Liquid::VERSION}.gem" 65 | system "rm liquid-#{Liquid::VERSION}.gem" 66 | end 67 | 68 | namespace :benchmark do 69 | desc "Run the liquid benchmark with lax parsing" 70 | task :run do 71 | ruby "./performance/benchmark.rb lax" 72 | end 73 | 74 | desc "Run the liquid benchmark with strict parsing" 75 | task :strict do 76 | ruby "./performance/benchmark.rb strict" 77 | end 78 | end 79 | 80 | namespace :profile do 81 | desc "Run the liquid profile/performance coverage" 82 | task :run do 83 | ruby "./performance/profile.rb" 84 | end 85 | 86 | desc "Run the liquid profile/performance coverage with strict parsing" 87 | task :strict do 88 | ruby "./performance/profile.rb strict" 89 | end 90 | end 91 | 92 | namespace :memory_profile do 93 | desc "Run memory profiler" 94 | task :run do 95 | ruby "./performance/memory_profile.rb" 96 | end 97 | end 98 | 99 | desc("Run example") 100 | task :example do 101 | ruby "-w -d -Ilib example/server/server.rb" 102 | end 103 | 104 | task :console do 105 | exec 'irb -I lib -r liquid' 106 | end 107 | -------------------------------------------------------------------------------- /performance/shopify/paginate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Paginate < Liquid::Block 4 | Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/ 5 | 6 | def initialize(tag_name, markup, options) 7 | super 8 | 9 | if markup =~ Syntax 10 | @collection_name = Regexp.last_match(1) 11 | @page_size = if Regexp.last_match(2) 12 | Regexp.last_match(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, "Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number" 23 | end 24 | end 25 | 26 | def render_to_output_buffer(context, output) 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, "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 | if current_page == page 57 | pagination['parts'] << no_link(page) 58 | elsif page == 1 59 | pagination['parts'] << link(page, page) 60 | elsif page == page_count - 1 61 | pagination['parts'] << link(page, page) 62 | elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size'] 63 | next if hellip_break 64 | pagination['parts'] << no_link('…') 65 | hellip_break = true 66 | next 67 | else 68 | pagination['parts'] << link(page, page) 69 | end 70 | 71 | hellip_break = false 72 | end 73 | end 74 | 75 | super 76 | end 77 | end 78 | 79 | private 80 | 81 | def no_link(title) 82 | { 'title' => title, 'is_link' => false } 83 | end 84 | 85 | def link(title, page) 86 | { 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true } 87 | end 88 | 89 | def current_url 90 | "/collections/frontpage" 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /performance/tests/dropify/product.liquid: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | {% for image in product.images %} 5 | {% if forloop.first %} 6 | 7 | {{product.title | escape }} 8 | 9 | {% else %} 10 | 11 | {{product.title | escape }} 12 | 13 | {% endif %} 14 | {% endfor %} 15 |
16 | 17 |

{{ product.title }}

18 | 19 | 23 | 24 | {{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %} 25 | 26 |
27 |
28 | 29 | 34 | 35 |
36 | 37 |
38 |
39 |
40 | 41 |
42 | {{ product.description }} 43 |
44 |
45 | 46 | 69 | -------------------------------------------------------------------------------- /lib/liquid/variable_lookup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class VariableLookup 5 | SQUARE_BRACKETED = /\A\[(.*)\]\z/m 6 | COMMAND_METHODS = ['size', 'first', 'last'].freeze 7 | 8 | attr_reader :name, :lookups 9 | 10 | def self.parse(markup) 11 | new(markup) 12 | end 13 | 14 | def initialize(markup) 15 | lookups = markup.scan(VariableParser) 16 | 17 | name = lookups.shift 18 | if name =~ SQUARE_BRACKETED 19 | name = Expression.parse(Regexp.last_match(1)) 20 | end 21 | @name = name 22 | 23 | @lookups = lookups 24 | @command_flags = 0 25 | 26 | @lookups.each_index do |i| 27 | lookup = lookups[i] 28 | if lookup =~ SQUARE_BRACKETED 29 | lookups[i] = Expression.parse(Regexp.last_match(1)) 30 | elsif COMMAND_METHODS.include?(lookup) 31 | @command_flags |= 1 << i 32 | end 33 | end 34 | end 35 | 36 | def evaluate(context) 37 | name = context.evaluate(@name) 38 | object = context.find_variable(name) 39 | 40 | @lookups.each_index do |i| 41 | key = context.evaluate(@lookups[i]) 42 | 43 | # If object is a hash- or array-like object we look for the 44 | # presence of the key and if its available we return it 45 | if object.respond_to?(:[]) && 46 | ((object.respond_to?(:key?) && object.key?(key)) || 47 | (object.respond_to?(:fetch) && key.is_a?(Integer))) 48 | 49 | # if its a proc we will replace the entry with the proc 50 | res = context.lookup_and_evaluate(object, key) 51 | object = res.to_liquid 52 | 53 | # Some special cases. If the part wasn't in square brackets and 54 | # no key with the same name was found we interpret following calls 55 | # as commands and call them on the current object 56 | elsif @command_flags & (1 << i) != 0 && object.respond_to?(key) 57 | object = object.send(key).to_liquid 58 | 59 | # No key was present with the desired value and it wasn't one of the directly supported 60 | # keywords either. The only thing we got left is to return nil or 61 | # raise an exception if `strict_variables` option is set to true 62 | else 63 | return nil unless context.strict_variables 64 | raise Liquid::UndefinedVariable, "undefined variable #{key}" 65 | end 66 | 67 | # If we are dealing with a drop here we have to 68 | object.context = context if object.respond_to?(:context=) 69 | end 70 | 71 | object 72 | end 73 | 74 | def ==(other) 75 | self.class == other.class && state == other.state 76 | end 77 | 78 | protected 79 | 80 | def state 81 | [@name, @lookups, @command_flags] 82 | end 83 | 84 | class ParseTreeVisitor < Liquid::ParseTreeVisitor 85 | def children 86 | @node.lookups 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /performance/tests/ripen/article.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{ article.title }}

3 |

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

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

Comments

15 | 16 | 17 | 30 | 31 | 32 |
33 | {% form article %} 34 |

Leave a comment

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

comments have to be approved before showing up

65 | {% endif %} 66 | 67 | 68 | {% endform %} 69 |
70 | 71 | 72 |
73 | {% endif %} 74 | 75 | -------------------------------------------------------------------------------- /lib/liquid/tags/render.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | class Render < Tag 5 | FOR = 'for' 6 | SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o 7 | 8 | disable_tags "include" 9 | 10 | attr_reader :template_name_expr, :attributes 11 | 12 | def initialize(tag_name, markup, options) 13 | super 14 | 15 | raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX 16 | 17 | template_name = Regexp.last_match(1) 18 | with_or_for = Regexp.last_match(3) 19 | variable_name = Regexp.last_match(4) 20 | 21 | @alias_name = Regexp.last_match(6) 22 | @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil 23 | @template_name_expr = Expression.parse(template_name) 24 | @for = (with_or_for == FOR) 25 | 26 | @attributes = {} 27 | markup.scan(TagAttributes) do |key, value| 28 | @attributes[key] = Expression.parse(value) 29 | end 30 | end 31 | 32 | def render_to_output_buffer(context, output) 33 | render_tag(context, output) 34 | end 35 | 36 | def render_tag(context, output) 37 | # Though we evaluate this here we will only ever parse it as a string literal. 38 | template_name = context.evaluate(@template_name_expr) 39 | raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name 40 | 41 | partial = PartialCache.load( 42 | template_name, 43 | context: context, 44 | parse_context: parse_context 45 | ) 46 | 47 | context_variable_name = @alias_name || template_name.split('/').last 48 | 49 | render_partial_func = ->(var, forloop) { 50 | inner_context = context.new_isolated_subcontext 51 | inner_context.template_name = template_name 52 | inner_context.partial = true 53 | inner_context['forloop'] = forloop if forloop 54 | 55 | @attributes.each do |key, value| 56 | inner_context[key] = context.evaluate(value) 57 | end 58 | inner_context[context_variable_name] = var unless var.nil? 59 | partial.render_to_output_buffer(inner_context, output) 60 | forloop&.send(:increment!) 61 | } 62 | 63 | variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil 64 | if @for && variable.respond_to?(:each) && variable.respond_to?(:count) 65 | forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil) 66 | variable.each { |var| render_partial_func.call(var, forloop) } 67 | else 68 | render_partial_func.call(variable, nil) 69 | end 70 | 71 | output 72 | end 73 | 74 | class ParseTreeVisitor < Liquid::ParseTreeVisitor 75 | def children 76 | [ 77 | @node.template_name_expr, 78 | ] + @node.attributes.values 79 | end 80 | end 81 | end 82 | 83 | Template.register_tag('render', Render) 84 | end 85 | -------------------------------------------------------------------------------- /performance/tests/dropify/article.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{ article.title }}

3 |

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

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

Comments

15 | 16 | 17 | 30 | 31 | 32 |
33 | {% form article %} 34 |

Leave a comment

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

comments have to be approved before showing up

65 | {% endif %} 66 | 67 | 68 | {% endform %} 69 |
70 | 71 | 72 |
73 | {% endif %} 74 | 75 | -------------------------------------------------------------------------------- /lib/liquid/file_system.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | # A Liquid file system is a way to let your templates retrieve other templates for use with the include tag. 5 | # 6 | # You can implement subclasses that retrieve templates from the database, from the file system using a different 7 | # path structure, you can provide them as hard-coded inline strings, or any manner that you see fit. 8 | # 9 | # You can add additional instance variables, arguments, or methods as needed. 10 | # 11 | # Example: 12 | # 13 | # Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path) 14 | # liquid = Liquid::Template.parse(template) 15 | # 16 | # This will parse the template with a LocalFileSystem implementation rooted at 'template_path'. 17 | class BlankFileSystem 18 | # Called by Liquid to retrieve a template file 19 | def read_template_file(_template_path) 20 | raise FileSystemError, "This liquid context does not allow includes." 21 | end 22 | end 23 | 24 | # This implements an abstract file system which retrieves template files named in a manner similar to Rails partials, 25 | # ie. with the template name prefixed with an underscore. The extension ".liquid" is also added. 26 | # 27 | # For security reasons, template paths are only allowed to contain letters, numbers, and underscore. 28 | # 29 | # Example: 30 | # 31 | # file_system = Liquid::LocalFileSystem.new("/some/path") 32 | # 33 | # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid" 34 | # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid" 35 | # 36 | # Optionally in the second argument you can specify a custom pattern for template filenames. 37 | # The Kernel::sprintf format specification is used. 38 | # Default pattern is "_%s.liquid". 39 | # 40 | # Example: 41 | # 42 | # file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html") 43 | # 44 | # file_system.full_path("index") # => "/some/path/index.html" 45 | # 46 | class LocalFileSystem 47 | attr_accessor :root 48 | 49 | def initialize(root, pattern = "_%s.liquid") 50 | @root = root 51 | @pattern = pattern 52 | end 53 | 54 | def read_template_file(template_path) 55 | full_path = full_path(template_path) 56 | raise FileSystemError, "No such template '#{template_path}'" unless File.exist?(full_path) 57 | 58 | File.read(full_path) 59 | end 60 | 61 | def full_path(template_path) 62 | raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path) 63 | 64 | full_path = if template_path.include?('/') 65 | File.join(root, File.dirname(template_path), @pattern % File.basename(template_path)) 66 | else 67 | File.join(root, @pattern % template_path) 68 | end 69 | 70 | raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root)) 71 | 72 | full_path 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /performance/tests/ripen/product.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{ product.title }}

3 |
4 |
5 | {% for image in product.images %} 6 | {% if forloop.first %} 7 | 8 | {{product.title | escape }} 9 | 10 | {% else %} 11 | 12 | {{product.title | escape }} 13 | 14 | {% endif %} 15 | {% endfor %} 16 |
17 | 18 |
    19 |
  • Vendor: {{ product.vendor | link_to_vendor }}
  • 20 |
  • Type: {{ product.type | link_to_type }}
  • 21 |
22 | 23 | {{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %} 24 | 25 |
26 | {% if product.available %} 27 | 28 |
29 | 30 | 35 | 36 |
37 | 38 |
39 |
40 | {% else %} 41 | Sold Out! 42 | {% endif %} 43 |
44 | 45 |
46 | {{ product.description }} 47 |
48 |
49 |
50 | 51 | 52 | 75 | 76 | -------------------------------------------------------------------------------- /test/unit/block_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class BlockUnitTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_blankspace 9 | template = Liquid::Template.parse(" ") 10 | assert_equal([" "], template.root.nodelist) 11 | end 12 | 13 | def test_variable_beginning 14 | template = Liquid::Template.parse("{{funk}} ") 15 | assert_equal(2, template.root.nodelist.size) 16 | assert_equal(Variable, template.root.nodelist[0].class) 17 | assert_equal(String, template.root.nodelist[1].class) 18 | end 19 | 20 | def test_variable_end 21 | template = Liquid::Template.parse(" {{funk}}") 22 | assert_equal(2, template.root.nodelist.size) 23 | assert_equal(String, template.root.nodelist[0].class) 24 | assert_equal(Variable, template.root.nodelist[1].class) 25 | end 26 | 27 | def test_variable_middle 28 | template = Liquid::Template.parse(" {{funk}} ") 29 | assert_equal(3, template.root.nodelist.size) 30 | assert_equal(String, template.root.nodelist[0].class) 31 | assert_equal(Variable, template.root.nodelist[1].class) 32 | assert_equal(String, template.root.nodelist[2].class) 33 | end 34 | 35 | def test_variable_many_embedded_fragments 36 | template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ") 37 | assert_equal(7, template.root.nodelist.size) 38 | assert_equal([String, Variable, String, Variable, String, Variable, String], 39 | block_types(template.root.nodelist)) 40 | end 41 | 42 | def test_with_block 43 | template = Liquid::Template.parse(" {% comment %} {% endcomment %} ") 44 | assert_equal([String, Comment, String], block_types(template.root.nodelist)) 45 | assert_equal(3, template.root.nodelist.size) 46 | end 47 | 48 | def test_with_custom_tag 49 | with_custom_tag('testtag', Block) do 50 | assert Liquid::Template.parse("{% testtag %} {% endtesttag %}") 51 | end 52 | end 53 | 54 | def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility 55 | klass1 = Class.new(Block) do 56 | def render(*) 57 | 'hello' 58 | end 59 | end 60 | 61 | with_custom_tag('blabla', klass1) do 62 | template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}") 63 | 64 | assert_equal 'hello', template.render 65 | 66 | buf = +'' 67 | output = template.render({}, output: buf) 68 | assert_equal 'hello', output 69 | assert_equal 'hello', buf 70 | assert_equal buf.object_id, output.object_id 71 | end 72 | 73 | klass2 = Class.new(klass1) do 74 | def render(*) 75 | 'foo' + super + 'bar' 76 | end 77 | end 78 | 79 | with_custom_tag('blabla', klass2) do 80 | template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}") 81 | 82 | assert_equal 'foohellobar', template.render 83 | 84 | buf = +'' 85 | output = template.render({}, output: buf) 86 | assert_equal 'foohellobar', output 87 | assert_equal 'foohellobar', buf 88 | assert_equal buf.object_id, output.object_id 89 | end 90 | end 91 | 92 | private 93 | 94 | def block_types(nodelist) 95 | nodelist.collect(&:class) 96 | end 97 | end # VariableTest 98 | -------------------------------------------------------------------------------- /performance/tests/vogue/cart.liquid: -------------------------------------------------------------------------------- 1 |

Shopping Cart

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

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

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

{{ item.title }}

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

Subtotal {{ cart.total_price | money }}

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

- or -

54 | {{ content_for_additional_checkout_buttons }} 55 |
56 | {% endif %} 57 |
58 |
{% endif %} 59 | -------------------------------------------------------------------------------- /performance/tests/tribble/theme.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{shop.name}} - {{page_title}} 5 | 6 | 7 | {{ 'reset.css' | asset_url | stylesheet_tag }} 8 | {{ 'style.css' | asset_url | stylesheet_tag }} 9 | 10 | {{ 'lightbox.css' | asset_url | stylesheet_tag }} 11 | {{ 'prototype/1.6/prototype.js' | global_asset_url | script_tag }} 12 | {{ 'scriptaculous/1.8.2/scriptaculous.js' | global_asset_url | script_tag }} 13 | {{ 'lightbox.js' | asset_url | script_tag }} 14 | {{ 'option_selection.js' | shopify_asset_url | script_tag }} 15 | 16 | {{ content_for_header }} 17 | 18 | 19 | 20 |
21 | 22 |
23 |
24 |

Shopping Cart

25 |

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

32 |
33 | 34 |
35 |

{{shop.name}}

36 |

Tribble: A Shopify Theme

37 | 38 |
39 |
40 | 41 | 46 | 47 | {{ content_for_layout }} 48 | 49 | 86 | 87 |
88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /performance/shopify/shop_filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ShopFilter 4 | def asset_url(input) 5 | "/files/1/[shop_id]/[shop_id]/assets/#{input}" 6 | end 7 | 8 | def global_asset_url(input) 9 | "/global/#{input}" 10 | end 11 | 12 | def shopify_asset_url(input) 13 | "/shopify/#{input}" 14 | end 15 | 16 | def script_tag(url) 17 | %() 18 | end 19 | 20 | def stylesheet_tag(url, media = "all") 21 | %() 22 | end 23 | 24 | def link_to(link, url, title = "") 25 | %(#{link}) 26 | end 27 | 28 | def img_tag(url, alt = "") 29 | %(#{alt}) 30 | end 31 | 32 | def link_to_vendor(vendor) 33 | if vendor 34 | link_to(vendor, url_for_vendor(vendor), vendor) 35 | else 36 | 'Unknown Vendor' 37 | end 38 | end 39 | 40 | def link_to_type(type) 41 | if type 42 | link_to(type, url_for_type(type), type) 43 | else 44 | 'Unknown Vendor' 45 | end 46 | end 47 | 48 | def url_for_vendor(vendor_title) 49 | "/collections/#{to_handle(vendor_title)}" 50 | end 51 | 52 | def url_for_type(type_title) 53 | "/collections/#{to_handle(type_title)}" 54 | end 55 | 56 | def product_img_url(url, style = 'small') 57 | unless url =~ %r{\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 | '/files/shops/random_number/' + url 64 | when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon' 65 | "/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(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 | html = [] 73 | html << %(#{link_to(paginate['previous']['title'], paginate['previous']['url'])}) if paginate['previous'] 74 | 75 | paginate['parts'].each do |part| 76 | html << if part['is_link'] 77 | %(#{link_to(part['title'], part['url'])}) 78 | elsif part['title'].to_i == paginate['current_page'].to_i 79 | %(#{part['title']}) 80 | else 81 | %(#{part['title']}) 82 | end 83 | end 84 | 85 | html << %(#{link_to(paginate['next']['title'], paginate['next']['url'])}) if paginate['next'] 86 | html.join(' ') 87 | end 88 | 89 | # Accepts a number, and two words - one for singular, one for plural 90 | # Returns the singular word if input equals 1, otherwise plural 91 | def pluralize(input, singular, plural) 92 | input == 1 ? singular : plural 93 | end 94 | 95 | private 96 | 97 | def to_handle(str) 98 | result = str.dup 99 | result.downcase! 100 | result.delete!("'\"()[]") 101 | result.gsub!(/\W+/, '-') 102 | result.gsub!(/-+\z/, '') if result[-1] == '-' 103 | result.gsub!(/\A-+/, '') if result[0] == '-' 104 | result 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /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 | 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/tags/include.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Liquid 4 | # Include allows templates to relate with other templates 5 | # 6 | # Simply include another template: 7 | # 8 | # {% include 'product' %} 9 | # 10 | # Include a template with a local variable: 11 | # 12 | # {% include 'product' with products[0] %} 13 | # 14 | # Include a template for a collection: 15 | # 16 | # {% include 'product' for products %} 17 | # 18 | class Include < Tag 19 | SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o 20 | Syntax = SYNTAX 21 | 22 | attr_reader :template_name_expr, :variable_name_expr, :attributes 23 | 24 | def initialize(tag_name, markup, options) 25 | super 26 | 27 | if markup =~ SYNTAX 28 | 29 | template_name = Regexp.last_match(1) 30 | variable_name = Regexp.last_match(3) 31 | 32 | @alias_name = Regexp.last_match(5) 33 | @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil 34 | @template_name_expr = Expression.parse(template_name) 35 | @attributes = {} 36 | 37 | markup.scan(TagAttributes) do |key, value| 38 | @attributes[key] = Expression.parse(value) 39 | end 40 | 41 | else 42 | raise SyntaxError, options[:locale].t("errors.syntax.include") 43 | end 44 | end 45 | 46 | def parse(_tokens) 47 | end 48 | 49 | def render_to_output_buffer(context, output) 50 | template_name = context.evaluate(@template_name_expr) 51 | raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name 52 | 53 | partial = PartialCache.load( 54 | template_name, 55 | context: context, 56 | parse_context: parse_context 57 | ) 58 | 59 | context_variable_name = @alias_name || template_name.split('/').last 60 | 61 | variable = if @variable_name_expr 62 | context.evaluate(@variable_name_expr) 63 | else 64 | context.find_variable(template_name, raise_on_not_found: false) 65 | end 66 | 67 | old_template_name = context.template_name 68 | old_partial = context.partial 69 | begin 70 | context.template_name = template_name 71 | context.partial = true 72 | context.stack do 73 | @attributes.each do |key, value| 74 | context[key] = context.evaluate(value) 75 | end 76 | 77 | if variable.is_a?(Array) 78 | variable.each do |var| 79 | context[context_variable_name] = var 80 | partial.render_to_output_buffer(context, output) 81 | end 82 | else 83 | context[context_variable_name] = variable 84 | partial.render_to_output_buffer(context, output) 85 | end 86 | end 87 | ensure 88 | context.template_name = old_template_name 89 | context.partial = old_partial 90 | end 91 | 92 | output 93 | end 94 | 95 | alias_method :parse_context, :options 96 | private :parse_context 97 | 98 | class ParseTreeVisitor < Liquid::ParseTreeVisitor 99 | def children 100 | [ 101 | @node.template_name_expr, 102 | @node.variable_name_expr, 103 | ] + @node.attributes.values 104 | end 105 | end 106 | end 107 | 108 | Template.register_tag('include', Include) 109 | end 110 | -------------------------------------------------------------------------------- /test/unit/strainer_factory_unit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class StrainerFactoryUnitTest < Minitest::Test 6 | include Liquid 7 | 8 | module AccessScopeFilters 9 | def public_filter 10 | "public" 11 | end 12 | 13 | def private_filter 14 | "private" 15 | end 16 | private :private_filter 17 | end 18 | 19 | StrainerFactory.add_global_filter(AccessScopeFilters) 20 | 21 | module LateAddedFilter 22 | def late_added_filter(_input) 23 | "filtered" 24 | end 25 | end 26 | 27 | def setup 28 | @context = Context.build 29 | end 30 | 31 | def test_strainer 32 | strainer = StrainerFactory.create(@context) 33 | assert_equal(5, strainer.invoke('size', 'input')) 34 | assert_equal("public", strainer.invoke("public_filter")) 35 | end 36 | 37 | def test_stainer_raises_argument_error 38 | strainer = StrainerFactory.create(@context) 39 | assert_raises(Liquid::ArgumentError) do 40 | strainer.invoke("public_filter", 1) 41 | end 42 | end 43 | 44 | def test_stainer_argument_error_contains_backtrace 45 | strainer = StrainerFactory.create(@context) 46 | 47 | exception = assert_raises(Liquid::ArgumentError) do 48 | strainer.invoke("public_filter", 1) 49 | end 50 | 51 | assert_match( 52 | /\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/, 53 | exception.message 54 | ) 55 | assert_equal(exception.backtrace[0].split(':')[0], __FILE__) 56 | end 57 | 58 | def test_strainer_only_invokes_public_filter_methods 59 | strainer = StrainerFactory.create(@context) 60 | assert_equal(false, strainer.class.invokable?('__test__')) 61 | assert_equal(false, strainer.class.invokable?('test')) 62 | assert_equal(false, strainer.class.invokable?('instance_eval')) 63 | assert_equal(false, strainer.class.invokable?('__send__')) 64 | assert_equal(true, strainer.class.invokable?('size')) # from the standard lib 65 | end 66 | 67 | def test_strainer_returns_nil_if_no_filter_method_found 68 | strainer = StrainerFactory.create(@context) 69 | assert_nil(strainer.invoke("private_filter")) 70 | assert_nil(strainer.invoke("undef_the_filter")) 71 | end 72 | 73 | def test_strainer_returns_first_argument_if_no_method_and_arguments_given 74 | strainer = StrainerFactory.create(@context) 75 | assert_equal("password", strainer.invoke("undef_the_method", "password")) 76 | end 77 | 78 | def test_strainer_only_allows_methods_defined_in_filters 79 | strainer = StrainerFactory.create(@context) 80 | assert_equal("1 + 1", strainer.invoke("instance_eval", "1 + 1")) 81 | assert_equal("puts", strainer.invoke("__send__", "puts", "Hi Mom")) 82 | assert_equal("has_method?", strainer.invoke("invoke", "has_method?", "invoke")) 83 | end 84 | 85 | def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation 86 | a = Module.new 87 | b = Module.new 88 | strainer = StrainerFactory.create(@context, [a, b]) 89 | assert_kind_of(StrainerTemplate, strainer) 90 | assert_kind_of(a, strainer) 91 | assert_kind_of(b, strainer) 92 | assert_kind_of(Liquid::StandardFilters, strainer) 93 | end 94 | 95 | def test_add_global_filter_clears_cache 96 | assert_equal('input', StrainerFactory.create(@context).invoke('late_added_filter', 'input')) 97 | StrainerFactory.add_global_filter(LateAddedFilter) 98 | assert_equal('filtered', StrainerFactory.create(nil).invoke('late_added_filter', 'input')) 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /test/integration/tags/liquid_tag_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class LiquidTagTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_liquid_tag 9 | assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3]) 10 | {%- liquid 11 | echo array | join: " " 12 | -%} 13 | LIQUID 14 | 15 | assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3]) 16 | {%- liquid 17 | for value in array 18 | echo value 19 | unless forloop.last 20 | echo " " 21 | endunless 22 | endfor 23 | -%} 24 | LIQUID 25 | 26 | assert_template_result('4 8 12 6', <<~LIQUID, 'array' => [1, 2, 3]) 27 | {%- liquid 28 | for value in array 29 | assign double_value = value | times: 2 30 | echo double_value | times: 2 31 | unless forloop.last 32 | echo " " 33 | endunless 34 | endfor 35 | 36 | echo " " 37 | echo double_value 38 | -%} 39 | LIQUID 40 | 41 | assert_template_result('abc', <<~LIQUID) 42 | {%- liquid echo "a" -%} 43 | b 44 | {%- liquid echo "c" -%} 45 | LIQUID 46 | end 47 | 48 | def test_liquid_tag_errors 49 | assert_match_syntax_error("syntax error (line 1): Unknown tag 'error'", <<~LIQUID) 50 | {%- liquid error no such tag -%} 51 | LIQUID 52 | 53 | assert_match_syntax_error("syntax error (line 7): Unknown tag 'error'", <<~LIQUID) 54 | {{ test }} 55 | 56 | {%- 57 | liquid 58 | for value in array 59 | 60 | error no such tag 61 | endfor 62 | -%} 63 | LIQUID 64 | 65 | assert_match_syntax_error("syntax error (line 2): Unknown tag '!!! the guards are vigilant'", <<~LIQUID) 66 | {%- liquid 67 | !!! the guards are vigilant 68 | -%} 69 | LIQUID 70 | 71 | assert_match_syntax_error("syntax error (line 4): 'for' tag was never closed", <<~LIQUID) 72 | {%- liquid 73 | for value in array 74 | echo 'forgot to close the for tag' 75 | -%} 76 | LIQUID 77 | end 78 | 79 | def test_line_number_is_correct_after_a_blank_token 80 | assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n\n error %}") 81 | assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n \n error %}") 82 | end 83 | 84 | def test_nested_liquid_tag 85 | assert_usage_increment("liquid_tag_contains_outer_tag", times: 0) do 86 | assert_template_result('good', <<~LIQUID) 87 | {%- if true %} 88 | {%- liquid 89 | echo "good" 90 | %} 91 | {%- endif -%} 92 | LIQUID 93 | end 94 | end 95 | 96 | def test_cannot_open_blocks_living_past_a_liquid_tag 97 | assert_match_syntax_error("syntax error (line 3): 'if' tag was never closed", <<~LIQUID) 98 | {%- liquid 99 | if true 100 | -%} 101 | {%- endif -%} 102 | LIQUID 103 | end 104 | 105 | def test_quirk_can_close_blocks_created_before_a_liquid_tag 106 | assert_usage_increment("liquid_tag_contains_outer_tag") do 107 | assert_template_result("42", <<~LIQUID) 108 | {%- if true -%} 109 | 42 110 | {%- liquid endif -%} 111 | LIQUID 112 | end 113 | end 114 | 115 | def test_liquid_tag_in_raw 116 | assert_template_result("{% liquid echo 'test' %}\n", <<~LIQUID) 117 | {% raw %}{% liquid echo 'test' %}{% endraw %} 118 | LIQUID 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /performance/tests/tribble/article.liquid: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 |
6 |

{{article.title}}

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

Comments

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

Leave a comment

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

comments have to be approved before showing up

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

Why Shop With Us?

83 |
    84 |
  • 85 |

    24 Hours

    86 |

    We're always here to help.

    87 |
  • 88 |
  • 89 |

    No Spam

    90 |

    We'll never share your info.

    91 |
  • 92 |
  • 93 |

    Secure Servers

    94 |

    Checkout is 256bit encrypted.

    95 |
  • 96 |
97 |
98 |
99 | -------------------------------------------------------------------------------- /test/integration/tags/table_row_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class TableRowTest < Minitest::Test 6 | include Liquid 7 | 8 | class ArrayDrop < Liquid::Drop 9 | include Enumerable 10 | 11 | def initialize(array) 12 | @array = array 13 | end 14 | 15 | def each(&block) 16 | @array.each(&block) 17 | end 18 | end 19 | 20 | def test_table_row 21 | assert_template_result("\n 1 2 3 \n 4 5 6 \n", 22 | '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', 23 | 'numbers' => [1, 2, 3, 4, 5, 6]) 24 | 25 | assert_template_result("\n\n", 26 | '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', 27 | 'numbers' => []) 28 | end 29 | 30 | def test_table_row_with_different_cols 31 | assert_template_result("\n 1 2 3 4 5 \n 6 \n", 32 | '{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}', 33 | 'numbers' => [1, 2, 3, 4, 5, 6]) 34 | end 35 | 36 | def test_table_col_counter 37 | assert_template_result("\n12\n12\n12\n", 38 | '{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}', 39 | 'numbers' => [1, 2, 3, 4, 5, 6]) 40 | end 41 | 42 | def test_quoted_fragment 43 | assert_template_result("\n 1 2 3 \n 4 5 6 \n", 44 | "{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}", 45 | 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] }) 46 | assert_template_result("\n 1 2 3 \n 4 5 6 \n", 47 | "{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}", 48 | 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] }) 49 | end 50 | 51 | def test_enumerable_drop 52 | assert_template_result("\n 1 2 3 \n 4 5 6 \n", 53 | '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', 54 | 'numbers' => ArrayDrop.new([1, 2, 3, 4, 5, 6])) 55 | end 56 | 57 | def test_offset_and_limit 58 | assert_template_result("\n 1 2 3 \n 4 5 6 \n", 59 | '{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}', 60 | 'numbers' => [0, 1, 2, 3, 4, 5, 6, 7]) 61 | end 62 | 63 | def test_blank_string_not_iterable 64 | assert_template_result("\n\n", "{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}", 'characters' => '') 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/liquid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright (c) 2005 Tobias Luetke 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | module Liquid 25 | FilterSeparator = /\|/ 26 | ArgumentSeparator = ',' 27 | FilterArgumentSeparator = ':' 28 | VariableAttributeSeparator = '.' 29 | WhitespaceControl = '-' 30 | TagStart = /\{\%/ 31 | TagEnd = /\%\}/ 32 | VariableSignature = /\(?[\w\-\.\[\]]\)?/ 33 | VariableSegment = /[\w\-]/ 34 | VariableStart = /\{\{/ 35 | VariableEnd = /\}\}/ 36 | VariableIncompleteEnd = /\}\}?/ 37 | QuotedString = /"[^"]*"|'[^']*'/ 38 | QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o 39 | TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o 40 | AnyStartingTag = /#{TagStart}|#{VariableStart}/o 41 | PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om 42 | TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om 43 | VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o 44 | 45 | singleton_class.send(:attr_accessor, :cache_classes) 46 | self.cache_classes = true 47 | end 48 | 49 | require "liquid/version" 50 | require 'liquid/parse_tree_visitor' 51 | require 'liquid/lexer' 52 | require 'liquid/parser' 53 | require 'liquid/i18n' 54 | require 'liquid/drop' 55 | require 'liquid/tablerowloop_drop' 56 | require 'liquid/forloop_drop' 57 | require 'liquid/extensions' 58 | require 'liquid/errors' 59 | require 'liquid/interrupts' 60 | require 'liquid/strainer_factory' 61 | require 'liquid/strainer_template' 62 | require 'liquid/expression' 63 | require 'liquid/context' 64 | require 'liquid/parser_switching' 65 | require 'liquid/tag' 66 | require 'liquid/block' 67 | require 'liquid/block_body' 68 | require 'liquid/document' 69 | require 'liquid/variable' 70 | require 'liquid/variable_lookup' 71 | require 'liquid/range_lookup' 72 | require 'liquid/file_system' 73 | require 'liquid/resource_limits' 74 | require 'liquid/template' 75 | require 'liquid/standardfilters' 76 | require 'liquid/condition' 77 | require 'liquid/utils' 78 | require 'liquid/tokenizer' 79 | require 'liquid/parse_context' 80 | require 'liquid/partial_cache' 81 | require 'liquid/usage' 82 | require 'liquid/register' 83 | require 'liquid/static_registers' 84 | require 'liquid/template_factory' 85 | 86 | # Load all the tags of the standard library 87 | # 88 | Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f } 89 | Dir["#{__dir__}/liquid/registers/*.rb"].each { |f| require f } 90 | -------------------------------------------------------------------------------- /test/integration/variable_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class VariableTest < Minitest::Test 6 | include Liquid 7 | 8 | def test_simple_variable 9 | template = Template.parse(%({{test}})) 10 | assert_equal('worked', template.render!('test' => 'worked')) 11 | assert_equal('worked wonderfully', template.render!('test' => 'worked wonderfully')) 12 | end 13 | 14 | def test_variable_render_calls_to_liquid 15 | assert_template_result('foobar', '{{ foo }}', 'foo' => ThingWithToLiquid.new) 16 | end 17 | 18 | def test_simple_with_whitespaces 19 | template = Template.parse(%( {{ test }} )) 20 | assert_equal(' worked ', template.render!('test' => 'worked')) 21 | assert_equal(' worked wonderfully ', template.render!('test' => 'worked wonderfully')) 22 | end 23 | 24 | def test_ignore_unknown 25 | template = Template.parse(%({{ test }})) 26 | assert_equal('', template.render!) 27 | end 28 | 29 | def test_using_blank_as_variable_name 30 | template = Template.parse("{% assign foo = blank %}{{ foo }}") 31 | assert_equal('', template.render!) 32 | end 33 | 34 | def test_using_empty_as_variable_name 35 | template = Template.parse("{% assign foo = empty %}{{ foo }}") 36 | assert_equal('', template.render!) 37 | end 38 | 39 | def test_hash_scoping 40 | template = Template.parse(%({{ test.test }})) 41 | assert_equal('worked', template.render!('test' => { 'test' => 'worked' })) 42 | end 43 | 44 | def test_false_renders_as_false 45 | assert_equal('false', Template.parse("{{ foo }}").render!('foo' => false)) 46 | assert_equal('false', Template.parse("{{ false }}").render!) 47 | end 48 | 49 | def test_nil_renders_as_empty_string 50 | assert_equal('', Template.parse("{{ nil }}").render!) 51 | assert_equal('cat', Template.parse("{{ nil | append: 'cat' }}").render!) 52 | end 53 | 54 | def test_preset_assigns 55 | template = Template.parse(%({{ test }})) 56 | template.assigns['test'] = 'worked' 57 | assert_equal('worked', template.render!) 58 | end 59 | 60 | def test_reuse_parsed_template 61 | template = Template.parse(%({{ greeting }} {{ name }})) 62 | template.assigns['greeting'] = 'Goodbye' 63 | assert_equal('Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi')) 64 | assert_equal('Hello ', template.render!('greeting' => 'Hello', 'unknown' => 'Tobi')) 65 | assert_equal('Hello Brian', template.render!('greeting' => 'Hello', 'name' => 'Brian')) 66 | assert_equal('Goodbye Brian', template.render!('name' => 'Brian')) 67 | assert_equal({ 'greeting' => 'Goodbye' }, template.assigns) 68 | end 69 | 70 | def test_assigns_not_polluted_from_template 71 | template = Template.parse(%({{ test }}{% assign test = 'bar' %}{{ test }})) 72 | template.assigns['test'] = 'baz' 73 | assert_equal('bazbar', template.render!) 74 | assert_equal('bazbar', template.render!) 75 | assert_equal('foobar', template.render!('test' => 'foo')) 76 | assert_equal('bazbar', template.render!) 77 | end 78 | 79 | def test_hash_with_default_proc 80 | template = Template.parse(%(Hello {{ test }})) 81 | assigns = Hash.new { |_h, k| raise "Unknown variable '#{k}'" } 82 | assigns['test'] = 'Tobi' 83 | assert_equal('Hello Tobi', template.render!(assigns)) 84 | assigns.delete('test') 85 | e = assert_raises(RuntimeError) do 86 | template.render!(assigns) 87 | end 88 | assert_equal("Unknown variable 'test'", e.message) 89 | end 90 | 91 | def test_multiline_variable 92 | assert_equal('worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')) 93 | end 94 | 95 | def test_render_symbol 96 | assert_template_result('bar', '{{ foo }}', 'foo' => :bar) 97 | end 98 | end 99 | --------------------------------------------------------------------------------