instiki/lib/xhtmldiff.rb

180 lines
4.1 KiB
Ruby

#!/usr/bin/env ruby
# Author: Aredridel <aredridel@nbtsc.org>
# Website: http://theinternetco.net/projects/ruby/xhtmldiff.html
# Licence: same as Ruby
# Version: 1.22
#
# Tweaks by Jacques Distler <distler@golem.ph.utexas.edu>
# -- add classnames to <del> and <ins> elements added by XHTMLDiff,
# for better CSS styling
require 'diff/lcs'
require 'rexml/document'
require 'delegate'
def Math.max(a, b)
a > b ? a : b
end
module REXML
class Text
def deep_clone
clone
end
end
class HashableElementDelegator < DelegateClass(Element)
def initialize(sub)
super sub
end
def == other
res = other.to_s.strip == self.to_s.strip
res
end
def eql? other
self == other
end
def[](k)
r = super
if r.kind_of? __getobj__.class
self.class.new(r)
else
r
end
end
def hash
r = __getobj__.to_s.hash
r
end
end
end
class XHTMLDiff
include REXML
attr_accessor :output
class << self
BLOCK_CONTAINERS = ['div', 'ul', 'li']
def diff(a, b)
if a == b
return a.deep_clone
end
if REXML::HashableElementDelegator === a and REXML::HashableElementDelegator === b
o = REXML::Element.new(a.name)
o.add_attributes a.attributes
hd = self.new(o)
Diff::LCS.traverse_balanced(a, b, hd)
o
elsif REXML::Text === a and REXML::Text === b
o = REXML::Element.new('span')
aa = a.value.split(/\s/)
ba = b.value.split(/\s/)
hd = XHTMLTextDiff.new(o)
Diff::LCS.traverse_balanced(aa, ba, hd)
o
else
raise ArgumentError.new("both arguments must be equal or both be elements. a is #{a.class.name} and b is #{b.class.name}")
end
end
end
def diff(a, b)
self.class.diff(a,b)
end
def initialize(output)
@output = output
end
# This will be called with both elements are the same
def match(event)
@output << event.old_element.deep_clone if event.old_element
end
# This will be called when there is an element in A that isn't in B
def discard_a(event)
@output << wrap(event.old_element, 'del', 'diffdel')
end
def change(event)
begin
sd = diff(event.old_element, event.new_element)
rescue ArgumentError
sd = nil
end
if sd and (ratio = (Float(rs = sd.to_s.gsub(%r{<(ins|del)>.*</\1>}, '').size) / bs = Math.max(event.old_element.to_s.size, event.new_element.to_s.size))) > 0.5
@output << sd
else
@output << wrap(event.old_element, 'del', 'diffmod')
@output << wrap(event.new_element, 'ins', 'diffmod')
end
end
# This will be called when there is an element in B that isn't in A
def discard_b(event)
@output << wrap(event.new_element, 'ins', 'diffins')
end
def choose_event(event, element, tag)
end
def wrap(element, tag = nil, class_name = nil)
if tag
el = Element.new tag
el << element.deep_clone
else
el = element.deep_clone
end
if class_name
el.add_attribute('class', class_name)
end
el
end
class XHTMLTextDiff < XHTMLDiff
def change(event)
@output << wrap(event.old_element, 'del', 'diffmod')
@output << wrap(event.new_element, 'ins', 'diffmod')
end
# This will be called with both elements are the same
def match(event)
@output << wrap(event.old_element, nil, nil) if event.old_element
end
# This will be called when there is an element in A that isn't in B
def discard_a(event)
@output << wrap(event.old_element, 'del', 'diffdel')
end
# This will be called when there is an element in B that isn't in A
def discard_b(event)
@output << wrap(event.new_element, 'ins', 'diffins')
end
def wrap(element, tag = nil, class_name = nil)
element = REXML::Text.new(" " << element) if String === element
return element unless tag
wrapper_element = REXML::Element.new(tag)
wrapper_element.add_text element
if class_name
wrapper_element.add_attribute('class', class_name)
end
wrapper_element
end
end
end
if $0 == __FILE__
$stderr.puts "No tests available yet"
exit(1)
end