Give a resize handle for sidebyside diffs.
[WebKit-https.git] / Websites / bugs.webkit.org / PrettyPatch / PrettyPatch.rb
1 require 'cgi'
2 require 'diff'
3 require 'open3'
4 require 'open-uri'
5 require 'pp'
6 require 'set'
7 require 'tempfile'
8
9 module PrettyPatch
10
11 public
12
13     GIT_PATH = "git"
14
15     def self.prettify(string)
16         $last_prettify_file_count = -1
17         $last_prettify_part_count = { "remove" => 0, "add" => 0, "shared" => 0, "binary" => 0, "extract-error" => 0 }
18         string = normalize_line_ending(string)
19         str = HEADER + "\n"
20
21         # Just look at the first line to see if it is an SVN revision number as added
22         # by webkit-patch for git checkouts.
23         $svn_revision = 0
24         string.each_line do |line|
25             match = /^Subversion\ Revision: (\d*)$/.match(line)
26             unless match.nil?
27                 str += "<span class='revision'>" + match[1] + "</span>\n"
28                 $svn_revision = match[1].to_i;
29             end
30             break
31         end
32
33         fileDiffs = FileDiff.parse(string)
34
35         $last_prettify_file_count = fileDiffs.length
36         str += fileDiffs.collect{ |diff| diff.to_html }.join
37     end
38
39     def self.filename_from_diff_header(line)
40         DIFF_HEADER_FORMATS.each do |format|
41             match = format.match(line)
42             return match[1] unless match.nil?
43         end
44         nil
45     end
46
47     def self.diff_header?(line)
48         RELAXED_DIFF_HEADER_FORMATS.any? { |format| line =~ format }
49     end
50
51 private
52     DIFF_HEADER_FORMATS = [
53         /^Index: (.*)\r?$/,
54         /^diff --git "?a\/.+"? "?b\/(.+)"?\r?$/,
55         /^\+\+\+ ([^\t]+)(\t.*)?\r?$/
56     ]
57
58     RELAXED_DIFF_HEADER_FORMATS = [
59         /^Index:/,
60         /^diff/
61     ]
62
63     BINARY_FILE_MARKER_FORMAT = /^Cannot display: file marked as a binary type.$/
64
65     IMAGE_FILE_MARKER_FORMAT = /^svn:mime-type = image\/png$/
66
67     GIT_INDEX_MARKER_FORMAT = /^index ([0-9a-f]{40})\.\.([0-9a-f]{40})/
68
69     GIT_BINARY_FILE_MARKER_FORMAT = /^GIT binary patch$/
70
71     GIT_BINARY_PATCH_FORMAT = /^(literal|delta) \d+$/
72
73     GIT_LITERAL_FORMAT = /^literal \d+$/
74
75     GIT_DELTA_FORMAT = /^delta \d+$/
76
77     START_OF_BINARY_DATA_FORMAT = /^[0-9a-zA-Z\+\/=]{20,}/ # Assume 20 chars without a space is base64 binary data.
78
79     START_OF_SECTION_FORMAT = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@\s*(.*)/
80
81     START_OF_EXTENT_STRING = "%c" % 0
82     END_OF_EXTENT_STRING = "%c" % 1
83
84     # We won't search for intra-line diffs in lines longer than this length, to avoid hangs. See <http://webkit.org/b/56109>.
85     MAXIMUM_INTRALINE_DIFF_LINE_LENGTH = 10000
86
87     SMALLEST_EQUAL_OPERATION = 3
88
89     OPENSOURCE_TRAC_URL = "http://trac.webkit.org/"
90
91     OPENSOURCE_DIRS = Set.new %w[
92         Examples
93         LayoutTests
94         PerformanceTests
95         Source
96         Tools
97         WebKitLibraries
98         Websites
99     ]
100
101     def self.normalize_line_ending(s)
102         s.gsub /\r\n?/, "\n"
103     end
104
105     def self.find_url_and_path(file_path)
106         # Search file_path from the bottom up, at each level checking whether
107         # we've found a directory we know exists in the source tree.
108
109         dirname, basename = File.split(file_path)
110         dirname.split(/\//).reverse.inject(basename) do |path, directory|
111             path = directory + "/" + path
112
113             return [OPENSOURCE_TRAC_URL, path] if OPENSOURCE_DIRS.include?(directory)
114
115             path
116         end
117
118         [nil, file_path]
119     end
120
121     def self.linkifyFilename(filename)
122         url, pathBeneathTrunk = find_url_and_path(filename)
123
124         url.nil? ? filename : "<a href='#{url}browser/trunk/#{pathBeneathTrunk}'>#{filename}</a>"
125     end
126
127
128     HEADER =<<EOF
129 <style>
130 :link, :visited {
131     text-decoration: none;
132     border-bottom: 1px dotted;
133 }
134
135 :link {
136     color: #039;
137 }
138
139 .FileDiff {
140     background-color: #f8f8f8;
141     border: 1px solid #ddd;
142     font-family: monospace;
143     margin: 1em 0;
144     position: relative;
145 }
146
147 h1 {
148     color: #333;
149     font-family: sans-serif;
150     font-size: 1em;
151     margin-left: 0.5em;
152     display: table-cell;
153     width: 100%;
154     padding: 0.5em;
155 }
156
157 h1 :link, h1 :visited {
158     color: inherit;
159 }
160
161 h1 :hover {
162     color: #555;
163     background-color: #eee;
164 }
165
166 .DiffLinks {
167     float: right;
168 }
169
170 .FileDiffLinkContainer {
171     opacity: 0;
172     display: table-cell;
173     padding-right: 0.5em;
174     white-space: nowrap;
175 }
176
177 .DiffSection {
178     background-color: white;
179     border: solid #ddd;
180     border-width: 1px 0px;
181 }
182
183 .ExpansionLine, .LineContainer {
184     white-space: nowrap;
185 }
186
187 .sidebyside .DiffBlockPart.add:first-child {
188     float: right;
189 }
190
191 .LineSide,
192 .sidebyside .DiffBlockPart.remove,
193 .sidebyside .DiffBlockPart.add {
194     display:inline-block;
195     width: 50%;
196     vertical-align: top;
197 }
198
199 .sidebyside .resizeHandle {
200     width: 5px;
201     height: 100%;
202     cursor: move;
203     position: absolute;
204     top: 0;
205     left: 50%;
206 }
207
208 .sidebyside .resizeHandle:hover {
209     background-color: grey;
210     opacity: 0.5;
211 }
212
213 .sidebyside .DiffBlockPart.remove .to,
214 .sidebyside .DiffBlockPart.add .from {
215     display: none;
216 }
217
218 .lineNumber, .expansionLineNumber {
219     border-bottom: 1px solid #998;
220     border-right: 1px solid #ddd;
221     color: #444;
222     display: inline-block;
223     padding: 1px 5px 0px 0px;
224     text-align: right;
225     vertical-align: bottom;
226     width: 3em;
227 }
228
229 .lineNumber {
230   background-color: #eed;
231 }
232
233 .expansionLineNumber {
234   background-color: #eee;
235 }
236
237 .text {
238     padding-left: 5px;
239     white-space: pre-wrap;
240     word-wrap: break-word;
241 }
242
243 .image {
244     border: 2px solid black;
245 }
246
247 .context, .context .lineNumber {
248     color: #849;
249     background-color: #fef;
250 }
251
252 .Line.add, .FileDiff .add {
253     background-color: #dfd;
254 }
255
256 .Line.add ins {
257     background-color: #9e9;
258     text-decoration: none;
259 }
260
261 .Line.remove, .FileDiff .remove {
262     background-color: #fdd;
263 }
264
265 .Line.remove del {
266     background-color: #e99;
267     text-decoration: none;
268 }
269
270 /* Support for inline comments */
271
272 .author {
273   font-style: italic;
274 }
275
276 .comment {
277   position: relative;
278 }
279
280 .comment textarea {
281   height: 6em;
282 }
283
284 .overallComments textarea {
285   height: 2em;
286   max-width: 100%;
287 }
288
289 .comment textarea, .overallComments textarea {
290   display: block;
291   width: 100%;
292 }
293
294 .overallComments .open {
295   -webkit-transition: height .2s;
296   height: 4em;
297 }
298
299 #statusBubbleContainer.wrap {
300   display: block;
301 }
302
303 body {
304   margin-bottom: 40px;
305 }
306
307 #toolbar {
308   display: -webkit-box;
309   display: -moz-box;
310   padding: 3px;
311   left: 0;
312   right: 0;
313   border: 1px solid #ddd;
314   background-color: #eee;
315   font-family: sans-serif;
316   position: fixed;
317 }
318
319 #toolbar.anchored {
320   bottom: 0;
321 }
322
323 #toolbar .actions {
324   float: right;
325 }
326
327 .winter {
328   position: fixed;
329   z-index: 5;
330   left: 0;
331   right: 0;
332   top: 0;
333   bottom: 0;
334   background-color: black;
335   opacity: 0.8;
336 }
337
338 .inactive {
339   display: none;
340 }
341
342 .lightbox {
343   position: fixed;
344   z-index: 6;
345   left: 10%;
346   right: 10%;
347   top: 10%;
348   bottom: 10%;
349   background: white;
350 }
351
352 .lightbox iframe {
353   width: 100%;
354   height: 100%;
355 }
356
357 .commentContext .lineNumber {
358   background-color: yellow;
359 }
360
361 .selected .lineNumber {
362   background-color: #69F;
363   border-bottom-color: #69F;
364   border-right-color: #69F;
365 }
366
367 .ExpandLinkContainer {
368   opacity: 0;
369   border-top: 1px solid #ddd;
370   border-bottom: 1px solid #ddd;
371 }
372
373 .ExpandArea {
374   margin: 0;
375 }
376
377 .ExpandText {
378   margin-left: 0.67em;
379 }
380
381 .LinkContainer {
382   font-family: sans-serif;
383   font-size: small;
384   font-style: normal;
385   -webkit-transition: opacity 0.5s;
386 }
387
388 .LinkContainer a {
389   border: 0;
390 }
391
392 .LinkContainer a:after {
393   content: " | ";
394   color: black;
395 }
396
397 .LinkContainer a:last-of-type:after {
398   content: "";
399 }
400
401 .help {
402  color: gray;
403  font-style: italic;
404 }
405
406 #message {
407   font-size: small;
408   font-family: sans-serif;
409 }
410
411 .commentStatus {
412   font-style: italic;
413 }
414
415 .comment, .previousComment, .frozenComment {
416   background-color: #ffd;
417 }
418
419 .overallComments {
420   -webkit-box-flex: 1;
421   -moz-box-flex: 1;
422   margin-right: 3px;
423 }
424
425 .previousComment, .frozenComment {
426   border: inset 1px;
427   padding: 5px;
428   white-space: pre-wrap;
429 }
430
431 .comment button {
432   width: 6em;
433 }
434
435 div:focus {
436   outline: 1px solid blue;
437   outline-offset: -1px;
438 }
439
440 .statusBubble {
441   /* The width/height get set to the bubble contents via postMessage on browsers that support it. */
442   width: 450px;
443   height: 20px;
444   margin: 2px 2px 0 0;
445   border: none;
446   vertical-align: middle;
447 }
448
449 .pseudo_resize_event_iframe {
450   height: 10%;
451   width: 10%;
452   position: absolute;
453   top: -11%;
454 }
455
456 .revision {
457   display: none;
458 }
459
460 .autosave-state {
461   position: absolute;
462   right: 0;
463   top: -1.3em;
464   padding: 0 3px;
465   outline: 1px solid #DDD;
466   color: #8FDF5F;
467   font-size: small;   
468   background-color: #EEE;
469 }
470
471 .autosave-state:empty {
472   outline: 0px;
473 }
474 .autosave-state.saving {
475   color: #E98080;
476 }
477
478 .clear_float {
479     clear: both;
480 }
481 </style>
482 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script> 
483 <script src="code-review.js?version=42"></script>
484 EOF
485
486     def self.revisionOrDescription(string)
487         case string
488         when /\(revision \d+\)/
489             /\(revision (\d+)\)/.match(string)[1]
490         when /\(.*\)/
491             /\((.*)\)/.match(string)[1]
492         end
493     end
494
495     def self.has_image_suffix(filename)
496         filename =~ /\.(png|jpg|gif)$/
497     end
498
499     class FileDiff
500         def initialize(lines)
501             @filename = PrettyPatch.filename_from_diff_header(lines[0].chomp)
502             startOfSections = 1
503             for i in 0...lines.length
504                 case lines[i]
505                 when /^--- /
506                     @from = PrettyPatch.revisionOrDescription(lines[i])
507                 when /^\+\+\+ /
508                     @filename = PrettyPatch.filename_from_diff_header(lines[i].chomp) if @filename.nil?
509                     @to = PrettyPatch.revisionOrDescription(lines[i])
510                     startOfSections = i + 1
511                     break
512                 when BINARY_FILE_MARKER_FORMAT
513                     @binary = true
514                     if (IMAGE_FILE_MARKER_FORMAT.match(lines[i + 1]) or PrettyPatch.has_image_suffix(@filename)) then
515                         @image = true
516                         startOfSections = i + 2
517                         for x in startOfSections...lines.length
518                             # Binary diffs often have property changes listed before the actual binary data.  Skip them.
519                             if START_OF_BINARY_DATA_FORMAT.match(lines[x]) then
520                                 startOfSections = x
521                                 break
522                             end
523                         end
524                     end
525                     break
526                 when GIT_INDEX_MARKER_FORMAT
527                     @git_indexes = [$1, $2]
528                 when GIT_BINARY_FILE_MARKER_FORMAT
529                     @binary = true
530                     if (GIT_BINARY_PATCH_FORMAT.match(lines[i + 1]) and PrettyPatch.has_image_suffix(@filename)) then
531                         @git_image = true
532                         startOfSections = i + 1
533                     end
534                     break
535                 end
536             end
537             lines_with_contents = lines[startOfSections...lines.length]
538             @sections = DiffSection.parse(lines_with_contents) unless @binary
539             if @image and not lines_with_contents.empty?
540                 @image_url = "data:image/png;base64," + lines_with_contents.join
541                 @image_checksum = FileDiff.read_checksum_from_png(lines_with_contents.join.unpack("m").join)
542             elsif @git_image
543                 begin
544                     raise "index line is missing" unless @git_indexes
545
546                     chunks = nil
547                     for i in 0...lines_with_contents.length
548                         if lines_with_contents[i] =~ /^$/
549                             chunks = [lines_with_contents[i + 1 .. -1], lines_with_contents[0 .. i]]
550                             break
551                         end
552                     end
553
554                     raise "no binary chunks" unless chunks
555
556                     from_filepath = FileDiff.extract_contents_of_from_revision(@filename, chunks[0], @git_indexes[0])
557                     to_filepath = FileDiff.extract_contents_of_to_revision(@filename, chunks[1], @git_indexes[1], from_filepath, @git_indexes[0])
558                     filepaths = from_filepath, to_filepath
559
560                     binary_contents = filepaths.collect { |filepath| File.exists?(filepath) ? File.read(filepath) : nil }
561
562                     @image_urls = binary_contents.collect { |content| (content and not content.empty?) ? "data:image/png;base64," + [content].pack("m") : nil }
563                     @image_checksums = binary_contents.collect { |content| FileDiff.read_checksum_from_png(content) }
564                 rescue
565                     $last_prettify_part_count["extract-error"] += 1
566                     @image_error = "Exception raised during decoding git binary patch:<pre>#{CGI.escapeHTML($!.to_s + "\n" + $!.backtrace.join("\n"))}</pre>"
567                 ensure
568                     File.unlink(from_filepath) if (from_filepath and File.exists?(from_filepath))
569                     File.unlink(to_filepath) if (to_filepath and File.exists?(to_filepath))
570                 end
571             end
572             nil
573         end
574
575         def image_to_html
576             if not @image_url then
577                 return "<span class='text'>Image file removed</span>"
578             end
579             image_snippet = "<img class='image' src='" + @image_url + "' />"
580             if not @image_checksum then
581                 return image_snippet
582             end
583             return "<p>" + @image_checksum + "</p>" + image_snippet
584         end
585
586         def to_html
587             str = "<div class='FileDiff'>\n"
588             str += "<h1>#{PrettyPatch.linkifyFilename(@filename)}</h1>\n"
589             if @image then
590                 str += self.image_to_html
591             elsif @git_image then
592                 if @image_error
593                     str += @image_error
594                 else
595                     for i in (0...2)
596                         image_url = @image_urls[i]
597                         image_checksum = @image_checksums[i]
598
599                         style = ["remove", "add"][i]
600                         str += "<p class=\"#{style}\">"
601
602                         if image_checksum
603                             str += image_checksum + "<br>"
604                         end
605                         if image_url
606                             str += "<img class='image' src='" + image_url + "' />"
607                         else
608                             str += ["</p>Added", "</p>Removed"][i]
609                         end
610                     end
611                 end
612             elsif @binary then
613                 $last_prettify_part_count["binary"] += 1
614                 str += "<span class='text'>Binary file, nothing to see here</span>"
615             else
616                 str += @sections.collect{ |section| section.to_html }.join("<br>\n") unless @sections.nil?
617             end
618
619             if @from then
620                 str += "<span class='revision'>" + @from + "</span>"
621             end
622
623             str += "</div>\n"
624         end
625
626         def self.parse(string)
627             haveSeenDiffHeader = false
628             linesForDiffs = []
629             string.each_line do |line|
630                 if (PrettyPatch.diff_header?(line))
631                     linesForDiffs << []
632                     haveSeenDiffHeader = true
633                 elsif (!haveSeenDiffHeader && line =~ /^--- /)
634                     linesForDiffs << []
635                     haveSeenDiffHeader = false
636                 end
637                 linesForDiffs.last << line unless linesForDiffs.last.nil?
638             end
639
640             linesForDiffs.collect { |lines| FileDiff.new(lines) }
641         end
642
643         def self.read_checksum_from_png(png_bytes)
644             match = png_bytes && png_bytes.match(/tEXtchecksum\0([a-fA-F0-9]{32})/)
645             match ? match[1] : nil
646         end
647
648         def self.git_new_file_binary_patch(filename, encoded_chunk, git_index)
649             return <<END
650 diff --git a/#{filename} b/#{filename}
651 new file mode 100644
652 index 0000000000000000000000000000000000000000..#{git_index}
653 GIT binary patch
654 #{encoded_chunk.join("")}literal 0
655 HcmV?d00001
656
657 END
658         end
659
660         def self.git_changed_file_binary_patch(to_filename, from_filename, encoded_chunk, to_git_index, from_git_index)
661             return <<END
662 diff --git a/#{from_filename} b/#{to_filename}
663 copy from #{from_filename}
664 +++ b/#{to_filename}
665 index #{from_git_index}..#{to_git_index}
666 GIT binary patch
667 #{encoded_chunk.join("")}literal 0
668 HcmV?d00001
669
670 END
671         end
672
673         def self.get_svn_uri(repository_path)
674             "http://svn.webkit.org/repository/webkit/!svn/bc/" + $svn_revision.to_s + "/trunk/" + (repository_path)
675         end
676
677         def self.get_new_temp_filepath_and_name
678             tempfile = Tempfile.new("PrettyPatch")
679             filepath = tempfile.path + '.bin'
680             filename = File.basename(filepath)
681             return filepath, filename
682         end
683
684         def self.download_from_revision_from_svn(repository_path)
685             filepath, filename = get_new_temp_filepath_and_name
686             svn_uri = get_svn_uri(repository_path)
687             open(filepath, 'wb') do |to_file|
688                 to_file << open(svn_uri) { |from_file| from_file.read }
689             end
690             return filepath
691         end
692
693         def self.run_git_apply_on_patch(output_filepath, patch)
694             # Apply the git binary patch using git-apply.
695             cmd = GIT_PATH + " apply --directory=" + File.dirname(output_filepath)
696             stdin, stdout, stderr = *Open3.popen3(cmd)
697             begin
698                 stdin.puts(patch)
699                 stdin.close
700
701                 error = stderr.read
702                 if error != ""
703                     error = "Error running " + cmd + "\n" + "with patch:\n" + patch[0..500] + "...\n" + error
704                 end
705                 raise error if error != ""
706             ensure
707                 stdin.close unless stdin.closed?
708                 stdout.close
709                 stderr.close
710             end
711         end
712
713         def self.extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index)
714             filepath, filename = get_new_temp_filepath_and_name
715             patch = FileDiff.git_new_file_binary_patch(filename, encoded_chunk, git_index)
716             run_git_apply_on_patch(filepath, patch)
717             return filepath
718         end
719
720         def self.extract_contents_from_git_binary_delta_chunk(from_filepath, from_git_index, encoded_chunk, to_git_index)
721             to_filepath, to_filename = get_new_temp_filepath_and_name
722             from_filename = File.basename(from_filepath)
723             patch = FileDiff.git_changed_file_binary_patch(to_filename, from_filename, encoded_chunk, to_git_index, from_git_index)
724             run_git_apply_on_patch(to_filepath, patch)
725             return to_filepath
726         end
727
728         def self.extract_contents_of_from_revision(repository_path, encoded_chunk, git_index)
729             # For literal encoded, simply reconstruct.
730             if GIT_LITERAL_FORMAT.match(encoded_chunk[0])
731                 return extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index)
732             end
733             #  For delta encoded, download from svn.
734             if GIT_DELTA_FORMAT.match(encoded_chunk[0])
735                 return download_from_revision_from_svn(repository_path)
736             end
737             raise "Error: unknown git patch encoding"
738         end
739
740         def self.extract_contents_of_to_revision(repository_path, encoded_chunk, git_index, from_filepath, from_git_index)
741             # For literal encoded, simply reconstruct.
742             if GIT_LITERAL_FORMAT.match(encoded_chunk[0])
743                 return extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index)
744             end
745             # For delta encoded, reconstruct using delta and previously constructed 'from' revision.
746             if GIT_DELTA_FORMAT.match(encoded_chunk[0])
747                 return extract_contents_from_git_binary_delta_chunk(from_filepath, from_git_index, encoded_chunk, git_index)
748             end
749             raise "Error: unknown git patch encoding"
750         end
751     end
752
753     class DiffBlock
754         attr_accessor :parts
755
756         def initialize(container)
757             @parts = []
758             container << self
759         end
760
761         def to_html
762             str = "<div class='DiffBlock'>\n"
763             str += @parts.collect{ |part| part.to_html }.join
764             str += "<div class='clear_float'></div></div>\n"
765         end
766     end
767
768     class DiffBlockPart
769         attr_reader :className
770         attr :lines
771
772         def initialize(className, container)
773             $last_prettify_part_count[className] += 1
774             @className = className
775             @lines = []
776             container.parts << self
777         end
778
779         def to_html
780             str = "<div class='DiffBlockPart %s'>\n" % @className
781             str += @lines.collect{ |line| line.to_html }.join
782             # Don't put white-space after this so adjacent inline-block DiffBlockParts will not wrap.
783             str += "</div>"
784         end
785     end
786
787     class DiffSection
788         def initialize(lines)
789             lines.length >= 1 or raise "DiffSection.parse only received %d lines" % lines.length
790
791             matches = START_OF_SECTION_FORMAT.match(lines[0])
792
793             if matches
794                 from, to = [matches[1].to_i, matches[3].to_i]
795                 if matches[2] and matches[4]
796                     from_end = from + matches[2].to_i
797                     to_end = to + matches[4].to_i
798                 end
799             end
800
801             @blocks = []
802             diff_block = nil
803             diff_block_part = nil
804
805             for line in lines[1...lines.length]
806                 startOfLine = line =~ /^[-\+ ]/ ? 1 : 0
807                 text = line[startOfLine...line.length].chomp
808                 case line[0]
809                 when ?-
810                     if (diff_block_part.nil? or diff_block_part.className != 'remove')
811                         diff_block = DiffBlock.new(@blocks)
812                         diff_block_part = DiffBlockPart.new('remove', diff_block)
813                     end
814
815                     diff_block_part.lines << CodeLine.new(from, nil, text)
816                     from += 1 unless from.nil?
817                 when ?+
818                     if (diff_block_part.nil? or diff_block_part.className != 'add')
819                         # Put add lines that immediately follow remove lines into the same DiffBlock.
820                         if (diff_block.nil? or diff_block_part.className != 'remove')
821                             diff_block = DiffBlock.new(@blocks)
822                         end
823
824                         diff_block_part = DiffBlockPart.new('add', diff_block)
825                     end
826
827                     diff_block_part.lines << CodeLine.new(nil, to, text)
828                     to += 1 unless to.nil?
829                 else
830                     if (diff_block_part.nil? or diff_block_part.className != 'shared')
831                         diff_block = DiffBlock.new(@blocks)
832                         diff_block_part = DiffBlockPart.new('shared', diff_block)
833                     end
834
835                     diff_block_part.lines << CodeLine.new(from, to, text)
836                     from += 1 unless from.nil?
837                     to += 1 unless to.nil?
838                 end
839
840                 break if from_end and to_end and from == from_end and to == to_end
841             end
842
843             changes = [ [ [], [] ] ]
844             for block in @blocks
845                 for block_part in block.parts
846                     for line in block_part.lines
847                         if (!line.fromLineNumber.nil? and !line.toLineNumber.nil?) then
848                             changes << [ [], [] ]
849                             next
850                         end
851                         changes.last.first << line if line.toLineNumber.nil?
852                         changes.last.last << line if line.fromLineNumber.nil?
853                     end
854                 end
855             end
856
857             for change in changes
858                 next unless change.first.length == change.last.length
859                 for i in (0...change.first.length)
860                     from_text = change.first[i].text
861                     to_text = change.last[i].text
862                     next if from_text.length > MAXIMUM_INTRALINE_DIFF_LINE_LENGTH or to_text.length > MAXIMUM_INTRALINE_DIFF_LINE_LENGTH
863                     raw_operations = HTMLDiff::DiffBuilder.new(from_text, to_text).operations
864                     operations = []
865                     back = 0
866                     raw_operations.each_with_index do |operation, j|
867                         if operation.action == :equal and j < raw_operations.length - 1
868                            length = operation.end_in_new - operation.start_in_new
869                            if length < SMALLEST_EQUAL_OPERATION
870                                back = length
871                                next
872                            end
873                         end
874                         operation.start_in_old -= back
875                         operation.start_in_new -= back
876                         back = 0
877                         operations << operation
878                     end
879                     change.first[i].operations = operations
880                     change.last[i].operations = operations
881                 end
882             end
883
884             @blocks.unshift(ContextLine.new(matches[5])) unless matches.nil? || matches[5].empty?
885         end
886
887         def to_html
888             str = "<div class='DiffSection'>\n"
889             str += @blocks.collect{ |block| block.to_html }.join
890             str += "</div>\n"
891         end
892
893         def self.parse(lines)
894             linesForSections = lines.inject([[]]) do |sections, line|
895                 sections << [] if line =~ /^@@/
896                 sections.last << line
897                 sections
898             end
899
900             linesForSections.delete_if { |lines| lines.nil? or lines.empty? }
901             linesForSections.collect { |lines| DiffSection.new(lines) }
902         end
903     end
904
905     class Line
906         attr_reader :fromLineNumber
907         attr_reader :toLineNumber
908         attr_reader :text
909
910         def initialize(from, to, text)
911             @fromLineNumber = from
912             @toLineNumber = to
913             @text = text
914         end
915
916         def text_as_html
917             CGI.escapeHTML(text)
918         end
919
920         def classes
921             lineClasses = ["Line", "LineContainer"]
922             lineClasses << ["add"] unless @toLineNumber.nil? or !@fromLineNumber.nil?
923             lineClasses << ["remove"] unless @fromLineNumber.nil? or !@toLineNumber.nil?
924             lineClasses
925         end
926
927         def to_html
928             markedUpText = self.text_as_html
929             str = "<div class='%s'>\n" % self.classes.join(' ')
930             str += "<span class='from lineNumber'>%s</span><span class='to lineNumber'>%s</span>" %
931                    [@fromLineNumber.nil? ? '&nbsp;' : @fromLineNumber,
932                     @toLineNumber.nil? ? '&nbsp;' : @toLineNumber] unless @fromLineNumber.nil? and @toLineNumber.nil?
933             str += "<span class='text'>%s</span>\n" % markedUpText
934             str += "</div>\n"
935         end
936     end
937
938     class CodeLine < Line
939         attr :operations, true
940
941         def text_as_html
942             html = []
943             tag = @fromLineNumber.nil? ? "ins" : "del"
944             if @operations.nil? or @operations.empty?
945                 return CGI.escapeHTML(@text)
946             end
947             @operations.each do |operation|
948                 start = @fromLineNumber.nil? ? operation.start_in_new : operation.start_in_old
949                 eend = @fromLineNumber.nil? ? operation.end_in_new : operation.end_in_old
950                 escaped_text = CGI.escapeHTML(@text[start...eend])
951                 if eend - start === 0 or operation.action === :equal
952                     html << escaped_text
953                 else
954                     html << "<#{tag}>#{escaped_text}</#{tag}>"
955                 end
956             end
957             html.join
958         end
959     end
960
961     class ContextLine < Line
962         def initialize(context)
963             super("@", "@", context)
964         end
965
966         def classes
967             super << "context"
968         end
969     end
970 end