patience_diff 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d37dfbd16df805a3d8b286765fe5ea12c665c156
4
+ data.tar.gz: f3df94dd269cba9149197be6ea584fa3c8f3f103
5
+ SHA512:
6
+ metadata.gz: c82eb37da25cbecb994d98fba1de7e355887a4576c7a50afa688d0e62f76c4887cc6d9f5a014d303fbfc92a1e17e6e999f7989ea17fcba6a06810885d786a212
7
+ data.tar.gz: 3c1fc29891874a7de6fcd510c466ec9c6088bff9ce6f8695f5c44b2a8035b52e708344c266b3f36b0b25cfc56d0534d1b2286da1109f49502e61b745d5c9b2ff
@@ -1,3 +1,8 @@
1
+ === 1.1.0 / 2012-08-12
2
+
3
+ * Introduced HTML output
4
+ * Fixed some major bugs in the diffing algorithm
5
+
1
6
  === 1.0.1 / 2012-06-19
2
7
 
3
8
  * Renamed gem from 'ruby_patience_diff' to just 'patience_diff'
@@ -4,7 +4,15 @@ README.md
4
4
  Rakefile
5
5
  bin/patience_diff
6
6
  lib/patience_diff.rb
7
- lib/patience_diff/card.rb
7
+ lib/patience_diff/differ.rb
8
+ lib/patience_diff/formatter.rb
9
+ lib/patience_diff/formatting_context.rb
8
10
  lib/patience_diff/sequence_matcher.rb
9
- lib/patience_diff/unified_differ.rb
10
11
  lib/patience_diff/usage_error.rb
12
+ lib/patience_diff/html/escaping.rb
13
+ lib/patience_diff/html/formatter.rb
14
+ lib/patience_diff/html/header_helper.rb
15
+ lib/patience_diff/html/hunk_helper.rb
16
+ templates/html.erb
17
+ templates/html_header.erb
18
+ templates/html_hunk.erb
data/Rakefile CHANGED
@@ -6,5 +6,4 @@ Hoe.plugin :rubyforge
6
6
  Hoe.spec 'patience_diff' do
7
7
  developer "Andrew Watt", "andrew@wattornot.com"
8
8
  dependency "trollop", "~> 1.16"
9
- self.rubyforge_name = 'patiencediff'
10
9
  end
@@ -1,38 +1,50 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'English'
3
+ require 'pathname'
3
4
  require 'trollop'
4
5
 
5
- lib_path = File.expand_path(File.join(File.dirname(__FILE__),"..","lib"))
6
+ lib_path = Pathname(File.join(File.dirname(__FILE__),"..","lib")).realpath
6
7
  $LOAD_PATH.unshift(lib_path)
7
8
  require 'patience_diff'
8
9
 
9
10
  begin
11
+ program_name = File.basename($PROGRAM_NAME)
10
12
  opts = Trollop::options do
11
- banner <<-EOF
12
- Usage: #{File.basename($0)} [options] left-file right-file
13
- Options:
14
- EOF
15
- version "patience_diff #{PatienceDiff::VERSION}"
13
+ banner <<-EOF.gsub(/^\s*/, '')
14
+ Usage: #{program_name} [options] left-file right-file (left-file-2 right-file-2 ...)
15
+ Options:
16
+ EOF
17
+
18
+ version "#{program_name} #{PatienceDiff::VERSION}"
19
+
16
20
  opt :debug, "Debugging mode"
17
21
  opt :context, "Lines of context", :default => 3
18
- opt :full_context, "Don't collapse common sections; output entire files"
22
+ opt :all_context, "Don't collapse common sections; output entire files. Ignored in html format."
23
+ opt :format, "Specify output format. Currently 'text' and 'html' are supported.", :default => "text"
19
24
  opt :ignore_whitespace,
20
25
  "Ignore trailing whitespace, and treat leading whitespace as either present or not. This switch is for compatibility with diff's -b option.",
21
26
  :short => '-b'
22
27
  end
23
28
 
24
- raise PatienceDiff::UsageError unless ARGV.length == 2
29
+ raise PatienceDiff::UsageError unless ARGV.length and ARGV.length % 2 == 0
25
30
 
26
- left_file, right_file = *ARGV
31
+ differ = PatienceDiff::Differ.new(opts)
32
+ formatter = case opts.delete(:format)
33
+ when 'text'
34
+ PatienceDiff::Formatter
35
+ when 'html'
36
+ PatienceDiff::Html::Formatter
37
+ else
38
+ raise PatienceDiff::UsageError
39
+ end.new(differ)
27
40
 
28
- left = File.read(left_file, :external_encoding => Encoding::BINARY).split($RS)
29
- left_timestamp = File.mtime(left_file)
30
- right = File.read(right_file, :external_encoding => Encoding::BINARY).split($RS)
31
- right_timestamp = File.mtime(right_file)
32
-
33
- opts[:no_grouping] = opts[:full_context]
34
- formatter = PatienceDiff::UnifiedDiffer.new(opts)
35
- puts formatter.diff(left, right, left_file, right_file, left_timestamp, right_timestamp)
41
+ formatter.format do |diff|
42
+ ARGV.each_slice(2) do |(left_file, right_file)|
43
+ diff.files left_file, right_file
44
+ end
45
+ end.tap do |result|
46
+ print result
47
+ end
36
48
 
37
49
  rescue PatienceDiff::UsageError => e
38
50
  Trollop.module_eval do
@@ -1,8 +1,12 @@
1
- require 'patience_diff/card'
1
+ require 'pathname'
2
+ require 'patience_diff/differ'
3
+ require 'patience_diff/formatter'
4
+ require 'patience_diff/formatting_context'
2
5
  require 'patience_diff/sequence_matcher'
3
- require 'patience_diff/unified_differ'
4
6
  require 'patience_diff/usage_error'
7
+ PatienceDiff.autoload(:Html, 'patience_diff/html/formatter')
5
8
 
6
9
  module PatienceDiff
7
- VERSION = "1.0.1"
10
+ VERSION = "1.1.0"
11
+ TEMPLATE_PATH = Pathname(File.join(File.dirname(__FILE__),'..','templates')).realpath
8
12
  end
@@ -0,0 +1,63 @@
1
+ require 'English'
2
+ require 'patience_diff/formatter'
3
+ require 'patience_diff/sequence_matcher'
4
+
5
+ module PatienceDiff
6
+ class Differ
7
+ attr_reader :matcher
8
+ attr_accessor :all_context, :line_ending, :ignore_whitespace
9
+
10
+ # Options:
11
+ # * :all_context: Output the entirety of each file. This overrides the sequence matcher's context setting.
12
+ # * :line_ending: Delimiter to use when joining diff output. Defaults to $RS.
13
+ # * :ignore_whitespace: Before comparing lines, strip trailing whitespace, and treat leading whitespace
14
+ # as either present or not. Does not affect output.
15
+ # Any additional options (e.g. :context) are passed on to the sequence matcher.
16
+ def initialize(opts = {})
17
+ @all_context = opts.delete(:all_context)
18
+ @line_ending = opts.delete(:line_ending) || $RS
19
+ @ignore_whitespace = opts.delete(:ignore_whitespace)
20
+ @matcher = SequenceMatcher.new(opts)
21
+ end
22
+
23
+ # Generates a unified diff from the contents of the files at the paths specified.
24
+ def diff_files(left_file, right_file, formatter=Formatter.new)
25
+ (left_data, left_timestamp), (right_data, right_timestamp) = [left_file, right_file].map do |filename|
26
+ # Read in binary encoding, so that we can diff any encoding and split() won't complain
27
+ File.open(filename, :external_encoding => Encoding::BINARY) do |file|
28
+ [file.read.split($RS), file.mtime]
29
+ end
30
+ end
31
+ diff_sequences(left_data, right_data, left_file, right_file, left_timestamp, right_timestamp, formatter)
32
+ end
33
+
34
+ # Generate a unified diff of the data specified. The left and right values should be strings, or any other indexable, sortable data.
35
+ # File names and timestamps do not affect the diff algorithm, but are used in the header text.
36
+ def diff_sequences(left, right, left_name=nil, right_name=nil, left_timestamp=nil, right_timestamp=nil, formatter=Formatter.new)
37
+ if @ignore_whitespace
38
+ a = left.map { |line| line.rstrip.gsub(/^\s+/, ' ') }
39
+ b = right.map { |line| line.rstrip.gsub(/^\s+/, ' ') }
40
+ else
41
+ a = left
42
+ b = right
43
+ end
44
+
45
+ if @all_context
46
+ hunks = [@matcher.diff_opcodes(a, b)]
47
+ else
48
+ hunks = @matcher.grouped_opcodes(a, b)
49
+ end
50
+
51
+ return nil unless hunks.any?
52
+
53
+ lines = []
54
+ lines << formatter.render_header(left_name, right_name, left_timestamp, right_timestamp)
55
+ last_hunk_end = -1
56
+ hunks.each do |opcodes|
57
+ lines << formatter.render_hunk(a, b, opcodes, last_hunk_end)
58
+ last_hunk_end = opcodes.last[4]
59
+ end
60
+ lines.flatten.compact.join(@line_ending) + @line_ending
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,66 @@
1
+ require 'patience_diff/formatting_context'
2
+
3
+ module PatienceDiff
4
+ # Formats a plaintext unified diff.
5
+ class Formatter
6
+ attr_reader :names
7
+ attr_accessor :left_name, :right_name, :left_timestamp, :right_timestamp, :title
8
+
9
+ def initialize(differ, title = nil)
10
+ @differ = differ
11
+ @names = []
12
+ @title = title || "Diff generated on #{Time.now.strftime('%c')}"
13
+ end
14
+
15
+ def format
16
+ context = FormattingContext.new(@differ, self)
17
+ yield context
18
+ context.format
19
+ end
20
+
21
+ def render_header(left_name=nil, right_name=nil, left_timestamp=nil, right_timestamp=nil)
22
+ @names << right_name
23
+ @left_name = left_name || "Original"
24
+ @right_name = right_name || "Current"
25
+ @left_timestamp = left_timestamp || Time.now
26
+ @right_timestamp = right_timestamp || Time.now
27
+ [
28
+ left_header_line(@left_name, @left_timestamp),
29
+ right_header_line(@right_name, @right_timestamp)
30
+ ]
31
+ end
32
+
33
+ def render_hunk_marker(opcodes)
34
+ a_start = opcodes.first[1] + 1
35
+ a_end = opcodes.last[2] + 2
36
+ b_start = opcodes.first[3] + 1
37
+ b_end = opcodes.last[4] + 2
38
+
39
+ "@@ -%d,%d +%d,%d @@" % [a_start, a_end-a_start, b_start, b_end-b_start]
40
+ end
41
+
42
+ def render_hunk(a, b, opcodes, last_line_shown)
43
+ lines = [render_hunk_marker(opcodes)]
44
+ lines << opcodes.collect do |(code, a_start, a_end, b_start, b_end)|
45
+ case code
46
+ when :equal
47
+ b[b_start..b_end].map { |line| ' ' + line }
48
+ when :delete
49
+ a[a_start..a_end].map { |line| '-' + line }
50
+ when :insert
51
+ b[b_start..b_end].map { |line| '+' + line }
52
+ end
53
+ end
54
+ lines
55
+ end
56
+
57
+ private
58
+ def left_header_line(name, timestamp)
59
+ "--- %s\t%s" % [name, timestamp.strftime("%Y-%m-%d %H:%m:%S.%N %z")]
60
+ end
61
+
62
+ def right_header_line(name, timestamp)
63
+ "+++ %s\t%s" % [name, timestamp.strftime("%Y-%m-%d %H:%m:%S.%N %z")]
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,36 @@
1
+ require 'stringio'
2
+
3
+ module PatienceDiff
4
+ # Delegate object yielded by the #format method.
5
+ class FormattingContext
6
+ def initialize(differ, formatter)
7
+ @differ = differ
8
+ @formatter = formatter
9
+ @out = StringIO.new
10
+ end
11
+
12
+ def files(left_file, right_file)
13
+ @out.print @differ.diff_files(left_file, right_file, @formatter)
14
+ end
15
+
16
+ def sequences(left, right, left_name=nil, right_name=nil, left_timestamp=nil, right_timestamp=nil)
17
+ @out.print @differ.diff_sequences(left, right, left_name, right_name, left_timestamp, right_timestamp, @formatter)
18
+ end
19
+
20
+ def orphan(sequence, name=nil, timestamp=nil)
21
+ @out.print @formatter.render_orphan(sequence, name, timestamp)
22
+ end
23
+
24
+ def format
25
+ @out.string
26
+ end
27
+
28
+ def title
29
+ @formatter.title
30
+ end
31
+
32
+ def names
33
+ @formatter.names
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ require 'cgi'
2
+
3
+ module PatienceDiff
4
+ module Html
5
+ module Escaping
6
+ # Escapes text for HTML output
7
+ def escape(raw)
8
+ CGI::escape_html(raw.to_s)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,71 @@
1
+ require 'patience_diff/formatter'
2
+ require 'patience_diff/formatting_context'
3
+ require 'patience_diff/html/header_helper'
4
+ require 'patience_diff/html/hunk_helper'
5
+
6
+ require 'erubis'
7
+
8
+ module PatienceDiff
9
+ module Html
10
+
11
+ # Produces a fancy HTML-formatted unified diff. All your friends will be jealous.
12
+ class Formatter < PatienceDiff::Formatter
13
+ def initialize(*args)
14
+ super(*args)
15
+ @erbs = Hash.new do |hash, key|
16
+ hash[key] = Erubis::Eruby.new(File.read(key))
17
+ end
18
+ @hunk_count = 0
19
+ end
20
+
21
+ def format
22
+ context = FormattingContext.new(@differ, self)
23
+ yield context
24
+ template.evaluate(context)
25
+ end
26
+
27
+ def render_header(*args)
28
+ left_header, right_header = *super(*args)
29
+ helper = HeaderHelper.new(left_header, right_header, @names.count - 1)
30
+ template("html_header.erb").evaluate(helper)
31
+ end
32
+
33
+ def render_hunk(a, b, opcodes, last_hunk_end)
34
+ helper = hunk_context(a, b, render_hunk_marker(opcodes), opcodes, last_hunk_end)
35
+ template("html_hunk.erb").evaluate(helper)
36
+ end
37
+
38
+ # Render a single file as if it were a diff with no changes & full context
39
+ def render_orphan(sequence, name, timestamp)
40
+ @names << name
41
+ left_header = "--- New file"
42
+ right_header = right_header_line(name, timestamp)
43
+ helper = HeaderHelper.new(left_header, right_header, @names.count - 1)
44
+ result = template("html_header.erb").evaluate(helper)
45
+
46
+ # create one opcode with the entire content
47
+ opcodes = [
48
+ [:equal, 0, sequence.length-1, 0, sequence.length-1]
49
+ ]
50
+ helper = hunk_context(sequence, sequence, nil, opcodes, 0)
51
+ result << template("html_hunk.erb").evaluate(helper)
52
+ result
53
+ end
54
+
55
+ private
56
+ def template(filename = "html.erb")
57
+ @erbs[template_path(filename)]
58
+ end
59
+
60
+ # override to add/change templates
61
+ def template_path(filename)
62
+ File.join(PatienceDiff::TEMPLATE_PATH, filename)
63
+ end
64
+
65
+ def hunk_context(a, b, hunk_marker, opcodes, last_hunk_end)
66
+ @hunk_count += 1
67
+ HunkHelper.new(a, b, render_hunk_marker(opcodes), opcodes, last_hunk_end, @hunk_count)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,16 @@
1
+ require 'patience_diff/html/escaping'
2
+
3
+ module PatienceDiff
4
+ module Html
5
+ class HeaderHelper
6
+ include Escaping
7
+ attr_accessor :left_header, :right_header, :header_id
8
+
9
+ def initialize(left_header, right_header, header_id)
10
+ @left_header = left_header
11
+ @right_header = right_header
12
+ @header_id = header_id
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,50 @@
1
+ require 'patience_diff/html/escaping'
2
+
3
+ module PatienceDiff
4
+ module Html
5
+ class HunkHelper
6
+ include Escaping
7
+ attr_accessor :a, :b, :hunk_marker, :opcodes, :last_hunk_end, :hunk_id
8
+
9
+ def initialize(a, b, hunk_marker, opcodes, last_hunk_end, hunk_id)
10
+ @a = a
11
+ @b = b
12
+ @hunk_marker = hunk_marker
13
+ @opcodes = opcodes
14
+ @last_hunk_end = last_hunk_end
15
+ @hunk_id = hunk_id
16
+ end
17
+
18
+ def hunk_start
19
+ @opcodes.first[3]
20
+ end
21
+
22
+ def hidden_line_count
23
+ hunk_start - @last_hunk_end - 1
24
+ end
25
+
26
+ def lines
27
+ @b
28
+ end
29
+
30
+ def each_line
31
+ opcodes.each do |(code, a_start, a_end, b_start, b_end)|
32
+ case code
33
+ when :delete
34
+ a[a_start..a_end].each { |line| yield 'delete', '-' + format_line(line) }
35
+ when :equal
36
+ b[b_start..b_end].each { |line| yield 'equal', ' ' + format_line(line) }
37
+ when :insert
38
+ b[b_start..b_end].each { |line| yield 'insert', '+' + format_line(line) }
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+ # override for additional behavior, e.g. syntax highlighting
45
+ def format_line(line)
46
+ escape(line)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,9 +1,12 @@
1
- require 'patience_diff/card'
2
-
3
1
  module PatienceDiff
2
+ # Matches indexed data (generally text) using the Patience diff algorithm.
4
3
  class SequenceMatcher
5
4
  attr_accessor :context
6
5
 
6
+ Card = Struct.new(:index, :value, :previous)
7
+
8
+ # Options:
9
+ # * :context: number of lines of context to use when grouping
7
10
  def initialize(opts = {})
8
11
  @context = opts[:context] || 3
9
12
  end
@@ -146,7 +149,7 @@ module PatienceDiff
146
149
  b_mid -= 1
147
150
  end
148
151
  recursively_match(a, b, a_lo, b_lo, a_mid, b_mid) { |match| yield match }
149
- 0...(a_hi-a_mid).each do |i|
152
+ (0...(a_hi-a_mid)).each do |i|
150
153
  yield [a_mid+i, b_mid+i]
151
154
  end
152
155
  end
@@ -0,0 +1,307 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title><%= title %></title>
6
+ <style>
7
+ body {
8
+ font-family: Consolas, Menlo, Monaco, monospace;
9
+ font-size: 8pt;
10
+ }
11
+ .diff, .marker { white-space: pre; }
12
+ .diff:hover { background-color: #ff9; }
13
+ .delete { background-color: #fdd; }
14
+ .insert { background-color: #dfd; }
15
+ .divider {
16
+ position: relative;
17
+ background-color: #cce5ff; /* hsl(210, 100%, 90%) */
18
+ background-image: -webkit-linear-gradient(top, hsl(210, 100%, 95%) 0%, hsl(210, 100%, 90%) 100%);
19
+ background-image: -moz-linear-gradient(top, hsl(210, 100%, 95%) 0%, hsl(210, 100%, 90%) 100%);
20
+ background-image: -o-linear-gradient(top, hsl(210, 100%, 95%) 0%, hsl(210, 100%, 90%) 100%);
21
+ background-image: linear-gradient(top, hsl(210, 100%, 95%) 0%, hsl(210, 100%, 90%) 100%);
22
+ font-family: Calibri, "Lucida Grande", sans-serif;
23
+ border: solid 1px #b3cce6; /* hsl(210, 50%, 80%) */
24
+ padding: 0 2px;
25
+ border-radius: 3px;
26
+ cursor: default;
27
+ }
28
+ .toggle:hover {
29
+ background-image: -webkit-linear-gradient(top, hsl(210, 100%, 90%) 0%, hsl(210, 100%, 80%) 100%);
30
+ background-image: -moz-linear-gradient(top, hsl(210, 100%, 90%) 0%, hsl(210, 100%, 80%) 100%);
31
+ background-image: -o-linear-gradient(top, hsl(210, 100%, 90%) 0%, hsl(210, 100%, 80%) 100%);
32
+ background-image: linear-gradient(top, hsl(210, 100%, 90%) 0%, hsl(210, 100%, 80%) 100%);
33
+ border: solid 1px #39f; /* hsl(210, 100%, 60%) */
34
+ }
35
+ .marker {
36
+ color: #000080;
37
+ font-family: Consolas, Menlo, Monaco, monospace;
38
+ position: absolute;
39
+ }
40
+ .hint {
41
+ color: #999;
42
+ text-align: center;
43
+ }
44
+ .divider:hover .hint { color: black; }
45
+ .down_arrow, .up_arrow { position: relative; }
46
+ .down_arrow:after, .down_arrow:before { top: 100%; }
47
+ .up_arrow:after, .up_arrow:before { bottom: 100%; }
48
+ .down_arrow:after, .down_arrow:before, .up_arrow:after, .up_arrow:before {
49
+ border: solid transparent;
50
+ content: " ";
51
+ height: 0;
52
+ width: 0;
53
+ position: absolute;
54
+ left: 50%;
55
+ }
56
+ .down_arrow:after, .up_arrow:after { border-width: 5px; margin-left: -5px; }
57
+ .down_arrow:before, .up_arrow:before { border-width: 6px; margin-left: -6px; }
58
+ .down_arrow:after { border-top-color: hsl(210, 100%, 90%); }
59
+ .up_arrow:after { border-bottom-color: hsl(210, 100%, 95%); }
60
+ .down_arrow:before { border-top-color: hsl(210, 50%, 80%); }
61
+ .up_arrow:before { border-bottom-color: hsl(210, 50%, 80%); }
62
+ .toggle:hover .down_arrow:after { border-top-color: hsl(210, 100%, 80%); }
63
+ .toggle:hover .down_arrow:before { border-top-color: hsl(210, 100%, 60%); }
64
+ .toggle:hover .up_arrow:after { border-bottom-color: hsl(210, 100%, 90%); }
65
+ .toggle:hover .up_arrow:before { border-bottom-color: hsl(210, 100%, 60%); }
66
+ .left_header { page-break-before: always; }
67
+ .left_header, .right_header { background-color: #ddd; color: #222; }
68
+ #content .collapsed {
69
+ -webkit-transition: bottom .5s, height .5s;
70
+ -moz-transition: bottom .5s, height .5s;
71
+ -o-transition: bottom .5s, height .5s;
72
+ transition: bottom .5s, height .5s;
73
+ height: 0;
74
+ overflow: hidden
75
+ }
76
+ #nav_container {
77
+ position: fixed;
78
+ top: 0;
79
+ right: 0;
80
+ border-radius: 4px 0 0 4px / 50% 0 0 50%;
81
+ box-shadow: 0 0 8px 0 rgba(0,0,0,.5);
82
+ -webkit-transition: opacity 1s, bottom .5s, height .5s;
83
+ -moz-transition: opacity 1s, bottom .5s, height .5s;
84
+ -o-transition: opacity 1s, bottom .5s, height .5s;
85
+ transition: opacity 1s, bottom .5s, height .5s;
86
+ }
87
+ #nav {
88
+ position: absolute;
89
+ top: 0;
90
+ bottom: 0;
91
+ left: 0;
92
+ right: 0;
93
+ border-left: solid 1px #999;
94
+ overflow: hidden;
95
+ background: #eee;
96
+ background-image: -webkit-linear-gradient(left, #e7e7e7 0%, #eee 25%, #eee 75%, #e7e7e7 100%);
97
+ background-image: -moz-linear-gradient(left, #e7e7e7 0%, #eee 25%, #eee 75%, #e7e7e7 100%);
98
+ background-image: -o-linear-gradient(left, #e7e7e7 0%, #eee 25%, #eee 75%, #e7e7e7 100%);
99
+ background-image: linear-gradient(left, #e7e7e7 0%, #eee 25%, #eee 75%, #e7e7e7 100%);
100
+ font-family: Calibri, "Lucida Grande", sans-serif;
101
+ font-size: 10pt;
102
+ }
103
+ #nav_scroll {
104
+ position: absolute;
105
+ top: 1.75em;
106
+ bottom: 0;
107
+ right: 0;
108
+ overflow-x: hidden;
109
+ overflow-y: auto;
110
+ }
111
+ #nav button {
112
+ height: 1.75em;
113
+ margin: 0;
114
+ width: 100%;
115
+ background: #ddd;
116
+ background-image: -webkit-linear-gradient(top, #eee 0%, #ccc 100%);
117
+ background-image: -moz-linear-gradient(top, #eee 0%, #ccc 100%);
118
+ background-image: -o-linear-gradient(top, #eee 0%, #ccc 100%);
119
+ background-image: linear-gradient(top, #eee 0%, #ccc 100%);
120
+ border: none;
121
+ border-top: solid white 1px;
122
+ border-bottom: solid #999 1px;
123
+ font-family: Calibri, "Lucida Grande", sans-serif;
124
+ font-size: 10pt;
125
+ text-shadow: 0 1px 0 white;
126
+ }
127
+ #nav button:hover {
128
+ background: #eee;
129
+ background-image: -webkit-linear-gradient(top, #fff 0%, #ddd 100%);
130
+ background-image: -moz-linear-gradient(top, #fff 0%, #ddd 100%);
131
+ background-image: -o-linear-gradient(top, #fff 0%, #ddd 100%);
132
+ background-image: linear-gradient(top, #fff 0%, #ddd 100%);
133
+ }
134
+ #nav button:active {
135
+ border-top: solid black 1px;
136
+ background: #333;
137
+ background-image: -webkit-linear-gradient(top, #333 0%, #666 100%);
138
+ background-image: -moz-linear-gradient(top, #333 0%, #666 100%);
139
+ background-image: -o-linear-gradient(top, #333 0%, #666 100%);
140
+ background-image: linear-gradient(top, #333 0%, #666 100%);
141
+ color: white;
142
+ text-shadow: 0 1px 0 black;
143
+ }
144
+ #nav_container.collapsed { opacity: 0.33; }
145
+ #nav_container.collapsed:hover {
146
+ opacity: 1;
147
+ -webkit-transition: opacity 0s, bottom .5s, height .5s;
148
+ -moz-transition: opacity 0s, bottom .5s, height .5s;
149
+ -o-transition: opacity 0s, bottom .5s, height .5s;
150
+ transition: opacity 0s, bottom .5s, height .5s;
151
+ }
152
+ #nav ol { margin: 0.5em; padding: 0 15px 0 30px; }
153
+ #nav a { color: black; text-decoration: none; display: block; width: 100%; }
154
+ #nav a:hover { color: gray; }
155
+ #nav_container.no_animation {
156
+ -webkit-transition: none;
157
+ -moz-transition: none;
158
+ -o-transition: all 0 none;
159
+ transition: none;
160
+ }
161
+ </style>
162
+ <script>
163
+ function $(id) {
164
+ return document.getElementById(id);
165
+ }
166
+ function show(id) {
167
+ $(id).style.display = "";
168
+ }
169
+ function hide(id) {
170
+ $(id).style.display = "none";
171
+ }
172
+ function hasClass(el, classname) {
173
+ var classes = el.className.split(/\s+/);
174
+ for (var i = 0, len = classes.length; i < len; i++) {
175
+ if (classes[i] == classname)
176
+ return true;
177
+ }
178
+ return false;
179
+ }
180
+ function init() {
181
+ // Set nav container width to the natural width of its absolutely positioned content
182
+ var navWidth = $("nav_scroll").offsetWidth;
183
+ var container = $("nav_container");
184
+ container.style.width = navWidth + "px";
185
+ if (container.addEventListener) {
186
+ container.addEventListener("webkitTransitionEnd", page.navTransitioned);
187
+ container.addEventListener("transitionend", page.navTransitioned);
188
+ container.addEventListener("oTransitionEnd", page.navTransitioned);
189
+ }
190
+ $("nav_hide").onclick = page.hideNav;
191
+ $("nav_show").onclick = page.showNav;
192
+
193
+ // set up nav links
194
+ function createJump(id) {
195
+ return function() {
196
+ page.jump(id);
197
+ return false;
198
+ }
199
+ }
200
+ var jumps = $("nav_list").getElementsByTagName("a");
201
+ for (var i = 0, len = jumps.length; i < len; i++) {
202
+ jumps[i].onclick = createJump("header_" + i);
203
+ }
204
+
205
+ // set up toggling of collapsed lines
206
+ function createToggle(hunkStart) {
207
+ return function() {
208
+ page.toggleCollapse(hunkStart);
209
+ return false;
210
+ }
211
+ }
212
+ var divs = $("content").getElementsByTagName("div");
213
+ for (var i = 0, len = divs.length; i < len; i++) {
214
+ var div = divs[i];
215
+ if (hasClass(div, "toggle")) {
216
+ var hunkStart = parseInt(div.id.replace(/^[^\d]*/,""));
217
+ if (hunkStart) {
218
+ div.onclick = createToggle(hunkStart);
219
+ }
220
+ }
221
+ }
222
+ }
223
+ function windowHeight() {
224
+ return window.innerHeight || document.documentElement.clientHeight;
225
+ }
226
+ page = {
227
+ hideNav: function() {
228
+ hide("nav_hide");
229
+ show("nav_show");
230
+ var container = $("nav_container");
231
+ var buttonHeight = $("nav_buttons").offsetHeight + "px";
232
+ container.className = "no_animation";
233
+ // set a fixed height, and clear the bottom
234
+ container.style.height = windowHeight() + "px";
235
+ container.style.bottom = "auto";
236
+ // this is a hack to make sure the height is recalculated so that the transition animation triggers
237
+ setTimeout(function() {
238
+ container.className = "";
239
+ container.style.height = buttonHeight;
240
+ }, 10);
241
+ },
242
+ showNav: function() {
243
+ hide("nav_show");
244
+ show("nav_hide");
245
+ var container = $("nav_container");
246
+ container.className = "no_animation";
247
+ // set a fixed bottom, and clear the height
248
+ container.style.bottom = (windowHeight() - $("nav_buttons").offsetHeight) + "px";
249
+ container.style.height = "auto";
250
+ setTimeout(function() {
251
+ container.className = "";
252
+ container.style.bottom = "0";
253
+ }, 10);
254
+ },
255
+ navTransitioned: function() {
256
+ var container = $("nav_container");
257
+ if (!container.classList)
258
+ return;
259
+ if (container.style.bottom === "auto") {
260
+ container.classList.add("collapsed");
261
+ } else {
262
+ container.classList.remove("collapsed");
263
+ }
264
+ },
265
+ jump: function(id) {
266
+ scrollTo(0,$(id).offsetTop);
267
+ },
268
+ toggleCollapse: function(line) {
269
+ var growDiv = $("hunk_" + line);
270
+ if (growDiv.offsetHeight) {
271
+ growDiv.style.height = 0;
272
+ hide("collapse_" + line);
273
+ show("expand_" + line);
274
+ } else {
275
+ var wrapper = growDiv.getElementsByTagName("div")[0];
276
+ growDiv.style.height = wrapper.offsetHeight + "px";
277
+ hide("expand_" + line);
278
+ show("collapse_" + line);
279
+ }
280
+ }
281
+ }
282
+ </script>
283
+ <!--[if lte IE 7]>
284
+ <style>
285
+ .unicode { font-family: "Lucida Sans Unicode"; }
286
+ </style>
287
+ <![endif]-->
288
+ </head>
289
+ <body onload="init();">
290
+ <div id="content"><%= format %></div>
291
+ <% if names.count > 1 %>
292
+ <div id="nav_container" style="bottom: 0; height: 100%"><div id="nav">
293
+ <div id="nav_buttons">
294
+ <button id="nav_hide"><span class="unicode">&#x25be;</span> Navigation</button>
295
+ <button id="nav_show" style="display: none"><span class="unicode">&#x25b8;</span> Navigation</button>
296
+ </div>
297
+ <div id="nav_scroll">
298
+ <ol id="nav_list">
299
+ <% names.each_with_index do |name, i| %>
300
+ <li><a href="#header_<%= i %>"><%= name %></a></li>
301
+ <% end %>
302
+ </ol>
303
+ </div>
304
+ </div></div>
305
+ <% end %>
306
+ </body>
307
+ </html>
@@ -0,0 +1,4 @@
1
+ <div id="header_<%= header_id %>">
2
+ <div class="left_header diff"><%= escape(left_header) %></div>
3
+ <div class="right_header diff"><%= escape(right_header) %></div>
4
+ </div>
@@ -0,0 +1,30 @@
1
+ <% if hidden_line_count > 0 %>
2
+ <div class="collapsed" id="hunk_<%= hunk_id %>">
3
+ <div>
4
+ <div class="divider toggle" id="toggle_collapse_<%= hunk_id %>">
5
+ <div class="hint down_arrow">Click to collapse <%= hidden_line_count %> lines</div>
6
+ </div>
7
+ <% lines[(last_hunk_end+1)...hunk_start].each do |line| %>
8
+ <div class="equal diff"> <%= format_line(line) %></div>
9
+ <% end %>
10
+ </div>
11
+ </div>
12
+ <div class="divider toggle" id="toggle_expand_<%= hunk_id %>">
13
+ <span class="marker"><%= escape(hunk_marker) %></span>
14
+ <div class="hint down_arrow" id="expand_<%= hunk_id %>">
15
+ Click to expand <%= hidden_line_count %> lines
16
+ </div>
17
+ <div class="hint up_arrow" id="collapse_<%= hunk_id %>" style="display: none">
18
+ Click to collapse <%= hidden_line_count %> lines
19
+ </div>
20
+ </div>
21
+ <% elsif hunk_marker.to_s == "" %>
22
+ <div class="divider">
23
+ <span class="marker"><%= escape(hunk_marker) %></span>
24
+ &nbsp;
25
+ </div>
26
+ <% end %>
27
+
28
+ <% each_line do |code, formatted_line| %>
29
+ <div class="<%= code %> diff"><%= formatted_line %></div>
30
+ <% end %>
metadata CHANGED
@@ -1,96 +1,66 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: patience_diff
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
5
- prerelease:
4
+ version: 1.1.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Andrew Watt
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2012-06-21 00:00:00.000000000 Z
11
+ date: 2014-10-23 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: trollop
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ~>
17
+ - - "~>"
20
18
  - !ruby/object:Gem::Version
21
19
  version: '1.16'
22
20
  type: :runtime
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
- - - ~>
24
+ - - "~>"
28
25
  - !ruby/object:Gem::Version
29
26
  version: '1.16'
30
- - !ruby/object:Gem::Dependency
31
- name: rubyforge
32
- requirement: !ruby/object:Gem::Requirement
33
- none: false
34
- requirements:
35
- - - ! '>='
36
- - !ruby/object:Gem::Version
37
- version: 2.0.4
38
- type: :development
39
- prerelease: false
40
- version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
43
- - - ! '>='
44
- - !ruby/object:Gem::Version
45
- version: 2.0.4
46
27
  - !ruby/object:Gem::Dependency
47
28
  name: rdoc
48
29
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
30
  requirements:
51
- - - ~>
31
+ - - "~>"
52
32
  - !ruby/object:Gem::Version
53
- version: '3.10'
33
+ version: '4.0'
54
34
  type: :development
55
35
  prerelease: false
56
36
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
37
  requirements:
59
- - - ~>
38
+ - - "~>"
60
39
  - !ruby/object:Gem::Version
61
- version: '3.10'
40
+ version: '4.0'
62
41
  - !ruby/object:Gem::Dependency
63
42
  name: hoe
64
43
  requirement: !ruby/object:Gem::Requirement
65
- none: false
66
44
  requirements:
67
- - - ~>
45
+ - - "~>"
68
46
  - !ruby/object:Gem::Version
69
- version: '3.0'
47
+ version: '3.13'
70
48
  type: :development
71
49
  prerelease: false
72
50
  version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
51
  requirements:
75
- - - ~>
52
+ - - "~>"
76
53
  - !ruby/object:Gem::Version
77
- version: '3.0'
78
- description: ! 'A Ruby implementation of the Patience diff algorithm.
79
-
80
-
81
- Patience Diff creates more readable diffs than other algorithms in some cases, particularly
82
- when much of the content has changed between the documents being compared. There''s
83
- a great explanation and example [here][example].
54
+ version: '3.13'
55
+ description: |-
56
+ A Ruby implementation of the Patience diff algorithm.
84
57
 
58
+ Patience Diff creates more readable diffs than other algorithms in some cases, particularly when much of the content has changed between the documents being compared. There's a great explanation and example [here][example].
85
59
 
86
- Patience diff was originally written by Bram Cohen and is used in the [Bazaar][bazaar]
87
- version control system. This version is loosely based off the Python implementation
88
- in Bazaar.
89
-
60
+ Patience diff was originally written by Bram Cohen and is used in the [Bazaar][bazaar] version control system. This version is loosely based off the Python implementation in Bazaar.
90
61
 
91
62
  [example]: http://alfedenzo.livejournal.com/170301.html
92
-
93
- [bazaar]: http://bazaar.canonical.com/'
63
+ [bazaar]: http://bazaar.canonical.com/
94
64
  email:
95
65
  - andrew@wattornot.com
96
66
  executables:
@@ -99,6 +69,7 @@ extensions: []
99
69
  extra_rdoc_files:
100
70
  - History.txt
101
71
  - Manifest.txt
72
+ - README.md
102
73
  files:
103
74
  - History.txt
104
75
  - Manifest.txt
@@ -106,34 +77,42 @@ files:
106
77
  - Rakefile
107
78
  - bin/patience_diff
108
79
  - lib/patience_diff.rb
109
- - lib/patience_diff/card.rb
80
+ - lib/patience_diff/differ.rb
81
+ - lib/patience_diff/formatter.rb
82
+ - lib/patience_diff/formatting_context.rb
83
+ - lib/patience_diff/html/escaping.rb
84
+ - lib/patience_diff/html/formatter.rb
85
+ - lib/patience_diff/html/header_helper.rb
86
+ - lib/patience_diff/html/hunk_helper.rb
110
87
  - lib/patience_diff/sequence_matcher.rb
111
- - lib/patience_diff/unified_differ.rb
112
88
  - lib/patience_diff/usage_error.rb
89
+ - templates/html.erb
90
+ - templates/html_header.erb
91
+ - templates/html_hunk.erb
113
92
  homepage: http://github.com/watt/ruby_patience_diff
114
- licenses: []
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
115
96
  post_install_message:
116
97
  rdoc_options:
117
- - --main
98
+ - "--main"
118
99
  - README.md
119
100
  require_paths:
120
101
  - lib
121
102
  required_ruby_version: !ruby/object:Gem::Requirement
122
- none: false
123
103
  requirements:
124
- - - ! '>='
104
+ - - ">="
125
105
  - !ruby/object:Gem::Version
126
106
  version: '0'
127
107
  required_rubygems_version: !ruby/object:Gem::Requirement
128
- none: false
129
108
  requirements:
130
- - - ! '>='
109
+ - - ">="
131
110
  - !ruby/object:Gem::Version
132
111
  version: '0'
133
112
  requirements: []
134
- rubyforge_project: patiencediff
135
- rubygems_version: 1.8.24
113
+ rubyforge_project:
114
+ rubygems_version: 2.2.2
136
115
  signing_key:
137
- specification_version: 3
116
+ specification_version: 4
138
117
  summary: A Ruby implementation of the Patience diff algorithm
139
118
  test_files: []
@@ -1,9 +0,0 @@
1
- module PatienceDiff
2
- class Card
3
- attr_accessor :previous, :index, :value
4
- def initialize(index, value)
5
- @index = index
6
- @value = value
7
- end
8
- end
9
- end
@@ -1,66 +0,0 @@
1
- require 'English'
2
-
3
- module PatienceDiff
4
- class UnifiedDiffer
5
- attr_reader :matcher
6
- attr_accessor :no_grouping, :line_ending, :ignore_whitespace
7
-
8
- def initialize(opts = {})
9
- @no_grouping = opts.delete(:no_grouping)
10
- @line_ending = opts.delete(:line_ending) || $RS
11
- @ignore_whitespace = opts.delete(:ignore_whitespace)
12
- @matcher = SequenceMatcher.new(opts)
13
- end
14
-
15
- def diff(left, right, left_name=nil, right_name=nil, left_timestamp=nil, right_timestamp=nil)
16
- left_name ||= "Original"
17
- right_name ||= "Current"
18
- left_timestamp ||= right_timestamp || Time.now
19
- right_timestamp ||= left_timestamp || Time.now
20
-
21
- if @ignore_whitespace
22
- puts "ignoring whitespace"
23
- a = left.map { |line| line.rstrip.gsub(/^\s+/, ' ') }
24
- b = right.map { |line| line.rstrip.gsub(/^\s+/, ' ') }
25
- else
26
- a = left
27
- b = right
28
- end
29
-
30
- if @no_grouping
31
- groups = [@matcher.diff_opcodes(a, b)]
32
- else
33
- groups = @matcher.grouped_opcodes(a, b)
34
- end
35
- [
36
- "--- %s\t%s" % [left_name, left_timestamp.strftime("%Y-%m-%d %H:%m:%S.%N %z")],
37
- "+++ %s\t%s" % [right_name, right_timestamp.strftime("%Y-%m-%d %H:%m:%S.%N %z")],
38
- groups.collect { |group| unified_diff_group(left, right, group) }.flatten.compact
39
- ].join(@line_ending)
40
- end
41
-
42
- private
43
- def unified_diff_group(a, b, opcodes)
44
- return nil if opcodes.empty?
45
-
46
- a_start = opcodes.first[1] + 1
47
- a_end = opcodes.last[2] + 2
48
- b_start = opcodes.first[3] + 1
49
- b_end = opcodes.last[4] + 2
50
-
51
- lines = ["@@ -%d,%d +%d,%d @@" % [a_start, a_end-a_start, b_start, b_end-b_start]]
52
-
53
- lines << opcodes.collect do |(code, a_start, a_end, b_start, b_end)|
54
- case code
55
- when :equal
56
- b[b_start..b_end].map { |line| ' ' + line }
57
- when :delete
58
- a[a_start..a_end].map { |line| '-' + line }
59
- when :insert
60
- b[b_start..b_end].map { |line| '+' + line }
61
- end
62
- end
63
- lines
64
- end
65
- end
66
- end