lib/haml/attribute_compiler.rb

Summary

Maintainability
A
2 hrs
Test Coverage
A
100%
# frozen_string_literal: true
require 'haml/attribute_builder'
require 'haml/attribute_parser'
require 'haml/ruby_expression'

module Haml
  # The list of boolean attributes. You may add custom attributes to this constant.
  BOOLEAN_ATTRIBUTES = %w[disabled readonly multiple checked autobuffer
                       autoplay controls loop selected hidden scoped async
                       defer reversed ismap seamless muted required
                       autofocus novalidate formnovalidate open pubdate
                       itemscope allowfullscreen default inert sortable
                       truespeed typemustmatch download]

  class AttributeCompiler
    def initialize(identity, options)
      @identity = identity
      @quote  = options[:attr_quote]
      @format = options[:format]
      @escape_attrs = options[:escape_attrs]
    end

    def compile(node)
      hashes = []
      if node.value[:object_ref] != :nil || !AttributeParser.available?
        return runtime_compile(node)
      end
      [node.value[:dynamic_attributes].new, node.value[:dynamic_attributes].old].compact.each do |attribute_str|
        hash = AttributeParser.parse(attribute_str)
        return runtime_compile(node) if hash.nil? || hash.any? { |_key, value| value.empty? }
        hashes << hash
      end
      static_compile(node.value[:attributes], hashes)
    end

    private

    def runtime_compile(node)
      attrs = []
      attrs.unshift(node.value[:attributes].inspect) if node.value[:attributes] != {}

      args = [@escape_attrs.inspect, "#{@quote.inspect}.freeze", @format.inspect, node.value[:object_ref]] + attrs
      [:html, :attrs, [:dynamic, "::Haml::AttributeBuilder.build(#{args.join(', ')}, #{node.value[:dynamic_attributes].to_literal})"]]
    end

    def static_compile(static_hash, dynamic_hashes)
      temple = [:html, :attrs]
      keys = [*static_hash.keys, *dynamic_hashes.map(&:keys).flatten].uniq.sort
      keys.each do |key|
        values = [[:static, static_hash[key]], *dynamic_hashes.map { |h| [:dynamic, h[key]] }]
        values.select! { |_, exp| exp != nil }

        case key
        when 'id'
          compile_id!(temple, key, values)
        when 'class'
          compile_class!(temple, key, values)
        when 'data', 'aria'
          compile_data!(temple, key, values)
        when *BOOLEAN_ATTRIBUTES, /\Adata-/, /\Aaria-/
          compile_boolean!(temple, key, values)
        else
          compile_common!(temple, key, values)
        end
      end
      temple
    end

    def compile_id!(temple, key, values)
      build_code = attribute_builder(:id, values)
      if values.all? { |type, exp| type == :static || Temple::StaticAnalyzer.static?(exp) }
        temple << [:html, :attr, key, [:static, eval(build_code).to_s]]
      else
        temple << [:html, :attr, key, [:dynamic, build_code]]
      end
    end

    def compile_class!(temple, key, values)
      build_code = attribute_builder(:class, values)
      if values.all? { |type, exp| type == :static || Temple::StaticAnalyzer.static?(exp) }
        temple << [:html, :attr, key, [:static, eval(build_code).to_s]]
      else
        temple << [:html, :attr, key, [:dynamic, build_code]]
      end
    end

    def compile_data!(temple, key, values)
      args = [@escape_attrs.inspect, "#{@quote.inspect}.freeze", values.map { |v| literal_for(v) }]
      build_code = "::Haml::AttributeBuilder.build_#{key}(#{args.join(', ')})"

      if values.all? { |type, exp| type == :static || Temple::StaticAnalyzer.static?(exp) }
        temple << [:static, eval(build_code).to_s]
      else
        temple << [:dynamic, build_code]
      end
    end

    def compile_boolean!(temple, key, values)
      exp = literal_for(values.last)

      if Temple::StaticAnalyzer.static?(exp)
        value = eval(exp)
        case value
        when true then temple << [:html, :attr, key, @format == :xhtml ? [:static, key] : [:multi]]
        when false, nil
        else temple << [:html, :attr, key, [:fescape, @escape_attrs, [:static, value.to_s]]]
        end
      else
        var = @identity.generate
        temple << [
          :case, "(#{var} = (#{exp}))",
          ['true', [:html, :attr, key, @format == :xhtml ? [:static, key] : [:multi]]],
          ['false, nil', [:multi]],
          [:else, [:multi, [:static, " #{key}=#{@quote}"], [:fescape, @escape_attrs, [:dynamic, var]], [:static, @quote]]],
        ]
      end
    end

    def compile_common!(temple, key, values)
      temple << [:html, :attr, key, [:fescape, @escape_attrs, values.last]]
    end

    def attribute_builder(type, values)
      args = [@escape_attrs.inspect, *values.map { |v| literal_for(v) }]
      "::Haml::AttributeBuilder.build_#{type}(#{args.join(', ')})"
    end

    def literal_for(value)
      type, exp = value
      type == :static ? "#{exp.inspect}.freeze" : exp
    end
  end
end