require 'uri'

class Html < String
  ABSOLUTIZE_TAGS = { 'a' => ['href'], 'img' => ['src'] }
        
  def self.def_variants(method, *args)
    arg_def = args.map do |t|
      t.kind_of?(Hash) ? "#{t.keys[0]} = #{t[t.keys[0]]}" : t
    end.join(', ')
    arg_list = args.map{|t|(t.kind_of?(Hash) ? t.keys[0] : t)}.join(', ')
    module_eval <<-EOS
      def #{method}(#{arg_def})
        t = dup
        t.#{method}!(#{arg_list})
        t
      end
      def self.#{method}(t#{args.empty? ? '' : ', ' + arg_def})
        t = new(t)
        t.#{method}!(#{arg_list})
        t.to_s
      end
    EOS
  end
  private_class_method :def_variants
  
  def absolutize!(url, tags = ABSOLUTIZE_TAGS)
    tags.each do |tag,attrs|
      self.gsub!(/<#{tag}\s*([^>]*)>/) do
        args = $~[1]
        attrs.each do |attr|
          args.gsub!(/\b#{attr}=(['"])(.*?)\1/) do
            "#{attr}=#{$1}#{URI.join(url, $2)}#{$1}"
          end
        end
        "<#{tag} #{args}>"
      end
    end
  end
  def_variants :absolutize, :url, {:tags => 'ABSOLUTIZE_TAGS'}
  
  ALLOWED_TAGS = {
    'a' => ['href', 'title'], 'img' => ['src', 'alt', 'title'],
    'br' => nil,
    'i' => nil, 'u' => nil, 'b' => nil,
    'pre' => nil, 'kbd' => nil, 'code' => ['lang'], 'cite' => nil,
    'strong' => nil, 'em' => nil, 'ins' => nil, 'sup' => nil, 'sub' => nil, 'del' => nil,
    'table' => nil, 'tr' => nil, 'td' => ['colspan', 'rowspan'], 'th' => nil,
    'ol' => nil, 'ul' => nil, 'li' => nil,
    'p' => nil,
    'h1' => nil, 'h2' => nil, 'h3' => nil, 'h4' => nil, 'h5' => nil, 'h6' => nil, 
    'blockquote' => ['cite']
  }

  def sanitize!(allowed_tags = ALLOWED_TAGS)
    gsub!(/<!\[CDATA\[(.*)\]\]>/){self.class.escape($1)}
    gsub!(/<!--.*?-->/, '')
    gsub!(/<!.*?>/, '')
    gsub!(/<(\/*)\s*(\w+)([^>]*?)(\/*)>/) do
      close1, tag, arg, close2 = $~[1], $~[2].downcase, $~[3], $~[4]
      if allowed_tags.has_key? tag
        if close1.empty?
          allowed_tags[tag].each do |prop|
            if arg =~ /#{prop}=(['"])(.*?)\1/
              tag << " #{prop}=#{$1}#{$2}#{$1}"
            elsif arg =~ /#{prop}=([^\s]*)/
              tag << " #{prop}=\"#{$1.gsub(/"/, '&quot;')}\""
            end
          end if allowed_tags[tag]
          "<#{tag}#{close2}>"
        else
          "</#{tag}>"
        end
      else
        ' '
      end
    end
  end
  def_variants :sanitize, {:allowed_tags => 'ALLOWED_TAGS'}
  
  ESCAPE_MAPPINGS = {'<' => '&lt;', '>' => '&gt;', '&' => '&amp;', '"' => '&quot;'}.freeze
  
  def escape!
    gsub!(/([<>&"])/) { ESCAPE_MAPPINGS[$1] }
  end
  def_variants :escape
end
