diff-display 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,590 @@
1
+ --- unified.rb (revision 620)
2
+ +++ unified.rb (revision 644)
3
+ @@ -1,298 +1,390 @@
4
+ module Diff
5
+ module Display
6
+ module Unified
7
+ -
8
+ - LINE_RE = /@@ [+-]([0-9]+),([0-9]+) [+-]([0-9]+),([0-9]+) @@/
9
+ - TABWIDTH = 4
10
+ - SPACE = ' ' #' '
11
+ - # By defaul don't wrap inline diffs in anything
12
+ - INLINE_REM_OPEN = "\e[4;33m"
13
+ - INLINE_REM_CLOSE = "\e[m"
14
+ - INLINE_ADD_OPEN = "\e[4;35m"
15
+ - INLINE_ADD_CLOSE = "\e[m"
16
+ - ESCAPE_HTML = false
17
+ -
18
+ class Line < String
19
+ - attr_reader :add_lineno, :rem_lineno
20
+ - def initialize(line, type, add_lineno, rem_lineno = add_lineno)
21
+ + def initialize(line, line_number)
22
+ super(line)
23
+ - @type = type
24
+ - @add_lineno = add_lineno
25
+ - @rem_lineno = rem_lineno
26
+ + @line_number = line_number
27
+ + self
28
+ end
29
+
30
+ + def contains_inline_change?
31
+ + @inline
32
+ + end
33
+ +
34
+ def number
35
+ - add_lineno ? add_lineno : rem_lineno
36
+ + @line_number
37
+ end
38
+
39
+ - def type
40
+ - @type
41
+ + def decorate(&block)
42
+ + yield self
43
+ end
44
+
45
+ - class << self
46
+ - def add(line, add_lineno)
47
+ - AddLine.new(line, add_lineno)
48
+ + def inline_add_open; '' end
49
+ + def inline_add_close; '' end
50
+ + def inline_rem_open; '' end
51
+ + def inline_rem_close; '' end
52
+ +
53
+ + protected
54
+ +
55
+ + def escape
56
+ + self
57
+ end
58
+
59
+ - def rem(line, rem_lineno)
60
+ - RemLine.new(line, rem_lineno)
61
+ + def expand
62
+ + escape.gsub("\t", ' ' * tabwidth).gsub(/ ( +)|^ /) do |match|
63
+ + (space + ' ') * (match.size / 2) +
64
+ + space * (match.size % 2)
65
+ + end
66
+ end
67
+
68
+ - def unmod(line, lineno)
69
+ - UnModLine.new(line, lineno)
70
+ + def tabwidth
71
+ + 4
72
+ end
73
+
74
+ - def mod(line, lineno)
75
+ - ModLine.new(line, lineno)
76
+ +
77
+ + def space
78
+ + ' '
79
+ end
80
+ +
81
+ + class << self
82
+ + def add(line, line_number, inline = false)
83
+ + AddLine.new(line, line_number, inline)
84
+ + end
85
+ +
86
+ + def rem(line, line_number, inline = false)
87
+ + RemLine.new(line, line_number, inline)
88
+ + end
89
+ +
90
+ + def unmod(line, line_number)
91
+ + UnModLine.new(line, line_number)
92
+ + end
93
+ end
94
+ end
95
+
96
+ class AddLine < Line
97
+ - def initialize(line, add_lineno)
98
+ - super(line, 'add', add_lineno, nil)
99
+ + def initialize(line, line_number, inline = false)
100
+ + line = inline ? line % [inline_add_open, inline_add_close] : line
101
+ + super(line, line_number)
102
+ + @inline = inline
103
+ + self
104
+ end
105
+ end
106
+
107
+ class RemLine < Line
108
+ - def initialize(line, rem_lineno)
109
+ - super(line, 'rem', nil, rem_lineno)
110
+ + def initialize(line, line_number, inline = false)
111
+ + line = inline ? line % [inline_rem_open, inline_rem_close] : line
112
+ + super(line, line_number)
113
+ + @inline = inline
114
+ + self
115
+ end
116
+ end
117
+
118
+ class UnModLine < Line
119
+ - def initialize(line, lineno)
120
+ - super(line, 'unmod', lineno)
121
+ + def initialize(line, line_number)
122
+ + super(line, line_number)
123
+ end
124
+ end
125
+
126
+ - class ModLine < Line
127
+ - def initialize(line, lineno)
128
+ - super(line, 'mod', lineno)
129
+ + class SepLine < Line
130
+ + def initialize(line = '...')
131
+ + super(line, nil)
132
+ end
133
+ end
134
+
135
+ + # This class is an array which contains Line objects. Just like Line
136
+ + # classes, several Block classes inherit from Block. If all the lines
137
+ + # in the block are added lines then it is an AddBlock. If all lines
138
+ + # in the block are removed lines then it is a RemBlock. If the lines
139
+ + # in the block are all unmodified then it is an UnMod block. If the
140
+ + # lines in the block are a mixture of added and removed lines then
141
+ + # it is a ModBlock. There are no blocks that contain a mixture of
142
+ + # modified and unmodified lines.
143
+ class Block < Array
144
+ - def initialize(type)
145
+ - super(0)
146
+ - @type = type
147
+ + def initialize
148
+ + super
149
+ + @line_types = []
150
+ end
151
+
152
+ def <<(line_object)
153
+ super(line_object)
154
+ - (@line_types ||= []).push(line_object.type)
155
+ - @line_types.uniq!
156
+ + line_class = line_object.class.name[/\w+$/]
157
+ + @line_types.push(line_class) unless @line_types.include?(line_class)
158
+ self
159
+ end
160
+
161
+ + def decorate(&block)
162
+ + yield self
163
+ + end
164
+ +
165
+ def line_types
166
+ @line_types
167
+ end
168
+
169
+ - def type
170
+ - @type
171
+ + class << self
172
+ + def add; AddBlock.new end
173
+ + def rem; RemBlock.new end
174
+ + def mod; ModBlock.new end
175
+ + def unmod; UnModBlock.new end
176
+ end
177
+ end
178
+
179
+ - class Generator < Array
180
+ + class AddBlock < Block; end
181
+ + class RemBlock < Block; end
182
+ + class ModBlock < Block; end
183
+ + class UnModBlock < Block; end
184
+ + class SepBlock < Block; end
185
+
186
+ + # This data object contains the generated diff data structure. It is an
187
+ + # array of Block objects which are themselves arrays of Line objects. The
188
+ + # Generator class returns a Data instance object after it is done
189
+ + # processing the diff.
190
+ + class Data < Array
191
+ + def initialize
192
+ + super
193
+ + end
194
+ +
195
+ + def debug
196
+ + demodularize = Proc.new {|obj| obj.class.name[/\w+$/]}
197
+ + each do |diff_block|
198
+ + print "*" * 40, ' ', demodularize.call(diff_block)
199
+ + puts
200
+ + puts diff_block.map {|line|
201
+ + "%5d" % line.number +
202
+ + " [#{demodularize.call(line)}]" +
203
+ + line
204
+ + }.join("\n")
205
+ + puts "*" * 40, ' '
206
+ + end
207
+ + end
208
+ +
209
+ + end
210
+ +
211
+ + # Processes the diff and generates a Data object which contains the
212
+ + # resulting data structure.
213
+ + class Generator
214
+ +
215
+ + # Extracts the line number info for a given diff section
216
+ + LINE_NUM_RE = /@@ [+-]([0-9]+),([0-9]+) [+-]([0-9]+),([0-9]+) @@/
217
+ + LINE_TYPES = {'+' => :add, '-' => :rem, ' ' => :unmod}
218
+ +
219
+ class << self
220
+ - def run(udiff, options = {})
221
+ - generator = new(options)
222
+ - udiff.split("\n").each {|line| generator.build(line) }
223
+ - generator.close
224
+ - generator
225
+ +
226
+ + # Runs the generator on a diff and returns a Data object without
227
+ + # instantiating a Generator object
228
+ + def run(udiff)
229
+ + raise ArgumentError, "Object must be enumerable" unless udiff.respond_to?(:each)
230
+ + generator = new
231
+ + udiff.each {|line| generator.process(line.chomp)}
232
+ + generator.render
233
+ end
234
+ end
235
+
236
+ - def initialize(options = {})
237
+ - super(0)
238
+ - default_options = {:inline_add_open => INLINE_ADD_OPEN,
239
+ - :inline_add_close => INLINE_ADD_CLOSE,
240
+ - :inline_rem_open => INLINE_REM_OPEN,
241
+ - :inline_rem_close => INLINE_REM_CLOSE,
242
+ - :escape_html => ESCAPE_HTML,
243
+ - :tabwidth => TABWIDTH,
244
+ - :space => SPACE}
245
+ -
246
+ - @options = default_options.merge(options)
247
+ - @block = []
248
+ - @ttype = nil
249
+ - @p_block = []
250
+ - @p_type = nil
251
+ - @changeno = -1
252
+ - @blockno = 0
253
+ + def initialize
254
+ + @buffer = []
255
+ + @prev_buffer = []
256
+ + @line_type = nil
257
+ + @prev_line_type = nil
258
+ @offset_base = 0
259
+ @offset_changed = 0
260
+ + @data = Diff::Display::Unified::Data.new
261
+ + self
262
+ end
263
+
264
+ - def current_block
265
+ - last
266
+ + # Operates on a single line from the diff and passes along the
267
+ + # collected data to the appropriate method for further processing. The
268
+ + # cycle of processing is in general:
269
+ + #
270
+ + # process --> identify_block --> process_block --> process_line
271
+ + #
272
+ + def process(line)
273
+ + return if ['++', '--'].include?(line[0,2])
274
+ +
275
+ + if match = LINE_NUM_RE.match(line)
276
+ + identify_block
277
+ + push SepBlock.new and current_block << SepLine.new unless @offset_changed.zero?
278
+ + @line_type = nil
279
+ + @offset_base = match[1].to_i - 1
280
+ + @offset_changed = match[3].to_i - 1
281
+ + return
282
+ + end
283
+ +
284
+ + new_line_type, line = LINE_TYPES[car(line)], cdr(line)
285
+ +
286
+ + # Add line to the buffer if it's the same diff line type
287
+ + # as the previous line
288
+ + #
289
+ + # e.g.
290
+ + #
291
+ + # + This is a new line
292
+ + # + As is this one
293
+ + # + And yet another one...
294
+ + #
295
+ + if new_line_type.eql?(@line_type)
296
+ + @buffer.push(line)
297
+ + else
298
+ + # Side by side inline diff
299
+ + #
300
+ + # e.g.
301
+ + #
302
+ + # - This line just had to go
303
+ + # + This line is on the way in
304
+ + #
305
+ + if new_line_type.eql?(LINE_TYPES['+']) and @line_type.eql?(LINE_TYPES['-'])
306
+ + @prev_buffer = @buffer
307
+ + @prev_line_type = @line_type
308
+ + else
309
+ + identify_block
310
+ + end
311
+ + @buffer = [line]
312
+ + @line_type = new_line_type
313
+ + end
314
+ end
315
+
316
+ + # Finishes up with the generation and returns the Data object (could
317
+ + # probably use a better name...maybe just #data?)
318
+ def render
319
+ close
320
+ - self
321
+ + @data
322
+ end
323
+ -
324
+ - def escape(text)
325
+ - return '' unless text
326
+ - return text unless @options[:escape_html]
327
+ - text.gsub('&', '&amp;').
328
+ - gsub('<', '&lt;' ).
329
+ - gsub('>', '&gt;' ).
330
+ - gsub('"', '&#34;')
331
+ - end
332
+
333
+ - def expand(text)
334
+ - escape(text).gsub(/ ( +)|^ /) do |match|
335
+ - (@options[:space] + ' ') * (match.size / 2) +
336
+ - @options[:space] * (match.size % 2)
337
+ - end
338
+ - end
339
+ + protected
340
+
341
+ - def inline_diff(line, start, ending, change)
342
+ - expand(line[0, start]) +
343
+ - change +
344
+ - expand(line[ending, ending.abs])
345
+ - end
346
+ + def identify_block
347
+ + if @prev_line_type.eql?(LINE_TYPES['-']) and @line_type.eql?(LINE_TYPES['+'])
348
+ + process_block(:mod, {:old => @prev_buffer, :new => @buffer})
349
+ + else
350
+ + if LINE_TYPES.values.include?(@line_type)
351
+ + process_block(@line_type, {:new => @buffer})
352
+ + end
353
+ + end
354
+
355
+ - def write_line(oldline, newline)
356
+ - start, ending = get_change_extent(oldline, newline)
357
+ - change = ''
358
+ - if oldline.size > start - ending
359
+ - change = @options[:inline_rem_open] +
360
+ - expand(oldline[start...ending]) +
361
+ - @options[:inline_rem_close]
362
+ + @prev_line_type = nil
363
+ end
364
+
365
+ - line = inline_diff(oldline, start, ending, change)
366
+ - current_block << Line.rem(line, @offset_base)
367
+ + def process_block(diff_line_type, blocks = {:old => nil, :new => nil})
368
+ + push Block.send(diff_line_type)
369
+ + old, new = blocks[:old], blocks[:new]
370
+
371
+ - change = ''
372
+ - if newline.size > start - ending
373
+ - change = @options[:inline_add_open] +
374
+ - expand(newline[start...ending]) +
375
+ - @options[:inline_add_close]
376
+ + # Mod block
377
+ + if diff_line_type.eql?(:mod) and old.size & new.size == 1
378
+ + process_line(old.first, new.first)
379
+ + return
380
+ + end
381
+ +
382
+ + if old and not old.empty?
383
+ + old.each do |line|
384
+ + @offset_base += 1
385
+ + current_block << Line.send(@prev_line_type, line, @offset_base)
386
+ + end
387
+ + end
388
+ +
389
+ + if new and not new.empty?
390
+ + new.each do |line|
391
+ + @offset_changed += 1
392
+ + current_block << Line.send(@line_type, line, @offset_changed)
393
+ + end
394
+ + end
395
+ end
396
+
397
+ - line = inline_diff(newline, start, ending, change)
398
+ - current_block << Line.add(line, @offset_changed)
399
+ - end
400
+ + # TODO Needs a better name...it does process a line (two in fact) but
401
+ + # its primary function is to add a Rem and an Add pair which
402
+ + # potentially have inline changes
403
+ + def process_line(oldline, newline)
404
+ + start, ending = get_change_extent(oldline, newline)
405
+
406
+ - def write_block(dtype, old = nil, new = nil)
407
+ - push Block.new(dtype)
408
+ + # -
409
+ + line = inline_diff(oldline, start, ending)
410
+ + current_block << Line.rem(line, @offset_base += 1, true)
411
+
412
+ - if dtype == 'mod' and old.size == 1 and new.size == 1
413
+ - write_line(old.first, new.first)
414
+ - return
415
+ + # +
416
+ + line = inline_diff(newline, start, ending)
417
+ + current_block << Line.add(line, @offset_changed += 1, true)
418
+ end
419
+
420
+ - if old and not old.empty?
421
+ - old.each do |e|
422
+ - current_block << Line.send(dtype, expand(e), @offset_base)
423
+ - @offset_base += 1
424
+ - end
425
+ + # Inserts string formating characters around the section of a string
426
+ + # that differs internally from another line so that the Line class
427
+ + # can insert the desired formating
428
+ + def inline_diff(line, start, ending)
429
+ + line[0, start] +
430
+ + '%s' + extract_change(line, start, ending) + '%s' +
431
+ + line[ending, ending.abs]
432
+ end
433
+
434
+ - if new and not new.empty?
435
+ - new.each do |e|
436
+ - current_block << Line.send(dtype, expand(e), @offset_changed)
437
+ - @offset_changed += 1
438
+ - end
439
+ + def extract_change(line, start, ending)
440
+ + line.size > (start - ending) ? line[start...ending] : ''
441
+ end
442
+ - end
443
+
444
+ - def print_block
445
+ - if @p_type.eql?('-') and @ttype.eql?('+')
446
+ - write_block('mod', @p_block, @block)
447
+ - else
448
+ - case @ttype
449
+ - when '+'
450
+ - write_block('add', @block)
451
+ - when '-'
452
+ - write_block('rem', @block)
453
+ - when ' '
454
+ - write_block('unmod', @block)
455
+ - end
456
+ + def car(line)
457
+ + line[0,1]
458
+ end
459
+
460
+ - @block = @p_block = []
461
+ - @p_type = ' '
462
+ - @blockno += 1
463
+ - end
464
+ + def cdr(line)
465
+ + line[1..-1]
466
+ + end
467
+
468
+ - def build(text)
469
+ - # TODO Names of the files and their versions go here perhaps
470
+ + # Returns the current Block object
471
+ + def current_block
472
+ + @data.last
473
+ + end
474
+
475
+ - return if ['++', '--'].include?(text[0,2])
476
+ + # Adds a Line object onto the current Block object
477
+ + def push(line)
478
+ + @data.push line
479
+ + end
480
+
481
+ - if match = LINE_RE.match(text)
482
+ - print_block
483
+ - @changeno += 1
484
+ - @blockno = 0
485
+ - @offset_base = match[1].to_i - 1
486
+ - @offset_changed = match[3].to_i - 1
487
+ - return
488
+ + # This method is called once the generator is done with the unified
489
+ + # diff. It is a finalizer of sorts. By the time it is called all data
490
+ + # has been collected and processed.
491
+ + def close
492
+ + # certain things could be set now that processing is done
493
+ + identify_block
494
+ end
495
+
496
+ - # Set ttype to first character of line
497
+ - ttype = text[0, 1]
498
+ - text = text[1..-1]
499
+ - text = text.gsub("\t", ' ' * @options[:tabwidth]) if text
500
+ - # If it's the same type of mod as the last line push this line onto the
501
+ - # block stack
502
+ - if ttype.eql?(@ttype)
503
+ - @block.push(text)
504
+ - else
505
+ - # If we have a side by side subtraction/addition
506
+ - if ttype == '+' and @ttype == '-'
507
+ - @p_block = @block
508
+ - @p_type = @ttype
509
+ - else
510
+ - print_block
511
+ + # Determines the extent of differences between two string. Returns
512
+ + # an array containing the offset at which changes start, and then
513
+ + # negative offset at which the chnages end. If the two strings have
514
+ + # neither a common prefix nor a common suffic, [0, 0] is returned.
515
+ + def get_change_extent(str1, str2)
516
+ + start = 0
517
+ + limit = [str1.size, str2.size].sort.first
518
+ + while start < limit and str1[start, 1] == str2[start, 1]
519
+ + start += 1
520
+ end
521
+ - @block = [text]
522
+ - @ttype = ttype
523
+ + ending = -1
524
+ + limit -= start
525
+ + while -ending <= limit and str1[ending, 1] == str2[ending, 1]
526
+ + ending -= 1
527
+ + end
528
+ +
529
+ + return [start, ending + 1]
530
+ end
531
+ - end
532
+ + end
533
+
534
+ - def debug
535
+ - each do |diff_block|
536
+ - print "*" * (40 - diff_block.type.size / 2), ' ', diff_block.type
537
+ - puts
538
+ - puts diff_block.map {|line| "#{line.number}" << line << " [#{line.type}]"}.join("\n")
539
+ - print "Line types:"
540
+ - puts diff_block.line_types.join(", ")
541
+ - puts
542
+ - end
543
+ + # Mostly a convinience class at this point that just overwrites various
544
+ + # customization methods
545
+ + class HTMLGenerator < Generator
546
+ +
547
+ + # This and the space method now don't work/make sense now that those
548
+ + # methods are part of the Line class and there certainly won't be an
549
+ + # HTMLLine class
550
+ + def escape(text)
551
+ + text.gsub('&', '&amp;').
552
+ + gsub('<', '&lt;' ).
553
+ + gsub('>', '&gt;' ).
554
+ + gsub('"', '&#34;')
555
+ end
556
+
557
+ - def close
558
+ - # certain things could be set now that processing is done
559
+ - print_block
560
+ + def space
561
+ + '&nbsp;'
562
+ end
563
+
564
+ - # Determines the extent of differences between two string. Returns
565
+ - # an array containing the offset at which changes start, and then
566
+ - # negative offset at which the chnages end. If the two strings have
567
+ - # neither a common prefix nor a common suffic, [0, 0] is returned.
568
+ - def get_change_extent(str1, str2)
569
+ - start = 0
570
+ - limit = [str1.size, str2.size].sort.first
571
+ - while start < limit and str1[start, 1] == str2[start, 1]
572
+ - start += 1
573
+ - end
574
+ - ending = -1
575
+ - limit -= start
576
+ - while -ending <= limit and str1[ending, 1] == str2[ending, 1]
577
+ - ending -= 1
578
+ - end
579
+ + end
580
+
581
+ - return [start, ending + 1]
582
+ - end
583
+ + # See doc string for HTMLGenerator
584
+ + class ASCIIGenerator < Generator
585
+ end
586
+ +
587
+ end
588
+ end
589
+ end
590
+ -