diff-display 0.0.1

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