lib/liquor/context.rb in liquor-0.1.1 vs lib/liquor/context.rb in liquor-0.9.1
- old
+ new
@@ -1,294 +1,116 @@
-module Liquor
+require 'set'
- # Context keeps the variable stack and resolves variables, as well as keywords
- #
- # context['variable'] = 'testing'
- # context['variable'] #=> 'testing'
- # context['true'] #=> true
- # context['10.2232'] #=> 10.2232
- #
- # context.stack do
- # context['bob'] = 'bobsen'
- # end
- #
- # context['bob'] #=> nil class Context
+module Liquor
class Context
- attr_reader :scopes, :errors, :registers, :environments
+ attr_reader :compiler, :emitter
+ attr_reader :externals, :variables
+ attr_reader :nesting
- def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false)
- @environments = [environments].flatten
- @scopes = [(outer_scope || {})]
- @registers = registers
- @errors = []
- @rethrow_errors = rethrow_errors
- squash_instance_assigns_with_environments
- end
+ RESERVED_NAMES = %w(_env _buf _storage
+ __LINE__ __FILE__ __ENCODING__ BEGIN END alias and begin
+ break case class def do else elsif end ensure false for in
+ module next nil not or redo rescue retry return self super
+ then true undef when yield if unless while until).freeze
- def strainer
- @strainer ||= Strainer.create(self)
- end
+ def initialize(compiler, externals)
+ @compiler = compiler
+ @externals = externals
- # adds filters to this context.
- # this does not register the filters with the main Template object. see <tt>Template.register_filter</tt>
- # for that
- def add_filters(filters)
- filters = [filters].flatten.compact
+ @emitter = Emitter.new(self)
- filters.each do |f|
- raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
- strainer.extend(f)
- end
- end
+ @variables = Set[]
+ @var_stack = []
- def handle_error(e)
- errors.push(e)
- raise if @rethrow_errors
+ @mapping = {}
+ @map_stack = []
- case e
- when SyntaxError
- "Liquor syntax error: #{e.message.gsub("<", "<").gsub(">", ">")}"
- else
- "Liquor error: #{e.message.gsub("<", "<").gsub(">", ">")}"
- end
- end
+ @retired = Set[]
+ @nesting = 1
- def invoke(method, *args)
- if args[0].class == Drop::DropProxy && args[0].has_scope?(method)
- scope_args = args[1..args.length-1]
- return args[0].send(method, *scope_args)
+ @externals.each do |external|
+ declare external
end
-
- if strainer.respond_to?(method)
- strainer.__send__(method, *args)
- else
- args.first
- end
end
- # push new local scope on the stack. use <tt>Context#stack</tt> instead
- def push(new_scope={})
- raise StackLevelError, "Nesting too deep" if @scopes.length > 100
- @scopes.unshift(new_scope)
+ def builtin?(name)
+ %w(true false null).include? name
end
- # merge a hash of variables in the current local scope
- def merge(new_scopes)
- @scopes[0].merge!(new_scopes)
+ def function?(name)
+ @compiler.has_function? name
end
- # pop from the stack. use <tt>Context#stack</tt> instead
- def pop
- raise ContextError if @scopes.size == 1
- @scopes.shift
+ def variable?(name)
+ @variables.include? name
end
- # pushes a new local scope on the stack, pops it at the end of the block
- #
- # Example:
- #
- # context.stack do
- # context['var'] = 'hi'
- # end
- # context['var] #=> nil
- #
- def stack(new_scope={},&block)
- result = nil
- push(new_scope)
- begin
- result = yield
- ensure
- pop
- end
- result
+ def allocated?(name)
+ builtin?(name) || function?(name) || variable?(name)
end
-
- def clear_instance_assigns
- @scopes[0] = {}
- end
- # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquor::Drop</tt>
- def []=(key, value)
- @scopes[0][key] = value
+ def type(name)
+ if builtin?(name)
+ :builtin
+ elsif function?(name)
+ :function
+ elsif variable?(name)
+ :variable
+ else
+ :free
+ end
end
- def [](key)
- resolve(key)
- end
+ def declare(name, loc=nil)
+ name = name.to_s
- def has_key?(key)
- resolve(key) != nil
- end
-
- private
-
- # 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)
- case key
- when nil, 'nil', 'null', ''
- nil
- when 'true'
- true
- when 'false'
- false
- when 'blank'
- :blank?
- when 'empty'
- :empty?
- # Single quoted strings
- when /^'(.*)'$/
- $1.to_s
- # Double quoted strings
- when /^"(.*)"$/
- $1.to_s
- # Integer
- when /^([+-]?\d+)$/
- $1.to_i
- # Ranges
- when /^\((\S+)\.\.(\S+)\)$/
- (resolve($1).to_i..resolve($2).to_i)
- # Floats
- when /^([+-]?\d[\d\.]+)$/
- $1.to_f
- # filtered variables
- when SpacelessFilter
- filtered_variable(key)
- else
- variable(key)
+ if builtin?(name) || function?(name)
+ raise NameError.new("identifier `#{name}' is already occupied by #{type name}", loc)
end
- end
- # fetches an object starting at the local scope and then moving up
- # the hierachy
- def find_variable(key)
- scope = @scopes.find { |s| s.has_key?(key) }
- if scope.nil?
- @environments.each do |e|
- if variable = lookup_and_evaluate(e, key)
- scope = e
- break
- end
+ shadow = @var_stack.count > 0 && @var_stack[-1].include?(name)
+
+ if !@variables.include?(name) || shadow
+ mapped, idx = name, 0
+ while RESERVED_NAMES.include?(mapped) ||
+ @mapping.values.include?(mapped) ||
+ @retired.include?(mapped)
+ mapped = "#{name}_m#{idx}" # `m' stands for `mangled'
+ idx += 1
end
+
+ @variables.add name
+ @mapping[name] = mapped
end
- scope ||= @environments.last || @scopes.last
- variable ||= lookup_and_evaluate(scope, key)
-
- variable = variable.to_liquor
-
- if variable.class != ActiveRecord::Relation
- variable.context = self if variable.respond_to?(:context=)
- end
- return variable
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 = markup.scan(VariableParser)
- square_bracketed = /^\[(.*)\]$/
+ def access(name, loc=nil)
+ name = name.to_s
- first_part = parts.shift
- if first_part =~ square_bracketed
- first_part = resolve($1)
+ if variable?(name)
+ @mapping[name]
+ else
+ raise NameError.new("variable `#{name}' is undefined", loc)
end
+ end
- if object = find_variable(first_part)
- parts.each do |part|
- part = resolve($1) if part_resolved = (part =~ square_bracketed)
- # DropProxy was designed like named_scopes for implementing has_many and named_scope wrappers in LiquorDrops.
- # DropProxy delegates most Array methods to its internal function to convert an array of ActiveRecord objects to
- # an array of liquor drops when you work with it like with an array.
- #
- # Also it is absolutely safe not to call here to_liquor because when you will try to work with the result it will be automatically converted
- # to an array of drops. So you have no chance to do something crucial.
- #
- # Also when 'my_object.referenced_objects' expression evaluates, the 'object' should be a DropProxy, but you can find
- # that 'object = res.to_liquor' will convert it into an array. So 'to_liquor' method was defined in the DropProxy and always returns the itself.
- #
- if object.class == ActiveRecord::Relation && (['size', 'first', 'last', 'paginate'].include?(part) || part.is_a?(Integer))
- if part.is_a?(Integer)
- res = object[part]
- else
- res = object.send(part.intern)
- end
-
- object = res.to_liquor
-
- elsif object.class == Drop::DropProxy || object.is_a?(Drop) && (object.has_scope?(part) || object.has_many?(part))
- if part.is_a?(Integer)
- object = object[part]
- else
- object = object.send(part.intern) rescue nil
- end
+ def nest
+ @var_stack.push @variables
+ @variables = @variables.dup
- # If object is a hash- or array-like object we look for the
- # presence of the key and if its available we return it
- elsif 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 = object[part]
- # res = object[part] = res.call(self) if res.is_a?(Proc) and object.respond_to?(:[]=)
- res = lookup_and_evaluate(object, part)
- object = res.to_liquor
+ @map_stack.push @mapping
+ @mapping = @mapping.dup
- # 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', 'first', 'last'].include?(part)
- object = object.send(part.intern).to_liquor
+ @nesting += 1
- # 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
+ yield
+ ensure
+ @retired += @mapping.values
- # If we are dealing with a drop here we have to
- object.context = self if object.respond_to?(:context=)
- end
- end
+ @variables = @var_stack.pop
+ @mapping = @map_stack.pop
- object
+ @nesting -= 1
end
-
- def lookup_and_evaluate(obj, key)
- if obj.class != ActiveRecord::Relation && (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
- obj[key] = (value.arity == 0 ) ? value.call : value.call(self)
- else
- value
- end
- end
-
- def squash_instance_assigns_with_environments
- @scopes.last.each_key do |k|
- @environments.each do |env|
- if env.has_key?(k)
- scopes.last[k] = lookup_and_evaluate(env, k)
- break
- end
- end
- end
- end
-
- def filtered_variable(markup)
- Variable.new(markup).render(self)
- end
-
end
-end
+end
\ No newline at end of file