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.
@@ -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