├── .gitignore ├── README.rdoc ├── Rakefile ├── VERSION.yml ├── differ.gemspec ├── lib ├── differ.rb └── differ │ ├── change.rb │ ├── diff.rb │ ├── format │ ├── ascii.rb │ ├── color.rb │ └── html.rb │ └── string.rb └── spec ├── differ ├── change_spec.rb ├── diff_spec.rb ├── format │ ├── ascii_spec.rb │ ├── color_spec.rb │ └── html_spec.rb └── string_spec.rb ├── differ_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | .DS_Store 3 | coverage 4 | rdoc 5 | pkg 6 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Differ 2 | 3 | As streams of text swirled before the young man's eyes, his mind swam with 4 | thoughts of many things. They would have to wait, however, as he focussed his 5 | full concentration on the shifting patterns ahead of him. A glint of light 6 | reflecting off a piece of buried code caught his eye and any hope he had was 7 | lost. For the very moment he glanced aside, the landscape became Different. 8 | The young man gave a small sigh and trudged onward in solemn resignation, 9 | fated to wander the desolate codebanks in perpetuity. 10 | 11 | Differ is a flexible, pure-Ruby diff library, suitable for use in both command 12 | line scripts and web applications. The flexibility comes from the fact that 13 | diffs can be built at completely arbitrary levels of granularity (some common 14 | ones are built-in), and can be output in a variety of formats. 15 | 16 | == Installation 17 | 18 | sudo gem install differ 19 | 20 | == Usage 21 | 22 | There are a number of ways to use Differ, depending on your situation and needs. 23 | 24 | @original = "Epic lolcat fail!" 25 | @current = "Epic wolfman fail!" 26 | 27 | You can call the Differ module directly. 28 | 29 | require 'differ' 30 | 31 | There are a number of built-in diff methods to choose from... 32 | 33 | @diff = Differ.diff_by_line(@current, @original) 34 | # => "{"Epic lolcat fail!" >> "Epic wolfman fail!"}" 35 | 36 | @diff = Differ.diff_by_word(@current, @original) 37 | # => "Epic {"lolcat" >> "wolfman"} fail!" 38 | 39 | @diff = Differ.diff_by_char(@current, @original) 40 | # => "Epic {+"wo"}l{-"olcat "}f{+"m"}a{+"n fa"}il!" 41 | 42 | ... or call #diff directly and supply your own boundary string! 43 | 44 | @diff = Differ.diff(@current, @original) # implicitly by line! 45 | # => "{"Epic lolcat fail!" >> "Epic wolfman fail!"}" 46 | 47 | @diff = Differ.diff(@current, @original, 'i') 48 | # => "Epi{"c lolcat fa" >> "c wolfman fa"}il" 49 | 50 | If you would like something a little more inline... 51 | 52 | require 'differ/string' 53 | 54 | @diff = @current.diff(@original) # implicitly by line! 55 | # => "{"Epic lolcat fail!" >> "Epic wolfman fail!"}" 56 | 57 | ... or a lot more inline... 58 | 59 | @diff = (@current - @original) # implicitly by line! 60 | # => "{"Epic lolcat fail!" >> "Epic wolfman fail!"}" 61 | 62 | $; = ' ' 63 | @diff = (@current - @original) 64 | # => "Epic {"lolcat" >> "wolfman"} fail!" 65 | 66 | ... we've pretty much got you covered. 67 | 68 | === Output Formatting 69 | 70 | Need a different output format? We've got a few of those too. 71 | 72 | Differ.format = :ascii # <- Default 73 | Differ.format = :color 74 | Differ.format = :html 75 | 76 | Differ.format = MyCustomFormatModule 77 | 78 | Don't want to change the system-wide default for only a single diff output? 79 | Yeah, me either. 80 | 81 | @diff = (@current - @original) 82 | @diff.format_as(:color) 83 | 84 | == Copyright 85 | 86 | Copyright (c) 2009 Pieter Vande Bruggen. 87 | 88 | (The GIFT License, v1) 89 | 90 | Permission is hereby granted to use this software and/or its source code for 91 | whatever purpose you should choose. Seriously, go nuts. Use it for your personal 92 | RSS feed reader, your wildly profitable social network, or your mission to Mars. 93 | 94 | I don't care, it's yours. Change the name on it if you want -- in fact, if you 95 | start significantly changing what it does, I'd rather you did! Make it your own 96 | little work of art, complete with a stylish flowing signature in the corner. All 97 | I really did was give you the canvas. And my blessing. 98 | 99 | Know always right from wrong, and let others see your good works. 100 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | begin 4 | require 'jeweler' 5 | Jeweler::Tasks.new do |gem| 6 | gem.name = "differ" 7 | gem.summary = "A simple gem for generating string diffs" 8 | gem.email = "pvande@gmail.com" 9 | gem.homepage = "http://github.com/pvande/differ" 10 | gem.authors = [ "Pieter van de Bruggen" ] 11 | end 12 | rescue LoadError 13 | puts "Jeweler not available. Install it with: gem install jeweler" 14 | end 15 | 16 | require 'rake/rdoctask' 17 | Rake::RDocTask.new do |rdoc| 18 | rdoc.rdoc_dir = 'rdoc' 19 | rdoc.title = 'differ' 20 | rdoc.options << '--line-numbers' << '--inline-source' 21 | rdoc.rdoc_files.include('README*') 22 | rdoc.rdoc_files.include('lib/**/*.rb') 23 | end 24 | 25 | require 'spec/rake/spectask' 26 | Spec::Rake::SpecTask.new(:spec) do |spec| 27 | spec.libs << 'lib' << 'spec' 28 | spec.spec_files = FileList['spec/**/*_spec.rb'] 29 | end 30 | 31 | Spec::Rake::SpecTask.new(:rcov) do |spec| 32 | spec.libs << 'lib' << 'spec' 33 | spec.pattern = 'spec/**/*_spec.rb' 34 | spec.rcov = true 35 | end 36 | 37 | 38 | task :default => :spec 39 | -------------------------------------------------------------------------------- /VERSION.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :patch: 2 3 | :major: 0 4 | :minor: 1 5 | -------------------------------------------------------------------------------- /differ.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{differ} 8 | s.version = "0.1.2" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Pieter van de Bruggen"] 12 | s.date = %q{2011-02-17} 13 | s.email = %q{pvande@gmail.com} 14 | s.extra_rdoc_files = [ 15 | "README.rdoc" 16 | ] 17 | s.files = [ 18 | "README.rdoc", 19 | "Rakefile", 20 | "VERSION.yml", 21 | "differ.gemspec", 22 | "lib/differ.rb", 23 | "lib/differ/change.rb", 24 | "lib/differ/diff.rb", 25 | "lib/differ/format/ascii.rb", 26 | "lib/differ/format/color.rb", 27 | "lib/differ/format/html.rb", 28 | "lib/differ/string.rb", 29 | "spec/differ/change_spec.rb", 30 | "spec/differ/diff_spec.rb", 31 | "spec/differ/format/ascii_spec.rb", 32 | "spec/differ/format/color_spec.rb", 33 | "spec/differ/format/html_spec.rb", 34 | "spec/differ/string_spec.rb", 35 | "spec/differ_spec.rb", 36 | "spec/spec_helper.rb" 37 | ] 38 | s.homepage = %q{http://github.com/pvande/differ} 39 | s.require_paths = ["lib"] 40 | s.rubygems_version = %q{1.3.7} 41 | s.summary = %q{A simple gem for generating string diffs} 42 | s.test_files = [ 43 | "spec/differ/change_spec.rb", 44 | "spec/differ/diff_spec.rb", 45 | "spec/differ/format/ascii_spec.rb", 46 | "spec/differ/format/color_spec.rb", 47 | "spec/differ/format/html_spec.rb", 48 | "spec/differ/string_spec.rb", 49 | "spec/differ_spec.rb", 50 | "spec/spec_helper.rb" 51 | ] 52 | 53 | if s.respond_to? :specification_version then 54 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 55 | s.specification_version = 3 56 | 57 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 58 | else 59 | end 60 | else 61 | end 62 | end 63 | 64 | -------------------------------------------------------------------------------- /lib/differ.rb: -------------------------------------------------------------------------------- 1 | require 'differ/change' 2 | require 'differ/diff' 3 | require 'differ/format/ascii' 4 | require 'differ/format/color' 5 | require 'differ/format/html' 6 | 7 | module Differ 8 | class << self 9 | 10 | def diff(target, source, separator = "\n") 11 | old_sep, $; = $;, separator 12 | 13 | target = target.split(separator) 14 | source = source.split(separator) 15 | 16 | $; = '' if separator.is_a? Regexp 17 | 18 | @diff = Diff.new 19 | advance(target, source) until source.empty? || target.empty? 20 | @diff.insert(*target) || @diff.delete(*source) 21 | return @diff 22 | ensure 23 | $; = old_sep 24 | end 25 | 26 | def diff_by_char(to, from) 27 | diff(to, from, '') 28 | end 29 | 30 | def diff_by_word(to, from) 31 | diff(to, from, /\b/) 32 | end 33 | 34 | def diff_by_line(to, from) 35 | diff(to, from, "\n") 36 | end 37 | 38 | def format=(f) 39 | @format = format_for(f) 40 | end 41 | 42 | def format 43 | return @format || Format::Ascii 44 | end 45 | 46 | def format_for(f) 47 | case f 48 | when Module then f 49 | when :ascii then Format::Ascii 50 | when :color then Format::Color 51 | when :html then Format::HTML 52 | when nil then nil 53 | else raise "Unknown format type #{f.inspect}" 54 | end 55 | end 56 | 57 | private 58 | def advance(target, source) 59 | del, add = source.shift, target.shift 60 | 61 | prioritize_insert = target.length > source.length 62 | insert = target.index(del) 63 | delete = source.index(add) 64 | 65 | if del == add 66 | @diff.same(add) 67 | elsif insert && prioritize_insert 68 | change(:insert, target.unshift(add), insert) 69 | elsif delete 70 | change(:delete, source.unshift(del), delete) 71 | elsif insert && !prioritize_insert 72 | change(:insert, target.unshift(add), insert) 73 | else 74 | @diff.insert(add) && @diff.delete(del) 75 | end 76 | end 77 | 78 | def change(method, array, index) 79 | @diff.send(method, *array.slice!(0..index)) 80 | @diff.same(array.shift) 81 | end 82 | end 83 | end -------------------------------------------------------------------------------- /lib/differ/change.rb: -------------------------------------------------------------------------------- 1 | module Differ 2 | class Change # :nodoc: 3 | attr_accessor :insert, :delete 4 | def initialize(options = {}) 5 | @insert = options[:insert] || '' 6 | @delete = options[:delete] || '' 7 | end 8 | 9 | def insert? 10 | !@insert.empty? 11 | end 12 | 13 | def delete? 14 | !@delete.empty? 15 | end 16 | 17 | def change? 18 | !@insert.empty? && !@delete.empty? 19 | end 20 | 21 | def to_s 22 | Differ.format.format(self) 23 | end 24 | alias :inspect :to_s 25 | 26 | def ==(other) 27 | self.insert == other.insert && self.delete == other.delete 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /lib/differ/diff.rb: -------------------------------------------------------------------------------- 1 | module Differ 2 | class Diff 3 | def initialize 4 | @raw = [] 5 | end 6 | 7 | def same(*str) 8 | return if str.empty? 9 | if @raw.last.is_a? String 10 | @raw.last << sep 11 | elsif @raw.last.is_a? Change 12 | if @raw.last.change? 13 | @raw << sep 14 | else 15 | change = @raw.pop 16 | if change.insert? && @raw.last 17 | @raw.last << sep if change.insert.sub!(/^#{Regexp.quote(sep)}/, '') 18 | end 19 | if change.delete? && @raw.last 20 | @raw.last << sep if change.delete.sub!(/^#{Regexp.quote(sep)}/, '') 21 | end 22 | @raw << change 23 | 24 | @raw.last.insert << sep if @raw.last.insert? 25 | @raw.last.delete << sep if @raw.last.delete? 26 | @raw << '' 27 | end 28 | else 29 | @raw << '' 30 | end 31 | @raw.last << str.join(sep) 32 | end 33 | 34 | def delete(*str) 35 | return if str.empty? 36 | if @raw.last.is_a? Change 37 | change = @raw.pop 38 | if change.insert? && @raw.last 39 | @raw.last << sep if change.insert.sub!(/^#{Regexp.quote(sep)}/, '') 40 | end 41 | change.delete << sep if change.delete? 42 | else 43 | change = Change.new(:delete => @raw.empty? ? '' : sep) 44 | end 45 | 46 | @raw << change 47 | @raw.last.delete << str.join(sep) 48 | end 49 | 50 | def insert(*str) 51 | return if str.empty? 52 | if @raw.last.is_a? Change 53 | change = @raw.pop 54 | if change.delete? && @raw.last 55 | @raw.last << sep if change.delete.sub!(/^#{Regexp.quote(sep)}/, '') 56 | end 57 | change.insert << sep if change.insert? 58 | else 59 | change = Change.new(:insert => @raw.empty? ? '' : sep) 60 | end 61 | 62 | @raw << change 63 | @raw.last.insert << str.join(sep) 64 | end 65 | 66 | def ==(other) 67 | @raw == other.raw_array 68 | end 69 | 70 | def to_s 71 | @raw.join() 72 | end 73 | 74 | def format_as(f) 75 | f = Differ.format_for(f) 76 | @raw.inject('') do |sum, part| 77 | part = case part 78 | when String then part 79 | when Change then f.format(part) 80 | end 81 | sum << part 82 | end 83 | end 84 | 85 | protected 86 | def raw_array 87 | @raw 88 | end 89 | 90 | private 91 | def sep 92 | "#{$;}" 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/differ/format/ascii.rb: -------------------------------------------------------------------------------- 1 | module Differ 2 | module Format 3 | module Ascii 4 | class << self 5 | def format(change) 6 | (change.change? && as_change(change)) || 7 | (change.delete? && as_delete(change)) || 8 | (change.insert? && as_insert(change)) || 9 | '' 10 | end 11 | 12 | private 13 | def as_insert(change) 14 | "{+#{change.insert.inspect}}" 15 | end 16 | 17 | def as_delete(change) 18 | "{-#{change.delete.inspect}}" 19 | end 20 | 21 | def as_change(change) 22 | "{#{change.delete.inspect} >> #{change.insert.inspect}}" 23 | end 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /lib/differ/format/color.rb: -------------------------------------------------------------------------------- 1 | module Differ 2 | module Format 3 | module Color 4 | class << self 5 | def format(change) 6 | (change.change? && as_change(change)) || 7 | (change.delete? && as_delete(change)) || 8 | (change.insert? && as_insert(change)) || 9 | '' 10 | end 11 | 12 | private 13 | def as_insert(change) 14 | "\033[32m#{change.insert}\033[0m" 15 | end 16 | 17 | def as_delete(change) 18 | "\033[31m#{change.delete}\033[0m" 19 | end 20 | 21 | def as_change(change) 22 | as_delete(change) << as_insert(change) 23 | end 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /lib/differ/format/html.rb: -------------------------------------------------------------------------------- 1 | module Differ 2 | module Format 3 | module HTML 4 | class << self 5 | def format(change) 6 | (change.change? && as_change(change)) || 7 | (change.delete? && as_delete(change)) || 8 | (change.insert? && as_insert(change)) || 9 | '' 10 | end 11 | 12 | private 13 | def as_insert(change) 14 | %Q{#{change.insert}} 15 | end 16 | 17 | def as_delete(change) 18 | %Q{#{change.delete}} 19 | end 20 | 21 | def as_change(change) 22 | as_delete(change) << as_insert(change) 23 | end 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /lib/differ/string.rb: -------------------------------------------------------------------------------- 1 | module Differ 2 | module StringDiffer 3 | def diff(old) 4 | Differ.diff(self, old, $; || "\n") 5 | end 6 | alias :- :diff 7 | end 8 | end 9 | 10 | String.class_eval do 11 | include Differ::StringDiffer 12 | end -------------------------------------------------------------------------------- /spec/differ/change_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Differ::Change do 4 | before(:each) do 5 | @format = Module.new { def self.format(c); end } 6 | Differ.format = @format 7 | end 8 | 9 | describe '(empty)' do 10 | before(:each) do 11 | @change = Differ::Change.new() 12 | end 13 | 14 | it 'should have a default insert' do 15 | @change.insert.should == '' 16 | end 17 | 18 | it 'should have a default delete' do 19 | @change.delete.should == '' 20 | end 21 | 22 | it 'should stringify to ""' do 23 | @format.should_receive(:format).once.and_return('') 24 | @change.to_s.should == '' 25 | end 26 | end 27 | 28 | describe '(insert only)' do 29 | before(:each) do 30 | @change = Differ::Change.new(:insert => 'foo') 31 | end 32 | 33 | it 'should populate the :insert parameter' do 34 | @change.insert.should == 'foo' 35 | end 36 | 37 | it 'should have a default delete' do 38 | @change.delete.should == '' 39 | end 40 | 41 | it { (@change).should be_an_insert } 42 | end 43 | 44 | describe '(delete only)' do 45 | before(:each) do 46 | @change = Differ::Change.new(:delete => 'bar') 47 | end 48 | 49 | it 'should have a default :insert' do 50 | @change.insert.should == '' 51 | end 52 | 53 | it 'should populate the :delete parameter' do 54 | @change.delete.should == 'bar' 55 | end 56 | 57 | it { (@change).should be_a_delete } 58 | end 59 | 60 | describe '(both insert and delete)' do 61 | before(:each) do 62 | @change = Differ::Change.new(:insert => 'foo', :delete => 'bar') 63 | end 64 | 65 | it 'should populate the :insert parameter' do 66 | @change.insert.should == 'foo' 67 | end 68 | 69 | it 'should populate the :delete parameter' do 70 | @change.delete.should == 'bar' 71 | end 72 | 73 | it { (@change).should be_an_insert } 74 | it { (@change).should be_a_delete } 75 | it { (@change).should be_a_change } 76 | end 77 | 78 | it "should stringify via the current format's #format method" do 79 | @format.should_receive(:format).once 80 | Differ::Change.new.to_s 81 | end 82 | end -------------------------------------------------------------------------------- /spec/differ/diff_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Differ::Diff do 4 | before(:each) do 5 | $; = nil 6 | @diff = Differ::Diff.new 7 | end 8 | 9 | describe '#to_s' do 10 | before(:each) do 11 | @format = Differ.format 12 | end 13 | 14 | it 'should concatenate the result list' do 15 | diff('a', 'b', 'c').to_s.should == 'abc' 16 | end 17 | 18 | it 'should concatenate without regard for the $;' do 19 | $; = '*' 20 | diff('a', 'b', 'c').to_s.should == 'abc' 21 | end 22 | 23 | it 'should delegate insertion changes to Differ#format' do 24 | i = +'b' 25 | @format.should_receive(:format).once.with(i).and_return('!') 26 | diff('a', i, 'c').to_s.should == 'a!c' 27 | end 28 | end 29 | 30 | describe '#format_as' do 31 | before(:each) do 32 | @change = +'b' 33 | Differ.format = Module.new { def self.format(c); raise :error; end } 34 | @format = Module.new { def self.format(c); end } 35 | end 36 | 37 | it 'should delegate change formatting to the given format' do 38 | @format.should_receive(:format).once.with(@change).and_return('!') 39 | diff('a', @change, 'c').format_as(@format).should == 'a!c' 40 | end 41 | 42 | it 'should use Differ#format_for to grab the correct format' do 43 | Differ.should_receive(:format_for).once.with(@format) 44 | diff().format_as(@format) 45 | end 46 | end 47 | 48 | describe '#same' do 49 | it 'should append to the result list' do 50 | @diff.same('c') 51 | @diff.should == diff('c') 52 | end 53 | 54 | it 'should concatenate its arguments' do 55 | @diff.same('a', 'b', 'c', 'd') 56 | @diff.should == diff('abcd') 57 | end 58 | 59 | it 'should join its arguments with $;' do 60 | $; = '*' 61 | @diff.same(*'a*b*c*d'.split) 62 | @diff.should == diff('a*b*c*d') 63 | end 64 | 65 | describe 'when the last result was a String' do 66 | before(:each) do 67 | @diff = diff('a') 68 | end 69 | 70 | it 'should append to the last result' do 71 | @diff.same('b') 72 | @diff.should == diff('ab') 73 | end 74 | 75 | it 'should join to the last result with $;' do 76 | $; = '*' 77 | @diff.same('b') 78 | @diff.should == diff('a*b') 79 | end 80 | end 81 | 82 | describe 'when the last result was a change' do 83 | before(:each) do 84 | @diff = diff('z' >> 'd') 85 | end 86 | 87 | it 'should append to the result list' do 88 | @diff.same('a') 89 | @diff.should == diff(('z' >> 'd'), 'a') 90 | end 91 | 92 | it 'should prepend $; to the result' do 93 | $; = '*' 94 | @diff.same('a') 95 | @diff.should == diff(('z' >> 'd'), '*a') 96 | end 97 | 98 | it "should do nothing to a leading $; on the insert" do 99 | @diff = diff('a', ('*-' >> '*+')) 100 | $; = '*' 101 | @diff.same('c') 102 | @diff.should == diff('a', ('*-' >> '*+'), '*c') 103 | end 104 | end 105 | 106 | describe 'when the last result was just a delete' do 107 | before(:each) do 108 | @diff = diff(-'z') 109 | end 110 | 111 | it 'should append to the result list' do 112 | @diff.same('a') 113 | @diff.should == diff(-'z', 'a') 114 | end 115 | 116 | it 'should append $; to the previous result' do 117 | $; = '*' 118 | @diff.same('a') 119 | @diff.should == diff(-'z*', 'a') 120 | end 121 | 122 | it "should relocate a leading $; on the delete to the previous item" do 123 | @diff = diff('a', -'*b') 124 | $; = '*' 125 | @diff.same('c') 126 | @diff.should == diff('a*', -'b*', 'c') 127 | end 128 | end 129 | 130 | describe 'when the last result was just an insert' do 131 | before(:each) do 132 | @diff = diff(+'z') 133 | end 134 | 135 | it 'should append to the result list' do 136 | @diff.same('a') 137 | @diff.should == diff(+'z', 'a') 138 | end 139 | 140 | it 'should append $; to the previous result' do 141 | $; = '*' 142 | @diff.same('a') 143 | @diff.should == diff(+'z*', 'a') 144 | end 145 | 146 | it "should relocate a leading $; on the insert to the previous item" do 147 | @diff = diff('a', +'*b') 148 | $; = '*' 149 | @diff.same('c') 150 | @diff.should == diff('a*', +'b*', 'c') 151 | end 152 | end 153 | end 154 | 155 | describe '#delete' do 156 | it 'should append to the result list' do 157 | @diff.delete('c') 158 | @diff.should == diff(-'c') 159 | end 160 | 161 | it 'should concatenate its arguments' do 162 | @diff.delete('a', 'b', 'c', 'd') 163 | @diff.should == diff(-'abcd') 164 | end 165 | 166 | it 'should join its arguments with $;' do 167 | $; = '*' 168 | @diff.delete(*'a*b*c*d'.split) 169 | @diff.should == diff(-'a*b*c*d') 170 | end 171 | 172 | describe 'when the last result was a Change' do 173 | describe '(delete)' do 174 | before(:each) do 175 | @diff = diff(-'a') 176 | end 177 | 178 | it 'should append to the last result' do 179 | @diff.delete('b') 180 | @diff.should == diff(-'ab') 181 | end 182 | 183 | it 'should join to the last result with $;' do 184 | $; = '*' 185 | @diff.delete('b') 186 | @diff.should == diff(-'a*b') 187 | end 188 | end 189 | 190 | describe '(insert)' do 191 | before(:each) do 192 | @diff = diff(+'a') 193 | end 194 | 195 | it "should turn the insert into a change" do 196 | @diff.delete('b') 197 | @diff.should == diff('b' >> 'a') 198 | end 199 | 200 | it "should relocate a leading $; on the insert to the previous item" do 201 | @diff = diff('a', +'*b') 202 | $; = '*' 203 | @diff.delete('z') 204 | @diff.should == diff('a*', ('z' >> 'b')) 205 | end 206 | end 207 | end 208 | 209 | describe 'when the last result was not a Change' do 210 | before(:each) do 211 | @diff = diff('a') 212 | end 213 | 214 | it 'should append a Change to the result list' do 215 | @diff.delete('b') 216 | @diff.should == diff('a', -'b') 217 | end 218 | 219 | it 'should prepend $; to the result' do 220 | $; = '*' 221 | @diff.delete('b') 222 | @diff.should == diff('a', -'*b') 223 | end 224 | end 225 | end 226 | 227 | describe '#insert' do 228 | it 'should append to the result list' do 229 | @diff.insert('c') 230 | @diff.should == diff(+'c') 231 | end 232 | 233 | it 'should concatenate its arguments' do 234 | @diff.insert('a', 'b', 'c', 'd') 235 | @diff.should == diff(+'abcd') 236 | end 237 | 238 | it 'should join its arguments with $;' do 239 | $; = '*' 240 | @diff.insert(*'a*b*c*d'.split) 241 | @diff.should == diff(+'a*b*c*d') 242 | end 243 | 244 | describe 'when the last result was a Change' do 245 | describe '(delete)' do 246 | before(:each) do 247 | @diff = diff(-'b') 248 | end 249 | 250 | it "should not change the 'insert' portion of the last result" do 251 | @diff.insert('a') 252 | @diff.should == diff('b' >> 'a') 253 | end 254 | 255 | it "should relocate a leading $; on the delete to the previous item" do 256 | @diff = diff('a', -'*b') 257 | $; = '*' 258 | @diff.insert('z') 259 | @diff.should == diff('a*', ('b' >> 'z')) 260 | end 261 | end 262 | 263 | describe '(insert)' do 264 | before(:each) do 265 | @diff = diff(+'a') 266 | end 267 | 268 | it 'should append to the last result' do 269 | @diff.insert('b') 270 | @diff.should == diff(+'ab') 271 | end 272 | 273 | it 'should join to the last result with $;' do 274 | $; = '*' 275 | @diff.insert('b') 276 | @diff.should == diff(+'a*b') 277 | end 278 | end 279 | end 280 | 281 | describe 'when the last result was not a Change' do 282 | before(:each) do 283 | @diff = diff('a') 284 | end 285 | 286 | it 'should append a Change to the result list' do 287 | @diff.insert('b') 288 | @diff.should == diff('a', +'b') 289 | end 290 | 291 | it 'should prepend $; to the result' do 292 | $; = '*' 293 | @diff.insert('b') 294 | @diff.should == diff('a', +'*b') 295 | end 296 | end 297 | end 298 | end -------------------------------------------------------------------------------- /spec/differ/format/ascii_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Differ::Format::Ascii do 4 | it 'should format inserts well' do 5 | @expected = '{+"SAMPLE"}' 6 | Differ::Format::Ascii.format(+'SAMPLE').should == @expected 7 | end 8 | 9 | it 'should format deletes well' do 10 | @expected = '{-"SAMPLE"}' 11 | Differ::Format::Ascii.format(-'SAMPLE').should == @expected 12 | end 13 | 14 | it 'should format changes well' do 15 | @expected = '{"THEN" >> "NOW"}' 16 | Differ::Format::Ascii.format('THEN' >> 'NOW').should == @expected 17 | end 18 | end -------------------------------------------------------------------------------- /spec/differ/format/color_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Differ::Format::Color do 4 | it 'should format inserts well' do 5 | @expected = "\033[32mSAMPLE\033[0m" 6 | Differ::Format::Color.format(+'SAMPLE').should == @expected 7 | end 8 | 9 | it 'should format deletes well' do 10 | @expected = "\033[31mSAMPLE\033[0m" 11 | Differ::Format::Color.format(-'SAMPLE').should == @expected 12 | end 13 | 14 | it 'should format changes well' do 15 | @expected = "\033[31mTHEN\033[0m\033[32mNOW\033[0m" 16 | Differ::Format::Color.format('THEN' >> 'NOW').should == @expected 17 | end 18 | end -------------------------------------------------------------------------------- /spec/differ/format/html_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Differ::Format::HTML do 4 | it 'should format inserts well' do 5 | @expected = 'SAMPLE' 6 | Differ::Format::HTML.format(+'SAMPLE').should == @expected 7 | end 8 | 9 | it 'should format deletes well' do 10 | @expected = 'SAMPLE' 11 | Differ::Format::HTML.format(-'SAMPLE').should == @expected 12 | end 13 | 14 | it 'should format changes well' do 15 | @expected = 'THENNOW' 16 | Differ::Format::HTML.format('THEN' >> 'NOW').should == @expected 17 | end 18 | end -------------------------------------------------------------------------------- /spec/differ/string_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'differ/string' 3 | 4 | describe Differ::StringDiffer do 5 | it 'should be automatically mixed into String' do 6 | String.included_modules.should include(Differ::StringDiffer) 7 | end 8 | 9 | before(:each) do 10 | $; = nil 11 | end 12 | 13 | describe '#diff' do 14 | it 'should call Differ#diff' do 15 | Differ.should_receive(:diff).with('TO', 'FROM', "\n").once 16 | 'TO'.diff('FROM') 17 | end 18 | 19 | it 'should call Differ#diff with $;' do 20 | $; = 'x' 21 | Differ.should_receive(:diff).with('TO', 'FROM', $;).once 22 | 'TO'.diff('FROM') 23 | end 24 | end 25 | 26 | describe '#-' do 27 | it 'should call Differ#diff' do 28 | Differ.should_receive(:diff).with('TO', 'FROM', "\n").once 29 | 'TO' - 'FROM' 30 | end 31 | 32 | it 'should call Differ#diff with $;' do 33 | $; = 'x' 34 | Differ.should_receive(:diff).with('TO', 'FROM', $;).once 35 | 'TO' - 'FROM' 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /spec/differ_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Differ do 4 | describe '#format' do 5 | before(:each) { Differ.format = nil } 6 | 7 | it 'should return the last value it was set to' do 8 | Differ.format = Differ::Format::HTML 9 | Differ.format.should == Differ::Format::HTML 10 | end 11 | 12 | it 'should default to Differ::Format::Ascii' do 13 | Differ.format.should == Differ::Format::Ascii 14 | end 15 | end 16 | 17 | describe '#format=' do 18 | it 'should call #format_for with the passed argument' do 19 | Differ.should_receive(:format_for).with(:format).once 20 | Differ.format = :format 21 | end 22 | 23 | it 'should raise an error on undefined behavior' do 24 | lambda { 25 | Differ.format = 'threeve' 26 | }.should raise_error('Unknown format type "threeve"') 27 | end 28 | end 29 | 30 | describe '#format_for' do 31 | before(:each) { Differ.format = nil } 32 | 33 | it 'should store any module passed to it' do 34 | formatter = Module.new 35 | Differ.format_for(formatter).should == formatter 36 | end 37 | 38 | it 'should permit nil (default behavior)' do 39 | Differ.format_for(nil).should == nil 40 | end 41 | 42 | it 'should raise an error on undefined behavior' do 43 | lambda { 44 | Differ.format_for('threeve') 45 | }.should raise_error('Unknown format type "threeve"') 46 | end 47 | 48 | describe 'when passed a symbol' do 49 | it 'should translate the symbol :ascii into Differ::Format::Ascii' do 50 | Differ.format_for(:ascii).should == Differ::Format::Ascii 51 | end 52 | 53 | it 'should translate the symbol :color into Differ::Format::Color' do 54 | Differ.format_for(:color).should == Differ::Format::Color 55 | end 56 | 57 | it 'should translate the symbol :html into Differ::Format::HTML' do 58 | Differ.format_for(:html).should == Differ::Format::HTML 59 | end 60 | end 61 | end 62 | 63 | describe '#diff_by_char' do 64 | def diff_by_char 65 | Differ.send(:diff_by_char, @to, @from) 66 | end 67 | 68 | before(:each) do 69 | @to = @from = 'self' 70 | end 71 | 72 | it 'should hande no-change situations' do 73 | @expected = diff('self') 74 | diff_by_char.should == @expected 75 | end 76 | 77 | it 'should handle prepends' do 78 | @to = "myself" 79 | @expected = diff(+'my', 'self') 80 | diff_by_char.should == @expected 81 | end 82 | 83 | it 'should handle appends' do 84 | @to = 'self-interest' 85 | @expected = diff('self', +'-interest') 86 | diff_by_char.should == @expected 87 | end 88 | 89 | it 'should handle leading deletes' do 90 | @to = 'elf' 91 | @expected = diff(-'s', 'elf') 92 | diff_by_char.should == @expected 93 | end 94 | 95 | it 'should handle trailing deletes' do 96 | @to = 'sel' 97 | @expected = diff('sel', -'f') 98 | diff_by_char.should == @expected 99 | end 100 | 101 | it 'should handle simultaneous leading changes' do 102 | @to = 'wood-elf' 103 | @expected = diff(('s' >> 'wood-'), 'elf') 104 | diff_by_char.should == @expected 105 | end 106 | 107 | it 'should handle simultaneous trailing changes' do 108 | @to = "seasoning" 109 | @expected = diff('se', ('lf' >> 'asoning')) 110 | diff_by_char.should == @expected 111 | end 112 | 113 | it 'should handle full-string changes' do 114 | @to = 'turgid' 115 | @expected = diff('self' >> 'turgid') 116 | diff_by_char.should == @expected 117 | end 118 | 119 | it 'should handle complex string additions' do 120 | @to = 'my sleeplife' 121 | @expected = diff(+'my ', 's', +'l', 'e', +'ep', 'l', +'i', 'f', +'e') 122 | diff_by_char.should == @expected 123 | end 124 | 125 | it 'should handle complex string deletions' do 126 | @from = 'my sleeplife' 127 | @expected = diff(-'my ', 's', -'l', 'e', -'ep', 'l', -'i', 'f', -'e') 128 | diff_by_char.should == @expected 129 | end 130 | 131 | it 'should handle complex string changes' do 132 | @from = 'my sleeplife' 133 | @to = 'seasonal' 134 | @expected = diff(-'my ', 's', -'l', 'e', ('ep' >> 'asona'), 'l', -'ife') 135 | diff_by_char.should == @expected 136 | end 137 | end 138 | 139 | describe '#diff_by_word' do 140 | def diff_by_word 141 | Differ.send(:diff_by_word, @to, @from) 142 | end 143 | 144 | before(:each) do 145 | @to = @from = 'the daylight will come' 146 | end 147 | 148 | it 'should hande no-change situations' do 149 | @expected = diff('the daylight will come') 150 | diff_by_word.should == @expected 151 | end 152 | 153 | it 'should handle prepends' do 154 | @to = "surely the daylight will come" 155 | @expected = diff(+'surely ', 'the daylight will come') 156 | diff_by_word.should == @expected 157 | end 158 | 159 | it 'should handle appends' do 160 | @to = 'the daylight will come in the morning' 161 | @expected = diff('the daylight will come', +' in the morning') 162 | diff_by_word.should == @expected 163 | end 164 | 165 | it 'should handle leading deletes' do 166 | @to = 'daylight will come' 167 | @expected = diff(-'the ', 'daylight will come') 168 | diff_by_word.should == @expected 169 | end 170 | 171 | it 'should handle trailing deletes' do 172 | @to = 'the daylight' 173 | @expected = diff('the daylight', -' will come') 174 | diff_by_word.should == @expected 175 | end 176 | 177 | it 'should handle simultaneous leading changes' do 178 | @to = 'some daylight will come' 179 | @expected = diff(('the' >> 'some'), ' daylight will come') 180 | diff_by_word.should == @expected 181 | end 182 | 183 | it 'should handle simultaneous trailing changes' do 184 | @to = "the daylight will flood the room" 185 | @expected = diff('the daylight will ', ('come' >> 'flood the room')) 186 | diff_by_word.should == @expected 187 | end 188 | 189 | it 'should handle full-string changes' do 190 | @to = 'if we should expect it' 191 | @expected = diff( 192 | ('the' >> 'if'), 193 | ' ', 194 | ('daylight' >> 'we'), 195 | ' ', 196 | ('will' >> 'should'), 197 | ' ', 198 | ('come' >> 'expect it') 199 | ) 200 | diff_by_word.should == @expected 201 | end 202 | 203 | it 'should handle complex string additions' do 204 | @to = 'the fresh daylight will surely come' 205 | @expected = diff('the ', +'fresh ', 'daylight will ', +'surely ', 'come') 206 | diff_by_word.should == @expected 207 | end 208 | 209 | it 'should handle complex string deletions' do 210 | @from = 'the fresh daylight will surely come' 211 | @expected = diff('the ', -'fresh ', 'daylight will ', -'surely ', 'come') 212 | diff_by_word.should == @expected 213 | end 214 | 215 | it 'should handle complex string changes' do 216 | @from = 'the fresh daylight will surely come' 217 | @to = 'something fresh will become surly' 218 | @expected = diff( 219 | ('the' >> 'something'), 220 | ' fresh ', 221 | -'daylight ', 222 | 'will ', 223 | ( 'surely' >> 'become'), 224 | ' ', 225 | ( 'come' >> 'surly' ) 226 | ) 227 | diff_by_word.should == @expected 228 | end 229 | end 230 | 231 | describe '#diff_by_line' do 232 | def diff_by_line 233 | Differ.send(:diff_by_line, @to, @from) 234 | end 235 | 236 | before(:each) do 237 | @to = @from = <<-HAIKU.gsub(/ +|\n\Z/, '') 238 | stallion sinks gently 239 | slowly, sleeplessly 240 | following harp flails 241 | HAIKU 242 | end 243 | 244 | it 'should hande no-change situations' do 245 | @expected = diff(@to) 246 | diff_by_line.should == @expected 247 | end 248 | 249 | it 'should handle prepends' do 250 | @to = <<-HAIKU.gsub(/ +|\n\Z/, '') 251 | A Haiku: 252 | stallion sinks gently 253 | slowly, sleeplessly 254 | following harp flails 255 | HAIKU 256 | @expected = diff(+"A Haiku:\n", @from) 257 | diff_by_line.should == @expected 258 | end 259 | 260 | it 'should handle appends' do 261 | @to = <<-HAIKU.gsub(/ +|\n\Z/, '') 262 | stallion sinks gently 263 | slowly, sleeplessly 264 | following harp flails 265 | -- http://everypoet.net 266 | HAIKU 267 | @expected = diff(@from, +"\n-- http://everypoet.net") 268 | diff_by_line.should == @expected 269 | end 270 | 271 | it 'should handle leading deletes' do 272 | @from = <<-HAIKU.gsub(/ +|\n\Z/, '') 273 | A Haiku: 274 | stallion sinks gently 275 | slowly, sleeplessly 276 | following harp flails 277 | HAIKU 278 | @expected = diff(-"A Haiku:\n", @to) 279 | diff_by_line.should == @expected 280 | end 281 | 282 | it 'should handle trailing deletes' do 283 | @from = <<-HAIKU.gsub(/ +|\n\Z/, '') 284 | stallion sinks gently 285 | slowly, sleeplessly 286 | following harp flails 287 | -- http://everypoet.net 288 | HAIKU 289 | @expected = diff(@to, -"\n-- http://everypoet.net") 290 | diff_by_line.should == @expected 291 | end 292 | 293 | it 'should handle simultaneous leading changes' do 294 | @to = <<-HAIKU.gsub(/ +|\n\Z/, '') 295 | stallion sings gently 296 | slowly, sleeplessly 297 | following harp flails 298 | HAIKU 299 | @expected = diff( 300 | ('stallion sinks gently' >> 'stallion sings gently'), 301 | "\nslowly, sleeplessly" << 302 | "\nfollowing harp flails" 303 | ) 304 | diff_by_line.should == @expected 305 | end 306 | 307 | it 'should handle simultaneous trailing changes' do 308 | @to = <<-HAIKU.gsub(/ +|\n\Z/, '') 309 | stallion sinks gently 310 | slowly, sleeplessly 311 | drifting ever on 312 | HAIKU 313 | @expected = diff( 314 | "stallion sinks gently\n" << 315 | "slowly, sleeplessly\n", 316 | ('following harp flails' >> 'drifting ever on') 317 | ) 318 | diff_by_line.should == @expected 319 | end 320 | 321 | it 'should handle full-string changes' do 322 | @to = <<-HAIKU.gsub(/ +|\n\Z/, '') 323 | glumly inert coals 324 | slumber lazily, shoulda 325 | used more Burma Shave 326 | HAIKU 327 | @expected = diff(@from >> @to) 328 | diff_by_line.should == @expected 329 | end 330 | 331 | it 'should handle complex string additions' do 332 | @to = <<-HAIKU.gsub(/ +|\n\Z/, '') 333 | A Haiku, with annotation: 334 | stallion sinks gently 335 | slowly, sleeplessly 336 | (flailing) 337 | following harp flails 338 | -- modified from source 339 | HAIKU 340 | @expected = diff( 341 | +"A Haiku, with annotation:\n", 342 | "stallion sinks gently\n" << 343 | "slowly, sleeplessly\n", 344 | +"(flailing)\n", 345 | 'following harp flails', 346 | +"\n-- modified from source" 347 | ) 348 | diff_by_line.should == @expected 349 | end 350 | 351 | it 'should handle complex string deletions' do 352 | @from = <<-HAIKU.gsub(/ +|\n\Z/, '') 353 | A Haiku, with annotation: 354 | stallion sinks gently 355 | slowly, sleeplessly 356 | (flailing) 357 | following harp flails 358 | -- modified from source 359 | HAIKU 360 | @expected = diff( 361 | -"A Haiku, with annotation:\n", 362 | "stallion sinks gently\n" << 363 | "slowly, sleeplessly\n", 364 | -"(flailing)\n", 365 | 'following harp flails', 366 | -"\n-- modified from source" 367 | ) 368 | diff_by_line.should == @expected 369 | end 370 | 371 | it 'should handle complex string changes' do 372 | @to = <<-HAIKU.gsub(/ +|\n\Z/, '') 373 | stallion sings gently 374 | slowly, sleeplessly 375 | (flailing) 376 | following harp flails 377 | -- modified from source 378 | HAIKU 379 | @expected = diff( 380 | ('stallion sinks gently' >> 'stallion sings gently'), 381 | "\nslowly, sleeplessly\n", 382 | +"(flailing)\n", 383 | 'following harp flails', 384 | +"\n-- modified from source" 385 | ) 386 | diff_by_line.should == @expected 387 | end 388 | end 389 | 390 | describe '#diff (with arbitrary boundary)' do 391 | def diff_by_comma 392 | Differ.send(:diff, @to, @from, ', ') 393 | end 394 | 395 | before(:each) do 396 | @to = @from = 'alteration, asymmetry, a deviation' 397 | end 398 | 399 | it 'should hande no-change situations' do 400 | @expected = diff('alteration, asymmetry, a deviation') 401 | diff_by_comma.should == @expected 402 | end 403 | 404 | it 'should handle prepends' do 405 | @to = "aberration, alteration, asymmetry, a deviation" 406 | @expected = diff(+'aberration, ', 'alteration, asymmetry, a deviation') 407 | diff_by_comma.should == @expected 408 | end 409 | 410 | it 'should handle appends' do 411 | @to = "alteration, asymmetry, a deviation, change" 412 | @expected = diff('alteration, asymmetry, a deviation', +', change') 413 | diff_by_comma.should == @expected 414 | end 415 | 416 | it 'should handle leading deletes' do 417 | @to = 'asymmetry, a deviation' 418 | @expected = diff(-'alteration, ', 'asymmetry, a deviation') 419 | diff_by_comma.should == @expected 420 | end 421 | 422 | it 'should handle trailing deletes' do 423 | @to = 'alteration, asymmetry' 424 | @expected = diff('alteration, asymmetry', -', a deviation') 425 | diff_by_comma.should == @expected 426 | end 427 | 428 | it 'should handle simultaneous leading changes' do 429 | @to = 'aberration, asymmetry, a deviation' 430 | @expected = diff(('alteration' >> 'aberration'), ', asymmetry, a deviation') 431 | diff_by_comma.should == @expected 432 | end 433 | 434 | it 'should handle simultaneous trailing changes' do 435 | @to = 'alteration, asymmetry, change' 436 | @expected = diff('alteration, asymmetry, ', ('a deviation' >> 'change')) 437 | diff_by_comma.should == @expected 438 | end 439 | 440 | it 'should handle full-string changes' do 441 | @to = 'uniformity, unison, unity' 442 | @expected = diff(@from >> @to) 443 | diff_by_comma.should == @expected 444 | end 445 | 446 | it 'should handle complex string additions' do 447 | @to = 'aberration, alteration, anomaly, asymmetry, a deviation, change' 448 | @expected = diff( 449 | +'aberration, ', 450 | 'alteration, ', 451 | +'anomaly, ', 452 | 'asymmetry, a deviation', 453 | +', change' 454 | ) 455 | diff_by_comma.should == @expected 456 | end 457 | 458 | it 'should handle complex string deletions' do 459 | @from = 'aberration, alteration, anomaly, asymmetry, a deviation, change' 460 | @expected = diff( 461 | -'aberration, ', 462 | 'alteration, ', 463 | -'anomaly, ', 464 | 'asymmetry, a deviation', 465 | -', change' 466 | ) 467 | diff_by_comma.should == @expected 468 | end 469 | 470 | it 'should handle complex string changes' do 471 | @from = 'a, d, g, gh, x' 472 | @to = 'a, b, c, d, e, f, g, h, i, j' 473 | @expected = diff( 474 | 'a, ', 475 | +'b, c, ', 476 | 'd, ', 477 | +'e, f, ', 478 | 'g, ', 479 | ('gh, x' >> 'h, i, j') 480 | ) 481 | diff_by_comma.should == @expected 482 | end 483 | end 484 | 485 | describe '#diff (with implied boundary)' do 486 | def diff_by_line 487 | Differ.send(:diff, @to, @from) 488 | end 489 | 490 | before(:each) do 491 | @to = @from = <<-HAIKU.gsub(/ +|\n\Z/, '') 492 | stallion sinks gently 493 | slowly, sleeplessly 494 | following harp flails 495 | HAIKU 496 | end 497 | 498 | it 'should do diffs by line' do 499 | @to = <<-HAIKU.gsub(/ +|\n\Z/, '') 500 | stallion sinks gently 501 | slowly, restlessly 502 | following harp flails 503 | HAIKU 504 | @expected = diff( 505 | "stallion sinks gently\n", 506 | ('slowly, sleeplessly' >> 'slowly, restlessly'), 507 | "\nfollowing harp flails" 508 | ) 509 | diff_by_line.should == @expected 510 | end 511 | end 512 | end 513 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 4 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 5 | require 'differ' 6 | 7 | Spec::Runner.configure do |config| 8 | 9 | end 10 | 11 | def diff(*parts) 12 | x = Differ::Diff.new 13 | x.instance_variable_set(:@raw, parts) 14 | return x 15 | end 16 | 17 | class String 18 | def +@ 19 | Differ::Change.new(:insert => self) 20 | end 21 | 22 | def -@ 23 | Differ::Change.new(:delete => self) 24 | end 25 | 26 | def >>(to) 27 | Differ::Change.new(:delete => self, :insert => to) 28 | end 29 | end 30 | --------------------------------------------------------------------------------