#! /home/batsman/usr/bin/ruby # rcov Copyright (c) 2004-2006 Mauricio Fernandez # # rcov originally based on # module COVERAGE__ originally (c) NAKAMURA Hiroshi # module PrettyCoverage originally (c) Simon Strandgaard # # rewritten & extended by Mauricio Fernández # # See LEGAL and LICENSE for additional licensing information. # require 'cgi' require 'rbconfig' require 'optparse' require 'ostruct' # load xx-0.1.0-1 eval File.read(File.expand_path(__FILE__)).gsub(/.*^__END__$/m,"") # 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__ require 'rcov/version' module Rcov class Formatter ignore_files = [/\A#{Regexp.escape(Config::CONFIG["libdir"])}/, /\btc_[^.]*.rb/, /_test\.rb\z/, /\btest\//, /\bvendor\//, /\A#{Regexp.escape(__FILE__)}\z/] DEFAULT_OPTS = {:ignore => ignore_files, :sort => :name, :sort_reverse => false, :output_threshold => 101, :dont_ignore => [], :callsite_analyzer => nil} def initialize(opts = {}) options = DEFAULT_OPTS.clone.update(opts) @files = {} @ignore_files = options[:ignore] @dont_ignore_files = options[:dont_ignore] @sort_criterium = case options[:sort] when :loc : lambda{|fname, finfo| finfo.num_code_lines} when :coverage : lambda{|fname, finfo| finfo.code_coverage} else lambda{|fname, finfo| fname} end @sort_reverse = options[:sort_reverse] @output_threshold = options[:output_threshold] @callsite_analyzer = options[:callsite_analyzer] @callsite_index = nil end def add_file(filename, lines, coverage, counts) old_filename = filename filename = normalize_filename(filename) SCRIPT_LINES__[filename] = SCRIPT_LINES__[old_filename] if @ignore_files.any?{|x| x === filename} && !@dont_ignore_files.any?{|x| x === filename} return nil end if @files[filename] @files[filename].merge(lines, coverage, counts) else @files[filename] = FileStatistics.new filename, lines, counts end end def normalize_filename(filename) File.expand_path(filename).gsub(/^#{Regexp.escape(Dir.getwd)}\//, '') end def each_file_pair_sorted(&b) return sorted_file_pairs unless block_given? sorted_file_pairs.each(&b) end def sorted_file_pairs pairs = @files.sort_by do |fname, finfo| @sort_criterium.call(fname, finfo) end.select{|_, finfo| 100 * finfo.code_coverage < @output_threshold} @sort_reverse ? pairs.reverse : pairs 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 private def cross_references_for(filename, lineno) return nil unless @callsite_analyzer @callsite_index ||= build_callsite_index @callsite_index[normalize_filename(filename)][lineno] end def build_callsite_index index = Hash.new{|h,k| h[k] = {}} @callsite_analyzer.analyzed_classes.each do |classname| @callsite_analyzer.analyzed_methods(classname).each do |methname| defsite = @callsite_analyzer.defsite(classname, methname) index[normalize_filename(defsite.file)][defsite.line] = @callsite_analyzer.callsites(classname, methname) end end index end end class TextSummary < Formatter def execute puts summary end def summary "%.1f%% %d file(s) %d Lines %d LOC" % [code_coverage * 100, @files.size, num_lines, num_code_lines] end end class TextReport < TextSummary def execute print_lines print_header print_lines each_file_pair_sorted do |fname, finfo| name = fname.size < 52 ? fname : "..." + fname[-48..-1] print_info(name, finfo.num_lines, finfo.num_code_lines, finfo.code_coverage) end print_lines print_info("Total", num_lines, num_code_lines, code_coverage) print_lines puts summary end def print_info(name, lines, loc, coverage) puts "|%-51s | %5d | %5d | %5.1f%% |" % [name, lines, loc, 100 * coverage] end def print_lines puts "+----------------------------------------------------+-------+-------+--------+" end def print_header puts "| File | Lines | LOC | COV |" end end class TextCoverage < Formatter DEFAULT_OPTS = {:textmode => :coverage} def initialize(opts = {}) options = DEFAULT_OPTS.clone.update(opts) @textmode = options[:textmode] @color = options[:color] super(options) end def execute each_file_pair_sorted do |filename, fileinfo| puts "=" * 80 puts filename puts "=" * 80 SCRIPT_LINES__[filename].each_with_index do |line, i| case @textmode when :counts puts "%-70s| %6d" % [line.chomp[0,70], fileinfo.counts[i]] when :coverage if @color prefix = fileinfo.coverage[i] ? "\e[32;40m" : "\e[31;40m" puts "#{prefix}%s\e[37;40m" % line.chomp else prefix = fileinfo.coverage[i] ? " " : "## " puts "#{prefix}#{line}" end end end end end end class HTMLCoverage < Formatter include XX::XHTML include XX::XMLish require 'fileutils' JAVASCRIPT_PROLOG = <<-EOS // span.cross-ref { display: none }" ) // ]]> EOS CSS_PROLOG = <<-EOS span.marked0 { background-color: rgb(185, 210, 200); display: block; } span.marked1 { background-color: rgb(190, 215, 205); display: block; } span.inferred0 { background-color: rgb(175, 200, 200); display: block; } span.inferred1 { background-color: rgb(180, 205, 205); display: block; } span.uncovered0 { background-color: rgb(225, 110, 110); display: block; } span.uncovered1 { background-color: rgb(235, 120, 120); 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; } /* Commented out for better readability, esp. on IE */ /* 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; } table.report tr.light { background-color: rgb(240, 240, 245); } table.report tr.dark { background-color: rgb(230, 230, 235); } EOS DEFAULT_OPTS = {:color => false, :fsr => 30, :destdir => "coverage"} def initialize(opts = {}) options = DEFAULT_OPTS.clone.update(opts) super(options) @dest = options[:destdir] @color = options[:color] @fsr = options[:fsr] @span_class_index = 0 end def execute return if @files.empty? FileUtils.mkdir_p @dest create_index(File.join(@dest, "index.html")) each_file_pair_sorted do |filename, fileinfo| create_file(File.join(@dest, mangle_filename(filename)), fileinfo) end end def mangle_filename(base) base.gsub(%r{^\w:[/\\]}, "").gsub(/\./, "_").gsub(/[\\\/]/, "-") + ".html" end private def blurb xmlish_ { p_ { t_{ "Generated using the " } a_(:href => "http://eigenclass.org/hiki.rb?rcov") { t_{ "rcov code coverage analysis tool for Ruby" } } t_{ " version #{Rcov::VERSION}." } } }.pretty end 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_ { color_class_index = 1 color_classes = %w[light dark] file_infos.each do |f| color_class_index += 1 color_class_index %= color_classes.size tr_(:class => color_classes[color_class_index]) { td_ { case f.name when "TOTAL": t_ { "TOTAL" } else a_(:href => mangle_filename(f.name)){ t_ { f.name } } end } [f.num_lines, f.num_code_lines].each do |value| td_(:class => "value") { tt_{ value } } end [f.total_coverage, f.code_coverage].each do |value| value *= 100 td_ { table_(:cellpadding => 0, :cellspacing => 0, :align => "right") { tr_ { td_ { tt_ { "%3.1f%%" % value } x_ " " } ivalue = value.round td_ { table_(:class => "percent_graph", :cellpadding => 0, :cellspacing => 0, :width => 100) { tr_ { td_(:class => "covered", :width => ivalue) td_(:class => "uncovered", :width => (100-ivalue)) } } } } } } 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)] + each_file_pair_sorted.map{|k,v| v} title = default_title output = xhtml_ { html_ { head_ { title_{ title } style_(:type => "text/css") { t_{ "body { background-color: #{default_color}; }" } } style_(:type => "text/css") { CSS_PROLOG } script_(:type => "text/javascript") { h_{ JAVASCRIPT_PROLOG } } } body_ { h3_{ t_{ title } } p_ { t_{ "Generated on #{Time.new.to_s} with " } a_(:href => Rcov::UPSTREAM_URL){ "rcov #{Rcov::VERSION}" } } p_ { "Threshold: #{@output_threshold}%" } if @output_threshold != 101 hr_ x_{ format_overview(*files) } hr_ x_{ blurb } p_ { a_(:href => "http://validator.w3.org/check/referer") { img_(:src => "http://www.w3.org/Icons/valid-xhtml11", :alt => "Valid XHTML 1.1!", :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!") } } } } } lines = output.pretty.to_a lines.unshift lines.pop if /DOCTYPE/ =~ lines[-1] File.open(destname, "w") do |f| f.puts lines 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] spanclass = span_class(file, marked, count) if spanclass != last result += end_of_span case spanclass when nil end_of_span = "" else result += %[] end_of_span = "" end end result += %[] + (format_line % (i+1)) + " " + create_cross_refs(file.name, i+1, CGI.escapeHTML(line)) + "\n" last = spanclass end result += end_of_span "
#{result}
" end def create_cross_refs(filename, lineno, linetext) return linetext unless @callsite_analyzer @cross_ref_idx ||= 0 ret = "" refs = cross_references_for(filename, lineno) return linetext unless refs refs = refs.sort_by{|k,count| count} ret << %[
#{linetext}] ret << %[] ret << %[\nThis method was called by:\n\n] known_files = sorted_file_pairs.map{|fname, finfo| normalize_filename(fname)} refs.reverse_each do |dst, count| dstfile = normalize_filename(dst.file) dstline = dst.line calling_method = dst.calling_method label = "%7d %s" % [count, CGI.escapeHTML("#{dstfile}:#{dstline} in '#{calling_method}'")] if known_files.include? dstfile ret << %[#{label}] else ret << label end ret << "\n" end ret << "" end def span_class(sourceinfo, marked, count) @span_class_index ^= 1 case marked when true "marked#{@span_class_index}" when :inferred "inferred#{@span_class_index}" else "uncovered#{@span_class_index}" 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 } script_(:type => "text/javascript") { h_ { JAVASCRIPT_PROLOG } } style_(:type => "text/css") { h_ { colorscale } } } body_ { h3_{ t_{ default_title } } p_ { t_{ "Generated on #{Time.new.to_s} with " } a_(:href => Rcov::UPSTREAM_URL){ "rcov #{Rcov::VERSION}" } } hr_ if do_ctable # this kludge needed to ensure .pretty doesn't mangle it x_ { <Code reported as executed by Ruby looks like this... and this: this line is also marked as covered. Lines considered as run by rcov, but not reported by Ruby, look like this, and this: these lines were inferred by rcov (using simple heuristics). Finally, here's a line marked as not executed. EOS } end x_{ body } hr_ x_ { blurb } 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!") } } } } } # .pretty needed to make sure DOCTYPE is in a separate line lines = output.pretty.to_a lines.unshift lines.pop if /DOCTYPE/ =~ lines[-1] File.open(destfile, "w") do |f| f.puts lines end end def colorscale colorscalebase =< "profiling"} def initialize(opts = {}) options = DEFAULT_OPTS.clone.update(opts) super(options) @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 end # Rcov #{{{ only run if executed directly #{{{ "main" code 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::DEFAULT_OPTS[:ignore] options.include = [] options.nohtml = false options.test_unit_only = false options.sort = :name options.sort_reverse = false options.output_threshold = 101 options.replace_prog_name = false options.callsites = 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 ARGV will be set to the specified parameters after --. Keep in mind that all the programs are run under the same process (i.e. they just get Kernel#load()'ed in sequence). $PROGRAM_NAME (aka. $0) will be set before each file is load()ed if --replace-progname is used. EOF #{{{ OptionParser opts = OptionParser.new do |opts| opts.banner = <<-EOF rcov #{Rcov::VERSION} #{Rcov::RELEASE_DATE} Usage: rcov [options] [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("--test-unit-only", "Only trace code executed in TestCases.") do options.test_unit_only = true end opts.on("-n", "--no-color", "Create colorblind-safe output.") do options.color = false end opts.on("-i", "--include-file PATTERNS", "Generate info for files matching a", "pattern (comma-separated regexp list)") do |list| begin regexps = list.split(/,/).map{|x| Regexp.new(x) } options.include += regexps rescue RegexpError => e raise OptionParser::InvalidArgument, e.message end end opts.on("-x", "--exclude PATTERNS", "Don't generate info for files matching a", "pattern (comma-separated regexp list)") do |list| begin regexps = list.split(/,/).map{|x| Regexp.new x} options.skip += regexps rescue RegexpError => e raise OptionParser::InvalidArgument, e.message end end opts.on("--exclude-only PATTERNS", "Skip info only for files matching the", "given patterns.") do |list| begin options.skip = list.split(/,/).map{|x| Regexp.new(x) } rescue RegexpError => e raise OptionParser::InvalidArgument, e.message end end opts.on("--rails", "Skip config/, environment/ and vendor/.") do options.skip.concat [%r{\bvendor/},%r{\bconfig/},%r{\benvironment/}] end opts.on("--[no-]callsites", "Show callsites in generated XHTML report.", "(much slower; disabled by default)") do |val| options.callsites = val 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-report", "Dump detailed plain-text report to stdout.", "(filename, LoC, total lines, coverage)") do options.textmode = :report end opts.on("-t", "--text-summary", "Dump plain-text summary to stdout.") do options.textmode = :summary end opts.on("--text-counts", "Dump execution counts in plaintext.") do options.textmode = :counts end opts.on("--text-coverage", "Dump coverage info to stdout, using", "ANSI color sequences unless -n.") do options.textmode = :coverage end opts.on("--no-html", "Don't generate HTML output.", "(no output unless text output specified)") do options.nohtml = true end opts.on("--sort CRITERION", [:name, :loc, :coverage], "Sort files in the output by the specified", "field (name, loc, coverage)") do |criterion| options.sort = criterion end opts.on("--sort-reverse", "Reverse files in the output.") do options.sort_reverse = true end opts.on("--threshold INT", "Only list files with coverage < INT %.", "(default: 101)") do |threshold| begin threshold = Integer(threshold) raise if threshold <= 0 || threshold > 101 rescue Exception raise OptionParser::InvalidArgument, threshold end options.output_threshold = threshold end opts.on("--only-uncovered", "Same as --threshold 100") do options.output_threshold = 100 end opts.on("--replace-progname", "Replace $0 when loading the .rb files.") do options.replace_prog_name = true end opts.on("-w", "Turn warnings on (like ruby).") do $VERBOSE = true end opts.on("--no-rcovrt", "Do not use the optimized C runtime.", "(will run 30-300 times slower)") do $rcov_do_not_use_rcovrt = true end opts.separator "" opts.on_tail("-h", "--help", "Show help message") do require 'pp' puts opts puts < e puts opts puts puts e.message exit(-1) end options.destdir ||= "coverage" unless ARGV[0] puts opts exit end # {{{ set loadpath options.loadpaths.reverse_each{|x| $:.unshift x} #{{{ require 'rcov': do it only now in order to be able to run rcov on itself # since we need to set $: before. require 'rcov' if options.callsites $rcov_callsite_analyzer = Rcov::CallSiteAnalyzer.new $rcov_callsite_analyzer.install_hook else $rcov_callsite_analyzer = nil end # {{{ create formatters formatters = [] make_formatter = lambda do |klass| klass.new(:destdir => options.destdir, :color => options.color, :fsr => options.range, :textmode => options.textmode, :ignore => options.skip, :dont_ignore => options.include, :sort => options.sort, :sort_reverse => options.sort_reverse, :output_threshold => options.output_threshold, :callsite_analyzer => $rcov_callsite_analyzer) end unless options.nohtml if options.profiling formatters << make_formatter[Rcov::HTMLProfiling] else formatters << make_formatter[Rcov::HTMLCoverage] end end textual_formatters = {:counts => Rcov::TextCoverage, :coverage => Rcov::TextCoverage, :summary => Rcov::TextSummary, :report => Rcov::TextReport} if textual_formatters[options.textmode] formatters << make_formatter[textual_formatters[options.textmode]] end $rcov_code_coverage_analyzer = Rcov::CodeCoverageAnalyzer.new # must be registered before test/unit puts its own END { $rcov_code_coverage_analyzer.remove_hook $rcov_callsite_analyzer.remove_hook if $rcov_callsite_analyzer $rcov_code_coverage_analyzer.dump_coverage_info(formatters) if formatters.all?{|formatter| formatter.sorted_file_pairs.empty? } require 'pp' $stderr.puts <<-EOF No file to analyze was found. All the files loaded by rcov matched one of the following expressions, and were thus ignored: #{PP.pp(options.skip, "").chomp} You can solve this by doing one or more of the following: * rename the files not to be ignored so they don't match the above regexps * use --include-file to give a list of patterns for files not to be ignored * use --exclude-only to give the new list of regexps to match against * structure your code as follows: test/test_*.rb for the test cases lib/**/*.rb for the target source code whose coverage you want making sure that the test/test_*.rb files are loading from lib/, e.g. by using the -Ilib command-line argument, adding $:.unshift File.join(File.dirname(__FILE__), "..", "lib") to test/test_*.rb, or running rcov via a Rakefile (read the RDoc documentation or README.rake in the source distribution). EOF exit(-1) end } if options.test_unit_only require 'test/unit' module Test::Unit class TestCase remove_method(:run) if instance_methods.include? "run" def run(result) yield(STARTED, name) @_result = result begin $rcov_code_coverage_analyzer.run_hooked do setup __send__(@method_name) end rescue AssertionFailedError => e add_failure(e.message, e.backtrace) rescue StandardError, ScriptError add_error($!) ensure begin $rcov_code_coverage_analyzer.run_hooked { teardown } rescue AssertionFailedError => e add_failure(e.message, e.backtrace) rescue StandardError, ScriptError add_error($!) end end result.add_run yield(FINISHED, name) end end end else $rcov_code_coverage_analyzer.install_hook end #{{{ Load scripts pending_scripts = ARGV.clone ARGV.replace extra_args until pending_scripts.empty? prog = pending_scripts.shift if options.replace_prog_name $0 = File.basename(File.expand_path(prog)) end 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_{ " xml " } 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: