BigW Consortium Gitlab

inline_diff_marker.rb 3.32 KB
Newer Older
1 2 3 4 5 6 7
module Gitlab
  module Diff
    class InlineDiffMarker
      attr_accessor :raw_line, :rich_line

      def initialize(raw_line, rich_line = raw_line)
        @raw_line = raw_line
8
        @rich_line = ERB::Util.html_escape(rich_line)
9 10 11
      end

      def mark(line_inline_diffs)
12 13
        return rich_line unless line_inline_diffs

14
        marker_ranges = []
15
        line_inline_diffs.each do |inline_diff_range|
Douwe Maan committed
16
          # Map the inline-diff range based on the raw line to character positions in the rich line
17
          inline_diff_positions = position_mapping[inline_diff_range].flatten
Douwe Maan committed
18
          # Turn the array of character positions into ranges
19 20
          marker_ranges.concat(collapse_ranges(inline_diff_positions))
        end
21

22 23
        offset = 0
        # Mark each range
24 25 26 27 28 29
        marker_ranges.each_with_index do |range, i|
          class_names = ["idiff"]
          class_names << "left"   if i == 0
          class_names << "right"  if i == marker_ranges.length - 1

          offset = insert_around_range(rich_line, range, "<span class='#{class_names.join(" ")}'>", "</span>", offset)
30 31
        end

32
        rich_line.html_safe
33 34
      end

Douwe Maan committed
35 36 37
      private

      # Mapping of character positions in the raw line, to the rich (highlighted) line
38 39 40 41 42 43 44
      def position_mapping
        @position_mapping ||= begin
          mapping = []
          rich_pos = 0
          (0..raw_line.length).each do |raw_pos|
            rich_char = rich_line[rich_pos]

Douwe Maan committed
45 46
            # The raw and rich lines are the same except for HTML tags,
            # so skip over any `<...>` segment
47 48 49 50 51 52 53 54 55 56
            while rich_char == '<'
              until rich_char == '>'
                rich_pos += 1
                rich_char = rich_line[rich_pos]
              end

              rich_pos += 1
              rich_char = rich_line[rich_pos]
            end

57 58 59 60 61 62 63 64 65 66 67 68 69
            # multi-char HTML entities in the rich line correspond to a single character in the raw line
            if rich_char == '&'
              multichar_mapping = [rich_pos]
              until rich_char == ';'
                rich_pos += 1
                multichar_mapping << rich_pos
                rich_char = rich_line[rich_pos]
              end

              mapping[raw_pos] = multichar_mapping
            else
              mapping[raw_pos] = rich_pos
            end
70 71 72 73 74 75 76 77

            rich_pos += 1
          end

          mapping
        end
      end

Douwe Maan committed
78
      # Takes an array of integers, and returns an array of ranges covering the same integers
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
      def collapse_ranges(positions)
        return [] if positions.empty?
        ranges = []

        start = prev = positions[0]
        range = start..prev
        positions[1..-1].each do |pos|
          if pos == prev + 1
            range = start..pos
            prev = pos
          else
            ranges << range
            start = prev = pos
            range = start..prev
          end
        end
        ranges << range

        ranges
      end

Douwe Maan committed
100
      # Inserts tags around the characters identified by the given range
101
      def insert_around_range(text, range, before, after, offset = 0)
102 103 104
        # Just to be sure
        return offset if offset + range.end + 1 > text.length

105 106 107 108 109 110 111 112 113 114 115
        text.insert(offset + range.begin, before)
        offset += before.length

        text.insert(offset + range.end + 1, after)
        offset += after.length

        offset
      end
    end
  end
end