patience_diff 1.0.1 → 1.1.0

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,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