Compare commits

..

59 Commits

Author SHA1 Message Date
Mike Angell
56a1034ac2 Remove forloop guard 2019-10-15 20:37:12 +10:00
Mike Angell
1c3dcb0ddc Add forloop to render for syntax 2019-10-14 17:24:34 +10:00
uchoudh
1223444738 Fix flaky tests (#1186) 2019-10-12 02:52:07 +11:00
Mike Angell
2bfeed2b00 Resolve InputIterator dropping context (#1184)
* Resolve InputIterator dropping context

* Prefer attr_reader
2019-10-09 08:00:16 +11:00
Mike Angell
04b800d768 Add support for as in Render and Include tags (#1181)
* Add support for alias

* Remove duplicate code

* Default to template name

* Improve variable matching

* Extract render_partial

* remove method
2019-10-09 07:59:52 +11:00
Mike Angell
f1d62978ef Allow default function to handle false as value (#1144)
* Allow default function to handle false as value

* Change to named parameter

* Remove redundant freeze

* add brackets to make intention clearer

* Use named param format from liquid

* Update syntax

* document default filter
2019-10-09 04:03:33 +11:00
uchoudh
ffadc64f28 Merge pull request #1172 from Shopify/add-liquid-profiling-attr
Add liquid profile attributes
2019-10-08 10:49:54 -04:00
Mike Angell
5302f40342 Rubocop fixes (#1182) 2019-10-07 17:06:47 +11:00
uchoudh
fefee4c675 Add liquid profile attributes
Attribute testing

Add partial name support
2019-10-03 10:12:39 -04:00
Mike Angell
1aa7d3d2ba Change registers to by symbols (#1178) 2019-09-27 04:32:24 +10:00
Mike Angell
0db9c56f34 Disable rendering of tag based on register (#1162)
* Disable rendering of tag based on register

* Improvements to disable tag

* Resolve disbale tag tests

* Test disable_tags register

* disabled_tags is now always avaiable

* Allow multiple tags to be disabled at once

* Move disabled check to block_body

* Code improvements

* Remove redundant nil check

* Improve disabled tag error output

* Improve disable tag API

* Code improvements

* Switch disabled? to not mutate output

* Fix array handling shortcut in disable_tags
2019-09-26 00:18:30 +10:00
Mike Angell
f4d134cd5c Remove jruby and truffleruby testing (#1167) 2019-09-20 02:28:43 +10:00
Mike Angell
b667bcb48b Shopify stye guide fixes (#1160) 2019-09-20 02:08:11 +10:00
Ashwin Maroli
2c14e0b2ba Use Regexp#match? when MatchData is not used (#1165)
* Use `Regexp#match?` when `MatchData` is not used

* Add `TargetRubyVersion: 2.4` to RuboCop config
2019-09-20 02:07:52 +10:00
Ashwin Maroli
ca207ed93f Cleanup RuboCop configuration file (#1161) 2019-09-20 00:55:01 +10:00
Mike Angell
ef13343591 Changes static registers to not be frozen (#1163)
* Changes static registers to not be frozen

* Add frozen test to static registers
2019-09-20 00:24:48 +10:00
Mike Angell
adb40c41b7 Enable frozen_string_literal 2019-09-18 13:40:07 +10:00
Mike Angell
d8403af515 Reimplementation of Static Registers (#1157) 2019-09-18 13:25:55 +10:00
Mike Angell
0d26f05bb8 Enabled frozen string literals (#1154)
* Enabled frozen string literals

* Update rubocop config

* Prefer string interpolation in simple cases

Co-Authored-By: Dylan Thacker-Smith <dylan.smith@shopify.com>
2019-09-18 13:19:45 +10:00
Thierry Joyal
1dcad34b06 Merge pull request #1151 from Shopify/invokable-methods-for-enumerable-reject-include
Invokable methods for enumerable reject include?
2019-09-16 09:49:40 -04:00
Mike Angell
9a42c8c8b2 Merge pull request #1149 from Shopify/liquid-usage
Add usage tracking
2019-09-16 12:14:50 +10:00
Mike Angell
1fcef2133f Merge pull request #1143 from Shopify/styling-fixes-1
Apply simple rubocop fixes
2019-09-16 12:14:32 +10:00
Mike Angell
d7514b1305 Merge pull request #1137 from Shopify/remove-lazy-stacks
Remove lazy load stacks
2019-09-16 12:14:14 +10:00
Thierry Joyal
c0ffee5919 Invokable methods for enumerable reject include? 2019-09-12 12:58:51 +00:00
Mike Angell
724d02e9b3 Disable interrupt fix in this round 2019-09-11 06:35:08 +10:00
Mike Angell
a5b387cdd4 Remove reserved word Interrupt to avoid confusion
Also resolves rubocop conflicts
2019-09-11 06:32:31 +10:00
Mike Angell
8318be2edc Update readme 2019-09-11 05:20:05 +10:00
Mike Angell
b6547f322e Simplify usage 2019-09-11 04:56:25 +10:00
Mike Angell
b316ff8413 Add usage tracking 2019-09-11 04:20:34 +10:00
Justin Li
806b2622da Switch back to Liquid-C master, since https://github.com/Shopify/liquid-c/pull/50 is merged 2019-09-04 15:12:51 -04:00
Mike Angell
1f90a37b63 Merge branch 'master' into styling-fixes-1 2019-09-04 14:26:27 +10:00
Mike Angell
c34f7c9b2c Merge pull request #1145 from Shopify/master-fixes
Render tag styling fixes
2019-09-04 14:25:38 +10:00
Mike Angell
604d899496 Render tag styling fixes 2019-08-31 22:48:25 +10:00
Mike Angell
799da202df Apply simple rubocop fixes 2019-08-31 21:58:33 +10:00
Mike Angell
ddb45cd658 Merge pull request #1139 from Shopify/shopify_ruby_style
Follow Shopify ruby style guide
2019-08-31 21:43:45 +10:00
Mike Angell
dafbb4ae90 Remove hasnling false scopes 2019-08-31 20:03:54 +10:00
Justin Li
9876096cf4 Merge pull request #1141 from ashmaroli/reduce-context-constructor-allocations
Reduce allocations from `Liquid::Context.new`
2019-08-30 12:53:50 -04:00
Ashwin Maroli
8750b4b006 Reduce allocations from Liquid::Context.new 2019-08-30 09:01:47 +05:30
Samuel Doiron
34083c96d5 Merge pull request #1122 from Shopify/render-tag
Add new `{% render %}` tag
2019-08-29 16:49:56 -04:00
Samuel
9672ed5285 Add a new {% render %} tag
Example:

```
// the_count.liquid
{{ number }}! Ah ah ah.

// my_template.liquid
{% for number in range (1..3) %}
  {% render "the_count", number: number %}
{% endfor %}

Output:
1! Ah ah ah.
2! Ah ah ah.
3! Ah ah ah.
```

The `render` tag is a more strict version of the `include` tag. It is
designed to isolate itself from the parent rendering context both by
creating a new scope (which does not inherit the parent scope) and by
only inheriting "static" registers.

Static registers are those that do not hold mutable state which could
affect rendering. This again helps `render`ed templates remain entirely
separate from their calling context.

Unlike `include`, `render` does not permit specifying the target
template using a variable, only a string literal. For example, this
means that `{% render my_dynamic_template %}` is invalid syntax. This
will make it possible to statically analyze the dependencies between
templates without making Turing angry.

Note that the `static_environment` of a rendered template is inherited, unlike
the scope and regular environment. This environment is immutable from within the
template.

An alternate syntax, which mimics the `{% include ... for %}` tag is
currently in design discussion.
2019-08-29 16:32:05 -04:00
Justin Li
f3112fc038 Merge pull request #1136 from ashmaroli/travis-selected-branches
Build only pushes to certain branches on Travis CI
2019-08-29 13:47:59 -04:00
Samuel
d338ccb9a6 Add isolated subcontexts
An isolated subcontext inherits the environment, filters,
and static registers of its supercontext, but with a fresh
(isolated) scope.

This will pave the way for adding the `render` tag, which renders
templates in such a subcontext.
2019-08-29 10:27:15 -04:00
Mike Angell
d67de1c9b2 Follow Shopify ruby style
This is the first step in bringing Liquid style inline with Shopify ruby style
2019-08-29 13:39:57 +10:00
Mike Angell
2324564743 Remove lazy load stacks
Remove lazy load stacks and instead only create a new scope when a tag is known to need one
2019-08-29 09:09:32 +10:00
Ashwin Maroli
b3097f143c Build only pushes to certain branches on Travis CI 2019-08-28 21:28:49 +05:30
Mike Angell
7b309dc75d Merge pull request #1135 from Shopify/fix-failing-rubocop
Resolve failing rubocop issues
2019-08-29 01:11:25 +10:00
Mike Angell
8f68cffdf1 Resolve failing rubocop issues 2019-08-29 00:45:38 +10:00
Mike Angell
dd27d0fd1d Merge pull request #1133 from Shopify/liquid-tag-fixes
Bugfix for new Liquid tag
2019-08-29 00:36:13 +10:00
Mike Angell
7a26e6b3d8 Merge pull request #1131 from Shopify/bump-ruby-2-4
Rubocop upgrade, Ruby 2.4 minimum and TruffleRuby
2019-08-29 00:33:42 +10:00
Mike Angell
cf4e77ab0c Merge branch 'master' into bump-ruby-2-4 2019-08-29 00:24:45 +10:00
Mike Angell
7bae55dd39 Bugfix for new Liquid tag 2019-08-28 23:39:19 +10:00
Tobias Lütke
0ce8aef229 Merge pull request #1103 from ashmaroli/ci-profile-memory
Add a CI job to profile memory usage of commit
2019-08-27 15:11:55 -04:00
Tobias Lütke
6eab595fae Merge pull request #1086 from Shopify/liquid-tag
Add {% liquid %} and {% echo %} tags
2019-08-27 15:10:20 -04:00
Mike Angell
b16b109a80 Bump Minimum version to 2.4 and bump Rubocop 2019-08-28 00:31:44 +10:00
Ashwin Maroli
ab698191b9 Add a CI job to profile memory usage of commit 2019-05-17 22:47:05 +05:30
Justin Li
7dc488a73b Simplifications from review 2019-04-09 15:19:47 -04:00
Justin Li
e6ed804ca5 Fix line number tracking after a non-empty blank token 2019-04-08 18:43:09 -04:00
Justin Li
951abb67ee Remove {% local %} tag 2019-04-08 18:34:39 -04:00
Justin Li
8d1cd41453 Add {% liquid %}, {% echo %}, and {% local %} tags 2019-04-01 20:08:38 -04:00
121 changed files with 3290 additions and 2021 deletions

1
.gitignore vendored
View File

@@ -4,7 +4,6 @@
pkg pkg
*.rbc *.rbc
.rvmrc .rvmrc
.idea
.ruby-version .ruby-version
Gemfile.lock Gemfile.lock
.bundle .bundle

View File

@@ -1,10 +1,17 @@
inherit_from: inherit_from:
- .rubocop.shopify.yml - 'https://shopify.github.io/ruby-style-guide/rubocop.yml'
- .rubocop_todo.yml
require: rubocop-performance require: rubocop-performance
Performance: Performance:
Enabled: true Enabled: true
Metrics/LineLength: AllCops:
Enabled: false TargetRubyVersion: 2.4
Exclude:
- 'vendor/bundle/**/*'
Naming/MethodName:
Exclude:
- 'example/server/liquid_servlet.rb'

48
.rubocop_todo.yml Normal file
View File

@@ -0,0 +1,48 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2019-09-11 06:34:25 +1000 using RuboCop version 0.74.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: runtime_error, standard_error
Lint/InheritException:
Exclude:
- 'lib/liquid/interrupts.rb'
# Offense count: 98
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 294
# Offense count: 44
Naming/ConstantName:
Exclude:
- 'lib/liquid.rb'
- 'lib/liquid/block_body.rb'
- 'lib/liquid/tags/assign.rb'
- 'lib/liquid/tags/capture.rb'
- 'lib/liquid/tags/case.rb'
- 'lib/liquid/tags/cycle.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb'
- 'performance/shopify/comment_form.rb'
- 'performance/shopify/paginate.rb'
- 'test/integration/tags/include_tag_test.rb'
# Offense count: 5
Style/ClassVars:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/template.rb'

View File

@@ -1,30 +1,26 @@
language: ruby language: ruby
cache: bundler
rvm: rvm:
- 2.4 - 2.4
- 2.5 - 2.5
- 2.6 - &latest_ruby 2.6
- 2.7 - 2.7
- ruby-head - ruby-head
- jruby-head
- truffleruby
sudo: false
addons:
apt:
packages:
- libgmp3-dev
matrix: matrix:
include:
- rvm: *latest_ruby
script: bundle exec rake memory_profile:run
name: Profiling Memory Usage
allow_failures: allow_failures:
- rvm: ruby-head - rvm: ruby-head
- rvm: jruby-head
install: branches:
- bundle install only:
- master
script: bundle exec rake - gh-pages
- /.*-stable/
notifications: notifications:
disable: true disable: true

View File

@@ -10,6 +10,7 @@ gemspec
group :benchmark, :test do group :benchmark, :test do
gem 'benchmark-ips' gem 'benchmark-ips'
gem 'memory_profiler' gem 'memory_profiler'
gem 'terminal-table'
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
gem 'stackprof' gem 'stackprof'
@@ -21,6 +22,6 @@ group :test do
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
platform :mri, :truffleruby do platform :mri, :truffleruby do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '7ba926791ef8411984d0f3e41c6353fd716041c6' gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master'
end end
end end

View File

@@ -274,7 +274,7 @@ Yanked from rubygems, as it contained too many changes that broke compatibility.
* Fixed bad error reporting when a filter called a method which doesn't exist. Liquid told you that it couldn't find the filter which was obviously misleading [Tobias Luetke] * Fixed bad error reporting when a filter called a method which doesn't exist. Liquid told you that it couldn't find the filter which was obviously misleading [Tobias Luetke]
* Removed count helper from standard lib. use size [Tobias Luetke] * Removed count helper from standard lib. use size [Tobias Luetke]
* Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond] * Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond]
* Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ARGUMENT_SEPARATOR: , by default [Paul Hammond] * Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ArgumentSeparator: , by default [Paul Hammond]
{{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }} {{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
* Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke] * Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke]

View File

@@ -106,3 +106,9 @@ template = Liquid::Template.parse("{{x}} {{y}}")
template.render!({ 'x' => 1}, { strict_variables: true }) template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y #=> Liquid::UndefinedVariable: Liquid error: undefined variable y
``` ```
### Usage tracking
To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.

View File

@@ -2,10 +2,10 @@
require 'rake' require 'rake'
require 'rake/testtask' require 'rake/testtask'
$LOAD_PATH.unshift(File.expand_path('lib', __dir__)) $LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
require 'liquid/version' require "liquid/version"
task(default: %i[test rubocop]) task(default: [:test, :rubocop])
desc('run test suite with default parser') desc('run test suite with default parser')
Rake::TestTask.new(:base_test) do |t| Rake::TestTask.new(:base_test) do |t|
@@ -37,7 +37,6 @@ task :test do
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby' if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
ENV['LIQUID_C'] = '1' ENV['LIQUID_C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax' ENV['LIQUID_PARSER_MODE'] = 'lax'
@@ -52,7 +51,7 @@ end
task(gem: :build) task(gem: :build)
task :build do task :build do
system 'gem build liquid.gemspec' system "gem build liquid.gemspec"
end end
task install: :build do task install: :build do
@@ -61,45 +60,45 @@ end
task release: :build do task release: :build do
system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'" system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'"
system 'git push --tags' system "git push --tags"
system "gem push liquid-#{Liquid::VERSION}.gem" system "gem push liquid-#{Liquid::VERSION}.gem"
system "rm liquid-#{Liquid::VERSION}.gem" system "rm liquid-#{Liquid::VERSION}.gem"
end end
namespace :benchmark do namespace :benchmark do
desc 'Run the liquid benchmark with lax parsing' desc "Run the liquid benchmark with lax parsing"
task :run do task :run do
ruby './performance/benchmark.rb lax' ruby "./performance/benchmark.rb lax"
end end
desc 'Run the liquid benchmark with strict parsing' desc "Run the liquid benchmark with strict parsing"
task :strict do task :strict do
ruby './performance/benchmark.rb strict' ruby "./performance/benchmark.rb strict"
end end
end end
namespace :profile do namespace :profile do
desc 'Run the liquid profile/performance coverage' desc "Run the liquid profile/performance coverage"
task :run do task :run do
ruby './performance/profile.rb' ruby "./performance/profile.rb"
end end
desc 'Run the liquid profile/performance coverage with strict parsing' desc "Run the liquid profile/performance coverage with strict parsing"
task :strict do task :strict do
ruby './performance/profile.rb strict' ruby "./performance/profile.rb strict"
end end
end end
namespace :memory_profile do namespace :memory_profile do
desc 'Run memory profiler' desc "Run memory profiler"
task :run do task :run do
ruby './performance/memory_profile.rb' ruby "./performance/memory_profile.rb"
end end
end end
desc('Run example') desc("Run example")
task :example do task :example do
ruby '-w -d -Ilib example/server/server.rb' ruby "-w -d -Ilib example/server/server.rb"
end end
task :console do task :console do

View File

@@ -2,7 +2,7 @@
module ProductsFilter module ProductsFilter
def price(integer) def price(integer)
format('$%.2d USD', integer / 100.0) format("$%.2d USD", integer / 100.0)
end end
def prettyprint(text) def prettyprint(text)
@@ -30,17 +30,17 @@ class Servlet < LiquidServlet
private private
def products_list def products_list
[{ 'name' => 'Arbor Draft', 'price' => 39_900, 'description' => 'the *arbor draft* is a excellent product' }, [{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
{ 'name' => 'Arbor Element', 'price' => 40_000, 'description' => 'the *arbor element* rocks for freestyling' }, { 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' },
{ 'name' => 'Arbor Diamond', 'price' => 59_900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }] { 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }]
end end
def more_products_list def more_products_list
[{ 'name' => 'Arbor Catalyst', 'price' => 39_900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' }, [{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
{ 'name' => 'Arbor Fish', 'price' => 40_000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }] { 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }]
end end
def description def description
'List of Products ~ This is a list of products with price and description.' "List of Products ~ This is a list of products with price and description."
end end
end end

View File

@@ -1,17 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
def get(req, res) def do_GET(req, res)
handle(:get, req, res) handle(:get, req, res)
end end
def post(req, res) def do_POST(req, res)
handle(:post, req, res) handle(:post, req, res)
end end
alias_method :do_GET, :get
alias_method :do_POST, :post
private private
def handle(_type, req, res) def handle(_type, req, res)
@@ -22,7 +19,7 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
@action = Regexp.last_match(1) || 'index' @action = Regexp.last_match(1) || 'index'
@assigns = send(@action) if respond_to?(@action) @assigns = send(@action) if respond_to?(@action)
@response['Content-Type'] = 'text/html' @response['Content-Type'] = "text/html"
@response.status = 200 @response.status = 200
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter]) @response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
end end

View File

@@ -10,5 +10,5 @@ require_relative 'example_servlet'
# Setup webrick # Setup webrick
server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000) server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000)
server.mount('/', Servlet) server.mount('/', Servlet)
trap('INT') { server.shutdown } trap("INT") { server.shutdown }
server.start server.start

View File

@@ -22,31 +22,31 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module Liquid module Liquid
FILTER_SEPARATOR = /\|/.freeze FilterSeparator = /\|/
ARGUMENT_SEPARATOR = ',' ArgumentSeparator = ','
FILTER_ARGUMENT_SEPARATOR = ':' FilterArgumentSeparator = ':'
VARIABLE_ATTRIBUTE_SEPARATOR = '.' VariableAttributeSeparator = '.'
WHITESPACE_CONTROL = '-' WhitespaceControl = '-'
TAG_START = /\{\%/.freeze TagStart = /\{\%/
TAG_END = /\%\}/.freeze TagEnd = /\%\}/
VARIABLE_SIGNATURE = /\(?[\w\-\.\[\]]\)?/.freeze VariableSignature = /\(?[\w\-\.\[\]]\)?/
VARIABLE_SEGMENT = /[\w\-]/.freeze VariableSegment = /[\w\-]/
VARIABLE_START = /\{\{/.freeze VariableStart = /\{\{/
VARIABLE_END = /\}\}/.freeze VariableEnd = /\}\}/
VARIABLE_INCOMPLETE_END = /\}\}?/.freeze VariableIncompleteEnd = /\}\}?/
QUOTED_STRING = /"[^"]*"|'[^']*'/.freeze QuotedString = /"[^"]*"|'[^']*'/
QUOTED_FRAGMENT = /#{QUOTED_STRING}|(?:[^\s,\|'"]|#{QUOTED_STRING})+/o.freeze QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TAG_ATTRIBUTES = /(\w+)\s*\:\s*(#{QUOTED_FRAGMENT})/o.freeze TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
ANY_STARTING_TAG = /#{TAG_START}|#{VARIABLE_START}/o.freeze AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PARTIAL_TEMPLATE_PARSER = /#{TAG_START}.*?#{TAG_END}|#{VARIABLE_START}.*?#{VARIABLE_INCOMPLETE_END}/om.freeze PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TEMPLATE_PARSER = /(#{PARTIAL_TEMPLATE_PARSER}|#{ANY_STARTING_TAG})/om.freeze TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VARIABLE_PARSER = /\[[^\]]+\]|#{VARIABLE_SEGMENT}+\??/o.freeze VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
singleton_class.send(:attr_accessor, :cache_classes) singleton_class.send(:attr_accessor, :cache_classes)
self.cache_classes = true self.cache_classes = true
end end
require 'liquid/version' require "liquid/version"
require 'liquid/parse_tree_visitor' require 'liquid/parse_tree_visitor'
require 'liquid/lexer' require 'liquid/lexer'
require 'liquid/parser' require 'liquid/parser'
@@ -76,7 +76,12 @@ require 'liquid/condition'
require 'liquid/utils' require 'liquid/utils'
require 'liquid/tokenizer' require 'liquid/tokenizer'
require 'liquid/parse_context' require 'liquid/parse_context'
require 'liquid/partial_cache'
require 'liquid/usage'
require 'liquid/register'
require 'liquid/static_registers'
# Load all the tags of the standard library # Load all the tags of the standard library
# #
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f } Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
Dir["#{__dir__}/liquid/registers/*.rb"].each { |f| require f }

View File

@@ -30,15 +30,15 @@ module Liquid
def unknown_tag(tag, _params, _tokens) def unknown_tag(tag, _params, _tokens)
if tag == 'else' if tag == 'else'
raise SyntaxError, parse_context.locale.t('errors.syntax.unexpected_else', raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
block_name: block_name) block_name: block_name)
elsif tag.start_with?('end') elsif tag.start_with?('end')
raise SyntaxError, parse_context.locale.t('errors.syntax.invalid_delimiter', raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter",
tag: tag, tag: tag,
block_name: block_name, block_name: block_name,
block_delimiter: block_delimiter) block_delimiter: block_delimiter)
else else
raise SyntaxError, parse_context.locale.t('errors.syntax.unknown_tag', tag: tag) raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end end
end end
@@ -53,15 +53,18 @@ module Liquid
protected protected
def parse_body(body, tokens) def parse_body(body, tokens)
raise StackLevelError, 'Nesting too deep' if parse_context.depth >= MAX_DEPTH if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep"
end
parse_context.depth += 1 parse_context.depth += 1
begin begin
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params| body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank? @blank &&= body.blank?
return false if end_tag_name == block_delimiter return false if end_tag_name == block_delimiter
raise SyntaxError, parse_context.locale.t('errors.syntax.tag_never_closed', block_name: block_name) unless end_tag_name unless end_tag_name
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end
# this tag is not registered with the system # this tag is not registered with the system
# pass it to the current block for special handling or error reporting # pass it to the current block for special handling or error reporting

View File

@@ -2,11 +2,12 @@
module Liquid module Liquid
class BlockBody class BlockBody
FULL_TOKEN = /\A#{TAG_START}#{WHITESPACE_CONTROL}?\s*(\w+)\s*(.*?)#{WHITESPACE_CONTROL}?#{TAG_END}\z/om.freeze LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
CONTENT_OF_VARIABLE = /\A#{VARIABLE_START}#{WHITESPACE_CONTROL}?(.*?)#{WHITESPACE_CONTROL}?#{VARIABLE_END}\z/om.freeze FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
WHITESPACE_OR_NOTHING = /\A\s*\z/.freeze ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
TAG_START = '{%' WhitespaceOrNothing = /\A\s*\z/
VAR_START = '{{' TAGSTART = "{%"
VARSTART = "{{"
attr_reader :nodelist attr_reader :nodelist
@@ -15,36 +16,83 @@ module Liquid
@blank = true @blank = true
end end
def parse(tokenizer, parse_context) def parse(tokenizer, parse_context, &block)
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
while (token = tokenizer.shift)
next if token.empty?
case if tokenizer.for_liquid_tag
when token.start_with?(TAG_START) parse_for_liquid_tag(tokenizer, parse_context, &block)
whitespace_handler(token, parse_context) else
raise_missing_tag_terminator(token, parse_context) unless token =~ FULL_TOKEN parse_for_document(tokenizer, parse_context, &block)
end
end
private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift)
unless token.empty? || token =~ WhitespaceOrNothing
unless token =~ LiquidTagToken
# line isn't empty but didn't match tag syntax, yield and let the
# caller raise a syntax error
return yield token, token
end
tag_name = Regexp.last_match(1) tag_name = Regexp.last_match(1)
markup = Regexp.last_match(2) markup = Regexp.last_match(2)
# fetch the tag from registered blocks
unless (tag = registered_tags[tag_name]) unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
end end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context) new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank? @blank &&= new_tag.blank?
@nodelist << new_tag @nodelist << new_tag
when token.start_with?(VAR_START) end
parse_context.line_number = tokenizer.line_number
end
yield nil, nil
end
private def parse_for_document(tokenizer, parse_context, &block)
while (token = tokenizer.shift)
next if token.empty?
case
when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context)
unless token =~ FullToken
raise_missing_tag_terminator(token, parse_context)
end
tag_name = Regexp.last_match(2)
markup = Regexp.last_match(4)
if parse_context.line_number
# newlines inside the tag should increase the line number,
# particularly important for multiline {% liquid %} tags
parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
end
if tag_name == 'liquid'
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
end
unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
when token.start_with?(VARSTART)
whitespace_handler(token, parse_context) whitespace_handler(token, parse_context)
@nodelist << create_variable(token, parse_context) @nodelist << create_variable(token, parse_context)
@blank = false @blank = false
else else
token.lstrip! if parse_context.trim_whitespace if parse_context.trim_whitespace
token.lstrip!
end
parse_context.trim_whitespace = false parse_context.trim_whitespace = false
@nodelist << token @nodelist << token
@blank &&= token.match?(WHITESPACE_OR_NOTHING) @blank &&= !!(token =~ WhitespaceOrNothing)
end end
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
end end
@@ -53,11 +101,13 @@ module Liquid
end end
def whitespace_handler(token, parse_context) def whitespace_handler(token, parse_context)
if token[2] == WHITESPACE_CONTROL if token[2] == WhitespaceControl
previous_token = @nodelist.last previous_token = @nodelist.last
previous_token.rstrip! if previous_token.is_a?(String) if previous_token.is_a?(String)
previous_token.rstrip!
end
end end
parse_context.trim_whitespace = (token[-3] == WHITESPACE_CONTROL) parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
end end
def blank? def blank?
@@ -104,7 +154,13 @@ module Liquid
private private
def render_node(context, output, node) def render_node(context, output, node)
node.render_to_output_buffer(context, output) if node.disabled?(context)
output << node.disabled_error_message
return
end
disable_tags(context, node.disabled_tags) do
node.render_to_output_buffer(context, output)
end
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number) context.handle_error(e, node.line_number)
rescue ::StandardError => e rescue ::StandardError => e
@@ -112,15 +168,19 @@ module Liquid
output << context.handle_error(e, line_number) output << context.handle_error(e, line_number)
end end
def disable_tags(context, tags, &block)
return yield if tags.empty?
context.registers[:disabled_tags].disable(tags, &block)
end
def raise_if_resource_limits_reached(context, length) def raise_if_resource_limits_reached(context, length)
context.resource_limits.render_length += length context.resource_limits.render_length += length
return unless context.resource_limits.reached? return unless context.resource_limits.reached?
raise MemoryError, "Memory limits exceeded"
raise MemoryError, 'Memory limits exceeded'
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)
token.scan(CONTENT_OF_VARIABLE) do |content| token.scan(ContentOfVariable) do |content|
markup = content.first markup = content.first
return Variable.new(markup, parse_context) return Variable.new(markup, parse_context)
end end
@@ -128,11 +188,11 @@ module Liquid
end end
def raise_missing_tag_terminator(token, parse_context) def raise_missing_tag_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t('errors.syntax.tag_termination', token: token, tag_end: TAG_END.inspect) raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
end end
def raise_missing_variable_terminator(token, parse_context) def raise_missing_variable_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t('errors.syntax.variable_termination', token: token, tag_end: VARIABLE_END.inspect) raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
end end
def registered_tags def registered_tags

View File

@@ -9,7 +9,7 @@ module Liquid
# c.evaluate #=> true # c.evaluate #=> true
# #
class Condition #:nodoc: class Condition #:nodoc:
@operators = { @@operators = {
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) }, '==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
@@ -27,8 +27,8 @@ module Liquid
end, end,
} }
class << self def self.operators
attr_accessor :operators @@operators
end end
attr_reader :attachment, :child_condition attr_reader :attachment, :child_condition

View File

@@ -14,28 +14,38 @@ module Liquid
# #
# context['bob'] #=> nil class Context # context['bob'] #=> nil class Context
class Context class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil) # rubocop:disable Metrics/ParameterLists
@environments = [environments].flatten def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {})
@scopes = [(outer_scope || {})] new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments)
@registers = registers end
@errors = []
@partial = false def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
@strict_variables = false @environments = [environments]
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) @environments.flatten!
@static_environments = [static_environments].flat_map(&:freeze).freeze
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@partial = false
@strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@base_scope_depth = 0
squash_instance_assigns_with_environments squash_instance_assigns_with_environments
@this_stack_used = false
self.exception_renderer = Template.default_exception_renderer self.exception_renderer = Template.default_exception_renderer
self.exception_renderer = ->(_e) { raise } if rethrow_errors if rethrow_errors
self.exception_renderer = ->(_e) { raise }
end
@interrupts = [] @interrupts = []
@filters = [] @filters = []
@global_filter = nil @global_filter = nil
end end
# rubocop:enable Metrics/ParameterLists
def warnings def warnings
@warnings ||= [] @warnings ||= []
@@ -89,7 +99,7 @@ module Liquid
# Push new local scope on the stack. use <tt>Context#stack</tt> instead # Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope = {}) def push(new_scope = {})
@scopes.unshift(new_scope) @scopes.unshift(new_scope)
raise StackLevelError, 'Nesting too deep' if @scopes.length > Block::MAX_DEPTH check_overflow
end end
# Merge a hash of variables in the current local scope # Merge a hash of variables in the current local scope
@@ -100,7 +110,6 @@ module Liquid
# Pop from the stack. use <tt>Context#stack</tt> instead # Pop from the stack. use <tt>Context#stack</tt> instead
def pop def pop
raise ContextError if @scopes.size == 1 raise ContextError if @scopes.size == 1
@scopes.shift @scopes.shift
end end
@@ -112,19 +121,30 @@ module Liquid
# end # end
# #
# context['var] #=> nil # context['var] #=> nil
def stack(new_scope = nil) def stack(new_scope = {})
old_stack_used = @this_stack_used push(new_scope)
if new_scope
push(new_scope)
@this_stack_used = true
else
@this_stack_used = false
end
yield yield
ensure ensure
pop if @this_stack_used pop
@this_stack_used = old_stack_used end
# Creates a new context inheriting resource limits, filters, environment etc.,
# but with an isolated scope.
def new_isolated_subcontext
check_overflow
Context.build(
resource_limits: resource_limits,
static_environments: static_environments,
registers: StaticRegisters.new(registers)
).tap do |subcontext|
subcontext.base_scope_depth = base_scope_depth + 1
subcontext.exception_renderer = exception_renderer
subcontext.filters = @filters
subcontext.strainer = nil
subcontext.errors = errors
subcontext.warnings = warnings
end
end end
def clear_instance_assigns def clear_instance_assigns
@@ -133,10 +153,6 @@ module Liquid
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt> # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
def []=(key, value) def []=(key, value)
unless @this_stack_used
@this_stack_used = true
push({})
end
@scopes[0][key] = value @scopes[0][key] = value
end end
@@ -165,25 +181,13 @@ module Liquid
# This was changed from find() to find_index() because this is a very hot # This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation # path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.key?(key) } index = @scopes.find_index { |s| s.key?(key) }
scope = @scopes[index] if index
variable = nil variable = if index
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
if scope.nil? else
@environments.each do |e| try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
# When lookup returned a value OR there is no value but the lookup also did not raise
# then it is the value we are looking for.
if !variable.nil? || @strict_variables && raise_on_not_found
scope = e
break
end
end
end end
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
variable = variable.to_liquid variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
@@ -191,7 +195,9 @@ module Liquid
end end
def lookup_and_evaluate(obj, key, raise_on_not_found: true) def lookup_and_evaluate(obj, key, raise_on_not_found: true)
raise Liquid::UndefinedVariable, "undefined variable #{key}" if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key) if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
value = obj[key] value = obj[key]
@@ -202,13 +208,43 @@ module Liquid
end end
end end
protected
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters
private private
attr_reader :base_scope_depth
def try_variable_find_in_environments(key, raise_on_not_found:)
@environments.each do |environment|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
if !found_variable.nil? || @strict_variables && raise_on_not_found
return found_variable
end
end
@static_environments.each do |environment|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
if !found_variable.nil? || @strict_variables && raise_on_not_found
return found_variable
end
end
nil
end
def check_overflow
raise StackLevelError, "Nesting too deep" if overflow?
end
def overflow?
base_scope_depth + @scopes.length > Block::MAX_DEPTH
end
def internal_error def internal_error
# raise and catch to set backtrace and cause on exception # raise and catch to set backtrace and cause on exception
raise Liquid::InternalError, 'internal' raise Liquid::InternalError, 'internal'
rescue Liquid::InternalError => e rescue Liquid::InternalError => exc
e exc
end end
def squash_instance_assigns_with_environments def squash_instance_assigns_with_environments

View File

@@ -20,9 +20,9 @@ module Liquid
def unknown_tag(tag, parse_context) def unknown_tag(tag, parse_context)
case tag case tag
when 'else', 'end' when 'else', 'end'
raise SyntaxError, parse_context.locale.t('errors.syntax.unexpected_outer_tag', tag: tag) raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
else else
raise SyntaxError, parse_context.locale.t('errors.syntax.unknown_tag', tag: tag) raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end end
end end
end end

View File

@@ -28,7 +28,6 @@ module Liquid
# Catch all for the method # Catch all for the method
def liquid_method_missing(method) def liquid_method_missing(method)
return nil unless @context&.strict_variables return nil unless @context&.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}" raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end end
@@ -70,7 +69,7 @@ module Liquid
if include?(Enumerable) if include?(Enumerable)
blacklist += Enumerable.public_instance_methods blacklist += Enumerable.public_instance_methods
blacklist -= %i[sort count first min max include?] blacklist -= [:sort, :count, :first, :min, :max]
end end
whitelist = [:to_liquid] + (public_instance_methods - blacklist) whitelist = [:to_liquid] + (public_instance_methods - blacklist)

View File

@@ -7,12 +7,12 @@ module Liquid
attr_accessor :markup_context attr_accessor :markup_context
def to_s(with_prefix = true) def to_s(with_prefix = true)
str = +'' str = +""
str << message_prefix if with_prefix str << message_prefix if with_prefix
str << super() str << super()
if markup_context if markup_context
str << ' ' str << " "
str << markup_context str << markup_context
end end
@@ -22,20 +22,20 @@ module Liquid
private private
def message_prefix def message_prefix
str = +'' str = +""
str << if is_a?(SyntaxError) str << if is_a?(SyntaxError)
'Liquid syntax error' "Liquid syntax error"
else else
'Liquid error' "Liquid error"
end end
if line_number if line_number
str << ' (' str << " ("
str << template_name << ' ' if template_name str << template_name << " " if template_name
str << 'line ' << line_number.to_s << ')' str << "line " << line_number.to_s << ")"
end end
str << ': ' str << ": "
str str
end end
end end

View File

@@ -23,11 +23,11 @@ module Liquid
'empty' => MethodLiteral.new(:empty?, '').freeze 'empty' => MethodLiteral.new(:empty?, '').freeze
}.freeze }.freeze
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m.freeze SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m.freeze DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
INTEGERS_REGEX = /\A(-?\d+)\z/.freeze INTEGERS_REGEX = /\A(-?\d+)\z/
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/.freeze FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/.freeze RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
def self.parse(markup) def self.parse(markup)
if LITERALS.key?(markup) if LITERALS.key?(markup)

View File

@@ -17,7 +17,7 @@ module Liquid
class BlankFileSystem class BlankFileSystem
# Called by Liquid to retrieve a template file # Called by Liquid to retrieve a template file
def read_template_file(_template_path) def read_template_file(_template_path)
raise FileSystemError, 'This liquid context does not allow includes.' raise FileSystemError, "This liquid context does not allow includes."
end end
end end
@@ -46,7 +46,7 @@ module Liquid
class LocalFileSystem class LocalFileSystem
attr_accessor :root attr_accessor :root
def initialize(root, pattern = '_%s.liquid') def initialize(root, pattern = "_%s.liquid")
@root = root @root = root
@pattern = pattern @pattern = pattern
end end
@@ -59,7 +59,7 @@ module Liquid
end end
def full_path(template_path) def full_path(template_path)
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ %r{\A[^./][a-zA-Z0-9_/]+\z} raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
full_path = if template_path.include?('/') full_path = if template_path.include?('/')
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path)) File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))

View File

@@ -4,7 +4,7 @@ require 'yaml'
module Liquid module Liquid
class I18n class I18n
DEFAULT_LOCALE = File.join(File.expand_path(__dir__), 'locales', 'en.yml') DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml")
TranslationError = Class.new(StandardError) TranslationError = Class.new(StandardError)

View File

@@ -6,13 +6,13 @@ module Liquid
attr_reader :message attr_reader :message
def initialize(message = nil) def initialize(message = nil)
@message = message || 'interrupt' @message = message || "interrupt"
end end
end end
# Interrupt that is thrown whenever a {% break %} is called. # Interrupt that is thrown whenever a {% break %} is called.
class BreakInterrupt < RuntimeError; end class BreakInterrupt < Interrupt; end
# Interrupt that is thrown whenever a {% continue %} is called. # Interrupt that is thrown whenever a {% continue %} is called.
class ContinueInterrupt < RuntimeError; end class ContinueInterrupt < Interrupt; end
end end

View File

@@ -1,79 +0,0 @@
# frozen_string_literal: true
module Liquid
FilterSeparator = FILTER_SEPARATOR
ArgumentSeparator = ARGUMENT_SEPARATOR
FilterArgumentSeparator = FILTER_ARGUMENT_SEPARATOR
VariableAttributeSeparator = VARIABLE_ATTRIBUTE_SEPARATOR
WhitespaceControl = WHITESPACE_CONTROL
TagStart = TAG_START
TagEnd = TAG_END
VariableSignature = VARIABLE_SIGNATURE
VariableSegment = VARIABLE_SEGMENT
VariableStart = VARIABLE_START
VariableEnd = VARIABLE_END
VariableIncompleteEnd = VARIABLE_INCOMPLETE_END
QuotedString = QUOTED_STRING
QuotedFragment = QUOTED_FRAGMENT
TagAttributes = TAG_ATTRIBUTES
AnyStartingTag = ANY_STARTING_TAG
PartialTemplateParser = PARTIAL_TEMPLATE_PARSER
TemplateParser = TEMPLATE_PARSER
VariableParser = VARIABLE_PARSER
class BlockBody
FullToken = FULL_TOKEN
ContentOfVariable = CONTENT_OF_VARIABLE
WhitespaceOrNothing = WHITESPACE_OR_NOTHING
TAGSTART = TAG_START
VARSTART = VAR_START
end
class Assign < Tag
Syntax = SYNTAX
end
class Capture < Block
Syntax = SYNTAX
end
class Case < Block
Syntax = SYNTAX
WhenSyntax = WHEN_SYNTAX
end
class Cycle < Tag
SimpleSyntax = SIMPLE_SYNTAX
NamedSyntax = NAMED_SYNTAX
end
class For < Block
Syntax = SYNTAX
end
class If < Block
Syntax = SYNTAX
ExpressionsAndOperators = EXPRESSIONS_AND_OPERATORS
end
class Include < Tag
Syntax = SYNTAX
end
class Raw < Block
Syntax = SYNTAX
FullTokenPossiblyInvalid = FULL_TOKEN_POSSIBLY_INVALID
end
class TableRow < Block
Syntax = SYNTAX
end
class Variable
FilterMarkupRegex = FILTER_MARKUP_REGEX
FilterParser = FILTER_PARSER
FilterArgsRegex = FILTER_ARGS_REGEX
JustTagAttributes = JUST_TAG_ATTRIBUTES
MarkupWithQuotedFragment = MARKUP_WITH_QUOTED_FRAGMENT
end
end

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'strscan' require "strscan"
module Liquid module Liquid
class Lexer class Lexer
SPECIALS = { SPECIALS = {
@@ -15,13 +15,13 @@ module Liquid
'?' => :question, '?' => :question,
'-' => :dash, '-' => :dash,
}.freeze }.freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/.freeze IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/.freeze SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/.freeze DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/.freeze NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./.freeze DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/.freeze COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
WHITESPACE_OR_NOTHING = /\s*/.freeze WHITESPACE_OR_NOTHING = /\s*/
def initialize(input) def initialize(input)
@ss = StringScanner.new(input) @ss = StringScanner.new(input)
@@ -33,13 +33,18 @@ module Liquid
until @ss.eos? until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING) @ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos? break if @ss.eos?
tok = if (t = @ss.scan(COMPARISON_OPERATOR))
tok = if (t = @ss.scan(COMPARISON_OPERATOR)) then [:comparison, t] [:comparison, t]
elsif (t = @ss.scan(SINGLE_STRING_LITERAL)) then [:string, t] elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
elsif (t = @ss.scan(DOUBLE_STRING_LITERAL)) then [:string, t] [:string, t]
elsif (t = @ss.scan(NUMBER_LITERAL)) then [:number, t] elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
elsif (t = @ss.scan(IDENTIFIER)) then [:id, t] [:string, t]
elsif (t = @ss.scan(DOTDOT)) then [:dotdot, t] elsif (t = @ss.scan(NUMBER_LITERAL))
[:number, t]
elsif (t = @ss.scan(IDENTIFIER))
[:id, t]
elsif (t = @ss.scan(DOTDOT))
[:dotdot, t]
else else
c = @ss.getch c = @ss.getch
if (s = SPECIALS[c]) if (s = SPECIALS[c])

View File

@@ -22,5 +22,8 @@
tag_never_closed: "'%{block_name}' tag was never closed" tag_never_closed: "'%{block_name}' tag was never closed"
meta_syntax_error: "Liquid syntax error: #{e.message}" meta_syntax_error: "Liquid syntax error: #{e.message}"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3" table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
argument: argument:
include: "Argument error in tag 'include' - Illegal template name" include: "Argument error in tag 'include' - Illegal template name"
disabled:
tag: "usage is not allowed in this context"

View File

@@ -14,8 +14,9 @@ module Liquid
def consume(type = nil) def consume(type = nil)
token = @tokens[@p] token = @tokens[@p]
raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}" if type && token[0] != type if type && token[0] != type
raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}"
end
@p += 1 @p += 1
token[1] token[1]
end end
@@ -26,7 +27,6 @@ module Liquid
def consume?(type) def consume?(type)
token = @tokens[@p] token = @tokens[@p]
return false unless token && token[0] == type return false unless token && token[0] == type
@p += 1 @p += 1
token[1] token[1]
end end
@@ -36,7 +36,6 @@ module Liquid
token = @tokens[@p] token = @tokens[@p]
return false unless token && token[0] == :id return false unless token && token[0] == :id
return false unless token[1] == str return false unless token[1] == str
@p += 1 @p += 1
token[1] token[1]
end end
@@ -44,11 +43,10 @@ module Liquid
def look(type, ahead = 0) def look(type, ahead = 0)
tok = @tokens[@p + ahead] tok = @tokens[@p + ahead]
return false unless tok return false unless tok
tok[0] == type tok[0] == type
end end
SINGLE_TOKEN_EXPRESSION_TYPES = %i[string number].freeze SINGLE_TOKEN_EXPRESSION_TYPES = [:string, :number].freeze
private_constant :SINGLE_TOKEN_EXPRESSION_TYPES private_constant :SINGLE_TOKEN_EXPRESSION_TYPES
def expression def expression
@@ -70,9 +68,11 @@ module Liquid
end end
def argument def argument
str = +'' str = +""
# might be a keyword argument (identifier: expression) # might be a keyword argument (identifier: expression)
str << consume << consume << ' ' if look(:id) && look(:colon, 1) if look(:id) && look(:colon, 1)
str << consume << consume << ' '
end
str << expression str << expression
str str

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
module Liquid
class PartialCache
def self.load(template_name, context:, parse_context:)
cached_partials = (context.registers[:cached_partials] ||= {})
cached = cached_partials[template_name]
return cached if cached
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
source = file_system.read_template_file(template_name)
parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context)
cached_partials[template_name] = partial
ensure
parse_context.partial = false
end
end
end

View File

@@ -46,7 +46,7 @@ module Liquid
include Enumerable include Enumerable
class Timing class Timing
attr_reader :code, :partial, :line_number, :children attr_reader :code, :partial, :line_number, :children, :total_time, :self_time
def initialize(node, partial) def initialize(node, partial)
@code = node.respond_to?(:raw) ? node.raw : node @code = node.respond_to?(:raw) ? node.raw : node
@@ -65,6 +65,17 @@ module Liquid
def finish def finish
@end_time = Time.now @end_time = Time.now
@total_time = @end_time - @start_time
if @children.empty?
@self_time = @total_time
else
total_children_time = 0
@children.each do |child|
total_children_time += child.total_time
end
@self_time = @total_time - total_children_time
end
end end
def render_time def render_time
@@ -98,10 +109,10 @@ module Liquid
Thread.current[:liquid_profiler] Thread.current[:liquid_profiler]
end end
def initialize def initialize(partial_name = "<root>")
@partial_stack = ['<root>'] @partial_stack = [partial_name]
@root_timing = Timing.new('', current_partial) @root_timing = Timing.new("", current_partial)
@timing_stack = [@root_timing] @timing_stack = [@root_timing]
@render_start_at = Time.now @render_start_at = Time.now

6
lib/liquid/register.rb Normal file
View File

@@ -0,0 +1,6 @@
# frozen_string_literal: true
module Liquid
class Register
end
end

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
module Liquid
class DisabledTags < Register
def initialize
@disabled_tags = {}
end
def disabled?(tag)
@disabled_tags.key?(tag) && @disabled_tags[tag] > 0
end
def disable(tags)
tags.each(&method(:increment))
yield
ensure
tags.each(&method(:decrement))
end
private
def increment(tag)
@disabled_tags[tag] ||= 0
@disabled_tags[tag] += 1
end
def decrement(tag)
@disabled_tags[tag] -= 1
end
end
Template.add_register(:disabled_tags, DisabledTags.new)
end

View File

@@ -3,7 +3,7 @@
module Liquid module Liquid
class ResourceLimits class ResourceLimits
attr_accessor :render_length, :render_score, :assign_score, attr_accessor :render_length, :render_score, :assign_score,
:render_length_limit, :render_score_limit, :assign_score_limit :render_length_limit, :render_score_limit, :assign_score_limit
def initialize(limits) def initialize(limits)
@render_length_limit = limits[:render_length_limit] @render_length_limit = limits[:render_length_limit]

View File

@@ -12,13 +12,13 @@ module Liquid
'"' => '&quot;', '"' => '&quot;',
"'" => '&#39;', "'" => '&#39;',
}.freeze }.freeze
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/.freeze HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
STRIP_HTML_BLOCKS = Regexp.union( STRIP_HTML_BLOCKS = Regexp.union(
%r{<script.*?</script>}m, %r{<script.*?</script>}m,
/<!--.*?-->/m, /<!--.*?-->/m,
%r{<style.*?</style>}m %r{<style.*?</style>}m
) )
STRIP_HTML_TAGS = /<.*?>/m.freeze STRIP_HTML_TAGS = /<.*?>/m
# Return the size of an array or of an string # Return the size of an array or of an string
def size(input) def size(input)
@@ -74,9 +74,8 @@ module Liquid
end end
# Truncate a string down to x characters # Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = '...') def truncate(input, length = 50, truncate_string = "...")
return if input.nil? return if input.nil?
input_str = input.to_s input_str = input.to_s
length = Utils.to_integer(length) length = Utils.to_integer(length)
truncate_string_str = truncate_string.to_s truncate_string_str = truncate_string.to_s
@@ -85,14 +84,13 @@ module Liquid
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
end end
def truncatewords(input, words = 15, truncate_string = '...') def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil? return if input.nil?
wordlist = input.to_s.split wordlist = input.to_s.split
words = Utils.to_integer(words) words = Utils.to_integer(words)
l = words - 1 l = words - 1
l = 0 if l < 0 l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(' ').concat(truncate_string.to_s) : input wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : input
end end
# Split input string into an array of substrings separated by given pattern. # Split input string into an array of substrings separated by given pattern.
@@ -130,13 +128,13 @@ module Liquid
# Join elements of the array with certain character between them # Join elements of the array with certain character between them
def join(input, glue = ' ') def join(input, glue = ' ')
InputIterator.new(input).join(glue) InputIterator.new(input, context).join(glue)
end end
# Sort elements of the array # Sort elements of the array
# provide optional property with which to sort an array of hashes or drops # provide optional property with which to sort an array of hashes or drops
def sort(input, property = nil) def sort(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
return [] if ary.empty? return [] if ary.empty?
@@ -156,7 +154,7 @@ module Liquid
# Sort elements of an array ignoring case if strings # Sort elements of an array ignoring case if strings
# provide optional property with which to sort an array of hashes or drops # provide optional property with which to sort an array of hashes or drops
def sort_natural(input, property = nil) def sort_natural(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
return [] if ary.empty? return [] if ary.empty?
@@ -176,7 +174,7 @@ module Liquid
# Filter the elements of an array to those with a certain property value. # Filter the elements of an array to those with a certain property value.
# By default the target is any truthy value. # By default the target is any truthy value.
def where(input, property, target_value = nil) def where(input, property, target_value = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
if ary.empty? if ary.empty?
[] []
@@ -198,7 +196,7 @@ module Liquid
# Remove duplicate elements from an array # Remove duplicate elements from an array
# provide optional property with which to determine uniqueness # provide optional property with which to determine uniqueness
def uniq(input, property = nil) def uniq(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
if property.nil? if property.nil?
ary.uniq ary.uniq
@@ -215,16 +213,16 @@ module Liquid
# Reverse the elements of an array # Reverse the elements of an array
def reverse(input) def reverse(input)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
ary.reverse ary.reverse
end end
# map/collect on a given property # map/collect on a given property
def map(input, property) def map(input, property)
InputIterator.new(input).map do |e| InputIterator.new(input, context).map do |e|
e = e.call if e.is_a?(Proc) e = e.call if e.is_a?(Proc)
if property == 'to_liquid' if property == "to_liquid"
e e
elsif e.respond_to?(:[]) elsif e.respond_to?(:[])
r = e[property] r = e[property]
@@ -238,7 +236,7 @@ module Liquid
# Remove nils within an array # Remove nils within an array
# provide optional property with which to check for nil # provide optional property with which to check for nil
def compact(input, property = nil) def compact(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
if property.nil? if property.nil?
ary.compact ary.compact
@@ -279,9 +277,10 @@ module Liquid
end end
def concat(input, array) def concat(input, array)
raise ArgumentError, 'concat filter requires an array argument' unless array.respond_to?(:to_ary) unless array.respond_to?(:to_ary)
raise ArgumentError, "concat filter requires an array argument"
InputIterator.new(input).concat(array) end
InputIterator.new(input, context).concat(array)
end end
# prepend a string to another # prepend a string to another
@@ -422,16 +421,26 @@ module Liquid
result.is_a?(BigDecimal) ? result.to_f : result result.is_a?(BigDecimal) ? result.to_f : result
end end
def default(input, default_value = '') # Set a default value when the input is nil, false or empty
if !input || input.respond_to?(:empty?) && input.empty? #
default_value # Example:
else # {{ product.title | default: "No Title" }}
input #
end # Use `allow_false` when an input should only be tested against nil or empty and not false.
#
# Example:
# {{ product.title | default: "No Title", allow_false: true }}
#
def default(input, default_value = '', options = {})
options = {} unless options.is_a?(Hash)
false_check = options['allow_false'] ? input.nil? : !input
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
end end
private private
attr_reader :context
def raise_property_error(property) def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{property}'" raise Liquid::ArgumentError, "cannot select the property '#{property}'"
end end
@@ -460,7 +469,8 @@ module Liquid
class InputIterator class InputIterator
include Enumerable include Enumerable
def initialize(input) def initialize(input, context)
@context = context
@input = if input.is_a?(Array) @input = if input.is_a?(Array)
input.flatten input.flatten
elsif input.is_a?(Hash) elsif input.is_a?(Hash)
@@ -499,6 +509,7 @@ module Liquid
def each def each
@input.each do |e| @input.each do |e|
e.context = @context if e.respond_to?(:context=)
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e) yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
end end
end end

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Liquid
class StaticRegisters
attr_reader :static, :registers
def initialize(registers = {})
@static = registers.is_a?(StaticRegisters) ? registers.static : registers
@registers = {}
end
def []=(key, value)
@registers[key] = value
end
def [](key)
if @registers.key?(key)
@registers[key]
else
@static[key]
end
end
def delete(key)
@registers.delete(key)
end
def fetch(key, default = nil)
key?(key) ? self[key] : default
end
def key?(key)
@registers.key?(key) || @static.key?(key)
end
end
end

View File

@@ -9,12 +9,12 @@ module Liquid
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter, # The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
# Context#add_filters or Template.register_filter # Context#add_filters or Template.register_filter
class Strainer #:nodoc: class Strainer #:nodoc:
@global_strainer = Class.new(Strainer) do @@global_strainer = Class.new(Strainer) do
@filter_methods = Set.new @filter_methods = Set.new
end end
@strainer_class_cache = Hash.new do |hash, filters| @@strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(@global_strainer) do hash[filters] = Class.new(@@global_strainer) do
@filter_methods = Strainer.global_strainer.filter_methods.dup @filter_methods = @@global_strainer.filter_methods.dup
filters.each { |f| add_filter(f) } filters.each { |f| add_filter(f) }
end end
end end
@@ -24,13 +24,11 @@ module Liquid
end end
class << self class << self
attr_accessor :strainer_class_cache, :global_strainer
attr_reader :filter_methods attr_reader :filter_methods
end end
def self.add_filter(filter) def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module) raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless include?(filter) unless include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) } invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any? if invokable_non_public_methods.any?
@@ -43,8 +41,8 @@ module Liquid
end end
def self.global_filter(filter) def self.global_filter(filter)
@strainer_class_cache.clear @@strainer_class_cache.clear
@global_strainer.add_filter(filter) @@global_strainer.add_filter(filter)
end end
def self.invokable?(method) def self.invokable?(method)
@@ -52,7 +50,7 @@ module Liquid
end end
def self.create(context, filters = []) def self.create(context, filters = [])
@strainer_class_cache[filters].new(context) @@strainer_class_cache[filters].new(context)
end end
def invoke(method, *args) def invoke(method, *args)

View File

@@ -7,13 +7,21 @@ module Liquid
include ParserSwitching include ParserSwitching
class << self class << self
def parse(tag_name, markup, tokenizer, options) def parse(tag_name, markup, tokenizer, parse_context)
tag = new(tag_name, markup, options) tag = new(tag_name, markup, parse_context)
tag.parse(tokenizer) tag.parse(tokenizer)
tag tag
end end
def disable_tags(*tags)
disabled_tags.push(*tags)
end
private :new private :new
def disabled_tags
@disabled_tags ||= []
end
end end
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
@@ -23,7 +31,8 @@ module Liquid
@line_number = parse_context.line_number @line_number = parse_context.line_number
end end
def parse(_tokens); end def parse(_tokens)
end
def raw def raw
"#{@tag_name} #{@markup}" "#{@tag_name} #{@markup}"
@@ -37,6 +46,14 @@ module Liquid
'' ''
end end
def disabled?(context)
context.registers[:disabled_tags].disabled?(tag_name)
end
def disabled_error_message
"#{tag_name} #{options[:locale].t('errors.disabled.tag')}"
end
# For backwards compatibility with custom tags. In a future release, the semantics # For backwards compatibility with custom tags. In a future release, the semantics
# of the `render_to_output_buffer` method will become the default and the `render` # of the `render_to_output_buffer` method will become the default and the `render`
# method will be removed. # method will be removed.
@@ -48,5 +65,9 @@ module Liquid
def blank? def blank?
false false
end end
def disabled_tags
self.class.disabled_tags
end
end end
end end

View File

@@ -10,17 +10,21 @@ module Liquid
# {{ foo }} # {{ foo }}
# #
class Assign < Tag class Assign < Tag
SYNTAX = /(#{VARIABLE_SIGNATURE}+)\s*=\s*(.*)\s*/om.freeze Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
def self.syntax_error_translation_key
"errors.syntax.assign"
end
attr_reader :to, :from attr_reader :to, :from
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ SYNTAX if markup =~ Syntax
@to = Regexp.last_match(1) @to = Regexp.last_match(1)
@from = Variable.new(Regexp.last_match(2), options) @from = Variable.new(Regexp.last_match(2), options)
else else
raise SyntaxError, options[:locale].t('errors.syntax.assign') raise SyntaxError, options[:locale].t(self.class.syntax_error_translation_key)
end end
end end

View File

@@ -13,14 +13,14 @@ module Liquid
# in a sidebar or footer. # in a sidebar or footer.
# #
class Capture < Block class Capture < Block
SYNTAX = /(#{VARIABLE_SIGNATURE}+)/o.freeze Syntax = /(#{VariableSignature}+)/o
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ SYNTAX if markup =~ Syntax
@to = Regexp.last_match(1) @to = Regexp.last_match(1)
else else
raise SyntaxError, options[:locale].t('errors.syntax.capture') raise SyntaxError, options[:locale].t("errors.syntax.capture")
end end
end end

View File

@@ -2,8 +2,8 @@
module Liquid module Liquid
class Case < Block class Case < Block
SYNTAX = /(#{QUOTED_FRAGMENT})/o.freeze Syntax = /(#{QuotedFragment})/o
WHEN_SYNTAX = /(#{QUOTED_FRAGMENT})(?:(?:\s+or\s+|\s*\,\s*)(#{QUOTED_FRAGMENT}.*))?/om.freeze WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
attr_reader :blocks, :left attr_reader :blocks, :left
@@ -11,10 +11,10 @@ module Liquid
super super
@blocks = [] @blocks = []
if markup =~ SYNTAX if markup =~ Syntax
@left = Expression.parse(Regexp.last_match(1)) @left = Expression.parse(Regexp.last_match(1))
else else
raise SyntaxError, options[:locale].t('errors.syntax.case') raise SyntaxError, options[:locale].t("errors.syntax.case")
end end
end end
@@ -39,16 +39,14 @@ module Liquid
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.stack do execute_else_block = true
execute_else_block = true
@blocks.each do |block| @blocks.each do |block|
if block.else? if block.else?
block.attachment.render_to_output_buffer(context, output) if execute_else_block block.attachment.render_to_output_buffer(context, output) if execute_else_block
elsif block.evaluate(context) elsif block.evaluate(context)
execute_else_block = false execute_else_block = false
block.attachment.render_to_output_buffer(context, output) block.attachment.render_to_output_buffer(context, output)
end
end end
end end
@@ -61,7 +59,9 @@ module Liquid
body = BlockBody.new body = BlockBody.new
while markup while markup
raise SyntaxError, options[:locale].t('errors.syntax.case_invalid_when') unless markup =~ WHEN_SYNTAX unless markup =~ WhenSyntax
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
end
markup = Regexp.last_match(2) markup = Regexp.last_match(2)
@@ -72,7 +72,9 @@ module Liquid
end end
def record_else_condition(markup) def record_else_condition(markup)
raise SyntaxError, options[:locale].t('errors.syntax.case_invalid_else') unless markup.strip.empty? unless markup.strip.empty?
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
end
block = ElseCondition.new block = ElseCondition.new
block.attach(BlockBody.new) block.attach(BlockBody.new)

View File

@@ -6,7 +6,8 @@ module Liquid
output output
end end
def unknown_tag(_tag, _markup, _tokens); end def unknown_tag(_tag, _markup, _tokens)
end
def blank? def blank?
true true

View File

@@ -14,47 +14,45 @@ module Liquid
# <div class="green"> Item five</div> # <div class="green"> Item five</div>
# #
class Cycle < Tag class Cycle < Tag
SIMPLE_SYNTAX = /\A#{QUOTED_FRAGMENT}+/o.freeze SimpleSyntax = /\A#{QuotedFragment}+/o
NAMED_SYNTAX = /\A(#{QUOTED_FRAGMENT})\s*\:\s*(.*)/om.freeze NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
attr_reader :variables attr_reader :variables
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
case markup case markup
when NAMED_SYNTAX when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2)) @variables = variables_from_string(Regexp.last_match(2))
@name = Expression.parse(Regexp.last_match(1)) @name = Expression.parse(Regexp.last_match(1))
when SIMPLE_SYNTAX when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
else else
raise SyntaxError, options[:locale].t('errors.syntax.cycle') raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end end
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.registers[:cycle] ||= {} context.registers[:cycle] ||= {}
context.stack do key = context.evaluate(@name)
key = context.evaluate(@name) iteration = context.registers[:cycle][key].to_i
iteration = context.registers[:cycle][key].to_i
val = context.evaluate(@variables[iteration]) val = context.evaluate(@variables[iteration])
if val.is_a?(Array) if val.is_a?(Array)
val = val.join val = val.join
elsif !val.is_a?(String) elsif !val.is_a?(String)
val = val.to_s val = val.to_s
end
output << val
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
end end
output << val
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
output output
end end
@@ -62,7 +60,7 @@ module Liquid
def variables_from_string(markup) def variables_from_string(markup)
markup.split(',').collect do |var| markup.split(',').collect do |var|
var =~ /\s*(#{QUOTED_FRAGMENT})\s*/o var =~ /\s*(#{QuotedFragment})\s*/o
Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil
end.compact end.compact
end end

26
lib/liquid/tags/echo.rb Normal file
View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
module Liquid
# Echo outputs an expression
#
# {% echo monkey %}
# {% echo user.name %}
#
# This is identical to variable output syntax, like {{ foo }}, but works
# inside {% liquid %} tags. The full syntax is supported, including filters:
#
# {% echo user | link %}
#
class Echo < Tag
def initialize(tag_name, markup, parse_context)
super
@variable = Variable.new(markup, parse_context)
end
def render(context)
@variable.render_to_output_buffer(context, +'')
end
end
Template.register_tag('echo', Echo)
end

View File

@@ -46,7 +46,7 @@ module Liquid
# forloop.parentloop:: Provides access to the parent loop, if present. # forloop.parentloop:: Provides access to the parent loop, if present.
# #
class For < Block class For < Block
SYNTAX = /\A(#{VARIABLE_SEGMENT}+)\s+in\s+(#{QUOTED_FRAGMENT}+)\s*(reversed)?/o.freeze Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
attr_reader :collection_name, :variable_name, :limit, :from attr_reader :collection_name, :variable_name, :limit, :from
@@ -60,7 +60,6 @@ module Liquid
def parse(tokens) def parse(tokens)
return unless parse_body(@for_block, tokens) return unless parse_body(@for_block, tokens)
parse_body(@else_block, tokens) parse_body(@else_block, tokens)
end end
@@ -70,7 +69,6 @@ module Liquid
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
return super unless tag == 'else' return super unless tag == 'else'
@else_block = BlockBody.new @else_block = BlockBody.new
end end
@@ -89,25 +87,24 @@ module Liquid
protected protected
def lax_parse(markup) def lax_parse(markup)
if markup =~ SYNTAX if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = Regexp.last_match(1)
collection_name = Regexp.last_match(2) collection_name = Regexp.last_match(2)
@reversed = !!Regexp.last_match(3) @reversed = !!Regexp.last_match(3)
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name) @collection_name = Expression.parse(collection_name)
markup.scan(TAG_ATTRIBUTES) do |key, value| markup.scan(TagAttributes) do |key, value|
set_attribute(key, value) set_attribute(key, value)
end end
else else
raise SyntaxError, options[:locale].t('errors.syntax.for') raise SyntaxError, options[:locale].t("errors.syntax.for")
end end
end end
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = Parser.new(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError, options[:locale].t('errors.syntax.for_invalid_in') unless p.id?('in') raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
collection_name = p.expression collection_name = p.expression
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name) @collection_name = Expression.parse(collection_name)
@@ -115,9 +112,8 @@ module Liquid
while p.look(:id) && p.look(:colon, 1) while p.look(:id) && p.look(:colon, 1)
unless (attribute = p.id?('limit') || p.id?('offset')) unless (attribute = p.id?('limit') || p.id?('offset'))
raise SyntaxError, options[:locale].t('errors.syntax.for_invalid_attribute') raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end end
p.consume p.consume
set_attribute(attribute, p.expression) set_attribute(attribute, p.expression)
end end
@@ -177,7 +173,6 @@ module Liquid
# Handle any interrupts if they exist. # Handle any interrupts if they exist.
next unless context.interrupt? next unless context.interrupt?
interrupt = context.pop_interrupt interrupt = context.pop_interrupt
break if interrupt.is_a?(BreakInterrupt) break if interrupt.is_a?(BreakInterrupt)
next if interrupt.is_a?(ContinueInterrupt) next if interrupt.is_a?(ContinueInterrupt)

View File

@@ -12,9 +12,9 @@ module Liquid
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need. # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
# #
class If < Block class If < Block
SYNTAX = /(#{QUOTED_FRAGMENT})\s*([=!<>a-z_]+)?\s*(#{QUOTED_FRAGMENT})?/o.freeze Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
EXPRESSIONS_AND_OPERATORS = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QUOTED_FRAGMENT}|\S+)\s*)+)/o.freeze ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
BOOLEAN_OPERATORS = %w[and or].freeze BOOLEAN_OPERATORS = %w(and or).freeze
attr_reader :blocks attr_reader :blocks
@@ -34,7 +34,7 @@ module Liquid
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
if %w[elsif else].include?(tag) if ['elsif', 'else'].include?(tag)
push_block(tag, markup) push_block(tag, markup)
else else
super super
@@ -42,9 +42,9 @@ module Liquid
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.stack do @blocks.each do |block|
@blocks.each do |block| if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output) if block.evaluate(context) return block.attachment.render_to_output_buffer(context, output)
end end
end end
@@ -65,19 +65,18 @@ module Liquid
end end
def lax_parse(markup) def lax_parse(markup)
expressions = markup.scan(EXPRESSIONS_AND_OPERATORS) expressions = markup.scan(ExpressionsAndOperators)
raise SyntaxError, options[:locale].t('errors.syntax.if') unless expressions.pop =~ SYNTAX raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3))) condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
until expressions.empty? until expressions.empty?
operator = expressions.pop.to_s.strip operator = expressions.pop.to_s.strip
raise SyntaxError, options[:locale].t('errors.syntax.if') unless expressions.pop.to_s =~ SYNTAX raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
new_condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3))) new_condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
raise SyntaxError, options[:locale].t('errors.syntax.if') unless BOOLEAN_OPERATORS.include?(operator) raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition) new_condition.send(operator, condition)
condition = new_condition condition = new_condition
end end

View File

@@ -3,14 +3,12 @@
module Liquid module Liquid
class Ifchanged < Block class Ifchanged < Block
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.stack do block_output = +''
block_output = +'' super(context, block_output)
super(context, block_output)
if block_output != context.registers[:ifchanged] if block_output != context.registers[:ifchanged]
context.registers[:ifchanged] = block_output context.registers[:ifchanged] = block_output
output << block_output output << block_output
end
end end
output output

View File

@@ -16,7 +16,8 @@ module Liquid
# {% include 'product' for products %} # {% include 'product' for products %}
# #
class Include < Tag class Include < Tag
SYNTAX = /(#{QUOTED_FRAGMENT}+)(\s+(?:with|for)\s+(#{QUOTED_FRAGMENT}+))?/o.freeze SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
Syntax = SYNTAX
attr_reader :template_name_expr, :variable_name_expr, :attributes attr_reader :template_name_expr, :variable_name_expr, :attributes
@@ -28,27 +29,34 @@ module Liquid
template_name = Regexp.last_match(1) template_name = Regexp.last_match(1)
variable_name = Regexp.last_match(3) variable_name = Regexp.last_match(3)
@alias_name = Regexp.last_match(5)
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = Expression.parse(template_name) @template_name_expr = Expression.parse(template_name)
@attributes = {} @attributes = {}
markup.scan(TAG_ATTRIBUTES) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = Expression.parse(value)
end end
else else
raise SyntaxError, options[:locale].t('errors.syntax.include') raise SyntaxError, options[:locale].t("errors.syntax.include")
end end
end end
def parse(_tokens); end def parse(_tokens)
end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr) template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t('errors.argument.include') unless template_name raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
partial = load_cached_partial(template_name, context) partial = PartialCache.load(
context_variable_name = template_name.split('/').last template_name,
context: context,
parse_context: parse_context
)
context_variable_name = @alias_name || template_name.split('/').last
variable = if @variable_name_expr variable = if @variable_name_expr
context.evaluate(@variable_name_expr) context.evaluate(@variable_name_expr)
@@ -84,36 +92,9 @@ module Liquid
output output
end end
private
alias_method :parse_context, :options alias_method :parse_context, :options
private :parse_context private :parse_context
def load_cached_partial(template_name, context)
cached_partials = context.registers[:cached_partials] || {}
if (cached = cached_partials[template_name])
return cached
end
source = read_template_from_file_system(context)
begin
parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context)
ensure
parse_context.partial = false
end
cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials
partial
end
def read_template_from_file_system(context)
file_system = context.registers[:file_system] || Liquid::Template.file_system
file_system.read_template_file(context.evaluate(@template_name_expr))
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
[ [

View File

@@ -25,6 +25,7 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0 value = context.environments.first[@variable] ||= 0
context.environments.first[@variable] = value + 1 context.environments.first[@variable] = value + 1
output << value.to_s output << value.to_s
output output
end end

View File

@@ -2,8 +2,8 @@
module Liquid module Liquid
class Raw < Block class Raw < Block
SYNTAX = /\A\s*\z/.freeze Syntax = /\A\s*\z/
FULL_TOKEN_POSSIBLY_INVALID = /\A(.*)#{TAG_START}\s*(\w+)\s*(.*)?#{TAG_END}\z/om.freeze FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
super super
@@ -14,14 +14,14 @@ module Liquid
def parse(tokens) def parse(tokens)
@body = +'' @body = +''
while (token = tokens.shift) while (token = tokens.shift)
if token =~ FULL_TOKEN_POSSIBLY_INVALID if token =~ FullTokenPossiblyInvalid
@body << Regexp.last_match(1) if Regexp.last_match(1) != '' @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return if block_delimiter == Regexp.last_match(2) return if block_delimiter == Regexp.last_match(2)
end end
@body << token unless token.empty? @body << token unless token.empty?
end end
raise SyntaxError, parse_context.locale.t('errors.syntax.tag_never_closed', block_name: block_name) raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end end
def render_to_output_buffer(_context, output) def render_to_output_buffer(_context, output)
@@ -40,7 +40,9 @@ module Liquid
protected protected
def ensure_valid_markup(tag_name, markup, parse_context) def ensure_valid_markup(tag_name, markup, parse_context)
raise SyntaxError, parse_context.locale.t('errors.syntax.tag_unexpected_args', tag: tag_name) unless markup =~ SYNTAX unless Syntax.match?(markup)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
end
end end
end end

80
lib/liquid/tags/render.rb Normal file
View File

@@ -0,0 +1,80 @@
# frozen_string_literal: true
module Liquid
class Render < Tag
SYNTAX = /(#{QuotedString}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
disable_tags "include"
attr_reader :template_name_expr, :attributes
def initialize(tag_name, markup, options)
super
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
template_name = Regexp.last_match(1)
variable_name = Regexp.last_match(3)
@alias_name = Regexp.last_match(5)
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = Expression.parse(template_name)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value)
end
end
def render_to_output_buffer(context, output)
render_tag(context, output)
end
def render_tag(context, output)
# Though we evaluate this here we will only ever parse it as a string literal.
template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context
)
context_variable_name = @alias_name || template_name.split('/').last
render_partial_func = ->(var, forloop) {
inner_context = context.new_isolated_subcontext
inner_context.template_name = template_name
inner_context.partial = true
inner_context['forloop'] = forloop if forloop
@attributes.each do |key, value|
inner_context[key] = context.evaluate(value)
end
inner_context[context_variable_name] = var unless var.nil?
partial.render_to_output_buffer(inner_context, output)
forloop&.send(:increment!)
}
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
if variable.is_a?(Array)
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
variable.each { |var| render_partial_func.call(var, forloop) }
else
render_partial_func.call(variable, nil)
end
output
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
] + @node.attributes.values
end
end
end
Template.register_tag('render', Render)
end

View File

@@ -2,21 +2,21 @@
module Liquid module Liquid
class TableRow < Block class TableRow < Block
SYNTAX = /(\w+)\s+in\s+(#{QUOTED_FRAGMENT}+)/o.freeze Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
attr_reader :variable_name, :collection_name, :attributes attr_reader :variable_name, :collection_name, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ SYNTAX if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = Regexp.last_match(1)
@collection_name = Expression.parse(Regexp.last_match(2)) @collection_name = Expression.parse(Regexp.last_match(2))
@attributes = {} @attributes = {}
markup.scan(TAG_ATTRIBUTES) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = Expression.parse(value)
end end
else else
raise SyntaxError, options[:locale].t('errors.syntax.table_row') raise SyntaxError, options[:locale].t("errors.syntax.table_row")
end end
end end
@@ -44,7 +44,9 @@ module Liquid
super super
output << '</td>' output << '</td>'
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">" if tablerowloop.col_last && !tablerowloop.last if tablerowloop.col_last && !tablerowloop.last
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
end
tablerowloop.send(:increment!) tablerowloop.send(:increment!)
end end

View File

@@ -9,14 +9,16 @@ module Liquid
# #
class Unless < If class Unless < If
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.stack do # First condition is interpreted backwards ( if not )
# First condition is interpreted backwards ( if not ) first_block = @blocks.first
first_block = @blocks.first unless first_block.evaluate(context)
return first_block.attachment.render_to_output_buffer(context, output) unless first_block.evaluate(context) return first_block.attachment.render_to_output_buffer(context, output)
end
# After the first condition unless works just like if # After the first condition unless works just like if
@blocks[1..-1].each do |block| @blocks[1..-1].each do |block|
return block.attachment.render_to_output_buffer(context, output) if block.evaluate(context) if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output)
end end
end end

View File

@@ -18,11 +18,7 @@ module Liquid
attr_accessor :root attr_accessor :root
attr_reader :resource_limits, :warnings attr_reader :resource_limits, :warnings
@file_system = BlankFileSystem.new @@file_system = BlankFileSystem.new
class << self
attr_accessor :file_system
end
class TagRegistry class TagRegistry
include Enumerable include Enumerable
@@ -80,6 +76,14 @@ module Liquid
exception exception
end end
def file_system
@@file_system
end
def file_system=(obj)
@@file_system = obj
end
def register_tag(name, klass) def register_tag(name, klass)
tags[name.to_s] = klass tags[name.to_s] = klass
end end
@@ -88,6 +92,14 @@ module Liquid
@tags ||= TagRegistry.new @tags ||= TagRegistry.new
end end
def add_register(name, klass)
registers[name.to_sym] = klass
end
def registers
@registers ||= {}
end
def error_mode def error_mode
@error_mode ||= :lax @error_mode ||= :lax
end end
@@ -169,7 +181,9 @@ module Liquid
when Liquid::Context when Liquid::Context
c = args.shift c = args.shift
c.exception_renderer = ->(_e) { raise } if @rethrow_errors if @rethrow_errors
c.exception_renderer = ->(_e) { raise }
end
c c
when Liquid::Drop when Liquid::Drop
@@ -180,23 +194,31 @@ module Liquid
when nil when nil
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits) Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
else else
raise ArgumentError, 'Expected Hash or Liquid::Context as parameter' raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end end
output = nil output = nil
context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
case args.last case args.last
when Hash when Hash
options = args.pop options = args.pop
output = options[:output] if options[:output] output = options[:output] if options[:output]
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash) options[:registers]&.each do |key, register|
context_register[key] = register
end
apply_options_to_context(context, options) apply_options_to_context(context, options)
when Module, Array when Module, Array
context.add_filters(args.pop) context.add_filters(args.pop)
end end
Template.registers.each do |key, register|
context_register[key] = register
end
# Retrying a render resets resource usage # Retrying a render resets resource usage
context.resource_limits.reset context.resource_limits.reset
@@ -232,7 +254,7 @@ module Liquid
if @profiling && !context.partial if @profiling && !context.partial
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler) raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
@profiler = Profiler.new @profiler = Profiler.new(context.template_name)
@profiler.start @profiler.start
begin begin

View File

@@ -2,27 +2,33 @@
module Liquid module Liquid
class Tokenizer class Tokenizer
attr_reader :line_number attr_reader :line_number, :for_liquid_tag
def initialize(source, line_numbers = false) def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@source = source @source = source
@line_number = line_numbers ? 1 : nil @line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag
@tokens = tokenize @tokens = tokenize
end end
def shift def shift
token = @tokens.shift (token = @tokens.shift) || return
@line_number += token.count("\n") if @line_number && token
if @line_number
@line_number += @for_liquid_tag ? 1 : token.count("\n")
end
token token
end end
private private
def tokenize def tokenize
@source = @source.source if @source.respond_to?(:source)
return [] if @source.to_s.empty? return [] if @source.to_s.empty?
tokens = @source.split(TEMPLATE_PARSER) return @source.split("\n") if @for_liquid_tag
tokens = @source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array # removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0]&.empty? tokens.shift if tokens[0]&.empty?

8
lib/liquid/usage.rb Normal file
View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
module Liquid
module Usage
def self.increment(name)
end
end
end

View File

@@ -21,9 +21,13 @@ module Liquid
return [] unless collection.respond_to?(:each) return [] unless collection.respond_to?(:each)
collection.each do |item| collection.each do |item|
break if to && to <= index if to && to <= index
break
end
segments << item if from <= index if from <= index
segments << item
end
index += 1 index += 1
end end
@@ -33,12 +37,11 @@ module Liquid
def self.to_integer(num) def self.to_integer(num)
return num if num.is_a?(Integer) return num if num.is_a?(Integer)
num = num.to_s num = num.to_s
begin begin
Integer(num) Integer(num)
rescue ::ArgumentError rescue ::ArgumentError
raise Liquid::ArgumentError, 'invalid integer' raise Liquid::ArgumentError, "invalid integer"
end end
end end
@@ -49,7 +52,7 @@ module Liquid
when Numeric when Numeric
obj obj
when String when String
obj.strip =~ /\A-?\d+\.\d+\z/ ? BigDecimal(obj) : obj.to_i /\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
else else
if obj.respond_to?(:to_number) if obj.respond_to?(:to_number)
obj.to_number obj.to_number
@@ -64,7 +67,6 @@ module Liquid
if obj.is_a?(String) if obj.is_a?(String)
return nil if obj.empty? return nil if obj.empty?
obj = obj.downcase obj = obj.downcase
end end

View File

@@ -12,11 +12,11 @@ module Liquid
# {{ user | link }} # {{ user | link }}
# #
class Variable class Variable
FILTER_MARKUP_REGEX = /#{FILTER_SEPARATOR}\s*(.*)/om.freeze FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
FILTER_PARSER = /(?:\s+|#{QUOTED_FRAGMENT}|#{ARGUMENT_SEPARATOR})+/o.freeze FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
FILTER_ARGS_REGEX = /(?:#{FILTER_ARGUMENT_SEPARATOR}|#{ARGUMENT_SEPARATOR})\s*((?:\w+\s*\:\s*)?#{QUOTED_FRAGMENT})/o.freeze FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
JUST_TAG_ATTRIBUTES = /\A#{TAG_ATTRIBUTES}\z/o.freeze JustTagAttributes = /\A#{TagAttributes}\z/o
MARKUP_WITH_QUOTED_FRAGMENT = /(#{QUOTED_FRAGMENT})(.*)/om.freeze MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
attr_accessor :filters, :name, :line_number attr_accessor :filters, :name, :line_number
attr_reader :parse_context attr_reader :parse_context
@@ -43,18 +43,17 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
@filters = [] @filters = []
return unless markup =~ MARKUP_WITH_QUOTED_FRAGMENT return unless markup =~ MarkupWithQuotedFragment
name_markup = Regexp.last_match(1) name_markup = Regexp.last_match(1)
filter_markup = Regexp.last_match(2) filter_markup = Regexp.last_match(2)
@name = Expression.parse(name_markup) @name = Expression.parse(name_markup)
if filter_markup =~ FILTER_MARKUP_REGEX if filter_markup =~ FilterMarkupRegex
filters = Regexp.last_match(1).scan(FILTER_PARSER) filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f| filters.each do |f|
next unless f =~ /\w+/ next unless f =~ /\w+/
filtername = Regexp.last_match(0) filtername = Regexp.last_match(0)
filterargs = f.scan(FILTER_ARGS_REGEX).flatten filterargs = f.scan(FilterArgsRegex).flatten
@filters << parse_filter_expressions(filtername, filterargs) @filters << parse_filter_expressions(filtername, filterargs)
end end
end end
@@ -105,13 +104,21 @@ module Liquid
output output
end end
def disabled?(_context)
false
end
def disabled_tags
[]
end
private private
def parse_filter_expressions(filter_name, unparsed_args) def parse_filter_expressions(filter_name, unparsed_args)
filter_args = [] filter_args = []
keyword_args = nil keyword_args = nil
unparsed_args.each do |a| unparsed_args.each do |a|
if (matches = a.match(JUST_TAG_ATTRIBUTES)) if (matches = a.match(JustTagAttributes))
keyword_args ||= {} keyword_args ||= {}
keyword_args[matches[1]] = Expression.parse(matches[2]) keyword_args[matches[1]] = Expression.parse(matches[2])
else else
@@ -139,7 +146,7 @@ module Liquid
return unless obj.tainted? return unless obj.tainted?
return if Template.taint_mode == :lax return if Template.taint_mode == :lax
@markup =~ QUOTED_FRAGMENT @markup =~ QuotedFragment
name = Regexp.last_match(0) name = Regexp.last_match(0)
error = TaintedError.new("variable '#{name}' is tainted and was not escaped") error = TaintedError.new("variable '#{name}' is tainted and was not escaped")

View File

@@ -2,8 +2,8 @@
module Liquid module Liquid
class VariableLookup class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m.freeze SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = %w[size first last].freeze COMMAND_METHODS = ['size', 'first', 'last'].freeze
attr_reader :name, :lookups attr_reader :name, :lookups
@@ -12,10 +12,12 @@ module Liquid
end end
def initialize(markup) def initialize(markup)
lookups = markup.scan(VARIABLE_PARSER) lookups = markup.scan(VariableParser)
name = lookups.shift name = lookups.shift
name = Expression.parse(Regexp.last_match(1)) if name =~ SQUARE_BRACKETED if name =~ SQUARE_BRACKETED
name = Expression.parse(Regexp.last_match(1))
end
@name = name @name = name
@lookups = lookups @lookups = lookups
@@ -41,8 +43,8 @@ module Liquid
# If object is a hash- or array-like object we look for the # If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it # presence of the key and if its available we return it
if object.respond_to?(:[]) && if object.respond_to?(:[]) &&
((object.respond_to?(:key?) && object.key?(key)) || ((object.respond_to?(:key?) && object.key?(key)) ||
(object.respond_to?(:fetch) && key.is_a?(Integer))) (object.respond_to?(:fetch) && key.is_a?(Integer)))
# if its a proc we will replace the entry with the proc # if its a proc we will replace the entry with the proc
res = context.lookup_and_evaluate(object, key) res = context.lookup_and_evaluate(object, key)
@@ -59,7 +61,6 @@ module Liquid
# raise an exception if `strict_variables` option is set to true # raise an exception if `strict_variables` option is set to true
else else
return nil unless context.strict_variables return nil unless context.strict_variables
raise Liquid::UndefinedVariable, "undefined variable #{key}" raise Liquid::UndefinedVariable, "undefined variable #{key}"
end end

View File

@@ -1,5 +1,6 @@
# encoding: utf-8
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
VERSION = '4.0.3' VERSION = "4.0.3"
end end

View File

@@ -1,31 +1,32 @@
# encoding: utf-8
# frozen_string_literal: true # frozen_string_literal: true
lib = File.expand_path('lib', __dir__) lib = File.expand_path('../lib/', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'liquid/version' require "liquid/version"
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = 'liquid' s.name = "liquid"
s.version = Liquid::VERSION s.version = Liquid::VERSION
s.platform = Gem::Platform::RUBY s.platform = Gem::Platform::RUBY
s.summary = 'A secure, non-evaling end user template engine with aesthetic markup.' s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
s.authors = ['Tobias Lütke'] s.authors = ["Tobias Lütke"]
s.email = ['tobi@leetsoft.com'] s.email = ["tobi@leetsoft.com"]
s.homepage = 'http://www.liquidmarkup.org' s.homepage = "http://www.liquidmarkup.org"
s.license = 'MIT' s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup." # s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = '>= 2.4.0' s.required_ruby_version = ">= 2.4.0"
s.required_rubygems_version = '>= 1.3.7' s.required_rubygems_version = ">= 1.3.7"
s.test_files = Dir.glob('{test}/**/*') s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob('{lib}/**/*') + %w[LICENSE README.md] s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
s.extra_rdoc_files = ['History.md', 'README.md'] s.extra_rdoc_files = ["History.md", "README.md"]
s.require_path = 'lib' s.require_path = "lib"
s.add_development_dependency('minitest')
s.add_development_dependency('rake', '~> 11.3') s.add_development_dependency('rake', '~> 11.3')
s.add_development_dependency('minitest')
end end

View File

@@ -14,7 +14,7 @@ Benchmark.ips do |x|
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)." puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
puts puts
x.report('parse:') { profiler.compile } x.report("parse:") { profiler.compile }
x.report('render:') { profiler.render } x.report("render:") { profiler.render }
x.report('parse & render:') { profiler.run } x.report("parse & render:") { profiler.run }
end end

View File

@@ -2,25 +2,62 @@
require 'benchmark/ips' require 'benchmark/ips'
require 'memory_profiler' require 'memory_profiler'
require 'terminal-table'
require_relative 'theme_runner' require_relative 'theme_runner'
def profile(phase, &block) class Profiler
puts LOG_LABEL = "Profiling: ".rjust(14).freeze
puts "#{phase}:" REPORTS_DIR = File.expand_path('.memprof', __dir__).freeze
puts
report = MemoryProfiler.report(&block) def self.run
puts
yield new
end
report.pretty_print( def initialize
color_output: true, @allocated = []
scale_bytes: true, @retained = []
detailed_report: true @headings = []
) end
def profile(phase, &block)
print(LOG_LABEL)
print("#{phase}.. ".ljust(10))
report = MemoryProfiler.report(&block)
puts 'Done.'
@headings << phase.capitalize
@allocated << "#{report.scale_bytes(report.total_allocated_memsize)} (#{report.total_allocated} objects)"
@retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)"
return if ENV['CI']
require 'fileutils'
report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt")
FileUtils.mkdir_p(REPORTS_DIR)
report.pretty_print(to_file: report_file, scale_bytes: true)
end
def tabulate
table = Terminal::Table.new(headings: @headings.unshift('Phase')) do |t|
t << @allocated.unshift('Total allocated')
t << @retained.unshift('Total retained')
end
puts
puts table
puts "\nDetailed report(s) saved to #{REPORTS_DIR}/" unless ENV['CI']
end
def sanitize(string)
string.downcase.gsub(/[\W]/, '-').squeeze('-')
end
end end
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new runner = ThemeRunner.new
Profiler.run do |x|
profile('Parsing') { profiler.compile } x.profile('parse') { runner.compile }
profile('Rendering') { profiler.render } x.profile('render') { runner.render }
x.tabulate
end

View File

@@ -7,7 +7,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
profiler.run profiler.run
%i[cpu object].each do |profile_type| [:cpu, :object].each do |profile_type|
puts "Profiling in #{profile_type} mode..." puts "Profiling in #{profile_type} mode..."
results = StackProf.run(mode: profile_type) do results = StackProf.run(mode: profile_type) do
200.times do 200.times do
@@ -22,5 +22,5 @@ profiler.run
end end
StackProf::Report.new(results).print_text(false, 20) StackProf::Report.new(results).print_text(false, 20)
File.write(ENV['FILENAME'] + '.' + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME'] File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME']
end end

View File

@@ -1,12 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class CommentForm < Liquid::Block class CommentForm < Liquid::Block
SYNTAX = /(#{Liquid::VARIABLE_SIGNATURE}+)/.freeze Syntax = /(#{Liquid::VariableSignature}+)/
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ SYNTAX if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = Regexp.last_match(1)
@attributes = {} @attributes = {}
else else

View File

@@ -21,9 +21,7 @@ module Database
db = db.each_with_object({}) do |(key, values), assigns| db = db.each_with_object({}) do |(key, values), assigns|
assigns[key] = values.each_with_object({}) do |v, h| assigns[key] = values.each_with_object({}) do |v, h|
h[v['handle']] = v h[v['handle']] = v
h
end end
assigns
end end
# Some standard direct accessors so that the specialized templates # Some standard direct accessors so that the specialized templates

View File

@@ -4,6 +4,6 @@ require 'json'
module JsonFilter module JsonFilter
def json(object) def json(object)
JSON.dump(object.reject { |k, _v| k == 'collections' }) JSON.dump(object.reject { |k, _v| k == "collections" })
end end
end end

View File

@@ -3,14 +3,12 @@
module MoneyFilter module MoneyFilter
def money_with_currency(money) def money_with_currency(money)
return '' if money.nil? return '' if money.nil?
format("$ %.2f USD", money / 100.0)
format('$ %.2f USD', money / 100.0)
end end
def money(money) def money(money)
return '' if money.nil? return '' if money.nil?
format("$ %.2f", money / 100.0)
format('$ %.2f', money / 100.0)
end end
private private

View File

@@ -1,12 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class Paginate < Liquid::Block class Paginate < Liquid::Block
SYNTAX = /(#{Liquid::QUOTED_FRAGMENT})\s*(by\s*(\d+))?/.freeze Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ SYNTAX if markup =~ Syntax
@collection_name = Regexp.last_match(1) @collection_name = Regexp.last_match(1)
@page_size = if Regexp.last_match(2) @page_size = if Regexp.last_match(2)
Regexp.last_match(3).to_i Regexp.last_match(3).to_i
@@ -15,7 +15,7 @@ class Paginate < Liquid::Block
end end
@attributes = { 'window_size' => 3 } @attributes = { 'window_size' => 3 }
markup.scan(Liquid::TAG_ATTRIBUTES) do |key, value| markup.scan(Liquid::TagAttributes) do |key, value|
@attributes[key] = value @attributes[key] = value
end end
else else
@@ -45,7 +45,7 @@ class Paginate < Liquid::Block
pagination['items'] = collection_size pagination['items'] = collection_size
pagination['pages'] = page_count - 1 pagination['pages'] = page_count - 1
pagination['previous'] = link('&laquo; Previous', current_page - 1) unless current_page <= 1 pagination['previous'] = link('&laquo; Previous', current_page - 1) unless 1 >= current_page
pagination['next'] = link('Next &raquo;', current_page + 1) unless page_count <= current_page + 1 pagination['next'] = link('Next &raquo;', current_page + 1) unless page_count <= current_page + 1
pagination['parts'] = [] pagination['parts'] = []
@@ -61,7 +61,6 @@ class Paginate < Liquid::Block
pagination['parts'] << link(page, page) pagination['parts'] << link(page, page)
elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size'] elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size']
next if hellip_break next if hellip_break
pagination['parts'] << no_link('&hellip;') pagination['parts'] << no_link('&hellip;')
hellip_break = true hellip_break = true
next next
@@ -88,6 +87,6 @@ class Paginate < Liquid::Block
end end
def current_url def current_url
'/collections/frontpage' "/collections/frontpage"
end end
end end

View File

@@ -17,21 +17,21 @@ module ShopFilter
%(<script src="#{url}" type="text/javascript"></script>) %(<script src="#{url}" type="text/javascript"></script>)
end end
def stylesheet_tag(url, media = 'all') def stylesheet_tag(url, media = "all")
%(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />) %(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />)
end end
def link_to(link, url, title = '') def link_to(link, url, title = "")
%(<a href="#{url}" title="#{title}">#{link}</a>) %(<a href="#{url}" title="#{title}">#{link}</a>)
end end
def img_tag(url, alt = '') def img_tag(url, alt = "")
%(<img src="#{url}" alt="#{alt}" />) %(<img src="#{url}" alt="#{alt}" />)
end end
def link_to_vendor(vendor) def link_to_vendor(vendor)
if vendor if vendor
link_to vendor, url_for_vendor(vendor), vendor link_to(vendor, url_for_vendor(vendor), vendor)
else else
'Unknown Vendor' 'Unknown Vendor'
end end
@@ -39,7 +39,7 @@ module ShopFilter
def link_to_type(type) def link_to_type(type)
if type if type
link_to type, url_for_type(type), type link_to(type, url_for_type(type), type)
else else
'Unknown Vendor' 'Unknown Vendor'
end end
@@ -54,7 +54,9 @@ module ShopFilter
end end
def product_img_url(url, style = 'small') def product_img_url(url, style = 'small')
raise ArgumentError, 'filter "size" can only be called on product images' unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})} unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})}
raise ArgumentError, 'filter "size" can only be called on product images'
end
case style case style
when 'original' when 'original'

View File

@@ -2,7 +2,7 @@
module WeightFilter module WeightFilter
def weight(grams) def weight(grams)
format('%.2f', grams / 1000) format("%.2f", grams / 1000)
end end
def weight_with_unit(grams) def weight_with_unit(grams)

View File

@@ -12,29 +12,29 @@ class AssignTest < Minitest::Test
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal 'Print this-thing', rendered.strip assert_equal("Print this-thing", rendered.strip)
end end
def test_assigned_variable def test_assigned_variable
assert_template_result('.foo.', assert_template_result('.foo.',
'{% assign foo = values %}.{{ foo[0] }}.', '{% assign foo = values %}.{{ foo[0] }}.',
'values' => %w[foo bar baz]) 'values' => %w(foo bar baz))
assert_template_result('.bar.', assert_template_result('.bar.',
'{% assign foo = values %}.{{ foo[1] }}.', '{% assign foo = values %}.{{ foo[1] }}.',
'values' => %w[foo bar baz]) 'values' => %w(foo bar baz))
end end
def test_assign_with_filter def test_assign_with_filter
assert_template_result('.bar.', assert_template_result('.bar.',
'{% assign foo = values | split: "," %}.{{ foo[1] }}.', '{% assign foo = values | split: "," %}.{{ foo[1] }}.',
'values' => 'foo,bar,baz') 'values' => "foo,bar,baz")
end end
def test_assign_syntax_error def test_assign_syntax_error
assert_match_syntax_error(/assign/, assert_match_syntax_error(/assign/,
'{% assign foo not values %}.', '{% assign foo not values %}.',
'values' => 'foo,bar,baz') 'values' => "foo,bar,baz")
end end
def test_assign_uses_error_mode def test_assign_uses_error_mode

View File

@@ -33,77 +33,77 @@ class BlankTest < Minitest::Test
def test_new_tags_are_not_blank_by_default def test_new_tags_are_not_blank_by_default
with_custom_tag('foobar', FoobarTag) do with_custom_tag('foobar', FoobarTag) do
assert_template_result(' ' * N, wrap_in_for('{% foobar %}')) assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
end end
end end
def test_loops_are_blank def test_loops_are_blank
assert_template_result('', wrap_in_for(' ')) assert_template_result("", wrap_in_for(" "))
end end
def test_if_else_are_blank def test_if_else_are_blank
assert_template_result('', '{% if true %} {% elsif false %} {% else %} {% endif %}') assert_template_result("", "{% if true %} {% elsif false %} {% else %} {% endif %}")
end end
def test_unless_is_blank def test_unless_is_blank
assert_template_result('', wrap('{% unless true %} {% endunless %}')) assert_template_result("", wrap("{% unless true %} {% endunless %}"))
end end
def test_mark_as_blank_only_during_parsing def test_mark_as_blank_only_during_parsing
assert_template_result(' ' * (N + 1), wrap(' {% if false %} this never happens, but still, this block is not blank {% endif %}')) assert_template_result(" " * (N + 1), wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}"))
end end
def test_comments_are_blank def test_comments_are_blank
assert_template_result('', wrap(' {% comment %} whatever {% endcomment %} ')) assert_template_result("", wrap(" {% comment %} whatever {% endcomment %} "))
end end
def test_captures_are_blank def test_captures_are_blank
assert_template_result('', wrap(' {% capture foo %} whatever {% endcapture %} ')) assert_template_result("", wrap(" {% capture foo %} whatever {% endcapture %} "))
end end
def test_nested_blocks_are_blank_but_only_if_all_children_are def test_nested_blocks_are_blank_but_only_if_all_children_are
assert_template_result('', wrap(wrap(' '))) assert_template_result("", wrap(wrap(" ")))
assert_template_result("\n but this is not " * (N + 1), assert_template_result("\n but this is not " * (N + 1),
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %} wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
{% if true %} but this is not {% endif %}')) {% if true %} but this is not {% endif %}'))
end end
def test_assigns_are_blank def test_assigns_are_blank
assert_template_result('', wrap(' {% assign foo = "bar" %} ')) assert_template_result("", wrap(' {% assign foo = "bar" %} '))
end end
def test_whitespace_is_blank def test_whitespace_is_blank
assert_template_result('', wrap(' ')) assert_template_result("", wrap(" "))
assert_template_result('', wrap("\t")) assert_template_result("", wrap("\t"))
end end
def test_whitespace_is_not_blank_if_other_stuff_is_present def test_whitespace_is_not_blank_if_other_stuff_is_present
body = ' x ' body = " x "
assert_template_result(body * (N + 1), wrap(body)) assert_template_result(body * (N + 1), wrap(body))
end end
def test_increment_is_not_blank def test_increment_is_not_blank
assert_template_result(' 0' * 2 * (N + 1), wrap('{% assign foo = 0 %} {% increment foo %} {% decrement foo %}')) assert_template_result(" 0" * 2 * (N + 1), wrap("{% assign foo = 0 %} {% increment foo %} {% decrement foo %}"))
end end
def test_cycle_is_not_blank def test_cycle_is_not_blank
assert_template_result(' ' * ((N + 1) / 2) + ' ', wrap("{% cycle ' ', ' ' %}")) assert_template_result(" " * ((N + 1) / 2) + " ", wrap("{% cycle ' ', ' ' %}"))
end end
def test_raw_is_not_blank def test_raw_is_not_blank
assert_template_result(' ' * (N + 1), wrap(' {% raw %} {% endraw %}')) assert_template_result(" " * (N + 1), wrap(" {% raw %} {% endraw %}"))
end end
def test_include_is_blank def test_include_is_blank
Liquid::Template.file_system = BlankTestFileSystem.new Liquid::Template.file_system = BlankTestFileSystem.new
assert_template_result 'foobar' * (N + 1), wrap("{% include 'foobar' %}") assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}"))
assert_template_result ' foobar ' * (N + 1), wrap("{% include ' foobar ' %}") assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}"))
assert_template_result ' ' * (N + 1), wrap(" {% include ' ' %} ") assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} "))
end end
def test_case_is_blank def test_case_is_blank
assert_template_result('', wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} ")) assert_template_result("", wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
assert_template_result('', wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} ")) assert_template_result("", wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
assert_template_result(' x ' * (N + 1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} ")) assert_template_result(" x " * (N + 1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} "))
end end
end end

View File

@@ -7,8 +7,8 @@ class BlockTest < Minitest::Test
def test_unexpected_end_tag def test_unexpected_end_tag
exc = assert_raises(SyntaxError) do exc = assert_raises(SyntaxError) do
Template.parse('{% if true %}{% endunless %}') Template.parse("{% if true %}{% endunless %}")
end end
assert_equal exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif" assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif")
end end
end end

View File

@@ -6,7 +6,7 @@ class CaptureTest < Minitest::Test
include Liquid include Liquid
def test_captures_block_content_in_variable def test_captures_block_content_in_variable
assert_template_result('test string', "{% capture 'var' %}test string{% endcapture %}{{var}}", {}) assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {})
end end
def test_capture_with_hyphen_in_variable_name def test_capture_with_hyphen_in_variable_name
@@ -16,7 +16,7 @@ class CaptureTest < Minitest::Test
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal 'Print this-thing', rendered.strip assert_equal("Print this-thing", rendered.strip)
end end
def test_capture_to_variable_from_outer_scope_if_existing def test_capture_to_variable_from_outer_scope_if_existing
@@ -32,7 +32,7 @@ class CaptureTest < Minitest::Test
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal 'test-string', rendered.gsub(/\s/, '') assert_equal("test-string", rendered.gsub(/\s/, ''))
end end
def test_assigning_from_capture def test_assigning_from_capture
@@ -47,6 +47,6 @@ class CaptureTest < Minitest::Test
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal '3-3', rendered.gsub(/\s/, '') assert_equal("3-3", rendered.gsub(/\s/, ''))
end end
end # CaptureTest end # CaptureTest

View File

@@ -7,15 +7,15 @@ class DocumentTest < Minitest::Test
def test_unexpected_outer_tag def test_unexpected_outer_tag
exc = assert_raises(SyntaxError) do exc = assert_raises(SyntaxError) do
Template.parse('{% else %}') Template.parse("{% else %}")
end end
assert_equal exc.message, "Liquid syntax error: Unexpected outer 'else' tag" assert_equal(exc.message, "Liquid syntax error: Unexpected outer 'else' tag")
end end
def test_unknown_tag def test_unknown_tag
exc = assert_raises(SyntaxError) do exc = assert_raises(SyntaxError) do
Template.parse('{% foo %}') Template.parse("{% foo %}")
end end
assert_equal exc.message, "Liquid syntax error: Unknown tag 'foo'" assert_equal(exc.message, "Liquid syntax error: Unknown tag 'foo'")
end end
end end

View File

@@ -23,7 +23,7 @@ end
class ProductDrop < Liquid::Drop class ProductDrop < Liquid::Drop
class TextDrop < Liquid::Drop class TextDrop < Liquid::Drop
def array def array
%w[text1 text2] ['text1', 'text2']
end end
def text def text
@@ -33,7 +33,7 @@ class ProductDrop < Liquid::Drop
class CatchallDrop < Liquid::Drop class CatchallDrop < Liquid::Drop
def liquid_method_missing(method) def liquid_method_missing(method)
+'catchall_method: ' << method.to_s "catchall_method: #{method}"
end end
end end
@@ -50,13 +50,13 @@ class ProductDrop < Liquid::Drop
end end
def user_input def user_input
(+'foo').taint (+"foo").taint
end end
protected protected
def callmenot def callmenot
'protected' "protected"
end end
end end
@@ -111,7 +111,7 @@ class DropsTest < Minitest::Test
def test_product_drop def test_product_drop
tpl = Liquid::Template.parse(' ') tpl = Liquid::Template.parse(' ')
assert_equal ' ', tpl.render!('product' => ProductDrop.new) assert_equal(' ', tpl.render!('product' => ProductDrop.new))
end end
def test_rendering_raises_on_tainted_attr def test_rendering_raises_on_tainted_attr
@@ -141,100 +141,105 @@ class DropsTest < Minitest::Test
end end
def test_drop_does_only_respond_to_whitelisted_methods def test_drop_does_only_respond_to_whitelisted_methods
assert_equal '', Liquid::Template.parse('{{ product.inspect }}').render!('product' => ProductDrop.new) assert_equal("", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new))
assert_equal '', Liquid::Template.parse('{{ product.pretty_inspect }}').render!('product' => ProductDrop.new) assert_equal("", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new))
assert_equal '', Liquid::Template.parse('{{ product.whatever }}').render!('product' => ProductDrop.new) assert_equal("", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new))
assert_equal '', Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new) assert_equal("", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new))
assert_equal '', Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new) assert_equal("", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new))
assert_equal '', Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new) assert_equal("", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new))
end end
def test_drops_respond_to_to_liquid def test_drops_respond_to_to_liquid
assert_equal 'text1', Liquid::Template.parse('{{ product.to_liquid.texts.text }}').render!('product' => ProductDrop.new) assert_equal("text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new))
assert_equal 'text1', Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new) assert_equal("text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new))
end end
def test_text_drop def test_text_drop
output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new)
assert_equal ' text1 ', output assert_equal(' text1 ', output)
end end
def test_catchall_unknown_method def test_catchall_unknown_method
output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
assert_equal ' catchall_method: unknown ', output assert_equal(' catchall_method: unknown ', output)
end end
def test_catchall_integer_argument_drop def test_catchall_integer_argument_drop
output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
assert_equal ' catchall_method: 8 ', output assert_equal(' catchall_method: 8 ', output)
end end
def test_text_array_drop def test_text_array_drop
output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new) output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new)
assert_equal ' text1 text2 ', output assert_equal(' text1 text2 ', output)
end end
def test_context_drop def test_context_drop
output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => 'carrot') output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot")
assert_equal ' carrot ', output assert_equal(' carrot ', output)
end
def test_context_drop_array_with_map
output = Liquid::Template.parse(' {{ contexts | map: "bar" }} ').render!('contexts' => [ContextDrop.new, ContextDrop.new], 'bar' => "carrot")
assert_equal(' carrotcarrot ', output)
end end
def test_nested_context_drop def test_nested_context_drop
output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => 'monkey') output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey")
assert_equal ' monkey ', output assert_equal(' monkey ', output)
end end
def test_protected def test_protected
output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new)
assert_equal ' ', output assert_equal(' ', output)
end end
def test_object_methods_not_allowed def test_object_methods_not_allowed
%i[dup clone singleton_class eval class_eval inspect].each do |method| [:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new) output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
assert_equal ' ', output assert_equal ' ', output
end end
end end
def test_scope def test_scope
assert_equal '1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new) assert_equal('1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new))
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
end end
def test_scope_though_proc def test_scope_though_proc
assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }) assert_equal('1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }))
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]) assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]))
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]) assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]))
end end
def test_scope_with_assigns def test_scope_with_assigns
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new) assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new))
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
assert_equal 'test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new) assert_equal('test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new))
assert_equal 'test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new) assert_equal('test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new))
end end
def test_scope_from_tags def test_scope_from_tags
assert_equal '1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal('1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
assert_equal '12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal('12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
end end
def test_access_context_from_drop def test_access_context_from_drop
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3]) assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3]))
end end
def test_enumerable_drop def test_enumerable_drop
assert_equal '123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new) assert_equal('123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new))
end end
def test_enumerable_drop_size def test_enumerable_drop_size
assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new) assert_equal('3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new))
end end
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
%w[select each map cycle].each do |method| ["select", "each", "map", "cycle"].each do |method|
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new) assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new) assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new) assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
@@ -243,33 +248,40 @@ class DropsTest < Minitest::Test
end end
def test_some_enumerable_methods_still_get_invoked def test_some_enumerable_methods_still_get_invoked
%i[count max].each do |method| [:count, :max].each do |method|
assert_equal '3', Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new) assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal '3', Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new) assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal '3', Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new) assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal '3', Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new) assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
end end
assert_equal 'yes', Liquid::Template.parse('{% if collection contains 3 %}yes{% endif %}').render!('collection' => RealEnumerableDrop.new) assert_equal("yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new))
%i[min first].each do |method| [:min, :first].each do |method|
assert_equal '1', Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new) assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal '1', Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new) assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal '1', Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new) assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal '1', Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new) assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
end end
end end
def test_empty_string_value_access def test_empty_string_value_access
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => '') assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => ''))
end end
def test_nil_value_access def test_nil_value_access
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil) assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil))
end end
def test_default_to_s_on_drops def test_default_to_s_on_drops
assert_equal 'ProductDrop', Liquid::Template.parse('{{ product }}').render!('product' => ProductDrop.new) assert_equal('ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new))
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new) assert_equal('EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new))
end
def test_invokable_methods
assert_equal(%w(to_liquid catchall user_input context texts).to_set, ProductDrop.invokable_methods)
assert_equal(%w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods)
assert_equal(%w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods)
assert_equal(%w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods)
end end
end # DropsTest end # DropsTest

View File

@@ -35,31 +35,31 @@ class ErrorHandlingTest < Minitest::Test
TEXT TEXT
output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new) output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new)
assert_equal expected, output assert_equal(expected, output)
end end
def test_standard_error def test_standard_error
template = Liquid::Template.parse(' {{ errors.standard_error }} ') template = Liquid::Template.parse(' {{ errors.standard_error }} ')
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new) assert_equal(' Liquid error: standard error ', template.render('errors' => ErrorDrop.new))
assert_equal 1, template.errors.size assert_equal(1, template.errors.size)
assert_equal StandardError, template.errors.first.class assert_equal(StandardError, template.errors.first.class)
end end
def test_syntax def test_syntax
template = Liquid::Template.parse(' {{ errors.syntax_error }} ') template = Liquid::Template.parse(' {{ errors.syntax_error }} ')
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new) assert_equal(' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new))
assert_equal 1, template.errors.size assert_equal(1, template.errors.size)
assert_equal SyntaxError, template.errors.first.class assert_equal(SyntaxError, template.errors.first.class)
end end
def test_argument def test_argument
template = Liquid::Template.parse(' {{ errors.argument_error }} ') template = Liquid::Template.parse(' {{ errors.argument_error }} ')
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new) assert_equal(' Liquid error: argument error ', template.render('errors' => ErrorDrop.new))
assert_equal 1, template.errors.size assert_equal(1, template.errors.size)
assert_equal ArgumentError, template.errors.first.class assert_equal(ArgumentError, template.errors.first.class)
end end
def test_missing_endtag_parse_time_error def test_missing_endtag_parse_time_error
@@ -78,9 +78,9 @@ class ErrorHandlingTest < Minitest::Test
def test_lax_unrecognized_operator def test_lax_unrecognized_operator
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax) template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
assert_equal ' Liquid error: Unknown operator =! ', template.render assert_equal(' Liquid error: Unknown operator =! ', template.render)
assert_equal 1, template.errors.size assert_equal(1, template.errors.size)
assert_equal Liquid::ArgumentError, template.errors.first.class assert_equal(Liquid::ArgumentError, template.errors.first.class)
end end
def test_with_line_numbers_adds_numbers_to_parser_errors def test_with_line_numbers_adds_numbers_to_parser_errors
@@ -92,7 +92,7 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
line_numbers: true) line_numbers: true)
end end
assert_match(/Liquid syntax error \(line 4\)/, err.message) assert_match(/Liquid syntax error \(line 4\)/, err.message)
@@ -107,7 +107,7 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
line_numbers: true) line_numbers: true)
end end
assert_match(/Liquid syntax error \(line 4\)/, err.message) assert_match(/Liquid syntax error \(line 4\)/, err.message)
@@ -121,11 +121,11 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :warn, error_mode: :warn,
line_numbers: true) line_numbers: true)
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'], assert_equal(['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
template.warnings.map(&:message) template.warnings.map(&:message))
end end
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
@@ -137,11 +137,11 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :strict, error_mode: :strict,
line_numbers: true) line_numbers: true)
end end
assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message)
end end
def test_syntax_errors_in_nested_blocks_have_correct_line_number def test_syntax_errors_in_nested_blocks_have_correct_line_number
@@ -155,40 +155,40 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
line_numbers: true) line_numbers: true)
end end
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message assert_equal("Liquid syntax error (line 5): Unknown tag 'foo'", err.message)
end end
def test_strict_error_messages def test_strict_error_messages
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict) Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict)
end end
assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message assert_equal('Liquid syntax error: Unexpected character = in "1 =! 2"', err.message)
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse('{{%%%}}', error_mode: :strict) Liquid::Template.parse('{{%%%}}', error_mode: :strict)
end end
assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message assert_equal('Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message)
end end
def test_warnings def test_warnings
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn) template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn)
assert_equal 3, template.warnings.size assert_equal(3, template.warnings.size)
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false) assert_equal('Unexpected character ~ in "~~~"', template.warnings[0].to_s(false))
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false) assert_equal('Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false))
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false) assert_equal('Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false))
assert_equal '', template.render assert_equal('', template.render)
end end
def test_warning_line_numbers def test_warning_line_numbers
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true) template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true)
assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message assert_equal('Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message)
assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message assert_equal('Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message)
assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message assert_equal('Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message)
assert_equal 3, template.warnings.size assert_equal(3, template.warnings.size)
assert_equal [1, 2, 3], template.warnings.map(&:line_number) assert_equal([1, 2, 3], template.warnings.map(&:line_number))
end end
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError # Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
@@ -204,14 +204,14 @@ class ErrorHandlingTest < Minitest::Test
output = template.render('errors' => ErrorDrop.new) output = template.render('errors' => ErrorDrop.new)
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output assert_equal('This is a runtime error: Liquid error (line 1): internal', output)
assert_equal [Liquid::InternalError], template.errors.map(&:class) assert_equal([Liquid::InternalError], template.errors.map(&:class))
end end
def test_setting_default_exception_renderer def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = [] exceptions = []
Liquid::Template.default_exception_renderer = lambda { |e| Liquid::Template.default_exception_renderer = ->(e) {
exceptions << e exceptions << e
'' ''
} }
@@ -219,8 +219,8 @@ class ErrorHandlingTest < Minitest::Test
output = template.render('errors' => ErrorDrop.new) output = template.render('errors' => ErrorDrop.new)
assert_equal 'This is a runtime error: ', output assert_equal('This is a runtime error: ', output)
assert_equal [Liquid::ArgumentError], template.errors.map(&:class) assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
ensure ensure
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
end end
@@ -228,22 +228,22 @@ class ErrorHandlingTest < Minitest::Test
def test_exception_renderer_exposing_non_liquid_error def test_exception_renderer_exposing_non_liquid_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
exceptions = [] exceptions = []
handler = lambda { |e| handler = ->(e) {
exceptions << e exceptions << e
e.cause e.cause
} }
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler) output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
assert_equal 'This is a runtime error: runtime error', output assert_equal('This is a runtime error: runtime error', output)
assert_equal [Liquid::InternalError], exceptions.map(&:class) assert_equal([Liquid::InternalError], exceptions.map(&:class))
assert_equal exceptions, template.errors assert_equal(exceptions, template.errors)
assert_equal '#<RuntimeError: runtime error>', exceptions.first.cause.inspect assert_equal('#<RuntimeError: runtime error>', exceptions.first.cause.inspect)
end end
class TestFileSystem class TestFileSystem
def read_template_file(_template_path) def read_template_file(_template_path)
'{{ errors.argument_error }}' "{{ errors.argument_error }}"
end end
end end
@@ -257,7 +257,7 @@ class ErrorHandlingTest < Minitest::Test
ensure ensure
Liquid::Template.file_system = old_file_system Liquid::Template.file_system = old_file_system
end end
assert_equal "Argument error:\nLiquid error (product line 1): argument error", page assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
assert_equal 'product', template.errors.first.template_name assert_equal("product", template.errors.first.template_name)
end end
end end

View File

@@ -29,7 +29,7 @@ class FiltersTest < Minitest::Test
module OverrideObjectMethodFilter module OverrideObjectMethodFilter
def tap(_input) def tap(_input)
'tap overridden' "tap overridden"
end end
end end
@@ -41,13 +41,13 @@ class FiltersTest < Minitest::Test
@context['var'] = 1000 @context['var'] = 1000
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal ' 1000$ ', Template.parse('{{var | money}}').render(@context) assert_equal(' 1000$ ', Template.parse("{{var | money}}").render(@context))
end end
def test_underscore_in_filter_name def test_underscore_in_filter_name
@context['var'] = 1000 @context['var'] = 1000
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal ' 1000$ ', Template.parse('{{var | money_with_underscore}}').render(@context) assert_equal(' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context))
end end
def test_second_filter_overwrites_first def test_second_filter_overwrites_first
@@ -55,49 +55,49 @@ class FiltersTest < Minitest::Test
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
@context.add_filters(CanadianMoneyFilter) @context.add_filters(CanadianMoneyFilter)
assert_equal ' 1000$ CAD ', Template.parse('{{var | money}}').render(@context) assert_equal(' 1000$ CAD ', Template.parse("{{var | money}}").render(@context))
end end
def test_size def test_size
@context['var'] = 'abcd' @context['var'] = 'abcd'
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal '4', Template.parse('{{var | size}}').render(@context) assert_equal('4', Template.parse("{{var | size}}").render(@context))
end end
def test_join def test_join
@context['var'] = [1, 2, 3, 4] @context['var'] = [1, 2, 3, 4]
assert_equal '1 2 3 4', Template.parse('{{var | join}}').render(@context) assert_equal("1 2 3 4", Template.parse("{{var | join}}").render(@context))
end end
def test_sort def test_sort
@context['value'] = 3 @context['value'] = 3
@context['numbers'] = [2, 1, 4, 3] @context['numbers'] = [2, 1, 4, 3]
@context['words'] = %w[expected as alphabetic] @context['words'] = ['expected', 'as', 'alphabetic']
@context['arrays'] = %w[flower are] @context['arrays'] = ['flower', 'are']
@context['case_sensitive'] = %w[sensitive Expected case] @context['case_sensitive'] = ['sensitive', 'Expected', 'case']
assert_equal '1 2 3 4', Template.parse('{{numbers | sort | join}}').render(@context) assert_equal('1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context))
assert_equal 'alphabetic as expected', Template.parse('{{words | sort | join}}').render(@context) assert_equal('alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context))
assert_equal '3', Template.parse('{{value | sort}}').render(@context) assert_equal('3', Template.parse("{{value | sort}}").render(@context))
assert_equal 'are flower', Template.parse('{{arrays | sort | join}}').render(@context) assert_equal('are flower', Template.parse("{{arrays | sort | join}}").render(@context))
assert_equal 'Expected case sensitive', Template.parse('{{case_sensitive | sort | join}}').render(@context) assert_equal('Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context))
end end
def test_sort_natural def test_sort_natural
@context['words'] = %w[case Assert Insensitive] @context['words'] = ['case', 'Assert', 'Insensitive']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }] @context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')] @context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
# Test strings # Test strings
assert_equal 'Assert case Insensitive', Template.parse('{{words | sort_natural | join}}').render(@context) assert_equal('Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context))
# Test hashes # Test hashes
assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context) assert_equal('A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context))
# Test objects # Test objects
assert_equal 'A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context) assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context))
end end
def test_compact def test_compact
@@ -106,37 +106,37 @@ class FiltersTest < Minitest::Test
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')] @context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
# Test strings # Test strings
assert_equal 'a b c', Template.parse('{{words | compact | join}}').render(@context) assert_equal('a b c', Template.parse("{{words | compact | join}}").render(@context))
# Test hashes # Test hashes
assert_equal 'A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context) assert_equal('A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context))
# Test objects # Test objects
assert_equal 'A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context) assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context))
end end
def test_strip_html def test_strip_html
@context['var'] = '<b>bla blub</a>' @context['var'] = "<b>bla blub</a>"
assert_equal 'bla blub', Template.parse('{{ var | strip_html }}').render(@context) assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
end end
def test_strip_html_ignore_comments_with_html def test_strip_html_ignore_comments_with_html
@context['var'] = '<!-- split and some <ul> tag --><b>bla blub</a>' @context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
assert_equal 'bla blub', Template.parse('{{ var | strip_html }}').render(@context) assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
end end
def test_capitalize def test_capitalize
@context['var'] = 'blub' @context['var'] = "blub"
assert_equal 'Blub', Template.parse('{{ var | capitalize }}').render(@context) assert_equal("Blub", Template.parse("{{ var | capitalize }}").render(@context))
end end
def test_nonexistent_filter_is_ignored def test_nonexistent_filter_is_ignored
@context['var'] = 1000 @context['var'] = 1000
assert_equal '1000', Template.parse('{{ var | xyzzy }}').render(@context) assert_equal('1000', Template.parse("{{ var | xyzzy }}").render(@context))
end end
def test_filter_with_keyword_arguments def test_filter_with_keyword_arguments
@@ -144,14 +144,14 @@ class FiltersTest < Minitest::Test
@context['input'] = 'hello %{first_name}, %{last_name}' @context['input'] = 'hello %{first_name}, %{last_name}'
@context.add_filters(SubstituteFilter) @context.add_filters(SubstituteFilter)
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context) output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
assert_equal 'hello john, doe', output assert_equal('hello john, doe', output)
end end
def test_override_object_method_in_filter def test_override_object_method_in_filter
assert_equal 'tap overridden', Template.parse('{{var | tap}}').render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter]) assert_equal("tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter]))
# tap still treated as a non-existent filter # tap still treated as a non-existent filter
assert_equal '1000', Template.parse('{{var | tap}}').render!('var' => 1000) assert_equal("1000", Template.parse("{{var | tap}}").render!('var' => 1000))
end end
end end
@@ -160,15 +160,15 @@ class FiltersInTemplate < Minitest::Test
def test_local_global def test_local_global
with_global_filter(MoneyFilter) do with_global_filter(MoneyFilter) do
assert_equal ' 1000$ ', Template.parse('{{1000 | money}}').render!(nil, nil) assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)
assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render!(nil, filters: CanadianMoneyFilter) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter)
assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render!(nil, filters: [CanadianMoneyFilter]) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter])
end end
end end
def test_local_filter_with_deprecated_syntax def test_local_filter_with_deprecated_syntax
assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render!(nil, CanadianMoneyFilter) assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter))
assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render!(nil, [CanadianMoneyFilter]) assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter]))
end end
end # FiltersTest end # FiltersTest

View File

@@ -19,7 +19,7 @@ class HashOrderingTest < Minitest::Test
def test_global_register_order def test_global_register_order
with_global_filter(MoneyFilter, CanadianMoneyFilter) do with_global_filter(MoneyFilter, CanadianMoneyFilter) do
assert_equal ' 1000$ CAD ', Template.parse('{{1000 | money}}').render(nil, nil) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil)
end end
end end
end end

View File

@@ -1,13 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
require 'liquid/legacy'
class FiltersTest < Minitest::Test
include Liquid
def test_constants
assert_equal Liquid::FilterSeparator, Liquid::FILTER_SEPARATOR
assert_equal Liquid::BlockBody::ContentOfVariable, Liquid::BlockBody::CONTENT_OF_VARIABLE
end
end

View File

@@ -11,11 +11,11 @@ module FunnyFilter
"LOL: #{input}" "LOL: #{input}"
end end
def add_smiley(input, smiley = ':-)') def add_smiley(input, smiley = ":-)")
"#{input} #{smiley}" "#{input} #{smiley}"
end end
def add_tag(input, tag = 'p', id = 'foo') def add_tag(input, tag = "p", id = "foo")
%(<#{tag} id="#{id}">#{input}</#{tag}>) %(<#{tag} id="#{id}">#{input}</#{tag}>)
end end
@@ -42,84 +42,84 @@ class OutputTest < Minitest::Test
text = %( {{best_cars}} ) text = %( {{best_cars}} )
expected = %( bmw ) expected = %( bmw )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_variable_traversing_with_two_brackets def test_variable_traversing_with_two_brackets
text = %({{ site.data.menu[include.menu][include.locale] }}) text = %({{ site.data.menu[include.menu][include.locale] }})
assert_equal 'it works!', Template.parse(text).render!( assert_equal("it works!", Template.parse(text).render!(
'site' => { 'data' => { 'menu' => { 'foo' => { 'bar' => 'it works!' } } } }, "site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
'include' => { 'menu' => 'foo', 'locale' => 'bar' } "include" => { "menu" => "foo", "locale" => "bar" }
) ))
end end
def test_variable_traversing def test_variable_traversing
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} ) text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )
expected = %( good bad good ) expected = %( good bad good )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_variable_piping def test_variable_piping
text = %( {{ car.gm | make_funny }} ) text = %( {{ car.gm | make_funny }} )
expected = %( LOL ) expected = %( LOL )
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_input def test_variable_piping_with_input
text = %( {{ car.gm | cite_funny }} ) text = %( {{ car.gm | cite_funny }} )
expected = %( LOL: bad ) expected = %( LOL: bad )
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_args def test_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' }} ! text = %! {{ car.gm | add_smiley : ':-(' }} !
expected = %| bad :-( | expected = %| bad :-( |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_no_args def test_variable_piping_with_no_args
text = %( {{ car.gm | add_smiley }} ) text = %( {{ car.gm | add_smiley }} )
expected = %| bad :-) | expected = %| bad :-) |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_multiple_variable_piping_with_args def test_multiple_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} ! text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
expected = %| bad :-( :-( | expected = %| bad :-( :-( |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_multiple_args def test_variable_piping_with_multiple_args
text = %( {{ car.gm | add_tag : 'span', 'bar'}} ) text = %( {{ car.gm | add_tag : 'span', 'bar'}} )
expected = %( <span id="bar">bad</span> ) expected = %( <span id="bar">bad</span> )
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_variable_args def test_variable_piping_with_variable_args
text = %( {{ car.gm | add_tag : 'span', car.bmw}} ) text = %( {{ car.gm | add_tag : 'span', car.bmw}} )
expected = %( <span id="good">bad</span> ) expected = %( <span id="good">bad</span> )
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_multiple_pipings def test_multiple_pipings
text = %( {{ best_cars | cite_funny | paragraph }} ) text = %( {{ best_cars | cite_funny | paragraph }} )
expected = %( <p>LOL: bmw</p> ) expected = %( <p>LOL: bmw</p> )
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_link_to def test_link_to
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} ) text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
expected = %( <a href="http://typo.leetsoft.com">Typo</a> ) expected = %( <a href="http://typo.leetsoft.com">Typo</a> )
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
end # OutputTest end # OutputTest

View File

@@ -7,217 +7,217 @@ class ParseTreeVisitorTest < Minitest::Test
def test_variable def test_variable
assert_equal( assert_equal(
['test'], ["test"],
visit(%({{ test }})) visit(%({{ test }}))
) )
end end
def test_varible_with_filter def test_varible_with_filter
assert_equal( assert_equal(
%w[test infilter], ["test", "infilter"],
visit(%({{ test | split: infilter }})) visit(%({{ test | split: infilter }}))
) )
end end
def test_dynamic_variable def test_dynamic_variable
assert_equal( assert_equal(
%w[test inlookup], ["test", "inlookup"],
visit(%({{ test[inlookup] }})) visit(%({{ test[inlookup] }}))
) )
end end
def test_if_condition def test_if_condition
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% if test %}{% endif %})) visit(%({% if test %}{% endif %}))
) )
end end
def test_complex_if_condition def test_complex_if_condition
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% if 1 == 1 and 2 == test %}{% endif %})) visit(%({% if 1 == 1 and 2 == test %}{% endif %}))
) )
end end
def test_if_body def test_if_body
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% if 1 == 1 %}{{ test }}{% endif %})) visit(%({% if 1 == 1 %}{{ test }}{% endif %}))
) )
end end
def test_unless_condition def test_unless_condition
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% unless test %}{% endunless %})) visit(%({% unless test %}{% endunless %}))
) )
end end
def test_complex_unless_condition def test_complex_unless_condition
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% unless 1 == 1 and 2 == test %}{% endunless %})) visit(%({% unless 1 == 1 and 2 == test %}{% endunless %}))
) )
end end
def test_unless_body def test_unless_body
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% unless 1 == 1 %}{{ test }}{% endunless %})) visit(%({% unless 1 == 1 %}{{ test }}{% endunless %}))
) )
end end
def test_elsif_condition def test_elsif_condition
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% if 1 == 1 %}{% elsif test %}{% endif %})) visit(%({% if 1 == 1 %}{% elsif test %}{% endif %}))
) )
end end
def test_complex_elsif_condition def test_complex_elsif_condition
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %})) visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %}))
) )
end end
def test_elsif_body def test_elsif_body
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %})) visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %}))
) )
end end
def test_else_body def test_else_body
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %})) visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %}))
) )
end end
def test_case_left def test_case_left
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% case test %}{% endcase %})) visit(%({% case test %}{% endcase %}))
) )
end end
def test_case_condition def test_case_condition
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% case 1 %}{% when test %}{% endcase %})) visit(%({% case 1 %}{% when test %}{% endcase %}))
) )
end end
def test_case_when_body def test_case_when_body
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %})) visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %}))
) )
end end
def test_case_else_body def test_case_else_body
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% case 1 %}{% else %}{{ test }}{% endcase %})) visit(%({% case 1 %}{% else %}{{ test }}{% endcase %}))
) )
end end
def test_for_in def test_for_in
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% for x in test %}{% endfor %})) visit(%({% for x in test %}{% endfor %}))
) )
end end
def test_for_limit def test_for_limit
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% for x in (1..5) limit: test %}{% endfor %})) visit(%({% for x in (1..5) limit: test %}{% endfor %}))
) )
end end
def test_for_offset def test_for_offset
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% for x in (1..5) offset: test %}{% endfor %})) visit(%({% for x in (1..5) offset: test %}{% endfor %}))
) )
end end
def test_for_body def test_for_body
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% for x in (1..5) %}{{ test }}{% endfor %})) visit(%({% for x in (1..5) %}{{ test }}{% endfor %}))
) )
end end
def test_tablerow_in def test_tablerow_in
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% tablerow x in test %}{% endtablerow %})) visit(%({% tablerow x in test %}{% endtablerow %}))
) )
end end
def test_tablerow_limit def test_tablerow_limit
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %})) visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %}))
) )
end end
def test_tablerow_offset def test_tablerow_offset
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %})) visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %}))
) )
end end
def test_tablerow_body def test_tablerow_body
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %})) visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %}))
) )
end end
def test_cycle def test_cycle
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% cycle test %})) visit(%({% cycle test %}))
) )
end end
def test_assign def test_assign
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% assign x = test %})) visit(%({% assign x = test %}))
) )
end end
def test_capture def test_capture
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% capture x %}{{ test }}{% endcapture %})) visit(%({% capture x %}{{ test }}{% endcapture %}))
) )
end end
def test_include def test_include
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% include test %})) visit(%({% include test %}))
) )
end end
def test_include_with def test_include_with
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% include "hai" with test %})) visit(%({% include "hai" with test %}))
) )
end end
def test_include_for def test_include_for
assert_equal( assert_equal(
['test'], ["test"],
visit(%({% include "hai" for test %})) visit(%({% include "hai" for test %}))
) )
end end
@@ -225,9 +225,9 @@ class ParseTreeVisitorTest < Minitest::Test
def test_preserve_tree_structure def test_preserve_tree_structure
assert_equal( assert_equal(
[[nil, [ [[nil, [
[nil, [[nil, [['other', []]]]]], [nil, [[nil, [["other", []]]]]],
['test', []], ["test", []],
['xs', []], ["xs", []],
]]], ]]],
traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit
) )

View File

@@ -6,38 +6,38 @@ class ParsingQuirksTest < Minitest::Test
include Liquid include Liquid
def test_parsing_css def test_parsing_css
text = ' div { font-weight: bold; } ' text = " div { font-weight: bold; } "
assert_equal text, Template.parse(text).render! assert_equal(text, Template.parse(text).render!)
end end
def test_raise_on_single_close_bracet def test_raise_on_single_close_bracet
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
Template.parse('text {{method} oh nos!') Template.parse("text {{method} oh nos!")
end end
end end
def test_raise_on_label_and_no_close_bracets def test_raise_on_label_and_no_close_bracets
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
Template.parse('TEST {{ ') Template.parse("TEST {{ ")
end end
end end
def test_raise_on_label_and_no_close_bracets_percent def test_raise_on_label_and_no_close_bracets_percent
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
Template.parse('TEST {% ') Template.parse("TEST {% ")
end end
end end
def test_error_on_empty_filter def test_error_on_empty_filter
assert Template.parse('{{test}}') assert(Template.parse("{{test}}"))
with_error_mode(:lax) do with_error_mode(:lax) do
assert Template.parse('{{|test}}') assert Template.parse("{{|test}}")
end end
with_error_mode(:strict) do with_error_mode(:strict) do
assert_raises(SyntaxError) { Template.parse('{{|test}}') } assert_raises(SyntaxError) { Template.parse("{{|test}}") }
assert_raises(SyntaxError) { Template.parse('{{test |a|b|}}') } assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") }
end end
end end
@@ -53,20 +53,20 @@ class ParsingQuirksTest < Minitest::Test
def test_unexpected_characters_syntax_error def test_unexpected_characters_syntax_error
with_error_mode(:strict) do with_error_mode(:strict) do
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
markup = 'true && false' markup = "true && false"
Template.parse("{% if #{markup} %} YES {% endif %}") Template.parse("{% if #{markup} %} YES {% endif %}")
end end
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
markup = 'false || true' markup = "false || true"
Template.parse("{% if #{markup} %} YES {% endif %}") Template.parse("{% if #{markup} %} YES {% endif %}")
end end
end end
end end
def test_no_error_on_lax_empty_filter def test_no_error_on_lax_empty_filter
assert Template.parse('{{test |a|b|}}', error_mode: :lax) assert(Template.parse("{{test |a|b|}}", error_mode: :lax))
assert Template.parse('{{test}}', error_mode: :lax) assert(Template.parse("{{test}}", error_mode: :lax))
assert Template.parse('{{|test|}}', error_mode: :lax) assert(Template.parse("{{|test|}}", error_mode: :lax))
end end
def test_meaningless_parens_lax def test_meaningless_parens_lax
@@ -79,9 +79,9 @@ class ParsingQuirksTest < Minitest::Test
def test_unexpected_characters_silently_eat_logic_lax def test_unexpected_characters_silently_eat_logic_lax
with_error_mode(:lax) do with_error_mode(:lax) do
markup = 'true && false' markup = "true && false"
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}") assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}")
markup = 'false || true' markup = "false || true"
assert_template_result('', "{% if #{markup} %} YES {% endif %}") assert_template_result('', "{% if #{markup} %} YES {% endif %}")
end end
end end
@@ -114,7 +114,7 @@ class ParsingQuirksTest < Minitest::Test
def test_extra_dots_in_ranges def test_extra_dots_in_ranges
with_error_mode(:lax) do with_error_mode(:lax) do
assert_template_result('12345', '{% for i in (1...5) %}{{ i }}{% endfor %}') assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
end end
end end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'test_helper'
class DisabledTagsTest < Minitest::Test
include Liquid
class DisableRaw < Block
disable_tags "raw"
end
class DisableRawEcho < Block
disable_tags "raw", "echo"
end
def test_disables_raw
with_custom_tag('disable', DisableRaw) do
assert_template_result 'raw usage is not allowed in this contextfoo', '{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}'
end
end
def test_disables_echo_and_raw
with_custom_tag('disable', DisableRawEcho) do
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 %}'
end
end
end

View File

@@ -19,35 +19,35 @@ class RenderProfilingTest < Minitest::Test
t = Template.parse("{{ 'a string' | upcase }}") t = Template.parse("{{ 'a string' | upcase }}")
t.render! t.render!
assert_nil t.profiler assert_nil(t.profiler)
end end
def test_parse_makes_available_simple_profiling def test_parse_makes_available_simple_profiling
t = Template.parse("{{ 'a string' | upcase }}", profile: true) t = Template.parse("{{ 'a string' | upcase }}", profile: true)
t.render! t.render!
assert_equal 1, t.profiler.length assert_equal(1, t.profiler.length)
node = t.profiler[0] node = t.profiler[0]
assert_equal " 'a string' | upcase ", node.code assert_equal(" 'a string' | upcase ", node.code)
end end
def test_render_ignores_raw_strings_when_profiling def test_render_ignores_raw_strings_when_profiling
t = Template.parse("This is raw string\nstuff\nNewline", profile: true) t = Template.parse("This is raw string\nstuff\nNewline", profile: true)
t.render! t.render!
assert_equal 0, t.profiler.length assert_equal(0, t.profiler.length)
end end
def test_profiling_includes_line_numbers_of_liquid_nodes def test_profiling_includes_line_numbers_of_liquid_nodes
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true) t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
t.render! t.render!
assert_equal 2, t.profiler.length assert_equal(2, t.profiler.length)
# {{ 'a string' | upcase }} # {{ 'a string' | upcase }}
assert_equal 1, t.profiler[0].line_number assert_equal(1, t.profiler[0].line_number)
# {{ increment test }} # {{ increment test }}
assert_equal 2, t.profiler[1].line_number assert_equal(2, t.profiler[1].line_number)
end end
def test_profiling_includes_line_numbers_of_included_partials def test_profiling_includes_line_numbers_of_included_partials
@@ -57,9 +57,9 @@ class RenderProfilingTest < Minitest::Test
included_children = t.profiler[0].children included_children = t.profiler[0].children
# {% assign template_name = 'a_template' %} # {% assign template_name = 'a_template' %}
assert_equal 1, included_children[0].line_number assert_equal(1, included_children[0].line_number)
# {{ template_name }} # {{ template_name }}
assert_equal 2, included_children[1].line_number assert_equal(2, included_children[1].line_number)
end end
def test_profiling_times_the_rendering_of_tokens def test_profiling_times_the_rendering_of_tokens
@@ -67,14 +67,14 @@ class RenderProfilingTest < Minitest::Test
t.render! t.render!
node = t.profiler[0] node = t.profiler[0]
refute_nil node.render_time refute_nil(node.render_time)
end end
def test_profiling_times_the_entire_render def test_profiling_times_the_entire_render
t = Template.parse("{% include 'a_template' %}", profile: true) t = Template.parse("{% include 'a_template' %}", profile: true)
t.render! t.render!
assert t.profiler.total_render_time >= 0, 'Total render time was not calculated' assert(t.profiler.total_render_time >= 0, "Total render time was not calculated")
end end
def test_profiling_uses_include_to_mark_children def test_profiling_uses_include_to_mark_children
@@ -82,7 +82,7 @@ class RenderProfilingTest < Minitest::Test
t.render! t.render!
include_node = t.profiler[1] include_node = t.profiler[1]
assert_equal 2, include_node.children.length assert_equal(2, include_node.children.length)
end end
def test_profiling_marks_children_with_the_name_of_included_partial def test_profiling_marks_children_with_the_name_of_included_partial
@@ -91,7 +91,7 @@ class RenderProfilingTest < Minitest::Test
include_node = t.profiler[1] include_node = t.profiler[1]
include_node.children.each do |child| include_node.children.each do |child|
assert_equal 'a_template', child.partial assert_equal "a_template", child.partial
end end
end end
@@ -101,12 +101,12 @@ class RenderProfilingTest < Minitest::Test
a_template = t.profiler[1] a_template = t.profiler[1]
a_template.children.each do |child| a_template.children.each do |child|
assert_equal 'a_template', child.partial assert_equal "a_template", child.partial
end end
b_template = t.profiler[2] b_template = t.profiler[2]
b_template.children.each do |child| b_template.children.each do |child|
assert_equal 'b_template', child.partial assert_equal "b_template", child.partial
end end
end end
@@ -116,12 +116,12 @@ class RenderProfilingTest < Minitest::Test
a_template1 = t.profiler[1] a_template1 = t.profiler[1]
a_template1.children.each do |child| a_template1.children.each do |child|
assert_equal 'a_template', child.partial assert_equal "a_template", child.partial
end end
a_template2 = t.profiler[2] a_template2 = t.profiler[2]
a_template2.children.each do |child| a_template2.children.each do |child|
assert_equal 'a_template', child.partial assert_equal "a_template", child.partial
end end
end end
@@ -134,23 +134,38 @@ class RenderProfilingTest < Minitest::Test
timing_count += 1 timing_count += 1
end end
assert_equal 2, timing_count assert_equal(2, timing_count)
end end
def test_profiling_marks_children_of_if_blocks def test_profiling_marks_children_of_if_blocks
t = Template.parse('{% if true %} {% increment test %} {{ test }} {% endif %}', profile: true) t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
t.render! t.render!
assert_equal 1, t.profiler.length assert_equal(1, t.profiler.length)
assert_equal 2, t.profiler[0].children.length assert_equal(2, t.profiler[0].children.length)
end end
def test_profiling_marks_children_of_for_blocks def test_profiling_marks_children_of_for_blocks
t = Template.parse('{% for item in collection %} {{ item }} {% endfor %}', profile: true) t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
t.render!('collection' => %w[one two]) t.render!("collection" => ["one", "two"])
assert_equal 1, t.profiler.length assert_equal(1, t.profiler.length)
# Will profile each invocation of the for block # Will profile each invocation of the for block
assert_equal 2, t.profiler[0].children.length assert_equal(2, t.profiler[0].children.length)
end
def test_profiling_supports_self_time
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
t.render!("collection" => ["one", "two"])
leaf = t.profiler[0].children[0]
assert_operator leaf.self_time, :>, 0
end
def test_profiling_supports_total_time
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
t.render!
assert_operator t.profiler[0].total_time, :>, 0
end end
end end

View File

@@ -19,28 +19,28 @@ class SecurityTest < Minitest::Test
text = %( {{ '1+1' | instance_eval }} ) text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 ) expected = %( 1+1 )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_no_existing_instance_eval def test_no_existing_instance_eval
text = %( {{ '1+1' | __instance_eval__ }} ) text = %( {{ '1+1' | __instance_eval__ }} )
expected = %( 1+1 ) expected = %( 1+1 )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_no_instance_eval_after_mixing_in_new_filter def test_no_instance_eval_after_mixing_in_new_filter
text = %( {{ '1+1' | instance_eval }} ) text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 ) expected = %( 1+1 )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_no_instance_eval_later_in_chain def test_no_instance_eval_later_in_chain
text = %( {{ '1+1' | add_one | instance_eval }} ) text = %( {{ '1+1' | add_one | instance_eval }} )
expected = %( 1+1 + 1 ) expected = %( 1+1 + 1 )
assert_equal expected, Template.parse(text).render!(@assigns, filters: SecurityFilter) assert_equal(expected, Template.parse(text).render!(@assigns, filters: SecurityFilter))
end end
def test_does_not_add_filters_to_symbol_table def test_does_not_add_filters_to_symbol_table
@@ -49,32 +49,32 @@ class SecurityTest < Minitest::Test
test = %( {{ "some_string" | a_bad_filter }} ) test = %( {{ "some_string" | a_bad_filter }} )
template = Template.parse(test) template = Template.parse(test)
assert_equal [], (Symbol.all_symbols - current_symbols) assert_equal([], (Symbol.all_symbols - current_symbols))
template.render! template.render!
assert_equal [], (Symbol.all_symbols - current_symbols) assert_equal([], (Symbol.all_symbols - current_symbols))
end end
def test_does_not_add_drop_methods_to_symbol_table def test_does_not_add_drop_methods_to_symbol_table
current_symbols = Symbol.all_symbols current_symbols = Symbol.all_symbols
assigns = { 'drop' => Drop.new } assigns = { 'drop' => Drop.new }
assert_equal '', Template.parse('{{ drop.custom_method_1 }}', assigns).render! assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!)
assert_equal '', Template.parse('{{ drop.custom_method_2 }}', assigns).render! assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!)
assert_equal '', Template.parse('{{ drop.custom_method_3 }}', assigns).render! assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!)
assert_equal [], (Symbol.all_symbols - current_symbols) assert_equal([], (Symbol.all_symbols - current_symbols))
end end
def test_max_depth_nested_blocks_does_not_raise_exception def test_max_depth_nested_blocks_does_not_raise_exception
depth = Liquid::Block::MAX_DEPTH depth = Liquid::Block::MAX_DEPTH
code = '{% if true %}' * depth + 'rendered' + '{% endif %}' * depth code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_equal 'rendered', Template.parse(code).render! assert_equal("rendered", Template.parse(code).render!)
end end
def test_more_than_max_depth_nested_blocks_raises_exception def test_more_than_max_depth_nested_blocks_raises_exception
depth = Liquid::Block::MAX_DEPTH + 1 depth = Liquid::Block::MAX_DEPTH + 1
code = '{% if true %}' * depth + 'rendered' + '{% endif %}' * depth code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_raises(Liquid::StackLevelError) do assert_raises(Liquid::StackLevelError) do
Template.parse(code).render! Template.parse(code).render!
end end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'test_helper'
class EchoTest < Minitest::Test
include Liquid
def test_echo_outputs_its_input
assert_template_result('BAR', <<~LIQUID, 'variable-name' => 'bar')
{%- echo variable-name | upcase -%}
LIQUID
end
end

View File

@@ -42,31 +42,31 @@ HERE
assert_template_result(' 1 2 3 ', '{%for item in (1..3) %} {{item}} {%endfor%}') assert_template_result(' 1 2 3 ', '{%for item in (1..3) %} {{item}} {%endfor%}')
assert_raises(Liquid::ArgumentError) do assert_raises(Liquid::ArgumentError) do
Template.parse('{% for i in (a..2) %}{% endfor %}').render!('a' => [1, 2]) Template.parse('{% for i in (a..2) %}{% endfor %}').render!("a" => [1, 2])
end end
assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', 'a' => 'invalid integer') assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', "a" => "invalid integer")
end end
def test_for_with_variable_range def test_for_with_variable_range
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', 'foobar' => 3) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3)
end end
def test_for_with_hash_value_range def test_for_with_hash_value_range
foobar = { 'value' => 3 } foobar = { "value" => 3 }
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', 'foobar' => foobar) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
end end
def test_for_with_drop_value_range def test_for_with_drop_value_range
foobar = ThingWithValue.new foobar = ThingWithValue.new
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', 'foobar' => foobar) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar)
end end
def test_for_with_variable def test_for_with_variable
assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', 'array' => [1, 2, 3]) assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', 'array' => [1, 2, 3])
assert_template_result('123', '{%for item in array%}{{item}}{%endfor%}', 'array' => [1, 2, 3]) assert_template_result('123', '{%for item in array%}{{item}}{%endfor%}', 'array' => [1, 2, 3])
assert_template_result('123', '{% for item in array %}{{item}}{% endfor %}', 'array' => [1, 2, 3]) assert_template_result('123', '{% for item in array %}{{item}}{% endfor %}', 'array' => [1, 2, 3])
assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', 'array' => %w[a b c d]) assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', 'b', 'c', 'd'])
assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', ' ', 'b', ' ', 'c']) assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', ' ', 'b', ' ', 'c'])
assert_template_result('abc', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', '', 'b', '', 'c']) assert_template_result('abc', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', '', 'b', '', 'c'])
end end
@@ -74,8 +74,8 @@ HERE
def test_for_helpers def test_for_helpers
assigns = { 'array' => [1, 2, 3] } assigns = { 'array' => [1, 2, 3] }
assert_template_result(' 1/3 2/3 3/3 ', assert_template_result(' 1/3 2/3 3/3 ',
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}', '{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
assigns) assigns)
assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns) assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns)
assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns) assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns)
assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns) assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns)
@@ -87,8 +87,8 @@ HERE
def test_for_and_if def test_for_and_if
assigns = { 'array' => [1, 2, 3] } assigns = { 'array' => [1, 2, 3] }
assert_template_result('+--', assert_template_result('+--',
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}', '{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
assigns) assigns)
end end
def test_for_else def test_for_else
@@ -116,7 +116,7 @@ HERE
exception = assert_raises(Liquid::ArgumentError) do exception = assert_raises(Liquid::ArgumentError) do
Template.parse(template).render!(assigns) Template.parse(template).render!(assigns)
end end
assert_equal('Liquid error: invalid integer', exception.message) assert_equal("Liquid error: invalid integer", exception.message)
end end
def test_limiting_with_invalid_offset def test_limiting_with_invalid_offset
@@ -130,7 +130,7 @@ HERE
exception = assert_raises(Liquid::ArgumentError) do exception = assert_raises(Liquid::ArgumentError) do
Template.parse(template).render!(assigns) Template.parse(template).render!(assigns)
end end
assert_equal('Liquid error: invalid integer', exception.message) assert_equal("Liquid error: invalid integer", exception.message)
end end
def test_dynamic_variable_limiting def test_dynamic_variable_limiting
@@ -159,14 +159,14 @@ HERE
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
MKUP MKUP
expected = <<-XPCTD expected = <<-XPCTD
123 123
next next
456 456
next next
789 789
XPCTD XPCTD
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
@@ -178,14 +178,14 @@ HERE
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%}
MKUP MKUP
expected = <<-XPCTD expected = <<-XPCTD
123 123
next next
456 456
next next
7 7
XPCTD XPCTD
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
@@ -197,14 +197,14 @@ HERE
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%}
MKUP MKUP
expected = <<-XPCTD expected = <<-XPCTD
123 123
next next
456 456
next next
7890 7890
XPCTD XPCTD
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
@@ -227,19 +227,19 @@ HERE
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } }
markup = '{% for i in array.items %}{% break %}{% endfor %}' markup = '{% for i in array.items %}{% break %}{% endfor %}'
expected = '' expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
expected = '1' expected = "1"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
expected = '' expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
expected = '1234' expected = "1234"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# tests to ensure it only breaks out of the local for loop # tests to ensure it only breaks out of the local for loop
@@ -267,23 +267,23 @@ HERE
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
markup = '{% for i in array.items %}{% continue %}{% endfor %}' markup = '{% for i in array.items %}{% continue %}{% endfor %}'
expected = '' expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
expected = '12345' expected = "12345"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
expected = '' expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = '123' expected = "123"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}' markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
expected = '1245' expected = "1245"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# tests to ensure it only continues the local for loop and not all of them. # tests to ensure it only continues the local for loop and not all of them.
@@ -312,49 +312,49 @@ HERE
# the functionality for backwards compatibility # the functionality for backwards compatibility
assert_template_result('test string', assert_template_result('test string',
'{%for val in string%}{{val}}{%endfor%}', '{%for val in string%}{{val}}{%endfor%}',
'string' => 'test string') 'string' => "test string")
assert_template_result('test string', assert_template_result('test string',
'{%for val in string limit:1%}{{val}}{%endfor%}', '{%for val in string limit:1%}{{val}}{%endfor%}',
'string' => 'test string') 'string' => "test string")
assert_template_result('val-string-1-1-0-1-0-true-true-test string', assert_template_result('val-string-1-1-0-1-0-true-true-test string',
'{%for val in string%}' \ '{%for val in string%}' \
'{{forloop.name}}-' \ '{{forloop.name}}-' \
'{{forloop.index}}-' \ '{{forloop.index}}-' \
'{{forloop.length}}-' \ '{{forloop.length}}-' \
'{{forloop.index0}}-' \ '{{forloop.index0}}-' \
'{{forloop.rindex}}-' \ '{{forloop.rindex}}-' \
'{{forloop.rindex0}}-' \ '{{forloop.rindex0}}-' \
'{{forloop.first}}-' \ '{{forloop.first}}-' \
'{{forloop.last}}-' \ '{{forloop.last}}-' \
'{{val}}{%endfor%}', '{{val}}{%endfor%}',
'string' => 'test string') 'string' => "test string")
end end
def test_for_parentloop_references_parent_loop def test_for_parentloop_references_parent_loop
assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ', assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ',
'{% for inner in outer %}{% for k in inner %}' \ '{% for inner in outer %}{% for k in inner %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \ '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}{% endfor %}', '{% endfor %}{% endfor %}',
'outer' => [[1, 1, 1], [1, 1, 1]]) 'outer' => [[1, 1, 1], [1, 1, 1]])
end end
def test_for_parentloop_nil_when_not_present def test_for_parentloop_nil_when_not_present
assert_template_result('.1 .2 ', assert_template_result('.1 .2 ',
'{% for inner in outer %}' \ '{% for inner in outer %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \ '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}', '{% endfor %}',
'outer' => [[1, 1, 1], [1, 1, 1]]) 'outer' => [[1, 1, 1], [1, 1, 1]])
end end
def test_inner_for_over_empty_input def test_inner_for_over_empty_input
assert_template_result 'oo', '{% for a in (1..2) %}o{% for b in empty %}{% endfor %}{% endfor %}' assert_template_result('oo', '{% for a in (1..2) %}o{% for b in empty %}{% endfor %}{% endfor %}')
end end
def test_blank_string_not_iterable def test_blank_string_not_iterable
assert_template_result('', '{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}', 'characters' => '') assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '')
end end
def test_bad_variable_naming_in_for_loop def test_bad_variable_naming_in_for_loop
@@ -394,8 +394,8 @@ HERE
expected = '12345' expected = '12345'
template = '{% for item in items %}{{item}}{% endfor %}' template = '{% for item in items %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
assert loader.each_called assert(loader.each_called)
assert !loader.load_slice_called assert(!loader.load_slice_called)
end end
def test_iterate_with_load_slice_when_limit_applied def test_iterate_with_load_slice_when_limit_applied
@@ -404,8 +404,8 @@ HERE
expected = '1' expected = '1'
template = '{% for item in items limit:1 %}{{item}}{% endfor %}' template = '{% for item in items limit:1 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
assert !loader.each_called assert(!loader.each_called)
assert loader.load_slice_called assert(loader.load_slice_called)
end end
def test_iterate_with_load_slice_when_limit_and_offset_applied def test_iterate_with_load_slice_when_limit_and_offset_applied
@@ -414,8 +414,8 @@ HERE
expected = '34' expected = '34'
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}' template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
assert !loader.each_called assert(!loader.each_called)
assert loader.load_slice_called assert(loader.load_slice_called)
end end
def test_iterate_with_load_slice_returns_same_results_as_without def test_iterate_with_load_slice_returns_same_results_as_without
@@ -435,6 +435,6 @@ HERE
Liquid::Template.parse('{% for i in (1..2) %}{{ standard_error }}{% endfor %}').render!(context) Liquid::Template.parse('{% for i in (1..2) %}{{ standard_error }}{% endfor %}').render!(context)
end end
assert context.registers[:for_stack].empty? assert(context.registers[:for_stack].empty?)
end end
end end

View File

@@ -8,7 +8,7 @@ class IfElseTagTest < Minitest::Test
def test_if def test_if
assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ') assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ')
assert_template_result(' this text should go into the output ', assert_template_result(' this text should go into the output ',
' {% if true %} this text should go into the output {% endif %} ') ' {% if true %} this text should go into the output {% endif %} ')
assert_template_result(' you rock ?', '{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?') assert_template_result(' you rock ?', '{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?')
end end
@@ -51,12 +51,12 @@ class IfElseTagTest < Minitest::Test
def test_comparison_of_expressions_starting_with_and_or_or def test_comparison_of_expressions_starting_with_and_or_or
assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } } assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } }
assert_template_result('YES', assert_template_result("YES",
"{% if android.name == 'Roy' %}YES{% endif %}", "{% if android.name == 'Roy' %}YES{% endif %}",
assigns) assigns)
assert_template_result('YES', assert_template_result("YES",
'{% if order.items_count == 0 %}YES{% endif %}', "{% if order.items_count == 0 %}YES{% endif %}",
assigns) assigns)
end end
def test_if_and def test_if_and
@@ -77,14 +77,14 @@ class IfElseTagTest < Minitest::Test
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => nil) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => nil)
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => true) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => true)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => 'text') assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => "text")
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => 1) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => 1)
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => {}) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => {})
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => []) assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => [])
assert_template_result(' YES ', '{% if "foo" %} YES {% endif %}') assert_template_result(' YES ', '{% if "foo" %} YES {% endif %}')
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => true }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => true })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => 'text' }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => "text" })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => 1 }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => 1 })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => {} }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => {} })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => [] }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => [] })
@@ -92,11 +92,11 @@ class IfElseTagTest < Minitest::Test
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => false) assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => false)
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => nil) assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => nil)
assert_template_result(' YES ', '{% if var %} YES {% else %} NO {% endif %}', 'var' => true) assert_template_result(' YES ', '{% if var %} YES {% else %} NO {% endif %}', 'var' => true)
assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => 'text') assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text")
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'bar' => false }) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'bar' => false })
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => true }) assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => true })
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => 'text' }) assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => "text" })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'notbar' => true }) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'notbar' => true })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {}) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {})
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => { 'bar' => true }) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => { 'bar' => true })
@@ -156,7 +156,7 @@ class IfElseTagTest < Minitest::Test
Condition.operators['contains'] = :[] Condition.operators['contains'] = :[]
assert_template_result('yes', assert_template_result('yes',
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %})) %({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}))
ensure ensure
Condition.operators['contains'] = original_op Condition.operators['contains'] = original_op
end end
@@ -168,7 +168,7 @@ class IfElseTagTest < Minitest::Test
end end
def test_multiple_conditions def test_multiple_conditions
tpl = '{% if a or b and c %}true{% else %}false{% endif %}' tpl = "{% if a or b and c %}true{% else %}false{% endif %}"
tests = { tests = {
[true, true, true] => true, [true, true, true] => true,

View File

@@ -5,35 +5,38 @@ require 'test_helper'
class TestFileSystem class TestFileSystem
def read_template_file(template_path) def read_template_file(template_path)
case template_path case template_path
when 'product' when "product"
'Product: {{ product.title }} ' "Product: {{ product.title }} "
when 'locale_variables' when "product_alias"
'Locale: {{echo1}} {{echo2}}' "Product: {{ product.title }} "
when 'variant' when "locale_variables"
'Variant: {{ variant.title }}' "Locale: {{echo1}} {{echo2}}"
when 'nested_template' when "variant"
"Variant: {{ variant.title }}"
when "nested_template"
"{% include 'header' %} {% include 'body' %} {% include 'footer' %}" "{% include 'header' %} {% include 'body' %} {% include 'footer' %}"
when 'body' when "body"
"body {% include 'body_detail' %}" "body {% include 'body_detail' %}"
when 'nested_product_template' when "nested_product_template"
"Product: {{ nested_product_template.title }} {%include 'details'%} " "Product: {{ nested_product_template.title }} {%include 'details'%} "
when 'recursively_nested_template' when "recursively_nested_template"
"-{% include 'recursively_nested_template' %}" "-{% include 'recursively_nested_template' %}"
when 'pick_a_source' when "pick_a_source"
'from TestFileSystem' "from TestFileSystem"
when 'assignments' when 'assignments'
"{% assign foo = 'bar' %}" "{% assign foo = 'bar' %}"
when 'break' when 'break'
'{% break %}' "{% break %}"
else else
template_path template_path
@@ -57,15 +60,16 @@ class CountingFileSystem
end end
class CustomInclude < Liquid::Tag class CustomInclude < Liquid::Tag
SYNTAX = /(#{Liquid::QUOTED_FRAGMENT}+)(\s+(?:with|for)\s+(#{Liquid::QUOTED_FRAGMENT}+))?/o.freeze Syntax = /(#{Liquid::QuotedFragment}+)(\s+(?:with|for)\s+(#{Liquid::QuotedFragment}+))?/o
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, tokens)
markup =~ SYNTAX markup =~ Syntax
@template_name = Regexp.last_match(1) @template_name = Regexp.last_match(1)
super super
end end
def parse(tokens); end def parse(tokens)
end
def render_to_output_buffer(_context, output) def render_to_output_buffer(_context, output)
output << @template_name[1..-2] output << @template_name[1..-2]
@@ -81,56 +85,66 @@ class IncludeTagTest < Minitest::Test
end end
def test_include_tag_looks_for_file_system_in_registers_first def test_include_tag_looks_for_file_system_in_registers_first
assert_equal 'from OtherFileSystem', assert_equal('from OtherFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new }) Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new }))
end end
def test_include_tag_with def test_include_tag_with
assert_template_result 'Product: Draft 151cm ', assert_template_result("Product: Draft 151cm ",
"{% include 'product' with products[0] %}", 'products' => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] "{% include 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_include_tag_with_alias
assert_template_result("Product: Draft 151cm ",
"{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_include_tag_for_alias
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_include_tag_with_default_name def test_include_tag_with_default_name
assert_template_result 'Product: Draft 151cm ', assert_template_result("Product: Draft 151cm ",
"{% include 'product' %}", 'product' => { 'title' => 'Draft 151cm' } "{% include 'product' %}", "product" => { 'title' => 'Draft 151cm' })
end end
def test_include_tag_for def test_include_tag_for
assert_template_result 'Product: Draft 151cm Product: Element 155cm ', assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% include 'product' for products %}", 'products' => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] "{% include 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_include_tag_with_local_variables def test_include_tag_with_local_variables
assert_template_result 'Locale: test123 ', "{% include 'locale_variables' echo1: 'test123' %}" assert_template_result("Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}")
end end
def test_include_tag_with_multiple_local_variables def test_include_tag_with_multiple_local_variables
assert_template_result 'Locale: test123 test321', assert_template_result("Locale: test123 test321",
"{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}" "{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}")
end end
def test_include_tag_with_multiple_local_variables_from_context def test_include_tag_with_multiple_local_variables_from_context
assert_template_result 'Locale: test123 test321', assert_template_result("Locale: test123 test321",
"{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}", "{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}",
'echo1' => 'test123', 'more_echos' => { 'echo2' => 'test321' } 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' })
end end
def test_included_templates_assigns_variables def test_included_templates_assigns_variables
assert_template_result 'bar', "{% include 'assignments' %}{{ foo }}" assert_template_result("bar", "{% include 'assignments' %}{{ foo }}")
end end
def test_nested_include_tag def test_nested_include_tag
assert_template_result 'body body_detail', "{% include 'body' %}" assert_template_result("body body_detail", "{% include 'body' %}")
assert_template_result 'header body body_detail footer', "{% include 'nested_template' %}" assert_template_result("header body body_detail footer", "{% include 'nested_template' %}")
end end
def test_nested_include_with_variable def test_nested_include_with_variable
assert_template_result 'Product: Draft 151cm details ', assert_template_result("Product: Draft 151cm details ",
"{% include 'nested_product_template' with product %}", 'product' => { 'title' => 'Draft 151cm' } "{% include 'nested_product_template' with product %}", "product" => { "title" => 'Draft 151cm' })
assert_template_result 'Product: Draft 151cm details Product: Element 155cm details ', assert_template_result("Product: Draft 151cm details Product: Element 155cm details ",
"{% include 'nested_product_template' for products %}", 'products' => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] "{% include 'nested_product_template' for products %}", "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }])
end end
def test_recursively_included_template_does_not_produce_endless_loop def test_recursively_included_template_does_not_produce_endless_loop
@@ -148,41 +162,41 @@ class IncludeTagTest < Minitest::Test
end end
def test_dynamically_choosen_template def test_dynamically_choosen_template
assert_template_result 'Test123', '{% include template %}', 'template' => 'Test123' assert_template_result("Test123", "{% include template %}", "template" => 'Test123')
assert_template_result 'Test321', '{% include template %}', 'template' => 'Test321' assert_template_result("Test321", "{% include template %}", "template" => 'Test321')
assert_template_result 'Product: Draft 151cm ', '{% include template for product %}', assert_template_result("Product: Draft 151cm ", "{% include template for product %}",
'template' => 'product', 'product' => { 'title' => 'Draft 151cm' } "template" => 'product', 'product' => { 'title' => 'Draft 151cm' })
end end
def test_include_tag_caches_second_read_of_same_partial def test_include_tag_caches_second_read_of_same_partial
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal 'from CountingFileSystemfrom CountingFileSystem', assert_equal('from CountingFileSystemfrom CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }) Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }))
assert_equal 1, file_system.count assert_equal(1, file_system.count)
end end
def test_include_tag_doesnt_cache_partials_across_renders def test_include_tag_doesnt_cache_partials_across_renders
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal 'from CountingFileSystem', assert_equal('from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }) Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }))
assert_equal 1, file_system.count assert_equal(1, file_system.count)
assert_equal 'from CountingFileSystem', assert_equal('from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }) Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }))
assert_equal 2, file_system.count assert_equal(2, file_system.count)
end end
def test_include_tag_within_if_statement def test_include_tag_within_if_statement
assert_template_result 'foo_if_true', "{% if true %}{% include 'foo_if_true' %}{% endif %}" assert_template_result("foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}")
end end
def test_custom_include_tag def test_custom_include_tag
original_tag = Liquid::Template.tags['include'] original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude Liquid::Template.tags['include'] = CustomInclude
begin begin
assert_equal 'custom_foo', assert_equal("custom_foo",
Template.parse("{% include 'custom_foo' %}").render! Template.parse("{% include 'custom_foo' %}").render!)
ensure ensure
Liquid::Template.tags['include'] = original_tag Liquid::Template.tags['include'] = original_tag
end end
@@ -192,8 +206,8 @@ class IncludeTagTest < Minitest::Test
original_tag = Liquid::Template.tags['include'] original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude Liquid::Template.tags['include'] = CustomInclude
begin begin
assert_equal 'custom_foo_if_true', assert_equal("custom_foo_if_true",
Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render! Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render!)
ensure ensure
Liquid::Template.tags['include'] = original_tag Liquid::Template.tags['include'] = original_tag
end end
@@ -204,21 +218,21 @@ class IncludeTagTest < Minitest::Test
a = Liquid::Template.parse(' {% include "nested_template" %}') a = Liquid::Template.parse(' {% include "nested_template" %}')
a.render! a.render!
assert_empty a.errors assert_empty(a.errors)
end end
def test_passing_options_to_included_templates def test_passing_options_to_included_templates
assert_raises(Liquid::SyntaxError) do assert_raises(Liquid::SyntaxError) do
Template.parse('{% include template %}', error_mode: :strict).render!('template' => '{{ "X" || downcase }}') Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}')
end end
with_error_mode(:lax) do with_error_mode(:lax) do
assert_equal 'x', Template.parse('{% include template %}', error_mode: :strict, include_options_blacklist: true).render!('template' => '{{ "X" || downcase }}') assert_equal 'x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}')
end end
assert_raises(Liquid::SyntaxError) do assert_raises(Liquid::SyntaxError) do
Template.parse('{% include template %}', error_mode: :strict, include_options_blacklist: [:locale]).render!('template' => '{{ "X" || downcase }}') Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}')
end end
with_error_mode(:lax) do with_error_mode(:lax) do
assert_equal 'x', Template.parse('{% include template %}', error_mode: :strict, include_options_blacklist: [:error_mode]).render!('template' => '{{ "X" || downcase }}') assert_equal 'x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}')
end end
end end
@@ -234,22 +248,22 @@ class IncludeTagTest < Minitest::Test
end end
def test_including_via_variable_value def test_including_via_variable_value
assert_template_result 'from TestFileSystem', "{% assign page = 'pick_a_source' %}{% include page %}" assert_template_result("from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}")
assert_template_result 'Product: Draft 151cm ', "{% assign page = 'product' %}{% include page %}", 'product' => { 'title' => 'Draft 151cm' } assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page %}", "product" => { 'title' => 'Draft 151cm' })
assert_template_result 'Product: Draft 151cm ', "{% assign page = 'product' %}{% include page for foo %}", 'foo' => { 'title' => 'Draft 151cm' } assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' })
end end
def test_including_with_strict_variables def test_including_with_strict_variables
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn) template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
template.render(nil, strict_variables: true) template.render(nil, strict_variables: true)
assert_equal [], template.errors assert_equal([], template.errors)
end end
def test_break_through_include def test_break_through_include
assert_template_result '1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}' assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}")
assert_template_result '1', "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}" assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}")
end end
end # IncludeTagTest end # IncludeTagTest

View File

@@ -9,17 +9,17 @@ class IncrementTagTest < Minitest::Test
assert_template_result('0', '{%increment port %}', {}) assert_template_result('0', '{%increment port %}', {})
assert_template_result('0 1', '{%increment port %} {%increment port%}', {}) assert_template_result('0 1', '{%increment port %} {%increment port%}', {})
assert_template_result('0 0 1 2 1', assert_template_result('0 0 1 2 1',
'{%increment port %} {%increment starboard%} ' \ '{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%increment port%} ' \ '{%increment port %} {%increment port%} ' \
'{%increment starboard %}', {}) '{%increment starboard %}', {})
end end
def test_dec def test_dec
assert_template_result('9', '{%decrement port %}', 'port' => 10) assert_template_result('9', '{%decrement port %}', 'port' => 10)
assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {}) assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {})
assert_template_result('1 5 2 2 5', assert_template_result('1 5 2 2 5',
'{%increment port %} {%increment starboard%} ' \ '{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%decrement port%} ' \ '{%increment port %} {%decrement port%} ' \
'{%decrement starboard %}', 'port' => 1, 'starboard' => 5) '{%decrement starboard %}', 'port' => 1, 'starboard' => 5)
end end
end end

View File

@@ -0,0 +1,106 @@
# frozen_string_literal: true
require 'test_helper'
class LiquidTagTest < Minitest::Test
include Liquid
def test_liquid_tag
assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3])
{%- liquid
echo array | join: " "
-%}
LIQUID
assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3])
{%- liquid
for value in array
echo value
unless forloop.last
echo " "
endunless
endfor
-%}
LIQUID
assert_template_result('4 8 12 6', <<~LIQUID, 'array' => [1, 2, 3])
{%- liquid
for value in array
assign double_value = value | times: 2
echo double_value | times: 2
unless forloop.last
echo " "
endunless
endfor
echo " "
echo double_value
-%}
LIQUID
assert_template_result('abc', <<~LIQUID)
{%- liquid echo "a" -%}
b
{%- liquid echo "c" -%}
LIQUID
end
def test_liquid_tag_errors
assert_match_syntax_error("syntax error (line 1): Unknown tag 'error'", <<~LIQUID)
{%- liquid error no such tag -%}
LIQUID
assert_match_syntax_error("syntax error (line 7): Unknown tag 'error'", <<~LIQUID)
{{ test }}
{%-
liquid
for value in array
error no such tag
endfor
-%}
LIQUID
assert_match_syntax_error("syntax error (line 2): Unknown tag '!!! the guards are vigilant'", <<~LIQUID)
{%- liquid
!!! the guards are vigilant
-%}
LIQUID
assert_match_syntax_error("syntax error (line 4): 'for' tag was never closed", <<~LIQUID)
{%- liquid
for value in array
echo 'forgot to close the for tag'
-%}
LIQUID
end
def test_line_number_is_correct_after_a_blank_token
assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n\n error %}")
assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n \n error %}")
end
def test_cannot_open_blocks_living_past_a_liquid_tag
assert_match_syntax_error("syntax error (line 3): 'if' tag was never closed", <<~LIQUID)
{%- liquid
if true
-%}
{%- endif -%}
LIQUID
end
def test_quirk_can_close_blocks_created_before_a_liquid_tag
assert_template_result("42", <<~LIQUID)
{%- if true -%}
42
{%- liquid endif -%}
LIQUID
end
def test_liquid_tag_in_raw
assert_template_result("{% liquid echo 'test' %}\n", <<~LIQUID)
{% raw %}{% liquid echo 'test' %}{% endraw %}
LIQUID
end
end

View File

@@ -6,23 +6,23 @@ class RawTagTest < Minitest::Test
include Liquid include Liquid
def test_tag_in_raw def test_tag_in_raw
assert_template_result '{% comment %} test {% endcomment %}', assert_template_result('{% comment %} test {% endcomment %}',
'{% raw %}{% comment %} test {% endcomment %}{% endraw %}' '{% raw %}{% comment %} test {% endcomment %}{% endraw %}')
end end
def test_output_in_raw def test_output_in_raw
assert_template_result '{{ test }}', '{% raw %}{{ test }}{% endraw %}' assert_template_result('{{ test }}', '{% raw %}{{ test }}{% endraw %}')
end end
def test_open_tag_in_raw def test_open_tag_in_raw
assert_template_result ' Foobar {% invalid ', '{% raw %} Foobar {% invalid {% endraw %}' assert_template_result(' Foobar {% invalid ', '{% raw %} Foobar {% invalid {% endraw %}')
assert_template_result ' Foobar invalid %} ', '{% raw %} Foobar invalid %} {% endraw %}' assert_template_result(' Foobar invalid %} ', '{% raw %} Foobar invalid %} {% endraw %}')
assert_template_result ' Foobar {{ invalid ', '{% raw %} Foobar {{ invalid {% endraw %}' assert_template_result(' Foobar {{ invalid ', '{% raw %} Foobar {{ invalid {% endraw %}')
assert_template_result ' Foobar invalid }} ', '{% raw %} Foobar invalid }} {% endraw %}' assert_template_result(' Foobar invalid }} ', '{% raw %} Foobar invalid }} {% endraw %}')
assert_template_result ' Foobar {% invalid {% {% endraw ', '{% raw %} Foobar {% invalid {% {% endraw {% endraw %}' assert_template_result(' Foobar {% invalid {% {% endraw ', '{% raw %} Foobar {% invalid {% {% endraw {% endraw %}')
assert_template_result ' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}' assert_template_result(' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}')
assert_template_result ' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}' assert_template_result(' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}')
assert_template_result ' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}' assert_template_result(' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}')
end end
def test_invalid_raw def test_invalid_raw

View File

@@ -0,0 +1,217 @@
# frozen_string_literal: true
require 'test_helper'
class RenderTagTest < Minitest::Test
include Liquid
def test_render_with_no_arguments
Liquid::Template.file_system = StubFileSystem.new('source' => 'rendered content')
assert_template_result('rendered content', '{% render "source" %}')
end
def test_render_tag_looks_for_file_system_in_registers_first
file_system = StubFileSystem.new('pick_a_source' => 'from register file system')
assert_equal('from register file system',
Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system }))
end
def test_render_passes_named_arguments_into_inner_scope
Liquid::Template.file_system = StubFileSystem.new('product' => '{{ inner_product.title }}')
assert_template_result('My Product', '{% render "product", inner_product: outer_product %}',
'outer_product' => { 'title' => 'My Product' })
end
def test_render_accepts_literals_as_arguments
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ price }}')
assert_template_result('123', '{% render "snippet", price: 123 %}')
end
def test_render_accepts_multiple_named_arguments
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ one }} {{ two }}')
assert_template_result('1 2', '{% render "snippet", one: 1, two: 2 %}')
end
def test_render_does_not_inherit_parent_scope_variables
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ outer_variable }}')
assert_template_result('', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}')
end
def test_render_does_not_inherit_variable_with_same_name_as_snippet
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ snippet }}')
assert_template_result('', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}")
end
def test_render_sets_the_correct_template_name_for_errors
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
with_taint_mode :error do
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
template.render(context)
assert_equal [Liquid::TaintedError], template.errors.map(&:class)
assert_equal 'snippet', template.errors.first.template_name
end
end
def test_render_sets_the_correct_template_name_for_warnings
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
with_taint_mode :warn do
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
template.render(context)
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
assert_equal 'snippet', context.warnings.first.template_name
end
end
def test_render_does_not_mutate_parent_scope
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}')
assert_template_result('', "{% render 'snippet' %}{{ inner }}")
end
def test_nested_render_tag
Liquid::Template.file_system = StubFileSystem.new(
'one' => "one {% render 'two' %}",
'two' => 'two'
)
assert_template_result('one two', "{% render 'one' %}")
end
def test_recursively_rendered_template_does_not_produce_endless_loop
Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}')
assert_raises Liquid::StackLevelError do
Template.parse('{% render "loop" %}').render!
end
end
def test_sub_contexts_count_towards_the_same_recursion_limit
Liquid::Template.file_system = StubFileSystem.new(
'loop_render' => '{% render "loop_render" %}',
)
assert_raises Liquid::StackLevelError do
Template.parse('{% render "loop_render" %}').render!
end
end
def test_dynamically_choosen_templates_are_not_allowed
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered')
assert_raises Liquid::SyntaxError do
Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}")
end
end
def test_include_tag_caches_second_read_of_same_partial
file_system = StubFileSystem.new('snippet' => 'echo')
assert_equal('echoecho',
Template.parse('{% render "snippet" %}{% render "snippet" %}')
.render!({}, registers: { file_system: file_system }))
assert_equal(1, file_system.file_read_count)
end
def test_render_tag_doesnt_cache_partials_across_renders
file_system = StubFileSystem.new('snippet' => 'my message')
assert_equal('my message',
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }))
assert_equal(1, file_system.file_read_count)
assert_equal('my message',
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }))
assert_equal(2, file_system.file_read_count)
end
def test_render_tag_within_if_statement
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'my message')
assert_template_result('my message', '{% if true %}{% render "snippet" %}{% endif %}')
end
def test_break_through_render
Liquid::Template.file_system = StubFileSystem.new('break' => '{% break %}')
assert_template_result('1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}')
assert_template_result('112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}')
end
def test_increment_is_isolated_between_renders
Liquid::Template.file_system = StubFileSystem.new('incr' => '{% increment %}')
assert_template_result('010', '{% increment %}{% increment %}{% render "incr" %}')
end
def test_decrement_is_isolated_between_renders
Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}')
assert_template_result('-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}')
end
def test_includes_will_not_render_inside_render_tag
Liquid::Template.file_system = StubFileSystem.new(
'foo' => 'bar',
'test_include' => '{% include "foo" %}'
)
assert_template_result('include usage is not allowed in this context', '{% render "test_include" %}')
end
def test_includes_will_not_render_inside_nested_sibling_tags
Liquid::Template.file_system = StubFileSystem.new(
'foo' => 'bar',
'nested_render_with_sibling_include' => '{% render "test_include" %}{% include "foo" %}',
'test_include' => '{% include "foo" %}'
)
assert_template_result('include usage is not allowed in this contextinclude usage is not allowed in this context', '{% render "nested_render_with_sibling_include" %}')
end
def test_render_tag_with
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm ",
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_with_alias
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm ",
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for_alias
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_forloop
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} {% if forloop.first %}first{% endif %} {% if forloop.last %}last{% endif %} index:{{ forloop.index }} ",
)
assert_template_result("Product: Draft 151cm first index:1 Product: Element 155cm last index:2 ",
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
end

View File

@@ -7,7 +7,7 @@ class StandardTagTest < Minitest::Test
def test_no_transform def test_no_transform
assert_template_result('this text should come out of the template without change...', assert_template_result('this text should come out of the template without change...',
'this text should come out of the template without change...') 'this text should come out of the template without change...')
assert_template_result('blah', 'blah') assert_template_result('blah', 'blah')
assert_template_result('<blah>', '<blah>') assert_template_result('<blah>', '<blah>')
@@ -21,7 +21,7 @@ class StandardTagTest < Minitest::Test
def test_has_a_block_which_does_nothing def test_has_a_block_which_does_nothing
assert_template_result(%(the comment block should be removed .. right?), assert_template_result(%(the comment block should be removed .. right?),
%(the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?)) %(the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?))
assert_template_result('', '{%comment%}{%endcomment%}') assert_template_result('', '{%comment%}{%endcomment%}')
assert_template_result('', '{%comment%}{% endcomment %}') assert_template_result('', '{%comment%}{% endcomment %}')
@@ -63,60 +63,60 @@ class StandardTagTest < Minitest::Test
def test_capture def test_capture
assigns = { 'var' => 'content' } assigns = { 'var' => 'content' }
assert_template_result('content foo content foo ', assert_template_result('content foo content foo ',
'{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', '{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
assigns) assigns)
end end
def test_capture_detects_bad_syntax def test_capture_detects_bad_syntax
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
assert_template_result('content foo content foo ', assert_template_result('content foo content foo ',
'{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', '{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}',
'var' => 'content') 'var' => 'content')
end end
end end
def test_case def test_case
assigns = { 'condition' => 2 } assigns = { 'condition' => 2 }
assert_template_result(' its 2 ', assert_template_result(' its 2 ',
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
assigns) assigns)
assigns = { 'condition' => 1 } assigns = { 'condition' => 1 }
assert_template_result(' its 1 ', assert_template_result(' its 1 ',
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
assigns) assigns)
assigns = { 'condition' => 3 } assigns = { 'condition' => 3 }
assert_template_result('', assert_template_result('',
'{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}',
assigns) assigns)
assigns = { 'condition' => 'string here' } assigns = { 'condition' => "string here" }
assert_template_result(' hit ', assert_template_result(' hit ',
'{% case condition %}{% when "string here" %} hit {% endcase %}', '{% case condition %}{% when "string here" %} hit {% endcase %}',
assigns) assigns)
assigns = { 'condition' => 'bad string here' } assigns = { 'condition' => "bad string here" }
assert_template_result('', assert_template_result('',
'{% case condition %}{% when "string here" %} hit {% endcase %}',\ '{% case condition %}{% when "string here" %} hit {% endcase %}',\
assigns) assigns)
end end
def test_case_with_else def test_case_with_else
assigns = { 'condition' => 5 } assigns = { 'condition' => 5 }
assert_template_result(' hit ', assert_template_result(' hit ',
'{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}',
assigns) assigns)
assigns = { 'condition' => 6 } assigns = { 'condition' => 6 }
assert_template_result(' else ', assert_template_result(' else ',
'{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}',
assigns) assigns)
assigns = { 'condition' => 6 } assigns = { 'condition' => 6 }
assert_template_result(' else ', assert_template_result(' else ',
'{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}', '{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}',
assigns) assigns)
end end
def test_case_on_size def test_case_on_size
@@ -130,57 +130,57 @@ class StandardTagTest < Minitest::Test
def test_case_on_size_with_else def test_case_on_size_with_else
assert_template_result('else', assert_template_result('else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
'a' => []) 'a' => [])
assert_template_result('1', assert_template_result('1',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
'a' => [1]) 'a' => [1])
assert_template_result('2', assert_template_result('2',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
'a' => [1, 1]) 'a' => [1, 1])
assert_template_result('else', assert_template_result('else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
'a' => [1, 1, 1]) 'a' => [1, 1, 1])
assert_template_result('else', assert_template_result('else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
'a' => [1, 1, 1, 1]) 'a' => [1, 1, 1, 1])
assert_template_result('else', assert_template_result('else',
'{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}',
'a' => [1, 1, 1, 1, 1]) 'a' => [1, 1, 1, 1, 1])
end end
def test_case_on_length_with_else def test_case_on_length_with_else
assert_template_result('else', assert_template_result('else',
'{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}) {})
assert_template_result('false', assert_template_result('false',
'{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}) {})
assert_template_result('true', assert_template_result('true',
'{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}) {})
assert_template_result('else', assert_template_result('else',
'{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', '{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}',
{}) {})
end end
def test_assign_from_case def test_assign_from_case
# Example from the shopify forums # Example from the shopify forums
code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}" code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}"
template = Liquid::Template.parse(code) template = Liquid::Template.parse(code)
assert_equal 'menswear', template.render!('collection' => { 'handle' => 'menswear-jackets' }) assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-jackets' }))
assert_equal 'menswear', template.render!('collection' => { 'handle' => 'menswear-t-shirts' }) assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-t-shirts' }))
assert_equal 'womenswear', template.render!('collection' => { 'handle' => 'x' }) assert_equal("womenswear", template.render!("collection" => { 'handle' => 'x' }))
assert_equal 'womenswear', template.render!('collection' => { 'handle' => 'y' }) assert_equal("womenswear", template.render!("collection" => { 'handle' => 'y' }))
assert_equal 'womenswear', template.render!('collection' => { 'handle' => 'z' }) assert_equal("womenswear", template.render!("collection" => { 'handle' => 'z' }))
end end
def test_case_when_or def test_case_when_or
@@ -214,7 +214,7 @@ class StandardTagTest < Minitest::Test
end end
def test_assign def test_assign
assert_template_result 'variable', '{% assign a = "variable"%}{{a}}' assert_template_result('variable', '{% assign a = "variable"%}{{a}}')
end end
def test_assign_unassigned def test_assign_unassigned
@@ -223,11 +223,11 @@ class StandardTagTest < Minitest::Test
end end
def test_assign_an_empty_string def test_assign_an_empty_string
assert_template_result '', '{% assign a = ""%}{{a}}' assert_template_result('', '{% assign a = ""%}{{a}}')
end end
def test_assign_is_global def test_assign_is_global
assert_template_result 'variable', '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}' assert_template_result('variable', '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}')
end end
def test_case_detects_bad_syntax def test_case_detects_bad_syntax
@@ -248,33 +248,33 @@ class StandardTagTest < Minitest::Test
assert_template_result('one two one', '{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}') assert_template_result('one two one', '{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}')
assert_template_result('text-align: left text-align: right', assert_template_result('text-align: left text-align: right',
'{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}') '{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}')
end end
def test_multiple_cycles def test_multiple_cycles
assert_template_result('1 2 1 1 2 3 1', assert_template_result('1 2 1 1 2 3 1',
'{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}') '{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}')
end end
def test_multiple_named_cycles def test_multiple_named_cycles
assert_template_result('one one two two one one', assert_template_result('one one two two one one',
'{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}') '{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}')
end end
def test_multiple_named_cycles_with_names_from_context def test_multiple_named_cycles_with_names_from_context
assigns = { 'var1' => 1, 'var2' => 2 } assigns = { "var1" => 1, "var2" => 2 }
assert_template_result('one one two two one one', assert_template_result('one one two two one one',
'{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', assigns) '{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', assigns)
end end
def test_size_of_array def test_size_of_array
assigns = { 'array' => [1, 2, 3, 4] } assigns = { "array" => [1, 2, 3, 4] }
assert_template_result('array has 4 elements', 'array has {{ array.size }} elements', assigns) assert_template_result('array has 4 elements', "array has {{ array.size }} elements", assigns)
end end
def test_size_of_hash def test_size_of_hash
assigns = { 'hash' => { a: 1, b: 2, c: 3, d: 4 } } assigns = { "hash" => { a: 1, b: 2, c: 3, d: 4 } }
assert_template_result('hash has 4 elements', 'hash has {{ hash.size }} elements', assigns) assert_template_result('hash has 4 elements', "hash has {{ hash.size }} elements", assigns)
end end
def test_illegal_symbols def test_illegal_symbols
@@ -293,6 +293,6 @@ class StandardTagTest < Minitest::Test
end end
def test_multiline_tag def test_multiline_tag
assert_template_result '0 1 2 3', "0{%\nfor i in (1..3)\n%} {{\ni\n}}{%\nendfor\n%}" assert_template_result('0 1 2 3', "0{%\nfor i in (1..3)\n%} {{\ni\n}}{%\nendfor\n%}")
end end
end # StandardTagTest end # StandardTagTest

View File

@@ -7,75 +7,75 @@ class StatementsTest < Minitest::Test
def test_true_eql_true def test_true_eql_true
text = ' {% if true == true %} true {% else %} false {% endif %} ' text = ' {% if true == true %} true {% else %} false {% endif %} '
assert_template_result ' true ', text assert_template_result(' true ', text)
end end
def test_true_not_eql_true def test_true_not_eql_true
text = ' {% if true != true %} true {% else %} false {% endif %} ' text = ' {% if true != true %} true {% else %} false {% endif %} '
assert_template_result ' false ', text assert_template_result(' false ', text)
end end
def test_true_lq_true def test_true_lq_true
text = ' {% if 0 > 0 %} true {% else %} false {% endif %} ' text = ' {% if 0 > 0 %} true {% else %} false {% endif %} '
assert_template_result ' false ', text assert_template_result(' false ', text)
end end
def test_one_lq_zero def test_one_lq_zero
text = ' {% if 1 > 0 %} true {% else %} false {% endif %} ' text = ' {% if 1 > 0 %} true {% else %} false {% endif %} '
assert_template_result ' true ', text assert_template_result(' true ', text)
end end
def test_zero_lq_one def test_zero_lq_one
text = ' {% if 0 < 1 %} true {% else %} false {% endif %} ' text = ' {% if 0 < 1 %} true {% else %} false {% endif %} '
assert_template_result ' true ', text assert_template_result(' true ', text)
end end
def test_zero_lq_or_equal_one def test_zero_lq_or_equal_one
text = ' {% if 0 <= 0 %} true {% else %} false {% endif %} ' text = ' {% if 0 <= 0 %} true {% else %} false {% endif %} '
assert_template_result ' true ', text assert_template_result(' true ', text)
end end
def test_zero_lq_or_equal_one_involving_nil def test_zero_lq_or_equal_one_involving_nil
text = ' {% if null <= 0 %} true {% else %} false {% endif %} ' text = ' {% if null <= 0 %} true {% else %} false {% endif %} '
assert_template_result ' false ', text assert_template_result(' false ', text)
text = ' {% if 0 <= null %} true {% else %} false {% endif %} ' text = ' {% if 0 <= null %} true {% else %} false {% endif %} '
assert_template_result ' false ', text assert_template_result(' false ', text)
end end
def test_zero_lqq_or_equal_one def test_zero_lqq_or_equal_one
text = ' {% if 0 >= 0 %} true {% else %} false {% endif %} ' text = ' {% if 0 >= 0 %} true {% else %} false {% endif %} '
assert_template_result ' true ', text assert_template_result(' true ', text)
end end
def test_strings def test_strings
text = " {% if 'test' == 'test' %} true {% else %} false {% endif %} " text = " {% if 'test' == 'test' %} true {% else %} false {% endif %} "
assert_template_result ' true ', text assert_template_result(' true ', text)
end end
def test_strings_not_equal def test_strings_not_equal
text = " {% if 'test' != 'test' %} true {% else %} false {% endif %} " text = " {% if 'test' != 'test' %} true {% else %} false {% endif %} "
assert_template_result ' false ', text assert_template_result(' false ', text)
end end
def test_var_strings_equal def test_var_strings_equal
text = ' {% if var == "hello there!" %} true {% else %} false {% endif %} ' text = ' {% if var == "hello there!" %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => 'hello there!' assert_template_result(' true ', text, 'var' => 'hello there!')
end end
def test_var_strings_are_not_equal def test_var_strings_are_not_equal
text = ' {% if "hello there!" == var %} true {% else %} false {% endif %} ' text = ' {% if "hello there!" == var %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => 'hello there!' assert_template_result(' true ', text, 'var' => 'hello there!')
end end
def test_var_and_long_string_are_equal def test_var_and_long_string_are_equal
text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} " text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} "
assert_template_result ' true ', text, 'var' => 'hello there!' assert_template_result(' true ', text, 'var' => 'hello there!')
end end
def test_var_and_long_string_are_equal_backwards def test_var_and_long_string_are_equal_backwards
text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} " text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} "
assert_template_result ' true ', text, 'var' => 'hello there!' assert_template_result(' true ', text, 'var' => 'hello there!')
end end
# def test_is_nil # def test_is_nil
@@ -87,27 +87,27 @@ class StatementsTest < Minitest::Test
def test_is_collection_empty def test_is_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} ' text = ' {% if array == empty %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'array' => [] assert_template_result(' true ', text, 'array' => [])
end end
def test_is_not_collection_empty def test_is_not_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} ' text = ' {% if array == empty %} true {% else %} false {% endif %} '
assert_template_result ' false ', text, 'array' => [1, 2, 3] assert_template_result(' false ', text, 'array' => [1, 2, 3])
end end
def test_nil def test_nil
text = ' {% if var == nil %} true {% else %} false {% endif %} ' text = ' {% if var == nil %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => nil assert_template_result(' true ', text, 'var' => nil)
text = ' {% if var == null %} true {% else %} false {% endif %} ' text = ' {% if var == null %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => nil assert_template_result(' true ', text, 'var' => nil)
end end
def test_not_nil def test_not_nil
text = ' {% if var != nil %} true {% else %} false {% endif %} ' text = ' {% if var != nil %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => 1 assert_template_result(' true ', text, 'var' => 1)
text = ' {% if var != null %} true {% else %} false {% endif %} ' text = ' {% if var != null %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => 1 assert_template_result(' true ', text, 'var' => 1)
end end
end # StatementsTest end # StatementsTest

View File

@@ -19,48 +19,48 @@ class TableRowTest < Minitest::Test
def test_table_row def test_table_row
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
'numbers' => [1, 2, 3, 4, 5, 6]) 'numbers' => [1, 2, 3, 4, 5, 6])
assert_template_result("<tr class=\"row1\">\n</tr>\n", assert_template_result("<tr class=\"row1\">\n</tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
'numbers' => []) 'numbers' => [])
end end
def test_table_row_with_different_cols def test_table_row_with_different_cols
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}',
'numbers' => [1, 2, 3, 4, 5, 6]) 'numbers' => [1, 2, 3, 4, 5, 6])
end end
def test_table_col_counter def test_table_col_counter
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n",
'{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}', '{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}',
'numbers' => [1, 2, 3, 4, 5, 6]) 'numbers' => [1, 2, 3, 4, 5, 6])
end end
def test_quoted_fragment def test_quoted_fragment
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}', "{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}",
'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] }) 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] })
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
"{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}", "{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}",
'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] }) 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] })
end end
def test_enumerable_drop def test_enumerable_drop
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
'numbers' => ArrayDrop.new([1, 2, 3, 4, 5, 6])) 'numbers' => ArrayDrop.new([1, 2, 3, 4, 5, 6]))
end end
def test_offset_and_limit def test_offset_and_limit
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}',
'numbers' => [0, 1, 2, 3, 4, 5, 6, 7]) 'numbers' => [0, 1, 2, 3, 4, 5, 6, 7])
end end
def test_blank_string_not_iterable def test_blank_string_not_iterable
assert_template_result("<tr class=\"row1\">\n</tr>\n", '{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}', 'characters' => '') assert_template_result("<tr class=\"row1\">\n</tr>\n", "{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}", 'characters' => '')
end end
end end

View File

@@ -8,7 +8,7 @@ class UnlessElseTagTest < Minitest::Test
def test_unless def test_unless
assert_template_result(' ', ' {% unless true %} this text should not go into the output {% endunless %} ') assert_template_result(' ', ' {% unless true %} this text should not go into the output {% endunless %} ')
assert_template_result(' this text should go into the output ', assert_template_result(' this text should go into the output ',
' {% unless false %} this text should go into the output {% endunless %} ') ' {% unless false %} this text should go into the output {% endunless %} ')
assert_template_result(' you rock ?', '{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?') assert_template_result(' you rock ?', '{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?')
end end
@@ -19,10 +19,10 @@ class UnlessElseTagTest < Minitest::Test
end end
def test_unless_in_loop def test_unless_in_loop
assert_template_result '23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', 'choices' => [1, nil, false] assert_template_result('23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', 'choices' => [1, nil, false])
end end
def test_unless_else_in_loop def test_unless_else_in_loop
assert_template_result ' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', 'choices' => [1, nil, false] assert_template_result(' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', 'choices' => [1, nil, false])
end end
end # UnlessElseTest end # UnlessElseTest

View File

@@ -40,195 +40,195 @@ class TemplateTest < Minitest::Test
def test_instance_assigns_persist_on_same_template_object_between_parses def test_instance_assigns_persist_on_same_template_object_between_parses
t = Template.new t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render! assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
assert_equal 'from instance assigns', t.parse('{{ foo }}').render! assert_equal('from instance assigns', t.parse("{{ foo }}").render!)
end end
def test_warnings_is_not_exponential_time def test_warnings_is_not_exponential_time
str = 'false' str = "false"
100.times do 100.times do
str = "{% if true %}true{% else %}#{str}{% endif %}" str = "{% if true %}true{% else %}#{str}{% endif %}"
end end
t = Template.parse(str) t = Template.parse(str)
assert_equal [], Timeout.timeout(1) { t.warnings } assert_equal([], Timeout.timeout(1) { t.warnings })
end end
def test_instance_assigns_persist_on_same_template_parsing_between_renders def test_instance_assigns_persist_on_same_template_parsing_between_renders
t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}") t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}")
assert_equal 'foo', t.render! assert_equal('foo', t.render!)
assert_equal 'foofoo', t.render! assert_equal('foofoo', t.render!)
end end
def test_custom_assigns_do_not_persist_on_same_template def test_custom_assigns_do_not_persist_on_same_template
t = Template.new t = Template.new
assert_equal 'from custom assigns', t.parse('{{ foo }}').render!('foo' => 'from custom assigns') assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns'))
assert_equal '', t.parse('{{ foo }}').render! assert_equal('', t.parse("{{ foo }}").render!)
end end
def test_custom_assigns_squash_instance_assigns def test_custom_assigns_squash_instance_assigns
t = Template.new t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render! assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
assert_equal 'from custom assigns', t.parse('{{ foo }}').render!('foo' => 'from custom assigns') assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns'))
end end
def test_persistent_assigns_squash_instance_assigns def test_persistent_assigns_squash_instance_assigns
t = Template.new t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render! assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
t.assigns['foo'] = 'from persistent assigns' t.assigns['foo'] = 'from persistent assigns'
assert_equal 'from persistent assigns', t.parse('{{ foo }}').render! assert_equal('from persistent assigns', t.parse("{{ foo }}").render!)
end end
def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders
t = Template.new t = Template.new
t.assigns['number'] = lambda { t.assigns['number'] = -> {
@global ||= 0 @global ||= 0
@global += 1 @global += 1
} }
assert_equal '1', t.parse('{{number}}').render! assert_equal('1', t.parse("{{number}}").render!)
assert_equal '1', t.parse('{{number}}').render! assert_equal('1', t.parse("{{number}}").render!)
assert_equal '1', t.render! assert_equal('1', t.render!)
@global = nil @global = nil
end end
def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders
t = Template.new t = Template.new
assigns = { 'number' => lambda { assigns = { 'number' => -> {
@global ||= 0 @global ||= 0
@global += 1 @global += 1
} } } }
assert_equal '1', t.parse('{{number}}').render!(assigns) assert_equal('1', t.parse("{{number}}").render!(assigns))
assert_equal '1', t.parse('{{number}}').render!(assigns) assert_equal('1', t.parse("{{number}}").render!(assigns))
assert_equal '1', t.render!(assigns) assert_equal('1', t.render!(assigns))
@global = nil @global = nil
end end
def test_resource_limits_works_with_custom_length_method def test_resource_limits_works_with_custom_length_method
t = Template.parse('{% assign foo = bar %}') t = Template.parse("{% assign foo = bar %}")
t.resource_limits.render_length_limit = 42 t.resource_limits.render_length_limit = 42
assert_equal '', t.render!('bar' => SomethingWithLength.new) assert_equal("", t.render!("bar" => SomethingWithLength.new))
end end
def test_resource_limits_render_length def test_resource_limits_render_length
t = Template.parse('0123456789') t = Template.parse("0123456789")
t.resource_limits.render_length_limit = 5 t.resource_limits.render_length_limit = 5
assert_equal 'Liquid error: Memory limits exceeded', t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
assert t.resource_limits.reached? assert(t.resource_limits.reached?)
t.resource_limits.render_length_limit = 10 t.resource_limits.render_length_limit = 10
assert_equal '0123456789', t.render! assert_equal("0123456789", t.render!)
refute_nil t.resource_limits.render_length refute_nil(t.resource_limits.render_length)
end end
def test_resource_limits_render_score def test_resource_limits_render_score
t = Template.parse('{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}') t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}")
t.resource_limits.render_score_limit = 50 t.resource_limits.render_score_limit = 50
assert_equal 'Liquid error: Memory limits exceeded', t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
assert t.resource_limits.reached? assert(t.resource_limits.reached?)
t = Template.parse('{% for a in (1..100) %} foo {% endfor %}') t = Template.parse("{% for a in (1..100) %} foo {% endfor %}")
t.resource_limits.render_score_limit = 50 t.resource_limits.render_score_limit = 50
assert_equal 'Liquid error: Memory limits exceeded', t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
assert t.resource_limits.reached? assert(t.resource_limits.reached?)
t.resource_limits.render_score_limit = 200 t.resource_limits.render_score_limit = 200
assert_equal (' foo ' * 100), t.render! assert_equal((" foo " * 100), t.render!)
refute_nil t.resource_limits.render_score refute_nil(t.resource_limits.render_score)
end end
def test_resource_limits_assign_score def test_resource_limits_assign_score
t = Template.parse('{% assign foo = 42 %}{% assign bar = 23 %}') t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
t.resource_limits.assign_score_limit = 1 t.resource_limits.assign_score_limit = 1
assert_equal 'Liquid error: Memory limits exceeded', t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
assert t.resource_limits.reached? assert(t.resource_limits.reached?)
t.resource_limits.assign_score_limit = 2 t.resource_limits.assign_score_limit = 2
assert_equal '', t.render! assert_equal("", t.render!)
refute_nil t.resource_limits.assign_score refute_nil(t.resource_limits.assign_score)
end end
def test_resource_limits_assign_score_counts_bytes_not_characters def test_resource_limits_assign_score_counts_bytes_not_characters
t = Template.parse("{% assign foo = 'すごい' %}") t = Template.parse("{% assign foo = 'すごい' %}")
t.render t.render
assert_equal 9, t.resource_limits.assign_score assert_equal(9, t.resource_limits.assign_score)
t = Template.parse('{% capture foo %}すごい{% endcapture %}') t = Template.parse("{% capture foo %}すごい{% endcapture %}")
t.render t.render
assert_equal 9, t.resource_limits.assign_score assert_equal(9, t.resource_limits.assign_score)
end end
def test_resource_limits_assign_score_nested def test_resource_limits_assign_score_nested
t = Template.parse("{% assign foo = 'aaaa' | reverse %}") t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
t.resource_limits.assign_score_limit = 3 t.resource_limits.assign_score_limit = 3
assert_equal 'Liquid error: Memory limits exceeded', t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
assert t.resource_limits.reached? assert(t.resource_limits.reached?)
t.resource_limits.assign_score_limit = 5 t.resource_limits.assign_score_limit = 5
assert_equal '', t.render! assert_equal("", t.render!)
end end
def test_resource_limits_aborts_rendering_after_first_error def test_resource_limits_aborts_rendering_after_first_error
t = Template.parse('{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}') t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
t.resource_limits.render_score_limit = 50 t.resource_limits.render_score_limit = 50
assert_equal 'Liquid error: Memory limits exceeded', t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
assert t.resource_limits.reached? assert(t.resource_limits.reached?)
end end
def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set
t = Template.parse('{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}') t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
t.render! t.render!
assert t.resource_limits.assign_score.positive? assert(t.resource_limits.assign_score > 0)
assert t.resource_limits.render_score.positive? assert(t.resource_limits.render_score > 0)
assert t.resource_limits.render_length.positive? assert(t.resource_limits.render_length > 0)
end end
def test_render_length_persists_between_blocks def test_render_length_persists_between_blocks
t = Template.parse('{% if true %}aaaa{% endif %}') t = Template.parse("{% if true %}aaaa{% endif %}")
t.resource_limits.render_length_limit = 7 t.resource_limits.render_length_limit = 7
assert_equal 'Liquid error: Memory limits exceeded', t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 8 t.resource_limits.render_length_limit = 8
assert_equal 'aaaa', t.render assert_equal("aaaa", t.render)
t = Template.parse('{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}') t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}")
t.resource_limits.render_length_limit = 13 t.resource_limits.render_length_limit = 13
assert_equal 'Liquid error: Memory limits exceeded', t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 14 t.resource_limits.render_length_limit = 14
assert_equal 'aaaabbb', t.render assert_equal("aaaabbb", t.render)
t = Template.parse('{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}') t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}")
t.resource_limits.render_length_limit = 5 t.resource_limits.render_length_limit = 5
assert_equal 'Liquid error: Memory limits exceeded', t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 11 t.resource_limits.render_length_limit = 11
assert_equal 'Liquid error: Memory limits exceeded', t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 12 t.resource_limits.render_length_limit = 12
assert_equal 'ababab', t.render assert_equal("ababab", t.render)
end end
def test_render_length_uses_number_of_bytes_not_characters def test_render_length_uses_number_of_bytes_not_characters
t = Template.parse('{% if true %}すごい{% endif %}') t = Template.parse("{% if true %}すごい{% endif %}")
t.resource_limits.render_length_limit = 10 t.resource_limits.render_length_limit = 10
assert_equal 'Liquid error: Memory limits exceeded', t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 18 t.resource_limits.render_length_limit = 18
assert_equal 'すごい', t.render assert_equal("すごい", t.render)
end end
def test_default_resource_limits_unaffected_by_render_with_context def test_default_resource_limits_unaffected_by_render_with_context
context = Context.new context = Context.new
t = Template.parse('{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}') t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
t.render!(context) t.render!(context)
assert context.resource_limits.assign_score.positive? assert(context.resource_limits.assign_score > 0)
assert context.resource_limits.render_score.positive? assert(context.resource_limits.render_score > 0)
assert context.resource_limits.render_length.positive? assert(context.resource_limits.render_length > 0)
end end
def test_can_use_drop_as_context def test_can_use_drop_as_context
t = Template.new t = Template.new
t.registers['lulz'] = 'haha' t.registers['lulz'] = 'haha'
drop = TemplateContextDrop.new drop = TemplateContextDrop.new
assert_equal 'fizzbuzz', t.parse('{{foo}}').render!(drop) assert_equal('fizzbuzz', t.parse('{{foo}}').render!(drop))
assert_equal 'bar', t.parse('{{bar}}').render!(drop) assert_equal('bar', t.parse('{{bar}}').render!(drop))
assert_equal 'haha', t.parse('{{baz}}').render!(drop) assert_equal('haha', t.parse("{{baz}}").render!(drop))
end end
def test_render_bang_force_rethrow_errors_on_passed_context def test_render_bang_force_rethrow_errors_on_passed_context
@@ -238,72 +238,72 @@ class TemplateTest < Minitest::Test
e = assert_raises RuntimeError do e = assert_raises RuntimeError do
t.render!(context) t.render!(context)
end end
assert_equal 'ruby error in drop', e.message assert_equal('ruby error in drop', e.message)
end end
def test_exception_renderer_that_returns_string def test_exception_renderer_that_returns_string
exception = nil exception = nil
handler = lambda { |e| handler = ->(e) {
exception = e exception = e
'<!-- error -->' '<!-- error -->'
} }
output = Template.parse('{{ 1 | divided_by: 0 }}').render({}, exception_renderer: handler) output = Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: handler)
assert exception.is_a?(Liquid::ZeroDivisionError) assert(exception.is_a?(Liquid::ZeroDivisionError))
assert_equal '<!-- error -->', output assert_equal('<!-- error -->', output)
end end
def test_exception_renderer_that_raises def test_exception_renderer_that_raises
exception = nil exception = nil
assert_raises(Liquid::ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
Template.parse('{{ 1 | divided_by: 0 }}').render({}, exception_renderer: lambda { |e| Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) {
exception = e exception = e
raise raise
}) })
end end
assert exception.is_a?(Liquid::ZeroDivisionError) assert(exception.is_a?(Liquid::ZeroDivisionError))
end end
def test_global_filter_option_on_render def test_global_filter_option_on_render
global_filter_proc = ->(output) { "#{output} filtered" } global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse('{{name}}').render({ 'name' => 'bob' }, global_filter: global_filter_proc) rendered_template = Template.parse("{{name}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal 'bob filtered', rendered_template assert_equal('bob filtered', rendered_template)
end end
def test_global_filter_option_when_native_filters_exist def test_global_filter_option_when_native_filters_exist
global_filter_proc = ->(output) { "#{output} filtered" } global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse('{{name | upcase}}').render({ 'name' => 'bob' }, global_filter: global_filter_proc) rendered_template = Template.parse("{{name | upcase}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal 'BOB filtered', rendered_template assert_equal('BOB filtered', rendered_template)
end end
def test_undefined_variables def test_undefined_variables
t = Template.parse('{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}') t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true) result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true)
assert_equal '33 32 ', result assert_equal('33 32 ', result)
assert_equal 3, t.errors.count assert_equal(3, t.errors.count)
assert_instance_of Liquid::UndefinedVariable, t.errors[0] assert_instance_of(Liquid::UndefinedVariable, t.errors[0])
assert_equal 'Liquid error: undefined variable y', t.errors[0].message assert_equal('Liquid error: undefined variable y', t.errors[0].message)
assert_instance_of Liquid::UndefinedVariable, t.errors[1] assert_instance_of(Liquid::UndefinedVariable, t.errors[1])
assert_equal 'Liquid error: undefined variable b', t.errors[1].message assert_equal('Liquid error: undefined variable b', t.errors[1].message)
assert_instance_of Liquid::UndefinedVariable, t.errors[2] assert_instance_of(Liquid::UndefinedVariable, t.errors[2])
assert_equal 'Liquid error: undefined variable d', t.errors[2].message assert_equal('Liquid error: undefined variable d', t.errors[2].message)
end end
def test_nil_value_does_not_raise def test_nil_value_does_not_raise
Liquid::Template.error_mode = :strict Liquid::Template.error_mode = :strict
t = Template.parse('some{{x}}thing') t = Template.parse("some{{x}}thing")
result = t.render!({ 'x' => nil }, strict_variables: true) result = t.render!({ 'x' => nil }, strict_variables: true)
assert_equal 0, t.errors.count assert_equal(0, t.errors.count)
assert_equal 'something', result assert_equal('something', result)
end end
def test_undefined_variables_raise def test_undefined_variables_raise
t = Template.parse('{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}') t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
assert_raises UndefinedVariable do assert_raises UndefinedVariable do
t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true) t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true)
@@ -315,9 +315,9 @@ class TemplateTest < Minitest::Test
t = Template.new.parse('{{ foo }} {{ woot }}') t = Template.new.parse('{{ foo }} {{ woot }}')
result = t.render(d, strict_variables: true) result = t.render(d, strict_variables: true)
assert_equal 'foo ', result assert_equal('foo ', result)
assert_equal 1, t.errors.count assert_equal(1, t.errors.count)
assert_instance_of Liquid::UndefinedDropMethod, t.errors[0] assert_instance_of(Liquid::UndefinedDropMethod, t.errors[0])
end end
def test_undefined_drop_methods_raise def test_undefined_drop_methods_raise
@@ -330,7 +330,7 @@ class TemplateTest < Minitest::Test
end end
def test_undefined_filters def test_undefined_filters
t = Template.parse('{{a}} {{x | upcase | somefilter1 | somefilter2 | somefilter3}}') t = Template.parse("{{a}} {{x | upcase | somefilter1 | somefilter2 | somefilter3}}")
filters = Module.new do filters = Module.new do
def somefilter3(v) def somefilter3(v)
"-#{v}-" "-#{v}-"
@@ -338,14 +338,14 @@ class TemplateTest < Minitest::Test
end end
result = t.render({ 'a' => 123, 'x' => 'foo' }, filters: [filters], strict_filters: true) result = t.render({ 'a' => 123, 'x' => 'foo' }, filters: [filters], strict_filters: true)
assert_equal '123 ', result assert_equal('123 ', result)
assert_equal 1, t.errors.count assert_equal(1, t.errors.count)
assert_instance_of Liquid::UndefinedFilter, t.errors[0] assert_instance_of(Liquid::UndefinedFilter, t.errors[0])
assert_equal 'Liquid error: undefined filter somefilter1', t.errors[0].message assert_equal('Liquid error: undefined filter somefilter1', t.errors[0].message)
end end
def test_undefined_filters_raise def test_undefined_filters_raise
t = Template.parse('{{x | somefilter1 | upcase | somefilter2}}') t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
assert_raises UndefinedFilter do assert_raises UndefinedFilter do
t.render!({ 'x' => 'foo' }, strict_filters: true) t.render!({ 'x' => 'foo' }, strict_filters: true)
@@ -353,12 +353,12 @@ class TemplateTest < Minitest::Test
end end
def test_using_range_literal_works_as_expected def test_using_range_literal_works_as_expected
t = Template.parse('{% assign foo = (x..y) %}{{ foo }}') t = Template.parse("{% assign foo = (x..y) %}{{ foo }}")
result = t.render('x' => 1, 'y' => 5) result = t.render('x' => 1, 'y' => 5)
assert_equal '1..5', result assert_equal('1..5', result)
t = Template.parse('{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}') t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}")
result = t.render('x' => 1, 'y' => 5) result = t.render('x' => 1, 'y' => 5)
assert_equal '12345', result assert_equal('12345', result)
end end
end end

Some files were not shown because too many files have changed in this diff Show More