Commit d2b7f117e224639e566f57beadd551c7e8e0adc5
1 parent
4c5fccb6
Exists in
master
and in
29 other branches
Add diffy lib
Showing
17 changed files
with
1352 additions
and
0 deletions
Show diff stats
@@ -0,0 +1,26 @@ | @@ -0,0 +1,26 @@ | ||
1 | +allow_empty_diff is true by default | ||
2 | + | ||
3 | +== 2.1.0 == | ||
4 | +Windows support | ||
5 | + | ||
6 | +== 2.0.10 == | ||
7 | +Close tempfile after it's been written to to avoid too many open file handles | ||
8 | + | ||
9 | +== 2.0.9 == | ||
10 | +Memoize calls to `which diff` which should result in a minor performance | ||
11 | +improvement in high use environments. | ||
12 | + | ||
13 | +== 2.0.8 == | ||
14 | +Handle non-UTF-8 byte sequences in Ruby 1.9. | ||
15 | +Avoid non-deterministic deletion of temp files when GC runs | ||
16 | + | ||
17 | +== 2.0.7 == | ||
18 | +Added :allow_empty_diff option | ||
19 | + | ||
20 | +== Oops, need to backfill changelog == | ||
21 | + | ||
22 | +== 1.0.1 == | ||
23 | +* Compatibility with ruby 1.8.6 and 1.9 | ||
24 | + | ||
25 | +== 1.0.0 == | ||
26 | +* HTML output and better documentation |
@@ -0,0 +1,19 @@ | @@ -0,0 +1,19 @@ | ||
1 | +Copyright (c) 2010 Sam Goldstein | ||
2 | + | ||
3 | +Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
4 | +this software and associated documentation files (the "Software"), to deal in | ||
5 | +the Software without restriction, including without limitation the rights to | ||
6 | +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | ||
7 | +of the Software, and to permit persons to whom the Software is furnished to do | ||
8 | +so, subject to the following conditions: | ||
9 | + | ||
10 | +The above copyright notice and this permission notice shall be included in all | ||
11 | +copies or substantial portions of the Software. | ||
12 | + | ||
13 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
14 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
15 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
16 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
17 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
18 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
19 | +SOFTWARE. |
@@ -0,0 +1,260 @@ | @@ -0,0 +1,260 @@ | ||
1 | +Diffy - Easy Diffing With Ruby [](http://travis-ci.org/samg/diffy) | ||
2 | +============================ | ||
3 | + | ||
4 | +Need diffs in your ruby app? Diffy has you covered. It provides a convenient | ||
5 | +way to generate a diff from two strings or files. Instead of reimplementing | ||
6 | +the LCS diff algorithm Diffy uses battle tested Unix diff to generate diffs, | ||
7 | +and focuses on providing a convenient interface, and getting out of your way. | ||
8 | + | ||
9 | +Supported Formats | ||
10 | +----------------- | ||
11 | + | ||
12 | +It provides several built in format options which can be passed to | ||
13 | +`Diffy::Diff#to_s`. | ||
14 | + | ||
15 | +* `:text` - Plain text output | ||
16 | +* `:color` - ANSI colorized text suitable for use in a terminal | ||
17 | +* `:html` - HTML output. Since version 2.0 this format does inline highlighting of the character changes between lines. | ||
18 | +* `:html_simple` - HTML output without inline highlighting. This may be useful in situations where high performance is required or simpler output is desired. | ||
19 | + | ||
20 | +A default format can be set like so: | ||
21 | + | ||
22 | + Diffy::Diff.default_format = :html | ||
23 | + | ||
24 | +Installation | ||
25 | +------------ | ||
26 | + | ||
27 | +###on Unix | ||
28 | + | ||
29 | + gem install diffy | ||
30 | + | ||
31 | +###on Windows: | ||
32 | + | ||
33 | +1. ensure that you have a working `which` and `diff` on your machine and on | ||
34 | + your search path. | ||
35 | + | ||
36 | + There are two options: | ||
37 | + | ||
38 | + 1. install unxutils <http://sourceforge.net/projects/unxutils> | ||
39 | + | ||
40 | + note that these tools contain diff 2.7 which has a different handling | ||
41 | + of whitespace in the diff results. This makes Diffy spec tests | ||
42 | + yielding one fail on Windows. | ||
43 | + | ||
44 | + 2. install these two individually from the gnuwin32 project | ||
45 | + <http://gnuwin32.sourceforge.net/> | ||
46 | + | ||
47 | + note that this delivers diff 2.8 which makes Diffy spec pass | ||
48 | + even on Windows. | ||
49 | + | ||
50 | + | ||
51 | +2. install the gem by | ||
52 | + | ||
53 | + gem install diffy | ||
54 | + | ||
55 | + | ||
56 | +Getting Started | ||
57 | +--------------- | ||
58 | + | ||
59 | +Here's an example of using Diffy to diff two strings | ||
60 | + | ||
61 | + $ irb | ||
62 | + >> string1 = <<-TXT | ||
63 | + >" Hello how are you | ||
64 | + >" I'm fine | ||
65 | + >" That's great | ||
66 | + >" TXT | ||
67 | + => "Hello how are you\nI'm fine\nThat's great\n" | ||
68 | + >> string2 = <<-TXT | ||
69 | + >" Hello how are you? | ||
70 | + >" I'm fine | ||
71 | + >" That's swell | ||
72 | + >" TXT | ||
73 | + => "Hello how are you?\nI'm fine\nThat's swell\n" | ||
74 | + >> puts Diffy::Diff.new(string1, string2) | ||
75 | + -Hello how are you | ||
76 | + +Hello how are you? | ||
77 | + I'm fine | ||
78 | + -That's great | ||
79 | + +That's swell | ||
80 | + | ||
81 | +HTML Output | ||
82 | +--------------- | ||
83 | + | ||
84 | +Outputing the diff as html is easy too. Here's an example using the | ||
85 | +`:html_simple` formatter. | ||
86 | + | ||
87 | + >> puts Diffy::Diff.new(string1, string2).to_s(:html_simple) | ||
88 | + <div class="diff"> | ||
89 | + <ul> | ||
90 | + <li class="del"><del>Hello how are you</del></li> | ||
91 | + <li class="ins"><ins>Hello how are you?</ins></li> | ||
92 | + <li class="unchanged"><span>I'm fine</span></li> | ||
93 | + <li class="del"><del>That's great</del></li> | ||
94 | + <li class="ins"><ins>That's swell</ins></li> | ||
95 | + </ul> | ||
96 | + </div> | ||
97 | + | ||
98 | +The `:html` formatter will give you inline highlighting a la github. | ||
99 | + | ||
100 | + >> puts Diffy::Diff.new("foo\n", "Foo\n").to_s(:html) | ||
101 | + <div class="diff"> | ||
102 | + <ul> | ||
103 | + <li class="del"><del><strong>f</strong>oo</del></li> | ||
104 | + <li class="ins"><ins><strong>F</strong>oo</ins></li> | ||
105 | + </ul> | ||
106 | + </div> | ||
107 | + | ||
108 | +There's some pretty nice css provided in `Diffy::CSS`. | ||
109 | + | ||
110 | + >> puts Diffy::CSS | ||
111 | + .diff{overflow:auto;} | ||
112 | + .diff ul{background:#fff;overflow:auto;font-size:13px;list-style:none;margin:0;padding:0;display:table;width:100%;} | ||
113 | + .diff del, .diff ins{display:block;text-decoration:none;} | ||
114 | + .diff li{padding:0; display:table-row;margin: 0;height:1em;} | ||
115 | + .diff li.ins{background:#dfd; color:#080} | ||
116 | + .diff li.del{background:#fee; color:#b00} | ||
117 | + .diff li:hover{background:#ffc} | ||
118 | + /* try 'whitespace:pre;' if you don't want lines to wrap */ | ||
119 | + .diff del, .diff ins, .diff span{white-space:pre-wrap;font-family:courier;} | ||
120 | + .diff del strong{font-weight:normal;background:#fcc;} | ||
121 | + .diff ins strong{font-weight:normal;background:#9f9;} | ||
122 | + .diff li.diff-comment { display: none; } | ||
123 | + .diff li.diff-block-info { background: none repeat scroll 0 0 gray; } | ||
124 | + | ||
125 | +Other Diff Options | ||
126 | +------------------ | ||
127 | + | ||
128 | +### Diffing files instead of strings | ||
129 | + | ||
130 | +You can diff files instead of strings by using the `:source` option. | ||
131 | + | ||
132 | + >> puts Diffy::Diff.new('/tmp/foo', '/tmp/bar', :source => 'files') | ||
133 | + | ||
134 | +### Full Diff Output | ||
135 | + | ||
136 | +By default Diffy removes the superfluous diff output. This is because its | ||
137 | +default is to show the complete diff'ed file (`diff -U 10000` is the default). | ||
138 | + | ||
139 | +Diffy does support full output, just use the `:include_diff_info => true` | ||
140 | +option when initializing: | ||
141 | + | ||
142 | + >> Diffy::Diff.new("foo\nbar\n", "foo\nbar\nbaz\n", :include_diff_info => true).to_s(:text) | ||
143 | + =>--- /Users/chaffeqa/Projects/stiwiki/tmp/diffy20111116-82153-ie27ex 2011-11-16 20:16:41.000000000 -0500 | ||
144 | + +++ /Users/chaffeqa/Projects/stiwiki/tmp/diffy20111116-82153-wzrhw5 2011-11-16 20:16:41.000000000 -0500 | ||
145 | + @@ -1,2 +1,3 @@ | ||
146 | + foo | ||
147 | + bar | ||
148 | + +baz | ||
149 | + | ||
150 | +And even deals a bit with the formatting! | ||
151 | + | ||
152 | +### Empty Diff Behavior | ||
153 | + | ||
154 | +By default Diffy will return empty string if there are no | ||
155 | +differences in inputs. In previous versions the full text of its first input | ||
156 | +was returned in this case. To restore this behaviour simply use the | ||
157 | +`:allow_empty_diff => false` option when initializing. | ||
158 | + | ||
159 | +### Plus and Minus symbols in HTML output | ||
160 | + | ||
161 | +By default Diffy doesn't include the `+`, `-`, and ` ` at the beginning of line for | ||
162 | +HTML output. | ||
163 | + | ||
164 | +You can use the `:include_plus_and_minus_in_html` option to include those | ||
165 | +symbols in the output. | ||
166 | + | ||
167 | + >> puts Diffy::Diff.new(string1, string2, :include_plus_and_minus_in_html => true).to_s(:html_simple) | ||
168 | + <div class="diff"> | ||
169 | + <ul> | ||
170 | + <li class="del"><del><span class="symbol">-</span>Hello how are you</del></li> | ||
171 | + <li class="ins"><ins><span class="symbol">+</span>Hello how are you?</ins></li> | ||
172 | + <li class="unchanged"><span class="symbol"> </span><span>I'm fine</span></li> | ||
173 | + <li class="del"><del><span class="symbol">-</span>That's great</del></li> | ||
174 | + <li class="ins"><ins><span class="symbol">+</span>That's swell</ins></li> | ||
175 | + </ul> | ||
176 | + </div> | ||
177 | + | ||
178 | +### Number of lines of context around changes | ||
179 | + | ||
180 | +You can use the `:context` option to override the number of lines of context | ||
181 | +that are shown around each change (this defaults to 10000 to show the full | ||
182 | +file). | ||
183 | + | ||
184 | + >> puts Diffy::Diff.new("foo\nfoo\nBAR\nbang\nbaz", "foo\nfoo\nbar\nbang\nbaz", :context => 1) | ||
185 | + foo | ||
186 | + -BAR | ||
187 | + +bar | ||
188 | + bang | ||
189 | + | ||
190 | + | ||
191 | +### Overriding the command line options passed to diff. | ||
192 | + | ||
193 | +You can use the `:diff` option to override the command line options that are | ||
194 | +passed to unix diff. They default to `-U 10000`. This option will noop if | ||
195 | +combined with the `:context` option. | ||
196 | + | ||
197 | + >> puts Diffy::Diff.new(" foo\nbar\n", "foo\nbar\n", :diff => "-w") | ||
198 | + foo | ||
199 | + bar | ||
200 | + | ||
201 | +Default Diff Options | ||
202 | +-------------------- | ||
203 | + | ||
204 | +You can set the default options for new `Diffy::Diff`s using the | ||
205 | +`Diffy::Diff.default_options` and `Diffy::Diff.default_options=` methods. | ||
206 | +Options passed to `Diffy::Diff.new` will be merged into the default options. | ||
207 | + | ||
208 | + >> Diffy::Diff.default_options | ||
209 | + => {:diff=>"-U 10000", :source=>"strings", :include_diff_info=>false, :include_plus_and_minus_in_html=>false} | ||
210 | + >> Diffy::Diff.default_options.merge!(:source => 'files') | ||
211 | + => {:diff=>"-U 10000", :source=>"files", :include_diff_info=>false, :include_plus_and_minus_in_html=>false} | ||
212 | + | ||
213 | + | ||
214 | +Custom Formats | ||
215 | +-------------- | ||
216 | + | ||
217 | +Diffy tries to make generating your own custom formatted output easy. | ||
218 | +`Diffy::Diff` provides an enumerable interface which lets you iterate over | ||
219 | +lines in the diff. | ||
220 | + | ||
221 | + >> Diffy::Diff.new("foo\nbar\n", "foo\nbar\nbaz\n").each do |line| | ||
222 | + >* case line | ||
223 | + >> when /^\+/ then puts "line #{line.chomp} added" | ||
224 | + >> when /^-/ then puts "line #{line.chomp} removed" | ||
225 | + >> end | ||
226 | + >> end | ||
227 | + line +baz added | ||
228 | + => [" foo\n", " bar\n", "+baz\n"] | ||
229 | + | ||
230 | +You can also use `Diffy::Diff#each_chunk` to iterate each grouping of additions, | ||
231 | +deletions, and unchanged in a diff. | ||
232 | + | ||
233 | + >> Diffy::Diff.new("foo\nbar\nbang\nbaz\n", "foo\nbar\nbing\nbong\n").each_chunk.to_a | ||
234 | + => [" foo\n bar\n", "-bang\n-baz\n", "+bing\n+bong\n"] | ||
235 | + | ||
236 | +Use `#map`, `#inject`, or any of Enumerable's methods. Go crazy. | ||
237 | + | ||
238 | + | ||
239 | +Ruby Version Compatibility | ||
240 | +------------------------- | ||
241 | + | ||
242 | +Support for Ruby 1.8.6 was dropped beginning at version 2.0 in order to support | ||
243 | +the chainable enumerators available in 1.8.7 and 1.9. | ||
244 | + | ||
245 | +If you want to use Diffy and Ruby 1.8.6 then: | ||
246 | + | ||
247 | + $ gem install diffy -v1.1.0 | ||
248 | + | ||
249 | +Testing | ||
250 | +------------ | ||
251 | + | ||
252 | +Diffy includes a full set of rspec tests. When contributing please include | ||
253 | +tests for your changes. | ||
254 | + | ||
255 | +[](http://travis-ci.org/samg/diffy) | ||
256 | + | ||
257 | +--------------------------------------------------------------------- | ||
258 | + | ||
259 | +Report bugs or request features at http://github.com/samg/diffy/issues | ||
260 | + |
@@ -0,0 +1,23 @@ | @@ -0,0 +1,23 @@ | ||
1 | +# coding: utf-8 | ||
2 | +lib = File.expand_path('../lib', __FILE__) | ||
3 | +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) | ||
4 | +require 'diffy/version' | ||
5 | + | ||
6 | +Gem::Specification.new do |spec| | ||
7 | + spec.name = "diffy" | ||
8 | + spec.version = Diffy::VERSION | ||
9 | + spec.authors = ["Sam Goldstein"] | ||
10 | + spec.email = ["sgrock@gmail.org"] | ||
11 | + spec.description = "Convenient diffing in ruby" | ||
12 | + spec.summary = "A convenient way to diff string in ruby" | ||
13 | + spec.homepage = "http://github.com/samg/diffy" | ||
14 | + spec.license = "MIT" | ||
15 | + | ||
16 | + spec.files = `git ls-files`.split($/) | ||
17 | + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } | ||
18 | + spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) | ||
19 | + spec.require_paths = ["lib"] | ||
20 | + | ||
21 | + spec.add_development_dependency "rake" | ||
22 | + spec.add_development_dependency "rspec", '~> 2.0' | ||
23 | +end |
@@ -0,0 +1,12 @@ | @@ -0,0 +1,12 @@ | ||
1 | +require 'tempfile' | ||
2 | +require 'erb' | ||
3 | +require 'rbconfig' | ||
4 | + | ||
5 | +module Diffy | ||
6 | + WINDOWS = (RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/) | ||
7 | +end | ||
8 | +require 'open3' unless Diffy::WINDOWS | ||
9 | +require File.join(File.dirname(__FILE__), 'diffy', 'format') | ||
10 | +require File.join(File.dirname(__FILE__), 'diffy', 'html_formatter') | ||
11 | +require File.join(File.dirname(__FILE__), 'diffy', 'diff') | ||
12 | +require File.join(File.dirname(__FILE__), 'diffy', 'css') |
@@ -0,0 +1,17 @@ | @@ -0,0 +1,17 @@ | ||
1 | +module Diffy | ||
2 | + CSS = <<-STYLE | ||
3 | +.diff{overflow:auto;} | ||
4 | +.diff ul{background:#fff;overflow:auto;font-size:13px;list-style:none;margin:0;padding:0;display:table;width:100%;} | ||
5 | +.diff del, .diff ins{display:block;text-decoration:none;} | ||
6 | +.diff li{padding:0; display:table-row;margin: 0;height:1em;} | ||
7 | +.diff li.ins{background:#dfd; color:#080} | ||
8 | +.diff li.del{background:#fee; color:#b00} | ||
9 | +.diff li:hover{background:#ffc} | ||
10 | +/* try 'whitespace:pre;' if you don't want lines to wrap */ | ||
11 | +.diff del, .diff ins, .diff span{white-space:pre-wrap;font-family:courier;} | ||
12 | +.diff del strong{font-weight:normal;background:#fcc;} | ||
13 | +.diff ins strong{font-weight:normal;background:#9f9;} | ||
14 | +.diff li.diff-comment { display: none; } | ||
15 | +.diff li.diff-block-info { background: none repeat scroll 0 0 gray; } | ||
16 | + STYLE | ||
17 | +end |
@@ -0,0 +1,161 @@ | @@ -0,0 +1,161 @@ | ||
1 | +module Diffy | ||
2 | + class Diff | ||
3 | + class << self | ||
4 | + attr_writer :default_format | ||
5 | + def default_format | ||
6 | + @default_format || :text | ||
7 | + end | ||
8 | + | ||
9 | + attr_writer :default_options | ||
10 | + # default options passed to new Diff objects | ||
11 | + def default_options | ||
12 | + @default_options ||= { | ||
13 | + :diff => '-U 10000', | ||
14 | + :source => 'strings', | ||
15 | + :include_diff_info => false, | ||
16 | + :include_plus_and_minus_in_html => false, | ||
17 | + :context => nil, | ||
18 | + :allow_empty_diff => true, | ||
19 | + } | ||
20 | + end | ||
21 | + | ||
22 | + end | ||
23 | + include Enumerable | ||
24 | + attr_reader :string1, :string2, :options, :diff | ||
25 | + | ||
26 | + # supported options | ||
27 | + # +:diff+:: A cli options string passed to diff | ||
28 | + # +:source+:: Either _strings_ or _files_. Determines whether string1 | ||
29 | + # and string2 should be interpreted as strings or file paths. | ||
30 | + # +:include_diff_info+:: Include diff header info | ||
31 | + # +:include_plus_and_minus_in_html+:: Show the +, -, ' ' at the | ||
32 | + # beginning of lines in html output. | ||
33 | + def initialize(string1, string2, options = {}) | ||
34 | + @options = self.class.default_options.merge(options) | ||
35 | + if ! ['strings', 'files'].include?(@options[:source]) | ||
36 | + raise ArgumentError, "Invalid :source option #{@options[:source].inspect}. Supported options are 'strings' and 'files'." | ||
37 | + end | ||
38 | + @string1, @string2 = string1, string2 | ||
39 | + end | ||
40 | + | ||
41 | + def diff | ||
42 | + @diff ||= begin | ||
43 | + paths = case options[:source] | ||
44 | + when 'strings' | ||
45 | + [tempfile(string1), tempfile(string2)] | ||
46 | + when 'files' | ||
47 | + [string1, string2] | ||
48 | + end | ||
49 | + | ||
50 | + if WINDOWS | ||
51 | + # don't use open3 on windows | ||
52 | + cmd = "\"#{diff_bin}\" #{diff_options.join(' ')} #{paths.map{|s| "\"#{s}\""}.join(' ')}" | ||
53 | + diff = `#{cmd}` | ||
54 | + else | ||
55 | + diff = Open3.popen3(diff_bin, *(diff_options + paths)) { |i, o, e| o.read } | ||
56 | + end | ||
57 | + diff.force_encoding('ASCII-8BIT') if diff.respond_to?(:valid_encoding?) && !diff.valid_encoding? | ||
58 | + if diff =~ /\A\s*\Z/ && !options[:allow_empty_diff] | ||
59 | + diff = case options[:source] | ||
60 | + when 'strings' then string1 | ||
61 | + when 'files' then File.read(string1) | ||
62 | + end.gsub(/^/, " ") | ||
63 | + end | ||
64 | + diff | ||
65 | + end | ||
66 | + ensure | ||
67 | + # unlink the tempfiles explicitly now that the diff is generated | ||
68 | + Array(@tempfiles).each do |t| | ||
69 | + begin | ||
70 | + # check that the path is not nil and file still exists. | ||
71 | + # REE seems to be very agressive with when it magically removes | ||
72 | + # tempfiles | ||
73 | + t.unlink if t.path && File.exist?(t.path) | ||
74 | + rescue => e | ||
75 | + warn "#{e.class}: #{e}" | ||
76 | + warn e.backtrace.join("\n") | ||
77 | + end | ||
78 | + end | ||
79 | + end | ||
80 | + | ||
81 | + def each | ||
82 | + lines = case @options[:include_diff_info] | ||
83 | + when false then diff.split("\n").reject{|x| x =~ /^(---|\+\+\+|@@|\\\\)/ }.map {|line| line + "\n" } | ||
84 | + when true then diff.split("\n").map {|line| line + "\n" } | ||
85 | + end | ||
86 | + if block_given? | ||
87 | + lines.each{|line| yield line} | ||
88 | + else | ||
89 | + lines.to_enum | ||
90 | + end | ||
91 | + end | ||
92 | + | ||
93 | + def each_chunk | ||
94 | + old_state = nil | ||
95 | + chunks = inject([]) do |cc, line| | ||
96 | + state = line.each_char.first | ||
97 | + if state == old_state | ||
98 | + cc.last << line | ||
99 | + else | ||
100 | + cc.push line.dup | ||
101 | + end | ||
102 | + old_state = state | ||
103 | + cc | ||
104 | + end | ||
105 | + | ||
106 | + if block_given? | ||
107 | + chunks.each{|chunk| yield chunk } | ||
108 | + else | ||
109 | + chunks.to_enum | ||
110 | + end | ||
111 | + end | ||
112 | + | ||
113 | + def tempfile(string) | ||
114 | + t = Tempfile.new('diffy') | ||
115 | + # ensure tempfiles aren't unlinked when GC runs by maintaining a | ||
116 | + # reference to them. | ||
117 | + @tempfiles ||=[] | ||
118 | + @tempfiles.push(t) | ||
119 | + t.print(string) | ||
120 | + t.flush | ||
121 | + t.close | ||
122 | + t.path | ||
123 | + end | ||
124 | + | ||
125 | + def to_s(format = nil) | ||
126 | + format ||= self.class.default_format | ||
127 | + formats = Format.instance_methods(false).map{|x| x.to_s} | ||
128 | + if formats.include? format.to_s | ||
129 | + enum = self | ||
130 | + enum.extend Format | ||
131 | + enum.send format | ||
132 | + else | ||
133 | + raise ArgumentError, | ||
134 | + "Format #{format.inspect} not found in #{formats.inspect}" | ||
135 | + end | ||
136 | + end | ||
137 | + private | ||
138 | + | ||
139 | + @@bin = nil | ||
140 | + def diff_bin | ||
141 | + return @@bin if @@bin | ||
142 | + | ||
143 | + @@bin ||= "" | ||
144 | + if WINDOWS | ||
145 | + @@bin = `which diff.exe`.chomp if @@bin.empty? | ||
146 | + end | ||
147 | + @@bin = `which diff`.chomp if @@bin.empty? | ||
148 | + | ||
149 | + if @@bin.empty? | ||
150 | + raise "Can't find a diff executable in PATH #{ENV['PATH']}" | ||
151 | + end | ||
152 | + @@bin | ||
153 | + end | ||
154 | + | ||
155 | + # options pass to diff program | ||
156 | + def diff_options | ||
157 | + Array(options[:context] ? "-U #{options[:context]}" : options[:diff]) | ||
158 | + end | ||
159 | + | ||
160 | + end | ||
161 | +end |
@@ -0,0 +1,37 @@ | @@ -0,0 +1,37 @@ | ||
1 | +module Diffy | ||
2 | + module Format | ||
3 | + # ANSI color output suitable for terminal output | ||
4 | + def color | ||
5 | + map do |line| | ||
6 | + case line | ||
7 | + when /^(---|\+\+\+|\\\\)/ | ||
8 | + "\033[90m#{line.chomp}\033[0m" | ||
9 | + when /^\+/ | ||
10 | + "\033[32m#{line.chomp}\033[0m" | ||
11 | + when /^-/ | ||
12 | + "\033[31m#{line.chomp}\033[0m" | ||
13 | + when /^@@/ | ||
14 | + "\033[36m#{line.chomp}\033[0m" | ||
15 | + else | ||
16 | + line.chomp | ||
17 | + end | ||
18 | + end.join("\n") + "\n" | ||
19 | + end | ||
20 | + | ||
21 | + # Basic text output | ||
22 | + def text | ||
23 | + to_a.join | ||
24 | + end | ||
25 | + | ||
26 | + # Basic html output which does not attempt to highlight the changes | ||
27 | + # between lines, and is more performant. | ||
28 | + def html_simple | ||
29 | + HtmlFormatter.new(self, options).to_s | ||
30 | + end | ||
31 | + | ||
32 | + # Html output which does inline highlighting of changes between two lines. | ||
33 | + def html | ||
34 | + HtmlFormatter.new(self, options.merge(:highlight_words => true)).to_s | ||
35 | + end | ||
36 | + end | ||
37 | +end |
@@ -0,0 +1,133 @@ | @@ -0,0 +1,133 @@ | ||
1 | +module Diffy | ||
2 | + class HtmlFormatter | ||
3 | + def initialize(diff, options = {}) | ||
4 | + @diff = diff | ||
5 | + @options = options | ||
6 | + end | ||
7 | + | ||
8 | + def to_s | ||
9 | + if @options[:highlight_words] | ||
10 | + wrap_lines(highlighted_words) | ||
11 | + else | ||
12 | + wrap_lines(@diff.map{|line| wrap_line(ERB::Util.h(line))}) | ||
13 | + end | ||
14 | + end | ||
15 | + | ||
16 | + private | ||
17 | + def wrap_line(line) | ||
18 | + cleaned = clean_line(line) | ||
19 | + case line | ||
20 | + when /^(---|\+\+\+|\\\\)/ | ||
21 | + ' <li class="diff-comment"><span>' + line.chomp + '</span></li>' | ||
22 | + when /^\+/ | ||
23 | + ' <li class="ins"><ins>' + cleaned + '</ins></li>' | ||
24 | + when /^-/ | ||
25 | + ' <li class="del"><del>' + cleaned + '</del></li>' | ||
26 | + when /^ / | ||
27 | + ' <li class="unchanged"><span>' + cleaned + '</span></li>' | ||
28 | + when /^@@/ | ||
29 | + ' <li class="diff-block-info"><span>' + line.chomp + '</span></li>' | ||
30 | + end | ||
31 | + end | ||
32 | + | ||
33 | + # remove +/- or wrap in html | ||
34 | + def clean_line(line) | ||
35 | + if @options[:include_plus_and_minus_in_html] | ||
36 | + line.sub(/^(.)/, '<span class="symbol">\1</span>') | ||
37 | + else | ||
38 | + line.sub(/^./, '') | ||
39 | + end.chomp | ||
40 | + end | ||
41 | + | ||
42 | + def wrap_lines(lines) | ||
43 | + if lines.empty? | ||
44 | + %'<div class="diff"></div>' | ||
45 | + else | ||
46 | + %'<div class="diff">\n <ul>\n#{lines.join("\n")}\n </ul>\n</div>\n' | ||
47 | + end | ||
48 | + end | ||
49 | + | ||
50 | + def highlighted_words | ||
51 | + chunks = @diff.each_chunk. | ||
52 | + reject{|c| c == '\ No newline at end of file'"\n"} | ||
53 | + | ||
54 | + processed = [] | ||
55 | + lines = chunks.each_with_index.map do |chunk1, index| | ||
56 | + next if processed.include? index | ||
57 | + processed << index | ||
58 | + chunk1 = chunk1 | ||
59 | + chunk2 = chunks[index + 1] | ||
60 | + if not chunk2 | ||
61 | + next ERB::Util.h(chunk1) | ||
62 | + end | ||
63 | + | ||
64 | + dir1 = chunk1.each_char.first | ||
65 | + dir2 = chunk2.each_char.first | ||
66 | + case [dir1, dir2] | ||
67 | + when ['-', '+'] | ||
68 | + if chunk1.each_char.take(3).join("") =~ /^(---|\+\+\+|\\\\)/ and | ||
69 | + chunk2.each_char.take(3).join("") =~ /^(---|\+\+\+|\\\\)/ | ||
70 | + ERB::Util.h(chunk1) | ||
71 | + else | ||
72 | + line_diff = Diffy::Diff.new( | ||
73 | + split_characters(chunk1), | ||
74 | + split_characters(chunk2) | ||
75 | + ) | ||
76 | + hi1 = reconstruct_characters(line_diff, '-') | ||
77 | + hi2 = reconstruct_characters(line_diff, '+') | ||
78 | + processed << (index + 1) | ||
79 | + [hi1, hi2] | ||
80 | + end | ||
81 | + else | ||
82 | + ERB::Util.h(chunk1) | ||
83 | + end | ||
84 | + end.flatten | ||
85 | + lines.map{|line| line.each_line.map(&:chomp).to_a if line }.flatten.compact. | ||
86 | + map{|line|wrap_line(line) }.compact | ||
87 | + end | ||
88 | + | ||
89 | + def split_characters(chunk) | ||
90 | + chunk.gsub(/^./, '').each_line.map do |line| | ||
91 | + chars = line.sub(/([\r\n]$)/, '').split('') | ||
92 | + # add escaped newlines | ||
93 | + chars += [($1 || "\n").gsub("\r", '\r').gsub("\n", '\n')] | ||
94 | + chars.map{|chr| ERB::Util.h(chr) } | ||
95 | + end.flatten.join("\n") + "\n" | ||
96 | + end | ||
97 | + | ||
98 | + def reconstruct_characters(line_diff, type) | ||
99 | + enum = line_diff.each_chunk | ||
100 | + enum.each_with_index.map do |l, i| | ||
101 | + re = /(^|\\n)#{Regexp.escape(type)}/ | ||
102 | + case l | ||
103 | + when re | ||
104 | + highlight(l) | ||
105 | + when /^ / | ||
106 | + if i > 1 and enum.to_a[i+1] and l.each_line.to_a.size < 4 | ||
107 | + highlight(l) | ||
108 | + else | ||
109 | + l.gsub(/^./, '').gsub("\n", ''). | ||
110 | + gsub('\r', "\r").gsub('\n', "\n") | ||
111 | + end | ||
112 | + end | ||
113 | + end.join('').split("\n").map do |l| | ||
114 | + type + l.gsub('</strong><strong>' , '') | ||
115 | + end | ||
116 | + end | ||
117 | + | ||
118 | + def highlight(lines) | ||
119 | + "<strong>" + | ||
120 | + lines. | ||
121 | + # strip diff tokens (e.g. +,-,etc.) | ||
122 | + gsub(/(^|\\n)./, ''). | ||
123 | + # mark line boundaries from higher level line diff | ||
124 | + # html is all escaped so using brackets should make this safe. | ||
125 | + gsub('\n', '<LINE_BOUNDARY>'). | ||
126 | + # join characters back by stripping out newlines | ||
127 | + gsub("\n", ''). | ||
128 | + # close and reopen strong tags. we don't want inline elements | ||
129 | + # spanning block elements which get added later. | ||
130 | + gsub('<LINE_BOUNDARY>',"</strong>\n<strong>") + "</strong>" | ||
131 | + end | ||
132 | + end | ||
133 | +end |
@@ -0,0 +1,46 @@ | @@ -0,0 +1,46 @@ | ||
1 | +require 'rubygems' | ||
2 | +require 'sinatra' | ||
3 | +require 'json' | ||
4 | +require File.dirname(__FILE__) + '/../lib/diffy' | ||
5 | + | ||
6 | +blk = proc do | ||
7 | + Diffy::Diff.default_options.merge! JSON.parse(params[:options]) rescue {} | ||
8 | + haml "- d = Diffy::Diff.new(params[:one].to_s, params[:two].to_s)\n%div= d.to_s(:html)\n%pre= d.to_s" | ||
9 | +end | ||
10 | +post '/', &blk | ||
11 | +get '/', &blk | ||
12 | +__END__ | ||
13 | + | ||
14 | +@@ layout | ||
15 | +%html | ||
16 | + %head | ||
17 | + :css | ||
18 | + .diff{overflow:auto;} | ||
19 | + .diff ul{background:#fff;overflow:auto;font-size:13px;list-style:none;margin:0;padding:0;display:table;width:100%;} | ||
20 | + .diff del, .diff ins{display:block;text-decoration:none;} | ||
21 | + .diff li{padding:0; display:table-row;margin: 0;height:1em;} | ||
22 | + .diff li.ins{background:#dfd; color:#080} | ||
23 | + .diff li.del{background:#fee; color:#b00} | ||
24 | + .diff li:hover{background:#ffc} | ||
25 | + .diff del, .diff ins, .diff span{white-space:pre-wrap;font-family:courier;} | ||
26 | + .diff del strong{font-weight:normal;background:#fcc;} | ||
27 | + .diff ins strong{font-weight:normal;background:#9f9;} | ||
28 | + .diff li.diff-comment { display: none; } | ||
29 | + .diff li.diff-block-info { background: none repeat scroll 0 0 gray; } | ||
30 | + %body | ||
31 | + = yield | ||
32 | + %form{:action => '', :method => 'post'} | ||
33 | + %label JSON diff options | ||
34 | + %textarea{:name => 'options', :style => 'width:100%;height:250px;'}= params[:options] | ||
35 | + %label One | ||
36 | + %textarea{:name => 'one', :style => 'width:100%;height:250px;'}= params[:one] | ||
37 | + %br/ | ||
38 | + %label Two | ||
39 | + %textarea{:name => 'two', :style => 'width:100%;height:250px;'}= params[:two] | ||
40 | + %br/ | ||
41 | + %input{:type => 'submit'} | ||
42 | + %br/ | ||
43 | + | ||
44 | +@@ index | ||
45 | +%div.title Hello world!!!!! | ||
46 | + |
@@ -0,0 +1,576 @@ | @@ -0,0 +1,576 @@ | ||
1 | +require 'rspec' | ||
2 | +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'diffy')) | ||
3 | + | ||
4 | +describe Diffy::Diff do | ||
5 | + | ||
6 | + describe "diffing two files" do | ||
7 | + def tempfile(string, fn = 'diffy-spec') | ||
8 | + t = Tempfile.new(fn) | ||
9 | + # ensure tempfiles aren't unlinked when GC runs by maintaining a | ||
10 | + # reference to them. | ||
11 | + @tempfiles ||=[] | ||
12 | + @tempfiles.push(t) | ||
13 | + t.print(string) | ||
14 | + t.flush | ||
15 | + t.close | ||
16 | + t.path | ||
17 | + end | ||
18 | + | ||
19 | + it "should accept file paths as arguments" do | ||
20 | + string1 = "foo\nbar\nbang\n" | ||
21 | + string2 = "foo\nbang\n" | ||
22 | + path1, path2 = tempfile(string1), tempfile(string2) | ||
23 | + Diffy::Diff.new(path1, path2, :source => 'files').to_s.should == <<-DIFF | ||
24 | + foo | ||
25 | +-bar | ||
26 | + bang | ||
27 | + DIFF | ||
28 | + end | ||
29 | + | ||
30 | + it "should accept file paths with spaces as arguments" do | ||
31 | + string1 = "foo\nbar\nbang\n" | ||
32 | + string2 = "foo\nbang\n" | ||
33 | + path1, path2 = tempfile(string1, 'path with spaces'), tempfile(string2, 'path with spaces') | ||
34 | + Diffy::Diff.new(path1, path2, :source => 'files').to_s.should == <<-DIFF | ||
35 | + foo | ||
36 | +-bar | ||
37 | + bang | ||
38 | + DIFF | ||
39 | + end | ||
40 | + | ||
41 | + it "should accept file paths with spaces as arguments on windows" do | ||
42 | + begin | ||
43 | + | ||
44 | + orig_verbose, $VERBOSE = $VERBOSE, nil #silence redefine constant warnings | ||
45 | + orig_windows, Diffy::WINDOWS = Diffy::WINDOWS, true | ||
46 | + string1 = "foo\nbar\nbang\n" | ||
47 | + string2 = "foo\nbang\n" | ||
48 | + path1, path2 = tempfile(string1, 'path with spaces'), tempfile(string2, 'path with spaces') | ||
49 | + Diffy::Diff.new(path1, path2, :source => 'files').to_s.should == <<-DIFF | ||
50 | + foo | ||
51 | +-bar | ||
52 | + bang | ||
53 | + DIFF | ||
54 | + ensure | ||
55 | + Diffy::WINDOWS, $VERBOSE = orig_windows, orig_verbose | ||
56 | + end | ||
57 | + | ||
58 | + end | ||
59 | + | ||
60 | + describe "with no line different" do | ||
61 | + before do | ||
62 | + string1 = "foo\nbar\nbang\n" | ||
63 | + string2 = "foo\nbar\nbang\n" | ||
64 | + @path1, @path2 = tempfile(string1), tempfile(string2) | ||
65 | + end | ||
66 | + | ||
67 | + it "should show everything" do | ||
68 | + Diffy::Diff.new(@path1, @path2, :source => 'files', :allow_empty_diff => false). | ||
69 | + to_s.should == <<-DIFF | ||
70 | + foo | ||
71 | + bar | ||
72 | + bang | ||
73 | + DIFF | ||
74 | + end | ||
75 | + | ||
76 | + it "should not show everything if the :allow_empty_diff option is set" do | ||
77 | + Diffy::Diff.new(@path1, @path2, :source => 'files', :allow_empty_diff => true).to_s.should == '' | ||
78 | + end | ||
79 | + end | ||
80 | + describe "with lines that start with backslashes" do | ||
81 | + before do | ||
82 | + string1 = "foo\n\\\\bag\nbang\n" | ||
83 | + string2 = "foo\n\\\\bar\nbang\n" | ||
84 | + @path1, @path2 = tempfile(string1), tempfile(string2) | ||
85 | + end | ||
86 | + | ||
87 | + it "should not leave lines out" do | ||
88 | + Diffy::Diff.new(@path1, @path2, :source => 'files').to_s.should == <<-DIFF | ||
89 | + foo | ||
90 | +-\\\\bag | ||
91 | ++\\\\bar | ||
92 | + bang | ||
93 | + DIFF | ||
94 | + end | ||
95 | + end | ||
96 | + | ||
97 | + describe "with non valid UTF bytes" do | ||
98 | + before do | ||
99 | + string1 = "Foo ICS95095010000000000083320000BS01030000004100+\xFF00000000000000000\n" | ||
100 | + string2 = "Bar ICS95095010000000000083320000BS01030000004100+\xFF00000000000000000\n" | ||
101 | + @path1, @path2 = tempfile(string1), tempfile(string2) | ||
102 | + end | ||
103 | + it "should not raise invalid encoding issues" do | ||
104 | + desired = <<-DIFF | ||
105 | +-Foo ICS95095010000000000083320000BS01030000004100+\xFF00000000000000000 | ||
106 | ++Bar ICS95095010000000000083320000BS01030000004100+\xFF00000000000000000 | ||
107 | + DIFF | ||
108 | + desired.force_encoding("ASCII-8BIT") if desired.respond_to?(:force_encoding) | ||
109 | + Diffy::Diff.new(@path1, @path2, :source => 'files').to_s.should == desired | ||
110 | + end | ||
111 | + end | ||
112 | + | ||
113 | + end | ||
114 | + | ||
115 | + describe "handling temp files" do | ||
116 | + it "should unlink tempfiles after generating the diff" do | ||
117 | + before_tmpfiles = Dir.entries(Dir.tmpdir) | ||
118 | + d = ::Diffy::Diff.new("a", "b").to_s | ||
119 | + after_tmpfiles = Dir.entries(Dir.tmpdir) | ||
120 | + before_tmpfiles.should =~ after_tmpfiles | ||
121 | + end | ||
122 | + | ||
123 | + it "should still be able to generate multiple diffs" do | ||
124 | + d = ::Diffy::Diff.new("a", "b") | ||
125 | + d.to_s.should be_a String | ||
126 | + d.to_s(:html).should be_a String | ||
127 | + end | ||
128 | + end | ||
129 | + | ||
130 | + describe "options[:context]" do | ||
131 | + it "should limit context lines to 1" do | ||
132 | + diff = Diffy::Diff.new("foo\nfoo\nBAR\nbang\nbaz", "foo\nfoo\nbar\nbang\nbaz", :context => 1) | ||
133 | + diff.to_s.should == <<-DIFF | ||
134 | + foo | ||
135 | +-BAR | ||
136 | ++bar | ||
137 | + bang | ||
138 | + DIFF | ||
139 | + end | ||
140 | + end | ||
141 | + | ||
142 | + describe "options[:include_plus_and_minus_in_html]" do | ||
143 | + it "defaults to false" do | ||
144 | + @diffy = Diffy::Diff.new(" foo\nbar\n", "foo\nbar\n") | ||
145 | + @diffy.options[:include_plus_and_minus_in_html].should == false | ||
146 | + end | ||
147 | + | ||
148 | + it "can be set to true" do | ||
149 | + @diffy = Diffy::Diff.new(" foo\nbar\n", "foo\nbar\n", :include_plus_and_minus_in_html=> true ) | ||
150 | + @diffy.options[:include_plus_and_minus_in_html].should == true | ||
151 | + end | ||
152 | + | ||
153 | + describe "formats" do | ||
154 | + it "includes symbols in html_simple" do | ||
155 | + output = Diffy::Diff.new("foo\nbar\nbang\n", "foo\nbang\n", :include_plus_and_minus_in_html => true ). | ||
156 | + to_s(:html_simple) | ||
157 | + output.should == <<-HTML | ||
158 | +<div class="diff"> | ||
159 | + <ul> | ||
160 | + <li class="unchanged"><span><span class="symbol"> </span>foo</span></li> | ||
161 | + <li class="del"><del><span class="symbol">-</span>bar</del></li> | ||
162 | + <li class="unchanged"><span><span class="symbol"> </span>bang</span></li> | ||
163 | + </ul> | ||
164 | +</div> | ||
165 | + HTML | ||
166 | + end | ||
167 | + | ||
168 | + it "includes symbols in html" do | ||
169 | + output = Diffy::Diff.new("foo\nbar\nbang\n", "foo\naba\nbang\n", :include_plus_and_minus_in_html => true ). | ||
170 | + to_s(:html) | ||
171 | + output.should == <<-HTML | ||
172 | +<div class="diff"> | ||
173 | + <ul> | ||
174 | + <li class="unchanged"><span><span class="symbol"> </span>foo</span></li> | ||
175 | + <li class="del"><del><span class="symbol">-</span>ba<strong>r</strong></del></li> | ||
176 | + <li class="ins"><ins><span class="symbol">+</span><strong>a</strong>ba</ins></li> | ||
177 | + <li class="unchanged"><span><span class="symbol"> </span>bang</span></li> | ||
178 | + </ul> | ||
179 | +</div> | ||
180 | + HTML | ||
181 | + end | ||
182 | + | ||
183 | + end | ||
184 | + | ||
185 | + end | ||
186 | + | ||
187 | + describe "options[:include_diff_info]" do | ||
188 | + it "defaults to false" do | ||
189 | + @diffy = Diffy::Diff.new(" foo\nbar\n", "foo\nbar\n") | ||
190 | + @diffy.options[:include_diff_info].should == false | ||
191 | + end | ||
192 | + | ||
193 | + it "can be set to true" do | ||
194 | + @diffy = Diffy::Diff.new(" foo\nbar\n", "foo\nbar\n", :include_diff_info => true ) | ||
195 | + @diffy.options[:include_diff_info].should == true | ||
196 | + end | ||
197 | + | ||
198 | + it "includes all diff output" do | ||
199 | + output = Diffy::Diff.new("foo\nbar\nbang\n", "foo\nbang\n", :include_diff_info => true ).to_s | ||
200 | + output.to_s.should match( /@@/) | ||
201 | + output.should match( /---/) | ||
202 | + output.should match( /\+\+\+/) | ||
203 | + end | ||
204 | + | ||
205 | + describe "formats" do | ||
206 | + it "works for :color" do | ||
207 | + output = Diffy::Diff.new("foo\nbar\nbang\n", "foo\nbang\n", :include_diff_info => true ).to_s(:color) | ||
208 | + output.should match( /\e\[0m\n\e\[36m\@\@/ ) | ||
209 | + output.to_s.should match( /\e\[90m---/) | ||
210 | + output.to_s.should match( /\e\[0m\n\e\[90m\+\+\+/) | ||
211 | + end | ||
212 | + | ||
213 | + it "works for :html_simple" do | ||
214 | + output = Diffy::Diff.new("foo\nbar\nbang\n", "foo\nbang\n", :include_diff_info => true ).to_s(:html_simple) | ||
215 | + output.split("\n").should include( " <li class=\"diff-block-info\"><span>@@ -1,3 +1,2 @@</span></li>" ) | ||
216 | + output.should include( "<li class=\"diff-comment\"><span>---") | ||
217 | + output.should include( "<li class=\"diff-comment\"><span>+++") | ||
218 | + end | ||
219 | + end | ||
220 | + end | ||
221 | + | ||
222 | + describe "options[:diff]" do | ||
223 | + it "should accept an option to diff" do | ||
224 | + @diff = Diffy::Diff.new(" foo\nbar\n", "foo\nbar\n", :diff => "-w", :allow_empty_diff => false) | ||
225 | + @diff.to_s.should == <<-DIFF | ||
226 | + foo | ||
227 | + bar | ||
228 | + DIFF | ||
229 | + end | ||
230 | + | ||
231 | + it "should accept multiple arguments to diff" do | ||
232 | + @diff = Diffy::Diff.new(" foo\nbar\n", "foo\nbaz\n", :diff => ["-w", "-U 3"]) | ||
233 | + @diff.to_s.should == <<-DIFF | ||
234 | + foo | ||
235 | +-bar | ||
236 | ++baz | ||
237 | + DIFF | ||
238 | + end | ||
239 | + end | ||
240 | + | ||
241 | + describe "#to_s" do | ||
242 | + describe "with no line different" do | ||
243 | + before do | ||
244 | + @string1 = "foo\nbar\nbang\n" | ||
245 | + @string2 = "foo\nbar\nbang\n" | ||
246 | + end | ||
247 | + | ||
248 | + it "should show everything" do | ||
249 | + Diffy::Diff.new(@string1, @string2, :allow_empty_diff => false).to_s.should == <<-DIFF | ||
250 | + foo | ||
251 | + bar | ||
252 | + bang | ||
253 | + DIFF | ||
254 | + end | ||
255 | + end | ||
256 | + describe "with one line different" do | ||
257 | + before do | ||
258 | + @string1 = "foo\nbar\nbang\n" | ||
259 | + @string2 = "foo\nbang\n" | ||
260 | + end | ||
261 | + | ||
262 | + it "should show one line removed" do | ||
263 | + Diffy::Diff.new(@string1, @string2).to_s.should == <<-DIFF | ||
264 | + foo | ||
265 | +-bar | ||
266 | + bang | ||
267 | + DIFF | ||
268 | + end | ||
269 | + | ||
270 | + it "to_s should accept a format key" do | ||
271 | + Diffy::Diff.new(@string1, @string2).to_s(:color). | ||
272 | + should == " foo\n\e[31m-bar\e[0m\n bang\n" | ||
273 | + end | ||
274 | + | ||
275 | + it "should accept a default format option" do | ||
276 | + old_format = Diffy::Diff.default_format | ||
277 | + Diffy::Diff.default_format = :color | ||
278 | + Diffy::Diff.new(@string1, @string2).to_s. | ||
279 | + should == " foo\n\e[31m-bar\e[0m\n bang\n" | ||
280 | + Diffy::Diff.default_format = old_format | ||
281 | + end | ||
282 | + | ||
283 | + it "should accept a default options" do | ||
284 | + old_options = Diffy::Diff.default_options | ||
285 | + Diffy::Diff.default_options = old_options.merge(:include_diff_info => true) | ||
286 | + Diffy::Diff.new(@string1, @string2).to_s. | ||
287 | + should include('@@ -1,3 +1,2 @@') | ||
288 | + Diffy::Diff.default_options = old_options | ||
289 | + end | ||
290 | + | ||
291 | + it "should show one line added" do | ||
292 | + Diffy::Diff.new(@string2, @string1).to_s. | ||
293 | + should == <<-DIFF | ||
294 | + foo | ||
295 | ++bar | ||
296 | + bang | ||
297 | + DIFF | ||
298 | + end | ||
299 | + end | ||
300 | + | ||
301 | + describe "with one line changed" do | ||
302 | + before do | ||
303 | + @string1 = "foo\nbar\nbang\n" | ||
304 | + @string2 = "foo\nbong\nbang\n" | ||
305 | + end | ||
306 | + it "should show one line added and one removed" do | ||
307 | + Diffy::Diff.new(@string1, @string2).to_s.should == <<-DIFF | ||
308 | + foo | ||
309 | +-bar | ||
310 | ++bong | ||
311 | + bang | ||
312 | + DIFF | ||
313 | + end | ||
314 | + end | ||
315 | + | ||
316 | + describe "with totally different strings" do | ||
317 | + before do | ||
318 | + @string1 = "foo\nbar\nbang\n" | ||
319 | + @string2 = "one\ntwo\nthree\n" | ||
320 | + end | ||
321 | + it "should show one line added and one removed" do | ||
322 | + Diffy::Diff.new(@string1, @string2).to_s.should == <<-DIFF | ||
323 | +-foo | ||
324 | +-bar | ||
325 | +-bang | ||
326 | ++one | ||
327 | ++two | ||
328 | ++three | ||
329 | + DIFF | ||
330 | + end | ||
331 | + end | ||
332 | + | ||
333 | + describe "with a somewhat complicated diff" do | ||
334 | + before do | ||
335 | + @string1 = "foo\nbar\nbang\nwoot\n" | ||
336 | + @string2 = "one\ntwo\nthree\nbar\nbang\nbaz\n" | ||
337 | + @diff = Diffy::Diff.new(@string1, @string2) | ||
338 | + end | ||
339 | + it "should show one line added and one removed" do | ||
340 | + @diff.to_s.should == <<-DIFF | ||
341 | +-foo | ||
342 | ++one | ||
343 | ++two | ||
344 | ++three | ||
345 | + bar | ||
346 | + bang | ||
347 | +-woot | ||
348 | ++baz | ||
349 | + DIFF | ||
350 | + end | ||
351 | + | ||
352 | + it "should make an awesome simple html diff" do | ||
353 | + @diff.to_s(:html_simple).should == <<-HTML | ||
354 | +<div class="diff"> | ||
355 | + <ul> | ||
356 | + <li class="del"><del>foo</del></li> | ||
357 | + <li class="ins"><ins>one</ins></li> | ||
358 | + <li class="ins"><ins>two</ins></li> | ||
359 | + <li class="ins"><ins>three</ins></li> | ||
360 | + <li class="unchanged"><span>bar</span></li> | ||
361 | + <li class="unchanged"><span>bang</span></li> | ||
362 | + <li class="del"><del>woot</del></li> | ||
363 | + <li class="ins"><ins>baz</ins></li> | ||
364 | + </ul> | ||
365 | +</div> | ||
366 | + HTML | ||
367 | + end | ||
368 | + | ||
369 | + it "should accept overrides to diff's options" do | ||
370 | + @diff = Diffy::Diff.new(@string1, @string2, :diff => "--rcs") | ||
371 | + @diff.to_s.should == <<-DIFF | ||
372 | +d1 1 | ||
373 | +a1 3 | ||
374 | +one | ||
375 | +two | ||
376 | +three | ||
377 | +d4 1 | ||
378 | +a4 1 | ||
379 | +baz | ||
380 | + DIFF | ||
381 | + end | ||
382 | + end | ||
383 | + | ||
384 | + describe "html" do | ||
385 | + it "should not allow html injection on the last line" do | ||
386 | + @string1 = "hahaha\ntime flies like an arrow\nfoo bar\nbang baz\n<script>\n" | ||
387 | + @string2 = "hahaha\nfruit flies like a banana\nbang baz\n<script>\n" | ||
388 | + @diff = Diffy::Diff.new(@string1, @string2) | ||
389 | + html = <<-HTML | ||
390 | +<div class="diff"> | ||
391 | + <ul> | ||
392 | + <li class="unchanged"><span>hahaha</span></li> | ||
393 | + <li class="del"><del><strong>time</strong> flies like a<strong>n arrow</strong></del></li> | ||
394 | + <li class="del"><del><strong>foo bar</strong></del></li> | ||
395 | + <li class="ins"><ins><strong>fruit</strong> flies like a<strong> banana</strong></ins></li> | ||
396 | + <li class="unchanged"><span>bang baz</span></li> | ||
397 | + <li class="unchanged"><span><script></span></li> | ||
398 | + </ul> | ||
399 | +</div> | ||
400 | + HTML | ||
401 | + @diff.to_s(:html).should == html | ||
402 | + end | ||
403 | + | ||
404 | + it "should highlight the changes within the line" do | ||
405 | + @string1 = "hahaha\ntime flies like an arrow\nfoo bar\nbang baz\n" | ||
406 | + @string2 = "hahaha\nfruit flies like a banana\nbang baz\n" | ||
407 | + @diff = Diffy::Diff.new(@string1, @string2) | ||
408 | + html = <<-HTML | ||
409 | +<div class="diff"> | ||
410 | + <ul> | ||
411 | + <li class="unchanged"><span>hahaha</span></li> | ||
412 | + <li class="del"><del><strong>time</strong> flies like a<strong>n arrow</strong></del></li> | ||
413 | + <li class="del"><del><strong>foo bar</strong></del></li> | ||
414 | + <li class="ins"><ins><strong>fruit</strong> flies like a<strong> banana</strong></ins></li> | ||
415 | + <li class="unchanged"><span>bang baz</span></li> | ||
416 | + </ul> | ||
417 | +</div> | ||
418 | + HTML | ||
419 | + @diff.to_s(:html).should == html | ||
420 | + end | ||
421 | + | ||
422 | + it "should not duplicate some lines" do | ||
423 | + @string1 = "hahaha\ntime flies like an arrow\n" | ||
424 | + @string2 = "hahaha\nfruit flies like a banana\nbang baz" | ||
425 | + @diff = Diffy::Diff.new(@string1, @string2) | ||
426 | + html = <<-HTML | ||
427 | +<div class="diff"> | ||
428 | + <ul> | ||
429 | + <li class="unchanged"><span>hahaha</span></li> | ||
430 | + <li class="del"><del><strong>time</strong> flies like a<strong>n arrow</strong></del></li> | ||
431 | + <li class="ins"><ins><strong>fruit</strong> flies like a<strong> banana</strong></ins></li> | ||
432 | + <li class="ins"><ins><strong>bang baz</strong></ins></li> | ||
433 | + </ul> | ||
434 | +</div> | ||
435 | + HTML | ||
436 | + @diff.to_s(:html).should == html | ||
437 | + end | ||
438 | + | ||
439 | + it "should escape html" do | ||
440 | + @string1 = "ha<br>haha\ntime flies like an arrow\n" | ||
441 | + @string2 = "ha<br>haha\nfruit flies like a banana\nbang baz" | ||
442 | + @diff = Diffy::Diff.new(@string1, @string2) | ||
443 | + html = <<-HTML | ||
444 | +<div class="diff"> | ||
445 | + <ul> | ||
446 | + <li class="unchanged"><span>ha<br>haha</span></li> | ||
447 | + <li class="del"><del><strong>time</strong> flies like a<strong>n arrow</strong></del></li> | ||
448 | + <li class="ins"><ins><strong>fruit</strong> flies like a<strong> banana</strong></ins></li> | ||
449 | + <li class="ins"><ins><strong>bang baz</strong></ins></li> | ||
450 | + </ul> | ||
451 | +</div> | ||
452 | + HTML | ||
453 | + @diff.to_s(:html).should == html | ||
454 | + end | ||
455 | + | ||
456 | + it "should not double escape html in wierd edge cases" do | ||
457 | + @string1 = "preface = (! title .)+ title &{YYACCEPT}\n" | ||
458 | + @string2 = "preface = << (! title .)+ title >> &{YYACCEPT}\n" | ||
459 | + @diff = Diffy::Diff.new @string1, @string2 | ||
460 | + html = <<-HTML | ||
461 | +<div class="diff"> | ||
462 | + <ul> | ||
463 | + <li class="del"><del>preface = (! title .)+ title &{YYACCEPT}</del></li> | ||
464 | + <li class="ins"><ins>preface = <strong><< </strong>(! title .)+ title <strong>>> </strong>&{YYACCEPT}</ins></li> | ||
465 | + </ul> | ||
466 | +</div> | ||
467 | + HTML | ||
468 | + @diff.to_s(:html).should == html | ||
469 | + end | ||
470 | + | ||
471 | + it "should highlight the changes within the line with windows style line breaks" do | ||
472 | + @string1 = "hahaha\r\ntime flies like an arrow\r\nfoo bar\r\nbang baz\n" | ||
473 | + @string2 = "hahaha\r\nfruit flies like a banana\r\nbang baz\n" | ||
474 | + @diff = Diffy::Diff.new(@string1, @string2) | ||
475 | + html = <<-HTML | ||
476 | +<div class="diff"> | ||
477 | + <ul> | ||
478 | + <li class="unchanged"><span>hahaha</span></li> | ||
479 | + <li class="del"><del><strong>time</strong> flies like a<strong>n arrow</strong></del></li> | ||
480 | + <li class="del"><del><strong>foo bar</strong></del></li> | ||
481 | + <li class="ins"><ins><strong>fruit</strong> flies like a<strong> banana</strong></ins></li> | ||
482 | + <li class="unchanged"><span>bang baz</span></li> | ||
483 | + </ul> | ||
484 | +</div> | ||
485 | + HTML | ||
486 | + @diff.to_s(:html).should == html | ||
487 | + end | ||
488 | + | ||
489 | + it "should treat unix vs windows newlines as differences" do | ||
490 | + @diff = Diffy::Diff.new("one\ntwo\nthree\n", "one\r\ntwo\r\nthree\r\n") | ||
491 | + html = <<-HTML | ||
492 | +<div class="diff"> | ||
493 | + <ul> | ||
494 | + <li class="del"><del>one</del></li> | ||
495 | + <li class="del"><del>two</del></li> | ||
496 | + <li class="del"><del>three</del></li> | ||
497 | + <li class="ins"><ins>one<strong></strong></ins></li> | ||
498 | + <li class="ins"><ins>two<strong></strong></ins></li> | ||
499 | + <li class="ins"><ins>three<strong></strong></ins></li> | ||
500 | + </ul> | ||
501 | +</div> | ||
502 | + HTML | ||
503 | + @diff.to_s(:html).should == html | ||
504 | + end | ||
505 | + | ||
506 | + describe 'with lines that include \n' do | ||
507 | + before do | ||
508 | + string1 = 'a\nb'"\n" | ||
509 | + | ||
510 | + string2 = 'acb'"\n" | ||
511 | + @string1, @string2 = string1, string2 | ||
512 | + end | ||
513 | + | ||
514 | + it "should not leave lines out" do | ||
515 | + Diffy::Diff.new(@string1, @string2 ).to_s(:html).should == <<-DIFF | ||
516 | +<div class="diff"> | ||
517 | + <ul> | ||
518 | + <li class="del"><del>a<strong>\\n</strong>b</del></li> | ||
519 | + <li class="ins"><ins>a<strong>c</strong>b</ins></li> | ||
520 | + </ul> | ||
521 | +</div> | ||
522 | + DIFF | ||
523 | + end | ||
524 | + end | ||
525 | + | ||
526 | + it "should do highlighting on the last line when there's no trailing newlines" do | ||
527 | + s1 = "foo\nbar\nbang" | ||
528 | + s2 = "foo\nbar\nbangleize" | ||
529 | + Diffy::Diff.new(s1,s2).to_s(:html).should == <<-DIFF | ||
530 | +<div class="diff"> | ||
531 | + <ul> | ||
532 | + <li class="unchanged"><span>foo</span></li> | ||
533 | + <li class="unchanged"><span>bar</span></li> | ||
534 | + <li class="del"><del>bang</del></li> | ||
535 | + <li class="ins"><ins>bang<strong>leize</strong></ins></li> | ||
536 | + </ul> | ||
537 | +</div> | ||
538 | + DIFF | ||
539 | + end | ||
540 | + end | ||
541 | + | ||
542 | + it "should escape diffed html in html output" do | ||
543 | + diff = Diffy::Diff.new("<script>alert('bar')</script>", "<script>alert('foo')</script>").to_s(:html) | ||
544 | + diff.should include('<script>') | ||
545 | + diff.should_not include('<script>') | ||
546 | + end | ||
547 | + | ||
548 | + it "should be easy to generate custom format" do | ||
549 | + Diffy::Diff.new("foo\nbar\n", "foo\nbar\nbaz\n").map do |line| | ||
550 | + case line | ||
551 | + when /^\+/ then "line #{line.chomp} added" | ||
552 | + when /^-/ then "line #{line.chomp} removed" | ||
553 | + end | ||
554 | + end.compact.join.should == "line +baz added" | ||
555 | + end | ||
556 | + | ||
557 | + it "should let you iterate over chunks instead of lines" do | ||
558 | + Diffy::Diff.new("foo\nbar\n", "foo\nbar\nbaz\n").each_chunk.map do |chunk| | ||
559 | + chunk | ||
560 | + end.should == [" foo\n bar\n", "+baz\n"] | ||
561 | + end | ||
562 | + | ||
563 | + it "should allow chaining enumerable methods" do | ||
564 | + Diffy::Diff.new("foo\nbar\n", "foo\nbar\nbaz\n").each.map do |line| | ||
565 | + line | ||
566 | + end.should == [" foo\n", " bar\n", "+baz\n"] | ||
567 | + end | ||
568 | + end | ||
569 | +end | ||
570 | + | ||
571 | +describe 'Diffy::CSS' do | ||
572 | + it "should be some css" do | ||
573 | + Diffy::CSS.should include 'diff{overflow:auto;}' | ||
574 | + end | ||
575 | +end | ||
576 | + |