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