lib/temple/html/fast.rb



module Temple
  module HTML
    class Fast
      DEFAULT_OPTIONS = {
        :format => :xhtml,
        :attr_wrapper => "'",
        :autoclose => %w[meta img link br hr input area param col base]
      }
      
      def initialize(options = {})
        @options = DEFAULT_OPTIONS.merge(options)
        
        unless [:xhtml, :html4, :html5].include?(@options[:format])
          raise "Invalid format #{@options[:format].inspect}"
        end
        
      end
      
      def xhtml?
        @options[:format] == :xhtml
      end
      
      def html?
        html5? or html4?
      end
      
      def html5?
        @options[:format] == :html5
      end
      
      def html4?
        @options[:format] == :html4
      end
      
      def compile(exp)
        case exp[0]
        when :multi, :capture
          send("on_#{exp[0]}", *exp[1..-1])
        when :html
          send("on_#{exp[1]}", *exp[2..-1])
        else
          exp
        end
      end
      
      def on_multi(*exp)
        [:multi, *exp.map { |e| compile(e) }]
      end
      
      def on_doctype(type)
        trailing_newlines = type[/(\A|[^\r])(\n+)\Z/, 2].to_s
        
        text = type.to_s.downcase.strip
        if text.index("xml") == 0
          if html?
            return [:multi].concat([[:newline]] * trailing_newlines.size)
          end
          
          wrapper = @options[:attr_wrapper]
          str = "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{text.split(' ')[1] || "utf-8"}#{wrapper} ?>"
        end
        
        str = "<!DOCTYPE html>" if html5?
        
        str ||= if xhtml?
          case text
          when /^1\.1/;     '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
          when /^5/;        '<!DOCTYPE html>'
          when "strict";    '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
          when "frameset";  '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
          when "mobile";    '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
          when "basic";     '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">'
          else              '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
          end
        elsif html4?
          case text
            when "strict";    '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
            when "frameset";  '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">'
            else              '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
          end
        end
        
        str << trailing_newlines
        [:static, str]
      end
      
      def on_comment(content)
        [:multi,
          [:static, "<!--"],
          compile(content),
          [:static, "-->"]]
      end
      
      def on_tag(name, attrs, content)
        ac = @options[:autoclose].include?(name)
        result = [:multi]
        result << [:static, "<#{name}"]
        result << compile(attrs)
        result << [:static, " /"] if ac && xhtml?
        result << [:static, ">"]
        result << compile(content)
        result << [:static, "</#{name}>"] if !ac
        result
      end
      
      def on_attrs(*exp)
        if exp.all? { |e| attr_easily_compilable?(e) }
          [:multi, *merge_basicattrs(exp).map { |e| compile(e) }]
        else
          raise "[:html, :attrs] currently only support basicattrs"
        end
      end
      
      def attr_easily_compilable?(exp)
        exp[1] == :basicattr and
        exp[2][0] == :static
      end
      
      def merge_basicattrs(attrs)
        result = []
        position = {}
        
        attrs.each do |(html, type, (name_type, name), value)|
          if pos = position[name]
            case name
            when 'class', 'id'
              value = [:multi,
                result[pos].last,  # previous value
                [:static, (name == 'class' ? ' ' : '_')], # delimiter
                value]             # new value
            end
            
            result[pos] = [name, value]
          else
            position[name] = result.size
            result << [name, value]
          end
        end
        
        final = []
        result.each_with_index do |(name, value), index|
          final << [:html, :basicattr, [:static, name], value]
        end
        final
      end
      
      def on_basicattr(name, value)
        [:multi,
          [:static, " "],
          name,
          [:static, "="],
          [:static, @options[:attr_wrapper]],
          value,
          [:static, @options[:attr_wrapper]]]
      end
    end
  end
end