#!/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 = %{
Diffs
Op | Pos | Expected | Actual |
}
patch << diffs.collect do |changeset|
changeset.collect do |change|
"%s | [%03d, %03d] | %s | %s |
" % [
change.action,
change.old_position,
change.new_position,
change.old_element.inspect,
change.new_element.inspect,
]
end.join("\n")
end.join( "" )
patch << %{
\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