#!/usr/bin/env ruby require 'bluecloth' require 'diff/lcs' require 'diff/lcs/callbacks' require 'spec/lib/constants' require 'rbconfig' ### Fixturing functions module BlueCloth::Matchers ### Matcher for comparing output of a BlueCloth-generated HTML fragment against a known-good ### string. class TransformMatcher ### Create a new matcher for the given +html+ def initialize( html ) @html = html end ### Strip tab indentation from the expected HTML output. def without_indentation if indent = @html[/\A\t+/] indent.gsub!( /\A\n/m, '' ) @html.gsub!( /^#{indent}/m, '' ) end return self end ### Returns true if the HTML generated by the given +bluecloth+ object matches the ### expected HTML, comparing only the salient document structures. def matches?( bluecloth ) @bluecloth = bluecloth @output_html = bluecloth.to_html.gsub( /\n\n\n/, "\n\n" ) return @output_html.strip == @html.strip end ### Build a failure message for the matching case. def failure_message if self.should_output_html? patch = self.make_html_patch( @html, @output_html ) return %{

Expected the generated html:

#@output_html
to be the same as:
#@html

Diffs:

#{patch} } else patch = self.make_patch( @html, @output_html ) return ("Expected the generated html:\n\n %p\n\nto be the same as:\n\n" + " %p\n\nDiffs:\n\n%s") % [ @output_html, @html, patch ] end end ### Build a failure message for the non-matching case. def negative_failure_message return "Expected the generated html:\n\n %p\n\nnot to be the same as:\n\n %p\n\n" % [ @output_html, @html ] end ### Returns true if it appears HTML output should be used instead of plain-text. This ### will be true if running from TextMate or if the HTML_LOGGING environment variable ### is set. def should_output_html? return false # return ENV['HTML_LOGGING'] || # (ENV['TM_FILENAME'] && ENV['TM_FILENAME'] =~ /_spec\.rb/) end ### Compute a patch between the given +expected+ output and the +actual+ output ### and return it as a string. def make_patch( expected, actual ) diffs = Diff::LCS.sdiff( expected.split("\n"), actual.split("\n"), Diff::LCS::ContextDiffCallbacks ) maxcol = diffs.flatten. collect {|d| [d.old_element.to_s.length, d.new_element.to_s.length ] }. flatten.max || 0 maxcol += 4 patch = " %#{maxcol}s | %s\n" % [ "Expected", "Actual" ] patch << diffs.collect do |changeset| changeset.collect do |change| "%s [%03d, %03d]: %#{maxcol}s | %-#{maxcol}s" % [ change.action, change.old_position, change.new_position, change.old_element.inspect, change.new_element.inspect, ] end.join("\n") end.join("\n---\n") end ### Compute a patch similar to #make_patch, but output HTML instead of plain text. def make_html_patch( expected, actual ) diffs = Diff::LCS.sdiff( expected.split("\n"), actual.split("\n"), Diff::LCS::ContextDiffCallbacks ) patch = %{ } patch << diffs.collect do |changeset| changeset.collect do |change| "" % [ change.action, change.old_position, change.new_position, change.old_element.inspect, change.new_element.inspect, ] end.join("\n") end.join( "" ) patch << %{
Diffs
OpPosExpectedActual
%s[%03d, %03d]%s%s
\n} end end ### Variant of the regular TransformMatcher that normalizes the two strings using the 'tidy' ### library before comparing. class TidyTransformMatcher < TransformMatcher TIDY_OPTIONS = {} @tidy = nil ### Fetch the class-global Tidy object, creating it if necessary def self::tidy_object unless @tidy require 'tidy' soext = Config::CONFIG['LIBRUBY_ALIASES'].sub( /.*\./, '' ) Tidy.path = "libtidy.#{soext}" @tidy = Tidy.new( TIDY_OPTIONS ) end return @tidy end ### Set the matcher's expected output to a tidied version of the input +html+. def initialize( html ) @html = self.class.tidy_object.clean( html ) end ### Returns true if the HTML generated by the given +bluecloth+ object matches the ### expected HTML after normalizing them both with 'tidy'. def matches?( bluecloth ) @bluecloth = bluecloth @output_html = self.class.tidy_object.clean( bluecloth.to_html ) return @output_html == @html end end class TransformRegexpMatcher ### Create a new matcher for the given +regexp+ def initialize( regexp ) @regexp = regexp end ### Returns true if the regexp associated with this matcher matches the output generated ### by the specified +bluecloth+ object. def matches?( bluecloth ) @bluecloth = bluecloth @output_html = bluecloth.to_html return @output_html =~ @regexp end ### Build a failure message for the matching case. def failure_message return "Expected the generated html:\n\n %pto match the regexp:\n\n%p\n\n" % [ @output_html, @regexp ] end ### Build a failure message for the negative matching case. def negative_failure_message return "Expected the generated html:\n\n %pnot to match the regexp:\n\n%p\n\n" % [ @output_html, @regexp ] end end ### Create a new BlueCloth object out of the given +string+ and +options+ and ### return it. def the_markdown( string, *options ) return BlueCloth.new( string, *options ) end ### Strip indentation from the given +string+, create a new BlueCloth object ### out of the result and any +options+, and return it. def the_indented_markdown( string, *options ) if indent = string[/\A\t+/] indent.gsub!( /\A\n/m, '' ) $stderr.puts "Source indent is: %p" % [ indent ] if $DEBUG string.gsub!( /^#{indent}/m, '' ) end return BlueCloth.new( string, *options ) end ### Generate a matcher that expects to equal the given +html+. def be_transformed_into( html ) return BlueCloth::Matchers::TransformMatcher.new( html ) end ### Generate a matcher that expects to match a normalized version of the specified +html+. def be_transformed_into_normalized_html( html ) return BlueCloth::Matchers::TidyTransformMatcher.new( html ) end ### Generate a matcher that expects to match the given +regexp+. def be_transformed_into_html_matching( regexp ) return BlueCloth::Matchers::TransformMatcher.new( regexp ) end end # module BlueCloth::Matchers