lib/temple/html/fast.rb



module Temple
  module HTML
    # @api public
    class Fast < Filter
      XHTML_DOCTYPES = {
        '1.1'          => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
        '5'            => '<!DOCTYPE html>',
        'html'         => '<!DOCTYPE html>',
        'strict'       => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
        'frameset'     => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
        'mobile'       => '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">',
        'basic'        => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
        'transitional' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
      }.freeze

      HTML4_DOCTYPES = {
        'strict'       => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
        'frameset'     => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">',
        'transitional' => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
      }.freeze

      set_default_options :format => :xhtml,
                          :attr_wrapper => "'",
                          :autoclose => %w[meta img link br hr input area param col base],
                          :attr_delimiter => {'id' => '_', 'class' => ' '}

      def initialize(opts = {})
        super
        # html5 is now called html only
        options[:format] = :html5 if options[:format] == :html
        unless [:xhtml, :html4, :html5].include?(options[:format])
          raise "Invalid format #{options[:format].inspect}"
        end
      end

      def xhtml?
        options[:format] == :xhtml
      end

      def html?
        options[:format] == :html5 || options[:format] == :html4
      end

      def on_html_doctype(type)
        type = type.to_s
        trailing_newlines = type[/(\A|[^\r])(\n+)\Z/, 2].to_s
        text = type.downcase.strip

        if text =~ /^xml/
          raise 'Invalid xml directive in html mode' if html?
          wrapper = options[:attr_wrapper]
          str = "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{text.split(' ')[1] || "utf-8"}#{wrapper} ?>"
        else
          case options[:format]
          when :html5
            str = '<!DOCTYPE html>'
          when :html4
            str = HTML4_DOCTYPES[text] || HTML4_DOCTYPES['transitional']
          when :xhtml
            str = XHTML_DOCTYPES[text] || XHTML_DOCTYPES['transitional']
          end
        end

        str << trailing_newlines
        [:static, str]
      end

      def on_html_comment(content)
        [:multi,
          [:static, '<!--'],
          compile(content),
          [:static, '-->']]
      end

      def on_html_tag(name, attrs, content = nil)
        name = name.to_s
        closed = !content || (empty_exp?(content) && options[:autoclose].include?(name))
        result = [:multi, [:static, "<#{name}"], compile(attrs)]
        result << [:static, (closed && xhtml? ? ' /' : '') + '>']
        result << compile(content) if content
        result << [:static, "</#{name}>"] if !closed
        result
      end

      def on_html_attrs(*attrs)
        result = {}
        attrs.each do |attr|
          raise(InvalidExpression, 'Attribute is not a html attr') if attr[0] != :html || attr[1] != :attr
          name, value = attr[2].to_s, attr[3]
          next if empty_exp?(value)
          if result[name]
            delimiter = options[:attr_delimiter][name]
            raise "Multiple #{name} attributes specified" unless delimiter
            if contains_static?(value)
              result[name] = [:html, :attr, name,
                              [:multi,
                               result[name][3],
                               [:static, delimiter],
                               value]]
            else
              tmp = unique_name
              result[name] = [:html, :attr, name,
                              [:multi,
                               result[name][3],
                               [:capture, tmp, value],
                               [:if, "!#{tmp}.empty?",
                                [:multi,
                                 [:static, delimiter],
                                 [:dynamic, tmp]]]]]
            end
          else
            result[name] = attr
          end
        end
        [:multi, *result.sort.map {|name,attr| compile(attr) }]
      end

      def on_html_attr(name, value)
        if empty_exp?(value)
          compile(value)
        elsif contains_static?(value)
          attribute(name, value)
        else
          tmp = unique_name
          [:multi,
           [:capture, tmp, compile(value)],
           [:if, "!#{tmp}.empty?",
            attribute(name, [:dynamic, tmp])]]
        end
      end

      protected

      def attribute(name, value)
        [:multi,
         [:static, " #{name}=#{options[:attr_wrapper]}"],
         compile(value),
         [:static, options[:attr_wrapper]]]
      end

      def contains_static?(exp)
        case exp[0]
        when :multi
          exp[1..-1].any? {|e| contains_static?(e) }
        when :escape
          contains_static?(exp[2])
        when :static
          true
        else
          false
        end
      end
    end
  end
end