Compare commits

..

1 Commits

Author SHA1 Message Date
Simon Génier
a61903da54 Add support for iterating ranges with float boundaries.
The meaning I gave is to start at the lower bound and step by one. This has the
advantage of having (n..m) and ((n.to_f)..(m.to_f)) behave the same. Another
thing we could do that have this same property is to cast bounds to integer.
This would do the right thing if there is a floating point rounding error, but I
feel it is more surprising for cases such as (1.5..8).

Note that it is not possible to create ranges with floating point boundaries
directly in Liquid at the moment. However, since there is no distinction between
Ruby and Liquid ranges, a drop can introduce such a value by returning it.
2017-08-09 12:13:00 -04:00
38 changed files with 166 additions and 986 deletions

2
.github/probots.yml vendored
View File

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

View File

@@ -7,6 +7,8 @@ AllCops:
Metrics/BlockNesting: Metrics/BlockNesting:
Max: 3 Max: 3
Exclude:
- 'lib/liquid/block_body.rb'
Metrics/ModuleLength: Metrics/ModuleLength:
Enabled: false Enabled: false
@@ -30,7 +32,7 @@ Lint/UnusedBlockArgument:
Enabled: false Enabled: false
Lint/EndAlignment: Lint/EndAlignment:
EnforcedStyleAlignWith: variable AlignWith: variable
Lint/UnusedMethodArgument: Lint/UnusedMethodArgument:
Enabled: false Enabled: false
@@ -59,10 +61,10 @@ Style/BracesAroundHashParameters:
Style/NumericLiterals: Style/NumericLiterals:
Enabled: false Enabled: false
Layout/SpaceInsideBrackets: Style/SpaceInsideBrackets:
Enabled: false Enabled: false
Layout/SpaceBeforeBlockBraces: Style/SpaceBeforeBlockBraces:
Enabled: false Enabled: false
Style/Documentation: Style/Documentation:
@@ -71,22 +73,19 @@ Style/Documentation:
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
Enabled: false Enabled: false
Style/TrailingCommaInArrayLiteral: Style/TrailingComma:
Enabled: false Enabled: false
Style/TrailingCommaInHashLiteral: Style/IndentHash:
Enabled: false
Layout/IndentHash:
EnforcedStyle: consistent EnforcedStyle: consistent
Style/FormatString: Style/FormatString:
Enabled: false Enabled: false
Layout/AlignParameters: Style/AlignParameters:
EnforcedStyle: with_fixed_indentation EnforcedStyle: with_fixed_indentation
Layout/MultilineOperationIndentation: Style/MultilineOperationIndentation:
EnforcedStyle: indented EnforcedStyle: indented
Style/IfUnlessModifier: Style/IfUnlessModifier:
@@ -95,7 +94,7 @@ Style/IfUnlessModifier:
Style/RaiseArgs: Style/RaiseArgs:
Enabled: false Enabled: false
Style/PreferredHashMethods: Style/DeprecatedHashMethods:
Enabled: false Enabled: false
Style/RegexpLiteral: Style/RegexpLiteral:
@@ -110,7 +109,7 @@ Performance/Count:
Style/ConstantName: Style/ConstantName:
Enabled: false Enabled: false
Layout/CaseIndentation: Style/CaseIndentation:
Enabled: false Enabled: false
Style/ClassVars: Style/ClassVars:
@@ -124,7 +123,3 @@ Style/TrivialAccessors:
Style/WordArray: Style/WordArray:
Enabled: false Enabled: false
Naming/MethodName:
Exclude:
- 'example/server/liquid_servlet.rb'

View File

@@ -1,248 +1,72 @@
# This configuration was generated by # This configuration was generated by `rubocop --auto-gen-config`
# `rubocop --auto-gen-config` # on 2015-06-08 18:16:16 +0000 using RuboCop version 0.32.0.
# on 2017-11-22 11:35:55 -0500 using RuboCop version 0.49.1.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 3
# Cop supports --auto-correct.
Layout/ClosingParenthesisIndentation:
Exclude:
- 'test/integration/error_handling_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
Layout/EmptyLineAfterMagicComment:
Exclude:
- 'lib/liquid/version.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Layout/ExtraSpacing:
Exclude:
- 'test/integration/parsing_quirks_test.rb'
# Offense count: 5 # Offense count: 5
# Cop supports --auto-correct. Lint/NestedMethodDefinition:
# Configuration parameters: EnforcedStyle, SupportedStyles. Enabled: false
# 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 # Offense count: 53
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# 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.
# SupportedStyles: runtime_error, standard_error
Lint/InheritException:
Exclude:
- 'lib/liquid/interrupts.rb'
# Offense count: 1
Lint/ScriptPermission:
Exclude:
- 'test/test_helper.rb'
# Offense count: 52
Metrics/AbcSize: Metrics/AbcSize:
Max: 56 Max: 58
# Offense count: 13 # Offense count: 12
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 12 Max: 15
# Offense count: 620 # Offense count: 553
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # Configuration parameters: AllowURI, URISchemes.
# URISchemes: http, https
Metrics/LineLength: Metrics/LineLength:
Max: 294 Max: 294
# Offense count: 102 # Offense count: 77
# Configuration parameters: CountComments. # Configuration parameters: CountComments.
Metrics/MethodLength: Metrics/MethodLength:
Max: 37 Max: 46
# Offense count: 9
Metrics/PerceivedComplexity:
Max: 11
# Offense count: 10
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# 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: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Exclude:
- 'lib/liquid/errors.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.
# SupportedStyles: compact, expanded
Style/EmptyMethod:
Exclude:
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/comment.rb'
- 'lib/liquid/tags/include.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 2
# Configuration parameters: SupportedStyles.
# SupportedStyles: annotated, template
Style/FormatStringToken:
EnforcedStyle: template
# 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: 4
# Configuration parameters: SupportedStyles.
# SupportedStyles: snake_case, camelCase
Style/MethodName:
EnforcedStyle: snake_case
# Offense count: 6 # Offense count: 6
# Cop supports --auto-correct. Metrics/PerceivedComplexity:
Style/MutableConstant: Max: 13
Exclude:
- 'lib/liquid/expression.rb' # Offense count: 1
- 'lib/liquid/lexer.rb' Style/AccessorMethodName:
- 'lib/liquid/standardfilters.rb' Enabled: false
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/variable_lookup.rb'
- 'lib/liquid/version.rb'
# Offense count: 1 # Offense count: 1
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. Style/ClosingParenthesisIndentation:
# SupportedStyles: skip_modifier_ifs, always Enabled: false
Style/Next:
Exclude: # Offense count: 3
- 'lib/liquid/tags/for.rb' # Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
# Offense count: 4 # Offense count: 4
# Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles.
# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles. Style/MethodName:
# SupportedStyles: predicate, comparison Enabled: false
Style/NumericPredicate:
Exclude:
- 'spec/**/*'
- 'lib/liquid/context.rb'
- 'lib/liquid/forloop_drop.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tablerowloop_drop.rb'
# Offense count: 14
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Exclude:
- 'lib/liquid/tags/if.rb'
- 'liquid.gemspec'
- 'test/integration/assign_test.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/RedundantParentheses:
Exclude:
- 'test/unit/condition_unit_test.rb'
# Offense count: 1 # Offense count: 1
# Cop supports --auto-correct. Style/MultilineBlockChain:
Style/RedundantSelf: Enabled: false
Exclude:
- 'lib/liquid/strainer.rb'
# Offense count: 9 # Offense count: 2
# Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
# Configuration parameters: AllowAsExpressionSeparator. Style/Next:
Style/Semicolon: Enabled: false
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 7 # Offense count: 7
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: MinSize, SupportedStyles. # Configuration parameters: AllowAsExpressionSeparator.
# SupportedStyles: percent, brackets Style/Semicolon:
Style/SymbolArray: Enabled: false
EnforcedStyle: brackets
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment.
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
Style/TernaryParentheses:
Exclude:
- 'lib/liquid/context.rb'
- 'lib/liquid/utils.rb'
# Offense count: 4
# Cop supports --auto-correct.
Style/UnneededInterpolation:
Exclude:
- 'lib/liquid/i18n.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/UnneededPercentQ:
Exclude:
- 'test/integration/error_handling_test.rb'
# Offense count: 1 # Offense count: 1
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: MaxLineLength. # Configuration parameters: MaxLineLength.
Style/WhileUntilModifier: Style/WhileUntilModifier:
Exclude: Enabled: false
- 'lib/liquid/tags/case.rb'

View File

@@ -3,9 +3,7 @@ language: ruby
rvm: rvm:
- 2.1 - 2.1
- 2.2 - 2.2
- 2.3 - 2.3.3
- 2.4
- 2.5
- ruby-head - ruby-head
- jruby-head - jruby-head
# - rbx-2 # - rbx-2
@@ -19,14 +17,13 @@ addons:
matrix: matrix:
allow_failures: allow_failures:
- rvm: ruby-head
- rvm: jruby-head - rvm: jruby-head
install: install:
- gem install rainbow -v 2.2.1 - gem install rainbow -v 2.2.1
- bundle install - bundle install
script: bundle exec rake script: "bundle exec rake"
notifications: notifications:
disable: true disable: true

View File

@@ -1,7 +1,4 @@
source 'https://rubygems.org' source 'https://rubygems.org'
git_source(:github) do |repo_name|
"https://github.com/#{repo_name}.git"
end
gemspec gemspec
@@ -12,9 +9,9 @@ group :benchmark, :test do
end end
group :test do group :test do
gem 'rubocop', '~> 0.49.0' gem 'rubocop', '0.34.2'
platform :mri do platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '9168659de45d6d576fce30c735f857e597fa26f6' gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'bd53db95de3d44d631e7c5a267c3d934e66107dd'
end end
end end

View File

@@ -42,8 +42,6 @@ Liquid is a template engine which was written with very specific requirements:
## How to use Liquid ## 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. 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. For standard use you can just pass it the content of a file and call render with a parameters hash.

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 end
require "liquid/version" require "liquid/version"
require 'liquid/parse_tree_visitor'
require 'liquid/lexer' require 'liquid/lexer'
require 'liquid/parser' require 'liquid/parser'
require 'liquid/i18n' require 'liquid/i18n'

View File

@@ -26,12 +26,12 @@ module Liquid
end end
def unknown_tag(tag, _params, _tokens) 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, raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
block_name: block_name)) block_name: block_name))
elsif tag.start_with?('end'.freeze) when 'end'.freeze
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze, raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
tag: tag,
block_name: block_name, block_name: block_name,
block_delimiter: block_delimiter)) block_delimiter: block_delimiter))
else else

View File

@@ -2,7 +2,6 @@ module Liquid
class BlockBody class BlockBody
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/
TAGSTART = "{%".freeze TAGSTART = "{%".freeze
VARSTART = "{{".freeze VARSTART = "{{".freeze
@@ -16,35 +15,38 @@ module Liquid
def parse(tokenizer, parse_context) def parse(tokenizer, parse_context)
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
while token = tokenizer.shift while token = tokenizer.shift
next if token.empty? unless token.empty?
case case
when token.start_with?(TAGSTART) when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context) whitespace_handler(token, parse_context)
unless token =~ FullToken if token =~ FullToken
raise_missing_tag_terminator(token, parse_context) tag_name = $1
markup = $2
# fetch the tag from registered blocks
if tag = registered_tags[tag_name]
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
else
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
else
raise_missing_tag_terminator(token, parse_context)
end
when token.start_with?(VARSTART)
whitespace_handler(token, parse_context)
@nodelist << create_variable(token, parse_context)
@blank = false
else
if parse_context.trim_whitespace
token.lstrip!
end
parse_context.trim_whitespace = false
@nodelist << token
@blank &&= !!(token =~ /\A\s*\z/)
end 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
return yield tag_name, markup
end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
when token.start_with?(VARSTART)
whitespace_handler(token, parse_context)
@nodelist << create_variable(token, parse_context)
@blank = false
else
if parse_context.trim_whitespace
token.lstrip!
end
parse_context.trim_whitespace = false
@nodelist << token
@blank &&= !!(token =~ WhitespaceOrNothing)
end end
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
end end
@@ -70,27 +72,33 @@ module Liquid
output = [] output = []
context.resource_limits.render_score += @nodelist.length context.resource_limits.render_score += @nodelist.length
idx = 0 @nodelist.each do |token|
while node = @nodelist[idx] # Break out if we have any unhanded interrupts.
case node break if context.interrupt?
when String
check_resources(context, node) begin
output << node
when Variable
render_node_to_output(node, output, context)
when Block
render_node_to_output(node, output, context, node.blank?)
break if context.interrupt? # might have happened in a for-block
when Continue, Break
# If we get an Interrupt that means the block must stop processing. An # If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %} # Interrupt is any command that stops block execution such as {% break %}
# or {% continue %} # or {% continue %}
context.push_interrupt(node.interrupt) if token.is_a?(Continue) || token.is_a?(Break)
break context.push_interrupt(token.interrupt)
else # Other non-Block tags break
render_node_to_output(node, output, context) end
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
idx += 1
end end
output.join output.join
@@ -98,25 +106,15 @@ module Liquid
private private
def render_node_to_output(node, output, context, skip_output = false) def render_node(node, context)
node_output = node.render(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 node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
check_resources(context, node_output)
output << node_output unless skip_output
rescue MemoryError => e
raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
output << nil
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
def check_resources(context, node_output)
context.resource_limits.render_length += node_output.length context.resource_limits.render_length += node_output.length
return unless context.resource_limits.reached? if context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze) raise MemoryError.new("Memory limits exceeded".freeze)
end
node_output
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)

View File

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

View File

@@ -171,9 +171,7 @@ module Liquid
if scope.nil? if scope.nil?
@environments.each do |e| @environments.each do |e|
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found) variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
# When lookup returned a value OR there is no value but the lookup also did not raise unless variable.nil?
# then it is the value we are looking for.
if !variable.nil? || @strict_variables && raise_on_not_found
scope = e scope = e
break break
end end

View File

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

View File

@@ -7,12 +7,6 @@ class String # :nodoc:
end end
end end
class Symbol # :nodoc:
def to_liquid
to_s
end
end
class Array # :nodoc: class Array # :nodoc:
def to_liquid def to_liquid
self self

View File

@@ -19,7 +19,6 @@ module Liquid
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./ DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
WHITESPACE_OR_NOTHING = /\s*/
def initialize(input) def initialize(input)
@ss = StringScanner.new(input) @ss = StringScanner.new(input)
@@ -29,7 +28,7 @@ module Liquid
@output = [] @output = []
until @ss.eos? until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING) @ss.skip(/\s*/)
break if @ss.eos? break if @ss.eos?
tok = case tok = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] 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]" if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]" include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
unknown_tag: "Unknown tag '%{tag}'" 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_else: "%{block_name} tag does not expect 'else' tag"
unexpected_outer_tag: "Unexpected outer '%{tag}' tag" unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}" 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

@@ -1,13 +1,13 @@
module Liquid module Liquid
class BlockBody class BlockBody
def render_node_with_profiling(node, output, context, skip_output = false) def render_node_with_profiling(node, context)
Profiler.profile_node_render(node) do Profiler.profile_node_render(node) do
render_node_without_profiling(node, output, context, skip_output) render_node_without_profiling(node, context)
end end
end end
alias_method :render_node_without_profiling, :render_node_to_output alias_method :render_node_without_profiling, :render_node
alias_method :render_node_to_output, :render_node_with_profiling alias_method :render_node, :render_node_with_profiling
end end
class Include < Tag class Include < Tag

View File

@@ -121,23 +121,17 @@ module Liquid
def sort(input, property = nil) def sort(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input)
if property.nil? if property.nil?
ary.sort do |a, b| ary.sort
if !a.nil? && !b.nil?
a <=> b
else
a.nil? ? 1 : -1
end
end
elsif ary.empty? # The next two cases assume a non-empty array. elsif ary.empty? # The next two cases assume a non-empty array.
[] []
elsif ary.all? { |el| el.respond_to?(:[]) } elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort do |a, b| ary.sort do |a, b|
a = a[property] a = a[property]
b = b[property] b = b[property]
if !a.nil? && !b.nil? if a && b
a <=> b a <=> b
else else
a.nil? ? 1 : -1 a ? -1 : 1
end end
end end
end end
@@ -149,39 +143,11 @@ module Liquid
ary = InputIterator.new(input) ary = InputIterator.new(input)
if property.nil? if property.nil?
ary.sort do |a, b| ary.sort { |a, b| a.casecmp(b) }
if !a.nil? && !b.nil?
a.to_s.casecmp(b.to_s)
else
a.nil? ? 1 : -1
end
end
elsif ary.empty? # The next two cases assume a non-empty array. elsif ary.empty? # The next two cases assume a non-empty array.
[] []
elsif ary.all? { |el| el.respond_to?(:[]) } elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort do |a, b| ary.sort { |a, b| a[property].casecmp(b[property]) }
a = a[property]
b = b[property]
if !a.nil? && !b.nil?
a.to_s.casecmp(b.to_s)
else
a.nil? ? 1 : -1
end
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?
[]
elsif ary.first.respond_to?(:[]) && target_value.nil?
ary.where_present(property)
elsif ary.first.respond_to?(:[])
ary.where(property, target_value)
end end
end end
@@ -387,22 +353,6 @@ module Liquid
raise Liquid::FloatDomainError, e.message raise Liquid::FloatDomainError, e.message
end 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) def default(input, default_value = ''.freeze)
if !input || input.respond_to?(:empty?) && input.empty? if !input || input.respond_to?(:empty?) && input.empty?
default_value default_value
@@ -463,24 +413,6 @@ module Liquid
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e) yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
end end
end end
def where(property, target_value)
select do |item|
item[property] == target_value
end
rescue TypeError
# Cannot index with the given property type (eg. indexing integers with strings
# which are only allowed to be indexed by other integers).
raise ArgumentError.new("cannot select the property `#{property}`")
end
def where_present(property)
select { |item| item[property] }
rescue TypeError
# Cannot index with the given property type (eg. indexing integers with strings
# which are only allowed to be indexed by other integers).
raise ArgumentError.new("cannot select the property `#{property}`")
end
end end
end end

View File

@@ -10,8 +10,6 @@ module Liquid
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
attr_reader :to, :from
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@@ -47,12 +45,6 @@ module Liquid
1 1
end end
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.from]
end
end
end end
Template.register_tag('assign'.freeze, Assign) Template.register_tag('assign'.freeze, Assign)

View File

@@ -3,8 +3,6 @@ module Liquid
Syntax = /(#{QuotedFragment})/o Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
attr_reader :blocks, :left
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@blocks = [] @blocks = []
@@ -82,12 +80,6 @@ module Liquid
block.attach(BlockBody.new) block.attach(BlockBody.new)
@blocks << block @blocks << block
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.left] + @node.blocks
end
end
end end
Template.register_tag('case'.freeze, Case) Template.register_tag('case'.freeze, Case)

View File

@@ -15,8 +15,6 @@ module Liquid
SimpleSyntax = /\A#{QuotedFragment}+/o SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
attr_reader :variables
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
case markup case markup
@@ -32,11 +30,11 @@ module Liquid
end end
def render(context) def render(context)
context.registers[:cycle] ||= {} context.registers[:cycle] ||= Hash.new(0)
context.stack do context.stack do
key = context.evaluate(@name) key = context.evaluate(@name)
iteration = context.registers[:cycle][key].to_i iteration = context.registers[:cycle][key]
result = context.evaluate(@variables[iteration]) result = context.evaluate(@variables[iteration])
iteration += 1 iteration += 1
iteration = 0 if iteration >= @variables.size iteration = 0 if iteration >= @variables.size
@@ -53,12 +51,6 @@ module Liquid
$1 ? Expression.parse($1) : nil $1 ? Expression.parse($1) : nil
end.compact end.compact
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
Array(@node.variables)
end
end
end end
Template.register_tag('cycle', Cycle) Template.register_tag('cycle', Cycle)

View File

@@ -46,7 +46,8 @@ module Liquid
class For < Block class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o 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) def initialize(tag_name, markup, options)
super super
@@ -119,7 +120,7 @@ module Liquid
private private
def collection_segment(context) def collection_segment(context)
offsets = context.registers[:for] ||= {} offsets = context.registers[:for] ||= Hash.new(0)
from = if @from == :continue from = if @from == :continue
offsets[@name].to_i offsets[@name].to_i
@@ -128,7 +129,7 @@ module Liquid
end end
collection = context.evaluate(@collection_name) collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range) collection = collection.step(1).to_a if collection.is_a?(Range)
limit = context.evaluate(@limit) limit = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil to = limit ? limit.to_i + from : nil
@@ -155,7 +156,7 @@ module Liquid
begin begin
context['forloop'.freeze] = loop_vars context['forloop'.freeze] = loop_vars
segment.each do |item| segment.each_with_index do |item, index|
context[@variable_name] = item context[@variable_name] = item
result << @for_block.render(context) result << @for_block.render(context)
loop_vars.send(:increment!) loop_vars.send(:increment!)
@@ -191,12 +192,6 @@ module Liquid
def render_else(context) def render_else(context)
@else_block ? @else_block.render(context) : ''.freeze @else_block ? @else_block.render(context) : ''.freeze
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
(super + [@node.limit, @node.from, @node.collection_name]).compact
end
end
end end
Template.register_tag('for'.freeze, For) Template.register_tag('for'.freeze, For)

View File

@@ -14,23 +14,21 @@ module Liquid
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/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) BOOLEAN_OPERATORS = %w(and or)
attr_reader :blocks
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@blocks = [] @blocks = []
push_block('if'.freeze, markup) push_block('if'.freeze, markup)
end end
def nodelist
@blocks.map(&:attachment)
end
def parse(tokens) def parse(tokens)
while parse_body(@blocks.last.attachment, tokens) while parse_body(@blocks.last.attachment, tokens)
end end
end end
def nodelist
@blocks.map(&:attachment)
end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
if ['elsif'.freeze, 'else'.freeze].include?(tag) if ['elsif'.freeze, 'else'.freeze].include?(tag)
push_block(tag, markup) push_block(tag, markup)
@@ -110,12 +108,6 @@ module Liquid
Condition.new(a) Condition.new(a)
end end
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.blocks
end
end
end end
Template.register_tag('if'.freeze, If) Template.register_tag('if'.freeze, If)

View File

@@ -16,8 +16,6 @@ module Liquid
class Include < Tag class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@@ -109,15 +107,6 @@ module Liquid
file_system.read_template_file(context.evaluate(@template_name_expr)) file_system.read_template_file(context.evaluate(@template_name_expr))
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
@node.variable_name_expr
] + @node.attributes.values
end
end
end end
Template.register_tag('include'.freeze, Include) Template.register_tag('include'.freeze, Include)

View File

@@ -2,8 +2,6 @@ module Liquid
class TableRow < Block class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
attr_reader :variable_name, :collection_name, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@@ -35,7 +33,7 @@ module Liquid
tablerowloop = Liquid::TablerowloopDrop.new(length, cols) tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'.freeze] = tablerowloop context['tablerowloop'.freeze] = tablerowloop
collection.each do |item| collection.each_with_index do |item, index|
context[@variable_name] = item context[@variable_name] = item
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>' result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
@@ -50,12 +48,6 @@ module Liquid
result << "</tr>\n" result << "</tr>\n"
result result
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
super + @node.attributes.values + [@node.collection_name]
end
end
end end
Template.register_tag('tablerow'.freeze, TableRow) Template.register_tag('tablerow'.freeze, TableRow)

View File

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

View File

@@ -10,16 +10,10 @@ module Liquid
# {{ user | link }} # {{ user | link }}
# #
class Variable class Variable
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o 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_accessor :filters, :name, :line_number
attr_reader :parse_context attr_reader :parse_context
alias_method :options, :parse_context alias_method :options, :parse_context
include ParserSwitching include ParserSwitching
def initialize(markup, parse_context) def initialize(markup, parse_context)
@@ -41,17 +35,17 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
@filters = [] @filters = []
return unless markup =~ MarkupWithQuotedFragment return unless markup =~ /(#{QuotedFragment})(.*)/om
name_markup = $1 name_markup = $1
filter_markup = $2 filter_markup = $2
@name = Expression.parse(name_markup) @name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
filters = $1.scan(FilterParser) filters = $1.scan(FilterParser)
filters.each do |f| filters.each do |f|
next unless f =~ /\w+/ next unless f =~ /\w+/
filtername = Regexp.last_match(0) filtername = Regexp.last_match(0)
filterargs = f.scan(FilterArgsRegex).flatten filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
@filters << parse_filter_expressions(filtername, filterargs) @filters << parse_filter_expressions(filtername, filterargs)
end end
end end
@@ -97,7 +91,7 @@ module Liquid
filter_args = [] filter_args = []
keyword_args = {} keyword_args = {}
unparsed_args.each do |a| unparsed_args.each do |a|
if matches = a.match(JustTagAttributes) if matches = a.match(/\A#{TagAttributes}\z/o)
keyword_args[matches[1]] = Expression.parse(matches[2]) keyword_args[matches[1]] = Expression.parse(matches[2])
else else
filter_args << Expression.parse(a) filter_args << Expression.parse(a)
@@ -138,11 +132,5 @@ module Liquid
raise error raise error
end end
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.name] + @node.filters.flatten
end
end
end end
end end

View File

@@ -78,11 +78,5 @@ module Liquid
def state def state
[@name, @lookups, @command_flags] [@name, @lookups, @command_flags]
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.lookups
end
end
end end
end end

View File

@@ -1,4 +1,4 @@
# encoding: utf-8 # encoding: utf-8
module Liquid module Liquid
VERSION = "4.0.1" VERSION = "4.0.0"
end end

View File

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

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

@@ -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

@@ -190,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") assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
end end
def test_sort_with_nils
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 def test_sort_when_property_is_sometimes_missing_puts_nils_last
input = [ input = [
{ "price" => 4, "handle" => "alpha" }, { "price" => 4, "handle" => "alpha" },
@@ -213,57 +208,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal expectation, @filters.sort(input, "price") assert_equal expectation, @filters.sort(input, "price")
end 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 def test_sort_empty_array
assert_equal [], @filters.sort([], "a") assert_equal [], @filters.sort([], "a")
end end
@@ -552,28 +496,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4) assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)
end end
def test_at_most
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 def test_append
assigns = { 'a' => 'bc', 'b' => 'd' } assigns = { 'a' => 'bc', 'b' => 'd' }
assert_template_result('bcd', "{{ a | append: 'd'}}", assigns) assert_template_result('bcd', "{{ a | append: 'd'}}", assigns)
@@ -614,78 +536,6 @@ class StandardFiltersTest < Minitest::Test
assert_template_result('abc', "{{ 'abc' | date: '%D' }}") assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
end 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 private
def with_timezone(tz) def with_timezone(tz)

View File

@@ -48,6 +48,10 @@ HERE
def test_for_with_variable_range def test_for_with_variable_range
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3)
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1..3.0))
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.0..3))
assert_template_result(' 1.0 2.0 3.0 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.0..3.0))
assert_template_result(' 1.5 2.5 ', '{%for item in foobar %} {{item}} {%endfor%}', "foobar" => (1.5..3))
end end
def test_for_with_hash_value_range def test_for_with_hash_value_range
@@ -159,7 +163,7 @@ HERE
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end 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] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%} {%for i in array.items limit:3 %}{{i}}{%endfor%}
@@ -178,7 +182,7 @@ HERE
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end 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] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%} markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%}
next next

View File

@@ -261,15 +261,6 @@ class TemplateTest < Minitest::Test
assert_equal 'Liquid error: undefined variable d', t.errors[2].message assert_equal 'Liquid error: undefined variable d', t.errors[2].message
end end
def test_nil_value_does_not_raise
Liquid::Template.error_mode = :strict
t = Template.parse("some{{x}}thing")
result = t.render!({ 'x' => nil }, strict_variables: true)
assert_equal 0, t.errors.count
assert_equal 'something', result
end
def test_undefined_variables_raise def test_undefined_variables_raise
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}") t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")

View File

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

View File

@@ -89,8 +89,4 @@ class VariableTest < Minitest::Test
def test_multiline_variable def test_multiline_variable
assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked') assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')
end end
def test_render_symbol
assert_template_result 'bar', '{{ foo }}', 'foo' => :bar
end
end end