ydiffy 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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')
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Diffy
2
+ VERSION = '0.0.2'
3
+ end