require 'volt/server/html_parser/attribute_scope' module Volt class ViewScope include AttributeScope attr_reader :html, :bindings attr_accessor :path, :binding_number def initialize(handler, path) @handler = handler @path = path @html = '' @bindings = {} @binding_number = 0 end def <<(html) @html << html end def add_binding(content) content = content.strip index = content.index(/[ \(]/) if index first_symbol = content[0...index] args = content[index..-1].strip case first_symbol when 'if' add_if(args) when 'elsif' add_else(args) when 'else' if args.blank? add_else(nil) else fail "else does not take a conditional, #{content} was provided." end when 'template' add_template(args) else if content =~ /.each\s+do\s+\|/ add_each(content) else add_content_binding(content) end end else case content when 'end' # Close the binding close_scope when 'else' add_else(nil) else add_content_binding(content) end end end def add_content_binding(content) @handler.html << "" save_binding(@binding_number, "lambda { |__p, __t, __c, __id| Volt::ContentBinding.new(__p, __t, __c, __id, Proc.new { #{content} }) }") @binding_number += 1 end def add_if(content) # Add with path for if group. @handler.scope << IfViewScope.new(@handler, @path + "/__ifg#{@binding_number}", content) @binding_number += 1 end def add_else(content) fail '#else can only be added inside of an if block' end def add_each(content) @handler.scope << EachScope.new(@handler, @path + "/__each#{@binding_number}", content) end def add_template(content) # Strip ( and ) from the outsides content = content.strip.gsub(/^\(/, '').gsub(/\)$/, '') @handler.html << "" save_binding(@binding_number, "lambda { |__p, __t, __c, __id| Volt::TemplateBinding.new(__p, __t, __c, __id, #{@path.inspect}, Proc.new { [#{content}] }) }") @binding_number += 1 end # Returns ruby code to fetch the parent. (by removing the last fetch) # TODO: Probably want to do this with AST transforms with the parser/unparser gems def parent_fetcher(getter) parent = getter.strip.gsub(/[.][^.]+$/, '') if parent.blank? || !getter.index('.') parent = 'self' end parent end def last_method_name(getter) getter.strip[/[^.]+$/] end def add_component(tag_name, attributes, unary) component_name = tag_name[1..-1].tr(':', '/') @handler.html << "" data_hash = [] attributes.each_pair do |name, value| name = name.tr('-', '_') parts, binding_count = binding_parts_and_count(value) # if this attribute has bindings if binding_count > 0 if binding_count > 1 # Multiple bindings elsif parts.size == 1 && binding_count == 1 # A single binding getter = value[2...-2].strip data_hash << "#{name.inspect} => Proc.new { #{getter} }" setter = getter_to_setter(getter) data_hash << "#{(name + '=').inspect} => Proc.new { |val| #{setter} }" # Add an _parent fetcher. Useful for things like volt-fields to get the parent model. parent = parent_fetcher(getter) # TODO: This adds some overhead, perhaps there is a way to compute this dynamically on the # front-end. data_hash << "#{(name + '_parent').inspect} => Proc.new { #{parent} }" # Add a _last_method property. This is useful data_hash << "#{(name + '_last_method').inspect} => #{last_method_name(getter).inspect}" end else # String data_hash << "#{name.inspect} => #{value.inspect}" end end arguments = "#{component_name.inspect}, { #{data_hash.join(',')} }" save_binding(@binding_number, "lambda { |__p, __t, __c, __id| Volt::ComponentBinding.new(__p, __t, __c, __id, #{@path.inspect}, Proc.new { [#{arguments}] }) }") @binding_number += 1 end def add_textarea(tag_name, attributes, unary) @handler.scope << TextareaScope.new(@handler, @path + "/__txtarea#{@binding_number}", attributes) @binding_number += 1 # close right away if unary @handler.last.close_scope if unary end # Called when this scope should be closed out def close_scope(pop = true) if pop scope = @handler.scope.pop else scope = @handler.last end fail "template path already exists: #{scope.path}" if @handler.templates[scope.path] template = { 'html' => scope.html } if scope.bindings.size > 0 # Add the bindings if there are any template['bindings'] = scope.bindings end @handler.templates[scope.path] = template end def save_binding(binding_number, code) @bindings[binding_number] ||= [] @bindings[binding_number] << code end end end