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("<", "&lt;").gsub(">", "&gt;")}" - else - "Liquor error: #{e.message.gsub("<", "&lt;").gsub(">", "&gt;")}" - 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