mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-12 23:40:45 -07:00
Separate expression parsing and rendering from Context#resolve.
This commit is contained in:
@@ -52,11 +52,14 @@ require 'liquid/extensions'
|
|||||||
require 'liquid/errors'
|
require 'liquid/errors'
|
||||||
require 'liquid/interrupts'
|
require 'liquid/interrupts'
|
||||||
require 'liquid/strainer'
|
require 'liquid/strainer'
|
||||||
|
require 'liquid/expression'
|
||||||
require 'liquid/context'
|
require 'liquid/context'
|
||||||
require 'liquid/tag'
|
require 'liquid/tag'
|
||||||
require 'liquid/block'
|
require 'liquid/block'
|
||||||
require 'liquid/document'
|
require 'liquid/document'
|
||||||
require 'liquid/variable'
|
require 'liquid/variable'
|
||||||
|
require 'liquid/variable_lookup'
|
||||||
|
require 'liquid/range_lookup'
|
||||||
require 'liquid/file_system'
|
require 'liquid/file_system'
|
||||||
require 'liquid/template'
|
require 'liquid/template'
|
||||||
require 'liquid/standardfilters'
|
require 'liquid/standardfilters'
|
||||||
|
|||||||
+11
-98
@@ -16,15 +16,13 @@ module Liquid
|
|||||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
||||||
attr_accessor :exception_handler
|
attr_accessor :exception_handler
|
||||||
|
|
||||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
|
||||||
|
|
||||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
|
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
|
||||||
@environments = [environments].flatten
|
@environments = [environments].flatten
|
||||||
@scopes = [(outer_scope || {})]
|
@scopes = [(outer_scope || {})]
|
||||||
@registers = registers
|
@registers = registers
|
||||||
@errors = []
|
@errors = []
|
||||||
@resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
|
@resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
|
||||||
@parsed_variables = Hash.new{ |cache, markup| cache[markup] = variable_parse(markup) }
|
@parsed_expression = Hash.new{ |cache, markup| cache[markup] = Expression.parse(markup) }
|
||||||
squash_instance_assigns_with_environments
|
squash_instance_assigns_with_environments
|
||||||
|
|
||||||
@this_stack_used = false
|
@this_stack_used = false
|
||||||
@@ -163,23 +161,6 @@ module Liquid
|
|||||||
@scopes[0][key] = value
|
@scopes[0][key] = value
|
||||||
end
|
end
|
||||||
|
|
||||||
def [](key)
|
|
||||||
resolve(key)
|
|
||||||
end
|
|
||||||
|
|
||||||
def has_key?(key)
|
|
||||||
resolve(key) != nil
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
LITERALS = {
|
|
||||||
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
|
|
||||||
'true'.freeze => true,
|
|
||||||
'false'.freeze => false,
|
|
||||||
'blank'.freeze => :blank?,
|
|
||||||
'empty'.freeze => :empty?
|
|
||||||
}
|
|
||||||
|
|
||||||
# Look up variable, either resolve directly after considering the name. We can directly handle
|
# Look up variable, either resolve directly after considering the name. We can directly handle
|
||||||
# Strings, digits, floats and booleans (true,false).
|
# Strings, digits, floats and booleans (true,false).
|
||||||
# If no match is made we lookup the variable in the current scope and
|
# If no match is made we lookup the variable in the current scope and
|
||||||
@@ -188,25 +169,16 @@ module Liquid
|
|||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
# products == empty #=> products.empty?
|
# products == empty #=> products.empty?
|
||||||
def resolve(key)
|
def [](expression)
|
||||||
if LITERALS.key?(key)
|
evaluate(@parsed_expression[expression])
|
||||||
LITERALS[key]
|
|
||||||
else
|
|
||||||
case key
|
|
||||||
when /\A'(.*)'\z/m # Single quoted strings
|
|
||||||
$1
|
|
||||||
when /\A"(.*)"\z/m # Double quoted strings
|
|
||||||
$1
|
|
||||||
when /\A(-?\d+)\z/ # Integer and floats
|
|
||||||
$1.to_i
|
|
||||||
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
|
|
||||||
(resolve($1).to_i..resolve($2).to_i)
|
|
||||||
when /\A(-?\d[\d\.]+)\z/ # Floats
|
|
||||||
$1.to_f
|
|
||||||
else
|
|
||||||
variable(key)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_key?(key)
|
||||||
|
self[key] != nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def evaluate(object)
|
||||||
|
object.respond_to?(:evaluate) ? object.evaluate(self) : object
|
||||||
end
|
end
|
||||||
|
|
||||||
# Fetches an object starting at the local scope and then moving up the hierachy
|
# Fetches an object starting at the local scope and then moving up the hierachy
|
||||||
@@ -238,74 +210,15 @@ module Liquid
|
|||||||
return variable
|
return variable
|
||||||
end
|
end
|
||||||
|
|
||||||
def variable_parse(markup)
|
|
||||||
parts = markup.scan(VariableParser)
|
|
||||||
needs_resolution = false
|
|
||||||
if parts.first =~ SQUARE_BRACKETED
|
|
||||||
needs_resolution = true
|
|
||||||
parts[0] = $1
|
|
||||||
end
|
|
||||||
{:first => parts.shift, :needs_resolution => needs_resolution, :rest => parts}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Resolves namespaced queries gracefully.
|
|
||||||
#
|
|
||||||
# Example
|
|
||||||
# @context['hash'] = {"name" => 'tobi'}
|
|
||||||
# assert_equal 'tobi', @context['hash.name']
|
|
||||||
# assert_equal 'tobi', @context['hash["name"]']
|
|
||||||
def variable(markup)
|
|
||||||
parts = @parsed_variables[markup]
|
|
||||||
|
|
||||||
first_part = parts[:first]
|
|
||||||
if parts[:needs_resolution]
|
|
||||||
first_part = resolve(parts[:first])
|
|
||||||
end
|
|
||||||
|
|
||||||
if object = find_variable(first_part)
|
|
||||||
|
|
||||||
parts[:rest].each do |part|
|
|
||||||
part = resolve($1) if part_resolved = (part =~ SQUARE_BRACKETED)
|
|
||||||
|
|
||||||
# If object is a hash- or array-like object we look for the
|
|
||||||
# presence of the key and if its available we return it
|
|
||||||
if object.respond_to?(:[]) and
|
|
||||||
((object.respond_to?(:has_key?) and object.has_key?(part)) or
|
|
||||||
(object.respond_to?(:fetch) and part.is_a?(Integer)))
|
|
||||||
|
|
||||||
# if its a proc we will replace the entry with the proc
|
|
||||||
res = lookup_and_evaluate(object, part)
|
|
||||||
object = res.to_liquid
|
|
||||||
|
|
||||||
# Some special cases. If the part wasn't in square brackets and
|
|
||||||
# no key with the same name was found we interpret following calls
|
|
||||||
# as commands and call them on the current object
|
|
||||||
elsif !part_resolved and object.respond_to?(part) and ['size'.freeze, 'first'.freeze, 'last'.freeze].include?(part)
|
|
||||||
|
|
||||||
object = object.send(part.intern).to_liquid
|
|
||||||
|
|
||||||
# No key was present with the desired value and it wasn't one of the directly supported
|
|
||||||
# keywords either. The only thing we got left is to return nil
|
|
||||||
else
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
# If we are dealing with a drop here we have to
|
|
||||||
object.context = self if object.respond_to?(:context=)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
object
|
|
||||||
end # variable
|
|
||||||
|
|
||||||
def lookup_and_evaluate(obj, key)
|
def lookup_and_evaluate(obj, key)
|
||||||
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
|
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
|
||||||
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
||||||
else
|
else
|
||||||
value
|
value
|
||||||
end
|
end
|
||||||
end # lookup_and_evaluate
|
end
|
||||||
|
|
||||||
|
private
|
||||||
def squash_instance_assigns_with_environments
|
def squash_instance_assigns_with_environments
|
||||||
@scopes.last.each_key do |k|
|
@scopes.last.each_key do |k|
|
||||||
@environments.each do |env|
|
@environments.each do |env|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
module Liquid
|
||||||
|
class Expression
|
||||||
|
LITERALS = {
|
||||||
|
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
|
||||||
|
'true'.freeze => true,
|
||||||
|
'false'.freeze => false,
|
||||||
|
'blank'.freeze => :blank?,
|
||||||
|
'empty'.freeze => :empty?
|
||||||
|
}
|
||||||
|
|
||||||
|
def self.parse(markup)
|
||||||
|
if LITERALS.key?(markup)
|
||||||
|
LITERALS[markup]
|
||||||
|
else
|
||||||
|
case markup
|
||||||
|
when /\A'(.*)'\z/m # Single quoted strings
|
||||||
|
$1
|
||||||
|
when /\A"(.*)"\z/m # Double quoted strings
|
||||||
|
$1
|
||||||
|
when /\A(-?\d+)\z/ # Integer and floats
|
||||||
|
$1.to_i
|
||||||
|
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
|
||||||
|
RangeLookup.parse($1, $2)
|
||||||
|
when /\A(-?\d[\d\.]+)\z/ # Floats
|
||||||
|
$1.to_f
|
||||||
|
else
|
||||||
|
VariableLookup.parse(markup)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
module Liquid
|
||||||
|
class RangeLookup
|
||||||
|
def self.parse(start_markup, end_markup)
|
||||||
|
start_obj = Expression.parse(start_markup)
|
||||||
|
end_obj = Expression.parse(end_markup)
|
||||||
|
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
||||||
|
new(start_obj, end_obj)
|
||||||
|
else
|
||||||
|
start_obj.to_i..end_obj.to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(start_obj, end_obj)
|
||||||
|
@start_obj = start_obj
|
||||||
|
@end_obj = end_obj
|
||||||
|
end
|
||||||
|
|
||||||
|
def evaluate(context)
|
||||||
|
context.evaluate(@start_obj).to_i..context.evaluate(@end_obj).to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
module Liquid
|
||||||
|
class VariableLookup
|
||||||
|
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||||
|
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
|
||||||
|
|
||||||
|
def self.parse(markup)
|
||||||
|
new(markup)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(markup)
|
||||||
|
lookups = markup.scan(VariableParser)
|
||||||
|
|
||||||
|
name = lookups.shift
|
||||||
|
if name =~ SQUARE_BRACKETED
|
||||||
|
name = Expression.parse($1)
|
||||||
|
end
|
||||||
|
@name = name
|
||||||
|
|
||||||
|
@lookups = lookups
|
||||||
|
@command_flags = 0
|
||||||
|
|
||||||
|
@lookups.each_index do |i|
|
||||||
|
lookup = lookups[i]
|
||||||
|
if lookup =~ SQUARE_BRACKETED
|
||||||
|
lookups[i] = Expression.parse($1)
|
||||||
|
elsif COMMAND_METHODS.include?(lookup)
|
||||||
|
@command_flags |= 1 << i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def evaluate(context)
|
||||||
|
name = context.evaluate(@name)
|
||||||
|
object = context.find_variable(name)
|
||||||
|
|
||||||
|
@lookups.each_index do |i|
|
||||||
|
key = context.evaluate(@lookups[i])
|
||||||
|
|
||||||
|
# If object is a hash- or array-like object we look for the
|
||||||
|
# presence of the key and if its available we return it
|
||||||
|
if object.respond_to?(:[]) &&
|
||||||
|
((object.respond_to?(:has_key?) && object.has_key?(key)) ||
|
||||||
|
(object.respond_to?(:fetch) && key.is_a?(Integer)))
|
||||||
|
|
||||||
|
# if its a proc we will replace the entry with the proc
|
||||||
|
res = context.lookup_and_evaluate(object, key)
|
||||||
|
object = res.to_liquid
|
||||||
|
|
||||||
|
# Some special cases. If the part wasn't in square brackets and
|
||||||
|
# no key with the same name was found we interpret following calls
|
||||||
|
# as commands and call them on the current object
|
||||||
|
elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
|
||||||
|
object = object.send(key).to_liquid
|
||||||
|
|
||||||
|
# No key was present with the desired value and it wasn't one of the directly supported
|
||||||
|
# keywords either. The only thing we got left is to return nil
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# If we are dealing with a drop here we have to
|
||||||
|
object.context = context if object.respond_to?(:context=)
|
||||||
|
end
|
||||||
|
|
||||||
|
object
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user