Blob of vendor/diff-display/lib/diff/display/unified.rb (raw blob data)

1 module Diff #:nodoc:#
2 module Display #:nodoc:#
3 # = Diff::Display::Unified
4 #
5 # Diff::Display::Unified is meant to make dealing with the presentation of
6 # diffs easy, customizable and succinct. It breaks a diff up into sections,
7 # or blocks, which are defined by the types of lines they contain. If, for
8 # example, there is a section where five lines have been added then an
9 # AddBlock is created and those five lines are placed into that AddBlock.
10 # The design is quite simple: The generated object is made up of Block
11 # objects which are themselves made up of Line objects.
12 #
13 # === Blocks
14 #
15 # Blocks represent various sections that one finds in a diff.
16 # There are five different Block classes:
17 #
18 # [AddBlock]
19 # Contains only instances of AddLine
20 #
21 # [RemBlock]
22 # Contains only instances of RemLine
23 #
24 # [ModBlock]
25 # Contains a set of RemLine objects followed by a set of AddLine
26 # objects
27 #
28 # [UnModBlock]
29 # Contains instances of UnModLine which represent sets of context lines
30 # that are unchanged in both the old and modified data set that
31 # surround Mod, Add or Rem blocks
32 #
33 # [SepBlock]
34 # Contains a single SepLine. SepBlocks are placed between blocks when
35 # the distances between one modification set and the next exceeds the
36 # number of context buffer surrounding them.
37 #
38 # === Lines
39 #
40 # The Line classes are much line the Block classes, just on a smaller
41 # scale.
42 #
43 # There are 4 lines classes:
44 #
45 # === Example
46 #
47 # Consider the following before and after on a diff.
48 #
49 # Before:
50 #
51 # - class OldName < Array
52 # + class NewName < Array
53 #
54 # - def initialize(boundry)
55 # - @boundry = boundry
56 # - end
57 # -
58 # def stay(the, same)
59 # + end
60 # +
61 # + def all_new
62 # + @this, @method = *IS_ALL_NEW
63 #
64 # After:
65 #
66 # ---------------------------------------- ModBlock
67 # 1 [RemLine] class OldName < Array
68 # 1 [AddLine] class NewName < Array
69 # ----------------------------------------
70 #
71 # ---------------------------------------- UnModBlock
72 # 2 [UnModLine]
73 # ----------------------------------------
74 #
75 # ---------------------------------------- RemBlock
76 # 3 [RemLine] def initialize(boundry)
77 # 4 [RemLine] @boundry = boundry
78 # 5 [RemLine] end
79 # 6 [RemLine]
80 # ----------------------------------------
81 #
82 # ---------------------------------------- UnModBlock
83 # 7 [UnModLine] def stay(the, same)
84 # ----------------------------------------
85 #
86 # ---------------------------------------- AddBlock
87 # 8 [AddLine] end
88 # 9 [AddLine]
89 # 10 [AddLine] def all_new
90 # 11 [AddLine] @this, @method = -IS_ALL_NEW
91 # ----------------------------------------
92 #
93 # Note: That is just a representation of the structure of the generated
94 # object. Also note that this example does not include any SepBlocks since
95 # the changes in the example diff are all contiguous.
96 #
97 # Internally the datastructure is quite simple: The Data object has an
98 # array of Block objects which themselves have an array of Line
99 # objects. Traversing the object on the block level or line level is
100 # equally simple so you can focus on what to do for each type of block and
101 # line.
102 module Unified
103 # Every line from the passed in diff gets transformed into an instance of
104 # one of line Line class's subclasses. One subclass exists for each line
105 # type in a diff. As such there is an AddLine class for added lines, a RemLine
106 # class for removed lines, an UnModLine class for lines which remain unchanged and
107 # a SepLine class which represents all the lines that aren't part of the diff.
108 class Line < String
109 def initialize(line, line_number)
110 super(line)
111 @line_number = line_number
112 self
113 end
114
115 def contains_inline_change?
116 @inline
117 end
118
119 # Returns the line number of the diff line
120 def number
121 @line_number
122 end
123
124 def decorate(&block)
125 yield self
126 end
127
128 protected
129
130 def inline_add_open; '' end
131 def inline_add_close; '' end
132 def inline_rem_open; '' end
133 def inline_rem_close; '' end
134
135 def escape
136 self
137 end
138
139 def expand
140 escape.gsub("\t", ' ' * tabwidth).gsub(/ ( +)|^ /) do |match|
141 (space + ' ') * (match.size / 2) +
142 space * (match.size % 2)
143 end
144 end
145
146 def tabwidth
147 4
148 end
149
150
151 def space
152 ' '
153 end
154
155 class << self
156 def add(line, line_number, inline = false)
157 AddLine.new(line, line_number, inline)
158 end
159
160 def rem(line, line_number, inline = false)
161 RemLine.new(line, line_number, inline)
162 end
163
164 def unmod(line, line_number)
165 UnModLine.new(line, line_number)
166 end
167 end
168 end
169
170 class AddLine < Line #:nodoc:#
171 def initialize(line, line_number, inline = false)
172 #line = inline ? line % [inline_add_open, inline_add_close] : line
173 super(line, line_number)
174 @inline = inline
175 self
176 end
177 end
178
179 class RemLine < Line #:nodoc:#
180 def initialize(line, line_number, inline = false)
181 #line = inline ? line % [inline_rem_open, inline_rem_close] : line
182 super(line, line_number)
183 @inline = inline
184 self
185 end
186 end
187
188 class UnModLine < Line #:nodoc:#
189 def initialize(line, line_number)
190 super(line, line_number)
191 end
192 end
193
194 class SepLine < Line #:nodoc:#
195 def initialize(line = '...')
196 super(line, nil)
197 end
198 end
199
200 # This class is an array which contains Line objects. Just like Line
201 # classes, several Block classes inherit from Block. If all the lines
202 # in the block are added lines then it is an AddBlock. If all lines
203 # in the block are removed lines then it is a RemBlock. If the lines
204 # in the block are all unmodified then it is an UnMod block. If the
205 # lines in the block are a mixture of added and removed lines then
206 # it is a ModBlock. There are no blocks that contain a mixture of
207 # modified and unmodified lines.
208 class Block < Array
209 def initialize
210 super
211 end
212
213 def <<(line_object)
214 super(line_object)
215 self
216 end
217
218 def decorate(&block)
219 yield self
220 end
221
222 class << self
223 def add; AddBlock.new end
224 def rem; RemBlock.new end
225 def mod; ModBlock.new end
226 def unmod; UnModBlock.new end
227 end
228 end
229
230 #:stopdoc:#
231 class AddBlock < Block; end
232 class RemBlock < Block; end
233 class ModBlock < Block; end
234 class UnModBlock < Block; end
235 class SepBlock < Block; end
236 #:startdoc:#
237
238 # A Data object contains the generated diff data structure. It is an
239 # array of Block objects which are themselves arrays of Line objects. The
240 # Generator class returns a Data instance object after it is done
241 # processing the diff.
242 class Data < Array
243 def initialize
244 super
245 end
246
247 def debug
248 demodularize = Proc.new {|obj| obj.class.name[/\w+$/]}
249 each do |diff_block|
250 print "-" * 40, ' ', demodularize.call(diff_block)
251 puts
252 puts diff_block.map {|line|
253 "%5d" % line.number +
254 " [#{demodularize.call(line)}]" +
255 line
256 }.join("\n")
257 puts "-" * 40, ' '
258 end
259 end
260
261 end
262
263 # Processes the diff and generates a Data object which contains the
264 # resulting data structure.
265 #
266 # The +run+ class method is fed a diff and returns a Data object. It will
267 # accept as its argument a String, an Array or a File object:
268 #
269 # Diff::Display::Unified::Generator.run(diff)
270 #
271 class Generator
272
273 # Extracts the line number info for a given diff section
274 LINE_NUM_RE = /@@ [+-]([0-9]+),([0-9]+) [+-]([0-9]+),([0-9]+) @@/
275 LINE_TYPES = {'+' => :add, '-' => :rem, ' ' => :unmod}
276
277 class << self
278
279 # Runs the generator on a diff and returns a Data object without
280 # instantiating a Generator object
281 def run(udiff)
282 raise ArgumentError, "Object must be enumerable" unless udiff.respond_to?(:each)
283 generator = new
284 udiff.each {|line| generator.process(line.chomp)}
285 generator.data
286 end
287 end
288
289 def initialize
290 @buffer = []
291 @prev_buffer = []
292 @line_type = nil
293 @prev_line_type = nil
294 @offset_base = 0
295 @offset_changed = 0
296 @data = Diff::Display::Unified::Data.new
297 self
298 end
299
300 # Operates on a single line from the diff and passes along the
301 # collected data to the appropriate method for further processing. The
302 # cycle of processing is in general:
303 #
304 # process --> identify_block --> process_block --> process_line
305 #
306 def process(line)
307 return if is_extra_header_line?(line)
308
309 if match = LINE_NUM_RE.match(line)
310 identify_block
311 add_separator unless @offset_changed.zero?
312 @line_type = nil
313 @offset_base = match[1].to_i - 1
314 @offset_changed = match[3].to_i - 1
315 return
316 end
317
318 new_line_type, line = LINE_TYPES[car(line)], cdr(line)
319
320 # Add line to the buffer if it's the same diff line type
321 # as the previous line
322 #
323 # e.g.
324 #
325 # + This is a new line
326 # + As is this one
327 # + And yet another one...
328 #
329 if new_line_type.eql?(@line_type)
330 @buffer.push(line)
331 else
332 # Side by side inline diff
333 #
334 # e.g.
335 #
336 # - This line just had to go
337 # + This line is on the way in
338 #
339 if new_line_type.eql?(LINE_TYPES['+']) and @line_type.eql?(LINE_TYPES['-'])
340 @prev_buffer = @buffer
341 @prev_line_type = @line_type
342 else
343 identify_block
344 end
345 @buffer = [line]
346 @line_type = new_line_type
347 end
348 end
349
350 # Finishes up with the generation and returns the Data object (could
351 # probably use a better name...maybe just #data?)
352 def data
353 close
354 @data
355 end
356
357 protected
358
359 def is_extra_header_line?(line)
360 return true if ['++', '--'].include?(line[0,2])
361 return true if line =~ /^(new|delete) file mode [0-9]+$/
362 return true if line =~ /^diff \-\-git/
363 return true if line =~ /^index \w+\.\.\w+ [0-9]+$/
364 false
365 end
366
367 def identify_block
368 if @prev_line_type.eql?(LINE_TYPES['-']) and @line_type.eql?(LINE_TYPES['+'])
369 process_block(:mod, true, true)
370 else
371 if LINE_TYPES.values.include?(@line_type)
372 process_block(@line_type, true)
373 end
374 end
375
376 @prev_line_type = nil
377 end
378
379 def process_block(diff_line_type, new = false, old = false)
380 push Block.send(diff_line_type)
381 # Mod block
382 if diff_line_type.eql?(:mod) && @prev_buffer.size && @buffer.size == 1
383 process_line(@prev_buffer.first, @buffer.first)
384 return
385 end
386 unroll_prev_buffer if old
387 unroll_buffer if new
388 end
389
390 # TODO Needs a better name...it does process a line (two in fact) but
391 # its primary function is to add a Rem and an Add pair which
392 # potentially have inline changes
393 def process_line(oldline, newline)
394 start, ending = get_change_extent(oldline, newline)
395
396 # -
397 line = inline_diff(oldline, start, ending)
398 current_block << Line.rem(line, @offset_base += 1, true)
399
400 # +
401 line = inline_diff(newline, start, ending)
402 current_block << Line.add(line, @offset_changed += 1, true)
403 end
404
405 # Inserts string formating characters around the section of a string
406 # that differs internally from another line so that the Line class
407 # can insert the desired formating
408 def inline_diff(line, start, ending)
409 line[0, start] +
410 #'%s' + extract_change(line, start, ending) + '%s' +
411 extract_change(line, start, ending) +
412 line[ending, ending.abs]
413 end
414
415 def add_separator
416 push SepBlock.new
417 current_block << SepLine.new
418 end
419
420 def extract_change(line, start, ending)
421 line.size > (start - ending) ? line[start...ending] : ''
422 end
423
424 def car(line)
425 line[0,1]
426 end
427
428 def cdr(line)
429 line[1..-1]
430 end
431
432 # Returns the current Block object
433 def current_block
434 @data.last
435 end
436
437 # Adds a Line object onto the current Block object
438 def push(line)
439 @data.push line
440 end
441
442 def prev_buffer
443 @prev_buffer
444 end
445
446 def unroll_prev_buffer
447 return if @prev_buffer.empty?
448 @prev_buffer.each do |line|
449 @offset_base += 1
450 current_block << Line.send(@prev_line_type, line, @offset_base)
451 end
452 end
453
454 def unroll_buffer
455 return if @buffer.empty?
456 @buffer.each do |line|
457 @offset_changed += 1
458 current_block << Line.send(@line_type, line, @offset_changed)
459 end
460 end
461
462 # This method is called once the generator is done with the unified
463 # diff. It is a finalizer of sorts. By the time it is called all data
464 # has been collected and processed.
465 def close
466 # certain things could be set now that processing is done
467 identify_block
468 end
469
470 # Determines the extent of differences between two string. Returns
471 # an array containing the offset at which changes start, and then
472 # negative offset at which the chnages end. If the two strings have
473 # neither a common prefix nor a common suffic, [0, 0] is returned.
474 def get_change_extent(str1, str2)
475 start = 0
476 limit = [str1.size, str2.size].sort.first
477 while start < limit and str1[start, 1] == str2[start, 1]
478 start += 1
479 end
480 ending = -1
481 limit -= start
482 while -ending <= limit and str1[ending, 1] == str2[ending, 1]
483 ending -= 1
484 end
485
486 return [start, ending + 1]
487 end
488 end
489
490 # The Renderer class is the single point of entry for the
491 # Diff::Display::Unified library. It can be used in two ways. One is to
492 # create a new instance which returns a Data object created by the
493 # Generator. This object contains the collections of Blocks and Lines
494 # which the user can iterate over.
495 #
496 # data_object = Diff::Display::Unified::Renderer.new(diff)
497 #
498 # The second way is to call the Renderer's +run+ class method, optionally
499 # passing in an instance of a class which inherits from
500 # Diff::Display::Unified::Callbacks. This then calls the +render+ method
501 # which uses the methods defined in the callback class instance to
502 # decorate the diff contents as it unrolls the Data object.
503 #
504 # Somewhere up above:
505 #
506 # class MyDiffCallbacks < Diff::Display::Unified::Callbacks
507 #
508 # def before_addline '<ins>' end
509 # def after_addline '</ins>' end
510 #
511 # end
512 #
513 # callback_obj = MyDiffCallbacks.new
514 #
515 # fully_rendered_diff = Diff::Display::Unified::Renderer.run(diff, callback_obj)
516 #
517 class Renderer
518 attr_reader :data
519
520 def initialize(diff, callback_object = nil)
521 @callbacks = callback_object || Diff::Display::Unified::Callbacks.new
522 @data = Diff::Display::Unified::Generator.run(diff)
523 end
524
525 # XXX The relationship between render and rendered and run is too complicated
526 # and nuanced
527 def render
528 @rendered = @data.inject([]) do |block_data, block|
529 block_data << before_method(block)
530 # Block must use braces rather than do/end due to precedence rules!
531 block_data.concat block.inject([]) { |line_data, line|
532 line_data << before_method(line) << escape(line) << after_method(line)
533 }
534 block_data << after_method(block)
535 end
536 end
537
538 def rendered
539 (@rendered ? @rendered : render).join(new_line)
540 end
541
542 class << self
543 def run(diff, callback_object = nil)
544 new(diff, callback_object).rendered
545 end
546 end
547
548 def escape(text)
549 text
550 end
551
552 private
553