lib/volt/server/html_parser/attribute_scope.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module Volt
  # Included into ViewScope to provide processing for attributes
  module AttributeScope
    module ClassMethods
      def methodize_string(str)
        # Convert the string passed in to the binding so it returns a ruby Method
        # instance
        parts = str.split('.')

        end_call = parts.last.strip

        # If no method(args) is passed, we assume they want to convert the method
        # to a Method, to be called with *args (from any trigger's), then event.
        if str !~ /[\[\]\$\@\=]/ && end_call =~ /[_a-z0-9!?]+$/
          parts[-1] = "method(:#{end_call})"

          str = parts.join('.')
        end

        str
      end
    end

    def self.included(base)
      base.send :extend, ClassMethods
    end

    # Take the attributes and create any bindings
    def process_attributes(tag_name, attributes)
      new_attributes = attributes.dup

      attributes.each_pair do |name, value|
        if name[0..1] == 'e-'
          process_event_binding(tag_name, new_attributes, name, value)
        else
          process_attribute(tag_name, new_attributes, name, value)
        end
      end

      new_attributes
    end

    def process_event_binding(tag_name, attributes, name, value)
      id = add_id_to_attributes(attributes)

      event = name[2..-1]

      if tag_name == 'a'
        # For links, we need to add blank href to make it clickable.
        attributes['href'] ||= ''
      end

      # Remove the e- attribute
      attributes.delete(name)

      value = self.class.methodize_string(value)

      save_binding(id, "lambda { |__p, __t, __c, __id| Volt::EventBinding.new(__p, __t, __c, __id, #{event.inspect}, Proc.new {|event| #{value} })}")
    end

    # Takes a string and splits on bindings, returns the string split on bindings
    # and the number of bindings.
    def binding_parts_and_count(value)
      if value.is_a?(String)
        parts = value.split(/(\{\{[^\}]+\}\})/).reject(&:blank?)
      else
        parts = ['']
      end
      binding_count = parts.count { |p| p[0] == '{' && p[1] == '{' && p[-2] == '}' && p[-1] == '}' }

      [parts, binding_count]
    end

    def process_attribute(tag_name, attributes, attribute_name, value)
      parts, binding_count = binding_parts_and_count(value)

      # if this attribute has bindings
      if binding_count > 0
        # Setup an id
        id = add_id_to_attributes(attributes)

        if parts.size > 1
          # Multiple bindings
          add_multiple_attribute(tag_name, id, attribute_name, parts, value)
        elsif parts.size == 1 && binding_count == 1
          getter = parts[0][2...-2].strip

          if getter =~ /^asset_url[ \(]/
            # asset url helper
            add_asset_url_attribute(getter, attributes)
            return # don't delete attr
          else
            # A single binding
            add_single_attribute(id, attribute_name, getter)
          end
        end

        # Remove the attribute
        attributes.delete(attribute_name)
      end
    end

    # TODO: We should use a real parser for this
    def getter_to_setter(getter)
      getter = getter.strip.gsub(/\(\s*\)/, '')

      # Check to see if this can be converted to a setter
      if getter[0] =~ /^[A-Z]/ && getter[-1] != ')'
        if getter.index('.')
          "#{getter}=(val)"
        else
          "raise \"could not auto generate setter for `#{getter}`\""
        end
      elsif getter[0] =~ /^[a-z_]/ && getter[-1] != ')'
        # Convert a getter into a setter
        if getter.index('.') || getter.index('@')
          prefix = ''
        else
          prefix = 'self.'
        end

        "#{prefix}#{getter}=(val)"
      else
        "raise \"could not auto generate setter for `#{getter}`\""
      end
    end

    # Add an attribute binding on the tag, bind directly to the getter in the binding
    def add_single_attribute(id, attribute_name, getter)
      setter = getter_to_setter(getter)

      save_binding(id, "lambda { |__p, __t, __c, __id| Volt::AttributeBinding.new(__p, __t, __c, __id, #{attribute_name.inspect}, Proc.new { #{getter} }, Proc.new { |val| #{setter} }) }")
    end

    def add_asset_url_attribute(getter, attributes)
      # Asset url helper binding
      asset_url_parts = getter.split(/[\s\(\)\'\"]/).reject(&:blank?)
      url = asset_url_parts[1]

      unless url
        raise "the `asset_url` helper requries a url argument ```{{ asset_url 'pic.png' }}```"
      end

      link_url = @handler.link_asset(url, false)

      attributes['src'] = link_url
    end

    def add_multiple_attribute(tag_name, id, attribute_name, parts, content)
      case attribute_name
        when 'checked', 'value'
          if parts.size > 1
            if tag_name == 'textarea'
              fail "The content of text area's can not be bound to multiple bindings."
            else
              # Multiple values can not be passed to value or checked attributes.
              fail "Multiple bindings can not be passed to a #{attribute_name} binding: #{parts.inspect}"
            end
          end
      end

      string_template_renderer_path = add_string_template_renderer(content)

      save_binding(id, "lambda { |__p, __t, __c, __id| Volt::AttributeBinding.new(__p, __t, __c, __id, #{attribute_name.inspect}, Proc.new { Volt::StringTemplateRenderer.new(__p, __c, #{string_template_renderer_path.inspect}) }) }")
    end

    def add_string_template_renderer(content)
      path            = @path + "/_rv#{@binding_number}"
      new_handler     = ViewHandler.new(path, nil, false)
      @binding_number += 1

      SandlebarsParser.new(content, new_handler)

      # Close out the last scope
      new_handler.scope.last.close_scope

      # Copy in the templates from the new handler
      new_handler.templates.each_pair do |key, value|
        @handler.templates[key] = value
      end

      path
    end

    def add_id_to_attributes(attributes)
      id              = attributes['id'] ||= "id#{@binding_number}"
      @binding_number += 1

      id.to_s
    end

    def attribute_string(attributes)
      attr_str = attributes.map { |v| "#{v[0]}=\"#{v[1]}\"" }.join(' ')
      if attr_str.size > 0
        # extra space
        attr_str = ' ' + attr_str
      end

      attr_str
    end
  end
end