Compare commits

..

16 Commits

Author SHA1 Message Date
Mike Angell
2d0917493d Add performance rubocop and fixes 2019-08-27 22:07:44 +10:00
Mike Angell
ef8a6dcda1 Add option legacy file which will load removed constants 2019-08-27 17:46:09 +10:00
Mike Angell
0dd22af7d6 Use optimised 2.4 syntax 2019-08-27 17:46:09 +10:00
Mike Angell
da9051eb18 Upgrade rubocop 2019-08-27 17:46:09 +10:00
Justin Li
831355dfbd Merge pull request #1117 from ashmaroli/reduce-allocations-template-lookup-class
Reduce allocations while registering Liquid tags
2019-08-07 16:37:39 -04:00
Ashwin Maroli
00702d8e63 Use Object.const_get directly 2019-08-07 11:44:53 +05:30
Justin Li
197c058208 Merge pull request #1099 from ashmaroli/stash-types-private-constant
Use a private constant to stash token-types
2019-08-06 17:56:56 -04:00
Justin Li
98dfe198e1 Merge pull request #1115 from ashmaroli/reduce-allocations-from-truncate-filters
Reduce string allocations from truncate filters
2019-08-06 17:48:43 -04:00
Ashwin Maroli
c2c1497ca8 Reduce allocations while registering Liquid tags 2019-07-22 20:42:37 +05:30
Ashwin Maroli
d19967a79d Reduce string allocations from truncate filters 2019-07-22 17:35:45 +05:30
Florian Weingarten
248c54a386 Merge pull request #1091 from Shopify/rendering-with-less-garbage
Rendering with less garbage
2019-07-19 15:53:22 +01:00
Ashwin Maroli
2c42447659 Rename constant to SINGLE_TOKEN_EXPRESSION_TYPES 2019-05-17 23:30:24 +05:30
Ashwin Maroli
9ef6f9b642 Freeze mutable object assigned to constant 2019-04-29 23:50:49 +05:30
Ashwin Maroli
4684478e94 Use a private constant to stash token-types 2019-04-29 23:45:45 +05:30
Florian Weingarten
9640e77805 render_to_output_buffer 2019-04-23 17:06:29 -04:00
Florian Weingarten
2a1ca3152d liquid without the garbage 2019-04-22 16:34:31 -04:00
123 changed files with 3194 additions and 2050 deletions

2
.gitignore vendored
View File

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

1027
.rubocop.shopify.yml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,132 +1,10 @@
inherit_from: inherit_from:
- .rubocop_todo.yml - .rubocop.shopify.yml
- ./.rubocop_todo.yml
AllCops: require: rubocop-performance
Exclude:
- 'performance/shopify/*'
- 'pkg/**'
Metrics/BlockNesting: Performance:
Max: 3 Enabled: true
Metrics/ModuleLength: Metrics/LineLength:
Enabled: false Enabled: false
Metrics/ClassLength:
Enabled: false
Lint/AssignmentInCondition:
Enabled: false
Lint/AmbiguousOperator:
Enabled: false
Lint/AmbiguousRegexpLiteral:
Enabled: false
Lint/ParenthesesAsGroupedExpression:
Enabled: false
Lint/UnusedBlockArgument:
Enabled: false
Layout/EndAlignment:
EnforcedStyleAlignWith: variable
Lint/UnusedMethodArgument:
Enabled: false
Style/SingleLineBlockParams:
Enabled: false
Style/DoubleNegation:
Enabled: false
Style/StringLiteralsInInterpolation:
Enabled: false
Style/AndOr:
Enabled: false
Style/SignalException:
Enabled: false
Style/StringLiterals:
Enabled: false
Style/BracesAroundHashParameters:
Enabled: false
Style/NumericLiterals:
Enabled: false
Layout/SpaceInsideArrayLiteralBrackets:
Enabled: false
Layout/SpaceBeforeBlockBraces:
Enabled: false
Style/Documentation:
Enabled: false
Style/ClassAndModuleChildren:
Enabled: false
Style/TrailingCommaInArrayLiteral:
Enabled: false
Style/TrailingCommaInHashLiteral:
Enabled: false
Layout/IndentHash:
EnforcedStyle: consistent
Style/FormatString:
Enabled: false
Layout/AlignParameters:
EnforcedStyle: with_fixed_indentation
Layout/MultilineOperationIndentation:
EnforcedStyle: indented
Style/IfUnlessModifier:
Enabled: false
Style/RaiseArgs:
Enabled: false
Style/PreferredHashMethods:
Enabled: false
Style/RegexpLiteral:
Enabled: false
Style/SymbolLiteral:
Enabled: false
Performance/Count:
Enabled: false
Naming/ConstantName:
Enabled: false
Layout/CaseIndentation:
Enabled: false
Style/ClassVars:
Enabled: false
Style/PerlBackrefs:
Enabled: false
Style/TrivialAccessors:
AllowPredicates: true
Style/WordArray:
Enabled: false
Naming/MethodName:
Exclude:
- 'example/server/liquid_servlet.rb'

View File

@@ -1,260 +0,0 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2019-03-19 11:04:37 -0400 using RuboCop version 0.53.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: 1
# Cop supports --auto-correct.
# Configuration parameters: Include, TreatCommentsAsGroupSeparators.
# Include: **/*.gemspec
Gemspec/OrderedDependencies:
Exclude:
- 'liquid.gemspec'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent
Layout/IndentHeredoc:
Exclude:
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/trim_mode_test.rb'
# Offense count: 6
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: symmetrical, new_line, same_line
Layout/MultilineMethodCallBraceLayout:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/unit/strainer_unit_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: runtime_error, standard_error
Lint/InheritException:
Exclude:
- 'lib/liquid/interrupts.rb'
# Offense count: 1
# Configuration parameters: CheckForMethodsWithNoSideEffects.
Lint/Void:
Exclude:
- 'lib/liquid/parse_context.rb'
# Offense count: 54
Metrics/AbcSize:
Max: 56
# Offense count: 12
Metrics/CyclomaticComplexity:
Max: 12
# Offense count: 112
# Configuration parameters: CountComments.
Metrics/MethodLength:
Max: 37
# Offense count: 8
Metrics/PerceivedComplexity:
Max: 11
# Offense count: 52
# Configuration parameters: Blacklist.
# Blacklist: END, (?-mix:EO[A-Z]{1})
Naming/HeredocDelimiterNaming:
Exclude:
- 'test/integration/assign_test.rb'
- 'test/integration/capture_test.rb'
- 'test/integration/trim_mode_test.rb'
# Offense count: 23
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
# AllowedNames: io, id
Naming/UncommunicativeMethodParamName:
Exclude:
- 'example/server/example_servlet.rb'
- 'lib/liquid/condition.rb'
- 'lib/liquid/context.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/utils.rb'
- 'lib/liquid/variable.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/condition_unit_test.rb'
# Offense count: 10
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: prefer_alias, prefer_alias_method
Style/Alias:
Exclude:
- 'lib/liquid/drop.rb'
- 'lib/liquid/i18n.rb'
- 'lib/liquid/profiler/hooks.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/variable.rb'
# Offense count: 22
Style/CommentedKeyword:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Exclude:
- 'lib/liquid/errors.rb'
# Offense count: 1
Style/DateTime:
Exclude:
- 'test/unit/context_unit_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/EmptyCaseCondition:
Exclude:
- 'lib/liquid/block_body.rb'
- 'lib/liquid/lexer.rb'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: compact, expanded
Style/EmptyMethod:
Exclude:
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/comment.rb'
- 'lib/liquid/tags/include.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 3
# Cop supports --auto-correct.
Style/Encoding:
Exclude:
- 'lib/liquid/version.rb'
- 'liquid.gemspec'
- 'test/integration/standard_filter_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/ExpandPathArguments:
Exclude:
- 'Rakefile'
- 'liquid.gemspec'
# Offense count: 7
# Configuration parameters: EnforcedStyle.
# SupportedStyles: annotated, template, unannotated
Style/FormatStringToken:
Exclude:
- 'test/integration/filter_test.rb'
- 'test/integration/hash_ordering_test.rb'
# Offense count: 14
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/tags/assign.rb'
- 'lib/liquid/tags/capture.rb'
- 'lib/liquid/tags/case.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb'
- 'test/unit/tokenizer_unit_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength.
# SupportedStyles: skip_modifier_ifs, always
Style/Next:
Exclude:
- 'lib/liquid/tags/for.rb'
# Offense count: 4
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Exclude:
- 'spec/**/*'
- 'lib/liquid/context.rb'
- 'lib/liquid/forloop_drop.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tablerowloop_drop.rb'
# Offense count: 14
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Exclude:
- 'lib/liquid/tags/if.rb'
- 'liquid.gemspec'
- 'test/integration/assign_test.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
Style/RedundantSelf:
Exclude:
- 'lib/liquid/strainer.rb'
# Offense count: 9
# Cop supports --auto-correct.
# Configuration parameters: AllowAsExpressionSeparator.
Style/Semicolon:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: MinSize.
# SupportedStyles: percent, brackets
Style/SymbolArray:
EnforcedStyle: brackets
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
Style/TernaryParentheses:
Exclude:
- 'lib/liquid/context.rb'
- 'lib/liquid/utils.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/UnneededPercentQ:
Exclude:
- 'test/integration/error_handling_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
Style/WhileUntilModifier:
Exclude:
- 'lib/liquid/tags/case.rb'
# Offense count: 640
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 294

View File

@@ -1,14 +1,13 @@
language: ruby language: ruby
rvm: rvm:
- 2.1
- 2.2
- 2.3
- 2.4 - 2.4
- 2.5 - 2.5
- 2.6
- 2.7
- ruby-head - ruby-head
- jruby-head - jruby-head
# - rbx-2 - truffleruby
sudo: false sudo: false

11
Gemfile
View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
source 'https://rubygems.org' source 'https://rubygems.org'
git_source(:github) do |repo_name| git_source(:github) do |repo_name|
"https://github.com/#{repo_name}.git" "https://github.com/#{repo_name}.git"
@@ -9,15 +11,16 @@ group :benchmark, :test do
gem 'benchmark-ips' gem 'benchmark-ips'
gem 'memory_profiler' gem 'memory_profiler'
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ } do install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
gem 'stackprof' gem 'stackprof'
end end
end end
group :test do group :test do
gem 'rubocop', '~> 0.53.0' gem 'rubocop', '~> 0.74.0', require: false
gem 'rubocop-performance', require: false
platform :mri do platform :mri, :truffleruby do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '9168659de45d6d576fce30c735f857e597fa26f6' gem 'liquid-c', github: 'Shopify/liquid-c', ref: '7ba926791ef8411984d0f3e41c6353fd716041c6'
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::ArgumentSeparator: , 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::ARGUMENT_SEPARATOR: , 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

@@ -1,29 +1,33 @@
# frozen_string_literal: true
require 'rake' require 'rake'
require 'rake/testtask' require 'rake/testtask'
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(File.expand_path('lib', __dir__))
require "liquid/version" require 'liquid/version'
task default: [:test, :rubocop] task(default: %i[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|
t.libs << '.' << 'lib' << 'test' t.libs << '.' << 'lib' << 'test'
t.test_files = FileList['test/{integration,unit}/**/*_test.rb'] t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
t.verbose = false t.verbose = false
end end
desc 'run test suite with warn error mode' desc('run test suite with warn error mode')
task :warn_test do task :warn_test do
ENV['LIQUID_PARSER_MODE'] = 'warn' ENV['LIQUID_PARSER_MODE'] = 'warn'
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
end end
task :rubocop do task :rubocop do
if RUBY_ENGINE == 'ruby'
require 'rubocop/rake_task' require 'rubocop/rake_task'
RuboCop::RakeTask.new RuboCop::RakeTask.new
end end
end
desc 'runs test suite with both strict and lax parsers' desc('runs test suite with both strict and lax parsers')
task :test do task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax' ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
@@ -32,8 +36,9 @@ task :test do
Rake::Task['base_test'].reenable Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
if RUBY_ENGINE == 'ruby' 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'
Rake::Task['base_test'].reenable Rake::Task['base_test'].reenable
@@ -45,9 +50,9 @@ task :test do
end end
end 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
@@ -56,45 +61,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

@@ -1,6 +1,8 @@
# frozen_string_literal: true
module ProductsFilter module ProductsFilter
def price(integer) def price(integer)
sprintf("$%.2d USD", integer / 100.0) format('$%.2d USD', integer / 100.0)
end end
def prettyprint(text) def prettyprint(text)
@@ -28,17 +30,17 @@ class Servlet < LiquidServlet
private private
def products_list def products_list
[{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' }, [{ 'name' => 'Arbor Draft', 'price' => 39_900, 'description' => 'the *arbor draft* is a excellent product' },
{ 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' }, { 'name' => 'Arbor Element', 'price' => 40_000, 'description' => 'the *arbor element* rocks for freestyling' },
{ 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }] { 'name' => 'Arbor Diamond', 'price' => 59_900, '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' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' }, [{ 'name' => 'Arbor Catalyst', 'price' => 39_900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }] { 'name' => 'Arbor Fish', 'price' => 40_000, '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,23 +1,28 @@
# frozen_string_literal: true
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
def do_GET(req, res) def get(req, res)
handle(:get, req, res) handle(:get, req, res)
end end
def do_POST(req, res) def 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)
@request = req @request = req
@response = res @response = res
@request.path_info =~ /(\w+)\z/ @request.path_info =~ /(\w+)\z/
@action = $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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'webrick' require 'webrick'
require 'rexml/document' require 'rexml/document'
@@ -8,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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (c) 2005 Tobias Luetke # Copyright (c) 2005 Tobias Luetke
# #
# Permission is hereby granted, free of charge, to any person obtaining # Permission is hereby granted, free of charge, to any person obtaining
@@ -20,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
FilterSeparator = /\|/ FILTER_SEPARATOR = /\|/.freeze
ArgumentSeparator = ','.freeze ARGUMENT_SEPARATOR = ','
FilterArgumentSeparator = ':'.freeze FILTER_ARGUMENT_SEPARATOR = ':'
VariableAttributeSeparator = '.'.freeze VARIABLE_ATTRIBUTE_SEPARATOR = '.'
WhitespaceControl = '-'.freeze WHITESPACE_CONTROL = '-'
TagStart = /\{\%/ TAG_START = /\{\%/.freeze
TagEnd = /\%\}/ TAG_END = /\%\}/.freeze
VariableSignature = /\(?[\w\-\.\[\]]\)?/ VARIABLE_SIGNATURE = /\(?[\w\-\.\[\]]\)?/.freeze
VariableSegment = /[\w\-]/ VARIABLE_SEGMENT = /[\w\-]/.freeze
VariableStart = /\{\{/ VARIABLE_START = /\{\{/.freeze
VariableEnd = /\}\}/ VARIABLE_END = /\}\}/.freeze
VariableIncompleteEnd = /\}\}?/ VARIABLE_INCOMPLETE_END = /\}\}?/.freeze
QuotedString = /"[^"]*"|'[^']*'/ QUOTED_STRING = /"[^"]*"|'[^']*'/.freeze
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o QUOTED_FRAGMENT = /#{QUOTED_STRING}|(?:[^\s,\|'"]|#{QUOTED_STRING})+/o.freeze
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o TAG_ATTRIBUTES = /(\w+)\s*\:\s*(#{QUOTED_FRAGMENT})/o.freeze
AnyStartingTag = /#{TagStart}|#{VariableStart}/o ANY_STARTING_TAG = /#{TAG_START}|#{VARIABLE_START}/o.freeze
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om PARTIAL_TEMPLATE_PARSER = /#{TAG_START}.*?#{TAG_END}|#{VARIABLE_START}.*?#{VARIABLE_INCOMPLETE_END}/om.freeze
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om TEMPLATE_PARSER = /(#{PARTIAL_TEMPLATE_PARSER}|#{ANY_STARTING_TAG})/om.freeze
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o VARIABLE_PARSER = /\[[^\]]+\]|#{VARIABLE_SEGMENT}+\??/o.freeze
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'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Block < Tag class Block < Tag
MAX_DEPTH = 100 MAX_DEPTH = 100
@@ -13,6 +15,7 @@ module Liquid
end end
end end
# For backwards compatibility
def render(context) def render(context)
@body.render(context) @body.render(context)
end end
@@ -26,16 +29,16 @@ module Liquid
end end
def unknown_tag(tag, _params, _tokens) def unknown_tag(tag, _params, _tokens)
if tag == 'else'.freeze if tag == 'else'
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze, raise SyntaxError, parse_context.locale.t('errors.syntax.unexpected_else',
block_name: block_name)) block_name: block_name)
elsif tag.start_with?('end'.freeze) elsif tag.start_with?('end')
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze, 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.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag)) raise SyntaxError, parse_context.locale.t('errors.syntax.unknown_tag', tag: tag)
end end
end end
@@ -50,18 +53,15 @@ module Liquid
protected protected
def parse_body(body, tokens) def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH raise StackLevelError, 'Nesting too deep' if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep".freeze
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
unless end_tag_name raise SyntaxError, parse_context.locale.t('errors.syntax.tag_never_closed', block_name: block_name) unless end_tag_name
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, 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

@@ -1,10 +1,12 @@
# frozen_string_literal: true
module Liquid module Liquid
class BlockBody class BlockBody
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om FULL_TOKEN = /\A#{TAG_START}#{WHITESPACE_CONTROL}?\s*(\w+)\s*(.*?)#{WHITESPACE_CONTROL}?#{TAG_END}\z/om.freeze
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om CONTENT_OF_VARIABLE = /\A#{VARIABLE_START}#{WHITESPACE_CONTROL}?(.*?)#{WHITESPACE_CONTROL}?#{VARIABLE_END}\z/om.freeze
WhitespaceOrNothing = /\A\s*\z/ WHITESPACE_OR_NOTHING = /\A\s*\z/.freeze
TAGSTART = "{%".freeze TAG_START = '{%'
VARSTART = "{{".freeze VAR_START = '{{'
attr_reader :nodelist attr_reader :nodelist
@@ -15,36 +17,34 @@ module Liquid
def parse(tokenizer, parse_context) def parse(tokenizer, parse_context)
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
while token = tokenizer.shift while (token = tokenizer.shift)
next if token.empty? next if token.empty?
case case
when token.start_with?(TAGSTART) when token.start_with?(TAG_START)
whitespace_handler(token, parse_context) whitespace_handler(token, parse_context)
unless token =~ FullToken raise_missing_tag_terminator(token, parse_context) unless token =~ FULL_TOKEN
raise_missing_tag_terminator(token, parse_context) tag_name = Regexp.last_match(1)
end markup = Regexp.last_match(2)
tag_name = $1
markup = $2
# fetch the tag from registered blocks # 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?(VARSTART) when token.start_with?(VAR_START)
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
if parse_context.trim_whitespace token.lstrip! if parse_context.trim_whitespace
token.lstrip!
end
parse_context.trim_whitespace = false parse_context.trim_whitespace = false
@nodelist << token @nodelist << token
@blank &&= !!(token =~ WhitespaceOrNothing) @blank &&= token.match?(WHITESPACE_OR_NOTHING)
end end
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
end end
@@ -53,13 +53,11 @@ module Liquid
end end
def whitespace_handler(token, parse_context) def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl if token[2] == WHITESPACE_CONTROL
previous_token = @nodelist.last previous_token = @nodelist.last
if previous_token.is_a? String previous_token.rstrip! 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?
@@ -67,19 +65,23 @@ module Liquid
end end
def render(context) def render(context)
output = [] render_to_output_buffer(context, +'')
end
def render_to_output_buffer(context, output)
context.resource_limits.render_score += @nodelist.length context.resource_limits.render_score += @nodelist.length
idx = 0 idx = 0
while node = @nodelist[idx] while (node = @nodelist[idx])
previous_output_size = output.bytesize
case node case node
when String when String
check_resources(context, node)
output << node output << node
when Variable when Variable
render_node_to_output(node, output, context) render_node(context, output, node)
when Block when Block
render_node_to_output(node, output, context, node.blank?) render_node(context, node.blank? ? +'' : output, node)
break if context.interrupt? # might have happened in a for-block break if context.interrupt? # might have happened in a for-block
when Continue, Break when Continue, Break
# If we get an Interrupt that means the block must stop processing. An # If we get an Interrupt that means the block must stop processing. An
@@ -88,40 +90,37 @@ module Liquid
context.push_interrupt(node.interrupt) context.push_interrupt(node.interrupt)
break break
else # Other non-Block tags else # Other non-Block tags
render_node_to_output(node, output, context) render_node(context, output, node)
break if context.interrupt? # might have happened through an include break if context.interrupt? # might have happened through an include
end end
idx += 1 idx += 1
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
end end
output.join output
end end
private private
def render_node_to_output(node, output, context, skip_output = false) def render_node(context, output, node)
node_output = node.render(context) node.render_to_output_buffer(context, output)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
check_resources(context, node_output)
output << node_output unless skip_output
rescue MemoryError => e
raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number) context.handle_error(e, node.line_number)
output << nil
rescue ::StandardError => e rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number) output << context.handle_error(e, line_number)
end end
def check_resources(context, node_output) def raise_if_resource_limits_reached(context, length)
context.resource_limits.render_length += node_output.bytesize context.resource_limits.render_length += length
return unless context.resource_limits.reached? return unless context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
raise MemoryError, 'Memory limits exceeded'
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)
token.scan(ContentOfVariable) do |content| token.scan(CONTENT_OF_VARIABLE) do |content|
markup = content.first markup = content.first
return Variable.new(markup, parse_context) return Variable.new(markup, parse_context)
end end
@@ -129,11 +128,11 @@ module Liquid
end end
def raise_missing_tag_terminator(token, parse_context) def raise_missing_tag_terminator(token, parse_context)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect)) raise SyntaxError, parse_context.locale.t('errors.syntax.tag_termination', token: token, tag_end: TAG_END.inspect)
end end
def raise_missing_variable_terminator(token, parse_context) def raise_missing_variable_terminator(token, parse_context)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect)) raise SyntaxError, parse_context.locale.t('errors.syntax.variable_termination', token: token, tag_end: VARIABLE_END.inspect)
end end
def registered_tags def registered_tags

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Container for liquid nodes which conveniently wraps decision making logic # Container for liquid nodes which conveniently wraps decision making logic
# #
@@ -7,26 +9,26 @@ module Liquid
# c.evaluate #=> true # c.evaluate #=> true
# #
class Condition #:nodoc: class Condition #:nodoc:
@@operators = { @operators = {
'=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) }, '==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<'.freeze => :<, '<' => :<,
'>'.freeze => :>, '>' => :>,
'>='.freeze => :>=, '>=' => :>=,
'<='.freeze => :<=, '<=' => :<=,
'contains'.freeze => lambda do |cond, left, right| 'contains' => lambda do |_cond, left, right|
if left && right && left.respond_to?(:include?) if left && right && left.respond_to?(:include?)
right = right.to_s if left.is_a?(String) right = right.to_s if left.is_a?(String)
left.include?(right) left.include?(right)
else else
false false
end end
end end,
} }
def self.operators class << self
@@operators attr_accessor :operators
end end
attr_reader :attachment, :child_condition attr_reader :attachment, :child_condition
@@ -78,7 +80,7 @@ module Liquid
end end
def inspect def inspect
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>" "#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
end end
protected protected
@@ -116,7 +118,7 @@ module Liquid
left = context.evaluate(left) left = context.evaluate(left)
right = context.evaluate(right) right = context.evaluate(right)
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}")) operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
if operation.respond_to?(:call) if operation.respond_to?(:call)
operation.call(self, left, right) operation.call(self, left, right)
@@ -124,7 +126,7 @@ module Liquid
begin begin
left.send(operation, right) left.send(operation, right)
rescue ::ArgumentError => e rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message) raise Liquid::ArgumentError, e.message
end end
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Context keeps the variable stack and resolves variables, as well as keywords # Context keeps the variable stack and resolves variables, as well as keywords
# #
@@ -12,12 +14,12 @@ module Liquid
# #
# context['bob'] #=> nil class Context # context['bob'] #=> nil class Context
class Context class Context
attr_reader :scope, :errors, :registers, :environments, :resource_limits attr_reader :scopes, :errors, :registers, :environments, :resource_limits
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) def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten @environments = [environments].flatten
@scope = outer_scope || {} @scopes = [(outer_scope || {})]
@registers = registers @registers = registers
@errors = [] @errors = []
@partial = false @partial = false
@@ -25,16 +27,14 @@ module Liquid
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
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
if rethrow_errors self.exception_renderer = ->(_e) { raise } if rethrow_errors
self.exception_renderer = ->(e) { raise }
end
@interrupts = [] @interrupts = []
@filters = [] @filters = []
@global_filter = nil @global_filter = nil
@stack_level = 0
end end
def warnings def warnings
@@ -86,9 +86,22 @@ module Liquid
strainer.invoke(method, *args).to_liquid strainer.invoke(method, *args).to_liquid
end end
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope = {})
@scopes.unshift(new_scope)
raise StackLevelError, 'Nesting too deep' if @scopes.length > Block::MAX_DEPTH
end
# Merge a hash of variables in the current local scope # Merge a hash of variables in the current local scope
def merge(new_scopes) def merge(new_scopes)
new_scopes.each { |k, v| self[k] = v } @scopes[0].merge!(new_scopes)
end
# Pop from the stack. use <tt>Context#stack</tt> instead
def pop
raise ContextError if @scopes.size == 1
@scopes.shift
end end
# Pushes a new local scope on the stack, pops it at the end of the block # Pushes a new local scope on the stack, pops it at the end of the block
@@ -99,20 +112,32 @@ module Liquid
# end # end
# #
# context['var] #=> nil # context['var] #=> nil
def stack(*variable_names) def stack(new_scope = nil)
@stack_level += 1 old_stack_used = @this_stack_used
raise StackLevelError, "Nesting too deep".freeze if @stack_level > Block::MAX_DEPTH if new_scope
push(new_scope)
@this_stack_used = true
else
@this_stack_used = false
end
begin
yield yield
ensure ensure
@stack_level -= 1 pop if @this_stack_used
@this_stack_used = old_stack_used
end end
def clear_instance_assigns
@scopes[0] = {}
end end
# 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)
(@scope[key] ||= [nil]) << value unless @this_stack_used
@this_stack_used = true
push({})
end
@scopes[0][key] = value
end end
# Look up variable, either resolve directly after considering the name. We can directly handle # Look up variable, either resolve directly after considering the name. We can directly handle
@@ -127,29 +152,6 @@ module Liquid
evaluate(Expression.parse(expression)) evaluate(Expression.parse(expression))
end end
def unset(key)
if @scope[key].size <= 1
@scope.delete(key)
else
@scope[key].pop
end
end
def set_root(key, val)
@scope[key] ||= []
@scope[key][0] = val
end
def set_level(key, val, int)
@scope[key] ||= []
@scope[key][int] = val
end
def create_level(key)
(@scope[key] ||= [nil]) << nil
@scope[key].size - 1
end
def key?(key) def key?(key)
self[key] != nil self[key] != nil
end end
@@ -160,23 +162,27 @@ module Liquid
# Fetches an object starting at the local scope and then moving up the hierachy # Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key, raise_on_not_found: true) def find_variable(key, raise_on_not_found: true)
trigger = false # This was changed from find() to find_index() because this is a very hot
value = @scope[key] # path and find_index() is optimized in MRI to reduce object allocation
scope = @scope unless value.nil? index = @scopes.find_index { |s| s.key?(key) }
trigger = true unless value.nil? scope = @scopes[index] if index
variable = nil
if scope.nil? if scope.nil?
index = @environments.find_index do |e| @environments.each do |e|
variable = lookup_and_evaluate(e, 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 # 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. # then it is the value we are looking for.
!variable.nil? || @strict_variables && raise_on_not_found if !variable.nil? || @strict_variables && raise_on_not_found
scope = e
break
end
end
end end
scope = @environments[index || -1] scope ||= @environments.last || @scopes.last
end variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
variable ||= lookup_and_evaluate(scope, key, trigger, 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=)
@@ -184,19 +190,13 @@ module Liquid
variable variable
end end
def lookup_and_evaluate(obj, key, trigger = false, raise_on_not_found: true) def lookup_and_evaluate(obj, key, raise_on_not_found: true)
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key) raise Liquid::UndefinedVariable, "undefined variable #{key}" if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
value = if trigger == true value = obj[key]
obj[key][-1]
else
obj[key]
end
if value.is_a?(Proc) && obj.respond_to?(:[]=) if value.is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self) obj[key] = value.arity == 0 ? value.call : value.call(self)
else else
value value
end end
@@ -207,15 +207,15 @@ module Liquid
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 => exc rescue Liquid::InternalError => e
exc e
end end
def squash_instance_assigns_with_environments def squash_instance_assigns_with_environments
@scope.each_key do |k| @scopes.last.each_key do |k|
@environments.each do |env| @environments.each do |env|
if env.key?(k) if env.key?(k)
@scope[k] = [lookup_and_evaluate(env, k)] scopes.last[k] = lookup_and_evaluate(env, k)
break break
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Document < BlockBody class Document < BlockBody
def self.parse(tokens, parse_context) def self.parse(tokens, parse_context)
@@ -7,7 +9,7 @@ module Liquid
end end
def parse(tokens, parse_context) def parse(tokens, parse_context)
super do |end_tag_name, end_tag_params| super do |end_tag_name, _end_tag_params|
unknown_tag(end_tag_name, parse_context) if end_tag_name unknown_tag(end_tag_name, parse_context) if end_tag_name
end end
rescue SyntaxError => e rescue SyntaxError => e
@@ -17,10 +19,10 @@ module Liquid
def unknown_tag(tag, parse_context) def unknown_tag(tag, parse_context)
case tag case tag
when 'else'.freeze, 'end'.freeze when 'else', 'end'
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag)) raise SyntaxError, parse_context.locale.t('errors.syntax.unexpected_outer_tag', tag: tag)
else else
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag)) raise SyntaxError, parse_context.locale.t('errors.syntax.unknown_tag', tag: tag)
end end
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'set' require 'set'
module Liquid module Liquid
@@ -25,7 +27,8 @@ 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 && @context.strict_variables return nil unless @context&.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}" raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end end
@@ -67,7 +70,7 @@ module Liquid
if include?(Enumerable) if include?(Enumerable)
blacklist += Enumerable.public_instance_methods blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max, :include?] blacklist -= %i[sort count first min max include?]
end end
whitelist = [:to_liquid] + (public_instance_methods - blacklist) whitelist = [:to_liquid] + (public_instance_methods - blacklist)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Error < ::StandardError class Error < ::StandardError
attr_accessor :line_number attr_accessor :line_number
@@ -5,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
@@ -20,20 +22,20 @@ module Liquid
private private
def message_prefix def message_prefix
str = "" str = +''
if is_a?(SyntaxError) str << if is_a?(SyntaxError)
str << "Liquid syntax error" 'Liquid syntax error'
else else
str << "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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Expression class Expression
class MethodLiteral class MethodLiteral
@@ -14,18 +16,18 @@ module Liquid
end end
LITERALS = { LITERALS = {
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil, nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
'true'.freeze => true, 'true' => true,
'false'.freeze => false, 'false' => false,
'blank'.freeze => MethodLiteral.new(:blank?, '').freeze, 'blank' => MethodLiteral.new(:blank?, '').freeze,
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze 'empty' => MethodLiteral.new(:empty?, '').freeze
}.freeze }.freeze
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m SINGLE_QUOTED_STRING = /\A'(.*)'\z/m.freeze
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m.freeze
INTEGERS_REGEX = /\A(-?\d+)\z/ INTEGERS_REGEX = /\A(-?\d+)\z/.freeze
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/ FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/.freeze
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/ RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/.freeze
def self.parse(markup) def self.parse(markup)
if LITERALS.key?(markup) if LITERALS.key?(markup)
@@ -33,13 +35,13 @@ module Liquid
else else
case markup case markup
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
$1 Regexp.last_match(1)
when INTEGERS_REGEX when INTEGERS_REGEX
$1.to_i Regexp.last_match(1).to_i
when RANGES_REGEX when RANGES_REGEX
RangeLookup.parse($1, $2) RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
when FLOATS_REGEX when FLOATS_REGEX
$1.to_f Regexp.last_match(1).to_f
else else
VariableLookup.parse(markup) VariableLookup.parse(markup)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'time' require 'time'
require 'date' require 'date'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# A Liquid file system is a way to let your templates retrieve other templates for use with the include tag. # A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
# #
@@ -15,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
@@ -44,7 +46,7 @@ module Liquid
class LocalFileSystem class LocalFileSystem
attr_accessor :root attr_accessor :root
def initialize(root, pattern = "_%s.liquid".freeze) def initialize(root, pattern = '_%s.liquid')
@root = root @root = root
@pattern = pattern @pattern = pattern
end end
@@ -57,9 +59,9 @@ module Liquid
end end
def full_path(template_path) def full_path(template_path)
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/ raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ %r{\A[^./][a-zA-Z0-9_/]+\z}
full_path = if template_path.include?('/'.freeze) 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))
else else
File.join(root, @pattern % template_path) File.join(root, @pattern % template_path)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class ForloopDrop < Drop class ForloopDrop < Drop
def initialize(name, length, parentloop) def initialize(name, length, parentloop)

View File

@@ -1,8 +1,10 @@
# frozen_string_literal: true
require 'yaml' 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)
@@ -26,13 +28,13 @@ module Liquid
def interpolate(name, vars) def interpolate(name, vars)
name.gsub(/%\{(\w+)\}/) do name.gsub(/%\{(\w+)\}/) do
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym] # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
(vars[$1.to_sym]).to_s (vars[Regexp.last_match(1).to_sym]).to_s
end end
end end
def deep_fetch_translation(name) def deep_fetch_translation(name)
name.split('.'.freeze).reduce(locale) do |level, cur| name.split('.').reduce(locale) do |level, cur|
level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}" level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}")
end end
end end
end end

View File

@@ -1,16 +1,18 @@
# frozen_string_literal: true
module Liquid module Liquid
# An interrupt is any command that breaks processing of a block (ex: a for loop). # An interrupt is any command that breaks processing of a block (ex: a for loop).
class Interrupt class Interrupt
attr_reader :message attr_reader :message
def initialize(message = nil) def initialize(message = nil)
@message = message || "interrupt".freeze @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 < Interrupt; end class BreakInterrupt < RuntimeError; end
# Interrupt that is thrown whenever a {% continue %} is called. # Interrupt that is thrown whenever a {% continue %} is called.
class ContinueInterrupt < Interrupt; end class ContinueInterrupt < RuntimeError; end
end end

79
lib/liquid/legacy.rb Normal file
View File

@@ -0,0 +1,79 @@
# 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,25 +1,27 @@
require "strscan" # frozen_string_literal: true
require 'strscan'
module Liquid module Liquid
class Lexer class Lexer
SPECIALS = { SPECIALS = {
'|'.freeze => :pipe, '|' => :pipe,
'.'.freeze => :dot, '.' => :dot,
':'.freeze => :colon, ':' => :colon,
','.freeze => :comma, ',' => :comma,
'['.freeze => :open_square, '[' => :open_square,
']'.freeze => :close_square, ']' => :close_square,
'('.freeze => :open_round, '(' => :open_round,
')'.freeze => :close_round, ')' => :close_round,
'?'.freeze => :question, '?' => :question,
'-'.freeze => :dash '-' => :dash,
}.freeze }.freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/ IDENTIFIER = /[a-zA-Z_][\w-]*\??/.freeze
SINGLE_STRING_LITERAL = /'[^\']*'/ SINGLE_STRING_LITERAL = /'[^\']*'/.freeze
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/.freeze
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/.freeze
DOTDOT = /\.\./ DOTDOT = /\.\./.freeze
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/.freeze
WHITESPACE_OR_NOTHING = /\s*/ WHITESPACE_OR_NOTHING = /\s*/.freeze
def initialize(input) def initialize(input)
@ss = StringScanner.new(input) @ss = StringScanner.new(input)
@@ -31,16 +33,16 @@ 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 = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] tok = if (t = @ss.scan(COMPARISON_OPERATOR)) then [:comparison, t]
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t] elsif (t = @ss.scan(SINGLE_STRING_LITERAL)) then [:string, t]
when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t] elsif (t = @ss.scan(DOUBLE_STRING_LITERAL)) then [:string, t]
when t = @ss.scan(NUMBER_LITERAL) then [:number, t] elsif (t = @ss.scan(NUMBER_LITERAL)) then [:number, t]
when t = @ss.scan(IDENTIFIER) then [:id, t] elsif (t = @ss.scan(IDENTIFIER)) then [:id, t]
when t = @ss.scan(DOTDOT) then [:dotdot, t] elsif (t = @ss.scan(DOTDOT)) then [:dotdot, t]
else else
c = @ss.getch c = @ss.getch
if s = SPECIALS[c] if (s = SPECIALS[c])
[s, c] [s, c]
else else
raise SyntaxError, "Unexpected character #{c}" raise SyntaxError, "Unexpected character #{c}"

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class ParseContext class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth attr_accessor :locale, :line_number, :trim_whitespace, :depth
@@ -19,7 +21,6 @@ module Liquid
@partial = value @partial = value
@options = value ? partial_options : @template_options @options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode @error_mode = @options[:error_mode] || Template.error_mode
value
end end
def partial_options def partial_options
@@ -28,7 +29,7 @@ module Liquid
if dont_pass == true if dont_pass == true
{ locale: locale } { locale: locale }
elsif dont_pass.is_a?(Array) elsif dont_pass.is_a?(Array)
@template_options.reject { |k, v| dont_pass.include?(k) } @template_options.reject { |k, _v| dont_pass.include?(k) }
else else
@template_options @template_options
end end

View File

@@ -28,7 +28,7 @@ module Liquid
item, new_context = @callbacks[node.class].call(node, context) item, new_context = @callbacks[node.class].call(node, context)
[ [
item, item,
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context) ParseTreeVisitor.for(node, @callbacks).visit(new_context || context),
] ]
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Parser class Parser
def initialize(input) def initialize(input)
@@ -12,9 +14,8 @@ module Liquid
def consume(type = nil) def consume(type = nil)
token = @tokens[@p] token = @tokens[@p]
if type && token[0] != type raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}" 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
@@ -25,6 +26,7 @@ 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
@@ -34,6 +36,7 @@ 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
@@ -41,14 +44,18 @@ 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
private_constant :SINGLE_TOKEN_EXPRESSION_TYPES
def expression def expression
token = @tokens[@p] token = @tokens[@p]
if token[0] == :id if token[0] == :id
variable_signature variable_signature
elsif [:string, :number].include? token[0] elsif SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0])
consume consume
elsif token.first == :open_round elsif token.first == :open_round
consume consume
@@ -63,11 +70,9 @@ module Liquid
end end
def argument def argument
str = "" str = +''
# might be a keyword argument (identifier: expression) # might be a keyword argument (identifier: expression)
if look(:id) && look(:colon, 1) str << consume << consume << ' ' if look(:id) && look(:colon, 1)
str << consume << consume << ' '.freeze
end
str << expression str << expression
str str

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
module ParserSwitching module ParserSwitching
def parse_with_selected_parser(markup) def parse_with_selected_parser(markup)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'liquid/profiler/hooks' require 'liquid/profiler/hooks'
module Liquid module Liquid
@@ -97,9 +99,9 @@ module Liquid
end end
def initialize def initialize
@partial_stack = ["<root>"] @partial_stack = ['<root>']
@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

View File

@@ -1,23 +1,25 @@
# frozen_string_literal: true
module Liquid module Liquid
class BlockBody class BlockBody
def render_node_with_profiling(node, output, context, skip_output = false) def render_node_with_profiling(context, output, node)
Profiler.profile_node_render(node) do Profiler.profile_node_render(node) do
render_node_without_profiling(node, output, context, skip_output) render_node_without_profiling(context, output, node)
end end
end end
alias_method :render_node_without_profiling, :render_node_to_output alias_method :render_node_without_profiling, :render_node
alias_method :render_node_to_output, :render_node_with_profiling alias_method :render_node, :render_node_with_profiling
end end
class Include < Tag class Include < Tag
def render_with_profiling(context) def render_to_output_buffer_with_profiling(context, output)
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
render_without_profiling(context) render_to_output_buffer_without_profiling(context, output)
end end
end end
alias_method :render_without_profiling, :render alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer
alias_method :render, :render_with_profiling alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class RangeLookup class RangeLookup
def self.parse(start_markup, end_markup) def self.parse(start_markup, end_markup)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class ResourceLimits class ResourceLimits
attr_accessor :render_length, :render_score, :assign_score, attr_accessor :render_length, :render_score, :assign_score,

View File

@@ -1,22 +1,24 @@
# frozen_string_literal: true
require 'cgi' require 'cgi'
require 'bigdecimal' require 'bigdecimal'
module Liquid module Liquid
module StandardFilters module StandardFilters
HTML_ESCAPE = { HTML_ESCAPE = {
'&'.freeze => '&amp;'.freeze, '&' => '&amp;',
'>'.freeze => '&gt;'.freeze, '>' => '&gt;',
'<'.freeze => '&lt;'.freeze, '<' => '&lt;',
'"'.freeze => '&quot;'.freeze, '"' => '&quot;',
"'".freeze => '&#39;'.freeze "'" => '&#39;',
}.freeze }.freeze
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/.freeze
STRIP_HTML_BLOCKS = Regexp.union( STRIP_HTML_BLOCKS = Regexp.union(
/<script.*?<\/script>/m, %r{<script.*?</script>}m,
/<!--.*?-->/m, /<!--.*?-->/m,
/<style.*?<\/style>/m %r{<style.*?</style>}m
) )
STRIP_HTML_TAGS = /<.*?>/m STRIP_HTML_TAGS = /<.*?>/m.freeze
# 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)
@@ -72,23 +74,25 @@ 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 = "...".freeze) 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
l = length - truncate_string_str.length l = length - truncate_string_str.length
l = 0 if l < 0 l = 0 if l < 0
input_str.length > length ? input_str[0...l] + 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 = "...".freeze) 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(" ".freeze) + 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.
@@ -113,7 +117,7 @@ module Liquid
end end
def strip_html(input) def strip_html(input)
empty = ''.freeze empty = ''
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty) result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
result.gsub!(STRIP_HTML_TAGS, empty) result.gsub!(STRIP_HTML_TAGS, empty)
result result
@@ -121,11 +125,11 @@ module Liquid
# Remove all newlines from the string # Remove all newlines from the string
def strip_newlines(input) def strip_newlines(input)
input.to_s.gsub(/\r?\n/, ''.freeze) input.to_s.gsub(/\r?\n/, '')
end end
# Join elements of the array with certain character between them # Join elements of the array with certain character between them
def join(input, glue = ' '.freeze) def join(input, glue = ' ')
InputIterator.new(input).join(glue) InputIterator.new(input).join(glue)
end end
@@ -220,7 +224,7 @@ module Liquid
InputIterator.new(input).map do |e| InputIterator.new(input).map do |e|
e = e.call if e.is_a?(Proc) e = e.call if e.is_a?(Proc)
if property == "to_liquid".freeze if property == 'to_liquid'
e e
elsif e.respond_to?(:[]) elsif e.respond_to?(:[])
r = e[property] r = e[property]
@@ -250,23 +254,23 @@ module Liquid
end end
# Replace occurrences of a string with another # Replace occurrences of a string with another
def replace(input, string, replacement = ''.freeze) def replace(input, string, replacement = '')
input.to_s.gsub(string.to_s, replacement.to_s) input.to_s.gsub(string.to_s, replacement.to_s)
end end
# Replace the first occurrences of a string with another # Replace the first occurrences of a string with another
def replace_first(input, string, replacement = ''.freeze) def replace_first(input, string, replacement = '')
input.to_s.sub(string.to_s, replacement.to_s) input.to_s.sub(string.to_s, replacement.to_s)
end end
# remove a substring # remove a substring
def remove(input, string) def remove(input, string)
input.to_s.gsub(string.to_s, ''.freeze) input.to_s.gsub(string.to_s, '')
end end
# remove the first occurrences of a substring # remove the first occurrences of a substring
def remove_first(input, string) def remove_first(input, string)
input.to_s.sub(string.to_s, ''.freeze) input.to_s.sub(string.to_s, '')
end end
# add one string to another # add one string to another
@@ -275,9 +279,8 @@ module Liquid
end end
def concat(input, array) def concat(input, array)
unless array.respond_to?(:to_ary) raise ArgumentError, 'concat filter requires an array argument' unless array.respond_to?(:to_ary)
raise ArgumentError.new("concat filter requires an array argument")
end
InputIterator.new(input).concat(array) InputIterator.new(input).concat(array)
end end
@@ -288,7 +291,7 @@ module Liquid
# Add <br /> tags in front of all newlines in input string # Add <br /> tags in front of all newlines in input string
def newline_to_br(input) def newline_to_br(input)
input.to_s.gsub(/\n/, "<br />\n".freeze) input.to_s.gsub(/\n/, "<br />\n")
end end
# Reformat a date using Ruby's core Time#strftime( string ) -> string # Reformat a date using Ruby's core Time#strftime( string ) -> string
@@ -325,7 +328,7 @@ module Liquid
def date(input, format) def date(input, format)
return input if format.to_s.empty? return input if format.to_s.empty?
return input unless date = Utils.to_date(input) return input unless (date = Utils.to_date(input))
date.strftime(format.to_s) date.strftime(format.to_s)
end end
@@ -419,7 +422,7 @@ 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 = ''.freeze) def default(input, default_value = '')
if !input || input.respond_to?(:empty?) && input.empty? if !input || input.respond_to?(:empty?) && input.empty?
default_value default_value
else else
@@ -430,7 +433,7 @@ module Liquid
private private
def raise_property_error(property) def raise_property_error(property)
raise Liquid::ArgumentError.new("cannot select the property '#{property}'") raise Liquid::ArgumentError, "cannot select the property '#{property}'"
end end
def apply_operation(input, operand, operation) def apply_operation(input, operand, operation)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'set' require 'set'
module Liquid module Liquid
@@ -7,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 = @@global_strainer.filter_methods.dup @filter_methods = Strainer.global_strainer.filter_methods.dup
filters.each { |f| add_filter(f) } filters.each { |f| add_filter(f) }
end end
end end
@@ -22,12 +24,14 @@ 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 self.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?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}" raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
@@ -39,8 +43,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)
@@ -48,13 +52,13 @@ 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)
if self.class.invokable?(method) if self.class.invokable?(method)
send(method, *args) send(method, *args)
elsif @context && @context.strict_filters elsif @context&.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}" raise Liquid::UndefinedFilter, "undefined filter #{method}"
else else
args.first args.first

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class TablerowloopDrop < Drop class TablerowloopDrop < Drop
def initialize(length, cols) def initialize(length, cols)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Tag class Tag
attr_reader :nodelist, :tag_name, :line_number, :parse_context attr_reader :nodelist, :tag_name, :line_number, :parse_context
@@ -21,8 +23,7 @@ module Liquid
@line_number = parse_context.line_number @line_number = parse_context.line_number
end end
def parse(_tokens) def parse(_tokens); end
end
def raw def raw
"#{@tag_name} #{@markup}" "#{@tag_name} #{@markup}"
@@ -33,7 +34,15 @@ module Liquid
end end
def render(_context) def render(_context)
''.freeze ''
end
# 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`
# method will be removed.
def render_to_output_buffer(context, output)
output << render(context)
output
end end
def blank? def blank?

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Assign sets a variable in your template. # Assign sets a variable in your template.
# #
@@ -8,25 +10,25 @@ module Liquid
# {{ foo }} # {{ foo }}
# #
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om SYNTAX = /(#{VARIABLE_SIGNATURE}+)\s*=\s*(.*)\s*/om.freeze
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 = $1 @to = Regexp.last_match(1)
@from = Variable.new($2, options) @from = Variable.new(Regexp.last_match(2), options)
else else
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze) raise SyntaxError, options[:locale].t('errors.syntax.assign')
end end
end end
def render(context) def render_to_output_buffer(context, output)
val = @from.render(context) val = @from.render(context)
context.set_root(@to, val) context.scopes.last[@to] = val
context.resource_limits.assign_score += assign_score_of(val) context.resource_limits.assign_score += assign_score_of(val)
''.freeze output
end end
def blank? def blank?
@@ -55,5 +57,5 @@ module Liquid
end end
end end
Template.register_tag('assign'.freeze, Assign) Template.register_tag('assign', Assign)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Break tag to be used to break out of a for loop. # Break tag to be used to break out of a for loop.
# #
@@ -14,5 +16,5 @@ module Liquid
end end
end end
Template.register_tag('break'.freeze, Break) Template.register_tag('break', Break)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Capture stores the result of a block into a variable without rendering it inplace. # Capture stores the result of a block into a variable without rendering it inplace.
# #
@@ -11,22 +13,23 @@ module Liquid
# in a sidebar or footer. # in a sidebar or footer.
# #
class Capture < Block class Capture < Block
Syntax = /(#{VariableSignature}+)/o SYNTAX = /(#{VARIABLE_SIGNATURE}+)/o.freeze
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ SYNTAX
@to = $1 @to = Regexp.last_match(1)
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.capture")) raise SyntaxError, options[:locale].t('errors.syntax.capture')
end end
end end
def render(context) def render_to_output_buffer(context, output)
output = super previous_output_size = output.bytesize
context.set_root(@to, output) super
context.resource_limits.assign_score += output.bytesize context.scopes.last[@to] = output
''.freeze context.resource_limits.assign_score += (output.bytesize - previous_output_size)
output
end end
def blank? def blank?
@@ -34,5 +37,5 @@ module Liquid
end end
end end
Template.register_tag('capture'.freeze, Capture) Template.register_tag('capture', Capture)
end end

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Liquid module Liquid
class Case < Block class Case < Block
Syntax = /(#{QuotedFragment})/o SYNTAX = /(#{QUOTED_FRAGMENT})/o.freeze
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om WHEN_SYNTAX = /(#{QUOTED_FRAGMENT})(?:(?:\s+or\s+|\s*\,\s*)(#{QUOTED_FRAGMENT}.*))?/om.freeze
attr_reader :blocks, :left attr_reader :blocks, :left
@@ -9,18 +11,16 @@ module Liquid
super super
@blocks = [] @blocks = []
if markup =~ Syntax if markup =~ SYNTAX
@left = Expression.parse($1) @left = Expression.parse(Regexp.last_match(1))
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze)) raise SyntaxError, options[:locale].t('errors.syntax.case')
end end
end end
def parse(tokens) def parse(tokens)
body = BlockBody.new body = BlockBody.new
while parse_body(body, tokens) body = @blocks.last.attachment while parse_body(body, tokens)
body = @blocks.last.attachment
end
end end
def nodelist def nodelist
@@ -29,25 +29,26 @@ module Liquid
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
case tag case tag
when 'when'.freeze when 'when'
record_when_condition(markup) record_when_condition(markup)
when 'else'.freeze when 'else'
record_else_condition(markup) record_else_condition(markup)
else else
super super
end end
end end
def render(context) def render_to_output_buffer(context, output)
context.stack do
execute_else_block = true execute_else_block = true
output = ''
@blocks.each do |block| @blocks.each do |block|
if block.else? if block.else?
return block.attachment.render(context) 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
output << block.attachment.render(context) block.attachment.render_to_output_buffer(context, output)
end
end end
end end
@@ -60,22 +61,18 @@ module Liquid
body = BlockBody.new body = BlockBody.new
while markup while markup
unless markup =~ WhenSyntax raise SyntaxError, options[:locale].t('errors.syntax.case_invalid_when') unless markup =~ WHEN_SYNTAX
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
end
markup = $2 markup = Regexp.last_match(2)
block = Condition.new(@left, '=='.freeze, Expression.parse($1)) block = Condition.new(@left, '==', Expression.parse(Regexp.last_match(1)))
block.attach(body) block.attach(body)
@blocks << block @blocks << block
end end
end end
def record_else_condition(markup) def record_else_condition(markup)
unless markup.strip.empty? raise SyntaxError, options[:locale].t('errors.syntax.case_invalid_else') unless markup.strip.empty?
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
end
block = ElseCondition.new block = ElseCondition.new
block.attach(BlockBody.new) block.attach(BlockBody.new)
@@ -89,5 +86,5 @@ module Liquid
end end
end end
Template.register_tag('case'.freeze, Case) Template.register_tag('case', Case)
end end

View File

@@ -1,16 +1,17 @@
# frozen_string_literal: true
module Liquid module Liquid
class Comment < Block class Comment < Block
def render(_context) def render_to_output_buffer(_context, output)
''.freeze output
end end
def unknown_tag(_tag, _markup, _tokens) def unknown_tag(_tag, _markup, _tokens); end
end
def blank? def blank?
true true
end end
end end
Template.register_tag('comment'.freeze, Comment) Template.register_tag('comment', Comment)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Continue tag to be used to break out of a for loop. # Continue tag to be used to break out of a for loop.
# #
@@ -14,5 +16,5 @@ module Liquid
end end
end end
Template.register_tag('continue'.freeze, Continue) Template.register_tag('continue', Continue)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes. # Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
# #
@@ -12,43 +14,56 @@ module Liquid
# <div class="green"> Item five</div> # <div class="green"> Item five</div>
# #
class Cycle < Tag class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o SIMPLE_SYNTAX = /\A#{QUOTED_FRAGMENT}+/o.freeze
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om NAMED_SYNTAX = /\A(#{QUOTED_FRAGMENT})\s*\:\s*(.*)/om.freeze
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 NamedSyntax when NAMED_SYNTAX
@variables = variables_from_string($2) @variables = variables_from_string(Regexp.last_match(2))
@name = Expression.parse($1) @name = Expression.parse(Regexp.last_match(1))
when SimpleSyntax when SIMPLE_SYNTAX
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze)) raise SyntaxError, options[:locale].t('errors.syntax.cycle')
end end
end end
def render(context) 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
result = context.evaluate(@variables[iteration])
val = context.evaluate(@variables[iteration])
if val.is_a?(Array)
val = val.join
elsif !val.is_a?(String)
val = val.to_s
end
output << val
iteration += 1 iteration += 1
iteration = 0 if iteration >= @variables.size iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration context.registers[:cycle][key] = iteration
result end
output
end end
private private
def variables_from_string(markup) def variables_from_string(markup)
markup.split(',').collect do |var| markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o var =~ /\s*(#{QUOTED_FRAGMENT})\s*/o
$1 ? Expression.parse($1) : nil Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil
end.compact end.compact
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# decrement is used in a place where one needs to insert a counter # decrement is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across # into a template, and needs the counter to survive across
@@ -23,13 +25,14 @@ module Liquid
@variable = markup.strip @variable = markup.strip
end end
def render(context) def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0 value = context.environments.first[@variable] ||= 0
value -= 1 value -= 1
context.environments.first[@variable] = value context.environments.first[@variable] = value
value.to_s output << value.to_s
output
end end
end end
Template.register_tag('decrement'.freeze, Decrement) Template.register_tag('decrement', Decrement)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# "For" iterates over an array or collection. # "For" iterates over an array or collection.
# Several useful variables are available to you within the loop. # Several useful variables are available to you within the loop.
@@ -44,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(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o SYNTAX = /\A(#{VARIABLE_SEGMENT}+)\s+in\s+(#{QUOTED_FRAGMENT}+)\s*(reversed)?/o.freeze
attr_reader :collection_name, :variable_name, :limit, :from attr_reader :collection_name, :variable_name, :limit, :from
@@ -58,6 +60,7 @@ 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
@@ -66,50 +69,55 @@ module Liquid
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
return super unless tag == 'else'.freeze return super unless tag == 'else'
@else_block = BlockBody.new @else_block = BlockBody.new
end end
def render(context) def render_to_output_buffer(context, output)
segment = collection_segment(context) segment = collection_segment(context)
if segment.empty? if segment.empty?
render_else(context) render_else(context, output)
else else
render_segment(context, segment) render_segment(context, output, segment)
end end
output
end end
protected protected
def lax_parse(markup) def lax_parse(markup)
if markup =~ Syntax if markup =~ SYNTAX
@variable_name = $1 @variable_name = Regexp.last_match(1)
collection_name = $2 collection_name = Regexp.last_match(2)
@reversed = !!$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(TagAttributes) do |key, value| markup.scan(TAG_ATTRIBUTES) do |key, value|
set_attribute(key, value) set_attribute(key, value)
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze)) 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.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze) 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)
@reversed = p.id?('reversed'.freeze) @reversed = p.id?('reversed')
while p.look(:id) && p.look(:colon, 1) while p.look(:id) && p.look(:colon, 1)
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze) unless (attribute = p.id?('limit') || p.id?('offset'))
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze)) 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
@@ -150,58 +158,57 @@ module Liquid
segment segment
end end
def render_segment(context, segment) def render_segment(context, output, segment)
for_stack = context.registers[:for_stack] ||= [] for_stack = context.registers[:for_stack] ||= []
length = segment.length length = segment.length
result = '' context.stack do
context.stack('forloop', @variable_name) do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1]) loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
for_stack.push(loop_vars) for_stack.push(loop_vars)
begin begin
context['forloop'.freeze] = loop_vars context['forloop'] = loop_vars
level = context.create_level(@variable_name)
segment.each do |item| segment.each do |item|
context.set_level(@variable_name, item, level) context[@variable_name] = item
result << @for_block.render(context) @for_block.render_to_output_buffer(context, output)
loop_vars.send(:increment!) loop_vars.send(:increment!)
# Handle any interrupts if they exist.
if context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
end
context.unset(@variable_name)
context.unset('forloop'.freeze)
ensure
# Handle any interrupts if they exist.
next unless context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a?(BreakInterrupt)
next if interrupt.is_a?(ContinueInterrupt)
end
ensure
for_stack.pop for_stack.pop
end end
end end
result output
end end
def set_attribute(key, expr) def set_attribute(key, expr)
case key case key
when 'offset'.freeze when 'offset'
@from = if expr == 'continue'.freeze @from = if expr == 'continue'
:continue :continue
else else
Expression.parse(expr) Expression.parse(expr)
end end
when 'limit'.freeze when 'limit'
@limit = Expression.parse(expr) @limit = Expression.parse(expr)
end end
end end
def render_else(context) def render_else(context, output)
@else_block ? @else_block.render(context) : ''.freeze if @else_block
@else_block.render_to_output_buffer(context, output)
else
output
end
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
@@ -211,5 +218,5 @@ module Liquid
end end
end end
Template.register_tag('for'.freeze, For) Template.register_tag('for', For)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# If is the conditional block # If is the conditional block
# #
@@ -10,16 +12,16 @@ 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 = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o SYNTAX = /(#{QUOTED_FRAGMENT})\s*([=!<>a-z_]+)?\s*(#{QUOTED_FRAGMENT})?/o.freeze
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/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
BOOLEAN_OPERATORS = %w(and or).freeze BOOLEAN_OPERATORS = %w[and or].freeze
attr_reader :blocks attr_reader :blocks
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@blocks = [] @blocks = []
push_block('if'.freeze, markup) push_block('if', markup)
end end
def nodelist def nodelist
@@ -32,26 +34,27 @@ module Liquid
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
if ['elsif'.freeze, 'else'.freeze].include?(tag) if %w[elsif else].include?(tag)
push_block(tag, markup) push_block(tag, markup)
else else
super super
end end
end end
def render(context) 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(context)
end end
end end
''.freeze
output
end end
private private
def push_block(tag, markup) def push_block(tag, markup)
block = if tag == 'else'.freeze block = if tag == 'else'
ElseCondition.new ElseCondition.new
else else
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@@ -62,18 +65,19 @@ module Liquid
end end
def lax_parse(markup) def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators) expressions = markup.scan(EXPRESSIONS_AND_OPERATORS)
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax raise SyntaxError, options[:locale].t('errors.syntax.if') unless expressions.pop =~ SYNTAX
condition = Condition.new(Expression.parse($1), $2, Expression.parse($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.new(options[:locale].t("errors.syntax.if".freeze))) 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)))
raise SyntaxError, options[:locale].t('errors.syntax.if') unless BOOLEAN_OPERATORS.include?(operator)
new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition) new_condition.send(operator, condition)
condition = new_condition condition = new_condition
end end
@@ -91,7 +95,7 @@ module Liquid
def parse_binary_comparisons(p) def parse_binary_comparisons(p)
condition = parse_comparison(p) condition = parse_comparison(p)
first_condition = condition first_condition = condition
while op = (p.id?('and'.freeze) || p.id?('or'.freeze)) while (op = (p.id?('and') || p.id?('or')))
child_condition = parse_comparison(p) child_condition = parse_comparison(p)
condition.send(op, child_condition) condition.send(op, child_condition)
condition = child_condition condition = child_condition
@@ -101,7 +105,7 @@ module Liquid
def parse_comparison(p) def parse_comparison(p)
a = Expression.parse(p.expression) a = Expression.parse(p.expression)
if op = p.consume?(:comparison) if (op = p.consume?(:comparison))
b = Expression.parse(p.expression) b = Expression.parse(p.expression)
Condition.new(a, op, b) Condition.new(a, op, b)
else else
@@ -116,5 +120,5 @@ module Liquid
end end
end end
Template.register_tag('if'.freeze, If) Template.register_tag('if', If)
end end

View File

@@ -1,16 +1,21 @@
# frozen_string_literal: true
module Liquid module Liquid
class Ifchanged < Block class Ifchanged < Block
def render(context) def render_to_output_buffer(context, output)
output = super context.stack do
block_output = +''
super(context, block_output)
if block_output != context.registers[:ifchanged]
context.registers[:ifchanged] = block_output
output << block_output
end
end
if output != context.registers[:ifchanged]
context.registers[:ifchanged] = output
output output
else
''.freeze
end
end end
end end
Template.register_tag('ifchanged'.freeze, Ifchanged) Template.register_tag('ifchanged', Ifchanged)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Include allows templates to relate with other templates # Include allows templates to relate with other templates
# #
@@ -14,40 +16,39 @@ module Liquid
# {% include 'product' for products %} # {% include 'product' for products %}
# #
class Include < Tag class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o SYNTAX = /(#{QUOTED_FRAGMENT}+)(\s+(?:with|for)\s+(#{QUOTED_FRAGMENT}+))?/o.freeze
attr_reader :template_name_expr, :variable_name_expr, :attributes attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ SYNTAX
template_name = $1 template_name = Regexp.last_match(1)
variable_name = $3 variable_name = Regexp.last_match(3)
@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(TagAttributes) do |key, value| markup.scan(TAG_ATTRIBUTES) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = Expression.parse(value)
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze)) raise SyntaxError, options[:locale].t('errors.syntax.include')
end end
end end
def parse(_tokens) def parse(_tokens); end
end
def render(context) def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr) template_name = context.evaluate(@template_name_expr)
raise ArgumentError.new(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 = load_cached_partial(template_name, context)
context_variable_name = template_name.split('/'.freeze).last context_variable_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)
@@ -60,25 +61,27 @@ module Liquid
begin begin
context.template_name = template_name context.template_name = template_name
context.partial = true context.partial = true
context.stack(context_variable_name, *@attributes.keys) do context.stack do
@attributes.each do |key, value| @attributes.each do |key, value|
context[key] = context.evaluate(value) context[key] = context.evaluate(value)
end end
if variable.is_a?(Array) if variable.is_a?(Array)
variable.collect do |var| variable.each do |var|
context[context_variable_name] = var context[context_variable_name] = var
partial.render(context) partial.render_to_output_buffer(context, output)
end end
else else
context[context_variable_name] = variable context[context_variable_name] = variable
partial.render(context) partial.render_to_output_buffer(context, output)
end end
end end
ensure ensure
context.template_name = old_template_name context.template_name = old_template_name
context.partial = old_partial context.partial = old_partial
end end
output
end end
private private
@@ -89,9 +92,10 @@ module Liquid
def load_cached_partial(template_name, context) def load_cached_partial(template_name, context)
cached_partials = context.registers[:cached_partials] || {} cached_partials = context.registers[:cached_partials] || {}
if cached = cached_partials[template_name] if (cached = cached_partials[template_name])
return cached return cached
end end
source = read_template_from_file_system(context) source = read_template_from_file_system(context)
begin begin
parse_context.partial = true parse_context.partial = true
@@ -114,11 +118,11 @@ module Liquid
def children def children
[ [
@node.template_name_expr, @node.template_name_expr,
@node.variable_name_expr @node.variable_name_expr,
] + @node.attributes.values ] + @node.attributes.values
end end
end end
end end
Template.register_tag('include'.freeze, Include) Template.register_tag('include', Include)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# increment is used in a place where one needs to insert a counter # increment is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across # into a template, and needs the counter to survive across
@@ -20,12 +22,13 @@ module Liquid
@variable = markup.strip @variable = markup.strip
end end
def render(context) 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
value.to_s output << value.to_s
output
end end
end end
Template.register_tag('increment'.freeze, Increment) Template.register_tag('increment', Increment)
end end

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Liquid module Liquid
class Raw < Block class Raw < Block
Syntax = /\A\s*\z/ SYNTAX = /\A\s*\z/.freeze
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om FULL_TOKEN_POSSIBLY_INVALID = /\A(.*)#{TAG_START}\s*(\w+)\s*(.*)?#{TAG_END}\z/om.freeze
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
super super
@@ -10,20 +12,21 @@ module Liquid
end end
def parse(tokens) def parse(tokens)
@body = '' @body = +''
while token = tokens.shift while (token = tokens.shift)
if token =~ FullTokenPossiblyInvalid if token =~ FULL_TOKEN_POSSIBLY_INVALID
@body << $1 if $1 != "".freeze @body << Regexp.last_match(1) if Regexp.last_match(1) != ''
return if block_delimiter == $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.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name)) raise SyntaxError, parse_context.locale.t('errors.syntax.tag_never_closed', block_name: block_name)
end end
def render(_context) def render_to_output_buffer(_context, output)
@body output << @body
output
end end
def nodelist def nodelist
@@ -37,11 +40,9 @@ module Liquid
protected protected
def ensure_valid_markup(tag_name, markup, parse_context) def ensure_valid_markup(tag_name, markup, parse_context)
unless markup =~ Syntax raise SyntaxError, parse_context.locale.t('errors.syntax.tag_unexpected_args', tag: tag_name) unless markup =~ SYNTAX
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
end
end end
end end
Template.register_tag('raw'.freeze, Raw) Template.register_tag('raw', Raw)
end end

View File

@@ -1,54 +1,57 @@
# frozen_string_literal: true
module Liquid module Liquid
class TableRow < Block class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o SYNTAX = /(\w+)\s+in\s+(#{QUOTED_FRAGMENT}+)/o.freeze
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 = $1 @variable_name = Regexp.last_match(1)
@collection_name = Expression.parse($2) @collection_name = Expression.parse(Regexp.last_match(2))
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TAG_ATTRIBUTES) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = Expression.parse(value)
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze)) raise SyntaxError, options[:locale].t('errors.syntax.table_row')
end end
end end
def render(context) def render_to_output_buffer(context, output)
collection = context.evaluate(@collection_name) or return ''.freeze (collection = context.evaluate(@collection_name)) || (return '')
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0 from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
collection = Utils.slice_collection(collection, from, to) collection = Utils.slice_collection(collection, from, to)
length = collection.length length = collection.length
cols = context.evaluate(@attributes['cols'.freeze]).to_i cols = context.evaluate(@attributes['cols']).to_i
result = "<tr class=\"row1\">\n" output << "<tr class=\"row1\">\n"
context.stack('tablerowloop', @variable_name) do context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols) tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'.freeze] = tablerowloop context['tablerowloop'] = tablerowloop
collection.each do |item| collection.each do |item|
context[@variable_name] = item context[@variable_name] = item
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>' output << "<td class=\"col#{tablerowloop.col}\">"
super
output << '</td>'
if tablerowloop.col_last && !tablerowloop.last output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">" if tablerowloop.col_last && !tablerowloop.last
result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
end
tablerowloop.send(:increment!) tablerowloop.send(:increment!)
end end
end end
result << "</tr>\n"
result output << "</tr>\n"
output
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
@@ -58,5 +61,5 @@ module Liquid
end end
end end
Template.register_tag('tablerow'.freeze, TableRow) Template.register_tag('tablerow', TableRow)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'if' require_relative 'if'
module Liquid module Liquid
@@ -6,23 +8,21 @@ module Liquid
# {% unless x < 0 %} x is greater than zero {% endunless %} # {% unless x < 0 %} x is greater than zero {% endunless %}
# #
class Unless < If class Unless < If
def render(context) 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(context)
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|
if block.evaluate(context) return block.attachment.render_to_output_buffer(context, output) if block.evaluate(context)
return block.attachment.render(context)
end end
end end
''.freeze output
end end
end end
Template.register_tag('unless'.freeze, Unless) Template.register_tag('unless', Unless)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Templates are central to liquid. # Templates are central to liquid.
# Interpretating templates is a two step process. First you compile the # Interpretating templates is a two step process. First you compile the
@@ -16,7 +18,11 @@ 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
@@ -50,7 +56,7 @@ module Liquid
private private
def lookup_class(name) def lookup_class(name)
name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) } Object.const_get(name)
end end
end end
@@ -74,14 +80,6 @@ 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
@@ -165,15 +163,13 @@ module Liquid
# filters and tags and might be useful to integrate liquid more with its host application # filters and tags and might be useful to integrate liquid more with its host application
# #
def render(*args) def render(*args)
return ''.freeze if @root.nil? return '' if @root.nil?
context = case args.first context = case args.first
when Liquid::Context when Liquid::Context
c = args.shift c = args.shift
if @rethrow_errors c.exception_renderer = ->(_e) { raise } if @rethrow_errors
c.exception_renderer = ->(e) { raise }
end
c c
when Liquid::Drop when Liquid::Drop
@@ -184,12 +180,15 @@ 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
case args.last case args.last
when Hash when Hash
options = args.pop options = args.pop
output = options[:output] if options[:output]
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash) registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
@@ -204,10 +203,9 @@ module Liquid
begin begin
# render the nodelist. # render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it. # for performance reasons we get an array back here. join will make a string out of it.
result = with_profiling(context) do with_profiling(context) do
@root.render(context) @root.render_to_output_buffer(context, output || +'')
end end
result.respond_to?(:join) ? result.join : result
rescue Liquid::MemoryError => e rescue Liquid::MemoryError => e
context.handle_error(e) context.handle_error(e)
ensure ensure
@@ -220,6 +218,10 @@ module Liquid
render(*args) render(*args)
end end
def render_to_output_buffer(context, output)
render(context, output: output)
end
private private
def tokenize(source) def tokenize(source)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Tokenizer class Tokenizer
attr_reader :line_number attr_reader :line_number
@@ -20,10 +22,10 @@ module Liquid
@source = @source.source if @source.respond_to?(:source) @source = @source.source if @source.respond_to?(:source)
return [] if @source.to_s.empty? return [] if @source.to_s.empty?
tokens = @source.split(TemplateParser) tokens = @source.split(TEMPLATE_PARSER)
# 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] && tokens[0].empty? tokens.shift if tokens[0]&.empty?
tokens tokens
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
module Utils module Utils
def self.slice_collection(collection, from, to) def self.slice_collection(collection, from, to)
@@ -19,13 +21,9 @@ module Liquid
return [] unless collection.respond_to?(:each) return [] unless collection.respond_to?(:each)
collection.each do |item| collection.each do |item|
if to && to <= index break if to && to <= index
break
end
if from <= index segments << item if from <= index
segments << item
end
index += 1 index += 1
end end
@@ -35,11 +33,12 @@ 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
@@ -50,7 +49,7 @@ module Liquid
when Numeric when Numeric
obj obj
when String when String
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal(obj) : obj.to_i obj.strip =~ /\A-?\d+\.\d+\z/ ? 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
@@ -65,11 +64,12 @@ 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
case obj case obj
when 'now'.freeze, 'today'.freeze when 'now', 'today'
Time.now Time.now
when /\A\d+\z/, Integer when /\A\d+\z/, Integer
Time.at(obj.to_i) Time.at(obj.to_i)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Holds variables. Variables are only loaded "just in time" # Holds variables. Variables are only loaded "just in time"
# and are not evaluated as part of the render stage # and are not evaluated as part of the render stage
@@ -10,11 +12,11 @@ module Liquid
# {{ user | link }} # {{ user | link }}
# #
class Variable class Variable
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om FILTER_MARKUP_REGEX = /#{FILTER_SEPARATOR}\s*(.*)/om.freeze
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o FILTER_PARSER = /(?:\s+|#{QUOTED_FRAGMENT}|#{ARGUMENT_SEPARATOR})+/o.freeze
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o FILTER_ARGS_REGEX = /(?:#{FILTER_ARGUMENT_SEPARATOR}|#{ARGUMENT_SEPARATOR})\s*((?:\w+\s*\:\s*)?#{QUOTED_FRAGMENT})/o.freeze
JustTagAttributes = /\A#{TagAttributes}\z/o JUST_TAG_ATTRIBUTES = /\A#{TAG_ATTRIBUTES}\z/o.freeze
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om MARKUP_WITH_QUOTED_FRAGMENT = /(#{QUOTED_FRAGMENT})(.*)/om.freeze
attr_accessor :filters, :name, :line_number attr_accessor :filters, :name, :line_number
attr_reader :parse_context attr_reader :parse_context
@@ -41,17 +43,18 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
@filters = [] @filters = []
return unless markup =~ MarkupWithQuotedFragment return unless markup =~ MARKUP_WITH_QUOTED_FRAGMENT
name_markup = $1 name_markup = Regexp.last_match(1)
filter_markup = $2 filter_markup = Regexp.last_match(2)
@name = Expression.parse(name_markup) @name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex if filter_markup =~ FILTER_MARKUP_REGEX
filters = $1.scan(FilterParser) filters = Regexp.last_match(1).scan(FILTER_PARSER)
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(FilterArgsRegex).flatten filterargs = f.scan(FILTER_ARGS_REGEX).flatten
@filters << parse_filter_expressions(filtername, filterargs) @filters << parse_filter_expressions(filtername, filterargs)
end end
end end
@@ -85,19 +88,30 @@ module Liquid
end end
obj = context.apply_global_filter(obj) obj = context.apply_global_filter(obj)
taint_check(context, obj) taint_check(context, obj)
obj obj
end end
def render_to_output_buffer(context, output)
obj = render(context)
if obj.is_a?(Array)
output << obj.join
elsif obj.nil?
else
output << obj.to_s
end
output
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(JustTagAttributes) if (matches = a.match(JUST_TAG_ATTRIBUTES))
keyword_args ||= {} keyword_args ||= {}
keyword_args[matches[1]] = Expression.parse(matches[2]) keyword_args[matches[1]] = Expression.parse(matches[2])
else else
@@ -125,7 +139,7 @@ module Liquid
return unless obj.tainted? return unless obj.tainted?
return if Template.taint_mode == :lax return if Template.taint_mode == :lax
@markup =~ QuotedFragment @markup =~ QUOTED_FRAGMENT
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

@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Liquid module Liquid
class VariableLookup class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m SQUARE_BRACKETED = /\A\[(.*)\]\z/m.freeze
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze].freeze COMMAND_METHODS = %w[size first last].freeze
attr_reader :name, :lookups attr_reader :name, :lookups
@@ -10,12 +12,10 @@ module Liquid
end end
def initialize(markup) def initialize(markup)
lookups = markup.scan(VariableParser) lookups = markup.scan(VARIABLE_PARSER)
name = lookups.shift name = lookups.shift
if name =~ SQUARE_BRACKETED name = Expression.parse(Regexp.last_match(1)) if name =~ SQUARE_BRACKETED
name = Expression.parse($1)
end
@name = name @name = name
@lookups = lookups @lookups = lookups
@@ -24,7 +24,7 @@ module Liquid
@lookups.each_index do |i| @lookups.each_index do |i|
lookup = lookups[i] lookup = lookups[i]
if lookup =~ SQUARE_BRACKETED if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse($1) lookups[i] = Expression.parse(Regexp.last_match(1))
elsif COMMAND_METHODS.include?(lookup) elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i @command_flags |= 1 << i
end end
@@ -59,6 +59,7 @@ 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,5 @@
# encoding: utf-8 # frozen_string_literal: true
module Liquid module Liquid
VERSION = "4.0.3".freeze VERSION = '4.0.3'
end end

View File

@@ -1,31 +1,31 @@
# encoding: utf-8 # frozen_string_literal: true
lib = File.expand_path('../lib/', __FILE__) lib = File.expand_path('lib', __dir__)
$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.1.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 'rake', '~> 11.3' s.add_development_dependency('minitest')
s.add_development_dependency 'minitest' s.add_development_dependency('rake', '~> 11.3')
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'benchmark/ips' require 'benchmark/ips'
require_relative 'theme_runner' require_relative 'theme_runner'
@@ -12,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

@@ -22,5 +22,5 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
profile("Parsing") { profiler.compile } profile('Parsing') { profiler.compile }
profile("Rendering") { profiler.render } profile('Rendering') { profiler.render }

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'stackprof' require 'stackprof'
require_relative 'theme_runner' require_relative 'theme_runner'
@@ -5,7 +7,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
profiler.run profiler.run
[:cpu, :object].each do |profile_type| %i[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
@@ -13,12 +15,12 @@ profiler.run
end end
end end
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME'] if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME'])
File.open(graph_filename, 'w') do |f| File.open(graph_filename, 'w') do |f|
StackProf::Report.new(results).print_graphviz(nil, f) StackProf::Report.new(results).print_graphviz(nil, f)
end end
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,29 +1,33 @@
# frozen_string_literal: true
class CommentForm < Liquid::Block class CommentForm < Liquid::Block
Syntax = /(#{Liquid::VariableSignature}+)/ SYNTAX = /(#{Liquid::VARIABLE_SIGNATURE}+)/.freeze
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ SYNTAX
@variable_name = $1 @variable_name = Regexp.last_match(1)
@attributes = {} @attributes = {}
else else
raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]") raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]"
end end
end end
def render(context) def render_to_output_buffer(context, output)
article = context[@variable_name] article = context[@variable_name]
context.stack('form') do context.stack do
context['form'] = { context['form'] = {
'posted_successfully?' => context.registers[:posted_successfully], 'posted_successfully?' => context.registers[:posted_successfully],
'errors' => context['comment.errors'], 'errors' => context['comment.errors'],
'author' => context['comment.author'], 'author' => context['comment.author'],
'email' => context['comment.email'], 'email' => context['comment.email'],
'body' => context['comment.body'] 'body' => context['comment.body'],
} }
wrap_in_form(article, render_all(@nodelist, context))
output << wrap_in_form(article, render_all(@nodelist, context, output))
output
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'yaml' require 'yaml'
module Database module Database
@@ -16,8 +18,11 @@ module Database
end end
# key the tables by handles, as this is how liquid expects it. # key the tables by handles, as this is how liquid expects it.
db = db.inject({}) do |assigns, (key, values)| db = db.each_with_object({}) do |(key, values), assigns|
assigns[key] = values.inject({}) { |h, v| h[v['handle']] = v; h; } assigns[key] = values.each_with_object({}) do |v, h|
h[v['handle']] = v
h
end
assigns assigns
end end
@@ -29,9 +34,9 @@ module Database
db['article'] = db['blog']['articles'].first db['article'] = db['blog']['articles'].first
db['cart'] = { db['cart'] = {
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] }, 'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] },
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] }, 'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] },
'items' => db['line_items'].values 'items' => db['line_items'].values,
} }
db db
@@ -40,6 +45,6 @@ module Database
end end
if __FILE__ == $PROGRAM_NAME if __FILE__ == $PROGRAM_NAME
p Database.tables['collections']['frontpage'].keys p(Database.tables['collections']['frontpage'].keys)
# p Database.tables['blog']['articles'] # p Database.tables['blog']['articles']
end end

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
require 'json' 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

@@ -1,4 +1,6 @@
$:.unshift __dir__ + '/../../lib' # frozen_string_literal: true
$LOAD_PATH.unshift(__dir__ + '/../../lib')
require_relative '../../lib/liquid' require_relative '../../lib/liquid'
require_relative 'comment_form' require_relative 'comment_form'
@@ -9,11 +11,11 @@ require_relative 'shop_filter'
require_relative 'tag_filter' require_relative 'tag_filter'
require_relative 'weight_filter' require_relative 'weight_filter'
Liquid::Template.register_tag 'paginate', Paginate Liquid::Template.register_tag('paginate', Paginate)
Liquid::Template.register_tag 'form', CommentForm Liquid::Template.register_tag('form', CommentForm)
Liquid::Template.register_filter JsonFilter Liquid::Template.register_filter(JsonFilter)
Liquid::Template.register_filter MoneyFilter Liquid::Template.register_filter(MoneyFilter)
Liquid::Template.register_filter WeightFilter Liquid::Template.register_filter(WeightFilter)
Liquid::Template.register_filter ShopFilter Liquid::Template.register_filter(ShopFilter)
Liquid::Template.register_filter TagFilter Liquid::Template.register_filter(TagFilter)

View File

@@ -1,12 +1,16 @@
# frozen_string_literal: true
module MoneyFilter module MoneyFilter
def money_with_currency(money) def money_with_currency(money)
return '' if money.nil? return '' if money.nil?
sprintf("$ %.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?
sprintf("$ %.2f", money / 100.0)
format('$ %.2f', money / 100.0)
end end
private private

View File

@@ -1,49 +1,51 @@
# frozen_string_literal: true
class Paginate < Liquid::Block class Paginate < Liquid::Block
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/ SYNTAX = /(#{Liquid::QUOTED_FRAGMENT})\s*(by\s*(\d+))?/.freeze
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ SYNTAX
@collection_name = $1 @collection_name = Regexp.last_match(1)
@page_size = if $2 @page_size = if Regexp.last_match(2)
$3.to_i Regexp.last_match(3).to_i
else else
20 20
end end
@attributes = { 'window_size' => 3 } @attributes = { 'window_size' => 3 }
markup.scan(Liquid::TagAttributes) do |key, value| markup.scan(Liquid::TAG_ATTRIBUTES) do |key, value|
@attributes[key] = value @attributes[key] = value
end end
else else
raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number") raise SyntaxError, "Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number"
end end
end end
def render(context) def render_to_output_buffer(context, output)
@context = context @context = context
context.stack('paginate') do context.stack do
current_page = context['current_page'].to_i current_page = context['current_page'].to_i
pagination = { pagination = {
'page_size' => @page_size, 'page_size' => @page_size,
'current_page' => 5, 'current_page' => 5,
'current_offset' => @page_size * 5 'current_offset' => @page_size * 5,
} }
context['paginate'] = pagination context['paginate'] = pagination
collection_size = context[@collection_name].size collection_size = context[@collection_name].size
raise ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection_size.nil? raise ArgumentError, "Cannot paginate array '#{@collection_name}'. Not found." if collection_size.nil?
page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1 page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
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 1 >= current_page pagination['previous'] = link('&laquo; Previous', current_page - 1) unless current_page <= 1
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'] = []
@@ -59,6 +61,7 @@ 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
@@ -85,6 +88,6 @@ class Paginate < Liquid::Block
end end
def current_url def current_url
"/collections/frontpage" '/collections/frontpage'
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module ShopFilter module ShopFilter
def asset_url(input) def asset_url(input)
"/files/1/[shop_id]/[shop_id]/assets/#{input}" "/files/1/[shop_id]/[shop_id]/assets/#{input}"
@@ -15,15 +17,15 @@ 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
@@ -52,15 +54,13 @@ module ShopFilter
end end
def product_img_url(url, style = 'small') def product_img_url(url, style = 'small')
unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/ raise ArgumentError, 'filter "size" can only be called on product images' 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'
return '/files/shops/random_number/' + url return '/files/shops/random_number/' + url
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon' when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
"/files/shops/random_number/products/#{$1}_#{style}.#{$2}" "/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
else else
raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon ' raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon '
end end
@@ -70,16 +70,14 @@ module ShopFilter
html = [] html = []
html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous'] html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous']
for part in paginate['parts'] paginate['parts'].each do |part|
html << if part['is_link']
if part['is_link'] %(<span class="page">#{link_to(part['title'], part['url'])}</span>)
html << %(<span class="page">#{link_to(part['title'], part['url'])}</span>)
elsif part['title'].to_i == paginate['current_page'].to_i elsif part['title'].to_i == paginate['current_page'].to_i
html << %(<span class="page current">#{part['title']}</span>) %(<span class="page current">#{part['title']}</span>)
else else
html << %(<span class="deco">#{part['title']}</span>) %(<span class="deco">#{part['title']}</span>)
end end
end end
html << %(<span class="next">#{link_to(paginate['next']['title'], paginate['next']['url'])}</span>) if paginate['next'] html << %(<span class="next">#{link_to(paginate['next']['title'], paginate['next']['url'])}</span>) if paginate['next']

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module TagFilter module TagFilter
def link_to_tag(label, tag) def link_to_tag(label, tag)
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>" "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>"
@@ -13,11 +15,11 @@ module TagFilter
def link_to_add_tag(label, tag) def link_to_add_tag(label, tag)
tags = (@context['current_tags'] + [tag]).uniq tags = (@context['current_tags'] + [tag]).uniq
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>" "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
end end
def link_to_remove_tag(label, tag) def link_to_remove_tag(label, tag)
tags = (@context['current_tags'] - [tag]).uniq tags = (@context['current_tags'] - [tag]).uniq
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>" "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
end end
end end

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
module WeightFilter module WeightFilter
def weight(grams) def weight(grams)
sprintf("%.2f", grams / 1000) format('%.2f', grams / 1000)
end end
def weight_with_unit(grams) def weight_with_unit(grams)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
# This profiler run simulates Shopify. # This profiler run simulates Shopify.
# We are looking in the tests directory for liquid files and render them within the designated layout file. # We are looking in the tests directory for liquid files and render them within the designated layout file.
# We will also export a substantial database to liquid which the templates can render values of. # We will also export a substantial database to liquid which the templates can render values of.
@@ -31,7 +33,7 @@ class ThemeRunner
{ {
liquid: File.read(test), liquid: File.read(test),
layout: (File.file?(theme_path) ? File.read(theme_path) : nil), layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
template_name: test template_name: test,
} }
end.compact end.compact

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class AssignTest < Minitest::Test class AssignTest < Minitest::Test
@@ -10,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

@@ -1,11 +1,12 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class FoobarTag < Liquid::Tag class FoobarTag < Liquid::Tag
def render(*args) def render_to_output_buffer(_context, output)
" " output << ' '
output
end end
Liquid::Template.register_tag('foobar', FoobarTag)
end end
class BlankTestFileSystem class BlankTestFileSystem
@@ -31,76 +32,78 @@ class BlankTest < Minitest::Test
end end
def test_new_tags_are_not_blank_by_default def test_new_tags_are_not_blank_by_default
assert_template_result(" " * N, wrap_in_for("{% foobar %}")) with_custom_tag('foobar', FoobarTag) do
assert_template_result(' ' * N, wrap_in_for('{% foobar %}'))
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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class BlockTest < Minitest::Test class BlockTest < Minitest::Test
@@ -5,7 +7,7 @@ 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

View File

@@ -1,10 +1,12 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class CaptureTest < Minitest::Test 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
@@ -14,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
@@ -30,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
@@ -45,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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ContextTest < Minitest::Test class ContextTest < Minitest::Test

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class DocumentTest < Minitest::Test class DocumentTest < Minitest::Test
@@ -5,14 +7,14 @@ 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

View File

@@ -1,6 +1,16 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ContextDrop < Liquid::Drop class ContextDrop < Liquid::Drop
def scopes
@context.scopes.size
end
def scopes_as_array
(1..@context.scopes.size).to_a
end
def loop_pos def loop_pos
@context['forloop.index'] @context['forloop.index']
end end
@@ -13,7 +23,7 @@ end
class ProductDrop < Liquid::Drop class ProductDrop < Liquid::Drop
class TextDrop < Liquid::Drop class TextDrop < Liquid::Drop
def array def array
['text1', 'text2'] %w[text1 text2]
end end
def text def text
@@ -23,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.to_s
end end
end end
@@ -40,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
@@ -131,17 +141,17 @@ 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
@@ -165,12 +175,12 @@ class DropsTest < Minitest::Test
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 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
@@ -180,12 +190,37 @@ class DropsTest < Minitest::Test
end end
def test_object_methods_not_allowed def test_object_methods_not_allowed
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method| %i[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
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 '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
end
def test_scope_though_proc
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 '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
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"%}{%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)
end
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 '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])
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
@@ -199,7 +234,7 @@ class DropsTest < Minitest::Test
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
["select", "each", "map", "cycle"].each do |method| %w[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)
@@ -208,20 +243,20 @@ class DropsTest < Minitest::Test
end end
def test_some_enumerable_methods_still_get_invoked def test_some_enumerable_methods_still_get_invoked
[ :count, :max ].each do |method| %i[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)
[ :min, :first ].each do |method| %i[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
@@ -234,7 +269,7 @@ class DropsTest < Minitest::Test
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 end
end # DropsTest end # DropsTest

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ErrorHandlingTest < Minitest::Test class ErrorHandlingTest < Minitest::Test
@@ -83,15 +85,14 @@ class ErrorHandlingTest < Minitest::Test
def test_with_line_numbers_adds_numbers_to_parser_errors def test_with_line_numbers_adds_numbers_to_parser_errors
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q( Liquid::Template.parse('
foobar foobar
{% "cat" | foobar %} {% "cat" | foobar %}
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)
@@ -99,15 +100,14 @@ class ErrorHandlingTest < Minitest::Test
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q( Liquid::Template.parse('
foobar foobar
{%- "cat" | foobar -%} {%- "cat" | foobar -%}
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)
@@ -122,8 +122,7 @@ 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)
@@ -139,8 +138,7 @@ 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
@@ -157,8 +155,7 @@ 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
@@ -205,7 +202,7 @@ class ErrorHandlingTest < Minitest::Test
def test_default_exception_renderer_with_internal_error def test_default_exception_renderer_with_internal_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)
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)
@@ -214,10 +211,13 @@ class ErrorHandlingTest < Minitest::Test
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 = ->(e) { exceptions << e; '' } Liquid::Template.default_exception_renderer = lambda { |e|
exceptions << e
''
}
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}') template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
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)
@@ -228,7 +228,10 @@ 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 = ->(e) { exceptions << e; e.cause } handler = lambda { |e|
exceptions << e
e.cause
}
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler) output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
@@ -239,8 +242,8 @@ class ErrorHandlingTest < Minitest::Test
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
@@ -255,6 +258,6 @@ class ErrorHandlingTest < Minitest::Test
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

@@ -1,24 +1,26 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
module MoneyFilter module MoneyFilter
def money(input) def money(input)
sprintf(' %d$ ', input) format(' %d$ ', input)
end end
def money_with_underscore(input) def money_with_underscore(input)
sprintf(' %d$ ', input) format(' %d$ ', input)
end end
end end
module CanadianMoneyFilter module CanadianMoneyFilter
def money(input) def money(input)
sprintf(' %d$ CAD ', input) format(' %d$ CAD ', input)
end end
end end
module SubstituteFilter module SubstituteFilter
def substitute(input, params = {}) def substitute(input, params = {})
input.gsub(/%\{(\w+)\}/) { |match| params[$1] } input.gsub(/%\{(\w+)\}/) { |_match| params[Regexp.last_match(1)] }
end end
end end
@@ -26,8 +28,8 @@ class FiltersTest < Minitest::Test
include Liquid include Liquid
module OverrideObjectMethodFilter module OverrideObjectMethodFilter
def tap(input) def tap(_input)
"tap overridden" 'tap overridden'
end end
end end
@@ -39,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
@@ -53,43 +55,43 @@ 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'] = ['expected', 'as', 'alphabetic'] @context['words'] = %w[expected as alphabetic]
@context['arrays'] = ['flower', 'are'] @context['arrays'] = %w[flower are]
@context['case_sensitive'] = ['sensitive', 'Expected', 'case'] @context['case_sensitive'] = %w[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'] = ['case', 'Assert', 'Insensitive'] @context['words'] = %w[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)
@@ -104,7 +106,7 @@ 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)
@@ -114,27 +116,27 @@ class FiltersTest < Minitest::Test
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
@@ -146,10 +148,10 @@ class FiltersTest < Minitest::Test
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
@@ -158,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

@@ -1,15 +1,17 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class HashOrderingTest < Minitest::Test class HashOrderingTest < Minitest::Test
module MoneyFilter module MoneyFilter
def money(input) def money(input)
sprintf(' %d$ ', input) format(' %d$ ', input)
end end
end end
module CanadianMoneyFilter module CanadianMoneyFilter
def money(input) def money(input)
sprintf(' %d$ CAD ', input) format(' %d$ CAD ', input)
end end
end end
@@ -17,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

@@ -0,0 +1,13 @@
# 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

@@ -1,7 +1,9 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
module FunnyFilter module FunnyFilter
def make_funny(input) def make_funny(_input)
'LOL' 'LOL'
end end
@@ -9,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
@@ -32,7 +34,7 @@ class OutputTest < Minitest::Test
def setup def setup
@assigns = { @assigns = {
'best_cars' => 'bmw', 'best_cars' => 'bmw',
'car' => { 'bmw' => 'good', 'gm' => 'bad' } 'car' => { 'bmw' => 'good', 'gm' => 'bad' },
} }
end end
@@ -45,9 +47,9 @@ class OutputTest < Minitest::Test
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

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(
["test", "infilter"], %w[test infilter],
visit(%({{ test | split: infilter }})) visit(%({{ test | split: infilter }}))
) )
end end
def test_dynamic_variable def test_dynamic_variable
assert_equal( assert_equal(
["test", "inlookup"], %w[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

@@ -1,41 +1,43 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ParsingQuirksTest < Minitest::Test 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
@@ -51,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
@@ -77,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
@@ -112,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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class RenderProfilingTest < Minitest::Test class RenderProfilingTest < Minitest::Test
@@ -72,7 +74,7 @@ class RenderProfilingTest < Minitest::Test
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
@@ -89,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
@@ -99,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
@@ -114,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
@@ -128,7 +130,7 @@ class RenderProfilingTest < Minitest::Test
t.render! t.render!
timing_count = 0 timing_count = 0
t.profiler.each do |timing| t.profiler.each do |_timing|
timing_count += 1 timing_count += 1
end end
@@ -136,7 +138,7 @@ class RenderProfilingTest < Minitest::Test
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
@@ -144,8 +146,8 @@ class RenderProfilingTest < Minitest::Test
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" => ["one", "two"] }) t.render!('collection' => %w[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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
module SecurityFilter module SecurityFilter
@@ -57,22 +59,22 @@ class SecurityTest < Minitest::Test
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

View File

@@ -1,4 +1,4 @@
# encoding: utf-8 # frozen_string_literal: true
require 'test_helper' require 'test_helper'
@@ -17,7 +17,7 @@ class TestThing
"woot: #{@foo}" "woot: #{@foo}"
end end
def [](whatever) def [](_whatever)
to_s to_s
end end
@@ -29,7 +29,7 @@ end
class TestDrop < Liquid::Drop class TestDrop < Liquid::Drop
def test def test
"testfoo" 'testfoo'
end end
end end
@@ -37,7 +37,7 @@ class TestEnumerable < Liquid::Drop
include Enumerable include Enumerable
def each(&block) def each(&block)
[ { "foo" => 1, "bar" => 2 }, { "foo" => 2, "bar" => 1 }, { "foo" => 3, "bar" => 3 } ].each(&block) [{ 'foo' => 1, 'bar' => 2 }, { 'foo' => 2, 'bar' => 1 }, { 'foo' => 3, 'bar' => 3 }].each(&block)
end end
end end
@@ -65,12 +65,12 @@ class StandardFiltersTest < Minitest::Test
end end
def test_downcase def test_downcase
assert_equal 'testing', @filters.downcase("Testing") assert_equal 'testing', @filters.downcase('Testing')
assert_equal '', @filters.downcase(nil) assert_equal '', @filters.downcase(nil)
end end
def test_upcase def test_upcase
assert_equal 'TESTING', @filters.upcase("Testing") assert_equal 'TESTING', @filters.upcase('Testing')
assert_equal '', @filters.upcase(nil) assert_equal '', @filters.upcase(nil)
end end
@@ -91,22 +91,22 @@ class StandardFiltersTest < Minitest::Test
@filters.slice('foobar', nil) @filters.slice('foobar', nil)
end end
assert_raises(Liquid::ArgumentError) do assert_raises(Liquid::ArgumentError) do
@filters.slice('foobar', 0, "") @filters.slice('foobar', 0, '')
end end
end end
def test_slice_on_arrays def test_slice_on_arrays
input = 'foobar'.split(//) input = 'foobar'.split(//)
assert_equal %w(o o b), @filters.slice(input, 1, 3) assert_equal %w[o o b], @filters.slice(input, 1, 3)
assert_equal %w(o o b a r), @filters.slice(input, 1, 1000) assert_equal %w[o o b a r], @filters.slice(input, 1, 1000)
assert_equal %w(), @filters.slice(input, 1, 0) assert_equal %w[], @filters.slice(input, 1, 0)
assert_equal %w(o), @filters.slice(input, 1, 1) assert_equal %w[o], @filters.slice(input, 1, 1)
assert_equal %w(b a r), @filters.slice(input, 3, 3) assert_equal %w[b a r], @filters.slice(input, 3, 3)
assert_equal %w(a r), @filters.slice(input, -2, 2) assert_equal %w[a r], @filters.slice(input, -2, 2)
assert_equal %w(a r), @filters.slice(input, -2, 1000) assert_equal %w[a r], @filters.slice(input, -2, 1000)
assert_equal %w(r), @filters.slice(input, -1) assert_equal %w[r], @filters.slice(input, -1)
assert_equal %w(), @filters.slice(input, 100, 10) assert_equal %w[], @filters.slice(input, 100, 10)
assert_equal %w(), @filters.slice(input, -100, 10) assert_equal %w[], @filters.slice(input, -100, 10)
end end
def test_truncate def test_truncate
@@ -114,16 +114,16 @@ class StandardFiltersTest < Minitest::Test
assert_equal '1234567890', @filters.truncate('1234567890', 20) assert_equal '1234567890', @filters.truncate('1234567890', 20)
assert_equal '...', @filters.truncate('1234567890', 0) assert_equal '...', @filters.truncate('1234567890', 0)
assert_equal '1234567890', @filters.truncate('1234567890') assert_equal '1234567890', @filters.truncate('1234567890')
assert_equal "测试...", @filters.truncate("测试测试测试测试", 5) assert_equal '测试...', @filters.truncate('测试测试测试测试', 5)
assert_equal '12341', @filters.truncate("1234567890", 5, 1) assert_equal '12341', @filters.truncate('1234567890', 5, 1)
end end
def test_split def test_split
assert_equal ['12', '34'], @filters.split('12~34', '~') assert_equal %w[12 34], @filters.split('12~34', '~')
assert_equal ['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~') assert_equal ['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~')
assert_equal ['A?Z'], @filters.split('A?Z', '~') assert_equal ['A?Z'], @filters.split('A?Z', '~')
assert_equal [], @filters.split(nil, ' ') assert_equal [], @filters.split(nil, ' ')
assert_equal ['A', 'Z'], @filters.split('A1Z', 1) assert_equal %w[A Z], @filters.split('A1Z', 1)
end end
def test_escape def test_escape
@@ -169,12 +169,12 @@ class StandardFiltersTest < Minitest::Test
assert_equal 'one two...', @filters.truncatewords('one two three', 2) assert_equal 'one two...', @filters.truncatewords('one two three', 2)
assert_equal 'one two three', @filters.truncatewords('one two three') assert_equal 'one two three', @filters.truncatewords('one two three')
assert_equal 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15) assert_equal 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15)
assert_equal "测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5) assert_equal '测试测试测试测试', @filters.truncatewords('测试测试测试测试', 5)
assert_equal 'one two1', @filters.truncatewords("one two three", 2, 1) assert_equal 'one two1', @filters.truncatewords('one two three', 2, 1)
end end
def test_strip_html def test_strip_html
assert_equal 'test', @filters.strip_html("<div>test</div>") assert_equal 'test', @filters.strip_html('<div>test</div>')
assert_equal 'test', @filters.strip_html("<div id='test'>test</div>") assert_equal 'test', @filters.strip_html("<div id='test'>test</div>")
assert_equal '', @filters.strip_html("<script type='text/javascript'>document.write('some stuff');</script>") assert_equal '', @filters.strip_html("<script type='text/javascript'>document.write('some stuff');</script>")
assert_equal '', @filters.strip_html("<style type='text/css'>foo bar</style>") assert_equal '', @filters.strip_html("<style type='text/css'>foo bar</style>")
@@ -183,7 +183,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal '', @filters.strip_html(nil) assert_equal '', @filters.strip_html(nil)
# Quirk of the existing implementation # Quirk of the existing implementation
assert_equal 'foo;', @filters.strip_html("<<<script </script>script>foo;</script>") assert_equal 'foo;', @filters.strip_html('<<<script </script>script>foo;</script>')
end end
def test_join def test_join
@@ -194,163 +194,163 @@ class StandardFiltersTest < Minitest::Test
def test_sort def test_sort
assert_equal [1, 2, 3, 4], @filters.sort([4, 3, 2, 1]) assert_equal [1, 2, 3, 4], @filters.sort([4, 3, 2, 1])
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a") assert_equal [{ 'a' => 1 }, { 'a' => 2 }, { 'a' => 3 }, { 'a' => 4 }], @filters.sort([{ 'a' => 4 }, { 'a' => 3 }, { 'a' => 1 }, { 'a' => 2 }], 'a')
end end
def test_sort_with_nils def test_sort_with_nils
assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1]) assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1])
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a") assert_equal [{ 'a' => 1 }, { 'a' => 2 }, { 'a' => 3 }, { 'a' => 4 }, {}], @filters.sort([{ 'a' => 4 }, { 'a' => 3 }, {}, { 'a' => 1 }, { 'a' => 2 }], 'a')
end end
def test_sort_when_property_is_sometimes_missing_puts_nils_last def test_sort_when_property_is_sometimes_missing_puts_nils_last
input = [ input = [
{ "price" => 4, "handle" => "alpha" }, { 'price' => 4, 'handle' => 'alpha' },
{ "handle" => "beta" }, { 'handle' => 'beta' },
{ "price" => 1, "handle" => "gamma" }, { 'price' => 1, 'handle' => 'gamma' },
{ "handle" => "delta" }, { 'handle' => 'delta' },
{ "price" => 2, "handle" => "epsilon" } { 'price' => 2, 'handle' => 'epsilon' },
] ]
expectation = [ expectation = [
{ "price" => 1, "handle" => "gamma" }, { 'price' => 1, 'handle' => 'gamma' },
{ "price" => 2, "handle" => "epsilon" }, { 'price' => 2, 'handle' => 'epsilon' },
{ "price" => 4, "handle" => "alpha" }, { 'price' => 4, 'handle' => 'alpha' },
{ "handle" => "delta" }, { 'handle' => 'delta' },
{ "handle" => "beta" } { 'handle' => 'beta' },
] ]
assert_equal expectation, @filters.sort(input, "price") assert_equal expectation, @filters.sort(input, 'price')
end end
def test_sort_natural def test_sort_natural
assert_equal ["a", "B", "c", "D"], @filters.sort_natural(["c", "D", "a", "B"]) assert_equal %w[a B c D], @filters.sort_natural(%w[c D a B])
assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, { "a" => "a" }, { "a" => "B" }], "a") assert_equal [{ 'a' => 'a' }, { 'a' => 'B' }, { 'a' => 'c' }, { 'a' => 'D' }], @filters.sort_natural([{ 'a' => 'D' }, { 'a' => 'c' }, { 'a' => 'a' }, { 'a' => 'B' }], 'a')
end end
def test_sort_natural_with_nils def test_sort_natural_with_nils
assert_equal ["a", "B", "c", "D", nil], @filters.sort_natural([nil, "c", "D", "a", "B"]) assert_equal ['a', 'B', 'c', 'D', nil], @filters.sort_natural([nil, 'c', 'D', 'a', 'B'])
assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }, {}], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, {}, { "a" => "a" }, { "a" => "B" }], "a") assert_equal [{ 'a' => 'a' }, { 'a' => 'B' }, { 'a' => 'c' }, { 'a' => 'D' }, {}], @filters.sort_natural([{ 'a' => 'D' }, { 'a' => 'c' }, {}, { 'a' => 'a' }, { 'a' => 'B' }], 'a')
end end
def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last
input = [ input = [
{ "price" => "4", "handle" => "alpha" }, { 'price' => '4', 'handle' => 'alpha' },
{ "handle" => "beta" }, { 'handle' => 'beta' },
{ "price" => "1", "handle" => "gamma" }, { 'price' => '1', 'handle' => 'gamma' },
{ "handle" => "delta" }, { 'handle' => 'delta' },
{ "price" => 2, "handle" => "epsilon" } { 'price' => 2, 'handle' => 'epsilon' },
] ]
expectation = [ expectation = [
{ "price" => "1", "handle" => "gamma" }, { 'price' => '1', 'handle' => 'gamma' },
{ "price" => 2, "handle" => "epsilon" }, { 'price' => 2, 'handle' => 'epsilon' },
{ "price" => "4", "handle" => "alpha" }, { 'price' => '4', 'handle' => 'alpha' },
{ "handle" => "delta" }, { 'handle' => 'delta' },
{ "handle" => "beta" } { 'handle' => 'beta' },
] ]
assert_equal expectation, @filters.sort_natural(input, "price") assert_equal expectation, @filters.sort_natural(input, 'price')
end end
def test_sort_natural_case_check def test_sort_natural_case_check
input = [ input = [
{ "key" => "X" }, { 'key' => 'X' },
{ "key" => "Y" }, { 'key' => 'Y' },
{ "key" => "Z" }, { 'key' => 'Z' },
{ "fake" => "t" }, { 'fake' => 't' },
{ "key" => "a" }, { 'key' => 'a' },
{ "key" => "b" }, { 'key' => 'b' },
{ "key" => "c" } { 'key' => 'c' },
] ]
expectation = [ expectation = [
{ "key" => "a" }, { 'key' => 'a' },
{ "key" => "b" }, { 'key' => 'b' },
{ "key" => "c" }, { 'key' => 'c' },
{ "key" => "X" }, { 'key' => 'X' },
{ "key" => "Y" }, { 'key' => 'Y' },
{ "key" => "Z" }, { 'key' => 'Z' },
{ "fake" => "t" } { 'fake' => 't' },
] ]
assert_equal expectation, @filters.sort_natural(input, "key") assert_equal expectation, @filters.sort_natural(input, 'key')
assert_equal ["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"]) assert_equal %w[a b c X Y Z], @filters.sort_natural(%w[X Y Z a b c])
end end
def test_sort_empty_array def test_sort_empty_array
assert_equal [], @filters.sort([], "a") assert_equal [], @filters.sort([], 'a')
end end
def test_sort_invalid_property def test_sort_invalid_property
foo = [ foo = [
[1], [1],
[2], [2],
[3] [3],
] ]
assert_raises Liquid::ArgumentError do assert_raises Liquid::ArgumentError do
@filters.sort(foo, "bar") @filters.sort(foo, 'bar')
end end
end end
def test_sort_natural_empty_array def test_sort_natural_empty_array
assert_equal [], @filters.sort_natural([], "a") assert_equal [], @filters.sort_natural([], 'a')
end end
def test_sort_natural_invalid_property def test_sort_natural_invalid_property
foo = [ foo = [
[1], [1],
[2], [2],
[3] [3],
] ]
assert_raises Liquid::ArgumentError do assert_raises Liquid::ArgumentError do
@filters.sort_natural(foo, "bar") @filters.sort_natural(foo, 'bar')
end end
end end
def test_legacy_sort_hash def test_legacy_sort_hash
assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 }) assert_equal [{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2)
end end
def test_numerical_vs_lexicographical_sort def test_numerical_vs_lexicographical_sort
assert_equal [2, 10], @filters.sort([10, 2]) assert_equal [2, 10], @filters.sort([10, 2])
assert_equal [{ "a" => 2 }, { "a" => 10 }], @filters.sort([{ "a" => 10 }, { "a" => 2 }], "a") assert_equal [{ 'a' => 2 }, { 'a' => 10 }], @filters.sort([{ 'a' => 10 }, { 'a' => 2 }], 'a')
assert_equal ["10", "2"], @filters.sort(["10", "2"]) assert_equal %w[10 2], @filters.sort(%w[10 2])
assert_equal [{ "a" => "10" }, { "a" => "2" }], @filters.sort([{ "a" => "10" }, { "a" => "2" }], "a") assert_equal [{ 'a' => '10' }, { 'a' => '2' }], @filters.sort([{ 'a' => '10' }, { 'a' => '2' }], 'a')
end end
def test_uniq def test_uniq
assert_equal ["foo"], @filters.uniq("foo") assert_equal ['foo'], @filters.uniq('foo')
assert_equal [1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1]) assert_equal [1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1])
assert_equal [{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a") assert_equal [{ 'a' => 1 }, { 'a' => 3 }, { 'a' => 2 }], @filters.uniq([{ 'a' => 1 }, { 'a' => 3 }, { 'a' => 1 }, { 'a' => 2 }], 'a')
testdrop = TestDrop.new testdrop = TestDrop.new
assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test') assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test')
end end
def test_uniq_empty_array def test_uniq_empty_array
assert_equal [], @filters.uniq([], "a") assert_equal [], @filters.uniq([], 'a')
end end
def test_uniq_invalid_property def test_uniq_invalid_property
foo = [ foo = [
[1], [1],
[2], [2],
[3] [3],
] ]
assert_raises Liquid::ArgumentError do assert_raises Liquid::ArgumentError do
@filters.uniq(foo, "bar") @filters.uniq(foo, 'bar')
end end
end end
def test_compact_empty_array def test_compact_empty_array
assert_equal [], @filters.compact([], "a") assert_equal [], @filters.compact([], 'a')
end end
def test_compact_invalid_property def test_compact_invalid_property
foo = [ foo = [
[1], [1],
[2], [2],
[3] [3],
] ]
assert_raises Liquid::ArgumentError do assert_raises Liquid::ArgumentError do
@filters.compact(foo, "bar") @filters.compact(foo, 'bar')
end end
end end
@@ -363,71 +363,71 @@ class StandardFiltersTest < Minitest::Test
end end
def test_map def test_map
assert_equal [1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a') assert_equal [1, 2, 3, 4], @filters.map([{ 'a' => 1 }, { 'a' => 2 }, { 'a' => 3 }, { 'a' => 4 }], 'a')
assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}", assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}",
'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }] 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }]
end end
def test_map_doesnt_call_arbitrary_stuff def test_map_doesnt_call_arbitrary_stuff
assert_template_result "", '{{ "foo" | map: "__id__" }}' assert_template_result '', '{{ "foo" | map: "__id__" }}'
assert_template_result "", '{{ "foo" | map: "inspect" }}' assert_template_result '', '{{ "foo" | map: "inspect" }}'
end end
def test_map_calls_to_liquid def test_map_calls_to_liquid
t = TestThing.new t = TestThing.new
assert_template_result "woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t] assert_template_result 'woot: 1', '{{ foo | map: "whatever" }}', 'foo' => [t]
end end
def test_map_on_hashes def test_map_on_hashes
assert_template_result "4217", '{{ thing | map: "foo" | map: "bar" }}', assert_template_result '4217', '{{ thing | map: "foo" | map: "bar" }}',
"thing" => { "foo" => [ { "bar" => 42 }, { "bar" => 17 } ] } 'thing' => { 'foo' => [{ 'bar' => 42 }, { 'bar' => 17 }] }
end end
def test_legacy_map_on_hashes_with_dynamic_key def test_legacy_map_on_hashes_with_dynamic_key
template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}" template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}"
hash = { "foo" => { "bar" => 42 } } hash = { 'foo' => { 'bar' => 42 } }
assert_template_result "42", template, "thing" => hash assert_template_result '42', template, 'thing' => hash
end end
def test_sort_calls_to_liquid def test_sort_calls_to_liquid
t = TestThing.new t = TestThing.new
Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t]) Liquid::Template.parse('{{ foo | sort: "whatever" }}').render('foo' => [t])
assert t.foo > 0 assert t.foo.positive?
end end
def test_map_over_proc def test_map_over_proc
drop = TestDrop.new drop = TestDrop.new
p = proc { drop } p = proc { drop }
templ = '{{ procs | map: "test" }}' templ = '{{ procs | map: "test" }}'
assert_template_result "testfoo", templ, "procs" => [p] assert_template_result 'testfoo', templ, 'procs' => [p]
end end
def test_map_over_drops_returning_procs def test_map_over_drops_returning_procs
drops = [ drops = [
{ {
"proc" => ->{ "foo" } 'proc' => -> { 'foo' },
}, },
{ {
"proc" => ->{ "bar" } 'proc' => -> { 'bar' },
} },
] ]
templ = '{{ drops | map: "proc" }}' templ = '{{ drops | map: "proc" }}'
assert_template_result "foobar", templ, "drops" => drops assert_template_result 'foobar', templ, 'drops' => drops
end end
def test_map_works_on_enumerables def test_map_works_on_enumerables
assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new assert_template_result '123', '{{ foo | map: "foo" }}', 'foo' => TestEnumerable.new
end end
def test_map_returns_empty_on_2d_input_array def test_map_returns_empty_on_2d_input_array
foo = [ foo = [
[1], [1],
[2], [2],
[3] [3],
] ]
assert_raises Liquid::ArgumentError do assert_raises Liquid::ArgumentError do
@filters.map(foo, "bar") @filters.map(foo, 'bar')
end end
end end
@@ -435,7 +435,7 @@ class StandardFiltersTest < Minitest::Test
foo = [ foo = [
[1], [1],
[2], [2],
[3] [3],
] ]
assert_raises Liquid::ArgumentError do assert_raises Liquid::ArgumentError do
@filters.map(foo, nil) @filters.map(foo, nil)
@@ -443,7 +443,7 @@ class StandardFiltersTest < Minitest::Test
end end
def test_sort_works_on_enumerables def test_sort_works_on_enumerables
assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new assert_template_result '213', '{{ foo | sort: "bar" | map: "foo" }}', 'foo' => TestEnumerable.new
end end
def test_first_and_last_call_to_liquid def test_first_and_last_call_to_liquid
@@ -452,37 +452,37 @@ class StandardFiltersTest < Minitest::Test
end end
def test_truncate_calls_to_liquid def test_truncate_calls_to_liquid
assert_template_result "wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new assert_template_result 'wo...', '{{ foo | truncate: 5 }}', 'foo' => TestThing.new
end end
def test_date def test_date
assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B") assert_equal 'May', @filters.date(Time.parse('2006-05-05 10:00:00'), '%B')
assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B") assert_equal 'June', @filters.date(Time.parse('2006-06-05 10:00:00'), '%B')
assert_equal 'July', @filters.date(Time.parse("2006-07-05 10:00:00"), "%B") assert_equal 'July', @filters.date(Time.parse('2006-07-05 10:00:00'), '%B')
assert_equal 'May', @filters.date("2006-05-05 10:00:00", "%B") assert_equal 'May', @filters.date('2006-05-05 10:00:00', '%B')
assert_equal 'June', @filters.date("2006-06-05 10:00:00", "%B") assert_equal 'June', @filters.date('2006-06-05 10:00:00', '%B')
assert_equal 'July', @filters.date("2006-07-05 10:00:00", "%B") assert_equal 'July', @filters.date('2006-07-05 10:00:00', '%B')
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") assert_equal '2006-07-05 10:00:00', @filters.date('2006-07-05 10:00:00', '')
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") assert_equal '2006-07-05 10:00:00', @filters.date('2006-07-05 10:00:00', '')
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") assert_equal '2006-07-05 10:00:00', @filters.date('2006-07-05 10:00:00', '')
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", nil) assert_equal '2006-07-05 10:00:00', @filters.date('2006-07-05 10:00:00', nil)
assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y") assert_equal '07/05/2006', @filters.date('2006-07-05 10:00:00', '%m/%d/%Y')
assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y") assert_equal '07/16/2004', @filters.date('Fri Jul 16 01:00:00 2004', '%m/%d/%Y')
assert_equal Date.today.year.to_s, @filters.date('now', '%Y') assert_equal Date.today.year.to_s, @filters.date('now', '%Y')
assert_equal Date.today.year.to_s, @filters.date('today', '%Y') assert_equal Date.today.year.to_s, @filters.date('today', '%Y')
assert_equal Date.today.year.to_s, @filters.date('Today', '%Y') assert_equal Date.today.year.to_s, @filters.date('Today', '%Y')
assert_nil @filters.date(nil, "%B") assert_nil @filters.date(nil, '%B')
assert_equal '', @filters.date('', "%B") assert_equal '', @filters.date('', '%B')
with_timezone("UTC") do with_timezone('UTC') do
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y") assert_equal '07/05/2006', @filters.date(1_152_098_955, '%m/%d/%Y')
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y") assert_equal '07/05/2006', @filters.date('1152098955', '%m/%d/%Y')
end end
end end
@@ -502,10 +502,10 @@ class StandardFiltersTest < Minitest::Test
end end
def test_remove def test_remove
assert_equal ' ', @filters.remove("a a a a", 'a') assert_equal ' ', @filters.remove('a a a a', 'a')
assert_equal ' ', @filters.remove("1 1 1 1", 1) assert_equal ' ', @filters.remove('1 1 1 1', 1)
assert_equal 'a a a', @filters.remove_first("a a a a", 'a ') assert_equal 'a a a', @filters.remove_first('a a a a', 'a ')
assert_equal ' 1 1 1', @filters.remove_first("1 1 1 1", 1) assert_equal ' 1 1 1', @filters.remove_first('1 1 1 1', 1)
assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}" assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}"
end end
@@ -514,148 +514,148 @@ class StandardFiltersTest < Minitest::Test
end end
def test_strip def test_strip
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " ab c " assert_template_result 'ab c', '{{ source | strip }}', 'source' => ' ab c '
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " \tab c \n \t" assert_template_result 'ab c', '{{ source | strip }}', 'source' => " \tab c \n \t"
end end
def test_lstrip def test_lstrip
assert_template_result 'ab c ', "{{ source | lstrip }}", 'source' => " ab c " assert_template_result 'ab c ', '{{ source | lstrip }}', 'source' => ' ab c '
assert_template_result "ab c \n \t", "{{ source | lstrip }}", 'source' => " \tab c \n \t" assert_template_result "ab c \n \t", '{{ source | lstrip }}', 'source' => " \tab c \n \t"
end end
def test_rstrip def test_rstrip
assert_template_result " ab c", "{{ source | rstrip }}", 'source' => " ab c " assert_template_result ' ab c', '{{ source | rstrip }}', 'source' => ' ab c '
assert_template_result " \tab c", "{{ source | rstrip }}", 'source' => " \tab c \n \t" assert_template_result " \tab c", '{{ source | rstrip }}', 'source' => " \tab c \n \t"
end end
def test_strip_newlines def test_strip_newlines
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc" assert_template_result 'abc', '{{ source | strip_newlines }}', 'source' => "a\nb\nc"
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc" assert_template_result 'abc', '{{ source | strip_newlines }}', 'source' => "a\r\nb\nc"
end end
def test_newlines_to_br def test_newlines_to_br
assert_template_result "a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc" assert_template_result "a<br />\nb<br />\nc", '{{ source | newline_to_br }}', 'source' => "a\nb\nc"
end end
def test_plus def test_plus
assert_template_result "2", "{{ 1 | plus:1 }}" assert_template_result '2', '{{ 1 | plus:1 }}'
assert_template_result "2.0", "{{ '1' | plus:'1.0' }}" assert_template_result '2.0', "{{ '1' | plus:'1.0' }}"
assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3) assert_template_result '5', "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
end end
def test_minus def test_minus
assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1 assert_template_result '4', '{{ input | minus:operand }}', 'input' => 5, 'operand' => 1
assert_template_result "2.3", "{{ '4.3' | minus:'2' }}" assert_template_result '2.3', "{{ '4.3' | minus:'2' }}"
assert_template_result "5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7) assert_template_result '5', "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)
end end
def test_abs def test_abs
assert_template_result "17", "{{ 17 | abs }}" assert_template_result '17', '{{ 17 | abs }}'
assert_template_result "17", "{{ -17 | abs }}" assert_template_result '17', '{{ -17 | abs }}'
assert_template_result "17", "{{ '17' | abs }}" assert_template_result '17', "{{ '17' | abs }}"
assert_template_result "17", "{{ '-17' | abs }}" assert_template_result '17', "{{ '-17' | abs }}"
assert_template_result "0", "{{ 0 | abs }}" assert_template_result '0', '{{ 0 | abs }}'
assert_template_result "0", "{{ '0' | abs }}" assert_template_result '0', "{{ '0' | abs }}"
assert_template_result "17.42", "{{ 17.42 | abs }}" assert_template_result '17.42', '{{ 17.42 | abs }}'
assert_template_result "17.42", "{{ -17.42 | abs }}" assert_template_result '17.42', '{{ -17.42 | abs }}'
assert_template_result "17.42", "{{ '17.42' | abs }}" assert_template_result '17.42', "{{ '17.42' | abs }}"
assert_template_result "17.42", "{{ '-17.42' | abs }}" assert_template_result '17.42', "{{ '-17.42' | abs }}"
end end
def test_times def test_times
assert_template_result "12", "{{ 3 | times:4 }}" assert_template_result '12', '{{ 3 | times:4 }}'
assert_template_result "0", "{{ 'foo' | times:4 }}" assert_template_result '0', "{{ 'foo' | times:4 }}"
assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}" assert_template_result '6', "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}"
assert_template_result "7.25", "{{ 0.0725 | times:100 }}" assert_template_result '7.25', '{{ 0.0725 | times:100 }}'
assert_template_result "-7.25", '{{ "-0.0725" | times:100 }}' assert_template_result '-7.25', '{{ "-0.0725" | times:100 }}'
assert_template_result "7.25", '{{ "-0.0725" | times: -100 }}' assert_template_result '7.25', '{{ "-0.0725" | times: -100 }}'
assert_template_result "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2) assert_template_result '4', '{{ price | times:2 }}', 'price' => NumberLikeThing.new(2)
end end
def test_divided_by def test_divided_by
assert_template_result "4", "{{ 12 | divided_by:3 }}" assert_template_result '4', '{{ 12 | divided_by:3 }}'
assert_template_result "4", "{{ 14 | divided_by:3 }}" assert_template_result '4', '{{ 14 | divided_by:3 }}'
assert_template_result "5", "{{ 15 | divided_by:3 }}" assert_template_result '5', '{{ 15 | divided_by:3 }}'
assert_equal "Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render assert_equal 'Liquid error: divided by 0', Template.parse('{{ 5 | divided_by:0 }}').render
assert_template_result "0.5", "{{ 2.0 | divided_by:4 }}" assert_template_result '0.5', '{{ 2.0 | divided_by:4 }}'
assert_raises(Liquid::ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}" assert_template_result '4', '{{ 1 | modulo: 0 }}'
end end
assert_template_result "5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10) assert_template_result '5', '{{ price | divided_by:2 }}', 'price' => NumberLikeThing.new(10)
end end
def test_modulo def test_modulo
assert_template_result "1", "{{ 3 | modulo:2 }}" assert_template_result '1', '{{ 3 | modulo:2 }}'
assert_raises(Liquid::ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}" assert_template_result '4', '{{ 1 | modulo: 0 }}'
end end
assert_template_result "1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3) assert_template_result '1', '{{ price | modulo:2 }}', 'price' => NumberLikeThing.new(3)
end end
def test_round def test_round
assert_template_result "5", "{{ input | round }}", 'input' => 4.6 assert_template_result '5', '{{ input | round }}', 'input' => 4.6
assert_template_result "4", "{{ '4.3' | round }}" assert_template_result '4', "{{ '4.3' | round }}"
assert_template_result "4.56", "{{ input | round: 2 }}", 'input' => 4.5612 assert_template_result '4.56', '{{ input | round: 2 }}', 'input' => 4.5612
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}" assert_template_result '4', '{{ 1.0 | divided_by: 0.0 | round }}'
end end
assert_template_result "5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6) assert_template_result '5', '{{ price | round }}', 'price' => NumberLikeThing.new(4.6)
assert_template_result "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3) assert_template_result '4', '{{ price | round }}', 'price' => NumberLikeThing.new(4.3)
end end
def test_ceil def test_ceil
assert_template_result "5", "{{ input | ceil }}", 'input' => 4.6 assert_template_result '5', '{{ input | ceil }}', 'input' => 4.6
assert_template_result "5", "{{ '4.3' | ceil }}" assert_template_result '5', "{{ '4.3' | ceil }}"
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}" assert_template_result '4', '{{ 1.0 | divided_by: 0.0 | ceil }}'
end end
assert_template_result "5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6) assert_template_result '5', '{{ price | ceil }}', 'price' => NumberLikeThing.new(4.6)
end end
def test_floor def test_floor
assert_template_result "4", "{{ input | floor }}", 'input' => 4.6 assert_template_result '4', '{{ input | floor }}', 'input' => 4.6
assert_template_result "4", "{{ '4.3' | floor }}" assert_template_result '4', "{{ '4.3' | floor }}"
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}" assert_template_result '4', '{{ 1.0 | divided_by: 0.0 | floor }}'
end end
assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4) assert_template_result '5', '{{ price | floor }}', 'price' => NumberLikeThing.new(5.4)
end end
def test_at_most def test_at_most
assert_template_result "4", "{{ 5 | at_most:4 }}" assert_template_result '4', '{{ 5 | at_most:4 }}'
assert_template_result "5", "{{ 5 | at_most:5 }}" assert_template_result '5', '{{ 5 | at_most:5 }}'
assert_template_result "5", "{{ 5 | at_most:6 }}" assert_template_result '5', '{{ 5 | at_most:6 }}'
assert_template_result "4.5", "{{ 4.5 | at_most:5 }}" assert_template_result '4.5', '{{ 4.5 | at_most:5 }}'
assert_template_result "5", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(6) assert_template_result '5', '{{ width | at_most:5 }}', 'width' => NumberLikeThing.new(6)
assert_template_result "4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4) assert_template_result '4', '{{ width | at_most:5 }}', 'width' => NumberLikeThing.new(4)
assert_template_result "4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4) assert_template_result '4', '{{ 5 | at_most: width }}', 'width' => NumberLikeThing.new(4)
end end
def test_at_least def test_at_least
assert_template_result "5", "{{ 5 | at_least:4 }}" assert_template_result '5', '{{ 5 | at_least:4 }}'
assert_template_result "5", "{{ 5 | at_least:5 }}" assert_template_result '5', '{{ 5 | at_least:5 }}'
assert_template_result "6", "{{ 5 | at_least:6 }}" assert_template_result '6', '{{ 5 | at_least:6 }}'
assert_template_result "5", "{{ 4.5 | at_least:5 }}" assert_template_result '5', '{{ 4.5 | at_least:5 }}'
assert_template_result "6", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(6) assert_template_result '6', '{{ width | at_least:5 }}', 'width' => NumberLikeThing.new(6)
assert_template_result "5", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(4) assert_template_result '5', '{{ width | at_least:5 }}', 'width' => NumberLikeThing.new(4)
assert_template_result "6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6) assert_template_result '6', '{{ 5 | at_least: width }}', 'width' => NumberLikeThing.new(6)
end end
def test_append def test_append
assigns = { 'a' => 'bc', 'b' => 'd' } assigns = { 'a' => 'bc', 'b' => 'd' }
assert_template_result('bcd', "{{ a | append: 'd'}}", assigns) assert_template_result('bcd', "{{ a | append: 'd'}}", assigns)
assert_template_result('bcd', "{{ a | append: b}}", assigns) assert_template_result('bcd', '{{ a | append: b}}', assigns)
end end
def test_concat def test_concat
@@ -663,7 +663,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a']) assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
assert_equal [1, 2, 10], @filters.concat([1, 2], [10]) assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do assert_raises(Liquid::ArgumentError, 'concat filter requires an array argument') do
@filters.concat([1, 2], 10) @filters.concat([1, 2], 10)
end end
end end
@@ -671,16 +671,16 @@ class StandardFiltersTest < Minitest::Test
def test_prepend def test_prepend
assigns = { 'a' => 'bc', 'b' => 'a' } assigns = { 'a' => 'bc', 'b' => 'a' }
assert_template_result('abc', "{{ a | prepend: 'a'}}", assigns) assert_template_result('abc', "{{ a | prepend: 'a'}}", assigns)
assert_template_result('abc', "{{ a | prepend: b}}", assigns) assert_template_result('abc', '{{ a | prepend: b}}', assigns)
end end
def test_default def test_default
assert_equal "foo", @filters.default("foo", "bar") assert_equal 'foo', @filters.default('foo', 'bar')
assert_equal "bar", @filters.default(nil, "bar") assert_equal 'bar', @filters.default(nil, 'bar')
assert_equal "bar", @filters.default("", "bar") assert_equal 'bar', @filters.default('', 'bar')
assert_equal "bar", @filters.default(false, "bar") assert_equal 'bar', @filters.default(false, 'bar')
assert_equal "bar", @filters.default([], "bar") assert_equal 'bar', @filters.default([], 'bar')
assert_equal "bar", @filters.default({}, "bar") assert_equal 'bar', @filters.default({}, 'bar')
end end
def test_cannot_access_private_methods def test_cannot_access_private_methods
@@ -694,74 +694,74 @@ class StandardFiltersTest < Minitest::Test
def test_where def test_where
input = [ input = [
{ "handle" => "alpha", "ok" => true }, { 'handle' => 'alpha', 'ok' => true },
{ "handle" => "beta", "ok" => false }, { 'handle' => 'beta', 'ok' => false },
{ "handle" => "gamma", "ok" => false }, { 'handle' => 'gamma', 'ok' => false },
{ "handle" => "delta", "ok" => true } { 'handle' => 'delta', 'ok' => true },
] ]
expectation = [ expectation = [
{ "handle" => "alpha", "ok" => true }, { 'handle' => 'alpha', 'ok' => true },
{ "handle" => "delta", "ok" => true } { 'handle' => 'delta', 'ok' => true },
] ]
assert_equal expectation, @filters.where(input, "ok", true) assert_equal expectation, @filters.where(input, 'ok', true)
assert_equal expectation, @filters.where(input, "ok") assert_equal expectation, @filters.where(input, 'ok')
end end
def test_where_no_key_set def test_where_no_key_set
input = [ input = [
{ "handle" => "alpha", "ok" => true }, { 'handle' => 'alpha', 'ok' => true },
{ "handle" => "beta" }, { 'handle' => 'beta' },
{ "handle" => "gamma" }, { 'handle' => 'gamma' },
{ "handle" => "delta", "ok" => true } { 'handle' => 'delta', 'ok' => true },
] ]
expectation = [ expectation = [
{ "handle" => "alpha", "ok" => true }, { 'handle' => 'alpha', 'ok' => true },
{ "handle" => "delta", "ok" => true } { 'handle' => 'delta', 'ok' => true },
] ]
assert_equal expectation, @filters.where(input, "ok", true) assert_equal expectation, @filters.where(input, 'ok', true)
assert_equal expectation, @filters.where(input, "ok") assert_equal expectation, @filters.where(input, 'ok')
end end
def test_where_non_array_map_input def test_where_non_array_map_input
assert_equal [{ "a" => "ok" }], @filters.where({ "a" => "ok" }, "a", "ok") assert_equal [{ 'a' => 'ok' }], @filters.where({ 'a' => 'ok' }, 'a', 'ok')
assert_equal [], @filters.where({ "a" => "not ok" }, "a", "ok") assert_equal [], @filters.where({ 'a' => 'not ok' }, 'a', 'ok')
end end
def test_where_indexable_but_non_map_value def test_where_indexable_but_non_map_value
assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok", true) } assert_raises(Liquid::ArgumentError) { @filters.where(1, 'ok', true) }
assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok") } assert_raises(Liquid::ArgumentError) { @filters.where(1, 'ok') }
end end
def test_where_non_boolean_value def test_where_non_boolean_value
input = [ input = [
{ "message" => "Bonjour!", "language" => "French" }, { 'message' => 'Bonjour!', 'language' => 'French' },
{ "message" => "Hello!", "language" => "English" }, { 'message' => 'Hello!', 'language' => 'English' },
{ "message" => "Hallo!", "language" => "German" } { 'message' => 'Hallo!', 'language' => 'German' },
] ]
assert_equal [{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French") assert_equal [{ 'message' => 'Bonjour!', 'language' => 'French' }], @filters.where(input, 'language', 'French')
assert_equal [{ "message" => "Hallo!", "language" => "German" }], @filters.where(input, "language", "German") assert_equal [{ 'message' => 'Hallo!', 'language' => 'German' }], @filters.where(input, 'language', 'German')
assert_equal [{ "message" => "Hello!", "language" => "English" }], @filters.where(input, "language", "English") assert_equal [{ 'message' => 'Hello!', 'language' => 'English' }], @filters.where(input, 'language', 'English')
end end
def test_where_array_of_only_unindexable_values def test_where_array_of_only_unindexable_values
assert_nil @filters.where([nil], "ok", true) assert_nil @filters.where([nil], 'ok', true)
assert_nil @filters.where([nil], "ok") assert_nil @filters.where([nil], 'ok')
end end
def test_where_no_target_value def test_where_no_target_value
input = [ input = [
{ "foo" => false }, { 'foo' => false },
{ "foo" => true }, { 'foo' => true },
{ "foo" => "for sure" }, { 'foo' => 'for sure' },
{ "bar" => true } { 'bar' => true },
] ]
assert_equal [{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo") assert_equal [{ 'foo' => true }, { 'foo' => 'for sure' }], @filters.where(input, 'foo')
end end
private private

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class BreakTagTest < Minitest::Test class BreakTagTest < Minitest::Test

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ContinueTagTest < Minitest::Test class ContinueTagTest < Minitest::Test

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ThingWithValue < Liquid::Drop class ThingWithValue < Liquid::Drop
@@ -23,7 +25,7 @@ class ForTagTest < Minitest::Test
yo yo
HERE HERE
template = <<HERE template = <<~HERE
{%for item in array%} {%for item in array%}
yo yo
{%endfor%} {%endfor%}
@@ -40,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' => ['a', 'b', 'c', 'd']) assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', 'array' => %w[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
@@ -114,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
@@ -128,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
@@ -225,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
@@ -265,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.
@@ -311,11 +313,11 @@ HERE
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%}' \
@@ -328,7 +330,7 @@ HERE
'{{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
@@ -352,7 +354,7 @@ HERE
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
@@ -368,23 +370,6 @@ HERE
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
end end
def test_overwriting_internal_variable
template = <<-HEREDOC
{% assign forloop = 'first' %}
{% for item in items %}
{{ forloop }}
{% assign forloop = 'second' %}
{{ forloop }}
{% endfor %}
{{ forloop }}
HEREDOC
result = Liquid::Template.parse(template).render('items' => '1')
assert_equal 'Liquid::ForloopDrop Liquid::ForloopDrop second', result.split.map(&:strip).join(' ')
end
class LoaderDrop < Liquid::Drop class LoaderDrop < Liquid::Drop
attr_accessor :each_called, :load_slice_called attr_accessor :each_called, :load_slice_called

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class IfElseTagTest < Minitest::Test class IfElseTagTest < Minitest::Test
@@ -49,11 +51,11 @@ 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
@@ -75,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' => [] })
@@ -90,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 })
@@ -166,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,
@@ -176,7 +178,7 @@ class IfElseTagTest < Minitest::Test
[false, true, true] => true, [false, true, true] => true,
[false, true, false] => false, [false, true, false] => false,
[false, false, true] => false, [false, false, true] => false,
[false, false, false] => false [false, false, false] => false,
} }
tests.each do |vals, expected| tests.each do |vals, expected|

View File

@@ -1,37 +1,39 @@
# frozen_string_literal: true
require 'test_helper' 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 'locale_variables'
"Locale: {{echo1}} {{echo2}}" 'Locale: {{echo1}} {{echo2}}'
when "variant" when 'variant'
"Variant: {{ variant.title }}" 'Variant: {{ variant.title }}'
when "nested_template" 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
@@ -40,14 +42,14 @@ class TestFileSystem
end end
class OtherFileSystem class OtherFileSystem
def read_template_file(template_path) def read_template_file(_template_path)
'from OtherFileSystem' 'from OtherFileSystem'
end end
end end
class CountingFileSystem class CountingFileSystem
attr_reader :count attr_reader :count
def read_template_file(template_path) def read_template_file(_template_path)
@count ||= 0 @count ||= 0
@count += 1 @count += 1
'from CountingFileSystem' 'from CountingFileSystem'
@@ -55,19 +57,19 @@ class CountingFileSystem
end end
class CustomInclude < Liquid::Tag class CustomInclude < Liquid::Tag
Syntax = /(#{Liquid::QuotedFragment}+)(\s+(?:with|for)\s+(#{Liquid::QuotedFragment}+))?/o SYNTAX = /(#{Liquid::QUOTED_FRAGMENT}+)(\s+(?:with|for)\s+(#{Liquid::QUOTED_FRAGMENT}+))?/o.freeze
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, tokens)
markup =~ Syntax markup =~ SYNTAX
@template_name = $1 @template_name = Regexp.last_match(1)
super super
end end
def parse(tokens) def parse(tokens); end
end
def render(context) def render_to_output_buffer(_context, output)
@template_name[1..-2] output << @template_name[1..-2]
output
end end
end end
@@ -84,56 +86,56 @@ class IncludeTagTest < Minitest::Test
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 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
infinite_file_system = Class.new do infinite_file_system = Class.new do
def read_template_file(template_path) def read_template_file(_template_path)
"-{% include 'loop' %}" "-{% include 'loop' %}"
end end
end end
@@ -146,11 +148,11 @@ 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
@@ -172,14 +174,14 @@ class IncludeTagTest < Minitest::Test
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
@@ -190,7 +192,7 @@ 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
@@ -207,16 +209,16 @@ class IncludeTagTest < Minitest::Test
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
@@ -232,11 +234,11 @@ 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
@@ -247,7 +249,7 @@ class IncludeTagTest < Minitest::Test
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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class IncrementTagTest < Minitest::Test class IncrementTagTest < Minitest::Test
@@ -13,11 +15,11 @@ class IncrementTagTest < Minitest::Test
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

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class RawTagTest < Minitest::Test class RawTagTest < Minitest::Test

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class StandardTagTest < Minitest::Test class StandardTagTest < Minitest::Test
@@ -69,7 +71,7 @@ class StandardTagTest < Minitest::Test
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
@@ -89,12 +91,12 @@ class StandardTagTest < Minitest::Test
'{% 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)
@@ -174,41 +176,41 @@ class StandardTagTest < Minitest::Test
# 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
code = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' code = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1)
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 2 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 2)
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 3 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 3)
assert_template_result(' its 4 ', code, { 'condition' => 4 }) assert_template_result(' its 4 ', code, 'condition' => 4)
assert_template_result('', code, { 'condition' => 5 }) assert_template_result('', code, 'condition' => 5)
code = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' code = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1)
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 'string' }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 'string')
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => nil }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => nil)
assert_template_result('', code, { 'condition' => 'something else' }) assert_template_result('', code, 'condition' => 'something else')
end end
def test_case_when_comma def test_case_when_comma
code = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' code = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1)
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 2 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 2)
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 3 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 3)
assert_template_result(' its 4 ', code, { 'condition' => 4 }) assert_template_result(' its 4 ', code, 'condition' => 4)
assert_template_result('', code, { 'condition' => 5 }) assert_template_result('', code, 'condition' => 5)
code = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' code = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}'
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 1 }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1)
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => 'string' }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 'string')
assert_template_result(' its 1 or 2 or 3 ', code, { 'condition' => nil }) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => nil)
assert_template_result('', code, { 'condition' => 'something else' }) assert_template_result('', code, 'condition' => 'something else')
end end
def test_assign def test_assign
@@ -260,19 +262,19 @@ class StandardTagTest < Minitest::Test
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

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