ydiffy 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CHANGELOG +40 -0
- data/CONTRIBUTORS +13 -0
- data/Gemfile +7 -0
- data/LICENSE +19 -0
- data/README.md +334 -0
- data/Rakefile +11 -0
- data/lib/diffy.rb +13 -0
- data/lib/diffy/css.rb +34 -0
- data/lib/diffy/diff.rb +171 -0
- data/lib/diffy/format.rb +37 -0
- data/lib/diffy/html_formatter.rb +135 -0
- data/lib/diffy/split_diff.rb +49 -0
- data/lib/diffy/version.rb +3 -0
- data/spec/demo_app.rb +46 -0
- data/spec/diffy_spec.rb +676 -0
- data/ydiffy.gemspec +23 -0
- metadata +93 -0
data/lib/diffy.rb
ADDED
@@ -0,0 +1,13 @@
|
|
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', 'split_diff')
|
13
|
+
require File.join(File.dirname(__FILE__), 'diffy', 'css')
|
data/lib/diffy/css.rb
ADDED
@@ -0,0 +1,34 @@
|
|
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
|
+
|
18
|
+
CSS_COLORBLIND_1 = <<-STYLE
|
19
|
+
.diff{overflow:auto;}
|
20
|
+
.diff ul{background:#fff;overflow:auto;font-size:13px;list-style:none;margin:0;padding:0;display:table;width:100%;}
|
21
|
+
.diff del, .diff ins{display:block;text-decoration:none;}
|
22
|
+
.diff li{padding:0; display:table-row;margin: 0;height:1em;}
|
23
|
+
.diff li.ins{background:#ddf; color:#008}
|
24
|
+
.diff li.del{background:#fee; color:#b00}
|
25
|
+
.diff li:hover{background:#ffc}
|
26
|
+
/* try 'whitespace:pre;' if you don't want lines to wrap */
|
27
|
+
.diff del, .diff ins, .diff span{white-space:pre-wrap;font-family:courier;}
|
28
|
+
.diff del strong{font-weight:normal;background:#fcc;}
|
29
|
+
.diff ins strong{font-weight:normal;background:#99f;}
|
30
|
+
.diff li.diff-comment { display: none; }
|
31
|
+
.diff li.diff-block-info { background: none repeat scroll 0 0 gray; }
|
32
|
+
STYLE
|
33
|
+
|
34
|
+
end
|
data/lib/diffy/diff.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
module Diffy
|
2
|
+
class Diff
|
3
|
+
ORIGINAL_DEFAULT_OPTIONS = {
|
4
|
+
:diff => '-U 10000',
|
5
|
+
:source => 'strings',
|
6
|
+
:include_diff_info => false,
|
7
|
+
:include_plus_and_minus_in_html => false,
|
8
|
+
:context => nil,
|
9
|
+
:allow_empty_diff => true,
|
10
|
+
}
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_writer :default_format
|
14
|
+
def default_format
|
15
|
+
@default_format ||= :text
|
16
|
+
end
|
17
|
+
|
18
|
+
# default options passed to new Diff objects
|
19
|
+
attr_writer :default_options
|
20
|
+
def default_options
|
21
|
+
@default_options ||= ORIGINAL_DEFAULT_OPTIONS.dup
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
include Enumerable
|
26
|
+
attr_reader :string1, :string2, :options
|
27
|
+
|
28
|
+
# supported options
|
29
|
+
# +:diff+:: A cli options string passed to diff
|
30
|
+
# +:source+:: Either _strings_ or _files_. Determines whether string1
|
31
|
+
# and string2 should be interpreted as strings or file paths.
|
32
|
+
# +:include_diff_info+:: Include diff header info
|
33
|
+
# +:include_plus_and_minus_in_html+:: Show the +, -, ' ' at the
|
34
|
+
# beginning of lines in html output.
|
35
|
+
def initialize(string1, string2, options = {})
|
36
|
+
@options = self.class.default_options.merge(options)
|
37
|
+
if ! ['strings', 'files'].include?(@options[:source])
|
38
|
+
raise ArgumentError, "Invalid :source option #{@options[:source].inspect}. Supported options are 'strings' and 'files'."
|
39
|
+
end
|
40
|
+
@string1, @string2 = string1, string2
|
41
|
+
end
|
42
|
+
|
43
|
+
def diff
|
44
|
+
@diff ||= begin
|
45
|
+
paths = case options[:source]
|
46
|
+
when 'strings'
|
47
|
+
[tempfile(string1), tempfile(string2)]
|
48
|
+
when 'files'
|
49
|
+
[string1, string2]
|
50
|
+
end
|
51
|
+
|
52
|
+
if WINDOWS
|
53
|
+
# don't use open3 on windows
|
54
|
+
cmd = sprintf '"%s" %s %s', diff_bin, diff_options.join(' '), paths.map { |s| %("#{s}") }.join(' ')
|
55
|
+
diff = `#{cmd}`
|
56
|
+
else
|
57
|
+
diff = Open3.popen3(diff_bin, *(diff_options + paths)) { |i, o, e| o.read }
|
58
|
+
end
|
59
|
+
diff.force_encoding('ASCII-8BIT') if diff.respond_to?(:valid_encoding?) && !diff.valid_encoding?
|
60
|
+
if diff =~ /\A\s*\Z/ && !options[:allow_empty_diff]
|
61
|
+
diff = case options[:source]
|
62
|
+
when 'strings' then string1
|
63
|
+
when 'files' then File.read(string1)
|
64
|
+
end.gsub(/^/, " ")
|
65
|
+
end
|
66
|
+
diff
|
67
|
+
end
|
68
|
+
ensure
|
69
|
+
# unlink the tempfiles explicitly now that the diff is generated
|
70
|
+
if defined? @tempfiles # to avoid Ruby warnings about undefined ins var.
|
71
|
+
Array(@tempfiles).each do |t|
|
72
|
+
begin
|
73
|
+
# check that the path is not nil and file still exists.
|
74
|
+
# REE seems to be very agressive with when it magically removes
|
75
|
+
# tempfiles
|
76
|
+
t.unlink if t.path && File.exist?(t.path)
|
77
|
+
rescue => e
|
78
|
+
warn "#{e.class}: #{e}"
|
79
|
+
warn e.backtrace.join("\n")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
@tempfiles = []
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def each
|
87
|
+
lines = case @options[:include_diff_info]
|
88
|
+
when false then diff.split("\n").reject{|x| x =~ /^(---|\+\+\+|@@|\\\\)/ }.map {|line| line + "\n" }
|
89
|
+
when true then diff.split("\n").map {|line| line + "\n" }
|
90
|
+
end
|
91
|
+
if block_given?
|
92
|
+
lines.each{|line| yield line}
|
93
|
+
else
|
94
|
+
lines.to_enum
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def each_chunk
|
99
|
+
old_state = nil
|
100
|
+
chunks = inject([]) do |cc, line|
|
101
|
+
state = line.each_char.first
|
102
|
+
if state == old_state
|
103
|
+
cc.last << line
|
104
|
+
else
|
105
|
+
cc.push line.dup
|
106
|
+
end
|
107
|
+
old_state = state
|
108
|
+
cc
|
109
|
+
end
|
110
|
+
|
111
|
+
if block_given?
|
112
|
+
chunks.each{|chunk| yield chunk }
|
113
|
+
else
|
114
|
+
chunks.to_enum
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def tempfile(string)
|
119
|
+
t = Tempfile.new('diffy')
|
120
|
+
# ensure tempfiles aren't unlinked when GC runs by maintaining a
|
121
|
+
# reference to them.
|
122
|
+
@tempfiles ||=[]
|
123
|
+
@tempfiles.push(t)
|
124
|
+
t.print(string)
|
125
|
+
t.flush
|
126
|
+
t.close
|
127
|
+
t.path
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_s(format = nil)
|
131
|
+
format ||= self.class.default_format
|
132
|
+
formats = Format.instance_methods(false).map{|x| x.to_s}
|
133
|
+
if formats.include? format.to_s
|
134
|
+
enum = self
|
135
|
+
enum.extend Format
|
136
|
+
enum.send format
|
137
|
+
else
|
138
|
+
raise ArgumentError,
|
139
|
+
"Format #{format.inspect} not found in #{formats.inspect}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
private
|
143
|
+
|
144
|
+
@@bin = nil
|
145
|
+
def diff_bin
|
146
|
+
return @@bin if @@bin
|
147
|
+
|
148
|
+
if @@bin = ENV['DIFFY_DIFF']
|
149
|
+
# system() trick from Minitest
|
150
|
+
raise "Can't execute diff program '#@@bin'" unless system(@@bin, __FILE__, __FILE__)
|
151
|
+
return @@bin
|
152
|
+
end
|
153
|
+
|
154
|
+
diffs = ['diff', 'ldiff']
|
155
|
+
diffs.first << '.exe' if WINDOWS # ldiff does not have exe extension
|
156
|
+
@@bin = diffs.find { |name| system(name, __FILE__, __FILE__) }
|
157
|
+
|
158
|
+
if @@bin.nil?
|
159
|
+
raise "Can't find a diff executable in PATH #{ENV['PATH']}"
|
160
|
+
end
|
161
|
+
|
162
|
+
@@bin
|
163
|
+
end
|
164
|
+
|
165
|
+
# options pass to diff program
|
166
|
+
def diff_options
|
167
|
+
Array(options[:context] ? "-U #{options[:context]}" : options[:diff])
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
data/lib/diffy/format.rb
ADDED
@@ -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,135 @@
|
|
1
|
+
module Diffy
|
2
|
+
class HtmlFormatter
|
3
|
+
|
4
|
+
def initialize(diff, options = {})
|
5
|
+
@diff = diff
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
if @options[:highlight_words]
|
11
|
+
wrap_lines(highlighted_words)
|
12
|
+
else
|
13
|
+
wrap_lines(@diff.map{|line| wrap_line(ERB::Util.h(line))})
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def wrap_line(line)
|
19
|
+
cleaned = clean_line(line)
|
20
|
+
case line
|
21
|
+
when /^(---|\+\+\+|\\\\)/
|
22
|
+
' <li class="diff-comment"><span>' + line.chomp + '</span></li>'
|
23
|
+
when /^\+/
|
24
|
+
' <li class="ins"><ins>' + cleaned + '</ins></li>'
|
25
|
+
when /^-/
|
26
|
+
' <li class="del"><del>' + cleaned + '</del></li>'
|
27
|
+
when /^ /
|
28
|
+
' <li class="unchanged"><span>' + cleaned + '</span></li>'
|
29
|
+
when /^@@/
|
30
|
+
' <li class="diff-block-info"><span>' + line.chomp + '</span></li>'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# remove +/- or wrap in html
|
35
|
+
def clean_line(line)
|
36
|
+
if @options[:include_plus_and_minus_in_html]
|
37
|
+
line.sub(/^(.)/, '<span class="symbol">\1</span>')
|
38
|
+
else
|
39
|
+
line.sub(/^./, '')
|
40
|
+
end.chomp
|
41
|
+
end
|
42
|
+
|
43
|
+
def wrap_lines(lines)
|
44
|
+
if lines.empty?
|
45
|
+
%'<div class="diff"></div>'
|
46
|
+
else
|
47
|
+
%'<div class="diff">\n <ul>\n#{lines.join("\n")}\n </ul>\n</div>\n'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def highlighted_words
|
52
|
+
chunks = @diff.each_chunk.
|
53
|
+
reject{|c| c == ''"\n"}
|
54
|
+
|
55
|
+
processed = []
|
56
|
+
lines = chunks.each_with_index.map do |chunk1, index|
|
57
|
+
next if processed.include? index
|
58
|
+
processed << index
|
59
|
+
chunk1 = chunk1
|
60
|
+
chunk2 = chunks[index + 1]
|
61
|
+
if not chunk2
|
62
|
+
next ERB::Util.h(chunk1)
|
63
|
+
end
|
64
|
+
|
65
|
+
dir1 = chunk1.each_char.first
|
66
|
+
dir2 = chunk2.each_char.first
|
67
|
+
case [dir1, dir2]
|
68
|
+
when ['-', '+']
|
69
|
+
if chunk1.each_char.take(3).join("") =~ /^(---|\+\+\+|\\\\)/ and
|
70
|
+
chunk2.each_char.take(3).join("") =~ /^(---|\+\+\+|\\\\)/
|
71
|
+
ERB::Util.h(chunk1)
|
72
|
+
else
|
73
|
+
line_diff = Diffy::Diff.new(
|
74
|
+
split_characters(chunk1),
|
75
|
+
split_characters(chunk2),
|
76
|
+
Diffy::Diff::ORIGINAL_DEFAULT_OPTIONS
|
77
|
+
)
|
78
|
+
hi1 = reconstruct_characters(line_diff, '-')
|
79
|
+
hi2 = reconstruct_characters(line_diff, '+')
|
80
|
+
processed << (index + 1)
|
81
|
+
[hi1, hi2]
|
82
|
+
end
|
83
|
+
else
|
84
|
+
ERB::Util.h(chunk1)
|
85
|
+
end
|
86
|
+
end.flatten
|
87
|
+
lines.map{|line| line.each_line.map(&:chomp).to_a if line }.flatten.compact.
|
88
|
+
map{|line|wrap_line(line) }.compact
|
89
|
+
end
|
90
|
+
|
91
|
+
def split_characters(chunk)
|
92
|
+
chunk.gsub(/^./, '').each_line.map do |line|
|
93
|
+
chars = line.sub(/([\r\n]$)/, '').split('')
|
94
|
+
# add escaped newlines
|
95
|
+
chars << '\n'
|
96
|
+
chars.map{|chr| ERB::Util.h(chr) }
|
97
|
+
end.flatten.join("\n") + "\n"
|
98
|
+
end
|
99
|
+
|
100
|
+
def reconstruct_characters(line_diff, type)
|
101
|
+
enum = line_diff.each_chunk.to_a
|
102
|
+
enum.each_with_index.map do |l, i|
|
103
|
+
re = /(^|\\n)#{Regexp.escape(type)}/
|
104
|
+
case l
|
105
|
+
when re
|
106
|
+
highlight(l)
|
107
|
+
when /^ /
|
108
|
+
if i > 1 and enum[i+1] and l.each_line.to_a.size < 4
|
109
|
+
highlight(l)
|
110
|
+
else
|
111
|
+
l.gsub(/^./, '').gsub("\n", '').
|
112
|
+
gsub('\r', "\r").gsub('\n', "\n")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end.join('').split("\n").map do |l|
|
116
|
+
type + l.gsub('</strong><strong>' , '')
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def highlight(lines)
|
121
|
+
"<strong>" +
|
122
|
+
lines.
|
123
|
+
# strip diff tokens (e.g. +,-,etc.)
|
124
|
+
gsub(/(^|\\n)./, '').
|
125
|
+
# mark line boundaries from higher level line diff
|
126
|
+
# html is all escaped so using brackets should make this safe.
|
127
|
+
gsub('\n', '<LINE_BOUNDARY>').
|
128
|
+
# join characters back by stripping out newlines
|
129
|
+
gsub("\n", '').
|
130
|
+
# close and reopen strong tags. we don't want inline elements
|
131
|
+
# spanning block elements which get added later.
|
132
|
+
gsub('<LINE_BOUNDARY>',"</strong>\n<strong>") + "</strong>"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Diffy
|
2
|
+
class SplitDiff
|
3
|
+
def initialize(left, right, options = {})
|
4
|
+
@format = options[:format] || Diffy::Diff.default_format
|
5
|
+
|
6
|
+
formats = Format.instance_methods(false).map { |x| x.to_s }
|
7
|
+
unless formats.include?(@format.to_s)
|
8
|
+
fail ArgumentError, "Format #{format.inspect} is not a valid format"
|
9
|
+
end
|
10
|
+
|
11
|
+
@diff = Diffy::Diff.new(left, right, options).to_s(@format)
|
12
|
+
@left_diff, @right_diff = split
|
13
|
+
end
|
14
|
+
|
15
|
+
%w(left right).each do |direction|
|
16
|
+
define_method direction do
|
17
|
+
instance_variable_get("@#{direction}_diff")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def split
|
24
|
+
[split_left, split_right]
|
25
|
+
end
|
26
|
+
|
27
|
+
def split_left
|
28
|
+
case @format
|
29
|
+
when :color
|
30
|
+
@diff.gsub(/\033\[32m\+(.*)\033\[0m\n/, '')
|
31
|
+
when :html, :html_simple
|
32
|
+
@diff.gsub(%r{\s+<li class="ins"><ins>(.*)</ins></li>}, '')
|
33
|
+
when :text
|
34
|
+
@diff.gsub(/^\+(.*)\n/, '')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def split_right
|
39
|
+
case @format
|
40
|
+
when :color
|
41
|
+
@diff.gsub(/\033\[31m\-(.*)\033\[0m\n/, '')
|
42
|
+
when :html, :html_simple
|
43
|
+
@diff.gsub(%r{\s+<li class="del"><del>(.*)</del></li>}, '')
|
44
|
+
when :text
|
45
|
+
@diff.gsub(/^-(.*)\n/, '')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|