Compare commits

..

3 Commits

Author SHA1 Message Date
Dylan Thacker-Smith
d5c31861f9 Fix theme runner for benchmarks 2020-08-18 10:04:42 -04:00
Dylan Thacker-Smith
895e63e40a Remove now unnecessary squash_instance_assigns_with_environments
Since it was just there to make sure instance assigns were overriden
when rending more than once with the same template instance.  It is
no longer necessary because the template no longer stores instance_assigns
which need to be overriden.
2020-08-18 10:04:42 -04:00
Dylan Thacker-Smith
219168e89f Remove mutable render state from Liquid::Template
Use Liquid::Context to access this mutable state after rendering or to have
state that is persisted between mutations
2020-08-18 10:04:41 -04:00
35 changed files with 477 additions and 649 deletions

View File

@@ -80,22 +80,24 @@ It is also recommended that you use it in the template editors of existing apps
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance.
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in an `errors` array on the `Liquid::Context` instance used for rendering.
Here are some examples:
```ruby
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
context = Liquid::Context.new({ 'x' => 1, 'z' => { 'a' => 2 } })
template.render(context, { strict_variables: true })
#=> '1 2 ' # when a variable is undefined, it's rendered as nil
template.errors
context.errors
#=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
```
```ruby
template = Liquid::Template.parse("{{x | filter1 | upcase}}")
template.render({ 'x' => 'foo' }, { strict_filters: true })
context = Liquid::Context.new({ 'x' => 'foo' })
template.render(context, { strict_filters: true })
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
template.errors
context.errors
#=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
```
@@ -111,4 +113,4 @@ template.render!({ 'x' => 1}, { strict_variables: true })
To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.
Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.

View File

@@ -63,8 +63,6 @@ require 'liquid/expression'
require 'liquid/context'
require 'liquid/parser_switching'
require 'liquid/tag'
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
require 'liquid/block'
require 'liquid/block_body'
require 'liquid/document'
@@ -88,3 +86,4 @@ require 'liquid/template_factory'
# Load all the tags of the standard library
#
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
Dir["#{__dir__}/liquid/registers/*.rb"].each { |f| require f }

View File

@@ -10,7 +10,7 @@ module Liquid
end
def parse(tokens)
@body = new_body
@body = BlockBody.new
while parse_body(@body, tokens)
end
end
@@ -28,12 +28,7 @@ module Liquid
@body.nodelist
end
def unknown_tag(tag_name, _markup, _tokenizer)
Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context)
end
# @api private
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
def unknown_tag(tag, _params, _tokens)
if tag == 'else'
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
block_name: block_name)
@@ -55,14 +50,8 @@ module Liquid
@block_delimiter ||= "end#{block_name}"
end
private
protected
# @api public
def new_body
BlockBody.new
end
# @api public
def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep"

View File

@@ -54,49 +54,28 @@ module Liquid
end
# @api private
def self.unknown_tag_in_liquid_tag(tag, parse_context)
Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
def self.unknown_tag_in_liquid_tag(end_tag_name, end_tag_markup)
yield end_tag_name, end_tag_markup
ensure
Usage.increment("liquid_tag_contains_outer_tag") unless $ERROR_INFO.is_a?(SyntaxError)
end
# @api private
def self.raise_missing_tag_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
end
# @api private
def self.raise_missing_variable_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
end
# @api private
def self.render_node(context, output, node)
node.render_to_output_buffer(context, output)
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
rescue MemoryError
raise
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
private def parse_liquid_tag(markup, parse_context)
private def parse_liquid_tag(markup, parse_context, &block)
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
if end_tag_name
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
end
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, end_tag_markup|
next unless end_tag_name
self.class.unknown_tag_in_liquid_tag(end_tag_name, end_tag_markup, &block)
end
end
private def parse_for_document(tokenizer, parse_context)
private def parse_for_document(tokenizer, parse_context, &block)
while (token = tokenizer.shift)
next if token.empty?
case
when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context)
unless token =~ FullToken
BlockBody.raise_missing_tag_terminator(token, parse_context)
raise_missing_tag_terminator(token, parse_context)
end
tag_name = Regexp.last_match(2)
markup = Regexp.last_match(4)
@@ -108,7 +87,7 @@ module Liquid
end
if tag_name == 'liquid'
parse_liquid_tag(markup, parse_context)
parse_liquid_tag(markup, parse_context, &block)
next
end
@@ -142,11 +121,7 @@ module Liquid
if token[2] == WhitespaceControl
previous_token = @nodelist.last
if previous_token.is_a?(String)
first_byte = previous_token.getbyte(0)
previous_token.rstrip!
if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
previous_token << first_byte
end
end
end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@@ -156,47 +131,38 @@ module Liquid
@blank
end
# Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
# with a blank body.
#
# For example, in a conditional assignment like the following
#
# ```
# {% if size > max_size %}
# {% assign size = max_size %}
# {% endif %}
# ```
#
# we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
# will remove them to reduce the render output size.
#
# Note that it is now preferred to use the `liquid` tag for this use case.
def remove_blank_strings
raise "remove_blank_strings only support being called on a blank block body" unless @blank
@nodelist.reject! { |node| node.instance_of?(String) }
end
def render(context)
render_to_output_buffer(context, +'')
end
def render_to_output_buffer(context, output)
context.resource_limits.increment_render_score(@nodelist.length)
context.resource_limits.render_score += @nodelist.length
idx = 0
while (node = @nodelist[idx])
if node.instance_of?(String)
previous_output_size = output.bytesize
case node
when String
output << node
else
when Variable
render_node(context, output, node)
when Block
render_node(context, node.blank? ? +'' : output, node)
break if context.interrupt? # might have happened in a for-block
when Continue, Break
# If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}. These tags may also occur through Block or Include tags.
break if context.interrupt? # might have happened in a for-block
# or {% continue %}
context.push_interrupt(node.interrupt)
break
else # Other non-Block tags
render_node(context, output, node)
break if context.interrupt? # might have happened through an include
end
idx += 1
context.resource_limits.increment_write_score(output)
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
end
output
@@ -205,7 +171,29 @@ module Liquid
private
def render_node(context, output, node)
BlockBody.render_node(context, output, node)
if node.disabled?(context)
output << node.disabled_error_message
return
end
disable_tags(context, node.disabled_tags) do
node.render_to_output_buffer(context, output)
end
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
def disable_tags(context, tags, &block)
return yield if tags.empty?
context.registers[:disabled_tags].disable(tags, &block)
end
def raise_if_resource_limits_reached(context, length)
context.resource_limits.render_length += length
return unless context.resource_limits.reached?
raise MemoryError, "Memory limits exceeded"
end
def create_variable(token, parse_context)
@@ -213,17 +201,15 @@ module Liquid
markup = content.first
return Variable.new(markup, parse_context)
end
BlockBody.raise_missing_variable_terminator(token, parse_context)
raise_missing_variable_terminator(token, parse_context)
end
# @deprecated Use {.raise_missing_tag_terminator} instead
def raise_missing_tag_terminator(token, parse_context)
BlockBody.raise_missing_tag_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
end
# @deprecated Use {.raise_missing_variable_terminator} instead
def raise_missing_variable_terminator(token, parse_context)
BlockBody.raise_missing_variable_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
end
def registered_tags

View File

@@ -34,7 +34,6 @@ module Liquid
@strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@base_scope_depth = 0
squash_instance_assigns_with_environments
self.exception_renderer = Template.default_exception_renderer
if rethrow_errors
@@ -44,7 +43,6 @@ module Liquid
@interrupts = []
@filters = []
@global_filter = nil
@disabled_tags = {}
end
# rubocop:enable Metrics/ParameterLists
@@ -145,7 +143,6 @@ module Liquid
subcontext.strainer = nil
subcontext.errors = errors
subcontext.warnings = warnings
subcontext.disabled_tags = @disabled_tags
end
end
@@ -210,24 +207,9 @@ module Liquid
end
end
def with_disabled_tags(tag_names)
tag_names.each do |name|
@disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
end
yield
ensure
tag_names.each do |name|
@disabled_tags[name] -= 1
end
end
def tag_disabled?(tag_name)
@disabled_tags.fetch(tag_name, 0) > 0
end
protected
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters
private
@@ -263,16 +245,5 @@ module Liquid
rescue Liquid::InternalError => exc
exc
end
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@environments.each do |env|
if env.key?(k)
scopes.last[k] = lookup_and_evaluate(env, k)
break
end
end
end
end # squash_instance_assigns_with_environments
end # Context
end # Liquid

View File

@@ -1,33 +1,23 @@
# frozen_string_literal: true
module Liquid
class Document
class Document < BlockBody
def self.parse(tokens, parse_context)
doc = new(parse_context)
doc = new
doc.parse(tokens, parse_context)
doc
end
attr_reader :parse_context, :body
def initialize(parse_context)
@parse_context = parse_context
@body = new_body
end
def nodelist
@body.nodelist
end
def parse(tokenizer, parse_context)
while parse_body(tokenizer)
def parse(tokens, parse_context)
super do |end_tag_name, _end_tag_params|
unknown_tag(end_tag_name, parse_context) if end_tag_name
end
rescue SyntaxError => e
e.line_number ||= parse_context.line_number
raise
end
def unknown_tag(tag, _markup, _tokenizer)
def unknown_tag(tag, parse_context)
case tag
when 'else', 'end'
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
@@ -35,30 +25,5 @@ module Liquid
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end
end
def render_to_output_buffer(context, output)
@body.render_to_output_buffer(context, output)
end
def render(context)
@body.render(context)
end
private
def new_body
Liquid::BlockBody.new
end
def parse_body(tokenizer)
@body.parse(tokenizer, parse_context) do |unknown_tag_name, unknown_tag_markup|
if unknown_tag_name
unknown_tag(unknown_tag_name, unknown_tag_markup, tokenizer)
true
else
false
end
end
end
end
end

View File

@@ -53,6 +53,5 @@ module Liquid
UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error)
DisabledError = Class.new(Error)
InternalError = Class.new(Error)
end

View File

@@ -1,21 +1,25 @@
# frozen_string_literal: true
module Liquid
module BlockBodyProfilingHook
def render_node(context, output, node)
class BlockBody
def render_node_with_profiling(context, output, node)
Profiler.profile_node_render(node) do
super
render_node_without_profiling(context, output, node)
end
end
end
BlockBody.prepend(BlockBodyProfilingHook)
module IncludeProfilingHook
def render_to_output_buffer(context, output)
alias_method :render_node_without_profiling, :render_node
alias_method :render_node, :render_node_with_profiling
end
class Include < Tag
def render_to_output_buffer_with_profiling(context, output)
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
super
render_to_output_buffer_without_profiling(context, output)
end
end
alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer
alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling
end
Include.prepend(IncludeProfilingHook)
end

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
module Liquid
class DisabledTags < Register
def initialize
@disabled_tags = {}
end
def disabled?(tag)
@disabled_tags.key?(tag) && @disabled_tags[tag] > 0
end
def disable(tags)
tags.each(&method(:increment))
yield
ensure
tags.each(&method(:decrement))
end
private
def increment(tag)
@disabled_tags[tag] ||= 0
@disabled_tags[tag] += 1
end
def decrement(tag)
@disabled_tags[tag] -= 1
end
end
Template.add_register(:disabled_tags, DisabledTags.new)
end

View File

@@ -2,8 +2,8 @@
module Liquid
class ResourceLimits
attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
attr_reader :render_score, :assign_score
attr_accessor :render_length, :render_score, :assign_score,
:render_length_limit, :render_score_limit, :assign_score_limit
def initialize(limits)
@render_length_limit = limits[:render_length_limit]
@@ -12,51 +12,14 @@ module Liquid
reset
end
def increment_render_score(amount)
@render_score += amount
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
end
def increment_assign_score(amount)
@assign_score += amount
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
end
# update either render_length or assign_score based on whether or not the writes are captured
def increment_write_score(output)
if (last_captured = @last_capture_length)
captured = output.bytesize
increment = captured - last_captured
@last_capture_length = captured
increment_assign_score(increment)
elsif @render_length_limit && output.bytesize > @render_length_limit
raise_limits_reached
end
end
def raise_limits_reached
@reached_limit = true
raise MemoryError, "Memory limits exceeded"
end
def reached?
@reached_limit
(@render_length_limit && @render_length > @render_length_limit) ||
(@render_score_limit && @render_score > @render_score_limit) ||
(@assign_score_limit && @assign_score > @assign_score_limit)
end
def reset
@reached_limit = false
@last_capture_length = nil
@render_score = @assign_score = 0
end
def with_capture
old_capture_length = @last_capture_length
begin
@last_capture_length = 0
yield
ensure
@last_capture_length = old_capture_length
end
@render_length = @render_score = @assign_score = 0
end
end
end

View File

@@ -13,13 +13,15 @@ module Liquid
tag
end
def disable_tags(*tag_names)
@disabled_tags ||= []
@disabled_tags.concat(tag_names)
prepend(Disabler)
def disable_tags(*tags)
disabled_tags.push(*tags)
end
private :new
def disabled_tags
@disabled_tags ||= []
end
end
def initialize(tag_name, markup, parse_context)
@@ -44,6 +46,14 @@ module Liquid
''
end
def disabled?(context)
context.registers[:disabled_tags].disabled?(tag_name)
end
def disabled_error_message
"#{tag_name} #{options[:locale].t('errors.disabled.tag')}"
end
# For backwards compatibility with custom tags. In a future release, the semantics
# of the `render_to_output_buffer` method will become the default and the `render`
# method will be removed.
@@ -55,5 +65,9 @@ module Liquid
def blank?
false
end
def disabled_tags
self.class.disabled_tags
end
end
end

View File

@@ -1,22 +0,0 @@
# frozen_string_literal: true
module Liquid
class Tag
module Disableable
def render_to_output_buffer(context, output)
if context.tag_disabled?(tag_name)
output << disabled_error(context)
return
end
super
end
def disabled_error(context)
# raise then rescue the exception so that the Context#exception_renderer can re-raise it
raise DisabledError, "#{tag_name} #{parse_context[:locale].t('errors.disabled.tag')}"
rescue DisabledError => exc
context.handle_error(exc, line_number)
end
end
end
end

View File

@@ -1,21 +0,0 @@
# frozen_string_literal: true
module Liquid
class Tag
module Disabler
module ClassMethods
attr_reader :disabled_tags
end
def self.prepended(base)
base.extend(ClassMethods)
end
def render_to_output_buffer(context, output)
context.with_disabled_tags(self.class.disabled_tags) do
super
end
end
end
end
end

View File

@@ -27,7 +27,7 @@ module Liquid
def render_to_output_buffer(context, output)
val = @from.render(context)
context.scopes.last[@to] = val
context.resource_limits.increment_assign_score(assign_score_of(val))
context.resource_limits.assign_score += assign_score_of(val)
output
end

View File

@@ -11,11 +11,8 @@ module Liquid
# {% endfor %}
#
class Break < Tag
INTERRUPT = BreakInterrupt.new.freeze
def render_to_output_buffer(context, output)
context.push_interrupt(INTERRUPT)
output
def interrupt
BreakInterrupt.new
end
end

View File

@@ -25,10 +25,10 @@ module Liquid
end
def render_to_output_buffer(context, output)
context.resource_limits.with_capture do
capture_output = render(context)
context.scopes.last[@to] = capture_output
end
previous_output_size = output.bytesize
super
context.scopes.last[@to] = output
context.resource_limits.assign_score += (output.bytesize - previous_output_size)
output
end

View File

@@ -19,11 +19,8 @@ module Liquid
end
def parse(tokens)
body = new_body
body = BlockBody.new
body = @blocks.last.attachment while parse_body(body, tokens)
if blank?
@blocks.each { |condition| condition.attachment.remove_blank_strings }
end
end
def nodelist
@@ -59,7 +56,7 @@ module Liquid
private
def record_when_condition(markup)
body = new_body
body = BlockBody.new
while markup
unless markup =~ WhenSyntax
@@ -80,7 +77,7 @@ module Liquid
end
block = ElseCondition.new
block.attach(new_body)
block.attach(BlockBody.new)
@blocks << block
end

View File

@@ -11,11 +11,8 @@ module Liquid
# {% endfor %}
#
class Continue < Tag
INTERRUPT = ContinueInterrupt.new.freeze
def render_to_output_buffer(context, output)
context.push_interrupt(INTERRUPT)
output
def interrupt
ContinueInterrupt.new
end
end

View File

@@ -54,18 +54,13 @@ module Liquid
super
@from = @limit = nil
parse_with_selected_parser(markup)
@for_block = new_body
@for_block = BlockBody.new
@else_block = nil
end
def parse(tokens)
if parse_body(@for_block, tokens)
parse_body(@else_block, tokens)
end
if blank?
@for_block.remove_blank_strings
@else_block&.remove_blank_strings
end
return unless parse_body(@for_block, tokens)
parse_body(@else_block, tokens)
end
def nodelist
@@ -74,7 +69,7 @@ module Liquid
def unknown_tag(tag, markup, tokens)
return super unless tag == 'else'
@else_block = new_body
@else_block = BlockBody.new
end
def render_to_output_buffer(context, output)

View File

@@ -31,9 +31,6 @@ module Liquid
def parse(tokens)
while parse_body(@blocks.last.attachment, tokens)
end
if blank?
@blocks.each { |condition| condition.attachment.remove_blank_strings }
end
end
def unknown_tag(tag, markup, tokens)
@@ -64,7 +61,7 @@ module Liquid
end
@blocks.push(block)
block.attach(new_body)
block.attach(BlockBody.new)
end
def lax_parse(markup)

View File

@@ -16,8 +16,6 @@ module Liquid
# {% include 'product' for products %}
#
class Include < Tag
prepend Tag::Disableable
SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
Syntax = SYNTAX

View File

@@ -16,7 +16,7 @@ module Liquid
#
class Template
attr_accessor :root
attr_reader :resource_limits, :warnings
attr_reader :warnings
class TagRegistry
include Enumerable
@@ -80,6 +80,14 @@ module Liquid
tags[name.to_s] = klass
end
attr_accessor :registers
Template.registers = {}
private :registers=
def add_register(name, klass)
registers[name.to_sym] = klass
end
# Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library
def register_filter(mod)
@@ -98,11 +106,6 @@ module Liquid
end
end
def initialize
@rethrow_errors = false
@resource_limits = ResourceLimits.new(Template.default_resource_limits)
end
# Parse source code.
# Returns self for easy chaining
def parse(source, options = {})
@@ -115,22 +118,6 @@ module Liquid
self
end
def registers
@registers ||= {}
end
def assigns
@assigns ||= {}
end
def instance_assigns
@instance_assigns ||= {}
end
def errors
@errors ||= []
end
# Render takes a hash with local variables.
#
# if you use the same filters over and over again consider registering them globally
@@ -145,37 +132,18 @@ module Liquid
# * <tt>registers</tt> : hash with register variables. Those can be accessed from
# filters and tags and might be useful to integrate liquid more with its host application
#
def render(*args)
def render(assigns_or_context = nil, options = nil)
return '' if @root.nil?
context = case args.first
when Liquid::Context
c = args.shift
if @rethrow_errors
c.exception_renderer = ->(_e) { raise }
end
c
when Liquid::Drop
drop = args.shift
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
when Hash
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
when nil
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
else
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end
context = coerce_context(assigns_or_context)
output = nil
context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
case args.last
case options
when Hash
options = args.pop
output = options[:output] if options[:output]
output = options[:output] if options[:output]
options[:registers]&.each do |key, register|
context_register[key] = register
@@ -183,7 +151,11 @@ module Liquid
apply_options_to_context(context, options)
when Module, Array
context.add_filters(args.pop)
context.add_filters(options)
end
Template.registers.each do |key, register|
context_register[key] = register unless context_register.key?(key)
end
# Retrying a render resets resource usage
@@ -197,14 +169,15 @@ module Liquid
end
rescue Liquid::MemoryError => e
context.handle_error(e)
ensure
@errors = context.errors
end
end
def render!(*args)
@rethrow_errors = true
render(*args)
def render!(assigns_or_context = nil, options = nil)
context = coerce_context(assigns_or_context)
# rethrow errors
context.exception_renderer = ->(_e) { raise }
render(context, options)
end
def render_to_output_buffer(context, output)
@@ -213,6 +186,22 @@ module Liquid
private
def coerce_context(assigns_or_context)
case assigns_or_context
when Liquid::Context
assigns_or_context
when Liquid::Drop
drop = assigns_or_context
drop.context = Context.build(environments: [drop])
when Hash
Context.build(environments: [assigns_or_context])
when nil
Context.build
else
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end
end
def tokenize(source)
Tokenizer.new(source, @line_numbers)
end

View File

@@ -73,26 +73,29 @@ class ThemeRunner
private
def compile_and_render(template, layout, assigns, page_template, template_file)
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
def compile_and_render(template, layout, assigns, page_template)
assigns = assigns.merge(
'page_title' => 'Page title',
'template' => page_template,
)
compiled_test = compile_test(template, layout, assigns)
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
compiled_test[:layout].render!(assigns) if layout
end
def compile_all_tests
@compiled_tests = []
each_test do |liquid, layout, assigns, page_template, template_name|
@compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name)
each_test do |liquid, layout, assigns, _page_template, _template_name|
@compiled_tests << compile_test(liquid, layout, assigns)
end
@compiled_tests
end
def compile_test(template, layout, assigns, page_template, template_file)
tmpl = init_template(page_template, template_file)
parsed_template = tmpl.parse(template).dup
def compile_test(template, layout, assigns)
parsed_template = Liquid::Template.parse(template).dup
if layout
parsed_layout = tmpl.parse(layout)
parsed_layout = Liquid::Template.parse(layout)
{ tmpl: parsed_template, assigns: assigns, layout: parsed_layout }
else
{ tmpl: parsed_template, assigns: assigns }
@@ -107,16 +110,7 @@ class ThemeRunner
@tests.each do |test_hash|
# Compute page_template outside of profiler run, uninteresting to profiler
page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name]))
yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name])
yield(test_hash[:liquid], test_hash[:layout], assigns, page_template)
end
end
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
def init_template(page_template, template_file)
tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
tmpl
end
end

View File

@@ -40,26 +40,29 @@ class ErrorHandlingTest < Minitest::Test
def test_standard_error
template = Liquid::Template.parse(' {{ errors.standard_error }} ')
assert_equal(' Liquid error: standard error ', template.render('errors' => ErrorDrop.new))
context = Liquid::Context.new('errors' => ErrorDrop.new)
assert_equal(' Liquid error: standard error ', template.render(context))
assert_equal(1, template.errors.size)
assert_equal(StandardError, template.errors.first.class)
assert_equal(1, context.errors.size)
assert_equal(StandardError, context.errors.first.class)
end
def test_syntax
template = Liquid::Template.parse(' {{ errors.syntax_error }} ')
assert_equal(' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new))
context = Liquid::Context.new('errors' => ErrorDrop.new)
assert_equal(' Liquid syntax error: syntax error ', template.render(context))
assert_equal(1, template.errors.size)
assert_equal(SyntaxError, template.errors.first.class)
assert_equal(1, context.errors.size)
assert_equal(SyntaxError, context.errors.first.class)
end
def test_argument
template = Liquid::Template.parse(' {{ errors.argument_error }} ')
assert_equal(' Liquid error: argument error ', template.render('errors' => ErrorDrop.new))
context = Liquid::Context.new('errors' => ErrorDrop.new)
assert_equal(' Liquid error: argument error ', template.render(context))
assert_equal(1, template.errors.size)
assert_equal(ArgumentError, template.errors.first.class)
assert_equal(1, context.errors.size)
assert_equal(ArgumentError, context.errors.first.class)
end
def test_missing_endtag_parse_time_error
@@ -78,9 +81,10 @@ class ErrorHandlingTest < Minitest::Test
def test_lax_unrecognized_operator
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
assert_equal(' Liquid error: Unknown operator =! ', template.render)
assert_equal(1, template.errors.size)
assert_equal(Liquid::ArgumentError, template.errors.first.class)
context = Liquid::Context.new('errors' => ErrorDrop.new)
assert_equal(' Liquid error: Unknown operator =! ', template.render(context))
assert_equal(1, context.errors.size)
assert_equal(Liquid::ArgumentError, context.errors.first.class)
end
def test_with_line_numbers_adds_numbers_to_parser_errors
@@ -202,10 +206,11 @@ class ErrorHandlingTest < Minitest::Test
def test_default_exception_renderer_with_internal_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
output = template.render('errors' => ErrorDrop.new)
context = Liquid::Context.new('errors' => ErrorDrop.new)
output = template.render(context)
assert_equal('This is a runtime error: Liquid error (line 1): internal', output)
assert_equal([Liquid::InternalError], template.errors.map(&:class))
assert_equal([Liquid::InternalError], context.errors.map(&:class))
end
def test_setting_default_exception_renderer
@@ -217,10 +222,11 @@ class ErrorHandlingTest < Minitest::Test
}
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
output = template.render('errors' => ErrorDrop.new)
context = Liquid::Context.new('errors' => ErrorDrop.new)
output = template.render(context)
assert_equal('This is a runtime error: ', output)
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
assert_equal([Liquid::ArgumentError], context.errors.map(&:class))
ensure
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
end
@@ -233,11 +239,12 @@ class ErrorHandlingTest < Minitest::Test
e.cause
}
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
context = Liquid::Context.new('errors' => ErrorDrop.new)
output = template.render(context, exception_renderer: handler)
assert_equal('This is a runtime error: runtime error', output)
assert_equal([Liquid::InternalError], exceptions.map(&:class))
assert_equal(exceptions, template.errors)
assert_equal(exceptions, context.errors)
assert_equal('#<RuntimeError: runtime error>', exceptions.first.cause.inspect)
end
@@ -250,15 +257,16 @@ class ErrorHandlingTest < Minitest::Test
def test_included_template_name_with_line_numbers
old_file_system = Liquid::Template.file_system
context = Liquid::Context.new('errors' => ErrorDrop.new)
begin
Liquid::Template.file_system = TestFileSystem.new
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
page = template.render('errors' => ErrorDrop.new)
page = template.render(context)
ensure
Liquid::Template.file_system = old_file_system
end
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
assert_equal("product", template.errors.first.template_name)
assert_equal("product", context.errors.first.template_name)
end
end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'test_helper'
class DisabledTagsTest < Minitest::Test
include Liquid
class DisableRaw < Block
disable_tags "raw"
end
class DisableRawEcho < Block
disable_tags "raw", "echo"
end
def test_disables_raw
with_custom_tag('disable', DisableRaw) do
assert_template_result 'raw usage is not allowed in this contextfoo', '{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}'
end
end
def test_disables_echo_and_raw
with_custom_tag('disable', DisableRawEcho) do
assert_template_result 'raw usage is not allowed in this contextecho usage is not allowed in this context', '{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}'
end
end
end

View File

@@ -1,51 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class TagDisableableTest < Minitest::Test
include Liquid
class DisableRaw < Block
disable_tags "raw"
end
class DisableRawEcho < Block
disable_tags "raw", "echo"
end
class DisableableRaw < Liquid::Raw
prepend Liquid::Tag::Disableable
end
class DisableableEcho < Liquid::Echo
prepend Liquid::Tag::Disableable
end
def test_disables_raw
with_disableable_tags do
with_custom_tag('disable', DisableRaw) do
output = Template.parse('{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}').render
assert_equal('Liquid error: raw usage is not allowed in this contextfoo', output)
end
end
end
def test_disables_echo_and_raw
with_disableable_tags do
with_custom_tag('disable', DisableRawEcho) do
output = Template.parse('{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}').render
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
private
def with_disableable_tags
with_custom_tag('raw', DisableableRaw) do
with_custom_tag('echo', DisableableEcho) do
yield
end
end
end
end

View File

@@ -217,8 +217,9 @@ class IncludeTagTest < Minitest::Test
Liquid::Template.file_system = TestFileSystem.new
a = Liquid::Template.parse(' {% include "nested_template" %}')
a.render!
assert_empty(a.errors)
context = Liquid::Context.new
a.render!(context)
assert_empty(context.errors)
end
def test_passing_options_to_included_templates
@@ -257,9 +258,10 @@ class IncludeTagTest < Minitest::Test
def test_including_with_strict_variables
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
template.render(nil, strict_variables: true)
context = Liquid::Context.new
template.render(context, strict_variables: true)
assert_equal([], template.errors)
assert_equal([], context.errors)
end
def test_break_through_include

View File

@@ -82,13 +82,15 @@ class LiquidTagTest < Minitest::Test
end
def test_nested_liquid_tag
assert_template_result('good', <<~LIQUID)
{%- if true %}
{%- liquid
echo "good"
%}
{%- endif -%}
LIQUID
assert_usage_increment("liquid_tag_contains_outer_tag", times: 0) do
assert_template_result('good', <<~LIQUID)
{%- if true %}
{%- liquid
echo "good"
%}
{%- endif -%}
LIQUID
end
end
def test_cannot_open_blocks_living_past_a_liquid_tag
@@ -100,12 +102,14 @@ class LiquidTagTest < Minitest::Test
LIQUID
end
def test_cannot_close_blocks_created_before_a_liquid_tag
assert_match_syntax_error("syntax error (line 3): 'endif' is not a valid delimiter for liquid tags. use %}", <<~LIQUID)
{%- if true -%}
42
{%- liquid endif -%}
LIQUID
def test_quirk_can_close_blocks_created_before_a_liquid_tag
assert_usage_increment("liquid_tag_contains_outer_tag") do
assert_template_result("42", <<~LIQUID)
{%- if true -%}
42
{%- liquid endif -%}
LIQUID
end
end
def test_liquid_tag_in_raw

View File

@@ -127,10 +127,7 @@ class RenderTagTest < Minitest::Test
'test_include' => '{% include "foo" %}'
)
exc = assert_raises(Liquid::DisabledError) do
Liquid::Template.parse('{% render "test_include" %}').render!
end
assert_equal('Liquid error: include usage is not allowed in this context', exc.message)
assert_template_result('include usage is not allowed in this context', '{% render "test_include" %}')
end
def test_includes_will_not_render_inside_nested_sibling_tags
@@ -140,8 +137,7 @@ class RenderTagTest < Minitest::Test
'test_include' => '{% include "foo" %}'
)
output = Liquid::Template.parse('{% render "nested_render_with_sibling_include" %}').render
assert_equal('Liquid error: include usage is not allowed in this contextLiquid error: include usage is not allowed in this context', output)
assert_template_result('include usage is not allowed in this contextinclude usage is not allowed in this context', '{% render "nested_render_with_sibling_include" %}')
end
def test_render_tag_with

View File

@@ -38,12 +38,6 @@ end
class TemplateTest < Minitest::Test
include Liquid
def test_instance_assigns_persist_on_same_template_object_between_parses
t = Template.new
assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
assert_equal('from instance assigns', t.parse("{{ foo }}").render!)
end
def test_warnings_is_not_exponential_time
str = "false"
100.times do
@@ -54,43 +48,15 @@ class TemplateTest < Minitest::Test
assert_equal([], Timeout.timeout(1) { t.warnings })
end
def test_instance_assigns_persist_on_same_template_parsing_between_renders
t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}")
assert_equal('foo', t.render!)
assert_equal('foofoo', t.render!)
end
def test_custom_assigns_do_not_persist_on_same_template
t = Template.new
assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns'))
assert_equal('', t.parse("{{ foo }}").render!)
end
def test_custom_assigns_squash_instance_assigns
t = Template.new
assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns'))
end
def test_persistent_assigns_squash_instance_assigns
t = Template.new
assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
t.assigns['foo'] = 'from persistent assigns'
assert_equal('from persistent assigns', t.parse("{{ foo }}").render!)
end
def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders
t = Template.new
t.assigns['number'] = -> {
@global ||= 0
@global += 1
}
assert_equal('1', t.parse("{{number}}").render!)
assert_equal('1', t.parse("{{number}}").render!)
assert_equal('1', t.render!)
@global = nil
end
def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders
t = Template.new
assigns = { 'number' => -> {
@@ -105,125 +71,143 @@ class TemplateTest < Minitest::Test
def test_resource_limits_works_with_custom_length_method
t = Template.parse("{% assign foo = bar %}")
t.resource_limits.render_length_limit = 42
assert_equal("", t.render!("bar" => SomethingWithLength.new))
context = Liquid::Context.new("bar" => SomethingWithLength.new)
context.resource_limits.render_length_limit = 42
assert_equal("", t.render!(context))
end
def test_resource_limits_render_length
t = Template.parse("0123456789")
t.resource_limits.render_length_limit = 9
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
context = Liquid::Context.new
context.resource_limits.render_length_limit = 5
assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert(context.resource_limits.reached?)
t.resource_limits.render_length_limit = 10
context.resource_limits.render_length_limit = 10
assert_equal("0123456789", t.render!)
refute_nil(context.resource_limits.render_length)
end
def test_resource_limits_render_score
t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}")
t.resource_limits.render_score_limit = 50
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
context = Liquid::Context.new
context.resource_limits.render_score_limit = 50
assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert(context.resource_limits.reached?)
t = Template.parse("{% for a in (1..100) %} foo {% endfor %}")
t.resource_limits.render_score_limit = 50
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
context.resource_limits.render_score_limit = 50
assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert(context.resource_limits.reached?)
t.resource_limits.render_score_limit = 200
assert_equal((" foo " * 100), t.render!)
refute_nil(t.resource_limits.render_score)
context.resource_limits.render_score_limit = 200
assert_equal((" foo " * 100), t.render!(context))
refute_nil(context.resource_limits.render_score)
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?)
context = Liquid::Context.new
context.resource_limits.assign_score_limit = 1
assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert(context.resource_limits.reached?)
t.resource_limits.assign_score_limit = 2
assert_equal("", t.render!)
refute_nil(t.resource_limits.assign_score)
context.resource_limits.assign_score_limit = 2
assert_equal("", t.render!(context))
refute_nil(context.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)
context = Liquid::Context.new
t.render(context)
assert_equal(9, context.resource_limits.assign_score)
t = Template.parse("{% capture foo %}すごい{% endcapture %}")
t.render
assert_equal(9, t.resource_limits.assign_score)
t.render(context)
assert_equal(9, context.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?)
context = Liquid::Context.new
context.resource_limits.assign_score_limit = 3
assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert(context.resource_limits.reached?)
t.resource_limits.assign_score_limit = 5
assert_equal("", t.render!)
context.resource_limits.assign_score_limit = 5
assert_equal("", t.render!(context))
end
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.resource_limits.render_score_limit = 50
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
context = Liquid::Context.new
context.resource_limits.render_score_limit = 50
assert_equal("Liquid error: Memory limits exceeded", t.render(context))
assert(context.resource_limits.reached?)
end
def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set
t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}")
t.render!
assert(t.resource_limits.assign_score > 0)
assert(t.resource_limits.render_score > 0)
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
context = Liquid::Context.new
t.render!(context)
assert(context.resource_limits.assign_score > 0)
assert(context.resource_limits.render_score > 0)
assert(context.resource_limits.render_length > 0)
end
def test_render_length_persists_between_blocks
t = Template.parse("{% if true %}aaaa{% endif %}")
t.resource_limits.render_length_limit = 3
assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 4
assert_equal("aaaa", t.render)
context = Liquid::Context.new
context.resource_limits.render_length_limit = 7
assert_equal("Liquid error: Memory limits exceeded", t.render(context))
context.resource_limits.render_length_limit = 8
assert_equal("aaaa", t.render(context))
t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}")
t.resource_limits.render_length_limit = 6
assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 7
assert_equal("aaaabbb", t.render)
context = Liquid::Context.new
context.resource_limits.render_length_limit = 13
assert_equal("Liquid error: Memory limits exceeded", t.render(context))
context.resource_limits.render_length_limit = 14
assert_equal("aaaabbb", t.render(context))
t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}")
t.resource_limits.render_length_limit = 5
assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 6
assert_equal("ababab", t.render)
context = Liquid::Context.new
context.resource_limits.render_length_limit = 5
assert_equal("Liquid error: Memory limits exceeded", t.render(context))
context.resource_limits.render_length_limit = 11
assert_equal("Liquid error: Memory limits exceeded", t.render(context))
context.resource_limits.render_length_limit = 12
assert_equal("ababab", t.render(context))
end
def test_render_length_uses_number_of_bytes_not_characters
t = Template.parse("{% if true %}すごい{% endif %}")
t.resource_limits.render_length_limit = 8
assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 9
assert_equal("すごい", t.render)
context = Liquid::Context.new
context.resource_limits.render_length_limit = 10
assert_equal("Liquid error: Memory limits exceeded", t.render(context))
context.resource_limits.render_length_limit = 18
assert_equal("すごい", t.render(context))
end
def test_default_resource_limits_unaffected_by_render_with_context
context = Context.new
t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}")
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
t.render!(context)
assert(context.resource_limits.assign_score > 0)
assert(context.resource_limits.render_score > 0)
assert(context.resource_limits.render_length > 0)
end
def test_can_use_drop_as_context
t = Template.new
t.registers['lulz'] = 'haha'
drop = TemplateContextDrop.new
assert_equal('fizzbuzz', t.parse('{{foo}}').render!(drop))
assert_equal('bar', t.parse('{{bar}}').render!(drop))
assert_equal('haha', t.parse("{{baz}}").render!(drop))
context = Liquid::Context.build(environments: drop, registers: { 'lulz' => 'haha' })
drop.context = context
assert_equal('fizzbuzz', t.parse('{{foo}}').render!(context))
assert_equal('bar', t.parse('{{bar}}').render!(context))
assert_equal('haha', t.parse("{{baz}}").render!(context))
end
def test_render_bang_force_rethrow_errors_on_passed_context
@@ -275,25 +259,27 @@ class TemplateTest < Minitest::Test
end
def test_undefined_variables
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true)
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
context = Liquid::Context.new('x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } })
result = t.render(context, strict_variables: true)
assert_equal('33 32 ', result)
assert_equal(3, t.errors.count)
assert_instance_of(Liquid::UndefinedVariable, t.errors[0])
assert_equal('Liquid error: undefined variable y', t.errors[0].message)
assert_instance_of(Liquid::UndefinedVariable, t.errors[1])
assert_equal('Liquid error: undefined variable b', t.errors[1].message)
assert_instance_of(Liquid::UndefinedVariable, t.errors[2])
assert_equal('Liquid error: undefined variable d', t.errors[2].message)
assert_equal(3, context.errors.count)
assert_instance_of(Liquid::UndefinedVariable, context.errors[0])
assert_equal('Liquid error: undefined variable y', context.errors[0].message)
assert_instance_of(Liquid::UndefinedVariable, context.errors[1])
assert_equal('Liquid error: undefined variable b', context.errors[1].message)
assert_instance_of(Liquid::UndefinedVariable, context.errors[2])
assert_equal('Liquid error: undefined variable d', context.errors[2].message)
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)
t = Template.parse("some{{x}}thing")
context = Liquid::Context.new('x' => nil)
result = t.render!(context, strict_variables: true)
assert_equal(0, t.errors.count)
assert_equal(0, context.errors.count)
assert_equal('something', result)
end
@@ -308,11 +294,13 @@ class TemplateTest < Minitest::Test
def test_undefined_drop_methods
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
result = t.render(d, strict_variables: true)
context = Liquid::Context.new(d)
d.context = context
result = t.render(context, strict_variables: true)
assert_equal('foo ', result)
assert_equal(1, t.errors.count)
assert_instance_of(Liquid::UndefinedDropMethod, t.errors[0])
assert_equal(1, context.errors.count)
assert_instance_of(Liquid::UndefinedDropMethod, context.errors[0])
end
def test_undefined_drop_methods_raise
@@ -331,12 +319,13 @@ class TemplateTest < Minitest::Test
"-#{v}-"
end
end
result = t.render({ 'a' => 123, 'x' => 'foo' }, filters: [filters], strict_filters: true)
context = Liquid::Context.new('a' => 123, 'x' => 'foo')
result = t.render(context, filters: [filters], strict_filters: true)
assert_equal('123 ', result)
assert_equal(1, t.errors.count)
assert_instance_of(Liquid::UndefinedFilter, t.errors[0])
assert_equal('Liquid error: undefined filter somefilter1', t.errors[0].message)
assert_equal(1, context.errors.count)
assert_instance_of(Liquid::UndefinedFilter, context.errors[0])
assert_equal('Liquid error: undefined filter somefilter1', context.errors[0].message)
end
def test_undefined_filters_raise
@@ -356,4 +345,48 @@ class TemplateTest < Minitest::Test
result = t.render('x' => 1, 'y' => 5)
assert_equal('12345', result)
end
def test_render_uses_correct_disabled_tags_instance
Liquid::Template.file_system = StubFileSystem.new(
'foo' => 'bar',
'test_include' => '{% include "foo" %}'
)
disabled_tags = DisabledTags.new
context = Context.build(registers: { disabled_tags: disabled_tags })
source = "{% render 'test_include' %}"
parse_context = Liquid::ParseContext.new(line_numbers: true, error_mode: :strict)
document = Document.parse(Liquid::Tokenizer.new(source, true), parse_context)
assert_equal("include usage is not allowed in this context", document.render(context))
end
def test_render_sets_context_static_register_when_register_key_does_exist
disabled_tags_for_test = DisabledTags.new
Template.add_register(:disabled_tags, disabled_tags_for_test)
t = Template.parse("{% if true %} Test Template {% endif %}")
context = Context.new
refute(context.registers.key?(:disabled_tags))
t.render(context)
assert(context.registers.key?(:disabled_tags))
assert_equal(disabled_tags_for_test, context.registers[:disabled_tags])
end
def test_render_does_not_override_context_static_register_when_register_key_exists
context = Context.new
context.registers[:random_register] = nil
Template.add_register(:random_register, {})
t = Template.parse("{% if true %} Test Template {% endif %}")
t.render(context)
assert_nil(context.registers[:random_register])
assert(context.registers.key?(:random_register))
end
end

View File

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

View File

@@ -51,29 +51,10 @@ class VariableTest < Minitest::Test
assert_equal('cat', Template.parse("{{ nil | append: 'cat' }}").render!)
end
def test_preset_assigns
template = Template.parse(%({{ test }}))
template.assigns['test'] = 'worked'
assert_equal('worked', template.render!)
end
def test_reuse_parsed_template
template = Template.parse(%({{ greeting }} {{ name }}))
template.assigns['greeting'] = 'Goodbye'
template = Template.parse(%({{ greeting }} {{ name }}))
assert_equal('Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi'))
assert_equal('Hello ', template.render!('greeting' => 'Hello', 'unknown' => 'Tobi'))
assert_equal('Hello Brian', template.render!('greeting' => 'Hello', 'name' => 'Brian'))
assert_equal('Goodbye Brian', template.render!('name' => 'Brian'))
assert_equal({ 'greeting' => 'Goodbye' }, template.assigns)
end
def test_assigns_not_polluted_from_template
template = Template.parse(%({{ test }}{% assign test = 'bar' %}{{ test }}))
template.assigns['test'] = 'baz'
assert_equal('bazbar', template.render!)
assert_equal('bazbar', template.render!)
assert_equal('foobar', template.render!('test' => 'foo'))
assert_equal('bazbar', template.render!)
assert_equal('Goodbye Brian', template.render!('greeting' => 'Goodbye', 'name' => 'Brian'))
end
def test_hash_with_default_proc

View File

@@ -98,17 +98,10 @@ module Minitest
end
def with_custom_tag(tag_name, tag_class)
old_tag = Liquid::Template.tags[tag_name]
begin
Liquid::Template.register_tag(tag_name, tag_class)
yield
ensure
if old_tag
Liquid::Template.tags[tag_name] = old_tag
else
Liquid::Template.tags.delete(tag_name)
end
end
Liquid::Template.register_tag(tag_name, tag_class)
yield
ensure
Liquid::Template.tags.delete(tag_name)
end
end
end

View File

@@ -563,35 +563,6 @@ class ContextUnitTest < Minitest::Test
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

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'test_helper'
class DisabledTagsUnitTest < Minitest::Test
include Liquid
def test_disables_tag_specified
register = DisabledTags.new
register.disable(%w(foo bar)) do
assert_equal true, register.disabled?("foo")
assert_equal true, register.disabled?("bar")
assert_equal false, register.disabled?("unknown")
end
end
def test_disables_nested_tags
register = DisabledTags.new
register.disable(["foo"]) do
register.disable(["foo"]) do
assert_equal true, register.disabled?("foo")
assert_equal false, register.disabled?("bar")
end
register.disable(["bar"]) do
assert_equal true, register.disabled?("foo")
assert_equal true, register.disabled?("bar")
register.disable(["foo"]) do
assert_equal true, register.disabled?("foo")
assert_equal true, register.disabled?("bar")
end
end
assert_equal true, register.disabled?("foo")
assert_equal false, register.disabled?("bar")
end
end
end