├── .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 |
4 | {% for product in collection.products %}
5 |
6 |
9 |
10 |
11 |
{{ product.description | strip_html | truncatewords: 35 }}
12 |
{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}
13 |
14 |
15 | {% endfor %}
16 |
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 |
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 |
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 |
10 | {% for product in collection.products %}
11 |
12 |
15 |
16 |
17 |
{{ product.description | strip_html | truncatewords: 35 }}
18 |
{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}
19 |
20 |
21 | {% endfor %}
22 |
23 |
24 |
27 |
28 | {% endpaginate %}
29 |
30 |
--------------------------------------------------------------------------------
/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 |
8 |
9 |
10 |
11 |
{{ product.title | truncate: 30 }}
12 |
{{ product.price | money }}{% if product.compare_at_price_max > product.price %} {{ product.compare_at_price_max | money }}{% endif %}
13 |
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 | %()
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 |
15 |
16 |
17 |
18 |
{{ product.title | truncate: 30 }}
19 |
{{ product.price | money }}{% if product.compare_at_price_max > product.price %} {{ product.compare_at_price_max | money }}{% endif %}
20 |
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 |
9 |
10 |
{{ article.created_at | date: "%b %d" }}
11 | {{ article.content }}
12 |
13 |
14 |
15 | {% endfor %}
16 |
17 |
18 | {{ paginate | default_pagination }}
19 |
20 |
21 | {% endpaginate %}
22 |
23 |
24 |
25 |
Why Shop With Us?
26 |
27 |
28 | 24 Hours
29 | We're always here to help.
30 |
31 |
32 | No Spam
33 | We'll never share your info.
34 |
35 |
36 | Secure Servers
37 | Checkout is 256bit encrypted.
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/test/integration/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 |
4 | {% for product in collections.frontpage.products %}
5 |
6 |
7 |
8 |
9 |
10 | {% if product.compare_at_price %}
11 | {% if product.price_min != product.compare_at_price %}
12 |
Was:{{product.compare_at_price | money}}
13 |
Now: {{product.price_min | money}}
14 | {% endif %}
15 | {% else %}
16 |
{{product.price_min | money}}
17 | {% endif %}
18 |
19 | {% endfor %}
20 |
21 |
22 |
23 | {% assign article = pages.frontpage %}
24 | {% if article.content != "" %}
25 |
{{ article.title }}
26 | {{ article.content }}
27 | {% else %}
28 | In Admin > Blogs & Pages , create a page with the handle frontpage and it will show up here.
29 | {{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }}
30 | {% endif %}
31 |
32 |
33 |
--------------------------------------------------------------------------------
/lib/liquid/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 |
34 |
35 | 24 Hours
36 | We're always here to help.
37 |
38 |
39 | No Spam
40 | We'll never share your info.
41 |
42 |
43 | Secure Servers
44 | Checkout is 256bit encrypted.
45 |
46 |
47 |
48 |
49 |
50 | {% endpaginate %}
51 | {% endif %}
52 |
--------------------------------------------------------------------------------
/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 |
33 |
34 | {% for product in all_products %}
35 |
36 | {{product.name}}
37 | Only {{product.price | price }}
38 |
39 | {{product.description | prettyprint | paragraph }}
40 |
41 | {{ 'it rocks!' | paragraph }}
42 |
43 |
44 | {% endfor %}
45 |
46 |
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 |
6 |
7 |
{{ product.description | strip_html | truncatewords: 18 }}
8 |
{{ product.price_min | money }}
9 |
10 | {% endfor %}
11 | {% for product in collections.frontpage.products offset:1 %}
12 |
13 |
14 |
15 |
{{ product.price_min | money }}
16 |
17 | {% endfor %}
18 |
19 |
20 |
21 | {% assign article = pages.frontpage %}
22 |
23 | {% if article.content != "" %}
24 |
{{ article.title }}
25 |
26 | {{ article.content }}
27 |
28 | {% else %}
29 |
30 | In Admin > Blogs & Pages , create a page with the handle frontpage and it will show up here.
31 | {{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }}
32 |
33 | {% endif %}
34 |
35 |
36 |
37 |
38 | {% for article in blogs.news.articles offset:1 %}
39 |
40 |
{{ article.title }}
41 |
42 | {{ article.content }}
43 |
44 |
45 | {% endfor %}
46 |
47 |
48 |
--------------------------------------------------------------------------------
/test/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 |
15 |
16 | {% for product in collections.frontpage.products %}
17 |
18 |
48 |
49 | {% endfor %}
50 |
51 |
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 |
13 | {% else %}
14 |
15 |
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 |
15 |
16 | {% for product in collections.frontpage.products %}
17 |
18 |
48 |
49 | {% endfor %}
50 |
51 |
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 |
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 |
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 |
10 | {% for product in collection.products %}
11 |
12 |
42 |
43 | {% endfor %}
44 |
45 |
46 |
47 | {{ paginate | default_pagination }}
48 |
49 |
50 |
51 |
52 |
Why Shop With Us?
53 |
54 |
55 | 24 Hours
56 | We're always here to help.
57 |
58 |
59 | No Spam
60 | We'll never share your info.
61 |
62 |
63 | Secure Servers
64 | Checkout is 256bit encrypted.
65 |
66 |
67 |
68 |
69 |
70 | {% endpaginate %}
71 |
--------------------------------------------------------------------------------
/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 |
4 |
{% else %}
5 |
6 |
7 |
{% endif %}{% endfor %}
8 |
9 |
10 |
{{ product.title }}
11 | {{ product.description }}
12 |
13 | {% if product.available %}
14 |
28 | {% else %}
29 |
This product is temporarily unavailable
30 | {% endif %}
31 |
32 |
33 | Continue Shopping
34 | Browse more {{ product.type | link_to_type }} or additional {{ product.vendor | link_to_vendor }} products.
35 |
36 |
37 |
38 |
39 |
62 |
63 |
--------------------------------------------------------------------------------
/performance/tests/ripen/theme.liquid:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{shop.name}} - {{page_title}}
5 |
6 |
7 | {{ 'main.css' | asset_url | stylesheet_tag }}
8 | {{ 'shop.js' | asset_url | script_tag }}
9 |
10 | {{ 'mootools.js' | asset_url | script_tag }}
11 | {{ 'slimbox.js' | asset_url | script_tag }}
12 | {{ 'option_selection.js' | shopify_asset_url | script_tag }}
13 | {{ 'slimbox.css' | asset_url | stylesheet_tag }}
14 |
15 | {{ content_for_header }}
16 |
17 |
18 |
19 | Skip to navigation.
20 |
21 |
22 |
25 |
26 | {{ content_for_layout }}
27 |
28 |
29 | {% if template != 'cart' %}
30 |
42 | {% endif %}
43 |
44 |
45 | Search
46 |
47 |
52 |
53 |
54 |
55 |
56 |
57 | Navigation
58 | {% for link in linklists.main-menu.links %}
59 | {{ link.title | link_to: link.url }}
60 | {% endfor %}
61 |
62 |
63 | {% if tags %}
64 |
65 | Tags
66 | {% for tag in collection.tags %}
67 | {{ tag | highlight_active_tag | link_to_tag: tag }}
68 | {% endfor %}
69 |
70 | {% endif %}
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/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 |
8 |
9 | {% else %}
10 |
11 |
12 |
13 | {% endif %}
14 | {% endfor %}
15 |
16 |
17 |
{{ product.title }}
18 |
19 |
20 | Vendor: {{ product.vendor | link_to_vendor }}
21 | Type: {{ product.type | link_to_type }}
22 |
23 |
24 |
{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}
25 |
26 |
40 |
41 |
42 | {{ product.description }}
43 |
44 |
45 |
46 |
69 |
--------------------------------------------------------------------------------
/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 {{ article.author }}
4 |
5 |
6 | {{ article.content }}
7 |
8 |
9 |
10 |
11 |
12 | {% if blog.comments_enabled? %}
13 |
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 {{ article.author }}
4 |
5 |
6 | {{ article.content }}
7 |
8 |
9 |
10 |
11 |
12 | {% if blog.comments_enabled? %}
13 |
73 | {% endif %}
74 |
75 |
--------------------------------------------------------------------------------
/lib/liquid/file_system.rb:
--------------------------------------------------------------------------------
1 | # 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 |
9 |
10 | {% else %}
11 |
12 |
13 |
14 | {% endif %}
15 | {% endfor %}
16 |
17 |
18 |
19 | Vendor: {{ product.vendor | link_to_vendor }}
20 | Type: {{ product.type | link_to_type }}
21 |
22 |
23 |
{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}
24 |
25 |
26 | {% if product.available %}
27 |
28 |
40 | {% else %}
41 |
Sold Out!
42 | {% endif %}
43 |
44 |
45 |
46 | {{ product.description }}
47 |
48 |
49 |
50 |
51 |
52 |
75 |
76 |
--------------------------------------------------------------------------------
/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 |
7 |
8 |
9 |
10 |
{{ product.title | truncate: 30 }}
11 |
{{ product.price | money }}{% if product.compare_at_price_max > product.price %} {{ product.compare_at_price_max | money }}{% endif %}
12 |
13 | {% endtablerow %}
14 |
15 | {% else %}
16 |
22 | {% endif %}
59 |
--------------------------------------------------------------------------------
/performance/tests/tribble/theme.liquid:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{shop.name}} - {{page_title}}
5 |
6 |
7 | {{ 'reset.css' | asset_url | stylesheet_tag }}
8 | {{ 'style.css' | asset_url | stylesheet_tag }}
9 |
10 | {{ 'lightbox.css' | asset_url | stylesheet_tag }}
11 | {{ 'prototype/1.6/prototype.js' | global_asset_url | script_tag }}
12 | {{ 'scriptaculous/1.8.2/scriptaculous.js' | global_asset_url | script_tag }}
13 | {{ 'lightbox.js' | asset_url | script_tag }}
14 | {{ 'option_selection.js' | shopify_asset_url | script_tag }}
15 |
16 | {{ content_for_header }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
Shopping Cart
25 |
26 | {% if cart.item_count == 0 %}
27 | Your cart is currently empty
28 | {% else %}
29 | {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }} - Total: {{cart.total_price | money_with_currency }} - View Cart
30 | {% endif %}
31 |
32 |
33 |
34 |
35 |
36 |
Tribble: A Shopify Theme
37 |
38 |
39 |
40 |
41 |
42 | {% for link in linklists.main-menu.links %}
43 | {{ link.title | link_to: link.url }}
44 | {% endfor %}
45 |
46 |
47 | {{ content_for_layout }}
48 |
49 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/performance/shopify/shop_filter.rb:
--------------------------------------------------------------------------------
1 | # 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 | %( )
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 |
26 |
27 | {% for product in collections.frontpage.products %}
28 |
29 |
59 |
60 | {% endfor %}
61 |
62 |
63 |
64 |
65 |
66 |
Why Shop With Us?
67 |
68 |
69 | 24 Hours
70 | We're always here to help.
71 |
72 |
73 | No Spam
74 | We'll never share your info.
75 |
76 |
77 | Save Energy
78 | We're green, all the way.
79 |
80 |
81 | Secure Servers
82 | Checkout is 256bits encrypted.
83 |
84 |
85 |
86 |
87 |
88 |
Our Company
89 | {{pages.about-us.content | truncatewords: 49}}
read more
90 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/lib/liquid/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 |
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("\n1 2 \n1 2 \n1 2 \n",
38 | '{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}',
39 | 'numbers' => [1, 2, 3, 4, 5, 6])
40 | end
41 |
42 | def test_quoted_fragment
43 | assert_template_result("\n 1 2 3 \n 4 5 6 \n",
44 | "{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}",
45 | 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] })
46 | assert_template_result("\n 1 2 3 \n 4 5 6 \n",
47 | "{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}",
48 | 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] })
49 | 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 |
--------------------------------------------------------------------------------
Comments
12 | 13 | 14 |15 | {% for comment in article.comments %} 16 |-
17 |
18 | {{ comment.content }}
19 |
20 |
21 |
22 | Posted by {{ comment.author }} on {{ comment.created_at | date: "%B %d, %Y" }}
23 |
24 |
25 | {% endfor %}
26 |
27 | 28 | 29 | {% form article %} 30 |Leave a comment
31 | 32 | 33 | {% if form.posted_successfully? %} 34 | {% if blog.moderated? %} 35 |37 | It will have to be approved by the blog owner first before showing up. 38 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 | 59 | {% if blog.moderated? %} 60 |comments have to be approved before showing up
61 | {% endif %} 62 | 63 | 64 | {% endform %} 65 |