summaryrefslogtreecommitdiff
path: root/lib/coderay/scanners/diff.rb
blob: a8a63e9af2062a067d302eee07c20b86aa142ecf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
module CodeRay
module Scanners
  
  # Scanner for output of the diff command.
  # 
  # Alias: +patch+
  class Diff < Scanner
    
    register_for :diff
    title 'diff output'
    
    DEFAULT_OPTIONS = {
      :highlight_code => true,
      :inline_diff => true,
    }
    
  protected
    
    require 'coderay/helpers/file_type'
    
    def scan_tokens encoder, options
      
      line_kind = nil
      state = :initial
      deleted_lines = 0
      scanners = Hash.new do |h, lang|
        h[lang] = Scanners[lang].new '', :keep_tokens => true, :keep_state => true
      end
      content_scanner = scanners[:plain]
      content_scanner_entry_state = nil
      
      until eos?
        
        if match = scan(/\n/)
          deleted_lines = 0 unless line_kind == :delete
          if line_kind
            encoder.end_line line_kind
            line_kind = nil
          end
          encoder.text_token match, :space
          next
        end
        
        case state
        
        when :initial
          if match = scan(/--- |\+\+\+ |=+|_+/)
            encoder.begin_line line_kind = :head
            encoder.text_token match, :head
            if match = scan(/.*?(?=$|[\t\n\x00]|  \(revision)/)
              encoder.text_token match, :filename
              if options[:highlight_code]
                content_scanner = scanners[FileType.fetch(match, :plaintext)]
                content_scanner_entry_state = nil
              end
            end
            next unless match = scan(/.+/)
            encoder.text_token match, :plain
          elsif match = scan(/Index: |Property changes on: /)
            encoder.begin_line line_kind = :head
            encoder.text_token match, :head
            next unless match = scan(/.+/)
            encoder.text_token match, :plain
          elsif match = scan(/Added: /)
            encoder.begin_line line_kind = :head
            encoder.text_token match, :head
            next unless match = scan(/.+/)
            encoder.text_token match, :plain
            state = :added
          elsif match = scan(/\\ /)
            encoder.begin_line line_kind = :change
            encoder.text_token match, :change
            next unless match = scan(/.+/)
            encoder.text_token match, :plain
          elsif match = scan(/@@(?>[^@\n]*)@@/)
            content_scanner.instance_variable_set(:@state, :initial) unless match?(/\n\+/)
            content_scanner_entry_state = nil
            if check(/\n|$/)
              encoder.begin_line line_kind = :change
            else
              encoder.begin_group :change
            end
            encoder.text_token match[0,2], :change
            encoder.text_token match[2...-2], :plain
            encoder.text_token match[-2,2], :change
            encoder.end_group :change unless line_kind
            next unless match = scan(/.+/)
            if options[:highlight_code]
              content_scanner.tokenize match, :tokens => encoder
            else
              encoder.text_token match, :plain
            end
            next
          elsif match = scan(/\+/)
            encoder.begin_line line_kind = :insert
            encoder.text_token match, :insert
            next unless match = scan(/.+/)
            if options[:highlight_code]
              content_scanner.tokenize match, :tokens => encoder
            else
              encoder.text_token match, :plain
            end
            next
          elsif match = scan(/-/)
            deleted_lines += 1
            encoder.begin_line line_kind = :delete
            encoder.text_token match, :delete
            if options[:inline_diff] && deleted_lines == 1 && check(/(?>.*)\n\+(?>.*)$(?!\n\+)/)
              if content_scanner.instance_variable_defined?(:@state)
                content_scanner_entry_state = content_scanner.instance_variable_get(:@state)
              end
              skip(/(.*)\n\+(.*)$/)
              head, deletion, insertion, tail = diff self[1], self[2]
              pre, deleted, post = content_scanner.tokenize [head, deletion, tail], :tokens => Tokens.new
              encoder.tokens pre
              encoder.begin_group :eyecatcher
              encoder.tokens deleted
              encoder.end_group :eyecatcher
              encoder.tokens post
              encoder.end_line line_kind
              encoder.text_token "\n", :space
              encoder.begin_line line_kind = :insert
              encoder.text_token '+', :insert
              content_scanner.instance_variable_set(:@state, content_scanner_entry_state || :initial)
              pre, inserted, post = content_scanner.tokenize [head, insertion, tail], :tokens => Tokens.new
              encoder.tokens pre
              encoder.begin_group :eyecatcher
              encoder.tokens inserted
              encoder.end_group :eyecatcher
              encoder.tokens post
            elsif match = scan(/.*/)
              if options[:highlight_code]
                if deleted_lines == 1 && content_scanner.instance_variable_defined?(:@state)
                  content_scanner_entry_state = content_scanner.instance_variable_get(:@state)
                end
                content_scanner.tokenize match, :tokens => encoder unless match.empty?
                if !match?(/\n-/)
                  if match?(/\n\+/)
                    content_scanner.instance_variable_set(:@state, content_scanner_entry_state || :initial)
                  end
                  content_scanner_entry_state = nil
                end
              else
                encoder.text_token match, :plain
              end
            end
            next
          elsif match = scan(/ .*/)
            if options[:highlight_code]
              content_scanner.tokenize match, :tokens => encoder
            else
              encoder.text_token match, :plain
            end
            next
          elsif match = scan(/.+/)
            encoder.begin_line line_kind = :comment
            encoder.text_token match, :plain
          else
            raise_inspect 'else case rached'
          end
        
        when :added
          if match = scan(/   \+/)
            encoder.begin_line line_kind = :insert
            encoder.text_token match, :insert
            next unless match = scan(/.+/)
            encoder.text_token match, :plain
          else
            state = :initial
            next
          end
        end
        
      end
      
      encoder.end_line line_kind if line_kind
      
      encoder
    end
    
  private
    
    def diff a, b
      # i will be the index of the leftmost difference from the left.
      i_max = [a.size, b.size].min
      i = 0
      i += 1 while i < i_max && a[i] == b[i]
      # j_min will be the index of the leftmost difference from the right.
      j_min = i - i_max
      # j will be the index of the rightmost difference from the right which
      # does not precede the leftmost one from the left.
      j = -1
      j -= 1 while j >= j_min && a[j] == b[j]
      return a[0...i], a[i..j], b[i..j], (j < -1) ? a[j+1..-1] : ''
    end
    
  end
  
end
end