Compare commits

..

2 Commits

Author SHA1 Message Date
Aaron Patterson
94df9a8f52 Update lib/liquid/standardfilters.rb
Co-authored-by: Dylan Thacker-Smith <dylan.smith@shopify.com>
2020-11-16 10:23:20 -08:00
Aaron Patterson
8bf0e7dfae Reduce allocations in truncatewords
I don't know if this is a bottleneck IRL, but while doing some
performance testing I noticed that this method allocates a bunch of
objects and I decided to fix it.

Here is the benchmark I used:

```ruby
require_relative 'theme_runner'

Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new

def count_alloc
  before = GC.stat(:total_allocated_objects)
  yield
  GC.stat(:total_allocated_objects) - before
end

profiler.render # heat
p count_alloc { profiler.render }
p count_alloc { profiler.render }
p count_alloc { profiler.render }
```

Before this change:

```
[aaron@tc-lan-adapter ~/g/liquid (master)]$ ruby -I lib performance/run_once.rb
15753
15750
15752
```

After this change:

```
[aaron@tc-lan-adapter ~/g/liquid (master)]$ ruby -I lib performance/run_once.rb
14015
14010
14011
```

About an 11% reduction in allocations for this test.  I also added some
tests around the current behavior of the method so we can be more sure
the replacement behaves the same way
2020-10-09 16:01:12 -07:00
44 changed files with 860 additions and 625 deletions

View File

@@ -1,5 +1,5 @@
name: Liquid name: Liquid
on: [push, pull_request] on: [push]
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

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

View File

@@ -13,7 +13,6 @@ module Liquid
@body = new_body @body = new_body
while parse_body(@body, tokens) while parse_body(@body, tokens)
end end
@body.freeze
end end
# For backwards compatibility # For backwards compatibility
@@ -77,9 +76,6 @@ module Liquid
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params| body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank? @blank &&= body.blank?
# Instrument for bug 1346
Usage.increment("end_tag_params") if end_tag_params && !end_tag_params.empty?
return false if end_tag_name == block_delimiter return false if end_tag_name == block_delimiter
raise_tag_never_closed(block_name) unless end_tag_name raise_tag_never_closed(block_name) unless end_tag_name

View File

@@ -19,8 +19,6 @@ module Liquid
end end
def parse(tokenizer, parse_context, &block) def parse(tokenizer, parse_context, &block)
raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
if tokenizer.for_liquid_tag if tokenizer.for_liquid_tag
@@ -30,11 +28,6 @@ module Liquid
end end
end end
def freeze
@nodelist.freeze
super
end
private def parse_for_liquid_tag(tokenizer, parse_context) private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift) while (token = tokenizer.shift)
unless token.empty? || token =~ WhitespaceOrNothing unless token.empty? || token =~ WhitespaceOrNothing
@@ -199,8 +192,6 @@ module Liquid
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
freeze unless frozen?
context.resource_limits.increment_render_score(@nodelist.length) context.resource_limits.increment_render_score(@nodelist.length)
idx = 0 idx = 0

View File

@@ -45,16 +45,8 @@ module Liquid
@@operators @@operators
end end
def self.parse_expression(parse_context, markup) def self.parse_expression(markup)
@@method_literals[markup] || parse_context.parse_expression(markup) @@method_literals[markup] || Expression.parse(markup)
end
def self.strict_parse_expression(parse_context, p)
if p.look(:id) && !p.look(:dot, 1) && !p.look(:open_square, 1)
parse_expression(parse_context, p.consume)
else
p.expression
end
end end
attr_reader :attachment, :child_condition attr_reader :attachment, :child_condition

View File

@@ -41,7 +41,7 @@ module Liquid
self.exception_renderer = Template.default_exception_renderer self.exception_renderer = Template.default_exception_renderer
if rethrow_errors if rethrow_errors
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA self.exception_renderer = ->(_e) { raise }
end end
# Do this last, since it could result in this object being passed to a Proc in the environment # Do this last, since it could result in this object being passed to a Proc in the environment

View File

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

View File

@@ -10,30 +10,27 @@ module Liquid
'empty' => '' 'empty' => ''
}.freeze }.freeze
SINGLE_QUOTED_STRING = /\A\s*'(.*)'\s*\z/m SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
DOUBLE_QUOTED_STRING = /\A\s*"(.*)"\s*\z/m DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
INTEGERS_REGEX = /\A\s*(-?\d+)\s*\z/ INTEGERS_REGEX = /\A(-?\d+)\z/
FLOATS_REGEX = /\A\s*(-?\d[\d\.]+)\s*\z/ FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
RANGES_REGEX = /\A\s*\(\s*(\S+)\s*\.\.\s*(\S+)\s*\)\s*\z/ RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
def self.parse(markup) def self.parse(markup)
case markup if LITERALS.key?(markup)
when nil LITERALS[markup]
nil
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
Regexp.last_match(1)
when INTEGERS_REGEX
Regexp.last_match(1).to_i
when RANGES_REGEX
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
when FLOATS_REGEX
Regexp.last_match(1).to_f
else else
markup = markup.strip case markup
if LITERALS.key?(markup) when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
LITERALS[markup] Regexp.last_match(1)
when INTEGERS_REGEX
Regexp.last_match(1).to_i
when RANGES_REGEX
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
when FLOATS_REGEX
Regexp.last_match(1).to_f
else else
VariableLookup.lax_parse(markup) VariableLookup.parse(markup)
end end
end end
end end

View File

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

View File

@@ -20,6 +20,7 @@
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}" tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}" variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
tag_never_closed: "'%{block_name}' tag was never closed" tag_never_closed: "'%{block_name}' tag was never closed"
meta_syntax_error: "Liquid syntax error: #{e.message}"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3" table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
render: "Syntax error in tag 'render' - Template name must be a quoted string" render: "Syntax error in tag 'render' - Template name must be a quoted string"
argument: argument:

View File

@@ -23,10 +23,6 @@ module Liquid
Liquid::BlockBody.new Liquid::BlockBody.new
end end
def parse_expression(markup)
Expression.parse(markup)
end
def partial=(value) def partial=(value)
@partial = value @partial = value
@options = value ? partial_options : @template_options @options = value ? partial_options : @template_options

View File

@@ -46,56 +46,50 @@ module Liquid
tok[0] == type tok[0] == type
end end
SINGLE_TOKEN_EXPRESSION_TYPES = [:string, :number].freeze
private_constant :SINGLE_TOKEN_EXPRESSION_TYPES
def expression def expression
token = @tokens[@p] token = @tokens[@p]
case token[0] if token[0] == :id
when :id variable_signature
if Expression::LITERALS.key?(token[1]) && !look(:dot, 1) && !look(:open_square, 1) elsif SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0])
Expression::LITERALS[consume] consume
else elsif token.first == :open_round
VariableLookup.strict_parse(self)
end
when :open_square
VariableLookup.strict_parse(self)
when :string
consume[1..-2]
when :number
num_str = consume
num_str.include?('.') ? num_str.to_f : num_str.to_i
when :open_round
consume consume
first = expression first = expression
consume(:dotdot) consume(:dotdot)
last = expression last = expression
consume(:close_round) consume(:close_round)
RangeLookup.build(first, last) "(#{first}..#{last})"
else else
raise SyntaxError, "#{token} is not a valid expression" raise SyntaxError, "#{token} is not a valid expression"
end end
end end
def arguments def argument
filter_args = [] str = +""
keyword_args = nil # might be a keyword argument (identifier: expression)
if look(:id) && look(:colon, 1)
loop do str << consume << consume << ' '
# keyword argument (identifier: expression)
if look(:colon, 1)
keyword_args ||= {}
k = consume(:id)
consume
v = expression
keyword_args[k] = v
else
filter_args << expression
end
break unless consume?(:comma)
end end
result = [filter_args] str << expression
result << keyword_args if keyword_args str
result end
def variable_signature
str = consume(:id)
while look(:open_square)
str << consume
str << expression
str << consume(:close_square)
end
if look(:dot)
str << consume
str << variable_signature
end
str
end end
end end
end end

View File

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

View File

@@ -5,43 +5,35 @@ module Liquid
def self.parse(start_markup, end_markup) def self.parse(start_markup, end_markup)
start_obj = Expression.parse(start_markup) start_obj = Expression.parse(start_markup)
end_obj = Expression.parse(end_markup) end_obj = Expression.parse(end_markup)
build(start_obj, end_obj)
end
def self.build(start_obj, end_obj)
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate) if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj) new(start_obj, end_obj)
else else
to_integer(start_obj)..to_integer(end_obj) start_obj.to_i..end_obj.to_i
end end
end end
def self.to_integer(input) def initialize(start_obj, end_obj)
@start_obj = start_obj
@end_obj = end_obj
end
def evaluate(context)
start_int = to_integer(context.evaluate(@start_obj))
end_int = to_integer(context.evaluate(@end_obj))
start_int..end_int
end
private
def to_integer(input)
case input case input
when Integer when Integer
input input
when NilClass, String, Float when NilClass, String
input.to_i input.to_i
else else
Utils.to_integer(input) Utils.to_integer(input)
end end
end end
attr_reader :start_expr, :end_expr
def initialize(start_expr, end_expr)
@start_expr = start_expr
@end_expr = end_expr
end
def evaluate(context)
start_int = self.class.to_integer(context.evaluate(@start_expr))
end_int = self.class.to_integer(context.evaluate(@end_expr))
start_int..end_int
end
def ==(other)
self.class == other.class && start_expr == other.start_expr && end_expr == other.end_expr
end
end end
end end

View File

@@ -89,13 +89,21 @@ module Liquid
def truncatewords(input, words = 15, truncate_string = "...") def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil? return if input.nil?
wordlist = input.to_s.split words = Utils.to_integer(words)
words = Utils.to_integer(words)
l = words - 1 words = 1 if words <= 0
l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : input # Scan for non-space characters followed by one or more space characters
# `words` times. Also ignore leading whitespace
str = input[/\A[ ]*(?:[^ ]*[ ]+){#{words}}/]
if str
str.strip! # Remove trailing space
str.gsub!(/[ ]{2,}/, " ") # Shrink multiple spaces to one space
str.concat(truncate_string.to_s)
else
input
end
end end
# Split input string into an array of substrings separated by given pattern. # Split input string into an array of substrings separated by given pattern.

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ module Liquid
case markup case markup
when NamedSyntax when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2)) @variables = variables_from_string(Regexp.last_match(2))
@name = parse_expression(Regexp.last_match(1)) @name = Expression.parse(Regexp.last_match(1))
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
@@ -61,7 +61,7 @@ module Liquid
def variables_from_string(markup) def variables_from_string(markup)
markup.split(',').collect do |var| markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o var =~ /\s*(#{QuotedFragment})\s*/o
Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil
end.compact end.compact
end end

View File

@@ -66,8 +66,6 @@ module Liquid
@for_block.remove_blank_strings @for_block.remove_blank_strings
@else_block&.remove_blank_strings @else_block&.remove_blank_strings
end end
@for_block.freeze
@else_block&.freeze
end end
def nodelist def nodelist
@@ -99,7 +97,7 @@ module Liquid
collection_name = Regexp.last_match(2) collection_name = Regexp.last_match(2)
@reversed = !!Regexp.last_match(3) @reversed = !!Regexp.last_match(3)
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = parse_expression(collection_name) @collection_name = Expression.parse(collection_name)
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
set_attribute(key, value) set_attribute(key, value)
end end
@@ -113,9 +111,10 @@ module Liquid
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in') raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
@collection_name = p.expression collection_name = p.expression
@collection_name = Expression.parse(collection_name)
@name = "#{@variable_name}-#{@collection_name}" @name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed') @reversed = p.id?('reversed')
while p.look(:id) && p.look(:colon, 1) while p.look(:id) && p.look(:colon, 1)
@@ -123,18 +122,7 @@ module Liquid
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute") raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end end
p.consume p.consume
case attribute set_attribute(attribute, p.expression)
when 'offset'
@from =
if p.id?('continue')
Usage.increment('for_offset_continue')
:continue
else
p.expression
end
when 'limit'
@limit = p.expression
end
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end end
@@ -208,13 +196,12 @@ module Liquid
case key case key
when 'offset' when 'offset'
@from = if expr == 'continue' @from = if expr == 'continue'
Usage.increment('for_offset_continue')
:continue :continue
else else
parse_expression(expr) Expression.parse(expr)
end end
when 'limit' when 'limit'
@limit = parse_expression(expr) @limit = Expression.parse(expr)
end end
end end

View File

@@ -31,17 +31,13 @@ module Liquid
def parse(tokens) def parse(tokens)
while parse_body(@blocks.last.attachment, tokens) while parse_body(@blocks.last.attachment, tokens)
end end
@blocks.each do |block| if blank?
block.attachment.remove_blank_strings if blank? @blocks.each { |condition| condition.attachment.remove_blank_strings }
block.attachment.freeze
end end
end end
ELSE_TAG_NAMES = ['elsif', 'else'].freeze
private_constant :ELSE_TAG_NAMES
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
if ELSE_TAG_NAMES.include?(tag) if ['elsif', 'else'].include?(tag)
push_block(tag, markup) push_block(tag, markup)
else else
super super
@@ -72,11 +68,7 @@ module Liquid
end end
def parse_expression(markup) def parse_expression(markup)
Condition.parse_expression(parse_context, markup) Condition.parse_expression(markup)
end
def strict_parse_expression(p)
Condition.strict_parse_expression(parse_context, p)
end end
def lax_parse(markup) def lax_parse(markup)
@@ -118,9 +110,9 @@ module Liquid
end end
def parse_comparison(p) def parse_comparison(p)
a = strict_parse_expression(p) a = parse_expression(p.expression)
if (op = p.consume?(:comparison)) if (op = p.consume?(:comparison))
b = strict_parse_expression(p) b = parse_expression(p.expression)
Condition.new(a, op, b) Condition.new(a, op, b)
else else
Condition.new(a) Condition.new(a)

View File

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

View File

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

View File

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

View File

@@ -153,7 +153,7 @@ module Liquid
c = args.shift c = args.shift
if @rethrow_errors if @rethrow_errors
c.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA c.exception_renderer = ->(_e) { raise }
end end
c c
@@ -191,6 +191,7 @@ module Liquid
begin begin
# render the nodelist. # render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it.
with_profiling(context) do with_profiling(context) do
@root.render_to_output_buffer(context, output || +'') @root.render_to_output_buffer(context, output || +'')
end end

View File

@@ -30,7 +30,7 @@ module Liquid
@parse_context = parse_context @parse_context = parse_context
@line_number = parse_context.line_number @line_number = parse_context.line_number
strict_parse_with_error_mode_fallback(markup) parse_with_selected_parser(markup)
end end
def raw def raw
@@ -63,23 +63,27 @@ module Liquid
@filters = [] @filters = []
p = Parser.new(markup) p = Parser.new(markup)
return if p.look(:end_of_string) @name = Expression.parse(p.expression)
@name = p.expression
while p.consume?(:pipe) while p.consume?(:pipe)
filtername = p.consume(:id) filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? p.arguments : [[]] filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
@filters << [filtername, *filterargs] @filters << parse_filter_expressions(filtername, filterargs)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end end
def render(context) def parse_filterargs(p)
obj = context.evaluate(@name) # first argument
filterargs = [p.argument]
# followed by comma separated others
filterargs << p.argument while p.consume?(:comma)
filterargs
end
@filters.each do |filter_name, filter_args, filter_kwargs| def render(context)
obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs) filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
obj = context.invoke(filter_name, obj, *filter_args) context.invoke(filter_name, output, *filter_args)
end end
context.apply_global_filter(obj) context.apply_global_filter(obj)

View File

@@ -7,66 +7,30 @@ module Liquid
attr_reader :name, :lookups attr_reader :name, :lookups
class << self def self.parse(markup)
def lax_parse(markup) new(markup)
lookups = markup.scan(VariableParser)
name = lookups.shift
if name =~ SQUARE_BRACKETED
name = Expression.parse(Regexp.last_match(1))
end
command_flags = 0
lookups.each_index do |i|
lookup = lookups[i]
if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse(Regexp.last_match(1))
elsif COMMAND_METHODS.include?(lookup)
command_flags |= 1 << i
end
end
new(name, lookups, command_flags)
end
def strict_parse(p)
if p.look(:id)
name = p.consume
else
p.consume(:open_square)
name = p.expression
p.consume(:close_square)
end
lookups = []
command_flags = 0
loop do
if p.consume?(:open_square)
lookups << p.expression
p.consume(:close_square)
elsif p.consume?(:dot)
lookup = p.consume(:id)
lookups << lookup
if COMMAND_METHODS.include?(lookup)
command_flags |= 1 << (lookups.length - 1)
end
else
break
end
end
new(name, lookups, command_flags)
end
private :new
end end
def initialize(name, lookups, command_flags) def initialize(markup)
lookups = markup.scan(VariableParser)
name = lookups.shift
if name =~ SQUARE_BRACKETED
name = Expression.parse(Regexp.last_match(1))
end
@name = name @name = name
@lookups = lookups
@command_flags = command_flags @lookups = lookups
@command_flags = 0
@lookups.each_index do |i|
lookup = lookups[i]
if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse(Regexp.last_match(1))
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i
end
end
end end
def evaluate(context) def evaluate(context)
@@ -111,19 +75,6 @@ module Liquid
self.class == other.class && state == other.state self.class == other.class && state == other.state
end end
def to_s
str = name.dup
lookups.each do |lookup|
str +=
if lookup.instance_of?(String)
"['#{lookup}']"
else
"[#{lookup}]"
end
end
str
end
protected protected
def state def state

View File

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

View File

@@ -55,24 +55,4 @@ class BlockTest < Minitest::Test
assert_equal buf.object_id, output.object_id assert_equal buf.object_id, output.object_id
end end
end end
def test_instrument_for_bug_1346
calls = []
Liquid::Usage.stub(:increment, ->(name) { calls << name }) do
Liquid::Template.parse("{% for i in (1..2) %}{{ i }}{% endfor {% foo %}")
end
assert_equal(["end_tag_params"], calls)
calls = []
Liquid::Usage.stub(:increment, ->(name) { calls << name }) do
Liquid::Template.parse("{% for i in (1..2) %}{{ i }}{% endfor test %}")
end
assert_equal(["end_tag_params"], calls)
calls = []
Liquid::Usage.stub(:increment, ->(name) { calls << name }) do
Liquid::Template.parse("{% for i in (1..2) %}{{ i }}{% endfor %}")
end
assert_equal([], calls)
end
end end

View File

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

View File

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

View File

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

View File

@@ -118,16 +118,6 @@ class ParsingQuirksTest < Minitest::Test
end end
end end
def test_blank_variable_markup
assert_template_result('', "{{}}")
end
def test_lookup_on_var_with_literal_name
assigns = { "blank" => { "x" => "result" } }
assert_template_result('result', "{{ blank.x }}", assigns)
assert_template_result('result', "{{ blank['x'] }}", assigns)
end
def test_contains_in_id def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true) assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
end end

View File

@@ -168,6 +168,8 @@ class StandardFiltersTest < Minitest::Test
def test_truncatewords def test_truncatewords
assert_equal('one two three', @filters.truncatewords('one two three', 4)) assert_equal('one two three', @filters.truncatewords('one two three', 4))
assert_equal('one two...', @filters.truncatewords('one two three', 2)) assert_equal('one two...', @filters.truncatewords('one two three', 2))
assert_equal('one two...', @filters.truncatewords('one two three', 2))
assert_equal('one two...', @filters.truncatewords(' one two three', 2))
assert_equal('one two three', @filters.truncatewords('one two three')) assert_equal('one two three', @filters.truncatewords('one two three'))
assert_equal( assert_equal(
'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...',
@@ -175,6 +177,8 @@ class StandardFiltersTest < Minitest::Test
) )
assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5)) assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5))
assert_equal('one two1', @filters.truncatewords("one two three", 2, 1)) assert_equal('one two1', @filters.truncatewords("one two three", 2, 1))
assert_equal('one1', @filters.truncatewords("one two three", 0, 1))
assert_equal('one1', @filters.truncatewords("one two three", -1, 1))
end end
def test_strip_html def test_strip_html

View File

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

View File

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

View File

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

View File

@@ -135,6 +135,38 @@ class TemplateTest < Minitest::Test
refute_nil(t.resource_limits.render_score) refute_nil(t.resource_limits.render_score)
end end
def test_resource_limits_assign_score
t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
t.resource_limits.assign_score_limit = 1
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
t.resource_limits.assign_score_limit = 2
assert_equal("", t.render!)
refute_nil(t.resource_limits.assign_score)
end
def test_resource_limits_assign_score_counts_bytes_not_characters
t = Template.parse("{% assign foo = 'すごい' %}")
t.render
assert_equal(9, t.resource_limits.assign_score)
t = Template.parse("{% capture foo %}すごい{% endcapture %}")
t.render
assert_equal(9, t.resource_limits.assign_score)
end
def test_resource_limits_assign_score_nested
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
t.resource_limits.assign_score_limit = 3
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
t.resource_limits.assign_score_limit = 5
assert_equal("", t.render!)
end
def test_resource_limits_aborts_rendering_after_first_error def test_resource_limits_aborts_rendering_after_first_error
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}") t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
t.resource_limits.render_score_limit = 50 t.resource_limits.render_score_limit = 50
@@ -257,8 +289,9 @@ class TemplateTest < Minitest::Test
end end
def test_nil_value_does_not_raise def test_nil_value_does_not_raise
t = Template.parse("some{{x}}thing", error_mode: :strict) Liquid::Template.error_mode = :strict
result = t.render!({ 'x' => nil }, strict_variables: true) t = Template.parse("some{{x}}thing")
result = t.render!({ 'x' => nil }, strict_variables: true)
assert_equal(0, t.errors.count) assert_equal(0, t.errors.count)
assert_equal('something', result) assert_equal('something', result)

View File

@@ -556,8 +556,4 @@ class TrimModeTest < Minitest::Test
template = Liquid::Template.parse("B\n {%- if true %}{% endif %}", bug_compatible_whitespace_trimming: true) template = Liquid::Template.parse("B\n {%- if true %}{% endif %}", bug_compatible_whitespace_trimming: true)
assert_equal("B", template.render) assert_equal("B", template.render)
end end
def test_trim_blank
assert_template_result('foobar', 'foo {{-}} bar')
end
end # TrimModeTest end # TrimModeTest

View File

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

View File

@@ -77,7 +77,7 @@ class ConditionUnitTest < Minitest::Test
def test_contains_works_on_arrays def test_contains_works_on_arrays
@context = Liquid::Context.new @context = Liquid::Context.new
@context['array'] = [1, 2, 3, 4, 5] @context['array'] = [1, 2, 3, 4, 5]
array_expr = parse_variable_lookup("array") array_expr = VariableLookup.new("array")
assert_evaluates_false(array_expr, 'contains', 0) assert_evaluates_false(array_expr, 'contains', 0)
assert_evaluates_true(array_expr, 'contains', 1) assert_evaluates_true(array_expr, 'contains', 1)
@@ -91,8 +91,8 @@ class ConditionUnitTest < Minitest::Test
def test_contains_returns_false_for_nil_operands def test_contains_returns_false_for_nil_operands
@context = Liquid::Context.new @context = Liquid::Context.new
assert_evaluates_false(parse_variable_lookup('not_assigned'), 'contains', '0') assert_evaluates_false(VariableLookup.new('not_assigned'), 'contains', '0')
assert_evaluates_false(0, 'contains', parse_variable_lookup('not_assigned')) assert_evaluates_false(0, 'contains', VariableLookup.new('not_assigned'))
end end
def test_contains_return_false_on_wrong_data_type def test_contains_return_false_on_wrong_data_type
@@ -145,20 +145,11 @@ class ConditionUnitTest < Minitest::Test
@context = Liquid::Context.new @context = Liquid::Context.new
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid" @context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
assert_evaluates_true(parse_variable_lookup("one"), '==', parse_variable_lookup("another")) assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another"))
end end
private private
def parse_variable_lookup(markup)
if Liquid::Template.error_mode == :strict
p = Liquid::Parser.new(markup)
VariableLookup.strict_parse(p)
else
VariableLookup.lax_parse(markup)
end
end
def assert_evaluates_true(left, op, right) def assert_evaluates_true(left, op, right)
assert(Condition.new(left, op, right).evaluate(@context), assert(Condition.new(left, op, right).evaluate(@context),
"Evaluated false: #{left} #{op} #{right}") "Evaluated false: #{left} #{op} #{right}")

View File

@@ -47,41 +47,32 @@ class ParserUnitTest < Minitest::Test
def test_expressions def test_expressions
p = Parser.new("hi.there hi?[5].there? hi.there.bob") p = Parser.new("hi.there hi?[5].there? hi.there.bob")
assert_equal(VariableLookup.send(:new, 'hi', ['there'], 0), p.expression) assert_equal('hi.there', p.expression)
assert_equal(VariableLookup.send(:new, 'hi?', [5, 'there?'], 0), p.expression) assert_equal('hi?[5].there?', p.expression)
assert_equal(VariableLookup.send(:new, 'hi', ['there', 'bob'], 0), p.expression) assert_equal('hi.there.bob', p.expression)
p = Parser.new("nil true false")
assert_nil(p.expression)
assert_equal(true, p.expression)
assert_equal(false, p.expression)
p = Parser.new("567 6.0 'lol' \"wut\"") p = Parser.new("567 6.0 'lol' \"wut\"")
assert_equal(567, p.expression) assert_equal('567', p.expression)
assert_equal(6.0, p.expression) assert_equal('6.0', p.expression)
assert_equal('lol', p.expression) assert_equal("'lol'", p.expression)
assert_equal('wut', p.expression) assert_equal('"wut"', p.expression)
end end
def test_ranges def test_ranges
p = Parser.new("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)") p = Parser.new("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)")
assert_equal(5..7, p.expression) assert_equal('(5..7)', p.expression)
assert_equal(1..9, p.expression) assert_equal('(1.5..9.6)', p.expression)
assert_equal( assert_equal('(young..old)', p.expression)
RangeLookup.new(VariableLookup.send(:new, 'young', [], 0), VariableLookup.send(:new, 'old', [], 0)), assert_equal('(hi[5].wat..old)', p.expression)
p.expression
)
assert_equal(
RangeLookup.new(VariableLookup.send(:new, 'hi', [5, "wat"], 0), VariableLookup.send(:new, 'old', [], 0)),
p.expression
)
end end
def test_arguments def test_arguments
p = Parser.new("filter: hi.there[5], keyarg: 7") p = Parser.new("filter: hi.there[5], keyarg: 7")
assert_equal('filter', p.consume(:id)) assert_equal('filter', p.consume(:id))
assert_equal(':', p.consume(:colon)) assert_equal(':', p.consume(:colon))
assert_equal([[VariableLookup.send(:new, "hi", ["there", 5], 0)], { "keyarg" => 7 }], p.arguments) assert_equal('hi.there[5]', p.argument)
assert_equal(',', p.consume(:comma))
assert_equal('keyarg: 7', p.argument)
end end
def test_invalid_expression def test_invalid_expression

View File

@@ -1,39 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class VariableLookupUnitTest < Minitest::Test
include Liquid
def test_variable_lookup_parsing
lookup = parse_variable_lookup('a.b.c')
assert_equal('a', lookup.name)
assert_equal(['b', 'c'], lookup.lookups)
lookup = parse_variable_lookup('a[b]')
assert_equal('a', lookup.name)
assert_equal([parse_variable_lookup('b')], lookup.lookups)
end
def test_to_s
lookup = parse_variable_lookup('a.b.c')
assert_equal("a['b']['c']", lookup.to_s)
lookup = parse_variable_lookup('a[b.c].d')
assert_equal("a[b['c']]['d']", lookup.to_s)
lookup = parse_variable_lookup('a["foo.bar"].d')
assert_equal("a['foo.bar']['d']", lookup.to_s)
end
private
def parse_variable_lookup(markup)
if Liquid::Template.error_mode == :strict
p = Liquid::Parser.new(markup)
VariableLookup.strict_parse(p)
else
VariableLookup.lax_parse(markup)
end
end
end

View File

@@ -7,20 +7,20 @@ class VariableUnitTest < Minitest::Test
def test_variable def test_variable
var = create_variable('hello') var = create_variable('hello')
assert_equal(parse_variable_lookup('hello'), var.name) assert_equal(VariableLookup.new('hello'), var.name)
end end
def test_filters def test_filters
var = create_variable('hello | textileze') var = create_variable('hello | textileze')
assert_equal(parse_variable_lookup('hello'), var.name) assert_equal(VariableLookup.new('hello'), var.name)
assert_equal([['textileze', []]], var.filters) assert_equal([['textileze', []]], var.filters)
var = create_variable('hello | textileze | paragraph') var = create_variable('hello | textileze | paragraph')
assert_equal(parse_variable_lookup('hello'), var.name) assert_equal(VariableLookup.new('hello'), var.name)
assert_equal([['textileze', []], ['paragraph', []]], var.filters) assert_equal([['textileze', []], ['paragraph', []]], var.filters)
var = create_variable(%( hello | strftime: '%Y')) var = create_variable(%( hello | strftime: '%Y'))
assert_equal(parse_variable_lookup('hello'), var.name) assert_equal(VariableLookup.new('hello'), var.name)
assert_equal([['strftime', ['%Y']]], var.filters) assert_equal([['strftime', ['%Y']]], var.filters)
var = create_variable(%( 'typo' | link_to: 'Typo', true )) var = create_variable(%( 'typo' | link_to: 'Typo', true ))
@@ -44,11 +44,11 @@ class VariableUnitTest < Minitest::Test
assert_equal([['repeat', [3, 3, 3]]], var.filters) assert_equal([['repeat', [3, 3, 3]]], var.filters)
var = create_variable(%( hello | strftime: '%Y, okay?')) var = create_variable(%( hello | strftime: '%Y, okay?'))
assert_equal(parse_variable_lookup('hello'), var.name) assert_equal(VariableLookup.new('hello'), var.name)
assert_equal([['strftime', ['%Y, okay?']]], var.filters) assert_equal([['strftime', ['%Y, okay?']]], var.filters)
var = create_variable(%( hello | things: "%Y, okay?", 'the other one')) var = create_variable(%( hello | things: "%Y, okay?", 'the other one'))
assert_equal(parse_variable_lookup('hello'), var.name) assert_equal(VariableLookup.new('hello'), var.name)
assert_equal([['things', ['%Y, okay?', 'the other one']]], var.filters) assert_equal([['things', ['%Y, okay?', 'the other one']]], var.filters)
end end
@@ -60,24 +60,22 @@ class VariableUnitTest < Minitest::Test
def test_filters_without_whitespace def test_filters_without_whitespace
var = create_variable('hello | textileze | paragraph') var = create_variable('hello | textileze | paragraph')
assert_equal(parse_variable_lookup('hello'), var.name) assert_equal(VariableLookup.new('hello'), var.name)
assert_equal([['textileze', []], ['paragraph', []]], var.filters) assert_equal([['textileze', []], ['paragraph', []]], var.filters)
var = create_variable('hello|textileze|paragraph') var = create_variable('hello|textileze|paragraph')
assert_equal(parse_variable_lookup('hello'), var.name) assert_equal(VariableLookup.new('hello'), var.name)
assert_equal([['textileze', []], ['paragraph', []]], var.filters) assert_equal([['textileze', []], ['paragraph', []]], var.filters)
var = create_variable("hello|replace:'foo','bar'|textileze") var = create_variable("hello|replace:'foo','bar'|textileze")
assert_equal(parse_variable_lookup('hello'), var.name) assert_equal(VariableLookup.new('hello'), var.name)
assert_equal([['replace', ['foo', 'bar']], ['textileze', []]], var.filters) assert_equal([['replace', ['foo', 'bar']], ['textileze', []]], var.filters)
end end
def test_symbol def test_symbol
with_error_mode(:lax) do var = create_variable("http://disney.com/logo.gif | image: 'med' ", error_mode: :lax)
var = create_variable("http://disney.com/logo.gif | image: 'med' ", error_mode: :lax) assert_equal(VariableLookup.new('http://disney.com/logo.gif'), var.name)
assert_equal(parse_variable_lookup('http://disney.com/logo.gif'), var.name) assert_equal([['image', ['med']]], var.filters)
assert_equal([['image', ['med']]], var.filters)
end
end end
def test_string_to_filter def test_string_to_filter
@@ -107,8 +105,8 @@ class VariableUnitTest < Minitest::Test
end end
def test_dashes def test_dashes
assert_equal(parse_variable_lookup('foo-bar'), create_variable('foo-bar').name) assert_equal(VariableLookup.new('foo-bar'), create_variable('foo-bar').name)
assert_equal(parse_variable_lookup('foo-bar-2'), create_variable('foo-bar-2').name) assert_equal(VariableLookup.new('foo-bar-2'), create_variable('foo-bar-2').name)
with_error_mode :strict do with_error_mode :strict do
assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') } assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') }
@@ -124,18 +122,18 @@ class VariableUnitTest < Minitest::Test
def test_string_dot def test_string_dot
var = create_variable(%( test.test )) var = create_variable(%( test.test ))
assert_equal(parse_variable_lookup('test.test'), var.name) assert_equal(VariableLookup.new('test.test'), var.name)
end end
def test_filter_with_keyword_arguments def test_filter_with_keyword_arguments
var = create_variable(%( hello | things: greeting: "world", farewell: 'goodbye')) var = create_variable(%( hello | things: greeting: "world", farewell: 'goodbye'))
assert_equal(parse_variable_lookup('hello'), var.name) assert_equal(VariableLookup.new('hello'), var.name)
assert_equal([['things', [], { 'greeting' => 'world', 'farewell' => 'goodbye' }]], var.filters) assert_equal([['things', [], { 'greeting' => 'world', 'farewell' => 'goodbye' }]], var.filters)
end end
def test_lax_filter_argument_parsing def test_lax_filter_argument_parsing
var = create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ), error_mode: :lax) var = create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ), error_mode: :lax)
assert_equal(parse_variable_lookup('number_of_comments'), var.name) assert_equal(VariableLookup.new('number_of_comments'), var.name)
assert_equal([['pluralize', ['comment', 'comments']]], var.filters) assert_equal([['pluralize', ['comment', 'comments']]], var.filters)
end end
@@ -152,17 +150,14 @@ class VariableUnitTest < Minitest::Test
assert_equal(" name_of_variable | upcase ", var.raw) assert_equal(" name_of_variable | upcase ", var.raw)
end end
private def test_variable_lookup_interface
lookup = VariableLookup.new('a.b.c')
def parse_variable_lookup(markup) assert_equal('a', lookup.name)
if Liquid::Template.error_mode == :strict assert_equal(['b', 'c'], lookup.lookups)
p = Liquid::Parser.new(markup)
VariableLookup.strict_parse(p)
else
VariableLookup.lax_parse(markup)
end
end end
private
def create_variable(markup, options = {}) def create_variable(markup, options = {})
Variable.new(markup, ParseContext.new(options)) Variable.new(markup, ParseContext.new(options))
end end