Compare commits

..

1 Commits

Author SHA1 Message Date
Samuel
fd5e3e87c7 Optimize range iteration
For loops support a range syntax for iterating a fixed number of times,
which looks like this.

```liquid
{% for i in range (1..100) %}
        ...
{% endfor %}
```

Previously, we converted these ranges to arrays using `to_a`, which
initialized an array containing each number in the range. Since all we
use these ranges for is iteration, this is far less efficient than
using a range iterator.

Doing this means that iterating over ranges now takes O(1) rather than O(n)
memory. See the PR for more benchmarks.

* Remove to_a cast on ranges
* Add ReversableRange iterator
* Add custom range-specific slicing logic
2019-07-09 10:57:39 -04:00
95 changed files with 1081 additions and 2426 deletions

1
.gitignore vendored
View File

@@ -7,4 +7,3 @@ pkg
.ruby-version .ruby-version
Gemfile.lock Gemfile.lock
.bundle .bundle
.byebug_history

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,132 @@
inherit_from: inherit_from:
- https://shopify.github.io/ruby-style-guide/rubocop.yml
- .rubocop_todo.yml - .rubocop_todo.yml
- ./.rubocop_todo.yml
require: rubocop-performance
Performance:
Enabled: true
AllCops: AllCops:
Exclude: Exclude:
- 'vendor/bundle/**/*' - 'performance/shopify/*'
- 'pkg/**'
Metrics/BlockNesting:
Max: 3
Metrics/ModuleLength:
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: Naming/MethodName:
Exclude: Exclude:
- 'example/server/liquid_servlet.rb' - 'example/server/liquid_servlet.rb'

View File

@@ -1,30 +1,36 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config` # `rubocop --auto-gen-config`
# on 2019-09-11 06:34:25 +1000 using RuboCop version 0.74.0. # 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 # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 2 # Offense count: 1
Lint/AmbiguousOperator: # Cop supports --auto-correct.
# Configuration parameters: Include, TreatCommentsAsGroupSeparators.
# Include: **/*.gemspec
Gemspec/OrderedDependencies:
Exclude: Exclude:
- 'test/unit/condition_unit_test.rb' - 'liquid.gemspec'
# Offense count: 21 # Offense count: 5
# Configuration parameters: AllowSafeAssignment. # Cop supports --auto-correct.
Lint/AssignmentInCondition: # Configuration parameters: EnforcedStyle.
# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent
Layout/IndentHeredoc:
Exclude: Exclude:
- 'lib/liquid/block_body.rb' - 'test/integration/tags/for_tag_test.rb'
- 'lib/liquid/lexer.rb' - 'test/integration/trim_mode_test.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/for.rb' # Offense count: 6
- 'lib/liquid/tags/if.rb' # Cop supports --auto-correct.
- 'lib/liquid/tags/raw.rb' # Configuration parameters: EnforcedStyle.
- 'lib/liquid/variable.rb' # SupportedStyles: symmetrical, new_line, same_line
- 'performance/profile.rb' Layout/MultilineMethodCallBraceLayout:
- 'test/test_helper.rb' Exclude:
- 'test/unit/tokenizer_unit_test.rb' - 'test/integration/error_handling_test.rb'
- 'test/unit/strainer_unit_test.rb'
# Offense count: 2 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
@@ -34,62 +40,181 @@ Lint/InheritException:
Exclude: Exclude:
- 'lib/liquid/interrupts.rb' - 'lib/liquid/interrupts.rb'
# Offense count: 2
Lint/UselessAssignment:
Exclude:
- 'performance/shopify/database.rb'
# Offense count: 1 # Offense count: 1
# Configuration parameters: CheckForMethodsWithNoSideEffects. # Configuration parameters: CheckForMethodsWithNoSideEffects.
Lint/Void: Lint/Void:
Exclude: Exclude:
- 'lib/liquid/parse_context.rb' - 'lib/liquid/parse_context.rb'
# Offense count: 98 # Offense count: 54
# Cop supports --auto-correct. Metrics/AbcSize:
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. Max: 56
# URISchemes: http, https
Metrics/LineLength:
Max: 294
# Offense count: 44 # Offense count: 12
Naming/ConstantName: Metrics/CyclomaticComplexity:
Exclude: Max: 12
- 'lib/liquid.rb'
- 'lib/liquid/block_body.rb'
- 'lib/liquid/tags/assign.rb'
- 'lib/liquid/tags/capture.rb'
- 'lib/liquid/tags/case.rb'
- 'lib/liquid/tags/cycle.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb'
- 'performance/shopify/comment_form.rb'
- 'performance/shopify/paginate.rb'
- 'test/integration/tags/include_tag_test.rb'
# Offense count: 5 # Offense count: 112
Style/ClassVars: # 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: 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/condition.rb'
- 'lib/liquid/strainer.rb' - 'lib/liquid/context.rb'
- 'lib/liquid/template.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 # Offense count: 1
# Configuration parameters: AllowCoercion.
Style/DateTime: Style/DateTime:
Exclude: Exclude:
- 'test/unit/context_unit_test.rb' - 'test/unit/context_unit_test.rb'
# Offense count: 119 # 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. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
# SupportedStyles: always, never # SupportedStyles: compact, expanded
Style/FrozenStringLiteralComment: Style/EmptyMethod:
Enabled: false 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 # Offense count: 9
# Cop supports --auto-correct. # Cop supports --auto-correct.
@@ -99,3 +224,37 @@ Style/Semicolon:
- 'test/integration/error_handling_test.rb' - 'test/integration/error_handling_test.rb'
- 'test/integration/template_test.rb' - 'test/integration/template_test.rb'
- 'test/unit/context_unit_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,30 +1,31 @@
language: ruby language: ruby
cache: bundler
rvm: rvm:
- 2.1
- 2.2
- 2.3
- 2.4 - 2.4
- 2.5 - 2.5
- &latest_ruby 2.6
- 2.7
- ruby-head - ruby-head
- jruby-head - jruby-head
- truffleruby # - rbx-2
sudo: false
addons:
apt:
packages:
- libgmp3-dev
matrix: matrix:
include:
- rvm: *latest_ruby
script: bundle exec rake memory_profile:run
name: Profiling Memory Usage
allow_failures: allow_failures:
- rvm: ruby-head - rvm: ruby-head
- rvm: jruby-head - rvm: jruby-head
- rvm: truffleruby
branches: install:
only: - bundle install
- master
- gh-pages script: bundle exec rake
- /.*-stable/
notifications: notifications:
disable: true disable: true

10
Gemfile
View File

@@ -8,18 +8,16 @@ gemspec
group :benchmark, :test do group :benchmark, :test do
gem 'benchmark-ips' gem 'benchmark-ips'
gem 'memory_profiler' gem 'memory_profiler'
gem 'terminal-table'
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ } do
gem 'stackprof' gem 'stackprof'
end end
end end
group :test do group :test do
gem 'rubocop', '~> 0.74.0', require: false gem 'rubocop', '~> 0.53.0'
gem 'rubocop-performance', require: false
platform :mri, :truffleruby do platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'liquid-tag' gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'reversable-range'
end end
end end

View File

@@ -1,31 +1,29 @@
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", __FILE__)
require "liquid/version" require "liquid/version"
task(default: [:test, :rubocop]) task default: [:test, :rubocop]
desc('run test suite with default parser') desc 'run test suite with default parser'
Rake::TestTask.new(:base_test) do |t| Rake::TestTask.new(:base_test) do |t|
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
@@ -34,8 +32,8 @@ 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' || RUBY_ENGINE == 'truffleruby' if RUBY_ENGINE == 'ruby'
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
@@ -47,7 +45,7 @@ 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
@@ -94,7 +92,7 @@ namespace :memory_profile do
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

View File

@@ -1,6 +1,6 @@
module ProductsFilter module ProductsFilter
def price(integer) def price(integer)
format("$%.2d USD", integer / 100.0) sprintf("$%.2d USD", integer / 100.0)
end end
def prettyprint(text) def prettyprint(text)

View File

@@ -9,12 +9,12 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
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 = Regexp.last_match(1) || 'index' @action = $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"

View File

@@ -8,5 +8,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

@@ -74,7 +74,7 @@ require 'liquid/condition'
require 'liquid/utils' require 'liquid/utils'
require 'liquid/tokenizer' require 'liquid/tokenizer'
require 'liquid/parse_context' require 'liquid/parse_context'
require 'liquid/partial_cache' require 'liquid/reversable_range'
# Load all the tags of the standard library # Load all the tags of the standard library
# #

View File

@@ -13,7 +13,6 @@ module Liquid
end end
end end
# For backwards compatibility
def render(context) def render(context)
@body.render(context) @body.render(context)
end end
@@ -28,15 +27,15 @@ module Liquid
def unknown_tag(tag, _params, _tokens) def unknown_tag(tag, _params, _tokens)
if tag == 'else'.freeze if tag == 'else'.freeze
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else".freeze, raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
block_name: block_name) block_name: block_name))
elsif tag.start_with?('end'.freeze) elsif tag.start_with?('end'.freeze)
raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter".freeze, raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
tag: tag, tag: tag,
block_name: block_name, block_name: block_name,
block_delimiter: block_delimiter) block_delimiter: block_delimiter))
else else
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag) raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
end end
end end
@@ -61,7 +60,7 @@ module Liquid
return false if end_tag_name == block_delimiter return false if end_tag_name == block_delimiter
unless end_tag_name unless end_tag_name
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name) raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end end
# this tag is not registered with the system # this tag is not registered with the system

View File

@@ -1,7 +1,6 @@
module Liquid module Liquid
class BlockBody class BlockBody
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/ WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%".freeze TAGSTART = "{%".freeze
@@ -14,42 +13,8 @@ module Liquid
@blank = true @blank = true
end end
def parse(tokenizer, parse_context, &block) def parse(tokenizer, parse_context)
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
if tokenizer.for_liquid_tag
parse_for_liquid_tag(tokenizer, parse_context, &block)
else
parse_for_document(tokenizer, parse_context, &block)
end
end
private def parse_for_liquid_tag(tokenizer, parse_context)
while token = tokenizer.shift
unless token.empty? || token =~ WhitespaceOrNothing
unless token =~ LiquidTagToken
# line isn't empty but didn't match tag syntax, yield and let the
# caller raise a syntax error
return yield token, token
end
tag_name = Regexp.last_match(1)
markup = Regexp.last_match(2)
unless tag = registered_tags[tag_name]
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
end
parse_context.line_number = tokenizer.line_number
end
yield nil, nil
end
private def parse_for_document(tokenizer, parse_context, &block)
while token = tokenizer.shift while token = tokenizer.shift
next if token.empty? next if token.empty?
case case
@@ -58,20 +23,9 @@ module Liquid
unless token =~ FullToken unless token =~ FullToken
raise_missing_tag_terminator(token, parse_context) raise_missing_tag_terminator(token, parse_context)
end end
tag_name = Regexp.last_match(2) tag_name = $1
markup = Regexp.last_match(4) markup = $2
# fetch the tag from registered blocks
if parse_context.line_number
# newlines inside the tag should increase the line number,
# particularly important for multiline {% liquid %} tags
parse_context.line_number += Regexp.last_match(1).count("\n".freeze) + Regexp.last_match(3).count("\n".freeze)
end
if tag_name == 'liquid'.freeze
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
end
unless tag = registered_tags[tag_name] 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
@@ -101,7 +55,7 @@ module Liquid
def whitespace_handler(token, parse_context) def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl if token[2] == WhitespaceControl
previous_token = @nodelist.last previous_token = @nodelist.last
if previous_token.is_a?(String) if previous_token.is_a? String
previous_token.rstrip! previous_token.rstrip!
end end
end end
@@ -113,23 +67,19 @@ module Liquid
end end
def render(context) def render(context)
render_to_output_buffer(context, '') output = []
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(context, output, node) render_node_to_output(node, output, context)
when Block when Block
render_node(context, node.blank? ? '' : output, node) render_node_to_output(node, output, context, node.blank?)
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
@@ -138,32 +88,36 @@ 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(context, output, node) render_node_to_output(node, output, context)
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 output.join
end end
private private
def render_node(context, output, node) def render_node_to_output(node, output, context, skip_output = false)
node.render_to_output_buffer(context, output) node_output = node.render(context)
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 raise_if_resource_limits_reached(context, length) def check_resources(context, node_output)
context.resource_limits.render_length += length context.resource_limits.render_length += node_output.bytesize
return unless context.resource_limits.reached? return unless context.resource_limits.reached?
raise MemoryError, "Memory limits exceeded".freeze raise MemoryError.new("Memory limits exceeded".freeze)
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)
@@ -175,11 +129,11 @@ module Liquid
end end
def raise_missing_tag_terminator(token, parse_context) def raise_missing_tag_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect) raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
end end
def raise_missing_variable_terminator(token, parse_context) def raise_missing_variable_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect) raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
end end
def registered_tags def registered_tags

View File

@@ -11,18 +11,18 @@ module Liquid
'=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) }, '=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<'.freeze => :<, '<'.freeze => :<,
'>'.freeze => :>, '>'.freeze => :>,
'>='.freeze => :>=, '>='.freeze => :>=,
'<='.freeze => :<=, '<='.freeze => :<=,
'contains'.freeze => lambda do |_cond, left, right| 'contains'.freeze => 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 def self.operators
@@ -36,7 +36,7 @@ module Liquid
@left = left @left = left
@operator = operator @operator = operator
@right = right @right = right
@child_relation = nil @child_relation = nil
@child_condition = nil @child_condition = nil
end end
@@ -116,7 +116,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, "Unknown operator #{op}") operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("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 +124,7 @@ module Liquid
begin begin
left.send(operation, right) left.send(operation, right)
rescue ::ArgumentError => e rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message raise Liquid::ArgumentError.new(e.message)
end end
end end
end end

View File

@@ -12,41 +12,30 @@ module Liquid
# #
# context['bob'] #=> nil class Context # context['bob'] #=> nil class Context
class Context class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments 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
# rubocop:disable Metrics/ParameterLists def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_registers: {}, static_environments: {}) @environments = [environments].flatten
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_registers, static_environments) @scopes = [(outer_scope || {})]
end @registers = registers
@errors = []
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_registers = {}, static_environments = {}) @partial = false
@environments = [environments] @strict_variables = false
@environments.flatten! @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@static_environments = [static_environments].flat_map(&:freeze).freeze
@scopes = [(outer_scope || {})]
@registers = registers
@static_registers = static_registers.freeze
@errors = []
@partial = false
@strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@base_scope_depth = 0
squash_instance_assigns_with_environments squash_instance_assigns_with_environments
@this_stack_used = false @this_stack_used = false
self.exception_renderer = Template.default_exception_renderer self.exception_renderer = Template.default_exception_renderer
if rethrow_errors if rethrow_errors
self.exception_renderer = ->(_e) { raise } self.exception_renderer = ->(e) { raise }
end end
@interrupts = [] @interrupts = []
@filters = [] @filters = []
@global_filter = nil @global_filter = nil
end end
# rubocop:enable Metrics/ParameterLists
def warnings def warnings
@warnings ||= [] @warnings ||= []
@@ -100,7 +89,7 @@ module Liquid
# Push new local scope on the stack. use <tt>Context#stack</tt> instead # Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope = {}) def push(new_scope = {})
@scopes.unshift(new_scope) @scopes.unshift(new_scope)
check_overflow raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH
end end
# Merge a hash of variables in the current local scope # Merge a hash of variables in the current local scope
@@ -137,25 +126,6 @@ module Liquid
@this_stack_used = old_stack_used @this_stack_used = old_stack_used
end end
# Creates a new context inheriting resource limits, filters, environment etc.,
# but with an isolated scope.
def new_isolated_subcontext
check_overflow
Context.build(
resource_limits: resource_limits,
static_environments: static_environments,
static_registers: static_registers
).tap do |subcontext|
subcontext.base_scope_depth = base_scope_depth + 1
subcontext.exception_renderer = exception_renderer
subcontext.filters = @filters
subcontext.strainer = nil
subcontext.errors = errors
subcontext.warnings = warnings
end
end
def clear_instance_assigns def clear_instance_assigns
@scopes[0] = {} @scopes[0] = {}
end end
@@ -194,13 +164,25 @@ module Liquid
# This was changed from find() to find_index() because this is a very hot # This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation # path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.key?(key) } index = @scopes.find_index { |s| s.key?(key) }
scope = @scopes[index] if index
variable = if index variable = nil
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
else if scope.nil?
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found) @environments.each do |e|
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
# When lookup returned a value OR there is no value but the lookup also did not raise
# then it is the value we are looking for.
if !variable.nil? || @strict_variables && raise_on_not_found
scope = e
break
end
end
end end
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
variable = variable.to_liquid variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
@@ -215,44 +197,14 @@ module Liquid
value = obj[key] value = obj[key]
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
end end
protected
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters
private private
attr_reader :base_scope_depth
def try_variable_find_in_environments(key, raise_on_not_found:)
@environments.each do |environment|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
if !found_variable.nil? || @strict_variables && raise_on_not_found
return found_variable
end
end
@static_environments.each do |environment|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
if !found_variable.nil? || @strict_variables && raise_on_not_found
return found_variable
end
end
nil
end
def check_overflow
raise StackLevelError, "Nesting too deep".freeze if overflow?
end
def overflow?
base_scope_depth + @scopes.length > Block::MAX_DEPTH
end
def internal_error def internal_error
# raise and catch to set backtrace and cause on exception # raise and catch to set backtrace and cause on exception
raise Liquid::InternalError, 'internal' raise Liquid::InternalError, 'internal'

View File

@@ -7,7 +7,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
@@ -18,9 +18,9 @@ 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'.freeze, 'end'.freeze
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag) raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
else else
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag) raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
end end
end end
end end

View File

@@ -25,7 +25,7 @@ module Liquid
# Catch all for the method # Catch all for the method
def liquid_method_missing(method) def liquid_method_missing(method)
return nil unless @context&.strict_variables return nil unless @context && @context.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}" raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end end

View File

@@ -21,10 +21,10 @@ module Liquid
def message_prefix def message_prefix
str = "" str = ""
str << if is_a?(SyntaxError) if is_a?(SyntaxError)
"Liquid syntax error" str << "Liquid syntax error"
else else
"Liquid error" str << "Liquid error"
end end
if line_number if line_number

View File

@@ -15,7 +15,7 @@ module Liquid
LITERALS = { LITERALS = {
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil, nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
'true'.freeze => true, 'true'.freeze => true,
'false'.freeze => false, 'false'.freeze => false,
'blank'.freeze => MethodLiteral.new(:blank?, '').freeze, 'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze 'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
@@ -33,13 +33,13 @@ module Liquid
else else
case markup case markup
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
Regexp.last_match(1) $1
when INTEGERS_REGEX when INTEGERS_REGEX
Regexp.last_match(1).to_i $1.to_i
when RANGES_REGEX when RANGES_REGEX
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2)) RangeLookup.parse($1, $2)
when FLOATS_REGEX when FLOATS_REGEX
Regexp.last_match(1).to_f $1.to_f
else else
VariableLookup.parse(markup) VariableLookup.parse(markup)
end end

View File

@@ -57,7 +57,7 @@ module Liquid
end end
def full_path(template_path) def full_path(template_path)
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ %r{\A[^./][a-zA-Z0-9_/]+\z} raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
full_path = if template_path.include?('/'.freeze) full_path = if template_path.include?('/'.freeze)
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path)) File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))

View File

@@ -26,13 +26,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[Regexp.last_match(1).to_sym]).to_s (vars[$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('.'.freeze).reduce(locale) do |level, cur|
level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}") level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
end end
end end
end end

View File

@@ -11,7 +11,7 @@ module Liquid
'('.freeze => :open_round, '('.freeze => :open_round,
')'.freeze => :close_round, ')'.freeze => :close_round,
'?'.freeze => :question, '?'.freeze => :question,
'-'.freeze => :dash, '-'.freeze => :dash
}.freeze }.freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/ SINGLE_STRING_LITERAL = /'[^\']*'/
@@ -31,12 +31,13 @@ module Liquid
until @ss.eos? until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING) @ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos? break if @ss.eos?
tok = if t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] tok = case
elsif t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t] when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
elsif t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t] when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
elsif t = @ss.scan(NUMBER_LITERAL) then [:number, t] when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
elsif t = @ss.scan(IDENTIFIER) then [:id, t] when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
elsif t = @ss.scan(DOTDOT) then [:dotdot, t] when t = @ss.scan(IDENTIFIER) then [:id, t]
when t = @ss.scan(DOTDOT) then [:dotdot, t]
else else
c = @ss.getch c = @ss.getch
if s = SPECIALS[c] if s = SPECIALS[c]

View File

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

View File

@@ -28,7 +28,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

@@ -44,14 +44,11 @@ module Liquid
tok[0] == type tok[0] == type
end end
SINGLE_TOKEN_EXPRESSION_TYPES = [: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 SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0]) elsif [:string, :number].include? token[0]
consume consume
elsif token.first == :open_round elsif token.first == :open_round
consume consume

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ module Liquid
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate) if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj) new(start_obj, end_obj)
else else
start_obj.to_i..end_obj.to_i ReversableRange.new(start_obj.to_i, end_obj.to_i)
end end
end end
@@ -18,7 +18,7 @@ module Liquid
def evaluate(context) def evaluate(context)
start_int = to_integer(context.evaluate(@start_obj)) start_int = to_integer(context.evaluate(@start_obj))
end_int = to_integer(context.evaluate(@end_obj)) end_int = to_integer(context.evaluate(@end_obj))
start_int..end_int ReversableRange.new(start_int, end_int)
end end
private private

View File

@@ -0,0 +1,77 @@
module Liquid
class ReversableRange
include Enumerable
def initialize(min, max)
@min = min
@max = max
@reversed = false
end
def each
if reversed
index = max
while index >= min
yield index
index -= 1
end
else
index = min
while index <= max
yield index
index += 1
end
end
end
def reverse!
@reversed = !reversed
self
end
def empty?
max < min
end
def size
difference = max - min
if difference > 0
difference + 1
else
0
end
end
def load_slice(from, to = nil)
to ||= max
slice_max = [max, to].min
slice_min = [min, from].max
range = ReversableRange.new(slice_min, slice_max)
range.reverse! if reversed
range
end
def ==(other)
other.is_a?(self.class) &&
other.min == min &&
other.max == max &&
other.reversed == reversed
end
def to_s
if reversed
"#{max}..#{min}"
else
"#{min}..#{max}"
end
end
def to_liquid
self
end
protected
attr_reader :min, :max, :reversed
end
end

View File

@@ -8,13 +8,13 @@ module Liquid
'>'.freeze => '&gt;'.freeze, '>'.freeze => '&gt;'.freeze,
'<'.freeze => '&lt;'.freeze, '<'.freeze => '&lt;'.freeze,
'"'.freeze => '&quot;'.freeze, '"'.freeze => '&quot;'.freeze,
"'".freeze => '&#39;'.freeze, "'".freeze => '&#39;'.freeze
}.freeze }.freeze
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
STRIP_HTML_BLOCKS = Regexp.union( STRIP_HTML_BLOCKS = Regexp.union(
%r{<script.*?</script>}m, /<script.*?<\/script>/m,
/<!--.*?-->/m, /<!--.*?-->/m,
%r{<style.*?</style>}m /<style.*?<\/style>/m
) )
STRIP_HTML_TAGS = /<.*?>/m STRIP_HTML_TAGS = /<.*?>/m
@@ -79,7 +79,7 @@ module Liquid
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].concat(truncate_string_str) : input_str input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
end end
def truncatewords(input, words = 15, truncate_string = "...".freeze) def truncatewords(input, words = 15, truncate_string = "...".freeze)
@@ -88,7 +88,7 @@ module Liquid
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).concat(truncate_string.to_s) : input wordlist.length > l ? wordlist[0..l].join(" ".freeze) + 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.
@@ -276,7 +276,7 @@ module Liquid
def concat(input, array) def concat(input, array)
unless array.respond_to?(:to_ary) unless array.respond_to?(:to_ary)
raise ArgumentError, "concat filter requires an array argument" raise ArgumentError.new("concat filter requires an array argument")
end end
InputIterator.new(input).concat(array) InputIterator.new(input).concat(array)
end end
@@ -430,7 +430,7 @@ module Liquid
private private
def raise_property_error(property) def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{property}'" raise Liquid::ArgumentError.new("cannot select the property '#{property}'")
end end
def apply_operation(input, operand, operation) def apply_operation(input, operand, operation)

View File

@@ -27,7 +27,7 @@ module Liquid
def self.add_filter(filter) def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module) raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless include?(filter) unless self.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(', ')}"
@@ -54,7 +54,7 @@ module Liquid
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&.strict_filters elsif @context && @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}" raise Liquid::UndefinedFilter, "undefined filter #{method}"
else else
args.first args.first

View File

@@ -5,8 +5,8 @@ module Liquid
include ParserSwitching include ParserSwitching
class << self class << self
def parse(tag_name, markup, tokenizer, parse_context) def parse(tag_name, markup, tokenizer, options)
tag = new(tag_name, markup, parse_context) tag = new(tag_name, markup, options)
tag.parse(tokenizer) tag.parse(tokenizer)
tag tag
end end
@@ -36,14 +36,6 @@ module Liquid
''.freeze ''.freeze
end 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
def blank? def blank?
false false
end end

View File

@@ -10,27 +10,23 @@ module Liquid
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
def self.syntax_error_translation_key
"errors.syntax.assign".freeze
end
attr_reader :to, :from attr_reader :to, :from
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@to = Regexp.last_match(1) @to = $1
@from = Variable.new(Regexp.last_match(2), options) @from = Variable.new($2, options)
else else
raise SyntaxError, options[:locale].t(self.class.syntax_error_translation_key) raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
end end
end end
def render_to_output_buffer(context, output) def render(context)
val = @from.render(context) val = @from.render(context)
context.scopes.last[@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)
output ''.freeze
end end
def blank? def blank?

View File

@@ -16,18 +16,17 @@ module Liquid
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@to = Regexp.last_match(1) @to = $1
else else
raise SyntaxError, options[:locale].t("errors.syntax.capture") raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
end end
end end
def render_to_output_buffer(context, output) def render(context)
previous_output_size = output.bytesize output = super
super
context.scopes.last[@to] = output context.scopes.last[@to] = output
context.resource_limits.assign_score += (output.bytesize - previous_output_size) context.resource_limits.assign_score += output.bytesize
output ''.freeze
end end
def blank? def blank?

View File

@@ -10,15 +10,17 @@ module Liquid
@blocks = [] @blocks = []
if markup =~ Syntax if markup =~ Syntax
@left = Expression.parse(Regexp.last_match(1)) @left = Expression.parse($1)
else else
raise SyntaxError, options[:locale].t("errors.syntax.case".freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
end end
end end
def parse(tokens) def parse(tokens)
body = BlockBody.new body = BlockBody.new
body = @blocks.last.attachment while parse_body(body, tokens) while parse_body(body, tokens)
body = @blocks.last.attachment
end
end end
def nodelist def nodelist
@@ -36,21 +38,21 @@ module Liquid
end end
end end
def render_to_output_buffer(context, output) def render(context)
context.stack do 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?
block.attachment.render_to_output_buffer(context, output) if execute_else_block return block.attachment.render(context) if execute_else_block
elsif block.evaluate(context) elsif block.evaluate(context)
execute_else_block = false execute_else_block = false
block.attachment.render_to_output_buffer(context, output) output << block.attachment.render(context)
end end
end end
output
end end
output
end end
private private
@@ -60,12 +62,12 @@ module Liquid
while markup while markup
unless markup =~ WhenSyntax unless markup =~ WhenSyntax
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when".freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
end end
markup = Regexp.last_match(2) markup = $2
block = Condition.new(@left, '=='.freeze, Expression.parse(Regexp.last_match(1))) block = Condition.new(@left, '=='.freeze, Expression.parse($1))
block.attach(body) block.attach(body)
@blocks << block @blocks << block
end end
@@ -73,7 +75,7 @@ module Liquid
def record_else_condition(markup) def record_else_condition(markup)
unless markup.strip.empty? unless markup.strip.empty?
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else".freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
end end
block = ElseCondition.new block = ElseCondition.new

View File

@@ -1,7 +1,7 @@
module Liquid module Liquid
class Comment < Block class Comment < Block
def render_to_output_buffer(_context, output) def render(_context)
output ''.freeze
end end
def unknown_tag(_tag, _markup, _tokens) def unknown_tag(_tag, _markup, _tokens)

View File

@@ -21,39 +21,28 @@ module Liquid
super super
case markup case markup
when NamedSyntax when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2)) @variables = variables_from_string($2)
@name = Expression.parse(Regexp.last_match(1)) @name = Expression.parse($1)
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
else else
raise SyntaxError, options[:locale].t("errors.syntax.cycle".freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
end end
end end
def render_to_output_buffer(context, output) def render(context)
context.registers[:cycle] ||= {} context.registers[:cycle] ||= {}
context.stack do 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 end
output
end end
private private
@@ -61,7 +50,7 @@ module Liquid
def variables_from_string(markup) def variables_from_string(markup)
markup.split(',').collect do |var| markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o var =~ /\s*(#{QuotedFragment})\s*/o
Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil $1 ? Expression.parse($1) : nil
end.compact end.compact
end end

View File

@@ -23,12 +23,11 @@ module Liquid
@variable = markup.strip @variable = markup.strip
end end
def render_to_output_buffer(context, output) def render(context)
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
output << value.to_s value.to_s
output
end end
end end

View File

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

View File

@@ -70,39 +70,37 @@ module Liquid
@else_block = BlockBody.new @else_block = BlockBody.new
end end
def render_to_output_buffer(context, output) def render(context)
segment = collection_segment(context) segment = collection_segment(context)
if segment.empty? if segment.empty?
render_else(context, output) render_else(context)
else else
render_segment(context, output, segment) render_segment(context, segment)
end end
output
end end
protected protected
def lax_parse(markup) def lax_parse(markup)
if markup =~ Syntax if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = $1
collection_name = Regexp.last_match(2) collection_name = $2
@reversed = !!Regexp.last_match(3) @reversed = !!$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(TagAttributes) do |key, value|
set_attribute(key, value) set_attribute(key, value)
end end
else else
raise SyntaxError, options[:locale].t("errors.syntax.for".freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
end end
end end
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = Parser.new(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in".freeze) unless p.id?('in'.freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
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)
@@ -110,7 +108,7 @@ module Liquid
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'.freeze) || p.id?('offset'.freeze)
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute".freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
end end
p.consume p.consume
set_attribute(attribute, p.expression) set_attribute(attribute, p.expression)
@@ -135,7 +133,6 @@ module Liquid
end end
collection = context.evaluate(@collection_name) collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range)
limit_value = context.evaluate(@limit) limit_value = context.evaluate(@limit)
to = if limit_value.nil? to = if limit_value.nil?
@@ -147,14 +144,16 @@ module Liquid
segment = Utils.slice_collection(collection, from, to) segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed segment.reverse! if @reversed
offsets[@name] = from + segment.length offsets[@name] = from + segment.size
segment segment
end end
def render_segment(context, output, segment) def render_segment(context, segment)
for_stack = context.registers[:for_stack] ||= [] for_stack = context.registers[:for_stack] ||= []
length = segment.length length = segment.size
result = ''
context.stack do context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1]) loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
@@ -166,21 +165,22 @@ module Liquid
segment.each do |item| segment.each do |item|
context[@variable_name] = item context[@variable_name] = item
@for_block.render_to_output_buffer(context, output) result << @for_block.render(context)
loop_vars.send(:increment!) loop_vars.send(:increment!)
# Handle any interrupts if they exist. # Handle any interrupts if they exist.
next unless context.interrupt? if context.interrupt?
interrupt = context.pop_interrupt interrupt = context.pop_interrupt
break if interrupt.is_a?(BreakInterrupt) break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a?(ContinueInterrupt) next if interrupt.is_a? ContinueInterrupt
end
end end
ensure ensure
for_stack.pop for_stack.pop
end end
end end
output result
end end
def set_attribute(key, expr) def set_attribute(key, expr)
@@ -196,12 +196,8 @@ module Liquid
end end
end end
def render_else(context, output) def render_else(context)
if @else_block @else_block ? @else_block.render(context) : ''.freeze
@else_block.render_to_output_buffer(context, output)
else
output
end
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor

View File

@@ -39,16 +39,15 @@ module Liquid
end end
end end
def render_to_output_buffer(context, output) def render(context)
context.stack do context.stack do
@blocks.each do |block| @blocks.each do |block|
if block.evaluate(context) if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output) return block.attachment.render(context)
end end
end end
''.freeze
end end
output
end end
private private
@@ -66,17 +65,17 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators) expressions = markup.scan(ExpressionsAndOperators)
raise SyntaxError, options[:locale].t("errors.syntax.if".freeze) unless expressions.pop =~ Syntax raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3))) condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
until expressions.empty? until expressions.empty?
operator = expressions.pop.to_s.strip operator = expressions.pop.to_s.strip
raise SyntaxError, options[:locale].t("errors.syntax.if".freeze) unless expressions.pop.to_s =~ Syntax raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
new_condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3))) new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
raise SyntaxError, options[:locale].t("errors.syntax.if".freeze) unless BOOLEAN_OPERATORS.include?(operator) 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

View File

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

View File

@@ -23,8 +23,8 @@ module Liquid
if markup =~ Syntax if markup =~ Syntax
template_name = Regexp.last_match(1) template_name = $1
variable_name = Regexp.last_match(3) variable_name = $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)
@@ -35,23 +35,18 @@ module Liquid
end end
else else
raise SyntaxError, options[:locale].t("errors.syntax.include".freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
end end
end end
def parse(_tokens) def parse(_tokens)
end end
def render_to_output_buffer(context, output) def render(context)
template_name = context.evaluate(@template_name_expr) template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context
)
partial = load_cached_partial(template_name, context)
context_variable_name = template_name.split('/'.freeze).last context_variable_name = template_name.split('/'.freeze).last
variable = if @variable_name_expr variable = if @variable_name_expr
@@ -71,31 +66,55 @@ module Liquid
end end
if variable.is_a?(Array) if variable.is_a?(Array)
variable.each do |var| variable.collect do |var|
context[context_variable_name] = var context[context_variable_name] = var
partial.render_to_output_buffer(context, output) partial.render(context)
end end
else else
context[context_variable_name] = variable context[context_variable_name] = variable
partial.render_to_output_buffer(context, output) partial.render(context)
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
alias_method :parse_context, :options alias_method :parse_context, :options
private :parse_context private :parse_context
def load_cached_partial(template_name, context)
cached_partials = context.registers[:cached_partials] || {}
if cached = cached_partials[template_name]
return cached
end
source = read_template_from_file_system(context)
begin
parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context)
ensure
parse_context.partial = false
end
cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials
partial
end
def read_template_from_file_system(context)
file_system = context.registers[:file_system] || Liquid::Template.file_system
file_system.read_template_file(context.evaluate(@template_name_expr))
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
[ [
@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

View File

@@ -20,12 +20,10 @@ module Liquid
@variable = markup.strip @variable = markup.strip
end end
def render_to_output_buffer(context, output) def render(context)
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

View File

@@ -13,18 +13,17 @@ module Liquid
@body = '' @body = ''
while token = tokens.shift while token = tokens.shift
if token =~ FullTokenPossiblyInvalid if token =~ FullTokenPossiblyInvalid
@body << Regexp.last_match(1) if Regexp.last_match(1) != "".freeze @body << $1 if $1 != "".freeze
return if block_delimiter == Regexp.last_match(2) return if block_delimiter == $2
end end
@body << token unless token.empty? @body << token unless token.empty?
end end
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name) raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end end
def render_to_output_buffer(_context, output) def render(_context)
output << @body @body
output
end end
def nodelist def nodelist
@@ -39,7 +38,7 @@ module Liquid
def ensure_valid_markup(tag_name, markup, parse_context) def ensure_valid_markup(tag_name, markup, parse_context)
unless markup =~ Syntax unless markup =~ Syntax
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name) raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
end end
end end
end end

View File

@@ -1,54 +0,0 @@
module Liquid
class Render < Tag
SYNTAX = /(#{QuotedString})#{QuotedFragment}*/o
attr_reader :template_name_expr, :attributes
def initialize(tag_name, markup, options)
super
raise SyntaxError, options[:locale].t("errors.syntax.render".freeze) unless markup =~ SYNTAX
template_name = Regexp.last_match(1)
@template_name_expr = Expression.parse(template_name)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value)
end
end
def render_to_output_buffer(context, output)
# Though we evaluate this here we will only ever parse it as a string literal.
template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context
)
inner_context = context.new_isolated_subcontext
inner_context.template_name = template_name
inner_context.partial = true
@attributes.each do |key, value|
inner_context[key] = context.evaluate(value)
end
partial.render_to_output_buffer(inner_context, output)
output
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
] + @node.attributes.values
end
end
end
Template.register_tag('render'.freeze, Render)
end

View File

@@ -7,19 +7,19 @@ module Liquid
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = $1
@collection_name = Expression.parse(Regexp.last_match(2)) @collection_name = Expression.parse($2)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = Expression.parse(value)
end end
else else
raise SyntaxError, options[:locale].t("errors.syntax.table_row".freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
end end
end end
def render_to_output_buffer(context, output) def render(context)
(collection = context.evaluate(@collection_name)) || (return ''.freeze) collection = context.evaluate(@collection_name) or return ''.freeze
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0 from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
@@ -30,7 +30,7 @@ module Liquid
cols = context.evaluate(@attributes['cols'.freeze]).to_i cols = context.evaluate(@attributes['cols'.freeze]).to_i
output << "<tr class=\"row1\">\n" result = "<tr class=\"row1\">\n"
context.stack do context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols) tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'.freeze] = tablerowloop context['tablerowloop'.freeze] = tablerowloop
@@ -38,20 +38,17 @@ module Liquid
collection.each do |item| collection.each do |item|
context[@variable_name] = item context[@variable_name] = item
output << "<td class=\"col#{tablerowloop.col}\">" result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
super
output << '</td>'
if tablerowloop.col_last && !tablerowloop.last if tablerowloop.col_last && !tablerowloop.last
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">" result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
end end
tablerowloop.send(:increment!) tablerowloop.send(:increment!)
end end
end end
result << "</tr>\n"
output << "</tr>\n" result
output
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor

View File

@@ -6,23 +6,23 @@ 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_to_output_buffer(context, output) def render(context)
context.stack do 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) unless first_block.evaluate(context)
return first_block.attachment.render_to_output_buffer(context, output) return first_block.attachment.render(context)
end 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) if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output) return block.attachment.render(context)
end end
end end
end
output ''.freeze
end
end end
end end

View File

@@ -50,7 +50,7 @@ module Liquid
private private
def lookup_class(name) def lookup_class(name)
Object.const_get(name) name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
end end
end end
@@ -172,7 +172,7 @@ module Liquid
c = args.shift c = args.shift
if @rethrow_errors if @rethrow_errors
c.exception_renderer = ->(_e) { raise } c.exception_renderer = ->(e) { raise }
end end
c c
@@ -187,12 +187,9 @@ module Liquid
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)
@@ -207,9 +204,10 @@ 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.
with_profiling(context) do result = with_profiling(context) do
@root.render_to_output_buffer(context, output || '') @root.render(context)
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
@@ -222,10 +220,6 @@ 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,35 +1,29 @@
module Liquid module Liquid
class Tokenizer class Tokenizer
attr_reader :line_number, :for_liquid_tag attr_reader :line_number
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false) def initialize(source, line_numbers = false)
@source = source @source = source
@line_number = line_number || (line_numbers ? 1 : nil) @line_number = line_numbers ? 1 : nil
@for_liquid_tag = for_liquid_tag
@tokens = tokenize @tokens = tokenize
end end
def shift def shift
(token = @tokens.shift) || return token = @tokens.shift
@line_number += token.count("\n") if @line_number && token
if @line_number
@line_number += @for_liquid_tag ? 1 : token.count("\n")
end
token token
end end
private private
def tokenize def tokenize
@source = @source.source if @source.respond_to?(:source)
return [] if @source.to_s.empty? return [] if @source.to_s.empty?
return @source.split("\n") if @for_liquid_tag
tokens = @source.split(TemplateParser) tokens = @source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array # removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0]&.empty? tokens.shift if tokens[0] && tokens[0].empty?
tokens tokens
end end

View File

@@ -1,6 +1,8 @@
module Liquid module Liquid
module Utils module Utils
def self.slice_collection(collection, from, to) def self.slice_collection(collection, from, to)
return collection.load_slice(from, to) if collection.is_a?(ReversableRange)
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice) if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
collection.load_slice(from, to) collection.load_slice(from, to)
else else
@@ -50,7 +52,7 @@ module Liquid
when Numeric when Numeric
obj obj
when String when String
obj.strip =~ /\A-?\d+\.\d+\z/ ? BigDecimal(obj) : obj.to_i (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

View File

@@ -43,11 +43,11 @@ module Liquid
@filters = [] @filters = []
return unless markup =~ MarkupWithQuotedFragment return unless markup =~ MarkupWithQuotedFragment
name_markup = Regexp.last_match(1) name_markup = $1
filter_markup = Regexp.last_match(2) filter_markup = $2
@name = Expression.parse(name_markup) @name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex if filter_markup =~ FilterMarkupRegex
filters = Regexp.last_match(1).scan(FilterParser) filters = $1.scan(FilterParser)
filters.each do |f| filters.each do |f|
next unless f =~ /\w+/ next unless f =~ /\w+/
filtername = Regexp.last_match(0) filtername = Regexp.last_match(0)
@@ -85,23 +85,12 @@ 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)
@@ -121,7 +110,7 @@ module Liquid
end end
def evaluate_filter_expressions(context, filter_args, filter_kwargs) def evaluate_filter_expressions(context, filter_args, filter_kwargs)
parsed_args = filter_args.map { |expr| context.evaluate(expr) } parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
if filter_kwargs if filter_kwargs
parsed_kwargs = {} parsed_kwargs = {}
filter_kwargs.each do |key, expr| filter_kwargs.each do |key, expr|

View File

@@ -14,7 +14,7 @@ module Liquid
name = lookups.shift name = lookups.shift
if name =~ SQUARE_BRACKETED if name =~ SQUARE_BRACKETED
name = Expression.parse(Regexp.last_match(1)) name = Expression.parse($1)
end end
@name = name @name = name
@@ -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(Regexp.last_match(1)) lookups[i] = Expression.parse($1)
elsif COMMAND_METHODS.include?(lookup) elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i @command_flags |= 1 << i
end end

View File

@@ -1,7 +1,7 @@
# encoding: utf-8 # encoding: utf-8
lib = File.expand_path('../lib/', __FILE__) lib = File.expand_path('../lib/', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
require "liquid/version" require "liquid/version"
@@ -16,7 +16,7 @@ Gem::Specification.new do |s|
s.license = "MIT" s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup." # s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.4.0" s.required_ruby_version = ">= 2.1.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}/**/*")
@@ -26,6 +26,6 @@ Gem::Specification.new do |s|
s.require_path = "lib" s.require_path = "lib"
s.add_development_dependency('rake', '~> 11.3') s.add_development_dependency 'rake', '~> 11.3'
s.add_development_dependency('minitest') s.add_development_dependency 'minitest'
end end

View File

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

View File

@@ -5,14 +5,14 @@ class CommentForm < Liquid::Block
super super
if markup =~ Syntax if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = $1
@attributes = {} @attributes = {}
else else
raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]" raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]")
end end
end end
def render_to_output_buffer(context, output) def render(context)
article = context[@variable_name] article = context[@variable_name]
context.stack do context.stack do
@@ -20,12 +20,10 @@ class CommentForm < Liquid::Block
'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

@@ -16,10 +16,9 @@ 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.each_with_object({}) do |(key, values), assigns| db = db.inject({}) do |assigns, (key, values)|
assigns[key] = values.each_with_object({}) do |v, h| assigns[key] = values.inject({}) { |h, v| h[v['handle']] = v; h; }
h[v['handle']] = v assigns
end
end end
# Some standard direct accessors so that the specialized templates # Some standard direct accessors so that the specialized templates
@@ -31,8 +30,8 @@ module Database
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
@@ -41,6 +40,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

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

View File

@@ -1,4 +1,4 @@
$LOAD_PATH.unshift(__dir__ + '/../../lib') $:.unshift __dir__ + '/../../lib'
require_relative '../../lib/liquid' require_relative '../../lib/liquid'
require_relative 'comment_form' require_relative 'comment_form'
@@ -9,11 +9,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,12 @@
module MoneyFilter module MoneyFilter
def money_with_currency(money) def money_with_currency(money)
return '' if money.nil? return '' if money.nil?
format("$ %.2f USD", money / 100.0) sprintf("$ %.2f USD", money / 100.0)
end end
def money(money) def money(money)
return '' if money.nil? return '' if money.nil?
format("$ %.2f", money / 100.0) sprintf("$ %.2f", money / 100.0)
end end
private private

View File

@@ -1,13 +1,13 @@
class Paginate < Liquid::Block class Paginate < Liquid::Block
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/ Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@collection_name = Regexp.last_match(1) @collection_name = $1
@page_size = if Regexp.last_match(2) @page_size = if $2
Regexp.last_match(3).to_i $3.to_i
else else
20 20
end end
@@ -17,27 +17,27 @@ class Paginate < Liquid::Block
@attributes[key] = value @attributes[key] = value
end end
else else
raise SyntaxError, "Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number" raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number")
end end
end end
def render_to_output_buffer(context, output) def render(context)
@context = context @context = context
context.stack 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, "Cannot paginate array '#{@collection_name}'. Not found." if collection_size.nil? raise ArgumentError.new("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

View File

@@ -52,7 +52,7 @@ module ShopFilter
end end
def product_img_url(url, style = 'small') def product_img_url(url, style = 'small')
unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})} unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/
raise ArgumentError, 'filter "size" can only be called on product images' raise ArgumentError, 'filter "size" can only be called on product images'
end end
@@ -60,7 +60,7 @@ module ShopFilter
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/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}" "/files/shops/random_number/products/#{$1}_#{style}.#{$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,14 +70,16 @@ 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']
paginate['parts'].each do |part| for part in paginate['parts']
html << if part['is_link']
%(<span class="page">#{link_to(part['title'], part['url'])}</span>) if part['is_link']
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
%(<span class="page current">#{part['title']}</span>) html << %(<span class="page current">#{part['title']}</span>)
else else
%(<span class="deco">#{part['title']}</span>) html << %(<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

@@ -13,11 +13,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,6 @@
module WeightFilter module WeightFilter
def weight(grams) def weight(grams)
format("%.2f", grams / 1000) sprintf("%.2f", grams / 1000)
end end
def weight_with_unit(grams) def weight_with_unit(grams)

View File

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

View File

@@ -201,9 +201,9 @@ class DropsTest < Minitest::Test
end end
def test_scope_though_proc def test_scope_though_proc
assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }) assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] })
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]) assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1])
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]) assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1])
end end
def test_scope_with_assigns def test_scope_with_assigns
@@ -241,7 +241,7 @@ 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| [ :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)
@@ -250,7 +250,7 @@ class DropsTest < Minitest::Test
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| [ :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)

View File

@@ -83,14 +83,15 @@ 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(' Liquid::Template.parse(%q(
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)
@@ -98,14 +99,15 @@ 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(' Liquid::Template.parse(%q(
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)
@@ -120,7 +122,8 @@ 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)
@@ -136,7 +139,8 @@ 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
@@ -153,7 +157,8 @@ 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
@@ -200,7 +205,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)
@@ -212,7 +217,7 @@ class ErrorHandlingTest < Minitest::Test
Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' } Liquid::Template.default_exception_renderer = ->(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)
@@ -234,7 +239,7 @@ 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

View File

@@ -2,23 +2,23 @@ require 'test_helper'
module MoneyFilter module MoneyFilter
def money(input) def money(input)
format(' %d$ ', input) sprintf(' %d$ ', input)
end end
def money_with_underscore(input) def money_with_underscore(input)
format(' %d$ ', input) sprintf(' %d$ ', input)
end end
end end
module CanadianMoneyFilter module CanadianMoneyFilter
def money(input) def money(input)
format(' %d$ CAD ', input) sprintf(' %d$ CAD ', input)
end end
end end
module SubstituteFilter module SubstituteFilter
def substitute(input, params = {}) def substitute(input, params = {})
input.gsub(/%\{(\w+)\}/) { |_match| params[Regexp.last_match(1)] } input.gsub(/%\{(\w+)\}/) { |match| params[$1] }
end end
end end
@@ -26,7 +26,7 @@ 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
@@ -149,7 +149,7 @@ class FiltersTest < Minitest::Test
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

View File

@@ -3,13 +3,13 @@ require 'test_helper'
class HashOrderingTest < Minitest::Test class HashOrderingTest < Minitest::Test
module MoneyFilter module MoneyFilter
def money(input) def money(input)
format(' %d$ ', input) sprintf(' %d$ ', input)
end end
end end
module CanadianMoneyFilter module CanadianMoneyFilter
def money(input) def money(input)
format(' %d$ CAD ', input) sprintf(' %d$ CAD ', input)
end end
end end

View File

@@ -1,7 +1,7 @@
require 'test_helper' require 'test_helper'
module FunnyFilter module FunnyFilter
def make_funny(_input) def make_funny(input)
'LOL' 'LOL'
end end
@@ -32,7 +32,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

View File

@@ -227,7 +227,7 @@ class ParseTreeVisitorTest < Minitest::Test
[[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

@@ -128,7 +128,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
@@ -145,7 +145,7 @@ class RenderProfilingTest < Minitest::Test
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" => ["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

@@ -17,7 +17,7 @@ class TestThing
"woot: #{@foo}" "woot: #{@foo}"
end end
def [](_whatever) def [](whatever)
to_s to_s
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
@@ -208,14 +208,14 @@ class StandardFiltersTest < Minitest::Test
{ "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
@@ -236,14 +236,14 @@ class StandardFiltersTest < Minitest::Test
{ "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
@@ -256,7 +256,7 @@ class StandardFiltersTest < Minitest::Test
{ "fake" => "t" }, { "fake" => "t" },
{ "key" => "a" }, { "key" => "a" },
{ "key" => "b" }, { "key" => "b" },
{ "key" => "c" }, { "key" => "c" }
] ]
expectation = [ expectation = [
{ "key" => "a" }, { "key" => "a" },
@@ -265,7 +265,7 @@ class StandardFiltersTest < Minitest::Test
{ "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 ["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"])
@@ -279,7 +279,7 @@ class StandardFiltersTest < Minitest::Test
foo = [ foo = [
[1], [1],
[2], [2],
[3], [3]
] ]
assert_raises Liquid::ArgumentError do assert_raises Liquid::ArgumentError do
@@ -295,7 +295,7 @@ class StandardFiltersTest < Minitest::Test
foo = [ foo = [
[1], [1],
[2], [2],
[3], [3]
] ]
assert_raises Liquid::ArgumentError do assert_raises Liquid::ArgumentError do
@@ -304,7 +304,7 @@ class StandardFiltersTest < Minitest::Test
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
@@ -330,7 +330,7 @@ class StandardFiltersTest < Minitest::Test
foo = [ foo = [
[1], [1],
[2], [2],
[3], [3]
] ]
assert_raises Liquid::ArgumentError do assert_raises Liquid::ArgumentError do
@@ -346,7 +346,7 @@ class StandardFiltersTest < Minitest::Test
foo = [ foo = [
[1], [1],
[2], [2],
[3], [3]
] ]
assert_raises Liquid::ArgumentError do assert_raises Liquid::ArgumentError do
@@ -380,7 +380,7 @@ class StandardFiltersTest < Minitest::Test
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
@@ -397,7 +397,7 @@ class StandardFiltersTest < Minitest::Test
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
@@ -405,10 +405,10 @@ class StandardFiltersTest < Minitest::Test
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" }}'
@@ -423,7 +423,7 @@ class StandardFiltersTest < Minitest::Test
foo = [ foo = [
[1], [1],
[2], [2],
[3], [3]
] ]
assert_raises Liquid::ArgumentError do assert_raises Liquid::ArgumentError do
@@ -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)
@@ -697,12 +697,12 @@ class StandardFiltersTest < Minitest::Test
{ "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)
@@ -714,12 +714,12 @@ class StandardFiltersTest < Minitest::Test
{ "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)
@@ -740,7 +740,7 @@ class StandardFiltersTest < Minitest::Test
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")
@@ -758,7 +758,7 @@ class StandardFiltersTest < Minitest::Test
{ "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")

View File

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

View File

@@ -23,19 +23,23 @@ class ForTagTest < Minitest::Test
yo yo
HERE HERE
template = <<~HERE template = <<HERE
{%for item in array%} {%for item in array%}
yo yo
{%endfor%} {%endfor%}
HERE HERE
assert_template_result(expected, template, 'array' => [1, 2, 3]) assert_template_result(expected, template, 'array' => [1, 2, 3])
end end
def test_for_reversed def test_for_reversed
assigns = { 'array' => [1, 2, 3] } assigns = { 'array' => [ 1, 2, 3] }
assert_template_result('321', '{%for item in array reversed %}{{item}}{%endfor%}', assigns) assert_template_result('321', '{%for item in array reversed %}{{item}}{%endfor%}', assigns)
end end
def test_for_range_reversed
assert_template_result('321', '{%for item in (1..3) reversed %}{{item}}{%endfor%}', {})
end
def test_for_with_range def test_for_with_range
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%}')

View File

@@ -132,7 +132,7 @@ class IfElseTagTest < Minitest::Test
end end
def test_syntax_error_no_variable def test_syntax_error_no_variable
assert_raises(SyntaxError) { assert_template_result('', '{% if jerry == 1 %}') } assert_raises(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}') }
end end
def test_syntax_error_no_expression def test_syntax_error_no_expression

View File

@@ -40,14 +40,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'
@@ -59,16 +59,15 @@ class CustomInclude < Liquid::Tag
def initialize(tag_name, markup, tokens) def initialize(tag_name, markup, tokens)
markup =~ Syntax markup =~ Syntax
@template_name = Regexp.last_match(1) @template_name = $1
super super
end end
def parse(tokens) def parse(tokens)
end end
def render_to_output_buffer(_context, output) def render(context)
output << @template_name[1..-2] @template_name[1..-2]
output
end end
end end
@@ -86,7 +85,7 @@ class IncludeTagTest < Minitest::Test
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
@@ -96,7 +95,7 @@ class IncludeTagTest < Minitest::Test
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
@@ -134,7 +133,7 @@ class IncludeTagTest < Minitest::Test
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

View File

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

View File

@@ -1,149 +0,0 @@
require 'test_helper'
class RenderTagTest < Minitest::Test
include Liquid
def test_render_with_no_arguments
Liquid::Template.file_system = StubFileSystem.new('source' => 'rendered content')
assert_template_result 'rendered content', '{% render "source" %}'
end
def test_render_tag_looks_for_file_system_in_registers_first
file_system = StubFileSystem.new('pick_a_source' => 'from register file system')
assert_equal 'from register file system',
Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system })
end
def test_render_passes_named_arguments_into_inner_scope
Liquid::Template.file_system = StubFileSystem.new('product' => '{{ inner_product.title }}')
assert_template_result 'My Product', '{% render "product", inner_product: outer_product %}',
'outer_product' => { 'title' => 'My Product' }
end
def test_render_accepts_literals_as_arguments
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ price }}')
assert_template_result '123', '{% render "snippet", price: 123 %}'
end
def test_render_accepts_multiple_named_arguments
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ one }} {{ two }}')
assert_template_result '1 2', '{% render "snippet", one: 1, two: 2 %}'
end
def test_render_does_not_inherit_parent_scope_variables
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ outer_variable }}')
assert_template_result '', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}'
end
def test_render_does_not_inherit_variable_with_same_name_as_snippet
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ snippet }}')
assert_template_result '', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}"
end
def test_render_sets_the_correct_template_name_for_errors
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
with_taint_mode :error do
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
template.render(context)
assert_equal [Liquid::TaintedError], template.errors.map(&:class)
assert_equal 'snippet', template.errors.first.template_name
end
end
def test_render_sets_the_correct_template_name_for_warnings
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
with_taint_mode :warn do
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
template.render(context)
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
assert_equal 'snippet', context.warnings.first.template_name
end
end
def test_render_does_not_mutate_parent_scope
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}')
assert_template_result '', "{% render 'snippet' %}{{ inner }}"
end
def test_nested_render_tag
Liquid::Template.file_system = StubFileSystem.new(
'one' => "one {% render 'two' %}",
'two' => 'two'
)
assert_template_result 'one two', "{% render 'one' %}"
end
def test_recursively_rendered_template_does_not_produce_endless_loop
Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}')
assert_raises Liquid::StackLevelError do
Template.parse('{% render "loop" %}').render!
end
end
def test_includes_and_renders_count_towards_the_same_recursion_limit
Liquid::Template.file_system = StubFileSystem.new(
'loop_render' => '{% render "loop_include" %}',
'loop_include' => '{% include "loop_render" %}'
)
assert_raises Liquid::StackLevelError do
Template.parse('{% render "loop_include" %}').render!
end
end
def test_dynamically_choosen_templates_are_not_allowed
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered')
assert_raises Liquid::SyntaxError do
Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}")
end
end
def test_include_tag_caches_second_read_of_same_partial
file_system = StubFileSystem.new('snippet' => 'echo')
assert_equal 'echoecho',
Template.parse('{% render "snippet" %}{% render "snippet" %}')
.render!({}, registers: { file_system: file_system })
assert_equal 1, file_system.file_read_count
end
def test_render_tag_doesnt_cache_partials_across_renders
file_system = StubFileSystem.new('snippet' => 'my message')
assert_equal 'my message',
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system })
assert_equal 1, file_system.file_read_count
assert_equal 'my message',
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system })
assert_equal 2, file_system.file_read_count
end
def test_render_tag_within_if_statement
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'my message')
assert_template_result 'my message', '{% if true %}{% render "snippet" %}{% endif %}'
end
def test_break_through_render
Liquid::Template.file_system = StubFileSystem.new('break' => '{% break %}')
assert_template_result '1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}'
assert_template_result '112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}'
end
def test_increment_is_isolated_between_renders
Liquid::Template.file_system = StubFileSystem.new('incr' => '{% increment %}')
assert_template_result '010', '{% increment %}{% increment %}{% render "incr" %}'
end
def test_decrement_is_isolated_between_renders
Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}')
assert_template_result '-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}'
end
end

View File

@@ -69,7 +69,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
@@ -183,32 +183,32 @@ class StandardTagTest < Minitest::Test
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
@@ -283,10 +283,10 @@ class StandardTagTest < Minitest::Test
end end
def test_ifchanged def test_ifchanged
assigns = { 'array' => [1, 1, 2, 2, 3, 3] } assigns = { 'array' => [ 1, 1, 2, 2, 3, 3] }
assert_template_result('123', '{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}', assigns) assert_template_result('123', '{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}', assigns)
assigns = { 'array' => [1, 1, 1, 1] } assigns = { 'array' => [ 1, 1, 1, 1] }
assert_template_result('1', '{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}', assigns) assert_template_result('1', '{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}', assigns)
end end

View File

@@ -224,7 +224,7 @@ class TemplateTest < Minitest::Test
end end
def test_render_bang_force_rethrow_errors_on_passed_context def test_render_bang_force_rethrow_errors_on_passed_context
context = Context.new('drop' => ErroneousDrop.new) context = Context.new({ 'drop' => ErroneousDrop.new })
t = Template.new.parse('{{ drop.bad_method }}') t = Template.new.parse('{{ drop.bad_method }}')
e = assert_raises RuntimeError do e = assert_raises RuntimeError do
@@ -267,7 +267,7 @@ class TemplateTest < Minitest::Test
def test_undefined_variables def test_undefined_variables
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}") t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true) result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
assert_equal '33 32 ', result assert_equal '33 32 ', result
assert_equal 3, t.errors.count assert_equal 3, t.errors.count
@@ -292,14 +292,14 @@ class TemplateTest < Minitest::Test
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}") t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
assert_raises UndefinedVariable do assert_raises UndefinedVariable do
t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true) t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
end end
end end
def test_undefined_drop_methods def test_undefined_drop_methods
d = DropWithUndefinedMethod.new d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}') t = Template.new.parse('{{ foo }} {{ woot }}')
result = t.render(d, strict_variables: true) result = t.render(d, { strict_variables: true })
assert_equal 'foo ', result assert_equal 'foo ', result
assert_equal 1, t.errors.count assert_equal 1, t.errors.count
@@ -311,7 +311,7 @@ class TemplateTest < Minitest::Test
t = Template.new.parse('{{ foo }} {{ woot }}') t = Template.new.parse('{{ foo }} {{ woot }}')
assert_raises UndefinedDropMethod do assert_raises UndefinedDropMethod do
t.render!(d, strict_variables: true) t.render!(d, { strict_variables: true })
end end
end end
@@ -322,7 +322,7 @@ class TemplateTest < Minitest::Test
"-#{v}-" "-#{v}-"
end end
end end
result = t.render({ 'a' => 123, 'x' => 'foo' }, filters: [filters], strict_filters: true) result = t.render({ 'a' => 123, 'x' => 'foo' }, { filters: [filters], strict_filters: true })
assert_equal '123 ', result assert_equal '123 ', result
assert_equal 1, t.errors.count assert_equal 1, t.errors.count
@@ -334,17 +334,17 @@ class TemplateTest < Minitest::Test
t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}") t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
assert_raises UndefinedFilter do assert_raises UndefinedFilter do
t.render!({ 'x' => 'foo' }, strict_filters: true) t.render!({ 'x' => 'foo' }, { strict_filters: true })
end end
end end
def test_using_range_literal_works_as_expected def test_using_range_literal_works_as_expected
t = Template.parse("{% assign foo = (x..y) %}{{ foo }}") t = Template.parse("{% assign foo = (x..y) %}{{ foo }}")
result = t.render('x' => 1, 'y' => 5) result = t.render({ 'x' => 1, 'y' => 5 })
assert_equal '1..5', result assert_equal '1..5', result
t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}") t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}")
result = t.render('x' => 1, 'y' => 5) result = t.render({ 'x' => 1, 'y' => 5 })
assert_equal '12345', result assert_equal '12345', result
end end
end end

View File

@@ -76,14 +76,14 @@ class TrimModeTest < Minitest::Test
</p> </p>
</div> </div>
END_TEMPLATE END_TEMPLATE
expected = <<~END_EXPECTED expected = <<-END_EXPECTED
<div> <div>
<p> <p>
#{whitespace} #{whitespace}
yes yes
#{whitespace} #{whitespace}
</p> </p>
</div> </div>
END_EXPECTED END_EXPECTED
assert_template_result(expected, text) assert_template_result(expected, text)
@@ -96,12 +96,12 @@ class TrimModeTest < Minitest::Test
</p> </p>
</div> </div>
END_TEMPLATE END_TEMPLATE
expected = <<~END_EXPECTED expected = <<-END_EXPECTED
<div> <div>
<p> <p>
#{whitespace} #{whitespace}
</p> </p>
</div> </div>
END_EXPECTED END_EXPECTED
assert_template_result(expected, text) assert_template_result(expected, text)
end end
@@ -337,12 +337,12 @@ class TrimModeTest < Minitest::Test
</p> </p>
</div> </div>
END_TEMPLATE END_TEMPLATE
expected = <<~END_EXPECTED expected = <<-END_EXPECTED
<div> <div>
<p> <p>
#{whitespace} #{whitespace}
</p> </p>
</div> </div>
END_EXPECTED END_EXPECTED
assert_template_result(expected, text) assert_template_result(expected, text)
end end
@@ -513,16 +513,16 @@ class TrimModeTest < Minitest::Test
{% endraw %} {% endraw %}
</div> </div>
END_TEMPLATE END_TEMPLATE
expected = <<~END_EXPECTED expected = <<-END_EXPECTED
<div> <div>
#{whitespace} #{whitespace}
{%- if true -%} {%- if true -%}
<p> <p>
{{- 'John' -}} {{- 'John' -}}
</p> </p>
{%- endif -%} {%- endif -%}
#{whitespace} #{whitespace}
</div> </div>
END_EXPECTED END_EXPECTED
assert_template_result(expected, text) assert_template_result(expected, text)
end end

View File

@@ -76,7 +76,7 @@ class VariableTest < Minitest::Test
def test_hash_with_default_proc def test_hash_with_default_proc
template = Template.parse(%(Hello {{ test }})) template = Template.parse(%(Hello {{ test }}))
assigns = Hash.new { |_h, k| raise "Unknown variable '#{k}'" } assigns = Hash.new { |h, k| raise "Unknown variable '#{k}'" }
assigns['test'] = 'Tobi' assigns['test'] = 'Tobi'
assert_equal 'Hello Tobi', template.render!(assigns) assert_equal 'Hello Tobi', template.render!(assigns)
assigns.delete('test') assigns.delete('test')

View File

@@ -14,7 +14,7 @@ if env_mode = ENV['LIQUID_PARSER_MODE']
end end
Liquid::Template.error_mode = mode Liquid::Template.error_mode = mode
if ENV['LIQUID_C'] == '1' if ENV['LIQUID-C'] == '1'
puts "-- LIQUID C" puts "-- LIQUID C"
require 'liquid/c' require 'liquid/c'
end end
@@ -37,18 +37,18 @@ module Minitest
include Liquid include Liquid
def assert_template_result(expected, template, assigns = {}, message = nil) def assert_template_result(expected, template, assigns = {}, message = nil)
assert_equal expected, Template.parse(template, line_numbers: true).render!(assigns), message assert_equal expected, Template.parse(template).render!(assigns), message
end end
def assert_template_result_matches(expected, template, assigns = {}, message = nil) def assert_template_result_matches(expected, template, assigns = {}, message = nil)
return assert_template_result(expected, template, assigns, message) unless expected.is_a?(Regexp) return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp
assert_match expected, Template.parse(template, line_numbers: true).render!(assigns), message assert_match expected, Template.parse(template).render!(assigns), message
end end
def assert_match_syntax_error(match, template, assigns = {}) def assert_match_syntax_error(match, template, assigns = {})
exception = assert_raises(Liquid::SyntaxError) do exception = assert_raises(Liquid::SyntaxError) do
Template.parse(template, line_numbers: true).render(assigns) Template.parse(template).render(assigns)
end end
assert_match match, exception.message assert_match match, exception.message
end end
@@ -84,13 +84,6 @@ module Minitest
ensure ensure
Liquid::Template.error_mode = old_mode Liquid::Template.error_mode = old_mode
end end
def with_custom_tag(tag_name, tag_class)
Liquid::Template.register_tag(tag_name, tag_class)
yield
ensure
Liquid::Template.tags.delete(tag_name)
end
end end
end end
@@ -121,17 +114,3 @@ class ErrorDrop < Liquid::Drop
raise Exception, 'exception' raise Exception, 'exception'
end end
end end
class StubFileSystem
attr_reader :file_read_count
def initialize(values)
@file_read_count = 0
@values = values
end
def read_template_file(template_path)
@file_read_count += 1
@values.fetch(template_path)
end
end

View File

@@ -44,47 +44,10 @@ class BlockUnitTest < Minitest::Test
end end
def test_with_custom_tag def test_with_custom_tag
with_custom_tag('testtag', Block) do Liquid::Template.register_tag("testtag", Block)
assert Liquid::Template.parse("{% testtag %} {% endtesttag %}") assert Liquid::Template.parse("{% testtag %} {% endtesttag %}")
end ensure
end Liquid::Template.tags.delete('testtag')
def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
klass1 = Class.new(Block) do
def render(*)
'hello'
end
end
with_custom_tag('blabla', klass1) do
template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}")
assert_equal 'hello', template.render
buf = ''
output = template.render({}, output: buf)
assert_equal 'hello', output
assert_equal 'hello', buf
assert_equal buf.object_id, output.object_id
end
klass2 = Class.new(klass1) do
def render(*)
'foo' + super + 'bar'
end
end
with_custom_tag('blabla', klass2) do
template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}")
assert_equal 'foohellobar', template.render
buf = ''
output = template.render({}, output: buf)
assert_equal 'foohellobar', output
assert_equal 'foohellobar', buf
assert_equal buf.object_id, output.object_id
end
end end
private private

View File

@@ -68,7 +68,7 @@ class ConditionUnitTest < Minitest::Test
assert_nil Condition.new({}, '>', 2).evaluate assert_nil Condition.new({}, '>', 2).evaluate
assert_nil Condition.new(2, '>', {}).evaluate assert_nil Condition.new(2, '>', {}).evaluate
assert_equal false, Condition.new({}, '==', 2).evaluate assert_equal false, Condition.new({}, '==', 2).evaluate
assert_equal true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate assert_equal true, Condition.new({ 'a' => 1 }, '==', { 'a' => 1 }).evaluate
assert_equal true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate assert_equal true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate
end end
@@ -107,11 +107,11 @@ class ConditionUnitTest < Minitest::Test
assert_equal false, condition.evaluate assert_equal false, condition.evaluate
condition.or(Condition.new(2, '==', 1)) condition.or Condition.new(2, '==', 1)
assert_equal false, condition.evaluate assert_equal false, condition.evaluate
condition.or(Condition.new(1, '==', 1)) condition.or Condition.new(1, '==', 1)
assert_equal true, condition.evaluate assert_equal true, condition.evaluate
end end
@@ -121,22 +121,22 @@ class ConditionUnitTest < Minitest::Test
assert_equal true, condition.evaluate assert_equal true, condition.evaluate
condition.and(Condition.new(2, '==', 2)) condition.and Condition.new(2, '==', 2)
assert_equal true, condition.evaluate assert_equal true, condition.evaluate
condition.and(Condition.new(2, '==', 1)) condition.and Condition.new(2, '==', 1)
assert_equal false, condition.evaluate assert_equal false, condition.evaluate
end end
def test_should_allow_custom_proc_operator def test_should_allow_custom_proc_operator
Condition.operators['starts_with'] = proc { |_cond, left, right| left =~ /^#{right}/ } Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} }
assert_evaluates_true 'bob', 'starts_with', 'b' assert_evaluates_true 'bob', 'starts_with', 'b'
assert_evaluates_false 'bob', 'starts_with', 'o' assert_evaluates_false 'bob', 'starts_with', 'o'
ensure ensure
Condition.operators.delete('starts_with') Condition.operators.delete 'starts_with'
end end
def test_left_or_right_may_contain_operators def test_left_or_right_may_contain_operators

View File

@@ -206,9 +206,9 @@ class ContextUnitTest < Minitest::Test
end end
def test_merge def test_merge
@context.merge("test" => "test") @context.merge({ "test" => "test" })
assert_equal 'test', @context['test'] assert_equal 'test', @context['test']
@context.merge("test" => "newvalue", "foo" => "bar") @context.merge({ "test" => "newvalue", "foo" => "bar" })
assert_equal 'newvalue', @context['test'] assert_equal 'newvalue', @context['test']
assert_equal 'bar', @context['foo'] assert_equal 'bar', @context['foo']
end end
@@ -235,10 +235,10 @@ class ContextUnitTest < Minitest::Test
def test_hash_to_array_transition def test_hash_to_array_transition
@context['colors'] = { @context['colors'] = {
'Blue' => ['003366', '336699', '6699CC', '99CCFF'], 'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
'Green' => ['003300', '336633', '669966', '99CC99'], 'Green' => ['003300', '336633', '669966', '99CC99'],
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'], 'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
'Red' => ['660000', '993333', 'CC6666', 'FF9999'], 'Red' => ['660000', '993333', 'CC6666', 'FF9999']
} }
assert_equal '003366', @context['colors.Blue[0]'] assert_equal '003366', @context['colors.Blue[0]']
@@ -263,7 +263,7 @@ class ContextUnitTest < Minitest::Test
def test_access_hashes_with_hash_notation def test_access_hashes_with_hash_notation
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } @context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
assert_equal 5, @context['products["count"]'] assert_equal 5, @context['products["count"]']
assert_equal 'deepsnow', @context['products["tags"][0]'] assert_equal 'deepsnow', @context['products["tags"][0]']
@@ -301,7 +301,7 @@ class ContextUnitTest < Minitest::Test
end end
def test_first_can_appear_in_middle_of_callchain def test_first_can_appear_in_middle_of_callchain
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } @context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] }
assert_equal 'draft151cm', @context['product.variants[0].title'] assert_equal 'draft151cm', @context['product.variants[0].title']
assert_equal 'element151cm', @context['product.variants[1].title'] assert_equal 'element151cm', @context['product.variants[1].title']
@@ -349,9 +349,9 @@ class ContextUnitTest < Minitest::Test
def test_ranges def test_ranges
@context.merge("test" => '5') @context.merge("test" => '5')
assert_equal (1..5), @context['(1..5)'] assert_equal ReversableRange.new(1, 5), @context['(1..5)']
assert_equal (1..5), @context['(1..test)'] assert_equal ReversableRange.new(1, 5), @context['(1..test)']
assert_equal (5..5), @context['(test..test)'] assert_equal ReversableRange.new(5, 5), @context['(test..test)']
end end
def test_cents_through_drop_nestedly def test_cents_through_drop_nestedly
@@ -453,7 +453,7 @@ class ContextUnitTest < Minitest::Test
end end
def test_context_initialization_with_a_proc_in_environment def test_context_initialization_with_a_proc_in_environment
contx = Context.new([test: ->(c) { c['poutine'] }], test: :foo) contx = Context.new([test: ->(c) { c['poutine'] }], { test: :foo })
assert contx assert contx
assert_nil contx['poutine'] assert_nil contx['poutine']
@@ -468,79 +468,11 @@ class ContextUnitTest < Minitest::Test
assert_equal 'hi filtered', context.apply_global_filter('hi') assert_equal 'hi filtered', context.apply_global_filter('hi')
end end
def test_static_environments_are_read_with_lower_priority_than_environments
context = Context.build(
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
environments: { 'shadowed' => 'dynamic' }
)
assert_equal 'dynamic', context['shadowed']
assert_equal 'static', context['unshadowed']
end
def test_apply_global_filter_when_no_global_filter_exist def test_apply_global_filter_when_no_global_filter_exist
context = Context.new context = Context.new
assert_equal 'hi', context.apply_global_filter('hi') assert_equal 'hi', context.apply_global_filter('hi')
end end
def test_new_isolated_subcontext_does_not_inherit_variables
super_context = Context.new
super_context['my_variable'] = 'some value'
subcontext = super_context.new_isolated_subcontext
assert_nil subcontext['my_variable']
end
def test_new_isolated_subcontext_inherits_static_environment
super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' })
subcontext = super_context.new_isolated_subcontext
assert_equal 'my value', subcontext['my_environment_value']
end
def test_new_isolated_subcontext_inherits_resource_limits
resource_limits = ResourceLimits.new({})
super_context = Context.new({}, {}, {}, false, resource_limits)
subcontext = super_context.new_isolated_subcontext
assert_equal resource_limits, subcontext.resource_limits
end
def test_new_isolated_subcontext_inherits_exception_renderer
super_context = Context.new
super_context.exception_renderer = ->(_e) { 'my exception message' }
subcontext = super_context.new_isolated_subcontext
assert_equal 'my exception message', subcontext.handle_error(Liquid::Error.new)
end
def test_new_isolated_subcontext_does_not_inherit_non_static_registers
registers = {
my_register: :my_value,
}
super_context = Context.new({}, {}, registers)
subcontext = super_context.new_isolated_subcontext
assert_nil subcontext.registers[:my_register]
end
def test_new_isolated_subcontext_inherits_static_registers
super_context = Context.build(static_registers: { my_register: :my_value })
subcontext = super_context.new_isolated_subcontext
assert_equal :my_value, subcontext.static_registers[:my_register]
end
def test_new_isolated_subcontext_inherits_filters
my_filter = Module.new do
def my_filter(*)
'my filter result'
end
end
super_context = Context.new
super_context.add_filters([my_filter])
subcontext = super_context.new_isolated_subcontext
template = Template.parse('{{ 123 | my_filter }}')
assert_equal 'my filter result', template.render(subcontext)
end
private private
def assert_no_object_allocations def assert_no_object_allocations

View File

@@ -1,91 +0,0 @@
require 'test_helper'
class PartialCacheUnitTest < Minitest::Test
def test_uses_the_file_system_register_if_present
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new('my_partial' => 'my partial body'),
}
)
partial = Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new
)
assert_equal 'my partial body', partial.render
end
def test_reads_from_the_file_system_only_once_per_file
file_system = StubFileSystem.new('my_partial' => 'some partial body')
context = Liquid::Context.build(
registers: { file_system: file_system }
)
2.times do
Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new
)
end
assert_equal 1, file_system.file_read_count
end
def test_cache_state_is_stored_per_context
parse_context = Liquid::ParseContext.new
shared_file_system = StubFileSystem.new(
'my_partial' => 'my shared value'
)
context_one = Liquid::Context.build(
registers: {
file_system: shared_file_system,
}
)
context_two = Liquid::Context.build(
registers: {
file_system: shared_file_system,
}
)
2.times do
Liquid::PartialCache.load(
'my_partial',
context: context_one,
parse_context: parse_context
)
end
Liquid::PartialCache.load(
'my_partial',
context: context_two,
parse_context: parse_context
)
assert_equal 2, shared_file_system.file_read_count
end
def test_cache_is_not_broken_when_a_different_parse_context_is_used
file_system = StubFileSystem.new('my_partial' => 'some partial body')
context = Liquid::Context.build(
registers: { file_system: file_system }
)
Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new(my_key: 'value one')
)
Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new(my_key: 'value two')
)
# Technically what we care about is that the file was parsed twice,
# but measuring file reads is an OK proxy for this.
assert_equal 1, file_system.file_read_count
end
end

View File

@@ -0,0 +1,116 @@
require 'test_helper'
class ReversableRangeTest < Minitest::Test
include Liquid
def test_each_iterates_through_items_in_the_range
actual_items = []
ReversableRange.new(1, 10).each { |item| actual_items << item }
expected_items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
assert_equal expected_items, actual_items
end
def test_implements_enumerable
actual_items = ReversableRange.new(1, 10).select(&:even?)
expected_items = [2, 4, 6, 8, 10]
assert_equal expected_items, actual_items
end
def test_is_not_empty_max_greater_than_min
range = ReversableRange.new(9, 10)
refute_predicate range, :empty?
end
def test_is_not_empty_max_equal_to_min
range = ReversableRange.new(10, 10)
refute_predicate range, :empty?
end
def test_is_empty_if_not_reversed_and_max_less_than_min
range = ReversableRange.new(10, 9)
assert_predicate range, :empty?
end
def test_ranges_with_the_same_max_and_min_have_one_element
actual_items = ReversableRange.new(1337, 1337).to_a
expected_items = [1337]
assert_equal expected_items, actual_items
end
def test_load_slice_returns_a_sub_range
actual_items = ReversableRange.new(1, 10).load_slice(5, 8).to_a
expected_items = [5, 6, 7, 8]
assert_equal expected_items, actual_items
end
def test_load_slice_returns_a_reversed_sub_range_if_reversed
range = ReversableRange.new(1, 10)
range.reverse!
actual_items = range.load_slice(5, 8).to_a
expected_items = [8, 7, 6, 5]
assert_equal expected_items, actual_items
end
def test_is_equal_to_other_if_also_a_reversable_range_and_has_same_properties
one = ReversableRange.new(1, 10)
one.reverse!
two = ReversableRange.new(1, 10)
two.reverse!
assert_equal one, two
end
def test_is_not_equal_to_a_non_reversable_range
range = ReversableRange.new(1, 10)
range.reverse!
refute_equal range, :something_else
end
def test_is_not_equal_if_ranges_have_different_mins
one = ReversableRange.new(1, 10)
two = ReversableRange.new(2, 10)
refute_equal one, two
end
def test_is_not_equal_if_ranges_have_different_maxes
one = ReversableRange.new(1, 10)
two = ReversableRange.new(1, 11)
refute_equal one, two
end
def test_is_not_equal_if_only_one_is_reversed
one = ReversableRange.new(1, 10)
two = ReversableRange.new(1, 10)
two.reverse!
refute_equal one, two
end
def test_to_s_mirrors_rubys_range_syntax
range = ReversableRange.new(1, 10)
assert_equal '1..10', range.to_s
end
def test_to_s_reverses_when_reversed
range = ReversableRange.new(1, 10)
range.reverse!
assert_equal '10..1', range.to_s
end
def test_size
range = ReversableRange.new(1, 10)
assert_equal 10, range.size
end
end

View File

@@ -36,8 +36,7 @@ class StrainerUnitTest < Minitest::Test
rescue Liquid::ArgumentError => e rescue Liquid::ArgumentError => e
assert_match( assert_match(
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/, /\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
e.message e.message)
)
assert_equal e.backtrace[0].split(':')[0], __FILE__ assert_equal e.backtrace[0].split(':')[0], __FILE__
end end
end end
@@ -136,7 +135,7 @@ class StrainerUnitTest < Minitest::Test
end end
module LateAddedFilter module LateAddedFilter
def late_added_filter(_input) def late_added_filter(input)
"filtered" "filtered"
end end
end end
@@ -151,7 +150,7 @@ class StrainerUnitTest < Minitest::Test
mod = Module.new do mod = Module.new do
class << self class << self
attr_accessor :include_count attr_accessor :include_count
def included(_mod) def included(mod)
self.include_count += 1 self.include_count += 1
end end
end end

View File

@@ -18,42 +18,4 @@ class TagUnitTest < Minitest::Test
tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new) tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new)
assert_equal 'some_tag', tag.tag_name assert_equal 'some_tag', tag.tag_name
end end
def test_custom_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
klass1 = Class.new(Tag) do
def render(*)
'hello'
end
end
with_custom_tag('blabla', klass1) do
template = Liquid::Template.parse("{% blabla %}")
assert_equal 'hello', template.render
buf = ''
output = template.render({}, output: buf)
assert_equal 'hello', output
assert_equal 'hello', buf
assert_equal buf.object_id, output.object_id
end
klass2 = Class.new(klass1) do
def render(*)
'foo' + super + 'bar'
end
end
with_custom_tag('blabla', klass2) do
template = Liquid::Template.parse("{% blabla %}")
assert_equal 'foohellobar', template.render
buf = ''
output = template.render({}, output: buf)
assert_equal 'foohellobar', output
assert_equal 'foohellobar', buf
assert_equal buf.object_id, output.object_id
end
end
end end

View File

@@ -3,6 +3,6 @@ require 'test_helper'
class IfTagUnitTest < Minitest::Test class IfTagUnitTest < Minitest::Test
def test_if_nodelist def test_if_nodelist
template = Liquid::Template.parse('{% if true %}IF{% else %}ELSE{% endif %}') template = Liquid::Template.parse('{% if true %}IF{% else %}ELSE{% endif %}')
assert_equal(['IF', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten) assert_equal ['IF', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten
end end
end end