mirror of
https://github.com/Shopify/liquid.git
synced 2026-10-02 16:45:12 -07:00
Add strict2 let closures for Liquid composition
This commit is contained in:
@@ -58,6 +58,9 @@ require 'liquid/parser_switching'
|
||||
require 'liquid/tag'
|
||||
require 'liquid/block'
|
||||
require 'liquid/parse_tree_visitor'
|
||||
require 'liquid/closure_body'
|
||||
require 'liquid/closure'
|
||||
require 'liquid/call_expression'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/tags'
|
||||
require "liquid/environment"
|
||||
|
||||
+14
-4
@@ -10,10 +10,13 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
@body = new_body
|
||||
while parse_body(@body, tokens)
|
||||
end
|
||||
@body.freeze
|
||||
parse_block_body(tokens)
|
||||
end
|
||||
|
||||
# Parse an external block body with closure validation and lexical declarations.
|
||||
def parse_closure_body(tokens, parameters: Const::EMPTY_ARRAY)
|
||||
parse_context.with_closure(parameters) { parse_block_body(tokens) }
|
||||
ClosureBody.new(@body, parameters)
|
||||
end
|
||||
|
||||
# For backwards compatibility
|
||||
@@ -66,6 +69,13 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def parse_block_body(tokens)
|
||||
@body = new_body
|
||||
while parse_body(@body, tokens)
|
||||
end
|
||||
@body.freeze
|
||||
end
|
||||
|
||||
# @api public
|
||||
def new_body
|
||||
parse_context.new_block_body
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class CallExpression
|
||||
attr_reader :target, :arguments
|
||||
|
||||
def initialize(target, arguments)
|
||||
@target = target.freeze
|
||||
@arguments = arguments.freeze
|
||||
freeze
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
context.resource_limits.with_capture do
|
||||
render_to_output_buffer(context, +'')
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
closure = context.evaluate(@target)
|
||||
raise Liquid::ArgumentError, "Expected a Liquid closure" unless closure.is_a?(Closure)
|
||||
|
||||
arguments = @arguments.map { |argument| context.evaluate(argument) }
|
||||
closure.render_to_output_buffer(context, output, arguments)
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.target] + @node.arguments
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,96 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# A render-time value. Captures retain binding hashes, not copies of their values.
|
||||
class Closure
|
||||
def initialize(body, parameters, context)
|
||||
@body = body
|
||||
@parameters = parameters
|
||||
@scopes = context.scopes.dup
|
||||
@environments = context.environments.dup
|
||||
@static_environments = context.static_environments
|
||||
end
|
||||
|
||||
# Render into the supplied buffer and return direct-child body-form let bindings.
|
||||
def invoke(context, arguments = Const::EMPTY_ARRAY, output: +'')
|
||||
invocation = render_invocation(context, arguments, output)
|
||||
@body.exports(invocation)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output, arguments = Const::EMPTY_ARRAY)
|
||||
render_invocation(context, arguments, output)
|
||||
output
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
|
||||
def to_s
|
||||
''
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_invocation(context, arguments, output)
|
||||
unless arguments.length == @parameters.length
|
||||
raise Liquid::ArgumentError, "Expected #{@parameters.length} closure arguments, got #{arguments.length}"
|
||||
end
|
||||
|
||||
invocation = context.new_closure_subcontext(@scopes, @environments, @static_environments)
|
||||
@parameters.each_with_index { |name, index| invocation[name] = arguments[index] }
|
||||
@body.render_to_output_buffer(invocation, output)
|
||||
invocation
|
||||
end
|
||||
|
||||
# @api private
|
||||
class CapturedBindings
|
||||
def initialize(scopes, environments, context)
|
||||
@scopes = scopes
|
||||
@environments = environments
|
||||
@context = context
|
||||
@locals = {}
|
||||
end
|
||||
|
||||
def key?(name)
|
||||
local_key?(name) || @environments.any? { |environment| environment.key?(name) }
|
||||
end
|
||||
|
||||
def local_key?(name)
|
||||
@locals.key?(name) || @scopes.any? { |scope| scope.key?(name) } ||
|
||||
@environments.any? { |environment| environment.is_a?(CapturedBindings) && environment.local_key?(name) }
|
||||
end
|
||||
|
||||
def []=(name, value)
|
||||
@locals[name] = value
|
||||
end
|
||||
|
||||
def [](name)
|
||||
find_variable(name, @context, raise_on_not_found: false)
|
||||
end
|
||||
|
||||
def find_variable(name, context, raise_on_not_found: true)
|
||||
return context.lookup_and_evaluate(@locals, name, raise_on_not_found: raise_on_not_found) if @locals.key?(name)
|
||||
|
||||
scope = @scopes.find { |candidate| candidate.key?(name) }
|
||||
return context.lookup_and_evaluate(scope, name, raise_on_not_found: raise_on_not_found) if scope
|
||||
|
||||
find_in_environments(name, context, raise_on_not_found: raise_on_not_found)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_in_environments(name, context, raise_on_not_found:)
|
||||
@environments.each do |environment|
|
||||
if environment.is_a?(CapturedBindings)
|
||||
return environment.find_variable(name, context, raise_on_not_found: raise_on_not_found)
|
||||
end
|
||||
|
||||
value = context.lookup_and_evaluate(environment, name, raise_on_not_found: raise_on_not_found)
|
||||
return value if !value.nil? || context.strict_variables && raise_on_not_found
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Parsed syntax shared by let and closure-aware custom blocks.
|
||||
class ClosureBody
|
||||
attr_reader :body
|
||||
|
||||
def initialize(body, parameters = Const::EMPTY_ARRAY)
|
||||
@body = body
|
||||
@parameters = parameters.dup.freeze
|
||||
@exports = body.nodelist.select { |node| node.is_a?(Let) && node.body_form? }.map(&:to).freeze
|
||||
freeze
|
||||
end
|
||||
|
||||
def bind(context)
|
||||
Closure.new(self, @parameters, context)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@body.render_to_output_buffer(context, output)
|
||||
end
|
||||
|
||||
def exports(context)
|
||||
context.scopes.last.slice(*@exports)
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@body.nodelist
|
||||
end
|
||||
end
|
||||
end
|
||||
+40
-2
@@ -162,6 +162,40 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def new_closure_subcontext(scopes, environments, static_environments)
|
||||
new_isolated_subcontext.tap do |invocation|
|
||||
captures = Closure::CapturedBindings.new(scopes, environments + static_environments, invocation)
|
||||
invocation.captured_bindings = captures
|
||||
invocation.environments = [captures]
|
||||
invocation.static_environments = static_environments
|
||||
invocation.strict_variables = strict_variables
|
||||
invocation.strict_filters = strict_filters
|
||||
invocation.global_filter = global_filter
|
||||
invocation.template_name = template_name
|
||||
invocation.partial = partial
|
||||
invocation.base_scope_depth = base_scope_depth + @scopes.length
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def with_binding_scope
|
||||
previous_root = @binding_root
|
||||
stack do
|
||||
@binding_root = @scopes.first
|
||||
yield
|
||||
end
|
||||
ensure
|
||||
@binding_root = previous_root
|
||||
end
|
||||
|
||||
# @api private
|
||||
def set_local(name, value)
|
||||
root = @binding_root || @scopes.last
|
||||
scope = @scopes.find { |owned| owned.key?(name) || owned.equal?(root) }
|
||||
scope[name] = value
|
||||
end
|
||||
|
||||
def clear_instance_assigns
|
||||
@scopes[0] = {}
|
||||
end
|
||||
@@ -206,7 +240,7 @@ module Liquid
|
||||
# path and find_index() is optimized in MRI to reduce object allocation
|
||||
index = @scopes.find_index { |s| s.key?(key) }
|
||||
|
||||
fallback_to_self_drop = key == Expression::SELF && index.nil?
|
||||
fallback_to_self_drop = key == Expression::SELF && index.nil? && !@captured_bindings&.local_key?(key)
|
||||
|
||||
variable = if index
|
||||
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
|
||||
@@ -262,13 +296,17 @@ module Liquid
|
||||
|
||||
protected
|
||||
|
||||
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
|
||||
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags, :environments, :static_environments, :captured_bindings
|
||||
|
||||
private
|
||||
|
||||
attr_reader :base_scope_depth
|
||||
|
||||
def try_variable_find_in_environments(key, raise_on_not_found:)
|
||||
if @captured_bindings
|
||||
return @captured_bindings.find_variable(key, self, raise_on_not_found: raise_on_not_found)
|
||||
end
|
||||
|
||||
@environments.each do |environment|
|
||||
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
|
||||
if !found_variable.nil? || @strict_variables && raise_on_not_found
|
||||
|
||||
@@ -20,7 +20,9 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse(tokenizer, parse_context)
|
||||
while parse_body(tokenizer)
|
||||
@local_bindings = parse_context.with_document_scope do
|
||||
while parse_body(tokenizer)
|
||||
end
|
||||
end
|
||||
@body.freeze
|
||||
rescue SyntaxError => e
|
||||
@@ -38,7 +40,9 @@ module Liquid
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@body.render_to_output_buffer(context, output)
|
||||
return @body.render_to_output_buffer(context, output) unless @local_bindings
|
||||
|
||||
context.with_binding_scope { @body.render_to_output_buffer(context, output) }
|
||||
end
|
||||
|
||||
def render(context)
|
||||
|
||||
@@ -36,6 +36,7 @@ module Liquid
|
||||
|
||||
def parse(markup, ss = StringScanner.new(""), cache = nil)
|
||||
return unless markup
|
||||
return markup unless markup.is_a?(String)
|
||||
|
||||
markup = markup.strip # markup can be a frozen string
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
module Liquid
|
||||
class Lexer
|
||||
ASSIGN = [:assign, "="].freeze
|
||||
CLOSE_ROUND = [:close_round, ")"].freeze
|
||||
CLOSE_SQUARE = [:close_square, "]"].freeze
|
||||
COLON = [:colon, ":"].freeze
|
||||
@@ -133,6 +134,8 @@ module Liquid
|
||||
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
|
||||
output << found
|
||||
ss.scan_byte
|
||||
elsif peeked == "=".ord && ss.peek_byte != "<".ord && ss.peek_byte != ">".ord && ss.peek_byte != "!".ord
|
||||
output << ASSIGN
|
||||
else
|
||||
raise_syntax_error(start_pos, ss)
|
||||
end
|
||||
|
||||
@@ -38,7 +38,11 @@ module Liquid
|
||||
|
||||
def new_parser(input)
|
||||
@string_scanner.string = input
|
||||
Parser.new(@string_scanner, reject_bare_brackets: @error_mode == :strict2 || @error_mode == :rigid)
|
||||
Parser.new(
|
||||
@string_scanner,
|
||||
reject_bare_brackets: @error_mode == :strict2 || @error_mode == :rigid,
|
||||
allow_calls: @error_mode == :strict2,
|
||||
)
|
||||
end
|
||||
|
||||
def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
|
||||
@@ -60,8 +64,8 @@ module Liquid
|
||||
# compatibility while raising awareness about strict2 parser standards,
|
||||
# the safe flag supports API users make a deliberate decision.
|
||||
#
|
||||
# In strict2 mode, markup MUST come from a string returned by the parser
|
||||
# (e.g., parser.expression). We're not calling the parser here to
|
||||
# In strict2 mode, markup MUST come from parser.expression, which returns
|
||||
# markup or a parsed callable node. We're not calling the parser here to
|
||||
# prevent redundant parser overhead.
|
||||
raise Liquid::InternalError, "unsafe parse_expression cannot be used in strict2 mode"
|
||||
end
|
||||
@@ -69,6 +73,54 @@ module Liquid
|
||||
Expression.parse(markup, @string_scanner, @expression_cache)
|
||||
end
|
||||
|
||||
def with_document_scope(&block)
|
||||
previous_usage = @uses_local_bindings
|
||||
@uses_local_bindings = false
|
||||
with_lexical_scope(&block)
|
||||
@uses_local_bindings
|
||||
ensure
|
||||
@uses_local_bindings = previous_usage
|
||||
end
|
||||
|
||||
def with_lexical_scope(parameters = Const::EMPTY_ARRAY)
|
||||
previous_scope = @lexical_scope
|
||||
@lexical_scope = parameters.to_h { |name| [name, true] }
|
||||
yield
|
||||
ensure
|
||||
@lexical_scope = previous_scope
|
||||
end
|
||||
|
||||
def local_declarations?
|
||||
@lexical_scope && !@lexical_scope.empty?
|
||||
end
|
||||
|
||||
def declare_local(name)
|
||||
@lexical_scope ||= {}
|
||||
raise SyntaxError, "Duplicate let declaration '#{name}'" if @lexical_scope.key?(name)
|
||||
|
||||
@lexical_scope[name] = true
|
||||
end
|
||||
|
||||
def with_closure(parameters = Const::EMPTY_ARRAY, &block)
|
||||
require_closures!
|
||||
previous_closure = @in_closure
|
||||
@in_closure = true
|
||||
with_lexical_scope(parameters, &block)
|
||||
ensure
|
||||
@in_closure = previous_closure
|
||||
end
|
||||
|
||||
def in_closure?
|
||||
@in_closure
|
||||
end
|
||||
|
||||
def require_closures!
|
||||
@uses_local_bindings = true
|
||||
return if @error_mode == :strict2
|
||||
|
||||
raise SyntaxError, "Closures require strict2 mode"
|
||||
end
|
||||
|
||||
def partial=(value)
|
||||
@partial = value
|
||||
@options = value ? partial_options : @template_options
|
||||
|
||||
+74
-1
@@ -2,11 +2,12 @@
|
||||
|
||||
module Liquid
|
||||
class Parser
|
||||
def initialize(input, reject_bare_brackets: false)
|
||||
def initialize(input, reject_bare_brackets: false, allow_calls: false)
|
||||
ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
|
||||
@tokens = Lexer.tokenize(ss)
|
||||
@p = 0 # pointer to current location
|
||||
@reject_bare_brackets = reject_bare_brackets
|
||||
@allow_calls = allow_calls
|
||||
end
|
||||
|
||||
def jump(point)
|
||||
@@ -51,6 +52,8 @@ module Liquid
|
||||
token = @tokens[@p]
|
||||
case token[0]
|
||||
when :id
|
||||
return callable_or_lookup if @allow_calls
|
||||
|
||||
str = consume
|
||||
str << variable_lookups
|
||||
when :open_square
|
||||
@@ -69,12 +72,41 @@ module Liquid
|
||||
consume(:dotdot)
|
||||
last = expression
|
||||
consume(:close_round)
|
||||
return RangeLookup.new(Expression.parse(first), Expression.parse(last)) unless first.is_a?(String) && last.is_a?(String)
|
||||
|
||||
"(#{first}..#{last})"
|
||||
else
|
||||
raise SyntaxError, "#{token} is not a valid expression"
|
||||
end
|
||||
end
|
||||
|
||||
def expression_with_markup
|
||||
start = @p
|
||||
# Parsing lookups appends to consumed identifier strings.
|
||||
markup_tokens = @tokens[start...-1].map { |token| token[1].dup }
|
||||
value = expression
|
||||
markup = value.is_a?(String) ? value : markup_tokens.take(@p - start).join
|
||||
[value, markup]
|
||||
end
|
||||
|
||||
def remaining_markup
|
||||
@tokens[@p...-1].map { |token| token[1] }.join(' ')
|
||||
end
|
||||
|
||||
def parameters
|
||||
parameters = []
|
||||
return parameters unless consume?(:open_round)
|
||||
|
||||
unless look(:close_round)
|
||||
parameters << consume(:id)
|
||||
parameters << consume(:id) while consume?(:comma) && !look(:close_round)
|
||||
end
|
||||
consume(:close_round)
|
||||
raise SyntaxError, "Duplicate closure parameter" unless parameters.uniq.length == parameters.length
|
||||
|
||||
parameters
|
||||
end
|
||||
|
||||
def argument
|
||||
str = +""
|
||||
# might be a keyword argument (identifier: expression)
|
||||
@@ -102,5 +134,46 @@ module Liquid
|
||||
end
|
||||
str
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def callable_or_lookup
|
||||
target = callable_lookups(consume(:id))
|
||||
return target unless consume?(:open_round)
|
||||
|
||||
arguments = []
|
||||
unless look(:close_round)
|
||||
arguments << Expression.parse(expression)
|
||||
arguments << Expression.parse(expression) while consume?(:comma) && !look(:close_round)
|
||||
end
|
||||
consume(:close_round)
|
||||
target = VariableLookup.parse(target) if target.is_a?(String)
|
||||
CallExpression.new(target, arguments)
|
||||
end
|
||||
|
||||
def callable_lookups(target)
|
||||
loop do
|
||||
if consume?(:dot)
|
||||
target = append_lookup(target, consume(:id), command: true)
|
||||
next
|
||||
end
|
||||
break unless consume?(:open_square)
|
||||
|
||||
key = expression
|
||||
consume(:close_square)
|
||||
target = append_lookup(target, key)
|
||||
end
|
||||
target
|
||||
end
|
||||
|
||||
def append_lookup(target, key, command: false)
|
||||
if target.is_a?(String) && key.is_a?(String)
|
||||
return target << (command ? ".#{key}" : "[#{key}]")
|
||||
end
|
||||
|
||||
target = VariableLookup.parse(target) if target.is_a?(String)
|
||||
target.add_lookup(command ? key : Expression.parse(key), command: command)
|
||||
target
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,6 +7,8 @@ require_relative "tags/break"
|
||||
require_relative "tags/inline_comment"
|
||||
require_relative "tags/for"
|
||||
require_relative "tags/assign"
|
||||
require_relative "tags/let"
|
||||
require_relative "tags/set"
|
||||
require_relative "tags/ifchanged"
|
||||
require_relative "tags/case"
|
||||
require_relative "tags/include"
|
||||
@@ -37,6 +39,8 @@ module Liquid
|
||||
'case' => Case,
|
||||
'ifchanged' => Ifchanged,
|
||||
'assign' => Assign,
|
||||
'let' => Let,
|
||||
'set' => SetTag,
|
||||
'for' => For,
|
||||
'#' => InlineComment,
|
||||
'break' => Break,
|
||||
|
||||
@@ -31,6 +31,8 @@ module Liquid
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
raise SyntaxError, "assign is not allowed in closure bodies" if parse_context.in_closure?
|
||||
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
@@ -65,7 +67,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.increment_assign_score(self.class.assign_score_of(val))
|
||||
output
|
||||
end
|
||||
|
||||
@@ -73,9 +75,8 @@ module Liquid
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_score_of(val)
|
||||
# @api private
|
||||
def self.assign_score_of(val)
|
||||
if val.instance_of?(String)
|
||||
val.bytesize
|
||||
elsif val.instance_of?(Array)
|
||||
|
||||
+28
-16
@@ -38,15 +38,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
if parse_body(@for_block, tokens)
|
||||
parse_body(@else_block, tokens)
|
||||
parse_context.with_lexical_scope do
|
||||
parse_loop(tokens)
|
||||
@has_local_declarations = parse_context.local_declarations?
|
||||
end
|
||||
if blank?
|
||||
@else_block&.remove_blank_strings
|
||||
@for_block.remove_blank_strings
|
||||
end
|
||||
@else_block&.freeze
|
||||
@for_block.freeze
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@@ -92,10 +87,10 @@ module Liquid
|
||||
@variable_name = p.consume(:id)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
||||
|
||||
collection_name = p.expression
|
||||
collection_name, collection_markup = p.expression_with_markup
|
||||
@collection_name = parse_expression(collection_name, safe: true)
|
||||
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@name = "#{@variable_name}-#{collection_markup}"
|
||||
@reversed = p.id?('reversed')
|
||||
|
||||
while p.look(:comma) || p.look(:id)
|
||||
@@ -111,6 +106,18 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
def parse_loop(tokens)
|
||||
if parse_body(@for_block, tokens)
|
||||
parse_body(@else_block, tokens)
|
||||
end
|
||||
if blank?
|
||||
@else_block&.remove_blank_strings
|
||||
@for_block.remove_blank_strings
|
||||
end
|
||||
@else_block&.freeze
|
||||
@for_block.freeze
|
||||
end
|
||||
|
||||
def strict2_parse(markup)
|
||||
strict_parse(markup)
|
||||
end
|
||||
@@ -161,7 +168,7 @@ module Liquid
|
||||
|
||||
segment.each do |item|
|
||||
context[@variable_name] = item
|
||||
@for_block.render_to_output_buffer(context, output)
|
||||
render_iteration(context, output)
|
||||
loop_vars.send(:increment!)
|
||||
|
||||
# Handle any interrupts if they exist.
|
||||
@@ -178,6 +185,12 @@ module Liquid
|
||||
output
|
||||
end
|
||||
|
||||
def render_iteration(context, output)
|
||||
return @for_block.render_to_output_buffer(context, output) unless @has_local_declarations
|
||||
|
||||
context.stack { @for_block.render_to_output_buffer(context, output) }
|
||||
end
|
||||
|
||||
def set_attribute(key, expr, safe: false)
|
||||
case key
|
||||
when 'offset'
|
||||
@@ -192,11 +205,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def render_else(context, output)
|
||||
if @else_block
|
||||
@else_block.render_to_output_buffer(context, output)
|
||||
else
|
||||
output
|
||||
end
|
||||
return output unless @else_block
|
||||
return @else_block.render_to_output_buffer(context, output) unless @has_local_declarations
|
||||
|
||||
context.stack { @else_block.render_to_output_buffer(context, output) }
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Let < Block
|
||||
attr_reader :to, :from, :parameters, :closure_body
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
parse_context.require_closures!
|
||||
parser = parse_context.new_parser(markup)
|
||||
@to = parser.consume(:id)
|
||||
if parser.consume?(:assign)
|
||||
raise SyntaxError, "Expected let initializer" if parser.look(:end_of_string)
|
||||
|
||||
@from = Variable.new(parser, parse_context)
|
||||
else
|
||||
@parameters = parser.parameters.freeze
|
||||
parser.consume(:end_of_string)
|
||||
end
|
||||
parse_context.declare_local(@to)
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
return unless body_form?
|
||||
|
||||
@closure_body = parse_closure_body(tokens, parameters: @parameters)
|
||||
end
|
||||
|
||||
def body_form?
|
||||
@from.nil?
|
||||
end
|
||||
|
||||
def nodelist
|
||||
body_form? ? super : Const::EMPTY_ARRAY
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
value = body_form? ? @closure_body.bind(context) : @from.render(context)
|
||||
context.resource_limits.increment_assign_score(Assign.assign_score_of(value))
|
||||
context[@to] = value
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
@node.body_form? ? [@node.closure_body] : [@node.from]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class SetTag < Tag
|
||||
attr_reader :to, :from
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
parse_context.require_closures!
|
||||
parser = parse_context.new_parser(markup)
|
||||
@to = parser.consume(:id)
|
||||
parser.consume(:assign)
|
||||
raise SyntaxError, "Expected set initializer" if parser.look(:end_of_string)
|
||||
|
||||
@from = Variable.new(parser, parse_context)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
value = if context.variable_defined?(@to)
|
||||
@from.render(context)
|
||||
else
|
||||
context.stack(@to => nil) { @from.render(context) }
|
||||
end
|
||||
context.resource_limits.increment_assign_score(Assign.assign_score_of(value))
|
||||
context.set_local(@to, value)
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.from]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -34,6 +34,13 @@ module Liquid
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
parse_context.with_lexical_scope do
|
||||
super
|
||||
@has_local_declarations = parse_context.local_declarations?
|
||||
end
|
||||
end
|
||||
|
||||
def strict2_parse(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
@@ -99,7 +106,11 @@ module Liquid
|
||||
context[@variable_name] = item
|
||||
|
||||
output << "<td class=\"col#{tablerowloop.col}\">"
|
||||
super
|
||||
if @has_local_declarations
|
||||
context.stack { super }
|
||||
else
|
||||
super
|
||||
end
|
||||
output << '</td>'
|
||||
|
||||
# Handle any interrupts if they exist.
|
||||
|
||||
@@ -25,7 +25,7 @@ module Liquid
|
||||
include ParserSwitching
|
||||
|
||||
def initialize(markup, parse_context)
|
||||
@markup = markup
|
||||
@markup = markup.is_a?(Parser) ? markup.remaining_markup : markup
|
||||
@name = nil
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
@@ -42,6 +42,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def markup_context(markup)
|
||||
markup = @markup if markup.is_a?(Parser)
|
||||
"in \"{{#{markup}}}\""
|
||||
end
|
||||
|
||||
@@ -80,7 +81,7 @@ module Liquid
|
||||
|
||||
def strict2_parse(markup)
|
||||
@filters = []
|
||||
p = @parse_context.new_parser(markup)
|
||||
p = markup.is_a?(Parser) ? markup : @parse_context.new_parser(markup)
|
||||
|
||||
return if p.look(:end_of_string)
|
||||
|
||||
@@ -109,6 +110,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
if @name.is_a?(CallExpression) && @filters.empty? && context.global_filter.nil?
|
||||
return @name.render_to_output_buffer(context, output)
|
||||
end
|
||||
|
||||
obj = render(context)
|
||||
render_obj_to_output(obj, output)
|
||||
output
|
||||
@@ -207,7 +212,7 @@ module Liquid
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.name] + @node.filters.flatten
|
||||
[@node.name] + @node.filters.flatten.flat_map { |value| value.is_a?(Hash) ? value.values : value }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,6 +40,12 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def add_lookup(key, command: false)
|
||||
@command_flags |= 1 << @lookups.length if command && COMMAND_METHODS.include?(key)
|
||||
@lookups << key
|
||||
end
|
||||
|
||||
def lookup_command?(lookup_index)
|
||||
@command_flags & (1 << lookup_index) != 0
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user