BigW Consortium Gitlab

inline_diff_marker.rb 3.89 KB
Newer Older
1 2 3
module Gitlab
  module Diff
    class InlineDiffMarker
4 5 6 7 8
      MARKDOWN_SYMBOLS = {
        addition: "+",
        deletion: "-"
      }

9 10 11 12
      attr_accessor :raw_line, :rich_line

      def initialize(raw_line, rich_line = raw_line)
        @raw_line = raw_line
13
        @rich_line = ERB::Util.html_escape(rich_line)
14 15
      end

16
      def mark(line_inline_diffs, mode: nil, markdown: false)
17 18
        return rich_line unless line_inline_diffs

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

27
        offset = 0
28

29 30
        # Mark each range
        marker_ranges.each_with_index do |range, index|
31 32 33 34 35 36 37 38 39 40 41 42
          before_content =
            if markdown
              "{#{MARKDOWN_SYMBOLS[mode]}"
            else
              "<span class='#{html_class_names(marker_ranges, mode, index)}'>"
            end
          after_content =
            if markdown
              "#{MARKDOWN_SYMBOLS[mode]}}"
            else
              "</span>"
            end
43
          offset = insert_around_range(rich_line, range, before_content, after_content, offset)
44 45
        end

46
        rich_line.html_safe
47 48
      end

Douwe Maan committed
49 50
      private

51 52 53 54 55 56 57 58
      def html_class_names(marker_ranges, mode, index)
        class_names = ["idiff"]
        class_names << "left"  if index == 0
        class_names << "right" if index == marker_ranges.length - 1
        class_names << mode if mode
        class_names.join(" ")
      end

Douwe Maan committed
59
      # Mapping of character positions in the raw line, to the rich (highlighted) line
60 61 62 63 64 65 66
      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
67 68
            # The raw and rich lines are the same except for HTML tags,
            # so skip over any `<...>` segment
69 70 71 72 73 74 75 76 77 78
            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

79 80 81 82 83 84 85 86 87 88 89 90 91
            # 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
92 93 94 95 96 97 98 99

            rich_pos += 1
          end

          mapping
        end
      end

Douwe Maan committed
100
      # Takes an array of integers, and returns an array of ranges covering the same integers
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
      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
122
      # Inserts tags around the characters identified by the given range
123
      def insert_around_range(text, range, before, after, offset = 0)
124 125 126
        # Just to be sure
        return offset if offset + range.end + 1 > text.length

127 128 129 130 131 132 133 134 135 136 137
        text.insert(offset + range.begin, before)
        offset += before.length

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

        offset
      end
    end
  end
end