Compare commits

..

1 Commits

Author SHA1 Message Date
Dylan Thacker-Smith
730c99ae3e refactor: Reduce maximum block nesting in Liquid::BlockBody#parse 2017-10-17 18:14:45 -04:00
64 changed files with 271 additions and 1716 deletions

2
.github/probots.yml vendored
View File

@@ -1,2 +0,0 @@
enabled:
- cla

1
.gitignore vendored
View File

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

View File

@@ -1,6 +1,4 @@
inherit_from:
- .rubocop_todo.yml
- ./.rubocop_todo.yml
inherit_from: ./.rubocop_todo.yml
AllCops:
Exclude:
@@ -31,8 +29,8 @@ Lint/ParenthesesAsGroupedExpression:
Lint/UnusedBlockArgument:
Enabled: false
Layout/EndAlignment:
EnforcedStyleAlignWith: variable
Lint/EndAlignment:
AlignWith: variable
Lint/UnusedMethodArgument:
Enabled: false
@@ -61,10 +59,10 @@ Style/BracesAroundHashParameters:
Style/NumericLiterals:
Enabled: false
Layout/SpaceInsideArrayLiteralBrackets:
Style/SpaceInsideBrackets:
Enabled: false
Layout/SpaceBeforeBlockBraces:
Style/SpaceBeforeBlockBraces:
Enabled: false
Style/Documentation:
@@ -73,22 +71,19 @@ Style/Documentation:
Style/ClassAndModuleChildren:
Enabled: false
Style/TrailingCommaInArrayLiteral:
Style/TrailingComma:
Enabled: false
Style/TrailingCommaInHashLiteral:
Enabled: false
Layout/IndentHash:
Style/IndentHash:
EnforcedStyle: consistent
Style/FormatString:
Enabled: false
Layout/AlignParameters:
Style/AlignParameters:
EnforcedStyle: with_fixed_indentation
Layout/MultilineOperationIndentation:
Style/MultilineOperationIndentation:
EnforcedStyle: indented
Style/IfUnlessModifier:
@@ -97,7 +92,7 @@ Style/IfUnlessModifier:
Style/RaiseArgs:
Enabled: false
Style/PreferredHashMethods:
Style/DeprecatedHashMethods:
Enabled: false
Style/RegexpLiteral:
@@ -109,10 +104,10 @@ Style/SymbolLiteral:
Performance/Count:
Enabled: false
Naming/ConstantName:
Style/ConstantName:
Enabled: false
Layout/CaseIndentation:
Style/CaseIndentation:
Enabled: false
Style/ClassVars:
@@ -126,7 +121,3 @@ Style/TrivialAccessors:
Style/WordArray:
Enabled: false
Naming/MethodName:
Exclude:
- 'example/server/liquid_servlet.rb'

View File

@@ -1,260 +1,72 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2019-04-22 19:11:24 -0400 using RuboCop version 0.53.0.
# This configuration was generated by `rubocop --auto-gen-config`
# on 2015-06-08 18:16:16 +0000 using RuboCop version 0.32.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: Include, TreatCommentsAsGroupSeparators.
# Include: **/*.gemspec
Gemspec/OrderedDependencies:
Exclude:
- 'liquid.gemspec'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent
Layout/IndentHeredoc:
Exclude:
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/trim_mode_test.rb'
# Offense count: 6
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: symmetrical, new_line, same_line
Layout/MultilineMethodCallBraceLayout:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/unit/strainer_unit_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: runtime_error, standard_error
Lint/InheritException:
Exclude:
- 'lib/liquid/interrupts.rb'
# Offense count: 1
# Configuration parameters: CheckForMethodsWithNoSideEffects.
Lint/Void:
Exclude:
- 'lib/liquid/parse_context.rb'
Lint/NestedMethodDefinition:
Enabled: false
# Offense count: 53
Metrics/AbcSize:
Max: 56
Max: 58
# Offense count: 12
Metrics/CyclomaticComplexity:
Max: 13
Max: 15
# Offense count: 112
# Offense count: 553
# Configuration parameters: AllowURI, URISchemes.
Metrics/LineLength:
Max: 294
# Offense count: 77
# Configuration parameters: CountComments.
Metrics/MethodLength:
Max: 38
Max: 46
# Offense count: 8
# Offense count: 6
Metrics/PerceivedComplexity:
Max: 11
Max: 13
# Offense count: 52
# Configuration parameters: Blacklist.
# Blacklist: END, (?-mix:EO[A-Z]{1})
Naming/HeredocDelimiterNaming:
Exclude:
- 'test/integration/assign_test.rb'
- 'test/integration/capture_test.rb'
- 'test/integration/trim_mode_test.rb'
# Offense count: 23
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
# AllowedNames: io, id
Naming/UncommunicativeMethodParamName:
Exclude:
- 'example/server/example_servlet.rb'
- 'lib/liquid/condition.rb'
- 'lib/liquid/context.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/utils.rb'
- 'lib/liquid/variable.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/condition_unit_test.rb'
# Offense count: 12
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: prefer_alias, prefer_alias_method
Style/Alias:
Exclude:
- 'lib/liquid/drop.rb'
- 'lib/liquid/i18n.rb'
- 'lib/liquid/profiler/hooks.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/variable.rb'
# Offense count: 22
Style/CommentedKeyword:
# Offense count: 1
Style/AccessorMethodName:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Exclude:
- 'lib/liquid/errors.rb'
# Offense count: 1
Style/DateTime:
Exclude:
- 'test/unit/context_unit_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/EmptyCaseCondition:
Exclude:
- 'lib/liquid/block_body.rb'
- 'lib/liquid/lexer.rb'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: compact, expanded
Style/EmptyMethod:
Exclude:
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/comment.rb'
- 'lib/liquid/tags/include.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/unit/context_unit_test.rb'
Style/ClosingParenthesisIndentation:
Enabled: false
# Offense count: 3
# Cop supports --auto-correct.
Style/Encoding:
Exclude:
- 'lib/liquid/version.rb'
- 'liquid.gemspec'
- 'test/integration/standard_filter_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/ExpandPathArguments:
Exclude:
- 'Rakefile'
- 'liquid.gemspec'
# Offense count: 7
# Configuration parameters: EnforcedStyle.
# SupportedStyles: annotated, template, unannotated
Style/FormatStringToken:
Exclude:
- 'test/integration/filter_test.rb'
- 'test/integration/hash_ordering_test.rb'
# Offense count: 14
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/tags/assign.rb'
- 'lib/liquid/tags/capture.rb'
- 'lib/liquid/tags/case.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb'
- 'test/unit/tokenizer_unit_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength.
# SupportedStyles: skip_modifier_ifs, always
Style/Next:
Exclude:
- 'lib/liquid/tags/for.rb'
Enabled: false
# Offense count: 4
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Exclude:
- 'spec/**/*'
- 'lib/liquid/context.rb'
- 'lib/liquid/forloop_drop.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tablerowloop_drop.rb'
# Offense count: 14
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Exclude:
- 'lib/liquid/tags/if.rb'
- 'liquid.gemspec'
- 'test/integration/assign_test.rb'
- 'test/integration/standard_filter_test.rb'
# Configuration parameters: EnforcedStyle, SupportedStyles.
Style/MethodName:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
Style/RedundantSelf:
Exclude:
- 'lib/liquid/strainer.rb'
Style/MultilineBlockChain:
Enabled: false
# Offense count: 9
# Offense count: 2
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
Style/Next:
Enabled: false
# Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: AllowAsExpressionSeparator.
Style/Semicolon:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: MinSize.
# SupportedStyles: percent, brackets
Style/SymbolArray:
EnforcedStyle: brackets
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
Style/TernaryParentheses:
Exclude:
- 'lib/liquid/context.rb'
- 'lib/liquid/utils.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/UnneededPercentQ:
Exclude:
- 'test/integration/error_handling_test.rb'
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/WhileUntilModifier:
Exclude:
- 'lib/liquid/tags/case.rb'
# Offense count: 648
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 294
Enabled: false

View File

@@ -3,32 +3,27 @@ language: ruby
rvm:
- 2.1
- 2.2
- 2.3
- 2.4
- 2.5
- &latest_ruby 2.6
- 2.3.3
- ruby-head
- jruby-head
# - rbx-2
sudo: false
addons:
apt:
packages:
- libgmp3-dev
matrix:
include:
- rvm: *latest_ruby
script: bundle exec rake memory_profile:run
name: Profiling Memory Usage
allow_failures:
- rvm: ruby-head
- rvm: jruby-head
install:
- gem install rainbow -v 2.2.1
- bundle install
script: bundle exec rake
script: "bundle exec rake"
notifications:
disable: true

15
Gemfile
View File

@@ -1,24 +1,17 @@
source 'https://rubygems.org'
git_source(:github) do |repo_name|
"https://github.com/#{repo_name}.git"
end
gemspec
gem 'stackprof', platforms: :mri
group :benchmark, :test do
gem 'benchmark-ips'
gem 'memory_profiler'
gem 'terminal-table'
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ } do
gem 'stackprof'
end
end
group :test do
gem 'rubocop', '~> 0.53.0'
gem 'rubocop', '0.34.2'
platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'liquid-tag'
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'bd53db95de3d44d631e7c5a267c3d934e66107dd'
end
end

View File

@@ -1,58 +1,5 @@
# Liquid Change Log
## 4.0.3 / 2019-03-12
### Fixed
* Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
## 4.0.2 / 2019-03-08
### Changed
* Add `where` filter (#1026) [Samuel Doiron]
* Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber]
* Improve `strip_html` performance (#1032) [printercu]
### Fixed
* Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang]
* Validate the character encoding in url_decode (#1070) [Clayton Smith]
## 4.0.1 / 2018-10-09
### Changed
* Add benchmark group in Gemfile (#855) [Jerry Liu]
* Allow benchmarks to benchmark render by itself (#851) [Jerry Liu]
* Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith]
* Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith]
* Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield]
* Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith]
* Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith]
* Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith]
* Remove Spy Gem (#896) [Dylan Thacker-Smith]
* Add `collection_name` and `variable_name` reader to `For` block (#909)
* Symbols render as strings (#920) [Justin Li]
* Remove default value from Hash objects (#932) [Maxime Bedard]
* Remove one level of nesting (#944) [Dylan Thacker-Smith]
* Update Rubocop version (#952) [Justin Li]
* Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal]
* Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith]
* Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith]
* Add tests against Ruby 2.4 (#963) and 2.5 (#981)
* Replace RegExp literals with constants (#988) [Ashwin Maroli]
* Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli]
* Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith]
* Refactor and optimize rendering (#1005) [Christopher Aue]
* Add installation instruction (#1006) [Ben Gift]
* Remove Circle CI (#1010)
* Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO]
* Rename deprecated Rubocop name (#1027) [Justin Li]
### Fixed
* Handle `join` filter on non String joiners (#857) [Richard Monette]
* Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861)
* Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal]
* Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz]
* Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan]
## 4.0.0 / 2016-12-14 / branch "4-0-stable"
### Changed

View File

@@ -42,8 +42,6 @@ Liquid is a template engine which was written with very specific requirements:
## How to use Liquid
Install Liquid by adding `gem 'liquid'` to your gemfile.
Liquid supports a very simple API based around the Liquid::Template class.
For standard use you can just pass it the content of a file and call render with a parameters hash.

View File

@@ -3,7 +3,7 @@ require 'rake/testtask'
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
require "liquid/version"
task default: [:test, :rubocop]
task default: [:rubocop, :test]
desc 'run test suite with default parser'
Rake::TestTask.new(:base_test) do |t|
@@ -85,13 +85,6 @@ namespace :profile do
end
end
namespace :memory_profile do
desc "Run memory profiler"
task :run do
ruby "./performance/memory_profile.rb"
end
end
desc "Run example"
task :example do
ruby "-w -d -Ilib example/server/server.rb"

3
circle.yml Normal file
View File

@@ -0,0 +1,3 @@
machine:
ruby:
version: ruby-2.1

View File

@@ -45,7 +45,6 @@ module Liquid
end
require "liquid/version"
require 'liquid/parse_tree_visitor'
require 'liquid/lexer'
require 'liquid/parser'
require 'liquid/i18n'

View File

@@ -13,7 +13,6 @@ module Liquid
end
end
# For backwards compatibility
def render(context)
@body.render(context)
end
@@ -27,12 +26,12 @@ module Liquid
end
def unknown_tag(tag, _params, _tokens)
if tag == 'else'.freeze
case tag
when 'else'.freeze
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
block_name: block_name))
elsif tag.start_with?('end'.freeze)
when 'end'.freeze
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
tag: tag,
block_name: block_name,
block_delimiter: block_delimiter))
else

View File

@@ -1,9 +1,7 @@
module Liquid
class BlockBody
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%".freeze
VARSTART = "{{".freeze
@@ -14,42 +12,8 @@ module Liquid
@blank = true
end
def parse(tokenizer, parse_context, &block)
def parse(tokenizer, parse_context)
parse_context.line_number = tokenizer.line_number
if tokenizer.for_liquid_tag
parse_for_liquid_tag(tokenizer, parse_context, &block)
else
parse_for_document(tokenizer, parse_context, &block)
end
end
private def parse_for_liquid_tag(tokenizer, parse_context)
while token = tokenizer.shift
unless token.empty? || token =~ WhitespaceOrNothing
unless token =~ LiquidTagToken
# line isn't empty but didn't match tag syntax, yield and let the
# caller raise a syntax error
return yield token, token
end
tag_name = $1
markup = $2
unless tag = registered_tags[tag_name]
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
end
parse_context.line_number = tokenizer.line_number
end
yield nil, nil
end
private def parse_for_document(tokenizer, parse_context, &block)
while token = tokenizer.shift
next if token.empty?
case
@@ -58,20 +22,9 @@ module Liquid
unless token =~ FullToken
raise_missing_tag_terminator(token, parse_context)
end
tag_name = $2
markup = $4
if parse_context.line_number
# newlines inside the tag should increase the line number,
# particularly important for multiline {% liquid %} tags
parse_context.line_number += $1.count("\n".freeze) + $3.count("\n".freeze)
end
if tag_name == 'liquid'.freeze
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
end
tag_name = $1
markup = $2
# fetch the tag from registered blocks
unless tag = registered_tags[tag_name]
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
@@ -90,7 +43,7 @@ module Liquid
end
parse_context.trim_whitespace = false
@nodelist << token
@blank &&= !!(token =~ WhitespaceOrNothing)
@blank &&= !!(token =~ /\A\s*\z/)
end
parse_context.line_number = tokenizer.line_number
end
@@ -113,57 +66,52 @@ module Liquid
end
def render(context)
render_to_output_buffer(context, '')
end
def render_to_output_buffer(context, output)
output = []
context.resource_limits.render_score += @nodelist.length
idx = 0
while node = @nodelist[idx]
previous_output_size = output.bytesize
@nodelist.each do |token|
# Break out if we have any unhanded interrupts.
break if context.interrupt?
case node
when String
output << node
when Variable
render_node(context, output, node)
when Block
render_node(context, node.blank? ? '' : output, node)
break if context.interrupt? # might have happened in a for-block
when Continue, Break
begin
# If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}
context.push_interrupt(node.interrupt)
break
else # Other non-Block tags
render_node(context, output, node)
break if context.interrupt? # might have happened through an include
end
idx += 1
if token.is_a?(Continue) || token.is_a?(Break)
context.push_interrupt(token.interrupt)
break
end
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
node_output = render_node(token, context)
unless token.is_a?(Block) && token.blank?
output << node_output
end
rescue MemoryError => e
raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, token.line_number)
output << nil
rescue ::StandardError => e
line_number = token.is_a?(String) ? nil : token.line_number
output << context.handle_error(e, line_number)
end
end
output
output.join
end
private
def render_node(context, output, node)
node.render_to_output_buffer(context, output)
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
def render_node(node, context)
node_output = node.is_a?(String) ? node : node.render(context)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
def raise_if_resource_limits_reached(context, length)
context.resource_limits.render_length += length
return unless context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
context.resource_limits.render_length += node_output.length
if context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
end
node_output
end
def create_variable(token, parse_context)

View File

@@ -29,7 +29,7 @@ module Liquid
@@operators
end
attr_reader :attachment, :child_condition
attr_reader :attachment
attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil)
@@ -83,7 +83,7 @@ module Liquid
protected
attr_reader :child_relation
attr_reader :child_relation, :child_condition
private
@@ -128,15 +128,6 @@ module Liquid
end
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.left, @node.right,
@node.child_condition, @node.attachment
].compact
end
end
end
class ElseCondition < Condition

View File

@@ -19,26 +19,22 @@ module Liquid
'false'.freeze => false,
'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
}.freeze
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
INTEGERS_REGEX = /\A(-?\d+)\z/
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
}
def self.parse(markup)
if LITERALS.key?(markup)
LITERALS[markup]
else
case markup
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
when /\A'(.*)'\z/m # Single quoted strings
$1
when INTEGERS_REGEX
when /\A"(.*)"\z/m # Double quoted strings
$1
when /\A(-?\d+)\z/ # Integer and floats
$1.to_i
when RANGES_REGEX
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
RangeLookup.parse($1, $2)
when FLOATS_REGEX
when /\A(-?\d[\d\.]+)\z/ # Floats
$1.to_f
else
VariableLookup.parse(markup)

View File

@@ -26,7 +26,7 @@ module Liquid
def interpolate(name, vars)
name.gsub(/%\{(\w+)\}/) do
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
(vars[$1.to_sym]).to_s
"#{vars[$1.to_sym]}"
end
end

View File

@@ -12,14 +12,13 @@ module Liquid
')'.freeze => :close_round,
'?'.freeze => :question,
'-'.freeze => :dash
}.freeze
}
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
WHITESPACE_OR_NOTHING = /\s*/
def initialize(input)
@ss = StringScanner.new(input)
@@ -29,7 +28,7 @@ module Liquid
@output = []
until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING)
@ss.skip(/\s*/)
break if @ss.eos?
tok = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]

View File

@@ -14,7 +14,7 @@
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
unknown_tag: "Unknown tag '%{tag}'"
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
unexpected_else: "%{block_name} tag does not expect 'else' tag"
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"

View File

@@ -1,42 +0,0 @@
# frozen_string_literal: true
module Liquid
class ParseTreeVisitor
def self.for(node, callbacks = Hash.new(proc {}))
if defined?(node.class::ParseTreeVisitor)
node.class::ParseTreeVisitor
else
self
end.new(node, callbacks)
end
def initialize(node, callbacks)
@node = node
@callbacks = callbacks
end
def add_callback_for(*classes, &block)
callback = block
callback = ->(node, _) { yield node } if block.arity.abs == 1
callback = ->(_, _) { yield } if block.arity.zero?
classes.each { |klass| @callbacks[klass] = callback }
self
end
def visit(context = nil)
children.map do |node|
item, new_context = @callbacks[node.class].call(node, context)
[
item,
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context)
]
end
end
protected
def children
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
end
end
end

View File

@@ -44,14 +44,11 @@ module Liquid
tok[0] == type
end
SINGLE_TOKEN_EXPRESSION_TYPES = [:string, :number].freeze
private_constant :SINGLE_TOKEN_EXPRESSION_TYPES
def expression
token = @tokens[@p]
if token[0] == :id
variable_signature
elsif SINGLE_TOKEN_EXPRESSION_TYPES.include? token[0]
elsif [:string, :number].include? token[0]
consume
elsif token.first == :open_round
consume

View File

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

View File

@@ -9,14 +9,8 @@ module Liquid
'<'.freeze => '&lt;'.freeze,
'"'.freeze => '&quot;'.freeze,
"'".freeze => '&#39;'.freeze
}.freeze
}
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
STRIP_HTML_BLOCKS = Regexp.union(
/<script.*?<\/script>/m,
/<!--.*?-->/m,
/<style.*?<\/style>/m
)
STRIP_HTML_TAGS = /<.*?>/m
# Return the size of an array or of an string
def size(input)
@@ -52,12 +46,7 @@ module Liquid
end
def url_decode(input)
return if input.nil?
result = CGI.unescape(input.to_s)
raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
result
CGI.unescape(input.to_s) unless input.nil?
end
def slice(input, offset, length = nil)
@@ -79,7 +68,7 @@ module Liquid
truncate_string_str = truncate_string.to_s
l = length - truncate_string_str.length
l = 0 if l < 0
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
end
def truncatewords(input, words = 15, truncate_string = "...".freeze)
@@ -88,7 +77,7 @@ module Liquid
words = Utils.to_integer(words)
l = words - 1
l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ".freeze).concat(truncate_string.to_s) : input
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input
end
# Split input string into an array of substrings separated by given pattern.
@@ -114,9 +103,7 @@ module Liquid
def strip_html(input)
empty = ''.freeze
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
result.gsub!(STRIP_HTML_TAGS, empty)
result
input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
end
# Remove all newlines from the string
@@ -133,18 +120,19 @@ module Liquid
# provide optional property with which to sort an array of hashes or drops
def sort(input, property = nil)
ary = InputIterator.new(input)
return [] if ary.empty?
if property.nil?
ary.sort
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort do |a, b|
nil_safe_compare(a, b)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
a = a[property]
b = b[property]
if a && b
a <=> b
else
a ? -1 : 1
end
end
end
end
@@ -154,40 +142,12 @@ module Liquid
def sort_natural(input, property = nil)
ary = InputIterator.new(input)
return [] if ary.empty?
if property.nil?
ary.sort do |a, b|
nil_safe_casecmp(a, b)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
end
end
# Filter the elements of an array to those with a certain property value.
# By default the target is any truthy value.
def where(input, property, target_value = nil)
ary = InputIterator.new(input)
if ary.empty?
ary.sort { |a, b| a.casecmp(b) }
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) && target_value.nil?
begin
ary.select { |item| item[property] }
rescue TypeError
raise_property_error(property)
end
elsif ary.first.respond_to?(:[])
begin
ary.select { |item| item[property] == target_value }
rescue TypeError
raise_property_error(property)
end
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort { |a, b| a[property].casecmp(b[property]) }
end
end
@@ -201,11 +161,7 @@ module Liquid
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
begin
ary.uniq { |a| a[property] }
rescue TypeError
raise_property_error(property)
end
ary.uniq{ |a| a[property] }
end
end
@@ -227,8 +183,6 @@ module Liquid
r.is_a?(Proc) ? r.call : r
end
end
rescue TypeError
raise_property_error(property)
end
# Remove nils within an array
@@ -241,22 +195,20 @@ module Liquid
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
begin
ary.reject { |a| a[property].nil? }
rescue TypeError
raise_property_error(property)
end
ary.reject{ |a| a[property].nil? }
end
end
# Replace occurrences of a string with another
def replace(input, string, replacement = ''.freeze)
input.to_s.gsub(string.to_s, replacement.to_s)
replacement = replacement.to_s
input.to_s.gsub(string.to_s) { replacement }
end
# Replace the first occurrences of a string with another
def replace_first(input, string, replacement = ''.freeze)
input.to_s.sub(string.to_s, replacement.to_s)
replacement = replacement.to_s
input.to_s.sub(string.to_s) { replacement }
end
# remove a substring
@@ -403,22 +355,6 @@ module Liquid
raise Liquid::FloatDomainError, e.message
end
def at_least(input, n)
min_value = Utils.to_number(n)
result = Utils.to_number(input)
result = min_value if min_value > result
result.is_a?(BigDecimal) ? result.to_f : result
end
def at_most(input, n)
max_value = Utils.to_number(n)
result = Utils.to_number(input)
result = max_value if max_value < result
result.is_a?(BigDecimal) ? result.to_f : result
end
def default(input, default_value = ''.freeze)
if !input || input.respond_to?(:empty?) && input.empty?
default_value
@@ -429,31 +365,11 @@ module Liquid
private
def raise_property_error(property)
raise Liquid::ArgumentError.new("cannot select the property '#{property}'")
end
def apply_operation(input, operand, operation)
result = Utils.to_number(input).send(operation, Utils.to_number(operand))
result.is_a?(BigDecimal) ? result.to_f : result
end
def nil_safe_compare(a, b)
if !a.nil? && !b.nil?
a <=> b
else
a.nil? ? 1 : -1
end
end
def nil_safe_casecmp(a, b)
if !a.nil? && !b.nil?
a.to_s.casecmp(b.to_s)
else
a.nil? ? 1 : -1
end
end
class InputIterator
include Enumerable

View File

@@ -5,8 +5,8 @@ module Liquid
include ParserSwitching
class << self
def parse(tag_name, markup, tokenizer, parse_context)
tag = new(tag_name, markup, parse_context)
def parse(tag_name, markup, tokenizer, options)
tag = new(tag_name, markup, options)
tag.parse(tokenizer)
tag
end
@@ -36,14 +36,6 @@ module Liquid
''.freeze
end
# For backwards compatibility with custom tags. In a future release, the semantics
# of the `render_to_output_buffer` method will become the default and the `render`
# method will be removed.
def render_to_output_buffer(context, output)
output << render(context)
output
end
def blank?
false
end

View File

@@ -10,27 +10,21 @@ module Liquid
class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
def self.syntax_error_translation_key
"errors.syntax.assign".freeze
end
attr_reader :to, :from
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@to = $1
@from = Variable.new($2, options)
else
raise SyntaxError.new(options[:locale].t(self.class.syntax_error_translation_key))
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
end
end
def render_to_output_buffer(context, output)
def render(context)
val = @from.render(context)
context.scopes.last[@to] = val
context.resource_limits.assign_score += assign_score_of(val)
output
''.freeze
end
def blank?
@@ -41,7 +35,7 @@ module Liquid
def assign_score_of(val)
if val.instance_of?(String)
val.bytesize
val.length
elsif val.instance_of?(Array) || val.instance_of?(Hash)
sum = 1
# Uses #each to avoid extra allocations.
@@ -51,12 +45,6 @@ module Liquid
1
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.from]
end
end
end
Template.register_tag('assign'.freeze, Assign)

View File

@@ -22,12 +22,11 @@ module Liquid
end
end
def render_to_output_buffer(context, output)
previous_output_size = output.bytesize
super
def render(context)
output = super
context.scopes.last[@to] = output
context.resource_limits.assign_score += (output.bytesize - previous_output_size)
output
context.resource_limits.assign_score += output.length
''.freeze
end
def blank?

View File

@@ -3,8 +3,6 @@ module Liquid
Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
attr_reader :blocks, :left
def initialize(tag_name, markup, options)
super
@blocks = []
@@ -38,21 +36,21 @@ module Liquid
end
end
def render_to_output_buffer(context, output)
def render(context)
context.stack do
execute_else_block = true
output = ''
@blocks.each do |block|
if block.else?
block.attachment.render_to_output_buffer(context, output) if execute_else_block
return block.attachment.render(context) if execute_else_block
elsif block.evaluate(context)
execute_else_block = false
block.attachment.render_to_output_buffer(context, output)
output << block.attachment.render(context)
end
end
output
end
output
end
private
@@ -82,12 +80,6 @@ module Liquid
block.attach(BlockBody.new)
@blocks << block
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.left] + @node.blocks
end
end
end
Template.register_tag('case'.freeze, Case)

View File

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

View File

@@ -15,8 +15,6 @@ module Liquid
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
attr_reader :variables
def initialize(tag_name, markup, options)
super
case markup
@@ -31,29 +29,18 @@ module Liquid
end
end
def render_to_output_buffer(context, output)
def render(context)
context.registers[:cycle] ||= {}
context.stack do
key = context.evaluate(@name)
iteration = context.registers[:cycle][key].to_i
val = context.evaluate(@variables[iteration])
if val.is_a?(Array)
val = val.join
elsif !val.is_a?(String)
val = val.to_s
end
output << val
result = context.evaluate(@variables[iteration])
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
result
end
output
end
private
@@ -64,12 +51,6 @@ module Liquid
$1 ? Expression.parse($1) : nil
end.compact
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
Array(@node.variables)
end
end
end
Template.register_tag('cycle', Cycle)

View File

@@ -23,12 +23,11 @@ module Liquid
@variable = markup.strip
end
def render_to_output_buffer(context, output)
def render(context)
value = context.environments.first[@variable] ||= 0
value -= 1
context.environments.first[@variable] = value
output << value.to_s
output
value.to_s
end
end

View File

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

View File

@@ -46,7 +46,8 @@ module Liquid
class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
attr_reader :collection_name, :variable_name, :limit, :from
attr_reader :collection_name
attr_reader :variable_name
def initialize(tag_name, markup, options)
super
@@ -70,16 +71,14 @@ module Liquid
@else_block = BlockBody.new
end
def render_to_output_buffer(context, output)
def render(context)
segment = collection_segment(context)
if segment.empty?
render_else(context, output)
render_else(context)
else
render_segment(context, output, segment)
render_segment(context, segment)
end
output
end
protected
@@ -126,23 +125,14 @@ module Liquid
from = if @from == :continue
offsets[@name].to_i
else
from_value = context.evaluate(@from)
if from_value.nil?
0
else
Utils.to_integer(from_value)
end
context.evaluate(@from).to_i
end
collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range)
limit_value = context.evaluate(@limit)
to = if limit_value.nil?
nil
else
Utils.to_integer(limit_value) + from
end
limit = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed
@@ -152,10 +142,12 @@ module Liquid
segment
end
def render_segment(context, output, segment)
def render_segment(context, segment)
for_stack = context.registers[:for_stack] ||= []
length = segment.length
result = ''
context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
@@ -164,9 +156,9 @@ module Liquid
begin
context['forloop'.freeze] = loop_vars
segment.each do |item|
segment.each_with_index do |item, index|
context[@variable_name] = item
@for_block.render_to_output_buffer(context, output)
result << @for_block.render(context)
loop_vars.send(:increment!)
# Handle any interrupts if they exist.
@@ -181,7 +173,7 @@ module Liquid
end
end
output
result
end
def set_attribute(key, expr)
@@ -197,18 +189,8 @@ module Liquid
end
end
def render_else(context, output)
if @else_block
@else_block.render_to_output_buffer(context, output)
else
output
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
(super + [@node.limit, @node.from, @node.collection_name]).compact
end
def render_else(context)
@else_block ? @else_block.render(context) : ''.freeze
end
end

View File

@@ -12,9 +12,7 @@ module Liquid
class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
BOOLEAN_OPERATORS = %w(and or).freeze
attr_reader :blocks
BOOLEAN_OPERATORS = %w(and or)
def initialize(tag_name, markup, options)
super
@@ -22,15 +20,15 @@ module Liquid
push_block('if'.freeze, markup)
end
def nodelist
@blocks.map(&:attachment)
end
def parse(tokens)
while parse_body(@blocks.last.attachment, tokens)
end
end
def nodelist
@blocks.map(&:attachment)
end
def unknown_tag(tag, markup, tokens)
if ['elsif'.freeze, 'else'.freeze].include?(tag)
push_block(tag, markup)
@@ -39,16 +37,15 @@ module Liquid
end
end
def render_to_output_buffer(context, output)
def render(context)
context.stack do
@blocks.each do |block|
if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output)
return block.attachment.render(context)
end
end
''.freeze
end
output
end
private
@@ -111,12 +108,6 @@ module Liquid
Condition.new(a)
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.blocks
end
end
end
Template.register_tag('if'.freeze, If)

View File

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

View File

@@ -16,8 +16,6 @@ module Liquid
class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options)
super
@@ -42,7 +40,7 @@ module Liquid
def parse(_tokens)
end
def render_to_output_buffer(context, output)
def render(context)
template_name = context.evaluate(@template_name_expr)
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
@@ -66,21 +64,19 @@ module Liquid
end
if variable.is_a?(Array)
variable.each do |var|
variable.collect do |var|
context[context_variable_name] = var
partial.render_to_output_buffer(context, output)
partial.render(context)
end
else
context[context_variable_name] = variable
partial.render_to_output_buffer(context, output)
partial.render(context)
end
end
ensure
context.template_name = old_template_name
context.partial = old_partial
end
output
end
private
@@ -111,15 +107,6 @@ module Liquid
file_system.read_template_file(context.evaluate(@template_name_expr))
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
@node.variable_name_expr
] + @node.attributes.values
end
end
end
Template.register_tag('include'.freeze, Include)

View File

@@ -20,11 +20,10 @@ module Liquid
@variable = markup.strip
end
def render_to_output_buffer(context, output)
def render(context)
value = context.environments.first[@variable] ||= 0
context.environments.first[@variable] = value + 1
output << value.to_s
output
value.to_s
end
end

View File

@@ -22,9 +22,8 @@ module Liquid
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end
def render_to_output_buffer(_context, output)
output << @body
output
def render(_context)
@body
end
def nodelist

View File

@@ -2,8 +2,6 @@ module Liquid
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
attr_reader :variable_name, :collection_name, :attributes
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@@ -18,7 +16,7 @@ module Liquid
end
end
def render_to_output_buffer(context, output)
def render(context)
collection = context.evaluate(@collection_name) or return ''.freeze
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
@@ -30,34 +28,25 @@ module Liquid
cols = context.evaluate(@attributes['cols'.freeze]).to_i
output << "<tr class=\"row1\">\n"
result = "<tr class=\"row1\">\n"
context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'.freeze] = tablerowloop
collection.each do |item|
collection.each_with_index do |item, index|
context[@variable_name] = item
output << "<td class=\"col#{tablerowloop.col}\">"
super
output << '</td>'
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
if tablerowloop.col_last && !tablerowloop.last
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
end
tablerowloop.send(:increment!)
end
end
output << "</tr>\n"
output
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
super + @node.attributes.values + [@node.collection_name]
end
result << "</tr>\n"
result
end
end

View File

@@ -6,23 +6,23 @@ module Liquid
# {% unless x < 0 %} x is greater than zero {% endunless %}
#
class Unless < If
def render_to_output_buffer(context, output)
def render(context)
context.stack do
# First condition is interpreted backwards ( if not )
first_block = @blocks.first
unless first_block.evaluate(context)
return first_block.attachment.render_to_output_buffer(context, output)
return first_block.attachment.render(context)
end
# After the first condition unless works just like if
@blocks[1..-1].each do |block|
if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output)
return block.attachment.render(context)
end
end
end
output
''.freeze
end
end
end

View File

@@ -50,7 +50,7 @@ module Liquid
private
def lookup_class(name)
Object.const_get(name)
name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
end
end
@@ -187,12 +187,9 @@ module Liquid
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end
output = nil
case args.last
when Hash
options = args.pop
output = options[:output] if options[:output]
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
@@ -207,9 +204,10 @@ module Liquid
begin
# render the nodelist.
# 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 || '')
result = with_profiling(context) do
@root.render(context)
end
result.respond_to?(:join) ? result.join : result
rescue Liquid::MemoryError => e
context.handle_error(e)
ensure
@@ -222,10 +220,6 @@ module Liquid
render(*args)
end
def render_to_output_buffer(context, output)
render(context, output: output)
end
private
def tokenize(source)

View File

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

View File

@@ -46,11 +46,11 @@ module Liquid
def self.to_number(obj)
case obj
when Float
BigDecimal(obj.to_s)
BigDecimal.new(obj.to_s)
when Numeric
obj
when String
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal(obj) : obj.to_i
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
else
if obj.respond_to?(:to_number)
obj.to_number

View File

@@ -10,16 +10,10 @@ module Liquid
# {{ user | link }}
#
class Variable
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
JustTagAttributes = /\A#{TagAttributes}\z/o
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
attr_accessor :filters, :name, :line_number
attr_reader :parse_context
alias_method :options, :parse_context
include ParserSwitching
def initialize(markup, parse_context)
@@ -41,17 +35,17 @@ module Liquid
def lax_parse(markup)
@filters = []
return unless markup =~ MarkupWithQuotedFragment
return unless markup =~ /(#{QuotedFragment})(.*)/om
name_markup = $1
filter_markup = $2
@name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex
if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
filters = $1.scan(FilterParser)
filters.each do |f|
next unless f =~ /\w+/
filtername = Regexp.last_match(0)
filterargs = f.scan(FilterArgsRegex).flatten
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
@filters << parse_filter_expressions(filtername, filterargs)
end
end
@@ -85,38 +79,26 @@ module Liquid
end
obj = context.apply_global_filter(obj)
taint_check(context, obj)
obj
end
def render_to_output_buffer(context, output)
obj = render(context)
if obj.is_a?(Array)
output << obj.join
elsif obj.nil?
else
output << obj.to_s
end
output
end
private
def parse_filter_expressions(filter_name, unparsed_args)
filter_args = []
keyword_args = nil
keyword_args = {}
unparsed_args.each do |a|
if matches = a.match(JustTagAttributes)
keyword_args ||= {}
if matches = a.match(/\A#{TagAttributes}\z/o)
keyword_args[matches[1]] = Expression.parse(matches[2])
else
filter_args << Expression.parse(a)
end
end
result = [filter_name, filter_args]
result << keyword_args if keyword_args
result << keyword_args unless keyword_args.empty?
result
end
@@ -150,11 +132,5 @@ module Liquid
raise error
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.name] + @node.filters.flatten
end
end
end
end

View File

@@ -1,7 +1,7 @@
module Liquid
class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze].freeze
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
attr_reader :name, :lookups
@@ -78,11 +78,5 @@ module Liquid
def state
[@name, @lookups, @command_flags]
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.lookups
end
end
end
end

View File

@@ -1,5 +1,4 @@
# encoding: utf-8
module Liquid
VERSION = "4.0.3".freeze
VERSION = "4.0.0"
end

View File

@@ -1,5 +1,4 @@
# encoding: utf-8
lib = File.expand_path('../lib/', __FILE__)
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)

View File

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

View File

@@ -12,7 +12,7 @@ class CommentForm < Liquid::Block
end
end
def render_to_output_buffer(context, output)
def render(context)
article = context[@variable_name]
context.stack do
@@ -23,9 +23,7 @@ class CommentForm < Liquid::Block
'email' => context['comment.email'],
'body' => context['comment.body']
}
output << wrap_in_form(article, render_all(@nodelist, context, output))
output
wrap_in_form(article, render_all(@nodelist, context))
end
end

View File

@@ -21,7 +21,7 @@ class Paginate < Liquid::Block
end
end
def render_to_output_buffer(context, output)
def render(context)
@context = context
context.stack do

View File

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

View File

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

View File

@@ -123,7 +123,7 @@ class ErrorHandlingTest < Minitest::Test
',
error_mode: :warn,
line_numbers: true
)
)
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
template.warnings.map(&:message)
@@ -140,7 +140,7 @@ class ErrorHandlingTest < Minitest::Test
',
error_mode: :strict,
line_numbers: true
)
)
end
assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message
@@ -158,7 +158,7 @@ class ErrorHandlingTest < Minitest::Test
bla
',
line_numbers: true
)
)
end
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message

View File

@@ -1,247 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class ParseTreeVisitorTest < Minitest::Test
include Liquid
def test_variable
assert_equal(
["test"],
visit(%({{ test }}))
)
end
def test_varible_with_filter
assert_equal(
["test", "infilter"],
visit(%({{ test | split: infilter }}))
)
end
def test_dynamic_variable
assert_equal(
["test", "inlookup"],
visit(%({{ test[inlookup] }}))
)
end
def test_if_condition
assert_equal(
["test"],
visit(%({% if test %}{% endif %}))
)
end
def test_complex_if_condition
assert_equal(
["test"],
visit(%({% if 1 == 1 and 2 == test %}{% endif %}))
)
end
def test_if_body
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{{ test }}{% endif %}))
)
end
def test_unless_condition
assert_equal(
["test"],
visit(%({% unless test %}{% endunless %}))
)
end
def test_complex_unless_condition
assert_equal(
["test"],
visit(%({% unless 1 == 1 and 2 == test %}{% endunless %}))
)
end
def test_unless_body
assert_equal(
["test"],
visit(%({% unless 1 == 1 %}{{ test }}{% endunless %}))
)
end
def test_elsif_condition
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{% elsif test %}{% endif %}))
)
end
def test_complex_elsif_condition
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %}))
)
end
def test_elsif_body
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %}))
)
end
def test_else_body
assert_equal(
["test"],
visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %}))
)
end
def test_case_left
assert_equal(
["test"],
visit(%({% case test %}{% endcase %}))
)
end
def test_case_condition
assert_equal(
["test"],
visit(%({% case 1 %}{% when test %}{% endcase %}))
)
end
def test_case_when_body
assert_equal(
["test"],
visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %}))
)
end
def test_case_else_body
assert_equal(
["test"],
visit(%({% case 1 %}{% else %}{{ test }}{% endcase %}))
)
end
def test_for_in
assert_equal(
["test"],
visit(%({% for x in test %}{% endfor %}))
)
end
def test_for_limit
assert_equal(
["test"],
visit(%({% for x in (1..5) limit: test %}{% endfor %}))
)
end
def test_for_offset
assert_equal(
["test"],
visit(%({% for x in (1..5) offset: test %}{% endfor %}))
)
end
def test_for_body
assert_equal(
["test"],
visit(%({% for x in (1..5) %}{{ test }}{% endfor %}))
)
end
def test_tablerow_in
assert_equal(
["test"],
visit(%({% tablerow x in test %}{% endtablerow %}))
)
end
def test_tablerow_limit
assert_equal(
["test"],
visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %}))
)
end
def test_tablerow_offset
assert_equal(
["test"],
visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %}))
)
end
def test_tablerow_body
assert_equal(
["test"],
visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %}))
)
end
def test_cycle
assert_equal(
["test"],
visit(%({% cycle test %}))
)
end
def test_assign
assert_equal(
["test"],
visit(%({% assign x = test %}))
)
end
def test_capture
assert_equal(
["test"],
visit(%({% capture x %}{{ test }}{% endcapture %}))
)
end
def test_include
assert_equal(
["test"],
visit(%({% include test %}))
)
end
def test_include_with
assert_equal(
["test"],
visit(%({% include "hai" with test %}))
)
end
def test_include_for
assert_equal(
["test"],
visit(%({% include "hai" for test %}))
)
end
def test_preserve_tree_structure
assert_equal(
[[nil, [
[nil, [[nil, [["other", []]]]]],
["test", []],
["xs", []]
]]],
traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit
)
end
private
def traversal(template)
ParseTreeVisitor
.for(Template.parse(template).root)
.add_callback_for(VariableLookup, &:name)
end
def visit(template)
traversal(template).visit.flatten.compact
end
end

View File

@@ -99,7 +99,7 @@ class ParsingQuirksTest < Minitest::Test
# After the messed up quotes a filter without parameters (reverse) should work
# but one with parameters (remove) shouldn't be detected.
assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}")
assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
end
end

View File

@@ -158,10 +158,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal '1', @filters.url_decode(1)
assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
assert_nil @filters.url_decode(nil)
exception = assert_raises Liquid::ArgumentError do
@filters.url_decode('%ff')
end
assert_equal 'Liquid error: invalid byte sequence in UTF-8', exception.message
end
def test_truncatewords
@@ -181,9 +177,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal 'test', @filters.strip_html("<div\nclass='multiline'>test</div>")
assert_equal 'test', @filters.strip_html("<!-- foo bar \n test -->test")
assert_equal '', @filters.strip_html(nil)
# Quirk of the existing implementation
assert_equal 'foo;', @filters.strip_html("<<<script </script>script>foo;</script>")
end
def test_join
@@ -197,11 +190,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
end
def test_sort_with_nils
assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1])
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a")
end
def test_sort_when_property_is_sometimes_missing_puts_nils_last
input = [
{ "price" => 4, "handle" => "alpha" },
@@ -220,89 +208,14 @@ class StandardFiltersTest < Minitest::Test
assert_equal expectation, @filters.sort(input, "price")
end
def test_sort_natural
assert_equal ["a", "B", "c", "D"], @filters.sort_natural(["c", "D", "a", "B"])
assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, { "a" => "a" }, { "a" => "B" }], "a")
end
def test_sort_natural_with_nils
assert_equal ["a", "B", "c", "D", nil], @filters.sort_natural([nil, "c", "D", "a", "B"])
assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }, {}], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, {}, { "a" => "a" }, { "a" => "B" }], "a")
end
def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last
input = [
{ "price" => "4", "handle" => "alpha" },
{ "handle" => "beta" },
{ "price" => "1", "handle" => "gamma" },
{ "handle" => "delta" },
{ "price" => 2, "handle" => "epsilon" }
]
expectation = [
{ "price" => "1", "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" },
{ "price" => "4", "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" }
]
assert_equal expectation, @filters.sort_natural(input, "price")
end
def test_sort_natural_case_check
input = [
{ "key" => "X" },
{ "key" => "Y" },
{ "key" => "Z" },
{ "fake" => "t" },
{ "key" => "a" },
{ "key" => "b" },
{ "key" => "c" }
]
expectation = [
{ "key" => "a" },
{ "key" => "b" },
{ "key" => "c" },
{ "key" => "X" },
{ "key" => "Y" },
{ "key" => "Z" },
{ "fake" => "t" }
]
assert_equal expectation, @filters.sort_natural(input, "key")
assert_equal ["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"])
end
def test_sort_empty_array
assert_equal [], @filters.sort([], "a")
end
def test_sort_invalid_property
foo = [
[1],
[2],
[3]
]
assert_raises Liquid::ArgumentError do
@filters.sort(foo, "bar")
end
end
def test_sort_natural_empty_array
assert_equal [], @filters.sort_natural([], "a")
end
def test_sort_natural_invalid_property
foo = [
[1],
[2],
[3]
]
assert_raises Liquid::ArgumentError do
@filters.sort_natural(foo, "bar")
end
end
def test_legacy_sort_hash
assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 })
end
@@ -326,34 +239,10 @@ class StandardFiltersTest < Minitest::Test
assert_equal [], @filters.uniq([], "a")
end
def test_uniq_invalid_property
foo = [
[1],
[2],
[3]
]
assert_raises Liquid::ArgumentError do
@filters.uniq(foo, "bar")
end
end
def test_compact_empty_array
assert_equal [], @filters.compact([], "a")
end
def test_compact_invalid_property
foo = [
[1],
[2],
[3]
]
assert_raises Liquid::ArgumentError do
@filters.compact(foo, "bar")
end
end
def test_reverse
assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4])
end
@@ -419,29 +308,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new
end
def test_map_returns_empty_on_2d_input_array
foo = [
[1],
[2],
[3]
]
assert_raises Liquid::ArgumentError do
@filters.map(foo, "bar")
end
end
def test_map_returns_empty_with_no_property
foo = [
[1],
[2],
[3]
]
assert_raises Liquid::ArgumentError do
@filters.map(foo, nil)
end
end
def test_sort_works_on_enumerables
assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new
end
@@ -472,9 +338,9 @@ class StandardFiltersTest < Minitest::Test
assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y")
assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y")
assert_equal Date.today.year.to_s, @filters.date('now', '%Y')
assert_equal Date.today.year.to_s, @filters.date('today', '%Y')
assert_equal Date.today.year.to_s, @filters.date('Today', '%Y')
assert_equal "#{Date.today.year}", @filters.date('now', '%Y')
assert_equal "#{Date.today.year}", @filters.date('today', '%Y')
assert_equal "#{Date.today.year}", @filters.date('Today', '%Y')
assert_nil @filters.date(nil, "%B")
@@ -496,8 +362,10 @@ class StandardFiltersTest < Minitest::Test
def test_replace
assert_equal '2 2 2 2', @filters.replace('1 1 1 1', '1', 2)
assert_equal '2 2 2 2', @filters.replace('1 1 1 1', 1, 2)
assert_equal "\\& \\& \\& \\&", @filters.replace('1 1 1 1', '1', "\\&")
assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2)
assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2)
assert_equal '\\& 1 1 1', @filters.replace_first("1 1 1 1", '1', "\\&")
assert_template_result '2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}"
end
@@ -630,28 +498,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)
end
def test_at_most
assert_template_result "4", "{{ 5 | at_most:4 }}"
assert_template_result "5", "{{ 5 | at_most:5 }}"
assert_template_result "5", "{{ 5 | at_most:6 }}"
assert_template_result "4.5", "{{ 4.5 | at_most:5 }}"
assert_template_result "5", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(6)
assert_template_result "4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4)
assert_template_result "4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4)
end
def test_at_least
assert_template_result "5", "{{ 5 | at_least:4 }}"
assert_template_result "5", "{{ 5 | at_least:5 }}"
assert_template_result "6", "{{ 5 | at_least:6 }}"
assert_template_result "5", "{{ 4.5 | at_least:5 }}"
assert_template_result "6", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(6)
assert_template_result "5", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(4)
assert_template_result "6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6)
end
def test_append
assigns = { 'a' => 'bc', 'b' => 'd' }
assert_template_result('bcd', "{{ a | append: 'd'}}", assigns)
@@ -692,78 +538,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
end
def test_where
input = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true }
]
expectation = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "delta", "ok" => true }
]
assert_equal expectation, @filters.where(input, "ok", true)
assert_equal expectation, @filters.where(input, "ok")
end
def test_where_no_key_set
input = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta" },
{ "handle" => "gamma" },
{ "handle" => "delta", "ok" => true }
]
expectation = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "delta", "ok" => true }
]
assert_equal expectation, @filters.where(input, "ok", true)
assert_equal expectation, @filters.where(input, "ok")
end
def test_where_non_array_map_input
assert_equal [{ "a" => "ok" }], @filters.where({ "a" => "ok" }, "a", "ok")
assert_equal [], @filters.where({ "a" => "not ok" }, "a", "ok")
end
def test_where_indexable_but_non_map_value
assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok", true) }
assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok") }
end
def test_where_non_boolean_value
input = [
{ "message" => "Bonjour!", "language" => "French" },
{ "message" => "Hello!", "language" => "English" },
{ "message" => "Hallo!", "language" => "German" }
]
assert_equal [{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French")
assert_equal [{ "message" => "Hallo!", "language" => "German" }], @filters.where(input, "language", "German")
assert_equal [{ "message" => "Hello!", "language" => "English" }], @filters.where(input, "language", "English")
end
def test_where_array_of_only_unindexable_values
assert_nil @filters.where([nil], "ok", true)
assert_nil @filters.where([nil], "ok")
end
def test_where_no_target_value
input = [
{ "foo" => false },
{ "foo" => true },
{ "foo" => "for sure" },
{ "bar" => true }
]
assert_equal [{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo")
end
private
def with_timezone(tz)

View File

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

View File

@@ -103,34 +103,6 @@ HERE
assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns)
end
def test_limiting_with_invalid_limit
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
template = <<-MKUP
{% for i in array limit: true offset: 1 %}
{{ i }}
{% endfor %}
MKUP
exception = assert_raises(Liquid::ArgumentError) do
Template.parse(template).render!(assigns)
end
assert_equal("Liquid error: invalid integer", exception.message)
end
def test_limiting_with_invalid_offset
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
template = <<-MKUP
{% for i in array limit: 1 offset: true %}
{{ i }}
{% endfor %}
MKUP
exception = assert_raises(Liquid::ArgumentError) do
Template.parse(template).render!(assigns)
end
assert_equal("Liquid error: invalid integer", exception.message)
end
def test_dynamic_variable_limiting
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
assigns['limit'] = 2
@@ -187,7 +159,7 @@ HERE
assert_template_result(expected, markup, assigns)
end
def test_pause_resume_big_limit
def test_pause_resume_BIG_limit
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%}
@@ -206,7 +178,7 @@ HERE
assert_template_result(expected, markup, assigns)
end
def test_pause_resume_big_offset
def test_pause_resume_BIG_offset
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%}
next

View File

@@ -30,9 +30,6 @@ class TestFileSystem
when 'assignments'
"{% assign foo = 'bar' %}"
when 'break'
"{% break %}"
else
template_path
end
@@ -66,9 +63,8 @@ class CustomInclude < Liquid::Tag
def parse(tokens)
end
def render_to_output_buffer(context, output)
output << @template_name[1..-2]
output
def render(context)
@template_name[1..-2]
end
end
@@ -246,9 +242,4 @@ class IncludeTagTest < Minitest::Test
assert_equal [], template.errors
end
def test_break_through_include
assert_template_result "1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}"
assert_template_result "1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}"
end
end # IncludeTagTest

View File

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

View File

@@ -139,16 +139,6 @@ class TemplateTest < Minitest::Test
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 %}")
@@ -197,14 +187,6 @@ class TemplateTest < Minitest::Test
assert_equal "ababab", t.render
end
def test_render_length_uses_number_of_bytes_not_characters
t = Template.parse("{% if true %}すごい{% endif %}")
t.resource_limits.render_length_limit = 10
assert_equal "Liquid error: Memory limits exceeded", t.render
t.resource_limits.render_length_limit = 18
assert_equal "すごい", t.render
end
def test_default_resource_limits_unaffected_by_render_with_context
context = Context.new
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")

View File

@@ -496,10 +496,6 @@ class TrimModeTest < Minitest::Test
assert_template_result(expected, text)
end
def test_right_trim_followed_by_tag
assert_template_result('ab c', '{{ "a" -}}{{ "b" }} c')
end
def test_raw_output
whitespace = ' '
text = <<-END_TEMPLATE

13
test/test_helper.rb Executable file → Normal file
View File

@@ -37,18 +37,18 @@ module Minitest
include Liquid
def assert_template_result(expected, template, assigns = {}, message = nil)
assert_equal expected, Template.parse(template, line_numbers: true).render!(assigns), message
assert_equal expected, Template.parse(template).render!(assigns), message
end
def assert_template_result_matches(expected, template, assigns = {}, message = nil)
return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp
assert_match expected, Template.parse(template, line_numbers: true).render!(assigns), message
assert_match expected, Template.parse(template).render!(assigns), message
end
def assert_match_syntax_error(match, template, assigns = {})
exception = assert_raises(Liquid::SyntaxError) do
Template.parse(template, line_numbers: true).render(assigns)
Template.parse(template).render(assigns)
end
assert_match match, exception.message
end
@@ -84,13 +84,6 @@ module Minitest
ensure
Liquid::Template.error_mode = old_mode
end
def with_custom_tag(tag_name, tag_class)
Liquid::Template.register_tag(tag_name, tag_class)
yield
ensure
Liquid::Template.tags.delete(tag_name)
end
end
end

View File

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

View File

@@ -24,9 +24,9 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_true 1, '<=', 1
# negative numbers
assert_evaluates_true 1, '>', -1
assert_evaluates_true -1, '<', 1
assert_evaluates_true (-1), '<', 1
assert_evaluates_true 1.0, '>', -1.0
assert_evaluates_true -1.0, '<', 1.0
assert_evaluates_true (-1.0), '<', 1.0
end
def test_default_operators_evalute_false

View File

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