Separate expression parsing and rendering from Context#resolve.

This commit is contained in:
Dylan Thacker-Smith
2014-07-21 22:16:24 -04:00
parent d5e57a8ea4
commit 2aa9bbbac2
5 changed files with 179 additions and 140 deletions

View File

@@ -52,11 +52,14 @@ require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer'
require 'liquid/expression'
require 'liquid/context'
require 'liquid/tag'
require 'liquid/block'
require 'liquid/document'
require 'liquid/variable'
require 'liquid/variable_lookup'
require 'liquid/range_lookup'
require 'liquid/file_system'
require 'liquid/template'
require 'liquid/standardfilters'

View File

@@ -16,15 +16,13 @@ module Liquid
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_handler
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
@environments = [environments].flatten
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@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
@this_stack_used = false
@@ -163,149 +161,64 @@ module Liquid
@scopes[0][key] = value
end
def [](key)
resolve(key)
# Look up variable, either resolve directly after considering the name. We can directly handle
# Strings, digits, floats and booleans (true,false).
# If no match is made we lookup the variable in the current scope and
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
#
# Example:
# products == empty #=> products.empty?
def [](expression)
evaluate(@parsed_expression[expression])
end
def has_key?(key)
resolve(key) != nil
self[key] != nil
end
def evaluate(object)
object.respond_to?(:evaluate) ? object.evaluate(self) : object
end
# Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key)
# This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.has_key?(key) }
scope = @scopes[index] if index
variable = nil
if scope.nil?
@environments.each do |e|
variable = lookup_and_evaluate(e, key)
unless variable.nil?
scope = e
break
end
end
end
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key)
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
return variable
end
def lookup_and_evaluate(obj, key)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
else
value
end
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
# Strings, digits, floats and booleans (true,false).
# If no match is made we lookup the variable in the current scope and
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
#
# Example:
# products == empty #=> products.empty?
def resolve(key)
if LITERALS.key?(key)
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
end
# Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key)
# This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.has_key?(key) }
scope = @scopes[index] if index
variable = nil
if scope.nil?
@environments.each do |e|
variable = lookup_and_evaluate(e, key)
unless variable.nil?
scope = e
break
end
end
end
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key)
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
return variable
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)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
else
value
end
end # lookup_and_evaluate
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@environments.each do |env|

33
lib/liquid/expression.rb Normal file
View File

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

View File

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

View File

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