#!/usr/bin/env ruby
# rcov Copyright (c) 2004-2006 Mauricio Fernandez <mfp@acm.org>
#
# rcov originally based on 
# module COVERAGE__ originally (c) NAKAMURA Hiroshi
# module PrettyCoverage originally (c) Simon Strandgaard
#
# rewritten & extended by Mauricio Fernndez <mfp@acm.org>
#
# See LEGAL and LICENSE for additional licensing information.
#

require 'cgi'
require 'rbconfig'
require 'optparse'
require 'ostruct'
include Config
eval DATA.read  # load xx-0.1.0-1

# extend XX
module XX
  module XMLish
    include Markup

    def xmlish_ *a, &b
      xx_which(XMLish){ xx_with_doc_in_effect(*a, &b)}
    end
  end
end

SCRIPT_LINES__ = {} unless defined? SCRIPT_LINES__

module Rcov

VERSION = "0.2.0"
RELEASE_DATE = "2006-02-25"
    
class CoverageInfo
    def initialize(coverage_array)
        @cover = coverage_array
    end

    def [](idx)
        @cover[idx]
    end

    def []=(idx, val)
        unless [true, false].include? val
            raise RuntimeError, "What does #{val} mean?" 
        end
        return if idx < 0 || idx >= @cover.size
        if val
            @cover[idx] = :inferred
        else
            @cover[idx] = false
        end
    end

    def method_missing(meth, *a, &b)
        @cover.send(meth, *a, &b)
    end
end

class SourceFile
    attr_reader :name, :lines, :coverage, :counts
    def initialize(name, lines, initial_coverage, counts)
        @name = name
        @lines = lines
        @coverage = CoverageInfo.new initial_coverage
        @counts = counts
        precompute_coverage false
    end

    def precompute_coverage(comments_run_by_default = true)
        changed = false
        (0...lines.size).each do |i|
            next if @coverage[i]
            line = @lines[i]
            if /^\s*(?:begin\s*(?:#.*)?|ensure\s*(?:#.*)?|else\s*(?:#.*)?)$/ =~line &&
                next_expr_marked?(i) or
                /^\s*(?:end|\})\s*(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
                prev_expr_continued?(i) && prev_expr_marked?(i) or
                comments_run_by_default && /^\s*(#|$)/ =~ line or 
                  /^\s*(?:rescue)/ =~ line && next_expr_marked?(i) or
                  /^\s*case\s*(?:#.*)?$/ =~ line && next_expr_marked?(i) or
                  /^\s*(\)|\]|\})(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
                 prev_expr_continued?(i+1) && next_expr_marked?(i)
                 @coverage[i] = true
                 changed = true
            end
        end
        (@lines.size-1).downto(0) do |i|
            next if @coverage[i]
            if @lines[i] =~ /^\s*(#|$)/ and @coverage[i+1] 
                @coverage[i] = true
                changed = true
            end
        end
        # if there was any change, we have to recompute; we'll eventually
        # reach a fixed point and stop there
        precompute_coverage(comments_run_by_default) if changed
    end

    def total_coverage
        return 0 if @coverage.size == 0
        @coverage.inject(0.0) {|s,a| s + (a ? 1:0) } / @coverage.size
    end

    def code_coverage
        indices = (0...@lines.size).select{|i| is_code? i }
        return 0 if indices.size == 0
        count = 0
        indices.each {|i| count += 1 if @coverage[i] }
        1.0 * count / indices.size
    end

    def num_code_lines
        (0...@lines.size).select{|i| is_code? i}.size
    end

    def num_lines
        @lines.size
    end

    def is_code?(lineno)
        #TODO: handle here docs?
        @lines[lineno] && @lines[lineno] !~ /^\s*(#|$)/ 
    end

    private
    def next_expr_marked?(lineno)
        return false if lineno >= @lines.size
        found = false
        idx = (lineno+1).upto(@lines.size-1) do |i|
            next unless is_code? i
            found = true
            break i
        end
        return false unless found
        @coverage[idx]
    end

    def prev_expr_marked?(lineno)
        return false if lineno <= 0
        found = false
        idx = (lineno-1).downto(0) do |i|
            next unless is_code? i
            found = true
            break i
        end
        return false unless found
        @coverage[idx]
    end

    def prev_expr_continued?(lineno)
        return false if lineno <= 0
        found = false
        idx = (lineno-1).downto(0) do |i|
            next unless is_code? i
            found = true
            break i
        end
        return false unless found
        #TODO: write a comprehensive list
        #TODO: handle here docs?
        #FIXME: / matches regexps too
        r = /(,|\.|\+|-|\*|\/|<|>|%|&&|\|\||<<|\(|\[|\{|=)\s*(?:#.*)?$/.match @lines[idx]
        if /(do|\{)\s*\|.*\|\s*(?:#.*)?$/.match @lines[idx]
            return false
        end
        r
    end
end

class Formatter
    class << self; attr_accessor :ignore_files end 
    @ignore_files = [/\A#{Regexp.escape(CONFIG["libdir"])}/, /tc_/, 
                     /\A#{Regexp.escape(__FILE__)}\z/]
    def initialize
        @files = {}
    end

    def add_file(filename, lines, coverage, counts)
        return nil if Formatter.ignore_files.any?{|x| x === filename}
        @files[filename] = SourceFile.new filename, lines, coverage, counts
    end

    def total_coverage
        lines = 0
        total = 0.0
        @files.each do |k,f| 
            total += f.num_lines * f.total_coverage 
            lines += f.num_lines 
        end
        return 0 if lines == 0
        total / lines
    end

    def code_coverage
        lines = 0
        total = 0.0
        @files.each do |k,f| 
            total += f.num_code_lines * f.code_coverage 
            lines += f.num_code_lines 
        end
        return 0 if lines == 0
        total / lines
    end

    def num_code_lines
        lines = 0
        @files.each{|k, f| lines += f.num_code_lines }
        lines
    end

    def num_lines
        lines = 0
        @files.each{|k, f| lines += f.num_lines }
        lines
    end
end

class HTMLCoverage < Formatter
    include XX::XHTML
    include XX::XMLish
    require 'fileutils'

    CSS_PROLOG = <<-EOS
span.marked {
  background-color: rgb(185, 200, 200);
  display: block;
}
span.inferred {
  background-color: rgb(170, 185, 185);
  display: block;
}
span.overview {
  border-bottom: 8px solid black;
}
div.overview {
  border-bottom: 8px solid black;
}
body {
    font-family: verdana, arial, helvetica;
}

div.footer {
    font-size: 68%;
    margin-top: 1.5em;
}

h1, h2, h3, h4, h5, h6 {
    margin-bottom: 0.5em;
}

h5 {
    margin-top: 0.5em;
}

.hidden {
    display: none;
}

div.separator {
    height: 10px;
}

table tr td, table tr th {
    font-size: 68%;
}

td.value table tr td {
    font-size: 11px;
}

table.percent_graph {
    height: 12px;
    border: #808080 1px solid;
    empty-cells: show;
}

table.percent_graph td.covered {
    height: 10px;
    background: #00f000;
}

table.percent_graph td.uncovered {
    height: 10px;
    background: #e00000;
}

table.percent_graph td.NA {
    height: 10px;
    background: #eaeaea;
}

table.report {
    border-collapse: collapse;
    width: 100%;
}

table.report td.heading {
    background: #dcecff;
    border: #d0d0d0 1px solid;
    font-weight: bold;
    text-align: center;
}

table.report td.heading:hover {
    background: #c0ffc0;
}

table.report td.text {
    border: #d0d0d0 1px solid;
}

table.report td.value {
    text-align: right;
    border: #d0d0d0 1px solid;
}
EOS


    
    DEFAULT_OPTS = {:color, false, :fsr, 30, :textmode, false, :nohtml, false}
    def initialize(dest_dir = ".", opts = {})
        super()
        @dest = dest_dir
        options = DEFAULT_OPTS.clone.update(opts)
        @color = options[:color]
        @fsr = options[:fsr]
        @textmode = options[:textmode]
        @nohtml = options[:nohtml]
    end

    def execute
        return if @files.size == 0
        FileUtils.mkdir_p @dest 
        create_index(File.join(@dest, "index.html")) unless @nohtml
        @files.each do |filename, fileinfo|
            create_file(File.join(@dest, file_name(filename)), fileinfo) unless @nohtml
            next unless @textmode
            puts "=" * 80
            puts filename
            puts "=" * 80
            SCRIPT_LINES__[filename].each_with_index do |line, i|
                case @textmode
                when :plain
                    puts "%-70s| %6d" % [line.chomp, fileinfo.counts[[i-1,0].max]]
                when :rich
                    color = fileinfo.coverage[i] ? "\e[32;40m" : "\e[31;40m"
                    puts "#{color}%s\e[37;40m" % line.chomp
                end
            end
        end
    end

    def file_name(base)
        base.gsub(%r{^\w:[/\\]}, "").gsub(/\./, "_").gsub(/\//, "-") + ".html"
    end

    private

    def output_color_table?
        true
    end

    def default_color
        "rgb(240, 240, 245)"
    end

    def default_title
        "C0 code coverage information"
    end

    def format_overview(*file_infos)
        table_text = xmlish_ {
            table_(:class => "report") {
                thead_ {
                    tr_ { 
                        ["Name", "Total lines", "Lines of code", "Total coverage",
                         "Code coverage"].each do |heading|
                            td_(:class => "heading") { heading }
                         end
                    }
                }
                tbody_ { 
                    file_infos.each do |f|
                        tr_ {
                            td_ { 
                                case f.name
                                when "TOTAL": 
                                    t_ { "TOTAL" }
                                else
                                    a_(:href => file_name(f.name)){ t_ { f.name } } 
                                end
                            }
                            [f.num_lines, f.num_lines].each do |value| 
                                td_(:class => "value") { tt_{ f.num_lines } }
                            end
                            [f.total_coverage, f.code_coverage].each do |value|
                                value *= 100
                                td_ { 
                                    table_(:cellpadding => 0, :cellspacing => 0, :align => "right") { 
                                        tr_ { 
                                            td_ {
                                                 tt_ { "%02.1f%%" % value } 
                                                 x_ "&nbsp;"
                                            }
                                            td_ {
                                                table_(:class => "percent_graph", :cellpadding => 0,
                                                   :cellspacing => 0, :width => 100) {
                                                    tr_ {
                                                        td_(:class => "covered", :width => value)
                                                        td_(:class => "uncovered", :width => (100-value))
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            end
                        }
                    end
                }
            }
        }
        table_text.pretty
    end

    class SummaryFileInfo 
        def initialize(obj); @o = obj end
        %w[num_lines num_code_lines code_coverage total_coverage].each do |m|
            define_method(m){ @o.send(m) }
        end
        def name; "TOTAL" end
    end

    def create_index(destname)
        files = [SummaryFileInfo.new(self)] + @files.keys.sort.map{|k| @files[k]}
        title = default_title
        output = xhtml_ {
            head_ { title_{ title } }
            style_(:type => "text/css") { t_{ "body { background-color: #{default_color}; }" }  }
            style_(:type => "text/css") { CSS_PROLOG }
            body_ {
                h3_{ "#{title} generated on #{Time.new.to_s}" }
                hr_
                x_{ format_overview(*files) }
                hr_
                p_ {
                    a_(:href => "http://validator.w3.org/check/referer") {
                    img_(:src => "http://www.w3.org/Icons/valid-xhtml11",
                         :alt => "Valid XHTML 1.1!", :heigth => 31, :width => 88)
                    }
                    a_(:href => "http://jigsaw.w3.org/css-validator/check/referer") {
                        img_(:style => "border:0;width:88px;height:31px",
                             :src => "http://jigsaw.w3.org/css-validator/images/vcss",
                             :alt => "Valid CSS!")
                    }
                }
            }
        }
        File.open(destname, "w") do |f|
            f.puts output.pretty
        end
    end

    def format_lines(file)
        result = ""
        last = nil
        end_of_span = ""
        format_line = "%#{file.num_lines.to_s.size}d"
        file.num_lines.times do |i|
            line = file.lines[i]
            marked = file.coverage[i]
            count = file.counts[[i-1,0].max]
            spanclass = span_class(file, marked, count)
            if spanclass != last
                result += end_of_span
                case spanclass
                when nil
                    end_of_span = ""
                else
                    result += "<span class=\"#{spanclass}\">"
                    end_of_span = "</span>"
                end
            end
            result += (format_line % (i+1)) + " " + CGI.escapeHTML(line) + "\n"
            last = spanclass
        end
        result += end_of_span
        "<pre>#{result}</pre>"
    end

    def span_class(sourceinfo, marked, count)
        case marked
        when true
            "marked"
        when :inferred
            "inferred"
        else 
            false
        end
    end

    def create_file(destfile, fileinfo)
        $stderr.puts "Generating #{destfile.inspect}"
        body = format_overview(fileinfo) + format_lines(fileinfo)
        title = fileinfo.name + " - #{default_title}"
        do_ctable = output_color_table?
        output = xhtml_ { html_ {
            head_ { 
                title_{ title } 
                style_(:type => "text/css") { t_{ "body { background-color: #{default_color}; }" }  }
                style_(:type => "text/css") { CSS_PROLOG }
                style_(:type => "text/css") { h_ { colorscale } }
            }
            body_ {
                h3_{ "#{default_title} generated on #{Time.new.to_s}" }
                hr_
                if do_ctable
                    pre_ {
                        span_(:class => "marked") { t_ { <<-EOS
Marked code looks like this.
This line is also marked as covered.
                        EOS
                        } }
                        span_(:class => "inferred") { t_ { <<-EOS
Lines considered as run by rcov, but not reported by Ruby, look like this.
                        EOS
#Ruby doesn't signal all the events it could in multi-line statements: for 
#instance, an 'end' block/class/module/method terminator will not be reported
#as run (and sure enough no event is reported for blank lines). rcov uses some
#heuristics to infer which lines have actually been executed (for a broad
#definition which comprises blank lines and comments) to cope with that.
#These heuristics are far from perfect and represent work in progress.
                        } }
                        span_(:class => "false") { t_ { <<-EOS
Finally, here's a line marked as not executed.
                        EOS
                        } }
                    }
                end
                x_{ body }
                hr_
                p_ {
                    a_(:href => "http://validator.w3.org/check/referer") {
                    img_(:src => "http://www.w3.org/Icons/valid-xhtml10",
                         :alt => "Valid XHTML 1.0!", :height => 31, :width => 88)
                    }
                    a_(:href => "http://jigsaw.w3.org/css-validator/check/referer") {
                        img_(:style => "border:0;width:88px;height:31px",
                             :src => "http://jigsaw.w3.org/css-validator/images/vcss",
                             :alt => "Valid CSS!")
                    }
                }
            }
        } }
        File.open(destfile, "w") do |f|
            f.puts output
        end
    end

    def colorscale
        colorscalebase =<<EOF
span.run%d {
  background-color: rgb(%d, %d, %d);
  display: block;
}
EOF
        cscale = ""
        101.times do |i|
            if @color
                r, g, b = hsv2rgb(220-(2.2*i).to_i, 0.3, 1)
                r = (r * 255).to_i
                g = (g * 255).to_i
                b = (b * 255).to_i
            else
                r = g = b = 255 - i 
            end
            cscale << colorscalebase % [i, r, g, b]
        end
        cscale
    end

    # thanks to kig @ #ruby-lang for this one
    def hsv2rgb(h,s,v)
        return [v,v,v] if s == 0
        h = h/60.0
        i = h.floor
        f = h-i
        p = v * (1-s)
        q = v * (1-s*f)
        t = v * (1-s*(1-f))
        case i
        when 0
            r = v
            g = t
            b = p
        when 1
            r = q
            g = v
            b = p
        when 2
            r = p
            g = v
            b = t
        when 3
            r = p
            g = q
            b = v
        when 4
            r = t
            g = p
            b = v
        when 5
            r = v
            g = p
            b = q
        end
        [r,g,b]
    end
end

class HTMLProfiling < HTMLCoverage

    def initialize(*a)
        super
        @max_cache = {}
        @median_cache = {}
    end
    
    def default_title
        "Bogo-profile information"
    end
    
    def default_color
        if @color
            "rgb(179,205,255)"
        else
            "rgb(255, 255, 255)"
        end
    end

    def output_color_table?
        false
    end

    def span_class(sourceinfo, marked, count)
        full_scale_range = @fsr # dB
        nz_count = sourceinfo.counts.select{|x| x && x != 0}
        nz_count << 1 # avoid div by 0
        max = @max_cache[sourceinfo] ||= nz_count.max
        #avg = @median_cache[sourceinfo] ||= 1.0 * 
        #    nz_count.inject{|a,b| a+b} / nz_count.size
        median = @median_cache[sourceinfo] ||= 1.0 * nz_count.sort[nz_count.size/2]
        max ||= 2
        max = 2 if max == 1
        if marked == true
            count = 1 if !count || count == 0
            idx = 50 + 1.0 * (500/full_scale_range) * Math.log(count/median) /
                Math.log(10)
            idx = idx.to_i
            idx = 0 if idx < 0
            idx = 100 if idx > 100
            "run#{idx}"
        else 
            nil
        end
    end
end


module RCOV__
    COVER = {}
    class << self; attr_accessor :formatter end
    
    @formatter = Rcov::HTMLCoverage.new "coverage"
    #@formatter = Rcov::HTMLProfiling.new "profiling", true, 10
    # the range has to be adjusted depending on what we want to see
    # and the app. (+-10--40dB are not bad)
    
    begin
        require 'rcovrt'
    rescue LoadError
        def self.install_hook
            set_trace_func lambda {|event, file, line, id, binding, klass|
                case event
                when 'c-call', 'c-return', 'class'
                    return
                end
                COVER[file] ||= []
                COVER[file][line - 1] ||= 0
                COVER[file][line - 1] += 1
            }
        end

        def self.remove_hook
            set_trace_func(nil)
        end
    end

    def self.reset
        COVER.replace {}
    end

    END {
        remove_hook
#Rcov::RCOV__::COVER.each_pair do |name, counts|
#    next if Formatter.ignore_files.any?{|x| x === filename}
#    next unless SCRIPT_LINES__[name]
#    puts "=" * 80
#    puts name
#    puts "=" * 80
#    SCRIPT_LINES__[name].each_with_index do |line, i|
#        puts "%-70s: %5d" % [line.chomp, counts[i]]
#    end
#end
        COVER.each do |file, lines|
            next if SCRIPT_LINES__.has_key?(file) == false
            lines = SCRIPT_LINES__[file]
            covers = COVER[file]
            line_info = []
            marked_info = []
            cover_info = []
            0.upto(lines.size - 1) do |c|
                line = lines[c].chomp
                marked = false
                marked = true if covers[c] && covers[c] > 0
                line_info << line
                marked_info << marked
                cover_info << (covers[c+1] || 0)
            end
            RCOV__.formatter.add_file(file, line_info, marked_info,
                                          cover_info)
        end
        RCOV__.formatter.execute
    }
end

end # Rcov


options = OpenStruct.new
options.color = true
options.range = 30.0
options.profiling = false
options.destdir = nil
options.loadpaths = []
options.textmode = false
options.skip = Rcov::Formatter.ignore_files
options.nohtml = false

EXTRA_HELP = <<-EOF

You can run several programs at once:
  rcov something.rb somethingelse.rb

The parameters to be passed to the program under inspection can be specified
after --:

  rcov -Ilib -t something.rb -- --theseopts --are --given --to --something.rb

If you run several programs, they will all be passed the same command-line
arguments. Keep in mind that all the programs are run under the same process
(i.e. they just get Kernel#load()'ed in sequence).
EOF

opts = OptionParser.new do |opts|
    opts.banner = <<-EOF
rcov #{Rcov::VERSION} #{Rcov::RELEASE_DATE}
Usage: rcov [options] <script1.rb> [script2.rb] [-- --extra-options]
EOF
    opts.separator ""
    opts.separator "Options:"
    opts.on("-o", "--output PATH", "Destination directory.") do |dir|
        options.destdir = dir
    end
    opts.on("-I", "--include PATHS", 
            "Prepend PATHS to $: (colon separated list)") do |paths|
                options.loadpaths = paths.split(/:/)
            end
    opts.on("-n", "--no-color", "Create colorblind-safe output.") do
        options.color = false
    end
    opts.on("-x", "--exclude PATTERNS", 
            "Don't generate info for the files matching any",
            "of the given patterns (comma-separated regexp list)") do |list|
                regexps = list.split(/,/).map{|x| Regexp.new x}
                options.skip += regexps
            end
    opts.on("-p", "--profile", "Generate bogo-profiling info.") do
        options.profiling = true
        options.destdir ||= "profiling"
    end
    opts.on("-r", "--range RANGE", Float, 
            "Color scale range for profiling info (dB).") do |val|
        options.range = val
    end
    opts.on("-t", "--text", "Output coverage info in plaintext to stdout.") do
        options.textmode = :plain
    end
    opts.on("-T", "--rich-text", "Output coverage info with ANSI sequences to stdout.") do
        options.textmode = :rich
    end
    opts.on("--no-html", "Don't generate HTML output (no output unless -T or -t).") do
        options.nohtml = true
    end
    opts.separator ""
    opts.on_tail("-h", "--help", "Show this message") do
        puts opts
        puts EXTRA_HELP
        exit
    end
    opts.on_tail("--version", "Show version") do
        puts "rcov " + Rcov::VERSION + " " + Rcov::RELEASE_DATE
        exit
    end
end

if (idx = ARGV.index("--"))
    extra_args = ARGV[idx+1..-1]
    ARGV.replace(ARGV[0,idx])
else
    extra_args = []
end

opts.parse! ARGV
options.destdir ||= "coverage"
unless ARGV[0]
    puts opts
    exit
end
if options.profiling
    klass = Rcov::HTMLProfiling
else
    klass = Rcov::HTMLCoverage
end
Rcov::RCOV__.formatter = klass.new options.destdir, :color => options.color, 
                                       :fsr => options.range, :textmode => options.textmode,
                                       :nohtml => options.nohtml

Rcov::Formatter.ignore_files = options.skip

Rcov::RCOV__.install_hook
options.loadpaths.reverse_each{|x| $:.unshift x}

pending_scripts = ARGV.clone
ARGV.replace extra_args
until pending_scripts.empty?
    prog = pending_scripts.shift
    $0 = prog
    load prog
end

# xx-0.1.0-1 follows
__END__
# xx can be redistributed and used under the following conditions
# (just keep the following copyright notice, list of conditions and disclaimer
# in order to satisfy rcov's "Ruby license" and xx's license simultaneously).
# 
#ePark Labs Public License version 1
#Copyright (c) 2005, ePark Labs, Inc. and contributors
#All rights reserved.
#
#Redistribution and use in source and binary forms, with or without modification,
#are permitted provided that the following conditions are met:
#
#  1. Redistributions of source code must retain the above copyright notice, this
#     list of conditions and the following disclaimer.
#  2. Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions and the following disclaimer in the documentation
#     and/or other materials provided with the distribution.
#  3. Neither the name of ePark Labs nor the names of its contributors may be
#     used to endorse or promote products derived from this software without
#     specific prior written permission.
#
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
#DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
#ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
#(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
#LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
#ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
#(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
#SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

unless defined? $__xx_rb__

require "rexml/document"


module XX
#--{{{
  VERSION = "0.1.0"

  %w(
    CRAZY_LIKE_A_HELL
    PERMISSIVE
    STRICT
    ANY
  ).each{|c| const_set c, c}

  class Document
#--{{{
    attr "doc"
    attr "stack"
    attr "size"

    def initialize *a, &b
#--{{{
      @doc = ::REXML::Document::new(*a, &b)
      @stack = [@doc]
      @size = 0
#--}}}
    end
    def top
#--{{{
      @stack.last
#--}}}
    end
    def push element
#--{{{
      @stack.push element
#--}}}
    end
    def pop
#--{{{
      @stack.pop unless @stack.size == 1
#--}}}
    end
    def tracking_additions
#--{{{
      n = @size
      yield
      return @size - n
#--}}}
    end
    def to_str port = ""
#--{{{
      @doc.write port, indent=-1, transitive=false, ie_hack=true
      port
#--}}}
    end
    alias_method "to_s", "to_str"
    def pretty port = '' 
#--{{{
      @doc.write port, indent=2, transitive=false, ie_hack=true
      port
#--}}}
    end
    def create element
#--{{{
      push element
      begin
        object = nil
        additions =
          tracking_additions do
            object = yield element if block_given?
          end
        if object and additions.zero?
          self << object
        end
      ensure
        pop
      end
      self << element
      element
#--}}}
    end
    def << object
#--{{{
      t, x = top, object

      if x
        case t
          when ::REXML::Document

            begin
              t <<
                case x
                  when ::REXML::Document
                    x.root || ::REXML::Text::new(x.to_s)
                  when ::REXML::Element
                    x
                  when ::REXML::CData
                    x
                  when ::REXML::Text
                    x
                  else # string
                    ::REXML::Text::new(x.to_s)
                end
            rescue
              if t.respond_to? "root"
                t = t.root
                retry
              else
                raise
              end
            end

          when ::REXML::Element
            t <<
              case x
                when ::REXML::Document
                  x.root || ::REXML::Text::new(x.to_s)
                when ::REXML::Element
                  x
                when ::REXML::CData
                  #::REXML::Text::new(x.write(""))
                  x
                when ::REXML::Text
                  x
                else # string
                  ::REXML::Text::new(x.to_s)
              end

          when ::REXML::Text
            t <<
              case x
                when ::REXML::Document
                  x.write ""
                when ::REXML::Element
                  x.write ""
                when ::REXML::CData
                  x.write ""
                when ::REXML::Text
                  x.write ""
                else # string
                  x.to_s
              end

          else # other - try anyhow 
            t <<
              case x
                when ::REXML::Document
                  x.write ""
                when ::REXML::Element
                  x.write ""
                when ::REXML::CData
                  x.write ""
                when ::REXML::Text
                  x.write ""
                else # string
                  x.to_s
              end
        end
      end

      @size += 1
      self
#--}}}
    end
#--}}}
  end

  module Markup
#--{{{
    class Error < ::StandardError; end

    module InstanceMethods
#--{{{
      def method_missing m, *a, &b
#--{{{
        m = m.to_s

        tag_method, tag_name = xx_class::xx_tag_method_name m

        c_method_missing = xx_class::xx_config_for "method_missing", xx_which
        c_tags = xx_class::xx_config_for "tags", xx_which

        pat =
          case c_method_missing
            when ::XX::CRAZY_LIKE_A_HELL
              %r/.*/
            when ::XX::PERMISSIVE
              %r/_$/o
            when ::XX::STRICT
              %r/_$/o
            else
              super
          end

        super unless m =~ pat

        if c_method_missing == ::XX::STRICT
          super unless c_tags.include? tag_name
        end

        ret, defined = nil

        begin
          xx_class::xx_define_tmp_method tag_method
          xx_class::xx_define_tag_method tag_method, tag_name
          ret = send tag_method, *a, &b
          defined = true
        ensure
          xx_class::xx_remove_tag_method tag_method unless defined
        end

        ret
#--}}}
      end
      def xx_tag_ tag_name, *a, &b
#--{{{
        tag_method, tag_name = xx_class::xx_tag_method_name tag_name 

        ret, defined = nil

        begin
          xx_class::xx_define_tmp_method tag_method
          xx_class::xx_define_tag_method tag_method, tag_name
          ret = send tag_method, *a, &b
          defined = true
        ensure
          xx_class::xx_remove_tag_method tag_method unless defined
        end

        ret
#--}}}
      end
      alias_method "g_", "xx_tag_"
      def xx_which *argv 
#--{{{
        @xx_which = nil unless defined? @xx_which
        if argv.empty?
          @xx_which
        else
          xx_which = @xx_which
          begin
            @xx_which = argv.shift 
            return yield
          ensure
            @xx_which = xx_which
          end
        end
#--}}}
      end
      def xx_with_doc_in_effect *a, &b
#--{{{
        @xx_docs ||= []
        doc = ::XX::Document::new(*a)
        ddoc = doc.doc
        begin
          @xx_docs.push doc
          b.call doc if b

          doctype = xx_config_for "doctype", xx_which
          if doctype
            unless ddoc.doctype
              doctype = ::REXML::DocType::new doctype unless 
                ::REXML::DocType === doctype
              ddoc << doctype
            end
          end

          xmldecl = xx_config_for "xmldecl", xx_which
          if xmldecl
            if ddoc.xml_decl == ::REXML::XMLDecl::default
              xmldecl = ::REXML::XMLDecl::new xmldecl unless
                ::REXML::XMLDecl === xmldecl
              ddoc << xmldecl
            end
          end

          return doc
        ensure
          @xx_docs.pop
        end
#--}}}
      end
      def xx_doc
#--{{{
        @xx_docs.last rescue raise "no xx_doc in effect!"
#--}}}
      end
      def xx_text_ *objects, &b
#--{{{
        doc = xx_doc

        text =
          ::REXML::Text::new("", 
            respect_whitespace=true, parent=nil
          )

        objects.each do |object| 
          text << object.to_s if object
        end

        doc.create text, &b
#--}}}
      end
      alias_method "text_", "xx_text_"
      alias_method "t_", "xx_text_"
      def xx_markup_ *objects, &b
#--{{{
        doc = xx_doc

        doc2 = ::REXML::Document::new ""

        objects.each do |object| 
          (doc2.root ? doc2.root : doc2) << ::REXML::Document::new(object.to_s)
        end


        ret = doc.create doc2, &b
        puts doc2.to_s
        STDIN.gets
        ret
#--}}}
      end
      alias_method "x_", "xx_markup_"
      def xx_any_ *objects, &b
#--{{{
        doc = xx_doc
        nothing = %r/.^/m

        text =
          ::REXML::Text::new("", 
            respect_whitespace=true, parent=nil, raw=true, entity_filter=nil, illegal=nothing
          )

        objects.each do |object| 
          text << object.to_s if object
        end

        doc.create text, &b
#--}}}
      end
      alias_method "h_", "xx_any_"
      remove_method "x_" if instance_methods.include? "x_"
      alias_method "x_", "xx_any_" # supplant for now
      def xx_cdata_ *objects, &b
#--{{{
        doc = xx_doc

        cdata = ::REXML::CData::new ""

        objects.each do |object| 
          cdata << object.to_s if object
        end

        doc.create cdata, &b
#--}}}
      end
      alias_method "c_", "xx_cdata_"
      def xx_parse_attributes string
#--{{{
        string = string.to_s
        tokens = string.split %r/,/o
        tokens.map{|t| t.sub!(%r/[^=]+=/){|key_eq| key_eq.chop << " : "}}
        xx_parse_yaml_attributes(tokens.join(','))
#--}}}
      end
      alias_method "att_", "xx_parse_attributes"
      def xx_parse_yaml_attributes string
#--{{{
        require "yaml"
        string = string.to_s
        string = "{" << string unless string =~ %r/^\s*[{]/o
        string = string << "}" unless string =~ %r/[}]\s*$/o
        obj = ::YAML::load string
        raise ArgumentError, "<#{ obj.class }> not Hash!" unless Hash === obj
        obj
#--}}}
      end
      alias_method "at_", "xx_parse_yaml_attributes"
      alias_method "yat_", "xx_parse_yaml_attributes"
      def xx_class
#--{{{
        @xx_class ||= self.class
#--}}}
      end
      def xx_tag_method_name *a, &b 
#--{{{
        xx_class.xx_tag_method_name(*a, &b)
#--}}}
      end
      def xx_define_tmp_method *a, &b 
#--{{{
        xx_class.xx_define_tmp_methodr(*a, &b)
#--}}}
      end
      def xx_define_tag_method *a, &b 
#--{{{
        xx_class.xx_define_tag_method(*a, &b)
#--}}}
      end
      def xx_remove_tag_method *a, &b 
#--{{{
        xx_class.xx_tag_remove_method(*a, &b)
#--}}}
      end
      def xx_ancestors
#--{{{
        raise Error, "no xx_which in effect" unless xx_which
        xx_class.xx_ancestors xx_which
#--}}}
      end
      def xx_config
#--{{{
        xx_class.xx_config
#--}}}
      end
      def xx_config_for *a, &b
#--{{{
        xx_class.xx_config_for(*a, &b)
#--}}}
      end
      def xx_configure *a, &b
#--{{{
        xx_class.xx_configure(*a, &b)
#--}}}
      end
#--}}}
    end

    module ClassMethods
#--{{{
      def xx_tag_method_name m
#--{{{
        m = m.to_s
        tag_method, tag_name = m, m.gsub(%r/_+$/, "")
        [ tag_method, tag_name ]
#--}}}
      end
      def xx_define_tmp_method m 
#--{{{
        define_method(m){ raise NotImplementedError, m.to_s }
#--}}}
      end
      def xx_define_tag_method tag_method, tag_name = nil
#--{{{
        tag_method = tag_method.to_s
        tag_name ||= tag_method.gsub %r/_+$/, ""

        remove_method tag_method if instance_methods.include? tag_method
        module_eval <<-code, __FILE__, __LINE__+1
          def #{ tag_method } *a, &b
            hashes, nothashes = a.partition{|x| Hash === x}

            doc = xx_doc
            element = ::REXML::Element::new '#{ tag_name }'

            hashes.each{|h| h.each{|k,v| element.add_attribute k.to_s, v}}
            nothashes.each{|nh| element << ::REXML::Text::new(nh.to_s)}

            doc.create element, &b
          end
        code
        tag_method
#--}}}
      end
      def xx_remove_tag_method tag_method
#--{{{
        remove_method tag_method rescue nil
#--}}}
      end
      def xx_ancestors xx_which = self
#--{{{
        list = []
        ancestors.each do |a|
          list << a if a < xx_which
        end
        xx_which.ancestors.each do |a|
          list << a if a <= Markup
        end
        list
#--}}}
      end
      def xx_config
#--{{{
        @@xx_config ||= Hash::new{|h,k| h[k] = {}}
#--}}}
      end
      def xx_config_for key, xx_which = nil 
#--{{{
        key = key.to_s 
        xx_which ||= self
        xx_ancestors(xx_which).each do |a|
          if xx_config[a].has_key? key
            return xx_config[a][key]
          end
        end
        nil
#--}}}
      end
      def xx_configure key, value, xx_which = nil 
#--{{{
        key = key.to_s
        xx_which ||= self
        xx_config[xx_which][key] = value
#--}}}
      end
#--}}}
    end

    extend ClassMethods
    include InstanceMethods

    def self::included other, *a, &b
#--{{{
      ret = super
      other.module_eval do
        include Markup::InstanceMethods
        extend Markup::ClassMethods
        class << self
          define_method("included", Markup::XX_MARKUP_RECURSIVE_INCLUSION_PROC)
        end
      end
      ret
#--}}}
    end
    XX_MARKUP_RECURSIVE_INCLUSION_PROC = method("included").to_proc

    xx_configure "method_missing", XX::PERMISSIVE
    xx_configure "tags", []
    xx_configure "doctype", nil
    xx_configure "xmldecl", nil
#--}}}
  end

  module XHTML
#--{{{
    include Markup
    xx_configure "doctype", %(html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd")

    def xhtml_ which = XHTML, *a, &b
#--{{{
      xx_which(which) do
        doc = xx_with_doc_in_effect(*a, &b)
        ddoc = doc.doc
        root = ddoc.root
        if root and root.name and root.name =~ %r/^html$/i 
          if root.attribute("lang",nil).nil? or root.attribute("lang",nil).to_s.empty?
            root.add_attribute "lang", "en"
          end
          if root.attribute("xml:lang").nil? or root.attribute("xml:lang").to_s.empty?
            root.add_attribute "xml:lang", "en"
          end
          if root.namespace.nil? or root.namespace.to_s.empty?
            root.add_namespace "http://www.w3.org/1999/xhtml"
          end
        end
        doc
      end
#--}}}
    end

    module Strict
#--{{{
      include XHTML
      xx_configure "doctype", %(html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd")
      xx_configure "tags", %w(
        html head body div span DOCTYPE title link meta style p
        h1 h2 h3 h4 h5 h6 strong em abbr acronym address bdo blockquote cite q code
        ins del dfn kbd pre samp var br a base img
        area map object param ul ol li dl dt dd table
        tr td th tbody thead tfoot col colgroup caption form input
        textarea select option optgroup button label fieldset legend script noscript b
        i tt sub sup big small hr
      )
      xx_configure "method_missing", ::XX::STRICT

      def xhtml_ which = XHTML::Strict, *a, &b
#--{{{
        super(which, *a, &b)
#--}}}
      end
#--}}}
    end

    module Transitional
#--{{{
      include XHTML
      xx_configure "doctype", %(html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd")
      def xhtml_ which = XHTML::Transitional, *a, &b
#--{{{
        super(which, *a, &b)
#--}}}
      end
#--}}}
    end
#--}}}
  end

  module HTML4
#--{{{
    include Markup
    xx_configure "doctype", %(html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN")
 
    def html4_ which = HTML4, *a, &b
#--{{{
      xx_which(which){ xx_with_doc_in_effect(*a, &b) }
#--}}}
    end

    module Strict
#--{{{
      include HTML4
      xx_configure "doctype", %(html PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN")
      xx_configure "tags", %w(
        html head body div span DOCTYPE title link meta style p
        h1 h2 h3 h4 h5 h6 strong em abbr acronym address bdo blockquote cite q code
        ins del dfn kbd pre samp var br a base img
        area map object param ul ol li dl dt dd table
        tr td th tbody thead tfoot col colgroup caption form input
        textarea select option optgroup button label fieldset legend script noscript b
        i tt sub sup big small hr
      )
      xx_configure "method_missing", ::XX::STRICT
      def html4_ which = HTML4::Strict, *a, &b
#--{{{
        super(which, *a, &b)
#--}}}
      end
#--}}}
    end

    module Transitional
#--{{{
      include HTML4
      xx_configure "doctype", %(html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN")
      def html4_ which = HTML4::Transitional, *a, &b
#--{{{
        super(which, *a, &b)
#--}}}
      end
#--}}}
    end
#--}}}
  end
  HTML = HTML4

  module XML
#--{{{
    include Markup
    xx_configure "xmldecl", ::REXML::XMLDecl::new

    def xml_ *a, &b
#--{{{
      xx_which(XML){ xx_with_doc_in_effect(*a, &b)}
#--}}}
    end
#--}}}
  end
#--}}}
end

$__xx_rb__ = __FILE__
end










#
# simple examples - see samples/ dir for more complete examples
#

if __FILE__ == $0

  class Table < ::Array
    include XX::XHTML::Strict
    include XX::HTML4::Strict
    include XX::XML

    def doc 
      html_{
        head_{ title_{ "xhtml/html4/xml demo" } }

        div_{
          h_{ "< malformed html & un-escaped symbols" }
        }

        t_{ "escaped & text > <" }

        x_{ "<any_valid> xml </any_valid>" }

        div_(:style => :sweet){ 
          em_ "this is a table"

          table_(:width => 42, :height => 42){
            each{|row| tr_{ row.each{|cell| td_ cell } } }
          }
        }

        script_(:type => :dangerous){ cdata_{ "javascript" } }
      }
    end
    def to_xhtml
      xhtml_{ doc }
    end
    def to_html4
      html4_{ doc }
    end
    def to_xml
      xml_{ doc }
    end
  end

  table = Table[ %w( 0 1 2 ), %w( a b c ) ]
  
  methods = %w( to_xhtml to_html4 to_xml )

  methods.each do |method|
    2.times{ puts "-" * 42 }
    puts(table.send(method).pretty)
    puts
  end

end
# vi: set sw=4: 
