Compare commits

..

5 Commits

Author SHA1 Message Date
Dylan Thacker-Smith
9567794a7a Only use MethodLiteral in condition expressions 2020-09-22 11:57:40 -04:00
Dylan Thacker-Smith
cddd8ea4ff Extract rescue code from BlockBody#render_node for re-use in liquid-c 2020-09-16 09:31:36 -04:00
Dylan Thacker-Smith
9917924adb Move some unit tests without internal coupling to integration tests
since I would like to continue supporting these tests in liquid-c
in the foreseeable future.
2020-09-15 16:32:12 -04:00
Dylan Thacker-Smith
17c3e9989a Move test/integration/parse_tree_visitor_test.rb to test/unit
The ParseTreeVisitor exposes the liquid internals that won't be
kept compatible with liquid-c, so move it out of the integration
tests directory so that we can easily ignore it when testing liquid-c
2020-09-15 16:32:12 -04:00
Dylan Thacker-Smith
2777b9d06c Allow creating symbols that are garbage collected in a test 2020-09-15 16:32:12 -04:00
66 changed files with 1030 additions and 1407 deletions

View File

@@ -1,40 +0,0 @@
name: Liquid
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
entry:
- { ruby: 2.5, allowed-failure: false }
- { ruby: 2.6, allowed-failure: false }
- { ruby: 2.7, allowed-failure: false }
- { ruby: ruby-head, allowed-failure: true }
name: test (${{ matrix.entry.ruby }})
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.entry.ruby }}
- uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
restore-keys: ${{ runner.os }}-gems-
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
- run: bundle exec rake
continue-on-error: ${{ matrix.entry.allowed-failure }}
memory_profile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
- uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
restore-keys: ${{ runner.os }}-gems-
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
- run: bundle exec rake memory_profile:run

View File

@@ -1,3 +1,5 @@
# Recommended rubocop version: ~> 0.78.0
AllCops: AllCops:
Exclude: Exclude:
- 'db/schema.rb' - 'db/schema.rb'
@@ -76,6 +78,13 @@ Style/BlockDelimiters:
- proc - proc
- it - it
Style/BracesAroundHashParameters:
EnforcedStyle: no_braces
SupportedStyles:
- braces
- no_braces
- context_dependent
Layout/CaseIndentation: Layout/CaseIndentation:
EnforcedStyle: end EnforcedStyle: end
SupportedStyles: SupportedStyles:
@@ -195,7 +204,6 @@ Style/FrozenStringLiteralComment:
SupportedStyles: SupportedStyles:
- always - always
- never - never
SafeAutoCorrect: true
Style/GlobalVars: Style/GlobalVars:
AllowedVariables: [] AllowedVariables: []
@@ -265,7 +273,7 @@ Style/MethodCallWithArgsParentheses:
- raise - raise
- puts - puts
Exclude: Exclude:
- '**/Gemfile' - Gemfile
Style/MethodDefParentheses: Style/MethodDefParentheses:
EnforcedStyle: require_parentheses EnforcedStyle: require_parentheses
@@ -578,7 +586,6 @@ Layout/BlockEndNewline:
Style/CaseEquality: Style/CaseEquality:
Enabled: true Enabled: true
AllowOnConstant: true
Style/CharacterLiteral: Style/CharacterLiteral:
Enabled: true Enabled: true
@@ -661,9 +668,6 @@ Style/IfWithSemicolon:
Style/IdenticalConditionalBranches: Style/IdenticalConditionalBranches:
Enabled: true Enabled: true
Layout/IndentationStyle:
Enabled: true
Style/InfiniteLoop: Style/InfiniteLoop:
Enabled: true Enabled: true
@@ -676,7 +680,7 @@ Style/LineEndConcatenation:
Style/MethodCallWithoutArgsParentheses: Style/MethodCallWithoutArgsParentheses:
Enabled: true Enabled: true
Lint/MissingSuper: Style/MethodMissingSuper:
Enabled: true Enabled: true
Style/MissingRespondToMissing: Style/MissingRespondToMissing:
@@ -808,6 +812,9 @@ Layout/SpaceInsideRangeLiteral:
Style/SymbolLiteral: Style/SymbolLiteral:
Enabled: true Enabled: true
Layout/Tab:
Enabled: true
Layout/TrailingWhitespace: Layout/TrailingWhitespace:
Enabled: true Enabled: true
@@ -836,7 +843,7 @@ Style/ZeroLengthPredicate:
Enabled: true Enabled: true
Layout/HeredocIndentation: Layout/HeredocIndentation:
Enabled: true EnforcedStyle: squiggly
Lint/AmbiguousOperator: Lint/AmbiguousOperator:
Enabled: true Enabled: true
@@ -874,6 +881,9 @@ Lint/EmptyEnsure:
Lint/EmptyInterpolation: Lint/EmptyInterpolation:
Enabled: true Enabled: true
Lint/EndInMethod:
Enabled: true
Lint/EnsureReturn: Lint/EnsureReturn:
Enabled: true Enabled: true
@@ -966,7 +976,7 @@ Lint/UselessAccessModifier:
Lint/UselessAssignment: Lint/UselessAssignment:
Enabled: true Enabled: true
Lint/BinaryOperatorWithIdenticalOperands: Lint/UselessComparison:
Enabled: true Enabled: true
Lint/UselessElseWithoutRescue: Lint/UselessElseWithoutRescue:
@@ -1017,6 +1027,3 @@ Style/ModuleFunction:
Lint/OrderedMagicComments: Lint/OrderedMagicComments:
Enabled: true Enabled: true
Lint/DeprecatedOpenSSLConstant:
Enabled: true

View File

@@ -9,7 +9,6 @@ Performance:
AllCops: AllCops:
TargetRubyVersion: 2.4 TargetRubyVersion: 2.4
NewCops: disable
Exclude: Exclude:
- 'vendor/bundle/**/*' - 'vendor/bundle/**/*'

View File

@@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config` # `rubocop --auto-gen-config`
# on 2020-12-11 18:53:41 UTC using RuboCop version 1.6.1. # on 2019-09-11 06:34:25 +1000 using RuboCop version 0.74.0.
# The point is for the user to remove these configuration records # 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
@@ -14,26 +14,14 @@ Lint/InheritException:
Exclude: Exclude:
- 'lib/liquid/interrupts.rb' - 'lib/liquid/interrupts.rb'
# Offense count: 113 # Offense count: 98
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https # URISchemes: http, https
Layout/LineLength: Layout/LineLength:
Max: 260 Max: 294
# Offense count: 8 # Offense count: 44
Lint/MissingSuper:
Exclude:
- 'lib/liquid/forloop_drop.rb'
- 'lib/liquid/tablerowloop_drop.rb'
- 'test/integration/assign_test.rb'
- 'test/integration/context_test.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/tags/table_row_test.rb'
# Offense count: 43
Naming/ConstantName: Naming/ConstantName:
Exclude: Exclude:
- 'lib/liquid.rb' - 'lib/liquid.rb'
@@ -44,6 +32,7 @@ Naming/ConstantName:
- 'lib/liquid/tags/cycle.rb' - 'lib/liquid/tags/cycle.rb'
- 'lib/liquid/tags/for.rb' - 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb' - 'lib/liquid/tags/if.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb' - 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb' - 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb' - 'lib/liquid/variable.rb'
@@ -51,7 +40,9 @@ Naming/ConstantName:
- 'performance/shopify/paginate.rb' - 'performance/shopify/paginate.rb'
- 'test/integration/tags/include_tag_test.rb' - 'test/integration/tags/include_tag_test.rb'
# Offense count: 2 # Offense count: 5
Style/ClassVars: Style/ClassVars:
Exclude: Exclude:
- 'lib/liquid/condition.rb' - 'lib/liquid/condition.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/template.rb'

26
.travis.yml Normal file
View File

@@ -0,0 +1,26 @@
language: ruby
cache: bundler
rvm:
- 2.4
- 2.5
- 2.6
- &latest_ruby 2.7
- ruby-head
matrix:
include:
- rvm: *latest_ruby
script: bundle exec rake memory_profile:run
name: Profiling Memory Usage
allow_failures:
- rvm: ruby-head
branches:
only:
- master
- gh-pages
- /.*-stable/
notifications:
disable: true

View File

@@ -18,7 +18,7 @@ group :benchmark, :test do
end end
group :test do group :test do
gem 'rubocop', '~> 1.4', require: false gem 'rubocop', '~> 0.78.0', require: false
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
platform :mri, :truffleruby do platform :mri, :truffleruby do

View File

@@ -1,37 +1,10 @@
# Liquid Change Log # Liquid Change Log
## 5.0.0 / 2021-01-06 ### Unreleased
### Features * Split Strainer class as a factory and a template (#1208) [Thierry Joyal]
* Add new `{% render %}` tag (#1122) [Samuel Doiron]
* Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell]
* Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li]
* Add [usage tracking](README.md#usage-tracking) [Mike Angell]
* Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell]
* Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith]
### Fixes
* Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith]
* Make sure the for tag's limit and offset are integers (#1094) [David Cornu]
* Invokable methods for enumerable reject include (#1151) [Thierry Joyal]
* Allow `default` filter to handle `false` as value (#1144) [Mike Angell]
* Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith]
* Fix duplication of text in raw tags (#1304) [Peter Zhu]
* Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith]
* Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith]
### Breaking Changes
* Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith]
* Remove support for taint checking (#1268) [Dylan Thacker-Smith]
* Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal]
* Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal] * Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
* Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith] * StaticRegisters#fetch to raise on missing key (#1250) [Thierry Joyal]
* Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
* And several internal changes
### Performance Improvements
* Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
* Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
## 4.0.3 / 2019-03-12 ## 4.0.3 / 2019-03-12

View File

@@ -9,17 +9,11 @@ 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
Rake::TestTask.new(:integration_test) do |t|
t.libs << 'lib' << 'test'
t.test_files = FileList['test/integration/**/*_test.rb']
t.verbose = false
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'
@@ -46,12 +40,12 @@ task :test do
ENV['LIQUID_C'] = '1' ENV['LIQUID_C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax' ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['integration_test'].reenable Rake::Task['base_test'].reenable
Rake::Task['integration_test'].invoke Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict' ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['integration_test'].reenable Rake::Task['base_test'].reenable
Rake::Task['integration_test'].invoke Rake::Task['base_test'].invoke
end end
end end

View File

@@ -42,8 +42,6 @@ module Liquid
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
singleton_class.send(:attr_accessor, :cache_classes) singleton_class.send(:attr_accessor, :cache_classes)
self.cache_classes = true self.cache_classes = true
end end

View File

@@ -13,7 +13,6 @@ module Liquid
@body = new_body @body = new_body
while parse_body(@body, tokens) while parse_body(@body, tokens)
end end
@body.freeze
end end
# For backwards compatibility # For backwards compatibility
@@ -48,10 +47,6 @@ module Liquid
end end
end end
def raise_tag_never_closed(block_name)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end
def block_name def block_name
@tag_name @tag_name
end end
@@ -78,7 +73,9 @@ module Liquid
@blank &&= body.blank? @blank &&= body.blank?
return false if end_tag_name == block_delimiter return false if end_tag_name == block_delimiter
raise_tag_never_closed(block_name) unless end_tag_name unless end_tag_name
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end
# this tag is not registered with the system # this tag is not registered with the system
# pass it to the current block for special handling or error reporting # pass it to the current block for special handling or error reporting

View File

@@ -4,7 +4,7 @@ require 'English'
module Liquid module Liquid
class BlockBody class BlockBody
LiquidTagToken = /\A\s*([\w#]+)\s*(.*?)\z/o 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/
@@ -19,8 +19,6 @@ module Liquid
end end
def parse(tokenizer, parse_context, &block) def parse(tokenizer, parse_context, &block)
raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
if tokenizer.for_liquid_tag if tokenizer.for_liquid_tag
@@ -30,11 +28,6 @@ module Liquid
end end
end end
def freeze
@nodelist.freeze
super
end
private def parse_for_liquid_tag(tokenizer, parse_context) private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift) while (token = tokenizer.shift)
unless token.empty? || token =~ WhitespaceOrNothing unless token.empty? || token =~ WhitespaceOrNothing
@@ -99,9 +92,7 @@ module Liquid
end end
private def parse_liquid_tag(markup, parse_context) private def parse_liquid_tag(markup, parse_context)
liquid_tag_tokenizer = parse_context.new_tokenizer( liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
markup, start_line_number: parse_context.line_number, for_liquid_tag: true
)
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup| parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
if end_tag_name if end_tag_name
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context) BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
@@ -162,11 +153,7 @@ module Liquid
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)
first_byte = previous_token.getbyte(0)
previous_token.rstrip! previous_token.rstrip!
if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
previous_token << first_byte
end
end end
end end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@@ -201,8 +188,6 @@ module Liquid
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
freeze unless frozen?
context.resource_limits.increment_render_score(@nodelist.length) context.resource_limits.increment_render_score(@nodelist.length)
idx = 0 idx = 0

View File

@@ -34,6 +34,10 @@ module Liquid
@method_name = method_name @method_name = method_name
@to_s = to_s @to_s = to_s
end end
def to_liquid
to_s
end
end end
@@method_literals = { @@method_literals = {
@@ -45,8 +49,8 @@ module Liquid
@@operators @@operators
end end
def self.parse_expression(parse_context, markup) def self.parse_expression(markup)
@@method_literals[markup] || parse_context.parse_expression(markup) @@method_literals[markup] || Expression.parse(markup)
end end
attr_reader :attachment, :child_condition attr_reader :attachment, :child_condition

View File

@@ -18,8 +18,8 @@ module Liquid
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 # rubocop:disable Metrics/ParameterLists
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block) def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {})
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block) new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments)
end end
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}) def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
@@ -34,20 +34,17 @@ module Liquid
@strict_variables = false @strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@base_scope_depth = 0 @base_scope_depth = 0
@interrupts = [] squash_instance_assigns_with_environments
@filters = []
@global_filter = nil
@disabled_tags = {}
self.exception_renderer = Template.default_exception_renderer self.exception_renderer = Template.default_exception_renderer
if rethrow_errors if rethrow_errors
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA self.exception_renderer = ->(_e) { raise }
end end
yield self if block_given? @interrupts = []
@filters = []
# Do this last, since it could result in this object being passed to a Proc in the environment @global_filter = nil
squash_instance_assigns_with_environments @disabled_tags = {}
end end
# rubocop:enable Metrics/ParameterLists # rubocop:enable Metrics/ParameterLists
@@ -137,7 +134,7 @@ module Liquid
def new_isolated_subcontext def new_isolated_subcontext
check_overflow check_overflow
self.class.build( Context.build(
resource_limits: resource_limits, resource_limits: resource_limits,
static_environments: static_environments, static_environments: static_environments,
registers: StaticRegisters.new(registers) registers: StaticRegisters.new(registers)

View File

@@ -22,7 +22,6 @@ module Liquid
def parse(tokenizer, parse_context) def parse(tokenizer, parse_context)
while parse_body(tokenizer) while parse_body(tokenizer)
end end
@body.freeze
rescue SyntaxError => e rescue SyntaxError => e
e.line_number ||= parse_context.line_number e.line_number ||= parse_context.line_number
raise raise
@@ -42,7 +41,7 @@ module Liquid
end end
def render(context) def render(context)
render_to_output_buffer(context, +'') @body.render(context)
end end
private private

View File

@@ -10,31 +10,25 @@ module Liquid
'empty' => '' 'empty' => ''
}.freeze }.freeze
SINGLE_QUOTED_STRING = /\A\s*'(.*)'\s*\z/m SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
DOUBLE_QUOTED_STRING = /\A\s*"(.*)"\s*\z/m DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
INTEGERS_REGEX = /\A\s*(-?\d+)\s*\z/ INTEGERS_REGEX = /\A(-?\d+)\z/
FLOATS_REGEX = /\A\s*(-?\d[\d\.]+)\s*\z/ FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
# Use an atomic group (?>...) to avoid pathological backtracing from
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
RANGES_REGEX = /\A\s*\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\s*\z/
def self.parse(markup) def self.parse(markup)
case markup if LITERALS.key?(markup)
when nil LITERALS[markup]
nil
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
Regexp.last_match(1)
when INTEGERS_REGEX
Regexp.last_match(1).to_i
when RANGES_REGEX
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
when FLOATS_REGEX
Regexp.last_match(1).to_f
else else
markup = markup.strip case markup
if LITERALS.key?(markup) when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
LITERALS[markup] Regexp.last_match(1)
when INTEGERS_REGEX
Regexp.last_match(1).to_i
when RANGES_REGEX
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
when FLOATS_REGEX
Regexp.last_match(1).to_f
else else
VariableLookup.parse(markup) VariableLookup.parse(markup)
end end

View File

@@ -9,12 +9,7 @@ module Liquid
@index = 0 @index = 0
end end
attr_reader :length, :parentloop attr_reader :name, :length, :parentloop
def name
Usage.increment('forloop_drop_name')
@name
end
def index def index
@index + 1 @index + 1

View File

@@ -20,6 +20,7 @@
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}" tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}" variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
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}"
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" render: "Syntax error in tag 'render' - Template name must be a quoted string"
argument: argument:

View File

@@ -23,14 +23,6 @@ module Liquid
Liquid::BlockBody.new Liquid::BlockBody.new
end end
def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag)
end
def parse_expression(markup)
Expression.parse(markup)
end
def partial=(value) def partial=(value)
@partial = value @partial = value
@options = value ? partial_options : @template_options @options = value ? partial_options : @template_options

View File

@@ -46,20 +46,16 @@ 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]
case token[0] if token[0] == :id
when :id variable_signature
str = consume elsif SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0])
str << variable_lookups
when :open_square
str = consume
str << expression
str << consume(:close_square)
str << variable_lookups
when :string, :number
consume consume
when :open_round elsif token.first == :open_round
consume consume
first = expression first = expression
consume(:dotdot) consume(:dotdot)
@@ -82,19 +78,16 @@ module Liquid
str str
end end
def variable_lookups def variable_signature
str = +"" str = consume(:id)
loop do while look(:open_square)
if look(:open_square) str << consume
str << consume str << expression
str << expression str << consume(:close_square)
str << consume(:close_square) end
elsif look(:dot) if look(:dot)
str << consume str << consume
str << consume(:id) str << variable_signature
else
break
end
end end
str str
end end

View File

@@ -2,18 +2,6 @@
module Liquid module Liquid
module ParserSwitching module ParserSwitching
def strict_parse_with_error_mode_fallback(markup)
strict_parse_with_error_context(markup)
rescue SyntaxError => e
case parse_context.error_mode
when :strict
raise
when :warn
parse_context.warnings << e
end
lax_parse(markup)
end
def parse_with_selected_parser(markup) def parse_with_selected_parser(markup)
case parse_context.error_mode case parse_context.error_mode
when :strict then strict_parse_with_error_context(markup) when :strict then strict_parse_with_error_context(markup)

View File

@@ -25,7 +25,7 @@ module Liquid
# node.code # node.code
# #
# # Which template and line number of this node. # # Which template and line number of this node.
# # The top-level template name is `nil` by default, but can be set in the Liquid::Context before rendering. # # If top level, this will be "<root>".
# node.partial # node.partial
# node.line_number # node.line_number
# #
@@ -46,94 +46,126 @@ module Liquid
include Enumerable include Enumerable
class Timing class Timing
attr_reader :code, :template_name, :line_number, :children attr_reader :code, :partial, :line_number, :children, :total_time, :self_time
attr_accessor :total_time
alias_method :render_time, :total_time
alias_method :partial, :template_name
def initialize(code: nil, template_name: nil, line_number: nil) def initialize(node, partial)
@code = code @code = node.respond_to?(:raw) ? node.raw : node
@template_name = template_name @partial = partial
@line_number = line_number @line_number = node.respond_to?(:line_number) ? node.line_number : nil
@children = [] @children = []
end end
def self_time def self.start(node, partial)
@self_time ||= begin new(node, partial).tap(&:start)
total_children_time = 0.0 end
def start
@start_time = Time.now
end
def finish
@end_time = Time.now
@total_time = @end_time - @start_time
if @children.empty?
@self_time = @total_time
else
total_children_time = 0
@children.each do |child| @children.each do |child|
total_children_time += child.total_time total_children_time += child.total_time
end end
@total_time - total_children_time @self_time = @total_time - total_children_time
end end
end end
def render_time
@end_time - @start_time
end
end end
attr_reader :total_time def self.profile_node_render(node)
alias_method :total_render_time, :total_time if Profiler.current_profile && node.respond_to?(:render)
Profiler.current_profile.start_node(node)
def initialize output = yield
@root_children = [] Profiler.current_profile.end_node(node)
@current_children = nil output
@total_time = 0.0
end
def profile(template_name, &block)
# nested renders are done from a tag that already has a timing node
return yield if @current_children
root_children = @root_children
render_idx = root_children.length
begin
@current_children = root_children
profile_node(template_name, &block)
ensure
@current_children = nil
if (timing = root_children[render_idx])
@total_time += timing.total_time
end
end
end
def children
children = @root_children
if children.length == 1
children.first.children
else else
children yield
end end
end end
def self.profile_children(template_name)
if Profiler.current_profile
Profiler.current_profile.push_partial(template_name)
output = yield
Profiler.current_profile.pop_partial
output
else
yield
end
end
def self.current_profile
Thread.current[:liquid_profiler]
end
def initialize(partial_name = "<root>")
@partial_stack = [partial_name]
@root_timing = Timing.new("", current_partial)
@timing_stack = [@root_timing]
@render_start_at = Time.now
@render_end_at = @render_start_at
end
def start
Thread.current[:liquid_profiler] = self
@render_start_at = Time.now
end
def stop
Thread.current[:liquid_profiler] = nil
@render_end_at = Time.now
end
def total_render_time
@render_end_at - @render_start_at
end
def each(&block) def each(&block)
children.each(&block) @root_timing.children.each(&block)
end end
def [](idx) def [](idx)
children[idx] @root_timing.children[idx]
end end
def length def length
children.length @root_timing.children.length
end end
def profile_node(template_name, code: nil, line_number: nil) def start_node(node)
timing = Timing.new(code: code, template_name: template_name, line_number: line_number) @timing_stack.push(Timing.start(node, current_partial))
parent_children = @current_children
start_time = monotonic_time
begin
@current_children = timing.children
yield
ensure
@current_children = parent_children
timing.total_time = monotonic_time - start_time
parent_children << timing
end
end end
private def end_node(_node)
timing = @timing_stack.pop
timing.finish
def monotonic_time @timing_stack.last.children << timing
Process.clock_gettime(Process::CLOCK_MONOTONIC) end
def current_partial
@partial_stack.last
end
def push_partial(partial_name)
@partial_stack.push(partial_name)
end
def pop_partial
@partial_stack.pop
end end
end end
end end

View File

@@ -3,33 +3,19 @@
module Liquid module Liquid
module BlockBodyProfilingHook module BlockBodyProfilingHook
def render_node(context, output, node) def render_node(context, output, node)
if (profiler = context.profiler) Profiler.profile_node_render(node) do
profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do
super
end
else
super super
end end
end end
end end
BlockBody.prepend(BlockBodyProfilingHook) BlockBody.prepend(BlockBodyProfilingHook)
module DocumentProfilingHook module IncludeProfilingHook
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
return super unless context.profiler Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
context.profiler.profile(context.template_name) { super } super
end
end end
end end
Document.prepend(DocumentProfilingHook) Include.prepend(IncludeProfilingHook)
module ContextProfilingHook
attr_accessor :profiler
def new_isolated_subcontext
new_context = super
new_context.profiler = profiler
new_context
end
end
Context.prepend(ContextProfilingHook)
end end

View File

@@ -55,11 +55,5 @@ module Liquid
def blank? def blank?
false false
end end
private
def parse_expression(markup)
parse_context.parse_expression(markup)
end
end end
end end

View File

@@ -12,20 +12,15 @@ module Liquid
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
# @api private
def self.raise_syntax_error(parse_context)
raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
end
attr_reader :to, :from attr_reader :to, :from
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@to = Regexp.last_match(1) @to = Regexp.last_match(1)
@from = Variable.new(Regexp.last_match(2), parse_context) @from = Variable.new(Regexp.last_match(2), options)
else else
self.class.raise_syntax_error(parse_context) raise SyntaxError, options[:locale].t('errors.syntax.assign')
end end
end end
@@ -45,18 +40,11 @@ module Liquid
def assign_score_of(val) def assign_score_of(val)
if val.instance_of?(String) if val.instance_of?(String)
val.bytesize val.bytesize
elsif val.instance_of?(Array) elsif val.instance_of?(Array) || val.instance_of?(Hash)
sum = 1 sum = 1
# Uses #each to avoid extra allocations. # Uses #each to avoid extra allocations.
val.each { |child| sum += assign_score_of(child) } val.each { |child| sum += assign_score_of(child) }
sum sum
elsif val.instance_of?(Hash)
sum = 1
val.each do |key, entry_value|
sum += assign_score_of(key)
sum += assign_score_of(entry_value)
end
sum
else else
1 1
end end

View File

@@ -12,23 +12,18 @@ module Liquid
@blocks = [] @blocks = []
if markup =~ Syntax if markup =~ Syntax
@left = parse_expression(Regexp.last_match(1)) @left = Expression.parse(Regexp.last_match(1))
else else
raise SyntaxError, options[:locale].t("errors.syntax.case") raise SyntaxError, options[:locale].t("errors.syntax.case")
end end
end end
def parse(tokens) def parse(tokens)
body = case_body = new_body body = new_body
body = @blocks.last.attachment while parse_body(body, tokens) body = @blocks.last.attachment while parse_body(body, tokens)
@blocks.reverse_each do |condition| if blank?
body = condition.attachment @blocks.each { |condition| condition.attachment.remove_blank_strings }
unless body.frozen?
body.remove_blank_strings if blank?
body.freeze
end
end end
case_body.freeze
end end
def nodelist def nodelist
@@ -73,7 +68,7 @@ module Liquid
markup = Regexp.last_match(2) markup = Regexp.last_match(2)
block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1))) block = Condition.new(@left, '==', Condition.parse_expression(Regexp.last_match(1)))
block.attach(body) block.attach(body)
@blocks << block @blocks << block
end end

View File

@@ -24,7 +24,7 @@ module Liquid
case markup case markup
when NamedSyntax when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2)) @variables = variables_from_string(Regexp.last_match(2))
@name = parse_expression(Regexp.last_match(1)) @name = Expression.parse(Regexp.last_match(1))
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
@@ -61,7 +61,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) ? parse_expression(Regexp.last_match(1)) : nil Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil
end.compact end.compact
end end

View File

@@ -63,11 +63,9 @@ module Liquid
parse_body(@else_block, tokens) parse_body(@else_block, tokens)
end end
if blank? if blank?
@else_block&.remove_blank_strings
@for_block.remove_blank_strings @for_block.remove_blank_strings
@else_block&.remove_blank_strings
end end
@else_block&.freeze
@for_block.freeze
end end
def nodelist def nodelist
@@ -99,7 +97,7 @@ module Liquid
collection_name = Regexp.last_match(2) collection_name = Regexp.last_match(2)
@reversed = !!Regexp.last_match(3) @reversed = !!Regexp.last_match(3)
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = parse_expression(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
@@ -114,7 +112,7 @@ module Liquid
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in') raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
collection_name = p.expression collection_name = p.expression
@collection_name = parse_expression(collection_name) @collection_name = Expression.parse(collection_name)
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed') @reversed = p.id?('reversed')
@@ -198,13 +196,12 @@ module Liquid
case key case key
when 'offset' when 'offset'
@from = if expr == 'continue' @from = if expr == 'continue'
Usage.increment('for_offset_continue')
:continue :continue
else else
parse_expression(expr) Expression.parse(expr)
end end
when 'limit' when 'limit'
@limit = parse_expression(expr) @limit = Expression.parse(expr)
end end
end end

View File

@@ -31,17 +31,13 @@ module Liquid
def parse(tokens) def parse(tokens)
while parse_body(@blocks.last.attachment, tokens) while parse_body(@blocks.last.attachment, tokens)
end end
@blocks.reverse_each do |block| if blank?
block.attachment.remove_blank_strings if blank? @blocks.each { |condition| condition.attachment.remove_blank_strings }
block.attachment.freeze
end end
end end
ELSE_TAG_NAMES = ['elsif', 'else'].freeze
private_constant :ELSE_TAG_NAMES
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
if ELSE_TAG_NAMES.include?(tag) if ['elsif', 'else'].include?(tag)
push_block(tag, markup) push_block(tag, markup)
else else
super super
@@ -72,7 +68,7 @@ module Liquid
end end
def parse_expression(markup) def parse_expression(markup)
Condition.parse_expression(parse_context, markup) Condition.parse_expression(markup)
end end
def lax_parse(markup) def lax_parse(markup)

View File

@@ -32,12 +32,12 @@ module Liquid
variable_name = Regexp.last_match(3) variable_name = Regexp.last_match(3)
@alias_name = Regexp.last_match(5) @alias_name = Regexp.last_match(5)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = parse_expression(template_name) @template_name_expr = Expression.parse(template_name)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value) @attributes[key] = Expression.parse(value)
end end
else else

View File

@@ -1,15 +0,0 @@
# frozen_string_literal: true
module Liquid
class InlineComment < Tag
def blank?
true
end
def render_to_output_buffer(_context, _output)
end
end
Template.register_tag('--', InlineComment)
end

View File

@@ -14,14 +14,14 @@ module Liquid
def parse(tokens) def parse(tokens)
@body = +'' @body = +''
while (token = tokens.shift) while (token = tokens.shift)
if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2) if token =~ FullTokenPossiblyInvalid
@body << Regexp.last_match(1) if Regexp.last_match(1) != "" @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return return if block_delimiter == Regexp.last_match(2)
end end
@body << token unless token.empty? @body << token unless token.empty?
end end
raise_tag_never_closed(block_name) raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end end
def render_to_output_buffer(_context, output) def render_to_output_buffer(_context, output)

View File

@@ -19,13 +19,13 @@ module Liquid
variable_name = Regexp.last_match(4) variable_name = Regexp.last_match(4)
@alias_name = Regexp.last_match(6) @alias_name = Regexp.last_match(6)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = parse_expression(template_name) @template_name_expr = Expression.parse(template_name)
@for = (with_or_for == FOR) @for = (with_or_for == FOR)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value) @attributes[key] = Expression.parse(value)
end end
end end

View File

@@ -10,10 +10,10 @@ module Liquid
super super
if markup =~ Syntax if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = Regexp.last_match(1)
@collection_name = parse_expression(Regexp.last_match(2)) @collection_name = Expression.parse(Regexp.last_match(2))
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value) @attributes[key] = Expression.parse(value)
end end
else else
raise SyntaxError, options[:locale].t("errors.syntax.table_row") raise SyntaxError, options[:locale].t("errors.syntax.table_row")

View File

@@ -106,9 +106,12 @@ module Liquid
# Parse source code. # Parse source code.
# Returns self for easy chaining # Returns self for easy chaining
def parse(source, options = {}) def parse(source, options = {})
parse_context = configure_options(options) @options = options
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1) @profiling = options[:profile]
@root = Document.parse(tokenizer, parse_context) @line_numbers = options[:line_numbers] || @profiling
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
@root = Document.parse(tokenize(source), parse_context)
@warnings = parse_context.warnings
self self
end end
@@ -150,7 +153,7 @@ module Liquid
c = args.shift c = args.shift
if @rethrow_errors if @rethrow_errors
c.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA c.exception_renderer = ->(_e) { raise }
end end
c c
@@ -186,13 +189,12 @@ module Liquid
# Retrying a render resets resource usage # Retrying a render resets resource usage
context.resource_limits.reset context.resource_limits.reset
if @profiling && context.profiler.nil?
@profiler = context.profiler = Liquid::Profiler.new
end
begin begin
# render the nodelist. # render the nodelist.
@root.render_to_output_buffer(context, output || +'') # for performance reasons we get an array back here. join will make a string out of it.
with_profiling(context) do
@root.render_to_output_buffer(context, output || +'')
end
rescue Liquid::MemoryError => e rescue Liquid::MemoryError => e
context.handle_error(e) context.handle_error(e)
ensure ensure
@@ -211,17 +213,25 @@ module Liquid
private private
def configure_options(options) def tokenize(source)
if (profiling = options[:profile]) Tokenizer.new(source, @line_numbers)
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler) end
end
@options = options def with_profiling(context)
@profiling = profiling if @profiling && !context.partial
@line_numbers = options[:line_numbers] || @profiling raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
@warnings = parse_context.warnings @profiler = Profiler.new(context.template_name)
parse_context @profiler.start
begin
yield
ensure
@profiler.stop
end
else
yield
end
end end
def apply_options_to_context(context, options) def apply_options_to_context(context, options)

View File

@@ -30,7 +30,7 @@ module Liquid
@parse_context = parse_context @parse_context = parse_context
@line_number = parse_context.line_number @line_number = parse_context.line_number
strict_parse_with_error_mode_fallback(markup) parse_with_selected_parser(markup)
end end
def raw def raw
@@ -63,8 +63,6 @@ module Liquid
@filters = [] @filters = []
p = Parser.new(markup) p = Parser.new(markup)
return if p.look(:end_of_string)
@name = Expression.parse(p.expression) @name = Expression.parse(p.expression)
while p.consume?(:pipe) while p.consume?(:pipe)
filtername = p.consume(:id) filtername = p.consume(:id)
@@ -83,11 +81,9 @@ module Liquid
end end
def render(context) def render(context)
obj = context.evaluate(@name) obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
@filters.each do |filter_name, filter_args, filter_kwargs|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs) filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
obj = context.invoke(filter_name, obj, *filter_args) context.invoke(filter_name, output, *filter_args)
end end
context.apply_global_filter(obj) context.apply_global_filter(obj)

View File

@@ -2,5 +2,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
VERSION = "5.0.0" VERSION = "4.0.3"
end end

View File

@@ -17,11 +17,9 @@ 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.5.0" s.required_ruby_version = ">= 2.4.0"
s.required_rubygems_version = ">= 1.3.7" s.required_rubygems_version = ">= 1.3.7"
s.metadata['allowed_push_host'] = 'https://rubygems.org'
s.test_files = Dir.glob("{test}/**/*") s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md) s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)

View File

@@ -73,14 +73,10 @@ class ThemeRunner
private private
def render_layout(template, layout, assigns)
assigns['content_for_layout'] = template.render!(assigns)
layout&.render!(assigns)
end
def compile_and_render(template, layout, assigns, page_template, template_file) def compile_and_render(template, layout, assigns, page_template, template_file)
compiled_test = compile_test(template, layout, assigns, page_template, template_file) compiled_test = compile_test(template, layout, assigns, page_template, template_file)
render_layout(compiled_test[:tmpl], compiled_test[:layout], compiled_test[:assigns]) assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
compiled_test[:layout].render!(assigns) if layout
end end
def compile_all_tests def compile_all_tests

View File

@@ -44,74 +44,7 @@ class AssignTest < Minitest::Test
end end
end end
with_error_mode(:lax) do with_error_mode(:lax) do
assert(Template.parse("{% assign foo = ('X' | downcase) %}")) assert Template.parse("{% assign foo = ('X' | downcase) %}")
end end
end end
end # AssignTest
def test_expression_with_whitespace_in_square_brackets
source = "{% assign r = a[ 'b' ] %}{{ r }}"
assert_template_result('result', source, 'a' => { 'b' => 'result' })
end
def test_assign_score_exceeding_resource_limit
t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
t.resource_limits.assign_score_limit = 1
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
t.resource_limits.assign_score_limit = 2
assert_equal("", t.render!)
refute_nil(t.resource_limits.assign_score)
end
def test_assign_score_exceeding_limit_from_composite_object
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
t.resource_limits.assign_score_limit = 3
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
t.resource_limits.assign_score_limit = 5
assert_equal("", t.render!)
end
def test_assign_score_of_int
assert_equal(1, assign_score_of(123))
end
def test_assign_score_of_string_counts_bytes
assert_equal(3, assign_score_of('123'))
assert_equal(5, assign_score_of('12345'))
assert_equal(9, assign_score_of('すごい'))
end
def test_assign_score_of_array
assert_equal(1, assign_score_of([]))
assert_equal(2, assign_score_of([123]))
assert_equal(6, assign_score_of([123, 'abcd']))
end
def test_assign_score_of_hash
assert_equal(1, assign_score_of({}))
assert_equal(5, assign_score_of('int' => 123))
assert_equal(12, assign_score_of('int' => 123, 'str' => 'abcd'))
end
private
class ObjectWrapperDrop < Liquid::Drop
def initialize(obj)
@obj = obj
end
def value
@obj
end
end
def assign_score_of(obj)
context = Liquid::Context.new('drop' => ObjectWrapperDrop.new(obj))
Liquid::Template.parse('{% assign obj = drop.value %}').render!(context)
context.resource_limits.assign_score
end
end

View File

@@ -14,7 +14,7 @@ class BlockTest < Minitest::Test
def test_with_custom_tag def test_with_custom_tag
with_custom_tag('testtag', Block) do with_custom_tag('testtag', Block) do
assert(Liquid::Template.parse("{% testtag %} {% endtesttag %}")) assert Liquid::Template.parse("{% testtag %} {% endtesttag %}")
end end
end end
@@ -28,13 +28,13 @@ class BlockTest < Minitest::Test
with_custom_tag('blabla', klass1) do with_custom_tag('blabla', klass1) do
template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}") template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}")
assert_equal('hello', template.render) assert_equal 'hello', template.render
buf = +'' buf = +''
output = template.render({}, output: buf) output = template.render({}, output: buf)
assert_equal('hello', output) assert_equal 'hello', output
assert_equal('hello', buf) assert_equal 'hello', buf
assert_equal(buf.object_id, output.object_id) assert_equal buf.object_id, output.object_id
end end
klass2 = Class.new(klass1) do klass2 = Class.new(klass1) do
@@ -46,13 +46,13 @@ class BlockTest < Minitest::Test
with_custom_tag('blabla', klass2) do with_custom_tag('blabla', klass2) do
template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}") template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}")
assert_equal('foohellobar', template.render) assert_equal 'foohellobar', template.render
buf = +'' buf = +''
output = template.render({}, output: buf) output = template.render({}, output: buf)
assert_equal('foohellobar', output) assert_equal 'foohellobar', output
assert_equal('foohellobar', buf) assert_equal 'foohellobar', buf
assert_equal(buf.object_id, output.object_id) assert_equal buf.object_id, output.object_id
end end
end end
end end

View File

@@ -49,10 +49,4 @@ class CaptureTest < Minitest::Test
rendered = template.render! rendered = template.render!
assert_equal("3-3", rendered.gsub(/\s/, '')) assert_equal("3-3", rendered.gsub(/\s/, ''))
end end
end # CaptureTest
def test_increment_assign_score_by_bytes_not_characters
t = Template.parse("{% capture foo %}すごい{% endcapture %}")
t.render!
assert_equal(9, t.resource_limits.assign_score)
end
end

View File

@@ -0,0 +1,608 @@
# frozen_string_literal: true
require 'test_helper'
class HundredCentes
def to_liquid
100
end
end
class CentsDrop < Liquid::Drop
def amount
HundredCentes.new
end
def non_zero?
true
end
end
class ContextSensitiveDrop < Liquid::Drop
def test
@context['test']
end
end
class Category < Liquid::Drop
attr_accessor :name
def initialize(name)
@name = name
end
def to_liquid
CategoryDrop.new(self)
end
end
class CategoryDrop
attr_accessor :category, :context
def initialize(category)
@category = category
end
end
class CounterDrop < Liquid::Drop
def count
@count ||= 0
@count += 1
end
end
class ArrayLike
def fetch(index)
end
def [](index)
@counts ||= []
@counts[index] ||= 0
@counts[index] += 1
end
def to_liquid
self
end
end
class ContextTest < Minitest::Test
include Liquid
def setup
@context = Liquid::Context.new
end
def test_variables
@context['string'] = 'string'
assert_equal('string', @context['string'])
@context['num'] = 5
assert_equal(5, @context['num'])
@context['time'] = Time.parse('2006-06-06 12:00:00')
assert_equal(Time.parse('2006-06-06 12:00:00'), @context['time'])
@context['date'] = Date.today
assert_equal(Date.today, @context['date'])
now = Time.now
@context['datetime'] = now
assert_equal(now, @context['datetime'])
@context['bool'] = true
assert_equal(true, @context['bool'])
@context['bool'] = false
assert_equal(false, @context['bool'])
@context['nil'] = nil
assert_nil(@context['nil'])
assert_nil(@context['nil'])
end
def test_variables_not_existing
assert_nil(@context['does_not_exist'])
end
def test_scoping
@context.push
@context.pop
assert_raises(Liquid::ContextError) do
@context.pop
end
assert_raises(Liquid::ContextError) do
@context.push
@context.pop
@context.pop
end
end
def test_length_query
@context['numbers'] = [1, 2, 3, 4]
assert_equal(4, @context['numbers.size'])
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4 }
assert_equal(4, @context['numbers.size'])
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 }
assert_equal(1000, @context['numbers.size'])
end
def test_hyphenated_variable
@context['oh-my'] = 'godz'
assert_equal('godz', @context['oh-my'])
end
def test_add_filter
filter = Module.new do
def hi(output)
output + ' hi!'
end
end
context = Context.new
context.add_filters(filter)
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
context = Context.new
assert_equal('hi?', context.invoke(:hi, 'hi?'))
context.add_filters(filter)
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
end
def test_only_intended_filters_make_it_there
filter = Module.new do
def hi(output)
output + ' hi!'
end
end
context = Context.new
assert_equal("Wookie", context.invoke("hi", "Wookie"))
context.add_filters(filter)
assert_equal("Wookie hi!", context.invoke("hi", "Wookie"))
end
def test_add_item_in_outer_scope
@context['test'] = 'test'
@context.push
assert_equal('test', @context['test'])
@context.pop
assert_equal('test', @context['test'])
end
def test_add_item_in_inner_scope
@context.push
@context['test'] = 'test'
assert_equal('test', @context['test'])
@context.pop
assert_nil(@context['test'])
end
def test_hierachical_data
@context['hash'] = { "name" => 'tobi' }
assert_equal('tobi', @context['hash.name'])
assert_equal('tobi', @context['hash["name"]'])
end
def test_keywords
assert_equal(true, @context['true'])
assert_equal(false, @context['false'])
end
def test_digits
assert_equal(100, @context['100'])
assert_equal(100.00, @context['100.00'])
end
def test_strings
assert_equal("hello!", @context['"hello!"'])
assert_equal("hello!", @context["'hello!'"])
end
def test_merge
@context.merge("test" => "test")
assert_equal('test', @context['test'])
@context.merge("test" => "newvalue", "foo" => "bar")
assert_equal('newvalue', @context['test'])
assert_equal('bar', @context['foo'])
end
def test_array_notation
@context['test'] = [1, 2, 3, 4, 5]
assert_equal(1, @context['test[0]'])
assert_equal(2, @context['test[1]'])
assert_equal(3, @context['test[2]'])
assert_equal(4, @context['test[3]'])
assert_equal(5, @context['test[4]'])
end
def test_recoursive_array_notation
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
assert_equal(1, @context['test.test[0]'])
@context['test'] = [{ 'test' => 'worked' }]
assert_equal('worked', @context['test[0].test'])
end
def test_hash_to_array_transition
@context['colors'] = {
'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
'Green' => ['003300', '336633', '669966', '99CC99'],
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
}
assert_equal('003366', @context['colors.Blue[0]'])
assert_equal('FF9999', @context['colors.Red[3]'])
end
def test_try_first
@context['test'] = [1, 2, 3, 4, 5]
assert_equal(1, @context['test.first'])
assert_equal(5, @context['test.last'])
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
assert_equal(1, @context['test.test.first'])
assert_equal(5, @context['test.test.last'])
@context['test'] = [1]
assert_equal(1, @context['test.first'])
assert_equal(1, @context['test.last'])
end
def test_access_hashes_with_hash_notation
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
assert_equal(5, @context['products["count"]'])
assert_equal('deepsnow', @context['products["tags"][0]'])
assert_equal('deepsnow', @context['products["tags"].first'])
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
assert_equal('element151cm', @context['product["variants"][1]["title"]'])
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
assert_equal('element151cm', @context['product["variants"].last["title"]'])
end
def test_access_variable_with_hash_notation
@context['foo'] = 'baz'
@context['bar'] = 'foo'
assert_equal('baz', @context['["foo"]'])
assert_equal('baz', @context['[bar]'])
end
def test_access_hashes_with_hash_access_variables
@context['var'] = 'tags'
@context['nested'] = { 'var' => 'tags' }
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
assert_equal('deepsnow', @context['products[var].first'])
assert_equal('freestyle', @context['products[nested.var].last'])
end
def test_hash_notation_only_for_hash_access
@context['array'] = [1, 2, 3, 4, 5]
@context['hash'] = { 'first' => 'Hello' }
assert_equal(1, @context['array.first'])
assert_nil(@context['array["first"]'])
assert_equal('Hello', @context['hash["first"]'])
end
def test_first_can_appear_in_middle_of_callchain
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
assert_equal('draft151cm', @context['product.variants[0].title'])
assert_equal('element151cm', @context['product.variants[1].title'])
assert_equal('draft151cm', @context['product.variants.first.title'])
assert_equal('element151cm', @context['product.variants.last.title'])
end
def test_cents
@context.merge("cents" => HundredCentes.new)
assert_equal(100, @context['cents'])
end
def test_nested_cents
@context.merge("cents" => { 'amount' => HundredCentes.new })
assert_equal(100, @context['cents.amount'])
@context.merge("cents" => { 'cents' => { 'amount' => HundredCentes.new } })
assert_equal(100, @context['cents.cents.amount'])
end
def test_cents_through_drop
@context.merge("cents" => CentsDrop.new)
assert_equal(100, @context['cents.amount'])
end
def test_nested_cents_through_drop
@context.merge("vars" => { "cents" => CentsDrop.new })
assert_equal(100, @context['vars.cents.amount'])
end
def test_drop_methods_with_question_marks
@context.merge("cents" => CentsDrop.new)
assert(@context['cents.non_zero?'])
end
def test_context_from_within_drop
@context.merge("test" => '123', "vars" => ContextSensitiveDrop.new)
assert_equal('123', @context['vars.test'])
end
def test_nested_context_from_within_drop
@context.merge("test" => '123', "vars" => { "local" => ContextSensitiveDrop.new })
assert_equal('123', @context['vars.local.test'])
end
def test_ranges
@context.merge("test" => '5')
assert_equal((1..5), @context['(1..5)'])
assert_equal((1..5), @context['(1..test)'])
assert_equal((5..5), @context['(test..test)'])
end
def test_cents_through_drop_nestedly
@context.merge("cents" => { "cents" => CentsDrop.new })
assert_equal(100, @context['cents.cents.amount'])
@context.merge("cents" => { "cents" => { "cents" => CentsDrop.new } })
assert_equal(100, @context['cents.cents.cents.amount'])
end
def test_drop_with_variable_called_only_once
@context['counter'] = CounterDrop.new
assert_equal(1, @context['counter.count'])
assert_equal(2, @context['counter.count'])
assert_equal(3, @context['counter.count'])
end
def test_drop_with_key_called_only_once
@context['counter'] = CounterDrop.new
assert_equal(1, @context['counter["count"]'])
assert_equal(2, @context['counter["count"]'])
assert_equal(3, @context['counter["count"]'])
end
def test_proc_as_variable
@context['dynamic'] = proc { 'Hello' }
assert_equal('Hello', @context['dynamic'])
end
def test_lambda_as_variable
@context['dynamic'] = proc { 'Hello' }
assert_equal('Hello', @context['dynamic'])
end
def test_nested_lambda_as_variable
@context['dynamic'] = { "lambda" => proc { 'Hello' } }
assert_equal('Hello', @context['dynamic.lambda'])
end
def test_array_containing_lambda_as_variable
@context['dynamic'] = [1, 2, proc { 'Hello' }, 4, 5]
assert_equal('Hello', @context['dynamic[2]'])
end
def test_lambda_is_called_once
@context['callcount'] = proc {
@global ||= 0
@global += 1
@global.to_s
}
assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount'])
@global = nil
end
def test_nested_lambda_is_called_once
@context['callcount'] = { "lambda" => proc {
@global ||= 0
@global += 1
@global.to_s
} }
assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda'])
@global = nil
end
def test_lambda_in_array_is_called_once
@context['callcount'] = [1, 2, proc {
@global ||= 0
@global += 1
@global.to_s
}, 4, 5]
assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]'])
@global = nil
end
def test_access_to_context_from_proc
@context.registers[:magic] = 345392
@context['magic'] = proc { @context.registers[:magic] }
assert_equal(345392, @context['magic'])
end
def test_to_liquid_and_context_at_first_level
@context['category'] = Category.new("foobar")
assert_kind_of(CategoryDrop, @context['category'])
assert_equal(@context, @context['category'].context)
end
def test_interrupt_avoids_object_allocations
assert_no_object_allocations do
@context.interrupt?
end
end
def test_context_initialization_with_a_proc_in_environment
contx = Context.new([test: ->(c) { c['poutine'] }], test: :foo)
assert(contx)
assert_nil(contx['poutine'])
end
def test_apply_global_filter
global_filter_proc = ->(output) { "#{output} filtered" }
context = Context.new
context.global_filter = global_filter_proc
assert_equal('hi filtered', context.apply_global_filter('hi'))
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
context = Context.new
assert_equal('hi', context.apply_global_filter('hi'))
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({}, {}, StaticRegisters.new(registers))
super_context.registers[:my_register] = :my_alt_value
subcontext = super_context.new_isolated_subcontext
assert_equal(:my_value, subcontext.registers[:my_register])
end
def test_new_isolated_subcontext_inherits_static_registers
super_context = Context.build(registers: { my_register: :my_value })
subcontext = super_context.new_isolated_subcontext
assert_equal(:my_value, subcontext.registers[:my_register])
end
def test_new_isolated_subcontext_registers_do_not_pollute_context
super_context = Context.build(registers: { my_register: :my_value })
subcontext = super_context.new_isolated_subcontext
subcontext.registers[:my_register] = :my_alt_value
assert_equal(:my_value, super_context.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
def test_disables_tag_specified
context = Context.new
context.with_disabled_tags(%w(foo bar)) do
assert_equal true, context.tag_disabled?("foo")
assert_equal true, context.tag_disabled?("bar")
assert_equal false, context.tag_disabled?("unknown")
end
end
def test_disables_nested_tags
context = Context.new
context.with_disabled_tags(["foo"]) do
context.with_disabled_tags(["foo"]) do
assert_equal true, context.tag_disabled?("foo")
assert_equal false, context.tag_disabled?("bar")
end
context.with_disabled_tags(["bar"]) do
assert_equal true, context.tag_disabled?("foo")
assert_equal true, context.tag_disabled?("bar")
context.with_disabled_tags(["foo"]) do
assert_equal true, context.tag_disabled?("foo")
assert_equal true, context.tag_disabled?("bar")
end
end
assert_equal true, context.tag_disabled?("foo")
assert_equal false, context.tag_disabled?("bar")
end
end
private
def assert_no_object_allocations
unless RUBY_ENGINE == 'ruby'
skip("stackprof needed to count object allocations")
end
require 'stackprof'
profile = StackProf.run(mode: :object) do
yield
end
assert_equal(0, profile[:samples])
end
end # ContextTest

View File

@@ -2,596 +2,9 @@
require 'test_helper' require 'test_helper'
class HundredCentes
def to_liquid
100
end
end
class CentsDrop < Liquid::Drop
def amount
HundredCentes.new
end
def non_zero?
true
end
end
class ContextSensitiveDrop < Liquid::Drop
def test
@context['test']
end
end
class Category < Liquid::Drop
attr_accessor :name
def initialize(name)
@name = name
end
def to_liquid
CategoryDrop.new(self)
end
end
class CategoryDrop
attr_accessor :category, :context
def initialize(category)
@category = category
end
end
class CounterDrop < Liquid::Drop
def count
@count ||= 0
@count += 1
end
end
class ArrayLike
def fetch(index)
end
def [](index)
@counts ||= []
@counts[index] ||= 0
@counts[index] += 1
end
def to_liquid
self
end
end
class ContextTest < Minitest::Test class ContextTest < Minitest::Test
include Liquid include Liquid
def setup
@context = Liquid::Context.new
end
def test_variables
@context['string'] = 'string'
assert_equal('string', @context['string'])
@context['num'] = 5
assert_equal(5, @context['num'])
@context['time'] = Time.parse('2006-06-06 12:00:00')
assert_equal(Time.parse('2006-06-06 12:00:00'), @context['time'])
@context['date'] = Date.today
assert_equal(Date.today, @context['date'])
now = Time.now
@context['datetime'] = now
assert_equal(now, @context['datetime'])
@context['bool'] = true
assert_equal(true, @context['bool'])
@context['bool'] = false
assert_equal(false, @context['bool'])
@context['nil'] = nil
assert_nil(@context['nil'])
assert_nil(@context['nil'])
end
def test_variables_not_existing
assert_nil(@context['does_not_exist'])
end
def test_scoping
@context.push
@context.pop
assert_raises(Liquid::ContextError) do
@context.pop
end
assert_raises(Liquid::ContextError) do
@context.push
@context.pop
@context.pop
end
end
def test_length_query
@context['numbers'] = [1, 2, 3, 4]
assert_equal(4, @context['numbers.size'])
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4 }
assert_equal(4, @context['numbers.size'])
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 }
assert_equal(1000, @context['numbers.size'])
end
def test_hyphenated_variable
@context['oh-my'] = 'godz'
assert_equal('godz', @context['oh-my'])
end
def test_add_filter
filter = Module.new do
def hi(output)
output + ' hi!'
end
end
context = Context.new
context.add_filters(filter)
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
context = Context.new
assert_equal('hi?', context.invoke(:hi, 'hi?'))
context.add_filters(filter)
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
end
def test_only_intended_filters_make_it_there
filter = Module.new do
def hi(output)
output + ' hi!'
end
end
context = Context.new
assert_equal("Wookie", context.invoke("hi", "Wookie"))
context.add_filters(filter)
assert_equal("Wookie hi!", context.invoke("hi", "Wookie"))
end
def test_add_item_in_outer_scope
@context['test'] = 'test'
@context.push
assert_equal('test', @context['test'])
@context.pop
assert_equal('test', @context['test'])
end
def test_add_item_in_inner_scope
@context.push
@context['test'] = 'test'
assert_equal('test', @context['test'])
@context.pop
assert_nil(@context['test'])
end
def test_hierachical_data
@context['hash'] = { "name" => 'tobi' }
assert_equal('tobi', @context['hash.name'])
assert_equal('tobi', @context['hash["name"]'])
end
def test_keywords
assert_equal(true, @context['true'])
assert_equal(false, @context['false'])
end
def test_digits
assert_equal(100, @context['100'])
assert_equal(100.00, @context['100.00'])
end
def test_strings
assert_equal("hello!", @context['"hello!"'])
assert_equal("hello!", @context["'hello!'"])
end
def test_merge
@context.merge("test" => "test")
assert_equal('test', @context['test'])
@context.merge("test" => "newvalue", "foo" => "bar")
assert_equal('newvalue', @context['test'])
assert_equal('bar', @context['foo'])
end
def test_array_notation
@context['test'] = [1, 2, 3, 4, 5]
assert_equal(1, @context['test[0]'])
assert_equal(2, @context['test[1]'])
assert_equal(3, @context['test[2]'])
assert_equal(4, @context['test[3]'])
assert_equal(5, @context['test[4]'])
end
def test_recoursive_array_notation
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
assert_equal(1, @context['test.test[0]'])
@context['test'] = [{ 'test' => 'worked' }]
assert_equal('worked', @context['test[0].test'])
end
def test_hash_to_array_transition
@context['colors'] = {
'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
'Green' => ['003300', '336633', '669966', '99CC99'],
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
}
assert_equal('003366', @context['colors.Blue[0]'])
assert_equal('FF9999', @context['colors.Red[3]'])
end
def test_try_first
@context['test'] = [1, 2, 3, 4, 5]
assert_equal(1, @context['test.first'])
assert_equal(5, @context['test.last'])
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
assert_equal(1, @context['test.test.first'])
assert_equal(5, @context['test.test.last'])
@context['test'] = [1]
assert_equal(1, @context['test.first'])
assert_equal(1, @context['test.last'])
end
def test_access_hashes_with_hash_notation
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
assert_equal(5, @context['products["count"]'])
assert_equal('deepsnow', @context['products["tags"][0]'])
assert_equal('deepsnow', @context['products["tags"].first'])
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
assert_equal('element151cm', @context['product["variants"][1]["title"]'])
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
assert_equal('element151cm', @context['product["variants"].last["title"]'])
end
def test_access_variable_with_hash_notation
@context['foo'] = 'baz'
@context['bar'] = 'foo'
assert_equal('baz', @context['["foo"]'])
assert_equal('baz', @context['[bar]'])
end
def test_access_hashes_with_hash_access_variables
@context['var'] = 'tags'
@context['nested'] = { 'var' => 'tags' }
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
assert_equal('deepsnow', @context['products[var].first'])
assert_equal('freestyle', @context['products[nested.var].last'])
end
def test_hash_notation_only_for_hash_access
@context['array'] = [1, 2, 3, 4, 5]
@context['hash'] = { 'first' => 'Hello' }
assert_equal(1, @context['array.first'])
assert_nil(@context['array["first"]'])
assert_equal('Hello', @context['hash["first"]'])
end
def test_first_can_appear_in_middle_of_callchain
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
assert_equal('draft151cm', @context['product.variants[0].title'])
assert_equal('element151cm', @context['product.variants[1].title'])
assert_equal('draft151cm', @context['product.variants.first.title'])
assert_equal('element151cm', @context['product.variants.last.title'])
end
def test_cents
@context.merge("cents" => HundredCentes.new)
assert_equal(100, @context['cents'])
end
def test_nested_cents
@context.merge("cents" => { 'amount' => HundredCentes.new })
assert_equal(100, @context['cents.amount'])
@context.merge("cents" => { 'cents' => { 'amount' => HundredCentes.new } })
assert_equal(100, @context['cents.cents.amount'])
end
def test_cents_through_drop
@context.merge("cents" => CentsDrop.new)
assert_equal(100, @context['cents.amount'])
end
def test_nested_cents_through_drop
@context.merge("vars" => { "cents" => CentsDrop.new })
assert_equal(100, @context['vars.cents.amount'])
end
def test_drop_methods_with_question_marks
@context.merge("cents" => CentsDrop.new)
assert(@context['cents.non_zero?'])
end
def test_context_from_within_drop
@context.merge("test" => '123', "vars" => ContextSensitiveDrop.new)
assert_equal('123', @context['vars.test'])
end
def test_nested_context_from_within_drop
@context.merge("test" => '123', "vars" => { "local" => ContextSensitiveDrop.new })
assert_equal('123', @context['vars.local.test'])
end
def test_ranges
@context.merge("test" => '5')
assert_equal((1..5), @context['(1..5)'])
assert_equal((1..5), @context['(1..test)'])
assert_equal((5..5), @context['(test..test)'])
end
def test_cents_through_drop_nestedly
@context.merge("cents" => { "cents" => CentsDrop.new })
assert_equal(100, @context['cents.cents.amount'])
@context.merge("cents" => { "cents" => { "cents" => CentsDrop.new } })
assert_equal(100, @context['cents.cents.cents.amount'])
end
def test_drop_with_variable_called_only_once
@context['counter'] = CounterDrop.new
assert_equal(1, @context['counter.count'])
assert_equal(2, @context['counter.count'])
assert_equal(3, @context['counter.count'])
end
def test_drop_with_key_called_only_once
@context['counter'] = CounterDrop.new
assert_equal(1, @context['counter["count"]'])
assert_equal(2, @context['counter["count"]'])
assert_equal(3, @context['counter["count"]'])
end
def test_proc_as_variable
@context['dynamic'] = proc { 'Hello' }
assert_equal('Hello', @context['dynamic'])
end
def test_lambda_as_variable
@context['dynamic'] = proc { 'Hello' }
assert_equal('Hello', @context['dynamic'])
end
def test_nested_lambda_as_variable
@context['dynamic'] = { "lambda" => proc { 'Hello' } }
assert_equal('Hello', @context['dynamic.lambda'])
end
def test_array_containing_lambda_as_variable
@context['dynamic'] = [1, 2, proc { 'Hello' }, 4, 5]
assert_equal('Hello', @context['dynamic[2]'])
end
def test_lambda_is_called_once
@context['callcount'] = proc {
@global ||= 0
@global += 1
@global.to_s
}
assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount'])
@global = nil
end
def test_nested_lambda_is_called_once
@context['callcount'] = { "lambda" => proc {
@global ||= 0
@global += 1
@global.to_s
} }
assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda'])
@global = nil
end
def test_lambda_in_array_is_called_once
@context['callcount'] = [1, 2, proc {
@global ||= 0
@global += 1
@global.to_s
}, 4, 5]
assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]'])
@global = nil
end
def test_access_to_context_from_proc
@context.registers[:magic] = 345392
@context['magic'] = proc { @context.registers[:magic] }
assert_equal(345392, @context['magic'])
end
def test_to_liquid_and_context_at_first_level
@context['category'] = Category.new("foobar")
assert_kind_of(CategoryDrop, @context['category'])
assert_equal(@context, @context['category'].context)
end
def test_interrupt_avoids_object_allocations
assert_no_object_allocations do
@context.interrupt?
end
end
def test_context_initialization_with_a_proc_in_environment
contx = Context.new([test: ->(c) { c['poutine'] }], test: :foo)
assert(contx)
assert_nil(contx['poutine'])
end
def test_apply_global_filter
global_filter_proc = ->(output) { "#{output} filtered" }
context = Context.new
context.global_filter = global_filter_proc
assert_equal('hi filtered', context.apply_global_filter('hi'))
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
context = Context.new
assert_equal('hi', context.apply_global_filter('hi'))
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({}, {}, StaticRegisters.new(registers))
super_context.registers[:my_register] = :my_alt_value
subcontext = super_context.new_isolated_subcontext
assert_equal(:my_value, subcontext.registers[:my_register])
end
def test_new_isolated_subcontext_inherits_static_registers
super_context = Context.build(registers: { my_register: :my_value })
subcontext = super_context.new_isolated_subcontext
assert_equal(:my_value, subcontext.registers[:my_register])
end
def test_new_isolated_subcontext_registers_do_not_pollute_context
super_context = Context.build(registers: { my_register: :my_value })
subcontext = super_context.new_isolated_subcontext
subcontext.registers[:my_register] = :my_alt_value
assert_equal(:my_value, super_context.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
def test_disables_tag_specified
context = Context.new
context.with_disabled_tags(%w(foo bar)) do
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(true, context.tag_disabled?("bar"))
assert_equal(false, context.tag_disabled?("unknown"))
end
end
def test_disables_nested_tags
context = Context.new
context.with_disabled_tags(["foo"]) do
context.with_disabled_tags(["foo"]) do
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(false, context.tag_disabled?("bar"))
end
context.with_disabled_tags(["bar"]) do
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(true, context.tag_disabled?("bar"))
context.with_disabled_tags(["foo"]) do
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(true, context.tag_disabled?("bar"))
end
end
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(false, context.tag_disabled?("bar"))
end
end
def test_override_global_filter def test_override_global_filter
global = Module.new do global = Module.new do
def notice(output) def notice(output)
@@ -606,30 +19,16 @@ class ContextTest < Minitest::Test
end end
with_global_filter(global) do with_global_filter(global) do
assert_equal('Global test', Template.parse("{{'test' | notice }}").render!) assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
assert_equal('Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local])) assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local])
end end
end end
def test_has_key_will_not_add_an_error_for_missing_keys def test_has_key_will_not_add_an_error_for_missing_keys
with_error_mode(:strict) do with_error_mode :strict do
context = Context.new context = Context.new
context.key?('unknown') context.key?('unknown')
assert_empty(context.errors) assert_empty context.errors
end end
end end
end
private
def assert_no_object_allocations
unless RUBY_ENGINE == 'ruby'
skip("stackprof needed to count object allocations")
end
require 'stackprof'
profile = StackProf.run(mode: :object) do
yield
end
assert_equal(0, profile[:samples])
end
end # ContextTest

View File

@@ -167,7 +167,7 @@ class DropsTest < Minitest::Test
def test_object_methods_not_allowed def test_object_methods_not_allowed
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method| [:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new) output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
assert_equal(' ', output) assert_equal ' ', output
end end
end end
@@ -210,28 +210,28 @@ class DropsTest < Minitest::Test
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
["select", "each", "map", "cycle"].each do |method| ["select", "each", "map", "cycle"].each do |method|
assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)) assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)) assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)) assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)) assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
end end
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)
assert_equal("3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)) assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
end end
assert_equal("yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new)) assert_equal("yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new))
[:min, :first].each do |method| [:min, :first].each do |method|
assert_equal("1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)) assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal("1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)) assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal("1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)) assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal("1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)) assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
end end
end end

View File

@@ -193,7 +193,7 @@ class ErrorHandlingTest < Minitest::Test
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError # Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
def test_exceptions_propagate def test_exceptions_propagate
assert_raises(Exception) do assert_raises Exception do
template = Liquid::Template.parse('{{ errors.exception }}') template = Liquid::Template.parse('{{ errors.exception }}')
template.render('errors' => ErrorDrop.new) template.render('errors' => ErrorDrop.new)
end end

View File

@@ -1,46 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class ExpressionTest < Minitest::Test
def test_keyword_literals
assert_equal(true, parse_and_eval("true"))
assert_equal(true, parse_and_eval(" true "))
end
def test_string
assert_equal("single quoted", parse_and_eval("'single quoted'"))
assert_equal("double quoted", parse_and_eval('"double quoted"'))
assert_equal("spaced", parse_and_eval(" 'spaced' "))
assert_equal("spaced2", parse_and_eval(' "spaced2" '))
end
def test_int
assert_equal(123, parse_and_eval("123"))
assert_equal(456, parse_and_eval(" 456 "))
assert_equal(12, parse_and_eval("012"))
end
def test_float
assert_equal(1.5, parse_and_eval("1.5"))
assert_equal(2.5, parse_and_eval(" 2.5 "))
end
def test_range
assert_equal(1..2, parse_and_eval("(1..2)"))
assert_equal(3..4, parse_and_eval(" ( 3 .. 4 ) "))
end
private
def parse_and_eval(markup, **assigns)
if Liquid::Template.error_mode == :strict
p = Liquid::Parser.new(markup)
markup = p.expression
p.consume(:end_of_string)
end
expression = Liquid::Expression.parse(markup)
context = Liquid::Context.new(assigns)
context.evaluate(expression)
end
end

View File

@@ -153,15 +153,6 @@ class FiltersTest < Minitest::Test
# 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
def test_liquid_argument_error
source = "{{ '' | size: 'too many args' }}"
exc = assert_raises(Liquid::ArgumentError) do
Template.parse(source).render!
end
assert_match(/\ALiquid error: wrong number of arguments /, exc.message)
assert_equal(exc.message, Template.parse(source).render)
end
end end
class FiltersInTemplate < Minitest::Test class FiltersInTemplate < Minitest::Test
@@ -169,9 +160,9 @@ class FiltersInTemplate < Minitest::Test
def test_local_global def test_local_global
with_global_filter(MoneyFilter) do with_global_filter(MoneyFilter) do
assert_equal(" 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)) assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter)) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter)
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter])) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter])
end end
end end

View File

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

View File

@@ -1,17 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class InlineCommentTest < Minitest::Test
include Liquid
def test_basic_usage
template_source = <<-END_TEMPLATE
foo{% # this is a comment %}bar
END_TEMPLATE
template = Template.parse(template_source)
rendered = template.render!
assert_equal("foobar", rendered.strip)
end
end

View File

@@ -32,7 +32,7 @@ class ParsingQuirksTest < Minitest::Test
assert(Template.parse("{{test}}")) assert(Template.parse("{{test}}"))
with_error_mode(:lax) do with_error_mode(:lax) do
assert(Template.parse("{{|test}}")) assert Template.parse("{{|test}}")
end end
with_error_mode(:strict) do with_error_mode(:strict) do
@@ -118,16 +118,6 @@ class ParsingQuirksTest < Minitest::Test
end end
end end
def test_blank_variable_markup
assert_template_result('', "{{}}")
end
def test_lookup_on_var_with_literal_name
assigns = { "blank" => { "x" => "result" } }
assert_template_result('result', "{{ blank.x }}", assigns)
assert_template_result('result', "{{ blank['x'] }}", assigns)
end
def test_contains_in_id def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true) assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
end end

View File

@@ -2,7 +2,7 @@
require 'test_helper' require 'test_helper'
class ProfilerTest < Minitest::Test class RenderProfilingTest < Minitest::Test
include Liquid include Liquid
class ProfilingFileSystem class ProfilingFileSystem
@@ -62,17 +62,6 @@ class ProfilerTest < Minitest::Test
assert_equal(2, included_children[1].line_number) assert_equal(2, included_children[1].line_number)
end end
def test_profiling_render_tag
t = Template.parse("{% render 'a_template' %}", profile: true)
t.render!
render_children = t.profiler[0].children
render_children.each do |timing|
assert_equal('a_template', timing.partial)
end
assert_equal([1, 2], render_children.map(&:line_number))
end
def test_profiling_times_the_rendering_of_tokens def test_profiling_times_the_rendering_of_tokens
t = Template.parse("{% include 'a_template' %}", profile: true) t = Template.parse("{% include 'a_template' %}", profile: true)
t.render! t.render!
@@ -88,37 +77,6 @@ class ProfilerTest < Minitest::Test
assert(t.profiler.total_render_time >= 0, "Total render time was not calculated") assert(t.profiler.total_render_time >= 0, "Total render time was not calculated")
end end
class SleepTag < Liquid::Tag
def initialize(tag_name, markup, parse_context)
super
@duration = Float(markup)
end
def render_to_output_buffer(_context, _output)
sleep(@duration)
end
end
def test_profiling_multiple_renders
with_custom_tag('sleep', SleepTag) do
context = Liquid::Context.new
t = Liquid::Template.parse("{% sleep 0.001 %}", profile: true)
context.template_name = 'index'
t.render!(context)
context.template_name = 'layout'
first_render_time = context.profiler.total_time
t.render!(context)
profiler = context.profiler
children = profiler.children
assert_operator(first_render_time, :>=, 0.001)
assert_operator(profiler.total_time, :>=, 0.001 + first_render_time)
assert_equal(["index", "layout"], children.map(&:template_name))
assert_equal([nil, nil], children.map(&:code))
assert_equal(profiler.total_time, children.map(&:total_time).reduce(&:+))
end
end
def test_profiling_uses_include_to_mark_children def test_profiling_uses_include_to_mark_children
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true) t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
t.render! t.render!
@@ -133,7 +91,7 @@ class ProfilerTest < Minitest::Test
include_node = t.profiler[1] include_node = t.profiler[1]
include_node.children.each do |child| include_node.children.each do |child|
assert_equal("a_template", child.partial) assert_equal "a_template", child.partial
end end
end end
@@ -143,12 +101,12 @@ class ProfilerTest < Minitest::Test
a_template = t.profiler[1] a_template = t.profiler[1]
a_template.children.each do |child| a_template.children.each do |child|
assert_equal("a_template", child.partial) assert_equal "a_template", child.partial
end end
b_template = t.profiler[2] b_template = t.profiler[2]
b_template.children.each do |child| b_template.children.each do |child|
assert_equal("b_template", child.partial) assert_equal "b_template", child.partial
end end
end end
@@ -158,12 +116,12 @@ class ProfilerTest < Minitest::Test
a_template1 = t.profiler[1] a_template1 = t.profiler[1]
a_template1.children.each do |child| a_template1.children.each do |child|
assert_equal("a_template", child.partial) assert_equal "a_template", child.partial
end end
a_template2 = t.profiler[2] a_template2 = t.profiler[2]
a_template2.children.each do |child| a_template2.children.each do |child|
assert_equal("a_template", child.partial) assert_equal "a_template", child.partial
end end
end end

View File

@@ -159,7 +159,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal('1', @filters.url_decode(1)) assert_equal('1', @filters.url_decode(1))
assert_equal('2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))) assert_equal('2001-02-03', @filters.url_decode(Date.new(2001, 2, 3)))
assert_nil(@filters.url_decode(nil)) assert_nil(@filters.url_decode(nil))
exception = assert_raises(Liquid::ArgumentError) do exception = assert_raises Liquid::ArgumentError do
@filters.url_decode('%ff') @filters.url_decode('%ff')
end end
assert_equal('Liquid error: invalid byte sequence in UTF-8', exception.message) assert_equal('Liquid error: invalid byte sequence in UTF-8', exception.message)
@@ -286,7 +286,7 @@ class StandardFiltersTest < Minitest::Test
[3], [3],
] ]
assert_raises(Liquid::ArgumentError) do assert_raises Liquid::ArgumentError do
@filters.sort(foo, "bar") @filters.sort(foo, "bar")
end end
end end
@@ -302,7 +302,7 @@ class StandardFiltersTest < Minitest::Test
[3], [3],
] ]
assert_raises(Liquid::ArgumentError) do assert_raises Liquid::ArgumentError do
@filters.sort_natural(foo, "bar") @filters.sort_natural(foo, "bar")
end end
end end
@@ -337,7 +337,7 @@ class StandardFiltersTest < Minitest::Test
[3], [3],
] ]
assert_raises(Liquid::ArgumentError) do assert_raises Liquid::ArgumentError do
@filters.uniq(foo, "bar") @filters.uniq(foo, "bar")
end end
end end
@@ -353,7 +353,7 @@ class StandardFiltersTest < Minitest::Test
[3], [3],
] ]
assert_raises(Liquid::ArgumentError) do assert_raises Liquid::ArgumentError do
@filters.compact(foo, "bar") @filters.compact(foo, "bar")
end end
end end
@@ -430,7 +430,7 @@ class StandardFiltersTest < Minitest::Test
[3], [3],
] ]
assert_raises(Liquid::ArgumentError) do assert_raises Liquid::ArgumentError do
@filters.map(foo, "bar") @filters.map(foo, "bar")
end end
end end
@@ -441,7 +441,7 @@ class StandardFiltersTest < Minitest::Test
[2], [2],
[3], [3],
] ]
assert_raises(Liquid::ArgumentError) do assert_raises Liquid::ArgumentError do
@filters.map(foo, nil) @filters.map(foo, nil)
end end
end end
@@ -485,8 +485,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal('', @filters.date('', "%B")) assert_equal('', @filters.date('', "%B"))
with_timezone("UTC") do with_timezone("UTC") do
assert_equal("07/05/2006", @filters.date(1152098955, "%m/%d/%Y")) assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
assert_equal("07/05/2006", @filters.date("1152098955", "%m/%d/%Y")) assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
end end
end end
@@ -587,7 +587,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("0.5", "{{ 2.0 | divided_by:4 }}") assert_template_result("0.5", "{{ 2.0 | divided_by:4 }}")
assert_raises(Liquid::ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
assert_template_result("4", "{{ 1 | modulo: 0 }}") assert_template_result "4", "{{ 1 | modulo: 0 }}"
end end
assert_template_result("5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10)) assert_template_result("5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10))
@@ -596,7 +596,7 @@ class StandardFiltersTest < Minitest::Test
def test_modulo def test_modulo
assert_template_result("1", "{{ 3 | modulo:2 }}") assert_template_result("1", "{{ 3 | modulo:2 }}")
assert_raises(Liquid::ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
assert_template_result("4", "{{ 1 | modulo: 0 }}") assert_template_result "4", "{{ 1 | modulo: 0 }}"
end end
assert_template_result("1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3)) assert_template_result("1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3))
@@ -607,7 +607,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("4", "{{ '4.3' | round }}") assert_template_result("4", "{{ '4.3' | round }}")
assert_template_result("4.56", "{{ input | round: 2 }}", 'input' => 4.5612) assert_template_result("4.56", "{{ input | round: 2 }}", 'input' => 4.5612)
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | round }}") assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}"
end end
assert_template_result("5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6)) assert_template_result("5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6))
@@ -618,7 +618,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("5", "{{ input | ceil }}", 'input' => 4.6) assert_template_result("5", "{{ input | ceil }}", 'input' => 4.6)
assert_template_result("5", "{{ '4.3' | ceil }}") assert_template_result("5", "{{ '4.3' | ceil }}")
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | ceil }}") assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
end end
assert_template_result("5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6)) assert_template_result("5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6))
@@ -628,7 +628,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("4", "{{ input | floor }}", 'input' => 4.6) assert_template_result("4", "{{ input | floor }}", 'input' => 4.6)
assert_template_result("4", "{{ '4.3' | floor }}") assert_template_result("4", "{{ '4.3' | floor }}")
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | floor }}") assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
end end
assert_template_result("5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)) assert_template_result("5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4))

View File

@@ -5,44 +5,36 @@ require 'test_helper'
class TagDisableableTest < Minitest::Test class TagDisableableTest < Minitest::Test
include Liquid include Liquid
module RenderTagName class DisableRaw < Block
def render(_context) disable_tags "raw"
tag_name
end
end end
class Custom < Tag class DisableRawEcho < Block
disable_tags "raw", "echo"
end
class DisableableRaw < Liquid::Raw
prepend Liquid::Tag::Disableable prepend Liquid::Tag::Disableable
include RenderTagName
end end
class Custom2 < Tag class DisableableEcho < Liquid::Echo
prepend Liquid::Tag::Disableable prepend Liquid::Tag::Disableable
include RenderTagName
end end
class DisableCustom < Block def test_disables_raw
disable_tags "custom"
end
class DisableBoth < Block
disable_tags "custom", "custom2"
end
def test_block_tag_disabling_nested_tag
with_disableable_tags do with_disableable_tags do
with_custom_tag('disable', DisableCustom) do with_custom_tag('disable', DisableRaw) do
output = Template.parse('{% disable %}{% custom %};{% custom2 %}{% enddisable %}').render output = Template.parse('{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}').render
assert_equal('Liquid error: custom usage is not allowed in this context;custom2', output) assert_equal('Liquid error: raw usage is not allowed in this contextfoo', output)
end end
end end
end end
def test_block_tag_disabling_multiple_nested_tags def test_disables_echo_and_raw
with_disableable_tags do with_disableable_tags do
with_custom_tag('disable', DisableBoth) do with_custom_tag('disable', DisableRawEcho) do
output = Template.parse('{% disable %}{% custom %};{% custom2 %}{% enddisable %}').render output = Template.parse('{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}').render
assert_equal('Liquid error: custom usage is not allowed in this context;Liquid error: custom2 usage is not allowed in this context', output) assert_equal('Liquid error: raw usage is not allowed in this contextLiquid error: echo usage is not allowed in this context', output)
end end
end end
end end
@@ -50,8 +42,8 @@ class TagDisableableTest < Minitest::Test
private private
def with_disableable_tags def with_disableable_tags
with_custom_tag('custom', Custom) do with_custom_tag('raw', DisableableRaw) do
with_custom_tag('custom2', Custom2) do with_custom_tag('echo', DisableableEcho) do
yield yield
end end
end end

View File

@@ -15,13 +15,13 @@ class TagTest < Minitest::Test
with_custom_tag('blabla', klass1) do with_custom_tag('blabla', klass1) do
template = Liquid::Template.parse("{% blabla %}") template = Liquid::Template.parse("{% blabla %}")
assert_equal('hello', template.render) assert_equal 'hello', template.render
buf = +'' buf = +''
output = template.render({}, output: buf) output = template.render({}, output: buf)
assert_equal('hello', output) assert_equal 'hello', output
assert_equal('hello', buf) assert_equal 'hello', buf
assert_equal(buf.object_id, output.object_id) assert_equal buf.object_id, output.object_id
end end
klass2 = Class.new(klass1) do klass2 = Class.new(klass1) do
@@ -33,13 +33,13 @@ class TagTest < Minitest::Test
with_custom_tag('blabla', klass2) do with_custom_tag('blabla', klass2) do
template = Liquid::Template.parse("{% blabla %}") template = Liquid::Template.parse("{% blabla %}")
assert_equal('foohellobar', template.render) assert_equal 'foohellobar', template.render
buf = +'' buf = +''
output = template.render({}, output: buf) output = template.render({}, output: buf)
assert_equal('foohellobar', output) assert_equal 'foohellobar', output
assert_equal('foohellobar', buf) assert_equal 'foohellobar', buf
assert_equal(buf.object_id, output.object_id) assert_equal buf.object_id, output.object_id
end end
end end
end end

View File

@@ -437,30 +437,4 @@ HERE
assert(context.registers[:for_stack].empty?) assert(context.registers[:for_stack].empty?)
end end
def test_instrument_for_offset_continue
assert_usage_increment('for_offset_continue') do
Template.parse('{% for item in items offset:continue %}{{item}}{% endfor %}')
end
assert_usage_increment('for_offset_continue', times: 0) do
Template.parse('{% for item in items offset:2 %}{{item}}{% endfor %}')
end
end
def test_instrument_forloop_drop_name
assigns = { 'items' => [1, 2, 3, 4, 5] }
assert_usage_increment('forloop_drop_name', times: 5) do
Template.parse('{% for item in items %}{{forloop.name}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{forloop.index}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{item}}{% endfor %}').render!(assigns)
end
end
end end

View File

@@ -184,7 +184,7 @@ class IfElseTagTest < Minitest::Test
tests.each do |vals, expected| tests.each do |vals, expected|
a, b, c = vals a, b, c = vals
assigns = { 'a' => a, 'b' => b, 'c' => c } assigns = { 'a' => a, 'b' => b, 'c' => c }
assert_template_result(expected.to_s, tpl, assigns, assigns.to_s) assert_template_result expected.to_s, tpl, assigns, assigns.to_s
end end
end end
end end

View File

@@ -226,13 +226,13 @@ class IncludeTagTest < Minitest::Test
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}') Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}')
end end
with_error_mode(:lax) do with_error_mode(:lax) do
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}')) assert_equal 'x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}')
end end
assert_raises(Liquid::SyntaxError) do assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}') Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}')
end end
with_error_mode(:lax) do with_error_mode(:lax) do
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}')) assert_equal 'x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}')
end end
end end

View File

@@ -23,7 +23,6 @@ class RawTagTest < Minitest::Test
assert_template_result(' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}') assert_template_result(' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}')
assert_template_result(' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}') assert_template_result(' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}')
assert_template_result(' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}') assert_template_result(' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}')
assert_template_result(' Foobar {% foo {% bar %}', '{% raw %} Foobar {% foo {% bar %}{% endraw %}')
end end
def test_invalid_raw def test_invalid_raw

View File

@@ -58,7 +58,7 @@ class RenderTagTest < Minitest::Test
def test_recursively_rendered_template_does_not_produce_endless_loop def test_recursively_rendered_template_does_not_produce_endless_loop
Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}') Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}')
assert_raises(Liquid::StackLevelError) do assert_raises Liquid::StackLevelError do
Template.parse('{% render "loop" %}').render! Template.parse('{% render "loop" %}').render!
end end
end end
@@ -67,7 +67,7 @@ class RenderTagTest < Minitest::Test
Liquid::Template.file_system = StubFileSystem.new( Liquid::Template.file_system = StubFileSystem.new(
'loop_render' => '{% render "loop_render" %}', 'loop_render' => '{% render "loop_render" %}',
) )
assert_raises(Liquid::StackLevelError) do assert_raises Liquid::StackLevelError do
Template.parse('{% render "loop_render" %}').render! Template.parse('{% render "loop_render" %}').render!
end end
end end
@@ -75,7 +75,7 @@ class RenderTagTest < Minitest::Test
def test_dynamically_choosen_templates_are_not_allowed def test_dynamically_choosen_templates_are_not_allowed
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered') Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered')
assert_raises(Liquid::SyntaxError) do assert_raises Liquid::SyntaxError do
Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}") Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}")
end end
end end

View File

@@ -213,11 +213,6 @@ class StandardTagTest < Minitest::Test
assert_template_result('', code, 'condition' => 'something else') assert_template_result('', code, 'condition' => 'something else')
end end
def test_case_when_comma_and_blank_body
code = '{% case condition %}{% when 1, 2 %} {% assign r = "result" %} {% endcase %}{{ r }}'
assert_template_result('result', code, 'condition' => 2)
end
def test_assign def test_assign
assert_template_result('variable', '{% assign a = "variable"%}{{a}}') assert_template_result('variable', '{% assign a = "variable"%}{{a}}')
end end

View File

@@ -135,6 +135,38 @@ class TemplateTest < Minitest::Test
refute_nil(t.resource_limits.render_score) refute_nil(t.resource_limits.render_score)
end end
def test_resource_limits_assign_score
t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
t.resource_limits.assign_score_limit = 1
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
t.resource_limits.assign_score_limit = 2
assert_equal("", t.render!)
refute_nil(t.resource_limits.assign_score)
end
def test_resource_limits_assign_score_counts_bytes_not_characters
t = Template.parse("{% assign foo = 'すごい' %}")
t.render
assert_equal(9, t.resource_limits.assign_score)
t = Template.parse("{% capture foo %}すごい{% endcapture %}")
t.render
assert_equal(9, t.resource_limits.assign_score)
end
def test_resource_limits_assign_score_nested
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
t.resource_limits.assign_score_limit = 3
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
t.resource_limits.assign_score_limit = 5
assert_equal("", t.render!)
end
def test_resource_limits_aborts_rendering_after_first_error def test_resource_limits_aborts_rendering_after_first_error
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}") t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
t.resource_limits.render_score_limit = 50 t.resource_limits.render_score_limit = 50
@@ -198,7 +230,7 @@ class TemplateTest < Minitest::Test
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
t.render!(context) t.render!(context)
end end
assert_equal('ruby error in drop', e.message) assert_equal('ruby error in drop', e.message)
@@ -257,8 +289,9 @@ class TemplateTest < Minitest::Test
end end
def test_nil_value_does_not_raise def test_nil_value_does_not_raise
t = Template.parse("some{{x}}thing", error_mode: :strict) Liquid::Template.error_mode = :strict
result = t.render!({ 'x' => nil }, strict_variables: true) t = Template.parse("some{{x}}thing")
result = t.render!({ 'x' => nil }, strict_variables: true)
assert_equal(0, t.errors.count) assert_equal(0, t.errors.count)
assert_equal('something', result) assert_equal('something', result)
@@ -267,7 +300,7 @@ class TemplateTest < Minitest::Test
def test_undefined_variables_raise def test_undefined_variables_raise
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}") t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
assert_raises(UndefinedVariable) do assert_raises UndefinedVariable do
t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true) t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true)
end end
end end
@@ -286,7 +319,7 @@ class TemplateTest < Minitest::Test
d = DropWithUndefinedMethod.new d = DropWithUndefinedMethod.new
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
@@ -309,7 +342,7 @@ class TemplateTest < Minitest::Test
def test_undefined_filters_raise def test_undefined_filters_raise
t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}") t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
assert_raises(UndefinedFilter) do assert_raises UndefinedFilter do
t.render!({ 'x' => 'foo' }, strict_filters: true) t.render!({ 'x' => 'foo' }, strict_filters: true)
end end
end end

View File

@@ -528,36 +528,4 @@ class TrimModeTest < Minitest::Test
END_EXPECTED END_EXPECTED
assert_template_result(expected, text) assert_template_result(expected, text)
end end
def test_pre_trim_blank_preceding_text
template = Liquid::Template.parse("\n{%- raw %}{% endraw %}")
assert_equal("", template.render)
template = Liquid::Template.parse("\n{%- if true %}{% endif %}")
assert_equal("", template.render)
template = Liquid::Template.parse("{{ 'B' }} \n{%- if true %}C{% endif %}")
assert_equal("BC", template.render)
end
def test_bug_compatible_pre_trim
template = Liquid::Template.parse("\n {%- raw %}{% endraw %}", bug_compatible_whitespace_trimming: true)
assert_equal("\n", template.render)
template = Liquid::Template.parse("\n {%- if true %}{% endif %}", bug_compatible_whitespace_trimming: true)
assert_equal("\n", template.render)
template = Liquid::Template.parse("{{ 'B' }} \n{%- if true %}C{% endif %}", bug_compatible_whitespace_trimming: true)
assert_equal("B C", template.render)
template = Liquid::Template.parse("B\n {%- raw %}{% endraw %}", bug_compatible_whitespace_trimming: true)
assert_equal("B", template.render)
template = Liquid::Template.parse("B\n {%- if true %}{% endif %}", bug_compatible_whitespace_trimming: true)
assert_equal("B", template.render)
end
def test_trim_blank
assert_template_result('foobar', 'foo {{-}} bar')
end
end # TrimModeTest end # TrimModeTest

View File

@@ -21,11 +21,6 @@ class VariableTest < Minitest::Test
assert_equal(' worked wonderfully ', template.render!('test' => 'worked wonderfully')) assert_equal(' worked wonderfully ', template.render!('test' => 'worked wonderfully'))
end end
def test_expression_with_whitespace_in_square_brackets
assert_template_result('result', "{{ a[ 'b' ] }}", 'a' => { 'b' => 'result' })
assert_template_result('result', "{{ a[ [ 'b' ] ] }}", 'b' => 'c', 'a' => { 'c' => 'result' })
end
def test_ignore_unknown def test_ignore_unknown
template = Template.parse(%({{ test }})) template = Template.parse(%({{ test }}))
assert_equal('', template.render!) assert_equal('', template.render!)
@@ -42,8 +37,8 @@ class VariableTest < Minitest::Test
end end
def test_hash_scoping def test_hash_scoping
assert_template_result('worked', "{{ test.test }}", 'test' => { 'test' => 'worked' }) template = Template.parse(%({{ test.test }}))
assert_template_result('worked', "{{ test . test }}", 'test' => { 'test' => 'worked' }) assert_equal('worked', template.render!('test' => { 'test' => 'worked' }))
end end
def test_false_renders_as_false def test_false_renders_as_false
@@ -100,8 +95,4 @@ class VariableTest < Minitest::Test
def test_render_symbol def test_render_symbol
assert_template_result('bar', '{{ foo }}', 'foo' => :bar) assert_template_result('bar', '{{ foo }}', 'foo' => :bar)
end end
def test_dynamic_find_var
assert_template_result('bar', '{{ [key] }}', 'key' => 'foo', 'foo' => 'bar')
end
end end

View File

@@ -28,7 +28,7 @@ class I18nUnitTest < Minitest::Test
# end # end
def test_raises_unknown_translation def test_raises_unknown_translation
assert_raises(I18n::TranslationError) do assert_raises I18n::TranslationError do
@i18n.translate("doesnt_exist") @i18n.translate("doesnt_exist")
end end
end end

View File

@@ -32,26 +32,21 @@ class TokenizerTest < Minitest::Test
private private
def new_tokenizer(source, parse_context: Liquid::ParseContext.new, start_line_number: nil)
parse_context.new_tokenizer(source, start_line_number: start_line_number)
end
def tokenize(source) def tokenize(source)
tokenizer = new_tokenizer(source) tokenizer = Liquid::Tokenizer.new(source)
tokens = [] tokens = []
# shift is private in Liquid::C::Tokenizer, since it is only for unit testing while (t = tokenizer.shift)
while (t = tokenizer.send(:shift))
tokens << t tokens << t
end end
tokens tokens
end end
def tokenize_line_numbers(source) def tokenize_line_numbers(source)
tokenizer = new_tokenizer(source, start_line_number: 1) tokenizer = Liquid::Tokenizer.new(source, true)
line_numbers = [] line_numbers = []
loop do loop do
line_number = tokenizer.line_number line_number = tokenizer.line_number
if tokenizer.send(:shift) if tokenizer.shift
line_numbers << line_number line_numbers << line_number
else else
break break

View File

@@ -108,7 +108,7 @@ class VariableUnitTest < Minitest::Test
assert_equal(VariableLookup.new('foo-bar'), create_variable('foo-bar').name) assert_equal(VariableLookup.new('foo-bar'), create_variable('foo-bar').name)
assert_equal(VariableLookup.new('foo-bar-2'), create_variable('foo-bar-2').name) assert_equal(VariableLookup.new('foo-bar-2'), create_variable('foo-bar-2').name)
with_error_mode(:strict) do with_error_mode :strict do
assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') } assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') }
assert_raises(Liquid::SyntaxError) { create_variable('-foo') } assert_raises(Liquid::SyntaxError) { create_variable('-foo') }
assert_raises(Liquid::SyntaxError) { create_variable('2foo') } assert_raises(Liquid::SyntaxError) { create_variable('2foo') }