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